From 4f3d273981f072a14a329d54d28223446d258eae Mon Sep 17 00:00:00 2001 From: PaulVua Date: Tue, 2 Jun 2026 14:27:11 +0200 Subject: [PATCH] =?UTF-8?q?v1.10.0:=20int=C3=A9gration=20capteur=20CCS811?= =?UTF-8?q?=20(TVOC/eCO2,=20I2C)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Nouveau capteur de qualité d'air CCS811 sur le bus I2C, calqué sur le pattern S88 (local-only, pas encore dans le payload de transmission). - CCS811/get_data.py (lecture live) + write_data.py (timer 10s, self-heal table) - table data_CCS811 (timestamp, eCO2, TVOC) dans create_db.py - config CCS811 (bool) + CCS811_address (0x5A/0x5B, défaut 0x5A) dans set_config.py - service+timer systemd nebuleair-ccs811-data (10s) + ajout boucle d'activation - admin.html: case d'activation + dropdown adresse I2C - sensors.html: carte Get Data (TVOC + eCO2) - database.html + launcher.php: consultation/export/stats data_CCS811 - lib adafruit-circuitpython-ccs811 dans installation_part1.sh - CCS811/README.md: câblage, adresses, warning clock-stretching I2C sur Pi - CLAUDE.md + changelog mis à jour Co-Authored-By: Claude Opus 4.8 (1M context) --- CCS811/README.md | 83 ++++++++++++++++++++++++++++++++ CCS811/get_data.py | 87 +++++++++++++++++++++++++++++++++ CCS811/write_data.py | 98 ++++++++++++++++++++++++++++++++++++++ CLAUDE.md | 3 ++ VERSION | 2 +- changelog.json | 13 +++++ html/admin.html | 19 ++++++++ html/database.html | 14 ++++-- html/launcher.php | 19 ++++++-- html/sensors.html | 79 ++++++++++++++++++++++++++++++ installation_part1.sh | 2 +- services/setup_services.sh | 34 ++++++++++++- sqlite/create_db.py | 9 ++++ sqlite/set_config.py | 2 + 14 files changed, 455 insertions(+), 9 deletions(-) create mode 100644 CCS811/README.md create mode 100644 CCS811/get_data.py create mode 100644 CCS811/write_data.py diff --git a/CCS811/README.md b/CCS811/README.md new file mode 100644 index 0000000..cf2f827 --- /dev/null +++ b/CCS811/README.md @@ -0,0 +1,83 @@ +# CCS811 — Capteur qualité d'air (eCO2 / TVOC) + +Capteur de gaz **MOX** (oxyde métallique) AMS CCS811. Connecté en **I2C**. + +## ⚠ À lire avant de câbler + +Le CCS811 **n'est pas** un capteur CO2 NDIR comme le Senseair S88. C'est un capteur +de COV (composés organiques volatils) qui mesure : + +- **TVOC** (Total Volatile Organic Compounds) — en **ppb**. C'est la mesure réellement + utile / fiable du capteur, et celle qui nous intéresse ici. +- **eCO2** (CO2 *équivalent*) — en **ppm**, plage 400–8192. Valeur *calculée* à partir + du TVOC par un algorithme interne, ce **n'est pas** une mesure directe du CO2. Pour + un vrai CO2, utiliser le S88. On stocke quand même l'eCO2 (gratuit, vient de la même + lecture) mais ne pas le confondre avec une mesure NDIR. + +## ⚠ Clock-stretching I2C sur Raspberry Pi + +Le CCS811 utilise massivement le **clock-stretching** I2C. Le contrôleur I2C matériel +du Raspberry Pi (BSC) gère **mal** le clock-stretching (bug matériel documenté). Sans +mitigation, les lectures échouent typiquement en `OSError` / `Remote I/O error`. + +**Mitigation** : ralentir le bus I2C dans `/boot/firmware/config.txt` : + +``` +dtparam=i2c_arm_baudrate=10000 +``` + +(10 kHz au lieu de 100 kHz par défaut.) Reboot ensuite. À valider au bench — sur +certains modules/CM4 ça passe à 100 kHz, sur d'autres non. + +Vérifier la présence du capteur : + +```bash +sudo i2cdetect -y 1 # doit montrer 5a (ou 5b selon la broche ADDR) +``` + +## Adresse I2C + +- **0x5A** : ADDR à GND — défaut des breakouts **Adafruit**. Valeur par défaut du firmware. +- **0x5B** : ADDR à VDD — défaut des breakouts **SparkFun** / modules génériques. + +Configurable dans `admin.html` (clé config `CCS811_address`, dropdown 0x5A / 0x5B). + +## Câblage I2C + +| CCS811 | Raspberry Pi | +|---|---| +| VCC / VIN | 3.3V | +| GND | GND | +| SDA | SDA (GPIO2) | +| SCL | SCL (GPIO3) | +| WAK / nWAKE | GND (réveil permanent ; sinon laisser le module gérer) | +| ADDR | GND → 0x5A, VDD → 0x5B | + +⚠ La plupart des breakouts CCS811 sont en **3.3V** logique. Ne pas alimenter en 5V +sans level-shifter sauf si le module embarque son propre régulateur + shifter. + +## Burn-in / conditionnement + +- **Burn-in initial** : ~48 h de fonctionnement continu avant des valeurs stables (1ère mise en service). +- **Warm-up** à chaque démarrage : ~20 min pour des valeurs fiables. Au démarrage le + capteur renvoie souvent eCO2=400 ppm / TVOC=0 ppb (valeurs de repos). + +## Implémentation NebuleAir + +- `CCS811/get_data.py` — lecture live (bouton "Get Data" du web). Affiche + `{"eCO2": , "TVOC": }` ou `{"error": "..."}`. +- `CCS811/write_data.py` — lecture périodique (timer systemd, toutes les 10 s), + écrit dans la table `data_CCS811 (timestamp, eCO2, TVOC)`. + +Librairie Python : `adafruit-circuitpython-ccs811` (installée par +`installation_part1.sh`). La table est créée par `sqlite/create_db.py` et +self-healée par `write_data.py` (CREATE TABLE IF NOT EXISTS) — garder les deux +schémas synchro. + +Activation : `admin.html` → case "Send VOC sensor data (CCS811)". + +### Pistes d'amélioration (non implémentées) + +Le CCS811 supporte une compensation température/humidité (`SET_ENV_DATA`). Comme le +boîtier embarque déjà un BME280, on pourrait lui pousser temp/hum à chaque lecture +pour améliorer la précision. Non fait en v1 pour garder le script simple et autonome. diff --git a/CCS811/get_data.py b/CCS811/get_data.py new file mode 100644 index 0000000..3efde19 --- /dev/null +++ b/CCS811/get_data.py @@ -0,0 +1,87 @@ +''' +Live read of the AMS CCS811 air-quality sensor (used by the web "Get Data" button). +Prints a JSON object: {"eCO2": , "TVOC": } or {"error": ""}. + +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. + +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] +''' + +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 + except Exception as e: + print(json.dumps({"error": f"library import failed: {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}"})) + 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 = ccs811.eco2 + tvoc = ccs811.tvoc + print(json.dumps({"eCO2": int(eco2), "TVOC": int(tvoc)})) + except Exception as e: + print(json.dumps({"error": f"CCS811 read error: {e}"})) + + +if __name__ == "__main__": + main() diff --git a/CCS811/write_data.py b/CCS811/write_data.py new file mode 100644 index 0000000..2f7bdd8 --- /dev/null +++ b/CCS811/write_data.py @@ -0,0 +1,98 @@ +''' +Script to get air-quality values from the AMS CCS811 sensor and write to database. +/usr/bin/python3 /var/www/nebuleair_pro_4g/CCS811/write_data.py + +CCS811 is a MOX gas sensor: eCO2 (equivalent CO2 in ppm, derived from VOCs) and +TVOC (Total VOC in ppb). TVOC is the primary measurement of interest. + +I2C, library adafruit-circuitpython-ccs811. Address from config_table +(key CCS811_address, e.g. "0x5A" / "0x5B"), default 0x5A. +''' + +import sqlite3 +import sys +import time + +DB_PATH = "/var/www/nebuleair_pro_4g/sqlite/sensors.db" +DEFAULT_ADDRESS = 0x5A +DATA_READY_RETRIES = 30 +DATA_READY_DELAY = 0.2 # seconds + + +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 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_CCS811 ( + timestamp TEXT, + eCO2 INTEGER, + TVOC INTEGER + ) + """) + conn.commit() + + addr_str = get_config(cursor, "CCS811_address", "0x5A") + try: + address = int(str(addr_str), 16) + except ValueError: + address = DEFAULT_ADDRESS + + try: + import board + import busio + import adafruit_ccs811 + except Exception as e: + print(f"CCS811: library import failed: {e}") + conn.close() + sys.exit(1) + + try: + i2c = busio.I2C(board.SCL, board.SDA) + ccs811 = adafruit_ccs811.CCS811(i2c, address=address) + except Exception as e: + print(f"CCS811: cannot init at {hex(address)}: {e}") + conn.close() + sys.exit(1) + + 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("CCS811: data not ready (warming up?), skipping.") + return + + eco2 = int(ccs811.eco2) + tvoc = int(ccs811.tvoc) + + cursor.execute("SELECT last_updated FROM timestamp_table LIMIT 1") + row = cursor.fetchone() + rtc_time_str = row[0] + + cursor.execute( + "INSERT INTO data_CCS811 (timestamp, eCO2, TVOC) VALUES (?, ?, ?)", + (rtc_time_str, eco2, tvoc), + ) + conn.commit() + print(f"eCO2: {eco2} ppm, TVOC: {tvoc} ppb (saved at {rtc_time_str})") + except Exception as e: + print(f"CCS811 error: {e}") + finally: + conn.close() + + +if __name__ == "__main__": + main() diff --git a/CLAUDE.md b/CLAUDE.md index 5caea7c..47aa916 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -23,6 +23,7 @@ NebuleAir Pro 4G is an environmental monitoring system running on Raspberry Pi 4 - NSRT MK4: Noise sensor via I2C (0x48) - SARA R4/R5: 4G cellular modem (ttyAMA2) - Senseair S88: CO2 sensor via Modbus RTU (any free ttyAMA — port configurable, see admin.html) +- CCS811: air-quality MOX sensor (TVOC + eCO2) via I2C (0x5A or 0x5B, configurable). Note: eCO2 is *derived* from VOCs, not a true NDIR CO2 measurement like the S88. - Wind meter: via ADS1115 ADC - MPPT: Solar charger monitoring @@ -49,6 +50,7 @@ When adding a new UART sensor (e.g. S88), it goes on one of the free NPM connect - `NPM/`: NextPM sensor scripts - `envea/`: Envea sensor scripts - `BME280/`: BME280 sensor scripts +- `CCS811/`: CCS811 air-quality sensor scripts (TVOC/eCO2, I2C) - `sound_meter/`: Noise sensor code (C program) - `SARA/`: 4G modem communication (AT commands) - `windMeter/`: Wind sensor scripts @@ -102,6 +104,7 @@ sudo /var/www/nebuleair_pro_4g/services/setup_services.sh - `nebuleair-envea-data.timer`: Every 10 seconds (Envea sensors) - `nebuleair-sara-data.timer`: Every 60 seconds (4G data transmission) - `nebuleair-bme280-data.timer`: Every 120 seconds (BME280 sensor) +- `nebuleair-ccs811-data.timer`: Every 10 seconds (CCS811 TVOC/eCO2 sensor) - `nebuleair-mppt-data.timer`: Every 120 seconds (MPPT charger) - `nebuleair-noise-data.timer`: Every 60 seconds (Noise sensor) - `nebuleair-db-cleanup-data.timer`: Daily (database cleanup) diff --git a/VERSION b/VERSION index 88b883e..81c871d 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.9.19 +1.10.0 diff --git a/changelog.json b/changelog.json index d668854..9f38337 100644 --- a/changelog.json +++ b/changelog.json @@ -1,5 +1,18 @@ { "versions": [ + { + "version": "1.10.0", + "date": "2026-06-02", + "changes": { + "features": [ + "Intégration du capteur de qualité d'air CCS811 (I2C). Mesure TVOC (ppb, mesure principale) + eCO2 (ppm, dérivé des COV — PAS un vrai CO2 NDIR comme le S88). Nouveau dossier CCS811/ (get_data.py lecture live + write_data.py timer). Table data_CCS811 (timestamp, eCO2, TVOC). Timer systemd toutes les 10 s. Activation + adresse I2C (0x5A Adafruit / 0x5B SparkFun, défaut 0x5A) configurables dans admin.html. Carte 'Get Data' dans sensors.html, consultation/export dans database.html. Lib adafruit-circuitpython-ccs811 ajoutée à installation_part1.sh." + ], + "improvements": [], + "fixes": [], + "compatibility": [] + }, + "notes": "⚠ Matériel : le CCS811 utilise le clock-stretching I2C que le contrôleur du Pi gère mal (bug BSC). Prévoir 'dtparam=i2c_arm_baudrate=10000' dans config.txt si les lectures échouent en I/O error — à valider au bench. Voir CCS811/README.md. Schéma data_CCS811 dupliqué dans write_data.py (self-heal CREATE IF NOT EXISTS) et create_db.py — garder synchro. CCS811 pas encore intégré au payload de transmission (local-only, comme le S88)." + }, { "version": "1.9.19", "date": "2026-06-01", diff --git a/html/admin.html b/html/admin.html index c848e60..46f4d9e 100755 --- a/html/admin.html +++ b/html/admin.html @@ -154,6 +154,20 @@ +
+ + +
+ + +
+
+
@@ -100,6 +101,7 @@ + @@ -117,6 +119,7 @@ + @@ -320,7 +323,8 @@ function buildTableHeader(table) { data_MPPT: ['Timestamp','Battery Voltage','Battery Current','solar_voltage','solar_power','charger_status'], data_NOISE: ['Timestamp','Curent LEQ','DB_A_value','Status'], data_MHZ19: ['Timestamp','CO2 (ppm)'], - data_S88: ['Timestamp','CO2 (ppm)'] + data_S88: ['Timestamp','CO2 (ppm)'], + data_CCS811: ['Timestamp','eCO2 (ppm)','TVOC (ppb)'] }; return (headers[table] || ['Data']).map(h => `${h}`).join(''); } @@ -343,7 +347,7 @@ function buildTableRow(table, columns) { return `${columns[1]}`; } // Default: render all available columns - const colCount = { data_BME280: 4, data_NPM_5channels: 6, data_envea: 6, data_WIND: 3, data_MPPT: 6, data_MHZ19: 2, data_S88: 2 }; + const colCount = { data_BME280: 4, data_NPM_5channels: 6, data_envea: 6, data_WIND: 3, data_MPPT: 6, data_MHZ19: 2, data_S88: 2, data_CCS811: 3 }; const n = colCount[table] || columns.length; return columns.slice(0, n).map(c => `${c}`).join(''); } @@ -485,6 +489,9 @@ function downloadCSV(response, table) { else if (table === "data_S88") { csvContent += "TimestampUTC,CO2_ppm\n"; } + else if (table === "data_CCS811") { + csvContent += "TimestampUTC,eCO2_ppm,TVOC_ppb\n"; + } // Format rows as CSV rows.forEach(row => { @@ -513,7 +520,8 @@ const tableDisplayNames = { 'data_MPPT': 'Batterie (MPPT)', 'data_NOISE': 'Bruit', 'data_MHZ19': 'CO2 (MH-Z19)', - 'data_S88': 'CO2 (Senseair S88)' + 'data_S88': 'CO2 (Senseair S88)', + 'data_CCS811': 'TVOC/eCO2 (CCS811)' }; function loadDbStats() { diff --git a/html/launcher.php b/html/launcher.php index 4b15b7e..4c9935f 100755 --- a/html/launcher.php +++ b/html/launcher.php @@ -794,7 +794,7 @@ if ($type == "db_table_stats") { $fileSizeMB = round($fileSizeBytes / (1024 * 1024), 2); // Sensor data tables to inspect - $tables = ['data_NPM', 'data_NPM_5channels', 'data_BME280', 'data_envea', 'data_WIND', 'data_MPPT', 'data_NOISE', 'data_MHZ19', 'data_S88']; + $tables = ['data_NPM', 'data_NPM_5channels', 'data_BME280', 'data_envea', 'data_WIND', 'data_MPPT', 'data_NOISE', 'data_MHZ19', 'data_S88', 'data_CCS811']; $tableStats = []; foreach ($tables as $tableName) { @@ -844,7 +844,7 @@ if ($type == "download_full_table") { $table = $_GET['table'] ?? ''; // Whitelist of allowed tables - $allowedTables = ['data_NPM', 'data_NPM_5channels', 'data_BME280', 'data_envea', 'data_WIND', 'data_MPPT', 'data_NOISE', 'data_MHZ19', 'data_S88']; + $allowedTables = ['data_NPM', 'data_NPM_5channels', 'data_BME280', 'data_envea', 'data_WIND', 'data_MPPT', 'data_NOISE', 'data_MHZ19', 'data_S88', 'data_CCS811']; if (!in_array($table, $allowedTables)) { header('Content-Type: application/json'); @@ -862,7 +862,8 @@ if ($type == "download_full_table") { 'data_MPPT' => 'TimestampUTC,Battery_voltage,Battery_current,Solar_voltage,Solar_power,Charger_status', 'data_NOISE' => 'TimestampUTC,Current_LEQ,DB_A_value', 'data_MHZ19' => 'TimestampUTC,CO2_ppm', - 'data_S88' => 'TimestampUTC,CO2_ppm' + 'data_S88' => 'TimestampUTC,CO2_ppm', + 'data_CCS811' => 'TimestampUTC,eCO2_ppm,TVOC_ppb' ]; try { @@ -1016,6 +1017,12 @@ if ($type == "s88") { echo $output; } +if ($type == "ccs811") { + $command = 'sudo /usr/bin/python3 /var/www/nebuleair_pro_4g/CCS811/get_data.py'; + $output = shell_exec($command); + echo $output; +} + if ($type == "table_mesure") { $table=$_GET['table']; @@ -1674,6 +1681,10 @@ if ($type == "get_systemd_services") { 'description' => 'Reads CO2 concentration from MH-Z19 sensor', 'frequency' => 'Every 2 minutes' ], + 'nebuleair-ccs811-data.timer' => [ + 'description' => 'Reads eCO2/TVOC from CCS811 air-quality sensor', + 'frequency' => 'Every 10 seconds' + ], 'nebuleair-s88-data.timer' => [ 'description' => 'Reads CO2 concentration from Senseair S88 sensor', 'frequency' => 'Every 10 seconds' @@ -1765,6 +1776,7 @@ if ($type == "restart_systemd_service") { 'nebuleair-noise-data.timer', 'nebuleair-mhz19-data.timer', 'nebuleair-s88-data.timer', + 'nebuleair-ccs811-data.timer', 'nebuleair-db-cleanup-data.timer', 'nebuleair-wifi-powersave.timer', 'nebuleair-cpu-power.service', @@ -1831,6 +1843,7 @@ if ($type == "toggle_systemd_service") { 'nebuleair-noise-data.timer', 'nebuleair-mhz19-data.timer', 'nebuleair-s88-data.timer', + 'nebuleair-ccs811-data.timer', 'nebuleair-db-cleanup-data.timer', 'nebuleair-wifi-powersave.timer', 'nebuleair-cpu-power.service', diff --git a/html/sensors.html b/html/sensors.html index 2bb275a..d45d110 100755 --- a/html/sensors.html +++ b/html/sensors.html @@ -409,6 +409,62 @@ }); } + function getCCS811_values() { + console.log("Data from CCS811 air-quality sensor:"); + $("#loading_ccs811").show(); + + $.ajax({ + url: 'launcher.php?type=ccs811', + dataType: 'json', + method: 'GET', + success: function (response) { + console.log(response); + const tableBody = document.getElementById("data-table-body_ccs811"); + tableBody.innerHTML = ""; + $("#loading_ccs811").hide(); + + if (response.error) { + $("#data-table-body_ccs811").append(` + + + ⚠ ${response.error} + + + `); + } else { + if (response.TVOC !== undefined) { + $("#data-table-body_ccs811").append(` + + TVOC + ${response.TVOC} ppb + + `); + } + if (response.eCO2 !== undefined) { + $("#data-table-body_ccs811").append(` + + eCO2 + ${response.eCO2} ppm + + `); + } + } + }, + error: function (xhr, status, error) { + console.error('AJAX request failed:', status, error); + $("#loading_ccs811").hide(); + const tableBody = document.getElementById("data-table-body_ccs811"); + tableBody.innerHTML = ` + + + ⚠ Erreur de communication avec le capteur + + + `; + } + }); + } + function getMHZ19_values() { console.log("Data from MH-Z19 CO2 sensor:"); $("#loading_mhz19").show(); @@ -675,6 +731,29 @@ container.innerHTML += S88_HTML; } + //creates CCS811 air-quality (eCO2/TVOC) card + if (config.CCS811) { + const CCS811_HTML = ` +
+
+
+ I2C +
+
+
CCS811 (TVOC / eCO2)
+

Capteur de composés organiques volatils.

+ + + + +
+
+
+
`; + + container.innerHTML += CCS811_HTML; + } + //Si on a des SONDES ENVEA connectée il faut faire un deuxième call dans la table envea_sondes_table //creates ENVEA debug card if (config.envea) { diff --git a/installation_part1.sh b/installation_part1.sh index db26b1b..1632500 100644 --- a/installation_part1.sh +++ b/installation_part1.sh @@ -27,7 +27,7 @@ sudo apt update && sudo apt install -y git gh apache2 sqlite3 php php-sqlite3 py # Install Python libraries info "Installing Python libraries..." -sudo pip3 install pyserial requests adafruit-circuitpython-bme280 crcmod psutil gpiozero ntplib adafruit-circuitpython-ads1x15 nsrt-mk3-dev pytz --break-system-packages || error "Failed to install Python libraries." +sudo pip3 install pyserial requests adafruit-circuitpython-bme280 adafruit-circuitpython-ccs811 crcmod psutil gpiozero ntplib adafruit-circuitpython-ads1x15 nsrt-mk3-dev pytz --break-system-packages || error "Failed to install Python libraries." # Install Tailscale (for remote SSH access via Headscale tailnet) info "Installing Tailscale..." diff --git a/services/setup_services.sh b/services/setup_services.sh index ba9ced7..59d0394 100644 --- a/services/setup_services.sh +++ b/services/setup_services.sh @@ -269,6 +269,38 @@ AccuracySec=1s WantedBy=timers.target EOL +# Create service and timer files for CCS811 air-quality (eCO2/TVOC) Data +cat > /etc/systemd/system/nebuleair-ccs811-data.service << 'EOL' +[Unit] +Description=NebuleAir CCS811 Air-Quality (eCO2/TVOC) Data Collection Service +After=network.target + +[Service] +Type=oneshot +ExecStart=/usr/bin/python3 /var/www/nebuleair_pro_4g/CCS811/write_data.py +User=root +WorkingDirectory=/var/www/nebuleair_pro_4g +StandardOutput=append:/var/www/nebuleair_pro_4g/logs/ccs811_service.log +StandardError=append:/var/www/nebuleair_pro_4g/logs/ccs811_service_errors.log + +[Install] +WantedBy=multi-user.target +EOL + +cat > /etc/systemd/system/nebuleair-ccs811-data.timer << 'EOL' +[Unit] +Description=Run NebuleAir CCS811 Air-Quality Data Collection every 10 seconds +Requires=nebuleair-ccs811-data.service + +[Timer] +OnBootSec=10s +OnUnitActiveSec=10s +AccuracySec=1s + +[Install] +WantedBy=timers.target +EOL + # Create service and timer files for Database Cleanup cat > /etc/systemd/system/nebuleair-db-cleanup-data.service << 'EOL' [Unit] @@ -402,7 +434,7 @@ systemctl daemon-reload # Enable and start all timers echo "Enabling and starting all services..." -for service in npm envea sara bme280 mppt mhz19 s88 db-cleanup noise; do +for service in npm envea sara bme280 mppt mhz19 s88 ccs811 db-cleanup noise; do systemctl enable nebuleair-$service-data.timer systemctl start nebuleair-$service-data.timer echo "Started nebuleair-$service-data timer" diff --git a/sqlite/create_db.py b/sqlite/create_db.py index d85767f..60fb44c 100755 --- a/sqlite/create_db.py +++ b/sqlite/create_db.py @@ -168,6 +168,15 @@ CREATE TABLE IF NOT EXISTS data_S88 ( ) """) +# Create a table CCS811 (AMS CCS811 air-quality sensor: eCO2 + TVOC) +cursor.execute(""" +CREATE TABLE IF NOT EXISTS data_CCS811 ( + timestamp TEXT, + eCO2 INTEGER, + TVOC INTEGER +) +""") + # Commit and close the connection conn.commit() conn.close() diff --git a/sqlite/set_config.py b/sqlite/set_config.py index 6103cce..432bec1 100644 --- a/sqlite/set_config.py +++ b/sqlite/set_config.py @@ -53,6 +53,8 @@ config_entries = [ ("MHZ19", "0", "bool"), ("S88", "0", "bool"), ("S88_port", "/dev/ttyAMA5", "str"), + ("CCS811", "0", "bool"), + ("CCS811_address", "0x5A", "str"), ("modem_version", "XXX", "str"), ("device_type", "nebuleair_pro", "str"), ("language", "fr", "str"),