diff --git a/README.md b/README.md index cf6b990..2f7b4e7 100644 --- a/README.md +++ b/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. diff --git a/formats/iso-pollutant-codes.md b/formats/iso-pollutant-codes.md index 51133b8..fbb2cf6 100644 --- a/formats/iso-pollutant-codes.md +++ b/formats/iso-pollutant-codes.md @@ -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 | | -| `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 | | +| 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` | 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. diff --git a/formats/json-payload.md b/formats/json-payload.md index 3b206df..b40ee98 100644 --- a/formats/json-payload.md +++ b/formats/json-payload.md @@ -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= +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 [`parsers/udp-miotiq.md`](../parsers/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). | +| `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. | diff --git a/parsers/udp-miotiq.md b/parsers/udp-miotiq.md index fc92163..69851b7 100644 --- a/parsers/udp-miotiq.md +++ b/parsers/udp-miotiq.md @@ -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. |