Skip to content

Conversation

@hazlamshamin
Copy link
Contributor

@hazlamshamin hazlamshamin commented Dec 20, 2025

  • Add Tecan Infinite 200 PRO backend + tests.
  • Supports absorbance and fluorescence, including masking to read specified list of wells, up to 384-well plate.
  • Luminescence is experimental due to lack of glowing sample for testing.
  • Make USB capture decoding resilient to malformed escape sequences.

EDIT: all three modes now reflect what OEM does with 100% accuracy, and luminescence is no longer experimental ([e92c6db])

Tests: make lint, make format-check, make typecheck, make test.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces a backend for the Tecan Infinite 200 PRO plate reader (M Plex series), adding support for absorbance and fluorescence measurements with experimental luminescence support. The implementation includes configurable well masking for reading specific wells on plates up to 384-well format.

Key changes:

  • New TecanInfinite200ProBackend class with USB communication and measurement decoding
  • Specialized decoders for absorbance, fluorescence, and luminescence measurement streams
  • Enhanced USB capture error handling to gracefully manage malformed escape sequences

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 5 comments.

File Description
pylabrobot/plate_reading/tecan_infinite_backend.py Implements the complete backend with transport layer, measurement decoders, and plate reading methods for all three modes
pylabrobot/plate_reading/tecan_infinite_backend_tests.py Comprehensive unit tests for decoders, scan geometry, and ASCII frame handling
pylabrobot/plate_reading/init.py Exports the new backend and configuration class for public API access
pylabrobot/io/usb.py Adds error handling to decode operations to prevent crashes from malformed USB data

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Copy link
Member

@rickwierenga rickwierenga left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

apologies for the late response, I was working on slas and then the old nimbus PR

I have two small initial questions / comments for things I think we can parameterize easily

rickwierenga and others added 3 commits January 20, 2026 16:10
- USB._read_packet() now accepts optional size to read exact bytes from wire
- USB.read() passes remaining byte count to _read_packet() when size specified
- PyUSBInfiniteTransport.read() uses new size parameter instead of slicing
- USBValidator.read() updated for interface consistency

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Remove InfiniteTransport protocol and PyUSBInfiniteTransport class
- TecanInfinite200ProBackend creates USB instance internally as self.io
- Move reset logic into backend's _recover_transport method
- Simplifies the transport layer by removing unnecessary abstraction

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@rickwierenga
Copy link
Member

could you please explain what _MODE_CAPABILITY_COMMANDS are?

are they just more parameters like this?

      f"BEAM DIAMETER={self._capability_numeric('ABS', '#BEAM DIAMETER', 700)}",

that we can add as commands? why not add them?

@rickwierenga rickwierenga mentioned this pull request Jan 21, 2026
rickwierenga and others added 5 commits January 20, 2026 19:30
Replace custom _u16be, _u32be, _i32be helper functions with the Reader
class from io/binary using little_endian=False for big-endian parsing.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
rickwierenga and others added 6 commits January 20, 2026 20:03
Remove redundant step_loss local variable and parameter since _end_run
can access self._active_step_loss_commands directly.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Inline command lists in _configure_absorbance, _configure_fluorescence,
  and _configure_luminescence with direct _send_ascii calls
- Refactor _cleanup_protocol to use a local helper function instead of
  building a commands list

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Rename:
- _send_ascii -> _send_command
- _frame_ascii_command -> _frame_command
- _drain_ascii -> _drain
- _read_ascii_response -> _read_command_response
- _ascii_parser -> _parser

Also update related docstrings.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Tests verify that open, close, read_absorbance, read_fluorescence, and
read_luminescence send the correct USB commands in the correct order.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
rickwierenga and others added 7 commits January 24, 2026 11:13
Reduces duplication across read_absorbance, read_fluorescence, and
read_luminescence methods. Also removes unused _active_mode field.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Callers now compute ordered_wells and scan_wells once and pass
ordered_wells to _run_scan directly.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The "Prepare" naming came from the device command "PREPARE REF" but
didn't convey what the data actually represents. These structures
contain calibration data (dark/bright references, gain values) used
to convert raw measurements into calibrated values.

Renamed:
- _AbsorbancePrepare -> _AbsorbanceCalibration
- _FluorescencePrepare -> _FluorescenceCalibration
- _LuminescencePrepare -> _LuminescenceCalibration
- Related functions, properties, and variables

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Use distinct variable names (cal/data) instead of reusing 'decoded'
for both calibration and measurement data decoding to avoid mypy
type conflicts.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The "marker" name was misleading - it's actually the binary payload
length extracted from protocol frames like "18,BIN:". The length
happens to identify packet type (different packet types have
characteristic sizes), but calling it "marker" obscured its true
meaning.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Comment on lines 975 to 977
async def _begin_run(self) -> None:
await self._initialize_device()
self._reset_stream_state()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is there a reason the device would un-initialize? every command calls _begin_run, which calls _initialize_device. but _initialize_device is already called on setup.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Currently, this is the fallback when there is any glitch with the usb. _device_initialized=True and no re-initialisation is being done if the flag remains True. However, I did notice, like sometimes, after leaving the machine for few hours, when I tried to read the plate, the computer fails to connect/control the plate reader (even after I have included this fallback). Still unsure what is the reason for this, that I had to physically power cycle and disconnect-reconnect the usb cable for it to work. Do you have any idea why this happen or what's the solution for this?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

chat suggests

  1. USB Selective Suspend - Windows/Linux power management suspends idle USB devices. Can be disabled in OS settings or
    via libusb hints.
  2. Device-side timeout - The plate reader itself may go into standby after idle time. The current _recover_transport
    only triggers on read timeout, but the connection may appear "alive" while the device is unresponsive.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

so _device_initialized = True after hours of idle doesn't mean the device is actually responsive. I am removing the test because it would involve sending a command to the machine as a check, but we are already sending a command to the machine anyway

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

rickwierenga and others added 4 commits January 24, 2026 14:40
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Lazily initialize the asyncio.Lock to avoid "no current event loop"
error on Python 3.9, which requires an event loop when creating locks.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@hazlamshamin
Copy link
Contributor Author

hazlamshamin commented Jan 30, 2026

could you please explain what _MODE_CAPABILITY_COMMANDS are?

are they just more parameters like this?

      f"BEAM DIAMETER={self._capability_numeric('ABS', '#BEAM DIAMETER', 700)}",

that we can add as commands? why not add them?

I'm unable to directly comment to this so I'm quote replying.

_MODE_CAPABILITY_COMMANDS are meant to fetch the capability parameters supported by the machines (in case it is different for other models), as UI does this during initialisation. And yes, they are similar to the line that you quote. The idea was to query those parameters, then save them to be used for downstream commands. However, sometimes, I got errors during querying the full capability commands (the reason why I commented them now and leaving them there as reference). Therefore, I just did the very necessary one now, which is the beam diameter, with safe fallback of the default for the machine.

rickwierenga and others added 6 commits January 30, 2026 21:02
…cence

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Remove misleading _device_initialized flag that didn't reflect actual
  device responsiveness
- Remove redundant _initialize_device() call from _begin_run()
- Call _initialize_device() directly in _recover_transport() after USB
  recovery

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants