Snapshot iniziale: bridge telemetria CRSF->LTM/MAVLink (ESP-NOW)
Stato di partenza prima dei fix. Il repo git era inizializzato a meta' (config.lock + objects/ mancante, zero commit): riparato. my_config.h (UID reale, tutti e 3 i sottoprogetti) escluso via .gitignore; aggiunto my_config.h.example. .pio/ escluso. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
5
02_telemetry_bridge/test/native_stubs/Arduino.h
Normal file
5
02_telemetry_bridge/test/native_stubs/Arduino.h
Normal file
@@ -0,0 +1,5 @@
|
||||
#pragma once
|
||||
// Shim minimale per compilare lib/Ltm nell'ambiente "native" (host, no ESP32).
|
||||
// Ltm.h/.cpp usano solo i tipi interi a dimensione fissa e size_t.
|
||||
#include <cstdint>
|
||||
#include <cstddef>
|
||||
@@ -0,0 +1,135 @@
|
||||
// ============================================================
|
||||
// Test on-device (Unity via USB, nessuna catena RF) per il path
|
||||
// di decodifica realmente usato in produzione: terseCRSF's
|
||||
// CRSF::decodeTelemetry(), chiamato da mode_human.cpp/mode_ltm.cpp
|
||||
// esattamente come qui, su frame CRSF sintetici byte-esatti
|
||||
// (nessun ESP-NOW/radio necessario).
|
||||
//
|
||||
// I vettori attesi (valori decodificati) sono stati calcolati
|
||||
// indipendentemente in Python replicando le formule di
|
||||
// terseCRSF.cpp (big-endian, *0.1, RADS2DEGS, ecc.), non copiati
|
||||
// dall'implementazione.
|
||||
// ============================================================
|
||||
#include <Arduino.h>
|
||||
#include <unity.h>
|
||||
#include <terseCRSF.h>
|
||||
|
||||
static CRSF crsf;
|
||||
|
||||
void setUp(void) {}
|
||||
void tearDown(void) {}
|
||||
|
||||
void test_decode_gps(void) {
|
||||
uint8_t buf[20] = {0};
|
||||
buf[0] = 0xC8; buf[1] = 17; buf[2] = GPS_ID;
|
||||
const uint8_t payload[] = {27,25,73,180, 5,122,72,96, 0,125, 35,40, 4,128, 11};
|
||||
memcpy(&buf[3], payload, sizeof(payload));
|
||||
|
||||
uint8_t id = crsf.decodeTelemetry(buf, sizeof(buf));
|
||||
TEST_ASSERT_EQUAL_UINT8(GPS_ID, id);
|
||||
TEST_ASSERT_FLOAT_WITHIN(0.0001f, 45.46421f, crsf.gpsF_lat);
|
||||
TEST_ASSERT_FLOAT_WITHIN(0.0001f, 9.19f, crsf.gpsF_lon);
|
||||
TEST_ASSERT_FLOAT_WITHIN(0.01f, 12.5f, crsf.gpsF_groundspeed);
|
||||
TEST_ASSERT_FLOAT_WITHIN(0.01f, 90.0f, crsf.gpsF_heading);
|
||||
TEST_ASSERT_EQUAL_INT16(152, crsf.gps_altitude);
|
||||
TEST_ASSERT_EQUAL_UINT8(11, crsf.gps_sats);
|
||||
}
|
||||
|
||||
void test_decode_attitude(void) {
|
||||
uint8_t buf[12] = {0};
|
||||
buf[0] = 0xC8; buf[1] = 8; buf[2] = ATTITUDE_ID;
|
||||
const uint8_t payload[] = {6,209, 252,151, 235,140};
|
||||
memcpy(&buf[3], payload, sizeof(payload));
|
||||
|
||||
uint8_t id = crsf.decodeTelemetry(buf, sizeof(buf));
|
||||
TEST_ASSERT_EQUAL_UINT8(ATTITUDE_ID, id);
|
||||
TEST_ASSERT_FLOAT_WITHIN(0.01f, 9.998f, crsf.attiF_pitch);
|
||||
TEST_ASSERT_FLOAT_WITHIN(0.01f, -5.002f, crsf.attiF_roll);
|
||||
// atti_yaw viene troncato a intero e wrappato in [0,360) dalla libreria
|
||||
TEST_ASSERT_FLOAT_WITHIN(0.01f, 330.0f, crsf.attiF_yaw);
|
||||
}
|
||||
|
||||
// NB: documenta un comportamento della libreria terseCRSF vendorizzata
|
||||
// (non codice nostro): il commento dice "uint24_t mAh drawn" ma
|
||||
// bat_fuel_drawn legge bytes2int32(&_buf[7]), cioe' 4 byte (buf[7..10]),
|
||||
// mentre lo standard CRSF prevede capacity_used a 3 byte + 1 byte percent
|
||||
// (buf[7..9] capacita', buf[10] percent). Il risultato e' che bat_fuel_drawn
|
||||
// include anche il byte percent come LSB e ignora correttamente solo il
|
||||
// confine a 3 byte: il valore "mAh" mostrato da HUMAN/LTM non corrisponde
|
||||
// alla capacita' realmente trasmessa. Se la cosa diventa rilevante in
|
||||
// pratica, va corretto a monte (fork/patch locale di terseCRSF), non qui.
|
||||
void test_decode_battery_documents_upstream_fuel_drawn_quirk(void) {
|
||||
uint8_t buf[12] = {0};
|
||||
buf[0] = 0xC8; buf[1] = 9; buf[2] = BATTERY_ID;
|
||||
// voltage=164(*0.1=16.4V) current=12(*0.1=1.2A) capacity=350mAh(3 byte) percent=78
|
||||
const uint8_t payload[] = {0,164, 0,12, 0,1,94, 78};
|
||||
memcpy(&buf[3], payload, sizeof(payload));
|
||||
|
||||
uint8_t id = crsf.decodeTelemetry(buf, sizeof(buf));
|
||||
TEST_ASSERT_EQUAL_UINT8(BATTERY_ID, id);
|
||||
TEST_ASSERT_FLOAT_WITHIN(0.01f, 16.4f, crsf.batF_voltage);
|
||||
TEST_ASSERT_FLOAT_WITHIN(0.01f, 1.2f, crsf.batF_current);
|
||||
TEST_ASSERT_EQUAL_UINT8(78, crsf.bat_remaining); // corretto
|
||||
TEST_ASSERT_EQUAL_UINT32(89678, crsf.bat_fuel_drawn); // NON e' 350: bug upstream
|
||||
}
|
||||
|
||||
void test_decode_link_statistics(void) {
|
||||
uint8_t buf[16] = {0};
|
||||
buf[0] = 0xC8; buf[1] = 12; buf[2] = LINK_ID;
|
||||
const uint8_t payload[] = {52, 0, 100, 247, 0, 2, 3, 60, 98, 245};
|
||||
memcpy(&buf[3], payload, sizeof(payload));
|
||||
|
||||
uint8_t id = crsf.decodeTelemetry(buf, sizeof(buf));
|
||||
TEST_ASSERT_EQUAL_UINT8(LINK_ID, id);
|
||||
TEST_ASSERT_EQUAL_UINT8(52, crsf.link_up_rssi_ant_1);
|
||||
TEST_ASSERT_EQUAL_UINT8(100, crsf.link_up_quality);
|
||||
TEST_ASSERT_EQUAL_INT8(-9, crsf.link_up_snr);
|
||||
TEST_ASSERT_EQUAL_UINT8(3, crsf.link_up_tx_power);
|
||||
TEST_ASSERT_EQUAL_UINT8(60, crsf.link_dn_rssi);
|
||||
TEST_ASSERT_EQUAL_INT8(-11, crsf.link_dn_snr);
|
||||
}
|
||||
|
||||
void test_decode_flight_mode(void) {
|
||||
uint8_t buf[10] = {0};
|
||||
buf[0] = 0xC8;
|
||||
buf[1] = 7; // flight_mode_lth = buf[1]-3 = 4 ("ACRO")
|
||||
buf[2] = FLIGHT_MODE_ID;
|
||||
memcpy(&buf[3], "ACRO", 4);
|
||||
|
||||
uint8_t id = crsf.decodeTelemetry(buf, sizeof(buf));
|
||||
TEST_ASSERT_EQUAL_UINT8(FLIGHT_MODE_ID, id);
|
||||
TEST_ASSERT_EQUAL_STRING("ACRO", crsf.flightMode.c_str());
|
||||
}
|
||||
|
||||
// Caratterizzazione: decodeTelemetry() NON valida sync byte ne' CRC.
|
||||
// La validazione CRC esiste solo in readCrsfFrame() (letture da UART),
|
||||
// che qui non usiamo perche' i frame arrivano da ESP-NOW. Un frame con
|
||||
// sync sbagliato e CRC spazzatura viene comunque decodificato.
|
||||
void test_decode_does_not_validate_sync_or_crc(void) {
|
||||
uint8_t buf[20] = {0};
|
||||
buf[0] = 0x00; // sync sbagliato (dovrebbe essere 0xC8/0xEA)
|
||||
buf[1] = 17;
|
||||
buf[2] = GPS_ID;
|
||||
const uint8_t payload[] = {27,25,73,180, 5,122,72,96, 0,125, 35,40, 4,128, 11};
|
||||
memcpy(&buf[3], payload, sizeof(payload));
|
||||
buf[18] = 0xFF; // crc "spazzatura"
|
||||
|
||||
uint8_t id = crsf.decodeTelemetry(buf, sizeof(buf));
|
||||
TEST_ASSERT_EQUAL_UINT8(GPS_ID, id);
|
||||
TEST_ASSERT_FLOAT_WITHIN(0.0001f, 45.46421f, crsf.gpsF_lat);
|
||||
}
|
||||
|
||||
void setup() {
|
||||
Serial.begin(115200);
|
||||
delay(2000);
|
||||
UNITY_BEGIN();
|
||||
RUN_TEST(test_decode_gps);
|
||||
RUN_TEST(test_decode_attitude);
|
||||
RUN_TEST(test_decode_battery_documents_upstream_fuel_drawn_quirk);
|
||||
RUN_TEST(test_decode_link_statistics);
|
||||
RUN_TEST(test_decode_flight_mode);
|
||||
RUN_TEST(test_decode_does_not_validate_sync_or_crc);
|
||||
UNITY_END();
|
||||
}
|
||||
|
||||
void loop() {}
|
||||
154
02_telemetry_bridge/test/test_embedded_espnowcrsf/test_ring.cpp
Normal file
154
02_telemetry_bridge/test/test_embedded_espnowcrsf/test_ring.cpp
Normal file
@@ -0,0 +1,154 @@
|
||||
// ============================================================
|
||||
// Test on-device (Unity via USB, nessuna catena RF) per
|
||||
// lib/EspNowCrsf: ring buffer, drop-oldest e filtro keep-alive.
|
||||
//
|
||||
// Usa test_injectRaw()/test_reset() (hook #ifdef UNIT_TEST,
|
||||
// zero impatto sul firmware reale) per richiamare il vero
|
||||
// onRecv() senza inizializzare WiFi/ESP-NOW: niente radio
|
||||
// necessaria, solo la board collegata via USB.
|
||||
// ============================================================
|
||||
#include <Arduino.h>
|
||||
#include <unity.h>
|
||||
#include "EspNowCrsf.h"
|
||||
|
||||
void setUp(void) { EspNowCrsf::test_reset(); }
|
||||
void tearDown(void) {}
|
||||
|
||||
// Costruisce un pacchetto "backpack": 8 byte header (contenuto
|
||||
// ignorato dal parser) + payload CRSF grezzo.
|
||||
static uint16_t buildPkt(uint8_t* out, const uint8_t* payload, uint16_t payloadLen) {
|
||||
memset(out, 0xAA, 8); // header: contenuto qualsiasi, non ispezionato
|
||||
memcpy(out + 8, payload, payloadLen);
|
||||
return 8 + payloadLen;
|
||||
}
|
||||
|
||||
void test_fifo_order_preserved(void) {
|
||||
uint8_t pkt[16];
|
||||
for (uint8_t i = 1; i <= 3; ++i) {
|
||||
uint8_t payload[2] = {i, (uint8_t)(0xF0 + i)};
|
||||
uint16_t n = buildPkt(pkt, payload, sizeof(payload));
|
||||
EspNowCrsf::test_injectRaw(pkt, n);
|
||||
}
|
||||
|
||||
uint8_t buf[64];
|
||||
uint16_t len;
|
||||
for (uint8_t i = 1; i <= 3; ++i) {
|
||||
TEST_ASSERT_TRUE(EspNowCrsf::poll(buf, &len, sizeof(buf)));
|
||||
TEST_ASSERT_EQUAL_UINT16(2, len);
|
||||
TEST_ASSERT_EQUAL_UINT8(i, buf[0]);
|
||||
TEST_ASSERT_EQUAL_UINT8(0xF0 + i, buf[1]);
|
||||
}
|
||||
TEST_ASSERT_FALSE(EspNowCrsf::poll(buf, &len, sizeof(buf))); // vuoto
|
||||
}
|
||||
|
||||
void test_drop_oldest_when_full(void) {
|
||||
const uint8_t INJECTED = 20; // ben oltre qualunque capacita' ragionevole del ring
|
||||
uint8_t pkt[16];
|
||||
for (uint8_t i = 1; i <= INJECTED; ++i) {
|
||||
uint8_t payload[2] = {i, 0};
|
||||
uint16_t n = buildPkt(pkt, payload, sizeof(payload));
|
||||
EspNowCrsf::test_injectRaw(pkt, n);
|
||||
}
|
||||
|
||||
uint8_t buf[64];
|
||||
uint16_t len;
|
||||
uint8_t polled = 0;
|
||||
uint8_t lastMarker = 0;
|
||||
while (EspNowCrsf::poll(buf, &len, sizeof(buf))) {
|
||||
lastMarker = buf[0];
|
||||
polled++;
|
||||
}
|
||||
|
||||
TEST_ASSERT_EQUAL_UINT32(INJECTED, EspNowCrsf::framesReceived());
|
||||
TEST_ASSERT_TRUE_MESSAGE(EspNowCrsf::framesDropped() > 0, "con 20 frame iniettati ci si aspetta almeno un drop-oldest");
|
||||
TEST_ASSERT_EQUAL_UINT32(INJECTED, polled + EspNowCrsf::framesDropped());
|
||||
// drop-oldest: l'ultimo frame iniettato deve essere sempre presente
|
||||
TEST_ASSERT_EQUAL_UINT8(INJECTED, lastMarker);
|
||||
}
|
||||
|
||||
// ---- Filtro keep-alive: regressione del bug off-by-one --------
|
||||
// Bug originale: `in[8]==0 && in[9]==0 && len>10` leggeva in[9] OOB
|
||||
// quando len==9, e non scartava il keep-alive legittimo a len==10.
|
||||
|
||||
void test_keepalive_len10_is_filtered(void) {
|
||||
uint8_t payload[2] = {0, 0}; // header(8) + 2 byte zero = len 10
|
||||
uint8_t pkt[16];
|
||||
uint16_t n = buildPkt(pkt, payload, sizeof(payload));
|
||||
EspNowCrsf::test_injectRaw(pkt, n);
|
||||
|
||||
uint8_t buf[64];
|
||||
uint16_t len;
|
||||
TEST_ASSERT_FALSE_MESSAGE(EspNowCrsf::poll(buf, &len, sizeof(buf)),
|
||||
"un keep-alive di len==10 deve essere scartato, non accodato");
|
||||
TEST_ASSERT_EQUAL_UINT32(0, EspNowCrsf::framesReceived());
|
||||
}
|
||||
|
||||
void test_len9_payload_not_oob_and_not_keepalive(void) {
|
||||
uint8_t payload[1] = {0}; // header(8) + 1 byte zero = len 9
|
||||
uint8_t pkt[16];
|
||||
uint16_t n = buildPkt(pkt, payload, sizeof(payload));
|
||||
EspNowCrsf::test_injectRaw(pkt, n); // non deve leggere OOB / non deve crashare
|
||||
|
||||
uint8_t buf[64];
|
||||
uint16_t len;
|
||||
TEST_ASSERT_TRUE_MESSAGE(EspNowCrsf::poll(buf, &len, sizeof(buf)),
|
||||
"len==9 e' troppo corto per essere un keep-alive valido: va accodato");
|
||||
TEST_ASSERT_EQUAL_UINT16(1, len);
|
||||
}
|
||||
|
||||
void test_keepalive_with_extra_trailing_byte_is_filtered(void) {
|
||||
uint8_t payload[3] = {0, 0, 0x7E}; // header(8) + 3 byte, primi due zero = len 11
|
||||
uint8_t pkt[16];
|
||||
uint16_t n = buildPkt(pkt, payload, sizeof(payload));
|
||||
EspNowCrsf::test_injectRaw(pkt, n);
|
||||
|
||||
uint8_t buf[64];
|
||||
uint16_t len;
|
||||
TEST_ASSERT_FALSE_MESSAGE(EspNowCrsf::poll(buf, &len, sizeof(buf)),
|
||||
"primi due byte payload a zero -> keep-alive, va scartato anche con byte extra");
|
||||
}
|
||||
|
||||
void test_normal_frame_with_nonzero_first_byte_is_kept(void) {
|
||||
uint8_t payload[3] = {0xC8, 0x05, 0x02}; // non e' un keep-alive (primo byte != 0)
|
||||
uint8_t pkt[16];
|
||||
uint16_t n = buildPkt(pkt, payload, sizeof(payload));
|
||||
EspNowCrsf::test_injectRaw(pkt, n);
|
||||
|
||||
uint8_t buf[64];
|
||||
uint16_t len;
|
||||
TEST_ASSERT_TRUE(EspNowCrsf::poll(buf, &len, sizeof(buf)));
|
||||
TEST_ASSERT_EQUAL_UINT16(3, len);
|
||||
TEST_ASSERT_EQUAL_UINT8(0xC8, buf[0]);
|
||||
}
|
||||
|
||||
// ---- Clamping a SLOT_SIZE (64) ---------------------------------
|
||||
void test_oversized_frame_is_clamped(void) {
|
||||
uint8_t pkt[8 + 100];
|
||||
uint8_t payload[100];
|
||||
for (uint16_t i = 0; i < sizeof(payload); ++i) payload[i] = (uint8_t)(i + 1); // mai 0 in [0]/[1]
|
||||
uint16_t n = buildPkt(pkt, payload, sizeof(payload));
|
||||
EspNowCrsf::test_injectRaw(pkt, n);
|
||||
|
||||
uint8_t buf[64];
|
||||
uint16_t len;
|
||||
TEST_ASSERT_TRUE(EspNowCrsf::poll(buf, &len, sizeof(buf)));
|
||||
TEST_ASSERT_EQUAL_UINT16(64, len); // clampato a SLOT_SIZE
|
||||
TEST_ASSERT_EQUAL_UINT8(1, buf[0]); // primi byte del payload originale preservati
|
||||
TEST_ASSERT_EQUAL_UINT8(64, buf[63]);
|
||||
}
|
||||
|
||||
void setup() {
|
||||
Serial.begin(115200);
|
||||
delay(2000);
|
||||
UNITY_BEGIN();
|
||||
RUN_TEST(test_fifo_order_preserved);
|
||||
RUN_TEST(test_drop_oldest_when_full);
|
||||
RUN_TEST(test_keepalive_len10_is_filtered);
|
||||
RUN_TEST(test_len9_payload_not_oob_and_not_keepalive);
|
||||
RUN_TEST(test_keepalive_with_extra_trailing_byte_is_filtered);
|
||||
RUN_TEST(test_normal_frame_with_nonzero_first_byte_is_kept);
|
||||
RUN_TEST(test_oversized_frame_is_clamped);
|
||||
UNITY_END();
|
||||
}
|
||||
|
||||
void loop() {}
|
||||
86
02_telemetry_bridge/test/test_native_ltm/test_ltm.cpp
Normal file
86
02_telemetry_bridge/test/test_native_ltm/test_ltm.cpp
Normal file
@@ -0,0 +1,86 @@
|
||||
// ============================================================
|
||||
// Test nativi (no hardware) per lib/Ltm: verifica byte-per-byte
|
||||
// di header, payload little-endian e CRC (XOR del payload) per
|
||||
// i frame G/A/S, inclusi casi limite (valori negativi).
|
||||
//
|
||||
// Vettori attesi calcolati indipendentemente in Python (struct.pack
|
||||
// little-endian + XOR), non copiati dall'implementazione.
|
||||
// ============================================================
|
||||
#include <unity.h>
|
||||
#include <cstdio>
|
||||
#include "Ltm.h"
|
||||
|
||||
void setUp(void) {}
|
||||
void tearDown(void) {}
|
||||
|
||||
static void assertBytes(const uint8_t* actual, const uint8_t* expected, size_t n) {
|
||||
char msg[64];
|
||||
for (size_t i = 0; i < n; ++i) {
|
||||
snprintf(msg, sizeof(msg), "byte[%zu]", i);
|
||||
TEST_ASSERT_EQUAL_UINT8_MESSAGE(expected[i], actual[i], msg);
|
||||
}
|
||||
}
|
||||
|
||||
// ---- G-frame ------------------------------------------------
|
||||
void test_gframe_typical(void) {
|
||||
static const uint8_t expected[] = {36, 84, 71, 180, 73, 25, 27, 96, 72, 122,
|
||||
5, 5, 96, 59, 0, 0, 47, 217};
|
||||
uint8_t out[18];
|
||||
size_t n = Ltm::gframe(out, 454642100, 91900000, 5, 15200, 11, 3);
|
||||
TEST_ASSERT_EQUAL_size_t(18, n);
|
||||
assertBytes(out, expected, sizeof(expected));
|
||||
}
|
||||
|
||||
void test_gframe_negative_lat_lon_alt_no_fix(void) {
|
||||
static const uint8_t expected[] = {36, 84, 71, 235, 50, 164, 248, 79, 151, 33,
|
||||
197, 0, 12, 254, 255, 255, 1, 74};
|
||||
uint8_t out[18];
|
||||
size_t n = Ltm::gframe(out, -123456789, -987654321, 0, -500, 0, 1);
|
||||
TEST_ASSERT_EQUAL_size_t(18, n);
|
||||
assertBytes(out, expected, sizeof(expected));
|
||||
}
|
||||
|
||||
// ---- A-frame --------------------------------------------------
|
||||
void test_aframe_typical(void) {
|
||||
static const uint8_t expected[] = {36, 84, 65, 45, 0, 166, 255, 14, 1, 123};
|
||||
uint8_t out[10];
|
||||
size_t n = Ltm::aframe(out, 45, -90, 270);
|
||||
TEST_ASSERT_EQUAL_size_t(10, n);
|
||||
assertBytes(out, expected, sizeof(expected));
|
||||
}
|
||||
|
||||
void test_aframe_negative_pitch_full_roll(void) {
|
||||
static const uint8_t expected[] = {36, 84, 65, 76, 255, 180, 0, 0, 0, 7};
|
||||
uint8_t out[10];
|
||||
size_t n = Ltm::aframe(out, -180, 180, 0);
|
||||
TEST_ASSERT_EQUAL_size_t(10, n);
|
||||
assertBytes(out, expected, sizeof(expected));
|
||||
}
|
||||
|
||||
// ---- S-frame --------------------------------------------------
|
||||
void test_sframe_typical(void) {
|
||||
static const uint8_t expected[] = {36, 84, 83, 16, 64, 176, 4, 254, 12, 0, 22};
|
||||
uint8_t out[11];
|
||||
size_t n = Ltm::sframe(out, 16400, 1200, 254, 12, 0, 0);
|
||||
TEST_ASSERT_EQUAL_size_t(11, n);
|
||||
assertBytes(out, expected, sizeof(expected));
|
||||
}
|
||||
|
||||
void test_sframe_all_zero(void) {
|
||||
static const uint8_t expected[] = {36, 84, 83, 0, 0, 0, 0, 0, 0, 0, 0};
|
||||
uint8_t out[11];
|
||||
size_t n = Ltm::sframe(out, 0, 0, 0, 0, 0, 0);
|
||||
TEST_ASSERT_EQUAL_size_t(11, n);
|
||||
assertBytes(out, expected, sizeof(expected));
|
||||
}
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
UNITY_BEGIN();
|
||||
RUN_TEST(test_gframe_typical);
|
||||
RUN_TEST(test_gframe_negative_lat_lon_alt_no_fix);
|
||||
RUN_TEST(test_aframe_typical);
|
||||
RUN_TEST(test_aframe_negative_pitch_full_roll);
|
||||
RUN_TEST(test_sframe_typical);
|
||||
RUN_TEST(test_sframe_all_zero);
|
||||
return UNITY_END();
|
||||
}
|
||||
Reference in New Issue
Block a user