docs(miotiq): Miotiq descriptor format as canonical parser + NebuleAir Pro 4G 83B descriptor
- Refactor parsers/udp-miotiq.md around the pipe-separated descriptor format used on Miotiq side. - Document the full 83-byte NebuleAir Pro 4G descriptor (PM, gases, noise, weather, power, status). - Keep legacy MobileAir 17B binary format for reference. - Add formats/iso-pollutant-codes.md placeholder for AirCarto ISO 7168 code mapping. - Open TODOs: flag W semantics, endianness, signed battery_current, MobileAir migration.
This commit is contained in:
@@ -11,6 +11,7 @@ aircarto-protocols/
|
||||
├── CONVENTIONS.md Nommage, versioning, style doc
|
||||
├── formats/ Formats d'échange de données
|
||||
│ ├── json-payload.md Format JSON canonique des mesures
|
||||
│ ├── iso-pollutant-codes.md Mapping ISO_XX → polluant / grandeur
|
||||
│ └── mqtt.md Topics et conventions MQTT
|
||||
├── sensors/ Un fichier par capteur
|
||||
│ ├── _TEMPLATE.md Gabarit à copier pour tout nouveau capteur
|
||||
@@ -28,8 +29,8 @@ aircarto-protocols/
|
||||
## Index des parsers
|
||||
|
||||
| Nom | Transport | Doc | État |
|
||||
|-----------------|------------------|-----------------------------------------------|---------|
|
||||
| UDP Miotiq | UDP → HTTPS JSON | [parsers/udp-miotiq.md](parsers/udp-miotiq.md) | Complet |
|
||||
|-----------------|------------------|-----------------------------------------------|-------------------------------|
|
||||
| UDP Miotiq | UDP → HTTPS JSON | [parsers/udp-miotiq.md](parsers/udp-miotiq.md) | Descripteur NebuleAir Pro 4G + legacy MobileAir |
|
||||
|
||||
## Comment ajouter une entrée
|
||||
|
||||
|
||||
37
formats/iso-pollutant-codes.md
Normal file
37
formats/iso-pollutant-codes.md
Normal file
@@ -0,0 +1,37 @@
|
||||
# Codes polluants ISO — convention AirCarto
|
||||
|
||||
Les descripteurs Miotiq (voir [`parsers/udp-miotiq.md`](../parsers/udp-miotiq.md)) utilisent des codes `ISO_XX` pour désigner les grandeurs physiques et polluants. Ces codes sont inspirés de la norme **ISO 7168-2** (échange de données de qualité de l'air) et du vocabulaire EIONET / Directive européenne 2008/50/CE, mais le mapping exact **est la convention AirCarto** — à verrouiller ici.
|
||||
|
||||
## Mapping
|
||||
|
||||
| Code | Grandeur / polluant | Unité de référence | Scale standard | Remarques |
|
||||
|----------|---------------------|--------------------|----------------|-----------|
|
||||
| `ISO_03` | *à confirmer* (gaz) | ppb | | Candidat : O₃ (ozone) |
|
||||
| `ISO_04` | *à confirmer* (gaz) | ppb | | Candidat : NO₂ |
|
||||
| `ISO_05` | *à confirmer* (gaz) | ppb | | Candidat : NO ou SO₂ |
|
||||
| `ISO_08` | *à confirmer* (gaz) | ppb | | Candidat : CO |
|
||||
| `ISO_21` | *à confirmer* (gaz) | ppb | | |
|
||||
| `ISO_24` | PM2.5 | µg/m³ | x/10 | Poussières fines |
|
||||
| `ISO_39` | PM10 | µg/m³ | x/10 | |
|
||||
| `ISO_53` | Pression | hPa | | Pression atmosphérique |
|
||||
| `ISO_54` | Température | °C | x/100 | |
|
||||
| `ISO_55` | Humidité relative | % | x/100 | |
|
||||
| `ISO_68` | PM1 | µg/m³ | x/10 | |
|
||||
|
||||
## Règles
|
||||
|
||||
- Un code ISO est **unique** au sein de l'écosystème AirCarto : pas de redéfinition par capteur.
|
||||
- Un nouveau polluant / nouvelle grandeur suit la numérotation ISO 7168-2 si elle existe ; sinon on alloue le prochain code libre au-dessus de 100 et on le documente ici.
|
||||
- Unité de référence : celle stockée dans InfluxDB (donc celle après application de `scale`). Les firmwares peuvent encoder à une précision différente, le `scale` ramène à l'unité finale.
|
||||
|
||||
## À faire
|
||||
|
||||
- [ ] Remplir les gaz `ISO_03`, `ISO_04`, `ISO_05`, `ISO_08`, `ISO_21` — les 5 gaz du NebuleAir Pro 4G.
|
||||
- [ ] Vérifier que le mapping ci-dessus correspond bien à la norme ISO 7168-2 version utilisée en interne chez AirCarto.
|
||||
- [ ] Ajouter les codes pour les grandeurs qui n'existent pas encore en ISO : bruit (`noise_cur_leq`, etc.), vent, batterie, solaire — ou laisser en noms `snake_case` non-ISO comme aujourd'hui dans le descripteur.
|
||||
|
||||
## Références
|
||||
|
||||
- ISO 7168-2:1999 — Air quality — Exchange of data, Part 2: Condensed data format.
|
||||
- EIONET Air Quality data flow — https://www.eionet.europa.eu/
|
||||
- Directive 2008/50/CE — annexe listant les codes polluants européens.
|
||||
@@ -1,13 +1,15 @@
|
||||
# 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**.
|
||||
[Miotiq](https://app.miotiq.com/) 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──> data.mobileair.fr/udp_miotiq_*.php ──> PostgreSQL + InfluxDB
|
||||
Capteur ──UDP──> Miotiq ──HTTPS POST JSON──> data.*.aircarto.fr/udp_miotiq_*.php ──> 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 c'est aussi lui qui sert de **contrat** pour le firmware capteur et le parser serveur.
|
||||
|
||||
## Enveloppe JSON reçue du webhook
|
||||
|
||||
Le corps `application/json` reçu par le script PHP contient :
|
||||
@@ -24,144 +26,232 @@ Le corps `application/json` reçu par le script PHP contient :
|
||||
|
||||
| Champ | Type | Description |
|
||||
|--------------|---------|--------------------------------------------------------------------|
|
||||
| `payload` | string | **Base64** des octets UDP envoyés par le capteur (charge utile brute). |
|
||||
| `payload` | string | **Base64** des octets UDP envoyés par le capteur. |
|
||||
| `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
|
||||
## Format descripteur Miotiq
|
||||
|
||||
Après décodage base64, le contenu est soit **binaire**, soit **CSV**, selon le firmware du capteur.
|
||||
Un descripteur est une suite de lignes, une par champ, au format :
|
||||
|
||||
### 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
|
||||
```
|
||||
<nb_chars_hex>|<nom>|<decodeur>|<unite>|<scale>|<flags>
|
||||
```
|
||||
|
||||
> 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.
|
||||
| Colonne | Description |
|
||||
|----------------|------------------------------------------------------------------------------------------|
|
||||
| `nb_chars_hex` | Taille du champ en **caractères hexadécimaux** (2 chars = 1 octet). |
|
||||
| `nom` | Identifiant logique du champ. Les codes polluants suivent [ISO 7168 AirCarto](../formats/iso-pollutant-codes.md). |
|
||||
| `decodeur` | `string` (ASCII), `hex2dec` (entier non-signé big-endian), `skip` (ignorer N octets). |
|
||||
| `unite` | Unité physique finale (`ugm3`, `degC`, `%`, `hPa`, `ppb`, `dB`, `V`, `A`, `W`, `m/s`, `degrees`, `count`). Peut être vide pour les champs d'état. |
|
||||
| `scale` | Facteur à appliquer après `hex2dec`. `x/10`, `x/100`… Vide = pas de division. |
|
||||
| `flags` | Optionnel. `W` observé en fin de certaines lignes — **signification à confirmer** (hypothèse : champ writable / configurable via downlink Miotiq). |
|
||||
|
||||
Python équivalent pour lire / écrire :
|
||||
**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 `nb_chars_hex / 2`. Toute trame de taille différente doit être rejetée.
|
||||
|
||||
```python
|
||||
import struct, base64
|
||||
## Descripteurs actuels
|
||||
|
||||
FMT = ">B HHHHH BBB" # 17 octets
|
||||
### NebuleAir Pro 4G (83 octets = 166 chars hex)
|
||||
|
||||
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)
|
||||
Descripteur de référence :
|
||||
|
||||
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"}
|
||||
```
|
||||
16|device_id|string|||W
|
||||
2|signal_quality|hex2dec|dB||
|
||||
2|version|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|||
|
||||
22|reserved|skip|||
|
||||
```
|
||||
|
||||
### Format CSV — MobileAir (legacy)
|
||||
Layout octet par octet :
|
||||
|
||||
Parser de référence : endpoint `/udp_miotiq_csv.php`.
|
||||
| Offset | Taille | Champ | Unité | Scale | Notes |
|
||||
|--------|--------|-------------------|---------|--------|------------------------------------------|
|
||||
| 0 | 8 | `device_id` | | | ASCII, identifiant capteur |
|
||||
| 8 | 1 | `signal_quality` | dB | | Signal cellulaire |
|
||||
| 9 | 1 | `version` | | | Version encodée (complète en v_major/minor/patch plus bas) |
|
||||
| 10 | 2 | `ISO_68` | µg/m³ | /10 | PM1 |
|
||||
| 12 | 2 | `ISO_39` | µg/m³ | /10 | PM10 |
|
||||
| 14 | 2 | `ISO_24` | µg/m³ | /10 | PM2.5 |
|
||||
| 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 | | Gaz — voir `formats/iso-pollutant-codes.md` |
|
||||
| 30 | 2 | `ISO_05` | ppb | | Gaz |
|
||||
| 32 | 2 | `ISO_21` | ppb | | Gaz |
|
||||
| 34 | 2 | `ISO_04` | ppb | | Gaz |
|
||||
| 36 | 2 | `ISO_08` | ppb | | Gaz |
|
||||
| 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 | | 0–359, 0 = Nord |
|
||||
| 66 | 1 | `error_flags` | | | Bitfield d'erreur, cf. firmware |
|
||||
| 67 | 1 | `npm_status` | | | Copie du `STATE` NextPM (voir [sensors/nextpm.md](../sensors/nextpm.md)) |
|
||||
| 68 | 1 | `device_status` | | | Bitfield global |
|
||||
| 69 | 1 | `version_major` | | | Version firmware `X.y.z` |
|
||||
| 70 | 1 | `version_minor` | | | `x.Y.z` |
|
||||
| 71 | 1 | `version_patch` | | | `x.y.Z` |
|
||||
| 72 | 11 | `reserved` | | | À ignorer (évolution future du descripteur) |
|
||||
| **83** | total | | | | |
|
||||
|
||||
Le payload base64-décodé est une chaîne ASCII :
|
||||
### 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}
|
||||
```
|
||||
|
||||
Exemple : `001,12.3,18.5,22.1,43.605000,1.444000,8,80,1`
|
||||
Valeurs manquantes codées `-1` (sentinelle legacy). À **ne pas reproduire** pour les nouveaux formats.
|
||||
|
||||
- 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)).
|
||||
## Parser serveur — squelette générique
|
||||
|
||||
## Côté serveur — squelette du webhook PHP
|
||||
Un parser générique qui consomme un descripteur Miotiq et décode n'importe quelle trame :
|
||||
|
||||
```php
|
||||
<?php
|
||||
$raw = file_get_contents("php://input");
|
||||
$json = json_decode($raw, true);
|
||||
```python
|
||||
import base64, struct
|
||||
from dataclasses import dataclass
|
||||
|
||||
if (!$json || !isset($json['payload'])) { http_response_code(400); exit; }
|
||||
@dataclass
|
||||
class Field:
|
||||
size: int # octets
|
||||
name: str
|
||||
decoder: str # string | hex2dec | skip
|
||||
unit: str
|
||||
scale: str # "x/10" | "x/100" | ""
|
||||
|
||||
$imsi = $json['srcImsi'] ?? null;
|
||||
$rcvTime = $json['rcvTime'] ?? time();
|
||||
$bin = base64_decode($json['payload'], true);
|
||||
def parse_descriptor(text: str) -> list[Field]:
|
||||
fields = []
|
||||
for line in text.strip().splitlines():
|
||||
parts = line.split("|")
|
||||
nb_chars, name, decoder, unit, scale = parts[0], parts[1], parts[2], parts[3], parts[4]
|
||||
fields.append(Field(int(nb_chars) // 2, name, decoder, unit, scale))
|
||||
return fields
|
||||
|
||||
if ($bin === false) { http_response_code(400); exit; }
|
||||
def decode(payload: bytes, fields: list[Field]) -> dict:
|
||||
out, off = {}, 0
|
||||
for f in fields:
|
||||
chunk = payload[off:off + f.size]
|
||||
if len(chunk) != f.size:
|
||||
raise ValueError(f"payload trop court au champ {f.name}")
|
||||
if f.decoder == "string":
|
||||
out[f.name] = chunk.decode("ascii", errors="replace").rstrip("\x00 ")
|
||||
elif f.decoder == "hex2dec":
|
||||
raw = int.from_bytes(chunk, "big", signed=False)
|
||||
if f.scale == "x/10": out[f.name] = raw / 10.0
|
||||
elif f.scale == "x/100": out[f.name] = raw / 100.0
|
||||
else: out[f.name] = raw
|
||||
elif f.decoder == "skip":
|
||||
pass
|
||||
off += f.size
|
||||
return out
|
||||
|
||||
// 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
|
||||
}
|
||||
# Webhook Miotiq
|
||||
def on_miotiq_webhook(body: dict, descriptor: str) -> dict:
|
||||
raw = base64.b64decode(body["payload"])
|
||||
fields = parse_descriptor(descriptor)
|
||||
total = sum(f.size for f in fields)
|
||||
if len(raw) != total:
|
||||
raise ValueError(f"payload {len(raw)} octets, descripteur attend {total}")
|
||||
return {
|
||||
"imsi": body.get("srcImsi"),
|
||||
"rcvTime": body.get("rcvTime"),
|
||||
"data": decode(raw, fields),
|
||||
}
|
||||
```
|
||||
|
||||
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**.
|
||||
Côté PHP (cf. implémentations existantes `udp_miotiq_byte.php` / `udp_miotiq_csv.php`), la logique sera à réécrire autour de ce même principe de descripteur au fur et à mesure de la migration.
|
||||
|
||||
## 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).
|
||||
- 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 un device (voir `miotiq-update` dans `routes/sensors.js`).
|
||||
- `POST https://app.miotiq.com/api/device/update?api_key=<KEY>` — renommer / associer.
|
||||
|
||||
## À faire
|
||||
|
||||
- [ ] Confirmer la signification du flag `W` dans le descripteur (hypothèse : writable via downlink).
|
||||
- [ ] 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 à partir des parsers PHP en prod. |
|
||||
|------------|----------|-----------------------------------------------------------------------------|
|
||||
| 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). |
|
||||
|
||||
Reference in New Issue
Block a user