Probleme vu sur pro100: sonde S88 muette (panne cablage) mais write_data.py n'ecrivait rien -> la base gardait la derniere valeur (487 ppm d'hier) et la loop d'envoi la transmettait en boucle. - data_S88: nouvelle colonne s88_status (0=OK, 0xFF=sonde muette), comme npm_status/noise_status. Migration via create_db.py + set_config.py + self-heal. - S88/write_data.py: ecrit DESORMAIS une ligne a chaque cycle (CO2=0 + 0xFF si pas de reponse). Connexion SQLite timeout=10 (anti database-is-locked). - SARA_send_data_v2.py: lit s88_status; si 0xFF -> bytes 81-82 restent 0xFFFF (CO2 absent) au lieu d'envoyer une valeur perimee. Compatible bases non migrees. - database.html + launcher.php: badge statut + colonne dans les exports CSV. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Senseair S88 — Capteur CO2 NDIR
Notes essentielles extraites des datasheets Senseair (Product Specification PSP14279 rev 3, et "Modbus on Senseair S88" TDE14367 rev 5). Les PDF originaux ne sont pas versionnés (trop lourds, pas utiles sur les capteurs).
Modèle
- Senseair S88 Residential — Article No. 004-1-0100
- Capteur CO2 miniature NDIR (Non-Dispersive InfraRed)
- Dimensions : 33.9 × 19.6 × 9.7 mm — poids < 5 g
- Compatibilité registres Modbus avec le Senseair S8
Caractéristiques mesure
| Paramètre | Valeur |
|---|---|
| Gaz mesuré | CO2 |
| Plage | 400 – 10 000 ppm |
| Intervalle de mesure | 2 s |
| Précision 400–3000 ppm | ±25 ppm ±3% de la lecture |
| Précision 3000–10000 ppm | ±10% de la lecture |
| Temps de chauffe | ≤ 10 s |
| Temps de réponse t63% | ≤ 30 s |
| Conditions d'opération | 0–50 °C, 0–85 %RH (sans condensation, dew point ≤ 35 °C) |
| Dépendance pression | 1.6 % par kPa d'écart à la pression normale |
| Durée de vie | > 15 ans |
| Maintenance | Sans entretien (ABC : Automatic Baseline Correction activé par défaut) |
Alimentation
- Tension : 4.5 – 5.25 V (le 5V du Pi convient)
- Courant pic : ≤ 300 mA (pendant la rampe de la lampe IR)
- Courant moyen : ≤ 30 mA
- Non protégée contre surtensions / inversion polarité — attention au câblage
Pinout
G+ ●─┐ ┌─● DVCC_out (3.3V, sortie régulateur interne — ne PAS utiliser)
G0 ●─┤ ├─● UART_TxD (3.3V CMOS, sortie capteur)
Alarm_OC●─┤ ├─● UART_RxD (3.3V CMOS, entrée capteur)
PWM 1kHz●─┘ ├─● UART_R/T (direction RS-485, à laisser flottant en UART direct)
└─● bCAL_in (entrée calibration manuelle)
Câblage vers la PCB NebuleAir Pro
Le S88 se branche sur un connecteur libre de la PCB custom (NPM1/NPM2/NPM3 — voir mapping silkscreen ↔ ttyAMA dans le CLAUDE.md à la racine). Sélectionne ensuite le port correspondant dans admin.html → "Send CO2 sensor data (Senseair S88)" → dropdown "Port UART".
| S88 | Connecteur PCB |
|---|---|
| G+ | 5V |
| G0 | GND |
| UART_TxD | RxD du connecteur (crossover) |
| UART_RxD | TxD du connecteur (crossover) |
| UART_R/T | non connecté |
Les niveaux UART du S88 sont 3.3V CMOS — directement compatibles avec le Pi. Pas besoin de level shifter, pas besoin de RS-485 transceiver.
Protocole Modbus RTU
- Mode : RTU (seul mode supporté)
- Baudrate : 9600 par défaut (19200 aussi supporté)
- Format : 8 bits de données, pas de parité, 1 stop bit en réception / 2 stop bits en transmission (config par défaut)
- Adresse esclave : 1–247 (configurable via HR). 0xFE = "any address" — répondue par n'importe quel S88, utile quand on ne connaît pas l'adresse individuelle (à n'utiliser qu'en bench, pas en réseau multi-capteurs)
- Adresse 0 : broadcast (commandes write seulement)
- Réponse timeout : ≤ 180 ms
Fonctions supportées
| Code | Fonction |
|---|---|
| 0x03 | Read Holding Registers (config, plage 0x0000–0x0020) |
| 0x04 | Read Input Registers (mesures, plage 0x0000–0x001F) |
| 0x06 | Write Single Register |
| 0x10 | Write Multiple Registers |
| 0x2B / 0x0E | Read Device Identification (Vendor Name, ProductCode, MajorMinorRevision) |
Input Registers (mesures, fonction 0x04)
| Reg | Offset | Nom | Description |
|---|---|---|---|
| IR1 | 0x0000 | MeterStatus | Bits d'état (DI1=Fatal error, DI3=Algorithm error, DI4=Output error, DI5=Self-diagnostic error, DI6=Out of range, DI7=Memory error, DI8=Warm Up) |
| IR2 | 0x0001 | AlarmStatus | Réservé |
| IR3 | 0x0002 | OutputStatus | DI33=Alarm Output status, DI34=PWM Output status |
| IR4 | 0x0003 | Space CO2 | Concentration CO2 en ppm (uint16) ⚠ voir note scaling |
| IR5 | 0x0004 | Space Temp | Température capteur (au-dessus de l'ambiant à cause de l'auto-échauffement) |
| IR6 | 0x0005 | Synchro | Incrémenté chaque cycle de mesure |
| IR7 | 0x0006 | Vbb | Tension VBB pendant lamp ramp (LSB = 1 mV) |
| IR22 | 0x0015 | PWM Output | Valeur PWM (0x3FFF = 100%) |
| IR24+IR25 | 0x0017/18 | ETC | Elapsed Time Counter (heures), 4 octets |
| IR28 | 0x001B | Memory Map version | |
| IR29 | 0x001C | FW version | high byte = Main, low byte = Sub |
| IR30+IR31 | 0x001D/1E | Sensor Serial Number | 4 octets |
⚠ Scaling CO2 : la plupart des S88 retournent la valeur directement en ppm. Certains futurs modèles de la famille S88 divisent par 10 (400 ppm → renvoie 40). À vérifier au bench. Le ProductCode (lu via fonction 0x2B/0x0E objet 0x01) permet d'identifier le modèle — pour le S88 Residential 004-1-0100 c'est ppm directement.
Exemple : lire CO2 seul (IR4)
Requête maître (adresse 0xFE, function 04, start 0x0003, qty 0x0001) :
FE 04 00 03 00 01 25 C5
└┬┘ └┬┘ └──┬──┘ └──┬──┘ └─┬─┘
addr fn start qty CRC (low byte first)
Réponse esclave (CO2 = 400 ppm = 0x0190) :
FE 04 02 01 90 AC DB
└┬┘ └┬┘ └┬┘ └──┬──┘ └─┬─┘
addr fn count value CRC
Exemple : lire status + CO2 en une commande (IR1 à IR4)
Requête maître :
FE 04 00 00 00 04 E5 C6
Réponse esclave (status=0, CO2=400ppm) :
FE 04 08 00 00 00 00 00 00 01 90 16 E6
└┬┘ └┬┘ └┬┘ └──┬──┘ └──┬──┘ └──┬──┘ └──┬──┘ └─┬─┘
addr fn cnt IR1=0 IR2=0 IR3=0 IR4=400 CRC
C'est la séquence recommandée pour le scraping périodique : un seul appel, on récupère l'état + la valeur. Si IR1 (status) ≠ 0, ne pas écrire la mesure.
Notes EEPROM
Les Holding Registers sont mappés en EEPROM (sauf HR1–HR4 et HR22) :
- Limite EEPROM : < 10 000 cycles d'écriture sur la durée de vie
- Une écriture multi-registres compte pour 1 cycle
- Attendre ≥ 180 ms après écriture d'un HR avant power-down/reset
⚠ Ne JAMAIS écrire les HR depuis une boucle qui tourne souvent — réservé à la configuration initiale (changement de baudrate, d'adresse Modbus, etc.).
Implémentation NebuleAir
Voir S88/write_data.py et S88/get_data.py. Le module Python minimalmodbus
ou pymodbus peut être utilisé, ou directement pyserial avec calcul CRC16 manuel.
Pour lecture périodique simple :
# Pseudocode — voir write_data.py pour la vraie implémentation
request = b'\xFE\x04\x00\x00\x00\x04' + crc16(...) # IR1..IR4
ser.write(request)
response = ser.read(13) # FE 04 08 + 8 octets data + 2 CRC
if response[0] == 0xFE and response[1] == 0x04:
status = (response[3] << 8) | response[4]
co2_ppm = (response[9] << 8) | response[10]
if status == 0:
# OK, enregistrer co2_ppm