Deployed firmwares hardcode command=0x01 (legacy from when the byte was named `version`); treating 0x01 as ping would have flagged every existing frame as a diagnostic. Keep 0x00/0x01 as normal-data and use 0x02 as the explicit ping trigger. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
16 KiB
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) surerror_flags,npm_status,device_statusuniquement : 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 colonneunitsdu descripteur est remplie et que l'export JSON estY(voirudp-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
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).
Polluants (codes ISO LCSQA)
Le mapping complet vit dans formats/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). 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)
{
"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 à
-1si 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 à0xFFuniquement 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_statussi== 255— c'est un firmware ancien, l'absence de diagnostic n'est pas une alarme. - Les
_unitsont purement informatifs / de debug. La valeur métier est toujours le champ sans suffixe. device_idest hex dans le JSON ; convertir en ASCII (bytes.fromhex(v).decode('ascii')) pour afficher le numéro de série lisible.- Lire
commandavant de persister la trame : sicommand == 0x02(ping test), logger la réception (timestamp,device_id,signal_quality) sans archiver les mesures. Une tramecommandabsent,0x00ou0x01est une trame de données normale (0x01= legacy, voir 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. |