# Parser UDP Miotiq [Miotiq](https://app.miotiq.com/) est la plateforme IoT cellulaire utilisée par AirCarto pour la connectivité LTE-M / NB-IoT (MobileAir, NebuleAir 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──> data.mobileair.fr/udp_miotiq_*.php ──> PostgreSQL + InfluxDB ``` ## Enveloppe JSON reçue du webhook Le corps `application/json` reçu par le script PHP contient : ```json { "payload": "", "customerId": "string", "rcvTime": 1713830400, "srcIP": "10.x.x.x", "srcImsi": "208xxxxxxxxxxx" } ``` | Champ | Type | Description | |--------------|---------|--------------------------------------------------------------------| | `payload` | string | **Base64** des octets UDP envoyés par le capteur (charge utile brute). | | `customerId` | string | Identifiant client Miotiq. | | `rcvTime` | integer | Timestamp Unix UTC de la réception Miotiq, en secondes. | | `srcIP` | string | IP source du modem cellulaire (côté opérateur). | | `srcImsi` | string | IMSI de la SIM, sert à rattacher la mesure à un capteur en DB. | ## Formats de payload interne Après décodage base64, le contenu est soit **binaire**, soit **CSV**, selon le firmware du capteur. ### Format binaire (17 octets) — MobileAir Format packé big-endian, 17 octets. Parser de référence : [`data.mobileair.fr/udp_miotiq_byte.php`](https://gitea.aircarto.fr/PaulVua) (endpoint `/udp_miotiq_byte.php`). | Offset | Taille | Champ | Type | Décodage | |--------|--------|------------------|--------|---------------------------------| | 0 | 1 | `device_id` | uint8 | `str(val).zfill(3)` → token `"001"` | | 1 | 2 | `pm1_x10` | uint16 BE | `raw / 10.0` → µg/m³ | | 3 | 2 | `pm25_x10` | uint16 BE | `raw / 10.0` → µg/m³ | | 5 | 2 | `pm10_x10` | uint16 BE | `raw / 10.0` → µg/m³ | | 7 | 2 | `lat_x10000` | uint16 BE | `raw / 10000.0` → degrés (0 si pas de fix) | | 9 | 2 | `lon_x10000` | uint16 BE | `raw / 10000.0` → degrés (0 si pas de fix) | | 11 | 1 | `num_sats` | uint8 | nombre de satellites | | 12 | 1 | `signal_quality` | uint8 | % qualité modem | | 13 | 1 | `moving_type` | uint8 | énumération déplacement | Format C sur capteur (pseudo, big-endian packé) : ```c struct __attribute__((packed)) mobileair_udp_t { uint8_t device_id; uint16_t pm1_x10; // htons avant envoi uint16_t pm25_x10; uint16_t pm10_x10; uint16_t lat_x10000; uint16_t lon_x10000; uint8_t num_sats; uint8_t signal_quality; uint8_t moving_type; }; // sizeof = 14 — attention : le format sur le fil fait 17 octets ``` > Un ancien format **15 octets** existe (sans `lat`/`lon`/`moving_type`) — le parser PHP le gère en fallback. Ne plus l'utiliser pour un nouveau firmware. Python équivalent pour lire / écrire : ```python import struct, base64 FMT = ">B HHHHH BBB" # 17 octets def pack(device_id, pm1, pm25, pm10, lat, lon, sats, sig, moving): return struct.pack(FMT, device_id, int(pm1*10), int(pm25*10), int(pm10*10), int(lat*10000), int(lon*10000), sats, sig, moving) def unpack(data: bytes): if len(data) != 17: raise ValueError(f"payload {len(data)} octets, attendu 17") dev, pm1, pm25, pm10, lat, lon, sats, sig, moving = struct.unpack(FMT, data) return { "device_id": f"{dev:03d}", "pm1": pm1 / 10.0, "pm25": pm25 / 10.0, "pm10": pm10 / 10.0, "lat": lat / 10000.0, "lon": lon / 10000.0, "sats": sats, "signal": sig, "moving_type": moving, } # Côté webhook body = {"payload": base64.b64encode(pack(1, 12.3, 18.5, 22.1, 43.605, 1.444, 8, 80, 1)).decode(), "customerId": "aircarto", "rcvTime": 1713830400, "srcIP": "10.0.0.1", "srcImsi": "208010000000001"} ``` ### Format CSV — MobileAir (legacy) Parser de référence : endpoint `/udp_miotiq_csv.php`. Le payload base64-décodé est une chaîne ASCII : ``` {device_id},{pm1},{pm25},{pm10},{lat},{lon},{num_sats},{signal_quality},{moving_type} ``` Exemple : `001,12.3,18.5,22.1,43.605000,1.444000,8,80,1` - Séparateur : virgule `,`. - Décimales : point `.`. - Valeurs manquantes : `-1` (sentinelle legacy — à **ne pas reproduire** pour les nouveaux formats, voir [`formats/json-payload.md`](../formats/json-payload.md)). ## Côté serveur — squelette du webhook PHP ```php ` — état d'un device par IMSI. - `POST https://app.miotiq.com/api/device/update?api_key=` — renommer / associer un device (voir `miotiq-update` dans `routes/sensors.js`). ## Historique | Date | Révision | Changement | |------------|----------|-------------------------------------------------------| | 2026-04-23 | v1 | Création à partir des parsers PHP en prod. |