v1.11.0: CCS811 en daemon + fix filtrage + I2C 10kHz requis
Vérif terrain sur pro100 : à 100 kHz le CCS811 renvoie des valeurs corrompues 0x8000 (32768) par clock-stretching, et le modèle oneshot-reset-toutes-les-10s ne donne que le 1er échantillon post-init (garbage). Refonte : - CCS811/daemon.py: service long-running (Type=simple, Restart=always). Init 1x, boucle lecture/écriture 10s, filtre eCO2 dans [400,8192], re-init auto sur erreurs I2C répétées. Remplace write_data.py (supprimé). - CCS811/get_data.py: lit la dernière ligne data_CCS811 au lieu du capteur (évite la collision I2C avec le daemon -> corruption observée). - setup_services.sh: service daemon + self-heal suppression de l'ancien .timer; activation hors boucle timers. - launcher.php: .timer -> .service (map statut + allowedServices x2). - update_firmware.sh: redémarre le daemon à l'OTA. - doc: README (archi daemon + I2C 10kHz confirmé requis), CLAUDE.md, changelog. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,90 +1,39 @@
|
||||
'''
|
||||
Live read of the AMS CCS811 air-quality sensor (used by the web "Get Data" button).
|
||||
Prints a JSON object: {"eCO2": <int_ppm>, "TVOC": <int_ppb>} or {"error": "<message>"}.
|
||||
Live value for the web "Get Data" button (CCS811 air-quality sensor).
|
||||
Prints {"eCO2": <ppm>, "TVOC": <ppb>, "timestamp": <str>} or {"error": "<msg>"}.
|
||||
|
||||
CCS811 is a MOX gas sensor: it outputs an equivalent CO2 (eCO2, derived from VOCs)
|
||||
and a Total VOC (TVOC). It is NOT an NDIR CO2 sensor like the S88. TVOC is the
|
||||
primary measurement of interest here.
|
||||
IMPORTANT: this does NOT read the I2C sensor. The CCS811 is owned by the
|
||||
long-running daemon (CCS811/daemon.py); opening the bus here would collide with
|
||||
it and corrupt the sensor. Instead we return the most recent row the daemon
|
||||
stored in data_CCS811. TVOC is the primary measurement.
|
||||
|
||||
I2C, library adafruit-circuitpython-ccs811. Address read from config_table
|
||||
(key CCS811_address, e.g. "0x5A" Adafruit / "0x5B" SparkFun), default 0x5A.
|
||||
|
||||
Usage: /usr/bin/python3 /var/www/nebuleair_pro_4g/CCS811/get_data.py [address]
|
||||
Usage: /usr/bin/python3 /var/www/nebuleair_pro_4g/CCS811/get_data.py
|
||||
'''
|
||||
|
||||
import json
|
||||
import sqlite3
|
||||
import sys
|
||||
import time
|
||||
|
||||
DB_PATH = "/var/www/nebuleair_pro_4g/sqlite/sensors.db"
|
||||
DEFAULT_ADDRESS = 0x5A
|
||||
# CCS811 produces a fresh sample every 1 s in drive mode 1. Poll data_ready a few
|
||||
# times to cover the case where the driver was just (re)initialised.
|
||||
DATA_READY_RETRIES = 30
|
||||
DATA_READY_DELAY = 0.2 # seconds
|
||||
|
||||
|
||||
def get_address_from_config():
|
||||
try:
|
||||
conn = sqlite3.connect(DB_PATH)
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("SELECT value FROM config_table WHERE key = ?", ("CCS811_address",))
|
||||
row = cursor.fetchone()
|
||||
conn.close()
|
||||
if row and row[0]:
|
||||
return int(str(row[0]), 16)
|
||||
except Exception:
|
||||
pass
|
||||
return DEFAULT_ADDRESS
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) > 1:
|
||||
try:
|
||||
address = int(sys.argv[1], 16)
|
||||
except ValueError:
|
||||
print(json.dumps({"error": f"invalid address {sys.argv[1]}"}))
|
||||
return
|
||||
else:
|
||||
address = get_address_from_config()
|
||||
|
||||
try:
|
||||
import board
|
||||
import busio
|
||||
import adafruit_ccs811
|
||||
conn = sqlite3.connect(DB_PATH, timeout=5)
|
||||
cursor = conn.cursor()
|
||||
cursor.execute(
|
||||
"SELECT timestamp, eCO2, TVOC FROM data_CCS811 ORDER BY timestamp DESC LIMIT 1"
|
||||
)
|
||||
row = cursor.fetchone()
|
||||
conn.close()
|
||||
except Exception as e:
|
||||
print(json.dumps({"error": f"library import failed: {e}"}))
|
||||
print(json.dumps({"error": f"DB read error: {e}"}))
|
||||
return
|
||||
|
||||
try:
|
||||
i2c = busio.I2C(board.SCL, board.SDA)
|
||||
ccs811 = adafruit_ccs811.CCS811(i2c, address=address)
|
||||
except Exception as e:
|
||||
print(json.dumps({"error": f"cannot init CCS811 at {hex(address)}: {e}"}))
|
||||
if not row:
|
||||
print(json.dumps({"error": "No CCS811 data yet (daemon warming up?)"}))
|
||||
return
|
||||
|
||||
try:
|
||||
ready = False
|
||||
for _ in range(DATA_READY_RETRIES):
|
||||
if ccs811.data_ready:
|
||||
ready = True
|
||||
break
|
||||
time.sleep(DATA_READY_DELAY)
|
||||
|
||||
if not ready:
|
||||
print(json.dumps({"error": "CCS811 data not ready (warming up?)"}))
|
||||
return
|
||||
|
||||
eco2 = int(ccs811.eco2)
|
||||
tvoc = int(ccs811.tvoc)
|
||||
# eCO2 floor is 400 ppm; a sub-400 value is a not-yet-settled sample.
|
||||
if eco2 < 400:
|
||||
print(json.dumps({"error": "CCS811 reading not settled (warming up?)"}))
|
||||
return
|
||||
print(json.dumps({"eCO2": eco2, "TVOC": tvoc}))
|
||||
except Exception as e:
|
||||
print(json.dumps({"error": f"CCS811 read error: {e}"}))
|
||||
print(json.dumps({"timestamp": row[0], "eCO2": int(row[1]), "TVOC": int(row[2])}))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
Reference in New Issue
Block a user