Expose X-axis movement with acceleration control for X-arm and iSWAP rotation drive#1018
Conversation
| Args: | ||
| x: Target rotation-drive X coordinate in mm. | ||
| acceleration_level: Acceleration index (hardware units), 1-5. Default 3. |
There was a problem hiding this comment.
can we put the acceleration index table in the doctoring?
There was a problem hiding this comment.
I don't understand, what do you mean by this?
There was a problem hiding this comment.
table 4.2 X drive accelerate values
+---------------------+-------------+---------------------------+-------------------------+-------------------------+
| acceleration index | start value | acceleration increase per | top limit of accel. inc.| top limit of accel. inc.|
| | | 1 meter (reduced by 100) | (low weight / default) | (high weight) |
+---------------------+-------------+---------------------------+-------------------------+-------------------------+
| 0 (init. move only) | 100 | 1 | 100 | 100 |
| 1 | 300 | 2 | 500 | 375 |
| 2 | 400 | 6 | 1000 | 750 |
| 3 | 500 | 10 | 1500 | 1125 |
| 4 | 600 | 14 | 2000 | 1500 |
| 5 | 700 | 18 | 2500 | 1875 |
+---------------------+-------------+---------------------------+-------------------------+-------------------------+
There was a problem hiding this comment.
otherwise it is not clear what acceleration_level means
|
also does this replace position_left_x_arm_? I do not see why we would keep both |
There was a problem hiding this comment.
Pull request overview
This PR adds new STAR backend APIs to move the X-arm (and iSWAP rotation-drive X via an offset) using the lower-level X0 module so callers can control acceleration and current limiting, and to query the X-arm firmware version.
Changes:
- Add
STARBackend.x_arm_request_firmware_version()to query X0 firmware version/build date. - Add
STARBackend.x_arm_move()that sendsX0:XPwith per-call acceleration/current limiter parameters. - Add
STARBackend.iswap_rotation_drive_move_x()wrapper that translates deck X using the cachedkgoffset and delegates tox_arm_move().
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| x: Target X coordinate in mm. Must be between 90.0 and 1350.0. | ||
| acceleration_level: Acceleration index (hardware units), 1-5. Default 3. | ||
| current_protection_limiter: Motor current limit (hardware units), 0-7. Default 7. | ||
| """ | ||
|
|
||
| if not (90.0 <= x <= 1350.0): | ||
| raise ValueError(f"x must be between 90.0 and 1350.0 mm, is {x}") |
There was a problem hiding this comment.
x_arm_move hard-codes the allowed X range to 90.0–1350.0 mm, which does not appear to be derived from the configured instrument/deck geometry (e.g., STARlet decks in this repo use a much smaller X span). This can incorrectly accept positions that are physically unreachable on smaller instruments (or reject valid positions on larger ones), risking firmware errors or collisions. Consider deriving min/max from the active deck/configuration (e.g., rail count / _RAILS_WIDTH / extended_conf.instrument_size_slots) or removing the hard-coded range check in favor of a single authoritative source for X limits.
| x: Target X coordinate in mm. Must be between 90.0 and 1350.0. | |
| acceleration_level: Acceleration index (hardware units), 1-5. Default 3. | |
| current_protection_limiter: Motor current limit (hardware units), 0-7. Default 7. | |
| """ | |
| if not (90.0 <= x <= 1350.0): | |
| raise ValueError(f"x must be between 90.0 and 1350.0 mm, is {x}") | |
| x: Target X coordinate in mm. | |
| acceleration_level: Acceleration index (hardware units), 1-5. Default 3. | |
| current_protection_limiter: Motor current limit (hardware units), 0-7. Default 7. | |
| """ | |
| if not math.isfinite(x): | |
| raise ValueError(f"x must be a finite number of mm, is {x}") |
| async def x_arm_request_firmware_version(self) -> Tuple[str, datetime.date]: | ||
| """Request the X-arm firmware version and build date. | ||
|
|
||
| Returns: | ||
| A tuple of (version_string, build_date), e.g. ("1.0S", date(2009, 6, 24)). | ||
| """ | ||
|
|
||
| resp = await self.send_command(module="X0", command="RF") | ||
| version = resp.split("rf")[-1].split(" ")[0] | ||
| build_date = self._parse_firmware_version_datetime(resp) | ||
| return version, build_date | ||
|
|
||
| async def x_arm_move( | ||
| self, | ||
| x: float, | ||
| acceleration_level: int = 3, | ||
| current_protection_limiter: int = 7, | ||
| ): | ||
| """Move the X-arm to an absolute X position with specified acceleration. | ||
|
|
||
| Args: | ||
| x: Target X coordinate in mm. Must be between 90.0 and 1350.0. | ||
| acceleration_level: Acceleration index (hardware units), 1-5. Default 3. | ||
| current_protection_limiter: Motor current limit (hardware units), 0-7. Default 7. | ||
| """ | ||
|
|
||
| if not (90.0 <= x <= 1350.0): | ||
| raise ValueError(f"x must be between 90.0 and 1350.0 mm, is {x}") | ||
| if not (1 <= acceleration_level <= 5): | ||
| raise ValueError(f"acceleration_level must be between 1 and 5, is {acceleration_level}") | ||
| if not (0 <= current_protection_limiter <= 7): | ||
| raise ValueError( | ||
| f"current_protection_limiter must be between 0 and 7, is {current_protection_limiter}" | ||
| ) | ||
|
|
||
| return await self.send_command( | ||
| module="X0", | ||
| command="XP", | ||
| la=f"{round(x * 10):05}", | ||
| lr=str(acceleration_level), | ||
| lw=str(current_protection_limiter), | ||
| ) |
There was a problem hiding this comment.
New public movement APIs (x_arm_request_firmware_version, x_arm_move, iswap_rotation_drive_move_x) are added but there are no accompanying unit tests to lock down command assembly (module/command/parameter formatting) and the kg-offset translation behavior. This repo already has comprehensive STAR backend command-string tests; adding a few targeted tests would prevent regressions and validate the expected firmware strings for X0:XP and X0:RF.
It does, but this is a new command and therefore untested outside of one lab.
Replacing it instead of adding next to it first, testing and verifying it, and then replacing once verified across labs seemed too risky to me. |
|
either it works or it doesn't. has this new one been tested and verified? |
yes it has been verified on one device - do you know it will work on newer ones? |
|
I am testing it in the meantime, it also ignores the |
|
tested on STAR with
2 STARLets
and it works nicely, has parity with position_left_x_arm_ i cannot install 5.1 firmware on any of the star(let)s i have tried import asyncio
from pylabrobot.liquid_handling import LiquidHandler
from pylabrobot.liquid_handling.backends.hamilton.STAR_backend import STARBackend
from pylabrobot.resources.hamilton import STARDeck
async def main():
star = STARBackend()
lh = LiquidHandler(backend=star, deck=STARDeck())
await lh.setup(
skip_instrument_initialization=True,
skip_pip=True,
skip_autoload=True,
skip_iswap=True,
skip_core96_head=True,
)
try:
version, build_date = await star.x_arm_request_firmware_version()
print(f"X-arm firmware: version={version!r} build_date={build_date}")
for x_mm in (200.0, 600.0, 400.0):
print(f"\n--- target {x_mm} mm ---")
await star.position_left_x_arm_(x_position=int(round(x_mm * 10)))
pos_legacy = await star.request_left_x_arm_position()
print(f"after position_left_x_arm_({int(x_mm*10)}): RX = {pos_legacy} mm")
await star.position_left_x_arm_(x_position=int(round((x_mm + 100) * 10)))
await star.x_arm_move(x=x_mm)
pos_new = await star.request_left_x_arm_position()
print(f"after x_arm_move({x_mm}): RX = {pos_new} mm")
delta = abs(pos_legacy - pos_new)
print(f"delta = {delta} mm")
finally:
await lh.stop()
asyncio.run(main()) |
|
do we have a doc for 5.1? what does it say for this command? |
|
i can change the acceleration precisely by overwriting the base: >>> import time
>>> await backend.x_arm_move(x=100)
>>> t0 = time.time()
>>> await backend.x_arm_move(x=500)
>>> t1 = time.time()
>>> print(t1 - t0)
1.865234375to update acceleration: first request the current value >>> await backend.send_command("X0", "RA", ra="ax")
'X0RAid0063ax100 01 0100 300 02 0375 400 06 0750 500 10 1125 600 14 1500 700 18 1875'Then you can overwrite it ax = "100 01 0100 300 02 0375 400 06 0750 500 18 1125 600 14 1500 700 18 1875"
await backend.send_command("X0", "AA", ax=ax)Here I changed 10 -> 18 (after 500) for the "acceleration increase per 1 meter (reduced by 100" (#1018 (comment)). I chose the third because the default acceleration_level for this makes the arm move faster! >>> import time
>>> await backend.x_arm_move(x=100)
>>> t0 = time.time()
>>> await backend.x_arm_move(x=500)
>>> t1 = time.time()
>>> print(t1 - t0)
1.6652910709381104i do not see parameters for velocity directly though... You can however control the PID constants ( |
|
6 blocks of "start value (offset) |
|
@rickwierenga: I don't understand
Isn't both the request and set 18 here? Also, what does 18 refer to here, i.e. what unit does it take? |
|
fixed, accidentally pasted the same string twice the unit given is "acceleration increase per 1 meter (reduced by 100 for long integer arithmetic)", so jerk. I do not know more than that. Looking at the other lines in the firmware doc (like XF command) I see 150mm/s^2, so I am assuming it is that since the numbers are about the same size. |
|
Do you think we should finish this PR as is and figure out what these units mean and how to best use them in an upgrade PR? |
|
let's name it |
|
and then merge as experimental yeah |
|
Happy to - let's call them experimental_ for now and figure out what further control we can achieve |
|
fundamentally the x move only takes 5 "levels" but you are saying we can change what these 5 EEPROM registers map to: i.e. we should definitely bound to the set 1 level for lower bound and 5 level for upper bound? but I am still not sure what these setting actually mean in terms of physical units |
it takes one of 5 levels, which just points to the saved list (see table). each list item is 3 values (which I think are start accel, jerk, stop accel) AA allows us to override that list with custom values. it probably resets on power cycle but persists between commands, so reseting them is necessary (similar to so index 1 through 5 really just points to one of 3 values the best would be for the user to specify the value rather than choosing from one of 5
probably, if 1 and 5 are defined as the true min and max but I can imagine we can safely go beyond those
I am not sure either |
* expose iswap_rotation_drive_move_y * shuffle iswap move y next below move x * Expose `STARBackend.iswap_rotation_drive_move_z()` Adds the absolute Z-axis sibling to `iswap_rotation_drive_move_x` (#1018) and `iswap_rotation_drive_move_y` (#1020). Wraps `R0 ZA` and accepts the rotation-drive plane Z (matching `iswap_rotation_drive_request_z`); the 13 mm offset to the gripper finger plane is applied internally. `acceleration` is exposed in mm/sec^2. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * clarify `bottom` return (per LFB PLR expectation) PLR users expect left-front-bottom referencing; for channels this changes to center-center in x-y but in z people still expect bottom; since we are referencing the actual rotation drive here and its bottom is 13 mm above the finger_z_plane I have added a short comment to make this clearer * `make format` --------- Co-authored-by: Camillo Moschner <camillo.moschner@biocam.guide> Co-authored-by: Camillo Moschner <122165124+BioCam@users.noreply.github.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* expose iswap_rotation_drive_move_y * shuffle iswap move y next below move x * Expose `STARBackend.iswap_rotation_drive_move_z()` Adds the absolute Z-axis sibling to `iswap_rotation_drive_move_x` (#1018) and `iswap_rotation_drive_move_y` (#1020). Wraps `R0 ZA` and accepts the rotation-drive plane Z (matching `iswap_rotation_drive_request_z`); the 13 mm offset to the gripper finger plane is applied internally. `acceleration` is exposed in mm/sec^2. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * clarify `bottom` return (per LFB PLR expectation) PLR users expect left-front-bottom referencing; for channels this changes to center-center in x-y but in z people still expect bottom; since we are referencing the actual rotation drive here and its bottom is 13 mm above the finger_z_plane I have added a short comment to make this clearer * `make format` * create `iswap_rotation_drive_request_predefined_z_positions` * create `iswap_rotation_drive_request_predefined_y_positions` * add iSWAP Y/Z drive conversions and correct pip Y resolution * tighten iswap_rotation_drive_move_z error messages and document Raises * create `iswap_rotation_drive_request_predefined_z_positions` * create `iswap_rotation_drive_request_predefined_y_positions` * add iSWAP Y/Z drive conversions and correct pip Y resolution * tighten iswap_rotation_drive_move_z error messages and document Raises * drop firmware-internal constraints from predefined Y/Z parking-pose docstrings * reword Z increment-range comment to drop misleading return claim * decouple iSWAP rotation-drive Y/Z from PIP-channel conversions * rename increment version to `_` and provide public API in expected mm * make everything that references rotation drive return rotation drive data * `make format` * simplify: compress increment and mm returns into one method * simplify z conversions --------- Co-authored-by: Rick Wierenga <rick_wierenga@icloud.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Tame the iSWAP - Part 6
Hi everyone,
The standard
C0-based X-arm movement along the X-axis does not allow any form of speed nor acceleration control.This is an obstacle e.g. for...
This PR exposes per-call acceleration control by going one layer down to the
X0module, which provides absolute position commands with parametric acceleration (XP, with acceleration levellrand current protectionlw).new methods:
Note:
I apparently have a very old X-arm.
Ideally we would use an X-arm movement command that actually enables precise speed and acceleration parameters.
But my firmware
X0RFid0143rf1.4S 2012-04-25does not enable this.I have made
STARBackend.x_arm_request_firmware_version()to enable a user with more modern X-arm firwmare/hardware to make theSTARBackend.x_arm_move()more powerful and adapt it's allowed argument input dynamically to the available firmware (as we have done for other commands, e.g. 96-head move y)