# Firmware di Pulcino

Sketch Arduino per **Seeed XIAO ESP32S3 Sense**. Pin, API e formati seguono
[`SPEC.md`](../SPEC.md), che resta la fonte di verità del progetto.

```
firmware/
  pulcino/
    pulcino.ino          setup() e loop(): ciclo di controllo a 50 Hz
    config.h             ← DA MODIFICARE: Wi-Fi, token, USE_PCA9685, SERVO_DIR, CALIB_OFFSET
    robot.h              nomi e limiti dei 6 giunti
    servos.h/.cpp        ESP32Servo (diretto) oppure PCA9685; occhi, batteria, sonar
    imu.h/.cpp           MPU6050 a registri + filtro complementare
    poses.h/.cpp         pose a keyframe: stand, rest, wave
    gait_cpg.h/.cpp      camminata CPG da gait.json (LittleFS)
    policy.h             pesi della rete (SEGNAPOSTO, POLICY_VALID 0)
    policy_mlp.h/.cpp    esecuzione della rete 16→32→32→6
    motion.h/.cpp        scelta del generatore, transizioni, watchdog 1 s
    camera.h/.cpp        OV2640/OV3660, frame in PSRAM
    audio.h/.cpp         mic PDM (I2S0) → WS, WS/beep → speaker MAX98357A (I2S1)
    web.h/.cpp           Wi-Fi/AP/mDNS, server HTTP nativi porta 80 e 81, WebSocket
    web/index.html       web app per iPhone (sorgente leggibile)
    web/calib.html       pagina di calibrazione (sorgente leggibile)
    web_index.h          GENERATO da web/index.html
    web_calib.h          GENERATO da web/calib.html
  tools/
    embed.py             rigenera web_index.h e web_calib.h
```

## 1. Librerie

| Cosa | Versione provata | Note |
|---|---|---|
| Core **esp32** di Espressif (Boards Manager) | **3.3.12** (serve ≥ 3.0) | include esp_camera, ESP_I2S, esp_http_server con WebSocket, LittleFS, Preferences, ESPmDNS |
| **ESP32Servo** (Kevin Harrington, madhephaestus) | **3.2.1** (serve ≥ 3.0) | solo variante diretta |
| **ArduinoJson** (Benoît Blanchon) | **7.4.3** (serve 7.x) | |
| **Adafruit PWM Servo Driver Library** | **3.0.3** | solo variante PCA9685; installa anche **Adafruit BusIO 1.17.4** |

Nessun'altra libreria: niente ESPAsyncWebServer (si usa il server HTTP nativo di ESP-IDF).

URL del Boards Manager (Arduino IDE → Impostazioni → URL aggiuntivi):
`https://raw.githubusercontent.com/espressif/arduino-esp32/gh-pages/package_esp32_index.json`

## 2. Impostazioni della scheda (Arduino IDE → Strumenti)

| Opzione | Valore |
|---|---|
| Scheda | **XIAO_ESP32S3** |
| PSRAM | **OPI PSRAM** (indispensabile per la camera) |
| Partition Scheme | **Default with spiffs (3MB APP/1.5MB SPIFFS)** — app 3 MB + 1,5 MB per LittleFS (usa la partizione "spiffs") |
| USB CDC On Boot | Enabled (per vedere i messaggi sul Monitor seriale a 115200) |

Lo sketch occupa ~1,17 MB su 3,2 MB disponibili.

### Da riga di comando (arduino-cli)

```sh
arduino-cli core install esp32:esp32
arduino-cli lib install ESP32Servo ArduinoJson "Adafruit PWM Servo Driver Library"

FQBN="esp32:esp32:XIAO_ESP32S3:PSRAM=opi,PartitionScheme=default_8MB"
arduino-cli compile --fqbn "$FQBN" firmware/pulcino
arduino-cli upload  --fqbn "$FQBN" -p /dev/cu.usbmodemXXXX firmware/pulcino

# Variante PCA9685 senza toccare config.h:
arduino-cli compile --fqbn "$FQBN" \
  --build-property "compiler.cpp.extra_flags=-DUSE_PCA9685=1" \
  --build-property "compiler.c.extra_flags=-DUSE_PCA9685=1" firmware/pulcino
```

> Non usare `build.extra_flags` per aggiungere define: sovrascrive i flag della
> scheda (PSRAM, USB CDC, `ESP32`) e rompe la compilazione.

Se la scheda non entra in modalità upload: tieni premuto **BOOT**, premi **RESET**, rilascia BOOT.

## 3. Primo avvio

1. In `config.h` metti `WIFI_SSID` / `WIFI_PASS` (oppure lasciali vuoti per usare solo l'Access Point).
2. Carica lo sketch **con il robot fermo e appoggiato**: all'avvio il giroscopio viene calibrato (~1 s).
3. Apri **http://pulcino.local** (o l'IP stampato sul Monitor seriale). Se la Wi-Fi non si
   collega entro 15 s, il robot crea la rete **`Pulcino-XXXX`** (password `pulcino123`): collegati e apri **http://192.168.4.1**.
4. Su iPhone: Safari → Condividi → **Aggiungi alla schermata Home**.

Token opzionale: se `API_TOKEN` non è vuoto, apri una volta `http://pulcino.local/?token=IL_TUO_TOKEN`;
la web app lo ricorda e lo aggiunge a WebSocket e POST.

## 4. Calibrazione dei servo

La formula è `angolo_servo = 90 + SERVO_DIR[i] · gradi(q_i) + CALIB_OFFSET[i]` (SPEC §3).

1. Collega i servo **senza** avvitare le gambe/i corni.
2. Apri **http://pulcino.local/calib** e premi **Tutti a 90°** (comando WS `calib90`).
3. Monta i corni il più dritti possibile: gambe verticali, piedi piatti, gambe parallele al corpo.
4. Con gli slider (o i tasti −/+ da 0,5°) correggi ogni servo: si muove subito (comando WS `servo`).
5. Premi **Salva**: `POST /api/calib` scrive gli offset in **NVS** (restano dopo il riavvio e dopo un nuovo upload).
6. Verifica i versi: dalla pagina principale premi **In piedi**, poi controlla che un offset positivo su
   hip_roll porti il piede **verso l'esterno**, su hip_pitch **in avanti**, su ankle_pitch la **punta in su**.
   Se un giunto va al contrario, cambia il segno in `SERVO_DIR[]` in `config.h` e ricarica.
7. (Facoltativo) copia gli offset salvati in `CALIB_OFFSET[]` di `config.h` come riserva.

## 5. Caricare un `gait.json` (camminata CPG)

Il formato è quello di SPEC §5.1. Il file vive in LittleFS come `/gait.json`; se manca si usa l'esempio della SPEC.

```sh
curl -X POST -H "Content-Type: application/json" \
     --data @gait.json "http://pulcino.local/api/gait?token=IL_TUO_TOKEN"   # token solo se impostato
curl http://pulcino.local/api/gait                                          # rilegge quello attivo
```

La POST valida il file (freq 0,2–4 Hz, ampiezze 0–1 rad…), lo applica subito e lo salva.
La **Palestra** del sito produce file già in questo formato.

## 6. Caricare una politica RL (`policy.h`)

1. Addestra in `training/` ed esporta: si ottiene un `policy.h` con `POLICY_VALID 1`.
2. Sostituisci `firmware/pulcino/policy.h` con quello nuovo e ricompila/ricarica.
3. Nella web app sposta l'interruttore su **Policy**. Se `POLICY_VALID` è 0 il robot usa il CPG
   (la web app lo segnala). Il cambio di modalità vale dalla camminata successiva.

Note sul formato accettato:
- `W1/W2/W3` possono essere array 1D row-major (`W[out*in]`) o 2D (`W[out][in]`).
- `ACTION_SCALE` può avere 3 valori (roll, pitch, ankle, ripetuti per le due gambe) o 6.
- Il core Arduino definisce già un simbolo `B1` (costanti binarie tipo `B101`): il firmware
  rinomina `B1` in `POLICY_B1` solo mentre include `policy.h`, quindi l'esportatore può usare i nomi della SPEC.
  `policy.h` non deve includere altri header.
- Convenzioni del firmware per l'osservazione: gravità = versore della gravità nel frame corpo
  (in piedi `(0,0,-1)`, x avanti, y sinistra, z su); giroscopio in rad/s nello stesso frame;
  "ultima azione" = uscita della rete prima di scala e filtro; filtro `q = 0.6·q_target + 0.4·q_prec`.

## 7. Web app

Sorgenti leggibili in `pulcino/web/index.html` e `pulcino/web/calib.html` (HTML+CSS+JS,
nessuna dipendenza esterna, funziona offline in AP). Dopo averle modificate:

```sh
python3 firmware/tools/embed.py     # rigenera web_index.h e web_calib.h (raw string C++)
```

- Video MJPEG da `http://<robot>:81/stream`; se la pagina è servita in HTTPS (tramite il Nido) usa `/stream`
  sullo stesso host. Si può forzare con `?stream=URL`.
- Le frecce e il joystick vanno **tenuti premuti**: la web app reinvia `move` ogni 250 ms e manda `stop` al rilascio.
- Per girare, la SPEC fa scalare a `wz` il passo di hip_pitch: da fermi (`vx=0`) il robot non ruoterebbe,
  quindi la web app aggiunge un piccolo passo avanti (`vx=0.4`) quando si chiede solo di girare.
- **Ascolta** apre `WS /audio` e suona il microfono del robot con WebAudio (serve un tap: regola di iOS).
- **Parla** (push-to-talk) usa `getUserMedia`, che Safari concede solo in **HTTPS**: collegandosi
  direttamente al robot (HTTP) la web app mostra "usa il Nido per parlare".

## 8. API (riassunto, dettagli in SPEC §6)

| Endpoint | Porta | Note |
|---|---|---|
| `GET /` · `GET /calib` | 80 | web app · calibrazione |
| `GET /stream` | 81 | MJPEG QVGA (un client alla volta per server; anche `/capture` sulla 81) |
| `GET /capture` | 80 | JPEG singolo |
| `GET /api/status` | 80 | telemetria + stato (camera, mic, policy_valid, gait, ip, heap…) |
| `GET/POST /api/gait` | 80 | gait.json (LittleFS) |
| `GET/POST /api/calib` | 80 | `{"offset":[6 gradi]}` (NVS); `"save":false` per non salvare |
| `WS /ws` | 80 | comandi JSON e telemetria ~5 Hz |
| `WS /audio` | 80 | PCM 16 kHz 16 bit mono in entrambe le direzioni |

- Watchdog di movimento: se per 1 s non arriva **alcun messaggio** su `/ws`, la camminata si ferma.
- Il comando `servo` usa l'angolo **grezzo** del servo (0–180°, senza offset); `calib90` porta tutti a 90° + offset.
  Entrambi mettono il robot in modalità "calib" finché non arriva una `pose` o un `move`.
- Telemetria: `vbat` vale 0 nella variante diretta (nessun pin libero per il partitore); nella variante
  PCA9685 c'è anche `dist` (cm, HC-SR04, −1 se nessuna eco).
- Occhi (`eyes`): canali 8/9 del PCA9685; nella variante diretta si usa il LED arancione della XIAO.

## 9. Note tecniche

- **Camera e servo**: la camera genera XCLK con LEDC timer 0. Il firmware riserva a ESP32Servo solo
  i timer LEDC 1–3 (`ESP32PWM::allocateTimer`). Su ESP32-S3 ESP32Servo ≥ 3.1 pilota i servo con la
  periferica **MCPWM** (a frequenza fissa), quindi il timer 0 non viene mai toccato; LEDC 1–3 resta come riserva.
  Per questo la camera va inizializzata per prima (vedi `setup()`). Il messaggio di compilazione
  "legacy MCPWM driver is deprecated" viene da ESP32Servo ed è innocuo.
- **Audio**: sull'S3 la ricezione PDM esiste solo su I2S0 → microfono su `I2S_NUM_0`, speaker su `I2S_NUM_1`.
- **I²C**: MPU6050 (e PCA9685) su `Wire` (I2C0, 400 kHz); la camera usa I2C1 internamente.
- **Task**: controllo a 50 Hz nel `loop()` (core 1); server HTTP, audio e telemetria sul core 0.
  I WebSocket vengono scritti solo tramite `httpd_queue_work` (thread-safe).
- **microSD** della Sense: non usarla (condivide D8–D10 con servo e I²S).
