Repository navigation
feat(firmware)!: v1.0 freeze fixes — firmware version on the bus, config structs, pinned dependencies (#295) - #359
Merged
Merged
Conversation
…atform (#295 A7) DCS-BIOS (*) and Adafruit TinyUSB (>=3.0.0) accepted any future major, and the RP2040 platform was a bare git URL that builds the fork's latest code. Bound the libraries to ^0.3.10 / ^3.7.7 (what releases resolve today) and pin the platform to 79ce473e, the commit the bench Gateway is built on. The release job still records what each release resolved.
The Gateway reported 0.1.0 in HELLO_ACK from hand-kept SIMGATEWAY_FW_* defines that release-please never touched, PowerMonitor's library.json was missing from extra-files, and 32 file headers carried their own @Version stamps (0.1.0-0.4.0) that meant nothing. Add the header-only FirmwareVersion library: OPENSKYHAWK_FW_MAJOR/MINOR/ PATCH carry release-please's x-release-please-* markers and are rewritten on every firmware release (generic updater). SimGateway's HELLO_ACK reads it; READY_n will too. Add it and PowerMonitor to extra-files, delete the @Version tags, and move the doc citations they shifted.
…DE_STATUS v4 (#295 A12) v1.0 says nodes on one bus share a major version, but nothing on the bus reported which firmware a board runs. READY_n, empty until now, carries a ReadyPayload: bytes 0-2 = the release version from FirmwareVersion.h, 3-7 reserved. Nodes send it through the new CANProtocol::sendReady(). PanelBridge caches the version per node, keeps it across a timeout, pushes a _NODE_STATUS delta when an alive node's version changes (a reflash inside the heartbeat timeout never goes dead), and reports it in _NODE_STATUS v4 (34 hex; node-0 row = the bridge's own build). An empty READY from older firmware reads 0.0.0, not reported. HEALTH's reserved bytes stay free for future health. Decision D21. The node_status test now covers the version delta and its retention across a timeout; node-imitating test sketches send the real READY.
…A16) _NODE_STATUS, _NODE_STATUS_END, _NODE_TELEM and the 0x86FE roster request existed only with -DPANELBRIDGE_NODE_STATUS, off in the library and on in the template. The client depends on them and v1.0 freezes them, so a PanelBridge env built without the template silently showed an empty Cockpit. PanelBridge.h now defaults the flag to 1 and the code tests it with #if, so -DPANELBRIDGE_NODE_STATUS=0 compiles it out (the NODE_HEALTH_TELEM pattern). The template and E2E drop the now-redundant flag; the two input_dispatch tests, which read the USB-UART stream, opt out. Node tracking itself never depended on the flag.
AnalogMultiPos(id, pin, n, 0) doesn't compile: a literal 0 matches both the deadband overload and the posVals (null pointer) overload. A named constant isn't a null-pointer constant, so NO_DEADBAND always picks the equal-spacing form. Additive. The equal_spacing test now builds one and checks the bands meet with no gap.
…t; hysteresis defaults by route (#295 A10, A5, A4) A10: AnalogInput(id, pin, reverse, minRaw, maxRaw, hysteresis, ewmaShift, pollMs) had six positional optionals, so tuning the dead zone meant writing reverse and the raw range first. It now takes one AnalogConfig, every field defaulted and set by name: AnalogInput roll(CTRL_ROLL, ROLL_PIN, { .hysteresis = 32, .ewmaShift = 4, .pollMs = 2 }); The tuning constants move onto AnalogConfig. A5: the hysteresis default is route-aware (HYSTERESIS_AUTO): 1024 for a DCSIN_* id, which both tested DCS-BIOS sketches used because 128 floods DCS-BIOS import on a sweep, and 128 for a CTRL_* id, the value the axis calibration was validated with. An explicit value always wins. A4 (reopened): AngleSensorInput's hysteresis was fixed at 128. It now takes an AngleConfig (hysteresis, ewmaShift, pollMs; route-aware by default) and can be overridden. Still no reverse: the sensor's DIR pin. BREAKING CHANGE: AnalogInput's positional tuning arguments are replaced by an AnalogConfig; AngleSensorInput's pollMs argument by an AngleConfig; AnalogInput::DEFAULT_HYSTERESIS is gone and the other tuning constants are AnalogConfig::DEFAULT_*. A DCSIN_* AnalogInput or AngleSensorInput with no hysteresis set now uses 1024.
…g takes 5 arguments (#295 A8) makeX27Config took 15 positional arguments with the per-gauge motor settings (stepsPerRev, rangeSteps, homeStepUs) last, so a 320-degree BKA-30 meant writing 13 arguments first, and after the freeze the list could only grow at the end. Every StepperConfig field now defaults to the X27/VID-29/BKA-30 family, so a sketch names only what differs: const StepperConfig OIL_MOTOR = { .maxPos = 960, .rangeSteps = 960 }; makeX27Config keeps the common case: (homePosition, parkPosition, minPos, maxPos, home = STALL), and returns exactly the defaults plus those. Positional test initialisers that stopped before rangeSteps now pass 0 explicitly, so their homing distance is unchanged. New test: config_defaults. BREAKING CHANGE: makeX27Config drops its 10 trailing optional arguments; set those fields by name on a StepperConfig instead. A positional StepperConfig initialiser that omits rangeSteps now gets 945, not 0.
…lass interface (#295 A9) v1.0 covers a defined sketch API, including the interface a custom control class is written against. Libraries marked their internals inconsistently, so a header alone couldn't tell a builder what is frozen. STM32Board: the seven functions outside the covered list (tick, setWarning, setLinkActive, clockFault, readDieTempC, readVddMv, logNodeFaultEdge) now say they are library-internal and not covered, like onCanStatus and canHandle already did. CANProtocol: one banner on the runtime namespace — internal except sendBatched, which a custom class uses. docs/guides/new-control-type.md is rewritten: it said most control types were unimplemented, described dispatch types #147 removed, and told authors to manage batch flushing. It now documents the covered InputBase/OutputBase interface, the frame families, and a compile-checked example.
Every control class takes a PinRef; the rule's two exceptions were only a convention. PanelGroup::registerExpander's INTA/INTB lines need a native EXTI pin, and ShiftBus's own SPI and load/latch lines can't be shift- register PinRefs. FirmwarePlan 05 and the PinRef TechSpec now name them as the only two, and 05's backend table gains the missing ShiftBus row.
…growth (#295 A13) The client dropped any _NODE_STATUS row that wasn't exactly 28 hex, so the first 1.x field would have blanked every 1.0 client. Write down the forward-compatibility rule as decision D22: - CAN (02): frames never grow, fields never move; reserved = send 0, ignore; unknown enum values kept; new data goes to HEALTH's reserved bytes (health), a new TelemPage (node telemetry), READY's reserved bytes or a new frame family in a free ID range (anything else). - Host lines (04): writers append; readers parse a longer row's known prefix, report a shorter one as old firmware, show unknown values as unknown, and warn on a board/Gateway major mismatch. The client side lands in client 1.0.
Every other multi-byte field in 02-can-protocol.md stated its byte order; ControlPacket, the most-used payload on the bus, only implied it through a packed struct on a little-endian MCU. State it (little-endian, controlId low byte first) with a ControlPacketPair byte table in 02 and one line in the published can-bus page, fix 02's "CTRL_BCAST and EVT_n only" to include the REL/DIR/ACTION frames, and drop the long-done Phase 1 migration note.
TEST_SEQ/ECHO_n is a bench RTT diagnostic. Record what the freeze covers — the two IDs and every node mirroring the 8 bytes unchanged — and label the payload layout bridge-internal, since only PanelBridge writes and reads it. Drop the stale prototype note about PanelGroup's old 8-byte TEST_SEQ.
…A17) A13's "readers accept longer" can't apply to the USB CDC calibration channel: LEN is fixed per type on purpose, so a longer payload is not a frame to an older reader. Write down how it grows instead: HELLO/ HELLO_ACK never change; a new message type is a minor (older firmware relays it and the client times out, as GET_DIAG does); any change to an existing type bumps CAL_PROTO_VERSION and is a major. #358's code also shows the BAD_TYPE NACK is unreachable — an unknown type fails calLenValidForType() first — so its row now says so.
…A21) Both calibration stores survive a normal reflash, but nothing said whether a release may reset them. It may: the Gateway's blob layout or EEPROM sector, or the PDU's record layout or page, can change in any release. The Gateway's client offers to restore from its per-board cache; the PDU reports UNCAL_* and is recalibrated at the bench. One line each in 07 and the PowerMonitor TechSpec.
…d DCSIN ids (#295 A22) The refresh workflow always titled its PR feat(a4ec) and only checked that the generator's tests passed and the repo's sketches compiled — not what changed. Upstream additions are the normal case, but a removed symbol breaks a user's sketch build and a moved A_4E_C_* address silently breaks a lamp or gauge. gen_a4ec.py now compares the committed headers with the new output and prints added/removed/moved symbols (--change-report writes the Markdown); the refresh PR body includes it, and the maintainer decides any version bump. It also refuses to run when an existing DCSIN_* id would change — the ledger is append-only, and until now only the generator logic was tested, not the committed file. Three new pytest cases.
… A24) The client requested the node roster by writing DCS-BIOS export address 0x86FE, chosen because the A-4E never exports there. In DCS-BIOS v0.11.8 it is a live address for the AH-64D (PLT_VIDEO_FLIR_GAIN) and the Mirage F1 (P_VOR_01_G), so flying either with the cockpit connected answered every export of that control with a full roster burst. NODE_STATUS_REQ_ADDR is now 0xFF00: every aircraft module ends by 0xB2FB and MetadataEnd sits at 0xFFFE. It ships with _NODE_STATUS v4, which the client adopts anyway. Decision D23. BREAKING CHANGE: the client must write 0xFF00, not 0x86FE, to request the roster (synced from NodeStatus.h).
… A23) A CAN bootloader and uploader are planned. Reserve their number spaces now so nothing else claims them before they exist: - CAN 0x740-0x7BF (BOOT_CMD_n = 0x740+n, BOOT_RSP_n = 0x780+n), the lowest priority on the bus so update traffic never delays a control; - calibration-channel message types 0x10-0x1F / 0x90-0x9F and NACK reasons 0x09-0x0E. Comments at the owners (CANProtocol.h, AxisCal.h), FirmwarePlan 02/03 and the published can-bus frame table. Nothing is assigned yet.
…#295 A25) The export window was 0x8000-0x86FF — the DCSIN_* input-ID range reused by accident, not the A-4E's export block (0x8400-0x8555) — so it already passed parts of the AH-64D, MB-339 and Mirage F1 to the cockpit. The bridge now forwards every export address (0x0002-0xFFFE): whatever aircraft is flying, plus CommonData and the metadata. Nodes already hand every packet to every output, and each keeps only its own address, so the cockpit decides what it uses (the multi-aircraft direction). Only 0x0000 is skipped: it is the ControlPacket empty-slot sentinel. CTRL_BCAST load rises (CommonData, per-frame update counters); confirm on the #290 soak. Decision D24.
…ssage names (#295 A26) The roster request moved off 0x86FE because DCS-BIOS gave that address to the AH-64D and the Mirage F1 after it was picked. To catch the same thing early, the refresh now checks every aircraft in the release against the roster-request address and the _NODE_* message names (read from NodeStatus.h) and adds a warning to the refresh PR if any clashes. It warns rather than fails. Run against DCS-BIOS v0.11.8 it reports 0xFF00 clear and flags both aircraft for the old 0x86FE. Three pytest cases.
…295 A27) A calibration frame's LEN is fixed per type (A17), so DIAG couldn't grow in place: a new counter would need a new message type or a protocol bump. DIAG (#358) is unreleased and the record most likely to grow, so it is now 64 bytes — 47 in use, CAL_DIAG_RESERVED 47-63 transmitted as 0 and ignored on receive — and a new counter fills reserved bytes as a minor change. The client's DIAG decoder (SkyHawkClient#169) syncs the new length; the bench tool skips the reserved bytes.
This was referenced Oct 25, 2026
Closed
8 tasks done
This was referenced Oct 25, 2026
mottihoresh
added a commit
that referenced
this pull request
Oct 25, 2026
#359 landed the v1.0 API freeze on main. Two changes reach these sketches. AnalogInput now takes (controlId, PinRef, const AnalogConfig&) and nothing positional, so instrPot and floodPot lose their six-argument form. Both drop their tuning entirely rather than restate it: hysteresis defaults to HYSTERESIS_AUTO, which resolves to AnalogConfig::DCS_HYSTERESIS (1024) for a DCSIN_* id -- exactly what they were passing. floodPot now declares identically to instrPot, which is the point of the station pair: only the PinRef differs. StepperConfig fields default to the X27 family, so the FULL_4STATE alternate build is a plain designated initializer and the makeCanopy4StateConfig() helper goes away. The comment keeps why it is spelled out rather than built from makeX27Config(): that helper fixes the pattern, so the older form had to overwrite the field after the call. AngleSensorInput and the makeX27Config calls compile unchanged -- the new AngleConfig is an optional trailing argument these sketches never passed. Builds: board A 73.6 % flash of a C8, board B 48.4 % of a CB. Board B's revision follows in the merge of fd31eeb. Refs #291
This was referenced Oct 25, 2026
Closed
mottihoresh
added a commit
to OpenSkyHawk/SkyHawkClient
that referenced
this pull request
Oct 27, 2026
Adds `CLAUDE.md`, the repo's agent instructions. The repo had none; it links to `ARCHITECTURE.md` and `PRD.md` rather than repeating them. **Rules:** - **Release facts:** never say a feature is "in firmware vX" without `git merge-base --is-ancestor <commit> firmware-vX` in OpenSkyhawk. Firmware before 1.0.0 is test-only; 1.0.0 is the first public release. Background: the `firmware-v0.2.0` tag was cut 7 minutes before OpenSkyHawk/OpenSkyhawk#359, so "from firmware v0.2.0" in #169 and #171 was wrong. - **Minimum firmware:** a client PR that depends on a firmware change names the firmware commit or release it needs, in the PR body. - **No magic numbers:** every address, code, enum and layout value comes from `npm run sync`; if the sync doesn't extract it yet, extend `scripts/sync-a4ec.ts` (example: `NODE_FORGET_REQ_ADDR`, #188). Points to the existing `_ACFT_NAME` exception in `ARCHITECTURE.md` so the two files agree. - **Ticket closure:** each box ticked with its evidence ("— #PR") or moved to a named follow-up before a ticket closes. - **Firmware claims get a review:** ticket text stating firmware behaviour, wire format or release facts goes to the firmware (OpenSkyhawk) session before it reaches the user. ## Verification - Docs only; no code changes. - `npx prettier --check CLAUDE.md` passes. - The `ARCHITECTURE.md` anchor link matches its "Firmware values come from `npm run sync`" heading. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Chunk 2 of #295: every fix from the v1.0 API + wire-format freeze review, plus the doc-only rules the review accepted. The findings, their decisions and the coverage checklist are in the #295 body. A20 (registering the Gateway's USB PID) is waiting on Raspberry Pi and isn't in this PR.
One commit per finding; the squash keeps them apart through the override block below.
Firmware and API
READY_n(ReadyPayload, bytes 0–2; empty READY = older firmware). PanelBridge caches it per node, keeps it across timeouts, pushes a delta when it changes, and reports it in_NODE_STATUSv4 (34 hex; node-0 row = the bridge's own). HEALTH's reserved bytes stay free for health. Decision D21.FirmwareVersionlibrary (OPENSKYHAWK_FW_*withx-release-please-*markers, verified against release-please's generic updater). SimGateway'sHELLO_ACKand READY both read it.PowerMonitoradded toextra-files; the 32 hand-kept@versiontags removed.-DPANELBRIDGE_NODE_STATUS=0opts out). The twoinput_dispatchtests opt out because they read the USB-UART stream.AnalogInput(id, pin, AnalogConfig)replaces six positional optionals:{ .hysteresis = 32, .ewmaShift = 4, .pollMs = 2 }. Hysteresis defaults by route: 1024 forDCSIN_*, 128 forCTRL_*; an explicit value wins.AngleSensorInputtakes anAngleConfig: hysteresis is now overridable (route-aware by default).StepperConfigcarries the X27 defaults, so{ .maxPos = 960, .rangeSteps = 960 }works;makeX27Configkeeps its common 5-argument form.AnalogMultiPos::NO_DEADBAND.^0.3.10and TinyUSB^3.7.7bounded; the RP2040 platform pinned to79ce473e(the bench Gateway's build).DCSIN_*id.0x86FEto0xFF00. In DCS-BIOS v0.11.8,0x86FEis a live address for the AH-64D (PLT_VIDEO_FLIR_GAIN) and the Mirage F1 (P_VOR_01_G), so flying either drew a roster burst on every export of that control. No module uses0xFF00(all end by0xB2FB). Decision D23.0x0002–0xFFFE) whatever aircraft is flying; nodes keep only their own addresses. The old window (0x8000–0x86FF) was the input-ID range reused by accident. Only0x0000is skipped (CAN's empty-slot marker).CTRL_BCASTload rises — confirm on the soak. Decision D24.DIAGgrows to 64 bytes, with bytes 47–63 reserved (send 0, ignore on receive). A calibration frame's length is fixed per type, so this leaves room for new link-health counters as a minor change. Unreleased (#358), so nothing in the field changes._NODE_*names.Breaking (pre-1.0, so a minor bump): A10/A5/A4, A8 and A24, each marked
!below. All in-repo call sites, tests and docs are updated.Docs and specs
A9 (library-internal marks on STM32Board/CANProtocol;
new-control-type.mdrewritten around the coveredInputBase/OutputBaseinterface, with a compile-checked example), A6 (PinRef native-pin exceptions), A13 (forward compatibility, decision D22), A14 (ControlPacket byte order), A17 (calibration channel's own rule;BAD_TYPEis unreachable), A19 (TEST_SEQpayload is bridge-internal), A21 (stored calibration isn't covered), A23 (CAN0x740–0x7BFand calibration types0x10–0x1F/0x90–0x9F, NACK reasons0x09–0x0E, reserved for the future firmware updater). FirmwarePlan, TechSpec,docs/firmwareanddocs/architectureare updated in the same commits as the code they describe. The compatibility policy page itself is chunk 3, a separate PR.Verification
node_status(v4 rows, version delta, retention across timeout),hysteresisandinherits(route-aware defaults and overrides),equal_spacing(NO_DEADBAND),config_defaults(StepperConfigdefaults).check_doc_refs.py, impact map,check_node_ids.pyand the A4EC pytest suite (34) pass; changed-Markdown link check clean.-DPANELBRIDGE_NODE_STATUS=0build contains no_NODE_STATUScode; default builds do.Follow-ups
_NODE_STATUSv4 parsing; roster request at0xFF00. Until it lands, don't run a Gateway flashed from this PR against the client._ACFT_NAME, not by address range (the AH-64D and MB-339 overlap the A-4E's addresses).GET_DIAGdecoder):DIAGis now 64 bytes (CAL_DIAG_BYTESvia sync); ignoreCAL_DIAG_RESERVED.AnalogInputlines need theAnalogConfigform after this lands; itsFULL_4STATEhelper can become{ .pattern = StepPattern::FULL_4STATE, .maxPos = 300 }.BEGIN_COMMIT_OVERRIDE
build(firmware): bound DCS-BIOS and TinyUSB ranges, pin the RP2040 platform (#295)
build(firmware): one release-managed firmware version (#295)
feat(firmware): nodes announce their firmware version in READY_n, _NODE_STATUS v4 (#295)
feat(firmware): PanelBridge node-status reporting on by default (#295)
feat(firmware): AnalogMultiPos::NO_DEADBAND (#295)
feat(firmware)!: AnalogInput and AngleSensorInput take a config struct; hysteresis defaults by route (#295)
feat(firmware)!: StepperConfig carries the X27 defaults; makeX27Config takes 5 arguments (#295)
feat(firmware)!: move the node-roster request to export address 0xFF00 (#295)
docs(firmware): mark library-internal APIs and document the control-class interface (#295)
docs(firmware): v1.0 wire compatibility rules, byte order and PinRef exceptions (#295)
docs(firmware): reserve CAN and calibration ID ranges for the future firmware updater (#295)
ci(a4ec): report removed or moved symbols on a DCS-BIOS refresh; guard DCSIN ids (#295)
feat(firmware): PanelBridge forwards the whole DCS-BIOS export stream (#295)
ci(a4ec): warn when a DCS-BIOS aircraft uses our roster address or message names (#295)
feat(firmware): reserve room in DIAG for future link-health counters (#295)
END_COMMIT_OVERRIDE