Skip to content

Commit f98063e

Browse files
committed
save...
1 parent ad870e7 commit f98063e

File tree

11 files changed

+376
-244
lines changed

11 files changed

+376
-244
lines changed

api/src/opentrons/drivers/absorbance_reader/abstract.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from abc import ABC, abstractmethod
2-
from typing import Dict, List
2+
from typing import Dict, List, Tuple
33
from opentrons.drivers.types import (
44
AbsorbanceReaderLidStatus,
55
AbsorbanceReaderDeviceState,
@@ -48,8 +48,18 @@ async def get_device_info(self) -> Dict[str, str]:
4848
"""Get device info"""
4949
...
5050

51+
@abstractmethod
52+
async def get_uptime(self) -> Dict[str, str]:
53+
"""Get device uptime"""
54+
...
55+
5156
@abstractmethod
5257
async def get_plate_presence(self) -> AbsorbanceReaderPlatePresence:
5358
"""Check if there is a plate in the reader."""
5459
...
5560

61+
@abstractmethod
62+
async def update_firmware(self, firmware_file_path: str) -> Tuple[bool, str]:
63+
"""Updates the firmware on the device."""
64+
...
65+

api/src/opentrons/drivers/absorbance_reader/async_byonoy.py

Lines changed: 151 additions & 135 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,19 @@
11
from __future__ import annotations
22

33
import asyncio
4+
import logging
5+
import os
46
import re
57
from concurrent.futures.thread import ThreadPoolExecutor
68
from functools import partial
7-
from typing import Optional, List, Dict
9+
from sys import version
10+
from typing import Optional, List, Dict, Tuple
11+
12+
# TODO: Make sure we move this back into the AsyncByonoy
13+
import pybyonoy_device_library as byonoy # type: ignore[import-not-found]
14+
15+
# for debugging
16+
byonoy.byonoy_enable_logging(False)
817

918

1019
from .hid_protocol import (
@@ -20,8 +29,11 @@
2029
)
2130
from opentrons.drivers.rpi_drivers.types import USBPort
2231

32+
log = logging.getLogger(__name__)
33+
2334

2435
SN_PARSER = re.compile(r'ATTRS{serial}=="(?P<serial>.+?)"')
36+
VERSION_PARSER = re.compile(r'Absorbance (?P<version>V\d+\.\d+\.\d+)')
2537

2638

2739
class AsyncByonoy:
@@ -71,8 +83,6 @@ async def create(
7183
loop = loop or asyncio.get_running_loop()
7284
executor = ThreadPoolExecutor(max_workers=1)
7385

74-
import pybyonoy_device_library as byonoy # type: ignore[import-not-found]
75-
7686
interface: AbsProtocol = byonoy
7787

7888
device_sn = cls.serial_number_from_port(usb_port.name)
@@ -111,61 +121,139 @@ def __init__(
111121
self._device_handle: Optional[int] = None
112122
self._current_config: Optional[AbsProtocol.MeasurementConfig] = None
113123

114-
def _cleanup(self) -> None:
115-
self._device_handle = None
124+
async def open(self) -> int:
125+
"""
126+
Open the connection.
116127
117-
def _open(self) -> None:
118-
err, device_handle = self._interface.byonoy_open_device(self._device)
119-
if err.name != "BYONOY_ERROR_NO_ERROR":
120-
raise RuntimeError(f"Error opening device: {err}")
128+
Returns: boolean denoting connection success.
129+
"""
130+
131+
log.warning("Opening connection!")
132+
err, device_handle = await self._loop.run_in_executor(
133+
executor=self._executor, func=partial(
134+
self._interface.byonoy_open_device, self._device),
135+
)
136+
self._raise_if_error(err.name, f"Error opening device: {err}")
121137
self._device_handle = device_handle
138+
log.warning(f"Connection Opened: <{device_handle}>")
139+
return bool(device_handle)
122140

123-
def _free(self) -> None:
124-
if self._device_handle:
125-
self._interface.byonoy_free_device(self._device_handle)
126-
self._cleanup()
141+
async def close(self) -> None:
142+
"""Close the connection."""
143+
handle = self._verify_device_handle()
144+
await self._loop.run_in_executor(
145+
executor=self._executor, func=partial(
146+
self._interface.byonoy_free_device, handle)
147+
)
148+
self._device_handle = None
127149

128-
def verify_device_handle(self) -> int:
129-
assert self._device_handle is not None, RuntimeError(
130-
"Device handle not set up."
150+
async def is_open(self) -> bool:
151+
"""True if connection is open."""
152+
handle = self._verify_device_handle()
153+
return await self._loop.run_in_executor(
154+
executor=self._executor, func=partial(
155+
self._interface.byonoy_device_open, handle)
131156
)
132-
return self._device_handle
133157

134-
def _raise_if_error(
135-
self,
136-
err_name: ErrorCodeNames,
137-
msg: str = "Error occurred: ",
138-
) -> None:
139-
if err_name != "BYONOY_ERROR_NO_ERROR":
140-
raise RuntimeError(msg, err_name)
158+
async def get_device_information(self) -> Dict[str, str]:
159+
"""Get serial number and version info."""
160+
handle = self._verify_device_handle()
161+
err, device_info = await self._loop.run_in_executor(
162+
executor=self._executor, func=partial(
163+
self._interface.byonoy_get_device_information, handle)
164+
)
165+
self._raise_if_error(err.name, f"Error getting device information: {err}")
141166

142-
def _get_device_information(self) -> AbsProtocol.DeviceInfo:
143-
handle = self.verify_device_handle()
144-
err, device_info = self._interface.byonoy_get_device_information(handle)
145-
self._raise_if_error(err.name, "Error getting device information: ")
146-
return device_info
167+
match = VERSION_PARSER.match(device_info.version)
168+
version = match["version"].lower() if match else "v0.0.0"
169+
info = {
170+
"serial": self._device.sn,
171+
"model": "ABS96",
172+
"version": version,
173+
}
174+
log.warning(f"ABS_DEVICE_INFO: {info}")
175+
log.warning(f"SERIAL_NUMBER: {self._device.sn}")
176+
return info
147177

148-
def _get_device_status(self) -> AbsProtocol.DeviceState:
149-
handle = self.verify_device_handle()
150-
err, status = self._interface.byonoy_get_device_status(handle)
178+
async def get_device_status(self) -> AbsorbanceReaderDeviceState:
179+
"""Get state information of the device."""
180+
handle = self._verify_device_handle()
181+
err, status = await self._loop.run_in_executor(
182+
executor=self._executor, func=partial(
183+
self._interface.byonoy_get_device_status, handle)
184+
)
151185
self._raise_if_error(err.name, "Error getting device status: ")
152-
return status
186+
return self.convert_device_state(status.name)
187+
188+
async def update_firmware(self, firmware_file_path: str) -> Tuple[bool, str]:
189+
"""Updates the firmware of the device."""
190+
handle = self._verify_device_handle()
191+
if not os.path.exists(firmware_file_path):
192+
return False, f"Firmware file not found: {firmware_file_path}"
193+
# TODO: validate file somehow?
194+
195+
# TODO: verify if this needs to run in this context or executor thread
196+
err = self._interface.byonoy_update_device(handle, firmware_file_path)
197+
if err.name != "BYONOY_ERROR_NO_ERROR":
198+
return False, f"Byonoy update failed with error: {err}"
199+
return True, ""
200+
201+
async def get_device_uptime(self) -> int:
202+
"""Get how long in seconds the device has been running for."""
203+
handle = self._verify_device_handle()
204+
err, uptime = await self._loop.run_in_executor(
205+
executor=self._executor, func=partial(
206+
self._interface.byonoy_get_device_uptime, handle)
207+
)
208+
self._raise_if_error(err.name, "Error getting device uptime: ")
209+
return uptime
210+
211+
async def get_lid_status(self) -> AbsorbanceReaderLidStatus:
212+
"""Get the state of the absorbance lid."""
213+
handle = self._verify_device_handle()
214+
err, lid_info = await self._loop.run_in_executor(
215+
executor=self._executor, func=partial(
216+
self._interface.byonoy_get_device_parts_aligned, handle)
217+
)
218+
self._raise_if_error(err.name, f"Error getting lid status: {err}")
219+
return (
220+
AbsorbanceReaderLidStatus.ON if lid_info else AbsorbanceReaderLidStatus.OFF
221+
)
222+
223+
async def get_supported_wavelengths(self) -> list[int]:
224+
"""Get a list of the wavelength readings this device supports."""
225+
handle = self._verify_device_handle()
226+
err, wavelengths = await self._loop.run_in_executor(
227+
executor=self._executor, func=partial(
228+
self._interface.byonoy_abs96_get_available_wavelengths, handle)
229+
)
230+
self._raise_if_error(err.name, "Error getting available wavelengths: ")
231+
self._supported_wavelengths = wavelengths
232+
return wavelengths
153233

154-
def _get_slot_status(self) -> AbsProtocol.SlotState:
155-
handle = self.verify_device_handle()
156-
err, slot_status = self._interface.byonoy_get_device_slot_status(handle)
157-
self._raise_if_error(err.name, "Error getting slot status: ")
158-
return slot_status
234+
async def get_single_measurement(self, wavelength: int) -> List[float]:
235+
"""Get a single measurement based on the current configuration."""
236+
handle = self._verify_device_handle()
237+
assert self._current_config and self._current_config.sample_wavelength == wavelength
238+
err, measurements = await self._loop.run_in_executor(
239+
executor=self._executor, func=partial(
240+
self._interface.byonoy_abs96_single_measure, handle, self._current_config)
241+
)
242+
self._raise_if_error(err.name, f"Error getting single measurement: {err}")
243+
return measurements
159244

160-
def _get_lid_status(self) -> bool:
161-
handle = self.verify_device_handle()
162-
lid_on: bool
163-
err, lid_on = self._interface.byonoy_get_device_parts_aligned(handle)
164-
self._raise_if_error(err.name, "Error getting lid status: ")
165-
return lid_on
245+
async def get_plate_presence(self) -> AbsorbanceReaderPlatePresence:
246+
"""Get the state of the plate for the reader."""
247+
handle = self._verify_device_handle()
248+
err, presence = await self._loop.run_in_executor(
249+
executor=self._executor, func=partial(
250+
self._interface.byonoy_get_device_slot_status, handle)
251+
)
252+
self._raise_if_error(err.name, f"Error getting slot status: {err}")
253+
return self.convert_plate_presence(presence.name)
166254

167255
def _get_supported_wavelengths(self) -> List[int]:
168-
handle = self.verify_device_handle()
256+
handle = self._verify_device_handle()
169257
wavelengths: List[int]
170258
err, wavelengths = self._interface.byonoy_abs96_get_available_wavelengths(
171259
handle
@@ -175,18 +263,11 @@ def _get_supported_wavelengths(self) -> List[int]:
175263
return wavelengths
176264

177265
def _initialize_measurement(self, conf: AbsProtocol.MeasurementConfig) -> None:
178-
handle = self.verify_device_handle()
266+
handle = self._verify_device_handle()
179267
err = self._interface.byonoy_abs96_initialize_single_measurement(handle, conf)
180268
self._raise_if_error(err.name, "Error initializing measurement: ")
181269
self._current_config = conf
182270

183-
def _single_measurement(self, conf: AbsProtocol.MeasurementConfig) -> List[float]:
184-
handle = self.verify_device_handle()
185-
measurements: List[float]
186-
err, measurements = self._interface.byonoy_abs96_single_measure(handle, conf)
187-
self._raise_if_error(err.name, "Error getting single measurement: ")
188-
return measurements
189-
190271
def _set_sample_wavelength(self, wavelength: int) -> AbsProtocol.MeasurementConfig:
191272
if not self._supported_wavelengths:
192273
self._get_supported_wavelengths()
@@ -204,100 +285,35 @@ def _initialize(self, wavelength: int) -> None:
204285
conf = self._set_sample_wavelength(wavelength)
205286
self._initialize_measurement(conf)
206287

207-
def _get_single_measurement(self, wavelength: int) -> List[float]:
208-
initialized = self._current_config
209-
assert initialized and initialized.sample_wavelength == wavelength
210-
return self._single_measurement(initialized)
211-
212-
async def open(self) -> None:
213-
"""
214-
Open the connection.
215-
216-
Returns: None
217-
"""
218-
return await self._loop.run_in_executor(
219-
executor=self._executor, func=self._open
220-
)
221-
222-
async def close(self) -> None:
223-
"""
224-
Close the connection
225-
226-
Returns: None
227-
"""
228-
await self._loop.run_in_executor(executor=self._executor, func=self._free)
229-
230-
async def is_open(self) -> bool:
231-
"""
232-
Check if connection is open.
233-
234-
Returns: boolean
235-
"""
236-
return self._device_handle is not None
237-
238-
async def get_device_static_info(self) -> Dict[str, str]:
239-
return {
240-
"serial": self._device.sn,
241-
"model": "ABS96",
242-
"version": "1.0",
243-
}
244-
245-
async def get_device_information(self) -> Dict[str, str]:
246-
device_info = await self._loop.run_in_executor(
247-
executor=self._executor, func=self._get_device_information
248-
)
249-
return {
250-
"serial_number": device_info.sn,
251-
"reference_number": device_info.ref_no,
252-
"version": device_info.version,
253-
}
254-
255-
async def get_lid_status(self) -> AbsorbanceReaderLidStatus:
256-
lid_info = await self._loop.run_in_executor(
257-
executor=self._executor, func=self._get_lid_status
258-
)
259-
return (
260-
AbsorbanceReaderLidStatus.ON if lid_info else AbsorbanceReaderLidStatus.OFF
261-
)
262-
263-
async def get_supported_wavelengths(self) -> list[int]:
264-
return await self._loop.run_in_executor(
265-
executor=self._executor, func=self._get_supported_wavelengths
266-
)
267-
268288
async def initialize(self, wavelength: int) -> None:
269-
return await self._loop.run_in_executor(
289+
"""???"""
290+
await self._loop.run_in_executor(
270291
executor=self._executor, func=partial(self._initialize, wavelength)
271292
)
272293

273-
async def get_single_measurement(self, wavelength: int) -> List[float]:
274-
return await self._loop.run_in_executor(
275-
executor=self._executor,
276-
func=partial(self._get_single_measurement, wavelength),
277-
)
278-
279-
async def get_plate_presence(self) -> AbsorbanceReaderPlatePresence:
280-
presence = await self._loop.run_in_executor(
281-
executor=self._executor, func=self._get_slot_status
294+
def _verify_device_handle(self) -> int:
295+
assert self._device_handle is not None, RuntimeError(
296+
"Device handle not set up."
282297
)
283-
return self.convert_plate_presence(presence.name)
298+
return self._device_handle
284299

285-
async def get_device_status(self) -> AbsorbanceReaderDeviceState:
286-
status = await self._loop.run_in_executor(
287-
executor=self._executor,
288-
func=self._get_device_status,
289-
)
290-
return self.convert_device_state(status.name)
300+
def _raise_if_error(
301+
self,
302+
err_name: ErrorCodeNames,
303+
msg: str = "Error occurred: ",
304+
) -> None:
305+
if err_name != "BYONOY_ERROR_NO_ERROR":
306+
raise RuntimeError(msg, err_name)
291307

292308
@staticmethod
293309
def convert_device_state(
294310
device_state: DeviceStateNames,
295311
) -> AbsorbanceReaderDeviceState:
296312
state_map: Dict[DeviceStateNames, AbsorbanceReaderDeviceState] = {
297-
"BYONOY_DEVICE_STATE_UNKNOWN": AbsorbanceReaderDeviceState.UNKNOWN,
298-
"BYONOY_DEVICE_STATE_OK": AbsorbanceReaderDeviceState.OK,
299-
"BYONOY_DEVICE_STATE_BROKEN_FW": AbsorbanceReaderDeviceState.BROKEN_FW,
300-
"BYONOY_DEVICE_STATE_ERROR": AbsorbanceReaderDeviceState.ERROR,
313+
"UNKNOWN": AbsorbanceReaderDeviceState.UNKNOWN,
314+
"OK": AbsorbanceReaderDeviceState.OK,
315+
"BROKEN_FW": AbsorbanceReaderDeviceState.BROKEN_FW,
316+
"ERROR": AbsorbanceReaderDeviceState.ERROR,
301317
}
302318
return state_map[device_state]
303319

0 commit comments

Comments
 (0)