Expose each SCS axis as its own top-level BEC motor sharing a singleton
SmargopoloController per host:port, plus a thin `dev.smargon` coordinator for
whole-goniometer ops. Replaces the single-device-with-axis-subcomponents design.
Why: BEC's motor UX (dev.wm, umv, limits, GUI motor widgets) is keyed on
top-level devices. With sub-components, dev.wm showed `readback N/A` (it looks up
a key equal to the device name) and `limits [0,0]` (the client reads limits from
the root device's low/high_limit_travel signals). General, not a sim artifact.
- controller.py: SmargopoloController(OphydObject), singleton per (host,port,sim);
owns transport + lock + mode; referencing / move_scs / nudge / q1..q6 diagnostics.
Reachable as dev.<axis>.controller.<op>() (USER_ACCESS).
- positioner.py: SmargopoloMotor(Device, PositionerBase) -- top-level per-axis motor.
scs_axis field binds the (user-chosen) BEC name to the fixed SCS axis. Exposes
low/high_limit_travel signals and aliases readback to the device name, so dev.wm
shows real per-axis readback + limits (low==high => unbounded). egu auto from axis.
- smargon.py: SmarGon(PSIDeviceBase) reduced to a thin coordinator delegating to the
shared controller; keeps read-only mode/rosout signals.
- YAML: smargon.yaml + smargon_sim_session.yaml rewritten to per-axis entries
(sgx/sgy/sgz/sgchi/sgphi[/sgomega]) + coordinator. Names user-configurable; SCS
binding explicit via scs_axis.
- transport/ layer + fake unchanged. Tests rewritten: 29 pass vs FakeTransport
(singleton reset fixture; covers sharing, wm readback aliasing, limits, fault paths).
Still DRAFT / untested against hardware.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>