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>
This commit is contained in:
@@ -2,8 +2,8 @@
|
||||
Script to get CO2 values from Senseair S88 sensor and write to database
|
||||
/usr/bin/python3 /var/www/nebuleair_pro_4g/S88/write_data.py
|
||||
|
||||
Port and protocol details come from config_table (key S88_port).
|
||||
The actual sensor read protocol is implemented in read_co2() below.
|
||||
Modbus RTU 9600 8N1. Reads IR1..IR4 in one frame to get status + CO2.
|
||||
If status (IR1) is non-zero the reading is skipped (warm-up or error).
|
||||
'''
|
||||
|
||||
import serial
|
||||
@@ -14,6 +14,25 @@ DB_PATH = "/var/www/nebuleair_pro_4g/sqlite/sensors.db"
|
||||
DEFAULT_PORT = "/dev/ttyAMA5"
|
||||
BAUDRATE = 9600
|
||||
|
||||
# Modbus slave address: 0xFE = "any address", any S88 responds regardless of
|
||||
# its configured individual address. Fine for single-sensor setups.
|
||||
SLAVE_ADDR = 0xFE
|
||||
# Read IR1..IR4 in one frame: function 0x04, start 0x0000, qty 0x0004
|
||||
READ_REQUEST = bytes([SLAVE_ADDR, 0x04, 0x00, 0x00, 0x00, 0x04, 0xE5, 0xC6])
|
||||
EXPECTED_RESPONSE_LEN = 13 # 1 addr + 1 fn + 1 count + 8 data + 2 CRC
|
||||
|
||||
|
||||
def crc16_modbus(data):
|
||||
crc = 0xFFFF
|
||||
for byte in data:
|
||||
crc ^= byte
|
||||
for _ in range(8):
|
||||
if crc & 1:
|
||||
crc = (crc >> 1) ^ 0xA001
|
||||
else:
|
||||
crc >>= 1
|
||||
return crc
|
||||
|
||||
|
||||
def get_config(cursor, key, default):
|
||||
cursor.execute("SELECT value FROM config_table WHERE key = ?", (key,))
|
||||
@@ -22,9 +41,42 @@ def get_config(cursor, key, default):
|
||||
|
||||
|
||||
def read_co2(ser):
|
||||
# TODO: implement the Senseair S88 read protocol once the datasheet is provided.
|
||||
# Expected return: integer CO2 concentration in ppm, or None on failure.
|
||||
raise NotImplementedError("Senseair S88 read protocol not implemented yet")
|
||||
ser.reset_input_buffer()
|
||||
ser.write(READ_REQUEST)
|
||||
response = ser.read(EXPECTED_RESPONSE_LEN)
|
||||
|
||||
if len(response) < 5:
|
||||
print(f"S88: short response ({len(response)} bytes)")
|
||||
return None
|
||||
|
||||
# Modbus exception response: function code has high bit set (0x84 instead of 0x04)
|
||||
if response[1] & 0x80:
|
||||
print(f"S88 Modbus exception: code={response[2]:#04x}")
|
||||
return None
|
||||
|
||||
if len(response) != EXPECTED_RESPONSE_LEN:
|
||||
print(f"S88: unexpected response length {len(response)} (expected {EXPECTED_RESPONSE_LEN})")
|
||||
return None
|
||||
|
||||
# Verify CRC (last two bytes, low byte first)
|
||||
received_crc = response[-2] | (response[-1] << 8)
|
||||
if crc16_modbus(response[:-2]) != received_crc:
|
||||
print("S88: CRC mismatch")
|
||||
return None
|
||||
|
||||
if response[0] != SLAVE_ADDR or response[1] != 0x04 or response[2] != 0x08:
|
||||
print(f"S88: unexpected header {response[:3].hex()}")
|
||||
return None
|
||||
|
||||
status = (response[3] << 8) | response[4]
|
||||
co2 = (response[9] << 8) | response[10]
|
||||
|
||||
if status != 0:
|
||||
# DI8 = Warm Up (bit 7 of low byte). Other bits = errors.
|
||||
print(f"S88: sensor not ready, status={status:#06x}")
|
||||
return None
|
||||
|
||||
return co2
|
||||
|
||||
|
||||
def main():
|
||||
@@ -65,8 +117,6 @@ def main():
|
||||
)
|
||||
conn.commit()
|
||||
print(f"CO2: {co2_ppm} ppm (saved at {rtc_time_str})")
|
||||
except NotImplementedError as e:
|
||||
print(f"S88 not ready: {e}")
|
||||
except Exception as e:
|
||||
print(f"S88 error: {e}")
|
||||
finally:
|
||||
|
||||
Reference in New Issue
Block a user