diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..ac2d999 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,135 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +ModuleAir Pro 4G is an IoT air quality monitoring system built on Raspberry Pi CM4 with 4G cellular connectivity. It collects real-time environmental data through multiple sensors, stores data locally in SQLite, transmits to remote servers via cellular modem, and displays live data on a 128x64 RGB LED matrix. + +## System Architecture + +### Hardware Components +- **NPM Sensor**: Particulate matter (PM1/PM2.5/PM10) via Modbus RTU +- **MH-Z19**: CO2 sensor via UART serial +- **BME280**: Temperature/humidity/pressure via I2C +- **SARA R4**: 4G cellular modem for data transmission +- **Matrix LED**: 128x64 RGB display for real-time visualization +- **Push Button**: GPIO6 (GND) for screen mode cycling +- **RTC Module (DS3231)**: Real-time clock via I2C +- **Optional**: Sensirion SFA30 formaldehyde sensor + +### Software Stack +- **Python 3**: Core data collection and processing +- **SQLite Database**: Local storage at `/var/www/moduleair_pro_4g/sqlite/` +- **Apache/PHP**: Web interface for configuration and monitoring +- **Systemd Services**: Automated data collection and transmission +- **C++ Matrix Library**: RGB LED matrix control (rpi-rgb-led-matrix) + +### Data Flow +Sensors → Python Scripts → SQLite → {SARA R4 → Remote Servers, Matrix Display, Web Interface} + +## Key Commands + +### Installation & Setup +```bash +# Full system installation (run once) +sudo /var/www/moduleair_pro_4g/services/setup_services.sh + +# Matrix LED library compilation +cd /var/www/moduleair_pro_4g/matrix/lib && make + +# Apache configuration +sudo sed -i 's|DocumentRoot /var/www/html|DocumentRoot /var/www/moduleair_pro_4g|' /etc/apache2/sites-available/000-default.conf +sudo systemctl reload apache2 +``` + +### Service Management +```bash +# Check all moduleair services status +systemctl list-timers | grep moduleair +systemctl status moduleair-npm-data.service + +# Manual data collection +python3 /var/www/moduleair_pro_4g/NPM/get_data_modbus_v3.py +python3 /var/www/moduleair_pro_4g/MH-Z19/write_data.py + +# Manual data transmission +python3 /var/www/moduleair_pro_4g/loop/SARA_send_data_v2.py + +# Restart services +sudo systemctl restart moduleair-sara-data.timer +sudo systemctl restart moduleair-npm-data.timer +``` + +### Matrix LED Display & Button Control +```bash +# Compile matrix programs +cd /var/www/moduleair_pro_4g/matrix/lib && make + +# Compile all display programs +g++ -I/var/www/moduleair_pro_4g/matrix/include -L/var/www/moduleair_pro_4g/matrix/lib /var/www/moduleair_pro_4g/matrix/screenNetwork/network_status.cc -o /var/www/moduleair_pro_4g/matrix/screenNetwork/network_status -lrgbmatrix -lsqlite3 +g++ -I/var/www/moduleair_pro_4g/matrix/include -L/var/www/moduleair_pro_4g/matrix/lib /var/www/moduleair_pro_4g/matrix/screenSensors/displayAll4_v2.cc -o /var/www/moduleair_pro_4g/matrix/screenSensors/displayAll4_v2 -lrgbmatrix -lsqlite3 +g++ -I/var/www/moduleair_pro_4g/matrix/include -L/var/www/moduleair_pro_4g/matrix/lib /var/www/moduleair_pro_4g/matrix/screenSensors/displayCO2_PM_Network.cc -o /var/www/moduleair_pro_4g/matrix/screenSensors/displayCO2_PM_Network -lrgbmatrix -lsqlite3 +g++ -I/var/www/moduleair_pro_4g/matrix/include -L/var/www/moduleair_pro_4g/matrix/lib /var/www/moduleair_pro_4g/matrix/screenSensors/blank_screen.cc -o /var/www/moduleair_pro_4g/matrix/screenSensors/blank_screen -lrgbmatrix + +# Start button-controlled display system +sudo systemctl restart moduleair-boot.service + +# Manual button controller start +sudo python3 /var/www/moduleair_pro_4g/matrix/button_screen_controller.py + +# Test button functionality only +sudo python3 /var/www/moduleair_pro_4g/test_button_controller.py +``` + +### Hardware Configuration Required +- Enable all UART ports in `/boot/firmware/config.txt` +- Enable I2C interface +- Set device permissions: `sudo chmod 777 /dev/ttyAMA* /dev/i2c-1` +- Configure sudo permissions for www-data user + +## Automated Services (Systemd) + +- **NPM Data**: Every 10 seconds (particulate matter) +- **CO2 Data**: Every 10 seconds +- **BME280 Data**: Every 120 seconds (temperature/humidity/pressure) +- **SARA Data Transmission**: Every 60 seconds (4G upload) +- **Database Cleanup**: Daily +- **Matrix Boot Display**: Once at startup (shows logo then starts button controller) +- **Matrix Button Controller**: Continuous (managed by boot service) + +## Matrix Display Modes + +The system features 4 button-controlled display modes accessible via GPIO6 push button: + +1. **Network Status**: 4G and WiFi connectivity status with signal strength +2. **All 4 Sensors**: PM1, PM2.5, PM10, and CO2 measurements with quality indicators +3. **CO2 + PM + Network**: CO2, PM2.5, PM10 readings plus network status summary +4. **Blank Screen**: All pixels off (black screen) + +Press the button connected to GPIO6 (GND when pressed) to cycle through modes. The system includes: +- 500ms debounce to prevent accidental double-presses +- Automatic program restart if display crashes +- Clean shutdown handling with proper GPIO cleanup + +## Database Schema + +SQLite database stores sensor readings with timestamps. Key tables include sensor data with datetime stamps for time-series analysis. + +## Communication Protocols + +- **Modbus RTU**: NPM particulate matter sensor +- **UART Serial**: CO2 sensor, 4G modem +- **I2C**: Environmental sensors, RTC +- **HTTP/HTTPS**: Data transmission to remote servers +- **MQTT**: Optional publishing via SARA module + +## Configuration + +- Main config: `/var/www/moduleair_pro_4g/config.json` +- Service logs: `/var/www/moduleair_pro_4g/logs/` +- Matrix input files: `/var/www/moduleair_pro_4g/matrix/input_*.txt` + +## Web Interface + +Local Apache server serves management interface at device IP address with real-time data visualization and system configuration. \ No newline at end of file diff --git a/README.md b/README.md index e8be8ac..8b3d9c6 100755 --- a/README.md +++ b/README.md @@ -1,6 +1,15 @@ -# moduleair_pro_4g +# ModuleAir Pro 4G Version Pro du ModuleAir avec CM4, SaraR4 et ecran Matrix LED p2 64x64. +**Capteurs d'air intérieur IoT avec écran LED RGB 128x64 contrôlé par bouton.** + +## Fonctionnalités +- **Capteurs multi-polluants**: PM1, PM2.5, PM10, CO2, température, humidité +- **Connectivité 4G/WiFi**: Transmission automatique des données via modem SARA R4 +- **Affichage LED interactif**: 4 modes d'écran commutables par bouton-poussoir +- **Interface web locale**: Configuration et visualisation en temps réel +- **Base de données SQLite**: Stockage local avec nettoyage automatique + # Installation ## General ``` @@ -144,48 +153,68 @@ Switch off on-board sound: Add `isolcpus=3` to `/boot/firmware/cmdline.txt` -## Start matrix loop at boot +## Affichage Matrix LED avec contrôle par bouton -We can use systemd to create a service (better than con because Cron doesn’t monitor the script; if it fails, it won’t restart automatically.). -``` -sudo nano /etc/systemd/system/matrix_display.service +Le système dispose de 4 modes d'affichage contrôlés par un bouton-poussoir connecté au GPIO6 (GND quand pressé): + +### Modes d'affichage + +1. **État du réseau**: Statut de connectivité 4G et WiFi avec qualité du signal +2. **Tous les capteurs**: PM1, PM2.5, PM10 et CO2 avec indicateurs de qualité +3. **CO2 + PM + Réseau**: CO2, PM2.5, PM10 et statut réseau résumé +4. **Écran noir**: Tous les pixels éteints + +### Compilation des programmes d'affichage + +```bash +# Compiler la bibliothèque matrix +cd /var/www/moduleair_pro_4g/matrix/lib && make + +# Compiler tous les programmes d'affichage +cd /var/www/moduleair_pro_4g +g++ -I/var/www/moduleair_pro_4g/matrix/include -L/var/www/moduleair_pro_4g/matrix/lib /var/www/moduleair_pro_4g/matrix/screenNetwork/network_status.cc -o /var/www/moduleair_pro_4g/matrix/screenNetwork/network_status -lrgbmatrix -lsqlite3 +g++ -I/var/www/moduleair_pro_4g/matrix/include -L/var/www/moduleair_pro_4g/matrix/lib /var/www/moduleair_pro_4g/matrix/screenSensors/displayAll4_v2.cc -o /var/www/moduleair_pro_4g/matrix/screenSensors/displayAll4_v2 -lrgbmatrix -lsqlite3 +g++ -I/var/www/moduleair_pro_4g/matrix/include -L/var/www/moduleair_pro_4g/matrix/lib /var/www/moduleair_pro_4g/matrix/screenSensors/displayCO2_PM_Network.cc -o /var/www/moduleair_pro_4g/matrix/screenSensors/displayCO2_PM_Network -lrgbmatrix -lsqlite3 +g++ -I/var/www/moduleair_pro_4g/matrix/include -L/var/www/moduleair_pro_4g/matrix/lib /var/www/moduleair_pro_4g/matrix/screenSensors/blank_screen.cc -o /var/www/moduleair_pro_4g/matrix/screenSensors/blank_screen -lrgbmatrix ``` -and we add the following: -``` -[Unit] -Description=Matrix Display Script -After=network.target +### Contrôle du système d'affichage -[Service] -ExecStart=/var/www/moduleair_pro_4g/matrix/screen_sensors_loop -WorkingDirectory=/var/www/moduleair_pro_4g/matrix -Restart=always -User=root -Group=root +```bash +# Démarrer le système avec contrôle par bouton +sudo systemctl restart moduleair-boot.service -[Install] -WantedBy=multi-user.target +# Démarrage manuel du contrôleur de bouton +sudo python3 /var/www/moduleair_pro_4g/matrix/button_screen_controller.py + +# Test du bouton uniquement +sudo python3 /var/www/moduleair_pro_4g/test_button_controller.py + +# Arrêter le système +sudo systemctl stop moduleair-boot.service + +# Vérifier le statut +sudo systemctl status moduleair-boot.service ``` -Then Reload systemd and Enable the Service: -``` -sudo systemctl daemon-reload -sudo systemctl enable matrix_display.service -sudo systemctl start matrix_display.service - -``` - -You can check/restart/stop this service (restart combines stop and start) - -``` -sudo systemctl status matrix_display.service -sudo systemctl stop matrix_display.service -sudo systemctl restart matrix_display.service - -``` +### Fonctionnalités du contrôleur +- **Debounce 500ms**: Évite les pressions accidentelles multiples +- **Redémarrage automatique**: Relance le programme d'affichage s'il plante +- **Arrêt propre**: Nettoyage GPIO et termination des processus +- **Gestion des signaux**: Réagit aux signaux SIGINT/SIGTERM +### Branchement du bouton +Connecter un bouton-poussoir entre GPIO6 et GND. Le système utilise une résistance de pull-up interne, donc aucune résistance externe n'est nécessaire. +## Hardware requis +- **Raspberry Pi CM4** avec connecteur GPIO +- **Bouton-poussoir** connecté GPIO6 → GND +- **Écran Matrix LED 128x64** FM6126A +- **Capteur NPM** (particules) via Modbus RTU +- **Capteur MH-Z19** (CO2) via UART +- **Module BME280** (T/H/P) via I2C +- **Modem SARA R4** (4G) via UART +- **Module RTC DS3231** via I2C diff --git a/matrix/button_screen_controller.py b/matrix/button_screen_controller.py new file mode 100644 index 0000000..0cafa96 --- /dev/null +++ b/matrix/button_screen_controller.py @@ -0,0 +1,208 @@ +#!/usr/bin/env python3 +""" +Button-controlled screen mode cycling for ModuleAir Pro 4G +Connected to GPIO6 (GND), cycles through 4 screen modes: +1. Network Status +2. All 4 Sensors (displayAll4_v2) +3. CO2 + PM2.5 + PM10 + Network Status +4. Blank Screen (black) + +Usage: +sudo python3 /var/www/moduleair_pro_4g/matrix/button_screen_controller.py +""" + +import RPi.GPIO as GPIO +import subprocess +import time +import signal +import sys +import os + +# GPIO pin configuration +BUTTON_PIN = 6 # GPIO6, connected to GND when pressed + +# Screen mode configuration +SCREEN_MODES = { + 0: { + 'name': 'Network Status', + 'program': '/var/www/moduleair_pro_4g/matrix/screenNetwork/network_status', + 'args': ['--led-no-hardware-pulse', '--led-row-addr-type=3', '--led-panel-type=FM6126A'] + }, + 1: { + 'name': 'All 4 Sensors', + 'program': '/var/www/moduleair_pro_4g/matrix/screenSensors/displayAll4_v2', + 'args': ['--led-no-hardware-pulse', '--led-row-addr-type=3', '--led-panel-type=FM6126A'] + }, + 2: { + 'name': 'CO2 + PM + Network', + 'program': '/var/www/moduleair_pro_4g/matrix/screenSensors/displayCO2_PM_Network', + 'args': ['--led-no-hardware-pulse', '--led-row-addr-type=3', '--led-panel-type=FM6126A'] + }, + 3: { + 'name': 'Blank Screen', + 'program': '/var/www/moduleair_pro_4g/matrix/screenSensors/blank_screen', + 'args': ['--led-no-hardware-pulse', '--led-row-addr-type=3', '--led-panel-type=FM6126A'] + } +} + +class ScreenController: + def __init__(self): + self.current_mode = 0 + self.current_process = None + self.running = True + self.last_button_press = 0 + self.debounce_time = 0.5 # 500ms debounce + self.last_button_state = True # Button starts as HIGH (pulled up) + + # Set up GPIO - clean initialization + try: + # Only cleanup if GPIO was previously initialized + if GPIO.getmode() is not None: + GPIO.cleanup() + except: + pass + + GPIO.setmode(GPIO.BCM) + GPIO.setup(BUTTON_PIN, GPIO.IN, pull_up_down=GPIO.PUD_UP) + + # Set up signal handlers + signal.signal(signal.SIGINT, self.signal_handler) + signal.signal(signal.SIGTERM, self.signal_handler) + + print(f"Screen controller initialized. Current mode: {SCREEN_MODES[self.current_mode]['name']}") + print("Using polling method for button detection (more reliable)") + + def check_button_pressed(self): + """Check for button press using polling method""" + current_button_state = GPIO.input(BUTTON_PIN) + + # Detect falling edge (button pressed) + if self.last_button_state == True and current_button_state == False: + current_time = time.time() + if current_time - self.last_button_press >= self.debounce_time: + self.last_button_press = current_time + self.cycle_screen_mode() + + self.last_button_state = current_button_state + + def cycle_screen_mode(self): + """Cycle to next screen mode""" + print(f"Button pressed! Cycling from mode {self.current_mode}") + + # Stop current program + self.stop_current_program() + + # Move to next mode + old_mode = self.current_mode + self.current_mode = (self.current_mode + 1) % len(SCREEN_MODES) + + print(f"Switching from mode {old_mode} to mode {self.current_mode}: {SCREEN_MODES[self.current_mode]['name']}") + + # Start new program + self.start_current_program() + print(f"Mode switch completed successfully") + + def stop_current_program(self): + """Stop the currently running display program""" + if self.current_process: + try: + self.current_process.terminate() + self.current_process.wait(timeout=3) + print("Previous display program terminated") + except subprocess.TimeoutExpired: + self.current_process.kill() + print("Previous display program killed (timeout)") + except Exception as e: + print(f"Error stopping previous program: {e}") + finally: + self.current_process = None + + # Kill any remaining matrix display processes (be very specific to avoid killing ourselves) + try: + subprocess.run(['pkill', '-f', '/var/www/moduleair_pro_4g/matrix/screenNetwork/network_status'], check=False) + subprocess.run(['pkill', '-f', '/var/www/moduleair_pro_4g/matrix/screenSensors/displayAll4'], check=False) + subprocess.run(['pkill', '-f', '/var/www/moduleair_pro_4g/matrix/screenSensors/displayCO2'], check=False) + subprocess.run(['pkill', '-f', '/var/www/moduleair_pro_4g/matrix/screenSensors/blank_screen'], check=False) + time.sleep(0.5) # Give processes time to exit + except Exception as e: + print(f"Error killing matrix processes: {e}") + + def start_current_program(self): + """Start the display program for current mode""" + mode_config = SCREEN_MODES[self.current_mode] + program_path = mode_config['program'] + + if not os.path.exists(program_path): + print(f"Warning: Program {program_path} does not exist") + return + + try: + # Build command with arguments + cmd = [program_path] + mode_config['args'] + + print(f"Starting: {' '.join(cmd)}") + + # Start the new program + self.current_process = subprocess.Popen( + cmd, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + preexec_fn=os.setsid + ) + + print(f"Started {mode_config['name']} (PID: {self.current_process.pid})") + + except Exception as e: + print(f"Error starting program {program_path}: {e}") + self.current_process = None + + def signal_handler(self, signum, frame): + """Handle shutdown signals""" + print(f"Received signal {signum}, shutting down...") + print(f"Signal received at frame: {frame}") + self.running = False + self.cleanup() + sys.exit(0) + + def cleanup(self): + """Clean up resources""" + print("Cleaning up...") + self.stop_current_program() + GPIO.cleanup() + + def run(self): + """Main loop""" + # Start with initial screen mode + self.start_current_program() + + try: + while self.running: + # Check for button press + self.check_button_pressed() + + # Check if current process is still running + if self.current_process and self.current_process.poll() is not None: + print(f"Display program ended unexpectedly (exit code: {self.current_process.returncode})") + self.current_process = None + # Restart the current program + time.sleep(1) + self.start_current_program() + + time.sleep(0.1) # Check 10 times per second for responsive button + + except KeyboardInterrupt: + print("Keyboard interrupt received") + finally: + self.cleanup() + +def main(): + """Main function""" + print("ModuleAir Pro 4G - Button Screen Controller") + print("Press button on GPIO6 to cycle through screen modes") + print("Modes: Network Status -> All 4 Sensors -> CO2+PM+Network -> Blank Screen") + + controller = ScreenController() + controller.run() + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/matrix/screenNetwork/network_status.cc b/matrix/screenNetwork/network_status.cc index b8aba97..f568190 100644 --- a/matrix/screenNetwork/network_status.cc +++ b/matrix/screenNetwork/network_status.cc @@ -14,6 +14,9 @@ g++ -I/var/www/moduleair_pro_4g/matrix/include -L/var/www/moduleair_pro_4g/matri Pour lancer: sudo /var/www/moduleair_pro_4g/matrix/screenNetwork/network_status + +Pour arréter: +sudo systemctl stop moduleair-boot.service */ #include "led-matrix.h" diff --git a/matrix/screenSensors/blank_screen b/matrix/screenSensors/blank_screen new file mode 100644 index 0000000..3c4b514 Binary files /dev/null and b/matrix/screenSensors/blank_screen differ diff --git a/matrix/screenSensors/blank_screen.cc b/matrix/screenSensors/blank_screen.cc new file mode 100644 index 0000000..9a6ec7a --- /dev/null +++ b/matrix/screenSensors/blank_screen.cc @@ -0,0 +1,90 @@ +/* + ____ _ _ _ _ _ __ +| __ )| | / \ | \ | | |/ / +| _ \| | / _ \ | \| | ' / +| |_) | |___ / ___ \| |\ | . \ +|____/|_____/_/ \_\_| \_|_|\_\ + +Simple blank screen (black) for LED matrix +Used when user wants to turn off the display + +Pour compiler: +g++ -I/var/www/moduleair_pro_4g/matrix/include -L/var/www/moduleair_pro_4g/matrix/lib /var/www/moduleair_pro_4g/matrix/screenSensors/blank_screen.cc -o /var/www/moduleair_pro_4g/matrix/screenSensors/blank_screen -lrgbmatrix + +Pour lancer: +sudo /var/www/moduleair_pro_4g/matrix/screenSensors/blank_screen +*/ + +#include "led-matrix.h" +#include "graphics.h" +#include +#include +#include +#include + +using rgb_matrix::RGBMatrix; +using rgb_matrix::Canvas; + +std::atomic running(true); + +void signal_handler(int signum) { + running = false; +} + +int main(int argc, char *argv[]) { + std::cout << "Blank Screen Display started" << std::endl; + + // Handle signals for graceful exit + signal(SIGINT, signal_handler); + signal(SIGTERM, signal_handler); + + // Initialize LED matrix + std::cout << "Initializing LED matrix..." << std::endl; + RGBMatrix::Options defaults; + defaults.hardware_mapping = "moduleair_pinout"; + defaults.rows = 64; + defaults.cols = 128; + defaults.chain_length = 1; + defaults.parallel = 1; + defaults.row_address_type = 3; + defaults.show_refresh_rate = false; + defaults.brightness = 100; + defaults.pwm_bits = 1; + defaults.panel_type = "FM6126A"; + defaults.disable_hardware_pulsing = false; + + rgb_matrix::RuntimeOptions runtime_opt; + runtime_opt.gpio_slowdown = 4; + runtime_opt.daemon = 0; + runtime_opt.drop_privileges = 0; + + Canvas *canvas = RGBMatrix::CreateFromOptions(defaults, runtime_opt); + if (canvas == NULL) { + std::cerr << "Error creating LED matrix canvas" << std::endl; + return 1; + } + std::cout << "LED matrix initialized successfully" << std::endl; + + // Clear the screen (turn all pixels black) + canvas->Clear(); + std::cout << "Screen cleared - all pixels black" << std::endl; + + // Main loop - just keep the screen blank + std::cout << "Blank screen active - press Ctrl+C to exit" << std::endl; + while (running) { + // Ensure screen stays clear + canvas->Clear(); + + // Sleep for 5 seconds before next clear + for (int i = 0; i < 5 && running; i++) { + sleep(1); + } + } + + // Clean up + std::cout << "Program terminating, cleaning up..." << std::endl; + canvas->Clear(); + delete canvas; + std::cout << "Blank Screen Display terminated" << std::endl; + return 0; +} \ No newline at end of file diff --git a/matrix/screenSensors/displayCO2_PM_Network b/matrix/screenSensors/displayCO2_PM_Network new file mode 100644 index 0000000..afbad5a Binary files /dev/null and b/matrix/screenSensors/displayCO2_PM_Network differ diff --git a/matrix/screenSensors/displayCO2_PM_Network.cc b/matrix/screenSensors/displayCO2_PM_Network.cc new file mode 100644 index 0000000..75d2082 --- /dev/null +++ b/matrix/screenSensors/displayCO2_PM_Network.cc @@ -0,0 +1,366 @@ +/* + ____ ___ ____ _ _ _____ _______ _____ ____ _ __ + / ___/ _ \___ \ | \ | | ____|_ _\ \ / / _ \| _ \| |/ / +| | | | | |__) || \| | _| | | \ \ /\ / / | | | |_) | ' / +| |__| |_| / __/ | |\ | |___ | | \ V V /| |_| | _ <| . \ + \____\___/_____||_| \_|_____| |_| \_/\_/ \___/|_| \_\_|\_\ + +Combined CO2 + PM2.5 + PM10 + Network Status Display +Shows 3 key measurements plus network connectivity status + +Pour compiler: +g++ -I/var/www/moduleair_pro_4g/matrix/include -L/var/www/moduleair_pro_4g/matrix/lib /var/www/moduleair_pro_4g/matrix/screenSensors/displayCO2_PM_Network.cc -o /var/www/moduleair_pro_4g/matrix/screenSensors/displayCO2_PM_Network -lrgbmatrix -lsqlite3 + +Pour lancer: +sudo /var/www/moduleair_pro_4g/matrix/screenSensors/displayCO2_PM_Network +*/ + +#include "led-matrix.h" +#include "graphics.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using rgb_matrix::RGBMatrix; +using rgb_matrix::Canvas; + +std::atomic running(true); + +// Define color codes +#define RESET "\033[0m" +#define RED "\033[31m" +#define GREEN "\033[32m" +#define YELLOW "\033[33m" +#define BLUE "\033[34m" +#define MAGENTA "\033[35m" +#define CYAN "\033[36m" +#define WHITE "\033[37m" + +// Path to the SQLite database +const std::string DB_PATH = "/var/www/moduleair_pro_4g/sqlite/sensors.db"; + +void signal_handler(int signum) { + running = false; +} + +void log(const std::string& type, const std::string& message) { + std::string color; + + if (type == "BLUE") color = BLUE; + else if (type == "GREEN") color = GREEN; + else if (type == "YELLOW") color = YELLOW; + else if (type == "RED") color = RED; + else color = WHITE; + + std::cout << color << message << RESET << std::endl; +} + +// Function to retrieve latest sensor data +bool get_sensor_data(sqlite3* db, float& pm25, float& pm10, float& co2) { + sqlite3_stmt* stmt; + bool pm_success = false; + bool co2_success = false; + + log("BLUE", "Querying sensor data from database..."); + + // Get PM2.5 and PM10 data from NPM table + const char* pm_query = "SELECT PM25, PM10 FROM data_NPM ORDER BY rowid DESC LIMIT 1"; + if (sqlite3_prepare_v2(db, pm_query, -1, &stmt, NULL) == SQLITE_OK) { + if (sqlite3_step(stmt) == SQLITE_ROW) { + pm25 = sqlite3_column_double(stmt, 0); + pm10 = sqlite3_column_double(stmt, 1); + pm_success = true; + std::cout << " Retrieved PM values - PM2.5: " << pm25 << ", PM10: " << pm10 << std::endl; + } + sqlite3_finalize(stmt); + } + + // Get CO2 data + const char* co2_query = "SELECT CO2 FROM data_CO2 ORDER BY rowid DESC LIMIT 1"; + if (sqlite3_prepare_v2(db, co2_query, -1, &stmt, NULL) == SQLITE_OK) { + if (sqlite3_step(stmt) == SQLITE_ROW) { + co2 = sqlite3_column_double(stmt, 0); + co2_success = true; + std::cout << " Retrieved CO2 value: " << co2 << " ppm" << std::endl; + } + sqlite3_finalize(stmt); + } + + return pm_success && co2_success; +} + +// Function to get network status +bool get_network_status(sqlite3* db, std::string& network_status, std::string& wifi_status) { + sqlite3_stmt* stmt; + bool status_found = false; + + // Get 4G network status + const char* network_query = "SELECT value FROM config_table WHERE key='SARA_network_status'"; + if (sqlite3_prepare_v2(db, network_query, -1, &stmt, NULL) == SQLITE_OK) { + if (sqlite3_step(stmt) == SQLITE_ROW) { + network_status = (char*)sqlite3_column_text(stmt, 0); + status_found = true; + } else { + network_status = "unknown"; + } + sqlite3_finalize(stmt); + } + + // Get WiFi status + const char* wifi_query = "SELECT value FROM config_table WHERE key='WIFI_status'"; + if (sqlite3_prepare_v2(db, wifi_query, -1, &stmt, NULL) == SQLITE_OK) { + if (sqlite3_step(stmt) == SQLITE_ROW) { + wifi_status = (char*)sqlite3_column_text(stmt, 0); + } else { + wifi_status = "unknown"; + } + sqlite3_finalize(stmt); + } + + return status_found; +} + +// Function to get color based on air quality thresholds +rgb_matrix::Color get_quality_color(float value, const std::string& type) { + if (type == "pm25") { + if (value < 10) return rgb_matrix::Color(0, 255, 0); // Green - Good + else if (value < 20) return rgb_matrix::Color(255, 255, 0); // Yellow - Moderate + else if (value < 50) return rgb_matrix::Color(255, 165, 0); // Orange - Degraded + else return rgb_matrix::Color(255, 0, 0); // Red - Bad + } else if (type == "pm10") { + if (value < 15) return rgb_matrix::Color(0, 255, 0); // Green - Good + else if (value < 30) return rgb_matrix::Color(255, 255, 0); // Yellow - Moderate + else if (value < 75) return rgb_matrix::Color(255, 165, 0); // Orange - Degraded + else return rgb_matrix::Color(255, 0, 0); // Red - Bad + } else if (type == "co2") { + if (value < 800) return rgb_matrix::Color(0, 255, 0); // Green - Good + else if (value < 1500) return rgb_matrix::Color(255, 165, 0); // Orange - Moderate + else return rgb_matrix::Color(255, 0, 0); // Red - Bad + } + return rgb_matrix::Color(255, 255, 255); // White default +} + +// Function to get status text based on thresholds +std::string get_status_text(float value, const std::string& type) { + if (type == "pm25") { + if (value < 10) return "BON"; + else if (value < 20) return "MOYEN"; + else if (value < 50) return "DEGRADE"; + else return "MAUVAIS"; + } else if (type == "pm10") { + if (value < 15) return "BON"; + else if (value < 30) return "MOYEN"; + else if (value < 75) return "DEGRADE"; + else return "MAUVAIS"; + } else if (type == "co2") { + if (value < 800) return "BON"; + else if (value < 1500) return "AERER SVP"; + else return "AERER VITE"; + } + return "UNKNOWN"; +} + +// Function to draw network connection indicators +void draw_network_indicators(Canvas* canvas, const std::string& network_status, const std::string& wifi_status) { + rgb_matrix::Color green(0, 255, 0); + rgb_matrix::Color red(255, 0, 0); + rgb_matrix::Color orange(255, 165, 0); + rgb_matrix::Color yellow(255, 255, 0); + + // Draw 4G indicator (small rectangle) + rgb_matrix::Color net_color = (network_status == "connected") ? green : + (network_status == "connecting" || network_status == "booting") ? yellow : red; + + for (int x = 100; x < 108; x++) { + for (int y = 2; y < 8; y++) { + canvas->SetPixel(x, y, net_color.r, net_color.g, net_color.b); + } + } + + // Draw WiFi indicator (small rectangle) + rgb_matrix::Color wifi_color = (wifi_status == "connected") ? green : + (wifi_status == "hotspot") ? orange : red; + + for (int x = 112; x < 120; x++) { + for (int y = 2; y < 8; y++) { + canvas->SetPixel(x, y, wifi_color.r, wifi_color.g, wifi_color.b); + } + } +} + +// Main display function +void draw_combined_screen(Canvas* canvas, float pm25, float pm10, float co2, + const std::string& network_status, const std::string& wifi_status) { + rgb_matrix::Color white(255, 255, 255); + rgb_matrix::Color cyan(0, 255, 255); + rgb_matrix::Color bg_color(0, 0, 0); + + rgb_matrix::Font title_font, value_font, small_font; + + // Load fonts + if (!title_font.LoadFont("/var/www/moduleair_pro_4g/matrix/fonts/6x9.bdf") || + !value_font.LoadFont("/var/www/moduleair_pro_4g/matrix/fonts/9x18B.bdf") || + !small_font.LoadFont("/var/www/moduleair_pro_4g/matrix/fonts/6x9.bdf")) { + std::cerr << "Error loading fonts!" << std::endl; + return; + } + + canvas->Clear(); + + // Draw title + rgb_matrix::DrawText(canvas, title_font, 2, title_font.baseline() + 2, cyan, &bg_color, "CO2 + PM + NET", 0); + + // Draw network indicators in top right + draw_network_indicators(canvas, network_status, wifi_status); + + // Format sensor values + std::stringstream ss_pm25, ss_pm10, ss_co2; + ss_pm25 << std::fixed << std::setprecision(1) << pm25; + ss_pm10 << std::fixed << std::setprecision(1) << pm10; + ss_co2 << std::fixed << std::setprecision(0) << co2; + + std::string str_pm25 = ss_pm25.str(); + std::string str_pm10 = ss_pm10.str(); + std::string str_co2 = ss_co2.str(); + + // Get colors and status for each measurement + rgb_matrix::Color pm25_color = get_quality_color(pm25, "pm25"); + rgb_matrix::Color pm10_color = get_quality_color(pm10, "pm10"); + rgb_matrix::Color co2_color = get_quality_color(co2, "co2"); + + std::string pm25_status = get_status_text(pm25, "pm25"); + std::string pm10_status = get_status_text(pm10, "pm10"); + std::string co2_status = get_status_text(co2, "co2"); + + // Layout: 3 measurements with proper spacing to avoid overlap + int y_start = 16; + int y_spacing = 16; // Increased spacing to prevent overlap + + // PM2.5 - using smaller positioning to fit everything + rgb_matrix::DrawText(canvas, small_font, 2, y_start, cyan, &bg_color, "PM2.5:", 0); + rgb_matrix::DrawText(canvas, small_font, 32, y_start, white, &bg_color, str_pm25.c_str(), 0); + rgb_matrix::DrawText(canvas, small_font, 55, y_start, pm25_color, &bg_color, pm25_status.c_str(), 0); + + // PM10 + y_start += y_spacing; + rgb_matrix::DrawText(canvas, small_font, 2, y_start, cyan, &bg_color, "PM10:", 0); + rgb_matrix::DrawText(canvas, small_font, 32, y_start, white, &bg_color, str_pm10.c_str(), 0); + rgb_matrix::DrawText(canvas, small_font, 55, y_start, pm10_color, &bg_color, pm10_status.c_str(), 0); + + // CO2 + y_start += y_spacing; + rgb_matrix::DrawText(canvas, small_font, 2, y_start, cyan, &bg_color, "CO2:", 0); + rgb_matrix::DrawText(canvas, small_font, 28, y_start, white, &bg_color, str_co2.c_str(), 0); + rgb_matrix::DrawText(canvas, small_font, 55, y_start, co2_color, &bg_color, co2_status.c_str(), 0); + + // Draw network status text at bottom - simplified to fit + y_start += y_spacing; + std::string net_text = std::string("4G:") + (network_status == "connected" ? "OK" : "NO") + + std::string(" WiFi:") + (wifi_status == "connected" ? "OK" : "NO"); + rgb_matrix::DrawText(canvas, small_font, 2, y_start, rgb_matrix::Color(150, 150, 150), &bg_color, net_text.c_str(), 0); + + // Draw timestamp + auto now = std::chrono::system_clock::now(); + auto time_t = std::chrono::system_clock::to_time_t(now); + auto tm = *std::localtime(&time_t); + + std::stringstream time_ss; + time_ss << std::put_time(&tm, "%H:%M"); + + rgb_matrix::DrawText(canvas, small_font, 100, 62, rgb_matrix::Color(100, 100, 100), &bg_color, + time_ss.str().c_str(), 0); +} + +int main(int argc, char *argv[]) { + log("BLUE", "CO2 + PM + Network Display started"); + + // Handle signals for graceful exit + signal(SIGINT, signal_handler); + signal(SIGTERM, signal_handler); + + // Initialize database connection + sqlite3* db; + std::cout << "Opening SQLite database at " << DB_PATH << std::endl; + int rc = sqlite3_open(DB_PATH.c_str(), &db); + if (rc) { + std::cerr << "Error opening SQLite database: " << sqlite3_errmsg(db) << std::endl; + sqlite3_close(db); + return 1; + } + + // Initialize LED matrix + log("BLUE", "Initializing LED matrix..."); + RGBMatrix::Options defaults; + defaults.hardware_mapping = "moduleair_pinout"; + defaults.rows = 64; + defaults.cols = 128; + defaults.chain_length = 1; + defaults.parallel = 1; + defaults.row_address_type = 3; + defaults.show_refresh_rate = false; + defaults.brightness = 100; + defaults.pwm_bits = 1; + defaults.panel_type = "FM6126A"; + defaults.disable_hardware_pulsing = false; + + rgb_matrix::RuntimeOptions runtime_opt; + runtime_opt.gpio_slowdown = 4; + runtime_opt.daemon = 0; + runtime_opt.drop_privileges = 0; + + Canvas *canvas = RGBMatrix::CreateFromOptions(defaults, runtime_opt); + if (canvas == NULL) { + std::cerr << "Error creating LED matrix canvas" << std::endl; + sqlite3_close(db); + return 1; + } + log("GREEN", "LED matrix initialized successfully"); + + // Main loop + log("BLUE", "Starting display loop"); + while (running) { + float pm25 = 0, pm10 = 0, co2 = 0; + std::string network_status, wifi_status; + + // Get sensor data + bool sensor_success = get_sensor_data(db, pm25, pm10, co2); + bool network_success = get_network_status(db, network_status, wifi_status); + + if (!sensor_success) { + std::cerr << "Error retrieving sensor data from database" << std::endl; + } + + if (!network_success) { + std::cerr << "Error retrieving network data from database" << std::endl; + network_status = "unknown"; + wifi_status = "unknown"; + } + + // Draw the combined screen + draw_combined_screen(canvas, pm25, pm10, co2, network_status, wifi_status); + + // Sleep before next update + std::cout << "Update complete, sleeping for 5 seconds..." << std::endl; + for (int i = 0; i < 5 && running; i++) { + sleep(1); + } + } + + // Clean up + std::cout << "Program terminating, cleaning up..." << std::endl; + canvas->Clear(); + delete canvas; + sqlite3_close(db); + std::cout << "CO2 + PM + Network Display terminated" << std::endl; + return 0; +} \ No newline at end of file diff --git a/services/matrix_boot.sh b/services/matrix_boot.sh index ddca6a3..cd873c5 100644 --- a/services/matrix_boot.sh +++ b/services/matrix_boot.sh @@ -1,15 +1,16 @@ #!/bin/bash -# Simple boot script - run animation then sensor display +# Boot script with button-controlled screen cycling # sudo chmod +x /var/www/moduleair_pro_4g/services/matrix_boot.sh # sudo /var/www/moduleair_pro_4g/services/matrix_boot.sh +# to stop the service +# sudo systemctl stop moduleair-boot.service + echo "$(date) - Starting boot animation..." sudo /var/www/moduleair_pro_4g/matrix/imageScreen/image_split_boot /var/www/moduleair_pro_4g/matrix/imageScreen/ModuleAir128x64.png -#0echo "$(date) - Boot animation done, starting sensor display..." -#sudo /var/www/moduleair_pro_4g/matrix/screenSensors/displayAll4_v2 +echo "$(date) - Boot animation done, starting button-controlled display..." +echo "$(date) - Press button on GPIO6 to cycle: Network -> All Sensors -> CO2+PM+Network -> Blank" +sudo python3 /var/www/moduleair_pro_4g/matrix/button_screen_controller.py -echo "$(date) - Boot animation done, starting network display..." -sudo /var/www/moduleair_pro_4g/matrix/screenNetwork/network_status - -echo "$(date) - Both programs started" \ No newline at end of file +echo "$(date) - Button controller started" \ No newline at end of file diff --git a/test_button_simple.py b/test_button_simple.py new file mode 100644 index 0000000..f31682b --- /dev/null +++ b/test_button_simple.py @@ -0,0 +1,63 @@ +#!/usr/bin/env python3 +""" +Simple polling-based button test for GPIO6 +No edge detection - just polls the pin state +""" + +import RPi.GPIO as GPIO +import time +import signal +import sys + +BUTTON_PIN = 6 + +def signal_handler(signum, frame): + print("Cleaning up GPIO...") + GPIO.cleanup() + sys.exit(0) + +def main(): + print("Simple Button Test - Polling GPIO6") + print("Press the button connected to GPIO6 (GND when pressed)") + print("Press Ctrl+C to exit") + + # Clean up any existing GPIO setup + try: + GPIO.cleanup() + except: + pass + + # Set up GPIO + GPIO.setmode(GPIO.BCM) + GPIO.setup(BUTTON_PIN, GPIO.IN, pull_up_down=GPIO.PUD_UP) + + # Set up signal handler + signal.signal(signal.SIGINT, signal_handler) + signal.signal(signal.SIGTERM, signal_handler) + + print(f"GPIO{BUTTON_PIN} setup complete. Current state: {GPIO.input(BUTTON_PIN)}") + print("Monitoring for button presses (1=released, 0=pressed)...") + + last_state = GPIO.input(BUTTON_PIN) + button_press_count = 0 + + try: + while True: + current_state = GPIO.input(BUTTON_PIN) + + # Detect falling edge (button press) + if last_state == 1 and current_state == 0: + button_press_count += 1 + print(f"Button press #{button_press_count} detected!") + time.sleep(0.5) # Simple debounce + + last_state = current_state + time.sleep(0.01) # Poll every 10ms + + except KeyboardInterrupt: + pass + finally: + GPIO.cleanup() + +if __name__ == "__main__": + main() \ No newline at end of file