Skip to content

Improve feetech driver#64

Merged
maximilianeberlein merged 11 commits into
mainfrom
meb/feetech-driver-improvements
May 12, 2026
Merged

Improve feetech driver#64
maximilianeberlein merged 11 commits into
mainfrom
meb/feetech-driver-improvements

Conversation

@maximilianeberlein

@maximilianeberlein maximilianeberlein commented May 6, 2026

Copy link
Copy Markdown
Contributor

This pull request introduces several improvements to motor hardware support, configuration flexibility, and robustness in the ORCA hand control software.

The main changes include enhanced auto-detection and configuration of motor types and baud rates, improved fallback handling for hardware communication failures, and new utilities for managing Feetech motors.

These updates make the system more adaptable to different hardware setups and more resilient to communication issues.

- Drop redundant read_pos_vel_cur sync wrapper; the public method now does
  the sync read directly with per-motor fallback
- Rename _read_*_individual fallbacks to _read_*_per_motor_fallback so the
  intent is obvious at the call site
- Centralize POSITION_DIRECTION via _raw_to_rad / _rad_to_raw helpers so
  the sign inversion stops leaking into read/write logic
- Define MotorError + MotionTimeoutError; wait_for_motion_complete now
  raises on timeout instead of silently returning False
- Add MotorClient.waits_for_motion class flag; OrcaHand.wait_for_motion
  skips the motor lock when the underlying client doesn't actually block
- Strip the verbose AI-style comment from calibrate_offset
- Add explicit parens to the sign-flip in write_positions_sync
- Rename scripts/test.py -> scripts/stress_test.py and update docs
Configs no longer need to declare motor_type, port, or baudrate. At
connect time the client probes attached USB serial adapters and pings
each candidate (motor_type, baudrate) pair until one responds.

- Add probe() static method on DynamixelClient and FeetechClient that
  opens a port at a given baudrate, sends a few pings, and reports
  whether any motor responds — without enabling torque
- Add MOTOR_BAUD_RATES (constants.py): {dynamixel: [1M, 3M], feetech: [1M]}
- Add motor_type_for_port and find_single_usb_serial_port helpers in
  utils for VID-based detection and lone-adapter fallback
- OrcaHandConfig.motor_type/port/baudrate become Optional; explicit
  values still win and short-circuit the probe along that axis
- OrcaHand.connect now does:
    1. resolve port (yaml > VID > sole USB adapter > interactive picker)
    2. trial-probe to find motor family + baudrate
    3. instantiate the right client and connect
  An auto-detected port is persisted to config.yaml; motor_type and
  baudrate are intentionally not persisted so configs stay driver-agnostic
- Strip motor_type/port/baudrate from all bundled v1/v2 model configs
- Delete orcahand_right_feetech model dir; orcahand_right is now used
  for both Dynamixel and Feetech builds of the same hand
- Update stress_test.py defaults: --num-steps from constants (50),
  --step-size from constants (0.01s), --hold default 2s — restoring
  pre-rename motion timing instead of the way-too-fast 25 steps × 1ms
- Add tests/test_connect_resolution.py covering port lookup, single-USB
  fallback, trial-probe at multiple baudrates, and pinned-motor_type
Brings the configure_motor_chain script and core calibration logic to
parity with the Feetech build of the hand. Ported from fb/dev with
adaptations for the auto-detect machinery introduced earlier on this
branch.

configure_motor_chain.py
- Optional --motor-type with auto-detection at factory defaults
  (Dynamixel @ 57600 ID 1, Feetech @ 1M ID 1) when omitted
- --reset to revert configured motors back to factory defaults
- --baudrate to bulk-change baud without touching IDs
- Safe power-cycle prompt for Feetech (unkeyed connectors → reverse
  polarity destroys the motor; force unplug-replug for each insert)
- HLS3930 wrist + HLS3915 fingers split with substring model checks,
  matching the existing XC430/XC330 split for Dynamixel
- _close_silently() helper used after each ID/baud mutation so the
  spurious "no status packet" disconnect logs go away
- Pre-scan filter recognises HLS3930 at ID 1 as a configured wrist
  (not a factory-default to ignore)
- Positional arg renamed model_path -> config_path for parity with
  every other script in the repo

FeetechClient
- scan_for_motors / change_motor_id / change_motor_baudrate
- FEETECH_BAUD_RATE_MAP (host-baud -> register code)
- FEETECH_MODELS populated for the three model_number values seen
  in our 17-motor builds: 4106 -> HLS3930, 6922/5130 -> HLS3915

Calibration core
- OrcaHand.calibrate(joints=...) and _calibrate(joints=...) filter the
  calibration sequence by an explicit joint allow-list. Lets you re-run
  one finger or one joint without touching the others. The
  scripts/calibrate.py CLI for this is intentionally a follow-up commit.

Bundled configs
- Each model's config.yaml gets a commented-out `port:` / `baudrate:`
  hint at the top so users can find the override path quickly without
  hunting through docs

utils
- Re-export auto_detect_port and get_and_choose_port from the package
  root so scripts can `from orca_core.utils import ...` directly
Calibrate part of the hand without disturbing the rest of an existing
calibration, and stop a partial re-run from leaving the YAML in a
half-state.

scripts/calibrate.py
- --fingers (thumb / index / middle / ring / pinky / wrist) and
  --joints (individual joint names) flags, mutually exclusive
- Resolves the selection to a flat joint list and forwards via
  hand.calibrate(joints=...)

orca_core/hardware_hand.py
- _calibrate accumulates direction measurements in a separate
  pending_limits dict; only commits a joint's [lower, upper] to
  motor_limits once BOTH directions have been measured
- Removes the upfront wipe loop that nulled motor_limits for every
  targeted joint before any motion. Previous behaviour was that an
  interrupted partial calibration zeroed out the joint's data even
  though the new values weren't measured yet — now the old values
  stay in motor_limits.yaml until the new ones are fully captured
- _wrap_offsets_dict reset moved into the per-step setup so it still
  gets cleared for joints we touch this run

Mirrors fb/dev's e80f7ad applied on top of our hardware_hand.py
structure (we have CalibrationResult / config dataclass; fb/dev had
flat attrs).
main contains a revert of PR #59 (the original Feetech-driver work
this branch is built on). Resolved by keeping our branch's content
in feetech_client.py, motor_client.py, and hardware_hand.py — our
branch is the desired new state for those files.

The PR #60 revert was an "uncursing" mass-revert with no per-file
rationale, so the conflicts are spurious to the actual goal here.
DynamixelClient
- Mirror the sync-with-fallback pattern Feetech got in 1df95b3.
  DynamixelReader.read() now falls back to per-motor reads on bulk
  failure (entire packet) or on partial response (specific IDs missing
  from the bulk packet)
- Default _read_per_motor_fallback is a no-op so subclasses without
  an override (e.g. _moving_status_reader) keep their stale-cache
  behaviour, matching the prior version
- DynamixelPosVelCurReader._read_per_motor_fallback uses
  read4ByteTxRx for position/velocity and read2ByteTxRx for current,
  same unsigned_to_signed + scale conversion as the bulk path. A
  motor whose individual read also fails keeps its previously-cached
  value
- DynamixelTempReader._read_per_motor_fallback does the same with
  read1ByteTxRx for ADDR_PRESENT_TEMPERATURE
- Resolves Francesco's PR #62 L276 comment ("do the same in dynamixel
  so both clients are on par"). One bad cable / dropped motor on the
  daisy chain no longer blanks every joint reading on screen — only
  the actual offender shows stale data

stress_test.py
- Switch hand.enable_torque() -> hand.init_joints(force_calibrate=
  args.mock). init_joints handles enable-torque, control-mode setup,
  AND auto-calibration if the hand isn't calibrated yet, matching
  every other position-commanding script in scripts/. Running
  stress_test on a fresh hand will now run the full calibration
  routine first instead of commanding undefined joint positions
@fracapuano fracapuano changed the title Meb/feetech driver improvements Improve feetech driver May 7, 2026
@fracapuano

Copy link
Copy Markdown
Collaborator

Supersedes #62 (which superseded #59, 😇)

@fracapuano
fracapuano self-requested a review May 7, 2026 13:30

@fracapuano fracapuano left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Ciao there 👋 Left a couple comments and leaving an approval conditioned on your assessment of them @maximilianeberlein! I think they could really improve UX, but the PR is in a great place to be merged and iterate

Comment thread orca_core/hardware/dynamixel_client.py Outdated
Comment thread orca_core/hardware/dynamixel_client.py
Comment thread orca_core/hardware/dynamixel_client.py
Comment thread orca_core/hardware/feetech_client.py Outdated
Comment thread orca_core/hardware/feetech_client.py Outdated
Comment thread scripts/configure_motor_chain.py Outdated
"""Get list of valid baudrates for the given motor type."""
if motor_type == 'feetech':
return sorted(FEETECH_BAUD_RATE_MAP.keys(), reverse=True)
return sorted(DYNAMIXEL_BAUD_RATE_MAP.keys(), reverse=True)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Suggested change
return sorted(DYNAMIXEL_BAUD_RATE_MAP.keys(), reverse=True)
elif motor_type == "dynamixel":
return sorted(DYNAMIXEL_BAUD_RATE_MAP.keys(), reverse=True)
else:
raise ValueError(f"Unknown motor type. Accepted motor types ['dynamixel', 'feetech'] (provided: {motor_type}).")

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I feel this file is way to long for what it does, and because the ideal PR has very few lines of code, maybe we could trim it down significantly?

Comment thread scripts/stress_test.py
# Max operating temp (°C) — conservative across XC330 (70°C) and XC430 (72°C)
MAX_TEMP = 70

MAX_TEMP = 70 # °C — conservative across Dynamixel XC330/430 and Feetech STS3215

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

we could also store a dictionary with two different values for clients, in case the maximal operating temperature is significantly different between the two clients

Comment thread tests/test_connect_resolution.py Outdated
# ----- _trial_probe -------------------------------------------------------

@pytest.fixture
def mock_hand(mock_config_dir):

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

it'd be better if fixtures were in conftests.py to keep tests tiny

Comment thread tests/test_connect_resolution.py Outdated
monkeypatch.setattr(
feetech_client.FeetechClient, "probe", staticmethod(feetech_probe)
)
OrcaHand_trial_probe(mock_hand, "/dev/cu.x")

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

nit: I don't like this helper defined outside of the function/fixture in here

Round of cleanups in response to Francesco's review comments on PR #64.

API
- New MotorRead NamedTuple (position / velocity / current) returned by
  read_position_velocity_current (renamed from read_pos_vel_cur). Named
  attribute access AND tuple unpacking so callers can keep doing
  `pos, vel, cur = client.read_position_velocity_current()`. Migrated
  every caller in hardware_hand, mock client, dynamixel demo block,
  and the three diagnostic scripts.

dynamixel_client
- dynamixel_sdk import hoisted to module level (PortHandler / PacketHandler /
  COMM_SUCCESS) so probe() doesn't pay name-resolution per call. Test
  fakes in conftest.py and scripts/common.py extended with COMM_SUCCESS.
- _read_per_motor_fallback hook docstring made explicit so the
  template-method pattern isn't misread as duplicate dead code.

feetech_client
- FEETECH_MODELS comment trimmed to one line.
- scan_for_motors signature uses Optional[list] = None with an explicit
  `if baud_rates is None` check rather than the truthy-fallback idiom.
- Brief inline comment on the position clamp in write_positions_sync.

calibrate.py
- Mutex error message wording: "Use either one." per Francesco's suggestion.

configure_motor_chain.py (significant rewrite)
- Top-level FEETECH/DYNAMIXEL/MOTOR_TYPE_DEFAULTS/MOTOR_MODELS constants
  replace scattered string literals.
- `mid` renamed to `motor_id` throughout.
- print() routed through `logger.error` / `logger.info` for error and
  status messages so users can silence them via logging config.
- get_valid_baudrates gets explicit elif/else with ValueError.
- AI-slop comments trimmed; docstrings added for scan_already_configured_motors
  and the obscure id-walk + expected-sequence prints inside it.
- Two near-identical port-wait helpers collapsed into one (`present=` kwarg).
- Two near-identical bulk loops (baudrate change vs factory reset)
  collapsed into _process_motors_loop with an action callback.
- Triple `client = ...; client.connect(); ...; _close_silently(client)`
  pattern replaced by a _config_session context manager.
- Per-family branching (`if motor_type == FEETECH` etc.) replaced by
  MOTOR_TYPE_LABELS / MOTOR_MODELS / VALID_BAUDRATES dict lookups.
- Decorative section dividers and unused colour constants dropped.
- Net: 701 -> 544 lines (~22% smaller) with the actual duplication gone.

yaml configs
- Inline port/baudrate "uncomment to override" hints removed across all
  bundled configs — Francesco's "ship batteries-included" point. Override
  documentation goes in docs, not in every shipped config.

tests
- fake_serial_port + patch_comports + mock_hand fixtures moved from
  test_connect_resolution.py into conftest.py so test files stay focused.
- Hop-over helper killed: tests now call OrcaHand._trial_probe(hand, port)
  directly to exercise the real implementation on a MockOrcaHand instance.
probe() now pings motor_ids[0] and motor_ids[-1] instead of the first
two, so partial bench setups (e.g. only the last motor on the chain)
get detected. Drops the unused ping_count parameter.

Promotes DynamixelClient/FeetechClient imports in hardware_hand to
module level to avoid per-call name resolution in _trial_probe and
_create_motor_client.
port / baudrate / motor_type were removed from bundled configs when
auto-detection landed, but the override path (drop them back into
config.yaml when you need to) wasn't documented anywhere users would
find it. Now covered in:

- README "Serial port, baudrate, and motor type" section — replaces the
  outdated "Serial Port Path" block that told users to set the port in
  yaml. New section explains auto-detect + three override use cases
  (multiple adapters / non-default baud / explicit family).
- setting-up-config.md General Settings — bundled yaml example trimmed,
  new "Optional driver overrides" subsection mirrors the README.
…robe

When motor_type or baudrate is missing from yaml the connect-time probe
fills it in, and the result is now written back to the file. Subsequent
connects find the populated values and skip the probe entirely.

Each field is only persisted if the yaml didn't have it — manual edits
stick. To swap motor families on the same hand, clear the field(s) in
yaml and reconnect to trigger a fresh probe.

_persist_resolved_port becomes _persist_resolved_driver and handles all
three fields uniformly; a single console line announces which were
written so the yaml mutation isn't silent.
`port: str = '/dev/ttyUSB0'` and `id_range: tuple = (0, 252)` were
vestigial — every caller passes both arguments explicitly. The Linux-
path default was actively misleading on macOS / custom adapters, and
the (0, 252) full-address-space default would silently make any
unguarded call do a 253-ID scan. Made both required on FeetechClient
and DynamixelClient; baud_rates stays Optional[list] = None since that
fallback is legitimately useful.

Also switched DynamixelClient.scan_for_motors's baud_rates fallback
from `... or list(...)` to an explicit `if baud_rates is None`, matching
the FeetechClient pattern.

Extends Francesco's "I would not add this default, rather use a None"
review note (PR #64) to the two other defaults in the same signature.
@maximilianeberlein
maximilianeberlein merged commit ccefb7c into main May 12, 2026
1 check passed
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