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:
Francesco Zanin
2026-06-23 19:32:11 +02:00
commit e22a47fce1
284 changed files with 132664 additions and 0 deletions

View File

@@ -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() {}