# PULCINO — specifica condivisa (contratto tra tutte le parti del repo)

> Questo file è la fonte di verità. Sito, firmware, simulazione, CAD e "Nido" DEVONO usare
> questi nomi, dimensioni, pin, formati e API. Se serve cambiare qualcosa, aggiorna prima qui.

Pulcino è un **mini robot bipede open source, stampato in 3D, molto economico (~60–70 €)**,
con **telecamera, microfono e speaker**, che **impara da solo a camminare in simulazione**
(sim2real: si allena nel mondo virtuale, poi la stessa "politica" gira sul robot vero).
Si comanda da **iPhone** (web app, niente App Store), può essere **"preso in controllo" da
altre persone** via link condiviso, oppure **agire in autonomia** tramite un cervello locale
("Nido") che parla con un LLM locale (Laya / Ollama / qualsiasi endpoint OpenAI-compatibile)
e con routine locali.

Ispirazione dichiarata (solo concettuale, nessun asset copiato): i progetti open source di
piccoli bipedi addestrati con RL in simulazione (es. "Open Duck Mini"). Nessun nome, logo o
artwork di personaggi/marchi protetti: solo illustrazioni SVG/Three.js originali.
Tutti i prezzi sono **indicativi**.

Lingua di tutto ciò che vede l'utente (sito, commenti nel codice, UI): **italiano**.

---

## 1. Architettura in una riga

```
[iPhone/altri browser] ⇄ (LAN: robot diretto) ⇄ [Pulcino: XIAO ESP32S3 Sense]
[iPhone/altri browser] ⇄ HTTPS ⇄ [Nido su Mac/PC/RPi] ⇄ WS/HTTP ⇄ [Pulcino]
                                     └─ LLM locale (Laya / Ollama /v1), routine, TTS/STT
[Simulazione MuJoCo (Python) o Rapier (browser)] → gait.json / policy.h → robot
```

## 2. Hardware

| Componente | Q.tà | Prezzo indicativo | Note |
|---|---|---|---|
| Seeed XIAO ESP32S3 **Sense** (camera OV2640/OV3660, mic PDM, 8 MB PSRAM) | 1 | ~15 € | cervello del robot: Wi-Fi, camera, microfono in un solo modulo |
| Servo **MG90S** (ingranaggi metallici) | 6 | ~2 € cad. (~12 €) | non SG90: più coppia (~2,2 vs 1,8 kg·cm) e ingranaggi che reggono le cadute |
| IMU **MPU6050** (GY-521) | 1 | ~2 € | orientamento: indispensabile per l'equilibrio e per la politica RL |
| Amplificatore I²S **MAX98357A** | 1 | ~3 € | pilota lo speaker |
| Speaker 8 Ω 1 W, Ø 28 mm | 1 | ~1,5 € | |
| Celle **18650** | 2 | ~8 € | in serie = 2S, 7,4 V nominali (8,4 V pieni) |
| Portabatterie 2×18650 + BMS 2S | 1 | ~3 € | |
| Step-down **5 V, ≥3 A** (es. Mini560 5 V 5 A) | 1 | ~2,5 € | alimenta servo + XIAO (pin 5V) |
| Interruttore, cavi dupont/silicone, connettori JST, viti M2 | – | ~5 € | |
| PLA (~200 g) | – | ~4 € | |
| **Opzionali**: PCA9685 (~3 €), 2 LED "occhi" (~0,2 €), HC-SR04 (~1,5 €), gommini TPU piedi | | | |

Totale target: **55–70 €** (indicativo). **Masse in comune** tra batteria, step-down, servo, XIAO, MAX98357A.

### 2.1 Pin (XIAO ESP32S3 Sense) — modalità predefinita `ESP32Servo` diretta

| Funzione | Pin XIAO | GPIO |
|---|---|---|
| Servo 0 `L_hip_roll` | D0 | 1 |
| Servo 1 `L_hip_pitch` | D1 | 2 |
| Servo 2 `L_ankle_pitch` | D2 | 3 |
| Servo 3 `R_hip_roll` | D3 | 4 |
| Servo 4 `R_hip_pitch` | D8 | 7 |
| Servo 5 `R_ankle_pitch` | D9 | 8 |
| I²C SDA (MPU6050, PCA9685) | D4 | 5 |
| I²C SCL | D5 | 6 |
| I²S BCLK (MAX98357A) | D6 | 43 |
| I²S LRC/WS | D7 | 44 |
| I²S DIN | D10 | 9 |
| Camera | interna (connettore B2B) | XCLK 10, SIOD 40, SIOC 39, Y9 48, Y8 11, Y7 12, Y6 14, Y5 16, Y4 18, Y3 17, Y2 15, VSYNC 38, HREF 47, PCLK 13 |
| Microfono PDM | interno | CLK 42, DATA 41 |

Nota: la slot microSD della Sense condivide D8–D10: **non usarla**.
Nota: la camera usa LEDC timer 0 per XCLK → i servo devono usare i timer LEDC 1–3.

### 2.2 Variante `PCA9685` (`#define USE_PCA9685 1`)
Servo sui canali PCA9685 0–5 (stesso ordine), LED occhi su canali 8 (sx) e 9 (dx),
D0 libero → partitore 200k/100k per leggere la tensione batteria (`VBAT = adc*3`),
D1 libero → trigger HC-SR04, D2 → echo HC-SR04 (con partitore 5V→3,3V).
Rail comune regolato a **5,0 V** per XIAO e MAX98357A. Se si usano 6 V per i servo, serve un rail separato a 5 V per elettronica e audio.
PCA9685 alimentato: VCC 3,3 V dal XIAO, V+ dallo step-down.

## 3. Cinematica e dimensioni (unità: mm, rad; frame robot: x avanti, y sinistra, z su)

6 giunti, 3 per gamba. Indici e nomi (identici ovunque):

| idx | nome | asse | limiti (rad) |
|---|---|---|---|
| 0 | `L_hip_roll` | x | −0.35 … +0.35 |
| 1 | `L_hip_pitch` | y | −0.80 … +0.80 |
| 2 | `L_ankle_pitch` | y | −0.80 … +0.80 |
| 3 | `R_hip_roll` | x | −0.35 … +0.35 |
| 4 | `R_hip_pitch` | y | −0.80 … +0.80 |
| 5 | `R_ankle_pitch` | y | −0.80 … +0.80 |

**Convenzione dei segni (speculare tra le gambe, semantica)**:
- `hip_roll` positivo = il piede si allontana dalla linea mediana (verso l'esterno).
- `hip_pitch` positivo = il piede va in avanti (+x).
- `ankle_pitch` positivo = punta del piede in su.
Ogni implementazione traduce questa semantica negli assi fisici (in MuJoCo con `axis` opportuni,
nel firmware con l'array `SERVO_DIR[]`).
Angolo servo (gradi) = `90 + SERVO_DIR[i] * deg(q_i) + CALIB_OFFSET[i]`.

Geometria (gambe "a trampolo", niente ginocchio: pochi motori, sim2real più facile):
- **Torso** (corpo): scatola arrotondata 56 (x) × 84 (y) × 72 (z), pareti 2 mm. Contiene 2×18650 orizzontali (asse y), XIAO in alto davanti con la camera che guarda dal "frontalino", speaker in basso davanti, MPU6050 al centro sul fondo.
- **Assi hip_roll**: lungo x, a y = ±22, 6 mm sotto il fondo del torso. I servo hip_roll sono dentro il fondo del torso.
- **Assi hip_pitch**: lungo y, 22 mm sotto l'asse hip_roll (servo nella `staffa_anca`).
- **Gamba**: dall'asse hip_pitch all'asse ankle_pitch = 68 mm (servo ankle alloggiato in basso nella `gamba`).
- **Piede**: suola 14 mm sotto l'asse caviglia, 76 (x) × 44 (y) × 4 spessore, centro spostato +6 mm in x.
- Altezza totale in piedi ≈ 72 + 6 + 22 + 68 + 14 = **182 mm**.

Masse (g): torso completo (batterie, elettronica, 2 servo hip_roll) 230; staffa_anca + servo hip_pitch 16 (×2); gamba + servo ankle 20 (×2); piede 12 (×2). Totale ≈ **326 g**.

Modello servo MG90S per la simulazione: controllo di posizione, velocità max 8 rad/s,
coppia max 0,2 N·m, banda morta ~0,017 rad, latenza di comando 10–30 ms (randomizzata),
**nessun feedback di posizione** (la politica non osserva gli angoli reali, solo le proprie azioni).

## 4. Parti stampabili (id usati da CAD, sito e viewer 3D)

| id | quantità | descrizione | stampa |
|---|---|---|---|
| `corpo` | 1 | guscio del torso, fondo con 2 tasche servo hip_roll, fori ventilazione | aperto verso l'alto, senza supporti |
| `coperchio` | 1 | coperchio posteriore/superiore a scatto con vano USB-C | piatto, senza supporti |
| `frontalino` | 1 | "faccia": finestra camera, 2 fori occhi LED 5 mm, griglia speaker, foro microfono | faccia sul piatto |
| `staffa_anca` | 2 | collega corno hip_roll ↔ corpo del servo hip_pitch | supporti solo sotto le alette (opz.) |
| `gamba` | 2 | trampolo 68 mm con alloggio servo ankle in basso | in piedi o coricata, senza supporti |
| `piede` | 2 | suola 76×44 con innesto corno servo, scanalature per gommini | piatto |

Impostazioni: PLA, ugello 0,4, layer 0,2, **2 pareti, infill 10–15 %** (gyroid), 4 top/bottom.
Sorgenti OpenSCAD parametriche in `hardware/parts/<id>.scad` (una parte per file) +
`hardware/pulcino_params.scad`. Gli STL si generano con OpenSCAD (nel workflow CI) in `stl/<id>.stl`.

## 5. Modalità di controllo e gait

Il firmware ha 3 generatori di movimento, selezionabili:
1. `pose`: pose a keyframe — `stand` (tutti 0), `rest` (hip_roll +0.25, gambe larghe, basso consumo), `wave` (sposta il peso a sinistra, solleva e agita il piede destro, suono "ciao", occhi lampeggianti).
2. `cpg`: camminata a oscillatori (parametri in `gait.json`, imparati nella **Palestra** del sito o scritti a mano).
3. `policy`: rete neurale (MLP) addestrata in MuJoCo con ARS, esportata in `policy.h`.

### 5.1 Formato `gait.json` (CPG)

Default dimostrativo ottenuto in MuJoCo; non validato sul robot reale.
```json
{
  "version": 1,
  "type": "cpg",
  "name": "ars-2026-09-29-demo-non-allenata-a-fondo",
  "freq": 2.2819,
  "joints": {
    "hip_roll": {
      "amp": 0.0638,
      "phase": -0.1908,
      "offset": 0.0454
    },
    "hip_pitch": {
      "amp": 0.0959,
      "phase": 1.4467,
      "offset": -0.0213
    },
    "ankle_pitch": {
      "amp": 0.0006,
      "phase": 1.9116,
      "offset": 0.0654
    }
  },
  "feedback": {
    "roll_gain": 0.0834,
    "pitch_gain": 0.1516
  }
}
```
Gamba sinistra: `q = offset + amp·sin(2π·freq·t + phase)`. Gamba destra: stessa formula con
`phase + π` (con la semantica speculare dei segni questo produce un'andatura alternata).
Feedback IMU: `hip_roll += roll_gain·roll_imu`, `ankle_pitch += pitch_gain·pitch_imu`.
Comandi: `vx∈[-1,1]` moltiplica `amp` di hip_pitch e ankle_pitch (negativo = indietro);
`wz∈[-1,1]` scala hip_pitch: sinistra ×(1−0.6·wz), destra ×(1+0.6·wz) (wz>0 = gira a sinistra);
`speed∈[0.3,1.5]` moltiplica `freq`. Tutto è limitato ai limiti dei giunti.

### 5.2 Politica RL (`policy.h`)
- 50 Hz. Osservazione (16): gravità nel frame corpo (3, dall'IMU), giroscopio (3, rad/s),
  ultima azione (6), comando `vx, wz` (2), `sin, cos` della fase del clock di passo (2, freq 1.6 Hz).
- Normalizzazione: `(obs - OBS_MEAN) / OBS_STD`, clip ±5.
- MLP 16 → 32 → 32 → 6, tanh. Azione a∈[-1,1]; `q_target = POSE_STAND + a · ACTION_SCALE` (ACTION_SCALE = {0.3,0.6,0.6} per roll/pitch/ankle), poi filtro passa-basso α=0.6 e clip ai limiti.
- `policy.h` definisce: `POLICY_VALID`, `POLICY_OBS`, `POLICY_H`, `POLICY_ACT`, `OBS_MEAN[]`, `OBS_STD[]`, `W1[] B1[] W2[] B2[] W3[] B3[]` (row-major, `W[out][in]`), `ACTION_SCALE[]`.
  Il repo include un placeholder con `POLICY_VALID 0` (il firmware allora ripiega sul CPG).

## 6. API del robot (firmware)

Rete: prova la Wi-Fi configurata (`WIFI_SSID`/`WIFI_PASS` in `config.h`); se fallisce apre un
Access Point `Pulcino-XXXX` (password `pulcino123`, IP 192.168.4.1). mDNS: `http://pulcino.local`.

| Endpoint | Porta | Descrizione |
|---|---|---|
| `GET /` | 80 | web app di controllo per iPhone (HTML embedded, "Aggiungi a Home") |
| `GET /stream` | 81 | video MJPEG (QVGA default) |
| `GET /capture` | 80 | singolo JPEG (usato dal Nido per l'LLM) |
| `GET /api/status` | 80 | JSON telemetria |
| `GET/POST /api/gait` | 80 | legge/scrive `gait.json` (salvato in LittleFS) |
| `GET/POST /api/calib` | 80 | legge/scrive `CALIB_OFFSET[6]` (salvato in NVS) |
| `WS /ws` | 80 | comandi + telemetria (JSON testo) |
| `WS /audio` | 80 | audio binario: robot→client PCM 16 kHz 16-bit mono dal mic; client→robot PCM 16 kHz 16-bit mono allo speaker |

Messaggi WS `/ws` (client → robot):
```
{"cmd":"move","vx":0.8,"wz":0.0}        // avanti/indietro/gira, continua finché non arriva stop o scade il watchdog (1 s senza messaggi)
{"cmd":"stop"}
{"cmd":"pose","name":"stand|rest|wave"}
{"cmd":"mode","m":"cpg|policy"}
{"cmd":"speed","v":1.0}
{"cmd":"servo","i":2,"deg":90}          // calibrazione: muove un singolo servo
{"cmd":"calib90"}                       // tutti i servo a 90° (+offset) per il montaggio
{"cmd":"beep","f":880,"ms":150}
{"cmd":"eyes","l":1,"r":1}
{"cmd":"hello","who":"nido|web","token":"..."}   // opzionale, identificazione
```
Robot → client, ~5 Hz: `{"t":"tel","roll":0.01,"pitch":-0.02,"vbat":7.9,"mode":"cpg","pose":"stand","moving":true,"rssi":-58,"clients":2}`.
Sicurezza: watchdog di movimento 1 s; se `API_TOKEN` in `config.h` è non vuoto, i comandi WS
e le POST richiedono `?token=...`.

## 7. Nido (cervello locale opzionale, cartella `nido/`)

Server Python (aiohttp) che gira su Mac/PC/Raspberry nella stessa rete (o via Tailscale):
- **Proxy HTTPS** della web app verso il robot → su iPhone funziona anche il **push-to-talk**
  (Safari concede il microfono solo in contesto sicuro).
- **Presa di controllo multiutente**: chiunque abbia il link può chiedere il controllo; un solo
  "pilota" alla volta con lease (60 s rinnovabili), coda, gli altri guardano; il proprietario
  (token admin) può sempre riprendere il controllo; link ospite con scadenza.
- **Autonomia**: quando nessuno pilota (o su richiesta), un loop ≤ 1 decisione/s manda
  immagine (`/capture`, se il modello è multimodale) + telemetria + trascrizione audio a un
  endpoint **OpenAI-compatibile** configurabile (Laya via mlx_lm.server, Ollama `http://localhost:11434/v1`, …)
  e riceve un'azione JSON da un set chiuso: `move`, `stop`, `pose`, `say`, `beep`, `eyes`, `wait`.
- **Routine locali** in `nido/routines/*.yaml` (es. `ronda`, `buongiorno` alle 8:00, `saluta_se_vedi_movimento`).
- **Voce**: `say` → TTS locale (macOS `say`/`piper`) → PCM 16 kHz → `WS /audio`. STT opzionale (whisper locale).
- Nessun dato esce dalla LAN se non lo decidi tu. Nessun IP personale nel repo (solo `config.example.yaml`).

## 8. Struttura del repo

```
site/            sito statico (HTML/CSS/JS, Three.js da CDN) — deploy GitHub Pages
  js/3d/         viewer Three.js condivisi + palestra (Rapier)
firmware/pulcino/ sketch Arduino (pulcino.ino + moduli .h/.cpp), config.h, policy.h placeholder
training/        MuJoCo MJCF + ARS in numpy + export policy.h
hardware/        OpenSCAD parametrico
nido/            cervello locale Python
scripts/         build.mjs (site → dist/, copia repo/ e STL), check.mjs (link), check-mobile.mjs (375 px)
.github/workflows/pages.yml
```
Sul sito pubblicato, i file sorgente del repo sono copiati in `repo/` (es. `repo/firmware/pulcino/pulcino.ino`)
così i link relativi funzionano; il link "GitHub" è ricavato a runtime da `<utente>.github.io/<repo>`.


## 9. Perturbazioni nella Palestra

La Home e la Palestra usano contatti Rapier, gravità e posizioni fisiche senza
correzioni artificiali dell'altezza. I viewer didattici delle pose restano cinematici.
La Palestra permette impulsi orizzontali sul torso (0,01–0,08 N·s), tocco del torso,
palline dinamiche da 20 g e 12 mm di raggio. Il verde indica suola a meno di 3 mm
dal piano; il giallo una suola sollevata. È un indicatore geometrico, non una misura
della forza di contatto. Gli urti delle palline sono contati dagli eventi di collisione.

Nell'allenamento si possono abilitare palline casuali, riproducibili con lo stesso
seed per le coppie di candidati. L'obiettivo equilibrio premia tempo in piedi e
penalizza inclinazione, deriva e cadute; la camminata premia anche avanzamento.
Le spinte manuali agiscono solo sul replay, senza contaminare la valutazione.
Cambiando obiettivo o perturbazioni si azzerano punteggi e miglior candidato.
Questa ricerca ottimizza il CPG e i suoi due guadagni IMU, non una rete neurale.
Per una politica reattiva MLP usare MuJoCo con `--cmd fermo` o `--cmd tutti`.
Il firmware corrente esegue una posa statica dopo stop: una politica allenata per
l'equilibrio da fermo richiede una modalità attiva dedicata prima del collaudo fisico.


## Curriculum server A→B (aggiornamento operativo)

Training persistente tramite `training/service.py`, un worker e budget medio
dell'80% di un core (worker cooperativo al 75%). Otto livelli: piano, terreno
random, ostacoli, terreno random con ostacoli; le stesse quattro condizioni con
inclinazione random ±3°. Traguardo 18 cm da A, piedi entro 3 cm da B, senza
caduta, appoggio e tempo massimo 20 secondi. Avanzamento solo dopo dieci
successi consecutivi della stessa politica su prove distinte dai semi di training.
Checkpoint atomici per livello, precedente training conservato. La Palestra
monitora ogni due secondi; la mappa deriva dalle verifiche MuJoCo, la scena
Rapier è una prova distinta. Il navigatore geometrico produce comandi locali:
non è apprendimento della navigazione visiva.
