Skip to content
Open
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
178 changes: 160 additions & 18 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
@@ -1,15 +1,29 @@
You are an embedded C++ assistant.
# GATAS Copilot Instructions

Target platform:
You are an embedded C++ assistant for the GATAS conspicuity device project.

## Project Overview

GATAS is an aviation conspicuity device supporting multiple radio protocols (OGN, FLARM, FANET, ADS-L, PAW) on a Raspberry Pi Pico. The firmware enables simultaneous multi-protocol transmission/reception through time-sharing technology and serves traffic data to EFBs via GDL90 or NMEA protocols.

**Repository structure:**
- `src/pico/` – Firmware (RP2040/RP2350 main application)
- `src/lib/` – Core libraries (units organized by domain: core, adsbdecoder, flarmgatas, ogn, fanetace, etc.)
- `src/SystemGUI/` – Web UI (Vue.js) for aircraft configuration
- `src/vendor/` – Third-party dependencies (Catch2, ETL, gdl90, libcrc, libmodes, ArduinoJson, minmea, etc.)

### Target Platform

* Raspberry Pi Pico (RP2040/RP2350, dual-core Cortex-M0+)
* Bare-metal or Pico SDK
* No operating system unless explicitly stated
* Pico SDK 2.1.0+ (required)
* FreeRTOS Kernel V11.2.0 with custom patches
* Bare-metal with FreeRTOS, no higher-level OS

Language and standard:
### Language and Standard

* C++17 only
* No GNU extensions unless explicitly used by the Pico SDK
* **C++17 only** (C++20 for test build but limited in firmware)
* No GNU extensions unless required by Pico SDK
* CMake-based build system with Ninja for both firmware and unit tests

### Core Constraints (Hard Rules)

Expand All @@ -34,7 +48,7 @@ Language and standard:

### Coding Style

* Always use braces `{}` for all conditionals and loops, even single-line bodies, a opning brace starts at a new line, closing brace ends at a new line
* Always use braces `{}` for all conditionals and loops, even single-line bodies; opening brace starts on a new line, closing brace ends on a new line
* Prefer `constexpr`, `const`, and `static` where applicable
* Prefer ETL enum utilities for strongly typed enums
* Prefer POD types and simple structs
Expand Down Expand Up @@ -75,13 +89,13 @@ Language and standard:
* Mark shared data as `volatile` where appropriate
* Use Pico SDK synchronization primitives when needed
* Avoid race conditions; prefer lock-free designs
* Prevere FreeRTOS tasks, but use queue_spsc_atomic queues
* Prevere to use mutex, but use for copy of shared memory, for example: auto ownship = SpinlockGuard::copyWithLock(CoreUtils::sharedSpinLock(), <The variable or struct>);
* Prefer FreeRTOS tasks, but use queue_spsc_atomic queues for inter-task communication
* Prefer SpinlockGuard for thread-safe shared memory access: `auto data = SpinlockGuard::copyWithLock(CoreUtils::sharedSpinLock(), <variable>);`

### Timing

* Prevere to use CoreUtils::timeUs32Raw() for microsecond timings not aligned to seconds
* Prevere to use CoreUtils::timeUs32() for microsecond timings aligned to seconds
* Prefer `CoreUtils::timeUs32Raw()` for microsecond timings not aligned to seconds (free-running counter)
* Prefer `CoreUtils::timeUs32()` for microsecond timings aligned to seconds (PPS-aligned)
* Avoid busy-wait loops unless interacting with hardware

### Documentation
Expand All @@ -104,6 +118,128 @@ If a requested feature violates these rules, explain why and propose a safe embe

---

## Build, Test, and Development Workflow

### Building Unit Tests (Host-Based)

**Full test suite:**
```bash
cd src
./runtests.sh
# or manually:
cmake -B build_test -G Ninja && ninja -C build_test
```

**Single test (e.g., adsbdecoder):**
```bash
cmake -B build_test -G Ninja && ninja -C build_test adsbdecoder_tests
./build_test/lib/adsbdecoder/adsbdecoder_tests/adsbdecoder_tests
```

**Key points:**
- Tests run on **host (x86)**, not on the Pico
- Compilation uses Clang 18 in CI; GCC 14 or Clang 18 supported locally
- Tests use Catch2 framework with no Pico SDK dependencies
- Each test module has its own CMakeLists.txt in `lib/*/lib_*_tests/`

### Building Firmware

**Environment setup:**
```bash
export PICO_SDK_PATH=<path-to>/pico-sdk/
export FREERTOS_KERNEL_PATH=<path-to>/FreeRTOS-Kernel
```

**Build firmware (RP2040 – Pico W):**
```bash
cd src/pico
cmake -B build -G Ninja -DCMAKE_BUILD_TYPE=Release -DPICO_PLATFORM=rp2040 -DPICO_BOARD=pico_w
ninja -C build
# Output: build/GATAS_rp2040.uf2
```

**Build firmware (RP2350 – Pico 2 W):**
```bash
cd src/pico
cmake -B build -G Ninja -DCMAKE_BUILD_TYPE=Release -DPICO_PLATFORM=rp2350 -DPICO_BOARD=pico2_w
ninja -C build
# Output: build/GATAS_rp2350-arm-s.uf2
```

**Debug build (USB UART enabled by default):**
```bash
cmake -B build -G Ninja -DCMAKE_BUILD_TYPE=Debug -DGATAS_UART_OVER_USB=1 -DPICO_PLATFORM=rp2040
```

### Configuration

- **Firmware defaults:** Configured in `src/pico/gatas_default_config.json` (processed by `external/optimizejson.py` into `generated/default_config.hpp`)
- **Build-time headers:** `src/pico/generated/config.hpp` and `build_time.hpp` auto-generated by CMake
- **UART over USB:** Controlled by `-DGATAS_UART_OVER_USB=1` flag (Release builds default to USB, Debug defaults to physical UART)

### CI/CD

- GitHub Actions workflows in `.github/workflows/ci.yml` and `release.yml`
- CI runs unit tests on every push, builds both firmware targets and Docker image
- Release builds create GitHub releases with UF2 files and changelog
- Custom GitHub Actions in `.github/actions/` for setup and build orchestration

---

## Architecture and Key Conventions

### Module Organization

**`src/lib/`** – Unit-testable pure-logic libraries:
- `core/` – Fundamental utilities (CoreUtils, poolallocator, timing, synchronization guards)
- `adsbdecoder/` – ADS-B frame decoding and CPR math
- `aircrafttracker/` – Aircraft state tracking and path prediction
- `config/` – Configuration storage (flash or in-memory)
- `radiotuner/` – Radio frequency tuning (rx/tx, country regulations)
- `gdl90service/` – GDL90 protocol formatting
- `utils/` – Bit/packet manipulation, COBS encoding, Manchester encoding, DDB database utils
- `sx1262/` – SX1262 transceiver driver (hardware-specific but testable via mocks)
- `flarmgatas/` – FLARM 2024 protocol implementation
- `fanetace/` – FANET protocol implementation
- `ogn/` – OGN protocol implementation
- `adslace/` – ADS-L protocol implementation
- `gatasconnect/` – GATASConnect UDP/TCP protocol
- `wifiservice/`, `bluetooth/`, `webserver/` – Connectivity and UI services
- `dataport/` – NMEA/GDL90 output over UART/serial

**`src/pico/`** – Firmware entry point:
- `main.cpp` – System initialization, module composition, FreeRTOS task creation
- `main.h` – Configuration and hardware setup
- `IdleMemory.c` – Memory optimization for idle time
- `CMakeLists.txt` – Firmware build configuration

**`src/SystemGUI/`** – Vue.js web UI for aircraft configuration (separate from firmware)

### Naming and File Organization

- **Header files:** `.hpp` for C++ libraries, `.h` for pure-C or hardware definitions
- **Test files:** `*_test.cpp` in `*_tests/` directories
- **ACE suffix:** Embedded implementations in `ace/` subdirectories (e.g., `lib/sx1262/ace/sx1262.hpp` vs. external driver in `lib/sx1262/driver/`)
- **Mock files:** `src/lib/mocks/` contains Pico SDK and FreeRTOS stubs for host-based testing

### Message Routing and Concurrency

- **MessageRouter:** Central pub/sub system for inter-module communication (see `core/messagerouter.hpp`)
- **Synchronization:**
- Use `SpinlockGuard::copyWithLock()` for thread-safe shared memory access
- Use `SemaphoreGuard` for critical sections
- Prefer queue_spsc_atomic for lock-free inter-task queues
- **FreeRTOS tasks:** Avoid creating new tasks; use message router and existing task infrastructure
- **Timing:** Use `CoreUtils::timeUs32()` (aligned to PPS) or `CoreUtils::timeUs32Raw()` (free-running) for microsecond timings

### Protocol Handling

- **Multi-protocol time-sharing:** Transceiver alternates between protocols based on active traffic and priority
- **Adaptive prioritization:** Protocols with recent RX activity get more airtime
- **Ground station mode (ADS-L):** Relays traffic back into the network; max 10 aircraft uplinked

---

## Unit Testing (Catch2 – Embedded Safe)

### General Rules
Expand All @@ -112,6 +248,8 @@ If a requested feature violates these rules, explain why and propose a safe embe
* Tests are **host-based by default** (x86/CI), not running on the RP2040
* Tests must compile **without Pico SDK dependencies**
* Hardware access must be abstracted or mocked
* Test file pattern: `lib/<module>/<module>_tests/<module>_test.cpp`
* Each test module auto-builds and links mocks from `src/lib/mocks/`

### Allowed in Tests

Expand All @@ -137,22 +275,26 @@ If a requested feature violates these rules, explain why and propose a safe embe
* Hardware abstraction layer (HAL)
* Use dependency injection via references or interfaces (non-virtual preferred)
* Prefer compile-time configuration over runtime setup
* when using REQUIRE follow REQUIRE(<expected value> == <actual value>); SO always set expected value first
* **Assertion format:** Always `REQUIRE(<expected> == <actual>)` – put expected value first
* Tests may include `#include "pico/rand.h"`, `"pico/time.h"` etc., which are mocked

### Mocking Strategy

* Do NOT use mocking frameworks
* Use:

* Simple fake structs
* Manual stubs with deterministic behavior
* Simple fake structs with deterministic behavior
* Manual stubs in `src/lib/mocks/` (pico.h, FreeRTOS.h, task.h, geomock.hpp, etc.)
* Compile-time substitution (`#ifdef UNIT_TEST` only if unavoidable)
* Mock headers shadow Pico/FreeRTOS APIs; globals like `time_us_Value`, `mockConfig`, `geomock_*` are used by tests
* Example: `coreutils_test.cpp` sets `time_us_Value` to simulate time, calls `CoreUtils::msSinceEpoch()`, verifies result

Example pattern:

* `Driver` depends on `Interface&`
* Tests provide a `FakeInterface`
* Production provides a hardware-backed implementation
* Interface: `class GnsDriver { virtual void read() = 0; }`
* Production: `class L76B : public GnsDriver { /*Pico SDK calls*/ }`
* Test fake: `struct FakeGnss { void read() { /*deterministic data*/ } }`
* Test code: `void testParsing(GnsDriver& driver)` – inject FakeGnss

### Assertions

Expand Down
20 changes: 20 additions & 0 deletions src/SystemGUI/test/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Frontend Unit Tests

This directory is reserved for SystemGUI frontend unit tests.

## Run tests

From `src/SystemGUI`:

```bash
npm test
```

## File naming

Use `*.test.mjs` files so they are picked up by the `npm test` script.

## Notes

- The default setup uses Node's built-in test runner.
- For browser/DOM-specific component tests, add a DOM-capable test stack (for example Vitest + jsdom) in a follow-up change.
7 changes: 4 additions & 3 deletions src/lib/adsbdecoder/ace/adsbdatacollector.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ class AdsbDataCollector
static constexpr uint8_t HAS_VELOCITY = 1 << 3;
static constexpr uint8_t HAS_ALTITUDE = 1 << 4; //
static constexpr uint8_t HAS_POSITION_UPDATED = 1 << 5;
static constexpr uint8_t NOT_HAS_POSITION_UPDATED = static_cast<uint8_t>(~HAS_POSITION_UPDATED);
static constexpr uint8_t CHECK_HAS_CALLSIGN = 1 << 6;
static constexpr uint8_t VALID_MASK = HAS_POSITION_ODD | HAS_POSITION_EVEN | HAS_HEADING | HAS_VELOCITY | HAS_ALTITUDE | HAS_POSITION_UPDATED;

Expand Down Expand Up @@ -209,15 +210,15 @@ class AdsbDataCollector
}
}

void updateRawOdd(uint32_t raw_latitude, uint32_t raw_longitude)
void updateRawOdd(int32_t raw_latitude, int32_t raw_longitude)
{
currentDataStatus->messageStatus |= HAS_POSITION_ODD;
currentDataStatus->raw_odd_latitude = raw_latitude;
currentDataStatus->raw_odd_longitude = raw_longitude;
decodePCR(true);
}

void updateRawEven(uint32_t raw_latitude, uint32_t raw_longitude)
void updateRawEven(int32_t raw_latitude, int32_t raw_longitude)
{
currentDataStatus->messageStatus |= HAS_POSITION_EVEN;
currentDataStatus->raw_even_latitude = raw_latitude;
Expand Down Expand Up @@ -249,7 +250,7 @@ class AdsbDataCollector
{
if ((currentDataStatus->messageStatus & VALID_MASK) == VALID_MASK)
{
currentDataStatus->messageStatus &= ~HAS_POSITION_UPDATED;
currentDataStatus->messageStatus &= NOT_HAS_POSITION_UPDATED;
return true;
}

Expand Down
2 changes: 1 addition & 1 deletion src/lib/adsbdecoder/ace/cpr.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@

#include <stdint.h>

void decodeCPR(bool fflag, uint32_t even_cprlat, uint32_t even_cprlon, uint32_t odd_cprlat, uint32_t odd_cprlon, float *pfLat, float *pfLon);
void decodeCPR(bool fflag, int32_t even_cprlat, int32_t even_cprlon, int32_t odd_cprlat, int32_t odd_cprlon, float *pfLat, float *pfLon);
4 changes: 2 additions & 2 deletions src/lib/adsbdecoder/ace/src/adsbdecoder.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ void ADSBDecoder::processAdsbData(const uint8_t *data, uint8_t length)

// THis might temporary create a incorrect offset, but should be corrected pretty quickly die to how ADSB sends different information in different packages
// In addition this is also not the true altitude
int32_t altitude = (mm.unit == MODE_S_UNIT_METERS ? mm.altitude : mm.altitude * FT_TO_M);
int32_t altitude = mm.unit == MODE_S_UNIT_METERS ? mm.altitude : static_cast<int32_t>(static_cast<float>(mm.altitude) * FT_TO_M);

if (mm.metype >= 20 && mm.metype <= 22) // GPS Altitude so far never seen this
{
Expand All @@ -126,7 +126,7 @@ void ADSBDecoder::processAdsbData(const uint8_t *data, uint8_t length)
if (mm.mesub == 1 || mm.mesub == 2)
{
// printf("%06lX Ellipsoid:%ldm\n", mm.aa, baro_gnss_diff);
adsbDataCollector.updateVelocityHeadingBaroDiff(mm.velocity, mm.vert_rate_sign ? -mm.vert_rate : mm.vert_rate, mm.heading, mm.head * FT_TO_M); // mm.head is always in feet
adsbDataCollector.updateVelocityHeadingBaroDiff(mm.velocity, mm.vert_rate_sign ? -mm.vert_rate : mm.vert_rate, mm.heading, static_cast<int16_t>(static_cast<float>(mm.head) * FT_TO_M)); // mm.head is always in feet
}
else if (mm.mesub == 3 || mm.mesub == 4)
{
Expand Down
14 changes: 7 additions & 7 deletions src/lib/adsbdecoder/ace/src/cpr.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ int16_t cprModint (int16_t a, int16_t b)
/* Always positive MOD operation, used for CPR decoding. */
float cprModDouble(float a, float b)
{
float res = fmod(a, b);
float res = fmodf(a, b);
if (res < 0) res += b;
return res;
}
Expand Down Expand Up @@ -119,7 +119,7 @@ int16_t cprNLFunction(float lat)

int16_t cprNFunction(float lat, bool fflag)
{
int16_t nl = cprNLFunction(lat) - (fflag ? 1 : 0);
int16_t nl = static_cast<int16_t>(cprNLFunction(lat) - (fflag ? 1 : 0));
if (nl < 1) nl = 1;
return nl;
}
Expand All @@ -140,7 +140,7 @@ float cprDlonFunction(float lat, bool fflag, bool surface=false)
* simplicity. This may provide a position that is less fresh of a few
* seconds.
*/
void decodeCPR(bool fflag, uint32_t even_cprlat, uint32_t even_cprlon, uint32_t odd_cprlat, uint32_t odd_cprlon, float *pfLat, float *pfLon)
void decodeCPR(bool fflag, int32_t even_cprlat, int32_t even_cprlon, int32_t odd_cprlat, int32_t odd_cprlon, float *pfLat, float *pfLon)
{
constexpr float AirDlat0 = 360.0f / 60;
constexpr float AirDlat1 = 360.0f / 59;
Expand All @@ -153,7 +153,7 @@ void decodeCPR(bool fflag, uint32_t even_cprlat, uint32_t even_cprlon, uint32_t
float rlat, rlon;

/* Compute the Latitude Index "j" */
int16_t j = static_cast<int16_t>(floor(((59*lat0 - 60.0f*lat1) / 131072) + 0.5));
int16_t j = static_cast<int16_t>(floorf(((59*lat0 - 60.0f*lat1) / 131072) + 0.5f));
float rlat0 = AirDlat0 * (cprModint (j,60) + lat0 / 131072.0f);
float rlat1 = AirDlat1 * (cprModint (j,59) + lat1 / 131072.0f);

Expand All @@ -175,7 +175,7 @@ void decodeCPR(bool fflag, uint32_t even_cprlat, uint32_t even_cprlon, uint32_t
{
/* Use odd packet. */
int16_t ni = cprNFunction(rlat1,true);
int16_t m = static_cast<int16_t>(floor((((lon0 * (cprLat1-1)) - (lon1 * cprLat1)) / 131072.0f) + 0.5f));
int16_t m = static_cast<int16_t>(floorf((((lon0 * (cprLat1-1)) - (lon1 * cprLat1)) / 131072.0f) + 0.5f));
rlon = cprDlonFunction(rlat1,true) * (cprModint (m,ni)+lon1/131072);
rlat = rlat1;

Expand All @@ -184,11 +184,11 @@ void decodeCPR(bool fflag, uint32_t even_cprlat, uint32_t even_cprlon, uint32_t
{
/* Use even packet. */
int16_t ni = cprNFunction(rlat0,false);
int16_t m = static_cast<int16_t>(floor((((lon0 * (cprLat0-1)) - (lon1 * cprLat0)) / 131072.0f) + 0.5f));
int16_t m = static_cast<int16_t>(floorf((((lon0 * (cprLat0-1)) - (lon1 * cprLat0)) / 131072.0f) + 0.5f));
rlon = cprDlonFunction(rlat0,false) * (cprModint (m,ni)+lon0/131072);
rlat = rlat0;
}
rlon -= floor( (rlon + 180.0f) / 360.0f ) * 360.0f;
rlon -= floorf( (rlon + 180.0f) / 360.0f ) * 360.0f;

*pfLat = rlat;
*pfLon = rlon;
Expand Down
4 changes: 3 additions & 1 deletion src/lib/aircrafttracker/ace/aircrafttracker.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
* Client that can connect to a host and a port and expect to receive line terminated NMEA Messages
* Part of this code taken from the example from Raspbery
*/
class AircraftTracker : public BaseModule, public etl::message_router<AircraftTracker, GATAS::ConfigUpdatedMsg, GATAS::RadioTxPositionRequestMsg, GATAS::IngressAircraftPositionMsg, GATAS::IngressAircraftPositionsMsg, GATAS::Every5SecMsg>
class AircraftTracker : public BaseModule, public etl::message_router<AircraftTracker, GATAS::ConfigUpdatedMsg, GATAS::RadioTxPositionRequestMsg, GATAS::IngressAircraftPositionMsg, GATAS::IngressAircraftPositionsMsg, GATAS::OwnshipPositionMsg, GATAS::Every5SecMsg>
{
private:
mutable SemaphoreHandle_t trackedAircraftMutex = nullptr;
Expand Down Expand Up @@ -61,6 +61,7 @@ class AircraftTracker : public BaseModule, public etl::message_router<AircraftTr
TaskHandle_t taskHandle = nullptr;
TrackerData<MAX_TRACKING_PLANES, TIMESLICES> trackedAircraft;
GATAS::AircraftAddress ownshipAddress;
float ownshipTrack = 0.f;
bool groundStation_ = false;

// Producer Consumer queue to handle data between this task and the send task
Expand All @@ -82,6 +83,7 @@ class AircraftTracker : public BaseModule, public etl::message_router<AircraftTr
void on_receive(const GATAS::ConfigUpdatedMsg &msg);
void on_receive(const GATAS::IngressAircraftPositionMsg &msg);
void on_receive(const GATAS::IngressAircraftPositionsMsg &msg);
void on_receive(const GATAS::OwnshipPositionMsg &msg);
void on_receive(const GATAS::RadioTxPositionRequestMsg &msg);
void on_receive(const GATAS::Every5SecMsg &msg);
static void aircraftTrackerTrampoline(void *arg);
Expand Down
Loading
Loading