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:
2026-04-23 00:55:25 +02:00
parent 96260120fc
commit efd1aa438a
4 changed files with 275 additions and 112 deletions

View File

@@ -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 | | 0359, 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. |