Le champ `version` (offset 9 du payload NebuleAir Pro 4G) était hardcodé `0x01` côté firmware et n'a jamais porté un vrai versioning protocole. Renommé en `command` et réaffecté à un type de trame : - 0x00 = données mesure - 0x01 = ping test (firmware → Miotiq → backend, pas d'archivage) Versioning protocole reste sur `version_major/minor/patch`. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
18 KiB
Parser UDP Miotiq
Miotiq est la plateforme IoT cellulaire utilisée par AirCarto pour la connectivité LTE-M / NB-IoT (NebuleAir Pro 4G, MobileAir, ModuleAir Pro 4G). Les capteurs envoient des datagrammes UDP vers un endpoint Miotiq ; Miotiq forwarde ces datagrammes à un webhook HTTPS AirCarto sous forme de POST JSON.
Chemin de données :
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 poste sur le backend le JSON canonique AirCarto spécifié dans json-payload.md — c'est ce JSON-là que consomment les scripts PHP receive_data.
Ce document couvre uniquement le format du descripteur et le layout binaire par capteur. Le schéma du JSON de sortie (champs, unités, bitfields, exemple complet) vit dans json-payload.md.
Métadonnées transport ajoutées par Miotiq en marge du JSON décodé (utiles pour l'audit / le rattachement device) : srcImsi (IMSI de la SIM — clé de correspondance device en DB), rcvTime (timestamp Unix UTC réception Miotiq), srcIP, customerId. En mode webhook « raw » (sans descripteur), Miotiq peut aussi poster le datagramme brut sous payload encodé base64 — utile si le backend veut re-décoder côté serveur (parser serveur à documenter séparément).
Format descripteur Miotiq
Un descripteur est une suite de lignes, une par champ. Format officiel Miotiq (cf. doc plateforme, section « How to create a Parsing table ») :
<length>|<variable name>|<base function>|<units>|<equation>|<export to JSON>
| Colonne | Description |
|---|---|
length |
Taille du champ en caractères hexadécimaux (2 chars = 1 octet). |
variable name |
Identifiant logique du champ. Les codes polluants suivent ISO 7168 AirCarto. |
base function |
Fonction de décodage appliquée aux octets bruts. Voir tableau ci-dessous. |
units |
Unité physique finale (ugm3, degC, %, hPa, ppb, dB, V, A, W, m/s, degrees, count). Vide pour les champs d'état / versions. |
equation |
Expression de transformation appliquée à la valeur décodée, où x est la valeur. Ex. x/10, x/100, (x-32)*5/9. Vide = pas de transformation. |
export to JSON |
Contrôle la sortie JSON côté Miotiq (voir ci-dessous). Valeurs : Y (défaut), W, N. |
Base functions
| Valeur | Effet |
|---|---|
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. |
skip |
Observé dans nos descripteurs (reserved) pour ignorer N octets. Les octets sautés ne sortent pas dans le JSON. |
Export to JSON
| Valeur | Comportement |
|---|---|
Y (défaut) |
Exporte la valeur et une ligne d'unité : "battery_voltage": 3600, "battery_voltage_unit": "3600 mV". |
W |
Exporte la valeur seule, sans ligne d'unité : "battery_voltage": 3600. |
N |
N'exporte rien dans le JSON transmis au webhook. |
Conventions implicites :
- Les champs
hex2decmulti-octets sont big-endian (à vérifier au cas par cas avec Miotiq si un doute apparaît). - Les champs
stringsont ASCII, non null-terminés, padés à droite. - L'ordre des lignes du descripteur est l'ordre des octets sur le fil : pas de séparateur, pas d'alignement.
- La taille totale du payload = somme des
length / 2. Toute trame de taille différente doit être rejetée.
Descripteurs actuels
NebuleAir Pro 4G (83 octets = 166 chars hex)
Descripteur de référence :
16|device_id|string|||W
2|signal_quality|hex2dec|dB||
2|command|hex2dec|||W
4|ISO_68|hex2dec|ugm3|x/10|
4|ISO_39|hex2dec|ugm3|x/10|
4|ISO_24|hex2dec|ugm3|x/10|
4|ISO_54|hex2dec|degC|x/100|
4|ISO_55|hex2dec|%|x/100|
4|ISO_53|hex2dec|hPa||
4|noise_cur_leq|hex2dec|dB|x/10|
4|noise_cur_level|hex2dec|dB|x/10|
4|max_noise|hex2dec|dB|x/10|
4|ISO_03|hex2dec|ppb||
4|ISO_05|hex2dec|ppb||
4|ISO_21|hex2dec|ppb||
4|ISO_04|hex2dec|ppb||
4|ISO_08|hex2dec|ppb||
4|npm_ch1|hex2dec|count||
4|npm_ch2|hex2dec|count||
4|npm_ch3|hex2dec|count||
4|npm_ch4|hex2dec|count||
4|npm_ch5|hex2dec|count||
4|npm_temp|hex2dec|°C|x/10|
4|npm_humidity|hex2dec|%|x/10|
4|battery_voltage|hex2dec|V|x/100|
4|battery_current|hex2dec|A|x/100|
4|solar_voltage|hex2dec|V|x/100|
4|solar_power|hex2dec|W||
4|charger_status|hex2dec|||
4|wind_speed|hex2dec|m/s|x/10|
4|wind_direction|hex2dec|degrees||
2|error_flags|hex2dec|||
2|npm_status|hex2dec|||
2|device_status|hex2dec|||
2|version_major|hex2dec|||
2|version_minor|hex2dec|||
2|version_patch|hex2dec|||
8|latitude|hex2dec|degrees|x/1000000-90|
8|longitude|hex2dec|degrees|x/1000000-180|
2|misc|hex2dec|||
4|reserved|skip|||
Encodage latitude / longitude — hex2dec est non-signé côté Miotiq. Pour transmettre des coordonnées négatives sans ambiguïté, le firmware encode avec un offset fixe :
raw_lat = round((lat_deg + 90) * 1_000_000)— range attendue[0, 180_000_000], tient dans uint32.raw_lon = round((lon_deg + 180) * 1_000_000)— range[0, 360_000_000], tient dans uint32.
Miotiq applique l'équation inverse (x/1000000-90, x/1000000-180) et exporte directement des degrés WGS84 signés dans le JSON. Précision ~11 cm (6 décimales), conforme à CONVENTIONS.md.
Pas de sentinelle numérique pour « no fix » : quand le GPS n'a pas de fix, le firmware positionne le bit GPS_NO_FIX dans device_status (voir json-payload.md). Le backend ignore latitude/longitude quand ce bit est levé, indépendamment de leur valeur brute.
Les firmwares antérieurs à cette extension envoyaient déjà 83 octets (bloc reserved = 11 zéros), qui décodent désormais comme lat=-90, lon=-180, misc=0. Sur ces firmwares device_status = 0xFF (= champ non supporté — cf. json-payload.md) : un backend prudent traite donc la combinaison device_status == 0xFF && (lat, lon) == (-90, -180) comme « coords non disponibles ».
Layout octet par octet :
| Offset | Taille | Champ | Unité | Scale | Notes |
|---|---|---|---|---|---|
| 0 | 8 | device_id |
Hex 16 chars, converti en ASCII côté client (ex. "D0524198") |
||
| 8 | 1 | signal_quality |
dB | Signal cellulaire | |
| 9 | 1 | command |
Type de trame — 0x00 = données mesure, 0x01 = ping test (déclenché par le firmware pour vérifier le lien capteur → Miotiq → backend). Voir json-payload.md. |
||
| 10 | 2 | ISO_68 |
µg/m³ | /10 | PM1 |
| 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 | 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 | |
| 44 | 2 | npm_ch4 |
count | NextPM canal 4 | |
| 46 | 2 | npm_ch5 |
count | NextPM canal 5 | |
| 48 | 2 | npm_temp |
°C | /10 | NextPM T interne |
| 50 | 2 | npm_humidity |
% | /10 | NextPM HR interne |
| 52 | 2 | battery_voltage |
V | /100 | |
| 54 | 2 | battery_current |
A | /100 | Signé ? à confirmer (décharge = négatif ?) |
| 56 | 2 | solar_voltage |
V | /100 | |
| 58 | 2 | solar_power |
W | ||
| 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 erreurs système, détail dans json-payload.md. 0xFF = firmware ancien. |
||
| 67 | 1 | npm_status |
Bitfield statut NextPM (copie du STATE UART, voir sensors/nextpm.md et json-payload.md). 0xFF = firmware ancien. |
||
| 68 | 1 | device_status |
Bitfield état boîtier, détail dans json-payload.md. 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 | 4 | latitude |
degrés | /1e6 − 90 | WGS84, offset unsigned. Voir encodage ci-dessus. Ignoré si device_status.GPS_NO_FIX. |
| 76 | 4 | longitude |
degrés | /1e6 − 180 | WGS84, offset unsigned. Idem. |
| 80 | 1 | misc |
Contexte de mesure 0–6 (voir json-payload.md) |
||
| 81 | 2 | reserved |
À ignorer (évolution future du descripteur) | ||
| 83 | total |
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.
Format packé big-endian, 17 octets. Parsers de référence en prod : server/sites/data.mobileair.fr/udp_miotiq_byte.php (binaire) et udp_miotiq_csv.php (CSV).
| Offset | Taille | Champ | Unité | Décodage |
|---|---|---|---|---|
| 0 | 1 | device_id |
str(val).zfill(3) |
|
| 1 | 2 | pm1_x10 |
µg/m³ × 10 | /10.0 |
| 3 | 2 | pm25_x10 |
µg/m³ × 10 | /10.0 |
| 5 | 2 | pm10_x10 |
µg/m³ × 10 | /10.0 |
| 7 | 2 | lat_x10000 |
deg × 1e4 | /10000.0 (0 si pas de fix) |
| 9 | 2 | lon_x10000 |
deg × 1e4 | /10000.0 |
| 11 | 1 | num_sats |
nombre de satellites | |
| 12 | 1 | signal_quality |
% | |
| 13 | 1 | moving_type |
énumération déplacement |
Un ancien format 15 octets existe aussi (sans lat/lon/moving_type) — le parser PHP le gère en fallback. À ne plus utiliser pour du nouveau firmware.
Format CSV (base64-décodé = chaîne ASCII) :
{device_id},{pm1},{pm25},{pm10},{lat},{lon},{num_sats},{signal_quality},{moving_type}
Valeurs manquantes codées -1 (sentinelle legacy). À ne pas reproduire pour les nouveaux formats.
Configuration Miotiq
- Clé API serveur : stockée dans le code backend (
server/sites/gestion.aircarto.fr/server/routes/sensors.js), référencée comme<API_KEY>. - Webhook à paramétrer côté Miotiq par capteur :
https://data.<projet>.aircarto.fr/udp_miotiq.php. - Descripteur à coller dans la fiche device Miotiq (ou par endpoint côté projet).
- API utile :
POST https://app.miotiq.com/api/device/detail?api_key=<KEY>— état d'un device par IMSI.POST https://app.miotiq.com/api/device/update?api_key=<KEY>— renommer / associer.
À faire
- Côté firmware NebuleAir Pro 4G : implémenter l'encodage offset de
latitude/longitude(raw = round((deg + 90|180) * 1_000_000)) et positionnerdevice_status.GPS_NO_FIXquand il n'y a pas de fix. - Valider en test réel que l'équation Miotiq
x/1000000-90est acceptée telle quelle dans la colonne equation (soustraction littérale). Fallback si refusée : firmware envoieraw / 1000(millidegrés + 90000 pour lat, + 180000 pour lon) et équation devientx/1000-90. À tester une fois, valable à vie. - Confirmer l'endianness des champs multi-octets (big-endian supposé).
- Confirmer le caractère signé/non-signé de
battery_current(décharge = négatif ?). - Migrer MobileAir du format binaire 17B vers un descripteur Miotiq formel.
- Ajouter un descripteur ModuleAir Pro 4G quand dispo.
Historique
| Date | Révision | Changement |
|---|---|---|
| 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. |
| 2026-04-23 | v5 | Extension prévue du descripteur NebuleAir Pro 4G avec latitude, longitude, misc dans le bloc reserved — proposition d'encodage dans la section À faire. |
| 2026-04-24 | v6 | Intégration effective de latitude (4B), longitude (4B), misc (1B) dans le descripteur NebuleAir Pro 4G. Encodage offset unsigned (`raw = (deg + 90 |
| 2026-04-27 | v7 | Octet 9 renommé version → command. Ce champ était hardcodé 0x01 côté firmware (jamais une vraie version de protocole). Réaffecté à un type de trame : 0x00 = données mesure, 0x01 = ping test. Permet au firmware de déclencher une trame de bout en bout (capteur → Miotiq → backend) sans envoyer de mesures réelles. Versioning protocole assuré par version_major/minor/patch (offsets 69-71). |