Repository navigation
Description
Consolidated 2026-10-27 against firmware 0.3.0 (firmware-v0.3.0).
Goal
The client updates the Gateway's USB side (RP2040) from a GitHub firmware release. It takes the image from the local cache (#192), reboots the Gateway into its UF2 drive, copies the image and reconnects, with no BOOTSEL button and no PlatformIO.
Background
- Release package, manifest and checks: feat: firmware release check and local cache #192. This ticket never touches the zip or the manifest.
- The USB-side image comes from the cache: feat: firmware release check and local cache #192
getFile({ kind: 'release', version }, { side: 'usb', format: 'uf2' })(the entry withboard: "gateway";firmware-release-assets.yml:196-198).- It also carries the release's cockpit-side app (feat(firmware): Gateway updates its own bridge over USART2 OpenSkyhawk#367, D30). After this update the cockpit side can be behind:
UPL_INFO_ACK.bridgeFw≠ the node-0 row (FirmwarePlan/07-simgateway-api.md:441-445). Updating it is feat: update the Gateway's cockpit side from a firmware release #187.
- It also carries the release's cockpit-side app (feat(firmware): Gateway updates its own bridge over USART2 OpenSkyhawk#367, D30). After this update the cockpit side can be behind:
- The 1200-baud touch: the Gateway already reboots into its UF2 drive on the standard touch.
- Adafruit TinyUSB 3.7.7 handles it, the version the 0.3.0
dependencies.txtrecords. When DTR drops on CDC 0 while the line coding is 1200,tud_cdc_line_state_cbcallsTinyUSB_Port_EnterDFU→reset_usb_boot. - Since D33 (fix(firmware)!: sim output reaches CAN, inputs win the bus and are never lost silently (#389) OpenSkyhawk#392),
tud_task()runs only after eachloop(), so the touch is serviced on the next loop pass (SimGateway.cpp:1087-1098). - No firmware change and no new message: a dedicated message was considered and dropped (feat(firmware): Gateway reboots into its UF2 drive on request (GW_BOOTSEL) OpenSkyhawk#369).
- Adafruit TinyUSB 3.7.7 handles it, the version the 0.3.0
- The drive: a blank RP2040 mounts as
RPI-RP2by itself. A running one is restarted into it by the touch. BOOTSEL by hand is needed only when the firmware hangs before USB comes up (Firmware/release/README-gateway.md:28-33, docs: bootloader rollout, first-setup page and video script OpenSkyhawk#380).
Design
Client design, final as of 2026-10-28: https://claude.ai/artifact/R8DXWWC4uyAby54tBQLo8v. Line refs are to the artifact's version 1793186986-be4e. The design dictates the UI. Settings › Cockpit › Firmware updates is #192's.
- Firmware button:
CockpitMap.dc.html:248(Firmware · n, tooltip:1390). - Firmware sheet (
CockpitMap.dc.html:496-525):- the release card
:500, with feat: firmware release check and local cache #192's states and copy; - Needs attention · n / All · n;
- rows
:514-518, chips fromfwInfo:817-858; - the hint
:522.
- the release card
- Shared update dialog (
CockpitMap.dc.html:551-624,upVals:1018-1220), USB-side path:- phases
:910; - the DESIGN-ONLY injections No drive appears · Copy fails · Doesn't reconnect · Wrong version · Calibration reset · Calibration reset · no saved copy (
:1179); - fail texts
:975,:982-983,:993; - the done notes
:606-612.
- phases
- Overview firmware banner:
Overview.dc.html:81.
Scope
- Release check, download and cache: feat: firmware release check and local cache #192. This ticket reads
latest()/checkState(). - Compare with the Gateway's version from
HELLO_ACK: up to date / update available / newer than the latest release. The version order comes from feat: firmware release check and local cache #192 (one owner). (Board-level versions are feat: show each board's firmware version and warn on a major mismatch #171.)HELLO_ACK.fwis the release version (FirmwarePlan/03-uart-usb-hid-protocol.md:300).- A Gateway on the 0.2.0 release reports 0.1.0 (docs(firmware): v1.0 API + wire-format freeze review and compatibility policy OpenSkyhawk#295 A11). It just reads as "update available".
- The target is feat: firmware release check and local cache #192's
latest(), the highest release on a supported major. A release on a newer major than the client supports is never a target: rows compare againstlatest(), and the newer major appears only on the release card (below) and as an unavailable row in feat: install a specific firmware version or a file — rollback, node-ID checks, history #177's Install version… (decision 2026-10-28).
- Update:
- Get the USB-side image from the cache (feat: firmware release check and local cache #192
getFile, SHA-256 already re-checked). - Release the port (the relay is already stopped, see Relay). Then open the port at 1200 baud and close it (DTR drop). The Gateway reboots into
RPI-RP2. - Find the drive: macOS
/Volumes/RPI-RP2, Windows: scan drive letters forINFO_UF2.TXT, Linux/media/$USER/RPI-RP2.- Copy the file as a plain write stream (
fs.createWriteStream/fs.writeFile), notfs.copyFile. On the macOS bench the system copy call took 41 s for 168 KB, a plain write 1.0 s (test(firmware): CAN bootloader spike — minimal own bootloader, two STM32s then the RP2040 OpenSkyhawk#361 results). - A drive that's already mounted (a blank Gateway, or an earlier copy that was interrupted) starts here.
- Copy the file as a plain write stream (
- Reconnect through the usual find-and-retry, and confirm
HELLO_ACKreports the new version.- The Gateway is VID
0x2E8A/ PID0x4134, with the CDC interface namedA-4E Skyhawk DCS-BIOS(FirmwarePlan/03-uart-usb-hid-protocol.md:99-116). - The PID is provisional: it isn't registered yet. When Raspberry Pi assigns one, a firmware issue switches
setIDand the docs, and the client switches with it. No old-PID list or product-string fallback (decided 2026-10-25, docs(firmware): v1.0 API + wire-format freeze review and compatibility policy OpenSkyhawk#295 A20).
- The Gateway is VID
- Get the USB-side image from the cache (feat: firmware release check and local cache #192
- Relay: Update is disabled until the DCS relay is stopped. That's the design's Stop relay check on the confirm step. After the update the relay stays stopped, and the user starts it from the pill. The client never restarts it by itself.
- Calibration: the Gateway's stored calibration may reset on any upgrade (docs(firmware): v1.0 API + wire-format freeze review and compatibility policy OpenSkyhawk#295 A21,
07-simgateway-api.md:86-90).- Restore automatically, with no prompt. If the Gateway comes back with its calibration actually cleared and this computer has a saved copy for it, the client restores that copy itself and writes one event-log row. The done screen carries a one-line note, e.g. "Axis calibration restored from this computer's copy".
- If it's cleared and there's no saved copy, the done screen notes that calibration was cleared and points to the HID page's axis calibration (Device I/O › HID), still with no prompt.
- Never prompt, and never offer to redo calibration after an update.
- Detecting "cleared" reuses the HID page's calibration owner,
HidCalibrationService(SkyHawkClientsrc/main/hid/calibration/hid-calibration-service.ts). On every port open it readsHELLO+GET_CALand keeps the per-axisCalSnapshot(present/calibrated, keyed by USB serial) behindcalSnapshot()(:171). That's the state the HID badges show. The update flow adds noCAL_DATAparsing or calibrated check of its own.- Before the update starts, record
calSnapshot()per axis. Take it before releasing the port:release()andclosed()clear it (:134,:151). - After reconnecting, the service reads it again on port open; read
calSnapshot()once that lands. Calibration was cleared if an axis was present and calibrated before and isn't calibrated now → restore the saved copy automatically (above). - Not calibrated before → nothing to restore, no note.
- The before-state isn't always there: the service's read can fail or time out, and an update that starts at step 3 has no port. Then fall back to the owner's existing check,
regressedAxes()(cache-rules.ts:162). It returns the axes in the saved copy that the device now reports present but uncalibrated. - Firmware background: there's no "cleared" flag. A stored blob that fails its check is zeroed at boot (
SimGateway.cpp:181-187), so a cleared Gateway reportscalibratedMask= 0 (03-uart-usb-hid-protocol.md:302,:351-352;07-simgateway-api.md:78-80). A never-calibrated Gateway reports the same, which is why the before-reading matters.
- Before the update starts, record
- Failures: retry only where it's safe.
- No
RPI-RP2drive within the drive-wait timeout after the touch, or no port to send the touch to → "Hold BOOTSEL while plugging the Gateway in", then continue from step 3 once the drive appears. On the bench the drive came up 1.7 s after the touch (test(firmware): CAN bootloader spike — minimal own bootloader, two STM32s then the RP2040 OpenSkyhawk#361). Nothing has been written yet, so Retry (the touch again) is safe. A failed USB-side update offers Close · Retry only (decision 2026-10-28): no Install another version…. - The copy fails (write error, or the drive goes away mid-copy) → Retry copies the whole file again to the drive. If the drive is gone and the Gateway doesn't come back as a port, show the BOOTSEL instruction, then copy again.
- No reconnect within the timeout after the copy → keep the usual find-and-retry running, and offer Retry for the reconnect only. If the
RPI-RP2drive is still there, the copy didn't take, so offer the copy again. Otherwise the result is failed ("didn't reconnect"). - The wrong version after reconnecting (
HELLO_ACK.fw≠ the target) → a failed result showing the version it reports. Never retried automatically. - Copy, as drawn (
CockpitMap.dc.html:975,:982-983,:993; footers):- No drive: No Gateway drive appeared — "hold BOOTSEL while plugging the Gateway in, then Retry. Nothing has been written yet." Foot: "Nothing was written."
- Copy fails: Copy failed — "the Gateway's drive went away during the copy. Retry copies the whole file again; if the Gateway doesn't come back, hold BOOTSEL while plugging it in." Foot: "The Gateway waits in its update drive until a copy goes through."
- No reconnect: Didn't reconnect — "the Gateway didn't come back after the copy. Retry looks for it again." Foot: "The copy finished. The client keeps looking for the Gateway."
- Wrong version: Wrong version — "it reports 1.0.0, not 1.0.1. Nothing is retried automatically." Foot: "It runs the version it reports."
- No
- Plain Node / Electron APIs only (
serialport,fs,fetch) — no picotool, no USB driver. - Time estimates (the shared dialog's, for every target) are rough. Under 60 s they're written in seconds, and from 60 s up in whole minutes, rounded to the nearest ("about 5 s", "about 1 min"). This ticket owns the rule; feat: update a board over CAN from a firmware release #175, feat: update the Gateway's cockpit side from a firmware release #187 and feat: Update all — the Gateway first, then the boards you pick #176 give their per-target figures and point here. The USB side is about 5 s.
Release check surfaces (expanded 2026-10-25 — these need only the release check (#192), so they ship with this ticket, ahead of the CAN-update tickets):
- Firmware button in the Cockpit plan toolbar (Firmware · n, n = boards needing attention) → Firmware sheet: release card: every check state, with its head, sub and button, as feat: firmware release check and local cache #192's state table gives them (feat: firmware release check and local cache #192 owns the copy). That includes Firmware 2.0.0 is out — update the client to use it / not downloaded · checked … for a release on a newer major (feat: firmware release check and local cache #192 § Check), Needs attention / All, one row per board — Gateway USB side, Gateway cockpit side, then the boards — with its status chip against the release: up to date · update available · newer than the release · custom firmware (no image for this node) · major mismatch · offline · not reported (its wording follows fix: firmware version handling — supported major 1, honest "not reported" copy #191's honest-copy rules). Versions from
HELLO_ACKand_NODE_STATUSv4 (feat: node status v4 — tolerant row parsing and the 0xFF00 roster request (firmware #359 re-sync) #170/feat: show each board's firmware version and warn on a major mismatch #171; v4 is current,NodeStatus.h:36).- offline (
CockpitMap.dc.html:843): OFFLINE · last reported x.y.z. An offline board that never reported reads OFFLINE · no version reported, Installed—(decision 2026-10-28). - custom firmware (
CockpitMap.dc.html:846): a PanelGroup board reads CUSTOM FIRMWARE · x.y.z · panel groups install their own build; another node with no image reads … · no image for #n in 1.0.1. Its row's button is From file… (:1362), which opens its Settings tab. - Custom and non-standard boards show no hardware revision anywhere.
- Needs attention (the button's count and the sheet's filter) is firmware-only, as in the design's
needlist: update available · major mismatch · manual update only (no bootloader) · stuck in bootloader · app unstable · not responding (the cockpit side, feat: update the Gateway's cockpit side from a firmware release #187). The design also keeps a board there while it's updating (CockpitMap.dc.html:857,:1359). The plan's general Attention list (gold/red, I²C, warnings) is separate, and this ticket doesn't change it.
- offline (
- The Update action on the Gateway's USB-side row runs this ticket's flow in the shared update dialog, which follows the design (
upVals):- Confirm: the target version, the
checks(DCS relay stopped / Stop relay, Gateway connected, the image's source) and the Gateway note ("restarts … calibration may need restoring"). - Step list: the USB phases
uf2(restarting) →copy(with %) →rec(reconnecting) →ver(confirming the version). - Done: "reports ", the relay hint (plus the calibration note from Scope, in place of
calAsk). - Fail: the title, text and buttons.
- The logic behind these is in Scope (Relay, Calibration, Failures).
- The Gateway cockpit-side row's Update is feat: update the Gateway's cockpit side from a firmware release #187, board rows' Update is feat: update a board over CAN from a firmware release #175, and Update all is feat: Update all — the Gateway first, then the boards you pick #176.
- As drawn:
- Source check: "Firmware 1.0.1 · saved on this computer, checked" (
CockpitMap.dc.html:1057). No download or checksum steps. - Gateway note: "it restarts to update and reconnects by itself. Keep it plugged in. If the update clears its axis calibration, this computer's copy is restored automatically." (
:1075). - Phases (
:910): Restarting the Gateway to update it → Copying the firmware · n% → Reconnecting → Confirming the version it reports. Confirm foot: "about 5 s". - Done: "Gateway · USB side reports 1.0.1". The calibration note is "Axis calibration restored from this computer's copy." or "Axis calibration was cleared and this computer has no saved copy — calibrate it on the HID page (Device I/O › HID)." (
:1169). NocalAskprompt. - Fail: the copy under Scope › Failures, with Close · Retry.
- Source check: "Firmware 1.0.1 · saved on this computer, checked" (
- Confirm: the target version, the
- Overview banner (state from feat: firmware release check and local cache #192) while boards are behind the latest release (
Overview.dc.html:81): "Firmware 1.0.1 is out — the Gateway and 2 PDUs can update. 1 more board needs a manual update." → Review in Cockpit →.- It names what can update, by kind: the Gateway, then PDUs. It adds the count of manual update only boards when there are any.
- Custom-firmware boards aren't counted.
- The banner follows
latest(); a newer-major release doesn't raise it.
Delivery
Two PRs (agreed 2026-10-27):
- The surfaces on real data: Firmware button / sheet, Overview banner. No Update buttons yet.
- The USB-side update flow (Update steps 1–4, Relay, Calibration, Failures). It lands the shared update dialog that feat: update the Gateway's cockpit side from a firmware release #187 and feat: update a board over CAN from a firmware release #175 reuse.
PR 1 landed in PR #198 (2026-10-28; independent review, two fixes applied): the Firmware button and sheet, the release card, the chips (incl. manual update only, moved here from #175), the Overview banner, Settings' Open Cockpit →. Row buttons (Update, Retry, From file…) come with PR 2, #175 and #177; the Gateway rows open the Gateway sheet once #177 builds it; the sheet hint gets its "From file…" clause back with #177.
Out of scope
- Updating the Gateway's cockpit side (PanelBridge STM32): feat: update the Gateway's cockpit side from a firmware release #187. It goes over the Gateway's own serial link (feat(firmware): Gateway updates its own bridge over USART2 OpenSkyhawk#367).
- Updating the panels: feat: update a board over CAN from a firmware release #175 (boards), feat: Update all — the Gateway first, then the boards you pick #176 (Update all), feat: install a specific firmware version or a file — rollback, node-ID checks, history #177 (version / file).
- The rollout order is USB side, then cockpit side, then nodes (docs: bootloader rollout, first-setup page and video script OpenSkyhawk#380).
Blocked by
#192 (release check and local cache).
Blocks
#187, #175, #176 (PR 2: the shared update dialog, and the USB side first), #177 (PR 2: the USB side's Install version… reuses this flow).
Build order (approved 2026-10-27): #192 → #173 PR 1 → #173 PR 2 → #187 → #175 → #177 → #176.
Done when
- From a release with assets, the client takes a Gateway on an older version to the release version end to end. macOS: bench PASS (0.2.0 → Update → reports 0.3.0, via the dialog's Stop relay). Windows: not run here; the drive-letter scan, the 1200-baud touch through the Windows driver, and copy speed are unit-tested only. They're carried to the v0.2.0 QA list.
- Bench re-check: the 1200-baud touch →
RPI-RP2works on firmware 0.3.0. PASS 2026-10-28: the drive appeared 1.96 s after the touch. It was last proven in test(firmware): CAN bootloader spike — minimal own bootloader, two STM32s then the RP2040 OpenSkyhawk#361, before D33 (fix(firmware)!: sim output reaches CAN, inputs win the bus and are never lost silently (#389) OpenSkyhawk#392), and hasn't been recorded since. - After the bench update, the connect read of
DIAG(feat: read and show the Gateway's link-health counters (GET_DIAG) #169) doesn't flag a recovery (PASS: power-on, all counters 0):RESET_REASONisn'tWATCHDOG, andUSB_REATTACHESis 0. This is feat: read and show the Gateway's link-health counters (GET_DIAG) #169 Done when step 4.
Landed in PR #198 (PR 1: surfaces) and PR #199 (PR 2: the USB-side update and the shared update dialog). Both had an independent review, and every real defect was fixed before merge. That includes:
- a guard so another RP2040 in BOOTSEL never gets the Gateway image (only a drive that appears after the touch counts, or a port-less drive when no Gateway port is listed);
- Retry after a late reconnect;
- the port always handed back;
- an in-drive path: a Gateway waiting in its
RPI-RP2drive shows IN UPDATE DRIVE + Update on the Firmware sheet, and the update starts at the copy.
Bench 2026-10-28 (macOS, real Gateway): touch → drive 1.96 s; 0.2.0 → 0.3.0 end to end; DIAG clean; in-drive update PASS; calibration n/a (no axes on the bench board).
Decided 2026-10-28: the update-drive state is shown only on the Firmware sheet row and in the dialog. Extra pill, Connection and Overview surfaces and a BOOTSEL hint were dropped as overkill.
Activity
mottihoresh commentedon Oct 27, 2026
Design update (2026-10-26, design artifact v178). The Firmware sheet's rows changed:
Marker:
CHANGED · #173 — name on its own line, chip and version under it, actions on the right.feat(nodes): show each board's firmware version and warn on a major m…
feat(releases): firmware release check and local cache (#196)
feat(settings): firmware updates rows (#197)
feat(cockpit): firmware button, sheet and Overview banner (#173 PR 1) (…
feat(update): update the Gateway USB side from a saved release (#173 …
mottihoresh commentedon Oct 28, 2026
Done in PR #198 and PR #199 (bench results in the body).
feat(update): update the Gateway's cockpit side (#201)