Skip to content

fix(registry): key shared controller by canonical device path#26

Open
Nick Hehr (HipsterBrown) wants to merge 1 commit into
mainfrom
fix/canonical-port-key
Open

fix(registry): key shared controller by canonical device path#26
Nick Hehr (HipsterBrown) wants to merge 1 commit into
mainfrom
fix/canonical-port-key

Conversation

@HipsterBrown

Copy link
Copy Markdown
Contributor

First hardening step from the shared-controller architecture review (#25) — the highest-value, lowest-risk footgun.

Problem

The registry that lets the arm (servos 1-5) and gripper (servo 6) share one serial bus keys on the raw port string. An arm configured /dev/ttyUSB0 and a gripper configured via a /dev/serial/by-id/usb-... symlink to the same device are treated as two different ports → two feetech.NewBus connections silently contending for one UART (garbled frames, intermittent timeouts). It fails at the hardware level with no error, which is the hardest kind to diagnose.

Fix

Resolve the port to a stable device identity with filepath.EvalSymlinks (best-effort: falls back to the raw string if unresolvable — device not present yet, or a platform without symlink semantics) and use that as the registry map key.

Only the key is normalized — the caller's config is deliberately not mutated, because it can be read concurrently via GetControllerStatus (mutating config.Port tripped the race detector in TestConcurrentRegistryAccess). Instead, configsEqual and the release-tracking trackCaller canonicalize port strings at comparison time. Net effect: different spellings of one device collapse to a single shared bus, and a differing spelling no longer trips the "conflict: different config" check.

The bus itself is still opened with the caller's original config.Port (a valid path to the same device), so behavior is unchanged when paths are already canonical.

Testing (no hardware)

  • TestCanonicalPortKey — a symlink and its target canonicalize equal; an unresolvable path falls back to the original string.
  • TestRegistry_SymlinkPortSharesController — a consumer acquiring via a by-id symlink reuses the entry created via the real device path (one registry entry, refcount 2) instead of opening a second bus.
  • go test -race ./cmd/module/ . — full so_arm package green, including the existing TestConcurrentRegistryAccess.

Scope

Independent of #24 (close-state race fix) — both branch off main and touch different parts of registry.go; whichever merges second is a trivial rebase. Addresses footgun #1 of #25; the lifecycle cleanup and conflict-diagnostics items there remain follow-ups.

Refs #25.

🤖 Generated with Claude Code

https://claude.ai/code/session_01JNvy6dGUT6wBHLNBv8YPYv

The shared-controller registry keyed on the raw `port` string, so an arm
configured as "/dev/ttyUSB0" and a gripper configured via a
"/dev/serial/by-id/usb-..." symlink to the same device were treated as two
different ports — opening two feetech buses that silently contend for one
UART (garbled frames, intermittent timeouts). This is footgun #1 from the
shared-controller architecture review (#25).

Resolve the port to a stable device identity (filepath.EvalSymlinks,
best-effort with fallback to the raw string) and use that as the registry
map key. Only the key is normalized — the caller's config is left untouched
because it can be read concurrently via GetControllerStatus (mutating it
tripped the race detector) — so configsEqual and trackCaller canonicalize
port strings at comparison time instead. Result: different spellings of one
device collapse to a single shared bus.

Tests (no hardware): canonicalPortKey resolves a symlink to its target and
falls back for unresolvable paths; a consumer acquiring via a symlink reuses
the entry created via the real path (one entry, refcount 2) rather than
opening a second bus. Full package passes under -race, including the existing
TestConcurrentRegistryAccess.

Refs #25.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01JNvy6dGUT6wBHLNBv8YPYv
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.

1 participant