# 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 :

```python
# 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
```
