Compare commits
25 Commits
c42ec407da
...
trunk
| Author | SHA1 | Date | |
|---|---|---|---|
| e4f4ca722d | |||
| 37b3d333cd | |||
| 974b33e827 | |||
| 412530df83 | |||
| 8ff2542fbc | |||
| df09615d3f | |||
| f58eb0d976 | |||
| d67d9b29d2 | |||
| ef5012b9d4 | |||
| 787a303cf5 | |||
| f1e4e1b61d | |||
| 1db5fdb374 | |||
| d2331229eb | |||
| 97c83aa460 | |||
| 7b3905084e | |||
| 8c4045e2a4 | |||
| 5d18258330 | |||
| eaaaf710a2 | |||
| 4f0c62d41a | |||
| 34dd25fecb | |||
| b97a7c5b3a | |||
| 8b181d0fcd | |||
| a846b063f9 | |||
| dde27ab566 | |||
| df3d399610 |
@@ -105,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 (see previous tags for style reference):
|
||||||
|
```sh
|
||||||
|
cat > RELEASES.md << 'EOF'
|
||||||
|
# Releases
|
||||||
|
|
||||||
|
## 0.2.0 (2026-01-01)
|
||||||
|
|
||||||
|
Short summary.
|
||||||
|
|
||||||
|
### Library (`celrs`)
|
||||||
|
|
||||||
|
- **Feature**: description.
|
||||||
|
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
|
||||||
@@ -131,8 +170,11 @@ 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..)
|
||||||
|
crsf_stream.h/.c Incremental streaming frame reader
|
||||||
|
crsf_param.h/.c Parameter protocol (read/write/set_power)
|
||||||
|
serial.h / serial.c Serial port abstraction (Win/POSIX)
|
||||||
logger.h / logger.c Level-filtering logger
|
logger.h / logger.c Level-filtering logger
|
||||||
log_write.h/.c stdout log sink
|
log_write.h/.c stdout log sink
|
||||||
tools/
|
tools/
|
||||||
@@ -239,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
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
@@ -74,7 +71,10 @@ mock files are excluded. Requires GCC or Clang with gcov support, and
|
|||||||
|
|
||||||
```
|
```
|
||||||
celrs/
|
celrs/
|
||||||
crsf.h / crsf.c CRSF protocol: CRC8, frame parse/build
|
crsf.h / crsf.c CRSF protocol: CRC8/DVB-S2, frame build/parse
|
||||||
|
crsf_telemetry.h/.c Telemetry frame decoders (GPS, battery, link..)
|
||||||
|
crsf_stream.h/.c Incremental streaming frame reader
|
||||||
|
crsf_param.h/.c Parameter protocol (read/write/set_power)
|
||||||
serial.h / serial.c Serial port abstraction (Win/POSIX)
|
serial.h / serial.c Serial port abstraction (Win/POSIX)
|
||||||
logger.h / logger.c Level-filtering logger
|
logger.h / logger.c Level-filtering logger
|
||||||
log_write.h/.c stdout log sink
|
log_write.h/.c stdout log sink
|
||||||
@@ -94,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
|
||||||
|
|
||||||
@@ -118,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 |
|
||||||
|
|||||||
+30
-19
@@ -1,30 +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)
|
||||||
|
|
||||||
# Platform-agnostic serial logic — calls cel_serial_platform_*();
|
# Platform-agnostic serial logic — calls cel_serial_platform_*();
|
||||||
# symbol resolved by celrs_serial_platform (or a mock in tests)
|
# symbol resolved by celserial_platform (or a mock in tests)
|
||||||
add_library(celrs_serial STATIC serial.c)
|
add_library(celserial STATIC serial.c)
|
||||||
target_include_directories(celrs_serial PUBLIC "${CMAKE_SOURCE_DIR}")
|
target_include_directories(celserial PUBLIC "${CMAKE_SOURCE_DIR}")
|
||||||
target_compile_features(celrs_serial PRIVATE c_std_23)
|
target_compile_features(celserial PRIVATE c_std_23)
|
||||||
|
add_library(cel::serial ALIAS celserial)
|
||||||
|
|
||||||
# Real platform backend — linked into production binaries only
|
# Real platform backend — linked into production binaries only
|
||||||
add_library(celrs_serial_platform STATIC)
|
add_library(celserial_platform STATIC)
|
||||||
target_include_directories(celrs_serial_platform PUBLIC "${CMAKE_SOURCE_DIR}")
|
target_include_directories(celserial_platform PUBLIC "${CMAKE_SOURCE_DIR}")
|
||||||
target_compile_features(celrs_serial_platform PRIVATE c_std_23)
|
target_compile_features(celserial_platform PRIVATE c_std_23)
|
||||||
if (IS_WINDOWS)
|
if (IS_WINDOWS)
|
||||||
target_sources(celrs_serial_platform PRIVATE platform/serial_win.c)
|
target_sources(celserial_platform PRIVATE platform/serial_win.c)
|
||||||
target_link_libraries(celrs_serial_platform PRIVATE advapi32)
|
target_link_libraries(celserial_platform PRIVATE advapi32 setupapi)
|
||||||
elseif(IS_LINUX OR IS_MACOS)
|
elseif(IS_LINUX OR IS_MACOS)
|
||||||
target_sources(celrs_serial_platform PRIVATE platform/serial_posix.c)
|
target_sources(celserial_platform PRIVATE platform/serial_posix.c)
|
||||||
endif()
|
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
@@ -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
@@ -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"
|
||||||
|
|||||||
@@ -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, ¶m, timeout_sec) != 0) {
|
||||||
|
/* No response = end of parameter list */
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Check if this is the power parameter */
|
||||||
|
if (is_power_param(¶m) && param.type == CEL_PARAM_TEXT_SELECT) {
|
||||||
|
/* Find matching option */
|
||||||
|
int opt_index = match_power_option(¶m, 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;
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,3 +28,9 @@ void cel_serial_platform_flush(cel_serial_platform_handle handle);
|
|||||||
* Returns the number of ports found, or -1 on error.
|
* Returns the number of ports found, or -1 on error.
|
||||||
* out_ports must be freed with cel_serial_free_ports(). */
|
* out_ports must be freed with cel_serial_free_ports(). */
|
||||||
int cel_serial_platform_list_ports(char*** out_ports, int max_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);
|
||||||
|
|||||||
@@ -34,3 +34,34 @@ int cel_serial_platform_list_ports(char*** out_ports, int max_ports) {
|
|||||||
*out_ports = ports;
|
*out_ports = ports;
|
||||||
return count;
|
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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
#include "celrs/platform/serial_internal.h"
|
#include "celrs/platform/serial_internal.h"
|
||||||
|
|
||||||
#include <windows.h>
|
#include <windows.h>
|
||||||
|
#include <setupapi.h>
|
||||||
|
#include <initguid.h>
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
@@ -25,6 +27,13 @@ cel_serial_platform_handle cel_serial_platform_open(char const* path, int baud_r
|
|||||||
dcb.Parity = NOPARITY;
|
dcb.Parity = NOPARITY;
|
||||||
dcb.StopBits = ONESTOPBIT;
|
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)) {
|
if (!SetCommState(h, &dcb)) {
|
||||||
CloseHandle(h);
|
CloseHandle(h);
|
||||||
return CEL_SERIAL_PLATFORM_INVALID_HANDLE;
|
return CEL_SERIAL_PLATFORM_INVALID_HANDLE;
|
||||||
@@ -97,3 +106,36 @@ int cel_serial_platform_list_ports(char*** out_ports, int max_ports) {
|
|||||||
*out_ports = ports;
|
*out_ports = ports;
|
||||||
return count;
|
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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -68,3 +68,49 @@ void cel_serial_free_ports(char** ports, int count) {
|
|||||||
}
|
}
|
||||||
free(ports);
|
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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -32,3 +32,17 @@ int cel_serial_list_ports(char*** out_ports, int max_ports);
|
|||||||
|
|
||||||
/* Free the port list returned by cel_serial_list_ports */
|
/* Free the port list returned by cel_serial_list_ports */
|
||||||
void cel_serial_free_ports(char** ports, int count);
|
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);
|
||||||
|
|||||||
+29
-3
@@ -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,15 +34,40 @@ 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)
|
||||||
|
|
||||||
|
# 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)
|
# 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/platform/serial_internal.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)
|
||||||
@@ -50,7 +76,7 @@ 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)
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
:cmock:
|
||||||
|
:mock_prefix: Mock
|
||||||
|
:when_no_prototypes: nothing
|
||||||
|
:weak: __attribute__((weak))
|
||||||
|
:plugins:
|
||||||
|
- :ignore_arg
|
||||||
|
- :expect_any_args
|
||||||
|
- :callback
|
||||||
+213
-50
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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(¶m, 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(¶m, 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(¶m, 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(¶m, 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(¶m, 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(¶m, 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(¶m, 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(¶m, 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, ¶m, 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, ¶m, 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, ¶m, 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(¶m, 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();
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
+77
-128
@@ -1,143 +1,92 @@
|
|||||||
#include <stdlib.h>
|
|
||||||
|
|
||||||
#include "unity.h"
|
#include "unity.h"
|
||||||
#include "celrs/serial.h"
|
#include "celrs/serial.h"
|
||||||
#include "Mockserial_internal.h"
|
#include "Mockserial_internal.h"
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
void setUp(void) { Mockserial_internal_Init(); }
|
void setUp(void) {
|
||||||
void tearDown(void) { Mockserial_internal_Verify(); Mockserial_internal_Destroy(); }
|
Mockserial_internal_Init();
|
||||||
|
}
|
||||||
|
|
||||||
void test_open_valid_path(void) {
|
void tearDown(void) {
|
||||||
cel_serial_platform_open_ExpectAndReturn("COM3", 400000, 1);
|
Mockserial_internal_Verify();
|
||||||
cel_serial_port* port = cel_serial_open("COM3", 400000);
|
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_platform_close_Expect(1);
|
|
||||||
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_platform_failure_returns_null(void) {
|
TEST_ASSERT_NULL(cel_serial_open_probe("COM999", bauds, 1, NULL));
|
||||||
cel_serial_platform_open_ExpectAndReturn("COM3", 400000, CEL_SERIAL_PLATFORM_INVALID_HANDLE);
|
|
||||||
TEST_ASSERT_NULL(cel_serial_open("COM3", 400000));
|
|
||||||
}
|
|
||||||
|
|
||||||
void test_open_preserves_path(void) {
|
|
||||||
cel_serial_platform_open_ExpectAndReturn("/dev/ttyUSB0", 400000, 1);
|
|
||||||
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_platform_close_Expect(1);
|
|
||||||
cel_serial_close(port);
|
|
||||||
}
|
|
||||||
|
|
||||||
void test_close_null(void) {
|
|
||||||
/* Should not crash */
|
|
||||||
cel_serial_close(NULL);
|
|
||||||
}
|
|
||||||
|
|
||||||
void test_read_delegates_to_platform(void) {
|
|
||||||
cel_serial_platform_open_ExpectAndReturn("COM3", 400000, 1);
|
|
||||||
cel_serial_port* port = cel_serial_open("COM3", 400000);
|
|
||||||
TEST_ASSERT_NOT_NULL(port);
|
|
||||||
|
|
||||||
uint8_t buf[16];
|
|
||||||
cel_serial_platform_read_ExpectAndReturn(1, buf, sizeof(buf), 4);
|
|
||||||
size_t n = cel_serial_read(port, buf, sizeof(buf));
|
|
||||||
TEST_ASSERT_EQUAL_UINT(4, n);
|
|
||||||
|
|
||||||
cel_serial_platform_close_Expect(1);
|
|
||||||
cel_serial_close(port);
|
|
||||||
}
|
|
||||||
|
|
||||||
void test_read_null_port_returns_zero(void) {
|
|
||||||
uint8_t buf[16];
|
|
||||||
TEST_ASSERT_EQUAL_UINT(0, cel_serial_read(NULL, buf, sizeof(buf)));
|
|
||||||
}
|
|
||||||
|
|
||||||
void test_write_delegates_to_platform(void) {
|
|
||||||
cel_serial_platform_open_ExpectAndReturn("COM3", 400000, 1);
|
|
||||||
cel_serial_port* port = cel_serial_open("COM3", 400000);
|
|
||||||
TEST_ASSERT_NOT_NULL(port);
|
|
||||||
|
|
||||||
uint8_t buf[4] = {0xC8, 0x10, 0x80, 0x03};
|
|
||||||
cel_serial_platform_write_ExpectAndReturn(1, buf, sizeof(buf), sizeof(buf));
|
|
||||||
size_t n = cel_serial_write(port, buf, sizeof(buf));
|
|
||||||
TEST_ASSERT_EQUAL_UINT(sizeof(buf), n);
|
|
||||||
|
|
||||||
cel_serial_platform_close_Expect(1);
|
|
||||||
cel_serial_close(port);
|
|
||||||
}
|
|
||||||
|
|
||||||
void test_write_null_port_returns_zero(void) {
|
|
||||||
uint8_t buf[4] = {0xC8, 0x10, 0x80, 0x03};
|
|
||||||
TEST_ASSERT_EQUAL_UINT(0, cel_serial_write(NULL, buf, sizeof(buf)));
|
|
||||||
}
|
|
||||||
|
|
||||||
void test_flush_delegates_to_platform(void) {
|
|
||||||
cel_serial_platform_open_ExpectAndReturn("COM3", 400000, 1);
|
|
||||||
cel_serial_port* port = cel_serial_open("COM3", 400000);
|
|
||||||
TEST_ASSERT_NOT_NULL(port);
|
|
||||||
|
|
||||||
cel_serial_platform_flush_Expect(1);
|
|
||||||
cel_serial_flush(port);
|
|
||||||
|
|
||||||
cel_serial_platform_close_Expect(1);
|
|
||||||
cel_serial_close(port);
|
|
||||||
}
|
|
||||||
|
|
||||||
void test_flush_null(void) {
|
|
||||||
cel_serial_flush(NULL); /* should not crash */
|
|
||||||
}
|
|
||||||
|
|
||||||
void test_list_ports_null_out(void) {
|
|
||||||
TEST_ASSERT_EQUAL_INT(-1, cel_serial_list_ports(NULL, 16));
|
|
||||||
}
|
|
||||||
|
|
||||||
void test_list_ports_passes_max_ports_through(void) {
|
|
||||||
char** ports = NULL;
|
|
||||||
cel_serial_platform_list_ports_ExpectAndReturn(&ports, 16, 2);
|
|
||||||
int count = cel_serial_list_ports(&ports, 16);
|
|
||||||
TEST_ASSERT_EQUAL_INT(2, count);
|
|
||||||
}
|
|
||||||
|
|
||||||
void test_list_ports_zero_max_uses_default(void) {
|
|
||||||
char** ports = NULL;
|
|
||||||
cel_serial_platform_list_ports_ExpectAndReturn(&ports, 64, 0);
|
|
||||||
int count = cel_serial_list_ports(&ports, 0);
|
|
||||||
TEST_ASSERT_EQUAL_INT(0, count);
|
|
||||||
}
|
|
||||||
|
|
||||||
void test_free_ports_null(void) {
|
|
||||||
cel_serial_free_ports(NULL, 0); /* should not crash */
|
|
||||||
}
|
|
||||||
|
|
||||||
void test_free_ports_zero_count(void) {
|
|
||||||
char** empty = (char**)calloc(1, sizeof(char*));
|
|
||||||
cel_serial_free_ports(empty, 0); /* 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_platform_failure_returns_null);
|
RUN_TEST(test_find_elrs_port_no_match);
|
||||||
RUN_TEST(test_open_preserves_path);
|
RUN_TEST(test_open_probe_null_path);
|
||||||
RUN_TEST(test_close_null);
|
RUN_TEST(test_open_probe_null_bauds);
|
||||||
RUN_TEST(test_read_delegates_to_platform);
|
RUN_TEST(test_open_probe_zero_count);
|
||||||
RUN_TEST(test_read_null_port_returns_zero);
|
RUN_TEST(test_open_probe_out_baud_set);
|
||||||
RUN_TEST(test_write_delegates_to_platform);
|
RUN_TEST(test_open_probe_success_sets_baud);
|
||||||
RUN_TEST(test_write_null_port_returns_zero);
|
RUN_TEST(test_open_probe_null_out_baud);
|
||||||
RUN_TEST(test_flush_delegates_to_platform);
|
|
||||||
RUN_TEST(test_flush_null);
|
|
||||||
RUN_TEST(test_list_ports_null_out);
|
|
||||||
RUN_TEST(test_list_ports_passes_max_ports_through);
|
|
||||||
RUN_TEST(test_list_ports_zero_max_uses_default);
|
|
||||||
RUN_TEST(test_free_ports_null);
|
|
||||||
RUN_TEST(test_free_ports_zero_count);
|
|
||||||
return UNITY_END();
|
return UNITY_END();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
add_executable(telemetry telemetry.c)
|
add_executable(telemetry telemetry.c)
|
||||||
target_include_directories(telemetry PRIVATE "${CMAKE_SOURCE_DIR}")
|
target_include_directories(telemetry PRIVATE "${CMAKE_SOURCE_DIR}")
|
||||||
target_compile_features(telemetry PRIVATE c_std_23)
|
target_compile_features(telemetry PRIVATE c_std_23)
|
||||||
target_link_libraries(telemetry PRIVATE celrs_crsf celrs_serial celrs_serial_platform celrs_logger celrs_log_write)
|
target_link_libraries(telemetry PRIVATE cel::cel celserial_platform)
|
||||||
|
|||||||
+477
-81
@@ -1,58 +1,413 @@
|
|||||||
#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>
|
||||||
|
|
||||||
|
#include "celrs/crsf.h"
|
||||||
|
#include "celrs/crsf_telemetry.h"
|
||||||
|
#include "celrs/logger.h"
|
||||||
|
#include "celrs/serial.h"
|
||||||
|
|
||||||
#ifdef _WIN32
|
#ifdef _WIN32
|
||||||
#include <windows.h>
|
#include <windows.h>
|
||||||
#else
|
#else
|
||||||
#include <time.h>
|
#include <unistd.h>
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
/* TX power index to dBm mapping (ELRS standard) */
|
/* Probe bauds: CP210x chips on ELRS can't hit 921600 exactly. */
|
||||||
static int const s_tx_power_dbm[] = {
|
static int const s_probe_bauds[] = {921600, 400000, 420000};
|
||||||
0, 20, 26, 30, 32, 34, 36, 38,
|
static int const s_probe_bauds_count =
|
||||||
0, 0, 0, 0, 0, 0, 0, 0
|
(int)(sizeof(s_probe_bauds) / sizeof(s_probe_bauds[0]));
|
||||||
};
|
|
||||||
|
|
||||||
/* Parse link telemetry payload (5 bytes) from CRSF frame type 0x02 */
|
static volatile int s_running = 1;
|
||||||
static int telemetry_parse_link(int16_t* rssi, uint8_t* link_quality,
|
|
||||||
int8_t* snr, int* tx_power_dbm,
|
|
||||||
uint8_t* rssi_rc,
|
|
||||||
uint8_t const* payload, size_t len) {
|
|
||||||
if (rssi == NULL || payload == NULL) return -1;
|
|
||||||
if (len < 5) return -1;
|
|
||||||
|
|
||||||
*rssi = (int16_t)payload[0]; /* 0-100% */
|
#ifdef _WIN32
|
||||||
*link_quality = payload[1]; /* 0-100% */
|
BOOL WINAPI handle_ctrl_c(DWORD dwCtrlType) {
|
||||||
*snr = (int8_t)payload[2]; /* signed dB */
|
if (dwCtrlType == CTRL_C_EVENT) {
|
||||||
uint8_t power_idx = payload[3];
|
s_running = 0;
|
||||||
*tx_power_dbm = (power_idx < sizeof(s_tx_power_dbm) / sizeof(s_tx_power_dbm[0]))
|
return TRUE;
|
||||||
? s_tx_power_dbm[power_idx]
|
}
|
||||||
: 0;
|
return FALSE;
|
||||||
*rssi_rc = payload[4]; /* 0-100% */
|
}
|
||||||
|
#else
|
||||||
|
static void handle_sigint(int sig) {
|
||||||
|
(void)sig;
|
||||||
|
s_running = 0;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
return 0;
|
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) {
|
static void sleep_ms(int ms) {
|
||||||
#ifdef _WIN32
|
#ifdef _WIN32
|
||||||
Sleep((DWORD)ms);
|
Sleep((DWORD)ms);
|
||||||
#else
|
#else
|
||||||
struct timespec ts = {.tv_sec = ms / 1000, .tv_nsec = (ms % 1000) * 1000000L};
|
struct timespec ts = {.tv_sec = ms / 1000,
|
||||||
|
.tv_nsec = (ms % 1000) * 1000000L};
|
||||||
nanosleep(&ts, NULL);
|
nanosleep(&ts, NULL);
|
||||||
#endif
|
#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 --port <serial_port> [--baudrate <rate>] [interval_ms]\n", prog);
|
printf("Usage: %s [--port <serial_port>] [--baudrate <rate>]\n", prog);
|
||||||
printf(" %s --list\n", prog);
|
printf(" %s --list\n", prog);
|
||||||
printf(" --port <serial_port> : COM3 (Windows) or /dev/ttyUSB0 (Linux)\n");
|
printf(" --port <serial_port> : COM3 (Windows) or /dev/ttyUSB0 (Linux)\n");
|
||||||
printf(" --baudrate <rate> : baud rate (default 400000)\n");
|
printf(" --baudrate <rate> : baud rate (default: auto-probe)\n");
|
||||||
printf(" interval_ms : poll interval in ms (default 200)\n");
|
|
||||||
printf(" --list : list available serial ports and exit\n");
|
printf(" --list : list available serial ports and exit\n");
|
||||||
|
printf("\nIf --port is omitted, an ELRS-like port is auto-detected.\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
static int list_ports(void) {
|
static int list_ports(void) {
|
||||||
@@ -73,8 +428,9 @@ static int list_ports(void) {
|
|||||||
|
|
||||||
int main(int argc, char const* argv[]) {
|
int main(int argc, char const* argv[]) {
|
||||||
char const* port_path = NULL;
|
char const* port_path = NULL;
|
||||||
int baud_rate = 400000;
|
int baud_rate = 0; /* 0 = auto-probe */
|
||||||
int interval_ms = 200;
|
char msg[256];
|
||||||
|
clock_t t_startup = clock();
|
||||||
|
|
||||||
for (int i = 1; i < argc; i++) {
|
for (int i = 1; i < argc; i++) {
|
||||||
if (strcmp(argv[i], "--list") == 0) {
|
if (strcmp(argv[i], "--list") == 0) {
|
||||||
@@ -84,77 +440,117 @@ int main(int argc, char const* argv[]) {
|
|||||||
} else if (strcmp(argv[i], "--baudrate") == 0 && i + 1 < argc) {
|
} else if (strcmp(argv[i], "--baudrate") == 0 && i + 1 < argc) {
|
||||||
baud_rate = atoi(argv[++i]);
|
baud_rate = atoi(argv[++i]);
|
||||||
if (baud_rate <= 0) baud_rate = 400000;
|
if (baud_rate <= 0) baud_rate = 400000;
|
||||||
} else {
|
|
||||||
interval_ms = atoi(argv[i]);
|
|
||||||
if (interval_ms <= 0) interval_ms = 200;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Auto-detect port if not specified */
|
||||||
if (port_path == NULL) {
|
if (port_path == NULL) {
|
||||||
print_usage(argv[0]);
|
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;
|
return 1;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* Open serial port */
|
setup_signal_handler();
|
||||||
cel_serial_port* port = cel_serial_open(port_path, baud_rate);
|
|
||||||
|
/* 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 (%d baud)", port_path, baud_rate);
|
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));
|
|
||||||
if (n == 0) {
|
|
||||||
sleep_ms(interval_ms);
|
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
cel_crsf_frame frame;
|
/* Send initial RC frame to establish link */
|
||||||
if (cel_crsf_frame_parse(&frame, buf, n) != 0) {
|
int16_t channels[16];
|
||||||
errors++;
|
cel_crsf_channel_default(channels);
|
||||||
continue;
|
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++;
|
||||||
}
|
}
|
||||||
|
|
||||||
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 RC frame every 20 ms (50 Hz) to keep link alive */
|
||||||
cel_log_info(msg);
|
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;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user