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:
@@ -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