Skip to content

DaMiao Studio: passive monitor + unified control/monitor UI - #6

Merged
jia-xie merged 14 commits into
mainfrom
feat/passive-monitor-dashboard
Jun 16, 2026
Merged

DaMiao Studio: passive monitor + unified control/monitor UI#6
jia-xie merged 14 commits into
mainfrom
feat/passive-monitor-dashboard

Conversation

@jia-xie

@jia-xie jia-xie commented Jun 16, 2026

Copy link
Copy Markdown
Owner

Summary

Adds a passive listen-only monitor and unifies it with active control into one app — DaMiao Studio — plus a fix to the recent CAN-FD change.

1. Passive monitor (listen-only)

  • New damiao_motor/monitor/ package: decodes both another controller's command frames and the motors' feedback off the bus, never transmitting (guaranteed structurally; asserted by test). Best-effort CAN_RAW_LISTEN_ONLY on socketcan.
  • Auto-learned signal registry with cmd↔feedback pairing; ring-buffered series.
  • Flask service + WebSocket streaming (flask-sock), subscription-scoped & server-decimated; --demo synthetic source for hardware-free use.

2. Unified Studio UI (control + monitor)

  • One server, one shared signal store, two modes: Monitor (passive, never sends — control endpoints return 409) and Control (active DaMiaoController; every command + feedback feeds the same live plots).
  • Full control parity: connect/scan, enable/disable, MIT/POS_VEL/VEL/FORCE_POS (single + continuous), set-zero, clear-error, store-params, editable register table (ms timeout, hex IDs, mode/baud dropdowns), motor-type.
  • damiao gui → Studio in control mode; damiao monitor → monitor mode. Legacy web_gui.py retained (unused).

3. Professional front-end (gui/webapp, React + TS + Vite)

  • Free-form widget canvas (GridStack): drag/resize/float widgets anywhere; add/remove; layout persists.
  • uPlot cmd-vs-actual plots (drag a signal onto a plot; drop cmd over fb to overlay), virtualized table + raw CAN log, per-motor cards.
  • Light/dark theme (default light). Prebuilt bundle committed so pip install needs no Node.

4. fd fix

  • DaMiaoMotor(fd=...) defaults to False again (the recent change made it a required positional arg — breaking).

Validation

  • Unit: 11 tests (decode round-trips vs the real encoders, never-transmit assertion).
  • Hardware (linearbot): passive monitor verified on both arms (real cmd+feedback decode, pairing, 0 errors, TX stays 0). Control verified on can_arm_l motor 7 through the full Studio path: connect → scan → clear-error → enable → MIT command physically moves the motor → return → register read → disable, with cmd+fb landing in the shared store.
  • Packaging/CI: wheel bundles the SPA (+ asserts it); fixed a latent bug where core/cli/gui subpackages were excluded from the wheel; release.yml builds the SPA before the wheel; repo is ruff-clean.

Notes / known limitations (beta)

  • Latched motor status 0x3 needs clear-error before enable (undocumented DaMiao state; surfaced honestly).
  • Base motors need a per-motor type selection for correct scaling (DM4310V is ±π).
  • WS not load-tested at full multi-bus 250 Hz; gs_usb passive path untested.
  • No automated tests yet for ControlService/the unified server (decode/store/listener are covered).

Intended to ship as prerelease v1.0.7b2 (CI auto-marks b/rc as prerelease).

🤖 Generated with Claude Code

jia-xie and others added 14 commits June 15, 2026 16:20
…ener

- damiao_motor/monitor/decode.py: reverse MIT/POS_VEL/VEL/FORCE_POS commands and
  feedback frames; cmd-vs-feedback disambiguation via p16 offset + structural check;
  extended motor-type presets (incl. DM4310V/FLOW_WHEEL +/-pi variants).
- store.py: auto-learned signal registry, per-signal ring buffers, cmd<->fb pairing,
  per-motor aggregate views.
- listener.py: listen-only PassiveCanListener; best-effort CAN_RAW_LISTEN_ONLY; never
  calls bus.send (guaranteed structurally + by test).
- tests/test_monitor.py: 10 round-trip + never-transmit tests (all green).
- motor.py: fd defaults to False (non-breaking).
- .gitignore: ship the built monitor SPA despite the build/ and dist/ rules.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- monitor/service.py: owns listener+store, raw-frame ring log, thread-safe snapshots,
  per-motor type override, optional demo source.
- monitor/server.py: Flask REST (status/signals/snapshot/motor-types) + WS /stream
  (subscription-scoped, ~30Hz, per-signal decimation cap); serves built SPA with
  client-routing fallback + dev placeholder.
- monitor/demo.py: synthetic cmd+feedback traffic (no CAN) for dev/screenshots/demo.
- pyproject: add flask-sock dependency.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Dashboard at damiao_motor/gui/webapp:
- dockview (VS Code-style) panels: drag to dock/split/merge into tabs; layout persisted.
- uPlot cmd-vs-actual plots: drag a signal chip onto a plot to add it; drop cmd onto fb
  to overlay (dashed=command, solid=feedback); per-plot time window; rAF render loop.
- views: multi-motor table, per-motor cards/gauges (+motor-type override), virtualized
  raw decoded CAN log; toolbar adds more panels (user-expandable).
- dnd-kit signal DnD kept disjoint from dockview tab DnD; Zustand registry + out-of-React
  ring buffers; WS client with auto-reconnect, subscription-scoped streaming, snapshot
  backfill on drop.
- prebuilt bundle committed to dist/ (end users need no node toolchain).

Fixes during bring-up: include series in uPlot options; hydrate plotConfigs
synchronously (avoid TDZ on PLOT_KEY + effect-ordering race that wiped plot signals).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- cli: new 'damiao monitor' subcommand (passive dashboard) mirroring 'gui', with
  --channel/--bustype/--bitrate/--feedback-offset/--motor-type/--demo; cmd_monitor handler.
- packaging: switch to setuptools packages.find (was packages=['damiao_motor'], which
  silently omitted core/cli/gui subpackages from the wheel — latent bug); ship
  gui/webapp/dist via package-data.
- CI: build the SPA (npm ci && npm run build) before the wheel and assert the bundle +
  monitor package are present in the wheel.

Verified: wheel contains monitor/*, gui/webapp/dist/*, and the previously-missing
core/cli/gui subpackages.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- panels/registry.tsx: single source of truth for panel types (kind/title/icon/
  description/render). Dock component map, toolbar add-buttons, panel titling, and
  default layout all derive from it — a new view is now one entry.
- raw CAN log: sticky header inside the scroll container sharing the exact grid + padding
  (columns now align); single-line cells with ellipsis; compact decoded fields; clock
  timestamp instead of raw epoch.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Real linearbot arm joints 4-7 report an undocumented status nibble (3), which made the
feedback detector fall through and misclassify their feedback as MIT commands (phantom
m20-23 signals). The arb-range + can-id-nibble match already disambiguates feedback
(ids offset+1..offset+15) from MIT commands (ids 1..15), so the status gate was both
unnecessary and wrong. Status still surfaces (e.g. UNKNOWN(3)) for display.

Validated live on can_arm_l and can_arm_r: all 7 joints decode (cmd+feedback), pairs
link, values plausible, 0 decode errors, monitor stays listen-only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replaces the rigid dockview tiling with a Grafana-style free-form canvas: every panel
is a widget you drag (by header) and resize (any edge) anywhere on a 12-col grid; add
from the toolbar, remove via the header ×, layout persists. Refreshed to a softer, more
modern dark theme (rounded surfaces, gentler palette, more breathing room).

- lib/widgets.ts: widget store (geometry + persistence, default layout, reset).
- components/Canvas.tsx: GridStack init + React content via portals (no DOM fighting);
  geometry synced back to the store on drag/resize.
- panels/registry.tsx: drop dockview component map; panels render unchanged.
- remove components/Dock.tsx, lib/dock.ts, dockview dep (bundle -25%).
- Toolbar adds widgets via the store; dnd-kit signal->plot drag still works.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- lib/theme.ts: persisted theme (default light), applied via data-theme on <html>
  before first paint.
- index.css: split palette into :root (light) + [data-theme=dark]; add --hover; replace
  hardcoded dark colors (toolbar, buttons, widget header, table hover) with variables.
- Toolbar: ☾/☀ toggle.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… weight/opacity cue

Dense data turned the dashed command trace into visual noise. Now: actual = bold solid,
command = a fainter (45% alpha), thinner 'ghost' line in the same hue. Legend swatches
match. Cleaner cmd-vs-actual read at any point density.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- monitor/control.py: ControlService wraps DaMiaoController; connect/scan/enable/disable/
  command(4 modes)/set-zero/clear-error/store-params/registers(GET+PUT w/ ms + ID changes)/
  motor-type. Pushes cmd+feedback into the shared SignalStore so the same plots/table/cards
  visualize active control (offset-agnostic). Transmits only in control mode.
- monitor/server.py: unified Studio server. Two modes — monitor (passive listener) and
  control (ControlService) feeding ONE shared store. /api/mode switch, /api/connect, gated
  /api/control/*, shared /api/monitor/{signals,snapshot,stream}. Control endpoints 409 in
  monitor mode (passivity preserved structurally).

Verified: demo monitor (33 signals/3 motors), control gating, mode switch, graceful
connect failure, register table — all green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Toolbar: Control/Monitor mode switch (+ active·TX / listen-only badge); 'DaMiao Studio'.
- New widgets (registry): Connection (bus/scan/motor select), Motor Control (MIT/POS_VEL/
  VEL/FORCE_POS, enable/disable, single+continuous send, set-zero/clear-error/store, motor-
  type), Registers (read/write incl. ms timeout, hex IDs, mode/baud dropdowns).
- lib/control.ts REST client; store gains mode/currentMotor/controlMotors/registerTable.
- Default canvas is now a full Studio layout; control widgets show an inert hint in monitor
  mode. Commands feed the same live plots/table/cards via the shared store.
- CLI: 'damiao gui' now launches the unified Studio in control mode (legacy web_gui kept).
- types: ServerStatus gains mode/connected.

Builds clean; backend + tests green. UI pending visual check (local browser wedged).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
ControlService passed UI/default names like 'DM4310' straight to DaMiaoController, whose
core preset table only knows '4310' -> scan silently added no motors. Map extended->core
(_core_motor_type) in scan + set_motor_type.

Hardware-validated on linearbot can_arm_l motor 7 through the full ControlService path:
connect -> scan (motors 1-7) -> clear_error -> enable (ENABLED) -> MIT command MOVES the
motor (-1.2255 -> -1.154 toward target) -> return -> registers read -> disable; cmd+fb
signals land in the shared store. (Tip surfaced: latched status 0x3 needs clear-error
before enable.)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Remove now-unused web_gui import from cli/commands.py and ruff-format the new monitor/
modules + edited cli/tests files so the Ruff workflow (ruff check . / ruff format --check .)
passes. No behavior change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@jia-xie
jia-xie merged commit 3427350 into main Jun 16, 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.

1 participant