From a27af69f27e2f2da4599ecf2b299acbb9c528688 Mon Sep 17 00:00:00 2001 From: Your Name Date: Mon, 4 May 2026 13:52:50 +0200 Subject: [PATCH] docs(nextpm): rewrite around Modbus RTU mode The previous doc described only the proprietary 0x81 UART protocol, but all current AirCarto firmwares (NebuleAir Pro 4G, ModuleAir light) talk to the NextPM in Modbus RTU. Document the actual register mapping (PM at 0x38/0x44 for 10s/60s averages, T/HR at 0x6B/0x6A, status at 0x13, 5 granulometric channels at 0x80-0x88), the LSW-MSW word order, and both reading strategies (per-register and bulk read). Move the proprietary 0x81 protocol to an annex; drop Python/C examples (to be published in separate docs). Co-Authored-By: Claude Opus 4.7 (1M context) --- sensors/nextpm.md | 257 ++++++++++++++++++++++------------------------ 1 file changed, 124 insertions(+), 133 deletions(-) diff --git a/sensors/nextpm.md b/sensors/nextpm.md index 045e664..6d77fd4 100644 --- a/sensors/nextpm.md +++ b/sensors/nextpm.md @@ -1,66 +1,118 @@ # NextPM (Tera Sensor) -Capteur de particules **Tera Sensor NextPM** : mesure PM1, PM2.5, PM10 en masse (µg/m³) et en nombre (#/cm³), intègre un capteur de température et d'humidité embarqué. Communication **UART**. +Capteur de particules **Tera Sensor NextPM** : mesure PM1, PM2.5, PM10 (µg/m³), 5 canaux de comptage granulométrique, et intègre un capteur de température et d'humidité interne. Communication **UART**. + +Le capteur expose deux modes de communication mutuellement exclusifs : + +- **Modbus RTU** (utilisé par défaut sur les intégrations AirCarto, dont le NebuleAir Pro 4G) — décrit dans le corps de cette doc. +- **Protocole propriétaire Tera Sensor** (préambule `0x81`) — résumé en annexe à la fin. ## Caractéristiques -| Paramètre | Valeur | -|------------------------|--------------------------------------------------| -| Fabricant / modèle | Tera Sensor — NextPM | -| Grandeurs | PM1, PM2.5, PM10 (µg/m³ et #/cm³), T (°C), HR (%) | -| Plage PM | 0 – 1000 µg/m³ | -| Alimentation | 5 V, ~100 mA pic, ~35 mA en veille | -| Interface | UART 3V3 (logique CMOS 3.3 V tolérante 5 V TX) | -| Vitesse UART par défaut | 115200 baud, 8N1 | -| Datasheet | https://www.tera-sensor.com/ | +| Paramètre | Valeur | +|-------------------------|-----------------------------------------------------| +| Fabricant / modèle | Tera Sensor — NextPM | +| Grandeurs | PM1, PM2.5, PM10 (µg/m³), 5 canaux #, T (°C), HR (%) | +| Plage PM | 0 – 1000 µg/m³ | +| Alimentation | 5 V, ~100 mA pic, ~35 mA en veille | +| Interface | UART 3V3 (logique CMOS 3.3 V tolérante 5 V TX) | +| Vitesse UART | 115200 baud, **8E1** en Modbus (8N1 en propriétaire) | +| Rafraîchissement | 10 s côté capteur | +| Datasheet | https://www.tera-sensor.com/ | ## Câblage Connecteur JST-PH 4 broches du NextPM (vue côté capteur) : -| Pin capteur | Fil usuel | Fonction | MCU (exemple) | -|-------------|-----------|----------------|------------------------| -| 1 | Rouge | VCC 5 V | 5V | -| 2 | Noir | GND | GND | -| 3 | Blanc | RX capteur (← TX MCU) | UART TX du MCU | -| 4 | Vert | TX capteur (→ RX MCU) | UART RX du MCU | +| Pin capteur | Fil usuel | Fonction | MCU (exemple) | +|-------------|-----------|-----------------------|------------------| +| 1 | Rouge | VCC 5 V | 5V | +| 2 | Noir | GND | GND | +| 3 | Blanc | RX capteur (← TX MCU) | UART TX du MCU | +| 4 | Vert | TX capteur (→ RX MCU) | UART RX du MCU | -> Le NextPM est en 3V3 côté logique : si le MCU est en 3V3 (nRF9151, ESP32…), connecter directement. Pour un MCU 5 V, prévoir un level shifter sur TX→RX. +> Le NextPM est en 3V3 côté logique : si le MCU est en 3V3 (nRF9151, ESP32, Raspberry Pi…), connecter directement. Pour un MCU 5 V, prévoir un level shifter sur TX→RX. -## Protocole UART +## Protocole Modbus RTU -- 115200 bauds, 8 bits, pas de parité, 1 stop. -- Toutes les trames commencent par **`0x81`** (préambule). -- Checksum = `(256 - somme(octets précédents)) mod 256`, placé en dernier octet. -- Délai de traitement côté capteur : 15–30 ms typique ; prévoir timeout de lecture de 200 ms. +### Paramètres de la liaison -### Structure des trames +- 115200 baud, **8 bits, parité paire (EVEN), 1 stop**. +- Slave address par défaut : `0x01` (configurable côté capteur). +- Function code utilisé : `0x03` (Read Holding Registers). +- Checksum : **CRC-16 Modbus** (poly `0xA001`), placé en fin de trame, **little-endian** (LSB d'abord). +- Temps de traitement côté capteur : prévoir un délai de **~200 ms** entre l'envoi de la requête et la lecture de la réponse. -**Requête MCU → capteur** (sans données) : +### Format de trame + +**Requête** (8 octets pour une lecture de registres) : ``` -+------+------+------+ -| 0x81 | CMD | CS | -+------+------+------+ ++--------+--------+----------+----------+--------+ +| ADDR | FUNC | REGADDR | QTY | CRC | +| 1B | 1B | 2B | 2B | 2B | ++--------+--------+----------+----------+--------+ + big-endian big-endian little-endian ``` -**Requête MCU → capteur** (avec données) : +**Réponse** : ``` -+------+------+----------+------+ -| 0x81 | CMD | DATA... | CS | -+------+------+----------+------+ ++--------+--------+--------+--------------------+--------+ +| ADDR | FUNC | BCNT | DATA | CRC | +| 1B | 1B | 1B | 2 × QTY octets | 2B | ++--------+--------+--------+--------------------+--------+ ``` -**Réponse capteur → MCU** : +`BCNT` = nombre d'octets de données = `2 × QTY`. Chaque registre fait 2 octets, big-endian. + +En cas d'erreur, le capteur répond avec `FUNC | 0x80` suivi d'un code d'exception Modbus standard. + +### Stratégies de lecture + +Deux approches pratiquées sur les firmwares AirCarto, toutes deux valides : + +- **Lecture par registre** (ex. ModuleAir) : une requête Modbus par valeur (PM1, PM2.5, PM10, T, HR, status). Plus simple à implémenter avec une lib Modbus standard (`ModbusMaster` Arduino, `pymodbus`…). +- **Lecture en bloc** (ex. NebuleAir Pro 4G) : une seule requête lit 85 registres à partir de `0x0038`, et le firmware extrait localement chaque champ. Évite les courses entre mesures et minimise les échanges UART quand on veut tout récupérer (PM + 5 canaux + T/HR) : ``` -+------+------+-------+------------+------+ -| 0x81 | CMD | STATE | DATA... | CS | -+------+------+-------+------------+------+ +Requête bloc complet : 01 03 00 38 00 55 ``` -`STATE` (1 octet) est un bitfield d'état : +### Mapping des registres + +Le NextPM expose les concentrations PM avec **plusieurs fenêtres de moyennage glissant** (10 s, 60 s, et — selon firmware — 900 s), chacune occupant un bloc de 6 registres consécutifs (PM1 / PM2.5 / PM10 sur 2 registres uint32 chacun). + +| Registre (déc) | Registre (hex) | Champ | Taille | Décodage | Unité | +|----------------|----------------|------------------------------------|------------|------------------------|------------| +| 19 | 0x0013 | Status capteur | 1 registre | `value & 0xFF` | bitfield | +| 56–57 | 0x0038 | PM1 — moyenne 10 s | 2 registres (uint32) | `value / 1000` | µg/m³ | +| 58–59 | 0x003A | PM2.5 — moyenne 10 s | 2 registres (uint32) | `value / 1000` | µg/m³ | +| 60–61 | 0x003C | PM10 — moyenne 10 s | 2 registres (uint32) | `value / 1000` | µg/m³ | +| 68–69 | 0x0044 | PM1 — moyenne 60 s | 2 registres (uint32) | `value / 1000` | µg/m³ | +| 70–71 | 0x0046 | PM2.5 — moyenne 60 s | 2 registres (uint32) | `value / 1000` | µg/m³ | +| 72–73 | 0x0048 | PM10 — moyenne 60 s | 2 registres (uint32) | `value / 1000` | µg/m³ | +| 106 | 0x006A | Humidité relative | 1 registre | `value / 100` | %HR | +| 107 | 0x006B | Température interne | 1 registre | `value / 100` (signed) | °C | +| 128–129 | 0x0080 | Canal 1 — particules 0.2–0.5 µm | 2 registres (uint32) | comptage brut | # | +| 130–131 | 0x0082 | Canal 2 — particules 0.5–1.0 µm | 2 registres (uint32) | comptage brut | # | +| 132–133 | 0x0084 | Canal 3 — particules 1.0–2.5 µm | 2 registres (uint32) | comptage brut | # | +| 134–135 | 0x0086 | Canal 4 — particules 2.5–5.0 µm | 2 registres (uint32) | comptage brut | # | +| 136–137 | 0x0088 | Canal 5 — particules 5.0–10 µm | 2 registres (uint32) | comptage brut | # | + +> Une fenêtre de moyennage 900 s (15 min) existe sur le mode propriétaire (commande `0x13`) ; le bloc lu en `0x38..0x8C` la couvre probablement entre `0x4A` et `0x55`, mais l'adresse exacte n'a pas été confirmée par lecture directe — vérifier datasheet avant usage. + +**Encodage des uint32 sur 2 registres :** chaque registre Modbus est big-endian sur ses 2 octets, mais l'ordre des deux registres est **LSW d'abord, MSW ensuite** (« little-endian word order »). Reconstruction : + +``` +value = (MSW << 16) | LSW +``` + +avec `LSW` = registre N et `MSW` = registre N+1. + +### Bitfield du registre status (`0x0013`) + +Octet de poids faible : | Bit | Signification | |-----|-------------------------------------------------| @@ -71,113 +123,52 @@ Connecteur JST-PH 4 broches du NextPM (vue côté capteur) : | 4 | Sleep mode | | 5–7 | Réservés | -Un `STATE == 0x00` indique un fonctionnement nominal. +Un status `0x00` indique un fonctionnement nominal. Loguer la valeur à chaque cycle de mesure et la remonter au backend si elle est non nulle. -### Commandes principales - -| Commande | Code | Taille requête | Taille réponse | Notes | -|-------------------------------|-------|----------------|----------------|----------------------------------------------| -| Read concentrations 10 s | 0x11 | 3 | 16 | Moyenne glissante 10 s | -| Read concentrations 60 s | 0x12 | 3 | 16 | Moyenne glissante 60 s (plus stable) | -| Read concentrations 900 s | 0x13 | 3 | 16 | Moyenne glissante 15 min | -| Read T/RH | 0x14 | 3 | 10 | Température et humidité | -| Sleep (fan off) | 0x15 | 3 | 4 | Passe en veille, ventilateur coupé | -| Wake / fan on | 0x16 | 3 | 4 | Sort de veille | -| Set fan speed | 0x17 | 4 | 4 | 1 octet supplémentaire (% vitesse) | -| Set clock | 0x21 | 9 | 4 | Horodatage interne | -| Read firmware version | 0x41 | 3 | variable | Retourne une chaîne ASCII | - -> Les codes et tailles ci-dessus sont issus de l'intégration de référence. **Toujours recroiser avec la datasheet Tera Sensor la plus récente** avant d'implémenter une nouvelle version firmware — certains registres ont changé entre révisions hardware. - -### Format de la réponse `0x11` / `0x12` / `0x13` - -16 octets de DATA, big-endian : - -| Offset | Taille | Champ | Unité | Décodage | -|--------|--------|-----------------------|---------------|-------------------------| -| 0 | 2 | PM1 number | #/cm³ | valeur brute | -| 2 | 2 | PM2.5 number | #/cm³ | valeur brute | -| 4 | 2 | PM10 number | #/cm³ | valeur brute | -| 6 | 2 | PM1 mass | µg/m³ × 10 | `raw / 10.0` | -| 8 | 2 | PM2.5 mass | µg/m³ × 10 | `raw / 10.0` | -| 10 | 2 | PM10 mass | µg/m³ × 10 | `raw / 10.0` | - -### Format de la réponse `0x14` - -| Offset | Taille | Champ | Unité | Décodage | -|--------|--------|---------------|--------------|---------------------------| -| 0 | 2 | Température | °C × 100 | `raw / 100.0` (signed) | -| 2 | 2 | Humidité | %HR × 100 | `raw / 100.0` | - -## Exemple de code - -### Construction de la requête (C) - -```c -#include -#include - -static uint8_t nextpm_checksum(const uint8_t *buf, size_t len) { - uint32_t sum = 0; - for (size_t i = 0; i < len; i++) sum += buf[i]; - return (uint8_t)(256 - (sum & 0xFF)); -} - -// Envoie une commande sans données (0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x41) -size_t nextpm_build_cmd(uint8_t cmd, uint8_t *out) { - out[0] = 0x81; - out[1] = cmd; - out[2] = nextpm_checksum(out, 2); - return 3; -} -``` - -### Décodage de la réponse 0x11 / 0x12 / 0x13 (Python) - -```python -import struct - -def decode_pm(frame: bytes) -> dict: - if len(frame) != 16 or frame[0] != 0x81: - raise ValueError("trame NextPM invalide") - cmd, state = frame[1], frame[2] - data = frame[3:15] - cs_expected = (256 - sum(frame[:15])) & 0xFF - if cs_expected != frame[15]: - raise ValueError("checksum NextPM incorrect") - - pm1_n, pm25_n, pm10_n, pm1_m, pm25_m, pm10_m = struct.unpack(">HHHHHH", data) - return { - "state": state, - "pm1_num": pm1_n, - "pm25_num": pm25_n, - "pm10_num": pm10_n, - "pm1": pm1_m / 10.0, - "pm25": pm25_m / 10.0, - "pm10": pm10_m / 10.0, - } -``` +> Les adresses et tailles de registres ci-dessus correspondent aux versions firmware NextPM utilisées par AirCarto en 2026. Toujours recroiser avec la datasheet Tera Sensor la plus récente avant d'intégrer une nouvelle révision hardware/firmware. ## Mise en œuvre recommandée -1. Au boot : `0x16` (wake) puis laisser 30 s de stabilisation du flux avant de lire. -2. Utiliser `0x12` (moyenne 60 s) pour l'envoi réseau standard — meilleur compromis bruit/latence. -3. Loguer `STATE` à chaque lecture ; remonter au backend si `STATE != 0`. -4. En cas de cycle veille/mesure (ex. applications sur batterie) : `0x15` (sleep), attendre la prochaine fenêtre, `0x16`, 30 s stabilisation, lire, renvoyer en sleep. +1. Choisir la fenêtre de moyennage selon le cas d'usage : + - **10 s** (`0x0038`) — réactivité, débogage, exposition courte. Cas typique : NebuleAir Pro 4G. + - **60 s** (`0x0044`) — stabilité, transmission réseau périodique. Cas typique : ModuleAir light. + - **900 s** — historisation longue durée (à confirmer datasheet). +2. Choisir la stratégie de lecture (cf. plus haut) : registre par registre si on n'a besoin que des PM, lecture en bloc si on veut aussi les 5 canaux et T/HR. +3. Cycle de lecture aligné sur le rafraîchissement interne du capteur (10 s) — il est inutile d'interroger plus vite. +4. Ignorer les **2 premières minutes** après mise sous tension : le ventilateur monte en régime et les concentrations sont sous-estimées. +5. Toujours valider le CRC-16 (la plupart des libs Modbus le font) avant d'utiliser la trame ; en cas d'échec, écarter la mesure et incrémenter un compteur d'erreurs. +6. Si toutes les valeurs lues sont nulles ou si la requête timeout, écrire en base une ligne « erreur » (ex. PM = 0, status = `0xFF`) plutôt que sauter le cycle, pour pouvoir distinguer une absence de capteur d'une vraie mesure à 0. ## Pièges connus -- Les **2 premières minutes** après le wake sont à ignorer (le ventilateur monte en régime, concentrations sous-estimées). -- Si le checksum est faux une fois sur deux : vérifier la masse commune (GND) entre MCU et NextPM — flottement du GND observé sur certains câbles longs. -- Le NextPM renvoie parfois un octet `0x00` avant le préambule si la ligne UART n'était pas propre au démarrage : implémenter une resynchronisation sur `0x81` côté parser MCU. +- **Parité Modbus** : le NextPM exige du 8E1, alors que la majorité des UART sont configurés en 8N1 par défaut. Une confusion sur la parité produit un silence radio total côté capteur. +- **Word order des uint32** : `(MSW << 16) | LSW` (LSW d'abord). Inverser donne des valeurs énormes parfois plausibles, à surveiller en intégration. +- **Masse commune** : si le checksum/CRC échoue de manière intermittente, vérifier la masse (GND) entre MCU et NextPM — flottement de GND observé sur câbles longs. +- **Démarrage à froid** : prévoir 30 s de stabilisation après mise sous tension ou sortie de veille avant la première lecture exploitée. + +## Annexe : protocole propriétaire `0x81` + +Le NextPM supporte également un protocole UART propriétaire Tera Sensor, **non utilisé par les firmwares AirCarto actuels** mais documenté dans la datasheet officielle : + +- UART 115200 **8N1** (pas de parité). +- Toutes les trames commencent par le préambule `0x81`. +- Checksum simple sur 1 octet : `(256 − somme(octets précédents)) mod 256`. +- Commandes (extrait) : `0x11` / `0x12` / `0x13` lecture concentrations 10/60/900 s, `0x14` lecture T/HR, `0x15` sleep, `0x16` wake, `0x17` set fan speed, `0x21` set clock, `0x41` firmware version. +- L'octet `STATE` est inclus inline dans la réponse, juste après le code de commande. + +Pour une intégration complète dans ce mode, se référer à la datasheet Tera Sensor — ce mode et le mode Modbus RTU s'excluent mutuellement. ## Références -- Datasheet : https://www.tera-sensor.com/ (demander la dernière révision PDF) -- Code firmware de référence AirCarto : `nebuleair_pro_4g` et `esp32_NPM_only` sur [gitea.aircarto.fr](https://gitea.aircarto.fr/PaulVua). +- Datasheet Tera Sensor : https://www.tera-sensor.com/ (demander la dernière révision PDF). +- Code de référence AirCarto en mode Modbus : + - Lecture en bloc (Python, Raspberry Pi) : `nebuleair_pro_4g/NPM/get_data_modbus_v3.py` sur [gitea.aircarto.fr](https://gitea.aircarto.fr/PaulVua). + - Lecture par registre (C++/Arduino, ESP32) : `moduleair_light/src/sensors.cpp` sur [github.com/aircarto](https://github.com/aircarto). ## Historique -| Date | Révision | Changement | -|------------|----------|---------------------------------| -| 2026-04-23 | v1 | Création de la doc. | +| Date | Révision | Changement | +|------------|----------|------------------------------------------------------------------------| +| 2026-04-23 | v1 | Création de la doc (mode propriétaire `0x81`). | +| 2026-05-04 | v2 | Réécriture autour du mode Modbus RTU effectivement utilisé en prod ; ajout du mapping de registres et des 5 canaux ; mode propriétaire déplacé en annexe ; exemples de code retirés (à publier dans des docs séparées). | +| 2026-05-04 | v2.1 | Ajout de la fenêtre de moyennage 60 s (`0x0044`/`0x0046`/`0x0048`) constatée sur ModuleAir light ; explicite les deux stratégies de lecture (registre par registre vs bloc complet). |