Files
aircarto-protocols/formats/udp-miotiq.md
Your Name c20ea823e9 docs(miotiq): repurpose payload byte 9 as command (ping test trigger)
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>
2026-04-27 16:22:27 +02:00

18 KiB
Raw Blame History

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 hex2dec multi-octets sont big-endian (à vérifier au cas par cas avec Miotiq si un doute apparaît).
  • Les champs string sont 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 / longitudehex2dec 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 0359, 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 06 (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 positionner device_status.GPS_NO_FIX quand il n'y a pas de fix.
  • Valider en test réel que l'équation Miotiq x/1000000-90 est acceptée telle quelle dans la colonne equation (soustraction littérale). Fallback si refusée : firmware envoie raw / 1000 (millidegrés + 90000 pour lat, + 180000 pour lon) et équation devient x/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é versioncommand. 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).