Files
aircarto-protocols/parsers/udp-miotiq.md

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, voir formats/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 (voir miotiq-update dans routes/sensors.js).

Historique

Date Révision Changement
2026-04-23 v1 Création à partir des parsers PHP en prod.