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). |