Protège contre le cas où l'OTA n'appelle pas create_db.py (problème bash classique: le script d'OTA est chargé en mémoire au lancement, le git pull du step 1 met à jour le fichier sur disque mais bash continue d'exécuter l'ancienne version). Le script crée la table data_S88 lui-même au démarrage. Schéma dupliqué de create_db.py — à garder synchro. Pattern à appliquer aux futurs capteurs pour éviter cette classe de bug. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
141 lines
3.9 KiB
Python
141 lines
3.9 KiB
Python
'''
|
|
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
|
|
|
|
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
|
|
import sqlite3
|
|
import sys
|
|
|
|
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,))
|
|
row = cursor.fetchone()
|
|
return row[0] if row else default
|
|
|
|
|
|
def read_co2(ser):
|
|
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():
|
|
conn = sqlite3.connect(DB_PATH)
|
|
cursor = conn.cursor()
|
|
|
|
# Self-heal: ensure the table exists even if create_db.py was skipped during OTA.
|
|
# Duplicates the canonical schema from sqlite/create_db.py — keep them in sync.
|
|
cursor.execute("""
|
|
CREATE TABLE IF NOT EXISTS data_S88 (
|
|
timestamp TEXT,
|
|
CO2 INTEGER
|
|
)
|
|
""")
|
|
|
|
port = get_config(cursor, "S88_port", DEFAULT_PORT)
|
|
|
|
try:
|
|
ser = serial.Serial(
|
|
port=port,
|
|
baudrate=BAUDRATE,
|
|
parity=serial.PARITY_NONE,
|
|
stopbits=serial.STOPBITS_ONE,
|
|
bytesize=serial.EIGHTBITS,
|
|
timeout=1,
|
|
)
|
|
except Exception as e:
|
|
print(f"Error opening serial port {port}: {e}")
|
|
conn.close()
|
|
sys.exit(1)
|
|
|
|
try:
|
|
co2 = read_co2(ser)
|
|
if co2 is None:
|
|
print("Failed to read CO2 from S88.")
|
|
return
|
|
|
|
co2_ppm = int(round(co2))
|
|
|
|
cursor.execute("SELECT last_updated FROM timestamp_table LIMIT 1")
|
|
row = cursor.fetchone()
|
|
rtc_time_str = row[0]
|
|
|
|
cursor.execute(
|
|
"INSERT INTO data_S88 (timestamp, CO2) VALUES (?, ?)",
|
|
(rtc_time_str, co2_ppm),
|
|
)
|
|
conn.commit()
|
|
print(f"CO2: {co2_ppm} ppm (saved at {rtc_time_str})")
|
|
except Exception as e:
|
|
print(f"S88 error: {e}")
|
|
finally:
|
|
try:
|
|
ser.close()
|
|
except Exception:
|
|
pass
|
|
conn.close()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|