Le champ `version` (offset 9 du payload NebuleAir Pro 4G) était hardcodé `0x01` côté firmware et n'a jamais porté un vrai versioning protocole. Renommé en `command` et réaffecté à un type de trame : - 0x00 = données mesure - 0x01 = ping test (firmware → Miotiq → backend, pas d'archivage) Versioning protocole reste sur `version_major/minor/patch`. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
252 lines
15 KiB
Markdown
252 lines
15 KiB
Markdown
# 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` = données mesure (les autres champs sont valides). `0x01` = 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` | 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. 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 |
|
||
|
||
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, 0–360 (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_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 == 0x01` (ping test), logger la réception (timestamp, `device_id`, `signal_quality`) sans archiver les mesures. Une trame `command` absent ou `0x00` est une trame de données normale.
|
||
|
||
## 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`. |
|