# Canale MQTT scanner ↔ cloud

Come gli scanner c4rgo (Raspberry Pi, daemon Rust) comunicano con questo server,
e come testarli senza passare dalla web-app.

## Perché un bridge, e non una connessione diretta

I daemon dello scanner (`network`, `modem`, `barcode`, `cmd`, `mon`,
`temperature`) espongono una ricca API MQTT — leggono **e modificano** la
configurazione — ma solo sul broker **locale** del Raspberry. Il cloud non li
raggiunge: verso `my.c4rgo.cloud` esistono unicamente canali HTTPS (registrazione
`c4rgo-activate`, upload log di `c4rgo-mon` disabilitato, coda comandi DB legacy
`comandi_exec`), e l'MQTT del cloud serve i soli tracker SIM7600.

Gli scanner stanno dietro NAT/cellulare, quindi **il cloud non può connettersi in
ingresso**: la connessione deve partire dal device. Da qui la scelta: un broker
MQTT centrale (qui) verso cui ogni device fa da **bridge mosquitto uscente** in
TLS 8883, ponticellando il solo sottoalbero `c4rgo/{device_id}/#`.

Così si riusa lo schema di topic esistente e l'intera API dei daemon **senza
modificarli**, e il device continua a funzionare offline sul suo broker locale.

```
Scanner (RPi)                                  Cloud (questo repo)
┌───────────────────────────┐                 ┌──────────────────────────────┐
│ daemon Rust ─┐            │   TLS 8883      │  mosquitto (servizio "mqtt")  │
│ (network,    │  mosquitto │  uscente,       │       ▲          │            │
│  modem, ...) ┼─ locale ───┼── bridge ──────►│───────┘          ▼            │
│              │  :8883     │  NAT-friendly   │ scanner-listener   probe.py   │
└───────────────────────────┘                 │ (worker Symfony)  (r_server)  │
   c4rgo/{device_id}/#                        └──────────────────────────────┘
```

## File

| File | |
|---|---|
| `mosquitto.conf` | broker: TLS 8883 per gli scanner + listener locale in chiaro per i servizi cloud |
| `acl` | **versionata** (non è un segreto, va revisionata): isolamento per-device |
| `setup-broker.sh` | genera il cert del broker e il file `passwd` |
| `device-bridge.conf.example` | il conf del bridge da installare **sul device** |
| `.gitignore` | tiene fuori da git certificati e `passwd` |

Fuori da questa cartella: `../../src/Command/ScannerMqttListenerCommand.php`
(il worker `app:scanner:listener`), i servizi `mqtt` e `scanner-listener` in
`../../docker-compose.yml`, i parametri in `../../config/services.yaml` e `.env`.

## Setup

### 1. Broker (una volta)

```bash
cd docker/mqtt
# Serve la chiave privata della C4rgo-CA (dall'infrastruttura di provisioning:
# la stessa CA che ha firmato i cert dei device). Mettila qui come ca.key,
# accanto a ca.crt.
bash setup-broker.sh                       # genera certs/ e passwd
# poi un utente broker per OGNI device, con username == device_id:
mosquitto_passwd -b passwd kiosk_36b94b28 '<password-del-device>'
```

Imposta `SCANNER_MQTT_PASSWORD` (utente `r_server`) in `.env.local`, poi:

```bash
docker compose up -d mqtt scanner-listener
```

### 2. Bridge sul device

Copia `device-bridge.conf.example` in `/etc/mosquitto/conf.d/bridge_to_cloud.conf`
sul Raspberry, sostituisci `device_id`, `address` e `remote_password`, quindi
`systemctl restart mosquitto`. Stato del bridge:

```bash
mosquitto_sub -t '$SYS/broker/connection/#' -v
```

Perché il worker correli i messaggi, la `Macchine` corrispondente deve avere
**`mqttDeviceId` valorizzato col `device_id` dello scanner**.

## Sicurezza

- **TLS obbligatorio da remoto.** Il cert del broker è firmato dalla **C4rgo-CA**
  già presente sui device (`/opt/.c4rgo/etc/certs/ca.crt`), quindi la fidano
  senza distribuire nulla di nuovo. Il bridge verifica il nome del broker
  (`bridge_insecure false`).
- **Isolamento per-device.** L'ACL usa `pattern readwrite c4rgo/%u/#` con
  **username broker == `device_id`**: un device compromesso non può leggere né
  falsificare i topic di un altro. Non dare mai l'utente `r_server` a un device.
- **Namespace del bridge ristretto.** `topic c4rgo/{device_id}/# both 0`: anche
  un device compromesso ponticella solo il proprio sottoalbero.
- **Password in chiaro sul device.** Le credenziali broker stanno in chiaro nei
  `.conf` del Raspberry (partizione LUKS, ma chi ha root le legge). Usa password
  **diverse per ogni device**, così un leak non è sistemico.
- **Comandi distruttivi.** `cmd/execute_command_by_name reboot|poweroff`, i
  comandi `network/*` disruptive e `mon/service_*` sono potenti: il worker
  dovrebbe autorizzare per ruolo (la tabella `audit_log` esiste già) prima di
  pubblicare su `.../command`.
- Il broker legacy SIM7600 gira **in chiaro su 1883** (`172.17.0.1`): valuta di
  consolidarlo su questo broker TLS.

## Il canale legacy SIM7600 è deprecato

Il SIM7600 **non è più un dispositivo a sé**: è gestito dal daemon `c4rgo-modem`
che gira sullo scanner e pubblica su `c4rgo/{device_id}/modem/...`, quindi arriva
qui attraverso questo bridge. Il vecchio canale — topic `c4rgo/sim7600/...`,
broker `172.17.0.1:1883` **in chiaro**, microservizio `c4rgoservice` — è
**superato e da dismettere**.

Marcati `@deprecated`, non estenderli:
- `src/Command/Sim7600MqttListenerCommand.php` → sostituito da `ScannerMqttListenerCommand`
- `src/Services/SIM7600Service.php` → usa `ScannerMqttListenerCommand::sendCommand($id, 'modem', …)`
- le env `MQTT_*` (il canale attuale usa `SCANNER_MQTT_*`)

`ScannerMqttListenerCommand` popola **le stesse entità** (`Sim7600DeviceStatus`,
`GPS`) leggendo dai topic del daemon modem, quindi la web-app non cambia.

Finché il legacy non è spento i due set di env convivono: **non riusare le
`MQTT_*` per il canale scanner**, faresti connettere il worker al broker sbagliato.

## Cosa il daemon modem pubblica davvero

Attenzione, qui è facile sbagliare: `mqtt/client.rs` del daemon **dichiara** topic
che **nessuno pubblica mai**. Se progetti un consumer leggendo quella lista,
aspetterai dati che non arriveranno.

| Topic | Contenuto |
|---|---|
| `/modem/info` | **tutta la telemetria** (modem, SIM, rete, **segnale**, dati) — retained, 60s |
| `/modem/gps/data` | posizione — 30s, solo con fix |
| `/modem/heartbeat` | liveness — 30s |
| `/modem/status` | online/offline + LWT — retained |

**Mai pubblicati** (dichiarati ma senza publisher): `/modem/signal`,
`/modem/data/status`, `/modem/gps/status`, `/modem/sms/received`, `/modem/sms/sent`.
Il **segnale sta dentro `/modem/info`**, non su `/modem/signal`.

**`/modem/info` è retained, e il retained invecchia.** Il daemon smette di pubblicare
se ModemManager non vede il modem, ma **l'ultimo payload resta nel broker per sempre**
e viene consegnato a ogni nuovo sottoscrittore. Osservato sul campo: un `/modem/info`
del **30 aprile** ancora servito a **luglio**, con IMEI, operatore e segnale di allora.
Un consumer ingenuo lo interpreta come stato attuale e marca il device "online, appena
visto". Perciò: **usa il campo `timestamp` del payload**, non l'ora di ricezione, e
ricava la liveness da `/modem/heartbeat` (non retained) e `/modem/status` (aggiornato
dal LWT).

Altre trappole note:
- **`has_fix` non è pubblicato** su `/modem/gps/data`: va derivato da `fix_quality > 0`.
- **Non esistono contatori di traffico** (`bytes_sent`/`bytes_received`) da nessuna
  parte nel daemon: lasciarli a null, non a 0 (0 direbbe "nessun byte", non "non noto").
- Lo **stato dati reale** (`state`/`apn`/`ip_address` in `/modem/info`) è affidabile
  solo da **c4rgo-modem ≥ 2.12.2**: prima il daemon ci pubblicava un default fisso
  (sempre `Disconnected`, APN vuoto, IP nullo) anche a link attivo.

Dati non presenti nella telemetria (es. `has_fix` reale, satelliti in vista, salute
GNSS, stato dettagliato del bearer) si ottengono **solo su richiesta**, con i comandi
`gps_status` / `data_status` / `system_status` via `sendCommand()`.

## Testare senza la web-app

`c4rgo_scanner_probe.py` (repo `c4rgopy`, `scripts/`) parla direttamente col
broker: verifica la comunicazione e legge/modifica la configurazione dei daemon,
indipendentemente da web-app e bridge REST.

```bash
pip install paho-mqtt
# dal cloud, come r_server verso il broker centrale:
python3 c4rgo_scanner_probe.py --host my.c4rgo.cloud --username r_server \
  --password <pwd> --ca-cert ca.crt --device-id kiosk_36b94b28 status network
```

Sottocomandi: `ping` (get_status a tutti i service), `status <service>`,
`cmd <service> <type> '<payload-json>'` per leggere/modificare qualsiasi
configurazione, `listen` per lo sniff in tempo reale.

Il catalogo completo dei comandi dei sei daemon (con payload, e marcati
lettura / modifica / azione) è in `scripts/docs/C4RGO_MQTT_COMMANDS.md` dello
stesso repo.

Due asimmetrie del protocollo da conoscere se scrivi un altro client:

- **due formati di comando**: `CommandEnvelope`
  `{id, timestamp, command:{type, payload}}` per network/modem/cmd/mon/temperature,
  e formato **piatto** `{action, request_id, ...}` per barcode;
- **tre campi di correlazione** nella response: `command_id`
  (network/cmd/mon/temperature), `id` (modem), `request_id` (barcode).

## Stato

Verificato: il container Symfony compila, `app:scanner:listener` è registrato,
gli argomenti risolvono su `SCANNER_MQTT_*`, e il canale SIM7600 resta intatto.

**Non ancora verificato end-to-end**: richiede un broker cloud raggiungibile e
almeno un device con `mqttDeviceId` valorizzato.

## Debito noto

Il bridge è **per-device**: generare a mano `.conf` e utente broker per ogni
scanner non scala. Il punto naturale dove agganciare il provisioning è la
**registrazione** (`c4rgo-activate`), che già parla col cloud e conosce
`device_id` e hardware: lì il cloud può restituire anche le credenziali broker e
il device generarsi il bridge da solo.

Il worker oggi persiste solo l'ultimo contatto: per la telemetria ricca serve
un'entità dedicata (es. `ScannerState` con un campo `payload` json), sul modello
di `Sim7600DeviceStatusRepository::getOrCreate()`.
