Inital commit
CI / macOS (push) Has been cancelled
CI / Windows / Clang (push) Has been cancelled
CI / macOS (push) Has been cancelled
CI / Windows / Clang (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,14 @@
|
|||||||
|
[*]
|
||||||
|
end_of_line = LF
|
||||||
|
charset = utf-8
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 4
|
||||||
|
insert_final_newline = true
|
||||||
|
|
||||||
|
[*.{yml,json,lua,md,html}]
|
||||||
|
charset = utf-8
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
|
|
||||||
|
[Makefile]
|
||||||
|
indent_style = tab
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
name: CI
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [trunk]
|
||||||
|
pull_request:
|
||||||
|
branches: [trunk]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
macos:
|
||||||
|
name: macOS
|
||||||
|
runs-on: macos-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install Ninja
|
||||||
|
run: brew install ninja
|
||||||
|
|
||||||
|
- name: Install gcovr
|
||||||
|
run: pip install gcovr
|
||||||
|
|
||||||
|
- name: Configure with coverage
|
||||||
|
run: cmake -S . -B build-cov -G Ninja -DENABLE_COVERAGE=ON
|
||||||
|
|
||||||
|
- name: Build and generate coverage
|
||||||
|
run: ninja -C build-cov coverage
|
||||||
|
|
||||||
|
- name: Upload coverage report
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: coverage-macos
|
||||||
|
path: build-cov/coverage/
|
||||||
|
|
||||||
|
windows-clang:
|
||||||
|
name: Windows / Clang
|
||||||
|
runs-on: windows-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- uses: ilammy/msvc-dev-cmd@v1
|
||||||
|
|
||||||
|
- name: Install gcovr
|
||||||
|
run: pip install gcovr
|
||||||
|
|
||||||
|
- name: Configure with coverage
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
cmake -S . -B build-cov -G Ninja \
|
||||||
|
-DENABLE_COVERAGE=ON \
|
||||||
|
-DCMAKE_C_COMPILER="C:/Program Files/LLVM/bin/clang.exe" \
|
||||||
|
-DCMAKE_CXX_COMPILER="C:/Program Files/LLVM/bin/clang++.exe"
|
||||||
|
|
||||||
|
- name: Build and generate coverage
|
||||||
|
run: ninja -C build-cov coverage
|
||||||
|
|
||||||
|
- name: Upload coverage report
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: coverage-windows-clang
|
||||||
|
path: build-cov/coverage/
|
||||||
+16
@@ -0,0 +1,16 @@
|
|||||||
|
# CMake
|
||||||
|
build
|
||||||
|
build-*
|
||||||
|
cmake-build-*
|
||||||
|
coverage
|
||||||
|
|
||||||
|
# Editors
|
||||||
|
.vscode
|
||||||
|
.idea
|
||||||
|
.ccls
|
||||||
|
.ccls-cache
|
||||||
|
.cache
|
||||||
|
compile_commands.json
|
||||||
|
|
||||||
|
# macOS
|
||||||
|
.DS_Store
|
||||||
@@ -0,0 +1,256 @@
|
|||||||
|
# AGENTS.md
|
||||||
|
|
||||||
|
## Project Overview
|
||||||
|
|
||||||
|
`celrs` is a C23 project for interfacing with ELRS TX modules via serial
|
||||||
|
USB using the CRSF (Crossfire Serial) protocol. Wired for test-driven
|
||||||
|
development using Unity and CMock.
|
||||||
|
|
||||||
|
## Build System
|
||||||
|
|
||||||
|
- **Generator:** Ninja
|
||||||
|
- **CMake minimum:** 3.21
|
||||||
|
- **C standard:** C23
|
||||||
|
|
||||||
|
### Commands
|
||||||
|
|
||||||
|
Configure (default):
|
||||||
|
```sh
|
||||||
|
cmake -S . -B build -G Ninja
|
||||||
|
```
|
||||||
|
|
||||||
|
Configure with coverage:
|
||||||
|
```sh
|
||||||
|
cmake -S . -B build-cov -G Ninja -DENABLE_COVERAGE=ON
|
||||||
|
```
|
||||||
|
|
||||||
|
Build:
|
||||||
|
```sh
|
||||||
|
ninja -C build
|
||||||
|
```
|
||||||
|
|
||||||
|
Run tests (full Unity output, colored):
|
||||||
|
```sh
|
||||||
|
ninja -C build check
|
||||||
|
```
|
||||||
|
|
||||||
|
Run tests (CTest summary only):
|
||||||
|
```sh
|
||||||
|
ninja -C build test
|
||||||
|
```
|
||||||
|
|
||||||
|
Run tests and generate coverage HTML:
|
||||||
|
```sh
|
||||||
|
ninja -C build-cov coverage
|
||||||
|
```
|
||||||
|
|
||||||
|
Run (Linux/macOS):
|
||||||
|
```sh
|
||||||
|
./build/main /dev/ttyUSB0
|
||||||
|
```
|
||||||
|
|
||||||
|
Run (Windows):
|
||||||
|
```sh
|
||||||
|
./build/main.exe COM3
|
||||||
|
```
|
||||||
|
|
||||||
|
### Dependencies
|
||||||
|
|
||||||
|
Dependencies are managed via custom `Find*.cmake` scripts in `deps/`.
|
||||||
|
These scripts use `FetchContent` under the hood to download and build
|
||||||
|
libraries automatically.
|
||||||
|
|
||||||
|
To add a new dependency:
|
||||||
|
|
||||||
|
1. Add the corresponding `Find<name>.cmake` to `deps/`
|
||||||
|
2. Add `find_package(<name> REQUIRED)` to `CMakeLists.txt`
|
||||||
|
3. Link with `<name>::<name>` in `target_link_libraries()`
|
||||||
|
|
||||||
|
### CMake Module Path
|
||||||
|
|
||||||
|
`deps/` is added to `CMAKE_MODULE_PATH` so `find_package()` resolves
|
||||||
|
to the custom scripts instead of system-installed packages.
|
||||||
|
|
||||||
|
## Coding Conventions
|
||||||
|
|
||||||
|
- **Language:** C23
|
||||||
|
- **Trailing return type** for function signatures (e.g. `auto fn() -> void`)
|
||||||
|
- **4-space indentation**
|
||||||
|
- `auto` for obvious types (e.g. `auto main(...) -> int`)
|
||||||
|
- **East const** (e.g. `char const*` not `const char*`)
|
||||||
|
- **Naming:** `snake_case` for variables, functions, and types
|
||||||
|
- **Naming:** `SCREAMING_SNAKE_CASE` only for macros and constants
|
||||||
|
- Include order:
|
||||||
|
1. C standard library headers (`<stdlib.h>`, `<string.h>`, etc.)
|
||||||
|
2. *(blank line)*
|
||||||
|
3. OS-specific headers (Windows API, POSIX, etc.)
|
||||||
|
4. *(blank line)*
|
||||||
|
5. Local/project headers
|
||||||
|
|
||||||
|
## Shell Scripts
|
||||||
|
|
||||||
|
- Always use `#!/bin/sh` shebang for shell scripts
|
||||||
|
- Scripts must be POSIX compliant (no bashisms)
|
||||||
|
- When providing commands to users:
|
||||||
|
- Windows/PowerShell: use `` ` `` for line continuation
|
||||||
|
- Unix/Linux/macOS: use `\` for line continuation
|
||||||
|
|
||||||
|
## Commit Messages
|
||||||
|
|
||||||
|
- Follow the 50/72 rule:
|
||||||
|
- Subject line: max 50 characters
|
||||||
|
- Body lines: wrapped at 72 characters
|
||||||
|
- Use conventional commit prefixes (`feat:`, `fix:`, `docs:`, `chore:`,
|
||||||
|
etc.)
|
||||||
|
- Separate subject from body with a blank line
|
||||||
|
- Do **not** add yourself as a co-author (`Co-Authored-By:` trailers are
|
||||||
|
forbidden)
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```
|
||||||
|
feat: add CRSF CRC8 calculation
|
||||||
|
|
||||||
|
Implement CRC8-CCITT (poly 0x07) for CRSF frame validation.
|
||||||
|
Added unit tests for empty, single-byte, and known-value cases.
|
||||||
|
```
|
||||||
|
|
||||||
|
## Documentation (Markdown)
|
||||||
|
|
||||||
|
- Wrap normal text and lists at **max 80 columns** (for readability in
|
||||||
|
terminals and editors).
|
||||||
|
- **Exceptions**: Tables and code blocks (```` ``` ````) can exceed 80
|
||||||
|
columns when formatting requires it (e.g. trees, alignment).
|
||||||
|
- Use standard Markdown: `**bold**`, `` `inline code` ``, `##` headings,
|
||||||
|
`-` or numbered lists, fenced code blocks with language hints
|
||||||
|
(```` ```c ````, ```` ```sh ````).
|
||||||
|
- Keep examples concise, up-to-date, and self-documenting.
|
||||||
|
- Do not use em dashes (`—`). Use a colon or rewrite the sentence.
|
||||||
|
- Each shell command gets its own fenced code block; do **not** combine
|
||||||
|
multiple commands into one block. Precede each block with a short
|
||||||
|
plain-text label describing what the command does:
|
||||||
|
|
||||||
|
Setup build:
|
||||||
|
```sh
|
||||||
|
cmake -S . -B build -G Ninja
|
||||||
|
```
|
||||||
|
|
||||||
|
- This file (`AGENTS.md`) follows its own rules.
|
||||||
|
|
||||||
|
## Source Layout
|
||||||
|
|
||||||
|
```text
|
||||||
|
celrs/
|
||||||
|
crsf.h / crsf.c CRSF protocol: CRC8, frame parse/build
|
||||||
|
serial.h / serial.c Serial port abstraction (Win/POSIX stubs)
|
||||||
|
logger.h / logger.c Level-filtering logger
|
||||||
|
log_write.h/.c stdout log sink
|
||||||
|
main.c Entry point — demo heartbeat + read
|
||||||
|
tests/
|
||||||
|
test_crsf.c CRSF CRC, parse, build tests
|
||||||
|
test_serial.c Serial open/close/stub tests
|
||||||
|
test_logger.c Logger level-filtering tests
|
||||||
|
deps/
|
||||||
|
FindUnity.cmake Fetches Unity v2.6.1 via ZIP
|
||||||
|
FindCMock.cmake Fetches CMock v2.6.0 via ZIP
|
||||||
|
```
|
||||||
|
|
||||||
|
## TDD Workflow
|
||||||
|
|
||||||
|
This project follows Red-Green-Refactor. All changes to testable source
|
||||||
|
files under `celrs/` should be test-driven: write a failing test first,
|
||||||
|
then implement.
|
||||||
|
|
||||||
|
### Adding a new module
|
||||||
|
|
||||||
|
1. Create `celrs/<module>.h` with the public prototype.
|
||||||
|
2. Create `tests/test_<module>.c`. Set CMock expectations for any
|
||||||
|
dependency calls, then assert the result.
|
||||||
|
3. Register the test in `tests/CMakeLists.txt`:
|
||||||
|
|
||||||
|
```cmake
|
||||||
|
add_executable(test_module test_module.c)
|
||||||
|
target_include_directories(test_module PRIVATE "${CMAKE_SOURCE_DIR}")
|
||||||
|
target_link_libraries(test_module PRIVATE celrs_module Unity::Unity CMock::CMock)
|
||||||
|
target_compile_features(test_module PRIVATE c_std_23)
|
||||||
|
cmock_generate_mock(test_module "${CMAKE_SOURCE_DIR}/celrs/dep.h")
|
||||||
|
add_test(NAME test_module COMMAND test_module)
|
||||||
|
list(APPEND TEST_TARGETS test_module)
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Stub `celrs/<module>.c` with a dummy return, confirm RED, implement,
|
||||||
|
confirm GREEN:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
ninja -C build check
|
||||||
|
```
|
||||||
|
|
||||||
|
### Mocking a dependency
|
||||||
|
|
||||||
|
Use `cmock_generate_mock` in the test target to generate a mock from a
|
||||||
|
header. Include `Mock<name>.h` in the test and use the generated API:
|
||||||
|
|
||||||
|
```c
|
||||||
|
#include "Mockdep.h"
|
||||||
|
|
||||||
|
void setUp(void) { Mockdep_Init(); }
|
||||||
|
void tearDown(void) { Mockdep_Verify(); Mockdep_Destroy(); }
|
||||||
|
|
||||||
|
void test_something(void) {
|
||||||
|
dep_fn_ExpectAndReturn(arg, expected);
|
||||||
|
TEST_ASSERT_TRUE(module_do_thing());
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Behavioral Guidelines
|
||||||
|
|
||||||
|
Reduce common LLM coding mistakes. Bias toward caution over speed.
|
||||||
|
For trivial tasks, use judgment.
|
||||||
|
|
||||||
|
### Think Before Coding
|
||||||
|
|
||||||
|
- State assumptions explicitly. If uncertain, ask.
|
||||||
|
- If multiple interpretations exist, present them, don't pick silently.
|
||||||
|
- If something is unclear, stop. Name what's confusing. Ask.
|
||||||
|
|
||||||
|
### Simplicity First
|
||||||
|
|
||||||
|
Minimum code that solves the problem. Nothing speculative.
|
||||||
|
|
||||||
|
- No features beyond what was asked.
|
||||||
|
- No abstractions for single-use code.
|
||||||
|
- If you write 200 lines and it could be 50, rewrite it.
|
||||||
|
|
||||||
|
### Surgical Changes
|
||||||
|
|
||||||
|
Touch only what you must. Clean up only your own mess.
|
||||||
|
|
||||||
|
- Don't "improve" adjacent code, comments, or formatting.
|
||||||
|
- Don't refactor things that aren't broken.
|
||||||
|
- Match existing style, even if you'd do it differently.
|
||||||
|
- Remove imports/variables/functions that YOUR changes made unused.
|
||||||
|
- Don't remove pre-existing dead code unless asked.
|
||||||
|
|
||||||
|
### Goal-Driven Execution
|
||||||
|
|
||||||
|
Transform tasks into verifiable goals:
|
||||||
|
|
||||||
|
- "Add validation" means: write tests for invalid inputs, then pass.
|
||||||
|
- "Fix the bug" means: write a test that reproduces it, then pass.
|
||||||
|
- "Refactor X" means: ensure tests pass before and after.
|
||||||
|
|
||||||
|
## Platform Support
|
||||||
|
|
||||||
|
The project supports Windows, Linux, macOS, Emscripten, and Android via
|
||||||
|
`Platform.cmake` and `Flags.cmake` in `deps/`.
|
||||||
|
|
||||||
|
## CRSF Protocol Notes
|
||||||
|
|
||||||
|
CRSF is the Crossfire Serial Protocol used by ELRS. Key points:
|
||||||
|
|
||||||
|
- Frame header is always `0xC8`
|
||||||
|
- CRC8-CCITT with polynomial `0x07`, init `0x00`
|
||||||
|
- CRC is computed over: destination + source + type + size + payload
|
||||||
|
- Standard baud rate for ELRS CRSF is 400000 bps
|
||||||
|
- TX modules expose CRSF over USB serial (appears as COM port on Windows,
|
||||||
|
/dev/ttyUSB* on Linux)
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
cmake_minimum_required(VERSION 3.21)
|
||||||
|
project(celrs VERSION 0.1.0)
|
||||||
|
|
||||||
|
# CMake configuration
|
||||||
|
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
|
||||||
|
list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/deps")
|
||||||
|
|
||||||
|
# Platform flags
|
||||||
|
include(Platform)
|
||||||
|
include(Flags)
|
||||||
|
|
||||||
|
include(Coverage)
|
||||||
|
|
||||||
|
# Core Library
|
||||||
|
add_subdirectory(celrs)
|
||||||
|
|
||||||
|
# Tools
|
||||||
|
add_subdirectory(tools)
|
||||||
|
|
||||||
|
# Testing
|
||||||
|
enable_testing()
|
||||||
|
add_subdirectory(tests)
|
||||||
|
|
||||||
|
# IDE configuration
|
||||||
|
include(IDE)
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2026 PorterSky
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
@@ -0,0 +1,147 @@
|
|||||||
|
# celrs
|
||||||
|
|
||||||
|
A C23 project for interfacing with ELRS TX modules (e.g., BAYCK Nano Dual
|
||||||
|
Band) via serial USB using the CRSF (Crossfire Serial) protocol.
|
||||||
|
|
||||||
|
Built on the same TDD foundation as [ctdd](https://github.com/PorterSky/ctdd)
|
||||||
|
using [Unity](https://github.com/ThrowTheSwitch/Unity) and
|
||||||
|
[CMock](https://github.com/ThrowTheSwitch/CMock).
|
||||||
|
|
||||||
|
All dependencies are fetched automatically via CMake `FetchContent` — no
|
||||||
|
manual installation required beyond the tools listed below.
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
| Tool | Purpose |
|
||||||
|
| ------------ | ----------------------------------------------------- |
|
||||||
|
| CMake ≥ 3.21 | Build system |
|
||||||
|
| Ninja | Build backend |
|
||||||
|
| C23 compiler | GCC 14+, Clang 18+ |
|
||||||
|
| Ruby ≥ 3.0 | CMock mock generation |
|
||||||
|
| gcovr ≥ 6.0 | Coverage reports — optional (`uv tool install gcovr`) |
|
||||||
|
|
||||||
|
## Build
|
||||||
|
|
||||||
|
Configure:
|
||||||
|
```sh
|
||||||
|
cmake -S . -B build -G Ninja
|
||||||
|
```
|
||||||
|
|
||||||
|
Build:
|
||||||
|
```sh
|
||||||
|
ninja -C build
|
||||||
|
```
|
||||||
|
|
||||||
|
## Test
|
||||||
|
|
||||||
|
Full Unity output with colors:
|
||||||
|
```sh
|
||||||
|
ninja -C build check
|
||||||
|
```
|
||||||
|
|
||||||
|
CTest summary only:
|
||||||
|
```sh
|
||||||
|
ninja -C build test
|
||||||
|
```
|
||||||
|
|
||||||
|
`check` builds all suites and runs CTest with `--output-on-failure`,
|
||||||
|
so assertion-level detail appears on any failure without running
|
||||||
|
binaries by hand.
|
||||||
|
|
||||||
|
## Coverage
|
||||||
|
|
||||||
|
Configure with coverage instrumentation:
|
||||||
|
```sh
|
||||||
|
cmake -S . -B build-cov -G Ninja -DENABLE_COVERAGE=ON
|
||||||
|
```
|
||||||
|
|
||||||
|
Generate the HTML report:
|
||||||
|
```sh
|
||||||
|
ninja -C build-cov coverage
|
||||||
|
```
|
||||||
|
|
||||||
|
Open `build-cov/coverage/index.html` in a browser to view results.
|
||||||
|
|
||||||
|
Only `celrs/` source files are measured. Unity, CMock, and generated
|
||||||
|
mock files are excluded. Requires GCC or Clang with gcov support, and
|
||||||
|
`gcovr` on `PATH`.
|
||||||
|
|
||||||
|
> **Windows note:** requires GCC (e.g. `scoop install gcc`) or a Clang
|
||||||
|
> build that includes compiler-rt. A custom Clang without compiler-rt
|
||||||
|
> will fail at link time.
|
||||||
|
|
||||||
|
## Run
|
||||||
|
|
||||||
|
Connect your ELRS TX module via USB, then run:
|
||||||
|
|
||||||
|
Windows:
|
||||||
|
```sh
|
||||||
|
./build/main.exe COM3
|
||||||
|
```
|
||||||
|
|
||||||
|
Linux / macOS:
|
||||||
|
```sh
|
||||||
|
./build/main /dev/ttyUSB0
|
||||||
|
```
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
celrs/
|
||||||
|
crsf.h / crsf.c CRSF protocol: CRC8, frame parse/build
|
||||||
|
serial.h / serial.c Serial port abstraction (Win/POSIX)
|
||||||
|
logger.h / logger.c Level-filtering logger
|
||||||
|
log_write.h/.c stdout log sink
|
||||||
|
main.c Entry point — demo heartbeat + read
|
||||||
|
tests/
|
||||||
|
test_crsf.c CRSF CRC, parse, build tests
|
||||||
|
test_serial.c Serial open/close/stub tests
|
||||||
|
test_logger.c Logger level-filtering tests
|
||||||
|
deps/
|
||||||
|
FindUnity.cmake Fetches Unity v2.6.1 via ZIP
|
||||||
|
FindCMock.cmake Fetches CMock v2.6.0 via ZIP
|
||||||
|
```
|
||||||
|
|
||||||
|
## CRSF Protocol
|
||||||
|
|
||||||
|
CRSF (Crossfire Serial Protocol) is the serial protocol used by ELRS for
|
||||||
|
communication between ground station and TX/RX modules.
|
||||||
|
|
||||||
|
### Frame format
|
||||||
|
|
||||||
|
```
|
||||||
|
+------+------+------+------+-------+-------+
|
||||||
|
| 0xC8 | dest | src | type | size | ... | CRC |
|
||||||
|
+------+------+------+------+-------+-------+
|
||||||
|
1 byte 1B 1B 1B 1B N B 1 byte
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Header:** Always `0xC8`
|
||||||
|
- **Destination:** Target device address
|
||||||
|
- **Source:** Sender device address
|
||||||
|
- **Type:** Frame type (heartbeat, RC channels, telemetry, etc.)
|
||||||
|
- **Size:** Payload length in bytes
|
||||||
|
- **Payload:** Frame-specific data
|
||||||
|
- **CRC:** CRC8-CCITT over dest+src+type+size+payload
|
||||||
|
|
||||||
|
### Common device addresses
|
||||||
|
|
||||||
|
| Address | Device |
|
||||||
|
| ------- | ----------------------------- |
|
||||||
|
| 0x00 | FC Broadcast |
|
||||||
|
| 0x10 | Flight Controller |
|
||||||
|
| 0x80 | TBS Ground Station |
|
||||||
|
| 0xEA | Custom Module |
|
||||||
|
| 0xDD | RC Device |
|
||||||
|
|
||||||
|
### Common frame types
|
||||||
|
|
||||||
|
| Type | Name |
|
||||||
|
| ------ | --------------------------- |
|
||||||
|
| 0x01 | RC Channels Packed |
|
||||||
|
| 0x02 | Packet Link Telemetry |
|
||||||
|
| 0x03 | Heartbeat |
|
||||||
|
| 0x08 | Device Info |
|
||||||
|
| 0x09 | Parameter List |
|
||||||
|
| 0x17 | MSP Read |
|
||||||
|
| 0x18 | MSP Write |
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
add_library(celrs_crsf STATIC crsf.c)
|
||||||
|
target_include_directories(celrs_crsf PUBLIC "${CMAKE_SOURCE_DIR}")
|
||||||
|
target_compile_features(celrs_crsf PRIVATE c_std_23)
|
||||||
|
|
||||||
|
add_library(celrs_serial STATIC serial.c)
|
||||||
|
target_include_directories(celrs_serial PUBLIC "${CMAKE_SOURCE_DIR}")
|
||||||
|
target_compile_features(celrs_serial PRIVATE c_std_23)
|
||||||
|
|
||||||
|
# Level-filtering logger — calls log_write(); symbol resolved by the final binary
|
||||||
|
add_library(celrs_logger STATIC logger.c)
|
||||||
|
target_include_directories(celrs_logger PUBLIC "${CMAKE_SOURCE_DIR}")
|
||||||
|
target_compile_features(celrs_logger PRIVATE c_std_23)
|
||||||
|
|
||||||
|
# Real log_write implementation — linked into production binaries only
|
||||||
|
add_library(celrs_log_write STATIC log_write.c)
|
||||||
|
target_include_directories(celrs_log_write PUBLIC "${CMAKE_SOURCE_DIR}")
|
||||||
|
target_compile_features(celrs_log_write PRIVATE c_std_23)
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
#include "celrs/crsf.h"
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
/* CRC8-CCITT with polynomial 0x07, init 0x00 (used by CRSF/ELRS) */
|
||||||
|
uint8_t cel_crsf_crc(uint8_t const* data, size_t len) {
|
||||||
|
uint8_t crc = 0x00;
|
||||||
|
for (size_t i = 0; i < len; i++) {
|
||||||
|
crc ^= data[i];
|
||||||
|
for (int j = 0; j < 8; j++) {
|
||||||
|
crc = (crc & 0x80) ? (crc << 1) ^ 0x07 : (crc << 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return crc;
|
||||||
|
}
|
||||||
|
|
||||||
|
int cel_crsf_frame_validate(cel_crsf_frame const* frame) {
|
||||||
|
/* Rebuild the data that was CRC'd: dest + src + type + size + payload */
|
||||||
|
uint8_t data[260];
|
||||||
|
size_t offset = 0;
|
||||||
|
data[offset++] = frame->destination;
|
||||||
|
data[offset++] = frame->source;
|
||||||
|
data[offset++] = frame->type;
|
||||||
|
data[offset++] = frame->size;
|
||||||
|
memcpy(data + offset, frame->payload, frame->size);
|
||||||
|
offset += frame->size;
|
||||||
|
|
||||||
|
uint8_t calc_crc = cel_crsf_crc(data, offset);
|
||||||
|
return calc_crc == frame->crc ? 0 : -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
int cel_crsf_frame_parse(cel_crsf_frame* frame, uint8_t const* buf, size_t len) {
|
||||||
|
if (frame == NULL || buf == NULL) return -1;
|
||||||
|
/* Minimum: header(1) + dest(1) + src(1) + type(1) + size(1) = 5 bytes */
|
||||||
|
if (len < 5) return -1;
|
||||||
|
|
||||||
|
/* Verify header */
|
||||||
|
if (buf[0] != CEL_CRSF_FRAME_HEADER) return -1;
|
||||||
|
|
||||||
|
frame->destination = buf[1];
|
||||||
|
frame->source = buf[2];
|
||||||
|
frame->type = buf[3];
|
||||||
|
frame->size = buf[4];
|
||||||
|
|
||||||
|
uint8_t size = buf[4];
|
||||||
|
/* Total: header(1) + dest(1) + src(1) + type(1) + size(1) + payload(N) + crc(1) */
|
||||||
|
size_t total = 6 + size;
|
||||||
|
if (len < total) return -1;
|
||||||
|
|
||||||
|
memcpy(frame->payload, buf + 5, size);
|
||||||
|
frame->crc = buf[5 + size];
|
||||||
|
|
||||||
|
return cel_crsf_frame_validate(frame);
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t cel_crsf_frame_build(uint8_t* dst, uint8_t destination, uint8_t source,
|
||||||
|
uint8_t type, uint8_t const* payload, uint8_t size) {
|
||||||
|
if (dst == NULL) return 0;
|
||||||
|
|
||||||
|
dst[0] = CEL_CRSF_FRAME_HEADER;
|
||||||
|
dst[1] = destination;
|
||||||
|
dst[2] = source;
|
||||||
|
dst[3] = type;
|
||||||
|
dst[4] = size;
|
||||||
|
|
||||||
|
if (payload != NULL && size > 0) {
|
||||||
|
memcpy(dst + 5, payload, size);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* CRC over dest + src + type + size + payload */
|
||||||
|
uint8_t crc = cel_crsf_crc(dst + 1, 3 + 1 + size);
|
||||||
|
dst[5 + size] = crc;
|
||||||
|
|
||||||
|
return 1 + 3 + 1 + size + 1; /* header + 3 fields + size byte + payload + crc */
|
||||||
|
}
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
#pragma once
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <stddef.h>
|
||||||
|
|
||||||
|
/* CRSF frame header byte */
|
||||||
|
#define CEL_CRSF_FRAME_HEADER 0xC8
|
||||||
|
|
||||||
|
/* CRSF device addresses */
|
||||||
|
#define CEL_CRSF_ADDRESS_FC_BROADCAST 0x00
|
||||||
|
#define CEL_CRSF_ADDRESS_FC 0x10
|
||||||
|
#define CEL_CRSF_ADDRESS_TBS_GROUND_STATION 0x80
|
||||||
|
#define CEL_CRSF_ADDRESS_CUSTOM_MODULE 0xEA
|
||||||
|
#define CEL_CRSF_ADDRESS_RC_DEVICE 0xDD
|
||||||
|
#define CEL_CRSF_ADDRESS_GPS 0xEC
|
||||||
|
#define CEL_CRSF_ADDRESS_FLIGHT_CONTROLLER 0xED
|
||||||
|
|
||||||
|
/* CRSF frame types */
|
||||||
|
typedef enum {
|
||||||
|
CEL_CRSF_FRAMETYPE_PACKET_LINK_TELEMETRY = 0x02,
|
||||||
|
CEL_CRSF_FRAMETYPE_RC_CHANNELS_PACKED = 0x01,
|
||||||
|
CEL_CRSF_FRAMETYPE_GPS = 0x02,
|
||||||
|
CEL_CRSF_FRAMETYPE_HEARTBEAT = 0x03,
|
||||||
|
CEL_CRSF_FRAMETYPE_VERSION = 0x04,
|
||||||
|
CEL_CRSF_FRAMETYPE_PARAMETER_SETTINGS_ENTRY = 0x05,
|
||||||
|
CEL_CRSF_FRAMETYPE_PARAMETER_READ = 0x06,
|
||||||
|
CEL_CRSF_FRAMETYPE_PARAMETER_WRITE = 0x07,
|
||||||
|
CEL_CRSF_FRAMETYPE_DEVICE_INFO = 0x08,
|
||||||
|
CEL_CRSF_FRAMETYPE_PARAMETER_LIST = 0x09,
|
||||||
|
CEL_CRSF_FRAMETYPE_RC_CHANNELS_RAW = 0x16,
|
||||||
|
CEL_CRSF_FRAMETYPE_MSP_READ = 0x17,
|
||||||
|
CEL_CRSF_FRAMETYPE_MSP_WRITE = 0x18,
|
||||||
|
CEL_CRSF_FRAMETYPE_CURR_VOLTAGE_TEMP = 0x1E,
|
||||||
|
CEL_CRSF_FRAMETYPE_BATTERY_SENSOR = 0x1F,
|
||||||
|
CEL_CRSF_FRAMETYPE_COMPRESSED_SENSORS = 0x28,
|
||||||
|
CEL_CRSF_FRAMETYPE_ARM = 0x0D,
|
||||||
|
CEL_CRSF_FRAMETYPE_SETTING = 0x9E,
|
||||||
|
CEL_CRSF_FRAMETYPE_SUPERBOX = 0xA0,
|
||||||
|
CEL_CRSF_FRAMETYPE_DEVICE_SUPERBOX = 0xA1,
|
||||||
|
} cel_crsf_frame_type;
|
||||||
|
|
||||||
|
/* Parsed CRSF frame */
|
||||||
|
typedef struct {
|
||||||
|
uint8_t destination;
|
||||||
|
uint8_t source;
|
||||||
|
uint8_t type;
|
||||||
|
uint8_t size;
|
||||||
|
uint8_t payload[255];
|
||||||
|
uint8_t crc;
|
||||||
|
} cel_crsf_frame;
|
||||||
|
|
||||||
|
/* CRC8 calculation over CRSF frame data (CCITT poly 0x07) */
|
||||||
|
uint8_t cel_crsf_crc(uint8_t const* data, size_t len);
|
||||||
|
|
||||||
|
/* Validate CRC of a CRSF frame (header already stripped, starts at dest addr) */
|
||||||
|
int cel_crsf_frame_validate(cel_crsf_frame const* frame);
|
||||||
|
|
||||||
|
/* Parse a raw buffer into a cel_crsf_frame. Returns 0 on success, -1 on error.
|
||||||
|
buf should start with 0xC8 header. */
|
||||||
|
int cel_crsf_frame_parse(cel_crsf_frame* frame, uint8_t const* buf, size_t len);
|
||||||
|
|
||||||
|
/* Build a CRSF frame into dst buffer. Returns total bytes written.
|
||||||
|
dst must have space for at least 5 + size bytes (header, addr, src, type,
|
||||||
|
size byte, payload, crc). */
|
||||||
|
size_t cel_crsf_frame_build(uint8_t* dst, uint8_t destination, uint8_t source,
|
||||||
|
uint8_t type, uint8_t const* payload, uint8_t size);
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
#include "celrs/log_write.h"
|
||||||
|
#include <stdio.h>
|
||||||
|
|
||||||
|
void cel_log_write(char const* msg) {
|
||||||
|
printf("%s\n", msg);
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
void cel_log_write(char const* msg);
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
#include "celrs/logger.h"
|
||||||
|
#include "celrs/log_write.h"
|
||||||
|
#include <stdio.h>
|
||||||
|
|
||||||
|
static cel_log_level s_level = CEL_LOG_DEBUG;
|
||||||
|
|
||||||
|
void cel_logger_set_level(cel_log_level level) { s_level = level; }
|
||||||
|
|
||||||
|
static void emit(char const* prefix, char const* msg) {
|
||||||
|
char buf[512];
|
||||||
|
snprintf(buf, sizeof(buf), "[%s] %s", prefix, msg);
|
||||||
|
cel_log_write(buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
void cel_log_debug(char const* msg) { if (s_level <= CEL_LOG_DEBUG) emit("DEBUG", msg); }
|
||||||
|
void cel_log_info(char const* msg) { if (s_level <= CEL_LOG_INFO) emit("INFO", msg); }
|
||||||
|
void cel_log_warn(char const* msg) { if (s_level <= CEL_LOG_WARN) emit("WARN", msg); }
|
||||||
|
void cel_log_err(char const* msg) { if (s_level <= CEL_LOG_ERROR) emit("ERROR", msg); }
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
typedef enum { CEL_LOG_DEBUG = 0, CEL_LOG_INFO, CEL_LOG_WARN, CEL_LOG_ERROR, CEL_LOG_NONE } cel_log_level;
|
||||||
|
|
||||||
|
void cel_logger_set_level(cel_log_level level);
|
||||||
|
void cel_log_debug(char const* msg);
|
||||||
|
void cel_log_info(char const* msg);
|
||||||
|
void cel_log_warn(char const* msg);
|
||||||
|
void cel_log_err(char const* msg);
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
#ifdef _MSC_VER
|
||||||
|
#define _CRT_SECURE_NO_WARNINGS
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#include "celrs/serial.h"
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Platform-agnostic serial port implementation.
|
||||||
|
*
|
||||||
|
* Windows uses Win32 CreateFile/ReadFile/WriteFile.
|
||||||
|
* POSIX uses open/read/write with termios.
|
||||||
|
*
|
||||||
|
* This is a stub implementation that compiles but does nothing.
|
||||||
|
* Real platform-specific code will be added when TDD tests drive it.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
struct cel_serial_port {
|
||||||
|
char path[256];
|
||||||
|
int baud_rate;
|
||||||
|
int fd; /* platform-specific handle (HANDLE on Win, int on POSIX) */
|
||||||
|
};
|
||||||
|
|
||||||
|
cel_serial_port* cel_serial_open(char const* path, int baud_rate) {
|
||||||
|
if (path == NULL) return NULL;
|
||||||
|
|
||||||
|
cel_serial_port* port = (cel_serial_port*)calloc(1, sizeof(cel_serial_port));
|
||||||
|
if (port == NULL) return NULL;
|
||||||
|
|
||||||
|
strncpy(port->path, path, sizeof(port->path) - 1);
|
||||||
|
port->path[sizeof(port->path) - 1] = '\0';
|
||||||
|
port->baud_rate = baud_rate;
|
||||||
|
port->fd = -1;
|
||||||
|
|
||||||
|
/* TODO: platform-specific open (CreateFile on Win, open+termios on POSIX) */
|
||||||
|
(void)baud_rate;
|
||||||
|
|
||||||
|
return port;
|
||||||
|
}
|
||||||
|
|
||||||
|
void cel_serial_close(cel_serial_port* port) {
|
||||||
|
if (port == NULL) return;
|
||||||
|
/* TODO: platform-specific close */
|
||||||
|
free(port);
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t cel_serial_read(cel_serial_port* port, unsigned char* buf, size_t len, int timeout_ms) {
|
||||||
|
(void)port;
|
||||||
|
(void)buf;
|
||||||
|
(void)len;
|
||||||
|
(void)timeout_ms;
|
||||||
|
/* TODO: platform-specific read */
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t cel_serial_write(cel_serial_port* port, unsigned char const* buf, size_t len) {
|
||||||
|
(void)port;
|
||||||
|
(void)buf;
|
||||||
|
(void)len;
|
||||||
|
/* TODO: platform-specific write */
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void cel_serial_flush(cel_serial_port* port) {
|
||||||
|
(void)port;
|
||||||
|
/* TODO: platform-specific flush */
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
#pragma once
|
||||||
|
#include <stddef.h>
|
||||||
|
|
||||||
|
/* Opaque serial port handle */
|
||||||
|
typedef struct cel_serial_port cel_serial_port;
|
||||||
|
|
||||||
|
/* Open a serial port. path is platform-specific:
|
||||||
|
* Windows: "COM3"
|
||||||
|
* Linux/macOS: "/dev/ttyUSB0"
|
||||||
|
* baud_rate: 400000 is standard for ELRS CRSF
|
||||||
|
* Returns NULL on failure. */
|
||||||
|
cel_serial_port* cel_serial_open(char const* path, int baud_rate);
|
||||||
|
|
||||||
|
/* Close and free the serial port */
|
||||||
|
void cel_serial_close(cel_serial_port* port);
|
||||||
|
|
||||||
|
/* Read up to len bytes. Returns bytes read, 0 on timeout/error. */
|
||||||
|
size_t cel_serial_read(cel_serial_port* port, unsigned char* buf, size_t len, int timeout_ms);
|
||||||
|
|
||||||
|
/* Write data. Returns bytes written, 0 on error. */
|
||||||
|
size_t cel_serial_write(cel_serial_port* port, unsigned char const* buf, size_t len);
|
||||||
|
|
||||||
|
/* Flush output buffer */
|
||||||
|
void cel_serial_flush(cel_serial_port* port);
|
||||||
Vendored
+56
@@ -0,0 +1,56 @@
|
|||||||
|
# ==============================================================================
|
||||||
|
# Coverage
|
||||||
|
# ==============================================================================
|
||||||
|
# gcov-based coverage via --coverage, reported by gcovr.
|
||||||
|
# Works with GCC and Clang on Linux and macOS out of the box.
|
||||||
|
# Windows requires GCC (e.g. MinGW via scoop install gcc) or a Clang build
|
||||||
|
# that includes compiler-rt (clang_rt.profile).
|
||||||
|
#
|
||||||
|
# Requires: gcovr on PATH (install via: uv tool install gcovr)
|
||||||
|
# Usage: cmake -DENABLE_COVERAGE=ON ... then ninja coverage
|
||||||
|
# Report: build/coverage/index.html
|
||||||
|
# ==============================================================================
|
||||||
|
|
||||||
|
option(ENABLE_COVERAGE "Build with coverage instrumentation" OFF)
|
||||||
|
|
||||||
|
if (ENABLE_COVERAGE)
|
||||||
|
if (NOT CMAKE_C_COMPILER_ID MATCHES "GNU|Clang")
|
||||||
|
message(FATAL_ERROR "ENABLE_COVERAGE requires GCC or Clang")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
find_program(GCOVR_EXE gcovr REQUIRED)
|
||||||
|
|
||||||
|
if (CMAKE_C_COMPILER_ID MATCHES "Clang")
|
||||||
|
# gcovr needs a single-token gcov executable. Wrap llvm-cov gcov in a
|
||||||
|
# script placed in the build dir (guaranteed no spaces in path).
|
||||||
|
# AppleClang ships llvm-cov inside Xcode, reached only via xcrun.
|
||||||
|
if (WIN32)
|
||||||
|
find_program(LLVM_COV_EXE llvm-cov REQUIRED)
|
||||||
|
set(GCOV_EXECUTABLE "${CMAKE_BINARY_DIR}/llvm-gcov.bat")
|
||||||
|
file(WRITE "${GCOV_EXECUTABLE}" "@echo off\r\n\"${LLVM_COV_EXE}\" gcov %*\r\n")
|
||||||
|
elseif (CMAKE_C_COMPILER_ID STREQUAL "AppleClang")
|
||||||
|
find_program(XCRUN_EXE xcrun REQUIRED)
|
||||||
|
set(GCOV_EXECUTABLE "${CMAKE_BINARY_DIR}/llvm-gcov.sh")
|
||||||
|
file(WRITE "${GCOV_EXECUTABLE}" "#!/bin/sh\nexec xcrun llvm-cov gcov \"$@\"\n")
|
||||||
|
file(CHMOD "${GCOV_EXECUTABLE}"
|
||||||
|
PERMISSIONS OWNER_READ OWNER_WRITE OWNER_EXECUTE
|
||||||
|
GROUP_READ GROUP_EXECUTE
|
||||||
|
WORLD_READ WORLD_EXECUTE)
|
||||||
|
else()
|
||||||
|
find_program(LLVM_COV_EXE llvm-cov REQUIRED)
|
||||||
|
set(GCOV_EXECUTABLE "${CMAKE_BINARY_DIR}/llvm-gcov.sh")
|
||||||
|
file(WRITE "${GCOV_EXECUTABLE}" "#!/bin/sh\nexec \"${LLVM_COV_EXE}\" gcov \"$@\"\n")
|
||||||
|
file(CHMOD "${GCOV_EXECUTABLE}"
|
||||||
|
PERMISSIONS OWNER_READ OWNER_WRITE OWNER_EXECUTE
|
||||||
|
GROUP_READ GROUP_EXECUTE
|
||||||
|
WORLD_READ WORLD_EXECUTE)
|
||||||
|
endif()
|
||||||
|
message(STATUS "Coverage: enabled (gcovr: ${GCOVR_EXE}, gcov: llvm-cov gcov)")
|
||||||
|
else()
|
||||||
|
set(GCOV_EXECUTABLE "gcov")
|
||||||
|
message(STATUS "Coverage: enabled (gcovr: ${GCOVR_EXE})")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
add_compile_options(--coverage -O0 -g)
|
||||||
|
add_link_options(--coverage)
|
||||||
|
endif()
|
||||||
Vendored
+92
@@ -0,0 +1,92 @@
|
|||||||
|
# ==============================================================================
|
||||||
|
# Find CMock
|
||||||
|
# ==============================================================================
|
||||||
|
# This module fetches the CMock mocking framework (depends on Unity).
|
||||||
|
#
|
||||||
|
# Targets provided:
|
||||||
|
# CMock::CMock - The CMock library target
|
||||||
|
#
|
||||||
|
# Variables set:
|
||||||
|
# CMock_FOUND - TRUE if CMock is available
|
||||||
|
# CMock_LIBRARIES - The CMock library target (CMock::CMock)
|
||||||
|
# CMock_INCLUDE_DIR - Include directories for CMock
|
||||||
|
# CMock_VERSION - Version of CMock (if available)
|
||||||
|
#
|
||||||
|
# Generator variables (set when Ruby is found):
|
||||||
|
# CMOCK_SCRIPT - Path to lib/cmock.rb
|
||||||
|
# RUBY_EXECUTABLE - Path to ruby interpreter
|
||||||
|
# ==============================================================================
|
||||||
|
|
||||||
|
if (DEFINED _FINDCMOCK_INCLUDED)
|
||||||
|
return()
|
||||||
|
endif()
|
||||||
|
set(_FINDCMOCK_INCLUDED TRUE)
|
||||||
|
|
||||||
|
find_package(Unity REQUIRED)
|
||||||
|
|
||||||
|
if (DEFINED CMock_FIND_VERSION AND NOT CMock_FIND_VERSION STREQUAL "")
|
||||||
|
set(CMOCK_VERSION "${CMock_FIND_VERSION}")
|
||||||
|
else()
|
||||||
|
set(CMOCK_VERSION "2.6.0")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
message(STATUS "Fetching CMock ${CMOCK_VERSION}")
|
||||||
|
|
||||||
|
include(FetchContent)
|
||||||
|
|
||||||
|
FetchContent_Declare(
|
||||||
|
cmock
|
||||||
|
URL https://github.com/ThrowTheSwitch/CMock/archive/refs/tags/v${CMOCK_VERSION}.zip
|
||||||
|
DOWNLOAD_EXTRACT_TIMESTAMP TRUE
|
||||||
|
)
|
||||||
|
|
||||||
|
# CMock uses Meson — bypass its build system and compile src/cmock.c directly.
|
||||||
|
# FetchContent_MakeAvailable cannot be used here (no CMakeLists.txt in CMock),
|
||||||
|
# so we call FetchContent_Populate directly and opt into the old policy.
|
||||||
|
cmake_policy(PUSH)
|
||||||
|
cmake_policy(SET CMP0169 OLD)
|
||||||
|
FetchContent_GetProperties(cmock)
|
||||||
|
if (NOT cmock_POPULATED)
|
||||||
|
FetchContent_Populate(cmock)
|
||||||
|
endif()
|
||||||
|
cmake_policy(POP)
|
||||||
|
|
||||||
|
# The Ruby generator expects vendor/unity/auto/type_sanitizer.rb — populate
|
||||||
|
# it from the Unity source we already have rather than needing a git submodule
|
||||||
|
set(_cmock_vendor_auto "${cmock_SOURCE_DIR}/vendor/unity/auto")
|
||||||
|
if (NOT EXISTS "${_cmock_vendor_auto}/type_sanitizer.rb")
|
||||||
|
file(MAKE_DIRECTORY "${_cmock_vendor_auto}")
|
||||||
|
file(COPY "${unity_SOURCE_DIR}/auto/" DESTINATION "${_cmock_vendor_auto}")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
if (NOT TARGET cmock)
|
||||||
|
add_library(cmock STATIC "${cmock_SOURCE_DIR}/src/cmock.c")
|
||||||
|
target_include_directories(cmock PUBLIC
|
||||||
|
"${cmock_SOURCE_DIR}/src"
|
||||||
|
"${unity_SOURCE_DIR}/src"
|
||||||
|
)
|
||||||
|
target_link_libraries(cmock PUBLIC unity)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
if (NOT TARGET CMock::CMock)
|
||||||
|
add_library(CMock::CMock ALIAS cmock)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
set(CMock_FOUND TRUE)
|
||||||
|
set(CMock_LIBRARIES CMock::CMock)
|
||||||
|
set(CMock_VERSION "${CMOCK_VERSION}")
|
||||||
|
set(CMock_INCLUDE_DIR "${cmock_SOURCE_DIR}/src")
|
||||||
|
set(CMOCK_SCRIPT "${cmock_SOURCE_DIR}/lib/cmock.rb" CACHE FILEPATH "Path to CMock Ruby generator")
|
||||||
|
|
||||||
|
if (CMock_INCLUDE_DIR AND TARGET cmock)
|
||||||
|
set_target_properties(cmock PROPERTIES
|
||||||
|
INTERFACE_SYSTEM_INCLUDE_DIRECTORIES "${CMock_INCLUDE_DIR}"
|
||||||
|
)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
find_program(RUBY_EXECUTABLE ruby)
|
||||||
|
if (NOT RUBY_EXECUTABLE)
|
||||||
|
message(WARNING "Ruby not found — CMock code generation unavailable")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
set(CMOCK_LICENSE_FILE "${cmock_SOURCE_DIR}/LICENSE.txt" CACHE FILEPATH "Path to CMock license file")
|
||||||
Vendored
+67
@@ -0,0 +1,67 @@
|
|||||||
|
# ==============================================================================
|
||||||
|
# Find Unity
|
||||||
|
# ==============================================================================
|
||||||
|
# This module fetches the Unity unit testing framework.
|
||||||
|
#
|
||||||
|
# Targets provided:
|
||||||
|
# Unity::Unity - The Unity library target
|
||||||
|
#
|
||||||
|
# Variables set:
|
||||||
|
# Unity_FOUND - TRUE if Unity is available
|
||||||
|
# Unity_LIBRARIES - The Unity library target (Unity::Unity)
|
||||||
|
# Unity_INCLUDE_DIR - Include directories for Unity
|
||||||
|
# Unity_VERSION - Version of Unity (if available)
|
||||||
|
# ==============================================================================
|
||||||
|
|
||||||
|
if (DEFINED _FINDUNITY_INCLUDED)
|
||||||
|
return()
|
||||||
|
endif()
|
||||||
|
set(_FINDUNITY_INCLUDED TRUE)
|
||||||
|
|
||||||
|
if (DEFINED Unity_FIND_VERSION AND NOT Unity_FIND_VERSION STREQUAL "")
|
||||||
|
set(UNITY_VERSION "${Unity_FIND_VERSION}")
|
||||||
|
else()
|
||||||
|
set(UNITY_VERSION "2.6.1")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
message(STATUS "Fetching Unity ${UNITY_VERSION}")
|
||||||
|
|
||||||
|
include(FetchContent)
|
||||||
|
|
||||||
|
FetchContent_Declare(
|
||||||
|
unity
|
||||||
|
URL https://github.com/ThrowTheSwitch/Unity/archive/refs/tags/v${UNITY_VERSION}.zip
|
||||||
|
DOWNLOAD_EXTRACT_TIMESTAMP TRUE
|
||||||
|
)
|
||||||
|
|
||||||
|
FetchContent_MakeAvailable(unity)
|
||||||
|
|
||||||
|
# Unity sets INTERFACE_SYSTEM_INCLUDE_DIRECTORIES to a path inside the build
|
||||||
|
# tree, which CMake rejects on newer versions. The path stays in
|
||||||
|
# INTERFACE_INCLUDE_DIRECTORIES so headers are still found.
|
||||||
|
if (TARGET unity)
|
||||||
|
set_target_properties(unity PROPERTIES INTERFACE_SYSTEM_INCLUDE_DIRECTORIES "")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
if (NOT TARGET Unity::Unity)
|
||||||
|
if (TARGET unity)
|
||||||
|
add_library(Unity::Unity ALIAS unity)
|
||||||
|
else()
|
||||||
|
message(FATAL_ERROR "Could not fetch Unity; no target unity or Unity::Unity available")
|
||||||
|
endif()
|
||||||
|
endif()
|
||||||
|
|
||||||
|
set(Unity_FOUND TRUE)
|
||||||
|
set(Unity_LIBRARIES Unity::Unity)
|
||||||
|
set(Unity_VERSION "${UNITY_VERSION}")
|
||||||
|
set(Unity_INCLUDE_DIR "${unity_SOURCE_DIR}/src")
|
||||||
|
|
||||||
|
|
||||||
|
if (TARGET unity)
|
||||||
|
target_compile_definitions(unity PUBLIC
|
||||||
|
UNITY_OUTPUT_COLOR
|
||||||
|
UNITY_INCLUDE_PRINT_FORMATTED
|
||||||
|
)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
set(UNITY_LICENSE_FILE "${unity_SOURCE_DIR}/LICENSE.txt" CACHE FILEPATH "Path to Unity license file")
|
||||||
Vendored
+27
@@ -0,0 +1,27 @@
|
|||||||
|
# ==============================================================================
|
||||||
|
# Compiler Flags
|
||||||
|
# ==============================================================================
|
||||||
|
# Sets BASE_OPTIONS (warning flags) and BASE_DEFINITIONS.
|
||||||
|
# Apply per-target via target_compile_options / target_compile_definitions
|
||||||
|
# to avoid polluting fetched dependencies.
|
||||||
|
#
|
||||||
|
# Requires: Platform.cmake (for IS_CLANG_OR_GCC / IS_MSVC)
|
||||||
|
# ==============================================================================
|
||||||
|
|
||||||
|
set(BASE_DEFINITIONS "")
|
||||||
|
set(BASE_LIBRARIES "")
|
||||||
|
|
||||||
|
set(BASE_OPTIONS "")
|
||||||
|
if (IS_CLANG_OR_GCC)
|
||||||
|
set(BASE_OPTIONS
|
||||||
|
"-Wall"
|
||||||
|
"-Wextra"
|
||||||
|
"-Werror"
|
||||||
|
)
|
||||||
|
elseif (IS_MSVC)
|
||||||
|
set(BASE_OPTIONS
|
||||||
|
"/W4"
|
||||||
|
"/WX"
|
||||||
|
"/utf-8"
|
||||||
|
)
|
||||||
|
endif()
|
||||||
Vendored
+17
@@ -0,0 +1,17 @@
|
|||||||
|
# ==============================================================================
|
||||||
|
# IDE Integration
|
||||||
|
# ==============================================================================
|
||||||
|
# Groups dependency targets into folders for Visual Studio / Xcode.
|
||||||
|
# Has no effect on the build.
|
||||||
|
# ==============================================================================
|
||||||
|
|
||||||
|
function(set_target_folder target folder)
|
||||||
|
if (TARGET ${target})
|
||||||
|
set_target_properties(${target} PROPERTIES FOLDER ${folder})
|
||||||
|
endif()
|
||||||
|
endfunction()
|
||||||
|
|
||||||
|
if (CMAKE_GENERATOR MATCHES "Visual Studio" OR CMAKE_GENERATOR MATCHES "Xcode")
|
||||||
|
set_target_folder(unity deps)
|
||||||
|
set_target_folder(cmock deps)
|
||||||
|
endif()
|
||||||
Vendored
+64
@@ -0,0 +1,64 @@
|
|||||||
|
# ==============================================================================
|
||||||
|
# Platform Detection
|
||||||
|
# ==============================================================================
|
||||||
|
# This module detects the current platform and compiler, setting IS_* variables
|
||||||
|
# that can be used throughout the build system for conditional logic.
|
||||||
|
#
|
||||||
|
# Compiler flags set:
|
||||||
|
# IS_CLANG_OR_GCC - TRUE if using Clang or GCC compiler
|
||||||
|
# IS_MSVC - TRUE if using Microsoft Visual C++ compiler
|
||||||
|
#
|
||||||
|
# Platform flags set:
|
||||||
|
# IS_WINDOWS - TRUE if building for Windows
|
||||||
|
# IS_LINUX - TRUE if building for Linux
|
||||||
|
# IS_MACOS - TRUE if building for macOS
|
||||||
|
# IS_IOS - TRUE if building for iOS
|
||||||
|
# IS_ANDROID - TRUE if building for Android
|
||||||
|
# IS_EMSCRIPTEN - TRUE if building for WebAssembly via Emscripten
|
||||||
|
# ==============================================================================
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
# Compiler Detection
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
set(IS_CLANG_OR_GCC FALSE)
|
||||||
|
set(IS_MSVC FALSE)
|
||||||
|
|
||||||
|
if(MSVC)
|
||||||
|
set(IS_MSVC TRUE)
|
||||||
|
elseif(CMAKE_C_COMPILER_ID MATCHES "Clang|GNU")
|
||||||
|
set(IS_CLANG_OR_GCC TRUE)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
# Platform Detection
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
set(IS_WINDOWS FALSE)
|
||||||
|
set(IS_LINUX FALSE)
|
||||||
|
set(IS_MACOS FALSE)
|
||||||
|
set(IS_IOS FALSE)
|
||||||
|
set(IS_ANDROID FALSE)
|
||||||
|
set(IS_EMSCRIPTEN FALSE)
|
||||||
|
|
||||||
|
if(EMSCRIPTEN)
|
||||||
|
message(STATUS "Platform: Emscripten")
|
||||||
|
set(IS_EMSCRIPTEN TRUE)
|
||||||
|
elseif(ANDROID)
|
||||||
|
message(STATUS "Platform: Android")
|
||||||
|
set(IS_ANDROID TRUE)
|
||||||
|
elseif(APPLE)
|
||||||
|
if(IOS)
|
||||||
|
message(STATUS "Platform: iOS")
|
||||||
|
set(IS_IOS TRUE)
|
||||||
|
else()
|
||||||
|
message(STATUS "Platform: macOS")
|
||||||
|
set(IS_MACOS TRUE)
|
||||||
|
endif()
|
||||||
|
elseif(WIN32)
|
||||||
|
message(STATUS "Platform: Windows")
|
||||||
|
set(IS_WINDOWS TRUE)
|
||||||
|
elseif(UNIX)
|
||||||
|
message(STATUS "Platform: Linux")
|
||||||
|
set(IS_LINUX TRUE)
|
||||||
|
else()
|
||||||
|
message(FATAL_ERROR "Unknown platform!")
|
||||||
|
endif()
|
||||||
Vendored
+28
@@ -0,0 +1,28 @@
|
|||||||
|
# ==============================================================================
|
||||||
|
# Sanitizers
|
||||||
|
# ==============================================================================
|
||||||
|
# AddressSanitizer (ASan) support.
|
||||||
|
# Works with GCC/Clang on Linux and macOS, Clang on Windows (requires
|
||||||
|
# compiler-rt with sanitizers), and MSVC on Windows (/fsanitize=address).
|
||||||
|
#
|
||||||
|
# Usage: cmake -DENABLE_ASAN=ON ...
|
||||||
|
# ==============================================================================
|
||||||
|
|
||||||
|
option(ENABLE_ASAN "Build with AddressSanitizer" OFF)
|
||||||
|
|
||||||
|
if (ENABLE_ASAN)
|
||||||
|
if (ENABLE_COVERAGE)
|
||||||
|
message(FATAL_ERROR "ENABLE_ASAN and ENABLE_COVERAGE cannot be used together")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
if (MSVC)
|
||||||
|
add_compile_options(/fsanitize=address)
|
||||||
|
message(STATUS "ASan: enabled (MSVC)")
|
||||||
|
elseif (CMAKE_C_COMPILER_ID MATCHES "GNU|Clang")
|
||||||
|
add_compile_options(-fsanitize=address -fno-omit-frame-pointer)
|
||||||
|
add_link_options(-fsanitize=address)
|
||||||
|
message(STATUS "ASan: enabled (${CMAKE_C_COMPILER_ID})")
|
||||||
|
else()
|
||||||
|
message(FATAL_ERROR "ENABLE_ASAN: unsupported compiler ${CMAKE_C_COMPILER_ID}")
|
||||||
|
endif()
|
||||||
|
endif()
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
find_package(Unity REQUIRED)
|
||||||
|
find_package(CMock REQUIRED)
|
||||||
|
|
||||||
|
set(MOCK_GEN_DIR "${CMAKE_CURRENT_BINARY_DIR}/mocks")
|
||||||
|
file(MAKE_DIRECTORY "${MOCK_GEN_DIR}")
|
||||||
|
|
||||||
|
# Generate a CMock mock from a header and attach it to a target.
|
||||||
|
# Usage: cmock_generate_mock(<target> <absolute-path-to-header>)
|
||||||
|
# CMock names generated files Mock<Name>.h/.c (capital M, no separator)
|
||||||
|
function(cmock_generate_mock target header)
|
||||||
|
if (NOT RUBY_EXECUTABLE)
|
||||||
|
message(FATAL_ERROR "Ruby is required for CMock generation")
|
||||||
|
endif()
|
||||||
|
get_filename_component(name "${header}" NAME_WE)
|
||||||
|
get_filename_component(header_dir "${header}" DIRECTORY)
|
||||||
|
set(mock_src "${MOCK_GEN_DIR}/Mock${name}.c")
|
||||||
|
set(mock_hdr "${MOCK_GEN_DIR}/Mock${name}.h")
|
||||||
|
add_custom_command(
|
||||||
|
OUTPUT "${mock_src}" "${mock_hdr}"
|
||||||
|
COMMAND "${RUBY_EXECUTABLE}" "${CMOCK_SCRIPT}"
|
||||||
|
"--mock_path=${MOCK_GEN_DIR}"
|
||||||
|
"${header}"
|
||||||
|
DEPENDS "${header}"
|
||||||
|
COMMENT "CMock: generating Mock${name}"
|
||||||
|
VERBATIM
|
||||||
|
)
|
||||||
|
target_sources("${target}" PRIVATE "${mock_src}")
|
||||||
|
target_include_directories("${target}" PRIVATE "${MOCK_GEN_DIR}" "${header_dir}")
|
||||||
|
endfunction()
|
||||||
|
|
||||||
|
set(TEST_TARGETS "")
|
||||||
|
|
||||||
|
# CRSF tests — pure functions (CRC, parse, build), no mock needed
|
||||||
|
add_executable(test_crsf test_crsf.c)
|
||||||
|
target_include_directories(test_crsf PRIVATE "${CMAKE_SOURCE_DIR}")
|
||||||
|
target_link_libraries(test_crsf PRIVATE celrs_crsf Unity::Unity)
|
||||||
|
target_compile_features(test_crsf PRIVATE c_std_23)
|
||||||
|
add_test(NAME test_crsf COMMAND test_crsf)
|
||||||
|
list(APPEND TEST_TARGETS test_crsf)
|
||||||
|
|
||||||
|
# Serial tests — mocks log_write.h for any logging calls
|
||||||
|
add_executable(test_serial test_serial.c)
|
||||||
|
target_include_directories(test_serial PRIVATE "${CMAKE_SOURCE_DIR}")
|
||||||
|
target_link_libraries(test_serial PRIVATE celrs_serial Unity::Unity CMock::CMock)
|
||||||
|
target_compile_features(test_serial PRIVATE c_std_23)
|
||||||
|
cmock_generate_mock(test_serial "${CMAKE_SOURCE_DIR}/celrs/log_write.h")
|
||||||
|
add_test(NAME test_serial COMMAND test_serial)
|
||||||
|
list(APPEND TEST_TARGETS test_serial)
|
||||||
|
|
||||||
|
# Logger tests — mocks log_write.h so output calls are intercepted
|
||||||
|
add_executable(test_logger test_logger.c)
|
||||||
|
target_include_directories(test_logger PRIVATE "${CMAKE_SOURCE_DIR}")
|
||||||
|
target_link_libraries(test_logger PRIVATE celrs_logger Unity::Unity CMock::CMock)
|
||||||
|
target_compile_features(test_logger PRIVATE c_std_23)
|
||||||
|
cmock_generate_mock(test_logger "${CMAKE_SOURCE_DIR}/celrs/log_write.h")
|
||||||
|
add_test(NAME test_logger COMMAND test_logger)
|
||||||
|
list(APPEND TEST_TARGETS test_logger)
|
||||||
|
|
||||||
|
# 'check' builds all suites and runs CTest with full Unity output.
|
||||||
|
# USES_TERMINAL keeps ANSI colors alive through Ninja's output buffering.
|
||||||
|
add_custom_target(check
|
||||||
|
COMMAND ${CMAKE_CTEST_COMMAND}
|
||||||
|
--test-dir "${CMAKE_BINARY_DIR}"
|
||||||
|
--output-on-failure
|
||||||
|
--progress
|
||||||
|
WORKING_DIRECTORY "${CMAKE_BINARY_DIR}"
|
||||||
|
USES_TERMINAL
|
||||||
|
DEPENDS ${TEST_TARGETS}
|
||||||
|
)
|
||||||
|
|
||||||
|
if (ENABLE_COVERAGE)
|
||||||
|
set(COVERAGE_DIR "${CMAKE_BINARY_DIR}/coverage")
|
||||||
|
add_custom_target(coverage
|
||||||
|
COMMAND ${CMAKE_CTEST_COMMAND} --output-on-failure
|
||||||
|
COMMAND ${CMAKE_COMMAND} -E make_directory "${COVERAGE_DIR}"
|
||||||
|
COMMAND ${GCOVR_EXE}
|
||||||
|
--gcov-executable "${GCOV_EXECUTABLE}"
|
||||||
|
--root "${CMAKE_SOURCE_DIR}"
|
||||||
|
--filter "${CMAKE_SOURCE_DIR}/celrs/"
|
||||||
|
--exclude "${CMAKE_SOURCE_DIR}/tests/"
|
||||||
|
--exclude ".*Mock.*"
|
||||||
|
--exclude ".*unity.*"
|
||||||
|
--exclude ".*cmock.*"
|
||||||
|
--html-details "${COVERAGE_DIR}/index.html"
|
||||||
|
--txt
|
||||||
|
--print-summary
|
||||||
|
"${CMAKE_BINARY_DIR}"
|
||||||
|
WORKING_DIRECTORY "${CMAKE_BINARY_DIR}"
|
||||||
|
USES_TERMINAL
|
||||||
|
DEPENDS ${TEST_TARGETS}
|
||||||
|
COMMENT "Coverage report: ${COVERAGE_DIR}/index.html"
|
||||||
|
)
|
||||||
|
endif()
|
||||||
@@ -0,0 +1,115 @@
|
|||||||
|
#include "unity.h"
|
||||||
|
#include "celrs/crsf.h"
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
void setUp(void) {}
|
||||||
|
void tearDown(void) {}
|
||||||
|
|
||||||
|
/* CRC tests */
|
||||||
|
void test_crc_empty(void) {
|
||||||
|
uint8_t data[1] = {0};
|
||||||
|
TEST_ASSERT_EQUAL_UINT8(0x00, cel_crsf_crc(data, 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_crc_single_byte(void) {
|
||||||
|
uint8_t data[1] = {0x01};
|
||||||
|
uint8_t crc = cel_crsf_crc(data, 1);
|
||||||
|
TEST_ASSERT_TRUE(crc != 0); /* non-trivial */
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_crc_known_value(void) {
|
||||||
|
/* CRSF heartbeat frame data (dest+src+type+size+payload):
|
||||||
|
* {0x10, 0x80, 0x03, 0x02, 0x80, 0x01}
|
||||||
|
* Known CRC for this sequence */
|
||||||
|
uint8_t data[6] = {0x10, 0x80, 0x03, 0x02, 0x80, 0x01};
|
||||||
|
uint8_t crc = cel_crsf_crc(data, 6);
|
||||||
|
TEST_ASSERT_TRUE(crc != 0);
|
||||||
|
/* Verify idempotency */
|
||||||
|
uint8_t crc2 = cel_crsf_crc(data, 6);
|
||||||
|
TEST_ASSERT_EQUAL_UINT8(crc, crc2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Frame parse tests */
|
||||||
|
void test_parse_invalid_header(void) {
|
||||||
|
cel_crsf_frame frame;
|
||||||
|
uint8_t buf[8] = {0x00, 0x10, 0x80, 0x03, 0x02, 0x80, 0x01, 0x00};
|
||||||
|
TEST_ASSERT_EQUAL_INT(-1, cel_crsf_frame_parse(&frame, buf, 8));
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_parse_too_short(void) {
|
||||||
|
cel_crsf_frame frame;
|
||||||
|
uint8_t buf[2] = {0xC8, 0x10};
|
||||||
|
TEST_ASSERT_EQUAL_INT(-1, cel_crsf_frame_parse(&frame, buf, 2));
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_parse_null_frame(void) {
|
||||||
|
uint8_t buf[8] = {0xC8, 0x10, 0x80, 0x03, 0x02, 0x80, 0x01, 0x00};
|
||||||
|
TEST_ASSERT_EQUAL_INT(-1, cel_crsf_frame_parse(NULL, buf, 8));
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_parse_null_buf(void) {
|
||||||
|
cel_crsf_frame frame;
|
||||||
|
TEST_ASSERT_EQUAL_INT(-1, cel_crsf_frame_parse(&frame, NULL, 8));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Frame build tests */
|
||||||
|
void test_build_heartbeat(void) {
|
||||||
|
uint8_t dst[256];
|
||||||
|
uint8_t payload[2] = {0x80, 0x01};
|
||||||
|
size_t len = cel_crsf_frame_build(dst, 0x00, 0x80, 0x03, payload, 2);
|
||||||
|
|
||||||
|
TEST_ASSERT_GREATER_THAN(0, len);
|
||||||
|
TEST_ASSERT_EQUAL_UINT8(CEL_CRSF_FRAME_HEADER, dst[0]);
|
||||||
|
TEST_ASSERT_EQUAL_UINT8(0x00, dst[1]); /* destination */
|
||||||
|
TEST_ASSERT_EQUAL_UINT8(0x80, dst[2]); /* source */
|
||||||
|
TEST_ASSERT_EQUAL_UINT8(0x03, dst[3]); /* type: heartbeat */
|
||||||
|
TEST_ASSERT_EQUAL_UINT8(0x02, dst[4]); /* size */
|
||||||
|
TEST_ASSERT_EQUAL_UINT8(0x80, dst[5]); /* payload[0] */
|
||||||
|
TEST_ASSERT_EQUAL_UINT8(0x01, dst[6]); /* payload[1] */
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_build_roundtrip(void) {
|
||||||
|
uint8_t dst[256];
|
||||||
|
uint8_t payload[4] = {0xAA, 0xBB, 0xCC, 0xDD};
|
||||||
|
size_t len = cel_crsf_frame_build(dst, 0x10, 0x80, 0x01, payload, 4);
|
||||||
|
|
||||||
|
/* Parse the built frame back */
|
||||||
|
cel_crsf_frame frame;
|
||||||
|
TEST_ASSERT_EQUAL_INT(0, cel_crsf_frame_parse(&frame, dst, len));
|
||||||
|
TEST_ASSERT_EQUAL_UINT8(0x10, frame.destination);
|
||||||
|
TEST_ASSERT_EQUAL_UINT8(0x80, frame.source);
|
||||||
|
TEST_ASSERT_EQUAL_UINT8(0x01, frame.type);
|
||||||
|
TEST_ASSERT_EQUAL_UINT8(4, frame.size);
|
||||||
|
TEST_ASSERT_EQUAL_UINT8(0xAA, frame.payload[0]);
|
||||||
|
TEST_ASSERT_EQUAL_UINT8(0xDD, frame.payload[3]);
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_build_null_dst(void) {
|
||||||
|
uint8_t payload[2] = {0x01, 0x02};
|
||||||
|
TEST_ASSERT_EQUAL_UINT(0, cel_crsf_frame_build(NULL, 0x00, 0x80, 0x03, payload, 2));
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_build_null_payload(void) {
|
||||||
|
uint8_t dst[256];
|
||||||
|
size_t len = cel_crsf_frame_build(dst, 0x10, 0x80, 0x03, NULL, 0);
|
||||||
|
TEST_ASSERT_GREATER_THAN(0, len);
|
||||||
|
/* Should still have valid CRC for empty payload */
|
||||||
|
cel_crsf_frame frame;
|
||||||
|
TEST_ASSERT_EQUAL_INT(0, cel_crsf_frame_parse(&frame, dst, len));
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(void) {
|
||||||
|
UNITY_BEGIN();
|
||||||
|
RUN_TEST(test_crc_empty);
|
||||||
|
RUN_TEST(test_crc_single_byte);
|
||||||
|
RUN_TEST(test_crc_known_value);
|
||||||
|
RUN_TEST(test_parse_invalid_header);
|
||||||
|
RUN_TEST(test_parse_too_short);
|
||||||
|
RUN_TEST(test_parse_null_frame);
|
||||||
|
RUN_TEST(test_parse_null_buf);
|
||||||
|
RUN_TEST(test_build_heartbeat);
|
||||||
|
RUN_TEST(test_build_roundtrip);
|
||||||
|
RUN_TEST(test_build_null_dst);
|
||||||
|
RUN_TEST(test_build_null_payload);
|
||||||
|
return UNITY_END();
|
||||||
|
}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
#include "unity.h"
|
||||||
|
#include "celrs/logger.h"
|
||||||
|
#include "Mocklog_write.h"
|
||||||
|
|
||||||
|
void setUp(void) { Mocklog_write_Init(); cel_logger_set_level(CEL_LOG_DEBUG); }
|
||||||
|
void tearDown(void) { Mocklog_write_Verify(); Mocklog_write_Destroy(); }
|
||||||
|
|
||||||
|
void test_log_debug_emits_at_debug_level(void) {
|
||||||
|
cel_log_write_Expect("[DEBUG] hello");
|
||||||
|
cel_log_debug("hello");
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_log_info_emits_at_debug_level(void) {
|
||||||
|
cel_log_write_Expect("[INFO] world");
|
||||||
|
cel_log_info("world");
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_log_warn_emits_at_warn_level(void) {
|
||||||
|
cel_logger_set_level(CEL_LOG_WARN);
|
||||||
|
cel_log_write_Expect("[WARN] alert");
|
||||||
|
cel_log_warn("alert");
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_log_err_emits_at_warn_level(void) {
|
||||||
|
cel_logger_set_level(CEL_LOG_WARN);
|
||||||
|
cel_log_write_Expect("[ERROR] fatal");
|
||||||
|
cel_log_err("fatal");
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_log_debug_suppressed_at_info_level(void) {
|
||||||
|
cel_logger_set_level(CEL_LOG_INFO);
|
||||||
|
cel_log_debug("silent");
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_log_info_suppressed_at_warn_level(void) {
|
||||||
|
cel_logger_set_level(CEL_LOG_WARN);
|
||||||
|
cel_log_info("silent");
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_log_warn_suppressed_at_error_level(void) {
|
||||||
|
cel_logger_set_level(CEL_LOG_ERROR);
|
||||||
|
cel_log_warn("silent");
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_log_none_suppresses_all(void) {
|
||||||
|
cel_logger_set_level(CEL_LOG_NONE);
|
||||||
|
cel_log_debug("silent");
|
||||||
|
cel_log_info("silent");
|
||||||
|
cel_log_warn("silent");
|
||||||
|
cel_log_err("silent");
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_level_can_be_raised_then_lowered(void) {
|
||||||
|
cel_logger_set_level(CEL_LOG_ERROR);
|
||||||
|
cel_log_info("silent");
|
||||||
|
cel_logger_set_level(CEL_LOG_DEBUG);
|
||||||
|
cel_log_write_Expect("[INFO] now visible");
|
||||||
|
cel_log_info("now visible");
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(void) {
|
||||||
|
UNITY_BEGIN();
|
||||||
|
RUN_TEST(test_log_debug_emits_at_debug_level);
|
||||||
|
RUN_TEST(test_log_info_emits_at_debug_level);
|
||||||
|
RUN_TEST(test_log_warn_emits_at_warn_level);
|
||||||
|
RUN_TEST(test_log_err_emits_at_warn_level);
|
||||||
|
RUN_TEST(test_log_debug_suppressed_at_info_level);
|
||||||
|
RUN_TEST(test_log_info_suppressed_at_warn_level);
|
||||||
|
RUN_TEST(test_log_warn_suppressed_at_error_level);
|
||||||
|
RUN_TEST(test_log_none_suppresses_all);
|
||||||
|
RUN_TEST(test_level_can_be_raised_then_lowered);
|
||||||
|
return UNITY_END();
|
||||||
|
}
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
#include "unity.h"
|
||||||
|
#include "celrs/serial.h"
|
||||||
|
#include "Mocklog_write.h"
|
||||||
|
|
||||||
|
void setUp(void) { Mocklog_write_Init(); }
|
||||||
|
void tearDown(void) { Mocklog_write_Verify(); Mocklog_write_Destroy(); }
|
||||||
|
|
||||||
|
void test_open_valid_path(void) {
|
||||||
|
cel_serial_port* port = cel_serial_open("COM3", 400000);
|
||||||
|
TEST_ASSERT_NOT_NULL(port);
|
||||||
|
cel_serial_close(port);
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_open_null_path(void) {
|
||||||
|
TEST_ASSERT_NULL(cel_serial_open(NULL, 400000));
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_open_preserves_path(void) {
|
||||||
|
cel_serial_port* port = cel_serial_open("/dev/ttyUSB0", 400000);
|
||||||
|
TEST_ASSERT_NOT_NULL(port);
|
||||||
|
/* path is stored internally; verify by roundtrip behavior */
|
||||||
|
cel_serial_close(port);
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_close_null(void) {
|
||||||
|
/* Should not crash */
|
||||||
|
cel_serial_close(NULL);
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_read_returns_zero_stub(void) {
|
||||||
|
cel_serial_port* port = cel_serial_open("COM3", 400000);
|
||||||
|
TEST_ASSERT_NOT_NULL(port);
|
||||||
|
uint8_t buf[16];
|
||||||
|
/* Stub implementation returns 0 */
|
||||||
|
size_t n = cel_serial_read(port, buf, sizeof(buf), 100);
|
||||||
|
TEST_ASSERT_EQUAL_UINT(0, n);
|
||||||
|
cel_serial_close(port);
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_write_returns_zero_stub(void) {
|
||||||
|
cel_serial_port* port = cel_serial_open("COM3", 400000);
|
||||||
|
TEST_ASSERT_NOT_NULL(port);
|
||||||
|
uint8_t buf[4] = {0xC8, 0x10, 0x80, 0x03};
|
||||||
|
/* Stub implementation returns 0 */
|
||||||
|
size_t n = cel_serial_write(port, buf, sizeof(buf));
|
||||||
|
TEST_ASSERT_EQUAL_UINT(0, n);
|
||||||
|
cel_serial_close(port);
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_flush_no_crash(void) {
|
||||||
|
cel_serial_port* port = cel_serial_open("COM3", 400000);
|
||||||
|
TEST_ASSERT_NOT_NULL(port);
|
||||||
|
cel_serial_flush(port); /* should not crash */
|
||||||
|
cel_serial_close(port);
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_flush_null(void) {
|
||||||
|
cel_serial_flush(NULL); /* should not crash */
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(void) {
|
||||||
|
UNITY_BEGIN();
|
||||||
|
RUN_TEST(test_open_valid_path);
|
||||||
|
RUN_TEST(test_open_null_path);
|
||||||
|
RUN_TEST(test_open_preserves_path);
|
||||||
|
RUN_TEST(test_close_null);
|
||||||
|
RUN_TEST(test_read_returns_zero_stub);
|
||||||
|
RUN_TEST(test_write_returns_zero_stub);
|
||||||
|
RUN_TEST(test_flush_no_crash);
|
||||||
|
RUN_TEST(test_flush_null);
|
||||||
|
return UNITY_END();
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
add_executable(tool_telemetry telemetry.c)
|
||||||
|
target_include_directories(tool_telemetry PRIVATE "${CMAKE_SOURCE_DIR}")
|
||||||
|
target_compile_features(tool_telemetry PRIVATE c_std_23)
|
||||||
|
target_link_libraries(tool_telemetry PRIVATE celrs_crsf celrs_serial celrs_logger celrs_log_write)
|
||||||
@@ -0,0 +1,112 @@
|
|||||||
|
#include "celrs/crsf.h"
|
||||||
|
#include "celrs/serial.h"
|
||||||
|
#include "celrs/logger.h"
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
/* TX power index to dBm mapping (ELRS standard) */
|
||||||
|
static int const s_tx_power_dbm[] = {
|
||||||
|
0, 20, 26, 30, 32, 34, 36, 38,
|
||||||
|
0, 0, 0, 0, 0, 0, 0, 0
|
||||||
|
};
|
||||||
|
|
||||||
|
/* Parse link telemetry payload (5 bytes) from CRSF frame type 0x02 */
|
||||||
|
static int telemetry_parse_link(int16_t* rssi, uint8_t* link_quality,
|
||||||
|
int8_t* snr, int* tx_power_dbm,
|
||||||
|
uint8_t* rssi_rc,
|
||||||
|
uint8_t const* payload, size_t len) {
|
||||||
|
if (rssi == NULL || payload == NULL) return -1;
|
||||||
|
if (len < 5) return -1;
|
||||||
|
|
||||||
|
*rssi = (int16_t)payload[0]; /* 0-100% */
|
||||||
|
*link_quality = payload[1]; /* 0-100% */
|
||||||
|
*snr = (int8_t)payload[2]; /* signed dB */
|
||||||
|
uint8_t power_idx = payload[3];
|
||||||
|
*tx_power_dbm = (power_idx < sizeof(s_tx_power_dbm) / sizeof(s_tx_power_dbm[0]))
|
||||||
|
? s_tx_power_dbm[power_idx]
|
||||||
|
: 0;
|
||||||
|
*rssi_rc = payload[4]; /* 0-100% */
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void print_usage(char const* prog) {
|
||||||
|
printf("Usage: %s <serial_port> [interval_ms]\n", prog);
|
||||||
|
printf(" serial_port : COM3 (Windows) or /dev/ttyUSB0 (Linux)\n");
|
||||||
|
printf(" interval_ms : poll interval in ms (default 200)\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(int argc, char const* argv[]) {
|
||||||
|
if (argc < 2) {
|
||||||
|
print_usage(argv[0]);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
char const* port_path = argv[1];
|
||||||
|
int interval_ms = 200;
|
||||||
|
if (argc >= 3) {
|
||||||
|
interval_ms = atoi(argv[2]);
|
||||||
|
if (interval_ms <= 0) interval_ms = 200;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Open serial port */
|
||||||
|
cel_serial_port* port = cel_serial_open(port_path, 400000);
|
||||||
|
if (port == NULL) {
|
||||||
|
cel_log_err("Failed to open serial port");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
char msg[256];
|
||||||
|
snprintf(msg, sizeof(msg), "Connected to %s (400000 baud)", port_path);
|
||||||
|
cel_log_info(msg);
|
||||||
|
|
||||||
|
/* Send heartbeat to establish CRSF link */
|
||||||
|
uint8_t hb_payload[2] = {CEL_CRSF_ADDRESS_TBS_GROUND_STATION, 0x01};
|
||||||
|
uint8_t hb_frame[256];
|
||||||
|
size_t hb_len = cel_crsf_frame_build(hb_frame, CEL_CRSF_ADDRESS_FC_BROADCAST,
|
||||||
|
CEL_CRSF_ADDRESS_TBS_GROUND_STATION,
|
||||||
|
CEL_CRSF_FRAMETYPE_HEARTBEAT, hb_payload, 2);
|
||||||
|
cel_serial_write(port, hb_frame, hb_len);
|
||||||
|
|
||||||
|
printf("RX\tLINK\tSNR\tTXP\tRSSI_RC\n");
|
||||||
|
|
||||||
|
/* Read loop */
|
||||||
|
uint8_t buf[256];
|
||||||
|
int frames = 0, errors = 0;
|
||||||
|
|
||||||
|
for (int i = 0; i < 20; i++) { /* read up to 20 telemetry frames */
|
||||||
|
size_t n = cel_serial_read(port, buf, sizeof(buf), interval_ms);
|
||||||
|
if (n == 0) continue;
|
||||||
|
|
||||||
|
cel_crsf_frame frame;
|
||||||
|
if (cel_crsf_frame_parse(&frame, buf, n) != 0) {
|
||||||
|
errors++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (frame.type != CEL_CRSF_FRAMETYPE_PACKET_LINK_TELEMETRY) {
|
||||||
|
continue; /* skip non-telemetry frames */
|
||||||
|
}
|
||||||
|
|
||||||
|
int16_t rssi;
|
||||||
|
uint8_t link_quality;
|
||||||
|
int8_t snr;
|
||||||
|
int tx_power;
|
||||||
|
uint8_t rssi_rc;
|
||||||
|
|
||||||
|
if (telemetry_parse_link(&rssi, &link_quality, &snr,
|
||||||
|
&tx_power, &rssi_rc,
|
||||||
|
frame.payload, frame.size) == 0) {
|
||||||
|
frames++;
|
||||||
|
printf("%d\t%d\t%d\t%d\t%d\n",
|
||||||
|
rssi, link_quality, snr, tx_power, rssi_rc);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
snprintf(msg, sizeof(msg), "Frames: %d, Errors: %d", frames, errors);
|
||||||
|
cel_log_info(msg);
|
||||||
|
|
||||||
|
cel_serial_close(port);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user