Skip to content

Conversation

@cmoscy
Copy link
Contributor

@cmoscy cmoscy commented Nov 4, 2025

  1. Added TCP I/O protocol with the Hamilton specific TCPBackend supporting Nimbus (and possibly Prep) specific communication and introspection through their IP->Harp2->Hoi2 protocol.
  2. Currently includes basics of instrument connection, introspection.
  3. Can use HamiltonCommand to construct message to run instrument commands while managing any responses received from the instrument.

cmoscy and others added 18 commits August 6, 2025 23:22
…ugh Hamilton .NET firmware interface. TCP connection works through proxy to log direct communication.
- Added tcp_comlink_proxy for setup of Nimbus Comlink instance. Manages TCP communication through .NET firmware libraries
- Removed setup.py in favor of pyproject.toml for package management.
- Updated pyproject.toml with project metadata, dependencies, and optional dependencies.
- Updated `README.md` to include instructions for using the test notebook and requirements for DLL dependencies.
- Introduced `dll_comlink_test.ipynb` to demonstrate TCP communication with Hamilton Nimbus instruments, including connection setup, module discovery, and pipettor operations.
- Added `nimbus-connect-validation.json` as logging and validating TCP communication example
- Fixed `firmware_assemblies.py` to load required Hamilton.Components.TransportLayer.Protocols.dll
…port

- Introduced `tcp_codec.py` for Hamilton protocol communication, implementing a three-layer packet structure (IpPacket, Harp2, Hoi2) and associated message builders for command execution.
- Introduced a new Hamilton TCP backend with connection management, message routing, and command execution capabilities.
- Implemented a layered architecture for the Hamilton protocol, including packet structures (IpPacket, HarpPacket, HoiPacket) and message builders (CommandMessage, InitMessage).
- HoiParams for automatic DataFragment wrapping in command parameters.
- Added HamiltonIntrospection class for dynamic discovery of instrument capabilities (Thanks Piglet for showing that pattern).
- Implemented commands for retrieving object interfaces, metadata, method signatures, enums, and structs.
- Example in nimbus_connection_test.ipynb demonstrates finding an interface (Doorlock in this case), and then constructing the corresponding commands to check status, lock, unlock, etc.
1. Improved HoiParameter handling for more efficient use
2. Updated nimbus_connection_test.ipynb to show pipettor introspection, and initialization + park execution
3. Restored original project setup.py and pyproject files for consistency.
4. Moved all DLL related features to separate branch
@cmoscy
Copy link
Contributor Author

cmoscy commented Nov 5, 2025

My fault with the typing/linting. Cleaning this up and will update shortly

@rickwierenga
Copy link
Member

it's fine to do those things last

for linting make format might do a lot easily

but otherwise don't bother making tests pass every revision, it's more about content than style at this point

@rickwierenga
Copy link
Member

do you think it would be possible to load some commands like aspirate and dispense and implement them in python in PLR?

@cmoscy
Copy link
Contributor Author

cmoscy commented Nov 5, 2025

Got it! Was pretty minor to get it to pass.

That's definitely possible! Primary bottleneck for me has been handling that Nimbus has several deck/waste block configurations. Pretty sure there's a way to detect a lot of this so there's no potentially dangerous assumptions made.

Could add basic instrument control into a NimbusBackend pretty easily in the next 1-2 days (User would have to specify coordinates themselves until we get a deck layout defined).

Tip pickup, drop, aspirate, dispense, initialization, and door control seem like a good minimal implementation? Anything else that would be worth prioritizing?

@rickwierenga
Copy link
Member

those "big four" + setup are the most important, everything else is nice to have. appreciate it!

for the stars the user has to specify some things about their deck layout, like where the core grippers are if they have them. after that it's just modeled through the universal resource model. we could have a similar thing for the nimbus

deck = NimbusDeck(
  trash_pos = "option a"
)

cmoscy and others added 2 commits November 4, 2025 20:43
…ecodes and displays command args and return values into their corresponding types.
@cmoscy
Copy link
Contributor Author

cmoscy commented Nov 5, 2025

Sounds good, thanks for the example. Checkpointed the cleanup and improved the introspection to help me put all that together next!

cmoscy and others added 4 commits November 4, 2025 20:56
…ls for testing. Example notebook with NimbusBackend Examples.

! Corrected message parsing for array types in HoiParamsParser.
- Added NimbusTipType enumeration for mapping tip types to Hamilton protocol commands.
- Implemented InitializeSmartRoll, SetChannelConfiguration, PickupTips, DropTips, and DropTipsRoll commands for Nimbus backend.
- Updated NimbusBackend setup process to include channel configuration and tip presence checks.
- Enhanced NimbusDeck to create default long waste block and associated waste positions.
- Improved error handling and logging for tip pickup and drop operations.
@cmoscy
Copy link
Contributor Author

cmoscy commented Jan 19, 2026

I am surprised PickupTips does not have z_final_positions

Right? PickupGripperTool also lacks a zFinal in the signature. Weird since most other operations have it. But double checked the extracted interface methods and their firmware documentation. They both don't include zFinal.

Pipettor Interface Methods from introspection layer for reference.
pipettor_method_ids.txt

Comment on lines 1908 to 1916
# flow_rate should not be None - if it is, it's an error (no hardcoded fallback)
flow_rates: List[float] = []
for op in ops:
if op.flow_rate is None:
raise ValueError(f"flow_rate cannot be None for operation {op}")
flow_rates.append(op.flow_rate)
blowout_volumes = [
op.blow_out_air_volume if op.blow_out_air_volume is not None else 40.0 for op in ops
] # in uL, default 40
Copy link
Member

Choose a reason for hiding this comment

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

this means the user has to specify flow rates, which isn't the case for other liquid handlers in PLR. they will often want to, but maybe not always (eg for simple prototyping)

what is the default value nimbus uses if anything?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

1000 ul tip: 250 asp / 400 disp
300 and 50 ul tip: 100 asp / 180 disp
10 ul tip: 100 asp / 75 disp

@rickwierenga
Copy link
Member

what is the difference between HoiPacket and HoiResponse?

@cmoscy
Copy link
Contributor Author

cmoscy commented Jan 20, 2026

what is the difference between HoiPacket and HoiResponse?

HoiPacket is the lower level wire protocol, handles low level encoding/decoding. HoiResponse in the interpreter layer that translates enums, action codes, etc that map to hamilton responses/behaviors.

@rickwierenga
Copy link
Member

HoiPacket is the lower level wire protocol, handles low level encoding/decoding. HoiResponse in the interpreter layer that translates enums, action codes, etc that map to hamilton responses/behaviors.

It seems they contain the same information?

parse_message checks the action for error, but then in the success case copies information over into a HoiResponse:

 return SuccessResponse(
      action=action,
      interface_id=cmd_response.hoi.interface_id,
      action_id=cmd_response.hoi.action_id,
      raw_params=cmd_response.hoi.params,
      response_required=cmd_response.hoi.response_required,
    )

and it's pretty similar in the error case.

    if isinstance(hoi_response, ErrorResponse):

in send_command could just be if action in (Hoi2Action.STATUS_EXCEPTION, Hoi2Action.COMMAND_EXCEPTION, Hoi2Action.INVALID_ACTION_RESPONSE)

is there a reason I shouldn't merge them into one? (delete HoiResponse probably incl related refactoring)

@cmoscy
Copy link
Contributor Author

cmoscy commented Jan 20, 2026

HoiPacket is the lower level wire protocol, handles low level encoding/decoding. HoiResponse in the interpreter layer that translates enums, action codes, etc that map to hamilton responses/behaviors.

It seems they contain the same information?

parse_message checks the action for error, but then in the success case copies information over into a HoiResponse:

 return SuccessResponse(
      action=action,
      interface_id=cmd_response.hoi.interface_id,
      action_id=cmd_response.hoi.action_id,
      raw_params=cmd_response.hoi.params,
      response_required=cmd_response.hoi.response_required,
    )

and it's pretty similar in the error case.

    if isinstance(hoi_response, ErrorResponse):

in send_command could just be if action in (Hoi2Action.STATUS_EXCEPTION, Hoi2Action.COMMAND_EXCEPTION, Hoi2Action.INVALID_ACTION_RESPONSE)

is there a reason I shouldn't merge them into one? (delete HoiResponse probably incl related refactoring)

Would be fine with me. I originally kept them separate to stay aligned with Hamilton’s .NET structure — it made it easier to test against the firmware DLLs.

Now that we've captured most of it, probably less important to be a 1:1 mirror.

@rickwierenga
Copy link
Member

Yes, that's right. Sounds great to make it more consistent across instruments

Unfortunately the spacing between channels is a little odd here:

  Position 1 to 2: 19.863 - 1.880 = 17.983mm                                                                                                                                                                           
  Position 2 to 3: 1.880 - (-76.149) = 78.029mm                                                                                                                                                                        
  Position 3 to 4: -76.149 - (-94.132) = 17.983mm                                                                                                                                                                      
  Position 4 to 5: -94.132 - (-152.349) = 58.217mm                                                                                                                                                                     
  Position 5 to 6: -152.349 - (-170.332) = 17.983mm                                                                                                                                                                    
  Position 6 to 7: -170.332 - (-219.549) = 49.217mm                                                                                                                                                                    
  Position 7 to 8: -219.549 - (-237.532) = 17.983mm    

This means we can't use get_tight_single_resource_liquid_op_offsets which is used by lh.discard_tips.

Since I have already decided I want to move having decks define a tip position for every tip spot and having discard use these, it is fine to keep it in this pattern for the nimbus (the new pattern that other backends will also use soon) rather than figuring these things out right now.

@rickwierenga
Copy link
Member

@cmoscy could you please make sure the code still works as expected? then it will be ready to merge.

@rickwierenga
Copy link
Member

I am merging the binary parser (https://github.com/PyLabRobot/pylabrobot/pull/740/changes#diff-8e70176e729f055fd4aae4c482c93135752ce382647b1d60f2084a3194c6c902) separately right now so I can use it the tecan infinite PR while finish this up

@rickwierenga rickwierenga mentioned this pull request Jan 21, 2026
@cmoscy
Copy link
Contributor Author

cmoscy commented Jan 21, 2026

@cmoscy could you please make sure the code still works as expected? then it will be ready to merge.

Will take a look tonight

@cmoscy
Copy link
Contributor Author

cmoscy commented Jan 21, 2026

Yes, that's right. Sounds great to make it more consistent across instruments

Unfortunately the spacing between channels is a little odd here:

  Position 1 to 2: 19.863 - 1.880 = 17.983mm                                                                                                                                                                           
  Position 2 to 3: 1.880 - (-76.149) = 78.029mm                                                                                                                                                                        
  Position 3 to 4: -76.149 - (-94.132) = 17.983mm                                                                                                                                                                      
  Position 4 to 5: -94.132 - (-152.349) = 58.217mm                                                                                                                                                                     
  Position 5 to 6: -152.349 - (-170.332) = 17.983mm                                                                                                                                                                    
  Position 6 to 7: -170.332 - (-219.549) = 49.217mm                                                                                                                                                                    
  Position 7 to 8: -219.549 - (-237.532) = 17.983mm    

This means we can't use get_tight_single_resource_liquid_op_offsets which is used by lh.discard_tips.

Since I have already decided I want to move having decks define a tip position for every tip spot and having discard use these, it is fine to keep it in this pattern for the nimbus (the new pattern that other backends will also use soon) rather than figuring these things out right now.

Right on, you reminded me of that little detail on the Nimbus. Funny enough those are the default tip waste positions from their deck definition. Think it avoids the teaching positions in case you don't place the little metal piece that is suposed to cover them when not being actively used on their default long waste.

@rickwierenga
Copy link
Member

That makes sense.

Also, I made a forum post to discuss making this behavior the default: https://discuss.pylabrobot.org/t/per-channel-tip-discard-positions-on-deck-instead-of-computed-offsets/420 because it is more flexible and generally nicer

rickwierenga and others added 2 commits January 20, 2026 21:35
…arameters

Drop operations now use fixed offsets (10mm) instead of tip length,
matching VantageBackend. Fixes regression from 8f2775c.
@cmoscy
Copy link
Contributor Author

cmoscy commented Jan 21, 2026

Couple regressions, but nothing too bad. I like the consolidation you implemented, will do a little smoothing tomorrow evening. Shouldn't take long.

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