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.
This commit is contained in:
@@ -1,83 +1,234 @@
|
||||
# Format JSON canonique — mesures capteurs
|
||||
# Format JSON canonique — mesures capteurs AirCarto (2026)
|
||||
|
||||
Format recommandé pour tout nouvel envoi de mesures d'un capteur AirCarto vers un backend (HTTP POST, MQTT publish, webhook…).
|
||||
JSON envoyé par les capteurs AirCarto (ou par le webhook Miotiq en leur nom) au serveur central.
|
||||
|
||||
> Les chemins d'envoi existants (Miotiq binaire 17 octets, CSV) restent documentés dans [`parsers/udp-miotiq.md`](../parsers/udp-miotiq.md) pour compatibilité. **Ce JSON est la cible** pour les nouveaux développements.
|
||||
## Endpoint
|
||||
|
||||
## Schéma
|
||||
```
|
||||
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, 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 |
|
||||
|
||||
## Exemple complet (NebuleAir_Pro)
|
||||
|
||||
```json
|
||||
{
|
||||
"token": "string",
|
||||
"ts": 1713830400,
|
||||
"type_conn": "LTE-M",
|
||||
"measurements": {
|
||||
"pm1": 0.0,
|
||||
"pm25": 0.0,
|
||||
"pm10": 0.0,
|
||||
"temperature": 0.0,
|
||||
"humidity": 0.0
|
||||
},
|
||||
"gps": {
|
||||
"lat": 43.605,
|
||||
"lon": 1.444,
|
||||
"sats": 8
|
||||
},
|
||||
"link": {
|
||||
"signal": -78,
|
||||
"imsi": "208xxxxxxxxxxx"
|
||||
},
|
||||
"fw": "1.4.2"
|
||||
"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
|
||||
}
|
||||
```
|
||||
|
||||
## Champs
|
||||
## Notes d'intégration
|
||||
|
||||
| Champ | Type | Obligatoire | Description |
|
||||
|-------------------------|---------|-------------|--------------------------------------------------------------------|
|
||||
| `token` | string | oui | Identifiant unique du capteur (= clé en base `capteurs.capteurs`). |
|
||||
| `ts` | integer | oui | Timestamp Unix UTC en secondes de la mesure (pas de la réception). |
|
||||
| `type_conn` | string | oui | Un des : `WiFi`, `LTE-M`, `LTE-BIN`, `NB-IoT`, `LoRa`, `Ethernet`. |
|
||||
| `measurements.pm1` | number | si mesuré | µg/m³ |
|
||||
| `measurements.pm25` | number | si mesuré | µg/m³ |
|
||||
| `measurements.pm10` | number | si mesuré | µg/m³ |
|
||||
| `measurements.temperature` | number | si mesuré | °C |
|
||||
| `measurements.humidity` | number | si mesuré | % HR |
|
||||
| `gps.lat` | number | si GPS | Degrés décimaux WGS84, 6 décimales. |
|
||||
| `gps.lon` | number | si GPS | Idem. |
|
||||
| `gps.sats` | integer | si GPS | Nombre de satellites vus. |
|
||||
| `link.signal` | integer | non | RSSI en dBm (négatif) **ou** % de qualité ; préciser dans `type_conn`. |
|
||||
| `link.imsi` | string | non | IMSI de la SIM (modem cellulaire uniquement). |
|
||||
| `fw` | string | non | Version firmware du capteur, ex. `1.4.2`. |
|
||||
### Côté capteur / firmware
|
||||
|
||||
## Règles
|
||||
- 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 ».
|
||||
|
||||
- Omettre un champ si non mesuré. Ne pas envoyer `null` ni `-1` comme valeur sentinelle (hérité du legacy CSV Miotiq).
|
||||
- `ts` côté capteur si dispo (GPS / NTP), sinon côté serveur à la réception — documenter au cas par cas dans la doc du capteur.
|
||||
- Toutes les valeurs numériques utilisent le point `.` comme séparateur décimal.
|
||||
- Un seul capteur par message. Pas de batch (à revoir si besoin).
|
||||
### Côté backend
|
||||
|
||||
## Réponse attendue du serveur
|
||||
- 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.
|
||||
|
||||
```json
|
||||
{ "ok": true }
|
||||
```
|
||||
## Historique
|
||||
|
||||
Ou en cas d'erreur :
|
||||
|
||||
```json
|
||||
{ "ok": false, "error": "token inconnu" }
|
||||
```
|
||||
|
||||
HTTP 200 dans les deux cas pour ne pas déclencher de retry cellulaire côté modem ; le `ok: false` indique à la supervision qu'il y a un problème applicatif.
|
||||
|
||||
## Exemple minimal
|
||||
|
||||
```json
|
||||
{
|
||||
"token": "001",
|
||||
"ts": 1713830400,
|
||||
"type_conn": "WiFi",
|
||||
"measurements": { "pm25": 12.3 }
|
||||
}
|
||||
```
|
||||
| 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. |
|
||||
|
||||
Reference in New Issue
Block a user