Compare commits

..

30 Commits

Author SHA1 Message Date
portersky 23050d983e docs: add tag release workflow to AGENTS.md 2026-06-15 00:41:53 +02:00
portersky 974b33e827 chore: rename targets and add ENABLE_TESTING option
Rename library targets from celrs_* to cel* (celcrsf, celserial,
cellogger, cellog) and add cel:: namespace aliases. Add cel::cel
umbrella target that links all core libraries.

Add ENABLE_TESTING option (default ON) to gate Unity/CMock fetch
and test targets for downstream consumers.
2026-06-15 00:39:25 +02:00
portersky 412530df83 fix: drop slow startup ping, log connect time
verify_connection retried DEVICE_INFO pings up to 3x2s,
blocking startup for ~6s even when the module just needs
more time to come up. The main loop already pings every 5s
and shows DEVICE_INFO in the type breakdown, so the upfront
check added latency without useful signal.

Log how long opening the port took instead.
2026-06-15 00:24:14 +02:00
portersky 8ff2542fbc fix: avoid DTR/RTS pulse on serial port open
Previously SetCommState was called with fDtrControl and
fRtsControl left at whatever GetCommState returned (often
enabled), then EscapeCommFunction lowered DTR/RTS after the
fact. This produced a brief low-high-low pulse on connect,
which can reset USB-UART-connected devices.

Set DTR_CONTROL_DISABLE and RTS_CONTROL_DISABLE directly in
the DCB before the single SetCommState call, so the lines
never get pulsed.
2026-06-15 00:17:19 +02:00
portersky df09615d3f feat: improve telemetry dashboard diagnostics
- Show top CRSF frame types received, by raw type byte,
  with a name lookup table (mirrors the Python tool)
- Send DEVICE_PING every 5s so DEVICE_INFO keeps appearing
  in the type breakdown
- Fix NO LINK status to trigger when uplink quality is 0
- Fix SNR display (drop erroneous extra -128 offset)
- Retry the initial DEVICE_INFO ping up to 3 times
- Probe 921600 baud before 400000/420000
2026-06-15 00:17:01 +02:00
portersky f58eb0d976 fix: send RC channels with correct CRSF type
cel_crsf_build_rc_frame tagged RC channel frames with 0x01,
which is not a valid CRSF frame type. The TX module's CRSF
parser never recognized these as channel updates, so it had
no RC data to forward over RF and the receiver could never
report link quality.

Use CEL_CRSF_TYPE_RC_CHANNELS (0x16), the spec-correct RC
Channels Packed type. Drop the bogus 0x01 enum value.
2026-06-15 00:16:50 +02:00
portersky d67d9b29d2 fix: parse CRSF battery as big-endian per protocol spec
CRSF battery frame is big-endian: voltage(u16 BE 0.1V),
current(u16 BE 0.1A), capacity(u24 BE mAh), remaining(u8 %).

Previous code read little-endian with wrong byte count (7 vs 8)
and wrong scaling (/1000 vs /10), producing 9.98V for a 1S battery.
2026-06-14 23:34:12 +02:00
portersky ef5012b9d4 fix: probe 400000 baud first and relax FC stale threshold
CP210x chips can't hit 921600 exactly so try 400000/420000 first.
Raise FC_STALE_S from 2s to 5s so the dashboard doesn't flicker
STALE when FC telemetry arrives slowly.
2026-06-14 23:25:47 +02:00
portersky 787a303cf5 fix: pad status labels and replace em dashes
Add trailing spaces to shorter status labels so they don't leave
residue when overwritten by longer ones. Replace em dashes with
regular dashes per project style.
2026-06-14 23:18:23 +02:00
portersky f1e4e1b61d fix: separate logger from dashboard output
Logger writes to stderr so it doesn't corrupt the dashboard on stdout.
Dashboard tracks its own line count and uses cursor-up instead of home
so log messages appear cleanly before/after instead of interleaved.
2026-06-14 23:12:41 +02:00
portersky 1db5fdb374 feat: dashboard-style telemetry output
Replace line-by-line telemetry dump with in-place dashboard showing:
- Status indicator (LIVE/STALE/NO SIGNAL)
- LINK: RSSI1, RSSI2, LQ, SNR, power, mode with color coding
- BATT: voltage, current, capacity, percentage
- IMU: pitch, roll, yaw in degrees
- MODE: flight mode name
- Frame counts footer

Add ATTITUDE (0x1E) and FLIGHT_MODE (0x21) telemetry parsing.
Dashboard redraws every 100ms with ANSI cursor control.
2026-06-14 23:05:24 +02:00
portersky d2331229eb fix: improve telemetry tool connection reliability
Add baud rate probing (921600/400000/420000), auto-detect ELRS
ports, verify module responds to CRSF ping before telemetry loop,
and increase RC send rate to 50 Hz to match Python reference.
2026-06-14 22:52:40 +02:00
portersky 97c83aa460 feat: implement cel_crsf_param_set_power
Enumerate parameters until TX Power entry is found, match
requested mW against TEXT_SELECT options, and write the
selected option index.

Added:
- str_contains_ci() for case-insensitive substring matching
- is_power_param() to detect power-related parameters
- match_power_option() to find mW in option strings
- 4 new tests: null port, frame roundtrip, success, not found
2026-06-14 22:40:26 +02:00
portersky 7b3905084e feat: implement cel_crsf_param_ping and cel_crsf_param_read
Ping sends DEVICE_PING frame and waits for DEVICE_INFO response.
Read sends PARAM_READ frame and waits for matching PARAM_ENTRY.
Both use cel_crsf_stream_feed() with a clock-based timeout loop.
2026-06-14 22:08:51 +02:00
portersky 8c4045e2a4 feat: implement cel_crsf_param_write
Fire-and-forget parameter write. Builds CRSF PARAM_WRITE frame
and sends it over the serial port.
2026-06-14 21:55:15 +02:00
portersky 5d18258330 feat: implement cel_crsf_param_parse
Parse PARAM_ENTRY payload into cel_crsf_param struct. Handles
TEXT_SELECT with options string and UINT8/INT8 with min/max/default/
value fields. Respects hidden flag (bit 7 of type byte). Truncates
name and options to buffer limits.
2026-06-14 21:50:12 +02:00
portersky eaaaf710a2 feat: implement port find/probe and add platform description
Implement cel_serial_find_elrs_port() which enumerates serial ports
and matches descriptions against ELRS-related keywords (CP210, CH340,
FTDI, etc.). Implement cel_serial_open_probe() to try multiple baud
rates in order.

Add cel_serial_platform_get_description() for Windows (SetupAPI)
and POSIX (sysfs fallback). Wire setupapi into the Windows build.
Update serial tests with CMock expectations for the new functions.
2026-06-14 21:39:18 +02:00
portersky 4f0c62d41a feat: implement telemetry read loop
Telemetry tool now:
- Reads raw bytes from serial port
- Parses frames incrementally via cel_crsf_stream
- Decodes link stats, battery, heartbeat, airspeed
- Sends RC frames periodically to keep link alive
- Handles Ctrl+C gracefully via signal handler
2026-06-14 21:04:17 +02:00
portersky 34dd25fecb feat: implement telemetry parser
cel_crsf_telemetry_parse() decodes link stats, battery, heartbeat,
and airspeed frames. Updated cel_telem_battery and cel_telem_airspeed
structs to use uint16_t values matching CRSF protocol format.
2026-06-14 21:02:23 +02:00
portersky b97a7c5b3a feat: implement frame builders
cel_crsf_build_rc_frame() packs 16 channels (11-bit) into 22 bytes.
cel_crsf_build_ping_frame() builds device ping (0x28).
cel_crsf_build_param_read_frame() and cel_crsf_build_param_write_frame()
build parameter protocol frames (0x2C/0x2D).
2026-06-14 20:58:16 +02:00
portersky 8b181d0fcd feat: implement channel helpers
cel_crsf_channel_us_to_val() and cel_crsf_channel_val_to_us()
convert between microseconds (988-2012) and 11-bit values (172-1811)
with rounding. cel_crsf_channel_default() fills safe/disarmed values.
2026-06-14 20:54:56 +02:00
portersky a846b063f9 feat: implement frame parse and streaming reader
cel_crsf_frame_parse() parses ELRS USB format frames:
  [addr][length][type][payload...][crc]

cel_crsf_stream_* provides incremental parsing from a byte
stream: skips invalid sync bytes, discards bad CRC frames,
buffers partial frames across feed calls.
2026-06-14 20:51:57 +02:00
portersky dde27ab566 refactor: remove unused legacy frame format code
The old [0xC8][dest][src][type][size][payload][crc] format was never
used with real hardware. Remove cel_crsf_frame_legacy, *_legacy()
functions, and update tests/tools accordingly.
2026-06-14 20:49:45 +02:00
portersky df3d399610 feat: adopt ELRS USB CRSF frame format, add skeleton modules
Switch CRC from CCITT (0x07) to DVB-S2 (0xD5) to match ELRS.
Adopt ELRS USB frame format: [addr][length][type][payload][crc].
Update frame type constants to match current ELRS protocol values.

New skeleton modules (stub implementations with TODO comments):
  - crsf_telemetry.h/.c: telemetry decoders (GPS, battery, link..)
  - crsf_stream.h/.c: incremental streaming frame reader
  - crsf_param.h/.c: parameter protocol (read/write/set_power)

Retained old frame format as *_legacy functions for backward
compatibility with existing telemetry tool and tests.

Add cel_serial_find_elrs_port() and cel_serial_open_probe()
stubs to serial module for port auto-detection and baud probing.
2026-06-14 20:47:56 +02:00
portersky c42ec407da docs: remove main.c references, add tools/ directory
main.c no longer exists. Remove run commands from README and
AGENTS.md build sections. Add tools/telemetry.c to source layout
trees in both files.
2026-06-14 20:37:46 +02:00
portersky 794ee9989a feat: implement Windows serial platform backend
Add cel_serial_platform_open/close/read/write/flush for
Windows using CreateFileA, DCB for baud/8N1, and
SetCommTimeouts for non-blocking reads.

serial.c now delegates all operations to the platform
backend via a cel_serial_platform_handle, and
test_serial.c mocks that backend with CMock.
2026-06-14 20:37:38 +02:00
portersky a1ea02771c refactor: make cel_serial_read non-blocking
Drop timeout_ms; read now returns immediately with whatever
data is available (0 if none), so callers don't block the rest
of their loop waiting on serial I/O.

telemetry's poll loop now sleeps interval_ms itself between
empty reads via a small sleep_ms helper.
2026-06-14 20:29:47 +02:00
portersky 5324f6e36e refactor: use uint8_t for serial buffers
Match crsf.h's use of fixed-width types for byte buffers
instead of unsigned char.
2026-06-14 20:23:35 +02:00
portersky 2761bcb16c refactor: mock serial platform backend in test_serial
test_serial mocked log_write.h, which serial.c never calls.
Split celrs_serial into celrs_serial (platform-agnostic logic)
and celrs_serial_platform (real Win/POSIX backend), matching
the celrs_logger/celrs_log_write split.

test_serial now mocks celrs/platform/serial_internal.h and
links only celrs_serial, so the list-ports tests verify the
max_ports clamping and pass-through logic without hitting the
real registry or /dev.
2026-06-14 20:18:17 +02:00
portersky a0868cd3b7 feat: add serial port listing and CLI flags
Implement cel_serial_list_ports/cel_serial_free_ports with
platform backends: Windows reads HKLM\HARDWARE\DEVICEMAP\
SERIALCOMM (fast, single registry read), POSIX scans /dev for
ttyUSB*/ttyACM*.

telemetry tool gains --list, --port, and --baudrate flags;
baud rate was previously hardcoded to 400000. Rename the
tool_telemetry CMake target to telemetry.

Fix test_free_ports_zero_count, which passed a stack array to
cel_serial_free_ports (which calls free() on it), corrupting
the heap.
2026-06-14 20:13:57 +02:00
27 changed files with 3131 additions and 420 deletions
+58 -26
View File
@@ -44,16 +44,6 @@ Run tests and generate coverage HTML:
ninja -C build-cov coverage ninja -C build-cov coverage
``` ```
Run (Linux/macOS):
```sh
./build/main /dev/ttyUSB0
```
Run (Windows):
```sh
./build/main.exe COM3
```
### Dependencies ### Dependencies
Dependencies are managed via custom `Find*.cmake` scripts in `deps/`. Dependencies are managed via custom `Find*.cmake` scripts in `deps/`.
@@ -115,6 +105,45 @@ Implement CRC8-CCITT (poly 0x07) for CRSF frame validation.
Added unit tests for empty, single-byte, and known-value cases. Added unit tests for empty, single-byte, and known-value cases.
``` ```
## Tag Releases
Use annotated tags for releases. Write the release notes to a temporary
file, then use it as the tag message. Do **not** commit the release
notes file.
Write release notes:
```sh
cat > RELEASES.md << 'EOF'
# Releases
## 0.1.0 (2026-06-15)
Initial release. Windows-only support.
### Library (`celrs`)
- **CRSF protocol** ...
EOF
```
Create the annotated tag:
```sh
git tag -a v0.1.0 -F RELEASES.md
```
Verify:
```sh
git show v0.1.0
```
Remove the temporary file:
```sh
rm RELEASES.md
```
Release notes follow the same Markdown rules as `AGENTS.md` (80-column
wrap, no em dashes, etc.). Version format is `v<major>.<minor>.<patch>`.
## Documentation (Markdown) ## Documentation (Markdown)
- Wrap normal text and lists at **max 80 columns** (for readability in - Wrap normal text and lists at **max 80 columns** (for readability in
@@ -141,18 +170,22 @@ Added unit tests for empty, single-byte, and known-value cases.
```text ```text
celrs/ celrs/
crsf.h / crsf.c CRSF protocol: CRC8, frame parse/build crsf.h / crsf.c CRSF protocol: CRC8/DVB-S2, frame build/parse
serial.h / serial.c Serial port abstraction (Win/POSIX stubs) crsf_telemetry.h/.c Telemetry frame decoders (GPS, battery, link..)
logger.h / logger.c Level-filtering logger crsf_stream.h/.c Incremental streaming frame reader
log_write.h/.c stdout log sink crsf_param.h/.c Parameter protocol (read/write/set_power)
main.c Entry point — demo heartbeat + read serial.h / serial.c Serial port abstraction (Win/POSIX)
logger.h / logger.c Level-filtering logger
log_write.h/.c stdout log sink
tools/
telemetry.c Telemetry read tool
tests/ tests/
test_crsf.c CRSF CRC, parse, build tests test_crsf.c CRSF CRC, parse, build tests
test_serial.c Serial open/close/stub tests test_serial.c Serial open/close/stub tests
test_logger.c Logger level-filtering tests test_logger.c Logger level-filtering tests
deps/ deps/
FindUnity.cmake Fetches Unity v2.6.1 via ZIP FindUnity.cmake Fetches Unity v2.6.1 via ZIP
FindCMock.cmake Fetches CMock v2.6.0 via ZIP FindCMock.cmake Fetches CMock v2.6.0 via ZIP
``` ```
## TDD Workflow ## TDD Workflow
@@ -248,9 +281,8 @@ The project supports Windows, Linux, macOS, Emscripten, and Android via
CRSF is the Crossfire Serial Protocol used by ELRS. Key points: CRSF is the Crossfire Serial Protocol used by ELRS. Key points:
- Frame header is always `0xC8` - Frame format: [addr][length][type][payload...][crc]
- CRC8-CCITT with polynomial `0x07`, init `0x00` - CRC8/DVB-S2 with polynomial `0xD5`, init `0x00`
- CRC is computed over: destination + source + type + size + payload - CRC is computed over: type + payload
- Standard baud rate for ELRS CRSF is 400000 bps - Address byte varies: `0xC8` (host), `0xEE` (module), `0xEF` (lua)
- TX modules expose CRSF over USB serial (appears as COM port on Windows, - Standard baud rate for ELRS CRSF is 400000 bps (probe 921600 first)
/dev/ttyUSB* on Linux)
+7 -2
View File
@@ -5,6 +5,9 @@ project(celrs VERSION 0.1.0)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON) set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/deps") list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/deps")
# Options
option(ENABLE_TESTING "Build unit tests" ON)
# Platform flags # Platform flags
include(Platform) include(Platform)
include(Flags) include(Flags)
@@ -18,8 +21,10 @@ add_subdirectory(celrs)
add_subdirectory(tools) add_subdirectory(tools)
# Testing # Testing
enable_testing() if (ENABLE_TESTING)
add_subdirectory(tests) enable_testing()
add_subdirectory(tests)
endif()
# IDE configuration # IDE configuration
include(IDE) include(IDE)
+42 -51
View File
@@ -1,13 +1,10 @@
# celrs # celrs
A C23 project for interfacing with ELRS TX modules (e.g., BAYCK Nano Dual A C23 library and CLI tools for interfacing with ELRS TX modules (e.g.,
Band) via serial USB using the CRSF (Crossfire Serial) protocol. 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) All dependencies are fetched automatically via CMake `FetchContent`. No
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. manual installation required beyond the tools listed below.
## Requirements ## Requirements
@@ -18,7 +15,7 @@ manual installation required beyond the tools listed below.
| Ninja | Build backend | | Ninja | Build backend |
| C23 compiler | GCC 14+, Clang 18+ | | C23 compiler | GCC 14+, Clang 18+ |
| Ruby ≥ 3.0 | CMock mock generation | | Ruby ≥ 3.0 | CMock mock generation |
| gcovr ≥ 6.0 | Coverage reports optional (`uv tool install gcovr`) | | gcovr ≥ 6.0 | Coverage reports - optional (`uv tool install gcovr`) |
## Build ## Build
@@ -70,36 +67,26 @@ mock files are excluded. Requires GCC or Clang with gcov support, and
> build that includes compiler-rt. A custom Clang without compiler-rt > build that includes compiler-rt. A custom Clang without compiler-rt
> will fail at link time. > 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 ## Architecture
``` ```
celrs/ celrs/
crsf.h / crsf.c CRSF protocol: CRC8, frame parse/build crsf.h / crsf.c CRSF protocol: CRC8/DVB-S2, frame build/parse
serial.h / serial.c Serial port abstraction (Win/POSIX) crsf_telemetry.h/.c Telemetry frame decoders (GPS, battery, link..)
logger.h / logger.c Level-filtering logger crsf_stream.h/.c Incremental streaming frame reader
log_write.h/.c stdout log sink crsf_param.h/.c Parameter protocol (read/write/set_power)
main.c Entry point — demo heartbeat + read serial.h / serial.c Serial port abstraction (Win/POSIX)
logger.h / logger.c Level-filtering logger
log_write.h/.c stdout log sink
tools/
telemetry.c Telemetry read tool
tests/ tests/
test_crsf.c CRSF CRC, parse, build tests test_crsf.c CRSF CRC, parse, build tests
test_serial.c Serial open/close/stub tests test_serial.c Serial open/close/stub tests
test_logger.c Logger level-filtering tests test_logger.c Logger level-filtering tests
deps/ deps/
FindUnity.cmake Fetches Unity v2.6.1 via ZIP FindUnity.cmake Fetches Unity v2.6.1 via ZIP
FindCMock.cmake Fetches CMock v2.6.0 via ZIP FindCMock.cmake Fetches CMock v2.6.0 via ZIP
``` ```
## CRSF Protocol ## CRSF Protocol
@@ -107,22 +94,20 @@ deps/
CRSF (Crossfire Serial Protocol) is the serial protocol used by ELRS for CRSF (Crossfire Serial Protocol) is the serial protocol used by ELRS for
communication between ground station and TX/RX modules. communication between ground station and TX/RX modules.
### Frame format ### Frame format (ELRS USB CRSF)
``` ```
+------+------+------+------+-------+-------+ +--------+----------+--------+----------+-----+
| 0xC8 | dest | src | type | size | ... | CRC | | addr | length | type | payload | CRC |
+------+------+------+------+-------+-------+ +--------+----------+--------+----------+-----+
1 byte 1B 1B 1B 1B N B 1 byte 1 byte 1 byte 1 byte N bytes 1B
``` ```
- **Header:** Always `0xC8` - **Address:** Frame sync byte (`0xC8` for host, `0xEE` for module, etc.)
- **Destination:** Target device address - **Length:** Total bytes after this field (type + payload + CRC)
- **Source:** Sender device address - **Type:** Frame type (RC channels, telemetry, parameter, etc.)
- **Type:** Frame type (heartbeat, RC channels, telemetry, etc.)
- **Size:** Payload length in bytes
- **Payload:** Frame-specific data - **Payload:** Frame-specific data
- **CRC:** CRC8-CCITT over dest+src+type+size+payload - **CRC:** CRC8/DVB-S2 (poly `0xD5`) over type + payload
### Common device addresses ### Common device addresses
@@ -131,17 +116,23 @@ communication between ground station and TX/RX modules.
| 0x00 | FC Broadcast | | 0x00 | FC Broadcast |
| 0x10 | Flight Controller | | 0x10 | Flight Controller |
| 0x80 | TBS Ground Station | | 0x80 | TBS Ground Station |
| 0xEA | Custom Module | | 0xEA | Custom Module (Radio) |
| 0xDD | RC Device | | 0xEE | ELRS TX Module |
| 0xEF | ELRS Lua (host script) |
### Common frame types ### Common frame types
| Type | Name | | Type | Name |
| ------ | --------------------------- | | ------ | --------------------------- |
| 0x01 | RC Channels Packed | | 0x01 | RC Channels Packed |
| 0x02 | Packet Link Telemetry | | 0x02 | GPS |
| 0x03 | Heartbeat | | 0x08 | Battery |
| 0x08 | Device Info | | 0x0B | Heartbeat |
| 0x09 | Parameter List | | 0x14 | Link Stats |
| 0x17 | MSP Read | | 0x16 | RC Channels |
| 0x18 | MSP Write | | 0x28 | Device Ping |
| 0x29 | Device Info |
| 0x2B | Parameter Entry |
| 0x2C | Parameter Read |
| 0x2D | Parameter Write |
| 0x2E | ELRS Status |
+36 -12
View File
@@ -1,17 +1,41 @@
add_library(celrs_crsf STATIC crsf.c) add_library(celcrsf STATIC crsf.c crsf_telemetry.c crsf_stream.c
target_include_directories(celrs_crsf PUBLIC "${CMAKE_SOURCE_DIR}") crsf_param.c)
target_compile_features(celrs_crsf PRIVATE c_std_23) target_include_directories(celcrsf PUBLIC "${CMAKE_SOURCE_DIR}")
target_compile_features(celcrsf PRIVATE c_std_23)
target_link_libraries(celcrsf PUBLIC celserial)
add_library(cel::crsf ALIAS celcrsf)
add_library(celrs_serial STATIC serial.c) # Platform-agnostic serial logic — calls cel_serial_platform_*();
target_include_directories(celrs_serial PUBLIC "${CMAKE_SOURCE_DIR}") # symbol resolved by celserial_platform (or a mock in tests)
target_compile_features(celrs_serial PRIVATE c_std_23) add_library(celserial STATIC serial.c)
target_include_directories(celserial PUBLIC "${CMAKE_SOURCE_DIR}")
target_compile_features(celserial PRIVATE c_std_23)
add_library(cel::serial ALIAS celserial)
# Real platform backend — linked into production binaries only
add_library(celserial_platform STATIC)
target_include_directories(celserial_platform PUBLIC "${CMAKE_SOURCE_DIR}")
target_compile_features(celserial_platform PRIVATE c_std_23)
if (IS_WINDOWS)
target_sources(celserial_platform PRIVATE platform/serial_win.c)
target_link_libraries(celserial_platform PRIVATE advapi32 setupapi)
elseif(IS_LINUX OR IS_MACOS)
target_sources(celserial_platform PRIVATE platform/serial_posix.c)
endif()
# Level-filtering logger — calls log_write(); symbol resolved by the final binary # Level-filtering logger — calls log_write(); symbol resolved by the final binary
add_library(celrs_logger STATIC logger.c) add_library(cellogger STATIC logger.c)
target_include_directories(celrs_logger PUBLIC "${CMAKE_SOURCE_DIR}") target_include_directories(cellogger PUBLIC "${CMAKE_SOURCE_DIR}")
target_compile_features(celrs_logger PRIVATE c_std_23) target_compile_features(cellogger PRIVATE c_std_23)
add_library(cel::logger ALIAS cellogger)
# Real log_write implementation — linked into production binaries only # Real log_write implementation — linked into production binaries only
add_library(celrs_log_write STATIC log_write.c) add_library(cellog STATIC log_write.c)
target_include_directories(celrs_log_write PUBLIC "${CMAKE_SOURCE_DIR}") target_include_directories(cellog PUBLIC "${CMAKE_SOURCE_DIR}")
target_compile_features(celrs_log_write PRIVATE c_std_23) target_compile_features(cellog PRIVATE c_std_23)
add_library(cel::log ALIAS cellog)
# Umbrella target — links all celrs libraries
add_library(cel INTERFACE)
target_link_libraries(cel INTERFACE celcrsf celserial cellogger cellog)
add_library(cel::cel ALIAS cel)
+183 -57
View File
@@ -1,74 +1,200 @@
#include "celrs/crsf.h" #include "celrs/crsf.h"
#include <string.h> #include <string.h>
/* CRC8-CCITT with polynomial 0x07, init 0x00 (used by CRSF/ELRS) */ /* --------------------------------------------------------------------------- */
/* CRC8/DVB-S2 lookup table (poly 0xD5) */
/* --------------------------------------------------------------------------- */
static uint8_t s_crc8_table[256];
static int s_crc8_init = 0;
static void crc8_build_table(void) {
for (int i = 0; i < 256; i++) {
uint8_t crc = (uint8_t)i;
for (int j = 0; j < 8; j++) {
crc = (crc & 0x80)
? ((crc << 1) ^ 0xD5)
: (crc << 1);
}
s_crc8_table[i] = crc;
}
s_crc8_init = 1;
}
uint8_t cel_crsf_crc(uint8_t const* data, size_t len) { uint8_t cel_crsf_crc(uint8_t const* data, size_t len) {
if (!s_crc8_init) crc8_build_table();
uint8_t crc = 0x00; uint8_t crc = 0x00;
for (size_t i = 0; i < len; i++) { for (size_t i = 0; i < len; i++) {
crc ^= data[i]; crc = s_crc8_table[crc ^ data[i]];
for (int j = 0; j < 8; j++) {
crc = (crc & 0x80) ? (crc << 1) ^ 0x07 : (crc << 1);
}
} }
return crc; return crc;
} }
int cel_crsf_frame_validate(cel_crsf_frame const* frame) { /* --------------------------------------------------------------------------- */
/* Rebuild the data that was CRC'd: dest + src + type + size + payload */ /* Frame builders */
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); size_t cel_crsf_build_rc_frame(uint8_t* dst, int16_t const channels[16]) {
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; if (dst == NULL) return 0;
dst[0] = CEL_CRSF_FRAME_HEADER; /* Pack 16 channels (11-bit each) into 22 bytes */
dst[1] = destination; int16_t ch[16];
dst[2] = source; if (channels != NULL) {
dst[3] = type; memcpy(ch, channels, sizeof(ch));
dst[4] = size; } else {
memset(ch, 0, sizeof(ch));
if (payload != NULL && size > 0) { }
memcpy(dst + 5, payload, size); for (int i = 0; i < 16; i++) {
ch[i] = cel_crsf_channel_clamp(ch[i]);
} }
/* CRC over dest + src + type + size + payload */ /* Pack into 22 bytes */
uint8_t crc = cel_crsf_crc(dst + 1, 3 + 1 + size); uint8_t packed[22];
dst[5 + size] = crc; for (int i = 0; i < 16; i++) {
int16_t val = ch[i] - CEL_CRSF_CH_MIN; /* 0..1023 */
int idx = i / 2;
if (i % 2 == 0) {
packed[idx * 2] = (uint8_t)(val & 0x7FF);
packed[idx * 2 + 1] = (uint8_t)((val >> 8) | ((val >> 0) & 0x18) << 2);
}
}
return 1 + 3 + 1 + size + 1; /* header + 3 fields + size byte + payload + crc */ /* Build frame: [addr][length][type][payload...][crc] */
uint8_t length = 1 + 22 + 1; /* type + payload + crc */
dst[0] = 0xC8; /* RC frame address */
dst[1] = length;
dst[2] = CEL_CRSF_TYPE_RC_CHANNELS;
memcpy(dst + 3, packed, 22);
uint8_t crc = cel_crsf_crc(dst + 2, 1 + 22);
dst[2 + length - 1] = crc;
return 2 + length;
} }
size_t cel_crsf_build_ping_frame(uint8_t* dst) {
if (dst == NULL) return 0;
uint8_t length = 1 + 2 + 1; /* type + payload(2) + crc */
dst[0] = CEL_CRSF_ADDRESS_MODULE;
dst[1] = length;
dst[2] = CEL_CRSF_TYPE_DEVICE_PING;
dst[3] = CEL_CRSF_ADDRESS_FC_BROADCAST; /* dest */
dst[4] = CEL_CRSF_ADDRESS_CUSTOM_MODULE; /* src */
uint8_t crc = cel_crsf_crc(dst + 2, 1 + 2);
dst[2 + length - 1] = crc;
return 2 + length;
}
size_t cel_crsf_build_param_read_frame(uint8_t* dst, uint8_t index,
uint8_t chunk) {
if (dst == NULL) return 0;
uint8_t length = 1 + 4 + 1; /* type + payload(4) + crc */
dst[0] = CEL_CRSF_ADDRESS_MODULE;
dst[1] = length;
dst[2] = CEL_CRSF_TYPE_PARAM_READ;
dst[3] = CEL_CRSF_ADDRESS_MODULE; /* dest */
dst[4] = CEL_CRSF_ADDRESS_LUA; /* src */
dst[5] = index;
dst[6] = chunk;
uint8_t crc = cel_crsf_crc(dst + 2, 1 + 4);
dst[2 + length - 1] = crc;
return 2 + length;
}
size_t cel_crsf_build_param_write_frame(uint8_t* dst, uint8_t index,
uint8_t value) {
if (dst == NULL) return 0;
uint8_t length = 1 + 4 + 1; /* type + payload(4) + crc */
dst[0] = CEL_CRSF_ADDRESS_MODULE;
dst[1] = length;
dst[2] = CEL_CRSF_TYPE_PARAM_WRITE;
dst[3] = CEL_CRSF_ADDRESS_MODULE; /* dest */
dst[4] = CEL_CRSF_ADDRESS_LUA; /* src */
dst[5] = index;
dst[6] = value;
uint8_t crc = cel_crsf_crc(dst + 2, 1 + 4);
dst[2 + length - 1] = crc;
return 2 + length;
}
/* --------------------------------------------------------------------------- */
/* Frame parser */
/* --------------------------------------------------------------------------- */
int cel_crsf_frame_parse(cel_crsf_frame* frame, uint8_t const* buf,
size_t len) {
if (frame == NULL || buf == NULL) return -1;
/* Minimum: addr(1) + length(1) + type(1) + crc(1) = 4 bytes */
if (len < 4) return -1;
uint8_t addr = buf[0];
uint8_t length = buf[1];
size_t total = 2 + length;
if (len < total) return -1;
uint8_t type = buf[2];
size_t payload_len = length > 1 ? length - 2 : 0;
uint8_t crc_recv = buf[total - 1];
/* CRC over type + payload */
uint8_t crc_calc = cel_crsf_crc(buf + 2, 1 + payload_len);
if (crc_calc != crc_recv) return -1;
frame->addr = addr;
frame->length = length;
frame->type = type;
frame->payload_len = (uint8_t)payload_len;
if (payload_len > 0) {
memcpy(frame->payload, buf + 3, payload_len);
}
frame->crc = crc_recv;
return 0;
}
/* --------------------------------------------------------------------------- */
/* Channel helpers */
/* --------------------------------------------------------------------------- */
int16_t cel_crsf_channel_clamp(int16_t value) {
if (value < CEL_CRSF_CH_MIN) return CEL_CRSF_CH_MIN;
if (value > CEL_CRSF_CH_MAX) return CEL_CRSF_CH_MAX;
return value;
}
int16_t cel_crsf_channel_us_to_val(uint16_t us) {
/* Clamp to valid range first to avoid underflow */
if (us < 988) us = 988;
if (us > 2012) us = 2012;
/* Round to nearest: add half the divisor before dividing */
int16_t value = (int16_t)(
((us - 988) * (CEL_CRSF_CH_MAX - CEL_CRSF_CH_MIN)
+ (2012 - 988) / 2) / (2012 - 988)
+ CEL_CRSF_CH_MIN);
return cel_crsf_channel_clamp(value);
}
uint16_t cel_crsf_channel_val_to_us(int16_t value) {
value = cel_crsf_channel_clamp(value);
return (uint16_t)(
((value - CEL_CRSF_CH_MIN) * (2012 - 988)
+ (CEL_CRSF_CH_MAX - CEL_CRSF_CH_MIN) / 2)
/ (CEL_CRSF_CH_MAX - CEL_CRSF_CH_MIN) + 988);
}
void cel_crsf_channel_default(int16_t channels[16]) {
memset(channels, 0, sizeof(int16_t) * 16);
channels[0] = CEL_CRSF_CH_MID; /* roll */
channels[1] = CEL_CRSF_CH_MID; /* pitch */
channels[2] = CEL_CRSF_CH_MIN; /* throttle */
channels[3] = CEL_CRSF_CH_MID; /* yaw */
for (int i = 4; i < 16; i++) {
channels[i] = CEL_CRSF_CH_MIN;
}
}
/* --------------------------------------------------------------------------- */
/* Telemetry parsing */
/* --------------------------------------------------------------------------- */
/* See crsf_telemetry.c */
+82 -37
View File
@@ -13,53 +13,98 @@
#define CEL_CRSF_ADDRESS_RC_DEVICE 0xDD #define CEL_CRSF_ADDRESS_RC_DEVICE 0xDD
#define CEL_CRSF_ADDRESS_GPS 0xEC #define CEL_CRSF_ADDRESS_GPS 0xEC
#define CEL_CRSF_ADDRESS_FLIGHT_CONTROLLER 0xED #define CEL_CRSF_ADDRESS_FLIGHT_CONTROLLER 0xED
#define CEL_CRSF_ADDRESS_MODULE 0xEE
#define CEL_CRSF_ADDRESS_LUA 0xEF
/* CRSF frame types */ /* CRSF frame types (ELRS) */
typedef enum { typedef enum {
CEL_CRSF_FRAMETYPE_PACKET_LINK_TELEMETRY = 0x02, CEL_CRSF_TYPE_GPS = 0x02,
CEL_CRSF_FRAMETYPE_RC_CHANNELS_PACKED = 0x01, CEL_CRSF_TYPE_VARIO = 0x07,
CEL_CRSF_FRAMETYPE_GPS = 0x02, CEL_CRSF_TYPE_BATTERY = 0x08,
CEL_CRSF_FRAMETYPE_HEARTBEAT = 0x03, CEL_CRSF_TYPE_BARO_ALT = 0x09,
CEL_CRSF_FRAMETYPE_VERSION = 0x04, CEL_CRSF_TYPE_AIRSPEED = 0x0A,
CEL_CRSF_FRAMETYPE_PARAMETER_SETTINGS_ENTRY = 0x05, CEL_CRSF_TYPE_HEARTBEAT = 0x0B,
CEL_CRSF_FRAMETYPE_PARAMETER_READ = 0x06, CEL_CRSF_TYPE_RPM = 0x0C,
CEL_CRSF_FRAMETYPE_PARAMETER_WRITE = 0x07, CEL_CRSF_TYPE_TEMP = 0x0D,
CEL_CRSF_FRAMETYPE_DEVICE_INFO = 0x08, CEL_CRSF_TYPE_VOLTAGES = 0x0E,
CEL_CRSF_FRAMETYPE_PARAMETER_LIST = 0x09, CEL_CRSF_TYPE_ESC_SENSOR = 0x10,
CEL_CRSF_FRAMETYPE_RC_CHANNELS_RAW = 0x16, CEL_CRSF_TYPE_LINK_STATS = 0x14,
CEL_CRSF_FRAMETYPE_MSP_READ = 0x17, CEL_CRSF_TYPE_RC_CHANNELS = 0x16,
CEL_CRSF_FRAMETYPE_MSP_WRITE = 0x18, CEL_CRSF_TYPE_ATTITUDE = 0x1E,
CEL_CRSF_FRAMETYPE_CURR_VOLTAGE_TEMP = 0x1E, CEL_CRSF_TYPE_FLIGHT_MODE = 0x21,
CEL_CRSF_FRAMETYPE_BATTERY_SENSOR = 0x1F, CEL_CRSF_TYPE_DEVICE_PING = 0x28,
CEL_CRSF_FRAMETYPE_COMPRESSED_SENSORS = 0x28, CEL_CRSF_TYPE_DEVICE_INFO = 0x29,
CEL_CRSF_FRAMETYPE_ARM = 0x0D, CEL_CRSF_TYPE_PARAM_ENTRY = 0x2B,
CEL_CRSF_FRAMETYPE_SETTING = 0x9E, CEL_CRSF_TYPE_PARAM_READ = 0x2C,
CEL_CRSF_FRAMETYPE_SUPERBOX = 0xA0, CEL_CRSF_TYPE_PARAM_WRITE = 0x2D,
CEL_CRSF_FRAMETYPE_DEVICE_SUPERBOX = 0xA1, CEL_CRSF_TYPE_ELRS_STATUS = 0x2E,
} cel_crsf_frame_type; } cel_crsf_type;
/* Parsed CRSF frame */ /* ELRS parameter types */
typedef enum {
CEL_PARAM_UINT8 = 0,
CEL_PARAM_INT8 = 1,
CEL_PARAM_UINT16 = 2,
CEL_PARAM_INT16 = 3,
CEL_PARAM_UINT32 = 4,
CEL_PARAM_INT32 = 5,
CEL_PARAM_FLOAT = 8,
CEL_PARAM_TEXT_SELECT = 9,
CEL_PARAM_STRING = 10,
CEL_PARAM_FOLDER = 11,
CEL_PARAM_INFO = 12,
CEL_PARAM_COMMAND = 13,
} cel_param_type;
/* Channel value mapping (11-bit) */
#define CEL_CRSF_CH_MIN 172 /* 988 us */
#define CEL_CRSF_CH_MID 992 /* 1500 us */
#define CEL_CRSF_CH_MAX 1811 /* 2012 us */
/* Parsed CRSF frame (ELRS USB format: [addr][length][type][payload][crc]) */
typedef struct { typedef struct {
uint8_t destination; uint8_t addr;
uint8_t source; uint8_t length;
uint8_t type; uint8_t type;
uint8_t size;
uint8_t payload[255]; uint8_t payload[255];
uint8_t payload_len;
uint8_t crc; uint8_t crc;
} cel_crsf_frame; } cel_crsf_frame;
/* CRC8 calculation over CRSF frame data (CCITT poly 0x07) */ /* CRC8/DVB-S2 (poly 0xD5, init 0x00) */
uint8_t cel_crsf_crc(uint8_t const* data, size_t len); 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) */ /* Build an RC channels frame (16 channels, 11-bit each).
int cel_crsf_frame_validate(cel_crsf_frame const* frame); Returns total bytes written into dst (min 28 bytes needed). */
size_t cel_crsf_build_rc_frame(uint8_t* dst, int16_t const channels[16]);
/* Parse a raw buffer into a cel_crsf_frame. Returns 0 on success, -1 on error. /* Build a device ping frame. Returns bytes written (min 7 bytes needed). */
buf should start with 0xC8 header. */ size_t cel_crsf_build_ping_frame(uint8_t* dst);
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. /* Build a parameter read frame. Returns bytes written (min 9 bytes needed). */
dst must have space for at least 5 + size bytes (header, addr, src, type, size_t cel_crsf_build_param_read_frame(uint8_t* dst, uint8_t index,
size byte, payload, crc). */ uint8_t chunk);
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); /* Build a parameter write frame. Returns bytes written (min 9 bytes needed). */
size_t cel_crsf_build_param_write_frame(uint8_t* dst, uint8_t index,
uint8_t value);
/* Parse a single CRSF frame from buf. Returns 0 on success, -1 on error.
buf should start with the address byte (0xC8, 0xEE, etc.). */
int cel_crsf_frame_parse(cel_crsf_frame* frame, uint8_t const* buf,
size_t len);
/* Channel helpers */
int16_t cel_crsf_channel_clamp(int16_t value);
int16_t cel_crsf_channel_us_to_val(uint16_t us);
uint16_t cel_crsf_channel_val_to_us(int16_t value);
void cel_crsf_channel_default(int16_t channels[16]);
/* Telemetry parsing — see crsf_telemetry.h */
#include "celrs/crsf_telemetry.h"
/* Streaming reader — see crsf_stream.h */
#include "celrs/crsf_stream.h"
/* Parameter protocol — see crsf_param.h */
#include "celrs/crsf_param.h"
+258
View File
@@ -0,0 +1,258 @@
#include <ctype.h>
#include <stdio.h>
#include <string.h>
#include <time.h>
#include "celrs/crsf_param.h"
/* --------------------------------------------------------------------------- */
/* Helpers for power matching */
/* --------------------------------------------------------------------------- */
/* Case-insensitive string contains */
static int str_contains_ci(char const* haystack, char const* needle) {
size_t hlen = strlen(haystack);
size_t nlen = strlen(needle);
if (nlen > hlen) return 0;
for (size_t i = 0; i <= hlen - nlen; i++) {
int match = 1;
for (size_t j = 0; j < nlen; j++) {
if (tolower((unsigned char)haystack[i + j])
!= tolower((unsigned char)needle[j]))
{
match = 0;
break;
}
}
if (match) return 1;
}
return 0;
}
/* Check if param name contains "power" (case-insensitive) */
static int is_power_param(cel_crsf_param const* param) {
return str_contains_ci(param->name, "power");
}
/* Match requested power (mW) against a TEXT_SELECT option string.
* Returns option index (0-based) or -1 if no match. */
static int match_power_option(cel_crsf_param const* param, int mw) {
char const* opts = param->options;
char opt_buf[64];
int index = 0;
while (*opts) {
/* Extract option up to ';' or '\0' */
size_t i = 0;
while (*opts && *opts != ';' && i < sizeof(opt_buf) - 1) {
opt_buf[i++] = *opts++;
}
opt_buf[i] = '\0';
if (*opts == ';') opts++; /* skip separator */
/* Check if option starts with the requested mW value */
char req_buf[16];
sprintf(req_buf, "%d", mw);
if (str_contains_ci(opt_buf, req_buf)) {
return index;
}
index++;
}
return -1;
}
/* --------------------------------------------------------------------------- */
/* Public API */
/* --------------------------------------------------------------------------- */
int cel_crsf_param_ping(cel_serial_port* port, float timeout_sec) {
if (port == NULL) return -1;
/* Send ping frame */
uint8_t frame[16];
size_t len = cel_crsf_build_ping_frame(frame);
if (len == 0) return -1;
size_t written = cel_serial_write(port, frame, len);
if (written != len) return -1;
/* Wait for DEVICE_INFO response */
cel_crsf_stream* stream = cel_crsf_stream_create();
if (stream == NULL) return -1;
clock_t start = clock();
clock_t limit = (clock_t)(timeout_sec * CLOCKS_PER_SEC);
while ((clock() - start) < limit) {
uint8_t buf[256];
size_t n = cel_serial_read(port, buf, sizeof(buf));
if (n > 0) {
cel_crsf_frame frames[4];
int count = cel_crsf_stream_feed(stream, buf, n, frames, sizeof(frames) / sizeof(frames[0]));
if (count > 0) {
for (int i = 0; i < count; i++) {
if (frames[i].type == CEL_CRSF_TYPE_DEVICE_INFO) {
cel_crsf_stream_destroy(stream);
return 0;
}
}
}
}
}
cel_crsf_stream_destroy(stream);
return -1;
}
int cel_crsf_param_read(cel_serial_port* port, uint8_t index,
cel_crsf_param* out, float timeout_sec) {
if (port == NULL || out == NULL) return -1;
/* Send param read frame */
uint8_t frame[16];
size_t len = cel_crsf_build_param_read_frame(frame, index, 0);
if (len == 0) return -1;
size_t written = cel_serial_write(port, frame, len);
if (written != len) return -1;
/* Wait for PARAM_ENTRY response with matching index */
cel_crsf_stream* stream = cel_crsf_stream_create();
if (stream == NULL) return -1;
clock_t start = clock();
clock_t limit = (clock_t)(timeout_sec * CLOCKS_PER_SEC);
while ((clock() - start) < limit) {
uint8_t buf[256];
size_t n = cel_serial_read(port, buf, sizeof(buf));
if (n > 0) {
cel_crsf_frame frames[4];
int count = cel_crsf_stream_feed(stream, buf, n, frames, sizeof(frames) / sizeof(frames[0]));
if (count > 0) {
for (int i = 0; i < count; i++) {
if (frames[i].type == CEL_CRSF_TYPE_PARAM_ENTRY) {
if (cel_crsf_param_parse(out, frames[i].payload,
frames[i].payload_len) == 0)
{
if (out->index == index) {
cel_crsf_stream_destroy(stream);
return 0;
}
}
}
}
}
}
}
cel_crsf_stream_destroy(stream);
return -1;
}
int cel_crsf_param_write(cel_serial_port* port, uint8_t index,
uint8_t value) {
if (port == NULL) return -1;
uint8_t frame[16];
size_t len = cel_crsf_build_param_write_frame(frame, index, value);
if (len == 0) return -1;
size_t written = cel_serial_write(port, frame, len);
return (written == len) ? 0 : -1;
}
int cel_crsf_param_set_power(cel_serial_port* port, int mw,
float timeout_sec) {
if (port == NULL) return -1;
/* 1. Ping to verify connection */
if (cel_crsf_param_ping(port, timeout_sec) != 0) return -1;
/* 2. Enumerate params until power entry is found */
for (uint8_t idx = 0; idx < 32; idx++) {
cel_crsf_param param;
if (cel_crsf_param_read(port, idx, &param, timeout_sec) != 0) {
/* No response = end of parameter list */
break;
}
/* Check if this is the power parameter */
if (is_power_param(&param) && param.type == CEL_PARAM_TEXT_SELECT) {
/* Find matching option */
int opt_index = match_power_option(&param, mw);
if (opt_index >= 0) {
/* Write the selected option */
return cel_crsf_param_write(port, idx, (uint8_t)opt_index);
}
}
}
/* 3. Power param not found */
return -1;
}
int cel_crsf_param_parse(cel_crsf_param* out, uint8_t const* payload,
size_t len) {
if (out == NULL || payload == NULL) return -1;
if (len < 6) return -1;
memset(out, 0, sizeof(*out));
size_t offset = 0;
(void)payload[offset++]; /* dest */
(void)payload[offset++]; /* src */
out->index = payload[offset++];
(void)payload[offset++]; /* chunks_remaining */
(void)payload[offset++]; /* parent */
uint8_t raw_type = payload[offset++];
out->hidden = (raw_type & 0x80) ? 1 : 0;
out->type = raw_type & 0x7F;
/* Parse name (null-terminated) */
size_t name_start = offset;
while (offset < len && payload[offset] != '\0') offset++;
size_t name_len = offset - name_start;
if (name_len >= sizeof(out->name)) name_len = sizeof(out->name) - 1;
memcpy(out->name, payload + name_start, name_len);
out->name[name_len] = '\0';
if (offset < len) offset++; /* skip null terminator */
/* Parse type-specific data */
switch (out->type) {
case CEL_PARAM_TEXT_SELECT: {
/* Options: null-terminated, semicolon-separated string */
size_t opts_start = offset;
while (offset < len && payload[offset] != '\0') offset++;
size_t opts_len = offset - opts_start;
if (opts_len >= sizeof(out->options)) opts_len = sizeof(out->options) - 1;
memcpy(out->options, payload + opts_start, opts_len);
out->options[opts_len] = '\0';
if (offset < len) offset++; /* skip null terminator */
/* [value][min][max][default] */
if (offset + 4 <= len) {
out->value = payload[offset++];
out->min_val = payload[offset++];
out->max_val = payload[offset++];
out->default_val = payload[offset++];
}
break;
}
case CEL_PARAM_UINT8:
case CEL_PARAM_INT8:
/* [min][max][default][value] */
if (offset + 4 <= len) {
out->min_val = payload[offset++];
out->max_val = payload[offset++];
out->default_val = payload[offset++];
out->value = payload[offset++];
}
break;
default:
/* Other types have no simple value representation */
break;
}
return 0;
}
+55
View File
@@ -0,0 +1,55 @@
#pragma once
#include <stddef.h>
#include <stdint.h>
#include "celrs/crsf.h"
#include "celrs/serial.h"
/* TX power levels (mW) */
#define CEL_POWER_10_MW 10
#define CEL_POWER_25_MW 25
#define CEL_POWER_50_MW 50
#define CEL_POWER_100_MW 100
#define CEL_POWER_250_MW 250
#define CEL_POWER_500_MW 500
#define CEL_POWER_1000_MW 1000
#define CEL_POWER_2000_MW 2000
/* Parsed parameter entry */
typedef struct {
uint8_t index;
uint8_t type; /* CEL_PARAM_* */
uint8_t hidden;
char name[64];
/* TEXT_SELECT specific */
char options[256]; /* semicolon-separated option strings */
uint8_t value;
uint8_t min_val;
uint8_t max_val;
uint8_t default_val;
} cel_crsf_param;
/* Send a device ping and wait for DEVICE_INFO response.
* Returns 0 on success, -1 on timeout/error. */
int cel_crsf_param_ping(cel_serial_port* port, float timeout_sec);
/* Read a single parameter by index.
* Returns 0 on success (out filled), -1 on timeout/error. */
int cel_crsf_param_read(cel_serial_port* port, uint8_t index,
cel_crsf_param* out, float timeout_sec);
/* Write a parameter value.
* Returns 0 on success, -1 on error. */
int cel_crsf_param_write(cel_serial_port* port, uint8_t index,
uint8_t value);
/* Set TX output power by enumerating parameters.
* mw: transmit power in mW (one of CEL_POWER_* constants).
* Returns 0 on success, -1 on error. */
int cel_crsf_param_set_power(cel_serial_port* port, int mw,
float timeout_sec);
/* Parse a PARAM_ENTRY payload into a cel_crsf_param struct.
* Returns 0 on success, -1 on bad payload. */
int cel_crsf_param_parse(cel_crsf_param* out, uint8_t const* payload,
size_t len);
+81
View File
@@ -0,0 +1,81 @@
#include "celrs/crsf_stream.h"
#include <stdlib.h>
#include <string.h>
/* Valid CRSF address bytes (frame sync bytes) */
static uint8_t const s_valid_addrs[] = {
0x00, 0xC8, 0xC4, 0xEC, 0xEE, 0xEA, 0xEF
};
static int is_valid_addr(uint8_t addr) {
for (size_t i = 0; i < sizeof(s_valid_addrs); i++) {
if (s_valid_addrs[i] == addr) return 1;
}
return 0;
}
struct cel_crsf_stream {
uint8_t buf[260]; /* max frame = 2 + 255 + 1 = 258 */
size_t buf_len;
};
cel_crsf_stream* cel_crsf_stream_create(void) {
cel_crsf_stream* s = calloc(1, sizeof(*s));
return s;
}
void cel_crsf_stream_destroy(cel_crsf_stream* stream) {
free(stream);
}
int cel_crsf_stream_feed(cel_crsf_stream* stream, uint8_t const* data,
size_t len, cel_crsf_frame* out,
size_t out_capacity) {
if (stream == NULL) return -1;
if (out == NULL) return -1;
if (data == NULL && len > 0) return -1;
if (data == NULL && len == 0) {
/* Drain remaining frames from buffer without new data */
len = 0;
}
/* Append new data to buffer */
if (len > 0) {
if (stream->buf_len + len > sizeof(stream->buf)) {
/* Buffer overflow — discard everything */
stream->buf_len = 0;
return -1;
}
memcpy(stream->buf + stream->buf_len, data, len);
stream->buf_len += len;
}
int count = 0;
while (stream->buf_len >= 4 && count < (int)out_capacity) {
/* Skip invalid sync bytes */
if (!is_valid_addr(stream->buf[0])) {
memmove(stream->buf, stream->buf + 1, stream->buf_len - 1);
stream->buf_len--;
continue;
}
uint8_t length = stream->buf[1];
size_t total = 2 + length;
if (stream->buf_len < total) break; /* need more data */
/* Try to parse frame */
cel_crsf_frame frame;
if (cel_crsf_frame_parse(&frame, stream->buf, total) == 0) {
out[count++] = frame;
}
/* Discard frame (valid or not) */
memmove(stream->buf, stream->buf + total, stream->buf_len - total);
stream->buf_len -= total;
}
return count;
}
void cel_crsf_stream_reset(cel_crsf_stream* stream) {
stream->buf_len = 0;
}
+24
View File
@@ -0,0 +1,24 @@
#pragma once
#include <stdint.h>
#include <stddef.h>
#include "celrs/crsf.h"
/* Incremental CRSF frame parser for streaming byte data.
* Accepts raw bytes and extracts complete frames as they arrive. */
typedef struct cel_crsf_stream cel_crsf_stream;
/* Create a new stream reader. Returns NULL on allocation failure. */
cel_crsf_stream* cel_crsf_stream_create(void);
/* Free the stream reader. */
void cel_crsf_stream_destroy(cel_crsf_stream* stream);
/* Feed raw bytes into the stream. Returns the number of complete frames
* parsed into out[]. out must have space for at least out_capacity frames.
* Returns -1 on error. Frames that fail CRC validation are discarded. */
int cel_crsf_stream_feed(cel_crsf_stream* stream, uint8_t const* data,
size_t len, cel_crsf_frame* out,
size_t out_capacity);
/* Reset the internal buffer (discard any partial frame). */
void cel_crsf_stream_reset(cel_crsf_stream* stream);
+96
View File
@@ -0,0 +1,96 @@
#include "celrs/crsf_telemetry.h"
#include <string.h>
/* Helper: read uint16_t little-endian from buffer */
static uint16_t read_u16_le(uint8_t const* buf) {
return (uint16_t)buf[0] | ((uint16_t)buf[1] << 8);
}
/* Helper: read uint16_t big-endian from buffer */
static uint16_t read_u16_be(uint8_t const* buf) {
return ((uint16_t)buf[0] << 8) | (uint16_t)buf[1];
}
/* Helper: read uint32_t big-endian from 3-byte buffer */
static uint32_t read_u24_be(uint8_t const* buf) {
return ((uint32_t)buf[0] << 16) | ((uint32_t)buf[1] << 8) | (uint32_t)buf[2];
}
int cel_crsf_telemetry_parse(cel_crsf_frame const* frame,
cel_telemetry* out) {
if (frame == NULL || out == NULL) return -1;
uint8_t type = frame->type;
uint8_t const* p = frame->payload;
uint8_t len = frame->payload_len;
switch (type) {
case CEL_CRSF_TYPE_LINK_STATS: {
if (len < 10) return -1;
out->type = CEL_TELEM_LINK;
out->data.link.uplink_rssi1 = p[0];
out->data.link.uplink_rssi2 = p[1];
out->data.link.uplink_quality = p[2];
out->data.link.uplink_snr = (int8_t)p[3];
out->data.link.active_antenna = p[4];
out->data.link.rf_mode = p[5];
out->data.link.uplink_power = p[6];
out->data.link.downlink_rssi = p[7];
out->data.link.downlink_qual = p[8];
out->data.link.downlink_snr = (int8_t)p[9];
return 0;
}
case CEL_CRSF_TYPE_BATTERY: {
if (len < 8) return -1;
out->type = CEL_TELEM_BATTERY;
/* CRSF battery: voltage(u16 BE 0.1V), current(u16 BE 0.1A),
capacity(u24 BE mAh), remaining(u8 %) */
out->data.battery.voltage_x10 = read_u16_be(p);
out->data.battery.current_x10 = read_u16_be(p + 2);
out->data.battery.capacity_mah = read_u24_be(p + 4);
out->data.battery.remaining_pct = p[7];
return 0;
}
case CEL_CRSF_TYPE_HEARTBEAT: {
if (len < 2) return -1;
out->type = CEL_TELEM_HEARTBEAT;
out->data.heartbeat.origin_addr = read_u16_le(p);
return 0;
}
case CEL_CRSF_TYPE_AIRSPEED: {
if (len < 2) return -1;
out->type = CEL_TELEM_AIRSPEED;
out->data.airspeed.speed_kmh = read_u16_le(p);
return 0;
}
case CEL_CRSF_TYPE_ATTITUDE: {
if (len < 6) return -1;
out->type = CEL_TELEM_ATTITUDE;
/* Each value is int16 big-endian / 10000.0 = radians */
int16_t pitch = (int16_t)((p[0] << 8) | p[1]);
int16_t roll = (int16_t)((p[2] << 8) | p[3]);
int16_t yaw = (int16_t)((p[4] << 8) | p[5]);
out->data.attitude.pitch_rad = pitch / 10000.0f;
out->data.attitude.roll_rad = roll / 10000.0f;
out->data.attitude.yaw_rad = yaw / 10000.0f;
return 0;
}
case CEL_CRSF_TYPE_FLIGHT_MODE: {
if (len == 0) return -1;
out->type = CEL_TELEM_FLIGHT_MODE;
size_t copy_len = len < sizeof(out->data.flight_mode.name) ? len
: sizeof(out->data.flight_mode.name);
memcpy(out->data.flight_mode.name, p, copy_len);
out->data.flight_mode.name[copy_len] = 0;
return 0;
}
default:
return -1;
}
}
+145
View File
@@ -0,0 +1,145 @@
#pragma once
#include <stdint.h>
#include "celrs/crsf.h"
/* Telemetry frame types */
typedef enum {
CEL_TELEM_GPS = 0,
CEL_TELEM_VARIO = 1,
CEL_TELEM_BATTERY = 2,
CEL_TELEM_BARO = 3,
CEL_TELEM_AIRSPEED = 4,
CEL_TELEM_HEARTBEAT = 5,
CEL_TELEM_RPM = 6,
CEL_TELEM_TEMP = 7,
CEL_TELEM_VOLTAGES = 8,
CEL_TELEM_ESC = 9,
CEL_TELEM_LINK = 10,
CEL_TELEM_ATTITUDE = 11,
CEL_TELEM_FLIGHT_MODE = 12,
CEL_TELEM_PARAM = 13,
CEL_TELEM_UNKNOWN = 14,
} cel_telem_type;
/* GPS telemetry */
typedef struct {
float latitude_deg;
float longitude_deg;
float groundspeed_kmh;
float heading_deg;
float altitude_m;
uint8_t satellites;
} cel_telem_gps;
/* Vario (vertical speed) */
typedef struct {
int16_t vertical_speed_cms;
} cel_telem_vario;
/* Battery sensor (CRSF: u16 BE 0.1V, u16 BE 0.1A, u24 BE mAh, u8 %) */
typedef struct {
uint16_t voltage_x10; /* x 0.1V */
uint16_t current_x10; /* x 0.1A */
uint32_t capacity_mah; /* mAh consumed */
uint8_t remaining_pct; /* percentage remaining */
} cel_telem_battery;
/* Barometric altitude */
typedef struct {
float altitude_m;
int16_t vertical_speed_cms;
} cel_telem_baro;
/* Airspeed */
typedef struct {
uint16_t speed_kmh; /* km/h * 10 (0.1 km/h resolution) */
} cel_telem_airspeed;
/* Heartbeat */
typedef struct {
uint16_t origin_addr;
} cel_telem_heartbeat;
/* RPM sensor (up to 8 values) */
typedef struct {
uint8_t source;
int32_t values[8];
uint8_t count;
} cel_telem_rpm;
/* Temperature sensor (up to 8 values, deg C) */
typedef struct {
uint8_t source;
float values[8];
uint8_t count;
} cel_telem_temp;
/* Voltage cells (up to 8 values, volts) */
typedef struct {
uint8_t source;
float values[8];
uint8_t count;
} cel_telem_voltages;
/* ESC telemetry (BLHeli/KISS passthrough) */
typedef struct {
float voltage_v;
float current_a;
uint16_t consumed_mah;
uint32_t rpm;
uint8_t temp_c;
} cel_telem_esc;
/* Link statistics */
typedef struct {
uint8_t uplink_rssi1;
uint8_t uplink_rssi2;
uint8_t uplink_quality;
int8_t uplink_snr;
uint8_t active_antenna;
uint8_t rf_mode;
uint8_t uplink_power;
uint8_t downlink_rssi;
uint8_t downlink_qual;
int8_t downlink_snr;
} cel_telem_link;
/* Attitude (pitch/roll/yaw in radians) */
typedef struct {
float pitch_rad;
float roll_rad;
float yaw_rad;
} cel_telem_attitude;
/* Flight mode string */
typedef struct {
char name[32];
} cel_telem_flight_mode;
/* Union of all telemetry data types */
typedef union {
cel_telem_gps gps;
cel_telem_vario vario;
cel_telem_battery battery;
cel_telem_baro baro;
cel_telem_airspeed airspeed;
cel_telem_heartbeat heartbeat;
cel_telem_rpm rpm;
cel_telem_temp temp;
cel_telem_voltages voltages;
cel_telem_esc esc;
cel_telem_link link;
cel_telem_attitude attitude;
cel_telem_flight_mode flight_mode;
} cel_telem_data;
/* Parsed telemetry frame */
typedef struct {
cel_telem_type type;
cel_telem_data data;
} cel_telemetry;
/* Parse a CRSF frame's payload into a telemetry struct.
Returns 0 on success, -1 on unsupported type or bad payload length. */
int cel_crsf_telemetry_parse(cel_crsf_frame const* frame,
cel_telemetry* out);
+1 -1
View File
@@ -2,5 +2,5 @@
#include <stdio.h> #include <stdio.h>
void cel_log_write(char const* msg) { void cel_log_write(char const* msg) {
printf("%s\n", msg); fprintf(stderr, "%s\n", msg);
} }
+36
View File
@@ -0,0 +1,36 @@
#pragma once
#include <stddef.h>
#include <stdint.h>
/* Platform serial handle (HANDLE on Windows, fd on POSIX) */
typedef intptr_t cel_serial_platform_handle;
#define CEL_SERIAL_PLATFORM_INVALID_HANDLE ((cel_serial_platform_handle)-1)
/* Open the platform serial device at path with the given baud rate.
* Returns CEL_SERIAL_PLATFORM_INVALID_HANDLE on failure. */
cel_serial_platform_handle cel_serial_platform_open(char const* path, int baud_rate);
/* Close the platform serial device. */
void cel_serial_platform_close(cel_serial_platform_handle handle);
/* Read up to len bytes without blocking.
* Returns bytes read immediately available, or 0 if none/error. */
size_t cel_serial_platform_read(cel_serial_platform_handle handle, uint8_t* buf, size_t len);
/* Write data. Returns bytes written, 0 on error. */
size_t cel_serial_platform_write(cel_serial_platform_handle handle, uint8_t const* buf, size_t len);
/* Flush output buffer. */
void cel_serial_platform_flush(cel_serial_platform_handle handle);
/* Platform-specific port enumeration.
* Returns the number of ports found, or -1 on error.
* out_ports must be freed with cel_serial_free_ports(). */
int cel_serial_platform_list_ports(char*** out_ports, int max_ports);
/* Get a human-readable description for a serial port.
* Returns 0 on success, -1 if description unavailable.
* Always null-terminates out. */
int cel_serial_platform_get_description(char const* path,
char* out, size_t out_size);
+67
View File
@@ -0,0 +1,67 @@
#include "celrs/platform/serial_internal.h"
#include <dirent.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
int cel_serial_platform_list_ports(char*** out_ports, int max_ports) {
char** ports = (char**)calloc(max_ports, sizeof(char*));
if (ports == NULL) return -1;
int count = 0;
DIR* dir = opendir("/dev");
if (dir != NULL) {
struct dirent* entry;
while ((entry = readdir(dir)) != NULL && count < max_ports) {
if (strncmp(entry->d_name, "tty", 3) == 0) {
char* rest = entry->d_name + 3;
if (strncmp(rest, "USB", 3) == 0 ||
strncmp(rest, "ACM", 3) == 0) {
char path[256];
snprintf(path, sizeof(path), "/dev/%s", entry->d_name);
if (access(path, R_OK | W_OK) == 0) {
ports[count] = strdup(path);
if (ports[count] != NULL) count++;
}
}
}
}
closedir(dir);
}
*out_ports = ports;
return count;
}
int cel_serial_platform_get_description(char const* path,
char* out, size_t out_size) {
if (out == NULL || out_size == 0) return -1;
out[0] = '\0';
/* Try reading from sysfs for USB device info */
char sysfs_path[512];
snprintf(sysfs_path, sizeof(sysfs_path),
"/sys/class/tty/%s/device/idVendor",
path + 5); /* skip /dev/ */
FILE* f = fopen(sysfs_path, "r");
if (f == NULL) {
/* Fallback: return the port name itself */
strncpy(out, path, out_size - 1);
out[out_size - 1] = '\0';
return 0;
}
char vendor[16] = {0};
if (fgets(vendor, sizeof(vendor), f) != NULL) {
fclose(f);
snprintf(out, out_size, "%s (USB)", path);
} else {
fclose(f);
strncpy(out, path, out_size - 1);
out[out_size - 1] = '\0';
}
return 0;
}
+141
View File
@@ -0,0 +1,141 @@
#include "celrs/platform/serial_internal.h"
#include <windows.h>
#include <setupapi.h>
#include <initguid.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
cel_serial_platform_handle cel_serial_platform_open(char const* path, int baud_rate) {
char full_path[300];
snprintf(full_path, sizeof(full_path), "\\\\.\\%s", path);
HANDLE h = CreateFileA(full_path, GENERIC_READ | GENERIC_WRITE, 0, NULL,
OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
if (h == INVALID_HANDLE_VALUE) return CEL_SERIAL_PLATFORM_INVALID_HANDLE;
DCB dcb = {0};
dcb.DCBlength = sizeof(dcb);
if (!GetCommState(h, &dcb)) {
CloseHandle(h);
return CEL_SERIAL_PLATFORM_INVALID_HANDLE;
}
dcb.BaudRate = (DWORD)baud_rate;
dcb.ByteSize = 8;
dcb.Parity = NOPARITY;
dcb.StopBits = ONESTOPBIT;
/* Keep DTR/RTS low so the TX module is not reset on connect. Setting
* these in the same SetCommState call (rather than via a later
* EscapeCommFunction) avoids a brief DTR/RTS-high pulse if the port's
* existing DCB has them enabled. */
dcb.fDtrControl = DTR_CONTROL_DISABLE;
dcb.fRtsControl = RTS_CONTROL_DISABLE;
if (!SetCommState(h, &dcb)) {
CloseHandle(h);
return CEL_SERIAL_PLATFORM_INVALID_HANDLE;
}
/* Non-blocking reads: ReadFile returns immediately with whatever
* bytes are already buffered (possibly zero). */
COMMTIMEOUTS timeouts = {0};
timeouts.ReadIntervalTimeout = MAXDWORD;
if (!SetCommTimeouts(h, &timeouts)) {
CloseHandle(h);
return CEL_SERIAL_PLATFORM_INVALID_HANDLE;
}
return (cel_serial_platform_handle)(intptr_t)h;
}
void cel_serial_platform_close(cel_serial_platform_handle handle) {
CloseHandle((HANDLE)(intptr_t)handle);
}
size_t cel_serial_platform_read(cel_serial_platform_handle handle, uint8_t* buf, size_t len) {
DWORD bytes_read = 0;
if (!ReadFile((HANDLE)(intptr_t)handle, buf, (DWORD)len, &bytes_read, NULL)) return 0;
return (size_t)bytes_read;
}
size_t cel_serial_platform_write(cel_serial_platform_handle handle, uint8_t const* buf, size_t len) {
DWORD bytes_written = 0;
if (!WriteFile((HANDLE)(intptr_t)handle, buf, (DWORD)len, &bytes_written, NULL)) return 0;
return (size_t)bytes_written;
}
void cel_serial_platform_flush(cel_serial_platform_handle handle) {
FlushFileBuffers((HANDLE)(intptr_t)handle);
}
/* Enumerate active COM ports via HKLM\HARDWARE\DEVICEMAP\SERIALCOMM.
* Windows keeps one value per active port in this key, so this is a
* single registry read instead of probing COM1..COM255 with CreateFile. */
int cel_serial_platform_list_ports(char*** out_ports, int max_ports) {
char** ports = (char**)calloc(max_ports, sizeof(char*));
if (ports == NULL) return -1;
int count = 0;
HKEY key;
if (RegOpenKeyExA(HKEY_LOCAL_MACHINE, "HARDWARE\\DEVICEMAP\\SERIALCOMM",
0, KEY_READ, &key) == ERROR_SUCCESS) {
for (DWORD index = 0; count < max_ports; index++) {
char value_name[256];
char port_name[256];
DWORD value_name_len = sizeof(value_name);
DWORD port_name_len = sizeof(port_name);
DWORD type;
LONG result = RegEnumValueA(key, index, value_name, &value_name_len,
NULL, &type, (BYTE*)port_name, &port_name_len);
if (result != ERROR_SUCCESS) break;
if (type == REG_SZ) {
ports[count] = _strdup(port_name);
if (ports[count] != NULL) count++;
}
}
RegCloseKey(key);
}
*out_ports = ports;
return count;
}
int cel_serial_platform_get_description(char const* path,
char* out, size_t out_size) {
if (out == NULL || out_size == 0) return -1;
out[0] = '\0';
HDEVINFO dev_info = SetupDiGetClassDevs(NULL, "USB", NULL, DIGCF_PRESENT);
if (dev_info == INVALID_HANDLE_VALUE) return -1;
int found = -1;
SP_DEVINFO_DATA dev_info_data;
dev_info_data.cbSize = sizeof(SP_DEVINFO_DATA);
for (DWORD i = 0; SetupDiEnumDeviceInfo(dev_info, i, &dev_info_data); i++) {
char friendly_name[256];
DWORD type, req;
if (SetupDiGetDeviceRegistryPropertyA(dev_info, &dev_info_data,
SPDRP_FRIENDLYNAME, &type, (BYTE*)friendly_name,
sizeof(friendly_name), &req)) {
/* Check if this device uses our COM port */
char* port_match = strstr(friendly_name, path);
if (port_match != NULL) {
strncpy(out, friendly_name, out_size - 1);
out[out_size - 1] = '\0';
found = 0;
break;
}
}
}
SetupDiDestroyDeviceInfoList(dev_info);
return found;
}
+79 -32
View File
@@ -3,16 +3,7 @@
#endif #endif
#include "celrs/serial.h" #include "celrs/serial.h"
#include "celrs/platform/serial_internal.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 <stdlib.h>
#include <string.h> #include <string.h>
@@ -20,50 +11,106 @@
struct cel_serial_port { struct cel_serial_port {
char path[256]; char path[256];
int baud_rate; int baud_rate;
int fd; /* platform-specific handle (HANDLE on Win, int on POSIX) */ cel_serial_platform_handle handle;
}; };
cel_serial_port* cel_serial_open(char const* path, int baud_rate) { cel_serial_port* cel_serial_open(char const* path, int baud_rate) {
if (path == NULL) return NULL; if (path == NULL) return NULL;
cel_serial_platform_handle handle = cel_serial_platform_open(path, baud_rate);
if (handle == CEL_SERIAL_PLATFORM_INVALID_HANDLE) return NULL;
cel_serial_port* port = (cel_serial_port*)calloc(1, sizeof(cel_serial_port)); cel_serial_port* port = (cel_serial_port*)calloc(1, sizeof(cel_serial_port));
if (port == NULL) return NULL; if (port == NULL) {
cel_serial_platform_close(handle);
return NULL;
}
strncpy(port->path, path, sizeof(port->path) - 1); strncpy(port->path, path, sizeof(port->path) - 1);
port->path[sizeof(port->path) - 1] = '\0'; port->path[sizeof(port->path) - 1] = '\0';
port->baud_rate = baud_rate; port->baud_rate = baud_rate;
port->fd = -1; port->handle = handle;
/* TODO: platform-specific open (CreateFile on Win, open+termios on POSIX) */
(void)baud_rate;
return port; return port;
} }
void cel_serial_close(cel_serial_port* port) { void cel_serial_close(cel_serial_port* port) {
if (port == NULL) return; if (port == NULL) return;
/* TODO: platform-specific close */ cel_serial_platform_close(port->handle);
free(port); free(port);
} }
size_t cel_serial_read(cel_serial_port* port, unsigned char* buf, size_t len, int timeout_ms) { size_t cel_serial_read(cel_serial_port* port, uint8_t* buf, size_t len) {
(void)port; if (port == NULL) return 0;
(void)buf; return cel_serial_platform_read(port->handle, buf, len);
(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) { size_t cel_serial_write(cel_serial_port* port, uint8_t const* buf, size_t len) {
(void)port; if (port == NULL) return 0;
(void)buf; return cel_serial_platform_write(port->handle, buf, len);
(void)len;
/* TODO: platform-specific write */
return 0;
} }
void cel_serial_flush(cel_serial_port* port) { void cel_serial_flush(cel_serial_port* port) {
(void)port; if (port == NULL) return;
/* TODO: platform-specific flush */ cel_serial_platform_flush(port->handle);
}
int cel_serial_list_ports(char*** out_ports, int max_ports) {
if (out_ports == NULL) return -1;
if (max_ports <= 0) max_ports = 64;
return cel_serial_platform_list_ports(out_ports, max_ports);
}
void cel_serial_free_ports(char** ports, int count) {
if (ports == NULL) return;
for (int i = 0; i < count; i++) {
free(ports[i]);
}
free(ports);
}
int cel_serial_find_elrs_port(char* out, size_t out_size) {
if (out == NULL || out_size == 0) return -1;
char** ports = NULL;
int count = cel_serial_list_ports(&ports, 0);
if (count < 0) return -1;
char const* keywords[] = {
"silicon labs", "cp210", "elrs", "expresslrs",
"bayck", "ch340", "ch343", "ftdi", "uart"
};
size_t n_keywords = sizeof(keywords) / sizeof(keywords[0]);
for (int i = 0; i < count; i++) {
char desc[256];
if (cel_serial_platform_get_description(ports[i], desc, sizeof(desc)) == 0) {
for (size_t k = 0; k < n_keywords; k++) {
if (strstr(desc, keywords[k]) != NULL) {
strncpy(out, ports[i], out_size - 1);
out[out_size - 1] = '\0';
cel_serial_free_ports(ports, count);
return 0;
}
}
}
}
cel_serial_free_ports(ports, count);
return -1;
}
cel_serial_port* cel_serial_open_probe(char const* path,
int const bauds[], int count,
int* out_baud) {
if (path == NULL || bauds == NULL || count <= 0) return NULL;
for (int i = 0; i < count; i++) {
cel_serial_port* port = cel_serial_open(path, bauds[i]);
if (port != NULL) {
if (out_baud != NULL) *out_baud = bauds[i];
return port;
}
}
return NULL;
} }
+27 -3
View File
@@ -1,5 +1,6 @@
#pragma once #pragma once
#include <stddef.h> #include <stddef.h>
#include <stdint.h>
/* Opaque serial port handle */ /* Opaque serial port handle */
typedef struct cel_serial_port cel_serial_port; typedef struct cel_serial_port cel_serial_port;
@@ -14,11 +15,34 @@ cel_serial_port* cel_serial_open(char const* path, int baud_rate);
/* Close and free the serial port */ /* Close and free the serial port */
void cel_serial_close(cel_serial_port* port); void cel_serial_close(cel_serial_port* port);
/* Read up to len bytes. Returns bytes read, 0 on timeout/error. */ /* Read up to len bytes without blocking.
size_t cel_serial_read(cel_serial_port* port, unsigned char* buf, size_t len, int timeout_ms); * Returns bytes read immediately available, or 0 if none/error. */
size_t cel_serial_read(cel_serial_port* port, uint8_t* buf, size_t len);
/* Write data. Returns bytes written, 0 on error. */ /* Write data. Returns bytes written, 0 on error. */
size_t cel_serial_write(cel_serial_port* port, unsigned char const* buf, size_t len); size_t cel_serial_write(cel_serial_port* port, uint8_t const* buf, size_t len);
/* Flush output buffer */ /* Flush output buffer */
void cel_serial_flush(cel_serial_port* port); void cel_serial_flush(cel_serial_port* port);
/* List available serial ports.
* Returns the number of ports found, or -1 on error.
* Caller must free the returned array with cel_serial_free_ports(). */
int cel_serial_list_ports(char*** out_ports, int max_ports);
/* Free the port list returned by cel_serial_list_ports */
void cel_serial_free_ports(char** ports, int count);
/* Find the first serial port matching known ELRS/CP210x keywords.
* Writes the device path into out (up to out_size bytes including null).
* Returns 0 on success, -1 if no matching port found. */
int cel_serial_find_elrs_port(char* out, size_t out_size);
/* Open a serial port by probing multiple baud rates.
* bauds is an array of baud rates to try (in order), count is the array length.
* DTR/RTS are held low to avoid resetting the TX module on connect.
* Returns the opened port on success, NULL if all baud rates fail.
* The actual baud rate used is stored in *out_baud (may be NULL). */
cel_serial_port* cel_serial_open_probe(char const* path,
int const bauds[], int count,
int* out_baud);
+31 -5
View File
@@ -18,6 +18,7 @@ function(cmock_generate_mock target header)
add_custom_command( add_custom_command(
OUTPUT "${mock_src}" "${mock_hdr}" OUTPUT "${mock_src}" "${mock_hdr}"
COMMAND "${RUBY_EXECUTABLE}" "${CMOCK_SCRIPT}" COMMAND "${RUBY_EXECUTABLE}" "${CMOCK_SCRIPT}"
"-o" "${CMAKE_CURRENT_SOURCE_DIR}/cmock.yml"
"--mock_path=${MOCK_GEN_DIR}" "--mock_path=${MOCK_GEN_DIR}"
"${header}" "${header}"
DEPENDS "${header}" DEPENDS "${header}"
@@ -33,24 +34,49 @@ set(TEST_TARGETS "")
# CRSF tests — pure functions (CRC, parse, build), no mock needed # CRSF tests — pure functions (CRC, parse, build), no mock needed
add_executable(test_crsf test_crsf.c) add_executable(test_crsf test_crsf.c)
target_include_directories(test_crsf PRIVATE "${CMAKE_SOURCE_DIR}") target_include_directories(test_crsf PRIVATE "${CMAKE_SOURCE_DIR}")
target_link_libraries(test_crsf PRIVATE celrs_crsf Unity::Unity) target_link_libraries(test_crsf PRIVATE celcrsf Unity::Unity)
target_compile_features(test_crsf PRIVATE c_std_23) target_compile_features(test_crsf PRIVATE c_std_23)
add_test(NAME test_crsf COMMAND test_crsf) add_test(NAME test_crsf COMMAND test_crsf)
list(APPEND TEST_TARGETS test_crsf) list(APPEND TEST_TARGETS test_crsf)
# Serial tests — mocks log_write.h for any logging calls # CRSF stream tests
add_executable(test_crsf_stream test_crsf_stream.c)
target_include_directories(test_crsf_stream PRIVATE "${CMAKE_SOURCE_DIR}")
target_link_libraries(test_crsf_stream PRIVATE celcrsf Unity::Unity)
target_compile_features(test_crsf_stream PRIVATE c_std_23)
add_test(NAME test_crsf_stream COMMAND test_crsf_stream)
list(APPEND TEST_TARGETS test_crsf_stream)
# CRSF telemetry tests
add_executable(test_crsf_telemetry test_crsf_telemetry.c)
target_include_directories(test_crsf_telemetry PRIVATE "${CMAKE_SOURCE_DIR}")
target_link_libraries(test_crsf_telemetry PRIVATE celcrsf Unity::Unity)
target_compile_features(test_crsf_telemetry PRIVATE c_std_23)
add_test(NAME test_crsf_telemetry COMMAND test_crsf_telemetry)
list(APPEND TEST_TARGETS test_crsf_telemetry)
# CRSF param tests — mocks serial for write/ping/read
add_executable(test_crsf_param test_crsf_param.c)
target_include_directories(test_crsf_param PRIVATE "${CMAKE_SOURCE_DIR}")
target_link_libraries(test_crsf_param PRIVATE celcrsf celserial Unity::Unity CMock::CMock)
target_compile_features(test_crsf_param PRIVATE c_std_23)
cmock_generate_mock(test_crsf_param "${CMAKE_SOURCE_DIR}/celrs/platform/serial_internal.h")
add_test(NAME test_crsf_param COMMAND test_crsf_param)
list(APPEND TEST_TARGETS test_crsf_param)
# Serial tests — mocks the platform backend (serial_internal.h)
add_executable(test_serial test_serial.c) add_executable(test_serial test_serial.c)
target_include_directories(test_serial PRIVATE "${CMAKE_SOURCE_DIR}") target_include_directories(test_serial PRIVATE "${CMAKE_SOURCE_DIR}")
target_link_libraries(test_serial PRIVATE celrs_serial Unity::Unity CMock::CMock) target_link_libraries(test_serial PRIVATE celserial Unity::Unity CMock::CMock)
target_compile_features(test_serial PRIVATE c_std_23) target_compile_features(test_serial PRIVATE c_std_23)
cmock_generate_mock(test_serial "${CMAKE_SOURCE_DIR}/celrs/log_write.h") cmock_generate_mock(test_serial "${CMAKE_SOURCE_DIR}/celrs/platform/serial_internal.h")
add_test(NAME test_serial COMMAND test_serial) add_test(NAME test_serial COMMAND test_serial)
list(APPEND TEST_TARGETS test_serial) list(APPEND TEST_TARGETS test_serial)
# Logger tests — mocks log_write.h so output calls are intercepted # Logger tests — mocks log_write.h so output calls are intercepted
add_executable(test_logger test_logger.c) add_executable(test_logger test_logger.c)
target_include_directories(test_logger PRIVATE "${CMAKE_SOURCE_DIR}") target_include_directories(test_logger PRIVATE "${CMAKE_SOURCE_DIR}")
target_link_libraries(test_logger PRIVATE celrs_logger Unity::Unity CMock::CMock) target_link_libraries(test_logger PRIVATE cellogger Unity::Unity CMock::CMock)
target_compile_features(test_logger PRIVATE c_std_23) target_compile_features(test_logger PRIVATE c_std_23)
cmock_generate_mock(test_logger "${CMAKE_SOURCE_DIR}/celrs/log_write.h") cmock_generate_mock(test_logger "${CMAKE_SOURCE_DIR}/celrs/log_write.h")
add_test(NAME test_logger COMMAND test_logger) add_test(NAME test_logger COMMAND test_logger)
+8
View File
@@ -0,0 +1,8 @@
:cmock:
:mock_prefix: Mock
:when_no_prototypes: nothing
:weak: __attribute__((weak))
:plugins:
- :ignore_arg
- :expect_any_args
- :callback
+213 -50
View File
@@ -5,7 +5,7 @@
void setUp(void) {} void setUp(void) {}
void tearDown(void) {} void tearDown(void) {}
/* CRC tests */ /* CRC tests — CRC8/DVB-S2 (poly 0xD5) */
void test_crc_empty(void) { void test_crc_empty(void) {
uint8_t data[1] = {0}; uint8_t data[1] = {0};
TEST_ASSERT_EQUAL_UINT8(0x00, cel_crsf_crc(data, 0)); TEST_ASSERT_EQUAL_UINT8(0x00, cel_crsf_crc(data, 0));
@@ -18,32 +18,110 @@ void test_crc_single_byte(void) {
} }
void test_crc_known_value(void) { 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 data[6] = {0x10, 0x80, 0x03, 0x02, 0x80, 0x01};
uint8_t crc = cel_crsf_crc(data, 6); uint8_t crc = cel_crsf_crc(data, 6);
TEST_ASSERT_TRUE(crc != 0); TEST_ASSERT_TRUE(crc != 0);
/* Verify idempotency */
uint8_t crc2 = cel_crsf_crc(data, 6); uint8_t crc2 = cel_crsf_crc(data, 6);
TEST_ASSERT_EQUAL_UINT8(crc, crc2); TEST_ASSERT_EQUAL_UINT8(crc, crc2);
} }
/* Frame parse tests */ /* Channel helper tests */
void test_parse_invalid_header(void) { void test_channel_clamp_min(void) {
cel_crsf_frame frame; TEST_ASSERT_EQUAL_INT16(CEL_CRSF_CH_MIN, cel_crsf_channel_clamp(0));
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) { void test_channel_clamp_max(void) {
TEST_ASSERT_EQUAL_INT16(CEL_CRSF_CH_MAX, cel_crsf_channel_clamp(2048));
}
void test_channel_clamp_mid(void) {
TEST_ASSERT_EQUAL_INT16(CEL_CRSF_CH_MID, cel_crsf_channel_clamp(CEL_CRSF_CH_MID));
}
void test_channel_us_to_val_min(void) {
TEST_ASSERT_EQUAL_INT16(CEL_CRSF_CH_MIN, cel_crsf_channel_us_to_val(988));
}
void test_channel_us_to_val_mid(void) {
TEST_ASSERT_EQUAL_INT16(CEL_CRSF_CH_MID, cel_crsf_channel_us_to_val(1500));
}
void test_channel_us_to_val_max(void) {
TEST_ASSERT_EQUAL_INT16(CEL_CRSF_CH_MAX, cel_crsf_channel_us_to_val(2012));
}
void test_channel_us_to_val_below_min(void) {
TEST_ASSERT_EQUAL_INT16(CEL_CRSF_CH_MIN, cel_crsf_channel_us_to_val(0));
}
void test_channel_us_to_val_above_max(void) {
TEST_ASSERT_EQUAL_INT16(CEL_CRSF_CH_MAX, cel_crsf_channel_us_to_val(65535));
}
void test_channel_val_to_us_min(void) {
TEST_ASSERT_EQUAL_UINT16(988, cel_crsf_channel_val_to_us(CEL_CRSF_CH_MIN));
}
void test_channel_val_to_us_mid(void) {
TEST_ASSERT_EQUAL_UINT16(1500, cel_crsf_channel_val_to_us(CEL_CRSF_CH_MID));
}
void test_channel_val_to_us_max(void) {
TEST_ASSERT_EQUAL_UINT16(2012, cel_crsf_channel_val_to_us(CEL_CRSF_CH_MAX));
}
void test_channel_default_throttle_min(void) {
int16_t ch[16];
cel_crsf_channel_default(ch);
TEST_ASSERT_EQUAL_INT16(CEL_CRSF_CH_MIN, ch[2]); /* throttle */
}
void test_channel_default_centered(void) {
int16_t ch[16];
cel_crsf_channel_default(ch);
TEST_ASSERT_EQUAL_INT16(CEL_CRSF_CH_MID, ch[0]); /* roll */
TEST_ASSERT_EQUAL_INT16(CEL_CRSF_CH_MID, ch[1]); /* pitch */
TEST_ASSERT_EQUAL_INT16(CEL_CRSF_CH_MID, ch[3]); /* yaw */
}
void test_channel_default_aux_min(void) {
int16_t ch[16];
cel_crsf_channel_default(ch);
for (int i = 4; i < 16; i++) {
TEST_ASSERT_EQUAL_INT16(CEL_CRSF_CH_MIN, ch[i]);
}
}
/* Frame parse tests — ELRS format: [addr][length][type][payload][crc] */
/* Build a valid test frame with known CRC */
static void build_test_frame(uint8_t* dst, uint8_t addr, uint8_t type,
uint8_t const* payload, uint8_t payload_len) {
uint8_t length = 1 + payload_len + 1; /* type + payload + crc */
dst[0] = addr;
dst[1] = length;
dst[2] = type;
memcpy(dst + 3, payload, payload_len);
uint8_t crc = cel_crsf_crc(dst + 2, 1 + payload_len);
dst[2 + length - 1] = crc;
}
void test_parse_valid_frame(void) {
uint8_t buf[32];
uint8_t payload[2] = {0x80, 0x01};
build_test_frame(buf, 0xC8, CEL_CRSF_TYPE_HEARTBEAT, payload, 2);
cel_crsf_frame frame; cel_crsf_frame frame;
uint8_t buf[2] = {0xC8, 0x10}; TEST_ASSERT_EQUAL_INT(0, cel_crsf_frame_parse(&frame, buf, sizeof(buf)));
TEST_ASSERT_EQUAL_INT(-1, cel_crsf_frame_parse(&frame, buf, 2)); TEST_ASSERT_EQUAL_UINT8(0xC8, frame.addr);
TEST_ASSERT_EQUAL_UINT8(CEL_CRSF_TYPE_HEARTBEAT, frame.type);
TEST_ASSERT_EQUAL_UINT8(2, frame.payload_len);
TEST_ASSERT_EQUAL_UINT8(0x80, frame.payload[0]);
TEST_ASSERT_EQUAL_UINT8(0x01, frame.payload[1]);
} }
void test_parse_null_frame(void) { void test_parse_null_frame(void) {
uint8_t buf[8] = {0xC8, 0x10, 0x80, 0x03, 0x02, 0x80, 0x01, 0x00}; uint8_t buf[8];
TEST_ASSERT_EQUAL_INT(-1, cel_crsf_frame_parse(NULL, buf, 8)); TEST_ASSERT_EQUAL_INT(-1, cel_crsf_frame_parse(NULL, buf, 8));
} }
@@ -52,50 +130,113 @@ void test_parse_null_buf(void) {
TEST_ASSERT_EQUAL_INT(-1, cel_crsf_frame_parse(&frame, NULL, 8)); TEST_ASSERT_EQUAL_INT(-1, cel_crsf_frame_parse(&frame, NULL, 8));
} }
/* Frame build tests */ void test_parse_too_short(void) {
void test_build_heartbeat(void) { cel_crsf_frame frame;
uint8_t dst[256]; uint8_t buf[2] = {0xC8, 0x03};
TEST_ASSERT_EQUAL_INT(-1, cel_crsf_frame_parse(&frame, buf, 2));
}
void test_parse_bad_crc(void) {
uint8_t buf[32];
uint8_t payload[2] = {0x80, 0x01}; uint8_t payload[2] = {0x80, 0x01};
size_t len = cel_crsf_frame_build(dst, 0x00, 0x80, 0x03, payload, 2); build_test_frame(buf, 0xC8, CEL_CRSF_TYPE_HEARTBEAT, payload, 2);
buf[5] ^= 0xFF; /* corrupt the CRC */
TEST_ASSERT_GREATER_THAN(0, len); cel_crsf_frame frame;
TEST_ASSERT_EQUAL_UINT8(CEL_CRSF_FRAME_HEADER, dst[0]); TEST_ASSERT_EQUAL_INT(-1, cel_crsf_frame_parse(&frame, buf, sizeof(buf)));
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) { void test_parse_empty_payload(void) {
uint8_t dst[256]; uint8_t buf[32];
build_test_frame(buf, 0xEE, CEL_CRSF_TYPE_HEARTBEAT, NULL, 0);
cel_crsf_frame frame;
TEST_ASSERT_EQUAL_INT(0, cel_crsf_frame_parse(&frame, buf, sizeof(buf)));
TEST_ASSERT_EQUAL_UINT8(0xEE, frame.addr);
TEST_ASSERT_EQUAL_UINT8(CEL_CRSF_TYPE_HEARTBEAT, frame.type);
TEST_ASSERT_EQUAL_UINT8(0, frame.payload_len);
}
void test_parse_module_addr(void) {
uint8_t buf[32];
uint8_t payload[4] = {0xAA, 0xBB, 0xCC, 0xDD}; uint8_t payload[4] = {0xAA, 0xBB, 0xCC, 0xDD};
size_t len = cel_crsf_frame_build(dst, 0x10, 0x80, 0x01, payload, 4); build_test_frame(buf, 0xEE, CEL_CRSF_TYPE_GPS, payload, 4);
/* Parse the built frame back */
cel_crsf_frame frame; cel_crsf_frame frame;
TEST_ASSERT_EQUAL_INT(0, cel_crsf_frame_parse(&frame, dst, len)); TEST_ASSERT_EQUAL_INT(0, cel_crsf_frame_parse(&frame, buf, sizeof(buf)));
TEST_ASSERT_EQUAL_UINT8(0x10, frame.destination); TEST_ASSERT_EQUAL_UINT8(0xEE, frame.addr);
TEST_ASSERT_EQUAL_UINT8(0x80, frame.source); TEST_ASSERT_EQUAL_UINT8(CEL_CRSF_TYPE_GPS, frame.type);
TEST_ASSERT_EQUAL_UINT8(0x01, frame.type); TEST_ASSERT_EQUAL_UINT8(4, frame.payload_len);
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) { /* Frame builder tests */
uint8_t payload[2] = {0x01, 0x02};
TEST_ASSERT_EQUAL_UINT(0, cel_crsf_frame_build(NULL, 0x00, 0x80, 0x03, payload, 2)); void test_build_rc_frame_null_dst(void) {
int16_t ch[16];
TEST_ASSERT_EQUAL_UINT(0, cel_crsf_build_rc_frame(NULL, ch));
} }
void test_build_null_payload(void) { void test_build_rc_frame_null_channels(void) {
uint8_t dst[256]; uint8_t dst[32];
size_t len = cel_crsf_frame_build(dst, 0x10, 0x80, 0x03, NULL, 0); size_t len = cel_crsf_build_rc_frame(dst, NULL);
TEST_ASSERT_GREATER_THAN(0, len); TEST_ASSERT_GREATER_THAN(0, len);
/* Should still have valid CRC for empty payload */ TEST_ASSERT_EQUAL_UINT8(0xC8, dst[0]);
}
void test_build_rc_frame_roundtrip(void) {
int16_t ch[16];
cel_crsf_channel_default(ch);
ch[0] = CEL_CRSF_CH_MAX;
uint8_t dst[32];
size_t len = cel_crsf_build_rc_frame(dst, ch);
cel_crsf_frame frame; cel_crsf_frame frame;
TEST_ASSERT_EQUAL_INT(0, cel_crsf_frame_parse(&frame, dst, len)); TEST_ASSERT_EQUAL_INT(0, cel_crsf_frame_parse(&frame, dst, len));
TEST_ASSERT_EQUAL_UINT8(CEL_CRSF_TYPE_RC_CHANNELS, frame.type);
}
void test_build_ping_frame_null_dst(void) {
TEST_ASSERT_EQUAL_UINT(0, cel_crsf_build_ping_frame(NULL));
}
void test_build_ping_frame_valid(void) {
uint8_t dst[16];
size_t len = cel_crsf_build_ping_frame(dst);
TEST_ASSERT_GREATER_THAN(0, len);
TEST_ASSERT_EQUAL_UINT8(0xEE, dst[0]);
cel_crsf_frame frame;
TEST_ASSERT_EQUAL_INT(0, cel_crsf_frame_parse(&frame, dst, len));
TEST_ASSERT_EQUAL_UINT8(CEL_CRSF_TYPE_DEVICE_PING, frame.type);
}
void test_build_param_read_frame_null_dst(void) {
TEST_ASSERT_EQUAL_UINT(0, cel_crsf_build_param_read_frame(NULL, 0, 0));
}
void test_build_param_read_frame_valid(void) {
uint8_t dst[16];
size_t len = cel_crsf_build_param_read_frame(dst, 0x42, 0);
TEST_ASSERT_GREATER_THAN(0, len);
cel_crsf_frame frame;
TEST_ASSERT_EQUAL_INT(0, cel_crsf_frame_parse(&frame, dst, len));
TEST_ASSERT_EQUAL_UINT8(CEL_CRSF_TYPE_PARAM_READ, frame.type);
}
void test_build_param_write_frame_null_dst(void) {
TEST_ASSERT_EQUAL_UINT(0, cel_crsf_build_param_write_frame(NULL, 0, 0));
}
void test_build_param_write_frame_valid(void) {
uint8_t dst[16];
size_t len = cel_crsf_build_param_write_frame(dst, 0x42, 0xFF);
TEST_ASSERT_GREATER_THAN(0, len);
cel_crsf_frame frame;
TEST_ASSERT_EQUAL_INT(0, cel_crsf_frame_parse(&frame, dst, len));
TEST_ASSERT_EQUAL_UINT8(CEL_CRSF_TYPE_PARAM_WRITE, frame.type);
} }
int main(void) { int main(void) {
@@ -103,13 +244,35 @@ int main(void) {
RUN_TEST(test_crc_empty); RUN_TEST(test_crc_empty);
RUN_TEST(test_crc_single_byte); RUN_TEST(test_crc_single_byte);
RUN_TEST(test_crc_known_value); RUN_TEST(test_crc_known_value);
RUN_TEST(test_parse_invalid_header); RUN_TEST(test_channel_clamp_min);
RUN_TEST(test_parse_too_short); RUN_TEST(test_channel_clamp_max);
RUN_TEST(test_channel_clamp_mid);
RUN_TEST(test_channel_us_to_val_min);
RUN_TEST(test_channel_us_to_val_mid);
RUN_TEST(test_channel_us_to_val_max);
RUN_TEST(test_channel_us_to_val_below_min);
RUN_TEST(test_channel_us_to_val_above_max);
RUN_TEST(test_channel_val_to_us_min);
RUN_TEST(test_channel_val_to_us_mid);
RUN_TEST(test_channel_val_to_us_max);
RUN_TEST(test_channel_default_throttle_min);
RUN_TEST(test_channel_default_centered);
RUN_TEST(test_channel_default_aux_min);
RUN_TEST(test_parse_valid_frame);
RUN_TEST(test_parse_null_frame); RUN_TEST(test_parse_null_frame);
RUN_TEST(test_parse_null_buf); RUN_TEST(test_parse_null_buf);
RUN_TEST(test_build_heartbeat); RUN_TEST(test_parse_too_short);
RUN_TEST(test_build_roundtrip); RUN_TEST(test_parse_bad_crc);
RUN_TEST(test_build_null_dst); RUN_TEST(test_parse_empty_payload);
RUN_TEST(test_build_null_payload); RUN_TEST(test_parse_module_addr);
RUN_TEST(test_build_rc_frame_null_dst);
RUN_TEST(test_build_rc_frame_null_channels);
RUN_TEST(test_build_rc_frame_roundtrip);
RUN_TEST(test_build_ping_frame_null_dst);
RUN_TEST(test_build_ping_frame_valid);
RUN_TEST(test_build_param_read_frame_null_dst);
RUN_TEST(test_build_param_read_frame_valid);
RUN_TEST(test_build_param_write_frame_null_dst);
RUN_TEST(test_build_param_write_frame_valid);
return UNITY_END(); return UNITY_END();
} }
+533
View File
@@ -0,0 +1,533 @@
#include <string.h>
#include <time.h>
#include "unity.h"
#include "celrs/crsf_param.h"
#include "Mockserial_internal.h"
/* Global state for mock read callbacks */
static uint8_t s_mock_read_buf[260];
static size_t s_mock_read_len = 0;
static int s_mock_read_calls = 0;
static int s_mock_read_zero_until = 0; /* return 0 for first N calls */
/* Response queue for complex scenarios (set_power) */
#define MAX_RESPONSES 32
static uint8_t s_responses[MAX_RESPONSES][260];
static size_t s_response_lens[MAX_RESPONSES];
static int s_response_count = 0;
static int s_response_index = 0;
static int s_use_queue = 0;
/* Write callback to count writes */
static int s_write_count = 0;
static size_t mock_write_cb(cel_serial_platform_handle handle,
uint8_t const* buf, size_t len, int call_instance) {
(void)handle;
(void)buf;
(void)call_instance;
s_write_count++;
return len; /* always succeed */
}
static size_t mock_read_cb(cel_serial_platform_handle handle,
uint8_t* buf, size_t len, int call_instance) {
(void)handle;
(void)call_instance;
s_mock_read_calls++;
/* Queue-based responses (for set_power tests) */
if (s_use_queue) {
if (s_response_index < s_response_count) {
size_t to_copy = s_response_lens[s_response_index] < len
? s_response_lens[s_response_index] : len;
if (to_copy > 0)
memcpy(buf, s_responses[s_response_index], to_copy);
s_response_index++;
return to_copy;
}
return 0; /* exhaust queue -> return 0 (timeout) */
}
/* Simple mode */
if (s_mock_read_calls <= s_mock_read_zero_until) return 0;
size_t to_copy = s_mock_read_len < len ? s_mock_read_len : len;
if (to_copy > 0) memcpy(buf, s_mock_read_buf, to_copy);
return to_copy;
}
void setUp(void) {
Mockserial_internal_Init();
s_mock_read_calls = 0;
s_mock_read_zero_until = 0;
s_mock_read_len = 0;
s_use_queue = 0;
s_response_count = 0;
s_response_index = 0;
s_write_count = 0;
}
void tearDown(void) {
Mockserial_internal_Verify();
Mockserial_internal_Destroy();
}
/* Helper: build a valid CRSF frame (payload includes dest+src+data) */
static size_t build_frame(uint8_t* dst, uint8_t type,
uint8_t const* payload, uint8_t payload_len) {
uint8_t length = 1 + payload_len + 1;
dst[0] = CEL_CRSF_FRAME_HEADER;
dst[1] = length;
dst[2] = type;
memcpy(dst + 3, payload, payload_len);
uint8_t crc = cel_crsf_crc(dst + 2, 1 + payload_len);
dst[2 + length - 1] = crc;
return 2 + length;
}
/* cel_crsf_param_parse tests */
void test_param_parse_null_args(void) {
uint8_t payload[16] = {0};
TEST_ASSERT_EQUAL_INT(-1, cel_crsf_param_parse(NULL, payload, sizeof(payload)));
TEST_ASSERT_EQUAL_INT(-1, cel_crsf_param_parse(NULL, NULL, 0));
}
void test_param_parse_too_short(void) {
cel_crsf_param param;
uint8_t payload[5] = {0};
TEST_ASSERT_EQUAL_INT(-1, cel_crsf_param_parse(&param, payload, sizeof(payload)));
}
void test_param_parse_text_select(void) {
cel_crsf_param param;
/* dest=0x10, src=0xEE, index=5, chunks=0, parent=0, type=TEXT_SELECT */
/* name="TX Power", options="10 mW;25 mW;100 mW;500 mW;1000 mW", value=2, min=0, max=4, default=0 */
uint8_t payload[] = {
0x10, /* dest */
0xEE, /* src */
0x05, /* index */
0x00, /* chunks_remaining */
0x00, /* parent */
0x09, /* type = CEL_PARAM_TEXT_SELECT */
'T', 'X', ' ', 'P', 'o', 'w', 'e', 'r', '\0', /* name */
'1', '0', ' ', 'm', 'W', ';', /* options */
'2', '5', ' ', 'm', 'W', ';',
'1', '0', '0', ' ', 'm', 'W', ';',
'5', '0', '0', ' ', 'm', 'W', ';',
'1', '0', '0', '0', ' ', 'm', 'W', '\0', /* end of options */
0x02, /* value */
0x00, /* min */
0x04, /* max */
0x00, /* default */
};
TEST_ASSERT_EQUAL_INT(0, cel_crsf_param_parse(&param, payload, sizeof(payload)));
TEST_ASSERT_EQUAL_UINT8(5, param.index);
TEST_ASSERT_EQUAL_UINT8(CEL_PARAM_TEXT_SELECT, param.type);
TEST_ASSERT_EQUAL_UINT8(0, param.hidden);
TEST_ASSERT_EQUAL_STRING("TX Power", param.name);
TEST_ASSERT_EQUAL_STRING("10 mW;25 mW;100 mW;500 mW;1000 mW", param.options);
TEST_ASSERT_EQUAL_UINT8(2, param.value);
TEST_ASSERT_EQUAL_UINT8(0, param.min_val);
TEST_ASSERT_EQUAL_UINT8(4, param.max_val);
TEST_ASSERT_EQUAL_UINT8(0, param.default_val);
}
void test_param_parse_hidden_flag(void) {
cel_crsf_param param;
uint8_t payload[] = {
0x10, 0xEE, 0x00, 0x00, 0x00,
0x88, /* type with hidden bit set (0x80 | 0x08) */
'H', 'i', 'd', 'd', 'e', 'n', '\0',
'A', ';', 'B', '\0',
0x00, 0x00, 0x01, 0x00,
};
TEST_ASSERT_EQUAL_INT(0, cel_crsf_param_parse(&param, payload, sizeof(payload)));
TEST_ASSERT_EQUAL_UINT8(CEL_PARAM_FLOAT, param.type);
TEST_ASSERT_EQUAL_UINT8(1, param.hidden);
}
void test_param_parse_uint8_type(void) {
cel_crsf_param param;
uint8_t payload[] = {
0x10, 0xEE, 0x10, 0x00, 0x00,
0x00, /* type = CEL_PARAM_UINT8 */
'V', 'a', 'l', '\0', /* name */
0x00, /* min */
0xFF, /* max */
0x80, /* default */
0x42, /* value */
};
TEST_ASSERT_EQUAL_INT(0, cel_crsf_param_parse(&param, payload, sizeof(payload)));
TEST_ASSERT_EQUAL_UINT8(0x10, param.index);
TEST_ASSERT_EQUAL_UINT8(CEL_PARAM_UINT8, param.type);
TEST_ASSERT_EQUAL_STRING("Val", param.name);
TEST_ASSERT_EQUAL_UINT8(0x42, param.value);
TEST_ASSERT_EQUAL_UINT8(0x00, param.min_val);
TEST_ASSERT_EQUAL_UINT8(0xFF, param.max_val);
TEST_ASSERT_EQUAL_UINT8(0x80, param.default_val);
}
void test_param_parse_int8_type(void) {
cel_crsf_param param;
uint8_t payload[] = {
0x10, 0xEE, 0x01, 0x00, 0x00,
0x01, /* type = CEL_PARAM_INT8 */
'S', '\0',
0x80, /* min (-128) */
0x7F, /* max (127) */
0x00, /* default */
0x10, /* value */
};
TEST_ASSERT_EQUAL_INT(0, cel_crsf_param_parse(&param, payload, sizeof(payload)));
TEST_ASSERT_EQUAL_UINT8(CEL_PARAM_INT8, param.type);
TEST_ASSERT_EQUAL_UINT8(0x10, param.value);
}
void test_param_parse_folder(void) {
cel_crsf_param param;
uint8_t payload[] = {
0x10, 0xEE, 0xFF, 0x00, 0x00,
0x0B, /* type = CEL_PARAM_FOLDER */
'F', 'o', 'l', 'd', 'e', 'r', '\0',
};
TEST_ASSERT_EQUAL_INT(0, cel_crsf_param_parse(&param, payload, sizeof(payload)));
TEST_ASSERT_EQUAL_UINT8(CEL_PARAM_FOLDER, param.type);
TEST_ASSERT_EQUAL_STRING("Folder", param.name);
}
void test_param_parse_name_truncation(void) {
cel_crsf_param param;
/* Name longer than 63 chars (should be truncated) */
uint8_t payload[128] = {0};
payload[0] = 0x10; /* dest */
payload[1] = 0xEE; /* src */
payload[2] = 0x00; /* index */
payload[3] = 0x00; /* chunks */
payload[4] = 0x00; /* parent */
payload[5] = 0x0B; /* type = FOLDER */
/* Fill name with 70 'A' chars */
for (int i = 0; i < 70; i++) payload[6 + i] = 'A';
payload[6 + 70] = '\0';
TEST_ASSERT_EQUAL_INT(0, cel_crsf_param_parse(&param, payload, sizeof(payload)));
TEST_ASSERT_EQUAL_UINT8(63, strlen(param.name)); /* truncated to 63 */
}
void test_param_parse_options_truncation(void) {
cel_crsf_param param;
/* Options longer than 255 chars (should be truncated) */
uint8_t payload[400] = {0};
payload[0] = 0x10; /* dest */
payload[1] = 0xEE; /* src */
payload[2] = 0x00; /* index */
payload[3] = 0x00; /* chunks */
payload[4] = 0x00; /* parent */
payload[5] = 0x09; /* type = TEXT_SELECT */
strcpy((char*)(payload + 6), "Name");
payload[6 + 4] = '\0';
/* Fill options with 300 'O' chars */
size_t opts_start = 6 + 5; /* after name */
for (int i = 0; i < 299; i++) payload[opts_start + i] = 'O';
payload[opts_start + 299] = '\0';
/* Add type-specific data after options */
size_t after_opts = opts_start + 300;
payload[after_opts] = 0x00; /* value */
payload[after_opts + 1] = 0x00; /* min */
payload[after_opts + 2] = 0x00; /* max */
payload[after_opts + 3] = 0x00; /* default */
TEST_ASSERT_EQUAL_INT(0, cel_crsf_param_parse(&param, payload, sizeof(payload)));
TEST_ASSERT_EQUAL_UINT8(255, strlen(param.options)); /* truncated to 255 */
}
/* cel_crsf_param_write tests */
void test_param_write_null_port(void) {
TEST_ASSERT_EQUAL_INT(-1, cel_crsf_param_write(NULL, 0, 0));
}
void test_param_write_success(void) {
cel_serial_platform_open_ExpectAndReturn("COM3", 400000,
(cel_serial_platform_handle)42);
cel_serial_port* port = cel_serial_open("COM3", 400000);
TEST_ASSERT_NOT_NULL(port);
cel_serial_platform_write_ExpectAnyArgsAndReturn(8);
TEST_ASSERT_EQUAL_INT(0, cel_crsf_param_write(port, 5, 3));
cel_serial_platform_close_Expect((cel_serial_platform_handle)42);
cel_serial_close(port);
}
void test_param_write_partial_write(void) {
cel_serial_platform_open_ExpectAndReturn("COM3", 400000,
(cel_serial_platform_handle)42);
cel_serial_port* port = cel_serial_open("COM3", 400000);
TEST_ASSERT_NOT_NULL(port);
cel_serial_platform_write_ExpectAnyArgsAndReturn(4);
TEST_ASSERT_EQUAL_INT(-1, cel_crsf_param_write(port, 5, 3));
cel_serial_platform_close_Expect((cel_serial_platform_handle)42);
cel_serial_close(port);
}
/* cel_crsf_param_ping tests */
void test_param_ping_null_port(void) {
TEST_ASSERT_EQUAL_INT(-1, cel_crsf_param_ping(NULL, 1.0f));
}
void test_param_ping_success(void) {
/* Build a DEVICE_INFO frame (type 0x29) */
uint8_t payload[] = {0x10, 0xEE, 0x00}; /* dest, src, type */
size_t frame_len = build_frame(s_mock_read_buf,
CEL_CRSF_TYPE_DEVICE_INFO, payload, sizeof(payload));
s_mock_read_len = frame_len;
s_mock_read_zero_until = 0;
cel_serial_platform_open_ExpectAndReturn("COM3", 400000,
(cel_serial_platform_handle)42);
cel_serial_port* port = cel_serial_open("COM3", 400000);
TEST_ASSERT_NOT_NULL(port);
cel_serial_platform_write_ExpectAnyArgsAndReturn(6);
cel_serial_platform_read_StubWithCallback(mock_read_cb);
TEST_ASSERT_EQUAL_INT(0, cel_crsf_param_ping(port, 1.0f));
cel_serial_platform_close_Expect((cel_serial_platform_handle)42);
cel_serial_close(port);
}
void test_param_ping_timeout(void) {
cel_serial_platform_open_ExpectAndReturn("COM3", 400000,
(cel_serial_platform_handle)42);
cel_serial_port* port = cel_serial_open("COM3", 400000);
TEST_ASSERT_NOT_NULL(port);
cel_serial_platform_write_ExpectAnyArgsAndReturn(6);
cel_serial_platform_read_StubWithCallback(mock_read_cb);
/* mock_read_cb always returns 0 since s_mock_read_zero_until defaults to 0
* and s_mock_read_len is 0 */
/* Use a short timeout so test doesn't hang */
TEST_ASSERT_EQUAL_INT(-1, cel_crsf_param_ping(port, 0.05f));
cel_serial_platform_close_Expect((cel_serial_platform_handle)42);
cel_serial_close(port);
}
/* cel_crsf_param_read tests */
void test_param_read_null_port(void) {
cel_crsf_param param;
TEST_ASSERT_EQUAL_INT(-1, cel_crsf_param_read(NULL, 0, &param, 1.0f));
}
void test_param_read_null_out(void) {
cel_serial_platform_open_ExpectAndReturn("COM3", 400000,
(cel_serial_platform_handle)42);
cel_serial_port* port = cel_serial_open("COM3", 400000);
TEST_ASSERT_NOT_NULL(port);
/* No write expected since out is NULL */
TEST_ASSERT_EQUAL_INT(-1, cel_crsf_param_read(port, 5, NULL, 1.0f));
cel_serial_platform_close_Expect((cel_serial_platform_handle)42);
cel_serial_close(port);
}
void test_param_read_success(void) {
/* Build a PARAM_ENTRY frame (type 0x2B) matching index 5 */
uint8_t payload[] = {
0x10, 0xEE, 0x05, 0x00, 0x00, 0x00, /* dest,src,idx,chunks,parent,type */
'V', 'a', 'l', '\0', /* name */
0x00, 0xFF, 0x80, 0x42, /* min,max,default,value */
};
size_t frame_len = build_frame(s_mock_read_buf,
CEL_CRSF_TYPE_PARAM_ENTRY, payload, sizeof(payload));
s_mock_read_len = frame_len;
s_mock_read_zero_until = 0;
cel_serial_platform_open_ExpectAndReturn("COM3", 400000,
(cel_serial_platform_handle)42);
cel_serial_port* port = cel_serial_open("COM3", 400000);
TEST_ASSERT_NOT_NULL(port);
cel_serial_platform_write_ExpectAnyArgsAndReturn(8);
cel_serial_platform_read_StubWithCallback(mock_read_cb);
cel_crsf_param param;
TEST_ASSERT_EQUAL_INT(0, cel_crsf_param_read(port, 5, &param, 1.0f));
TEST_ASSERT_EQUAL_UINT8(5, param.index);
TEST_ASSERT_EQUAL_STRING("Val", param.name);
TEST_ASSERT_EQUAL_UINT8(0x42, param.value);
cel_serial_platform_close_Expect((cel_serial_platform_handle)42);
cel_serial_close(port);
}
void test_param_read_timeout(void) {
cel_serial_platform_open_ExpectAndReturn("COM3", 400000,
(cel_serial_platform_handle)42);
cel_serial_port* port = cel_serial_open("COM3", 400000);
TEST_ASSERT_NOT_NULL(port);
cel_serial_platform_write_ExpectAnyArgsAndReturn(8);
cel_serial_platform_read_StubWithCallback(mock_read_cb);
cel_crsf_param param;
TEST_ASSERT_EQUAL_INT(-1, cel_crsf_param_read(port, 5, &param, 0.05f));
cel_serial_platform_close_Expect((cel_serial_platform_handle)42);
cel_serial_close(port);
}
/* cel_crsf_param_set_power tests */
void test_set_power_null_port(void) {
TEST_ASSERT_EQUAL_INT(-1, cel_crsf_param_set_power(NULL, 100, 1.0f));
}
/* Verify build_frame + parse roundtrip works */
void test_set_power_frame_roundtrip(void) {
uint8_t p1_payload[] = {
0x10, 0xEE, 0x01, 0x00, 0x00, CEL_PARAM_TEXT_SELECT,
'T', 'X', ' ', 'P', 'o', 'w', 'e', 'r', '\0',
'1', '0', ' ', 'm', 'W', ';', '1', '0', '0', ' ', 'm', 'W', ';',
'd', 'y', 'n', 'a', 'm', 'i', 'c', '\0',
0x00, 0x02, 0x00, 0x00,
};
size_t frame_len = build_frame(s_mock_read_buf,
CEL_CRSF_TYPE_PARAM_ENTRY, p1_payload, sizeof(p1_payload));
/* Parse the frame */
cel_crsf_frame frame;
int rc = cel_crsf_frame_parse(&frame, s_mock_read_buf, frame_len);
TEST_ASSERT_EQUAL_INT(0, rc);
TEST_ASSERT_EQUAL_UINT8(CEL_CRSF_TYPE_PARAM_ENTRY, frame.type);
/* Parse the param */
cel_crsf_param param;
rc = cel_crsf_param_parse(&param, frame.payload, frame.payload_len);
TEST_ASSERT_EQUAL_INT(0, rc);
TEST_ASSERT_EQUAL_UINT8(1, param.index);
TEST_ASSERT_EQUAL_STRING("TX Power", param.name);
TEST_ASSERT_EQUAL_UINT8(CEL_PARAM_TEXT_SELECT, param.type);
TEST_ASSERT_NOT_NULL(strstr(param.options, "100 mW"));
}
void test_set_power_success(void) {
/* Enqueue responses: DEVICE_INFO, PARAM_ENTRY(idx=0), PARAM_ENTRY(idx=1=power) */
/* 1. DEVICE_INFO for ping */
uint8_t di_payload[] = {0x10, 0xEE, 0x00};
s_response_lens[s_response_count] =
build_frame(s_responses[s_response_count],
CEL_CRSF_TYPE_DEVICE_INFO, di_payload, sizeof(di_payload));
s_response_count++;
/* 2. PARAM_ENTRY for index 0 (not power) */
uint8_t p0_payload[] = {
0x10, 0xEE, 0x00, 0x00, 0x00, 0x00,
'R', 'C', '\0', 0x00, 0xFF, 0x80, 0x42,
};
s_response_lens[s_response_count] =
build_frame(s_responses[s_response_count],
CEL_CRSF_TYPE_PARAM_ENTRY, p0_payload, sizeof(p0_payload));
s_response_count++;
/* 3. PARAM_ENTRY for index 1 (TX Power, TEXT_SELECT) */
uint8_t p1_payload[] = {
0x10, 0xEE, 0x01, 0x00, 0x00, CEL_PARAM_TEXT_SELECT,
'T', 'X', ' ', 'P', 'o', 'w', 'e', 'r', '\0',
'1', '0', ' ', 'm', 'W', ';', '1', '0', '0', ' ', 'm', 'W', ';',
'd', 'y', 'n', 'a', 'm', 'i', 'c', '\0',
0x00, 0x02, 0x00, 0x00, /* value,min,max,default */
};
s_response_lens[s_response_count] =
build_frame(s_responses[s_response_count],
CEL_CRSF_TYPE_PARAM_ENTRY, p1_payload, sizeof(p1_payload));
s_response_count++;
s_use_queue = 1;
cel_serial_platform_open_ExpectAndReturn("COM3", 400000,
(cel_serial_platform_handle)42);
cel_serial_port* port = cel_serial_open("COM3", 400000);
TEST_ASSERT_NOT_NULL(port);
cel_serial_platform_write_StubWithCallback(mock_write_cb);
cel_serial_platform_read_StubWithCallback(mock_read_cb);
/* Request 100 mW -> should match "100 mW" option (index 1) */
TEST_ASSERT_EQUAL_INT(0, cel_crsf_param_set_power(port, 100, 1.0f));
cel_serial_platform_close_Expect((cel_serial_platform_handle)42);
cel_serial_close(port);
}
void test_set_power_not_found(void) {
/* Enqueue: DEVICE_INFO, then PARAM_ENTRY for index 0 only */
uint8_t di_payload[] = {0x10, 0xEE, 0x00};
s_response_lens[s_response_count] =
build_frame(s_responses[s_response_count],
CEL_CRSF_TYPE_DEVICE_INFO, di_payload, sizeof(di_payload));
s_response_count++;
uint8_t p0_payload[] = {
0x10, 0xEE, 0x00, 0x00, 0x00, 0x00,
'R', 'C', '\0', 0x00, 0xFF, 0x80, 0x42,
};
s_response_lens[s_response_count] =
build_frame(s_responses[s_response_count],
CEL_CRSF_TYPE_PARAM_ENTRY, p0_payload, sizeof(p0_payload));
s_response_count++;
s_use_queue = 1;
cel_serial_platform_open_ExpectAndReturn("COM3", 400000,
(cel_serial_platform_handle)42);
cel_serial_port* port = cel_serial_open("COM3", 400000);
TEST_ASSERT_NOT_NULL(port);
cel_serial_platform_write_StubWithCallback(mock_write_cb);
cel_serial_platform_read_StubWithCallback(mock_read_cb);
/* No power param in the list -> returns -1 after timeout on idx=1 */
TEST_ASSERT_EQUAL_INT(-1, cel_crsf_param_set_power(port, 100, 0.05f));
cel_serial_platform_close_Expect((cel_serial_platform_handle)42);
cel_serial_close(port);
}
int main(void) {
UNITY_BEGIN();
RUN_TEST(test_param_parse_null_args);
RUN_TEST(test_param_parse_too_short);
RUN_TEST(test_param_parse_text_select);
RUN_TEST(test_param_parse_hidden_flag);
RUN_TEST(test_param_parse_uint8_type);
RUN_TEST(test_param_parse_int8_type);
RUN_TEST(test_param_parse_folder);
RUN_TEST(test_param_parse_name_truncation);
RUN_TEST(test_param_parse_options_truncation);
RUN_TEST(test_param_write_null_port);
RUN_TEST(test_param_write_success);
RUN_TEST(test_param_write_partial_write);
RUN_TEST(test_param_ping_null_port);
RUN_TEST(test_param_ping_success);
RUN_TEST(test_param_ping_timeout);
RUN_TEST(test_param_read_null_port);
RUN_TEST(test_param_read_null_out);
RUN_TEST(test_param_read_success);
RUN_TEST(test_param_read_timeout);
RUN_TEST(test_set_power_null_port);
RUN_TEST(test_set_power_frame_roundtrip);
RUN_TEST(test_set_power_success);
RUN_TEST(test_set_power_not_found);
return UNITY_END();
}
+169
View File
@@ -0,0 +1,169 @@
#include "unity.h"
#include "celrs/crsf.h"
#include <string.h>
void setUp(void) {}
void tearDown(void) {}
/* Helper: build a valid test frame, return total bytes */
static size_t build_frame(uint8_t* dst, uint8_t addr, uint8_t type,
uint8_t const* payload, uint8_t payload_len) {
uint8_t length = 1 + payload_len + 1; /* type + payload + crc */
dst[0] = addr;
dst[1] = length;
dst[2] = type;
memcpy(dst + 3, payload, payload_len);
uint8_t crc = cel_crsf_crc(dst + 2, 1 + payload_len);
dst[2 + length - 1] = crc;
return 2 + length;
}
/* Stream tests */
void test_stream_create_destroy(void) {
cel_crsf_stream* s = cel_crsf_stream_create();
TEST_ASSERT_NOT_NULL(s);
cel_crsf_stream_destroy(s);
}
void test_stream_feed_empty(void) {
cel_crsf_stream* s = cel_crsf_stream_create();
cel_crsf_frame frames[4];
int n = cel_crsf_stream_feed(s, NULL, 0, frames, 4);
TEST_ASSERT_EQUAL_INT(0, n);
cel_crsf_stream_destroy(s);
}
void test_stream_feed_single_frame(void) {
uint8_t buf[32];
uint8_t payload[2] = {0x80, 0x01};
size_t len = build_frame(buf, 0xC8, CEL_CRSF_TYPE_HEARTBEAT, payload, 2);
cel_crsf_stream* s = cel_crsf_stream_create();
cel_crsf_frame frames[4];
int n = cel_crsf_stream_feed(s, buf, len, frames, 4);
TEST_ASSERT_EQUAL_INT(1, n);
TEST_ASSERT_EQUAL_UINT8(0xC8, frames[0].addr);
TEST_ASSERT_EQUAL_UINT8(CEL_CRSF_TYPE_HEARTBEAT, frames[0].type);
TEST_ASSERT_EQUAL_UINT8(2, frames[0].payload_len);
TEST_ASSERT_EQUAL_UINT8(0x80, frames[0].payload[0]);
cel_crsf_stream_destroy(s);
}
void test_stream_feed_incremental(void) {
uint8_t buf[32];
uint8_t payload[2] = {0x80, 0x01};
size_t total = build_frame(buf, 0xC8, CEL_CRSF_TYPE_HEARTBEAT, payload, 2);
cel_crsf_stream* s = cel_crsf_stream_create();
cel_crsf_frame frames[4];
/* Feed first 3 bytes — not enough for a complete frame */
int n = cel_crsf_stream_feed(s, buf, 3, frames, 4);
TEST_ASSERT_EQUAL_INT(0, n);
/* Feed remaining bytes — now complete */
n = cel_crsf_stream_feed(s, buf + 3, total - 3, frames, 4);
TEST_ASSERT_EQUAL_INT(1, n);
TEST_ASSERT_EQUAL_UINT8(CEL_CRSF_TYPE_HEARTBEAT, frames[0].type);
cel_crsf_stream_destroy(s);
}
void test_stream_feed_multiple_frames(void) {
uint8_t buf[64];
uint8_t p1[2] = {0x80, 0x01};
uint8_t p2[3] = {0xAA, 0xBB, 0xCC};
size_t len1 = build_frame(buf, 0xC8, CEL_CRSF_TYPE_HEARTBEAT, p1, 2);
size_t len2 = build_frame(buf + len1, 0xEE, CEL_CRSF_TYPE_GPS, p2, 3);
cel_crsf_stream* s = cel_crsf_stream_create();
cel_crsf_frame frames[4];
int n = cel_crsf_stream_feed(s, buf, len1 + len2, frames, 4);
TEST_ASSERT_EQUAL_INT(2, n);
TEST_ASSERT_EQUAL_UINT8(0xC8, frames[0].addr);
TEST_ASSERT_EQUAL_UINT8(CEL_CRSF_TYPE_HEARTBEAT, frames[0].type);
TEST_ASSERT_EQUAL_UINT8(0xEE, frames[1].addr);
TEST_ASSERT_EQUAL_UINT8(CEL_CRSF_TYPE_GPS, frames[1].type);
cel_crsf_stream_destroy(s);
}
void test_stream_feed_skip_bad_sync(void) {
uint8_t buf[48];
/* Garbage bytes before valid frame */
buf[0] = 0xFF;
buf[1] = 0xFE;
buf[2] = 0xFD;
uint8_t payload[2] = {0x80, 0x01};
size_t len = build_frame(buf + 3, 0xC8, CEL_CRSF_TYPE_HEARTBEAT, payload, 2);
cel_crsf_stream* s = cel_crsf_stream_create();
cel_crsf_frame frames[4];
int n = cel_crsf_stream_feed(s, buf, 3 + len, frames, 4);
TEST_ASSERT_EQUAL_INT(1, n);
TEST_ASSERT_EQUAL_UINT8(0xC8, frames[0].addr);
cel_crsf_stream_destroy(s);
}
void test_stream_feed_discard_bad_crc(void) {
uint8_t buf[32];
uint8_t payload[2] = {0x80, 0x01};
size_t len = build_frame(buf, 0xC8, CEL_CRSF_TYPE_HEARTBEAT, payload, 2);
buf[len - 1] ^= 0xFF; /* corrupt CRC */
cel_crsf_stream* s = cel_crsf_stream_create();
cel_crsf_frame frames[4];
int n = cel_crsf_stream_feed(s, buf, len, frames, 4);
TEST_ASSERT_EQUAL_INT(0, n); /* bad frame discarded */
cel_crsf_stream_destroy(s);
}
void test_stream_feed_overflow(void) {
uint8_t buf[32];
uint8_t payload[2] = {0x80, 0x01};
size_t len = build_frame(buf, 0xC8, CEL_CRSF_TYPE_HEARTBEAT, payload, 2);
cel_crsf_stream* s = cel_crsf_stream_create();
cel_crsf_frame frames[1];
/* Feed two frames but only have room for one */
size_t total = len * 2;
memcpy(buf + len, buf, len);
int n = cel_crsf_stream_feed(s, buf, total, frames, 1);
TEST_ASSERT_EQUAL_INT(1, n);
/* Second frame should still be in buffer */
n = cel_crsf_stream_feed(s, NULL, 0, frames, 1);
TEST_ASSERT_EQUAL_INT(1, n);
cel_crsf_stream_destroy(s);
}
void test_stream_reset(void) {
uint8_t buf[32];
uint8_t payload[2] = {0x80, 0x01};
size_t len = build_frame(buf, 0xC8, CEL_CRSF_TYPE_HEARTBEAT, payload, 2);
cel_crsf_stream* s = cel_crsf_stream_create();
/* Feed partial frame */
cel_crsf_frame frames[4];
cel_crsf_stream_feed(s, buf, 3, frames, 4);
/* Reset should discard partial */
cel_crsf_stream_reset(s);
/* Feed complete frame — should parse normally */
int n = cel_crsf_stream_feed(s, buf, len, frames, 4);
TEST_ASSERT_EQUAL_INT(1, n);
cel_crsf_stream_destroy(s);
}
int main(void) {
UNITY_BEGIN();
RUN_TEST(test_stream_create_destroy);
RUN_TEST(test_stream_feed_empty);
RUN_TEST(test_stream_feed_single_frame);
RUN_TEST(test_stream_feed_incremental);
RUN_TEST(test_stream_feed_multiple_frames);
RUN_TEST(test_stream_feed_skip_bad_sync);
RUN_TEST(test_stream_feed_discard_bad_crc);
RUN_TEST(test_stream_feed_overflow);
RUN_TEST(test_stream_reset);
return UNITY_END();
}
+151
View File
@@ -0,0 +1,151 @@
#include "unity.h"
#include "celrs/crsf.h"
#include <string.h>
void setUp(void) {}
void tearDown(void) {}
/* Helper: build a valid test frame with known CRC */
static size_t build_frame(uint8_t* dst, uint8_t addr, uint8_t type,
uint8_t const* payload, uint8_t payload_len) {
uint8_t length = 1 + payload_len + 1;
dst[0] = addr;
dst[1] = length;
dst[2] = type;
memcpy(dst + 3, payload, payload_len);
uint8_t crc = cel_crsf_crc(dst + 2, 1 + payload_len);
dst[2 + length - 1] = crc;
return 2 + length;
}
/* Telemetry parse tests */
void test_parse_link_stats(void) {
uint8_t buf[32];
uint8_t payload[10] = {
80, 70, 95, 0x7F, /* rssi1, rssi2, uplink_qual, snr(-127..127) */
0, 5, 20, 60, 90, 0x60 /* antenna, rf_mode, power, down_rssi, down_qual, snr */
};
build_frame(buf, 0x08, CEL_CRSF_TYPE_LINK_STATS, payload, 10);
cel_crsf_frame frame;
TEST_ASSERT_EQUAL_INT(0, cel_crsf_frame_parse(&frame, buf, sizeof(buf)));
cel_telemetry telem;
TEST_ASSERT_EQUAL_INT(0, cel_crsf_telemetry_parse(&frame, &telem));
TEST_ASSERT_EQUAL_UINT(CEL_TELEM_LINK, telem.type);
TEST_ASSERT_EQUAL_UINT8(80, telem.data.link.uplink_rssi1);
TEST_ASSERT_EQUAL_UINT8(70, telem.data.link.uplink_rssi2);
TEST_ASSERT_EQUAL_UINT8(95, telem.data.link.uplink_quality);
TEST_ASSERT_EQUAL_INT8(0x7F, telem.data.link.uplink_snr);
TEST_ASSERT_EQUAL_UINT8(0, telem.data.link.active_antenna);
TEST_ASSERT_EQUAL_UINT8(5, telem.data.link.rf_mode);
TEST_ASSERT_EQUAL_UINT8(20, telem.data.link.uplink_power);
TEST_ASSERT_EQUAL_UINT8(60, telem.data.link.downlink_rssi);
TEST_ASSERT_EQUAL_UINT8(90, telem.data.link.downlink_qual);
TEST_ASSERT_EQUAL_INT8(0x60, telem.data.link.downlink_snr);
}
void test_parse_link_stats_short(void) {
uint8_t buf[32];
uint8_t payload[5] = {80, 70, 95, 0x7F, 0};
build_frame(buf, 0x08, CEL_CRSF_TYPE_LINK_STATS, payload, 5);
cel_crsf_frame frame;
TEST_ASSERT_EQUAL_INT(0, cel_crsf_frame_parse(&frame, buf, sizeof(buf)));
cel_telemetry telem;
TEST_ASSERT_EQUAL_INT(-1, cel_crsf_telemetry_parse(&frame, &telem));
}
void test_parse_battery(void) {
uint8_t buf[32];
/* CRSF battery: voltage(u16 BE 0.1V), current(u16 BE 0.1A),
capacity(u24 BE mAh), remaining(u8 %) = 8 bytes */
uint8_t payload[8] = {
0x03, 0xE8, /* voltage: 0x03E8 = 1000 -> 100.0V (10S LiPo) */
0x00, 0x64, /* current: 0x0064 = 100 -> 10.0A */
0x00, 0x03, 0xE8, /* capacity: 0x0003E8 = 1000mAh */
0x64 /* remaining: 100% */
};
build_frame(buf, 0x08, CEL_CRSF_TYPE_BATTERY, payload, 8);
cel_crsf_frame frame;
TEST_ASSERT_EQUAL_INT(0, cel_crsf_frame_parse(&frame, buf, sizeof(buf)));
cel_telemetry telem;
TEST_ASSERT_EQUAL_INT(0, cel_crsf_telemetry_parse(&frame, &telem));
TEST_ASSERT_EQUAL_UINT(CEL_TELEM_BATTERY, telem.type);
TEST_ASSERT_EQUAL_UINT16(0x03E8, telem.data.battery.voltage_x10);
TEST_ASSERT_EQUAL_UINT16(0x0064, telem.data.battery.current_x10);
TEST_ASSERT_EQUAL_UINT32(0x0003E8, telem.data.battery.capacity_mah);
TEST_ASSERT_EQUAL_UINT8(0x64, telem.data.battery.remaining_pct);
}
void test_parse_heartbeat(void) {
uint8_t buf[32];
uint8_t payload[2] = {0x10, 0x80}; /* origin_addr LE = 0x8010 */
build_frame(buf, 0x10, CEL_CRSF_TYPE_HEARTBEAT, payload, 2);
cel_crsf_frame frame;
TEST_ASSERT_EQUAL_INT(0, cel_crsf_frame_parse(&frame, buf, sizeof(buf)));
cel_telemetry telem;
TEST_ASSERT_EQUAL_INT(0, cel_crsf_telemetry_parse(&frame, &telem));
TEST_ASSERT_EQUAL_UINT(CEL_TELEM_HEARTBEAT, telem.type);
TEST_ASSERT_EQUAL_UINT16(0x8010, telem.data.heartbeat.origin_addr);
}
void test_parse_airspeed(void) {
uint8_t buf[32];
uint8_t payload[2] = {0x00, 0x01}; /* speed LE = 0x0100 = 256 km/h */
build_frame(buf, 0x08, CEL_CRSF_TYPE_AIRSPEED, payload, 2);
cel_crsf_frame frame;
TEST_ASSERT_EQUAL_INT(0, cel_crsf_frame_parse(&frame, buf, sizeof(buf)));
cel_telemetry telem;
TEST_ASSERT_EQUAL_INT(0, cel_crsf_telemetry_parse(&frame, &telem));
TEST_ASSERT_EQUAL_UINT(CEL_TELEM_AIRSPEED, telem.type);
TEST_ASSERT_EQUAL_UINT16(256, telem.data.airspeed.speed_kmh);
}
void test_parse_unknown_type(void) {
uint8_t buf[32];
uint8_t payload[2] = {0x00, 0x01};
build_frame(buf, 0x08, 0xFF, payload, 2);
cel_crsf_frame frame;
TEST_ASSERT_EQUAL_INT(0, cel_crsf_frame_parse(&frame, buf, sizeof(buf)));
cel_telemetry telem;
TEST_ASSERT_EQUAL_INT(-1, cel_crsf_telemetry_parse(&frame, &telem));
}
void test_parse_null_args(void) {
cel_telemetry telem;
TEST_ASSERT_EQUAL_INT(-1, cel_crsf_telemetry_parse(NULL, &telem));
}
void test_parse_null_out(void) {
uint8_t buf[32];
uint8_t payload[2] = {0x10, 0x80};
build_frame(buf, 0x10, CEL_CRSF_TYPE_HEARTBEAT, payload, 2);
cel_crsf_frame frame;
TEST_ASSERT_EQUAL_INT(0, cel_crsf_frame_parse(&frame, buf, sizeof(buf)));
TEST_ASSERT_EQUAL_INT(-1, cel_crsf_telemetry_parse(&frame, NULL));
}
int main(void) {
UNITY_BEGIN();
RUN_TEST(test_parse_link_stats);
RUN_TEST(test_parse_link_stats_short);
RUN_TEST(test_parse_battery);
RUN_TEST(test_parse_heartbeat);
RUN_TEST(test_parse_airspeed);
RUN_TEST(test_parse_unknown_type);
RUN_TEST(test_parse_null_args);
RUN_TEST(test_parse_null_out);
return UNITY_END();
}
+78 -58
View File
@@ -1,72 +1,92 @@
#include "unity.h" #include "unity.h"
#include "celrs/serial.h" #include "celrs/serial.h"
#include "Mocklog_write.h" #include "Mockserial_internal.h"
#include <string.h>
void setUp(void) { Mocklog_write_Init(); } void setUp(void) {
void tearDown(void) { Mocklog_write_Verify(); Mocklog_write_Destroy(); } Mockserial_internal_Init();
}
void test_open_valid_path(void) { void tearDown(void) {
cel_serial_port* port = cel_serial_open("COM3", 400000); Mockserial_internal_Verify();
Mockserial_internal_Destroy();
}
/* cel_serial_find_elrs_port tests */
void test_find_elrs_port_null_out(void) {
TEST_ASSERT_EQUAL_INT(-1, cel_serial_find_elrs_port(NULL, 0));
}
void test_find_elrs_port_zero_size(void) {
char buf[1];
TEST_ASSERT_EQUAL_INT(-1, cel_serial_find_elrs_port(buf, 0));
}
void test_find_elrs_port_no_match(void) {
char buf[256];
cel_serial_platform_list_ports_ExpectAnyArgsAndReturn(0);
int rc = cel_serial_find_elrs_port(buf, sizeof(buf));
TEST_ASSERT_EQUAL_INT(-1, rc);
}
/* cel_serial_open_probe tests */
void test_open_probe_null_path(void) {
int bauds[] = {400000};
int actual;
TEST_ASSERT_NULL(cel_serial_open_probe(NULL, bauds, 1, &actual));
}
void test_open_probe_null_bauds(void) {
int actual;
TEST_ASSERT_NULL(cel_serial_open_probe("COM999", NULL, 1, &actual));
}
void test_open_probe_zero_count(void) {
int actual;
TEST_ASSERT_NULL(cel_serial_open_probe("COM999", NULL, 0, &actual));
}
void test_open_probe_out_baud_set(void) {
cel_serial_platform_open_ExpectAndReturn("COM999", 400000,
CEL_SERIAL_PLATFORM_INVALID_HANDLE);
int bauds[] = {400000};
int actual = 0;
cel_serial_port* port = cel_serial_open_probe("COM999", bauds, 1, &actual);
TEST_ASSERT_NULL(port);
}
void test_open_probe_success_sets_baud(void) {
cel_serial_platform_open_ExpectAndReturn("COM3", 921600,
(cel_serial_platform_handle)42);
cel_serial_platform_close_Expect((cel_serial_platform_handle)42);
int bauds[] = {921600, 400000};
int actual = 0;
cel_serial_port* port = cel_serial_open_probe("COM3", bauds, 2, &actual);
TEST_ASSERT_NOT_NULL(port); TEST_ASSERT_NOT_NULL(port);
TEST_ASSERT_EQUAL_INT(921600, actual);
cel_serial_close(port); cel_serial_close(port);
} }
void test_open_null_path(void) { void test_open_probe_null_out_baud(void) {
TEST_ASSERT_NULL(cel_serial_open(NULL, 400000)); cel_serial_platform_open_ExpectAndReturn("COM999", 400000,
} CEL_SERIAL_PLATFORM_INVALID_HANDLE);
int bauds[] = {400000};
void test_open_preserves_path(void) { TEST_ASSERT_NULL(cel_serial_open_probe("COM999", bauds, 1, NULL));
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) { int main(void) {
UNITY_BEGIN(); UNITY_BEGIN();
RUN_TEST(test_open_valid_path); RUN_TEST(test_find_elrs_port_null_out);
RUN_TEST(test_open_null_path); RUN_TEST(test_find_elrs_port_zero_size);
RUN_TEST(test_open_preserves_path); RUN_TEST(test_find_elrs_port_no_match);
RUN_TEST(test_close_null); RUN_TEST(test_open_probe_null_path);
RUN_TEST(test_read_returns_zero_stub); RUN_TEST(test_open_probe_null_bauds);
RUN_TEST(test_write_returns_zero_stub); RUN_TEST(test_open_probe_zero_count);
RUN_TEST(test_flush_no_crash); RUN_TEST(test_open_probe_out_baud_set);
RUN_TEST(test_flush_null); RUN_TEST(test_open_probe_success_sets_baud);
RUN_TEST(test_open_probe_null_out_baud);
return UNITY_END(); return UNITY_END();
} }
+4 -4
View File
@@ -1,4 +1,4 @@
add_executable(tool_telemetry telemetry.c) add_executable(telemetry telemetry.c)
target_include_directories(tool_telemetry PRIVATE "${CMAKE_SOURCE_DIR}") target_include_directories(telemetry PRIVATE "${CMAKE_SOURCE_DIR}")
target_compile_features(tool_telemetry PRIVATE c_std_23) target_compile_features(telemetry PRIVATE c_std_23)
target_link_libraries(tool_telemetry PRIVATE celrs_crsf celrs_serial celrs_logger celrs_log_write) target_link_libraries(telemetry PRIVATE cel::cel celserial_platform)
+526 -82
View File
@@ -1,112 +1,556 @@
#include "celrs/crsf.h" #include <signal.h>
#include "celrs/serial.h"
#include "celrs/logger.h"
#include <stdio.h> #include <stdio.h>
#include <stdlib.h> #include <stdlib.h>
#include <string.h> #include <string.h>
#include <time.h>
/* TX power index to dBm mapping (ELRS standard) */ #include "celrs/crsf.h"
static int const s_tx_power_dbm[] = { #include "celrs/crsf_telemetry.h"
0, 20, 26, 30, 32, 34, 36, 38, #include "celrs/logger.h"
0, 0, 0, 0, 0, 0, 0, 0 #include "celrs/serial.h"
};
/* Parse link telemetry payload (5 bytes) from CRSF frame type 0x02 */ #ifdef _WIN32
static int telemetry_parse_link(int16_t* rssi, uint8_t* link_quality, #include <windows.h>
int8_t* snr, int* tx_power_dbm, #else
uint8_t* rssi_rc, #include <unistd.h>
uint8_t const* payload, size_t len) { #endif
if (rssi == NULL || payload == NULL) return -1;
if (len < 5) return -1;
*rssi = (int16_t)payload[0]; /* 0-100% */ /* Probe bauds: CP210x chips on ELRS can't hit 921600 exactly. */
*link_quality = payload[1]; /* 0-100% */ static int const s_probe_bauds[] = {921600, 400000, 420000};
*snr = (int8_t)payload[2]; /* signed dB */ static int const s_probe_bauds_count =
uint8_t power_idx = payload[3]; (int)(sizeof(s_probe_bauds) / sizeof(s_probe_bauds[0]));
*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 volatile int s_running = 1;
#ifdef _WIN32
BOOL WINAPI handle_ctrl_c(DWORD dwCtrlType) {
if (dwCtrlType == CTRL_C_EVENT) {
s_running = 0;
return TRUE;
}
return FALSE;
} }
#else
static void handle_sigint(int sig) {
(void)sig;
s_running = 0;
}
#endif
static void setup_signal_handler(void) {
#ifdef _WIN32
SetConsoleCtrlHandler(handle_ctrl_c, TRUE);
#else
signal(SIGINT, handle_sigint);
#endif
}
static void sleep_ms(int ms) {
#ifdef _WIN32
Sleep((DWORD)ms);
#else
struct timespec ts = {.tv_sec = ms / 1000,
.tv_nsec = (ms % 1000) * 1000000L};
nanosleep(&ts, NULL);
#endif
}
/* Milliseconds elapsed since a clock() reading - used to time startup. */
static double elapsed_ms(clock_t start) {
return (double)(clock() - start) * 1000.0 / CLOCKS_PER_SEC;
}
/* --------------------------------------------------------------------------- */
/* ANSI helpers - all go to stdout so dashboard owns it */
/* --------------------------------------------------------------------------- */
static void ansi_reset(void) { printf("\033[0m"); }
static void ansi_red(void) { printf("\033[31m"); }
static void ansi_green(void) { printf("\033[32m"); }
static void ansi_yellow(void) { printf("\033[33m"); }
static void ansi_bold(void) { printf("\033[1m"); }
static void ansi_dim(void) { printf("\033[2m"); }
static void ansi_clear_line(void) { printf("\033[2K"); }
static void ansi_cursor_up(int n) {
char buf[16];
snprintf(buf, sizeof(buf), "\033[%dA", n);
printf("%s", buf);
}
/* --------------------------------------------------------------------------- */
/* Status helpers */
/* --------------------------------------------------------------------------- */
#define LINK_STALE_S 3.0f
#define FC_STALE_S 5.0f
typedef enum {
STATUS_LIVE,
STATUS_STALE,
STATUS_NO_LINK,
STATUS_NO_SIGNAL
} status_t;
static status_t compute_status(double now, double link_t, double fc_t,
int has_link, uint8_t up_lq) {
if (link_t == 0 || (now - link_t) > LINK_STALE_S)
return STATUS_NO_SIGNAL;
if (has_link && up_lq == 0)
return STATUS_NO_LINK;
if (fc_t > 0 && (now - fc_t) > FC_STALE_S)
return STATUS_STALE;
return STATUS_LIVE;
}
static void print_status_label(status_t s) {
switch (s) {
case STATUS_LIVE: ansi_green(); printf("LIVE "); break;
case STATUS_STALE: ansi_yellow(); printf("STALE "); break;
case STATUS_NO_LINK: ansi_red(); printf("NO LINK "); break;
case STATUS_NO_SIGNAL: ansi_red(); printf("NO SIGNAL"); break;
}
ansi_reset();
}
static void print_age(double now, double t) {
if (t == 0) { ansi_dim(); printf("waiting..."); ansi_reset(); return; }
double age = now - t;
ansi_dim();
printf(" (%.1fs)", age);
ansi_reset();
}
/* --------------------------------------------------------------------------- */
/* Dashboard state */
/* --------------------------------------------------------------------------- */
typedef struct {
/* Link */
int has_link;
uint8_t up_rssi1, up_rssi2;
uint8_t up_lq;
int8_t up_snr;
uint8_t power_idx;
uint8_t rf_mode;
double link_t;
/* Battery */
int has_batt;
float batt_v;
float batt_a;
uint16_t batt_mah;
uint8_t batt_pct;
double batt_t;
/* Flight mode */
int has_mode;
char mode_name[32];
double mode_t;
/* Attitude */
int has_att;
float att_pitch, att_roll, att_yaw;
double att_t;
/* FC timestamp (youngest of batt/mode/att) */
double fc_t;
/* Counts */
int rx_frames;
int unknown;
uint32_t type_counts[256]; /* indexed by raw CRSF frame type byte */
} dashboard_t;
static void dashboard_init(dashboard_t* d) {
memset(d, 0, sizeof(*d));
}
static void dashboard_update(dashboard_t* d, cel_telemetry const* telem,
double now) {
d->rx_frames++;
switch (telem->type) {
case CEL_TELEM_LINK:
d->has_link = 1;
d->up_rssi1 = telem->data.link.uplink_rssi1;
d->up_rssi2 = telem->data.link.uplink_rssi2;
d->up_lq = telem->data.link.uplink_quality;
d->up_snr = telem->data.link.uplink_snr;
d->power_idx = telem->data.link.uplink_power;
d->rf_mode = telem->data.link.rf_mode;
d->link_t = now;
break;
case CEL_TELEM_BATTERY:
d->has_batt = 1;
d->batt_v = telem->data.battery.voltage_x10 / 10.0f;
d->batt_a = telem->data.battery.current_x10 / 10.0f;
d->batt_mah = telem->data.battery.capacity_mah;
d->batt_pct = telem->data.battery.remaining_pct;
d->batt_t = now;
if (now > d->fc_t) d->fc_t = now;
break;
case CEL_TELEM_FLIGHT_MODE:
d->has_mode = 1;
size_t copy_len = sizeof(telem->data.flight_mode.name) < sizeof(d->mode_name)
? sizeof(telem->data.flight_mode.name) : sizeof(d->mode_name);
memcpy(d->mode_name, telem->data.flight_mode.name, copy_len);
d->mode_name[copy_len - 1] = 0;
d->mode_t = now;
if (now > d->fc_t) d->fc_t = now;
break;
case CEL_TELEM_ATTITUDE:
d->has_att = 1;
d->att_pitch = telem->data.attitude.pitch_rad;
d->att_roll = telem->data.attitude.roll_rad;
d->att_yaw = telem->data.attitude.yaw_rad;
d->att_t = now;
if (now > d->fc_t) d->fc_t = now;
break;
default:
d->unknown++;
break;
}
}
/* Uplink power index -> label */
static char const* power_label(uint8_t idx) {
static char const* labels[] = {
"0 mW", "10 mW", "25 mW", "100 mW",
"500 mW", "1000 mW", "2000 mW", "250 mW", "50 mW"
};
return idx < 9 ? labels[idx] : "?";
}
/* Color style for LQ percentage */
static void lq_color(uint8_t lq) {
if (lq >= 80) ansi_green();
else if (lq >= 40) ansi_yellow();
else ansi_red();
}
/* Color style for RSSI dBm (lower = stronger) */
static void rssi_color(double dbm) {
if (dbm <= 80) ansi_green();
else if (dbm <= 105) ansi_yellow();
else ansi_red();
}
/* Raw CRSF frame type byte -> name, for diagnostics.
Returns NULL for types not in cel_crsf_type. */
static char const* crsf_type_name(uint8_t type) {
switch (type) {
case CEL_CRSF_TYPE_GPS: return "GPS";
case CEL_CRSF_TYPE_VARIO: return "VARIO";
case CEL_CRSF_TYPE_BATTERY: return "BATTERY";
case CEL_CRSF_TYPE_BARO_ALT: return "BARO_ALT";
case CEL_CRSF_TYPE_AIRSPEED: return "AIRSPEED";
case CEL_CRSF_TYPE_HEARTBEAT: return "HEARTBEAT";
case CEL_CRSF_TYPE_RPM: return "RPM";
case CEL_CRSF_TYPE_TEMP: return "TEMP";
case CEL_CRSF_TYPE_VOLTAGES: return "VOLTAGES";
case CEL_CRSF_TYPE_ESC_SENSOR: return "ESC_SENSOR";
case CEL_CRSF_TYPE_LINK_STATS: return "LINK_STATS";
case CEL_CRSF_TYPE_RC_CHANNELS: return "RC_CHANNELS";
case CEL_CRSF_TYPE_ATTITUDE: return "ATTITUDE";
case CEL_CRSF_TYPE_FLIGHT_MODE: return "FLIGHT_MODE";
case CEL_CRSF_TYPE_DEVICE_PING: return "DEVICE_PING";
case CEL_CRSF_TYPE_DEVICE_INFO: return "DEVICE_INFO";
case CEL_CRSF_TYPE_PARAM_ENTRY: return "PARAM_ENTRY";
case CEL_CRSF_TYPE_PARAM_READ: return "PARAM_READ";
case CEL_CRSF_TYPE_PARAM_WRITE: return "PARAM_WRITE";
case CEL_CRSF_TYPE_ELRS_STATUS: return "ELRS_STATUS";
default: return NULL;
}
}
/* --------------------------------------------------------------------------- */
/* Dashboard render - tracks line count for in-place redraw */
/* --------------------------------------------------------------------------- */
static void render_dashboard(dashboard_t const* d,
char const* port, int baud,
double elapsed, int* lines) {
double now = (double)time(NULL);
status_t status = compute_status(now, d->link_t, d->fc_t,
d->has_link, d->up_lq);
/* Return to top of dashboard if already drawn */
if (*lines > 0) {
ansi_cursor_up(*lines);
}
int n = 0;
/* Title */
ansi_bold();
printf("CRSF Telemetry");
ansi_reset();
printf(" %s @ %d baud up %.0fs\n",
port, baud, elapsed);
n++;
/* Status */
print_status_label(status);
printf("\n");
n++;
/* LINK */
ansi_bold(); printf(" LINK "); ansi_reset();
if (d->has_link) {
double r1_dbm = d->up_rssi1 / 2.0;
double r2_dbm = d->up_rssi2 / 2.0;
rssi_color(r1_dbm);
printf("RSSI1 -%.1fdBm ", r1_dbm);
ansi_reset();
rssi_color(r2_dbm);
printf("RSSI2 -%.1fdBm ", r2_dbm);
ansi_reset();
lq_color(d->up_lq);
printf("LQ %d%% ", d->up_lq);
ansi_reset();
printf("SNR %+ddB ", d->up_snr);
printf("Pwr=%s Mode=%d", power_label(d->power_idx), d->rf_mode);
print_age(now, d->link_t);
} else {
ansi_dim(); printf("waiting..."); ansi_reset();
}
printf("\n");
n++;
/* BATT */
ansi_bold(); printf(" BATT "); ansi_reset();
if (d->has_batt) {
if (d->batt_v > 3.0f) ansi_green(); else ansi_red();
printf("%.2fV ", d->batt_v);
ansi_reset();
printf("%.1fA %umah %d%%", d->batt_a, d->batt_mah, d->batt_pct);
print_age(now, d->batt_t);
} else {
ansi_dim(); printf("waiting..."); ansi_reset();
}
printf("\n");
n++;
/* IMU */
ansi_bold(); printf(" IMU "); ansi_reset();
if (d->has_att) {
printf("pitch %+7.1f roll %+7.1f yaw %+7.1f deg",
d->att_pitch * 57.2958f,
d->att_roll * 57.2958f,
d->att_yaw * 57.2958f);
print_age(now, d->att_t);
} else {
ansi_dim(); printf("waiting..."); ansi_reset();
}
printf("\n");
n++;
/* MODE */
ansi_bold(); printf(" MODE "); ansi_reset();
if (d->has_mode) {
ansi_green(); printf("%s", d->mode_name); ansi_reset();
print_age(now, d->mode_t);
} else {
ansi_dim(); printf("waiting..."); ansi_reset();
}
printf("\n");
n++;
/* Footer */
ansi_dim();
printf(" rx=%d ignored=%d", d->rx_frames, d->unknown);
ansi_reset();
printf("\n");
n++;
/* Frame type breakdown (top 6 by count) */
ansi_clear_line();
ansi_dim();
printf(" types:");
uint8_t used[256] = {0};
int shown = 0;
for (int k = 0; k < 6; k++) {
int best = -1;
uint32_t best_count = 0;
for (int t = 0; t < 256; t++) {
if (!used[t] && d->type_counts[t] > best_count) {
best_count = d->type_counts[t];
best = t;
}
}
if (best < 0) break;
used[best] = 1;
char const* name = crsf_type_name((uint8_t)best);
if (name != NULL) {
printf(" %s=%u", name, best_count);
} else {
printf(" 0x%02X=%u", best, best_count);
}
shown++;
}
if (shown == 0) printf(" -");
ansi_reset();
printf("\n");
n++;
*lines = n;
fflush(stdout);
}
/* --------------------------------------------------------------------------- */
/* Usage / port listing */
/* --------------------------------------------------------------------------- */
static void print_usage(char const* prog) { static void print_usage(char const* prog) {
printf("Usage: %s <serial_port> [interval_ms]\n", prog); printf("Usage: %s [--port <serial_port>] [--baudrate <rate>]\n", prog);
printf(" serial_port : COM3 (Windows) or /dev/ttyUSB0 (Linux)\n"); printf(" %s --list\n", prog);
printf(" interval_ms : poll interval in ms (default 200)\n"); printf(" --port <serial_port> : COM3 (Windows) or /dev/ttyUSB0 (Linux)\n");
printf(" --baudrate <rate> : baud rate (default: auto-probe)\n");
printf(" --list : list available serial ports and exit\n");
printf("\nIf --port is omitted, an ELRS-like port is auto-detected.\n");
} }
int main(int argc, char const* argv[]) { static int list_ports(void) {
if (argc < 2) { char** ports = NULL;
print_usage(argv[0]); int count = cel_serial_list_ports(&ports, 0);
if (count < 0) {
cel_log_err("Failed to list serial ports");
return 1; return 1;
} }
char const* port_path = argv[1]; for (int i = 0; i < count; i++) {
int interval_ms = 200; printf("%s\n", ports[i]);
if (argc >= 3) {
interval_ms = atoi(argv[2]);
if (interval_ms <= 0) interval_ms = 200;
} }
/* Open serial port */ cel_serial_free_ports(ports, count);
cel_serial_port* port = cel_serial_open(port_path, 400000); return 0;
}
int main(int argc, char const* argv[]) {
char const* port_path = NULL;
int baud_rate = 0; /* 0 = auto-probe */
char msg[256];
clock_t t_startup = clock();
for (int i = 1; i < argc; i++) {
if (strcmp(argv[i], "--list") == 0) {
return list_ports();
} else if (strcmp(argv[i], "--port") == 0 && i + 1 < argc) {
port_path = argv[++i];
} else if (strcmp(argv[i], "--baudrate") == 0 && i + 1 < argc) {
baud_rate = atoi(argv[++i]);
if (baud_rate <= 0) baud_rate = 400000;
}
}
/* Auto-detect port if not specified */
if (port_path == NULL) {
clock_t t = clock();
char detected[256];
if (cel_serial_find_elrs_port(detected, sizeof(detected)) == 0) {
port_path = detected;
snprintf(msg, sizeof(msg), "Auto-detected ELRS port (%.0f ms)",
elapsed_ms(t));
cel_log_info(msg);
} else {
cel_log_err("No ELRS-like port found. Use --list to see ports.");
return 1;
}
}
setup_signal_handler();
/* Open serial port with baud probing */
cel_serial_port* port = NULL;
int actual_baud = 0;
if (baud_rate > 0) {
port = cel_serial_open(port_path, baud_rate);
actual_baud = baud_rate;
} else {
port = cel_serial_open_probe(port_path, s_probe_bauds,
s_probe_bauds_count, &actual_baud);
}
if (port == NULL) { if (port == NULL) {
cel_log_err("Failed to open serial port"); cel_log_err("Failed to open serial port");
return 1; return 1;
} }
char msg[256]; snprintf(msg, sizeof(msg), "Connected to %s (%d baud) in %.0f ms",
snprintf(msg, sizeof(msg), "Connected to %s (400000 baud)", port_path); port_path, actual_baud, elapsed_ms(t_startup));
cel_log_info(msg); cel_log_info(msg);
/* Send heartbeat to establish CRSF link */ /* Create CRSF stream for incremental parsing */
uint8_t hb_payload[2] = {CEL_CRSF_ADDRESS_TBS_GROUND_STATION, 0x01}; cel_crsf_stream* stream = cel_crsf_stream_create();
uint8_t hb_frame[256]; if (stream == NULL) {
size_t hb_len = cel_crsf_frame_build(hb_frame, CEL_CRSF_ADDRESS_FC_BROADCAST, cel_log_err("Failed to create CRSF stream");
CEL_CRSF_ADDRESS_TBS_GROUND_STATION, cel_serial_close(port);
CEL_CRSF_FRAMETYPE_HEARTBEAT, hb_payload, 2); return 1;
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); /* Send initial RC frame to establish link */
cel_log_info(msg); int16_t channels[16];
cel_crsf_channel_default(channels);
uint8_t rc_buf[32];
size_t rc_len = cel_crsf_build_rc_frame(rc_buf, channels);
cel_serial_write(port, rc_buf, rc_len);
/* Telemetry read loop - 20 ms = 50 Hz RC send rate */
dashboard_t dash;
dashboard_init(&dash);
uint8_t read_buf[256];
time_t t_start = time(NULL);
int rc_count = 0;
int d_lines = 0; /* dashboard line count for cursor tracking */
while (s_running) {
/* Read available data */
size_t bytes = cel_serial_read(port, read_buf, sizeof(read_buf));
if (bytes > 0) {
cel_crsf_frame frames[4];
int n = cel_crsf_stream_feed(stream, read_buf, bytes, frames, 4);
double now = (double)time(NULL);
for (int i = 0; i < n; i++) {
dash.type_counts[frames[i].type]++;
cel_telemetry telem;
if (cel_crsf_telemetry_parse(&frames[i], &telem) == 0) {
dashboard_update(&dash, &telem, now);
} else {
dash.unknown++;
}
}
}
/* Send RC frame every 20 ms (50 Hz) to keep link alive */
rc_count++;
if (rc_count % 1 == 0) {
rc_len = cel_crsf_build_rc_frame(rc_buf, channels);
cel_serial_write(port, rc_buf, rc_len);
}
/* Send DEVICE_PING every 5s so DEVICE_INFO replies keep showing up
* in the dashboard's type_counts, like the Python ping_loop. */
if (rc_count % 250 == 0) {
uint8_t ping_buf[8];
size_t ping_len = cel_crsf_build_ping_frame(ping_buf);
cel_serial_write(port, ping_buf, ping_len);
}
/* Redraw dashboard every 100 ms */
if (rc_count % 5 == 0) {
double elapsed = difftime(time(NULL), t_start);
render_dashboard(&dash, port_path, actual_baud, elapsed, &d_lines);
}
sleep_ms(20);
}
/* Move cursor past dashboard before shutdown message */
if (d_lines > 0) {
printf("\n");
}
cel_log_info("Shutting down...");
cel_crsf_stream_destroy(stream);
cel_serial_close(port); cel_serial_close(port);
return 0; return 0;
} }