# Nido — il cervello locale (opzionale) di Pulcino

Il Nido è un piccolo server Python (aiohttp) che gira su un Mac, un PC o un Raspberry Pi nella
stessa rete del robot (o raggiungibile via Tailscale). Pulcino funziona benissimo anche senza:
il Nido aggiunge

- **HTTPS** e quindi il **push-to-talk** da iPhone (Safari dà il microfono solo a pagine sicure);
- **controllo multiutente**: un pilota alla volta, turni (lease) da 60 s, coda, spettatori,
  link ospite con scadenza, il proprietario può sempre riprendersi il comando;
- **un solo flusso video** verso il robot, rilanciato a tutti gli spettatori (l'ESP32 regge pochi client);
- **autonomia** con un LLM locale (Laya via `mlx_lm.server`, Ollama, qualsiasi endpoint OpenAI-compatibile),
  al massimo **1 decisione al secondo**;
- **routine** in YAML (ronda, sveglia delle 8:00, saluto quando vede movimento);
- **voce**: il robot parla con il TTS del Mac (`say`) o con `piper`; trascrizione opzionale con whisper locale.

```
iPhone ──HTTPS──▶ Nido ──WS/HTTP──▶ Pulcino (ESP32)
                   └──▶ LLM locale (OpenAI /v1)   └── routine, TTS/STT
```

## Installazione

Serve Python 3.11 o più recente.

```bash
cd nido
python3 -m venv .venv
.venv/bin/pip install -r requirements.txt
cp config.example.yaml config.yaml      # config.yaml non va nel repo
# modifica config.yaml: robot.host (pulcino.local o l'IP del robot), preset LLM, ecc.
.venv/bin/python -m nido --config config.yaml
```

Al primo avvio il Nido genera un **token admin** e un segreto per i link ospite in
`~/.pulcino-nido/` (permessi 600). Il link del proprietario si ottiene con:

```bash
.venv/bin/python -m nido --config config.yaml --link-admin
# → https://nido.tuo-tailnet.ts.net/#k=....
```

Aprilo sull'iPhone e fai "Condividi → Aggiungi alla schermata Home": l'icona conserva il link.
**Chi ha quel link è il proprietario**: non condividerlo, condividi invece i link ospite.

### Provare senza robot

```bash
.venv/bin/python fake_robot.py --port 8781 --stream-port 8782 --movimento &
.venv/bin/python -m nido --robot 127.0.0.1:8781:8782 --port 8765
.venv/bin/python -m nido --link-admin     # apri il link stampato nel browser
```

Il robot finto implementa l'API §6 della SPEC: WS `/ws` con telemetria e watchdog di 1 s,
`/capture` con un'immagine generata che cambia quando "cammina", `/stream` MJPEG, `/api/status`,
`/api/gait`, `/api/calib`, WS `/audio` (manda un tono tenue e conta i byte ricevuti).

### Test

```bash
.venv/bin/python -m pytest -q tests/
```

Coprono l'arbitro (lease, coda, presa di controllo admin, link ospite scaduti/manomessi/revocati,
rate-limit), il parsing delle risposte LLM "sporche" e un percorso end-to-end con robot finto e
finto endpoint OpenAI (controllo multiutente, proxy video con una sola connessione al robot,
autonomia, routine su movimento, audio, TTS con `say` su macOS).

## Accesso da fuori casa con Tailscale

Il modo consigliato è **`tailscale serve`**: il Nido resta in HTTP su `127.0.0.1` e Tailscale
aggiunge HTTPS con un certificato valido, solo per i dispositivi della tua tailnet.

```bash
# config.yaml: server.host: 127.0.0.1, server.port: 8765, server.public_url: https://<macchina>.<tailnet>.ts.net
tailscale serve --bg 8765
```

In alternativa, HTTPS diretto dal Nido con i certificati di Tailscale:

```bash
tailscale cert <macchina>.<tailnet>.ts.net     # crea .crt e .key
# config.yaml: server.tls_cert / server.tls_key con quei percorsi, server.host: 0.0.0.0
```

**`tailscale funnel`** rende il Nido raggiungibile da **tutta Internet** (utile per far guidare
un amico senza Tailscale). Avvertenze:
- chiunque abbia un link valido può vedere la tua casa attraverso la camera e muovere il robot;
- usa link ospite brevi (15–60 min) e revocali quando hai finito (`POST /api/revoca-ospiti`
  con il token admin, oppure riavvia dopo aver cancellato `~/.pulcino-nido/guest_secret`);
- tieni `access.require_link: true`; spegni il funnel (`tailscale funnel reset`) quando non serve;
- non lasciare il robot vicino a bordi di tavoli, scale o animali durante le sessioni remote.

## Controllo multiutente

- Chi apre un link valido entra come **spettatore** (video + telemetria).
- "**Prendi il controllo**": se nessuno pilota diventi **pilota**, altrimenti vai **in coda**.
- Il turno dura **60 s**; si rinnova (a mano o semplicemente guidando) **solo se nessuno è in coda**.
  Alla scadenza il controllo passa al primo della coda.
- Il **proprietario** (token admin) può sempre riprendersi il controllo: il pilota spodestato
  torna in testa alla coda.
- Se il pilota chiude la pagina o perde la connessione, il Nido manda subito **stop** al robot
  (e comunque il firmware si ferma da solo dopo 1 s senza comandi).
- Comandi limitati a 20/s per utente; calibrazione e cambio generatore (`servo`, `calib90`, `mode`)
  solo al proprietario.
- "**Condividi link ospite**" crea un link firmato HMAC con scadenza (60 min dall'app; un ospite
  può ricondividere solo fino alla propria scadenza).

## Autonomia con un LLM locale

Il loop parte solo quando accendi "Autonomia" (o, se `autonomy.auto_when_idle: true`, dopo
`idle_after_s` senza pilota) e si ferma appena un umano dà un comando. A ogni ciclo (≤ 1/s)
manda all'LLM: obiettivo, telemetria, ultime azioni, trascrizione (se attiva) e — se il modello
"vede" (`vision: true`) — il fotogramma della camera in base64. La risposta deve essere un JSON
del set chiuso `move` (vx, wz, secondi ≤ 3), `stop`, `pose` (stand/rest/wave), `say` (≤ 120
caratteri), `beep`, `eyes`, `wait`. Il parser tollera testo intorno, blocchi ```json, `<think>`,
apici singoli, virgole finali e sinonimi italiani; valori fuori range vengono limitati
(`max_vx`, `max_wz`); qualsiasi cosa non interpretabile diventa **stop**. Con errori ripetuti
dell'LLM il ritmo rallenta fino a una chiamata ogni 30 s. Il prompt di sistema è in
`prompts/sistema.txt`.

### Ollama

```bash
ollama pull qwen2.5vl:3b          # oppure llava:7b
# config.yaml → llm.preset: ollama   (http://localhost:11434/v1, vision: true)
# consigliato: llm.json_mode: true
```

### Laya / mlx_lm.server

```bash
mlx_lm.server --model <modello-mlx> --port 8081
# config.yaml → llm.preset: laya    (http://localhost:8081/v1, vision: false)
```

`mlx_lm.server` è solo testo: il robot decide da telemetria, storia e trascrizione. Qualsiasi altro
server compatibile (llama.cpp, LM Studio, vLLM) va bene: imposta `llm.base_url`, `llm.model`,
`llm.vision`.

## Routine

File YAML in `routines/` (vedi gli esempi `ronda.yaml`, `buongiorno.yaml`, `saluta.yaml`).
Trigger: `manuale` (pulsante nella web app), `orario` (`"08:00"`, giorni opzionali),
`movimento` (differenza tra due fotogrammi 32×24 in grigio, una cattura ogni
`routines.motion_interval_s` secondi, solo a robot fermo, libero e online). I passi usano le
stesse azioni (e la stessa validazione) dell'autonomia. Le routine automatiche non partono mai
mentre qualcuno pilota o l'autonomia è accesa.

## Voce

- **TTS**: `voice.tts: say` (macOS, voce italiana `Alice`; conversione con `afconvert` in PCM
  16 kHz) oppure `piper` con un modello italiano (`voice.piper_model`). L'audio va a WS `/audio`
  del robot a pezzi, al ritmo del tempo reale.
- **Push-to-talk** dalla web app: il microfono dell'iPhone viene ricampionato a 16 kHz PCM16 nel
  browser e inviato allo speaker del robot (solo pilota o proprietario). Richiede HTTPS.
- **Ascolta**: il microfono del robot in cuffia.
- **STT** (spento di default): `voice.stt: mlx-whisper` (Apple Silicon) o `faster-whisper`,
  installati a parte. Attivo solo mentre l'autonomia è accesa; la trascrizione va nel prompt.

## Consumi (il Nido è pensato per un Mac sempre acceso)

- nessun polling fisso: le scadenze (lease, link) sono gestite con timer "alla prossima scadenza";
- robot spento → riconnessione con backoff fino a 30 s;
- video e audio verso il robot aperti solo se qualcuno guarda/ascolta (chiusi dopo 5 s);
- autonomia spenta → zero chiamate all'LLM; accesa → al massimo 1 al secondo;
- rilevamento movimento solo se esiste una routine `movimento`, una cattura ogni 3 s.

## Sicurezza e privacy

- Tutto resta in locale: immagini e audio vanno solo al robot, al tuo LLM locale e ai browser
  collegati. **Nessun dato esce dalla LAN/tailnet se non lo decidi tu** (funnel, LLM remoto).
- Se punti `llm.base_url` a un servizio in cloud, le immagini della tua casa finiranno lì.
- Accesso solo con token admin o link ospite (HMAC-SHA256, scadenza); i token viaggiano nell'hash
  dell'URL (`#k=`, mai inviato ai server né nei Referer) e poi come query string solo verso il Nido;
  l'access log non registra le query string.
- CSP restrittiva, nessuna risorsa esterna (niente CDN), controllo dell'`Origin` sui WebSocket.
- Se nel firmware imposti `API_TOKEN`, mettilo anche in `robot.token`.
- Nel repo non ci sono IP o segreti personali: solo `config.example.yaml`.

## Avvio automatico (facoltativo)

Esempi in `deploy/`: `it.pulcino.nido.plist` (LaunchAgent macOS) e `pulcino-nido.service`
(systemd). Nessuno dei due viene installato automaticamente: copiali e adatta i percorsi.

## Struttura

```
nido/
  nido/            pacchetto: server, arbitro, robot, video, autonomia, llm, azioni, routine, voce
  web/             web app per iPhone (HTML/CSS/JS, nessuna dipendenza esterna)
  prompts/         prompt di sistema in italiano
  routines/        routine YAML d'esempio
  tests/           pytest
  fake_robot.py    simulatore dell'API del robot
  deploy/          launchd / systemd d'esempio
```

## Limiti noti

- Il proxy della web app del robot non è un proxy trasparente: il Nido serve la propria web app
  (stesse funzioni) e parla con il robot via API.
- Il rilevamento di movimento è volutamente semplice (differenza media tra fotogrammi): luci che
  cambiano o il robot che si muove possono dare falsi positivi (per questo lavora solo a robot fermo).
- Lo STT è sperimentale e non è coperto dai test automatici (librerie opzionali).
- La revoca dei link ospite invalida **tutti** i link ospite (rotazione del segreto).
