Right now in v0.2, all channel operations (as well as all other star methods) live as flat methods on a single STARBackend class. It is over 12000 lines!
This is changing in v1 with the introduction of capabilities (see Next developer meeting: march 27 9am PT - #7, Updating PLR API for machine interfaces discussion - #76 by rickwierenga): we are putting aspirate et al in the STARPIPBackend(PIPBackend) (PIP meaning x-connected yz-independent channels), aspriate96 in STARHead96Backend(Head96Backend), iswap stuff in iSWAPBackend(OrientableGripperArmBackend).
These capabilities will live in separate files inside pylabrobot/hamilton/liquid_handlers/star.
We also have some subsystems that are not (yet?) capabilities, such as the autoload, integrated warsher, cover, etc. These also live in separate files, and are accessible through STARDriver.cover etc.
See this file pylabrobot/pylabrobot/hamilton/liquid_handlers/star/misc/architecture.md at v1b1 · PyLabRobot/pylabrobot · GitHub for an overview of the current new architecture. Code is on that branch next to that file.
So we already have a pretty decent approach for splitting a lot of the star backend.
STARPIPBackend(PIPBackend) should obviously take care of commands that span multiple channels, that is what “PIP” currently means (we might rename though if people have better ideas, but the concept is fixed). The current backend implementation I have so far already does “the big 4” (tip pickup/drop, aspirate, dispense). It should also take care of methods like pierce_foil which span multiple channels.
Now the question is where do we put single channel commands? move_{y,z}, request_tip_len_on_channel, ztouch_probe_z_height_using_channel, etc.? My proposal is to create an attribute STARDriver.channels: List[Channel] where each is a star channel that houses these methods. This proposal is purely about modeling the star driver, not about the rest of PLR.
star_driver.channels[0].move_y(Y)
star_driver.channels[0].ztouch_probe_z_height_using_channel()
The idea is to introduce a Channel class that owns all PX-module commands for a single channel. Each Channel would know its channel id (P1, P2, PA, etc.) and expose methods like move_{x,y,z}, request_tip_len_on_channel, ztouch_probe_z_height_using_channel, etc.
When you want to do something on multiple channels you’d just asyncio.gather(*[ch.probe_z_height(...) for ch in channels]).
This will work fine for commands that are entirely based on PX. It does, however, not work for commands that require C0 due to it not taking multiple commands at once. To the extent possible, we could try to rewrite as many methods using PX. Some commands like request_y_pos_channel_n on the star backend uses C0RB right now, but could use PXRY to query the y position.
However, and this is the main problem with this approach, some commands like request_tip_len_on_channel fundamentally require C0. My proposal for this is to introduce a lock on the star driver for pip tasks. The commands return quickly so should be seamless from the user calling it. It will allow us to preserve the nice API.
This seems pretty obvious to me, but sharing in case people have better ideas.
Another thing you will find on that branch is that I have created a class STARXArm for X arm control. (methods: move_to(x_position), move_to_safe(x_position), request_position() -> float, last_collision_type() -> bool).
STARDriver has left_x_arm and right_x_arm attributes, setting us up for dual arm support. Channels of course live on an arm, so the real implementation will be something like
class STARArm:
x_arm: STARXArm
config: DriveConfiguration
pip: Optional[STARPIPBackend] = None
head96: Optional[STARHead96Backend] = None
iswap: Optional[iSWAP] = None
(note this doesn’t exist yet)
The point I am debating is: should the methods of STARXArm just live on STARArm here? like the move_to() method just on STARArm? driver.left_x_arm.move_to() vs driver. left_x_arm.x_arm.move_to seems so much easier. However, at this point the object that manages the x arm movement also becomes the same object that has the reference to pip, iswap, etc. Will that lead to problems? My proposal is to include them.
class STARArm:
# no x arm object
config: DriveConfiguration
pip: Optional[STARPIPBackend] = None
head96: Optional[STARHead96Backend] = None
iswap: Optional[iSWAP] = None
async def move_to(...):
TLDR the split becomes: Channel = “things you do to one channel independently”, STARPIPBackend = “things that use many channels at once.”
Should the STARArm object contain the x-arm commands or reference another object STARXArm for those?
(The manifest destiny is we will rewrite STARPIPBackend to use the low level channel methods rather than communicating through C0, but that’s a story for another day)