Files
aircarto-protocols/formats/json-payload.md
Paul Vuarambon efd1aa438a docs: align with AirCarto 2026 JSON template + fix ISO mapping
formats/json-payload.md: full rewrite around the actual server-side template
  (endpoint api.aircarto.com/receive_data?device_type=<model>, flat schema,
  _unit suffix companions, -1 and 255 sentinel semantics, full bitfield
  tables for error_flags/npm_status/device_status, misc context codes).

formats/iso-pollutant-codes.md: fill in the LCSQA mapping. Fixes my earlier
  inversion — ISO_39=PM2.5 and ISO_24=PM10 (not the other way). Add gases:
  ISO_03=NO2, ISO_04=CO, ISO_05=H2S, ISO_08=O3, ISO_21=NH3.

parsers/udp-miotiq.md:
  - string base function outputs hex (not ASCII) — update description and
    generic Python parser accordingly.
  - Fix ISO_39/ISO_24 labels in NebuleAir Pro 4G byte layout.
  - Name the 5 gases by offset, cross-link bitfield docs and JSON canonical.
  - New TODO: origin of latitude/longitude/misc in final JSON (not in 83B
    descriptor).

README.md: reflect the new file layout and data flow summary.
2026-04-23 00:55:25 +02:00

235 lines
13 KiB
Markdown
Raw 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 [`parsers/udp-miotiq.md`](../parsers/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). |
| `version` | int | Version du protocole de communication (pas la version firmware — voir `version_*` plus bas). |
## 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, 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 |
## Exemple complet (NebuleAir_Pro)
```json
{
"device_id": "4430353234313938",
"signal_quality": -22,
"signal_quality_unit": "-22 dB",
"version": 1,
"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.
## 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. |