docs: merge parsers/ into formats/ and drop misleading JSON wrapper
Parsers and formats are tightly linked (a parser produces a format) and the split made cross-links heavy for a single parser file. Also removed the confusing "Enveloppe JSON" block in udp-miotiq.md that mixed the raw webhook wrapper with what the backend actually consumes — the decoded payload schema lives in json-payload.md and is now referenced directly. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
# Codes polluants ISO — convention AirCarto
|
||||
|
||||
Les descripteurs Miotiq (voir [`parsers/udp-miotiq.md`](../parsers/udp-miotiq.md)) et le JSON canonique (voir [`json-payload.md`](json-payload.md)) utilisent des codes `ISO_XX` pour désigner les polluants et grandeurs physiques. Ces codes suivent la nomenclature du **LCSQA** (Laboratoire Central de Surveillance de la Qualité de l'Air), basée sur la norme **ISO 7168**.
|
||||
Les descripteurs Miotiq (voir [`udp-miotiq.md`](udp-miotiq.md)) et le JSON canonique (voir [`json-payload.md`](json-payload.md)) utilisent des codes `ISO_XX` pour désigner les polluants et grandeurs physiques. Ces codes suivent la nomenclature du **LCSQA** (Laboratoire Central de Surveillance de la Qualité de l'Air), basée sur la norme **ISO 7168**.
|
||||
|
||||
Les codes vont théoriquement de `ISO_01` à `ISO_99`. Seuls ceux effectivement mesurés par au moins un capteur AirCarto sont documentés ici.
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ Le **type de capteur** est passé en query string. Modèles supportés :
|
||||
- **Tous les champs sont optionnels** sauf `device_id`. Un capteur n'envoie que les champs qu'il mesure.
|
||||
- **Valeur sentinelle `-1`** : donnée non disponible ou capteur non renseigné (ex. pas de GPS fix, pas de pression).
|
||||
- **Valeur sentinelle `255` (`0xFF`)** sur `error_flags`, `npm_status`, `device_status` uniquement : le firmware du capteur est antérieur à l'introduction du champ. À interpréter comme « non disponible », **pas** comme « toutes les erreurs actives ». Les nouveaux firmwares initialisent ces octets et envoient une valeur ≤ 254.
|
||||
- **Champs `<nom>_unit`** : optionnels, ajoutés par Miotiq quand la colonne `units` du descripteur est remplie et que l'export JSON est `Y` (voir [`parsers/udp-miotiq.md`](../parsers/udp-miotiq.md)). Format : `"<valeur> <unité>"`. Le backend peut les ignorer, la valeur canonique est toujours le champ sans suffixe.
|
||||
- **Champs `<nom>_unit`** : optionnels, ajoutés par Miotiq quand la colonne `units` du descripteur est remplie et que l'export JSON est `Y` (voir [`udp-miotiq.md`](udp-miotiq.md)). Format : `"<valeur> <unité>"`. Le backend peut les ignorer, la valeur canonique est toujours le champ sans suffixe.
|
||||
|
||||
## Identification
|
||||
|
||||
|
||||
299
formats/udp-miotiq.md
Normal file
299
formats/udp-miotiq.md
Normal file
@@ -0,0 +1,299 @@
|
||||
# Parser UDP Miotiq
|
||||
|
||||
[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──> api.aircarto.com/receive_data ──> 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 poste sur le backend le **JSON canonique AirCarto** spécifié dans [`json-payload.md`](json-payload.md) — c'est ce JSON-là que consomment les scripts PHP `receive_data`.
|
||||
|
||||
Ce document couvre uniquement le **format du descripteur** et le **layout binaire** par capteur. Le schéma du JSON de sortie (champs, unités, bitfields, exemple complet) vit dans [`json-payload.md`](json-payload.md).
|
||||
|
||||
Métadonnées transport ajoutées par Miotiq en marge du JSON décodé (utiles pour l'audit / le rattachement device) : `srcImsi` (IMSI de la SIM — clé de correspondance device en DB), `rcvTime` (timestamp Unix UTC réception Miotiq), `srcIP`, `customerId`. En mode webhook « raw » (sans descripteur), Miotiq peut aussi poster le datagramme brut sous `payload` encodé base64 — utile si le backend veut re-décoder côté serveur (voir le squelette Python plus bas).
|
||||
|
||||
## Format descripteur Miotiq
|
||||
|
||||
Un descripteur est une suite de lignes, une par champ. **Format officiel Miotiq** (cf. doc plateforme, section « How to create a Parsing table ») :
|
||||
|
||||
```
|
||||
<length>|<variable name>|<base function>|<units>|<equation>|<export to JSON>
|
||||
```
|
||||
|
||||
| Colonne | Description |
|
||||
|----------------------|--------------------------------------------------------------------------------------------------|
|
||||
| `length` | Taille du champ en **caractères hexadécimaux** (2 chars = 1 octet). |
|
||||
| `variable name` | Identifiant logique du champ. Les codes polluants suivent [ISO 7168 AirCarto](iso-pollutant-codes.md). |
|
||||
| `base function` | Fonction de décodage appliquée aux octets bruts. Voir tableau ci-dessous. |
|
||||
| `units` | Unité physique finale (`ugm3`, `degC`, `%`, `hPa`, `ppb`, `dB`, `V`, `A`, `W`, `m/s`, `degrees`, `count`). Vide pour les champs d'état / versions. |
|
||||
| `equation` | Expression de transformation appliquée à la valeur décodée, où `x` est la valeur. Ex. `x/10`, `x/100`, `(x-32)*5/9`. Vide = pas de transformation. |
|
||||
| `export to JSON` | Contrôle la sortie JSON côté Miotiq (voir ci-dessous). Valeurs : `Y` (défaut), `W`, `N`. |
|
||||
|
||||
### Base functions
|
||||
|
||||
| Valeur | Effet |
|
||||
|------------|-------------------------------------------------------------------------------------------------|
|
||||
| `string` | Sort la **représentation hexadécimale** des octets tels quels (n'effectue *pas* de décodage ASCII). Ex. `device_id` 8 octets `0x44 30 35 32 34 31 39 38` sort `"4430353234313938"` ; le client convertit en ASCII si besoin (`bytes.fromhex(v).decode("ascii")` → `"D0524198"`). |
|
||||
| `hex2dec` | Convertit les octets en entier non-signé **big-endian**. |
|
||||
| `hex2bin` | Convertit les octets en chaîne binaire (bitfield lisible, utile pour les registres d'état). |
|
||||
| `userdef` | Pas de transformation — les octets bruts sont passés tels quels comme valeur. |
|
||||
| `skip` | Observé dans nos descripteurs (`reserved`) pour ignorer N octets. Les octets sautés ne sortent pas dans le JSON. |
|
||||
|
||||
### Export to JSON
|
||||
|
||||
| Valeur | Comportement |
|
||||
|-----------|---------------------------------------------------------------------------------------------------------|
|
||||
| `Y` (défaut) | Exporte la valeur **et** une ligne d'unité : `"battery_voltage": 3600, "battery_voltage_unit": "3600 mV"`. |
|
||||
| `W` | Exporte la valeur seule, sans ligne d'unité : `"battery_voltage": 3600`. |
|
||||
| `N` | N'exporte rien dans le JSON transmis au webhook. |
|
||||
|
||||
**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 `length / 2`. Toute trame de taille différente doit être rejetée.
|
||||
|
||||
## Descripteurs actuels
|
||||
|
||||
### NebuleAir Pro 4G (83 octets = 166 chars hex)
|
||||
|
||||
Descripteur de référence :
|
||||
|
||||
```
|
||||
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|||
|
||||
```
|
||||
|
||||
Layout octet par octet :
|
||||
|
||||
| Offset | Taille | Champ | Unité | Scale | Notes |
|
||||
|--------|--------|-------------------|---------|--------|------------------------------------------|
|
||||
| 0 | 8 | `device_id` | | | Hex 16 chars, converti en ASCII côté client (ex. `"D0524198"`) |
|
||||
| 8 | 1 | `signal_quality` | dB | | Signal cellulaire |
|
||||
| 9 | 1 | `version` | | | Version du protocole de communication |
|
||||
| 10 | 2 | `ISO_68` | µg/m³ | /10 | PM1 |
|
||||
| 12 | 2 | `ISO_39` | µg/m³ | /10 | PM2.5 |
|
||||
| 14 | 2 | `ISO_24` | µg/m³ | /10 | PM10 |
|
||||
| 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 | | NO₂ |
|
||||
| 30 | 2 | `ISO_05` | ppb | | H₂S |
|
||||
| 32 | 2 | `ISO_21` | ppb | | NH₃ |
|
||||
| 34 | 2 | `ISO_04` | ppb | | CO |
|
||||
| 36 | 2 | `ISO_08` | ppb | | O₃ |
|
||||
| 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 erreurs système, détail dans [`json-payload.md`](json-payload.md#error_flags--bitfield-système-1-octet). `0xFF` = firmware ancien. |
|
||||
| 67 | 1 | `npm_status` | | | Bitfield statut NextPM (copie du `STATE` UART, voir [`sensors/nextpm.md`](../sensors/nextpm.md) et [`json-payload.md`](json-payload.md#npm_status--bitfield-nextpm-1-octet)). `0xFF` = firmware ancien. |
|
||||
| 68 | 1 | `device_status` | | | Bitfield état boîtier, détail dans [`json-payload.md`](json-payload.md#device_status--bitfield-boîtier-1-octet). `0xFF` = firmware ancien. |
|
||||
| 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 | | | | |
|
||||
|
||||
> Les champs `latitude`, `longitude`, `misc` présents dans le JSON final (voir [`json-payload.md`](json-payload.md#géolocalisation--contexte)) **ne sont pas** dans ce descripteur 83B. **À intégrer** : voir la section [À faire](#à-faire) — l'emplacement naturel est le bloc `reserved` de 11 octets (4+4+1 = 9 octets suffisent).
|
||||
|
||||
### 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}
|
||||
```
|
||||
|
||||
Valeurs manquantes codées `-1` (sentinelle legacy). À **ne pas reproduire** pour les nouveaux formats.
|
||||
|
||||
## Parser serveur — squelette générique
|
||||
|
||||
Un parser générique qui consomme un descripteur Miotiq complet (6 colonnes) et décode n'importe quelle trame. Utile si ton backend veut refaire le décodage côté serveur plutôt que s'appuyer uniquement sur le JSON pré-décodé de Miotiq (utile en cas de doute, ou pour rejouer des trames brutes).
|
||||
|
||||
```python
|
||||
import base64
|
||||
from dataclasses import dataclass
|
||||
|
||||
@dataclass
|
||||
class Field:
|
||||
size: int # octets
|
||||
name: str
|
||||
fn: str # string | hex2dec | hex2bin | userdef | skip
|
||||
unit: str
|
||||
equation: str # ex. "x/10", "(x-32)*5/9", "" = identité
|
||||
export: str # Y (défaut) | W | N
|
||||
|
||||
def parse_descriptor(text: str) -> list[Field]:
|
||||
fields = []
|
||||
for line in text.strip().splitlines():
|
||||
parts = line.split("|")
|
||||
while len(parts) < 6:
|
||||
parts.append("")
|
||||
length, name, fn, unit, eq, export = parts[:6]
|
||||
fields.append(Field(int(length) // 2, name, fn, unit, eq, export or "Y"))
|
||||
return fields
|
||||
|
||||
def _apply_equation(x, equation: str):
|
||||
if not equation:
|
||||
return x
|
||||
# équation est une expression Miotiq en x ; on l'évalue avec x comme seul nom autorisé.
|
||||
return eval(equation, {"__builtins__": {}}, {"x": x})
|
||||
|
||||
def decode(payload: bytes, fields: list[Field], emit_units: bool = True) -> dict:
|
||||
out, off = {}, 0
|
||||
for f in fields:
|
||||
chunk = payload[off:off + f.size]
|
||||
off += f.size
|
||||
if f.fn == "skip" or f.export == "N":
|
||||
continue
|
||||
if len(chunk) != f.size:
|
||||
raise ValueError(f"payload trop court au champ {f.name}")
|
||||
|
||||
if f.fn == "string":
|
||||
# Miotiq: garde la représentation hex des octets, pas de décodage ASCII.
|
||||
val = chunk.hex()
|
||||
elif f.fn == "hex2dec":
|
||||
val = _apply_equation(int.from_bytes(chunk, "big", signed=False), f.equation)
|
||||
elif f.fn == "hex2bin":
|
||||
val = "".join(f"{b:08b}" for b in chunk)
|
||||
elif f.fn == "userdef":
|
||||
val = chunk.hex()
|
||||
else:
|
||||
raise ValueError(f"base function inconnue: {f.fn}")
|
||||
|
||||
out[f.name] = val
|
||||
# export=Y : ajouter la ligne d'unité comme le fait Miotiq
|
||||
if emit_units and f.export == "Y" and f.unit:
|
||||
out[f"{f.name}_unit"] = f"{val} {f.unit}"
|
||||
return out
|
||||
|
||||
# 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),
|
||||
}
|
||||
```
|
||||
|
||||
> Attention : `eval()` sur le champ `equation` est acceptable tant que le descripteur vient d'une source de confiance (ta conf Miotiq). Si un jour un descripteur peut être injecté par un tiers, remplacer par un parseur d'expressions arithmétiques restreint.
|
||||
|
||||
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`), 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.
|
||||
|
||||
## À faire
|
||||
|
||||
- [ ] **Intégrer `latitude` et `longitude` dans le descripteur NebuleAir Pro 4G** — aujourd'hui absents du descripteur 83B mais attendus dans le JSON final. Emplacement proposé : dans le bloc `reserved` de 11 octets. Encodage suggéré (à valider) — `hex2dec` signé sur 4 octets chacun, équation `x/1000000` pour obtenir des degrés WGS84 avec précision ~10 cm :
|
||||
|
||||
```
|
||||
8|latitude|hex2dec|degrees|x/1000000|
|
||||
8|longitude|hex2dec|degrees|x/1000000|
|
||||
```
|
||||
|
||||
> À confirmer : Miotiq supporte-t-il les entiers **signés** sur `hex2dec` ? Si non, décaler les coordonnées (ex. `(x - 2^31) / 1000000`) via la colonne equation, ou passer en représentation non-signée avec un offset.
|
||||
|
||||
- [ ] **Trouver un emplacement pour `misc`** (contexte de mesure 0–6) — 1 octet `hex2dec` sans unité ni équation. Placement proposé : à la suite de `longitude` dans l'ancien bloc `reserved`.
|
||||
|
||||
```
|
||||
2|misc|hex2dec|||
|
||||
```
|
||||
|
||||
- [ ] Une fois les trois champs ajoutés : il reste **2 octets** sur les 11 du `reserved` initial (4+4+1 = 9). Garder une ligne `4|reserved|skip|||` (2 octets) pour évolutions futures, ou les réallouer.
|
||||
|
||||
- [ ] 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 — MobileAir 17B + CSV à partir des parsers PHP en prod. |
|
||||
| 2026-04-23 | v2 | Refonte autour du format descripteur Miotiq, ajout NebuleAir Pro 4G (83B). |
|
||||
| 2026-04-23 | v3 | Format descripteur aligné sur doc officielle Miotiq : 6e colonne = export JSON (W/Y/N), ajout base functions `hex2bin` et `userdef`, colonne `equation` (expression en x). |
|
||||
| 2026-04-23 | v4 | Correction : `string` produit du hex (pas ASCII). Correction ISO_39=PM2.5 et ISO_24=PM10 (inversion). Gaz confirmés (NO₂/CO/H₂S/NH₃/O₃). Lien vers JSON canonique AirCarto 2026. |
|
||||
| 2026-04-23 | v5 | Extension prévue du descripteur NebuleAir Pro 4G avec `latitude`, `longitude`, `misc` dans le bloc `reserved` — proposition d'encodage dans la section À faire. |
|
||||
Reference in New Issue
Block a user