commit ba3221ae4f432d3488d332bf978658c5c7142189 Author: nicoboy Date: Wed Jun 3 18:34:14 2026 +0200 Initial commit — firmware WireClaw ESP32-S3 fonctionnel - Heartbeat MAVLink v1 toutes les 1s (broadcast 192.168.4.255:14553) - Relais WiFi <-> UART (Jetson <-> Pi Zero) sur ports 14550-14553 - LED RGB WS2812 GPIO48 : rouge pulsé (attente), vert dim (idle), flash vert (heartbeat) - MODE_AP=true : hotspot WireClaw-CAM / false : connexion Jetson - Fix: retrait ARDUINO_USB_CDC_ON_BOOT (carte Freenove utilise CH343/UART0) diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c9c44d1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,11 @@ +.pio +.vscode/.browse.c_cpp.db* +.vscode/c_cpp_properties.json +.vscode/launch.json +.vscode/ipch + +# Secrets +include/credentials.h + +# Fichiers temporaires +src/main.cpp.old diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..615fd1a --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,95 @@ +# WireClaw ESP32-S3 — Contexte projet + +## Carte +- **Freenove ESP32-S3-WROOM** (8MB Flash, PSRAM OPI) +- LED RGB WS2812 sur **GPIO48** (adressable, lib FastLED) +- UART2 : RX=GPIO16, TX=GPIO17 (vers Pi Zero, 57600 baud) +- Flash mode : QIO 80MHz (`board_build.flash_mode = qio`) + +## Rôle de ce firmware +Nœud de communication drone WireClaw : +- Envoie un **HEARTBEAT MAVLink v1** toutes les 1 seconde +- Relaie les trames MAVLink **WiFi ↔ UART** (Jetson ↔ Pi Zero) +- Indique l'état par la **LED RGB** (voir section LED) + +## Basculer entre mode AP et mode Jetson +En haut de `src/main.cpp`, une seule constante suffit : + +```cpp +#define MODE_AP true // true = ESP32 crée son propre hotspot (test sans Jetson) + // false = ESP32 se connecte au hotspot WireClaw du Jetson +``` + +- **`MODE_AP true`** : hotspot `WireClaw-CAM`, IP fixe `192.168.4.1`, heartbeat broadcast `192.168.4.255` +- **`MODE_AP false`** : connexion à `WIFI_SSID`, heartbeat envoyé à `JETSON_IP:14553` + +Les credentials sont dans `include/credentials.h` : +```cpp +// Mode AP +#define AP_SSID "WireClaw-CAM" +#define AP_PASSWORD "" + +// Mode STA (Jetson) +#define WIFI_SSID "WireClaw" +#define WIFI_PASSWORD "" +#define JETSON_IP "10.42.0.1" +``` + +## LED RGB (GPIO48, WS2812, FastLED) +| État | Couleur | Pattern | +|---|---|---| +| Attente WiFi / AP init | Rouge | Pulsé fade in/out | +| Connecté, idle | Vert | Très dim fixe | +| Heartbeat envoyé | Vert | Flash vif 80ms — 1 Hz | + +## MAVLink Heartbeat +- Implémenté sans lib externe dans `include/mavlink_heartbeat.h` +- MAVLink v1, msg_id=0, SYS_ID=255 (GCS), CRC-16/MCRF4XX +- Séquence incrémentale `mavSeq` (uint8_t, overflow auto) +- Log hex dans le monitor série à chaque envoi + +## Ports UDP +| Constante | Port | Usage | +|---|---|---| +| `LOCAL_PORT` | 14550 | Écoute ESP32 (commandes depuis Jetson) | +| `JETSON_PORT_MAVSDK` | 14551 | MAVLink vers Jetson (MAVLink SDK) | +| `JETSON_PORT_PYMAV` | 14552 | MAVLink vers Jetson (pymavlink) | +| `JETSON_PORT_HB` | 14553 | Heartbeat vers Jetson | + +## Architecture réseau + +### Mode AP (test) +``` +PC/smartphone → WiFi "WireClaw-CAM" → ESP32-S3 (192.168.4.1) +``` + +### Mode STA (production) +``` +Jetson (10.42.0.1) ←→ WiFi "WireClaw" ←→ ESP32-S3 (10.42.0.77) + ↕ UART 57600 + Pi Zero (caméra CSI) +``` + +## platformio.ini points importants +- `board_build.flash_mode = qio` — obligatoire pour cette carte +- `board_build.f_flash = 80000000L` — 80MHz +- Ne PAS utiliser `-DARDUINO_USB_CDC_ON_BOOT=1` — la carte Freenove passe par un chip CH343 (UART0→COM11), pas par l'USB natif ESP32. Ce flag redirige Serial vers l'USB OTG qui n'est pas connecté → app muette. +- Ne pas utiliser `board_build.arduino.memory_type = qio_opi` — cause boot loop + +## Fichiers +``` +wireclaw-esp32/ +├── CLAUDE.md ← ce fichier +├── platformio.ini +├── include/ +│ ├── credentials.h ← SSID, passwords, IP Jetson +│ └── mavlink_heartbeat.h ← construction trame MAVLink v1 +└── src/ + └── main.cpp ← firmware principal +``` + +## Travail futur +- [ ] IP statique ESP32 via `WiFi.config()` en mode STA +- [ ] Valider réception heartbeat côté Jetson (pymavlink) +- [ ] Câblage UART ESP32 ↔ Pi Zero (GPIO16/17 ↔ GPIO14/15) +- [ ] Remplacer broadcast par IP Jetson fixe une fois en production diff --git a/include/README b/include/README new file mode 100644 index 0000000..49819c0 --- /dev/null +++ b/include/README @@ -0,0 +1,37 @@ + +This directory is intended for project header files. + +A header file is a file containing C declarations and macro definitions +to be shared between several project source files. You request the use of a +header file in your project source file (C, C++, etc) located in `src` folder +by including it, with the C preprocessing directive `#include'. + +```src/main.c + +#include "header.h" + +int main (void) +{ + ... +} +``` + +Including a header file produces the same results as copying the header file +into each source file that needs it. Such copying would be time-consuming +and error-prone. With a header file, the related declarations appear +in only one place. If they need to be changed, they can be changed in one +place, and programs that include the header file will automatically use the +new version when next recompiled. The header file eliminates the labor of +finding and changing all the copies as well as the risk that a failure to +find one copy will result in inconsistencies within a program. + +In C, the convention is to give header files names that end with `.h'. + +Read more about using header files in official GCC documentation: + +* Include Syntax +* Include Operation +* Once-Only Headers +* Computed Includes + +https://gcc.gnu.org/onlinedocs/cpp/Header-Files.html diff --git a/include/mavlink_heartbeat.h b/include/mavlink_heartbeat.h new file mode 100644 index 0000000..c4f1aff --- /dev/null +++ b/include/mavlink_heartbeat.h @@ -0,0 +1,73 @@ +#pragma once +#include + +// ───────────────────────────────────────────────────────────── +// MAVLink v1 — HEARTBEAT (msg_id = 0) +// Trame complète : 17 octets +// +// Byte Field +// 0 STX 0xFE (MAVLink v1 magic) +// 1 LEN 9 (payload = 9 octets) +// 2 SEQ séquence incrémentale +// 3 SYS_ID system id (on se déclare comme GCS = 255) +// 4 COMP_ID component id (GCS = 0) +// 5 MSG_ID 0 (HEARTBEAT) +// 6-14 PAYLOAD 9 octets (voir ci-dessous) +// 15-16 CRC CRC-16/MCRF4XX avec CRC_EXTRA = 50 +// +// Payload HEARTBEAT (9 octets, little-endian) : +// [0-3] custom_mode uint32 0x00000000 +// [4] type uint8 MAV_TYPE_GCS = 6 +// [5] autopilot uint8 MAV_AUTOPILOT_INVALID = 8 +// [6] base_mode uint8 0 +// [7] system_status uint8 MAV_STATE_ACTIVE = 4 +// [8] mavlink_version uint8 3 +// ───────────────────────────────────────────────────────────── + +// CRC-16/MCRF4XX (algorithme MAVLink) +static inline void mavCrcAccumulate(uint8_t data, uint16_t &crc) { + uint8_t tmp = data ^ (uint8_t)(crc & 0xFF); + tmp ^= (tmp << 4); + crc = (crc >> 8) ^ ((uint16_t)tmp << 8) ^ ((uint16_t)tmp << 3) ^ ((uint16_t)tmp >> 4); +} + +// Construit un paquet HEARTBEAT MAVLink v1 dans buf (doit faire >= 17 octets) +// Retourne la taille du paquet (toujours 17) +static inline uint8_t mavlinkBuildHeartbeat(uint8_t *buf, uint8_t &seq) { + const uint8_t PAYLOAD_LEN = 9; + const uint8_t SYS_ID = 255; // GCS + const uint8_t COMP_ID = 0; // GCS component + const uint8_t MSG_ID = 0; // HEARTBEAT + const uint8_t CRC_EXTRA = 50; // valeur fixe MAVLink pour msg #0 + + // Header + buf[0] = 0xFE; // STX + buf[1] = PAYLOAD_LEN; + buf[2] = seq++; // numéro de séquence (overflow géré par uint8_t) + buf[3] = SYS_ID; + buf[4] = COMP_ID; + buf[5] = MSG_ID; + + // Payload (little-endian) + buf[6] = 0x00; // custom_mode [0] + buf[7] = 0x00; // custom_mode [1] + buf[8] = 0x00; // custom_mode [2] + buf[9] = 0x00; // custom_mode [3] + buf[10] = 6; // type = MAV_TYPE_GCS + buf[11] = 8; // autopilot = MAV_AUTOPILOT_INVALID + buf[12] = 0; // base_mode + buf[13] = 4; // system_status = MAV_STATE_ACTIVE + buf[14] = 3; // mavlink_version + + // CRC sur header (sans STX) + payload + CRC_EXTRA + uint16_t crc = 0xFFFF; + for (uint8_t i = 1; i <= 5 + PAYLOAD_LEN; i++) { + mavCrcAccumulate(buf[i], crc); + } + mavCrcAccumulate(CRC_EXTRA, crc); + + buf[15] = crc & 0xFF; // CRC low byte + buf[16] = (crc >> 8) & 0xFF; // CRC high byte + + return 17; +} diff --git a/platformio.ini b/platformio.ini new file mode 100644 index 0000000..2b4ae63 --- /dev/null +++ b/platformio.ini @@ -0,0 +1,17 @@ +[env:freenove_esp32s3] +platform = espressif32 +board = esp32-s3-devkitc-1 +framework = arduino + +board_build.flash_mode = qio +board_build.f_flash = 80000000L +board_build.partitions = default_8MB.csv +board_upload.flash_size = 8MB + +build_flags = + +lib_deps = + fastled/FastLED @ ^3.6.0 + +monitor_speed = 115200 +upload_speed = 460800 diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 0000000..436c84d --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,192 @@ +#include +#include +#include +#include +#include "credentials.h" +#include "mavlink_heartbeat.h" + +// ── Mode : true = ESP32 crée son propre hotspot false = connexion au Jetson ── +#define MODE_AP true + +// ── Ports ──────────────────────────────────────────────── +const uint16_t LOCAL_PORT = 14550; +const uint16_t JETSON_PORT_MAVSDK = 14551; +const uint16_t JETSON_PORT_PYMAV = 14552; +const uint16_t JETSON_PORT_HB = 14553; + +// ── UART vers Pi Zero ───────────────────────────────────── +#define UART_BAUD 57600 +#define UART_RX_PIN 16 +#define UART_TX_PIN 17 + +// ── LED RGB WS2812 (GPIO48) ─────────────────────────────── +#define LED_PIN 48 +#define NUM_LEDS 1 +CRGB leds[NUM_LEDS]; + +typedef enum { LED_WAITING_WIFI, LED_HEARTBEAT, LED_IDLE } LedState; +LedState currentLedState = LED_WAITING_WIFI; +unsigned long ledStateStart = 0; + +// ── Variables globales ──────────────────────────────────── +WiFiUDP udp; +HardwareSerial MavSerial(2); +bool wifiConnected = false; +unsigned long lastHeartbeat = 0; +uint8_t mavSeq = 0; + +#if MODE_AP + const char* BROADCAST_IP = "192.168.4.255"; +#else + const char* BROADCAST_IP = JETSON_IP; +#endif + +// ── LED non bloquante ───────────────────────────────────── +void updateLED() { + unsigned long elapsed = millis() - ledStateStart; + switch (currentLedState) { + case LED_WAITING_WIFI: + leds[0] = CRGB((uint8_t)(128 + 127 * sin(elapsed * 0.00628f)), 0, 0); + break; + case LED_HEARTBEAT: + if (elapsed < 80) { + leds[0] = CRGB(0, 255, 0); + } else { + currentLedState = LED_IDLE; + ledStateStart = millis(); + } + break; + case LED_IDLE: + leds[0] = CRGB(0, 8, 0); + break; + } + FastLED.show(); +} + +void setLedState(LedState s) { + currentLedState = s; + ledStateStart = millis(); +} + +// ── Heartbeat MAVLink ───────────────────────────────────── +void sendMavlinkHeartbeat() { + uint8_t buf[17]; + uint8_t len = mavlinkBuildHeartbeat(buf, mavSeq); + + udp.beginPacket(BROADCAST_IP, JETSON_PORT_HB); + udp.write(buf, len); + udp.endPacket(); + + setLedState(LED_HEARTBEAT); + + Serial.printf("[HB] seq=%d [", mavSeq - 1); + for (uint8_t i = 0; i < len; i++) { + Serial.printf("%02X", buf[i]); + if (i < len - 1) Serial.print(" "); + } + Serial.println("]"); +} + +// ── Reconnexion WiFi (mode STA uniquement) ──────────────── +void checkWiFi() { +#if !MODE_AP + if (WiFi.status() == WL_CONNECTED) { + if (!wifiConnected) { + wifiConnected = true; + WiFi.setSleep(false); + udp.begin(LOCAL_PORT); + setLedState(LED_IDLE); + Serial.print("WiFi connecte. IP: "); + Serial.println(WiFi.localIP()); + } + return; + } + if (wifiConnected) { + wifiConnected = false; + setLedState(LED_WAITING_WIFI); + Serial.println("WiFi perdu, reconnexion..."); + } + WiFi.begin(WIFI_SSID, WIFI_PASSWORD); +#endif +} + +// ── Setup ───────────────────────────────────────────────── +void setup() { + Serial.begin(115200); + + FastLED.addLeds(leds, NUM_LEDS); + FastLED.setBrightness(80); + setLedState(LED_WAITING_WIFI); + + Serial.println("\n=== WireClaw ESP32-S3 ==="); +#if MODE_AP + Serial.println("Mode: AP (hotspot autonome)"); +#else + Serial.println("Mode: STA (connexion Jetson)"); +#endif + + MavSerial.begin(UART_BAUD, SERIAL_8N1, UART_RX_PIN, UART_TX_PIN); + Serial.printf("UART2: %d baud (RX=%d TX=%d)\n", UART_BAUD, UART_RX_PIN, UART_TX_PIN); + +#if MODE_AP + WiFi.mode(WIFI_AP); + WiFi.softAP(AP_SSID, AP_PASSWORD); + delay(500); + wifiConnected = true; + udp.begin(LOCAL_PORT); + setLedState(LED_IDLE); + Serial.printf("Hotspot: %s\n", AP_SSID); + Serial.printf("IP : %s\n", WiFi.softAPIP().toString().c_str()); +#else + WiFi.mode(WIFI_STA); + WiFi.setSleep(false); + WiFi.begin(WIFI_SSID, WIFI_PASSWORD); + Serial.printf("Connexion a %s...\n", WIFI_SSID); +#endif + + Serial.printf("UDP ecoute: port %d\n", LOCAL_PORT); + delay(500); + if (wifiConnected) sendMavlinkHeartbeat(); +} + +// ── Loop ────────────────────────────────────────────────── +void loop() { + updateLED(); + checkWiFi(); + + if (wifiConnected && millis() - lastHeartbeat > 1000) { + lastHeartbeat = millis(); + sendMavlinkHeartbeat(); + } + + if (!wifiConnected) return; + + // WiFi → UART (Jetson → Pi Zero) + int packetSize = udp.parsePacket(); + if (packetSize > 0) { + uint8_t buf[512]; + int len = udp.read(buf, sizeof(buf)); + if (len > 0) { + MavSerial.write(buf, len); + Serial.printf("[WiFi->UART] %d bytes\n", len); + } + } + + // UART → WiFi (Pi Zero → Jetson) + if (MavSerial.available()) { + uint8_t buf[512]; + int len = 0; + while (MavSerial.available() && len < (int)sizeof(buf)) { + buf[len++] = MavSerial.read(); + } + if (len > 0) { + udp.beginPacket(BROADCAST_IP, JETSON_PORT_MAVSDK); + udp.write(buf, len); + udp.endPacket(); + udp.beginPacket(BROADCAST_IP, JETSON_PORT_PYMAV); + udp.write(buf, len); + udp.endPacket(); + Serial.printf("[UART->WiFi] %d bytes\n", len); + } + } +}