Add lcmflow: animated LCM packet highway TUI#2456
Conversation
Each topic is a lane; every packet drives across it as a vehicle. Packet size picks the vehicle class (tiny fast dots for cmd_vel/tf, long slow trucks for images and point clouds); bursts faster than a lane can fit coalesce into one vehicle with a xN count badge. - dimos/utils/cli/lcmflow/: model (size classes, lanes, coalescing), Textual TUI (line-API renderer, pause/sort/scroll, web mode) - new entry points: lcmflow + dimos lcmflow - lcmspy: remove stray debug print on new-topic creation - docs: cli.md section, AGENTS.md CLI table
Greptile SummaryThis PR introduces
Confidence Score: 4/5Safe to merge; all issues are cosmetic or affect optional subcommands only. The model and rendering logic are correct and well-tested. The three findings are all non-blocking: a badge rendering off-by-one that silently suppresses 3-char count badges (×10–×999) on exactly 4-cell vehicles, the The badge placement condition and web-server spawn command in Important Files Changed
Sequence DiagramsequenceDiagram
participant LCM as LCM Network
participant PS as PacketSpy (LCM callback thread)
participant HW as Highway
participant HV as HighwayView (Textual @ 20 fps)
participant HD as HeaderBar (@ 0.5 Hz)
LCM->>PS: msg(channel, data)
PS->>PS: super().msg() — update Topic stats
PS->>PS: pending.append((channel, len(data)))
loop Every 50 ms
HV->>HW: tick(road_width)
HW->>PS: drain() — always, even when paused
alt not paused
HW->>HW: "_clock += dt"
loop each packet
HW->>HW: lane.spawn(nbytes, clock)
end
HW->>HW: lane.prune(clock, road_width)
else paused
HW->>HW: discard drained packets
end
HV->>HV: _sorted_lanes() snapshot
HV->>HV: refresh() — render_line per visible row
end
loop Every 500 ms
HD->>PS: freq / kbps_hr / total_traffic_hr
HD->>HD: update header markup
end
Reviews (1): Last reviewed commit: "Add lcmflow: animated LCM packet highway..." | Re-trigger Greptile |
| badge = f"×{min(vehicle.count, 999)}" # noqa: RUF001 — intentional UI glyph | ||
| badge_style = Style(color=theme.BACKGROUND, bgcolor=lane.color, bold=True) | ||
| start = tail + max(0, (length - 1 - len(badge)) // 2) | ||
| if start >= 0 and start + len(badge) < head: |
There was a problem hiding this comment.
Off-by-one in badge placement guard:
start + len(badge) < head excludes the case where the badge ends exactly one cell before the head character. For a 4-cell vehicle (indices [tail, tail+1, tail+2, head]) with a 3-char badge (counts 10–999), start + 3 == head which fails the strict-less-than check, so the badge is silently dropped even though it fits perfectly without touching the head cell. The condition should be <= head.
| if start >= 0 and start + len(badge) < head: | |
| if start >= 0 and start + len(badge) <= head: |
|
|
||
| from textual_serve.server import Server # type: ignore[import-not-found] | ||
|
|
||
| server = Server(f"python {os.path.abspath(__file__)}") |
There was a problem hiding this comment.
The
web subcommand spawns textual-serve with a hard-coded python executable. In virtual environments where python is not on PATH (only python3, or a venv-local python3.12), lcmflow web will fail at runtime. This copies the same pattern from run_lcmspy.py, but since this PR introduces the feature, using sys.executable here would be the right fix for both.
| server = Server(f"python {os.path.abspath(__file__)}") | |
| server = Server(f"{sys.executable} {os.path.abspath(__file__)}") |
| digest = sum(channel.encode()) | ||
| return FALLBACK_PALETTE[digest % len(FALLBACK_PALETTE)] |
There was a problem hiding this comment.
The fallback hash
sum(channel.encode()) sums byte values, so any two channel names that are anagrams of each other (e.g. /ab and /ba) hash to the same bucket and get the same color. A slightly stronger but still stable alternative uses position-weighted byte values to distinguish permutations.
| digest = sum(channel.encode()) | |
| return FALLBACK_PALETTE[digest % len(FALLBACK_PALETTE)] | |
| digest = sum(b * (i + 1) for i, b in enumerate(channel.encode())) | |
| return FALLBACK_PALETTE[digest % len(FALLBACK_PALETTE)] |
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
|
is this a real PR? I wouldn't merge this,or put in experimental |
|
No just for fun / maybe experimental |
What
lcmflow— a real-time animated visualization of LCM traffic, sibling tolcmspy. Each topic is a lane on a highway; every packet drives across it as a vehicle:cmd_vel,tf, ≤256 B) are fast 1-cell dots; raw images and point clouds (>512 KiB) are 11-cell trucks at 0.65× speed. Five log-scale bands in between.×Ncount badge (e.g. a 14 Hz raw image stream shows as×15convoys). Lossless — stats always reflect true packet counts/bytes.Image=yellow,PointCloud2=purple,OccupancyGrid=blue,TFMessage=green, …), stable hash fallback for unknown types.spacepause,scycle sort (arrival/traffic/name),qquit.lcmflow webserves the TUI in a browser via textual-serve (same as lcmspy).How
dimos/utils/cli/lcmflow/lcmflow.py: renderer-agnostic model —PacketSpy(LCMSpy)collector with a thread-safe packet queue,Lane/Vehiclephysics with on-ramp coalescing. Reuses lcmspy'sTopicsliding-window stats.dimos/utils/cli/lcmflow/run_lcmflow.py: Textual app using the line API (render_line/Strip) at 20 fps.lcmflowconsole script +dimos lcmflowsubcommand; docs indocs/usage/cli.mdand AGENTS.md.print(self.config)debug line inlcmspy.pythat fired on every new topic and corrupted TUI output.Testing
test_lcmflow.py): size classing, coalescing, class upgrade, convoy stretch, pruning, drain, pause semantics. All lcmspy tests still pass.dimos --replay run unitree-go2: 7 lanes at ~42 MiB/s, verified in a real xterm and via headless Textual screenshots.