Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
fe3c00a
feat(hamilton): port full TCP transport client onto transport/tcp
cmoscy Aug 8, 2026
4e71a71
fix(hamilton.transport.tcp): run client API tests under an event loop
cmoscy Aug 9, 2026
4ea2458
feat(hamilton): add Prep liquid handler on TCP transport
cmoscy Aug 8, 2026
5f8bc64
feat(hamilton.prep): add PrepGripperArm drop_resource and coordinate …
cmoscy Aug 9, 2026
61e8e58
feat(hamilton.prep): switch channels and head8 to resource-facing APIs
cmoscy Aug 9, 2026
83fdb2c
feat(resources): add shared tip/volume/deck state finalize helpers
cmoscy Aug 9, 2026
7b46677
feat(hamilton.prep): track tips and volumes on channels and head8
cmoscy Aug 9, 2026
c7d7ddf
fix(hamilton.prep): pre-position channels before CoRe tool pickup
cmoscy Aug 9, 2026
d1d14df
docs(hamilton.prep): enable tip and volume tracking in basic demo
cmoscy Aug 9, 2026
6ae095c
docs(hamilton.prep): add head8 transfer to basic demo and wire Sphinx
cmoscy Aug 9, 2026
82a52ba
fix(hamilton.prep): make PrepCommand.dest Python 3.9 / mypy clean
cmoscy Aug 9, 2026
04adcbe
style(resources): isort resource_state re-exports in __init__
cmoscy Aug 9, 2026
24a0c51
docs(hamilton.prep): drop local notebook excludes from Sphinx conf
cmoscy Aug 9, 2026
cccf1de
Merge remote-tracking branch 'origin/main' into hamilton-tcp-transport
rickwierenga Aug 16, 2026
fb26a97
fix(hamilton): mark transport as a package
rickwierenga Aug 16, 2026
8627f84
Move Nimbus and Prep error tables
rickwierenga Aug 16, 2026
f6431fc
fix(hamilton.transport.tcp): never retransmit a command; drop auto-re…
cmoscy Aug 24, 2026
3337d56
refactor(hamilton.transport.tcp): replace attribute reflection with d…
cmoscy Aug 24, 2026
1ff6cd0
feat(hamilton.transport.tcp): serialize commands on a transaction lock
cmoscy Aug 24, 2026
4fe6613
feat(hamilton.transport.tcp): read frames on a background task
cmoscy Aug 24, 2026
19b49ae
test(hamilton.transport.tcp): cover connection lifecycle and reader r…
cmoscy Aug 24, 2026
e463803
fix(hamilton.transport.tcp): keep the reader alive on non-command frames
cmoscy Aug 24, 2026
3fae488
docs(changelog): note the HARP control-frame fix (#1195)
cmoscy Aug 24, 2026
c7ed8eb
fix(hamilton.transport.tcp): stop error enrichment from recursing
cmoscy Aug 24, 2026
599b2ec
Merge remote-tracking branch 'origin/hamilton-tcp-transport' into ham…
cmoscy Aug 24, 2026
3fa6edb
Merge hamilton-tcp-transport into hamilton-prep
cmoscy Aug 24, 2026
12fe4f4
fix(hamilton.transport.tcp): keep command identity attributes overrid…
cmoscy Aug 24, 2026
8423ca7
style(hamilton): sort imports after the error-table move
cmoscy Aug 24, 2026
e64163a
test(hamilton.transport.tcp): feed reader frames only once a command …
cmoscy Aug 24, 2026
3dfe2e9
Merge branch 'hamilton-tcp-transport' into hamilton-prep
cmoscy Aug 24, 2026
3e07fd9
style(hamilton.transport.tcp): spell unparsable the way the repo does
cmoscy Aug 24, 2026
6d42e7c
Merge branch 'hamilton-tcp-transport' into hamilton-prep
cmoscy Aug 24, 2026
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
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,22 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
- `Plate`: optional `stacking_z_height` parameter -- the per-plate vertical pitch when plates are stacked directly on top of each other (`size_z` minus the nesting overlap), mirroring `NestedTipRack.stacking_z_height`. Because it is a physical dimension, plates that differ in it no longer compare equal; `Plate` also now serializes `stacking_z_height` and the pre-existing `plate_type` so both round-trip through `deserialize`/`copy`. (#1110)
- `ResourceStack`: bare plates stacked in the z direction now nest into one another by their `stacking_z_height` (a stack of `N` identical plates is `size_z + (N - 1) * stacking_z_height` tall, for both `get_size_z()` and child placement). Plates without a `stacking_z_height`, and plates wearing a lid, do not nest, so existing behaviour is unchanged. (#1112)

- Background reader task on `pylabrobot.hamilton.transport.tcp.HamiltonTCPClient` that owns the socket for the session, so `on_event` subscribers receive events between commands and a response arriving with no command waiting is dropped and logged instead of being handed to the next command (#1195).
- Command serialization on `HamiltonTCPClient`: one command is in flight at a time. The lock spans write through terminal response and is released before the response is decoded, because error enrichment sends further commands through the same path (#1195).
- `ObjectRegistry.clear()` (`pylabrobot.hamilton.transport.tcp.introspection`), used to drop path and address mappings that are scoped to a single connected session (#1195).

### Fixed

- Imported `unittest.mock` in `pylabrobot/centrifuge/centrifuge_tests.py` (pre-existing bug that prevented the test class from running).
- `HamiltonTCPClient` no longer retransmits a command after a failed read. A read timeout on a slow motion command previously re-sent it, which could execute the motion twice (#1195).
- `HamiltonTCPClient.setup()` now resets all per-session state (client id, sequence numbers, instrument addresses, object registry) rather than carrying it into the new session, and refuses to run on an already-connected client instead of leaking the socket (#1195).
- `HamiltonTCPClient` no longer recurses without bound when a device fails the introspection queries that error enrichment itself issues. Enrichment is now non-re-entrant and falls back to the static HC_RESULT tables, so a degraded instrument yields a terse error instead of a `RecursionError` (#1195).
- `HamiltonTCPClient` no longer fails every command after the device sends a HARP control frame. MLPrep firmware sends one (options, no HOI body) shortly after registration; it was parsed as a command response and killed the reader. Frames with no routable message are skipped, as are unparsable frames, which is safe because frames are length-prefixed and consumed whole (#1195).

### Changed

- `HamiltonTCPClient` no longer reconnects automatically; `auto_reconnect` and `max_reconnect_attempts` are gone from its constructor. Recovery is `await client.stop()` followed by `await client.setup()`, matching every other transport in the library. `is_connected` remains for callers implementing their own policy (#1195).
- `TCPCommand` declares `Response` and `uses_physical_channels` as class attributes instead of the transport inferring them by attribute probing. Commands with per-channel firmware errors must set `uses_physical_channels = True` to raise `ChannelizedError` (#1195).

## 0.2.1

Expand Down
7 changes: 7 additions & 0 deletions docs/api/pylabrobot.hamilton.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,10 @@

pylabrobot.hamilton package
===========================

.. autosummary::
:toctree: _autosummary
:recursive:

prep
transport.tcp
485 changes: 485 additions & 0 deletions docs/user_guide/00_liquid-handling/hamilton-prep/prep_basic_demo.ipynb

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions docs/user_guide/hamilton/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@
:maxdepth: 1

star/index
prep/index
```
7 changes: 7 additions & 0 deletions docs/user_guide/hamilton/prep/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Prep

```{toctree}
:maxdepth: 1

../../00_liquid-handling/hamilton-prep/prep_basic_demo
```
100 changes: 100 additions & 0 deletions pylabrobot/hamilton/liquid_class_resolver.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
"""Resolve Hamilton liquid classes and corrected volumes for Prep PIP ops.

Automatic lookup defaults to
:func:`~pylabrobot.legacy.liquid_handling.liquid_classes.hamilton.star.get_star_liquid_class`
(STAR calibration tables); pass ``lookup=`` for instrument-specific tables.
"""

from __future__ import annotations

from typing import Any, Callable, List, Optional, Sequence, Union

from pylabrobot.legacy.liquid_handling.liquid_classes.hamilton.base import HamiltonLiquidClass
from pylabrobot.resources.hamilton import HamiltonTip
from pylabrobot.resources.liquid import Liquid

_Lookup = Callable[..., Optional[HamiltonLiquidClass]]


def resolve_hamilton_liquid_classes(
explicit: Optional[List[Optional[HamiltonLiquidClass]]],
ops: list,
*,
jet: Union[bool, List[bool]] = False,
blow_out: Union[bool, List[bool]] = False,
is_aspirate: bool = True,
lookup: Optional[_Lookup] = None,
) -> List[Optional[HamiltonLiquidClass]]:
"""Resolve per-op Hamilton liquid classes.

If ``explicit`` is None, resolve from each op's tip via ``lookup`` (default
:func:`get_star_liquid_class`). Non-``HamiltonTip`` tips yield ``None``.

If ``explicit`` is a list, it is returned as a shallow copy; ``None`` entries
are preserved (legacy STAR behavior).

Args:
explicit: Caller-provided liquid classes, or None for automatic lookup.
ops: Aspiration or dispense operations (must have a ``tip`` attribute).
jet: Per-op or scalar flags passed to automatic liquid class lookup.
blow_out: Per-op or scalar flags passed to automatic liquid class lookup.
is_aspirate: Reserved for API compatibility with STAR; unused.
lookup: Optional callable with the same signature as ``get_star_liquid_class``.
"""
del is_aspirate
n = len(ops)
if isinstance(jet, bool):
jet = [jet] * n
if isinstance(blow_out, bool):
blow_out = [blow_out] * n

if explicit is not None:
return list(explicit)

if lookup is None:
# Lazy import avoids circular import: star package __init__ may pull in pip_backend,
# which imports this module.
from pylabrobot.legacy.liquid_handling.liquid_classes.hamilton.star import get_star_liquid_class

fn = get_star_liquid_class
else:
fn = lookup
result: List[Optional[HamiltonLiquidClass]] = []
for i, op in enumerate(ops):
tip = op.tip
if not isinstance(tip, HamiltonTip):
result.append(None)
continue
result.append(
fn(
tip_volume=tip.maximal_volume,
is_core=False,
is_tip=True,
has_filter=tip.has_filter,
liquid=Liquid.WATER,
jet=jet[i],
blow_out=blow_out[i],
)
)

return result


def corrected_volumes_for_ops(
ops: Sequence[Any],
hlcs: Sequence[Optional[HamiltonLiquidClass]],
disable_volume_correction: Optional[Sequence[bool]] = None,
) -> List[float]:
"""Apply liquid-class volume correction per op when enabled."""
n = len(ops)
if len(hlcs) != n:
raise ValueError(f"hlcs length must match ops ({n}), got {len(hlcs)}")
dvc = list(disable_volume_correction) if disable_volume_correction is not None else [False] * n
if len(dvc) != n:
raise ValueError(f"disable_volume_correction length must match ops ({n}), got {len(dvc)}")
return [
float(hlc.compute_corrected_volume(op.volume))
if hlc is not None and not disabled
else float(op.volume)
for op, hlc, disabled in zip(ops, hlcs, dvc)
]
1 change: 1 addition & 0 deletions pylabrobot/hamilton/nimbus/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Hamilton Nimbus support."""
Loading
Loading