read_co2() lit IR1..IR4 en une trame (status + CO2) à 9600 8N1, adresse 0xFE 'any address', avec vérification CRC16-Modbus et rejet de la mesure si status non-nul (warm-up ou erreur). CRC requête/réponse validés contre les exemples du datasheet TDE14367. Doc protocole consolidée dans S88/README.md. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
6.5 KiB
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 Raspberry Pi (UART 3.3V direct)
| S88 | Raspberry Pi CM4 |
|---|---|
| G+ | 5V |
| G0 | GND |
| UART_TxD | RxD du ttyAMAx (ex. GPIO15 pour ttyAMA0) |
| UART_RxD | TxD du ttyAMAx (ex. GPIO14 pour ttyAMA0) |
| 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