Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
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
59 changes: 58 additions & 1 deletion .github/workflows/firmware.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,14 @@ on:
paths:
- 'firmware/**'
- '.github/workflows/firmware.yml'
- 'software/control/protocol_v2/**'
- 'software/tools/gen_protocol_golden.py'
pull_request:
paths:
- 'firmware/**'
- '.github/workflows/firmware.yml'
- 'software/control/protocol_v2/**'
- 'software/tools/gen_protocol_golden.py'

jobs:
build-and-test:
Expand All @@ -27,14 +31,67 @@ jobs:
- name: Install PlatformIO
run: pip install platformio

- name: Build controller firmware (Teensy 4.1)
- name: Build controller firmware (Teensy 4.1, Squid v1 board)
run: pio run -e teensy41
working-directory: ./firmware/controller

- name: Build controller firmware (Teensy 4.1, Squid v2 board)
run: pio run -e teensy41_boardv2
working-directory: ./firmware/controller

- name: Build joystick firmware (Teensy LC)
run: pio run -e teensyLC
working-directory: ./firmware/joystick

- name: Run unit tests
run: pio test -e native
working-directory: ./firmware/controller

- name: Check protocol-v2 golden vectors are up to date
run: |
python software/tools/gen_protocol_golden.py
git diff --exit-code \
software/tests/data/protocol_v2_golden.json \
firmware/controller/test/test_golden/golden_cases.h

static-analysis:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4.2.2

- name: Install static-analysis tools
run: sudo apt-get update && sudo apt-get install -y cppcheck clang-tidy

- name: cppcheck (protocol + boot + hal core)
working-directory: ./firmware/controller
run: |
cppcheck --std=c++11 --enable=warning,performance,portability,style \
--inline-suppr --suppress=missingIncludeSystem --error-exitcode=1 \
-I src src/protocol src/boot/boot.cpp src/hal/boards

- name: clang-tidy (pure protocol + boot + hal sources)
working-directory: ./firmware/controller
run: |
clang-tidy \
src/protocol/claims.cpp src/protocol/cobs.cpp src/protocol/crc16.cpp \
src/protocol/framer.cpp src/protocol/slots.cpp src/protocol/dispatch_v2.cpp \
src/boot/boot.cpp \
src/hal/boards/board_squid_v1.cpp src/hal/boards/board_squid_v2.cpp \
-- -std=c++11 -Isrc

fuzz:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4.2.2

- name: Install clang (with libFuzzer)
run: sudo apt-get update && sudo apt-get install -y clang

- name: Build + run libFuzzer over the RX->dispatch->TX path (60s)
working-directory: ./firmware/controller
run: |
clang++ -std=c++11 -g -fsanitize=address,fuzzer -I src \
fuzz/fuzz_framer.cpp src/protocol/*.cpp -o fuzz_framer
./fuzz_framer -max_total_time=60 -timeout=10 -rss_limit_mb=2048
59 changes: 44 additions & 15 deletions firmware/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -151,29 +151,58 @@ PLATFORMIO_BUILD_FLAGS="-DDISABLE_LASER_INTERLOCK" pio run -e teensy41 -t upload

```
controller/
├── main_controller_teensy41.ino # Entry point
├── platformio.ini # PlatformIO config
├── main_controller_teensy41.ino # Entry point (v1 protocol — still the live path)
├── platformio.ini # PlatformIO config (teensy41, teensy41_boardv2, native)
├── fuzz/
│ └── fuzz_framer.cpp # libFuzzer/ASAN harness for the protocol-v2 path
├── test/ # Unit tests (run with pio test -e native)
│ ├── test_crc8/ # CRC8 checksum tests
│ └── test_protocol/ # Protocol/command ID tests
│ ├── test_crc8/ # CRC8 checksum tests (v1)
│ ├── test_protocol/ # Protocol/command ID tests (v1)
│ ├── test_crc16/ test_cobs/ # protocol-v2 framing codec
│ ├── test_frames/ # protocol-v2 wire-struct layout
│ ├── test_framer/ # protocol-v2 COBS framer
│ ├── test_claims/ test_slots/ # protocol-v2 claims + slot manager
│ ├── test_dispatch/ # protocol-v2 dispatcher + system commands
│ ├── test_boot/ # boot/fault module core
│ ├── test_board/ test_board_v2/ # board descriptors (v1/v2)
│ └── test_golden/ # C<->Python golden vectors (generated)
└── src/
├── commands/ # Command handlers
│ ├── commands.cpp/h # General commands
│ ├── light_commands.cpp/h # Illumination control
│ └── stage_commands.cpp/h # Motion control
├── def/
│ └── def_v1.h # Hardware configuration
├── commands/ # Command handlers (v1)
├── def/ # Hardware configuration (v1)
├── tmc/ # TMC stepper driver library
├── utils/
│ └── crc8.cpp/h # CRC calculation
├── init.cpp/h # Initialization routines
├── operations.cpp/h # Main loop operations
├── serial_communication.cpp/h # Serial protocol handling
├── utils/ # crc8 and other pure utilities
├── protocol/ # protocol-v2 core (NOT yet wired to serial — Phase C)
│ ├── crc16, cobs, frames # CRC-16/CCITT-FALSE, COBS codec, wire contract
│ ├── framer # COBS framer (resync + non-blocking TX)
│ ├── claims, claims_table # resource-claims table + conflict checker
│ ├── slots # 5-slot manager + completion ring (RETRY dedup)
│ └── dispatch_v2 # claims-gated dispatcher + HELLO/GET_INFO/GET_STATE/DIAG
├── boot/ # boot/fault module (NOT yet wired — Phase C)
│ ├── boot.cpp/h # watchdog/safe-state/reset-cause/nonce/fault-ring (native-tested)
│ └── boot_bind_teensy41.cpp # RT1062 binding (WDOG1/SRC_SRSR/EEPROM/DWT; teensy41 build only)
├── hal/ # board profiles (compile-time selected)
│ ├── board.h # GET_INFO descriptor + board-scoped pin constants
│ └── boards/ # board_squid_v1.cpp, board_squid_v2.cpp
├── init.cpp/h # Initialization routines (v1)
├── operations.cpp/h # Main loop operations (v1)
├── serial_communication.cpp/h # Serial protocol handling (v1 — the live path)
├── functions.cpp/h # Utility functions
├── globals.cpp/h # Global state variables
└── constants.h # Constants and pin definitions
```

### Protocol v2 (Phase B — native-tested, not yet live)

`src/protocol/`, `src/boot/`, and `src/hal/` implement the protocol-v2 core
(COBS + CRC-16 framing, claims-gated 5-slot command dispatch with a completion
ring, system commands, and per-board GET_INFO descriptors). These modules
compile into the firmware binary but are **not wired to `SerialUSB`** — the v1
protocol in `serial_communication.cpp` remains the live path. Phase C performs
the single-PR switchover. The mirrored host codec lives in
`software/control/protocol_v2/`, and C↔Python agreement is enforced by the
golden vectors in `test/test_golden/` (regenerate with
`software/tools/gen_protocol_golden.py`).

## Joystick

Control panel firmware for Teensy LC. Handles:
Expand Down
11 changes: 11 additions & 0 deletions firmware/controller/.clang-tidy
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Static-analysis gate for the protocol-v2 firmware core (design D9 robustness).
# Focused, high-signal checks; CI runs it over src/protocol, src/boot, src/hal.
# WarningsAsErrors makes any enabled diagnostic fail CI.
Checks: >
clang-analyzer-*,
bugprone-*,
performance-*,
-bugprone-easily-swappable-parameters
WarningsAsErrors: '*'
HeaderFilterRegex: 'src/(protocol|boot|hal)/'
FormatStyle: none
144 changes: 144 additions & 0 deletions firmware/controller/fuzz/fuzz_framer.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
/**
* libFuzzer harness for the protocol-v2 RX -> dispatch -> TX path.
*
* Arbitrary bytes are fed into a Framer whose FrameSink is a Dispatcher backed
* by fake state; the dispatcher's responses are re-framed back out. This
* exercises COBS decode, CRC check, command dispatch, slot management, and
* COBS encode against untrusted input. ASAN + libFuzzer assert no crash / UB.
*
* CI runs the coverage-guided libFuzzer build. Locally (Apple clang ships no
* libFuzzer runtime) build with -DFUZZ_STANDALONE for an ASAN smoke driver,
* linking fuzz_framer.cpp against the src/protocol sources under
* -fsanitize=address -I src.
*/

#include <stddef.h>
#include <stdint.h>
#include <string.h>

#include "protocol/dispatch_v2.h"
#include "protocol/frames.h"
#include "protocol/framer.h"
#include "protocol/slots.h"

using namespace protocol;

namespace {

class ZeroProvider : public StateProvider {
public:
void fill_state(StandardResponse&) override {}
void fill_hello(HelloPayload& h) override { memset(&h, 0, sizeof(h)); }
void fill_info(InfoPayload& i) override { memset(&i, 0, sizeof(i)); }
void fill_diag_page0(DiagPayload& d) override { memset(&d, 0, sizeof(d)); }
uint8_t fill_diag_faults(uint8_t, FaultEntryWire*, uint8_t) override { return 0; }
};

class NullByteSink : public ByteSink {
public:
size_t avail;
NullByteSink() : avail(4096) {}
size_t writable() override { return avail; }
void write(const uint8_t*, size_t) override {}
};

void noop_handler(Dispatcher&, const uint8_t*, size_t, ResponseWriter&) {}

} // namespace

extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) {
ZeroProvider provider;
SlotManager slots;
Dispatcher dispatcher(slots, provider);
dispatcher.register_system_commands();
dispatcher.register_command(0x01, false, 0, 64, noop_handler); // a slotted command

NullByteSink out;
Framer framer(dispatcher, out);
dispatcher.set_framer(framer);

// Let the input steer TX backpressure so send_frame's drop path is reached.
if (size > 0) {
out.avail = (data[0] & 1) ? 0 : 4096;
}
for (size_t i = 0; i < size; ++i) {
framer.feed_rx(data[i]);
}
return 0;
}

#ifdef FUZZ_STANDALONE
// Local ASAN smoke driver (no libFuzzer runtime on Apple clang).
#include <stdio.h>

#include "protocol/cobs.h"
#include "protocol/crc16.h"

namespace {

uint32_t g_lcg = 0x1234567u;

uint32_t lcg_next() {
g_lcg = g_lcg * 1103515245u + 12345u;
return (g_lcg >> 8) & 0xFFFFFF;
}

void feed_valid_and_corruptions(uint8_t type, uint8_t id, uint8_t ct, uint8_t fl,
const uint8_t* pl, size_t pn) {
uint8_t frame[128];
frame[0] = type;
frame[1] = id;
frame[2] = ct;
frame[3] = fl;
if (pn) {
memcpy(frame + 4, pl, pn);
}
size_t flen = 4 + pn;
uint16_t crc = crc16_ccitt(frame, flen);
frame[flen] = (uint8_t)(crc & 0xFF);
frame[flen + 1] = (uint8_t)(crc >> 8);

uint8_t wire[160];
size_t enc = cobs_encode(frame, flen + 2, wire, sizeof(wire));
wire[enc] = 0x00;
size_t wlen = enc + 1;

LLVMFuzzerTestOneInput(wire, wlen);
for (size_t p = 0; p < wlen; ++p) { // single-byte corruptions
uint8_t save = wire[p];
wire[p] ^= 0xFF;
LLVMFuzzerTestOneInput(wire, wlen);
wire[p] = save;
}
uint8_t dbl[320]; // back-to-back frames
memcpy(dbl, wire, wlen);
memcpy(dbl + wlen, wire, wlen);
LLVMFuzzerTestOneInput(dbl, 2 * wlen);
}

} // namespace

int main() {
LLVMFuzzerTestOneInput(nullptr, 0);

uint8_t buf[640];
for (int iter = 0; iter < 100000; ++iter) {
size_t n = lcg_next() % (sizeof(buf) + 1);
for (size_t i = 0; i < n; ++i) {
buf[i] = (uint8_t)lcg_next();
}
LLVMFuzzerTestOneInput(buf, n);
}

const uint8_t p_diag[] = {0x00};
const uint8_t p_cmd[] = {0x11, 0x22, 0x33};
feed_valid_and_corruptions(REQUEST, 1, GET_STATE, 0, nullptr, 0);
feed_valid_and_corruptions(REQUEST, 2, HELLO, 0, nullptr, 0);
feed_valid_and_corruptions(REQUEST, 3, DIAG, 0, p_diag, sizeof(p_diag));
feed_valid_and_corruptions(REQUEST, 4, 0x01, 0, p_cmd, sizeof(p_cmd));
feed_valid_and_corruptions(REQUEST, 4, 0x01, FLAG_RETRY, p_cmd, sizeof(p_cmd));

printf("standalone fuzz driver: OK (no crash / no ASAN finding)\n");
return 0;
}
#endif // FUZZ_STANDALONE
15 changes: 14 additions & 1 deletion firmware/controller/platformio.ini
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,12 @@ build_flags =
-D ARDUINO_TEENSY41
-I src

; Exclude test directory from firmware build
; Exclude test directory + the OTHER board profile from this build
; (board_squid_v1.cpp is the default; see env:teensy41_boardv2 for v2).
build_src_filter =
+<*>
-<test/>
-<src/hal/boards/board_squid_v2.cpp>

; Library dependencies
lib_deps =
Expand All @@ -31,6 +33,17 @@ upload_protocol = teensy-gui
; Monitor settings
monitor_speed = 2000000

; Squid v2 board: same as teensy41 but selects the v2 board profile.
[env:teensy41_boardv2]
extends = env:teensy41
build_flags =
${env:teensy41.build_flags}
-DBOARD_SQUID_V2
build_src_filter =
+<*>
-<test/>
-<src/hal/boards/board_squid_v1.cpp>

; Native test environment (runs on host machine)
[env:native]
platform = native
Expand Down
Loading
Loading