Files
aircarto-protocols/formats/json-payload.md
Your Name ace447c933 docs(co2): ajouter ISO_17 (CO2, ppm) au protocole NebuleAir Pro 4G
- iso-pollutant-codes.md: code ISO_17 = CO2 (ppm), sentinelle 0xFFFF
- udp-miotiq.md: ISO_17 consomme les 2 octets reserved (offset 81),
  trame inchangee a 83 octets, retrocompat (0 = anciens fw)
- json-payload.md: tableau polluants, note sentinelle, exemple

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-01 17:44:25 +02:00

259 lines
17 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Format JSON canonique — mesures capteurs AirCarto (2026)
JSON envoyé par les capteurs AirCarto (ou par le webhook Miotiq en leur nom) au serveur central.
## Endpoint
```
POST https://api.aircarto.com/receive_data?device_type=<modèle>
Content-Type: application/json
```
Le **type de capteur** est passé en query string. Modèles supportés :
| `device_type` | Description |
|------------------|--------------------------------------------|
| `NebuleAir` | Station fixe NebuleAir |
| `NebuleAir_Pro` | Station fixe NebuleAir Pro (4G) |
| `ModuleAir` | Module AirCarto |
| `ModuleAir_Pro` | Module AirCarto Pro (4G) |
| `MobileAir` | Capteur mobile |
## Règles générales
- **Tous les champs sont optionnels** sauf `device_id`. Un capteur n'envoie que les champs qu'il mesure.
- **Valeur sentinelle `-1`** : donnée non disponible ou capteur non renseigné (ex. pas de GPS fix, pas de pression).
- **Valeur sentinelle `255` (`0xFF`)** sur `error_flags`, `npm_status`, `device_status` uniquement : le firmware du capteur est antérieur à l'introduction du champ. À interpréter comme « non disponible », **pas** comme « toutes les erreurs actives ». Les nouveaux firmwares initialisent ces octets et envoient une valeur ≤ 254.
- **Champs `<nom>_unit`** : optionnels, ajoutés par Miotiq quand la colonne `units` du descripteur est remplie et que l'export JSON est `Y` (voir [`udp-miotiq.md`](udp-miotiq.md)). Format : `"<valeur> <unité>"`. Le backend peut les ignorer, la valeur canonique est toujours le champ sans suffixe.
## Identification
| Champ | Type | Description |
|------------------|--------|--------------------------------------------------------------------------------------------------|
| `device_id` | string | Identifiant unique, **représentation hexadécimale** de 16 caractères (8 octets ASCII). Convertir hex → ASCII pour obtenir le numéro de série imprimé sur le boîtier. Ex. `"4430353234313938"``"D0524198"`. |
| `signal_quality` | int | Qualité du signal réseau (dB, souvent RSSI négatif). |
| `command` | int | Type de trame. `0x00` / `0x01` = données mesure (les autres champs sont valides ; `0x01` est conservé pour la rétrocompatibilité avec les firmwares qui hardcodaient l'ancien champ `version`). `0x02` = ping test (trame émise volontairement par le firmware pour vérifier le lien capteur → Miotiq → backend ; le backend ne doit pas archiver les mesures associées). Voir [Commande / type de trame](#commande--type-de-trame). |
## Commande / type de trame
Le champ `command` (1 octet, offset 9 du payload binaire) discrimine la nature de la trame.
| Valeur | Sens | Action backend attendue |
|---------|------------------|---------------------------------------------------------------------------------------------------------------|
| `0x00` | données | Trame de mesure normale. Décoder et persister les champs métier comme d'habitude. |
| `0x01` | données (legacy) | Identique à `0x00`. Émis par les firmwares antérieurs au renommage `version``command` qui hardcodaient cet octet à `0x01` ; conservé comme alias de mesure normale pour ne pas casser le parc déployé. |
| `0x02` | ping test | Trame de diagnostic émise par le firmware pour vérifier le chemin capteur → Miotiq → backend. Logger la réception (timestamp, `device_id`, `signal_quality`) puis **ne pas archiver** les autres champs comme mesures réelles — leur contenu n'est pas garanti significatif. |
Toute autre valeur doit être traitée comme une trame de données (`0x00`) en attendant qu'elle soit officiellement allouée — éviter de rejeter la trame sur la seule base d'un `command` inconnu pour rester forward-compatible.
**Note historique** : ce champ s'appelait `version` avant 2026-04-27 et était hardcodé `0x01` côté firmware. C'est cette valeur héritée qui force `0x01` à rester une trame de données et pousse le ping sur `0x02`. Le versioning du protocole est exclusivement porté par `version_major/minor/patch` (voir [Version firmware](#version-firmware)).
## Polluants (codes ISO LCSQA)
Le mapping complet vit dans [`formats/iso-pollutant-codes.md`](iso-pollutant-codes.md). Liste rappel :
| Champ | Grandeur | Unité |
|-----------|--------------|--------|
| `ISO_68` | PM1 | µg/m³ |
| `ISO_39` | PM2.5 | µg/m³ |
| `ISO_24` | PM10 | µg/m³ |
| `ISO_54` | Température | °C |
| `ISO_55` | Humidité | % |
| `ISO_53` | Pression | hPa |
| `ISO_03` | NO₂ | ppb |
| `ISO_05` | H₂S | ppb |
| `ISO_21` | NH₃ | ppb |
| `ISO_04` | CO | ppb |
| `ISO_08` | O₃ | ppb |
| `ISO_17` | CO₂ | ppm |
> **CO₂ (`ISO_17`)** : encodé en **ppm bruts** (`uint16`), pas en ppb comme les gaz traces. Comme ce champ vient d'un descripteur Miotiq `hex2dec` (non signé), il ne peut pas porter la sentinelle `-1` : un capteur sans CO₂ envoie `0xFFFF` (65535) — ou `0` sur les firmwares qui émettaient encore l'ancien bloc `reserved`. Le backend traite `ISO_17 ∈ {0, 65535}` comme « CO₂ non disponible ». Voir [`udp-miotiq.md`](udp-miotiq.md).
Les codes ISO vont théoriquement de `ISO_01` à `ISO_99`. Seuls les polluants effectivement mesurés par le capteur sont présents dans le JSON.
## Bruit
| Champ | Unité | Description |
|-------------------|-------|-----------------------------------------------------|
| `noise_cur_leq` | dB | Niveau sonore équivalent continu (Leq) courant. |
| `noise_cur_level` | dB | Niveau sonore instantané courant. |
| `max_noise` | dB | Niveau sonore maximal sur la période. |
## Comptage particulaire NPM (Naneos Partector)
| Champ | Unité | Description |
|----------------|-------|--------------------------------------|
| `npm_ch1` | count | Comptage canal 1. |
| `npm_ch2` | count | Comptage canal 2. |
| `npm_ch3` | count | Comptage canal 3. |
| `npm_ch4` | count | Comptage canal 4. |
| `npm_ch5` | count | Comptage canal 5. |
| `npm_temp` | °C | Température interne du module NPM. |
| `npm_humidity` | % | Humidité interne du module NPM. |
| `npm_status` | int | Statut NPM — bitfield, voir ci-dessous. |
## Alimentation
| Champ | Unité | Description |
|-------------------|-------|------------------------------------|
| `battery_voltage` | V | Tension batterie. |
| `battery_current` | A | Courant batterie. |
| `solar_voltage` | V | Tension panneau solaire. |
| `solar_power` | W | Puissance panneau solaire. |
| `charger_status` | int | Code de statut du chargeur MPPT. |
## Vent
| Champ | Unité | Description |
|------------------|---------|-----------------------------------|
| `wind_speed` | m/s | Vitesse du vent. |
| `wind_direction` | degrés | Direction du vent, 0360 (0 = Nord). |
## Diagnostic & firmware
### `error_flags` — bitfield système (1 octet)
Erreurs matérielles détectées par le capteur. `255` (0xFF) = firmware ancien, champ non supporté.
| Bit | Masque | Nom | Signification |
|-----|--------|---------------------|--------------------------------------------------------------|
| 0 | 0x01 | `RTC_DISCONNECTED` | Module RTC DS3231 non détecté (I2C). |
| 1 | 0x02 | `RTC_RESET` | RTC en date par défaut (année 2000). |
| 2 | 0x04 | `BME280_ERROR` | Capteur BME280 non détecté ou erreur. |
| 3 | 0x08 | `NPM_ERROR` | Capteur NextPM non détecté ou erreur. |
| 4 | 0x10 | `ENVEA_ERROR` | Capteurs Envea non détectés ou erreur. |
| 5 | 0x20 | `NOISE_ERROR` | Capteur bruit NSRT MK4 non détecté. |
| 6 | 0x40 | `MPPT_ERROR` | Chargeur solaire MPPT non détecté. |
| 7 | 0x80 | `WIND_ERROR` / `CO2_ERROR` | NebuleAir : vent non détecté. ModuleAir : CO₂ non détecté. |
Exemple : `error_flags = 5` → RTC déconnecté + BME280 en erreur.
### `npm_status` — bitfield NextPM (1 octet)
Registre d'état interne du capteur NextPM. Copie du byte `STATE` de la trame UART NextPM (voir [`sensors/nextpm.md`](../sensors/nextpm.md)). `255` = firmware ancien.
| Bit | Masque | Nom | Signification |
|-----|--------|------------------|------------------------------------------------------|
| 0 | 0x01 | `SLEEP_STATE` | Capteur en veille. |
| 1 | 0x02 | `DEGRADED_STATE` | Erreur mineure, précision réduite. |
| 2 | 0x04 | `NOT_READY` | Démarrage en cours (~15 s). |
| 3 | 0x08 | `HEAT_ERROR` | Humidité > 60 % pendant > 10 min. |
| 4 | 0x10 | `TRH_ERROR` | T/HR interne hors spécification. |
| 5 | 0x20 | `FAN_ERROR` | Ventilateur hors plage. |
| 6 | 0x40 | `MEMORY_ERROR` | Accès mémoire impossible. |
| 7 | 0x80 | `LASER_ERROR` | Aucune particule > 240 s, erreur laser. |
Exemple : `npm_status = 40``HEAT_ERROR` + `FAN_ERROR`.
### `device_status` — bitfield boîtier (1 octet)
État général du boîtier capteur. `255` = firmware ancien.
| Bit | Masque | Nom | Signification |
|-----|--------|------------------|--------------------------------------------------------------------|
| 0 | 0x01 | `SARA_REBOOTED` | Modem SARA a rebooté (hardware) au cycle précédent. |
| 1 | 0x02 | `WIFI_CONNECTED` | Device connecté en WiFi (mode atelier). |
| 2 | 0x04 | `HOTSPOT_ACTIVE` | Hotspot WiFi actif (mode configuration). |
| 3 | 0x08 | `GPS_NO_FIX` | Pas de position GPS valide. |
| 4 | 0x10 | `BATTERY_LOW` | Tension batterie sous seuil critique. |
| 5 | 0x20 | `DISK_FULL` | Espace disque critique (< 5 %). |
| 6 | 0x40 | `DB_ERROR` | Erreur d'accès à la base SQLite locale. |
| 7 | 0x80 | `BOOT_RECENT` | Device redémarré récemment (uptime < 5 min). |
Exemple : `device_status = 145` (0x91) → modem reboot + batterie faible + boot récent.
### Version firmware
| Champ | Type | Description |
|------------------|------|-----------------------------------------------|
| `version_major` | int | Numéro majeur (`X.y.z`). |
| `version_minor` | int | Numéro mineur (`x.Y.z`). |
| `version_patch` | int | Numéro de patch (`x.y.Z`). |
Reconstitution : `f"{version_major}.{version_minor}.{version_patch}"` → ex. `"1.2.3"`.
## Géolocalisation & contexte
| Champ | Type | Unité | Description |
|-------------|--------|---------|-------------------------------------------------------------------|
| `latitude` | number | degrés | Latitude GPS WGS84 (décimal). |
| `longitude` | number | degrés | Longitude GPS WGS84 (décimal). |
| `misc` | int | | Contexte de mesure, voir table ci-dessous. |
| `misc` | Contexte |
|--------|-----------------------------------------|
| 0 | Aucun |
| 1 | Mesure en intérieur |
| 2 | Mesure en extérieur |
| 3 | Mesure en voiture |
| 4 | Mesure en piéton |
| 5 | Mesure en vélo |
| 6 | Mesure en transport en commun |
Enum extensible — de nouvelles valeurs peuvent être ajoutées (7, 8, …) sans casser l'encodage 1 octet. Un consommateur qui rencontre une valeur inconnue doit la traiter comme *contexte non renseigné* (équivalent à `0`), pas rejeter la trame.
## Exemple complet (NebuleAir_Pro)
```json
{
"device_id": "4430353234313938",
"signal_quality": -22,
"signal_quality_unit": "-22 dB",
"command": 0,
"ISO_68": 0.8, "ISO_68_unit": "0,8 ugm3",
"ISO_17": 437, "ISO_17_unit": "437 ppm",
"ISO_54": 25.5, "ISO_54_unit": "25.5 °C",
"noise_cur_leq": 25.5, "noise_cur_leq_unit": "25,5 dB",
"noise_cur_level": 25.5, "noise_cur_unit": "25,5 dB",
"max_noise": 25.5, "max_noise_unit": "25,5 dB",
"npm_ch1": 255, "npm_ch1_unit": "255 nb",
"npm_ch2": 255, "npm_ch2_unit": "255 nb",
"npm_ch3": 255, "npm_ch3_unit": "255 nb",
"npm_ch4": 255, "npm_ch4_unit": "255 nb",
"npm_ch5": 255, "npm_ch5_unit": "255 nb",
"battery_voltage": 25.5, "battery_voltage_unit": "25,5 V",
"battery_current": 25.5, "battery_current_unit": "25,5 A",
"solar_voltage": 25.5, "solar_voltage_unit": "25,5 V",
"solar_power": 255, "solar_power_unit": "255 W",
"npm_temp": 25.5, "npm_temp_unit": "25,5 °C",
"npm_humidity": 25.5, "npm_humidity_unit": "25,5 %",
"wind_speed": 25.5, "wind_speed_unit": "25,5 m/s",
"wind_direction": 255, "wind_direction_unit": "255 degrees",
"charger_status": 255, "charger_status_unit": "255",
"error_flags": 0,
"npm_status": 0,
"device_status": 0,
"version_major": 1,
"version_minor": 2,
"version_patch": 3,
"latitude": 43.2964,
"longitude": 5.36978,
"misc": 2
}
```
## Notes d'intégration
### Côté capteur / firmware
- Envoyer uniquement les champs mesurés par ton modèle. Omettre les autres, ou les remplir à `-1` si leur présence est structurellement attendue par un parser amont (ex. descripteur Miotiq à taille fixe).
- Initialiser `error_flags`, `npm_status`, `device_status` à `0`. Les laisser à `0xFF` uniquement si tu ne sais pas renseigner (= firmware non migré), pour que le backend interprète bien « non disponible ».
### Côté backend
- Ignorer toute valeur `-1` (ne pas la stocker comme une mesure).
- Ignorer `error_flags`, `npm_status`, `device_status` si `== 255` — c'est un firmware ancien, l'absence de diagnostic n'est pas une alarme.
- Les `_unit` sont purement informatifs / de debug. La valeur métier est toujours le champ sans suffixe.
- `device_id` est **hex** dans le JSON ; convertir en ASCII (`bytes.fromhex(v).decode('ascii')`) pour afficher le numéro de série lisible.
- Lire `command` **avant** de persister la trame : si `command == 0x02` (ping test), logger la réception (timestamp, `device_id`, `signal_quality`) sans archiver les mesures. Une trame `command` absent, `0x00` ou `0x01` est une trame de données normale (`0x01` = legacy, voir [Commande / type de trame](#commande--type-de-trame)).
## Historique
| Date | Révision | Changement |
|------------|----------|---------------------------------------------------------------------------|
| 2026-04-23 | v1 | Version initiale inventée (schéma imbriqué avec `token`/`ts`/`measurements`) — remplacée. |
| 2026-04-23 | v2 | **Format officiel AirCarto 2026** : schéma plat, bitfields détaillés, compat firmware ancien. |
| 2026-04-27 | v3 | Champ `version` (offset 9 du binaire, hardcodé `0x01` côté firmware) renommé `command` et réaffecté à un type de trame : `0x00` = données, `0x01` = ping test. Ajout de la section « Commande / type de trame ». Versioning protocole reste sur `version_major/minor/patch`. |
| 2026-04-27 | v4 | Rétrocompatibilité : ping test déplacé de `0x01` vers `0x02`. Les firmwares déployés émettent déjà `0x01` (héritage de l'ancien champ `version`) ; les compter comme pings aurait masqué toutes leurs mesures. `0x00` et `0x01` sont désormais tous deux des trames de données normales, `0x02` est le déclencheur explicite du ping. |
| 2026-06-01 | v5 | Ajout du CO₂ (`ISO_17`, ppm) au tableau des polluants et à l'exemple NebuleAir_Pro. Sentinelle « non disponible » = `0` ou `65535` (`0xFFFF`) car le champ vient d'un descripteur Miotiq `hex2dec` non signé (pas de `-1` possible). |