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:
11
README.md
11
README.md
@@ -40,6 +40,13 @@ aircarto-protocols/
|
||||
|
||||
## Pourquoi ce repo
|
||||
|
||||
Avant : chaque firmware AirCarto (NebuleAir, ModuleAir, MobileAir…) redéfinissait ses trames, ses topics et son format JSON dans son coin. Les parsers côté serveur (`data.mobileair.fr/udp_miotiq_*.php`, `gestion.aircarto.fr`) devaient suivre. Résultat : dérives silencieuses entre capteurs, bugs d'intégration.
|
||||
Avant : chaque firmware AirCarto (NebuleAir, ModuleAir, MobileAir…) redéfinissait ses trames et son format JSON dans son coin. Les parsers côté serveur (`data.mobileair.fr/udp_miotiq_*.php`, `gestion.aircarto.fr`) devaient suivre. Résultat : dérives silencieuses entre capteurs, bugs d'intégration.
|
||||
|
||||
Ici on centralise la **spécification**. Le code de référence reste dans les repos des projets (firmwares, backends) ; ce repo décrit ce qui est attendu sur le fil.
|
||||
Ici on centralise la **spécification** :
|
||||
|
||||
- **Capteur → Miotiq** : payload UDP binaire, décodé côté Miotiq via un *descripteur* ([`parsers/udp-miotiq.md`](parsers/udp-miotiq.md)).
|
||||
- **Miotiq → serveur AirCarto** : JSON canonique 2026 ([`formats/json-payload.md`](formats/json-payload.md)) posté sur `api.aircarto.com/receive_data`.
|
||||
- **Vocabulaire polluants** : codes ISO LCSQA ([`formats/iso-pollutant-codes.md`](formats/iso-pollutant-codes.md)).
|
||||
- **Capteurs physiques** : docs individuelles sous `sensors/` (protocole UART/I2C, câblage, commandes).
|
||||
|
||||
Le code de référence reste dans les repos des projets (firmwares, backends) ; ce repo décrit ce qui est attendu sur le fil.
|
||||
|
||||
@@ -1,37 +1,37 @@
|
||||
# Codes polluants ISO — convention AirCarto
|
||||
|
||||
Les descripteurs Miotiq (voir [`parsers/udp-miotiq.md`](../parsers/udp-miotiq.md)) utilisent des codes `ISO_XX` pour désigner les grandeurs physiques et polluants. Ces codes sont inspirés de la norme **ISO 7168-2** (échange de données de qualité de l'air) et du vocabulaire EIONET / Directive européenne 2008/50/CE, mais le mapping exact **est la convention AirCarto** — à verrouiller ici.
|
||||
Les descripteurs Miotiq (voir [`parsers/udp-miotiq.md`](../parsers/udp-miotiq.md)) et le JSON canonique (voir [`json-payload.md`](json-payload.md)) utilisent des codes `ISO_XX` pour désigner les polluants et grandeurs physiques. Ces codes suivent la nomenclature du **LCSQA** (Laboratoire Central de Surveillance de la Qualité de l'Air), basée sur la norme **ISO 7168**.
|
||||
|
||||
Les codes vont théoriquement de `ISO_01` à `ISO_99`. Seuls ceux effectivement mesurés par au moins un capteur AirCarto sont documentés ici.
|
||||
|
||||
## Mapping
|
||||
|
||||
| Code | Grandeur / polluant | Unité de référence | Scale standard | Remarques |
|
||||
|----------|---------------------|--------------------|----------------|-----------|
|
||||
| `ISO_03` | *à confirmer* (gaz) | ppb | | Candidat : O₃ (ozone) |
|
||||
| `ISO_04` | *à confirmer* (gaz) | ppb | | Candidat : NO₂ |
|
||||
| `ISO_05` | *à confirmer* (gaz) | ppb | | Candidat : NO ou SO₂ |
|
||||
| `ISO_08` | *à confirmer* (gaz) | ppb | | Candidat : CO |
|
||||
| `ISO_21` | *à confirmer* (gaz) | ppb | | |
|
||||
| `ISO_24` | PM2.5 | µg/m³ | x/10 | Poussières fines |
|
||||
| `ISO_39` | PM10 | µg/m³ | x/10 | |
|
||||
| Code | Grandeur / polluant | Unité de référence | Equation descripteur Miotiq | Remarques |
|
||||
|----------|---------------------|--------------------|------------------------------|------------------------------|
|
||||
| `ISO_03` | NO₂ — dioxyde d'azote | ppb | | |
|
||||
| `ISO_04` | CO — monoxyde de carbone | ppb | | |
|
||||
| `ISO_05` | H₂S — sulfure d'hydrogène | ppb | | |
|
||||
| `ISO_08` | O₃ — ozone | ppb | | |
|
||||
| `ISO_21` | NH₃ — ammoniac | ppb | | |
|
||||
| `ISO_24` | PM10 | µg/m³ | `x/10` | Particules ≤ 10 µm |
|
||||
| `ISO_39` | PM2.5 | µg/m³ | `x/10` | Particules fines ≤ 2.5 µm |
|
||||
| `ISO_53` | Pression | hPa | | Pression atmosphérique |
|
||||
| `ISO_54` | Température | °C | x/100 | |
|
||||
| `ISO_55` | Humidité relative | % | x/100 | |
|
||||
| `ISO_68` | PM1 | µg/m³ | x/10 | |
|
||||
| `ISO_54` | Température | °C | `x/100` | |
|
||||
| `ISO_55` | Humidité relative | % | `x/100` | |
|
||||
| `ISO_68` | PM1 | µg/m³ | `x/10` | Particules ultrafines ≤ 1 µm |
|
||||
|
||||
## Règles
|
||||
|
||||
- Un code ISO est **unique** au sein de l'écosystème AirCarto : pas de redéfinition par capteur.
|
||||
- Un nouveau polluant / nouvelle grandeur suit la numérotation ISO 7168-2 si elle existe ; sinon on alloue le prochain code libre au-dessus de 100 et on le documente ici.
|
||||
- Unité de référence : celle stockée dans InfluxDB (donc celle après application de `scale`). Les firmwares peuvent encoder à une précision différente, le `scale` ramène à l'unité finale.
|
||||
- Pour ajouter un polluant :
|
||||
1. Chercher son code dans la nomenclature LCSQA / ISO 7168.
|
||||
2. S'il existe, ajouter la ligne ici avec l'unité de référence et l'équation standard.
|
||||
3. S'il n'existe pas, allouer le prochain code libre > 70 et documenter la décision en « À faire » ci-dessous.
|
||||
- **Unité de référence** = unité stockée en base (après application de l'équation du descripteur). Les firmwares peuvent encoder à une précision différente, l'équation ramène à l'unité finale.
|
||||
- Les grandeurs **non-polluantes** propres aux capteurs AirCarto (bruit, vent, batterie, solaire…) ne reçoivent **pas** de code ISO : elles gardent leur nom `snake_case` (`wind_speed`, `battery_voltage`, etc.). Seuls les polluants atmosphériques et paramètres ambiants standard utilisent `ISO_XX`.
|
||||
|
||||
## À faire
|
||||
## Sources
|
||||
|
||||
- [ ] Remplir les gaz `ISO_03`, `ISO_04`, `ISO_05`, `ISO_08`, `ISO_21` — les 5 gaz du NebuleAir Pro 4G.
|
||||
- [ ] Vérifier que le mapping ci-dessus correspond bien à la norme ISO 7168-2 version utilisée en interne chez AirCarto.
|
||||
- [ ] Ajouter les codes pour les grandeurs qui n'existent pas encore en ISO : bruit (`noise_cur_leq`, etc.), vent, batterie, solaire — ou laisser en noms `snake_case` non-ISO comme aujourd'hui dans le descripteur.
|
||||
|
||||
## Références
|
||||
|
||||
- ISO 7168-2:1999 — Air quality — Exchange of data, Part 2: Condensed data format.
|
||||
- EIONET Air Quality data flow — https://www.eionet.europa.eu/
|
||||
- Directive 2008/50/CE — annexe listant les codes polluants européens.
|
||||
- **LCSQA** — nomenclature polluants : https://www.lcsqa.org/
|
||||
- **ISO 7168-2:1999** — Air quality — Exchange of data, Part 2: Condensed data format.
|
||||
- **Directive 2008/50/CE** — codes polluants européens.
|
||||
|
||||
@@ -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. |
|
||||
|
||||
@@ -5,10 +5,10 @@
|
||||
Chemin de données :
|
||||
|
||||
```
|
||||
Capteur ──UDP──> Miotiq ──HTTPS POST JSON──> data.*.aircarto.fr/udp_miotiq_*.php ──> PostgreSQL + InfluxDB
|
||||
Capteur ──UDP──> Miotiq ──HTTPS POST JSON──> api.aircarto.com/receive_data ──> PostgreSQL + InfluxDB
|
||||
```
|
||||
|
||||
**Principe** : chaque capteur a un **descripteur Miotiq** (format texte pipe-séparé) qui décrit l'ordonnancement et le décodage de sa charge utile. Miotiq applique ce descripteur à l'ingestion, et c'est aussi lui qui sert de **contrat** pour le firmware capteur et le parser serveur.
|
||||
**Principe** : chaque capteur a un **descripteur Miotiq** (format texte pipe-séparé) qui décrit l'ordonnancement et le décodage de sa charge utile. Miotiq applique ce descripteur à l'ingestion, et produit le **JSON canonique AirCarto** (voir [`formats/json-payload.md`](../formats/json-payload.md)) qui est posté sur le backend.
|
||||
|
||||
## Enveloppe JSON reçue du webhook
|
||||
|
||||
@@ -53,7 +53,7 @@ Un descripteur est une suite de lignes, une par champ. **Format officiel Miotiq*
|
||||
|
||||
| Valeur | Effet |
|
||||
|------------|-------------------------------------------------------------------------------------------------|
|
||||
| `string` | Décode les octets comme ASCII (non null-terminé, padé à droite). |
|
||||
| `string` | Sort la **représentation hexadécimale** des octets tels quels (n'effectue *pas* de décodage ASCII). Ex. `device_id` 8 octets `0x44 30 35 32 34 31 39 38` sort `"4430353234313938"` ; le client convertit en ASCII si besoin (`bytes.fromhex(v).decode("ascii")` → `"D0524198"`). |
|
||||
| `hex2dec` | Convertit les octets en entier non-signé **big-endian**. |
|
||||
| `hex2bin` | Convertit les octets en chaîne binaire (bitfield lisible, utile pour les registres d'état). |
|
||||
| `userdef` | Pas de transformation — les octets bruts sont passés tels quels comme valeur. |
|
||||
@@ -124,23 +124,23 @@ Layout octet par octet :
|
||||
|
||||
| Offset | Taille | Champ | Unité | Scale | Notes |
|
||||
|--------|--------|-------------------|---------|--------|------------------------------------------|
|
||||
| 0 | 8 | `device_id` | | | ASCII, identifiant capteur |
|
||||
| 0 | 8 | `device_id` | | | Hex 16 chars, converti en ASCII côté client (ex. `"D0524198"`) |
|
||||
| 8 | 1 | `signal_quality` | dB | | Signal cellulaire |
|
||||
| 9 | 1 | `version` | | | Version encodée (complète en v_major/minor/patch plus bas) |
|
||||
| 9 | 1 | `version` | | | Version du protocole de communication |
|
||||
| 10 | 2 | `ISO_68` | µg/m³ | /10 | PM1 |
|
||||
| 12 | 2 | `ISO_39` | µg/m³ | /10 | PM10 |
|
||||
| 14 | 2 | `ISO_24` | µg/m³ | /10 | PM2.5 |
|
||||
| 12 | 2 | `ISO_39` | µg/m³ | /10 | PM2.5 |
|
||||
| 14 | 2 | `ISO_24` | µg/m³ | /10 | PM10 |
|
||||
| 16 | 2 | `ISO_54` | °C | /100 | Température |
|
||||
| 18 | 2 | `ISO_55` | % HR | /100 | Humidité |
|
||||
| 20 | 2 | `ISO_53` | hPa | | Pression |
|
||||
| 22 | 2 | `noise_cur_leq` | dB | /10 | Leq instantané |
|
||||
| 24 | 2 | `noise_cur_level` | dB | /10 | Niveau sonore courant |
|
||||
| 26 | 2 | `max_noise` | dB | /10 | Crête |
|
||||
| 28 | 2 | `ISO_03` | ppb | | Gaz — voir `formats/iso-pollutant-codes.md` |
|
||||
| 30 | 2 | `ISO_05` | ppb | | Gaz |
|
||||
| 32 | 2 | `ISO_21` | ppb | | Gaz |
|
||||
| 34 | 2 | `ISO_04` | ppb | | Gaz |
|
||||
| 36 | 2 | `ISO_08` | ppb | | Gaz |
|
||||
| 28 | 2 | `ISO_03` | ppb | | NO₂ |
|
||||
| 30 | 2 | `ISO_05` | ppb | | H₂S |
|
||||
| 32 | 2 | `ISO_21` | ppb | | NH₃ |
|
||||
| 34 | 2 | `ISO_04` | ppb | | CO |
|
||||
| 36 | 2 | `ISO_08` | ppb | | O₃ |
|
||||
| 38 | 2 | `npm_ch1` | count | | NextPM — nombre de particules canal 1 |
|
||||
| 40 | 2 | `npm_ch2` | count | | NextPM canal 2 |
|
||||
| 42 | 2 | `npm_ch3` | count | | NextPM canal 3 |
|
||||
@@ -155,15 +155,17 @@ Layout octet par octet :
|
||||
| 60 | 2 | `charger_status` | | | Bitfield, cf. firmware |
|
||||
| 62 | 2 | `wind_speed` | m/s | /10 | |
|
||||
| 64 | 2 | `wind_direction` | degrés | | 0–359, 0 = Nord |
|
||||
| 66 | 1 | `error_flags` | | | Bitfield d'erreur, cf. firmware |
|
||||
| 67 | 1 | `npm_status` | | | Copie du `STATE` NextPM (voir [sensors/nextpm.md](../sensors/nextpm.md)) |
|
||||
| 68 | 1 | `device_status` | | | Bitfield global |
|
||||
| 66 | 1 | `error_flags` | | | Bitfield erreurs système, détail dans [`formats/json-payload.md`](../formats/json-payload.md#error_flags--bitfield-système-1-octet). `0xFF` = firmware ancien. |
|
||||
| 67 | 1 | `npm_status` | | | Bitfield statut NextPM (copie du `STATE` UART, voir [`sensors/nextpm.md`](../sensors/nextpm.md) et [`json-payload.md`](../formats/json-payload.md#npm_status--bitfield-nextpm-1-octet)). `0xFF` = firmware ancien. |
|
||||
| 68 | 1 | `device_status` | | | Bitfield état boîtier, détail dans [`formats/json-payload.md`](../formats/json-payload.md#device_status--bitfield-boîtier-1-octet). `0xFF` = firmware ancien. |
|
||||
| 69 | 1 | `version_major` | | | Version firmware `X.y.z` |
|
||||
| 70 | 1 | `version_minor` | | | `x.Y.z` |
|
||||
| 71 | 1 | `version_patch` | | | `x.y.Z` |
|
||||
| 72 | 11 | `reserved` | | | À ignorer (évolution future du descripteur) |
|
||||
| **83** | total | | | | |
|
||||
|
||||
> Les champs `latitude`, `longitude`, `misc` présents dans le JSON final (voir [`json-payload.md`](../formats/json-payload.md#géolocalisation--contexte)) **ne sont pas** dans ce descripteur 83B — à documenter : ajoutés par Miotiq depuis les métadonnées device, ou transmis via un autre canal.
|
||||
|
||||
### MobileAir (17 octets — legacy, pré-descripteur)
|
||||
|
||||
> Ce capteur envoie encore un format binaire packé **sans descripteur Miotiq formel**. Migration prévue vers la même approche descripteur que NebuleAir Pro 4G.
|
||||
@@ -236,7 +238,8 @@ def decode(payload: bytes, fields: list[Field], emit_units: bool = True) -> dict
|
||||
raise ValueError(f"payload trop court au champ {f.name}")
|
||||
|
||||
if f.fn == "string":
|
||||
val = chunk.decode("ascii", errors="replace").rstrip("\x00 ")
|
||||
# Miotiq: garde la représentation hex des octets, pas de décodage ASCII.
|
||||
val = chunk.hex()
|
||||
elif f.fn == "hex2dec":
|
||||
val = _apply_equation(int.from_bytes(chunk, "big", signed=False), f.equation)
|
||||
elif f.fn == "hex2bin":
|
||||
@@ -283,6 +286,7 @@ Côté PHP (cf. implémentations existantes `udp_miotiq_byte.php` / `udp_miotiq_
|
||||
|
||||
- [ ] Confirmer l'endianness des champs multi-octets (big-endian supposé).
|
||||
- [ ] Confirmer le caractère signé/non-signé de `battery_current` (décharge = négatif ?).
|
||||
- [ ] D'où viennent `latitude` / `longitude` / `misc` dans le JSON final ? (pas dans le descripteur 83B ; métadonnées Miotiq ? trame séparée ?)
|
||||
- [ ] Migrer MobileAir du format binaire 17B vers un descripteur Miotiq formel.
|
||||
- [ ] Ajouter un descripteur ModuleAir Pro 4G quand dispo.
|
||||
|
||||
@@ -293,3 +297,4 @@ Côté PHP (cf. implémentations existantes `udp_miotiq_byte.php` / `udp_miotiq_
|
||||
| 2026-04-23 | v1 | Création — MobileAir 17B + CSV à partir des parsers PHP en prod. |
|
||||
| 2026-04-23 | v2 | Refonte autour du format descripteur Miotiq, ajout NebuleAir Pro 4G (83B). |
|
||||
| 2026-04-23 | v3 | Format descripteur aligné sur doc officielle Miotiq : 6e colonne = export JSON (W/Y/N), ajout base functions `hex2bin` et `userdef`, colonne `equation` (expression en x). |
|
||||
| 2026-04-23 | v4 | Correction : `string` produit du hex (pas ASCII). Correction ISO_39=PM2.5 et ISO_24=PM10 (inversion). Gaz confirmés (NO₂/CO/H₂S/NH₃/O₃). Lien vers JSON canonique AirCarto 2026. |
|
||||
|
||||
Reference in New Issue
Block a user