From 9bc9edb7ba029d6504eb4ccc0b154d6c45fdfff6 Mon Sep 17 00:00:00 2001 From: Camillo Moschner Date: Wed, 25 Feb 2026 22:48:35 +0000 Subject: [PATCH 1/5] Expose `STARBackend.channel_request_cycle_counts()` --- .../backends/hamilton/STAR_backend.py | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/pylabrobot/liquid_handling/backends/hamilton/STAR_backend.py b/pylabrobot/liquid_handling/backends/hamilton/STAR_backend.py index 8ad0392546e..b662cdb7d44 100644 --- a/pylabrobot/liquid_handling/backends/hamilton/STAR_backend.py +++ b/pylabrobot/liquid_handling/backends/hamilton/STAR_backend.py @@ -1603,6 +1603,52 @@ def ensure_can_reach_position( "Robots with more than 8 channels have limited Y-axis reach per channel; they don't have random access to the full deck area.\n" "Try the operation with different channels or a different target position (i.e. different labware placement)." ) + + async def channel_request_cycle_counts(self, channel_idx: int) -> dict: + """Request cycle counters for a single channel. + + Returns the number of tip pick-up, tip discard, aspiration, and dispensing cycles + performed by the channel. + + Args: + channel_idx: The channel index to query (0-indexed). + + Returns: + A dict with keys ``tip_pick_up_cycles``, ``tip_discard_cycles``, + ``aspiration_cycles``, and ``dispensing_cycles``. + """ + + if not (0 <= channel_idx < self.num_channels): + raise ValueError( + f"channel_idx must be between 0 and {self.num_channels - 1}, got {channel_idx}." + ) + + resp = await self.send_command( + module=STARBackend.channel_id(channel_idx), + command="RV", + fmt="na##########nb##########nc##########nd##########", + ) + return { + "tip_pick_up_cycles": resp["na"], + "tip_discard_cycles": resp["nb"], + "aspiration_cycles": resp["nc"], + "dispensing_cycles": resp["nd"], + } + + async def channels_request_cycle_counts(self) -> List[dict]: + """Request cycle counters for all channels. + + Returns: + A list of dicts (one per channel, ordered by channel index), each with keys + ``tip_pick_up_cycles``, ``tip_discard_cycles``, ``aspiration_cycles``, + and ``dispensing_cycles``. + """ + + results: List[dict] = [] + for idx in range(self.num_channels): + counts = await self.channel_request_cycle_counts(channel_idx=idx) + results.append(counts) + return results # # # ACTION Commands # # # From 3671634d9480fca82a74a6fcbfccc040b531211c Mon Sep 17 00:00:00 2001 From: Rick Wierenga Date: Wed, 25 Feb 2026 14:57:56 -0800 Subject: [PATCH 2/5] Use asyncio.gather for channels_request_cycle_counts Co-Authored-By: Claude Opus 4.6 --- .../liquid_handling/backends/hamilton/STAR_backend.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/pylabrobot/liquid_handling/backends/hamilton/STAR_backend.py b/pylabrobot/liquid_handling/backends/hamilton/STAR_backend.py index b662cdb7d44..245c05f0836 100644 --- a/pylabrobot/liquid_handling/backends/hamilton/STAR_backend.py +++ b/pylabrobot/liquid_handling/backends/hamilton/STAR_backend.py @@ -1644,11 +1644,9 @@ async def channels_request_cycle_counts(self) -> List[dict]: and ``dispensing_cycles``. """ - results: List[dict] = [] - for idx in range(self.num_channels): - counts = await self.channel_request_cycle_counts(channel_idx=idx) - results.append(counts) - return results + return list(await asyncio.gather( + *(self.channel_request_cycle_counts(channel_idx=idx) for idx in range(self.num_channels)) + )) # # # ACTION Commands # # # From eb7be1d4174b965cfd78348fa4b51b6b9f316a2d Mon Sep 17 00:00:00 2001 From: Rick Wierenga Date: Wed, 25 Feb 2026 14:59:09 -0800 Subject: [PATCH 3/5] Update pylabrobot/liquid_handling/backends/hamilton/STAR_backend.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- pylabrobot/liquid_handling/backends/hamilton/STAR_backend.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pylabrobot/liquid_handling/backends/hamilton/STAR_backend.py b/pylabrobot/liquid_handling/backends/hamilton/STAR_backend.py index 245c05f0836..fa48a8c5bb7 100644 --- a/pylabrobot/liquid_handling/backends/hamilton/STAR_backend.py +++ b/pylabrobot/liquid_handling/backends/hamilton/STAR_backend.py @@ -1624,7 +1624,7 @@ async def channel_request_cycle_counts(self, channel_idx: int) -> dict: ) resp = await self.send_command( - module=STARBackend.channel_id(channel_idx), + module=self.channel_id(channel_idx), command="RV", fmt="na##########nb##########nc##########nd##########", ) From a88e6f8792c982cf20229a74e518dde313acd9e8 Mon Sep 17 00:00:00 2001 From: Rick Wierenga Date: Wed, 25 Feb 2026 15:16:17 -0800 Subject: [PATCH 4/5] Use TypedDict for cycle counts return type, asyncio.gather for multi-channel Co-Authored-By: Claude Opus 4.6 --- .../liquid_handling/backends/hamilton/STAR_backend.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/pylabrobot/liquid_handling/backends/hamilton/STAR_backend.py b/pylabrobot/liquid_handling/backends/hamilton/STAR_backend.py index fa48a8c5bb7..3d132ba247b 100644 --- a/pylabrobot/liquid_handling/backends/hamilton/STAR_backend.py +++ b/pylabrobot/liquid_handling/backends/hamilton/STAR_backend.py @@ -20,6 +20,7 @@ Sequence, Tuple, Type, + TypedDict, TypeVar, Union, cast, @@ -1604,7 +1605,13 @@ def ensure_can_reach_position( "Try the operation with different channels or a different target position (i.e. different labware placement)." ) - async def channel_request_cycle_counts(self, channel_idx: int) -> dict: + class ChannelCycleCounts(TypedDict): + tip_pick_up_cycles: int + tip_discard_cycles: int + aspiration_cycles: int + dispensing_cycles: int + + async def channel_request_cycle_counts(self, channel_idx: int) -> ChannelCycleCounts: """Request cycle counters for a single channel. Returns the number of tip pick-up, tip discard, aspiration, and dispensing cycles @@ -1635,7 +1642,7 @@ async def channel_request_cycle_counts(self, channel_idx: int) -> dict: "dispensing_cycles": resp["nd"], } - async def channels_request_cycle_counts(self) -> List[dict]: + async def channels_request_cycle_counts(self) -> List[ChannelCycleCounts]: """Request cycle counters for all channels. Returns: From 6a12c7d87d4cf0b9c4fd6c397bbe620b4b38a993 Mon Sep 17 00:00:00 2001 From: Rick Wierenga Date: Wed, 25 Feb 2026 15:27:22 -0800 Subject: [PATCH 5/5] lint Co-Authored-By: Claude Opus 4.6 --- .../liquid_handling/backends/hamilton/STAR_backend.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/pylabrobot/liquid_handling/backends/hamilton/STAR_backend.py b/pylabrobot/liquid_handling/backends/hamilton/STAR_backend.py index 3d132ba247b..e2f2d3953c2 100644 --- a/pylabrobot/liquid_handling/backends/hamilton/STAR_backend.py +++ b/pylabrobot/liquid_handling/backends/hamilton/STAR_backend.py @@ -1604,7 +1604,7 @@ def ensure_can_reach_position( "Robots with more than 8 channels have limited Y-axis reach per channel; they don't have random access to the full deck area.\n" "Try the operation with different channels or a different target position (i.e. different labware placement)." ) - + class ChannelCycleCounts(TypedDict): tip_pick_up_cycles: int tip_discard_cycles: int @@ -1651,9 +1651,11 @@ async def channels_request_cycle_counts(self) -> List[ChannelCycleCounts]: and ``dispensing_cycles``. """ - return list(await asyncio.gather( - *(self.channel_request_cycle_counts(channel_idx=idx) for idx in range(self.num_channels)) - )) + return list( + await asyncio.gather( + *(self.channel_request_cycle_counts(channel_idx=idx) for idx in range(self.num_channels)) + ) + ) # # # ACTION Commands # # #