chore: initial skeleton — NextPM sensor, JSON format, Miotiq UDP parser
This commit is contained in:
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
.DS_Store
|
||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
*.swp
|
||||||
|
*~
|
||||||
39
CONVENTIONS.md
Normal file
39
CONVENTIONS.md
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
# Conventions
|
||||||
|
|
||||||
|
## Nommage des fichiers
|
||||||
|
|
||||||
|
- Un capteur = un fichier `sensors/<nom>.md` en minuscules, sans tiret dans le nom s'il n'est pas dans la marque (`nextpm.md`, `sps30.md`, `bme280.md`).
|
||||||
|
- Un parser = un fichier `parsers/<transport>-<origine>.md` (`udp-miotiq.md`, `mqtt-tb.md`).
|
||||||
|
- Un format = un fichier `formats/<nom>.md` décrivant le schéma et les exemples.
|
||||||
|
|
||||||
|
## Style Markdown
|
||||||
|
|
||||||
|
- Titre de niveau 1 unique en haut du fichier.
|
||||||
|
- Tables pour les registres, commandes, champs de payload.
|
||||||
|
- Blocs de code annotés du langage (` ```c `, ` ```python `, ` ```json `).
|
||||||
|
- Unités explicites dans chaque champ : `µg/m³`, `°C`, `%HR`, `ms`.
|
||||||
|
- Endianness toujours précisée pour les champs multi-octets.
|
||||||
|
|
||||||
|
## Versioning
|
||||||
|
|
||||||
|
- Ce repo suit la branche `main`. Pas de tag de version pour l'instant.
|
||||||
|
- Si un protocole évolue de façon **incompatible**, créer une section « Historique » en bas du fichier capteur/format, dater le changement et décrire le breaking change. Les firmwares référencent la révision du fichier qu'ils implémentent.
|
||||||
|
- Pour les breaking changes majeurs (nouvelle structure payload, renommage de champ), ouvrir une PR et taguer le repo `vX` après merge.
|
||||||
|
|
||||||
|
## Code d'exemple
|
||||||
|
|
||||||
|
- Les snippets dans cette doc sont **illustratifs**. Le code de production vit dans les repos firmware/backend.
|
||||||
|
- Toujours lier vers le fichier source réel quand c'est possible (ex : `server/sites/data.mobileair.fr/udp_miotiq_byte.php`).
|
||||||
|
- Éviter les exemples qui dépendent de constantes secrètes (clés API, tokens) — remplacer par `<API_KEY>`.
|
||||||
|
|
||||||
|
## Unités et types de données canoniques
|
||||||
|
|
||||||
|
| Grandeur | Unité | Type JSON | Notes |
|
||||||
|
|------------------------|---------|-----------|-------------------------------------|
|
||||||
|
| PM1 / PM2.5 / PM10 | µg/m³ | number | 1 décimale suffit |
|
||||||
|
| Température | °C | number | 1 décimale |
|
||||||
|
| Humidité relative | % | number | 0–100 |
|
||||||
|
| Pression | hPa | number | |
|
||||||
|
| Latitude / Longitude | degrés | number | WGS84, 6 décimales |
|
||||||
|
| Timestamp | secondes UTC | integer | Unix epoch, UTC toujours |
|
||||||
|
| Signal cellulaire | dBm ou % | integer | préciser selon capteur |
|
||||||
44
README.md
Normal file
44
README.md
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
# aircarto-protocols
|
||||||
|
|
||||||
|
Documentation de référence pour tous les capteurs AirCarto : protocoles de communication (UART, I2C, UDP, HTTP, MQTT…), formats de données, parsers et conventions.
|
||||||
|
|
||||||
|
**Public visé** — chaque projet firmware ou backend AirCarto consomme cette doc pour intégrer la logique commune (dialogue capteur, format d'envoi, parsing côté serveur) sans réinventer ni diverger.
|
||||||
|
|
||||||
|
## Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
aircarto-protocols/
|
||||||
|
├── CONVENTIONS.md Nommage, versioning, style doc
|
||||||
|
├── formats/ Formats d'échange de données
|
||||||
|
│ ├── json-payload.md Format JSON canonique des mesures
|
||||||
|
│ └── mqtt.md Topics et conventions MQTT
|
||||||
|
├── sensors/ Un fichier par capteur
|
||||||
|
│ ├── _TEMPLATE.md Gabarit à copier pour tout nouveau capteur
|
||||||
|
│ └── nextpm.md NextPM (Tera Sensor) — UART
|
||||||
|
└── parsers/ Parsers côté serveur / passerelle
|
||||||
|
└── udp-miotiq.md Webhook Miotiq (UDP → HTTPS JSON)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Index des capteurs
|
||||||
|
|
||||||
|
| Capteur | Interface | Doc | État |
|
||||||
|
|-------------|-----------|-----------------------------------|------------|
|
||||||
|
| NextPM | UART | [sensors/nextpm.md](sensors/nextpm.md) | Complet |
|
||||||
|
|
||||||
|
## Index des parsers
|
||||||
|
|
||||||
|
| Nom | Transport | Doc | État |
|
||||||
|
|-----------------|------------------|-----------------------------------------------|---------|
|
||||||
|
| UDP Miotiq | UDP → HTTPS JSON | [parsers/udp-miotiq.md](parsers/udp-miotiq.md) | Complet |
|
||||||
|
|
||||||
|
## Comment ajouter une entrée
|
||||||
|
|
||||||
|
- **Nouveau capteur** : copier `sensors/_TEMPLATE.md` vers `sensors/<nom>.md`, remplir les sections, mettre à jour l'index ci-dessus.
|
||||||
|
- **Nouveau format / parser** : créer le fichier sous `formats/` ou `parsers/`, mettre à jour l'index.
|
||||||
|
- Voir [CONVENTIONS.md](CONVENTIONS.md) pour le style et le nommage.
|
||||||
|
|
||||||
|
## Pourquoi ce repo
|
||||||
|
|
||||||
|
Avant : chaque firmware AirCarto (NebuleAir, ModuleAir, MobileAir…) redéfinissait ses trames, ses topics et son format JSON dans son coin. Les parsers côté serveur (`data.mobileair.fr/udp_miotiq_*.php`, `gestion.aircarto.fr`) devaient suivre. Résultat : dérives silencieuses entre capteurs, bugs d'intégration.
|
||||||
|
|
||||||
|
Ici on centralise la **spécification**. Le code de référence reste dans les repos des projets (firmwares, backends) ; ce repo décrit ce qui est attendu sur le fil.
|
||||||
83
formats/json-payload.md
Normal file
83
formats/json-payload.md
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
# Format JSON canonique — mesures capteurs
|
||||||
|
|
||||||
|
Format recommandé pour tout nouvel envoi de mesures d'un capteur AirCarto vers un backend (HTTP POST, MQTT publish, webhook…).
|
||||||
|
|
||||||
|
> Les chemins d'envoi existants (Miotiq binaire 17 octets, CSV) restent documentés dans [`parsers/udp-miotiq.md`](../parsers/udp-miotiq.md) pour compatibilité. **Ce JSON est la cible** pour les nouveaux développements.
|
||||||
|
|
||||||
|
## Schéma
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"token": "string",
|
||||||
|
"ts": 1713830400,
|
||||||
|
"type_conn": "LTE-M",
|
||||||
|
"measurements": {
|
||||||
|
"pm1": 0.0,
|
||||||
|
"pm25": 0.0,
|
||||||
|
"pm10": 0.0,
|
||||||
|
"temperature": 0.0,
|
||||||
|
"humidity": 0.0
|
||||||
|
},
|
||||||
|
"gps": {
|
||||||
|
"lat": 43.605,
|
||||||
|
"lon": 1.444,
|
||||||
|
"sats": 8
|
||||||
|
},
|
||||||
|
"link": {
|
||||||
|
"signal": -78,
|
||||||
|
"imsi": "208xxxxxxxxxxx"
|
||||||
|
},
|
||||||
|
"fw": "1.4.2"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Champs
|
||||||
|
|
||||||
|
| Champ | Type | Obligatoire | Description |
|
||||||
|
|-------------------------|---------|-------------|--------------------------------------------------------------------|
|
||||||
|
| `token` | string | oui | Identifiant unique du capteur (= clé en base `capteurs.capteurs`). |
|
||||||
|
| `ts` | integer | oui | Timestamp Unix UTC en secondes de la mesure (pas de la réception). |
|
||||||
|
| `type_conn` | string | oui | Un des : `WiFi`, `LTE-M`, `LTE-BIN`, `NB-IoT`, `LoRa`, `Ethernet`. |
|
||||||
|
| `measurements.pm1` | number | si mesuré | µg/m³ |
|
||||||
|
| `measurements.pm25` | number | si mesuré | µg/m³ |
|
||||||
|
| `measurements.pm10` | number | si mesuré | µg/m³ |
|
||||||
|
| `measurements.temperature` | number | si mesuré | °C |
|
||||||
|
| `measurements.humidity` | number | si mesuré | % HR |
|
||||||
|
| `gps.lat` | number | si GPS | Degrés décimaux WGS84, 6 décimales. |
|
||||||
|
| `gps.lon` | number | si GPS | Idem. |
|
||||||
|
| `gps.sats` | integer | si GPS | Nombre de satellites vus. |
|
||||||
|
| `link.signal` | integer | non | RSSI en dBm (négatif) **ou** % de qualité ; préciser dans `type_conn`. |
|
||||||
|
| `link.imsi` | string | non | IMSI de la SIM (modem cellulaire uniquement). |
|
||||||
|
| `fw` | string | non | Version firmware du capteur, ex. `1.4.2`. |
|
||||||
|
|
||||||
|
## Règles
|
||||||
|
|
||||||
|
- Omettre un champ si non mesuré. Ne pas envoyer `null` ni `-1` comme valeur sentinelle (hérité du legacy CSV Miotiq).
|
||||||
|
- `ts` côté capteur si dispo (GPS / NTP), sinon côté serveur à la réception — documenter au cas par cas dans la doc du capteur.
|
||||||
|
- Toutes les valeurs numériques utilisent le point `.` comme séparateur décimal.
|
||||||
|
- Un seul capteur par message. Pas de batch (à revoir si besoin).
|
||||||
|
|
||||||
|
## Réponse attendue du serveur
|
||||||
|
|
||||||
|
```json
|
||||||
|
{ "ok": true }
|
||||||
|
```
|
||||||
|
|
||||||
|
Ou en cas d'erreur :
|
||||||
|
|
||||||
|
```json
|
||||||
|
{ "ok": false, "error": "token inconnu" }
|
||||||
|
```
|
||||||
|
|
||||||
|
HTTP 200 dans les deux cas pour ne pas déclencher de retry cellulaire côté modem ; le `ok: false` indique à la supervision qu'il y a un problème applicatif.
|
||||||
|
|
||||||
|
## Exemple minimal
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"token": "001",
|
||||||
|
"ts": 1713830400,
|
||||||
|
"type_conn": "WiFi",
|
||||||
|
"measurements": { "pm25": 12.3 }
|
||||||
|
}
|
||||||
|
```
|
||||||
38
formats/mqtt.md
Normal file
38
formats/mqtt.md
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
# MQTT — conventions
|
||||||
|
|
||||||
|
> **Statut** : squelette. À compléter au premier déploiement MQTT AirCarto.
|
||||||
|
|
||||||
|
## Broker
|
||||||
|
|
||||||
|
- À définir : URL, port (1883 / 8883 TLS), credentials.
|
||||||
|
|
||||||
|
## Topics
|
||||||
|
|
||||||
|
Convention proposée :
|
||||||
|
|
||||||
|
```
|
||||||
|
aircarto/<projet>/<token>/<canal>
|
||||||
|
```
|
||||||
|
|
||||||
|
- `projet` : `nebuleair`, `moduleair`, `mobileair`, …
|
||||||
|
- `token` : identifiant unique du capteur.
|
||||||
|
- `canal` :
|
||||||
|
- `telemetry` : mesures périodiques (publish capteur → broker), payload JSON conforme à [`json-payload.md`](json-payload.md).
|
||||||
|
- `status` : en ligne / hors ligne, version firmware (retain, LWT).
|
||||||
|
- `cmd` : commandes broker → capteur (ex. reboot, changer la fréquence).
|
||||||
|
- `ack` : accusés capteur → broker.
|
||||||
|
|
||||||
|
Exemple : `aircarto/nebuleair/001/telemetry`.
|
||||||
|
|
||||||
|
## QoS et retain
|
||||||
|
|
||||||
|
- `telemetry` : **QoS 0** (fire-and-forget, InfluxDB tolère les pertes occasionnelles).
|
||||||
|
- `status` : **QoS 1** + **retain = true** + LWT pour détecter les déconnexions.
|
||||||
|
- `cmd` : **QoS 1**, pas de retain.
|
||||||
|
|
||||||
|
## À faire
|
||||||
|
|
||||||
|
- [ ] Nommer le broker de référence (Mosquitto ? HiveMQ ? EMQX ?).
|
||||||
|
- [ ] Documenter l'authentification (username+password, certificats ?).
|
||||||
|
- [ ] Schéma exact des payloads `status` et `cmd`.
|
||||||
|
- [ ] Exemple de wildcard subscription côté backend.
|
||||||
167
parsers/udp-miotiq.md
Normal file
167
parsers/udp-miotiq.md
Normal file
@@ -0,0 +1,167 @@
|
|||||||
|
# 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. |
|
||||||
90
sensors/_TEMPLATE.md
Normal file
90
sensors/_TEMPLATE.md
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
# <Nom du capteur>
|
||||||
|
|
||||||
|
Remplace ce bloc par une ligne de résumé : fabricant, grandeurs mesurées, interface principale.
|
||||||
|
|
||||||
|
> Exemple : « Tera Sensor NextPM — capteur de particules PM1/PM2.5/PM10, interface UART. »
|
||||||
|
|
||||||
|
## Caractéristiques
|
||||||
|
|
||||||
|
| Paramètre | Valeur |
|
||||||
|
|--------------------------|-------------------------------|
|
||||||
|
| Fabricant / modèle | |
|
||||||
|
| Grandeurs mesurées | PM1, PM2.5, PM10 / T / HR / … |
|
||||||
|
| Plage | |
|
||||||
|
| Précision | |
|
||||||
|
| Alimentation | 3V3 / 5V, mA typique |
|
||||||
|
| Interface | UART / I2C / SPI / analogique |
|
||||||
|
| Datasheet | URL |
|
||||||
|
|
||||||
|
## Câblage
|
||||||
|
|
||||||
|
Lister **chaque fil** avec sa couleur, le pin côté capteur et le pin côté MCU de référence.
|
||||||
|
|
||||||
|
| Fil capteur | Pin capteur | Fonction | Pin MCU (exemple nRF9151) |
|
||||||
|
|-------------|-------------|----------|---------------------------|
|
||||||
|
| Rouge | 1 | VCC | 3V3 |
|
||||||
|
| Noir | 2 | GND | GND |
|
||||||
|
| … | | | |
|
||||||
|
|
||||||
|
## Protocole
|
||||||
|
|
||||||
|
- Transport : UART 115200 8N1 / I2C 100 kHz / …
|
||||||
|
- Adresse (si I2C) :
|
||||||
|
- Polarité, endianness :
|
||||||
|
|
||||||
|
### Format de trame
|
||||||
|
|
||||||
|
Décrire exactement la structure : header, champs, checksum, taille.
|
||||||
|
|
||||||
|
```
|
||||||
|
+------+-----+------+------+-----+
|
||||||
|
| HDR | CMD | LEN | DATA | CS |
|
||||||
|
+------+-----+------+------+-----+
|
||||||
|
1B 1B 1B Nb 1B
|
||||||
|
```
|
||||||
|
|
||||||
|
Checksum : algorithme exact (`256 - sum(prev) mod 256`, CRC8, CRC16 poly…).
|
||||||
|
|
||||||
|
### Commandes
|
||||||
|
|
||||||
|
| Commande | Code | Requête | Réponse (taille, champs) |
|
||||||
|
|----------|------|------------------------|-----------------------------------|
|
||||||
|
| Read X | 0x11 | `81 11 …` | `81 11 state <data> cs` |
|
||||||
|
| Sleep | 0x15 | | |
|
||||||
|
|
||||||
|
### Décodage des champs
|
||||||
|
|
||||||
|
Pour chaque donnée retournée, précise :
|
||||||
|
- Position, taille, endianness.
|
||||||
|
- Facteur d'échelle (ex. `val / 10.0` pour obtenir des µg/m³).
|
||||||
|
- Unité finale.
|
||||||
|
|
||||||
|
## Exemple de code
|
||||||
|
|
||||||
|
### Lecture (pseudo-C)
|
||||||
|
|
||||||
|
```c
|
||||||
|
// TODO
|
||||||
|
```
|
||||||
|
|
||||||
|
### Lecture (Python)
|
||||||
|
|
||||||
|
```python
|
||||||
|
# TODO
|
||||||
|
```
|
||||||
|
|
||||||
|
## Spécificités connues / pièges
|
||||||
|
|
||||||
|
- …
|
||||||
|
|
||||||
|
## Références
|
||||||
|
|
||||||
|
- Datasheet : URL
|
||||||
|
- Code de référence utilisé en prod : `<repo>/src/drivers/<fichier>`
|
||||||
|
- Note d'application constructeur : URL
|
||||||
|
|
||||||
|
## Historique
|
||||||
|
|
||||||
|
| Date | Révision | Changement |
|
||||||
|
|------------|----------|-----------------------------------------------|
|
||||||
|
| YYYY-MM-DD | v1 | Création de la doc. |
|
||||||
183
sensors/nextpm.md
Normal file
183
sensors/nextpm.md
Normal file
@@ -0,0 +1,183 @@
|
|||||||
|
# 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**.
|
||||||
|
|
||||||
|
## 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/ |
|
||||||
|
|
||||||
|
## 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 |
|
||||||
|
|
||||||
|
> 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.
|
||||||
|
|
||||||
|
## Protocole UART
|
||||||
|
|
||||||
|
- 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.
|
||||||
|
|
||||||
|
### Structure des trames
|
||||||
|
|
||||||
|
**Requête MCU → capteur** (sans données) :
|
||||||
|
|
||||||
|
```
|
||||||
|
+------+------+------+
|
||||||
|
| 0x81 | CMD | CS |
|
||||||
|
+------+------+------+
|
||||||
|
```
|
||||||
|
|
||||||
|
**Requête MCU → capteur** (avec données) :
|
||||||
|
|
||||||
|
```
|
||||||
|
+------+------+----------+------+
|
||||||
|
| 0x81 | CMD | DATA... | CS |
|
||||||
|
+------+------+----------+------+
|
||||||
|
```
|
||||||
|
|
||||||
|
**Réponse capteur → MCU** :
|
||||||
|
|
||||||
|
```
|
||||||
|
+------+------+-------+------------+------+
|
||||||
|
| 0x81 | CMD | STATE | DATA... | CS |
|
||||||
|
+------+------+-------+------------+------+
|
||||||
|
```
|
||||||
|
|
||||||
|
`STATE` (1 octet) est un bitfield d'état :
|
||||||
|
|
||||||
|
| Bit | Signification |
|
||||||
|
|-----|-------------------------------------------------|
|
||||||
|
| 0 | Fan default (1 = vitesse dégradée / anormale) |
|
||||||
|
| 1 | Memory error |
|
||||||
|
| 2 | Sensor laser default |
|
||||||
|
| 3 | T/RH sensor default |
|
||||||
|
| 4 | Sleep mode |
|
||||||
|
| 5–7 | Réservés |
|
||||||
|
|
||||||
|
Un `STATE == 0x00` indique un fonctionnement nominal.
|
||||||
|
|
||||||
|
### 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 <stdint.h>
|
||||||
|
#include <stddef.h>
|
||||||
|
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 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.
|
||||||
|
|
||||||
|
## 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.
|
||||||
|
|
||||||
|
## 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).
|
||||||
|
|
||||||
|
## Historique
|
||||||
|
|
||||||
|
| Date | Révision | Changement |
|
||||||
|
|------------|----------|---------------------------------|
|
||||||
|
| 2026-04-23 | v1 | Création de la doc. |
|
||||||
Reference in New Issue
Block a user