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,78 @@
# Backpack Emulator — bench Tier 3 per 02_telemetry_bridge
ESP32-S3 che spedisce via **ESP-NOW** pacchetti sintetici identici (per
formato) a quelli del TX Backpack ELRS reale, verso l'UID del ricevitore
sotto test. Permette di validare `02_telemetry_bridge` (o `01_telemetry_reader`)
**senza aereo, GPS o radio ELRS**: bastano le due board collegate via USB sullo
stesso banco.
## Perché esiste
I Tier 02 della suite di test (`pio check`, unit test nativi su `lib/Ltm`,
unit test on-device su `lib/EspNowCrsf` e sul path di decodifica CRSF)
verificano la logica in isolamento. Il Tier 3 chiude il cerchio: due board
reali, ESP-NOW vero, stesso firmware di produzione sul ricevitore.
## Formato del pacchetto
Identico a quello del backpack reale (vedi `lib/EspNowCrsf/EspNowCrsf.cpp` sul
ricevitore):
```
[0..7] header 8 byte (contenuto ignorato dal parser del ricevitore)
[8..] frame CRSF grezzo: sync(0xC8) len type payload... crc
```
Il CRC è calcolato con lo stesso CRC8 (poly `0xD5`) usato da terseCRSF, quindi
i frame sono validi byte-per-byte, non solo "abbastanza buoni da passare il
parser".
## Cosa manda
Cicla ogni 250 ms tra 5 frame CRSF, con payload **identici** ai vettori già
verificati in `02_telemetry_bridge/test/test_embedded_crsf_decode/test_decode.cpp`:
| Tipo | ID | Valori attesi sul ricevitore (modo HUMAN) |
|------------|--------|--------------------------------------------|
| GPS | 0x02 | lat 45.46421, lon 9.19, alt 152 m, sat 11 |
| BATTERY | 0x08 | 16.4 V, 1.2 A, 78% |
| ATTITUDE | 0x1E | pitch 9.998°, roll -5.002°, yaw 330° |
| LINK | 0x14 | RSSI -52dBm ant1, LQ 100%, SNR -9dB |
| FLIGHTMODE | 0x21 | "ACRO" |
Ogni 8 invii (~2s) manda anche un **keep-alive** (header + 2 byte a zero), per
esercitare il filtro anti-keep-alive su hardware reale e non solo nel test
unitario (regressione del bug off-by-one corretto in `EspNowCrsf.cpp`).
Confrontando i valori stampati dal ricevitore con la tabella sopra si valida
in un colpo il path completo: ESP-NOW → ring buffer → filtro keep-alive →
decode CRSF → dashboard.
## Setup
1. `src/my_config.h``TARGET_UID` deve essere **lo stesso UID** configurato
in `02_telemetry_bridge/src/my_config.h` (o `01_telemetry_reader`) sulla
board ricevitrice sotto test. Non serve la bind phrase reale: è solo
l'indirizzo a cui spedire.
2. Collega l'ESP32-S3 (qui: COM7) e l'ESP32 ricevitore (qui: COM4) allo stesso
PC, due porte separate.
3. Flasha il ricevitore in modo **HUMAN** (così vedi i valori decodificati)
o **CRSF_DUMP** (per vedere i frame grezzi).
4. Build → Upload → Monitor su questa cartella (115200).
5. Confronta l'output del ricevitore con la tabella sopra.
## Struttura del codice
```
platformio.ini
src/
main.cpp # init ESP-NOW + invio ciclico GPS/BATTERY/ATTITUDE/LINK/FLIGHTMODE + keep-alive
my_config.h # TARGET_UID -- DA COMPILARE (stesso UID del ricevitore)
```
## Limiti
Emula solo il **formato del pacchetto**, non l'intero stack ELRS (niente link
budget, niente vera bind phrase, niente canale/RF reale oltre a ESP-NOW su
canale 1). Per la validazione con catena RF completa serve comunque la radio
ELRS configurata con `Telemetry = ESPNOW` e l'UID reale dalla bind phrase.

View File

@@ -0,0 +1,19 @@
; ============================================================
; Backpack Emulator - bench test per 02_telemetry_bridge
; Target: ESP32-S3 DevKitC-1 (USB nativo)
;
; Non e' soggetto al pin di versione di 01/02 (quella serve solo
; per la firma della callback di RICEZIONE ESP-NOW, che qui non
; usiamo: questo sketch spedisce soltanto). Platform di default.
; ============================================================
[env:esp32-s3-devkitc-1]
platform = espressif32
board = esp32-s3-devkitc-1
framework = arduino
monitor_speed = 115200
upload_protocol = esptool
build_flags =
-D CORE_DEBUG_LEVEL=0

View File

@@ -0,0 +1,158 @@
// ============================================================
// Backpack Emulator - bench Tier 3 per 02_telemetry_bridge
// Board: ESP32-S3 DevKitC-1 (USB nativo)
//
// Spedisce via ESP-NOW pacchetti "backpack" sintetici (8 byte di
// header + frame CRSF grezzo con CRC valido) all'UID del
// ricevitore sotto test (src/my_config.h -> TARGET_UID). Cicla
// GPS/BATTERY/ATTITUDE/LINK/FLIGHTMODE usando gli stessi payload
// byte-esatti gia' verificati in
// 02_telemetry_bridge/test/test_embedded_crsf_decode, cosi' i
// valori mostrati dal modo HUMAN del ricevitore sono confrontabili
// a vista con quelli documentati li'. Intercala anche keep-alive
// (header + 2 byte zero) per esercitare il filtro su hardware
// reale, non solo nel test unitario (Tier 2).
//
// Non serve aereo/GPS/radio ELRS: solo le due board sul banco,
// collegate via USB.
// ============================================================
#include <Arduino.h>
#include <WiFi.h>
#include <esp_wifi.h>
#include <esp_now.h>
#if __has_include("my_config.h")
#include "my_config.h"
#endif
#ifndef TARGET_UID
#define TARGET_UID { 0, 0, 0, 0, 0, 0 }
#endif
static uint8_t targetMac[6] = TARGET_UID;
// ---- CRC8 CRSF (poly 0xD5, stesso algoritmo di terseCRSF) -----
static uint8_t crc8_dvb_s2(uint8_t crc, uint8_t a) {
crc ^= a;
for (uint8_t i = 0; i < 8; ++i) {
crc = (crc & 0x80) ? (uint8_t)((crc << 1) ^ 0xD5) : (uint8_t)(crc << 1);
}
return crc;
}
// Costruisce un frame CRSF valido (sync, len, type, payload, crc)
// in out[]. Ritorna la lunghezza totale del frame.
static uint8_t buildCrsfFrame(uint8_t* out, uint8_t type, const uint8_t* payload, uint8_t payloadLen) {
out[0] = 0xC8; // CRSF_TEL_SYNC_BYTE
out[1] = payloadLen + 2; // len = type + payload + crc
out[2] = type;
memcpy(out + 3, payload, payloadLen);
uint8_t crc = 0;
for (uint8_t i = 0; i < payloadLen + 1; ++i) crc = crc8_dvb_s2(crc, out[2 + i]);
out[3 + payloadLen] = crc;
return 3 + payloadLen + 1;
}
// ---- Payload di esempio (identici ai vettori verificati in
// test_embedded_crsf_decode/test_decode.cpp) --------------------
#define GPS_ID 0x02
#define BATTERY_ID 0x08
#define LINK_ID 0x14
#define ATTITUDE_ID 0x1E
#define FLIGHT_MODE_ID 0x21
static const uint8_t PAYLOAD_GPS[] = {27,25,73,180, 5,122,72,96, 0,125, 35,40, 4,128, 11};
static const uint8_t PAYLOAD_BATTERY[] = {0,164, 0,12, 0,1,94, 78};
static const uint8_t PAYLOAD_ATTITUDE[] = {6,209, 252,151, 235,140};
static const uint8_t PAYLOAD_LINK[] = {52, 0, 100, 247, 0, 2, 3, 60, 98, 245};
static const uint8_t PAYLOAD_FLIGHTMODE[] = {'A','C','R','O', 0x00}; // stringa + null (vedi CRSF flight mode)
struct TelemSample { uint8_t type; const uint8_t* payload; uint8_t len; const char* label; };
static const TelemSample SAMPLES[] = {
{ GPS_ID, PAYLOAD_GPS, sizeof(PAYLOAD_GPS), "GPS" },
{ BATTERY_ID, PAYLOAD_BATTERY, sizeof(PAYLOAD_BATTERY), "BATTERY" },
{ ATTITUDE_ID, PAYLOAD_ATTITUDE, sizeof(PAYLOAD_ATTITUDE), "ATTITUDE" },
{ LINK_ID, PAYLOAD_LINK, sizeof(PAYLOAD_LINK), "LINK" },
{ FLIGHT_MODE_ID, PAYLOAD_FLIGHTMODE, sizeof(PAYLOAD_FLIGHTMODE), "FLIGHTMODE" },
};
static const uint8_t NUM_SAMPLES = sizeof(SAMPLES) / sizeof(SAMPLES[0]);
static uint32_t tick = 0;
static uint8_t sampleIdx = 0;
// Header "backpack": 8 byte, contenuto ignorato dal parser del
// ricevitore (vedi EspNowCrsf::onRecv) -- qui solo per diagnostica.
static void fillHeader(uint8_t* out) {
memset(out, 0, 8);
out[0] = 'E'; out[1] = 'M'; out[2] = 'U'; out[3] = 'L';
memcpy(out + 4, &tick, 4);
}
static void sendTelemetry(const TelemSample& sample) {
uint8_t pkt[8 + 32];
fillHeader(pkt);
uint8_t frameLen = buildCrsfFrame(pkt + 8, sample.type, sample.payload, sample.len);
esp_err_t res = esp_now_send(targetMac, pkt, 8 + frameLen);
Serial.printf("[%lu] TX %-10s frame=%u byte %s\n",
(unsigned long)tick, sample.label, frameLen,
res == ESP_OK ? "ok" : "ERR");
}
static void sendKeepAlive() {
uint8_t pkt[10];
fillHeader(pkt);
pkt[8] = 0; pkt[9] = 0; // keep-alive: i due byte dopo l'header a zero
esp_err_t res = esp_now_send(targetMac, pkt, sizeof(pkt));
Serial.printf("[%lu] TX keep-alive %s\n",
(unsigned long)tick, res == ESP_OK ? "ok" : "ERR");
}
static void initRadio() {
WiFi.mode(WIFI_STA);
WiFi.disconnect();
esp_wifi_set_protocol(WIFI_IF_STA, WIFI_PROTOCOL_11B); // stesso protocollo del backpack reale
esp_wifi_start();
esp_wifi_set_channel(1, WIFI_SECOND_CHAN_NONE); // deve combaciare col canale del ricevitore
if (esp_now_init() != ESP_OK) {
Serial.println(F("[ERR] esp_now_init fallito"));
return;
}
esp_now_peer_info_t peer = {};
memcpy(peer.peer_addr, targetMac, 6);
peer.channel = 1;
peer.encrypt = false;
esp_now_add_peer(&peer);
}
void setup() {
Serial.begin(115200);
delay(3000);
Serial.println();
Serial.println(F("=== Backpack Emulator (bench Tier 3) ==="));
Serial.printf("Target UID: %u %u %u %u %u %u\n",
targetMac[0], targetMac[1], targetMac[2], targetMac[3], targetMac[4], targetMac[5]);
if (targetMac[0] == 0 && targetMac[1] == 0 && targetMac[2] == 0)
Serial.println(F("[ATTENZIONE] TARGET_UID non configurato: modifica src/my_config.h"));
initRadio();
Serial.println(F("Invio telemetria sintetica ogni 250ms (ciclo GPS/BATTERY/ATTITUDE/LINK/FLIGHTMODE + keep-alive)..."));
}
static uint32_t lastSend = 0;
static const uint16_t SEND_PERIOD_MS = 250;
static const uint8_t KEEPALIVE_EVERY = 8; // 1 keep-alive ogni 8 invii (~2s)
void loop() {
if (millis() - lastSend < SEND_PERIOD_MS) return;
lastSend = millis();
tick++;
if (tick % KEEPALIVE_EVERY == 0) {
sendKeepAlive();
return;
}
sendTelemetry(SAMPLES[sampleIdx]);
sampleIdx = (sampleIdx + 1) % NUM_SAMPLES;
}