Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
55 changes: 53 additions & 2 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,8 +106,8 @@ Always run builds from the `firmware/` directory – `platformio.ini` is there.

### Static Analysis (cppcheck)
- CI runs cppcheck with `--enable=warning,style,performance,portability`
- Suppressed globally: `missingIncludeSystem`, `missingInclude`, `constParameterPointer`, `constParameterCallback`
- Use `// cppcheck-suppress <id>` for inline suppressions when needed
- Suppressed globally: `missingIncludeSystem`, `missingInclude`
- Use `// cppcheck-suppress <id>` for inline suppressions when needed (e.g. `constParameterCallback` for ESP-IDF callbacks)

## Key Architecture Details

Expand Down Expand Up @@ -172,6 +172,57 @@ After ANY code change, verify at minimum:
1. `pio test -e native` – all 90 tests pass
2. `pio run -e receiver` – compiles
3. `pio run -e transmitter` – compiles
4. **clang-format** – all changed files must be formatted before committing
5. **cppcheck** – no new warnings allowed

### clang-format (mandatory)

CI **rejects** any PR with formatting violations. Always run clang-format **19**
on every touched `.cpp` / `.h` / `.c` file before committing:

```bash
# Format a single file
clang-format -i --style=file firmware/src/receiver/web/ReceiverApi.cpp

# Format all project sources
find firmware/src firmware/lib \
\( -name '*.cpp' -o -name '*.h' -o -name '*.c' \) \
! -path '*/.*' \
-exec clang-format -i --style=file {} +
```

The `.clang-format` file in the repo root defines the style. Do **not** override
it with `--style=…` other than `--style=file`.

### cppcheck (mandatory)

CI **rejects** any PR that introduces new cppcheck warnings. Run locally:

```bash
cppcheck \
--error-exitcode=1 \
--enable=warning,style,performance,portability \
--suppress=missingIncludeSystem \
--suppress=missingInclude \
--suppress=unmatchedSuppression \
--check-level=exhaustive \
--inline-suppr \
-I firmware/src/receiver \
-I firmware/src/transmitter \
-I firmware/lib/odh-protocol \
-I firmware/lib/odh-config \
-I firmware/lib/odh-radio \
-I firmware/lib/odh-telemetry \
-I firmware/lib/odh-web \
firmware/src \
firmware/lib
```

If a warning is a false positive, suppress it inline:
```cpp
// cppcheck-suppress constParameterCallback
void onData(uint8_t *mac, uint8_t *data, int len) { … }
```

## Common Pitfalls

Expand Down
3 changes: 1 addition & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -196,9 +196,8 @@ jobs:
--enable=warning,style,performance,portability \
--suppress=missingIncludeSystem \
--suppress=missingInclude \
--suppress=constParameterPointer \
--suppress=constParameterCallback \
--suppress=unmatchedSuppression \
--check-level=exhaustive \
--inline-suppr \
-I firmware/src/receiver \
-I firmware/src/transmitter \
Expand Down
1 change: 1 addition & 0 deletions firmware/lib/odh-radio/ReceiverRadioLink.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ namespace odh {
static volatile int8_t sPromiscRssi = 0;

#ifndef NATIVE_SIM
// cppcheck-suppress constParameterCallback
static void promiscRxCb(void *buf, wifi_promiscuous_pkt_type_t /*type*/) {
const auto *pkt = static_cast<const wifi_promiscuous_pkt_t *>(buf);
sPromiscRssi = pkt->rx_ctrl.rssi;
Expand Down
5 changes: 3 additions & 2 deletions firmware/lib/odh-radio/TransmitterRadioLink.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
#include <Arduino.h>
#include <WiFi.h>

#include <algorithm>
#include <cstring>

namespace odh {
Expand Down Expand Up @@ -45,7 +46,7 @@ void TransmitterRadioLink::startScanning() {
_bound = false;
_scanning = true;
_discoveredCount = 0;
std::memset(_discovered, 0, sizeof(_discovered));
std::fill(std::begin(_discovered), std::end(_discovered), DiscoveredVehicle{});
}

const DiscoveredVehicle *TransmitterRadioLink::discoveredVehicle(uint8_t index) const {
Expand All @@ -63,7 +64,7 @@ void TransmitterRadioLink::pruneStaleVehicles(uint32_t timeoutMs) {
_discovered[j] = _discovered[j + 1];
}
_discoveredCount--;
std::memset(&_discovered[_discoveredCount], 0, sizeof(DiscoveredVehicle));
_discovered[_discoveredCount] = DiscoveredVehicle{};
} else {
i++;
}
Expand Down
70 changes: 52 additions & 18 deletions firmware/platformio.ini
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@
; native – Host unit tests (no hardware)
; sim_rx – Linux receiver simulation (terminal)
; sim_tx – Linux transmitter simulation (SDL2 display)
;
; Inheritance:
; esp32_base → receiver, transmitter
; native_base → native, sim_base
; sim_base → sim_rx, sim_tx
; ═══════════════════════════════════════════════════════════════════════

[platformio]
Expand All @@ -25,10 +30,42 @@ upload_speed = 921600

build_flags =
-std=gnu++2a
-Wall
-Wextra
-Werror
; ESP-IDF framework headers (gpio_ll.h, soc_memory_types.h) have unused params
; in inline functions – cannot fix, must relax for ESP32 builds.
-Wno-error=unused-parameter
; Third-party libraries (TFT_eSPI, etc.) trigger these.
-Wno-error=missing-field-initializers
-Wno-error=unused-variable
-DCORE_DEBUG_LEVEL=3

build_unflags = -std=gnu++11

; ── Shared native settings (tests + simulations) ────────────────────────
[native_base]
platform = native

build_flags =
-std=gnu++20
-Wall
-Wextra
-Werror

; ── Shared simulation settings ───────────────────────────────────────────
[sim_base]
extends = native_base
extra_scripts = pre:sim/sim_build.py

build_flags =
${native_base.build_flags}
-DNATIVE_SIM
-lpthread

Copilot AI Mar 10, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In sim_base, -lpthread is included in build_flags while native_base enables -Werror. PlatformIO applies build_flags during compilation as well as linking, so passing -lpthread on compile-only steps commonly triggers an “unused command line argument” warning that becomes an error with -Werror. Prefer -pthread (works for both compile+link) or move this into a dedicated link flag setting so native/sim builds don’t fail.

Suggested change
-lpthread
-pthread

Copilot uses AI. Check for mistakes.

lib_deps =
bblanchon/ArduinoJson @ ^7.0.0

; ── Receiver ─────────────────────────────────────────────────────────────
[env:receiver]
extends = esp32_base
Expand Down Expand Up @@ -69,46 +106,43 @@ board_build.filesystem = littlefs

; ── Native unit tests ────────────────────────────────────────────────────
[env:native]
platform = native
build_flags = -std=gnu++20 -DNATIVE_TEST
extends = native_base
build_flags =
${native_base.build_flags}
-DNATIVE_TEST
build_src_filter = -<*>
test_build_src = no

; ── Simulation: Receiver (terminal, no SDL2) ─────────────────────────────
[env:sim_rx]
platform = native
extra_scripts = pre:sim/sim_build.py

extends = sim_base
build_src_filter = +<receiver/>

build_flags =
-std=gnu++20
-DNATIVE_SIM
${sim_base.build_flags}
-DODH_RECEIVER
-DSIM_RX
-lpthread

lib_deps =
bblanchon/ArduinoJson @ ^7.0.0

; ── Simulation: Transmitter (SDL2 display) ───────────────────────────────
[env:sim_tx]
platform = native
extra_scripts = pre:sim/sim_build.py

extends = sim_base
build_src_filter = +<transmitter/>

build_flags =
-std=gnu++20
-DNATIVE_SIM
${sim_base.build_flags}
; LVGL has unused parameters in its C code.
-Wno-error=unused-parameter
-DODH_TRANSMITTER
-DSIM_TX
-I include
-DLV_CONF_INCLUDE_SIMPLE
!pkg-config --cflags sdl2
!pkg-config --libs sdl2
-lpthread

; Re-enable strict unused-parameter for our own source code.
build_src_flags =
-Werror=unused-parameter

lib_deps =
${sim_base.lib_deps}
lvgl/lvgl @ ^8.3.11
bblanchon/ArduinoJson @ ^7.0.0
3 changes: 2 additions & 1 deletion firmware/src/receiver/web/ReceiverApi.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,10 @@ void ReceiverApi::begin() {
// POST /api/config (JSON body)
_server.onBody(
"/api/config", HTTP_POST,
[](AsyncWebServerRequest *req) {
[](AsyncWebServerRequest *) {
// Handler called after body is received – see body handler below.
},
// cppcheck-suppress constParameterPointer
[this](AsyncWebServerRequest *req, uint8_t *data, size_t len, size_t index, size_t total) { handlePostConfig(req, data, len, index, total); });

// POST /api/config/reset
Expand Down
2 changes: 1 addition & 1 deletion firmware/src/transmitter/modules/PotModule.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ int16_t PotModule::rawValue(uint8_t index) const {
return _raw[index];
}

uint16_t PotModule::mapToChannel(int16_t raw) {
uint16_t PotModule::mapToChannel(int32_t raw) {
if (raw < 0)
raw = 0;
if (raw > kPotAdcMax)
Expand Down
4 changes: 2 additions & 2 deletions firmware/src/transmitter/modules/PotModule.h
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ namespace odh {

inline constexpr uint8_t kPotI2cAddr = 0x48;
inline constexpr uint8_t kPotCount = 4;
inline constexpr int16_t kPotAdcMax = 32767;
inline constexpr int32_t kPotAdcMax = 32767;

class PotModule : public IModule {
public:
Expand All @@ -35,7 +35,7 @@ class PotModule : public IModule {
int16_t _raw[kPotCount] = {};
uint16_t _values[kPotCount] = {};

static uint16_t mapToChannel(int16_t raw);
static uint16_t mapToChannel(int32_t raw);
};

} // namespace odh
1 change: 1 addition & 0 deletions firmware/src/transmitter/web/TransmitterApi.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ void TransmitterApi::begin() {

_server.on("/api/config", HTTP_GET, [this](AsyncWebServerRequest *r) { handleGetConfig(r); });

// cppcheck-suppress constParameterPointer
_server.onBody("/api/config", HTTP_POST, [](AsyncWebServerRequest *) {}, [this](AsyncWebServerRequest *r, uint8_t *d, size_t l, size_t, size_t) { handlePostConfig(r, d, l); });

_server.on("/api/config/reset", HTTP_POST, [this](AsyncWebServerRequest *r) { handlePostReset(r); });
Expand Down