# 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= 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 `_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 : `" "`. 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`. |