Files
nebuleair_pro_4g/S88
PaulVua 7681578f22 v1.9.14: Senseair S88 - implémentation lecture Modbus RTU
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>
2026-06-01 16:24:44 +02:00
..

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 4003000 ppm ±25 ppm ±3% de la lecture
Précision 300010000 ppm ±10% de la lecture
Temps de chauffe ≤ 10 s
Temps de réponse t63% ≤ 30 s
Conditions d'opération 050 °C, 085 %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 : 1247 (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 0x00000x0020)
0x04 Read Input Registers (mesures, plage 0x00000x001F)
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 HR1HR4 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