168 lines
6.8 KiB
Markdown
168 lines
6.8 KiB
Markdown
# 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": "<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`](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
|
|
<?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. |
|