Skip to content

W6: the Raspberry Pi on the cart, with Wi-Fi settings in the web app - #14

Merged
Tecnologic merged 5 commits into
mainfrom
claude/unimoc-debug-gui-lrgfap
Oct 25, 2026
Merged

Tecnologic merged 5 commits into
mainfrom
claude/unimoc-debug-gui-lrgfap

Conversation

@Tecnologic

@Tecnologic Tecnologic commented Oct 25, 2026 •

Copy link
Copy Markdown
Owner

Phase W6 of docs/web_plan.md, with the hardware decided on 2026-10-25: the MCP2518FD CAN FD HAT. For Wi-Fi, the Pi joins a known network when it is in range, else it opens its own access point. On request, the Wi-Fi can be set in the web app.

Changes

  • scripts/pi-setup.sh (Raspberry Pi OS 64-bit, Bookworm or later; run as root):

    • SPI and dtoverlay=mcp251xfd,<spi>,oscillator=<Hz>,interrupt=<GPIO> in config.txt, in a marked block that a second run replaces;
    • can0 by systemd-networkd: 1 Mbit/s, CAN FD without bit rate switch, restart after a bus-off;
    • cypilot in /opt/cypilot for a system user cypilot; the DSDL and the register description file are fetched during setup, because the cart is offline;
    • cypilot.service: cypilot-ctl -i socketcan:can0 serve --host 0.0.0.0 --port 80, Restart=always, CAP_NET_BIND_SERVICE. The PIN, the runs directory, the cache and CYPILOT_WIFI=1 are in /etc/cypilot/cypilot.env;
    • Wi-Fi: a NetworkManager hotspot cypilot-ap that does not autoconnect. cypilot-wifi-fallback.service opens it when no known Wi-Fi has connected within 45 s of the boot (--no-ap for none);
    • a polkit rule lets the user cypilot use NetworkManager, for the Wi-Fi view;
    • a second run keeps the PIN, and keeps the access point key as the web app last set it;
    • the host name for http://<name>.local (avahi);
    • --dry-run --root DIR writes the files under DIR and prints the commands.
  • Wi-Fi in the web app (src/cypilot/wifi.py, serve --wifi or $CYPILOT_WIFI=1):

    • the current network and address, scan, the known networks;
    • join a network (saved as known), forget one;
    • the access point's SSID and key, and open it now.

    nmcli runs with argument lists, never a shell. Reading is open; changes need the control lease. If joining fails while the access point was up, the access point is opened again, so the cart is never left without a network. The view appears only when the server manages the Wi-Fi.

  • serve --pin defaults to $CYPILOT_PIN.

  • scripts/load_test.py: demo devices or a real bus, a recording run, and browsers showing every subject of every node. Locally, 4 demo devices and 2 browsers: about 30 % of one core, status median 47 ms, 155 frames/s recorded.

  • Scope (from CI): in a multi-node capture, once the trigger node has its capture, the other nodes get 2 s plus twice the capture's length. One that missed the trigger event then fails with "node N did not trigger on the trigger event of node M". Before, it waited the whole capture timeout (30 s) without a message.

  • docs/pi.md covers hardware, setup, use, Wi-Fi in the web app and the load test. README, AGENTS and web_plan.md are updated; the plan marks the day on the bench as open.

Tests

  • tests/unit/test_pi_setup.py, the dry run:
    • the overlay, can0, the service and the env file;
    • the polkit rule, the access point commands and the DSDL prefetch;
    • a second run keeps the PIN and the key;
    • bad options are refused.
  • tests/unit/test_wifi.py with a fake nmcli (tests/fake_nmcli.py; one call at a time, the state written atomically): the terse parser, status, scan, known networks, join with the access point fallback, forget, the access point, and the API with the control lease.
  • tests/browser/test_wifi_ui.py: join, forget and change the access point in Chromium; no Wi-Fi view without --wifi.
  • tests/unit/test_scope.py: a follower that misses the trigger event fails within a few seconds.
  • CI robustness, test code only:
    • test_web_sim uses the 1 s lease and checks the second enable;
    • the scope browser test reports the state and the message on a failure.
  • shellcheck is clean.
  • Locally: ruff, format and mypy clean; pytest 132 passed, 1 skipped. CI is green on all jobs.

Not testable here: the W6 acceptance test, a day on the cart bench with the drives. Nothing in this PR has run on a real Pi with NetworkManager yet.

🤖 Generated with Claude Code

https://claude.ai/code/session_01WEh6hu8r51ZDjgfECmELvM

- scripts/pi-setup.sh for Raspberry Pi OS 64-bit with the MCP2518FD CAN FD
  HAT: SPI and the mcp251xfd overlay (crystal, interrupt and chip select as
  options), can0 at 1 Mbit/s CAN FD without bit rate switch by
  systemd-networkd, cypilot in /opt/cypilot for a user cypilot, the DSDL
  and the register description file fetched while online, cypilot.service
  on port 80 with a PIN, runs on the SSD, the host name for mDNS, and
  Wi-Fi: a known network, else the Pi's own access point after 45 s.
  --dry-run writes the files under --root and prints the commands.
- serve takes the PIN from $CYPILOT_PIN.
- scripts/load_test.py: drives (demo or a real bus), a recording run and
  browsers on the plots of every node; reports CPU, status latency, the
  run's frames and size and the browsers' events.
- docs/pi.md, README, AGENTS, web_plan (the day on the bench is open).
- Tests: the dry run (files, commands, a second run keeps the PIN, bad
  options); shellcheck clean.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WEh6hu8r51ZDjgfECmELvM
- cypilot/wifi.py: the Wi-Fi over nmcli (argument lists, no shell): the
  state and address, scan, the known networks, join (saved as known),
  forget, the access point's SSID and key, open it now. A failed join
  reopens the access point when it was up, so the cart keeps a network.
- API /api/wifi (state, scan for everybody; join, forget, access point
  for the operator), serve --wifi / CYPILOT_WIFI=1.
- Wi-Fi view, shown only when the server manages the Wi-Fi.
- pi-setup.sh: a polkit rule lets the user cypilot use NetworkManager,
  CYPILOT_WIFI=1, and a second run keeps the access point key as the web
  app set it.
- Tests with a fake nmcli (tests/fake_nmcli.py): the parser, the wrapper
  with the fallback, the API with the control lease, the view in Chromium.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WEh6hu8r51ZDjgfECmELvM
@Tecnologic Tecnologic changed the title W6: the Raspberry Pi on the cart W6: the Raspberry Pi on the cart, with Wi-Fi settings in the web app Oct 25, 2026
On CI (Python 3.13) the drive did not show as engaged after the second
enable; the test ignored that enable's answer. A runner that stalls the
loop for the 0.5 s lease trips the watchdog, which is right, but not
what this test is about: it now runs with the 1 s lease, checks the
answer and names the control, stop and log events when it fails.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WEh6hu8r51ZDjgfECmELvM
…omically

The web app runs several nmcli calls at once (a reload on the wifi event
next to the view's own). The fake read its state and wrote it back
unlocked, so a read that started before a delete undid it: the forgotten
network came back (CI, and 1 of 3 runs of a local race check). It now
locks the state file and replaces it in one step.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WEh6hu8r51ZDjgfECmELvM
The two-node scope test timed out on CI (Python 3.13) without a message:
a follower that never sees the trigger node's event waited for the whole
capture timeout (30 s), as long as the test. Once the trigger node has its
capture, the others now get 2 s plus twice the capture's length and then
fail with 'node N did not trigger on the trigger event of node M'. The
browser test reports the scope state and the message when it fails.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WEh6hu8r51ZDjgfECmELvM
@Tecnologic
Tecnologic merged commit 02db2b2 into main Oct 25, 2026
10 checks 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.

2 participants