From 9fa44dd3a2337d2f3844e1c065a8d1e812509400 Mon Sep 17 00:00:00 2001 From: nedru004 Date: Thu, 11 Dec 2025 15:28:00 -0600 Subject: [PATCH 1/5] Add verification function this will find locations of carriers that should be loaded and then verify if they are loaded. --- .../backends/hamilton/STAR_backend.py | 85 +++++++++++++++++++ 1 file changed, 85 insertions(+) diff --git a/pylabrobot/liquid_handling/backends/hamilton/STAR_backend.py b/pylabrobot/liquid_handling/backends/hamilton/STAR_backend.py index 987fb9b53da..d1461d0a6e0 100644 --- a/pylabrobot/liquid_handling/backends/hamilton/STAR_backend.py +++ b/pylabrobot/liquid_handling/backends/hamilton/STAR_backend.py @@ -7599,6 +7599,91 @@ def pattern2hex(pattern: List[bool]) -> str: cb=blink_pattern_hex, ) + async def verify_and_wait_for_carriers( + self, + check_interval: float = 1.0, + ): + """Verify that carriers have been loaded at expected rail positions. + + This function checks if carriers are physically present on the deck at the specified + rail positions using the deck's presence sensors. If any carriers are missing, it will: + 1. Prompt the user to load the missing carriers + 2. Flash LEDs at the missing positions using set_loading_indicators + 3. Continue checking until all carriers are detected + + Args: + check_interval: Interval in seconds between presence checks (default: 1.0) + + Raises: + ValueError: If no carriers are found on the deck. + """ + # Extract from deck children + expected_rail_positions = [] + for child in self.deck.children: + if isinstance(child, Carrier): + rails = self.deck._rails_for_x_coordinate(child.coordinate.x) + if 1 <= rails <= 54: + expected_rail_positions.append(rails) + + if not expected_rail_positions: + raise ValueError("No carriers found on deck. Assign carriers to the deck.") + + # Check initial presence + detected_rails = set(await self.request_presence_of_carriers_on_deck()) + missing_rails = sorted(set(expected_rail_positions) - detected_rails) + + if not missing_rails: + logger.info(f"All carriers detected at rail positions: {expected_rail_positions}") + # Turn off all indicators + await self.set_loading_indicators( + bit_pattern=[False] * 54, + blink_pattern=[False] * 54, + ) + return + + # Prompt user about missing carriers + print( + f"\n{'='*60}\n" + f"CARRIER LOADING REQUIRED\n" + f"{'='*60}\n" + f"Expected carriers at rail positions: {expected_rail_positions}\n" + f"Detected carriers at rail positions: {sorted(detected_rails)}\n" + f"Missing carriers at rail positions: {missing_rails}\n" + f"{'='*60}\n" + f"Please load the missing carriers. LEDs will flash at the missing positions.\n" + f"The system will automatically detect when all carriers are loaded.\n" + f"{'='*60}\n" + ) + + # Flash LEDs until all carriers are detected + while missing_rails: + # Create bit pattern for missing rails + bit_pattern = [False] * 54 + blink_pattern = [False] * 54 + + for rail in missing_rails: + indicator_index = rail - 1 # Convert rail (1-54) to index (0-53) + bit_pattern[indicator_index] = True + blink_pattern[indicator_index] = True + + # Set loading indicators + await self.set_loading_indicators(bit_pattern[::-1], blink_pattern[::-1]) + + # Check for presence again + detected_rails = set(await self.request_presence_of_carriers_on_deck()) + missing_rails = sorted(set(expected_rail_positions) - detected_rails) + + # Brief pause before next check cycle + await asyncio.sleep(check_interval) + + # All carriers detected, turn off all indicators + logger.info(f"All carriers successfully detected at rail positions: {expected_rail_positions}") + await self.set_loading_indicators( + bit_pattern=[False] * 54, + blink_pattern=[False] * 54, + ) + print(f"\nAll carriers successfully loaded and detected!\n") + async def unload_carrier( self, carrier: Carrier, From ef5ca212fb8e076f095e047d33497a0bba78d133 Mon Sep 17 00:00:00 2001 From: nedru004 Date: Thu, 11 Dec 2025 15:51:47 -0600 Subject: [PATCH 2/5] Change to end rail for detection actually look at end rail. also light up all LED involved in that carrier --- .../backends/hamilton/STAR_backend.py | 81 +++++++++++++------ 1 file changed, 55 insertions(+), 26 deletions(-) diff --git a/pylabrobot/liquid_handling/backends/hamilton/STAR_backend.py b/pylabrobot/liquid_handling/backends/hamilton/STAR_backend.py index d1461d0a6e0..5b0a638a4bf 100644 --- a/pylabrobot/liquid_handling/backends/hamilton/STAR_backend.py +++ b/pylabrobot/liquid_handling/backends/hamilton/STAR_backend.py @@ -83,7 +83,10 @@ TipPickupMethod, TipSize, ) -from pylabrobot.resources.hamilton.hamilton_decks import HamiltonCoreGrippers +from pylabrobot.resources.hamilton.hamilton_decks import ( + HamiltonCoreGrippers, + _rails_for_x_coordinate, +) from pylabrobot.resources.liquid import Liquid from pylabrobot.resources.rotation import Rotation from pylabrobot.resources.trash import Trash @@ -7617,23 +7620,36 @@ async def verify_and_wait_for_carriers( Raises: ValueError: If no carriers are found on the deck. """ - # Extract from deck children - expected_rail_positions = [] + # Extract carriers from deck children with start and end rail positions + track_width = 22.5 + carrier_rails = [] # List of (start_rail, end_rail) tuples + for child in self.deck.children: if isinstance(child, Carrier): - rails = self.deck._rails_for_x_coordinate(child.coordinate.x) - if 1 <= rails <= 54: - expected_rail_positions.append(rails) + # Get x coordinate relative to deck + carrier_x = child.get_location_wrt(self.deck).x + carrier_start_rail = int((carrier_x - 100.0) / track_width) + 1 + carrier_end_rail = int((carrier_x - 100.0 + child.get_absolute_size_x()) / track_width) + 1 - if not expected_rail_positions: + # Clamp rails to valid range (1-54) + carrier_start_rail = max(1, min(carrier_start_rail, 54)) + carrier_end_rail = max(1, min(carrier_end_rail, 54)) + + carrier_rails.append((carrier_start_rail, carrier_end_rail)) + + if not carrier_rails: raise ValueError("No carriers found on deck. Assign carriers to the deck.") + # Extract end rails for comparison with detected rails + # The presence detection reports the end rail position + expected_end_rails = [end_rail for _, end_rail in carrier_rails] + # Check initial presence detected_rails = set(await self.request_presence_of_carriers_on_deck()) - missing_rails = sorted(set(expected_rail_positions) - detected_rails) + missing_end_rails = sorted(set(expected_end_rails) - detected_rails) - if not missing_rails: - logger.info(f"All carriers detected at rail positions: {expected_rail_positions}") + if not missing_end_rails: + logger.info(f"All carriers detected at end rail positions: {expected_end_rails}") # Turn off all indicators await self.set_loading_indicators( bit_pattern=[False] * 54, @@ -7646,43 +7662,56 @@ async def verify_and_wait_for_carriers( f"\n{'='*60}\n" f"CARRIER LOADING REQUIRED\n" f"{'='*60}\n" - f"Expected carriers at rail positions: {expected_rail_positions}\n" + f"Expected carriers at end rail positions: {expected_end_rails}\n" f"Detected carriers at rail positions: {sorted(detected_rails)}\n" - f"Missing carriers at rail positions: {missing_rails}\n" + f"Missing carriers at end rail positions: {missing_end_rails}\n" f"{'='*60}\n" - f"Please load the missing carriers. LEDs will flash at the missing positions.\n" + f"Please load the missing carriers. LEDs will flash at the carrier positions.\n" f"The system will automatically detect when all carriers are loaded.\n" f"{'='*60}\n" ) # Flash LEDs until all carriers are detected - while missing_rails: - # Create bit pattern for missing rails + blink_state = False + while missing_end_rails: + # Create bit pattern for missing carriers + # Flash all LEDs from start_rail to end_rail (inclusive) for each missing carrier bit_pattern = [False] * 54 blink_pattern = [False] * 54 - for rail in missing_rails: - indicator_index = rail - 1 # Convert rail (1-54) to index (0-53) - bit_pattern[indicator_index] = True - blink_pattern[indicator_index] = True + # For each missing carrier (identified by missing end rail), flash all its rails + for missing_end_rail in missing_end_rails: + # Find the carrier with this end rail + for start_rail, end_rail in carrier_rails: + if end_rail == missing_end_rail: + # Flash all LEDs from start_rail to end_rail (inclusive) + for rail in range(start_rail, end_rail + 1): + if 1 <= rail <= 54: + indicator_index = rail - 1 # Convert rail (1-54) to index (0-53) + bit_pattern[indicator_index] = True + blink_pattern[indicator_index] = blink_state + break # Set loading indicators - await self.set_loading_indicators(bit_pattern[::-1], blink_pattern[::-1]) + await self.set_loading_indicators(bit_pattern, blink_pattern) - # Check for presence again - detected_rails = set(await self.request_presence_of_carriers_on_deck()) - missing_rails = sorted(set(expected_rail_positions) - detected_rails) + # Toggle blink state for next cycle + blink_state = not blink_state - # Brief pause before next check cycle + # Wait before checking again await asyncio.sleep(check_interval) + # Check for presence again + detected_rails = set(await self.request_presence_of_carriers_on_deck()) + missing_end_rails = sorted(set(expected_end_rails) - detected_rails) + # All carriers detected, turn off all indicators - logger.info(f"All carriers successfully detected at rail positions: {expected_rail_positions}") + logger.info(f"All carriers successfully detected at end rail positions: {expected_end_rails}") await self.set_loading_indicators( bit_pattern=[False] * 54, blink_pattern=[False] * 54, ) - print(f"\nAll carriers successfully loaded and detected!\n") + print(f"\n✓ All carriers successfully loaded and detected!\n") async def unload_carrier( self, From eca990c3a66fb9d78083fbdad9346e96a6717cf3 Mon Sep 17 00:00:00 2001 From: nedru004 Date: Thu, 11 Dec 2025 16:10:54 -0600 Subject: [PATCH 3/5] Fix end_rail and invert LED --- .../liquid_handling/backends/hamilton/STAR_backend.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/pylabrobot/liquid_handling/backends/hamilton/STAR_backend.py b/pylabrobot/liquid_handling/backends/hamilton/STAR_backend.py index 5b0a638a4bf..f89e5cbb7b2 100644 --- a/pylabrobot/liquid_handling/backends/hamilton/STAR_backend.py +++ b/pylabrobot/liquid_handling/backends/hamilton/STAR_backend.py @@ -7629,7 +7629,7 @@ async def verify_and_wait_for_carriers( # Get x coordinate relative to deck carrier_x = child.get_location_wrt(self.deck).x carrier_start_rail = int((carrier_x - 100.0) / track_width) + 1 - carrier_end_rail = int((carrier_x - 100.0 + child.get_absolute_size_x()) / track_width) + 1 + carrier_end_rail = int((carrier_x - 100.0 + child.get_absolute_size_x()) / track_width) # Clamp rails to valid range (1-54) carrier_start_rail = max(1, min(carrier_start_rail, 54)) @@ -7672,7 +7672,6 @@ async def verify_and_wait_for_carriers( ) # Flash LEDs until all carriers are detected - blink_state = False while missing_end_rails: # Create bit pattern for missing carriers # Flash all LEDs from start_rail to end_rail (inclusive) for each missing carrier @@ -7689,14 +7688,11 @@ async def verify_and_wait_for_carriers( if 1 <= rail <= 54: indicator_index = rail - 1 # Convert rail (1-54) to index (0-53) bit_pattern[indicator_index] = True - blink_pattern[indicator_index] = blink_state + blink_pattern[indicator_index] = True break # Set loading indicators - await self.set_loading_indicators(bit_pattern, blink_pattern) - - # Toggle blink state for next cycle - blink_state = not blink_state + await self.set_loading_indicators(bit_pattern[::-1], blink_pattern[::-1]) # Wait before checking again await asyncio.sleep(check_interval) From 64f67ef601073949bf7f21f73fe6d204e63b113a Mon Sep 17 00:00:00 2001 From: nedru004 Date: Thu, 11 Dec 2025 16:28:06 -0600 Subject: [PATCH 4/5] Update rail max and min start_rail can overhang but end_rail needs to be within 1 to 54. --- .../liquid_handling/backends/hamilton/STAR_backend.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pylabrobot/liquid_handling/backends/hamilton/STAR_backend.py b/pylabrobot/liquid_handling/backends/hamilton/STAR_backend.py index f89e5cbb7b2..b8f9729f525 100644 --- a/pylabrobot/liquid_handling/backends/hamilton/STAR_backend.py +++ b/pylabrobot/liquid_handling/backends/hamilton/STAR_backend.py @@ -7631,11 +7631,10 @@ async def verify_and_wait_for_carriers( carrier_start_rail = int((carrier_x - 100.0) / track_width) + 1 carrier_end_rail = int((carrier_x - 100.0 + child.get_absolute_size_x()) / track_width) - # Clamp rails to valid range (1-54) + # Verify rails are valid carrier_start_rail = max(1, min(carrier_start_rail, 54)) - carrier_end_rail = max(1, min(carrier_end_rail, 54)) - - carrier_rails.append((carrier_start_rail, carrier_end_rail)) + if 1 <= carrier_end_rail <= 54: + carrier_rails.append((carrier_start_rail, carrier_end_rail)) if not carrier_rails: raise ValueError("No carriers found on deck. Assign carriers to the deck.") @@ -7655,6 +7654,7 @@ async def verify_and_wait_for_carriers( bit_pattern=[False] * 54, blink_pattern=[False] * 54, ) + print(f"\n✓ All carriers successfully detected at end rail positions: {expected_end_rails}\n") return # Prompt user about missing carriers From b3911dd29c9864db810947f9beb02710e206db36 Mon Sep 17 00:00:00 2001 From: nedru004 Date: Thu, 11 Dec 2025 16:30:06 -0600 Subject: [PATCH 5/5] revert import ended up not using _rails_for_x_coordinate --- pylabrobot/liquid_handling/backends/hamilton/STAR_backend.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/pylabrobot/liquid_handling/backends/hamilton/STAR_backend.py b/pylabrobot/liquid_handling/backends/hamilton/STAR_backend.py index b8f9729f525..9ff7f2e474a 100644 --- a/pylabrobot/liquid_handling/backends/hamilton/STAR_backend.py +++ b/pylabrobot/liquid_handling/backends/hamilton/STAR_backend.py @@ -83,10 +83,7 @@ TipPickupMethod, TipSize, ) -from pylabrobot.resources.hamilton.hamilton_decks import ( - HamiltonCoreGrippers, - _rails_for_x_coordinate, -) +from pylabrobot.resources.hamilton.hamilton_decks import HamiltonCoreGrippers from pylabrobot.resources.liquid import Liquid from pylabrobot.resources.rotation import Rotation from pylabrobot.resources.trash import Trash