6.8 KiB
Parser UDP Miotiq
Miotiq 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 :
{
"payload": "<base64 du datagramme UDP d'origine>",
"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 (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é) :
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 :
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, voirformats/json-payload.md).
Côté serveur — squelette du webhook PHP
<?php
$raw = file_get_contents("php://input");
$json = json_decode($raw, true);
if (!$json || !isset($json['payload'])) { http_response_code(400); exit; }
$imsi = $json['srcImsi'] ?? null;
$rcvTime = $json['rcvTime'] ?? time();
$bin = base64_decode($json['payload'], true);
if ($bin === false) { http_response_code(400); exit; }
// Dispatcher selon la taille pour les formats binaires ;
// pour le CSV, tester si $bin est une chaîne imprimable commençant par un chiffre.
if (strlen($bin) === 17) {
$u = unpack('Cdev/npm1/npm25/npm10/nlat/nlon/Csats/Csig/Cmoving', $bin);
// …
} else if (strlen($bin) === 15) {
// format legacy — décoder sans lat/lon
}
Voir les implémentations complètes en prod : server/sites/data.mobileair.fr/udp_miotiq_byte.php et udp_miotiq_csv.php.
Côté capteur — envoi UDP
Côté modem cellulaire (nRF9151 / autre), envoyer le datagramme à l'IP/port fournis par Miotiq pour ta SIM. Le tunnel Miotiq encapsule ensuite et ajoute l'enveloppe JSON avant de poster sur le webhook AirCarto.
Pas d'ACK côté capteur : fire-and-forget. Un cycle d'envoi typique est de 1 à 5 minutes.
Configuration Miotiq
- Clé API serveur : stockée dans le code backend (
server/sites/gestion.aircarto.fr/server/routes/sensors.js). - Webhook à paramétrer côté Miotiq :
https://data.mobileair.fr/udp_miotiq_byte.php(binaire) ou/udp_miotiq_csv.php(CSV). - 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 un device (voirmiotiq-updatedansroutes/sensors.js).
Historique
| Date | Révision | Changement |
|---|---|---|
| 2026-04-23 | v1 | Création à partir des parsers PHP en prod. |