Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
123183d
build(firmware): bound DCS-BIOS and TinyUSB ranges, pin the RP2040 pl…
mottihoresh Oct 25, 2026
0fe91dc
build(firmware): one release-managed firmware version (#295 A11)
mottihoresh Oct 25, 2026
67183ae
feat(firmware): nodes announce their firmware version in READY_n, _NO…
mottihoresh Oct 25, 2026
678fe2d
feat(firmware): PanelBridge node-status reporting on by default (#295…
mottihoresh Oct 25, 2026
4ab91dc
feat(firmware): AnalogMultiPos::NO_DEADBAND (#295 A2)
mottihoresh Oct 25, 2026
9680e97
feat(firmware)!: AnalogInput and AngleSensorInput take a config struc…
mottihoresh Oct 25, 2026
05a896b
feat(firmware)!: StepperConfig carries the X27 defaults; makeX27Confi…
mottihoresh Oct 25, 2026
b20308f
docs(firmware): mark library-internal APIs and document the control-c…
mottihoresh Oct 25, 2026
1da0212
docs(firmware): write down the PinRef native-pin exceptions (#295 A6)
mottihoresh Oct 25, 2026
1f4e904
docs(firmware): within a major the wire only grows; readers tolerate …
mottihoresh Oct 25, 2026
99456a4
docs(firmware): state ControlPacket's byte order (#295 A14)
mottihoresh Oct 25, 2026
af967bd
docs(firmware): TEST_SEQ's payload is bridge-internal (#295 A19)
mottihoresh Oct 25, 2026
c4536f4
docs(firmware): the calibration channel's own compatibility rule (#29…
mottihoresh Oct 25, 2026
a4d9cda
docs(firmware): stored calibration is not covered by the freeze (#295…
mottihoresh Oct 25, 2026
376ee3a
ci(a4ec): report removed or moved symbols on a DCS-BIOS refresh; guar…
mottihoresh Oct 25, 2026
7714dff
ci(a4ec): make the change-report warning one explicit string (#295)
mottihoresh Oct 25, 2026
d41506b
feat(firmware)!: move the roster request to export address 0xFF00 (#2…
mottihoresh Oct 25, 2026
c5b1940
docs(firmware): reserve ID ranges for the future firmware updater (#2…
mottihoresh Oct 25, 2026
dd53acf
feat(firmware): PanelBridge forwards the whole DCS-BIOS export stream…
mottihoresh Oct 25, 2026
2221fbe
ci(a4ec): warn when a DCS-BIOS aircraft uses our roster address or me…
mottihoresh Oct 25, 2026
2d7d941
feat(firmware): reserve room in DIAG for future link-health counters …
mottihoresh Oct 25, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions .github/firmware-platformio-impact.json
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,43 @@
"Firmware/Examples/E2E_DCS_Test/PanelGroup",
"Firmware/Tests/DrumDisplay"
],
"FirmwareVersion": [
"Firmware/Examples/E2E_DCS_Test/PanelBridge",
"Firmware/Examples/E2E_DCS_Test/PanelGroup",
"Firmware/Examples/E2E_DCS_Test/PanelGroupInputs",
"Firmware/Examples/E2E_DCS_Test/SimGateway",
"Firmware/Panels/Center_Armament",
"Firmware/Panels/PDU_Center",
"Firmware/Panels/PDU_Left",
"Firmware/Panels/PDU_Right",
"Firmware/Templates/PanelBridge",
"Firmware/Templates/PanelGroup",
"Firmware/Templates/SimGateway",
"Firmware/Tests/ActionButton",
"Firmware/Tests/AnalogInput",
"Firmware/Tests/AnalogMultiPos",
"Firmware/Tests/AngleSensorInput",
"Firmware/Tests/CANProtocol",
"Firmware/Tests/Dimmer",
"Firmware/Tests/DrumDisplay",
"Firmware/Tests/IntegerOutput",
"Firmware/Tests/LED",
"Firmware/Tests/NeedleGauge",
"Firmware/Tests/PanelBridge",
"Firmware/Tests/PanelGroup",
"Firmware/Tests/PinRef",
"Firmware/Tests/PowerMonitor",
"Firmware/Tests/RotaryAcceleratedEncoder",
"Firmware/Tests/RotaryEncoder",
"Firmware/Tests/STM32Board",
"Firmware/Tests/ShiftBus",
"Firmware/Tests/SimGateway",
"Firmware/Tests/StepperMotor",
"Firmware/Tests/Switch2Pos",
"Firmware/Tests/Switch3Pos",
"Firmware/Tests/SwitchMultiPos",
"Firmware/Tests/SwitchWithCover2Pos"
],
"HIDControls": [
"Firmware/Examples/E2E_DCS_Test/PanelBridge",
"Firmware/Examples/E2E_DCS_Test/PanelGroup",
Expand Down
16 changes: 15 additions & 1 deletion .github/workflows/a4ec-refresh.yml
Original file line number Diff line number Diff line change
Expand Up @@ -82,24 +82,35 @@ jobs:
fi

cp "$json_path" tools/gen_a4ec/data/A-4E-C.jsonp
# Every aircraft's JSON, for the reserved-address warning (#295).
modules_dir="$(find "$extract_dir" -type d -path '*/doc/json' -print -quit)"

{
echo "tag=$tag"
echo "safe_tag=$safe_tag"
echo "published_at=$published_at"
echo "source_ref=$source_ref"
echo "json_path=$json_path"
echo "modules_dir=$modules_dir"
} >> "$GITHUB_OUTPUT"

echo "Using DCS-BIOS $tag from $source_ref"
echo "A-4E-C metadata: $json_path"

- name: Regenerate A4EC metadata
env:
MODULES_DIR: ${{ steps.release.outputs.modules_dir }}
run: |
# The reserved-address warning (#295) needs every aircraft's JSON; skip it if the
# archive has none rather than fail the refresh.
extra=()
if [ -n "$MODULES_DIR" ]; then extra=(--modules-dir "$MODULES_DIR"); fi
python tools/gen_a4ec/gen_a4ec.py \
--source tools/gen_a4ec/data/A-4E-C.jsonp \
--source-label "DCS-BIOS ${{ steps.release.outputs.tag }} (${{ steps.release.outputs.source_ref }})" \
--timestamp "${{ steps.release.outputs.published_at }}"
--timestamp "${{ steps.release.outputs.published_at }}" \
--change-report "$RUNNER_TEMP/a4ec-changes.md" \
"${extra[@]}"

- name: Test A4EC generator
run: |
Expand Down Expand Up @@ -197,6 +208,9 @@ jobs:
- A4EC generator pytest suite
- PlatformIO build for projects that reference `A4EC`
BODY
# Removed or moved symbols break sketches built against the old headers; the generator
# lists them so the maintainer decides the version bump (#295). Additions only is normal.
{ echo; cat "$RUNNER_TEMP/a4ec-changes.md"; } >> "$body_file"

existing_pr="$(gh pr list \
--repo "$GITHUB_REPOSITORY" \
Expand Down
4 changes: 3 additions & 1 deletion .github/workflows/release-please.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,10 @@ jobs:
manifest-file: .release-please-manifest.json

firmware-dependencies:
# Third-party versions are declared as ranges (^2.0.0, >=3.0.0), so
# Third-party libraries are declared as caret ranges (^2.0.0, ^3.7.7), so
# record what a release actually resolved to rather than pinning them.
# (The RP2040 platform is the exception: a git URL has no range, so it is
# pinned to a commit.)
name: Record third-party versions
needs: release-please
if: needs.release-please.outputs.firmware_released == 'true'
Expand Down
5 changes: 1 addition & 4 deletions Firmware/Examples/E2E_DCS_Test/PanelBridge/platformio.ini
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,6 @@ build_flags =
-DHAL_CAN_MODULE_ENABLED
-DUSB_NONE
-DHSE_VALUE=8000000
; Report connected PanelGroup nodes to the host over DCS-BIOS (#86).
; Client half shipped (SkyHawkClient#16), so it is on by default now.
-DPANELBRIDGE_NODE_STATUS

lib_extra_dirs = ${PROJECT_DIR}/../../../Libraries
lib_deps =
Expand All @@ -26,7 +23,7 @@ lib_deps =
PanelBridge
CANProtocol
A4EC
dcs-skunkworks/DCS-BIOS
dcs-skunkworks/DCS-BIOS@^0.3.10

upload_protocol = stlink
; Some STM32F103CB clones require the CPUTAPID override below.
Expand Down
2 changes: 1 addition & 1 deletion Firmware/Examples/E2E_DCS_Test/PanelGroup/src/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ RotaryEncoder destLat (DCSIN_DEST_LAT_KNB, PinRef(PA8), PinRef(PB5), Encode
RotaryEncoder freq10 (DCSIN_ARC51_FREQ_10MHZ, PinRef(PB3), PinRef(PB4), EncoderStepsPerDetent::Four, EncoderMode::Dir);
// hysteresis 1024 (≈64 steps over the throw) — default 128 floods DCS-BIOS import on a sweep (~125
// cmd/s) and lags the cockpit seconds behind; 1024 cuts that ~5× while staying smooth for a vol knob.
AnalogInput arcVol (DCSIN_ARC51_VOL, PinRef(PA2), /*reverse=*/false, 0, 65535, /*hysteresis=*/1024);
AnalogInput arcVol (DCSIN_ARC51_VOL, PinRef(PA2)); // DCSIN → hysteresis 1024 by default
AnalogMultiPos arcMode (DCSIN_ARC51_MODE, PinRef(PA3), 4);

void setup() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ using namespace OpenSkyhawk;

RotaryEncoder destLat (DCSIN_DEST_LAT_KNB, PinRef(PA8), PinRef(PB5), EncoderStepsPerDetent::Four, EncoderMode::Rel, /*step=*/1600);
RotaryEncoder freq10 (DCSIN_ARC51_FREQ_10MHZ, PinRef(PB3), PinRef(PB4), EncoderStepsPerDetent::Four, EncoderMode::Dir);
AnalogInput arcVol (DCSIN_ARC51_VOL, PinRef(PA2), /*reverse=*/false, 0, 65535, /*hysteresis=*/1024);
AnalogInput arcVol (DCSIN_ARC51_VOL, PinRef(PA2)); // DCSIN → hysteresis 1024 by default
AnalogMultiPos arcMode (DCSIN_ARC51_MODE, PinRef(PA3), 4);

void setup() {
Expand Down
4 changes: 2 additions & 2 deletions Firmware/Examples/E2E_DCS_Test/SimGateway/platformio.ini
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[env:E2E_SimGateway]
platform = https://github.com/maxgerhardt/platform-raspberrypi.git
platform = https://github.com/maxgerhardt/platform-raspberrypi.git#79ce473e1a8010ed8096388690222fd2c94858a6
; Change board to match your module: rpipico, rpipico2, waveshare_rp2040_zero, etc.
board = rpipico
board_build.core = earlephilhower
Expand All @@ -13,6 +13,6 @@ lib_extra_dirs = ${PROJECT_DIR}/../../../Libraries
lib_deps =
HIDControls
SimGateway
adafruit/Adafruit TinyUSB Library
adafruit/Adafruit TinyUSB Library@^3.7.7

monitor_speed = 250000
14 changes: 14 additions & 0 deletions Firmware/Libraries/CANProtocol/CANProtocol.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

#include "CANProtocol.h"
#include <STM32Board.h>
#include <FirmwareVersion.h> // OPENSKYHAWK_FW_* — the release version READY_n carries (#295)
#include <Arduino.h>
#include <string.h>

Expand Down Expand Up @@ -464,6 +465,19 @@ NodeHealthPayload makeNodeHealthPayload(uint8_t nodeId, NodeKind kind, int8_t di
return p;
}

ReadyPayload makeReadyPayload() {
ReadyPayload p = {};
p.fwMajor = OPENSKYHAWK_FW_MAJOR;
p.fwMinor = OPENSKYHAWK_FW_MINOR;
p.fwPatch = OPENSKYHAWK_FW_PATCH;
return p;
}

void sendReady() {
const ReadyPayload p = makeReadyPayload();
send(canIdReady(NODE_ID), reinterpret_cast<const uint8_t*>(&p), sizeof(p));
}

uint32_t txDropCount() { return _txDrops; }

const NodeHealthPayload& lastNodeHealth() { return _lastHealth; }
Expand Down
41 changes: 40 additions & 1 deletion Firmware/Libraries/CANProtocol/CANProtocol.h
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@
* Dependency: STM32Board::begin() must be called before CANProtocol::start().
* CANProtocol owns CAN bus operation; STM32Board owns peripheral hardware init.
*
* @version 0.3.0
* @copyright GPL-2.0-only — see Firmware/LICENSE
*/

Expand Down Expand Up @@ -105,6 +104,22 @@ struct __attribute__((packed)) TelemPayload {
};
static_assert(sizeof(TelemPayload) == 8, "TelemPayload must fill one classic CAN frame");

/**
* @brief 8-byte payload carried by READY_n — the node's boot announcement (#295).
*
* Sent once per boot, after the boot EVT burst, so every reflash, reset or power cycle announces
* the firmware the node now runs. PanelBridge caches the version per node and reports it in
* `_NODE_STATUS` (v4). Firmware older than this payload sends READY_n empty (DLC 0): the bridge
* then reports the version as not reported (0.0.0).
*/
struct __attribute__((packed)) ReadyPayload {
uint8_t fwMajor; ///< 0: firmware release version (FirmwareVersion.h)
uint8_t fwMinor; ///< 1
uint8_t fwPatch; ///< 2
uint8_t rsvd[5]; ///< 3-7: reserved, transmit 0
};
static_assert(sizeof(ReadyPayload) == 8, "ReadyPayload must fill one classic CAN frame");

/** @brief CAN bus health states. Reported to STM32Board via onStatusChange(). */
enum class CanStatus {
STARTING, ///< CAN peripheral configured, not yet started
Expand Down Expand Up @@ -161,6 +176,10 @@ constexpr uint32_t canIdEcho(uint8_t n) { return 0x300 + n; }
/** @brief Boot-complete READY frame ID for node n. Range 0x401-0x43F. */
constexpr uint32_t canIdReady(uint8_t n) { return 0x400 + n; }

// Reserved, not yet assigned (#295): 0x740-0x7BF for a future boot/update frame family —
// BOOT_CMD_n = 0x740+n (bridge → node n), BOOT_RSP_n = 0x780+n (node n → bridge). The lowest
// priority on the bus, so update traffic never delays a control. Don't allocate it to anything else.

// ── controlId namespace ───────────────────────────────────────────────────────
// Payload controlIds live inside ControlPacket data bytes. They are intentionally
// separate from the 11-bit CAN arbitration IDs above.
Expand Down Expand Up @@ -191,6 +210,11 @@ using CanRxCallback = void(*)(uint32_t canId, const uint8_t* data, uint8_t
using CanSyncReqCallback = void(*)();

// ── Runtime API (STM32 only) ──────────────────────────────────────────────────
//
// Library-internal: the node libraries (PanelGroup, PanelBridge, PduNode) drive the bus through
// these, not sketches. Not covered by the compatibility policy (#295) — except sendBatched(), which
// a custom control class uses to send its value (with ControlPacket and the canIdEvt* functions
// above, which are covered with the wire format).

#ifdef ARDUINO_ARCH_STM32

Expand Down Expand Up @@ -379,6 +403,21 @@ namespace CANProtocol {
NodeHealthPayload makeNodeHealthPayload(uint8_t nodeId, NodeKind kind, int8_t dieTempC,
NodeFaultCode fault = NodeFaultCode::NONE);

/**
* @brief Build the 8-byte READY_n payload: this build's firmware release version (#295).
* @return ReadyPayload with fwMajor/fwMinor/fwPatch from FirmwareVersion.h, reserved bytes 0.
*/
ReadyPayload makeReadyPayload();

/**
* @brief Send this node's READY_n (`canIdReady(NODE_ID)`) carrying makeReadyPayload().
*
* Call once at the end of the node's setup(), after its boot EVT burst. PanelBridge marks the
* node alive, caches its version and broadcasts SYNC_REQ. Bounded retry like every other
* special frame (02-can-protocol.md).
*/
void sendReady();

/** @brief Return the cumulative TX queue drop count since startup. */
uint32_t txDropCount();

Expand Down
3 changes: 3 additions & 0 deletions Firmware/Libraries/CANProtocol/library.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@
"ststm32"
],
"dependencies": [
{
"name": "FirmwareVersion"
},
{
"name": "HIDControls"
},
Expand Down
1 change: 0 additions & 1 deletion Firmware/Libraries/DrumDisplay/DrumDisplay.h
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@
* construct N DrumDisplay instances sharing one I2cMux, each with its own channel; the class
* re-selects its channel before every buffer send.
*
* @version 0.1.0
* @copyright GPL-2.0-only — see Firmware/LICENSE
*/

Expand Down
20 changes: 20 additions & 0 deletions Firmware/Libraries/FirmwareVersion/FirmwareVersion.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/**
* @file FirmwareVersion.h
* @brief The firmware release version — one source for every board that reports it.
*
* Every board is built from the same release, so there is one version, not one per library.
* It goes on the wire in two places: SimGateway's calibration HELLO_ACK, and each STM32 node's
* READY_n payload (#295). release-please rewrites the three numbers below when a firmware release
* PR merges (generic updater, `x-release-please-*` markers; see release-please-config.json) —
* never edit them by hand.
*
* Shared between STM32 and RP2040. Contains only `#define` constants.
*
* @copyright GPL-2.0-only — see Firmware/LICENSE
*/

#pragma once

#define OPENSKYHAWK_FW_MAJOR 0 // x-release-please-major
#define OPENSKYHAWK_FW_MINOR 1 // x-release-please-minor
#define OPENSKYHAWK_FW_PATCH 0 // x-release-please-patch
10 changes: 10 additions & 0 deletions Firmware/Libraries/FirmwareVersion/library.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"name": "FirmwareVersion",
"version": "0.1.0",
"description": "The OpenSkyhawk firmware release version, one source for every board",
"frameworks": "arduino",
"platforms": "*",
"build": {
"srcFilter": "-<*>"
}
}
1 change: 0 additions & 1 deletion Firmware/Libraries/HIDControls/HIDControls.h
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
* 0x0030–0x00AF HID buttons — routed to button setters on SimGateway
* 0x00B0–0x00FF Reserved HID expansion slots
*
* @version 0.1.0
* @copyright GPL-2.0-only — see Firmware/LICENSE
*/

Expand Down
24 changes: 16 additions & 8 deletions Firmware/Libraries/NodeStatus/NodeStatus.h
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@
* frame struct) stays in `CANProtocol.h`; the fault *detail* strings stay local (DiagSerial),
* never on the wire. Header-only contract + a tiny `FaultSource` registry (`NodeStatus.cpp`).
*
* @version 0.1.0
* @copyright GPL-2.0-only — see Firmware/LICENSE
*/

Expand All @@ -33,9 +32,14 @@
// ── _NODE_STATUS DCS-BIOS host-reporting contract (#86) ───────────────────────
// PanelBridge reports connected nodes + health to the host over DCS-BIOS. Bump
// NODE_STATUS_PROTO_VERSION on any _NODE_STATUS wire change so the client's sync fails loudly.
// Full 28-hex field decode: FirmwarePlan/04-dcs-bios-integration.md.
#define NODE_STATUS_PROTO_VERSION 3
#define NODE_STATUS_REQ_ADDR 0x86FE // host→device roster-request export address
// Full 34-hex field decode: FirmwarePlan/04-dcs-bios-integration.md.
#define NODE_STATUS_PROTO_VERSION 4
// Host→device roster-request export address (#295, v4). No DCS-BIOS module exports here: every
// aircraft ends by 0xB2FB and MetadataEnd is 0xFFFE (checked against DCS-BIOS v0.11.8). The old
// 0x86FE is a live address for the AH-64D and the Mirage F1, so flying either answered every
// export of that control with a roster burst. The A4EC refresh warns if a new DCS-BIOS release ever
// puts an aircraft here (tools/gen_a4ec, --modules-dir).
#define NODE_STATUS_REQ_ADDR 0xFF00
#define NODE_STATUS_MSG_NAME "_NODE_STATUS"
#define NODE_STATUS_END_MSG_NAME "_NODE_STATUS_END"

Expand All @@ -47,17 +51,18 @@
*/
constexpr uint8_t GATEWAY_NODE_ID = 0;

/// `_NODE_STATUS` row length in bytes, sent as twice as many hex chars (proto v3: 14 → 28 hex).
constexpr uint8_t NODE_STATUS_ROW_BYTES = 14;
/// `_NODE_STATUS` row length in bytes, sent as twice as many hex chars (proto v4: 17 → 34 hex).
constexpr uint8_t NODE_STATUS_ROW_BYTES = 17;

/**
* @brief `_NODE_STATUS` row field offsets, in bytes of the decoded row (hex index = 2 × offset).
*
* Each field is its numeric value as fixed-width uppercase hex, most-significant byte first, so
* the u16 fields read big-endian here although they are little-endian on CAN. A field runs to the
* next offset; the last runs to NODE_STATUS_ROW_BYTES. The first six come from the node's HB_n
* (plus `present`), the last five from its HEALTH_n. (Not `NODE_ID`: every node build defines
* that as a macro.)
* (plus `present`), the next five from its HEALTH_n, the last three from its READY_n (v4, #295).
* Fields are only ever appended: a reader parses the fields it knows from a longer row and ignores
* the rest. (Not `NODE_ID`: every node build defines that as a macro.)
*/
enum class NodeStatusField : uint8_t {
ID = 0, // u8: node ID; GATEWAY_NODE_ID = the bridge's own row, 1-63 = CAN nodes
Expand All @@ -71,6 +76,9 @@ enum class NodeStatusField : uint8_t {
FAULT_MASK = 11, // u8: reserved, 00
FAULT_ID = 12, // u8: NodeFaultCode
NODE_KIND = 13, // u8: NodeKind
FW_MAJOR = 14, // u8: firmware release version from the node's READY_n; 00 00 00 = not reported
FW_MINOR = 15, // u8
FW_PATCH = 16, // u8
};

// ── _NODE_TELEM DCS-BIOS host-reporting contract (#216) ───────────────────────
Expand Down
Loading
Loading