Skip to content

feat(installer): MDB-first install with standalone DBC flash#5

Open
teal-bauer wants to merge 44 commits into
mainfrom
feat/mdb-first-standalone-dbc-flash
Open

feat(installer): MDB-first install with standalone DBC flash#5
teal-bauer wants to merge 44 commits into
mainfrom
feat/mdb-first-standalone-dbc-flash

Conversation

@teal-bauer

Copy link
Copy Markdown
Member

Summary

Reworks the installer into two stages so the host does all MDB-side work in one pass, then the scooter flashes the DBC standalone after a single cable swap, with no host reconnect on the happy path. Cuts the four cable swaps to two and takes the blind-RNDIS-reconnect path (the most field-broken part) off the happy flow.

  • Two-stage flow: a dashboardPrep step runs Bluetooth pairing + keycard enrollment (both MDB-side hardware) while the DBC image uploads in the background, then a single dbcSwapAndFlash. reconnect is demoted to a failure-only path.
  • Progress record in /data/installer/state.json on the MDB drives precise, machine-independent resume; the scooter-written trampoline-status stays the Stage-2 verdict.
  • Trampoline hardening: debounced laptop-disconnect gate (keys off the udc state); DBC identity read + idempotent skip when already at target; pre-write target sanity gate; retry-until-complete with a terminal short-circuit on the SDP/BootROM hard-brick.
  • DBC tiles copied over UMS (grow the data partition + filesystem from the MDB) so the DBC boots once already complete; the proven onboot HTTP push is kept as a fallback if grow-over-UMS fails.
  • Success is now "the dashboard lights up"; failure is a binary hazards/red cue. Per-phase progress LEDs removed. A walk-away finish screen replaces the LED legend.
  • A sh -n render test guards the trampoline template against shell-syntax regressions.

Reviewed commit-by-commit (spec + quality) and as a whole. flutter analyze clean (no new issues); all new unit tests pass. The 3 failing download_service tests are pre-existing offline-network failures in untouched code.

Test plan (on-hardware, cannot run in CI)

  • Full stock -> Librescoot walk-away run from the Linux build: BT + keycard complete in Stage 1 before any cable swap, one swap to the DBC, walk away, dashboard comes up complete with map tiles, no laptop reconnect.
  • V3 grow-over-UMS: confirm the DBC /data is full-size and holds tiles after flash; verify the data partition number (mmcblk3p4 -> UMS sdX4) and that the MDB carries parted/sfdisk/resize2fs. If grow-over-UMS is unreliable, confirm the onboot HTTP fallback still installs tiles.
  • Debounced disconnect gate: a clean unplug proceeds; a deliberate USB blip does not flip to host mode. Tune the ~250ms threshold.
  • Pre-write sanity gate: a real DBC passes; no-DBC-plugged fails clearly with hazards/red and no destructive write.
  • Failure path + resume: induce a failure, confirm hazards/red, plug the laptop back in -> state.json-driven resume surfaces the error and offers retry; an SDP (15a2:) condition short-circuits with the imx_usb_loader message.
  • Idempotent skip + re-flash; Bluetooth bond + keycard persistence across the install.

Notes / deferred

  • DBC identity skip is exact-version + librescoot-ID only; on the stable channel VERSION_ID (1.0.1) will not equal the release tag, so it simply flashes (safe by default). Wiring the real DBC image version is a follow-up.
  • Out of scope: CLI parity, meta-librescoot image changes, MDB self-flash. Minor cleanup: orphaned LED-legend l10n strings remain in the ARBs.

Reorder install phases into two stages: a Stage-1 dashboardPrep step that
runs Bluetooth pairing and keycard enrollment in the foreground while the
DBC image/tiles upload in the background, then a standalone DBC swap-and-flash
step. The Begin button unlocks once BT and keycard are done-or-skipped and the
upload has completed.

- Replace dbcPrep/dbcFlash phases with dashboardPrep and dbcSwapAndFlash;
  group BT+keycard under the new mdbPrep major step; make reconnect a
  hidden failure-resume phase.
- Extract BT/keycard builders into content methods reused by both the
  standalone phases and the dashboardPrep screen (no duplicated UI).
- Drop the host-side DBC-flash MDB reconnect poll on the happy path; the
  user confirms via the dashboard-lit-up button, failures route to verify.
- Persist resume checkpoints (mdb-booted, dbc-staged, bt-paired,
  keycard-enrolled, trampoline-armed) to /data/installer/state.json.
- Add l10n keys for the new phase/major-step labels.
…nup paths

The hazards tails, signal_error_stop and stop-error-signals.sh deactivated
the four blinker PWM channels. The imx_pwm_led active flag gates output, so
a deactivated channel stays dark even while vehicle-service drives it, until
vehicle-service re-inits. stop-error-signals.sh runs on reconnect/resume
with nothing to re-activate afterwards, so blinkers could stay dead until
the finish reboot. Set duty 0 but leave the channels active everywhere.

The success path here never deactivates the blinkers (the progress bar that
did was dropped), so this is the error/resume-path counterpart of the main
fade_progress_off / stop-error-signals fixes.
Derive the offered map regions from the published OSM and Valhalla tile
assets (intersection of both, so a region only shows when display and
routing tiles both exist) instead of a hardcoded list. This surfaces
Belgium, Netherlands, Luxembourg, Ile-de-France and Italy (northwest),
which already had tiles but were never selectable.

Group the region dropdown by country, and switch IP preselection from
ip-api.com (HTTP, non-commercial only) to ipwho.is (HTTPS) keyed on the
ISO 3166-2 region_code. Auto-detect covers the German states plus the
neighbour regions.
The resolveRelease/buildDownloadQueue tests still mocked the old direct
GitHub /releases endpoint and the in-resolveRelease stable->testing
fallback, both of which were replaced by the per-channel latest.json
manifest. Rewrite them against the current behaviour (manifest Map keyed
by channel, fallback moved to channel selection) and clear the shared
on-disk manifest cache between tests for isolation.
Releases now ship librescoot-unu-mdb-minimal-* / librescoot-unu-dbc-minimal-*
sdimg assets alongside the full images. The unu-mdb-/unu-dbc- substring
match classified these as full firmware too, so the installer could queue
two "MDB firmware" downloads and flash the bootstrap image onto a scooter.
The step-1 wait required /sys/class/udc/ci_hdrc.0/state to read
'not attached', but the i.MX6 OTG port has no VBUS sensing from
userspace, so that state never appears on unplug: the chipidea suspend
irq parks the state at 'suspended' (an idle port reads 'default').
The trampoline hung in step 1 forever and the DBC flash never started.

Exit when the state leaves 'configured' instead, held for 3 consecutive
1s samples so an RNDIS re-enumeration blip can't fire it early.

Also kill any still-running trampoline instance before arming a new
one, so a retry after a failed round can't stack two instances racing
over the USB role and DBC power.
…lone-dbc-flash

# Conflicts:
#	assets/trampoline.sh.template
@cla-bot cla-bot Bot added the cla-signed label Jul 13, 2026
@librescoot librescoot deleted a comment from cla-bot Bot Jul 13, 2026
@librescoot librescoot deleted a comment from cla-bot Bot Jul 13, 2026
- parse latest.json before writing cache so a captive-portal 200 can't poison it
- guard both cache reads so a corrupt cache falls through to the bundled fallback
- close the download sink in a finally so a mid-transfer error can't leak it
- verify OSM/Valhalla tiles against GitHub's per-asset sha256 digest
- detect a declined pkexec/sudo prompt so a cancelled elevation shows the
  elevation-required dialog instead of silently exiting the app
- write the macOS elevation launcher to a unique temp dir, not a fixed
  world-writable /tmp path (symlink TOCTOU before an admin-privileged run)
- delete the Windows driver temp dir recursively so rndis.cat no longer leaks it
- drop dead _sanitizeOutput
- localize the unhandled-error snackbar instead of hardcoding German
- start(): verify the trampoline actually launched (pgrep) and throw otherwise,
  so a silent launch failure surfaces instead of a stuck screen
- hold usb0 up during the pairing window by masking librescoot-ums at upload
  start, so the OTG UDC isn't grabbed away and SSH survives to the flash
- skip path: guard restore_gadget with gadget_rescue_start and stop the watchdog
- kill the journalctl capture on the fail, skip, and V3-success exit paths
- treat a failed umount in grow_and_populate_dbc as fatal so the USB role is
  never flipped with the DBC partition still mounted
- treat a CHECKSUM MISMATCH from the flasher as fatal instead of only logging
  it, so a corrupt write can never report success
- add set -o pipefail to the dd fallback scripts (run the Linux one under bash)
  so a dd/gunzip failure mid-write aborts instead of passing a boot-sector-only
  verify on a truncated image
- cross-check the macOS flash target against the 0525/A4A5 USB identity and
  refuse to write if it can't be confirmed, so a second external disk can't be
  picked as the target
- close the SSH session on the timeout/error path in _runCommandOnce and
  downloadFile so a stalled command no longer leaks a channel until the
  connection's channel limit is hit
- ping every 5s (below dartssh2's 10s default) through the idle pairing window
…done

- Dashboard Prep shows DBC upload as a compact strip while Bluetooth pairing
  and keycard teach-in are in progress, then promotes the prep + Begin button
  to the main content once both are done
- run the existing FlashService.validateDevice safety check before the MDB
  write and abort if it fails (it was never called)
- sidebar: Dashboard Prep is one phase; Flash DBC shows active (not done)
  during reconnect
- Retry DBC flash no longer forces the user back through Bluetooth pairing
- localize the sidebar tile labels; drop dead builders, _waitForCbb, the
  unused elevation warning, and the unused download_progress widget
Remove 77 strings with no remaining call site: the superseded LED/blinker
walkthrough, the old single-device flow, and stale image placeholders.
…ebar

They were folded into dashboardPrep and are never entered on their own, so
mark them hiddenUnlessActive (they belonged to no MajorStep and left the
sidebar invariant broken). Update the phase-model tests to the merged design
and drop the l10n keys orphaned by removing the dead reconnect helper.
- drop the orphaned dd-phase flash helpers, the unused _reconnectToMdb and
  _batteryRemovalStarted, and the unused LaunchArgs import
- debugPrint instead of print in network_service; escape angle brackets in
  two doc comments
@CLAassistant

CLAassistant commented Jul 13, 2026

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

Signal battery-service that the seatbox is open (stop vehicle-service so it
can't re-assert the latch, then HSET vehicle seatbox:lock open and PUBLISH
vehicle seatbox:lock) so it opens the main pack's contactors. Wait for
battery:0 to leave the active state, with a 30s fallback into the UMS reboot
which deactivates the pack regardless. Drops the physical-removal step and its
now-dead openSeatbox helper.
Since the main battery is now deactivated in place rather than removed, it can
be live after the reflash, and reconnecting the CBB onto a powered HV bus is
unsafe. Move the CBB reconnect ahead of the AUX reconnect (both in the boot
step, with a warning) so the CBB is connected while the scooter is fully
depowered. The CBB-reconnect screen becomes verification-only: confirm CBB and
main battery are detected, drop the physical reconnect and insert-battery
instructions and the now-stale insert-battery step.
pkill -f matches the full command line of every process. The [t] bracket
keeps the pkill token itself from matching, but the nohup clause on the
same line contained the bare /data/installer/trampoline.sh path, so pkill
killed its own launching shell before nohup ran. The trampoline never
started, no log was written, and state.json still said trampoline-armed.

Split the kill and the launch into separate SSH commands, and clear a
stale trampoline-status when arming so an old result can't be read as
the new run's outcome.
- Resuming into dbcSwapAndFlash now checks whether the trampoline
  process is alive and re-arms it when it is gone (launch failure, MDB
  reboot, crash) instead of sending the user to the swap-cables screen
  with nothing listening for the cable swap.
- Walk-away screen: the success signal is the keycard LED blinking
  green, not the dashboard turning on. The dashboard cycles several
  times mid-install, and the fallback path finishes with the DBC off.
- The headline no longer asks to unplug a laptop that is already
  unplugged when the screen appears.
- Finish screen: skip the cable-swap steps and the MDB-side settings
  reset and reboot when arriving from the walk-away path. There is no
  MDB connection on that path; the stale SSH transport stacked
  multi-minute TCP timeouts on every step.
- Resume text no longer claims the install restarts from the beginning.
- Point the unknown-status hint at /data/installer/trampoline.log.
ipwho.is places Berlin consumer IPs in Hessen; ip-api.com resolves the
same IP correctly. Its region field is the ISO 3166-2 subdivision code,
so the existing geo map works unchanged. The free tier is HTTP-only and
non-commercial, which is fine here: worst case a spoofed answer
preselects a dropdown the user can change.
The boot LED guard now doubles as a progress display: amber pulse
groups, one pulse per started quarter, fed from step boundaries and
the block-device sector counter during the long image write. Green
and red stay reserved for the terminal states.

The success exits that skip the MDB reboot (V3 UMS tile copy, DBC
already at target) left the scooter degraded: keycard/ums/pm stopped
or masked and the installer-only overrides (usb0-policy=always-on,
auto-standby=0, alarm off) persisted, so a walk-away user ended up
with a dead keycard reader and no alarm. All success paths now
restore the services and wipe the override settings. Starting keycard
is safe in the MDB-first flow: cards are enrolled before the DBC is
staged, so a master exists and auto-master-learn stays off.
Resuming into a post-flash phase now starts the keycard service when
state.json says cards are enrolled; the resolver carries the
bt_paired/keycard_enrolled flags through the trampoline phases so the
resume handler can tell. Previously a resume that landed on the finish
screen left the reader dead until the next reboot.

Text continuity fixes from a full flow audit:
- disconnectCbbDesc claimed the battery must be removed; the previous
  step only switches it off in place (safety-instruction mismatch)
- the DBC prep strip showed "DBC flash successful!" once the upload
  to the MDB finished, before the flash ever ran; new dbcPrepComplete
  key for the staged state
- sidebar step said "Remove Battery" for the deactivate-in-place phase
- the disabled Begin button hinted "waiting for downloads" during the
  upload stage
- unlockScooterDesc assumed keycards and Bluetooth were set up; both
  are skippable
- reconnectUsbToLaptop now says to unplug the DBC cable first (the MDB
  port is occupied at that point)
- finish-screen cable steps are phrased state-safely for the resume
  path, which cannot know whether the swap already happened
Non-release builds skipped the MDB cleanup entirely, so state.json
stayed at trampoline-armed and every reconnect after a successful
finish resumed to the finish screen as an unfinished install. Keep the
images and logs for postmortem but remove the resume triggers.

Also reword the reboot-wait screen: the old text read like an
instruction to unplug USB, but the app is just waiting for the MDB
reboot to drop the link on its own.
The trampoline skips the DBC flash when the target version is already
installed. That makes full end-to-end testing impossible on an
up-to-date scooter; this flag threads through to FORCE_DBC_REFLASH in
the trampoline template and flashes unconditionally. Carried across
the self-elevation relaunch like --dry-run.
When version-service data in Redis (version:dbc, written the last time
the DBC booted Librescoot) shows the DBC already at the exact target
version, ask the user: flash again or skip. Skipping reuses the
existing skip-DBC path straight to finish, avoiding the pointless
cable swap the trampoline's silent at-target exit required. Re-flash
answers force FORCE_DBC_REFLASH into the generated trampoline, same
as --force-dbc-reflash. With no version:dbc key (stock DBC, or no DBC
boot since the last MDB reboot) nothing changes: the trampoline's own
SSH check still handles the silent skip.
Success used to arm a persistent green blink that ran until someone
power-cycled the scooter or the installer reconnected. With the
success paths now restoring normal operation, the scooter is
immediately usable, so signal completion by silence: the amber
progress pulses stop and the LED goes dark. Red blink and hazards
stay as the error signal.
…t unlock

The DBC-flash walk-away screen now carries the full story: the install
runs on its own for 10-20 minutes, progress reads as amber pulse
groups on the keycard LED (one pulse after start, up to four near the
end), done means the LED stops pulsing and stays off, and the
getting-started/handbook block moves up so there is something to read
in the meantime. The installer is done at this point; the primary
button just continues to the finish screen.

Bluetooth pairing no longer unlocks the vehicle. The nRF52 accepts
bonding whenever it advertises without whitelist (its gate is the
advertising mode, not the lock state) and the passkey reaches the
installer via the ble hash, so pairing now pushes
advertising-restart-no-whitelisting instead of scooter:state unlock.
Unlocking also powered on the DBC, which at that point may still run
the stock firmware. Done/skip restore whitelist advertising instead
of re-locking.
Waiting users should land in the getting-started tips and handbook
link right below, not be told to close the installer.
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