Skip to content

Commit 6e48b50

Browse files
authored
add io layer (#392)
1 parent 984c83d commit 6e48b50

File tree

45 files changed

+1812
-897
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

45 files changed

+1812
-897
lines changed

docs/user_guide/index.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ hamilton-star/hamilton-star
2020
moving-channels-around
2121
tip-spot-generators
2222
96head
23+
validation
2324
```
2425

2526
```{toctree}

docs/user_guide/validation.ipynb

Lines changed: 255 additions & 0 deletions
Large diffs are not rendered by default.

pylabrobot/__init__.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
11
import datetime
22
import logging
3-
import sys
4-
import warnings
53
from pathlib import Path
64
from typing import Optional, Union
75

86
from pylabrobot.__version__ import __version__
97
from pylabrobot.config import Config, load_config
8+
from pylabrobot.io import end_validation, start_capture, stop_capture, validate
109

1110
CONFIG_FILE_NAME = "pylabrobot"
1211

pylabrobot/centrifuge/backend.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
from abc import ABCMeta, abstractmethod
44

5-
from pylabrobot.machines.backends import MachineBackend
5+
from pylabrobot.machines.backend import MachineBackend
66

77

88
class CentrifugeBackend(MachineBackend, metaclass=ABCMeta):

pylabrobot/centrifuge/vspin.py

Lines changed: 27 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,12 @@
33
import time
44
from typing import Optional, Union
55

6+
from pylabrobot.io.ftdi import FTDI
7+
68
from .backend import CentrifugeBackend, LoaderBackend
79
from .standard import LoaderNoPlateError
810

9-
try:
10-
from pylibftdi import Device
11-
12-
USE_FTDI = True
13-
except ImportError:
14-
USE_FTDI = False
15-
16-
17-
logger = logging.getLogger("pylabrobot.centrifuge.vspin")
11+
logger = logging.getLogger(__name__)
1812

1913

2014
class Access2Backend(LoaderBackend):
@@ -28,15 +22,15 @@ def __init__(
2822
device_id: The libftdi id for the loader. Find using
2923
`python3 -m pylibftdi.examples.list_devices`
3024
"""
31-
self.dev = Device(lazy_open=True, device_id=device_id)
25+
self.io = FTDI(device_id=device_id)
3226
self.timeout = timeout
3327

3428
async def _read(self) -> bytes:
3529
x = b""
3630
r = None
3731
start = time.time()
3832
while r != b"" or x == b"":
39-
r = self.dev.read(1)
33+
r = self.io.read(1)
4034
x += r
4135
if r == b"":
4236
await asyncio.sleep(0.1)
@@ -46,17 +40,16 @@ async def _read(self) -> bytes:
4640

4741
async def send_command(self, command: bytes) -> bytes:
4842
logger.debug("[loader] Sending %s", command.hex())
49-
self.dev.write(command)
43+
self.io.write(command)
5044
return await self._read()
5145

5246
async def setup(self):
5347
logger.debug("[loader] setup")
5448

55-
self.dev.open()
56-
self.dev.baudrate = 115384
49+
await self.io.setup()
50+
self.io.set_baudrate(115384)
5751

5852
status = await self.get_status()
59-
print("status", status)
6053
if not status.startswith(bytes.fromhex("1105")):
6154
raise RuntimeError("Failed to get status")
6255

@@ -75,10 +68,10 @@ async def setup(self):
7568

7669
async def stop(self):
7770
logger.debug("[loader] stop")
78-
self.dev.close()
71+
await self.io.stop()
7972

8073
def serialize(self):
81-
return {"device_id": self.dev.device_id, "timeout": self.timeout}
74+
return {"io": self.io.serialize(), "timeout": self.timeout}
8275

8376
async def get_status(self) -> bytes:
8477
logger.debug("[loader] get_status")
@@ -145,16 +138,12 @@ def __init__(self, bucket_1_position: int, device_id: Optional[str] = None):
145138
an arbitrary value, move to the bucket, and call get_position() to get the position. Then
146139
use this value for future runs.
147140
"""
148-
if not USE_FTDI:
149-
raise RuntimeError("pylibftdi is not installed.")
150-
self.dev = Device(lazy_open=True, device_id=device_id)
141+
self.io = FTDI(device_id=device_id)
151142
self.bucket_1_position = bucket_1_position
152143
self.homing_position = 0
153-
self.device_id = device_id
154144

155145
async def setup(self):
156-
self.dev.open()
157-
logger.debug("open")
146+
await self.io.setup()
158147
# TODO: add functionality where if robot has been intialized before nothing needs to happen
159148
for _ in range(3):
160149
await self.configure_and_initialize()
@@ -166,9 +155,9 @@ async def setup(self):
166155
await self.send(b"\xaa\x00\x21\x03\xff\x23")
167156
await self.send(b"\xaa\xff\x1a\x14\x2d")
168157

169-
self.dev.baudrate = 57600
170-
self.dev.ftdi_fn.ftdi_setrts(1)
171-
self.dev.ftdi_fn.ftdi_setdtr(1)
158+
self.io.set_baudrate(57600)
159+
self.io.set_rts(True)
160+
self.io.set_dtr(True)
172161

173162
await self.send(b"\xaa\x01\x0e\x0f")
174163
await self.send(b"\xaa\x01\x12\x1f\x32")
@@ -264,7 +253,7 @@ async def setup(self):
264253
async def stop(self):
265254
await self.send(b"\xaa\x02\x0e\x10")
266255
await self.configure_and_initialize()
267-
self.dev.close()
256+
await self.io.stop()
268257

269258
async def get_status(self):
270259
"""Returns 14 bytes
@@ -296,15 +285,12 @@ async def get_position(self):
296285
async def read_resp(self, timeout=20) -> bytes:
297286
"""Read a response from the centrifuge. If the timeout is reached, return the data that has
298287
been read so far."""
299-
if not self.dev:
300-
raise RuntimeError("Device not initialized")
301-
302288
data = b""
303289
end_byte_found = False
304290
start_time = time.time()
305291

306292
while True:
307-
chunk = self.dev.read(25)
293+
chunk = self.io.read(25)
308294
if chunk:
309295
data += chunk
310296
end_byte_found = data[-1] == 0x0D
@@ -320,9 +306,7 @@ async def read_resp(self, timeout=20) -> bytes:
320306
return data
321307

322308
async def send(self, cmd: Union[bytearray, bytes], read_timeout=0.2) -> bytes:
323-
logger.debug("Sending %s", cmd.hex())
324-
written = self.dev.write(cmd.decode("latin-1"))
325-
logger.debug("Wrote %s bytes", written)
309+
written = self.io.write(bytes(cmd)) # TODO: why decode? .decode("latin-1")
326310

327311
if written != len(cmd):
328312
raise RuntimeError("Failed to write all bytes")
@@ -343,18 +327,17 @@ async def configure_and_initialize(self):
343327

344328
async def set_configuration_data(self):
345329
"""Set the device configuration data."""
346-
self.dev.ftdi_fn.ftdi_set_latency_timer(16)
347-
self.dev.ftdi_fn.ftdi_set_line_property(8, 1, 0)
348-
self.dev.ftdi_fn.ftdi_setflowctrl(0)
349-
self.dev.baudrate = 19200
330+
self.io.set_latency_timer(16)
331+
self.io.set_line_property(bits=8, stopbits=1, parity=0)
332+
self.io.set_flowctrl(0)
333+
self.io.set_baudrate(19200)
350334

351335
async def initialize(self):
352-
if self.dev:
353-
self.dev.write(b"\x00" * 20)
354-
for i in range(33):
355-
packet = b"\xaa" + bytes([i & 0xFF, 0x0E, 0x0E + (i & 0xFF)]) + b"\x00" * 8
356-
self.dev.write(packet)
357-
await self.send(b"\xaa\xff\x0f\x0e")
336+
self.io.write(b"\x00" * 20)
337+
for i in range(33):
338+
packet = b"\xaa" + bytes([i & 0xFF, 0x0E, 0x0E + (i & 0xFF)]) + b"\x00" * 8
339+
self.io.write(packet)
340+
await self.send(b"\xaa\xff\x0f\x0e")
358341

359342
# Centrifuge operations
360343

pylabrobot/heating_shaking/hamilton.py

Lines changed: 12 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,17 @@
22
from typing import Literal
33

44
from pylabrobot.heating_shaking.backend import HeaterShakerBackend
5-
from pylabrobot.machines.backends.usb import USBBackend
5+
from pylabrobot.io.usb import USB
66

77

88
class PlateLockPosition(Enum):
99
LOCKED = 1
1010
UNLOCKED = 0
1111

1212

13-
class HamiltonHeatShaker(HeaterShakerBackend, USBBackend):
13+
class HamiltonHeatShaker(HeaterShakerBackend):
1414
"""
15-
Backend for Hamilton Heater Shaker devices connected through
16-
an Heat Shaker Box
15+
Backend for Hamilton Heater Shaker devices connected through an Heater Shaker Box
1716
"""
1817

1918
def __init__(
@@ -30,39 +29,35 @@ def __init__(
3029
self.shaker_index = shaker_index
3130
self.command_id = 0
3231

33-
HeaterShakerBackend.__init__(self)
34-
USBBackend.__init__(self, id_vendor, id_product)
32+
super().__init__()
33+
self.io = USB(id_vendor=id_vendor, id_product=id_product)
3534

3635
async def setup(self):
3736
"""
38-
If USBBackend.setup() fails, ensure that libusb drivers were installed
39-
for the HHS as per PyLabRobot documentation.
37+
If io.setup() fails, ensure that libusb drivers were installed for the HHS as per docs.
4038
"""
41-
await USBBackend.setup(self)
39+
await self.io.setup()
4240
await self._initialize_lock()
4341

4442
async def stop(self):
45-
await USBBackend.stop(self)
43+
await self.io.stop()
4644

4745
def serialize(self) -> dict:
48-
usb_backend_serialized = USBBackend.serialize(self)
46+
usb_serialized = self.io.serialize()
4947
heater_shaker_serialized = HeaterShakerBackend.serialize(self)
5048
return {
51-
**usb_backend_serialized,
49+
**usb_serialized,
5250
**heater_shaker_serialized,
5351
"shaker_index": self.shaker_index,
5452
}
5553

5654
def _send_command(self, command: str, **kwargs):
5755
assert len(command) == 2, "Command must be 2 characters long"
5856
args = "".join([f"{key}{value}" for key, value in kwargs.items()])
59-
USBBackend.write(
60-
self,
61-
f"T{self.shaker_index}{command}id{str(self.command_id).zfill(4)}{args}",
62-
)
57+
self.io.write(f"T{self.shaker_index}{command}id{str(self.command_id).zfill(4)}{args}".encode())
6358

6459
self.command_id = (self.command_id + 1) % 10_000
65-
return USBBackend.read(self)
60+
return self.io.read()
6661

6762
async def shake(
6863
self,

pylabrobot/heating_shaking/inheco.py

Lines changed: 7 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,7 @@
22
import typing
33

44
from pylabrobot.heating_shaking.backend import HeaterShakerBackend
5-
6-
try:
7-
import hid # type: ignore
8-
9-
USE_IDE = True
10-
except ImportError:
11-
USE_IDE = False
5+
from pylabrobot.io.hid import HID
126

137

148
class InhecoThermoShake(HeaterShakerBackend):
@@ -18,26 +12,20 @@ class InhecoThermoShake(HeaterShakerBackend):
1812
"""
1913

2014
def __init__(self, vid=0x03EB, pid=0x2023, serial_number=None):
21-
self.vid = vid
22-
self.pid = pid
23-
self.serial_number = serial_number
15+
self.io = HID(vid=vid, pid=pid, serial_number=serial_number)
2416

2517
async def setup(self):
26-
if not USE_IDE:
27-
raise RuntimeError("This backend requires the `hid` package to be installed")
28-
self.device = hid.Device(vid=self.vid, pid=self.pid, serial=self.serial_number)
18+
await self.io.setup()
2919

3020
async def stop(self):
3121
await self.stop_shaking()
3222
await self.stop_temperature_control()
33-
self.device.close()
23+
await self.io.stop()
3424

3525
def serialize(self) -> dict:
3626
return {
3727
**super().serialize(),
38-
"vid": self.vid,
39-
"pid": self.pid,
40-
"serial_number": self.serial_number,
28+
**self.io.serialize(),
4129
}
4230

4331
@typing.no_type_check
@@ -102,7 +90,7 @@ def _read_until_end(self, timeout: int) -> str:
10290
start = time.time()
10391
response = b""
10492
while time.time() - start < timeout:
105-
packet = self.device.read(64, timeout=timeout)
93+
packet = self.io.read(64, timeout=timeout)
10694
if packet is not None and packet != b"":
10795
if packet.endswith(b"\x00"):
10896
response += packet.rstrip(b"\x00") # strip trailing \x00's
@@ -139,7 +127,7 @@ async def send_command(self, command: str, timeout: int = 3):
139127
"""Send a command to the device and return the response"""
140128
packets = self._generate_packets(command)
141129
for packet in packets:
142-
self.device.write(bytes(packet))
130+
self.io.write(bytes(packet))
143131

144132
response = self._read_response(command, timeout=timeout)
145133

pylabrobot/incubators/backend.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from abc import ABCMeta, abstractmethod
22
from typing import List, Optional
33

4-
from pylabrobot.machines.backends import MachineBackend
4+
from pylabrobot.machines.backend import MachineBackend
55
from pylabrobot.resources import Plate, PlateCarrier, PlateHolder
66

77

0 commit comments

Comments
 (0)