Skip to content

ros2-poc: ROS 2 bridge proof of concept (LAB-303) - #252

Closed
mbreiser wants to merge 10 commits into
mainfrom
claude/ecstatic-noether-art3qx
Closed

mbreiser wants to merge 10 commits into
mainfrom
claude/ecstatic-noether-art3qx

Conversation

@mbreiser

@mbreiser mbreiser commented Oct 14, 2026 •

Copy link
Copy Markdown
Contributor

Summary

The bounded ROS 2 spike from LAB-303 (decision in flight: ROS 2 graph vs. connector contract vs. Zenoh wire). Everything lives under ros2-poc/ as its own pixi workspace (RoboStack Jazzy + conda-forge, osx-arm64 / win-64 / linux-64); the root pixi.toml and fictrac-bridge/ are untouched, so bridge 3.4 stays installed side by side.

  • Middleware: Zenoh router + rmw_zenoh_cpp, loopback-only configs (stock rmw_zenoh files with only the endpoint lines changed — the stock router binds tcp/[::]:7447 and fails on a host without IPv6).
  • Adapters, no custom message types (std_msgs/String carrying JSON): fictrac_udp_adapter (FicTrac UDP → /fictrac/raw, FicTrac untouched), kinefly_udp_adapter (KF/1 from mbreiser/Kinefly-MacOS → /kinefly/raw, protocol discriminated before decoding), bias_node (BIAS HTTP command port ⇄ /bias/cmd, /bias/reply, /bias/status; timeout = unknown outcome + get-status reconcile; video file reported as an artifact).
  • bridge_node imports fictrac-bridge/bridge.py by path and reuses Hub, LogWriter, Pipeline, make_dispatcher unmodified. The unmodified Studio protocol on ws://localhost:8765, the same behavior_v2 log. Additions are all visible in the log: connector_status (frames, adapter→bridge latency p50/p99), source_lost / source_resumed watchdog, ["kf",…] rows, BIAS connector events, plus an /arena/frame ROS mirror for introspection.
  • pixi run rig supervises router + nodes (one command, port pre-flight with a one-line culprit per port); kinefly_sim.py, bias_sim.py, bias_cmd.py for hardware-free runs.
  • Foxglove dashboards (--foxglove): nodes/viz_node.py republishes the JSON streams as ros-base typed messages (std_msgs/Float64 signals, diagnostic_msgs/DiagnosticArray health rows, visualization_msgs/MarkerArray ball + fly + arena + pattern feature) and the stock foxglove_bridge serves them on ws://localhost:8766; two layouts in foxglove/ (Arena + FicTrac, Arena + BIAS).
  • Log-format sizing (sizing/, README "Log format"): NDJSON vs MCAP measured on real logs — per-message overhead dominates; with the per-message index off, arena echoes batched 1 s and zstd-19, MCAP reaches parity with today's gzip NDJSON; 300 Hz controller frame records every-frame vs changes-only; the two-channel plan (frames batched + index changes per message) and 25 MiB file rolling.
  • Acceptance harness pixi run test (tests/test_ros2_poc.py): whole graph + fictrac_sim.py --model fly --kp 2 + tests/studio_emulator.py (the Studio runner's exact bridge conversation, 3 constant-bias epochs), FicTrac adapter SIGKILLed mid-run and restarted, BIAS commands mid-run; then scripts/closed-loop-report.py, bridge.py --convert round trip, and the JS readers (js/runlog-format.js, js/runlog-replay.js) over the exported log. 29 / 29 on Linux and on the Mac.

Criterion-by-criterion evidence, the further automated checks (soak, zero loss under load, router kill/restart, student-mistake messages), the Mac results, the Foxglove and log-format sections, and the Windows plan (WINDOWS-TEST.md) are in ros2-poc/README.md.

Tier

No web-tool code is touched (new ros2-poc/ directory only), so nothing here goes to Next or Production — treat like docs/skills-only. Confirm before merging.

Release notes

none

Test plan

  • cd ros2-poc && pixi install (fresh, no manual steps; 3 platforms in the lock file)
  • pixi run router, pixi run talker / listener under rmw_zenoh_cpp
  • pixi run fictrac-adapter + root fictrac_sim.py → ros2 topic hz /fictrac/raw = 50.0 Hz
  • pixi run rig -- --sim --kinefly --bias --bias-sim, then SIGINT and SIGTERM: all children stop, no tracebacks, no abort
  • pixi run test → 29 / 29 (rerun after the review fixes and after the Foxglove additions)
  • soak: 78 / 78 runs over 60 min, flat latency (p50 1.6–1.8 ms, p99 3.2–4.6 ms); RSS creep noted as a watch item
  • zero loss: sim 200 Hz × 4000 + Kinefly 500 Hz × 10 000 → every frame logged, no sequence gaps
  • Zenoh router killed and restarted mid-run → no data gap, log intact
  • second pixi run rig / rig while bridge 3.4 holds :8765 → pre-flight names the ports, exit 2
  • root pixi run test (unchanged code) passes; pixi run format-check warnings are pre-existing files on main
  • On the Mac (2026-10-14, MacBook Pro osx-arm64, macOS 27.0.1): pixi install 25 s fresh (1.0 GB env, no manual step); talker/listener every message heard under rmw_zenoh_cpp; pixi run test 29 / 29 in 27 s, latency p50 0.89 / p99 1.43 ms
  • On the Mac: unmodified Arena Studio v0.94 from GitHub Pages against pixi run rig -- --sim --sim-args "--model fly --kp 2 --seed 1" with a G6 2×10 controller over Web Serial; no Chrome local-network-access prompt; two Mode-3 constant-bias runs auto-committed the normal way to reiserlab/cshl-2026-course runlogs/bench00/, each byte-identical to the bridge node's file; closed-loop-report.py idx mismatch 0.00 % in all 6 epochs; runlog-check.py OK
  • On the Mac: real BIAS Mac port on :5010 via bias-node + bias-cmd — all commands ok, reply shape identical to bias_sim.py; relative filename resolves against BIAS's cwd (node now warns); the Mac port writes <stem>_cam_0_…_v001/movie.mjpg rather than the <stem>.jpg it reports. Real Kinefly fork --udp 127.0.0.1:60001: 9600 / 9600 datagrams, 0 rejected, 9487 ["kf",…] rows in the committed log
  • Foxglove (container, no GUI): pixi run rig -- --sim --bias --bias-sim --foxglove → 22 typed channels advertised on ws://127.0.0.1:8766 (CDR), binary frames on subscription, /diagnostics rows at 50/100 Hz, 6 markers, BIAS state at 1 Hz; both layouts parse
  • Foxglove on the Mac: import foxglove/arena-fictrac.json and arena-bias.json, check the 3D scene, plots and health lights against pixi run rig -- --sim --sim-args "--model fly --kp 2 --seed 1" --bias --bias-sim --foxglove
  • Windows execution (win-64 resolves and locks only) — rig-PC session scripted in ros2-poc/WINDOWS-TEST.md

🤖 Generated with Claude Code

https://claude.ai/code/session_01KHyggowbCA5ucBg71bU26V

cshl-2026 and others added 5 commits October 14, 2026 19:11
…ters, bridge node reusing bridge.py

Separate pixi workspace (RoboStack Jazzy + conda-forge; osx-arm64/win-64/linux-64) under
ros2-poc/; the root pixi.toml and fictrac-bridge/ are untouched so bridge 3.4 stays side by side.

- Zenoh router + rmw_zenoh_cpp with loopback-only configs (stock files, endpoint lines only)
- fictrac_udp_adapter: FicTrac UDP → /fictrac/raw (std_msgs/String JSON), FicTrac untouched
- kinefly_udp_adapter + kinefly_sim: KF/1 (mbreiser/Kinefly-MacOS) → /kinefly/raw
- bias_node + bias_sim + bias_cmd: BIAS HTTP command port ⇄ /bias/cmd,/bias/reply,/bias/status;
  timeout = unknown outcome + reconcile; video file reported as an artifact
- bridge_node: bridge.py's Hub/LogWriter/Pipeline/make_dispatcher on ROS input, same WebSocket
  schema on ws://localhost:8765, same behavior_v2 log; adds connector_status latency lines,
  source_lost/resumed watchdog, ["kf",…] rows, BIAS connector events, /arena/frame mirror
- rig.py: one `pixi run rig` command supervising router + nodes
- tests: studio_emulator (the Studio runner's exact bridge conversation), check_runlog_readers
  (JS readers accept the log), test_ros2_poc (29 checks incl. adapter SIGKILL mid-run,
  closed-loop report rejection/idx checks, convert round trip): 29/29 on Linux

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KHyggowbCA5ucBg71bU26V
…outer restart, launcher pre-flight)

- bridge_node: RosLogWriter hooks (on_open/on_close) replace the message re-parsing wrapper;
  source watchdog scoped per log file; ordered ROS shutdown (join the spin thread before
  destroy_node — removes "terminate called without an active exception" on SIGTERM)
- bias_node: worker exception guard (a malformed BIAS body no longer kills the node),
  get-status polls coalesced to one pending
- rig.py: port pre-flight with a one-line culprit per port (SO_REUSEADDR probe), router death
  stops the rig, Zenoh config paths set explicitly (Windows shells leave $PIXI_PROJECT_ROOT literal)
- nodes/_common.py: shared envelope, QoS, spin/shutdown boilerplate, wait_port
- test harness: adapter kill anchored on the runner's fictracApply line in the log; export guard
- README: soak (12 runs, flat RSS, p99 ≈ 3.5 ms), zero loss at 200 Hz + 500 Hz, router
  kill/restart with no data gap, student-mistake messages

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KHyggowbCA5ucBg71bU26V
… TerminateProcess)

Python cannot send SIGINT to another process on Windows (send_signal raises ValueError), so
Ctrl-C on the rig PC would have looked like a launcher crash. popen_group/signal_group in
nodes/_common.py give every child its own process group on both platforms and map
SIGINT → CTRL_BREAK_EVENT, SIGTERM/SIGKILL → TerminateProcess on Windows; rig.py and the
test harness use them (and catch ValueError). Linux path unchanged (setsid + killpg).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KHyggowbCA5ucBg71bU26V
…s a watch item)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KHyggowbCA5ucBg71bU26V
Mac run of the items the Linux container could not cover: pixi install 25 s
fresh, talker/listener, 29/29 acceptance, the unmodified Pages Studio against
`pixi run rig -- --sim` with a G6 controller (two Mode-3 constant-bias runs
auto-committed to the course repo, byte-identical to the bridge node's file,
idx mismatch 0.00 %, runlog-check OK), the real BIAS Mac port and the real
Kinefly fork (9600/9600 datagrams, 0 rejected, kf rows in the committed log).

bias_node: the real Mac port resolves a relative `filename` against its own
cwd and start-capture then succeeds without capturing; the reply now carries
a warning for relative names.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
mbreiser and others added 2 commits October 14, 2026 17:55
WINDOWS-TEST.md scripts the one open acceptance item (install timing, the
$PIXI_PROJECT_ROOT expansion check, talker/listener, the 29-check harness,
rig + unmodified Studio + controller, real BIAS over HTTP with an absolute
video path and the unknown-outcome path, Ctrl-C cleanup, student mistakes)
and lists what to send back.

Windows' signal module has no SIGKILL, so rig.py's stop escalation, the
harness's mid-run adapter kill and _common.signal_group would have raised
AttributeError there; they now share _common.SIGKILL (getattr fallback 9).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…tudy + two-channel log plan

- nodes/viz_node.py: typed republisher so Foxglove plots natively with NO custom message types —
  std_msgs/Float64 signals (/viz/fictrac|arena|kinefly|bias/*), Bool/String states,
  diagnostic_msgs/DiagnosticArray on /diagnostics (OK/STALE/WARN/ERROR per source with rate,
  seq, latency, last error + time), visualization_msgs/MarkerArray on /viz/markers (ball rotated
  by FicTrac lab-frame orientation with markings, fixed fly, arena cylinder, pattern feature at
  the mapped azimuth, readout)
- foxglove/arena-fictrac.json, foxglove/arena-bias.json: layouts (3D, closed-loop plot,
  kinematics, index, health lights + gauges, BIAS state timeline, latest reply, last error, log)
- pixi: ros-jazzy-foxglove-bridge (ws://127.0.0.1:8766; 8765 stays the Studio bridge), tasks
  viz / foxglove / size-study; rig.py --foxglove starts both (+ port pre-flight on 8766)
- sizing/compare.py + sizing/frames300.py: NDJSON vs MCAP size study (per-message overhead,
  index, batching, zstd level; 300 Hz controller frame records every-frame vs changes-only)
- README: Foxglove section; "Log format" section with the measured tables, per-hour numbers,
  the two-channel plan (frames batched 1 s + index changes per message) and 25 MiB file rolling
- test harness: the no-custom-messages check now allows the ros-base interface packages

Verified in the container: 22 typed channels advertised (CDR) on :8766, binary frames on
subscription, 6 markers, diagnostics 50/100 Hz rows, BIAS state 1 Hz; acceptance 29/29.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KHyggowbCA5ucBg71bU26V
mbreiser added a commit that referenced this pull request Oct 16, 2026
…252's dashboards

Opened runs 5.2 and the soak in Foxglove web 3.4 as local files: arena-fictrac,
arena-bias and the PR #254 layout all draw; one benign profile-mismatch warning.
Seven dashboard suggestions from the data (viz_node frame drops, unwrapped display
angle, plot scaling, clipped indicators, /rosout not recorded, grating feature bar).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rer layouts

From checking both dashboards in Foxglove (web 3.4) on ww18 hardware runs replayed at 1x
through viz_node (data branch data/ros2-poc-ww18-2026-10-15):

- /arena/frame subscription: RELIABLE_QOS (depth 200) like the other inputs. With depth
  10 viz_node dropped 15 of 9,969 frames (step 5.2), 17 of 58,918 (soak), 8 of 12,226 (5c);
  now 9,969/9,969, 58,918/58,918 and 12,226/12,226.
- /viz/arena/display_deg (DisplayAngle): the display's unwrapped rotation from the wrapped
  index (sum of index steps taken the short way round the modulus x frame_dir x
  deg_per_frame). The modulus is --frames N, else learned per epoch (largest index + 1). A
  20-frame grating kept azimuth_deg inside one 36-degree period while the fly turned
  thousands of degrees; on the ww18 closed-loop runs display - turn - bias is now constant
  within +/-0.9 degrees (half a frame) in every epoch. The 3D feature bar sits at
  wrap180(display_deg): unchanged for a pattern spanning the arena, turning with a grating.
- /viz/fictrac/fwd_dps, side_dps: rates of FicTrac's integrated cols 20/21 in deg/s (per
  frame-counter step, so a dropped frame does not double them), the units of speed_dps.
- arena-fictrac: closed loop = fly turn vs display rotation; bias angle in its own plot;
  kinematics = speed / forward / side in deg/s; short indicator labels + panel titles.
- arena-bias: camera frame count and displayed index on separate plots; BIAS / Camera /
  Video indicators with short labels; ROS log panels titled live-only (bags do not record
  /rosout).
- tests/test_viz_node.py (pixi run test-viz): 11 checks of the math, no ROS graph.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
mbreiser added a commit that referenced this pull request Oct 16, 2026
Replayed all five bags at 1x through PR #258's viz_node: display_deg, fwd_dps and side_dps
are new, and no frames are dropped (every frame's index + heading at its exact original
time). Layouts are PR #258's fixed arena-fictrac + arena-bias; PR #252's first versions
and their viz files stay in history at 659c234. README + tools updated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
mbreiser and others added 2 commits October 16, 2026 11:11
…ffline

In Foxglove (1316 px window) the BIAS light still clipped "reachable" and the gauge
title "Camera fps": the three lights now stack in a 60 % column beside the gauge, and
the BIAS light reads online / offline.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ros2-poc viz: display rotation, deg/s rates, no dropped frames + clearer layouts
mbreiser added a commit that referenced this pull request Oct 18, 2026
…viz port

Studio / runner / client
- connectorCommand waits timeout_s + CONNECTOR_TIMEOUT_MARGIN_MS (default 15 s): the
  browser outlasts the device node's own deadline, so a reply at the deadline is an
  outcome, not an 'unknown'. 2P protocol's waitIdle already declares timeout_s.
- plugin-registry: ScanImagePlugin `set.value` is text (the allowlist holds numbers,
  booleans and paths); the node coerces "640" / "true" to the JSON scalar.
- protocol-yaml-v3: `variables:` written as a list of anchored scalars (`- &name v`,
  the 2P protocols) kept their identity wrong — `.anchor` was read off a primitive
  (String.prototype.anchor), hence the 2026-10-16 "Variable "function anchor() …"
  is defined more than once" warning. extractVariables/_findVariablePair handle
  both shapes; v3 round-trip suite 39 (823/823).
- runner test: connector plugin steps make a run bridge-dependent (loss in the ITI
  before a camera step → BRIDGE_LOST; without the map → continues). 547/547.
- footer stamp; release notes v0.97 extended; CLAUDE.md: ROS-era rules.

ros2-poc
- mcap_index.reindex replaces the bag atomically (os.replace), never unlink-then-rename.
- recorder: ONE constant storage sidecar per log dir (not one hidden file per run);
  optional max_bag_bytes → bridge_node --max-bag-mb (off by default: bags are archived
  on a share, not pushed to GitHub).
- scanimage_node: coerce_scalar for `set` values.
- kf_row lives once in nodes/_rows.py (no ROS imports), used by the node and the
  converter — `converted == live` depends on one encoder.
- ported from #252: nodes/viz_node.py, foxglove/*.json, tests/test_viz_node.py,
  sizing/; rig.py --foxglove also starts viz_node (+ --viz-args); pixi tasks viz,
  test-viz, fox-json (nodes/foxglove_json.py salvaged from the data branch).
- repo hygiene: colcon build logs (interfaces/log) and the one-off win-64 channel
  payload removed from the tree (+ .gitignore); the typed-prebuilt env still reads
  the pinned commit URL.
- stamp.py: one stamping path again (stamp_tree takes a skip set); tiers 109/109.

Harness: ros2-poc `pixi run test` all PASS (default env), test-viz PASS.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@mbreiser

Copy link
Copy Markdown
Contributor Author

Closing as folded into #254 (LAB-303). Everything here that #254 lacked is now on that branch (commit f7ae2eb): nodes/viz_node.py, foxglove/arena-bias.json + arena-fictrac.json, tests/test_viz_node.py (pixi run test-viz), sizing/, and rig.py --foxglove now starts the typed republisher with foxglove_bridge. The acceptance harness, Windows results and Mac results are carried in #254's ros2-poc/README.md. The ROS 2 graph was adopted on 2026-10-16; #254 ships as constituent 3 of candidate 2026-10-20.

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