Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
2a8c0a7
initial byonoy draft
rickwierenga Jul 23, 2025
6df2963
improve response parsing
rickwierenga Jul 26, 2025
f3a7c98
absorbance
rickwierenga Jul 27, 2025
d9be377
support device wavelengths
rickwierenga Jul 27, 2025
8f25dbf
use send_command in luminescence
rickwierenga Jul 27, 2025
d47624f
move into seperate classes
rickwierenga Jul 28, 2025
581d659
rename to backend
rickwierenga Jul 28, 2025
bce4852
resource modeling draft
rickwierenga Aug 9, 2025
7ba4fcc
some tests
rickwierenga Aug 10, 2025
fda5bd2
rewrite byonoy with delf suggestions
rickwierenga Oct 1, 2025
26492cb
Merge branch 'main' into byonoy-luminescence
rickwierenga Oct 1, 2025
204d7b7
lint, typo, type (not tests yet)
rickwierenga Oct 1, 2025
911d427
Merge branch 'main' into byonoy-luminescence
rickwierenga Oct 17, 2025
7c3fa78
Small Docs Update for Byonoy Absorbance 96 Automate (#690)
BioCam Oct 17, 2025
15a6a27
update type
rickwierenga Oct 17, 2025
a3f1c5c
relax the checks
rickwierenga Oct 17, 2025
125b7ff
lint
rickwierenga Oct 17, 2025
90f4262
try
rickwierenga Oct 21, 2025
701be0c
set get_available_absorbance_wavelengths routing info to 8040
rickwierenga Oct 21, 2025
e8b1b21
fix typo
rickwierenga Oct 21, 2025
36b2bd4
update
rickwierenga Oct 21, 2025
6ed6d33
typo
rickwierenga Oct 22, 2025
f9bca03
fix missing measurement initialization
BioCam Nov 30, 2025
e0dfbf2
upgrade pylabrobot.hid
BioCam Nov 30, 2025
36a033a
Merge branch 'main' into byonoy-luminescence
BioCam Dec 16, 2025
6d36ae9
update docs
BioCam Dec 17, 2025
23f573a
Merge branch 'main' into byonoy-luminescence
rickwierenga Dec 18, 2025
287ea02
fixes
rickwierenga Dec 18, 2025
c6c9367
type, use plr plate reading standard
rickwierenga Dec 18, 2025
903781f
testing
BioCam Dec 19, 2025
363092e
Merge branch 'main' into byonoy-luminescence
BioCam Dec 21, 2025
db772e6
slowly building new resource model
BioCam Dec 22, 2025
673557f
Merge branch 'main' into byonoy-luminescence
BioCam Dec 22, 2025
d4d5f6b
parameterise `ByonoyBaseUnit`
BioCam Dec 22, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,592 changes: 1,592 additions & 0 deletions docs/user_guide/02_analytical/plate-reading/byonoy.ipynb

Large diffs are not rendered by default.

Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
":maxdepth: 1\n",
"\n",
"bmg-clariostar\n",
"cytation5\n",
"byonoy\n",
"cytation\n",
"synergyh1\n",
"```\n",
Expand Down
59 changes: 56 additions & 3 deletions pylabrobot/io/hid.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,12 +40,64 @@ def __init__(self, vid=0x03EB, pid=0x2023, serial_number: Optional[str] = None):
raise RuntimeError("Cannot create a new HID object while capture or validation is active")

async def setup(self):
"""
Sets up the HID device by enumerating connected devices, matching the specified
VID, PID, and optional serial number, and opening a connection to the device.
"""
if not USE_HID:
raise RuntimeError(
f"This backend requires the `hid` package to be installed. Import error: {_HID_IMPORT_ERROR}"
)
self.device = hid.Device(vid=self.vid, pid=self.pid, serial=self.serial_number)

# --- 1. Enumerate all HID devices ---
all_devices = hid.enumerate()
matching = [
d for d in all_devices if d.get("vendor_id") == self.vid and d.get("product_id") == self.pid
]

# --- 2. No devices found ---
if not matching:
raise RuntimeError(f"No HID devices found for VID=0x{self.vid:04X}, PID=0x{self.pid:04X}.")

# --- 3. Serial number specified: must match exactly 1 ---
if self.serial_number is not None:
matching_sn = [d for d in matching if d.get("serial_number") == self.serial_number]

if not matching_sn:
raise RuntimeError(
f"No HID devices found with VID=0x{self.vid:04X}, PID=0x{self.pid:04X}, "
f"serial={self.serial_number}."
)

if len(matching_sn) > 1:
# Extremely unlikely, but must follow serial semantics
raise RuntimeError(
f"Multiple HID devices found with identical serial number "
f"{self.serial_number} for VID/PID {self.vid}:{self.pid}. "
"Ambiguous; cannot continue."
)

chosen = matching_sn[0]

# --- 4. Serial number not specified: require exactly one device ---
else:
if len(matching) > 1:
raise RuntimeError(
f"Multiple HID devices detected for VID=0x{self.vid:04X}, "
f"PID=0x{self.pid:04X}.\n"
f"Serial numbers: {[d.get('serial_number') for d in matching]}\n"
"Please specify `serial_number=` explicitly."
)
chosen = matching[0]

# --- 5. Open the device ---
self.device = hid.Device(
path=chosen["path"] # safer than vid/pid/serial triple
)
self._executor = ThreadPoolExecutor(max_workers=1)

self.device_info = chosen

logger.log(LOG_LEVEL_IO, "Opened HID device %s", self._unique_id)
capturer.record(HIDCommand(device_id=self._unique_id, action="open", data=""))

Expand Down Expand Up @@ -107,8 +159,9 @@ def _read():
if self._executor is None:
raise RuntimeError("Call setup() first.")
r = await loop.run_in_executor(self._executor, _read)
logger.log(LOG_LEVEL_IO, "[%s] read %s", self._unique_id, r)
capturer.record(HIDCommand(device_id=self._unique_id, action="read", data=r.hex()))
if len(r.hex()) != 0:
logger.log(LOG_LEVEL_IO, "[%s] read %s", self._unique_id, r)
capturer.record(HIDCommand(device_id=self._unique_id, action="read", data=r.hex()))
return cast(bytes, r)

def serialize(self):
Expand Down
5 changes: 4 additions & 1 deletion pylabrobot/liquid_handling/liquid_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -2040,6 +2040,9 @@ async def drop_resource(
raise RuntimeError("No resource picked up")
resource = self._resource_pickup.resource

if isinstance(destination, Resource):
destination.check_can_drop_resource_here(resource)

# compute rotation based on the pickup_direction and drop_direction
if self._resource_pickup.direction == direction:
rotation_applied_by_move = 0
Expand Down Expand Up @@ -2389,7 +2392,7 @@ async def move_plate(
**backend_kwargs,
)

def serialize(self):
def serialize(self) -> dict:
return {
**Resource.serialize(self),
**Machine.serialize(self),
Expand Down
6 changes: 6 additions & 0 deletions pylabrobot/plate_reading/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@
CytationImagingConfig,
)
from .agilent_biotek_synergyh1_backend import SynergyH1Backend
from .byonoy import (
ByonoyAbsorbance96AutomateBackend,
ByonoyLuminescence96AutomateBackend,
byonoy_absorbance96_base_and_reader,
byonoy_absorbance_adapter,
)
from .chatterbox import PlateReaderChatterboxBackend
from .clario_star_backend import CLARIOstarBackend
from .image_reader import ImageReader
Expand Down
9 changes: 9 additions & 0 deletions pylabrobot/plate_reading/byonoy/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
from .byonoy import (
byonoy_sbs_adapter,
ByonoyBaseUnit,
byonoy_a96a_detection_unit,
byonoy_a96a_illumination_unit,
byonoy_absorbance96_base_and_reader,
byonoy_absorbance_adapter,
)
from .byonoy_backend import ByonoyAbsorbance96AutomateBackend, ByonoyLuminescence96AutomateBackend
Loading
Loading