Skip to content

Expose X-axis movement with acceleration control for X-arm and iSWAP rotation drive#1018

Merged
rickwierenga merged 4 commits intoPyLabRobot:mainfrom
BioCam:expose-iswap-rotation-drive-move-x
Apr 30, 2026
Merged

Expose X-axis movement with acceleration control for X-arm and iSWAP rotation drive#1018
rickwierenga merged 4 commits intoPyLabRobot:mainfrom
BioCam:expose-iswap-rotation-drive-move-x

Conversation

@BioCam
Copy link
Copy Markdown
Collaborator

@BioCam BioCam commented Apr 29, 2026

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...

  • traversing tips with slippery liquids along the X dimension
  • generating smoother motion for the iSWAP

This PR exposes per-call acceleration control by going one layer down to the X0 module, which provides absolute position commands with parametric acceleration (XP, with acceleration level lr and current protection lw).

new methods:

STARBackend.x_arm_move(x, acceleration_level=3, current_protection_limiter=7)

STARBackend.iswap_rotation_drive_move_x(x, acceleration_level=3, current_protection_limiter=7)

STARBackend.x_arm_request_firmware_version()

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-25 does 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 the STARBackend.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)

Comment thread pylabrobot/liquid_handling/backends/hamilton/STAR_backend.py Outdated
Comment on lines +9906 to +9908
Args:
x: Target rotation-drive X coordinate in mm.
acceleration_level: Acceleration index (hardware units), 1-5. Default 3.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

can we put the acceleration index table in the doctoring?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

I don't understand, what do you mean by this?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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                    |
+---------------------+-------------+---------------------------+-------------------------+-------------------------+

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

otherwise it is not clear what acceleration_level means

@rickwierenga
Copy link
Copy Markdown
Member

also does this replace position_left_x_arm_? I do not see why we would keep both

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

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 sends X0:XP with per-call acceleration/current limiter parameters.
  • Add STARBackend.iswap_rotation_drive_move_x() wrapper that translates deck X using the cached kg offset and delegates to x_arm_move().

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +1840 to +1846
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}")
Copy link

Copilot AI Apr 29, 2026

Choose a reason for hiding this comment

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

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.

Suggested change
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}")

Copilot uses AI. Check for mistakes.
Comment thread pylabrobot/liquid_handling/backends/hamilton/STAR_backend.py
Comment on lines +1819 to +1860
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),
)
Copy link

Copilot AI Apr 29, 2026

Choose a reason for hiding this comment

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

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.

Copilot uses AI. Check for mistakes.
@BioCam
Copy link
Copy Markdown
Collaborator Author

BioCam commented Apr 29, 2026

also does this replace position_left_x_arm_? I do not see why we would keep both

It does, but this is a new command and therefore untested outside of one lab.

position_left_x_arm_ is the foundation of move_channel_x which (at least for me) is used in every single script I have ever written.

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.

@rickwierenga
Copy link
Copy Markdown
Member

either it works or it doesn't. has this new one been tested and verified?

@BioCam
Copy link
Copy Markdown
Collaborator Author

BioCam commented Apr 29, 2026

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?

@rickwierenga
Copy link
Copy Markdown
Member

I am testing it

in the meantime, it also ignores the left_side_panel_installed which constrains the range of x movement. Ideally we add a min_x function that returns the value rather than it living in every individual function

@rickwierenga
Copy link
Copy Markdown
Member

tested on STAR with

  • X-arm firmware: version='1.4S' build_date=2012-04-25

2 STARLets

  • 1.4S, build date 2012-04-25
  • 1.4S, build date 2012-04-25

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())

@rickwierenga
Copy link
Copy Markdown
Member

do we have a doc for 5.1? what does it say for this command?

@rickwierenga
Copy link
Copy Markdown
Member

rickwierenga commented Apr 29, 2026

i can change the acceleration precisely by overwriting the ax parameter using command X0AA (analogous to slow_iswap)

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.865234375

to 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 x_arm_move is 3.

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.6652910709381104

i do not see parameters for velocity directly though... You can however control the PID constants (kp,ki and kd) which i havent tried yet

@rickwierenga
Copy link
Copy Markdown
Member

6 blocks of "start value (offset)
acceleration increase
top limit of acceleration increase"

@BioCam
Copy link
Copy Markdown
Collaborator Author

BioCam commented Apr 30, 2026

@rickwierenga: I don't understand

Here I changed 10 -> 18 (after 500)

Isn't both the request and set 18 here?

Also, what does 18 refer to here, i.e. what unit does it take?

@rickwierenga
Copy link
Copy Markdown
Member

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.

@BioCam
Copy link
Copy Markdown
Collaborator Author

BioCam commented Apr 30, 2026

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?

@rickwierenga
Copy link
Copy Markdown
Member

let's name it experimental_ until we at least figure out the acceleration ramps. when it is no longer experimental we can replace position_left_x_arm_ with this

@rickwierenga
Copy link
Copy Markdown
Member

and then merge as experimental yeah

@BioCam
Copy link
Copy Markdown
Collaborator Author

BioCam commented Apr 30, 2026

Happy to - let's call them experimental_ for now and figure out what further control we can achieve

@BioCam
Copy link
Copy Markdown
Collaborator Author

BioCam commented Apr 30, 2026

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

@rickwierenga
Copy link
Copy Markdown
Member

fundamentally the x move only takes 5 "levels"

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 slow_iswap)

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

i.e. we should definitely bound to the set 1 level for lower bound and 5 level for upper bound?

probably, if 1 and 5 are defined as the true min and max but I can imagine we can safely go beyond those

but I am still not sure what these setting actually mean in terms of physical units

I am not sure either

@rickwierenga rickwierenga merged commit b0ce637 into PyLabRobot:main Apr 30, 2026
18 of 19 checks passed
@BioCam BioCam deleted the expose-iswap-rotation-drive-move-x branch April 30, 2026 18:43
BioCam added a commit that referenced this pull request May 1, 2026
* 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>
BioCam added a commit that referenced this pull request May 1, 2026
* 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>
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.

3 participants