Hey all, and maybe @CamilloMoschner and @ben in particular: I’m trying to incorporate some CORE-based plate moves and having some issues with picking up the CORE grippers. The default pickup location for PLR seems to not correspond to where my grippers are held (see photo).
It looks like the robot thinks the grippers are and inch or so towards the back from where they actually are. I know there are different CORE gripper mounts, but I’m not sure which I have, or if/how it’s specified in PLR, or if I can specify manually with x,y coords. Any ideas?
there are multiple co-re configuration types, and right now the firmware command in star.py uses the coordinates of a different gripper block than yours. perhaps you can help by adding your configuration?
In your image, is this the channel positioning that you have after asking PLR to get the grippers, i.e. the channels are shifted ‘backwards’ in the y-dimension by about 15mm?
Is the distance between the grippers in this position equivalent to the distance of the grippers in their holder?
For 1000ul channel gripper holders I believe there are only 2 options, the AtWaste and the OnWaste options. So we should be able to figure this out and add the one we’re missing to PLR
Can you please generate a VENUS “method” that uses the grippers, e.g. to pick up a plate and place it down again, and then share the firmware command file with us?
The firmware command file will be in C:\Program Files (x86)\HAMILTON\LogFiles and start with HxUsbComm followed be the date yyyymmdd.
This file contains the information about how far off the robot is when picking up the grippers and should hopefully be enough information to add an OnWaste or AtWaste option to plate movement functions in PLR.
Hey guys, thanks for helping out. I’m happy to contribute to additional configurations here, but you may have to bear with me as I learn my way around it all. I’m working on this again now, so I can share some of the info you requested @CamilloMoschner.
I’m actually working with two separate (quite old, mfd '05 and '10) systems, and I noticed as part of this that they each have different gripper mount configurations. The newer one has the OnWaste tool, while the older one (which was originally referenced above) has the AtWaste tool. The default core move commands work fine on the OnWaste configured machine, so we can assume that’s the default in PLR and the AtWaste is the missing config.
I’ve made a simple VENUS method as Camillo requested for the AtWaste configured robot, see below for log file. You’ll see there are several errors there, those are from it attempting to pick up the front gripper but failing several times because of a slight misalignment between the channel and the gripper. I got it to pick up finally by pushing them into alignment manually .
The method I made can be seen in the screenshot below as well. If it matters, I set the manual override to 84mm grip width and 90 mm approach width, as it forced me to manually override the labware parameters.
Can you please confirm:
Even with VENUS you had to manually push the front gripper a bit to make the channel pick it up?
I believe you are referring to the “Grip Move Plate (Single Step)” command here?
“Approach width” being the distance between the two channels when moving down in z to get to the pickup height, next to the plate.
And “grip width” being the distance between the two channels move towards each other when picking up the plate, and moving the plate around.
The gripper pickup is taking place one step before this and should not be affected by these parameters.
Something to try out:
Here is a PLR command I generated for you based on the log file you provided:
Get CoRe gripper tool
await lh.send_command(
module="C0",
command="ZT",
xs="07980", # CoRe gripper tool X position [0.1mm]
xd="0", # X direction 0 = positive 1 = negative
ya="1055", # First (lower channel) CoRe gripper tool Y pos. [0.1mm] <======= THIS IS WHAT YOU WANT TO CHANGE
yb="0795", # Second (higher channel) CoRe gripper tool Y pos.[0.1mm] <======= THIS IS WHAT YOU WANT TO CHANGE
pa="07", # channel to use in the back
pb="08", # channel to use in the front
tp="2350", # Begin of CoRe gripper tool picking up process (Z- range) [0.1mm]
tz="2250", # End of CoRe gripper tool picking up process (Z- range) [0.1mm]
th="2450", # Minimum traverse height at beginning of a command [0.1mm]
tt="14"
)
…you can see the two command parameters ya and yb I marked. These look to me like the position arguments for the two grippers in the y dimension. We want to identify the correct positions for your system in a safe manner.
Since even your VENUS cannot pick up the grippers and nobody else seems to have a working AtWaste system (in this chat) we need to empirically determine these ya and yb positions.
For testing, I recommend:
start a PLR Notebook
Find the correct y-positions for each channel (let’s use P7 & P8 → i.e. channel_idx 6 & 7 in PLR)
first, move all channels into a safe back position (PLR commands do not automatically move channels out of the way which means you can tell a channel to go to from y=1800 to y=1500 even if another channel is at y=1600 → smashing the two against one another, so this step tries to prevent this from happening to you by moving channel_0 to y=220, channel_1 to y=210, …):
# Safe positions
y_post_start = 220
for channel_idx in range(0,8):
print(f"channel {channel_idx} -> {y_post_start}")
await lh.backend.move_channel_y(y=y_post_start, channel=channel_idx)
y_post_start -= 10
now move channel_7 above the front gripper
await lh.backend.move_channel_z(z=240, channel=7) # move channel down to make it easier to see whether it is above the gripper but not too far as to not crash the channel into the gripper
await lh.backend.move_channel_y(y=<empirically_test_y_valye_here>, channel=7)
figure out what the correct y-value should be, i.e. where the channel is right above the front gripper → note down value as e.g. correct_AtWaste_front_gripper_y_value.
now do the same for the back gripper, using channel=6, to identify correct_AtWaste_back_gripper_y_value.
Only once you have these two values,
correct_AtWaste_front_gripper_y_value and
correct_AtWaste_back_gripper_y_value,
…I recommend you test out the above PLR “Get CoRe gripper tool” command.
If the only issue with AtWaste is the y-positions for the gripper pickup, then this should solve it.
If there are also differences in height of the grippers between AtWaste and OnWaste we’ll have to modify a bit more.
Also, once you have the grippers on your channels you need to be able to put them back as well
The cheap trick is to restart the system, during initialisation the robot recognises it has “tips” on its channels and will try to throw them into the trash… not ideal because the grippers will crash into the waste wall.
So you need this PLR command to place the grippers back:
await lh.send_command(
module="C0",
command="ZS",
xs="07980", # CoRe gripper tool X direction [0.1mm]
xd="0", # X direction 0 = positive 1 = negative
ya="1055", # First (lower channel) CoRe gripper tool Y pos. [0.1mm] <=======
yb="0795", # Second (higher channel) CoRe gripper tool Y pos.[0.1mm] <=======
pa="07",
pb="08",
tp="2350", # Begin of CoRe gripper tool picking up process (Z- range) [0.1mm]
tz="2250", # End of CoRe gripper tool picking up process (Z- range) [0.1mm]
th="2450", # Minimum traverse height at beginning of a command [0.1mm]
tt="14", # Tip type (see command "TT” & Tip type default values)
Don’t forget to change ya and yb here too to the corrected values you identify.
If at any point you are unsure just change xs="06500" → this should just drop the grippers 150 mm to the left of the AtWaste in mid air onto the deck.
Disclaimer: I am not a Hamilton employee, all use of PLR is at ones own responsibility and I have not done this before, so please be careful with my troubleshooting suggestions
I looked at the log file I shared in this thread on 3rd January, and the functional PLR definition for get_core (STAR.py) with an OnWaste CO-RE gripper parking slot:
@rickwierenga, this troubleshooting got me to search for a better solution to the pushing plates flush to the carrier problem I mentioned above. After all “quick & dirty” is just supposed to be an interim solution:
So I looked into the log file I submitted here, and after 2 months and 5 days of working with PLR I think I am getting decent at reading Hamilton firmware
Converted put-plate & check-plate-is-there (equivalent to pushing plate flush to carrier) to PLR commands (because they are just easier to read):
# "Put plate using CO-RE gripper"
await self.send_command(
module="C0",
command="ZR",
xs="07075", # Plate center in X direction [0.1mm]
yj="1145", # Plate center in Y direction [0.1mm]
zj="1952", # Plate deposit height in Z direction [0.1mm] = z position grippers will move to!!!!!
xd="0", # X direction 0 = positive 1 = negative
xg="4", # X acceleration index <============== NEW ???
zi="010", # Press on distance [0.1mm] <============== NEW = to overcome springs? (might scratch plate)
zy="1287", # Z speed [0.1mm/s]ec <============== CHANGED = fast placement to overcome springs
yo="0920", # Open gripper position [0.1mm] <============== CHANGED
th="2182", # Minimum traverse height at beginning of a command [0.1mm]
te="2182", # Minimum z-position at end of a command [0.1mm] <============== CHANGED = don't waste time with unnecessary
# movements when you know the check happens next
)
# "Check plate exists" -> actually a "Get plate using CO-RE gripper" + error handling!
await self.send_command(
module="C0",
command="ZP",
xs="07075", # Plate center in X direction [0.1mm] ->
yj="1145", # Plate center in Y direction [0.1mm] ->
zj="1952", # Plate deposit height in Z direction [0.1mm] -> exact same dimensions! = because the grippers will try to get to
# the "center" of the plate -> pushing the plate from the top down to
# its center -> triggers error 62 in the channels used!
yv = "2778", # Y gripping speed [0.1mm] <============== NEW - not used
xd="0", # X direction 0 = positive 1 = negative
zy="0600", # Z speed [0.1mm/s]ec <============== CHANGED = slow crash
yo="0630", # Open gripper position [0.1mm] <============== CHANGED = grippers offset inwards = plate.get_size_y - 2*offset <--- finally enables us to change the offset :)
yg="0630", # Plate width [0.1mm] <============== NEW = grippers offset inwards = plate.get_size_y - 2*offset <--- finally enables us to change the offset :)
yw="20", # Grip strength 0 = low .. 99 = high <============== NEW - not used
th="2182", # Minimum traverse height at beginning of a command [0.1mm]
te="2750", # Minimum z-position at end of a command [0.1mm] <============== CHANGED = move grippers to save distance at the end
# of the command, will need separate implementation in PLR due to purposeful error triggering
)
# returns
# > 10:30:50.670 8AF#8000#00: C0ZPid0224er99/00 P602/62 P702/62 <===== ERROR 62 = Z-drive movement error, triggered by step monitoring, step loss
→ The checking of whether a plate is present is actually a “get plate” command in disguise, destined to crash the z drive (just like lld_mode(4)) with appropriate handling of the receiving error message
That is quite an ingenious engineering decision… but it makes me think of the machine more as a bumper car than a delicate fine precision instrument haha
Equipped with this knowledge, I propose we expose this functionality to our current lh.move_plate() method as extra arguments check_plate_exists_at_source=None and check_plate_exists_at_destination=None?
I am frankly a bit surprised the log files don’t have a re-initialize z-drive command after the targeted crash… but I guess the crash is actually not bad enough to need it, maybe because of the reduced z speed?
@CamilloMoschner Thanks a ton for the detailed help! I have some other stuff I need to get to right now, but I will likely work on this over the weekend and report back.
Thanks! This was still buried in my todo list somewhere. What values other than None should check_plate_exists_at_source take? True/False? I think that makes sense, because move_plate also does iswap where this functionality does not exist (?). Do you want to create a PR for this?
No worries, I remembered you wanted to look into this but jonlaurent’s question got me into troubleshoot mode and I wanted to test how much my Hamilton firmware understanding improved, plus I know you are very busy atm
Good question. I think the answer is multi-layered, and has to do with whether we just want this command to work as a “push-plate-flush-to-carrier” command or whether we want it to work in the original meaning it was created in, i.e. “check-plate-exists”, and have the “push-plate-flush-to-carrier” as a handy byproduct:
Do we want the robot to stop executing the automated protocol if the check_plate_exists_at_source returns False, i.e. raise an error?
a. If yes then a tuple like (True, 'cancel_protocol_if_not_found') vs (True, 'ignore_if_not_found') would be useful.
b. Though, if yes might be simpler to just always raise raise a ValueError(f"Plate not found at location {loc.x, loc.y, loc.z}") and indeed then just requires check_plate_exists_at_source=False as default and can be set to True by users.
To be honest, I propose we approach this implementation through the creation of two new PLR features, an exposure and a new functionality:
New functionality:
a standalone STAR.check_if_resource_exists_at_known_location(<resource_instance>)method → I believe this has its own merit. For example, it can be used by any protocol, at any time to check that all labware is where it is supposed to be, using only a Resource class instance (or any of its derivatives, including plates, tip_racks, tubes, reservoirs, …).
→ This also has the added benefit of pushing each of these resources into their lowest possible z position, i.e. push them flush to whatever they are standing on.
→ This part is a bit tricky because of the potential size differences between resources but I’ve got a couple of ideas how to make this work.
Exposure of existing functionality:
add this functionality, nicely wrapped up as the above mentioned check_plate_exists_at_source and check_plate_exists_at_destination arguments to the move_plate method → set their defaults to False, and raise an error (i.e. halting the entire protocol) if the check figures out the plate does not exist. Since we don’t want this to interfere with how the iSWAP moves resources around, I suggest we only execute the checking of labware if use_arm == 'core'.
(Unfortunately, I don’t have an iSWAP atm… but if I’m not mistaken, doesn’t the iSWAP recognise whether it is picking up a resource of the correct size anyway with some sort of lateral pressure sensors? And this enables the iSWAP to recognise when it loses a plate mid-transport?)
→ This part is pretty easy, the move_plate method already computes all the information we need.
Yes, I am happy to get on this next week. Please let me know what you think about my two suggestion but suggestion no2 I can create quite quickly.
Fun question: What do you think about adding a car bumper sound or the coin sound from Super Mario to this method?
I worked on audio-feedback between users and robots during my PhD, and got a simple function ready that we can add the the move_plate method that is executed when the machine finds a plate/resource in the location it searches.
During my PhD I used it to play a soundtrack to inform users that the machine had finished a task that requires user input next.
This might sound a bit too playful, but I actually found, for my users it was the most memorable idiosyncrasy of my automated workflows and it improved the user-robot interaction massively.
(I’d need to check the sound rights for anything we use though… Super Mario soundeffects might be protected, and I am unsure how open-source projects like PLR have to deal with this)
I really like the approach you suggested below, which I think corresponds to b. Perhaps it’d be nice to re-use the PLR ResourceNotFoundError that is already used in the PLR resource model instead of a ValueError.
I think it should be doable to get this working on iswap as well. I can add that.
Haha let’s do it. PLR should be the highest fun lab automation framework. As you say, let’s make sure we don’t infringe their copyright though (I don’t know what the rules are exactly either), and be extremely conservative about dependencies / errors.