Compare commits
62 Commits
ai_branch_
...
fe604791f0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fe604791f0 | ||
|
|
624fb4abbc | ||
|
|
163d60bf34 | ||
|
|
906eaa851d | ||
|
|
954680ef6e | ||
|
|
1f4d38257e | ||
|
|
a38ce79555 | ||
|
|
62ef47aa67 | ||
|
|
ca7533a344 | ||
|
|
403c57bf18 | ||
|
|
129b2de68e | ||
|
|
d2c88e0d18 | ||
|
|
fba5af53cb | ||
|
|
04fbf81798 | ||
|
|
65beead82b | ||
|
|
26ee893a96 | ||
|
|
5cf37c3cee | ||
|
|
3ecc27fd3e | ||
|
|
072fca72cc | ||
|
|
c038084343 | ||
|
|
6069ab04cf | ||
|
|
79f3ede17f | ||
|
|
9de903f2db | ||
|
|
77fcdaa08e | ||
|
|
1fca3091eb | ||
|
|
d0b49bf30c | ||
|
|
4779f426d9 | ||
|
|
9aab95edb6 | ||
|
|
fe61b56b5b | ||
|
|
25c5a7a65a | ||
|
|
4d512685a0 | ||
|
|
44b2e2189d | ||
|
|
74fc3baece | ||
|
|
0539cb67af | ||
|
|
98115ab22b | ||
|
|
2989a7a9ed | ||
|
|
aa458fbac4 | ||
| 707dffd6f8 | |||
| c917131b2d | |||
|
|
057dc7d87b | ||
|
|
fcc30243f5 | ||
|
|
75774cea62 | ||
|
|
3731c2b7cf | ||
|
|
1240ebf6cd | ||
|
|
e27f2430b7 | ||
|
|
ebdc4ae353 | ||
|
|
6cd5191138 | ||
|
|
8d989de425 | ||
|
|
381cf85336 | ||
|
|
caf5488b06 | ||
|
|
5d4f7225b0 | ||
|
|
6d997ff550 | ||
|
|
aa71e359bb | ||
|
|
7bd1d81bf9 | ||
|
|
4bc0dc2acc | ||
|
|
694edfaf27 | ||
|
|
93d77db853 | ||
|
|
122763a4e5 | ||
|
|
c6a8b02c38 | ||
|
|
b93f205fd4 | ||
|
|
8fdd1d6ac5 | ||
|
|
6796aa95bb |
21
.claude/README.md
Normal file
21
.claude/README.md
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
# Claude Code Settings
|
||||||
|
|
||||||
|
This directory contains configuration for Claude Code.
|
||||||
|
|
||||||
|
## Files
|
||||||
|
|
||||||
|
- **settings.json**: Project-wide default settings (tracked in git)
|
||||||
|
- **settings.local.json**: Local user-specific overrides (not tracked in git)
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
The `settings.json` file contains the default configuration that applies to all developers/devices. If you need to customize settings for your local environment, create a `settings.local.json` file which will override the defaults.
|
||||||
|
|
||||||
|
### Example: Create local overrides
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp .claude/settings.json .claude/settings.local.json
|
||||||
|
# Edit settings.local.json with your preferences
|
||||||
|
```
|
||||||
|
|
||||||
|
Your local changes will not be committed to git.
|
||||||
9
.claude/settings.json
Normal file
9
.claude/settings.json
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"permissions": {
|
||||||
|
"allow": [
|
||||||
|
"Bash(python3:*)"
|
||||||
|
],
|
||||||
|
"deny": []
|
||||||
|
},
|
||||||
|
"enableAllProjectMcpServers": false
|
||||||
|
}
|
||||||
7
.gitignore
vendored
7
.gitignore
vendored
@@ -14,4 +14,9 @@ NPM/data/*.txt
|
|||||||
NPM/data/*.json
|
NPM/data/*.json
|
||||||
*.lock
|
*.lock
|
||||||
sqlite/*.db
|
sqlite/*.db
|
||||||
tests/
|
sqlite/*.sql
|
||||||
|
|
||||||
|
tests/
|
||||||
|
|
||||||
|
# Claude Code local settings
|
||||||
|
.claude/settings.local.json
|
||||||
270
CLAUDE.md
Normal file
270
CLAUDE.md
Normal file
@@ -0,0 +1,270 @@
|
|||||||
|
# CLAUDE.md
|
||||||
|
|
||||||
|
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||||
|
|
||||||
|
## Project Overview
|
||||||
|
|
||||||
|
NebuleAir Pro 4G is an environmental monitoring system running on Raspberry Pi 4/CM4. It collects air quality and environmental data from multiple sensors and transmits it via 4G cellular modem. The system includes a self-hosted web interface for configuration and monitoring.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
### Data Flow
|
||||||
|
1. **Data Collection**: Sensors are polled by individual Python scripts triggered by systemd timers
|
||||||
|
2. **Local Storage**: All sensor data is stored in SQLite database (`sqlite/sensors.db`)
|
||||||
|
3. **Data Transmission**: Main loop script reads aggregated data from SQLite and transmits via SARA R4/R5 4G modem
|
||||||
|
4. **Web Interface**: Apache serves PHP pages that interact with SQLite and execute Python scripts
|
||||||
|
|
||||||
|
### Key Components
|
||||||
|
|
||||||
|
**Sensors & Hardware:**
|
||||||
|
- NextPM (NPM): Particulate matter sensor via Modbus (ttyAMA0)
|
||||||
|
- Envea CAIRSENS: Gas sensors (NO2, H2S, NH3, CO, O3) via serial (ttyAMA2-5)
|
||||||
|
- BME280: Temperature, humidity, pressure via I2C (0x76)
|
||||||
|
- NSRT MK4: Noise sensor via I2C (0x48)
|
||||||
|
- SARA R4/R5: 4G cellular modem (ttyAMA2)
|
||||||
|
- Wind meter: via ADS1115 ADC
|
||||||
|
- MPPT: Solar charger monitoring
|
||||||
|
|
||||||
|
**Software Stack:**
|
||||||
|
- OS: Raspberry Pi OS (Linux)
|
||||||
|
- Web server: Apache2
|
||||||
|
- Database: SQLite3
|
||||||
|
- Languages: Python 3, PHP, Bash
|
||||||
|
- Network: NetworkManager (nmcli)
|
||||||
|
|
||||||
|
### Directory Structure
|
||||||
|
- `NPM/`: NextPM sensor scripts
|
||||||
|
- `envea/`: Envea sensor scripts
|
||||||
|
- `BME280/`: BME280 sensor scripts
|
||||||
|
- `sound_meter/`: Noise sensor code (C program)
|
||||||
|
- `SARA/`: 4G modem communication (AT commands)
|
||||||
|
- `windMeter/`: Wind sensor scripts
|
||||||
|
- `MPPT/`: Solar charger scripts
|
||||||
|
- `RTC/`: DS3231 real-time clock module scripts
|
||||||
|
- `sqlite/`: Database scripts (create, read, write, config)
|
||||||
|
- `loop/`: Main data transmission script
|
||||||
|
- `html/`: Web interface files
|
||||||
|
- `services/`: Systemd service/timer configuration
|
||||||
|
- `logs/`: Application logs
|
||||||
|
|
||||||
|
## Common Development Commands
|
||||||
|
|
||||||
|
### Database Operations
|
||||||
|
```bash
|
||||||
|
# Initialize database
|
||||||
|
/usr/bin/python3 /var/www/nebuleair_pro_4g/sqlite/create_db.py
|
||||||
|
|
||||||
|
# Set configuration
|
||||||
|
/usr/bin/python3 /var/www/nebuleair_pro_4g/sqlite/set_config.py
|
||||||
|
|
||||||
|
# Read data from specific table
|
||||||
|
/usr/bin/python3 /var/www/nebuleair_pro_4g/sqlite/read.py <table_name> <limit>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Systemd Services
|
||||||
|
The system uses systemd timers for scheduling sensor data collection:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# View all NebuleAir timers
|
||||||
|
systemctl list-timers | grep nebuleair
|
||||||
|
|
||||||
|
# Check specific service status
|
||||||
|
systemctl status nebuleair-npm-data.service
|
||||||
|
systemctl status nebuleair-sara-data.service
|
||||||
|
|
||||||
|
# View service logs
|
||||||
|
journalctl -u nebuleair-npm-data.service
|
||||||
|
journalctl -u nebuleair-sara-data.service -f # follow
|
||||||
|
|
||||||
|
# Restart services
|
||||||
|
systemctl restart nebuleair-npm-data.timer
|
||||||
|
systemctl restart nebuleair-sara-data.timer
|
||||||
|
|
||||||
|
# Setup all services
|
||||||
|
sudo /var/www/nebuleair_pro_4g/services/setup_services.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
**Service Schedule:**
|
||||||
|
- `nebuleair-npm-data.timer`: Every 10 seconds (NextPM sensor)
|
||||||
|
- `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-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)
|
||||||
|
|
||||||
|
### Sensor Testing
|
||||||
|
```bash
|
||||||
|
# Test NextPM sensor
|
||||||
|
/usr/bin/python3 /var/www/nebuleair_pro_4g/NPM/get_data_modbus_v3.py
|
||||||
|
|
||||||
|
# Test Envea sensors (read reference/ID)
|
||||||
|
/usr/bin/python3 /var/www/nebuleair_pro_4g/envea/read_ref_v2.py
|
||||||
|
|
||||||
|
# Test Envea sensor values
|
||||||
|
/usr/bin/python3 /var/www/nebuleair_pro_4g/envea/read_value_v2.py
|
||||||
|
|
||||||
|
# Test BME280
|
||||||
|
/usr/bin/python3 /var/www/nebuleair_pro_4g/BME280/get_data_v2.py
|
||||||
|
|
||||||
|
# Test noise sensor
|
||||||
|
/usr/bin/python3 /var/www/nebuleair_pro_4g/sound_meter/NSRT_mk4_get_data.py
|
||||||
|
|
||||||
|
# Test RTC module
|
||||||
|
/usr/bin/python3 /var/www/nebuleair_pro_4g/RTC/read.py
|
||||||
|
|
||||||
|
# Test MPPT charger
|
||||||
|
/usr/bin/python3 /var/www/nebuleair_pro_4g/MPPT/read.py
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4G Modem (SARA R4/R5)
|
||||||
|
```bash
|
||||||
|
# Send AT command
|
||||||
|
/usr/bin/python3 /var/www/nebuleair_pro_4g/SARA/sara.py ttyAMA2 "AT+CSQ" 5
|
||||||
|
|
||||||
|
# Check network connection
|
||||||
|
/usr/bin/python3 /var/www/nebuleair_pro_4g/SARA/sara_connectNetwork.py ttyAMA2 <networkID> 120
|
||||||
|
|
||||||
|
# Set server URL (HTTP profile 0 for AirCarto)
|
||||||
|
/usr/bin/python3 /var/www/nebuleair_pro_4g/SARA/sara_setURL.py ttyAMA2 data.nebuleair.fr 0
|
||||||
|
|
||||||
|
# Check modem status
|
||||||
|
/usr/bin/python3 /var/www/nebuleair_pro_4g/SARA/check_running.py
|
||||||
|
```
|
||||||
|
|
||||||
|
### Network Configuration
|
||||||
|
```bash
|
||||||
|
# Scan WiFi networks (saved to wifi_list.csv)
|
||||||
|
nmcli device wifi list ifname wlan0
|
||||||
|
|
||||||
|
# Connect to WiFi
|
||||||
|
sudo nmcli device wifi connect "SSID" password "PASSWORD"
|
||||||
|
|
||||||
|
# Create hotspot (done automatically at boot if no connection)
|
||||||
|
sudo nmcli device wifi hotspot ifname wlan0 ssid nebuleair_pro password nebuleaircfg
|
||||||
|
|
||||||
|
# Check connection status
|
||||||
|
nmcli device show wlan0
|
||||||
|
nmcli device show eth0
|
||||||
|
```
|
||||||
|
|
||||||
|
### Permissions & Serial Ports
|
||||||
|
```bash
|
||||||
|
# Grant serial port access (run at boot via cron)
|
||||||
|
chmod 777 /dev/ttyAMA* /dev/i2c-1
|
||||||
|
|
||||||
|
# Check I2C devices
|
||||||
|
sudo i2cdetect -y 1
|
||||||
|
```
|
||||||
|
|
||||||
|
### Logs
|
||||||
|
```bash
|
||||||
|
# View main application log
|
||||||
|
tail -f /var/www/nebuleair_pro_4g/logs/app.log
|
||||||
|
|
||||||
|
# View SARA transmission log
|
||||||
|
tail -f /var/www/nebuleair_pro_4g/logs/sara_service.log
|
||||||
|
|
||||||
|
# View service-specific logs
|
||||||
|
tail -f /var/www/nebuleair_pro_4g/logs/npm_service.log
|
||||||
|
tail -f /var/www/nebuleair_pro_4g/logs/envea_service.log
|
||||||
|
|
||||||
|
# Clear logs (done daily via cron)
|
||||||
|
find /var/www/nebuleair_pro_4g/logs -name "*.log" -type f -exec truncate -s 0 {} \;
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration System
|
||||||
|
|
||||||
|
Configuration is stored in SQLite database (`sqlite/sensors.db`) in the `config_table`:
|
||||||
|
|
||||||
|
**Key Configuration Parameters:**
|
||||||
|
- `deviceID`: Unique device identifier (string)
|
||||||
|
- `modem_config_mode`: When true, disables data transmission loop (bool)
|
||||||
|
- `modem_version`: SARA R4 or R5 (string)
|
||||||
|
- `SaraR4_baudrate`: Modem baudrate, usually 115200 or 9600 (int)
|
||||||
|
- `SARA_R4_neworkID`: Cellular network ID for connection (int)
|
||||||
|
- `send_miotiq`: Enable UDP transmission to Miotiq server (bool)
|
||||||
|
- `send_aircarto`: Enable HTTP transmission to AirCarto server (bool)
|
||||||
|
- `send_uSpot`: Enable HTTPS transmission to uSpot server (bool)
|
||||||
|
- `npm_5channel`: Enable 5-channel particle size distribution (bool)
|
||||||
|
- `envea`: Enable Envea gas sensors (bool)
|
||||||
|
- `windMeter`: Enable wind meter (bool)
|
||||||
|
- `BME280`: Enable BME280 sensor (bool)
|
||||||
|
- `MPPT`: Enable MPPT charger monitoring (bool)
|
||||||
|
- `NOISE`: Enable noise sensor (bool)
|
||||||
|
- `latitude_raw`, `longitude_raw`: GPS coordinates (float)
|
||||||
|
|
||||||
|
Configuration can be updated via web interface (launcher.php) or Python scripts in `sqlite/`.
|
||||||
|
|
||||||
|
## Data Transmission Protocols
|
||||||
|
|
||||||
|
### 1. UDP to Miotiq (Binary Protocol)
|
||||||
|
- 100-byte fixed binary payload via UDP socket
|
||||||
|
- Server: 192.168.0.20:4242
|
||||||
|
- Format: Device ID (8 bytes) + signal quality + sensor data (all packed as binary)
|
||||||
|
|
||||||
|
### 2. HTTP POST to AirCarto
|
||||||
|
- CSV payload sent as file attachment
|
||||||
|
- URL: `data.nebuleair.fr/pro_4G/data.php?sensor_id={id}&datetime={timestamp}`
|
||||||
|
- Uses SARA HTTP client (AT+UHTTPC profile 0)
|
||||||
|
|
||||||
|
### 3. HTTPS POST to uSpot
|
||||||
|
- JSON payload with SSL/TLS
|
||||||
|
- URL: `api-prod.uspot.probesys.net/nebuleair?token=2AFF6dQk68daFZ` (port 443)
|
||||||
|
- Uses SARA HTTPS client with certificate validation (AT+UHTTPC profile 1)
|
||||||
|
|
||||||
|
## Important Implementation Notes
|
||||||
|
|
||||||
|
### SARA R4/R5 Modem Communication
|
||||||
|
- The main transmission script (`loop/SARA_send_data_v2.py`) handles complex error recovery
|
||||||
|
- Error codes from AT+UHTTPER command trigger specific recovery actions:
|
||||||
|
- Error 4: Invalid hostname → Reset HTTP profile URL
|
||||||
|
- Error 11: Server connection error → Hardware modem reboot
|
||||||
|
- Error 22: PDP connection issue → Reset PSD/CSD connection (R5 only)
|
||||||
|
- Error 26/44: Timeout/connection lost → Send WiFi notification
|
||||||
|
- Error 73: SSL error → Re-import certificate and reset HTTPS profile
|
||||||
|
- Modem hardware reboot uses GPIO 16 (transistor control to cut power)
|
||||||
|
- Script waits 2 minutes after system boot before executing
|
||||||
|
|
||||||
|
### Serial Communication
|
||||||
|
- All UART ports must be enabled in `/boot/firmware/config.txt`
|
||||||
|
- Permissions reset after reboot, must be re-granted via cron @reboot
|
||||||
|
- Read functions use custom timeout logic to handle slow modem responses
|
||||||
|
- Special handling for multi-line AT command responses using `wait_for_lines` parameter
|
||||||
|
|
||||||
|
### Time Synchronization
|
||||||
|
- DS3231 RTC module maintains time when no network available
|
||||||
|
- RTC timestamp stored in SQLite (`timestamp_table`)
|
||||||
|
- Script compares server datetime (from HTTP headers) with RTC
|
||||||
|
- Auto-updates RTC if difference > 60 seconds
|
||||||
|
- Handles RTC reset condition (year 2000) and disconnection
|
||||||
|
|
||||||
|
### LED Indicators
|
||||||
|
- GPIO 23 (blue LED): Successful data transmission
|
||||||
|
- GPIO 24 (red LED): Transmission errors
|
||||||
|
- Uses thread locking to prevent GPIO conflicts
|
||||||
|
|
||||||
|
### Web Interface
|
||||||
|
- Apache DocumentRoot set to `/var/www/nebuleair_pro_4g`
|
||||||
|
- `html/launcher.php` provides REST-like API for all operations
|
||||||
|
- Uses shell_exec() to run Python scripts with proper sudo permissions
|
||||||
|
- Configuration updates modify SQLite database, not JSON files
|
||||||
|
|
||||||
|
### Security Considerations
|
||||||
|
- www-data user has sudo access to specific commands (defined in /etc/sudoers)
|
||||||
|
- Terminal command execution in launcher.php has whitelist of allowed commands
|
||||||
|
- No sensitive credentials should be committed (all in database/config files)
|
||||||
|
|
||||||
|
## Boot Sequence
|
||||||
|
|
||||||
|
1. Grant serial/I2C permissions (cron @reboot)
|
||||||
|
2. Check WiFi connection, start hotspot if needed (`boot_hotspot.sh`)
|
||||||
|
3. Start SARA modem initialization (`SARA/reboot/start.py`)
|
||||||
|
4. Systemd timers begin sensor data collection
|
||||||
|
5. SARA transmission loop begins after 2-minute delay
|
||||||
|
|
||||||
|
## Cron Jobs
|
||||||
|
|
||||||
|
Located in `/var/www/nebuleair_pro_4g/cron_jobs`:
|
||||||
|
- @reboot: Permissions setup, hotspot check, SARA initialization
|
||||||
|
- Daily 00:00: Truncate log files
|
||||||
275
MPPT/read.py
275
MPPT/read.py
@@ -1,11 +1,12 @@
|
|||||||
'''
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
__ __ ____ ____ _____
|
__ __ ____ ____ _____
|
||||||
| \/ | _ \| _ \_ _|
|
| \/ | _ \| _ \_ _|
|
||||||
| |\/| | |_) | |_) || |
|
| |\/| | |_) | |_) || |
|
||||||
| | | | __/| __/ | |
|
| | | | __/| __/ | |
|
||||||
|_| |_|_| |_| |_|
|
|_| |_|_| |_| |_|
|
||||||
|
|
||||||
Chargeur solaire Victron MPPT interface UART
|
MPPT Chargeur solaire Victron interface UART
|
||||||
|
|
||||||
MPPT connections
|
MPPT connections
|
||||||
5V / Rx / TX / GND
|
5V / Rx / TX / GND
|
||||||
@@ -13,107 +14,125 @@ RPI connection
|
|||||||
-- / GPIO9 / GPIO8 / GND
|
-- / GPIO9 / GPIO8 / GND
|
||||||
* pas besoin de connecter le 5V (le GND uniquement)
|
* pas besoin de connecter le 5V (le GND uniquement)
|
||||||
|
|
||||||
typical response from uart:
|
Fixed version - properly handles continuous data stream
|
||||||
|
"""
|
||||||
|
|
||||||
PID 0xA075 ->product ID
|
|
||||||
FW 164 ->firmware version
|
|
||||||
SER# HQ2249VJV9W ->serial num
|
|
||||||
|
|
||||||
V 13310 ->Battery voilatage in mV
|
|
||||||
I -130 ->Battery current in mA (negative means its discharging)
|
|
||||||
VPV 10 ->Solar Panel voltage
|
|
||||||
PPV 0 ->Solar Panel power (in W)
|
|
||||||
CS 0 ->Charger status:
|
|
||||||
0=off (no charging),
|
|
||||||
2=Bulk (Max current is being delivered to the battery),
|
|
||||||
3=Absorbtion (battery is nearly full,voltage is held constant.),
|
|
||||||
4=Float (Battery is fully charged, only maintaining charge)
|
|
||||||
MPPT 0 ->MPPT (Maximum Power Point Tracking) state: 0 = Off, 1 = Active, 2 = Not tracking
|
|
||||||
OR 0x00000001
|
|
||||||
ERR 0
|
|
||||||
LOAD ON
|
|
||||||
IL 100
|
|
||||||
H19 18 ->historical data (Total energy absorbed in kWh)
|
|
||||||
H20 0 -> Total energy discharged in kWh
|
|
||||||
H21 0
|
|
||||||
H22 9
|
|
||||||
H23 92
|
|
||||||
HSDS 19
|
|
||||||
Checksum u
|
|
||||||
|
|
||||||
sudo /usr/bin/python3 /var/www/nebuleair_pro_4g/MPPT/read.py
|
|
||||||
|
|
||||||
'''
|
|
||||||
import serial
|
import serial
|
||||||
import time
|
import time
|
||||||
import sqlite3
|
import sqlite3
|
||||||
|
import os
|
||||||
|
|
||||||
|
# ===== LOGGING CONFIGURATION =====
|
||||||
|
# Set to True to enable all print statements, False to run silently
|
||||||
|
DEBUG_MODE = False
|
||||||
|
|
||||||
|
# Alternative: Use environment variable (can be set in systemd service)
|
||||||
|
# DEBUG_MODE = os.environ.get('MPPT_DEBUG', 'false').lower() == 'true'
|
||||||
|
|
||||||
|
# Alternative: Check if running under systemd
|
||||||
|
# DEBUG_MODE = os.isatty(1) # True if running in terminal, False if systemd/cron
|
||||||
|
|
||||||
|
# Alternative: Use different log levels
|
||||||
|
# LOG_LEVEL = "ERROR" # Options: "DEBUG", "INFO", "ERROR", "NONE"
|
||||||
|
# =================================
|
||||||
|
|
||||||
# Connect to the SQLite database
|
# Connect to the SQLite database
|
||||||
conn = sqlite3.connect("/var/www/nebuleair_pro_4g/sqlite/sensors.db")
|
conn = sqlite3.connect("/var/www/nebuleair_pro_4g/sqlite/sensors.db")
|
||||||
cursor = conn.cursor()
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
# Logging function
|
||||||
|
def log(message, level="INFO"):
|
||||||
|
"""Print message only if DEBUG_MODE is True"""
|
||||||
|
if DEBUG_MODE:
|
||||||
|
print(message)
|
||||||
|
# Alternative: could write to a log file instead
|
||||||
|
# with open('/var/log/mppt.log', 'a') as f:
|
||||||
|
# f.write(f"{time.strftime('%Y-%m-%d %H:%M:%S')} [{level}] {message}\n")
|
||||||
|
|
||||||
def read_vedirect(port='/dev/ttyAMA4', baudrate=19200, timeout=20, max_attempts=3):
|
|
||||||
|
def read_vedirect(port='/dev/ttyAMA4', baudrate=19200, timeout=10):
|
||||||
"""
|
"""
|
||||||
Read and parse data from Victron MPPT controller with retry logic
|
Read and parse data from Victron MPPT controller
|
||||||
Returns parsed data as a dictionary or None if all attempts fail
|
Returns parsed data as a dictionary
|
||||||
"""
|
"""
|
||||||
required_keys = ['V', 'I', 'VPV', 'PPV', 'CS'] # Essential keys we need
|
required_keys = ['V', 'I', 'VPV', 'PPV', 'CS'] # Essential keys we need
|
||||||
|
|
||||||
for attempt in range(max_attempts):
|
try:
|
||||||
try:
|
log(f"Opening serial port {port} at {baudrate} baud...")
|
||||||
print(f"Attempt {attempt+1} of {max_attempts}...")
|
ser = serial.Serial(port, baudrate, timeout=1)
|
||||||
ser = serial.Serial(port, baudrate, timeout=1)
|
|
||||||
|
# Clear any buffered data
|
||||||
# Initialize data dictionary and tracking variables
|
ser.reset_input_buffer()
|
||||||
data = {}
|
time.sleep(0.5)
|
||||||
start_time = time.time()
|
|
||||||
|
# Initialize data dictionary
|
||||||
while time.time() - start_time < timeout:
|
data = {}
|
||||||
|
start_time = time.time()
|
||||||
|
lines_read = 0
|
||||||
|
blocks_seen = 0
|
||||||
|
|
||||||
|
while time.time() - start_time < timeout:
|
||||||
|
try:
|
||||||
line = ser.readline().decode('utf-8', errors='ignore').strip()
|
line = ser.readline().decode('utf-8', errors='ignore').strip()
|
||||||
|
|
||||||
if not line:
|
if not line:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Check if line contains a key-value pair
|
|
||||||
if '\t' in line:
|
|
||||||
key, value = line.split('\t', 1)
|
|
||||||
data[key] = value
|
|
||||||
print(f"{key}: {value}")
|
|
||||||
else:
|
|
||||||
print(f"Info: {line}")
|
|
||||||
|
|
||||||
# Check if we have a complete data block
|
lines_read += 1
|
||||||
if 'Checksum' in data:
|
|
||||||
|
# Check if this line contains tab-separated key-value pair
|
||||||
|
if '\t' in line:
|
||||||
|
parts = line.split('\t', 1)
|
||||||
|
if len(parts) == 2:
|
||||||
|
key, value = parts
|
||||||
|
data[key] = value
|
||||||
|
log(f"{key}: {value}")
|
||||||
|
|
||||||
|
# Check for checksum line (end of block)
|
||||||
|
elif line.startswith('Checksum'):
|
||||||
|
blocks_seen += 1
|
||||||
|
log(f"--- End of block {blocks_seen} ---")
|
||||||
|
|
||||||
# Check if we have all required keys
|
# Check if we have all required keys
|
||||||
missing_keys = [key for key in required_keys if key not in data]
|
missing_keys = [key for key in required_keys if key not in data]
|
||||||
|
|
||||||
if not missing_keys:
|
if not missing_keys:
|
||||||
|
log(f"✓ Complete data block received after {lines_read} lines!")
|
||||||
ser.close()
|
ser.close()
|
||||||
return data
|
return data
|
||||||
else:
|
else:
|
||||||
print(f"Incomplete data, missing: {', '.join(missing_keys)}")
|
log(f"Block {blocks_seen} incomplete, missing: {', '.join(missing_keys)}")
|
||||||
# Clear data and continue reading
|
# Don't clear data, maybe we missed the beginning of first block
|
||||||
data = {}
|
if blocks_seen > 1:
|
||||||
|
# If we've seen multiple blocks and still missing data,
|
||||||
# Timeout occurred
|
# something is wrong
|
||||||
print(f"Timeout on attempt {attempt+1}: Could not get complete data")
|
log("Multiple incomplete blocks, clearing data...")
|
||||||
ser.close()
|
data = {}
|
||||||
|
|
||||||
# Add small delay between attempts
|
|
||||||
if attempt < max_attempts - 1:
|
|
||||||
print("Waiting before next attempt...")
|
|
||||||
time.sleep(2)
|
|
||||||
|
|
||||||
except Exception as e:
|
except UnicodeDecodeError as e:
|
||||||
print(f"Error on attempt {attempt+1}: {e}")
|
log(f"Decode error: {e}", "ERROR")
|
||||||
try:
|
continue
|
||||||
ser.close()
|
except Exception as e:
|
||||||
except:
|
log(f"Error reading line: {e}", "ERROR")
|
||||||
pass
|
continue
|
||||||
|
|
||||||
|
# Timeout reached
|
||||||
|
log(f"Timeout after {timeout}s, read {lines_read} lines, saw {blocks_seen} blocks")
|
||||||
|
ser.close()
|
||||||
|
|
||||||
|
# If we have some data but not all required keys, return what we have
|
||||||
|
if data and len(data) >= len(required_keys) - 1:
|
||||||
|
log("Returning partial data...")
|
||||||
|
return data
|
||||||
|
|
||||||
|
except serial.SerialException as e:
|
||||||
|
log(f"Serial port error: {e}", "ERROR")
|
||||||
|
except Exception as e:
|
||||||
|
log(f"Unexpected error: {e}", "ERROR")
|
||||||
|
|
||||||
print("All attempts failed")
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def parse_values(data):
|
def parse_values(data):
|
||||||
"""Convert string values to appropriate types"""
|
"""Convert string values to appropriate types"""
|
||||||
if not data:
|
if not data:
|
||||||
@@ -135,13 +154,13 @@ def parse_values(data):
|
|||||||
'OR': str,
|
'OR': str,
|
||||||
'ERR': int,
|
'ERR': int,
|
||||||
'LOAD': str,
|
'LOAD': str,
|
||||||
'IL': int,
|
'IL': lambda x: float(x)/1000, # Convert mA to A
|
||||||
'H19': int, # Total energy absorbed in kWh
|
'H19': float, # Total energy absorbed in kWh (already in kWh)
|
||||||
'H20': int, # Total energy discharged in kWh
|
'H20': float, # Total energy discharged in kWh
|
||||||
'H21': int,
|
'H21': int, # Maximum power today (W)
|
||||||
'H22': int,
|
'H22': float, # Energy generated today (kWh)
|
||||||
'H23': int,
|
'H23': int, # Maximum power yesterday (W)
|
||||||
'HSDS': int
|
'HSDS': int # Day sequence number
|
||||||
}
|
}
|
||||||
|
|
||||||
# Convert values according to their type
|
# Convert values according to their type
|
||||||
@@ -149,18 +168,19 @@ def parse_values(data):
|
|||||||
if key in conversions:
|
if key in conversions:
|
||||||
try:
|
try:
|
||||||
parsed[key] = conversions[key](value)
|
parsed[key] = conversions[key](value)
|
||||||
except (ValueError, TypeError):
|
except (ValueError, TypeError) as e:
|
||||||
|
log(f"Conversion error for {key}={value}: {e}", "ERROR")
|
||||||
parsed[key] = value # Keep as string if conversion fails
|
parsed[key] = value # Keep as string if conversion fails
|
||||||
else:
|
else:
|
||||||
parsed[key] = value
|
parsed[key] = value
|
||||||
|
|
||||||
return parsed
|
return parsed
|
||||||
|
|
||||||
|
|
||||||
def get_charger_status(cs_value):
|
def get_charger_status(cs_value):
|
||||||
"""Convert CS numeric value to human-readable status"""
|
"""Convert CS numeric value to human-readable status"""
|
||||||
status_map = {
|
status_map = {
|
||||||
0: "Off",
|
0: "Off",
|
||||||
1: "Low power mode",
|
|
||||||
2: "Fault",
|
2: "Fault",
|
||||||
3: "Bulk",
|
3: "Bulk",
|
||||||
4: "Absorption",
|
4: "Absorption",
|
||||||
@@ -175,8 +195,22 @@ def get_charger_status(cs_value):
|
|||||||
}
|
}
|
||||||
return status_map.get(cs_value, f"Unknown ({cs_value})")
|
return status_map.get(cs_value, f"Unknown ({cs_value})")
|
||||||
|
|
||||||
|
|
||||||
|
def get_mppt_status(mppt_value):
|
||||||
|
"""Convert MPPT value to human-readable status"""
|
||||||
|
mppt_map = {
|
||||||
|
0: "Off",
|
||||||
|
1: "Voltage or current limited",
|
||||||
|
2: "MPP Tracker active"
|
||||||
|
}
|
||||||
|
return mppt_map.get(mppt_value, f"Unknown ({mppt_value})")
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
# Read data (with retry logic)
|
log("=== Victron MPPT Reader ===")
|
||||||
|
log(f"Started at: {time.strftime('%Y-%m-%d %H:%M:%S')}")
|
||||||
|
|
||||||
|
# Read data
|
||||||
raw_data = read_vedirect()
|
raw_data = read_vedirect()
|
||||||
|
|
||||||
if raw_data:
|
if raw_data:
|
||||||
@@ -184,42 +218,65 @@ if __name__ == "__main__":
|
|||||||
parsed_data = parse_values(raw_data)
|
parsed_data = parse_values(raw_data)
|
||||||
|
|
||||||
if parsed_data:
|
if parsed_data:
|
||||||
# Check if we have valid battery voltage
|
# Display summary
|
||||||
if parsed_data.get('V', 0) > 0:
|
log("\n===== MPPT Status Summary =====")
|
||||||
print("\n===== MPPT Summary =====")
|
log(f"Product: {parsed_data.get('PID', 'Unknown')} (FW: {parsed_data.get('FW', '?')})")
|
||||||
print(f"Battery: {parsed_data.get('V', 0):.2f}V, {parsed_data.get('I', 0):.2f}A")
|
log(f"Serial: {parsed_data.get('SER#', 'Unknown')}")
|
||||||
print(f"Solar: {parsed_data.get('VPV', 0):.2f}V, {parsed_data.get('PPV', 0)}W")
|
log(f"\nBattery: {parsed_data.get('V', 0):.2f}V, {parsed_data.get('I', 0):.2f}A")
|
||||||
print(f"Charger status: {get_charger_status(parsed_data.get('CS', 0))}")
|
log(f"Solar Panel: {parsed_data.get('VPV', 0):.2f}V, {parsed_data.get('PPV', 0)}W")
|
||||||
|
log(f"Charger Status: {get_charger_status(parsed_data.get('CS', 0))}")
|
||||||
# Save to SQLite
|
log(f"MPPT Status: {get_mppt_status(parsed_data.get('MPPT', 0))}")
|
||||||
cursor.execute("SELECT * FROM timestamp_table LIMIT 1")
|
log(f"Load Output: {parsed_data.get('LOAD', 'Unknown')}, {parsed_data.get('IL', 0):.2f}A")
|
||||||
row = cursor.fetchone()
|
log(f"\nToday's Energy: {parsed_data.get('H22', 0)}kWh (Max: {parsed_data.get('H21', 0)}W)")
|
||||||
rtc_time_str = row[1]
|
log(f"Total Energy: {parsed_data.get('H19', 0)}kWh")
|
||||||
|
|
||||||
# Extract values
|
# Validate critical values
|
||||||
battery_voltage = parsed_data.get('V', 0)
|
battery_voltage = parsed_data.get('V', 0)
|
||||||
|
|
||||||
|
if battery_voltage > 0:
|
||||||
|
# Get timestamp
|
||||||
|
try:
|
||||||
|
cursor.execute("SELECT * FROM timestamp_table LIMIT 1")
|
||||||
|
row = cursor.fetchone()
|
||||||
|
rtc_time_str = row[1] if row else time.strftime('%Y-%m-%d %H:%M:%S')
|
||||||
|
except:
|
||||||
|
rtc_time_str = time.strftime('%Y-%m-%d %H:%M:%S')
|
||||||
|
|
||||||
|
# Extract values for database
|
||||||
battery_current = parsed_data.get('I', 0)
|
battery_current = parsed_data.get('I', 0)
|
||||||
solar_voltage = parsed_data.get('VPV', 0)
|
solar_voltage = parsed_data.get('VPV', 0)
|
||||||
solar_power = parsed_data.get('PPV', 0)
|
solar_power = parsed_data.get('PPV', 0)
|
||||||
charger_status = parsed_data.get('CS', 0)
|
charger_status = parsed_data.get('CS', 0)
|
||||||
|
|
||||||
|
# Save to database
|
||||||
try:
|
try:
|
||||||
cursor.execute('''
|
cursor.execute('''
|
||||||
INSERT INTO data_MPPT (timestamp, battery_voltage, battery_current, solar_voltage, solar_power, charger_status)
|
INSERT INTO data_MPPT (timestamp, battery_voltage, battery_current, solar_voltage, solar_power, charger_status)
|
||||||
VALUES (?, ?, ?, ?, ?, ?)''',
|
VALUES (?, ?, ?, ?, ?, ?)''',
|
||||||
(rtc_time_str, battery_voltage, battery_current, solar_voltage, solar_power, charger_status))
|
(rtc_time_str, battery_voltage, battery_current, solar_voltage, solar_power, charger_status))
|
||||||
|
|
||||||
conn.commit()
|
conn.commit()
|
||||||
print("MPPT data saved successfully!")
|
log(f"\n✓ Data saved to database at {rtc_time_str}")
|
||||||
|
|
||||||
except Exception as e:
|
except sqlite3.Error as e:
|
||||||
print(f"Database error: {e}")
|
# Always log database errors regardless of DEBUG_MODE
|
||||||
|
if not DEBUG_MODE:
|
||||||
|
print(f"Database error: {e}")
|
||||||
|
else:
|
||||||
|
log(f"\n✗ Database error: {e}", "ERROR")
|
||||||
|
conn.rollback()
|
||||||
else:
|
else:
|
||||||
print("Invalid data: Battery voltage is zero or missing")
|
log("\n✗ Invalid data: Battery voltage is zero or missing", "ERROR")
|
||||||
else:
|
else:
|
||||||
print("Failed to parse data")
|
log("\n✗ Failed to parse data", "ERROR")
|
||||||
else:
|
else:
|
||||||
print("No valid data received from MPPT controller")
|
log("\n✗ No valid data received from MPPT controller", "ERROR")
|
||||||
|
log("\nPossible issues:")
|
||||||
|
log("- Check serial connection (TX/RX/GND)")
|
||||||
|
log("- Verify port is /dev/ttyAMA4")
|
||||||
|
log("- Ensure MPPT is powered on")
|
||||||
|
log("- Check baudrate (should be 19200)")
|
||||||
|
|
||||||
# Always close the connection
|
# Always close the connection
|
||||||
conn.close()
|
conn.close()
|
||||||
|
log("\nDone.")
|
||||||
@@ -14,9 +14,9 @@ import serial
|
|||||||
import requests
|
import requests
|
||||||
import json
|
import json
|
||||||
import sys
|
import sys
|
||||||
|
import time
|
||||||
|
|
||||||
parameter = sys.argv[1:] # Exclude the script name
|
parameter = sys.argv[1:] # Exclude the script name
|
||||||
#print("Parameters received:")
|
|
||||||
port='/dev/'+parameter[0]
|
port='/dev/'+parameter[0]
|
||||||
|
|
||||||
ser = serial.Serial(
|
ser = serial.Serial(
|
||||||
@@ -34,42 +34,93 @@ ser.write(b'\x81\x11\x6E') #data10s
|
|||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
byte_data = ser.readline()
|
byte_data = ser.readline()
|
||||||
#print(byte_data)
|
|
||||||
|
# Convert raw data to hex string for debugging
|
||||||
|
raw_hex = byte_data.hex() if byte_data else ""
|
||||||
|
|
||||||
|
# Check if we received data
|
||||||
|
if not byte_data or len(byte_data) < 15:
|
||||||
|
data = {
|
||||||
|
'PM1': 0.0,
|
||||||
|
'PM25': 0.0,
|
||||||
|
'PM10': 0.0,
|
||||||
|
'sleep': 0,
|
||||||
|
'degradedState': 0,
|
||||||
|
'notReady': 0,
|
||||||
|
'heatError': 0,
|
||||||
|
't_rhError': 0,
|
||||||
|
'fanError': 0,
|
||||||
|
'memoryError': 0,
|
||||||
|
'laserError': 0,
|
||||||
|
'raw': raw_hex,
|
||||||
|
'message': f"No data received or incomplete frame (length: {len(byte_data)})"
|
||||||
|
}
|
||||||
|
json_data = json.dumps(data)
|
||||||
|
print(json_data)
|
||||||
|
break
|
||||||
|
|
||||||
stateByte = int.from_bytes(byte_data[2:3], byteorder='big')
|
stateByte = int.from_bytes(byte_data[2:3], byteorder='big')
|
||||||
Statebits = [int(bit) for bit in bin(stateByte)[2:].zfill(8)]
|
Statebits = [int(bit) for bit in bin(stateByte)[2:].zfill(8)]
|
||||||
PM1 = int.from_bytes(byte_data[9:11], byteorder='big')/10
|
PM1 = int.from_bytes(byte_data[9:11], byteorder='big')/10
|
||||||
PM25 = int.from_bytes(byte_data[11:13], byteorder='big')/10
|
PM25 = int.from_bytes(byte_data[11:13], byteorder='big')/10
|
||||||
PM10 = int.from_bytes(byte_data[13:15], byteorder='big')/10
|
PM10 = int.from_bytes(byte_data[13:15], byteorder='big')/10
|
||||||
#print(f"State: {Statebits}")
|
|
||||||
#print(f"PM1: {PM1}")
|
# Create JSON with raw data and status message
|
||||||
#print(f"PM25: {PM25}")
|
|
||||||
#print(f"PM10: {PM10}")
|
|
||||||
#create JSON
|
|
||||||
data = {
|
data = {
|
||||||
'capteurID': 'nebuleairpro1',
|
|
||||||
'sondeID':'USB2',
|
|
||||||
'PM1': PM1,
|
'PM1': PM1,
|
||||||
'PM25': PM25,
|
'PM25': PM25,
|
||||||
'PM10': PM10,
|
'PM10': PM10,
|
||||||
'sleep' : Statebits[0],
|
'sleep': Statebits[0],
|
||||||
'degradedState' : Statebits[1],
|
'degradedState': Statebits[1],
|
||||||
'notReady' : Statebits[2],
|
'notReady': Statebits[2],
|
||||||
'heatError' : Statebits[3],
|
'heatError': Statebits[3],
|
||||||
't_rhError' : Statebits[4],
|
't_rhError': Statebits[4],
|
||||||
'fanError' : Statebits[5],
|
'fanError': Statebits[5],
|
||||||
'memoryError' : Statebits[6],
|
'memoryError': Statebits[6],
|
||||||
'laserError' : Statebits[7]
|
'laserError': Statebits[7],
|
||||||
|
'raw': raw_hex,
|
||||||
|
'message': 'OK' if sum(Statebits[1:]) == 0 else 'Sensor error detected'
|
||||||
}
|
}
|
||||||
json_data = json.dumps(data)
|
json_data = json.dumps(data)
|
||||||
print(json_data)
|
print(json_data)
|
||||||
break
|
break
|
||||||
|
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
print("User interrupt encountered. Exiting...")
|
data = {
|
||||||
|
'PM1': 0.0,
|
||||||
|
'PM25': 0.0,
|
||||||
|
'PM10': 0.0,
|
||||||
|
'sleep': 0,
|
||||||
|
'degradedState': 0,
|
||||||
|
'notReady': 0,
|
||||||
|
'heatError': 0,
|
||||||
|
't_rhError': 0,
|
||||||
|
'fanError': 0,
|
||||||
|
'memoryError': 0,
|
||||||
|
'laserError': 0,
|
||||||
|
'raw': '',
|
||||||
|
'message': 'User interrupt encountered'
|
||||||
|
}
|
||||||
|
print(json.dumps(data))
|
||||||
time.sleep(3)
|
time.sleep(3)
|
||||||
exit()
|
exit()
|
||||||
except:
|
|
||||||
# for all other kinds of error, but not specifying which one
|
except Exception as e:
|
||||||
print("Unknown error...")
|
data = {
|
||||||
|
'PM1': 0.0,
|
||||||
|
'PM25': 0.0,
|
||||||
|
'PM10': 0.0,
|
||||||
|
'sleep': 0,
|
||||||
|
'degradedState': 0,
|
||||||
|
'notReady': 0,
|
||||||
|
'heatError': 0,
|
||||||
|
't_rhError': 0,
|
||||||
|
'fanError': 0,
|
||||||
|
'memoryError': 0,
|
||||||
|
'laserError': 0,
|
||||||
|
'raw': '',
|
||||||
|
'message': f'Error: {str(e)}'
|
||||||
|
}
|
||||||
|
print(json.dumps(data))
|
||||||
time.sleep(3)
|
time.sleep(3)
|
||||||
exit()
|
exit()
|
||||||
|
|
||||||
177
NPM/get_data_modbus_v2_1.py
Normal file
177
NPM/get_data_modbus_v2_1.py
Normal file
@@ -0,0 +1,177 @@
|
|||||||
|
'''
|
||||||
|
_ _ ____ __ __
|
||||||
|
| \ | | _ \| \/ |
|
||||||
|
| \| | |_) | |\/| |
|
||||||
|
| |\ | __/| | | |
|
||||||
|
|_| \_|_| |_| |_|
|
||||||
|
|
||||||
|
Script to get NPM data via Modbus
|
||||||
|
|
||||||
|
Improved version with data stream lenght check
|
||||||
|
|
||||||
|
/usr/bin/python3 /var/www/nebuleair_pro_4g/NPM/get_data_modbus_v3.py
|
||||||
|
|
||||||
|
Modbus RTU
|
||||||
|
[Slave Address][Function Code][Starting Address][Quantity of Registers][CRC]
|
||||||
|
|
||||||
|
Pour récupérer
|
||||||
|
les concentrations en PM1, PM10 et PM2.5 (a partir du registre 0x38)
|
||||||
|
les 5 cannaux
|
||||||
|
la température et l'humidité à l'intérieur du capteur
|
||||||
|
Donnée actualisée toutes les 10 secondes
|
||||||
|
|
||||||
|
Request
|
||||||
|
\x01\x03\x00\x38\x00\x55\...\...
|
||||||
|
|
||||||
|
\x01 Slave Address (slave device address)
|
||||||
|
\x03 Function code (read multiple holding registers)
|
||||||
|
\x00\x38 Starting Address (The request starts reading from holding register address x38 or 56)
|
||||||
|
\x00\x55 Quantity of Registers (Requests to read x55 or 85 consecutive registers starting from address 56)
|
||||||
|
\...\... Cyclic Redundancy Check (checksum )
|
||||||
|
|
||||||
|
'''
|
||||||
|
import serial
|
||||||
|
import requests
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
import crcmod
|
||||||
|
import sqlite3
|
||||||
|
import time
|
||||||
|
|
||||||
|
# Connect to the SQLite database
|
||||||
|
conn = sqlite3.connect("/var/www/nebuleair_pro_4g/sqlite/sensors.db")
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
def load_config(config_file):
|
||||||
|
try:
|
||||||
|
with open(config_file, 'r') as file:
|
||||||
|
config_data = json.load(file)
|
||||||
|
return config_data
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error loading config file: {e}")
|
||||||
|
return {}
|
||||||
|
|
||||||
|
# Load the configuration data
|
||||||
|
npm_solo_port = "/dev/ttyAMA5" #port du NPM solo
|
||||||
|
|
||||||
|
#GET RTC TIME from SQlite
|
||||||
|
cursor.execute("SELECT * FROM timestamp_table LIMIT 1")
|
||||||
|
row = cursor.fetchone() # Get the first (and only) row
|
||||||
|
rtc_time_str = row[1] # '2025-02-07 12:30:45'
|
||||||
|
|
||||||
|
# Initialize serial port
|
||||||
|
ser = serial.Serial(
|
||||||
|
port=npm_solo_port,
|
||||||
|
baudrate=115200,
|
||||||
|
parity=serial.PARITY_EVEN,
|
||||||
|
stopbits=serial.STOPBITS_ONE,
|
||||||
|
bytesize=serial.EIGHTBITS,
|
||||||
|
timeout=2
|
||||||
|
)
|
||||||
|
|
||||||
|
# Define Modbus CRC-16 function
|
||||||
|
crc16 = crcmod.predefined.mkPredefinedCrcFun('modbus')
|
||||||
|
|
||||||
|
# Request frame without CRC
|
||||||
|
data = b'\x01\x03\x00\x38\x00\x55'
|
||||||
|
|
||||||
|
# Calculate and append CRC
|
||||||
|
crc = crc16(data)
|
||||||
|
crc_low = crc & 0xFF
|
||||||
|
crc_high = (crc >> 8) & 0xFF
|
||||||
|
request = data + bytes([crc_low, crc_high])
|
||||||
|
|
||||||
|
# Clear serial buffer before sending
|
||||||
|
ser.flushInput()
|
||||||
|
|
||||||
|
# Send request
|
||||||
|
ser.write(request)
|
||||||
|
time.sleep(0.2) # Wait for sensor to respond
|
||||||
|
|
||||||
|
# Read response
|
||||||
|
response_length = 2 + 1 + (2 * 85) + 2 # Address + Function + Data + CRC
|
||||||
|
byte_data = ser.read(response_length)
|
||||||
|
|
||||||
|
# Validate response length
|
||||||
|
if len(byte_data) < response_length:
|
||||||
|
print("[ERROR] Incomplete response received:", byte_data.hex())
|
||||||
|
exit()
|
||||||
|
|
||||||
|
# Verify CRC
|
||||||
|
received_crc = int.from_bytes(byte_data[-2:], byteorder='little')
|
||||||
|
calculated_crc = crc16(byte_data[:-2])
|
||||||
|
|
||||||
|
if received_crc != calculated_crc:
|
||||||
|
print("[ERROR] CRC check failed! Corrupted data received.")
|
||||||
|
exit()
|
||||||
|
|
||||||
|
# Convert response to hex for debugging
|
||||||
|
formatted = ''.join(f'\\x{byte:02x}' for byte in byte_data)
|
||||||
|
#print("Response:", formatted)
|
||||||
|
|
||||||
|
# Extract and print PM values
|
||||||
|
def extract_value(byte_data, register, scale=1, single_register=False, round_to=None):
|
||||||
|
REGISTER_START = 56
|
||||||
|
offset = (register - REGISTER_START) * 2 + 3
|
||||||
|
|
||||||
|
if single_register:
|
||||||
|
value = int.from_bytes(byte_data[offset:offset+2], byteorder='big')
|
||||||
|
else:
|
||||||
|
lsw = int.from_bytes(byte_data[offset:offset+2], byteorder='big')
|
||||||
|
msw = int.from_bytes(byte_data[offset+2:offset+4], byteorder='big')
|
||||||
|
value = (msw << 16) | lsw
|
||||||
|
|
||||||
|
value = value / scale
|
||||||
|
|
||||||
|
if round_to == 0:
|
||||||
|
return int(value)
|
||||||
|
elif round_to is not None:
|
||||||
|
return round(value, round_to)
|
||||||
|
else:
|
||||||
|
return value
|
||||||
|
|
||||||
|
pm1_10s = extract_value(byte_data, 56, 1000, round_to=1)
|
||||||
|
pm25_10s = extract_value(byte_data, 58, 1000, round_to=1)
|
||||||
|
pm10_10s = extract_value(byte_data, 60, 1000, round_to=1)
|
||||||
|
|
||||||
|
#print("10 sec concentration:")
|
||||||
|
#print(f"PM1: {pm1_10s}")
|
||||||
|
#print(f"PM2.5: {pm25_10s}")
|
||||||
|
#print(f"PM10: {pm10_10s}")
|
||||||
|
|
||||||
|
# Extract values for 5 channels
|
||||||
|
channel_1 = extract_value(byte_data, 128, round_to=0) # 0.2 - 0.5μm
|
||||||
|
channel_2 = extract_value(byte_data, 130, round_to=0) # 0.5 - 1.0μm
|
||||||
|
channel_3 = extract_value(byte_data, 132, round_to=0) # 1.0 - 2.5μm
|
||||||
|
channel_4 = extract_value(byte_data, 134, round_to=0) # 2.5 - 5.0μm
|
||||||
|
channel_5 = extract_value(byte_data, 136, round_to=0) # 5.0 - 10.0μm
|
||||||
|
|
||||||
|
#print(f"Channel 1 (0.2->0.5): {channel_1}")
|
||||||
|
#print(f"Channel 2 (0.5->1.0): {channel_2}")
|
||||||
|
#print(f"Channel 3 (1.0->2.5): {channel_3}")
|
||||||
|
#print(f"Channel 4 (2.5->5.0): {channel_4}")
|
||||||
|
#print(f"Channel 5 (5.0->10.): {channel_5}")
|
||||||
|
|
||||||
|
|
||||||
|
# Retrieve relative humidity from register 106 (0x6A)
|
||||||
|
relative_humidity = extract_value(byte_data, 106, 100, single_register=True)
|
||||||
|
# Retrieve temperature from register 106 (0x6A)
|
||||||
|
temperature = extract_value(byte_data, 107, 100, single_register=True)
|
||||||
|
|
||||||
|
#print(f"Internal Relative Humidity: {relative_humidity} %")
|
||||||
|
#print(f"Internal temperature: {temperature} °C")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
cursor.execute('''
|
||||||
|
INSERT INTO data_NPM_5channels (timestamp,PM_ch1, PM_ch2, PM_ch3, PM_ch4, PM_ch5) VALUES (?,?,?,?,?,?)'''
|
||||||
|
, (rtc_time_str,channel_1,channel_2,channel_3,channel_4,channel_5))
|
||||||
|
|
||||||
|
cursor.execute('''
|
||||||
|
INSERT INTO data_NPM (timestamp,PM1, PM25, PM10, temp_npm, hum_npm) VALUES (?,?,?,?,?,?)'''
|
||||||
|
, (rtc_time_str,pm1_10s,pm25_10s,pm10_10s,temperature,relative_humidity ))
|
||||||
|
|
||||||
|
# Commit and close the connection
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
conn.close()
|
||||||
@@ -29,6 +29,8 @@ Request
|
|||||||
\x00\x55 Quantity of Registers (Requests to read x55 or 85 consecutive registers starting from address 56)
|
\x00\x55 Quantity of Registers (Requests to read x55 or 85 consecutive registers starting from address 56)
|
||||||
\...\... Cyclic Redundancy Check (checksum )
|
\...\... Cyclic Redundancy Check (checksum )
|
||||||
|
|
||||||
|
MAJ 2026 --> renvoie des 0 si pas de réponse du NPM
|
||||||
|
|
||||||
'''
|
'''
|
||||||
import serial
|
import serial
|
||||||
import requests
|
import requests
|
||||||
@@ -59,119 +61,137 @@ cursor.execute("SELECT * FROM timestamp_table LIMIT 1")
|
|||||||
row = cursor.fetchone() # Get the first (and only) row
|
row = cursor.fetchone() # Get the first (and only) row
|
||||||
rtc_time_str = row[1] # '2025-02-07 12:30:45'
|
rtc_time_str = row[1] # '2025-02-07 12:30:45'
|
||||||
|
|
||||||
# Initialize serial port
|
# Initialize default error values
|
||||||
ser = serial.Serial(
|
pm1_10s = 0
|
||||||
port=npm_solo_port,
|
pm25_10s = 0
|
||||||
baudrate=115200,
|
pm10_10s = 0
|
||||||
parity=serial.PARITY_EVEN,
|
channel_1 = 0
|
||||||
stopbits=serial.STOPBITS_ONE,
|
channel_2 = 0
|
||||||
bytesize=serial.EIGHTBITS,
|
channel_3 = 0
|
||||||
timeout=2
|
channel_4 = 0
|
||||||
)
|
channel_5 = 0
|
||||||
|
relative_humidity = 0
|
||||||
|
temperature = 0
|
||||||
|
|
||||||
# Define Modbus CRC-16 function
|
try:
|
||||||
crc16 = crcmod.predefined.mkPredefinedCrcFun('modbus')
|
# Initialize serial port
|
||||||
|
ser = serial.Serial(
|
||||||
|
port=npm_solo_port,
|
||||||
|
baudrate=115200,
|
||||||
|
parity=serial.PARITY_EVEN,
|
||||||
|
stopbits=serial.STOPBITS_ONE,
|
||||||
|
bytesize=serial.EIGHTBITS,
|
||||||
|
timeout=2
|
||||||
|
)
|
||||||
|
|
||||||
# Request frame without CRC
|
# Define Modbus CRC-16 function
|
||||||
data = b'\x01\x03\x00\x38\x00\x55'
|
crc16 = crcmod.predefined.mkPredefinedCrcFun('modbus')
|
||||||
|
|
||||||
# Calculate and append CRC
|
# Request frame without CRC
|
||||||
crc = crc16(data)
|
data = b'\x01\x03\x00\x38\x00\x55'
|
||||||
crc_low = crc & 0xFF
|
|
||||||
crc_high = (crc >> 8) & 0xFF
|
|
||||||
request = data + bytes([crc_low, crc_high])
|
|
||||||
|
|
||||||
# Clear serial buffer before sending
|
# Calculate and append CRC
|
||||||
ser.flushInput()
|
crc = crc16(data)
|
||||||
|
crc_low = crc & 0xFF
|
||||||
|
crc_high = (crc >> 8) & 0xFF
|
||||||
|
request = data + bytes([crc_low, crc_high])
|
||||||
|
|
||||||
# Send request
|
# Clear serial buffer before sending
|
||||||
ser.write(request)
|
ser.flushInput()
|
||||||
time.sleep(0.2) # Wait for sensor to respond
|
|
||||||
|
|
||||||
# Read response
|
# Send request
|
||||||
response_length = 2 + 1 + (2 * 85) + 2 # Address + Function + Data + CRC
|
ser.write(request)
|
||||||
byte_data = ser.read(response_length)
|
time.sleep(0.2) # Wait for sensor to respond
|
||||||
|
|
||||||
# Validate response length
|
# Read response
|
||||||
if len(byte_data) < response_length:
|
response_length = 2 + 1 + (2 * 85) + 2 # Address + Function + Data + CRC
|
||||||
print("[ERROR] Incomplete response received:", byte_data.hex())
|
byte_data = ser.read(response_length)
|
||||||
exit()
|
|
||||||
|
|
||||||
# Verify CRC
|
# Validate response length
|
||||||
received_crc = int.from_bytes(byte_data[-2:], byteorder='little')
|
if len(byte_data) < response_length:
|
||||||
calculated_crc = crc16(byte_data[:-2])
|
print(f"[ERROR] Incomplete response received: {byte_data.hex()}")
|
||||||
|
raise Exception("Incomplete response")
|
||||||
|
|
||||||
if received_crc != calculated_crc:
|
# Verify CRC
|
||||||
print("[ERROR] CRC check failed! Corrupted data received.")
|
received_crc = int.from_bytes(byte_data[-2:], byteorder='little')
|
||||||
exit()
|
calculated_crc = crc16(byte_data[:-2])
|
||||||
|
|
||||||
# Convert response to hex for debugging
|
if received_crc != calculated_crc:
|
||||||
formatted = ''.join(f'\\x{byte:02x}' for byte in byte_data)
|
print("[ERROR] CRC check failed! Corrupted data received.")
|
||||||
#print("Response:", formatted)
|
raise Exception("CRC check failed")
|
||||||
|
|
||||||
# Extract and print PM values
|
# Convert response to hex for debugging
|
||||||
def extract_value(byte_data, register, scale=1, single_register=False, round_to=None):
|
formatted = ''.join(f'\\x{byte:02x}' for byte in byte_data)
|
||||||
REGISTER_START = 56
|
#print("Response:", formatted)
|
||||||
offset = (register - REGISTER_START) * 2 + 3
|
|
||||||
|
|
||||||
if single_register:
|
# Extract and print PM values
|
||||||
value = int.from_bytes(byte_data[offset:offset+2], byteorder='big')
|
def extract_value(byte_data, register, scale=1, single_register=False, round_to=None):
|
||||||
else:
|
REGISTER_START = 56
|
||||||
lsw = int.from_bytes(byte_data[offset:offset+2], byteorder='big')
|
offset = (register - REGISTER_START) * 2 + 3
|
||||||
msw = int.from_bytes(byte_data[offset+2:offset+4], byteorder='big')
|
|
||||||
value = (msw << 16) | lsw
|
|
||||||
|
|
||||||
value = value / scale
|
if single_register:
|
||||||
|
value = int.from_bytes(byte_data[offset:offset+2], byteorder='big')
|
||||||
|
else:
|
||||||
|
lsw = int.from_bytes(byte_data[offset:offset+2], byteorder='big')
|
||||||
|
msw = int.from_bytes(byte_data[offset+2:offset+4], byteorder='big')
|
||||||
|
value = (msw << 16) | lsw
|
||||||
|
|
||||||
if round_to == 0:
|
value = value / scale
|
||||||
return int(value)
|
|
||||||
elif round_to is not None:
|
|
||||||
return round(value, round_to)
|
|
||||||
else:
|
|
||||||
return value
|
|
||||||
|
|
||||||
pm1_10s = extract_value(byte_data, 56, 1000, round_to=1)
|
if round_to == 0:
|
||||||
pm25_10s = extract_value(byte_data, 58, 1000, round_to=1)
|
return int(value)
|
||||||
pm10_10s = extract_value(byte_data, 60, 1000, round_to=1)
|
elif round_to is not None:
|
||||||
|
return round(value, round_to)
|
||||||
|
else:
|
||||||
|
return value
|
||||||
|
|
||||||
#print("10 sec concentration:")
|
pm1_10s = extract_value(byte_data, 56, 1000, round_to=1)
|
||||||
#print(f"PM1: {pm1_10s}")
|
pm25_10s = extract_value(byte_data, 58, 1000, round_to=1)
|
||||||
#print(f"PM2.5: {pm25_10s}")
|
pm10_10s = extract_value(byte_data, 60, 1000, round_to=1)
|
||||||
#print(f"PM10: {pm10_10s}")
|
|
||||||
|
|
||||||
# Extract values for 5 channels
|
#print("10 sec concentration:")
|
||||||
channel_1 = extract_value(byte_data, 128, round_to=0) # 0.2 - 0.5μm
|
#print(f"PM1: {pm1_10s}")
|
||||||
channel_2 = extract_value(byte_data, 130, round_to=0) # 0.5 - 1.0μm
|
#print(f"PM2.5: {pm25_10s}")
|
||||||
channel_3 = extract_value(byte_data, 132, round_to=0) # 1.0 - 2.5μm
|
#print(f"PM10: {pm10_10s}")
|
||||||
channel_4 = extract_value(byte_data, 134, round_to=0) # 2.5 - 5.0μm
|
|
||||||
channel_5 = extract_value(byte_data, 136, round_to=0) # 5.0 - 10.0μm
|
|
||||||
|
|
||||||
#print(f"Channel 1 (0.2->0.5): {channel_1}")
|
# Extract values for 5 channels
|
||||||
#print(f"Channel 2 (0.5->1.0): {channel_2}")
|
channel_1 = extract_value(byte_data, 128, round_to=0) # 0.2 - 0.5μm
|
||||||
#print(f"Channel 3 (1.0->2.5): {channel_3}")
|
channel_2 = extract_value(byte_data, 130, round_to=0) # 0.5 - 1.0μm
|
||||||
#print(f"Channel 4 (2.5->5.0): {channel_4}")
|
channel_3 = extract_value(byte_data, 132, round_to=0) # 1.0 - 2.5μm
|
||||||
#print(f"Channel 5 (5.0->10.): {channel_5}")
|
channel_4 = extract_value(byte_data, 134, round_to=0) # 2.5 - 5.0μm
|
||||||
|
channel_5 = extract_value(byte_data, 136, round_to=0) # 5.0 - 10.0μm
|
||||||
|
|
||||||
|
#print(f"Channel 1 (0.2->0.5): {channel_1}")
|
||||||
# Retrieve relative humidity from register 106 (0x6A)
|
#print(f"Channel 2 (0.5->1.0): {channel_2}")
|
||||||
relative_humidity = extract_value(byte_data, 106, 100, single_register=True)
|
#print(f"Channel 3 (1.0->2.5): {channel_3}")
|
||||||
# Retrieve temperature from register 106 (0x6A)
|
#print(f"Channel 4 (2.5->5.0): {channel_4}")
|
||||||
temperature = extract_value(byte_data, 107, 100, single_register=True)
|
#print(f"Channel 5 (5.0->10.): {channel_5}")
|
||||||
|
|
||||||
#print(f"Internal Relative Humidity: {relative_humidity} %")
|
|
||||||
#print(f"Internal temperature: {temperature} °C")
|
# Retrieve relative humidity from register 106 (0x6A)
|
||||||
|
relative_humidity = extract_value(byte_data, 106, 100, single_register=True)
|
||||||
|
# Retrieve temperature from register 106 (0x6A)
|
||||||
|
temperature = extract_value(byte_data, 107, 100, single_register=True)
|
||||||
|
|
||||||
|
#print(f"Internal Relative Humidity: {relative_humidity} %")
|
||||||
|
#print(f"Internal temperature: {temperature} °C")
|
||||||
|
|
||||||
|
ser.close()
|
||||||
|
|
||||||
cursor.execute('''
|
except Exception as e:
|
||||||
INSERT INTO data_NPM_5channels (timestamp,PM_ch1, PM_ch2, PM_ch3, PM_ch4, PM_ch5) VALUES (?,?,?,?,?,?)'''
|
print(f"[ERROR] Sensor communication failed: {e}")
|
||||||
, (rtc_time_str,channel_1,channel_2,channel_3,channel_4,channel_5))
|
# Variables already set to -1 at the beginning
|
||||||
|
|
||||||
cursor.execute('''
|
finally:
|
||||||
INSERT INTO data_NPM (timestamp,PM1, PM25, PM10, temp_npm, hum_npm) VALUES (?,?,?,?,?,?)'''
|
# Always save data to database, even if all values are -1
|
||||||
, (rtc_time_str,pm1_10s,pm25_10s,pm10_10s,temperature,relative_humidity ))
|
cursor.execute('''
|
||||||
|
INSERT INTO data_NPM_5channels (timestamp,PM_ch1, PM_ch2, PM_ch3, PM_ch4, PM_ch5) VALUES (?,?,?,?,?,?)'''
|
||||||
|
, (rtc_time_str, channel_1, channel_2, channel_3, channel_4, channel_5))
|
||||||
|
|
||||||
# Commit and close the connection
|
cursor.execute('''
|
||||||
conn.commit()
|
INSERT INTO data_NPM (timestamp,PM1, PM25, PM10, temp_npm, hum_npm) VALUES (?,?,?,?,?,?)'''
|
||||||
|
, (rtc_time_str, pm1_10s, pm25_10s, pm10_10s, temperature, relative_humidity))
|
||||||
|
|
||||||
conn.close()
|
# Commit and close the connection
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
26
README.md
26
README.md
@@ -29,7 +29,7 @@ Line by line installation.
|
|||||||
```
|
```
|
||||||
sudo apt update
|
sudo apt update
|
||||||
sudo apt install git gh apache2 sqlite3 php php-sqlite3 python3 python3-pip jq autossh i2c-tools python3-smbus -y
|
sudo apt install git gh apache2 sqlite3 php php-sqlite3 python3 python3-pip jq autossh i2c-tools python3-smbus -y
|
||||||
sudo pip3 install pyserial requests RPi.GPIO adafruit-circuitpython-bme280 crcmod psutil ntplib pytz gpiozero adafruit-circuitpython-ads1x15 numpy --break-system-packages
|
sudo pip3 install pyserial requests RPi.GPIO adafruit-circuitpython-bme280 crcmod psutil ntplib pytz gpiozero adafruit-circuitpython-ads1x15 numpy nsrt-mk3-dev --break-system-packages
|
||||||
sudo mkdir -p /var/www/.ssh
|
sudo mkdir -p /var/www/.ssh
|
||||||
sudo ssh-keygen -t rsa -b 4096 -f /var/www/.ssh/id_rsa -N ""
|
sudo ssh-keygen -t rsa -b 4096 -f /var/www/.ssh/id_rsa -N ""
|
||||||
sudo ssh-copy-id -i /var/www/.ssh/id_rsa.pub -p 50221 airlab_server1@aircarto.fr
|
sudo ssh-copy-id -i /var/www/.ssh/id_rsa.pub -p 50221 airlab_server1@aircarto.fr
|
||||||
@@ -212,4 +212,28 @@ This can be doned with script boot_hotspot.sh.
|
|||||||
@reboot /var/www/nebuleair_pro_4g/boot_hotspot.sh
|
@reboot /var/www/nebuleair_pro_4g/boot_hotspot.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Claude Code
|
||||||
|
|
||||||
|
Instructions to use claude code on the RPI.
|
||||||
|
|
||||||
|
### Install NPM
|
||||||
|
|
||||||
|
```
|
||||||
|
sudo apt install -y nodejs npm
|
||||||
|
node -v
|
||||||
|
npm -v
|
||||||
|
```
|
||||||
|
|
||||||
|
### Install Claude
|
||||||
|
|
||||||
|
```
|
||||||
|
sudo npm install -g @anthropic-ai/claude-code
|
||||||
|
```
|
||||||
|
|
||||||
|
### Run claude
|
||||||
|
|
||||||
|
```
|
||||||
|
claude
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
14
SARA/PPP/README.md
Normal file
14
SARA/PPP/README.md
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
# PPP activation
|
||||||
|
|
||||||
|
Une fois la connexion PPP activée on peut retrouver la connexion pp0 avec `ifconfig`.
|
||||||
|
|
||||||
|
### Test avec curl
|
||||||
|
|
||||||
|
On peut forcer l'utilisation du réseau pp0 avec curl:
|
||||||
|
|
||||||
|
`curl --interface ppp0 https://ifconfig.me`
|
||||||
|
|
||||||
|
ou avec ping:
|
||||||
|
|
||||||
|
`ping -I ppp0 google.com`
|
||||||
|
|
||||||
4
SARA/PPP/activate_ppp.sh
Normal file
4
SARA/PPP/activate_ppp.sh
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
|
||||||
|
sudo pppd /dev/ttyAMA2 115200 \
|
||||||
|
connect '/usr/sbin/chat -v -s "" "AT" OK "ATD*99#" CONNECT' \
|
||||||
|
noauth debug dump nodetach nocrtscts
|
||||||
@@ -17,55 +17,10 @@ import sys
|
|||||||
import json
|
import json
|
||||||
import re
|
import re
|
||||||
|
|
||||||
#get data from config
|
|
||||||
def load_config(config_file):
|
|
||||||
try:
|
|
||||||
with open(config_file, 'r') as file:
|
|
||||||
config_data = json.load(file)
|
|
||||||
return config_data
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Error loading config file: {e}")
|
|
||||||
return {}
|
|
||||||
|
|
||||||
#Fonction pour mettre à jour le JSON de configuration
|
|
||||||
def update_json_key(file_path, key, value):
|
|
||||||
"""
|
|
||||||
Updates a specific key in a JSON file with a new value.
|
|
||||||
|
|
||||||
:param file_path: Path to the JSON file.
|
|
||||||
:param key: The key to update in the JSON file.
|
|
||||||
:param value: The new value to assign to the key.
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
# Load the existing data
|
|
||||||
with open(file_path, "r") as file:
|
|
||||||
data = json.load(file)
|
|
||||||
|
|
||||||
# Check if the key exists in the JSON file
|
|
||||||
if key in data:
|
|
||||||
data[key] = value # Update the key with the new value
|
|
||||||
else:
|
|
||||||
print(f"Key '{key}' not found in the JSON file.")
|
|
||||||
return
|
|
||||||
|
|
||||||
# Write the updated data back to the file
|
|
||||||
with open(file_path, "w") as file:
|
|
||||||
json.dump(data, file, indent=2) # Use indent for pretty printing
|
|
||||||
|
|
||||||
print(f"💾 updating '{key}' to '{value}'.")
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Error updating the JSON file: {e}")
|
|
||||||
|
|
||||||
# Define the config file path
|
|
||||||
config_file = '/var/www/nebuleair_pro_4g/config.json'
|
|
||||||
# Load the configuration data
|
|
||||||
config = load_config(config_file)
|
|
||||||
baudrate = config.get('SaraR4_baudrate', 115200) #baudrate du sara R4
|
|
||||||
device_id = config.get('deviceID', '').upper() #device ID en maj
|
|
||||||
|
|
||||||
ser_sara = serial.Serial(
|
ser_sara = serial.Serial(
|
||||||
port='/dev/ttyAMA2',
|
port='/dev/ttyAMA2',
|
||||||
baudrate=baudrate, #115200 ou 9600
|
baudrate=115200, #115200 ou 9600
|
||||||
parity=serial.PARITY_NONE, #PARITY_NONE, PARITY_EVEN or PARITY_ODD
|
parity=serial.PARITY_NONE, #PARITY_NONE, PARITY_EVEN or PARITY_ODD
|
||||||
stopbits=serial.STOPBITS_ONE,
|
stopbits=serial.STOPBITS_ONE,
|
||||||
bytesize=serial.EIGHTBITS,
|
bytesize=serial.EIGHTBITS,
|
||||||
|
|||||||
12
SARA/UDP/receiveUDP_downlink.py
Normal file
12
SARA/UDP/receiveUDP_downlink.py
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
'''
|
||||||
|
____ _ ____ _
|
||||||
|
/ ___| / \ | _ \ / \
|
||||||
|
\___ \ / _ \ | |_) | / _ \
|
||||||
|
___) / ___ \| _ < / ___ \
|
||||||
|
|____/_/ \_\_| \_\/_/ \_\
|
||||||
|
|
||||||
|
Script to read UDP message
|
||||||
|
|
||||||
|
/usr/bin/python3 /var/www/nebuleair_pro_4g/SARA/UDP/receiveUDP_downlink.py
|
||||||
|
|
||||||
|
'''
|
||||||
129
SARA/UDP/sendUDP_message.py
Normal file
129
SARA/UDP/sendUDP_message.py
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
'''
|
||||||
|
____ _ ____ _
|
||||||
|
/ ___| / \ | _ \ / \
|
||||||
|
\___ \ / _ \ | |_) | / _ \
|
||||||
|
___) / ___ \| _ < / ___ \
|
||||||
|
|____/_/ \_\_| \_\/_/ \_\
|
||||||
|
|
||||||
|
Script to send UDP message
|
||||||
|
|
||||||
|
/usr/bin/python3 /var/www/nebuleair_pro_4g/SARA/UDP/sendUDP_message.py
|
||||||
|
|
||||||
|
'''
|
||||||
|
import serial
|
||||||
|
import time
|
||||||
|
import sys
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
ser_sara = serial.Serial(
|
||||||
|
port='/dev/ttyAMA2',
|
||||||
|
baudrate=115200, #115200 ou 9600
|
||||||
|
parity=serial.PARITY_NONE, #PARITY_NONE, PARITY_EVEN or PARITY_ODD
|
||||||
|
stopbits=serial.STOPBITS_ONE,
|
||||||
|
bytesize=serial.EIGHTBITS,
|
||||||
|
timeout = 2
|
||||||
|
)
|
||||||
|
|
||||||
|
def read_complete_response(serial_connection, timeout=2, end_of_response_timeout=2, wait_for_lines=None, debug=True):
|
||||||
|
'''
|
||||||
|
Fonction très importante !!!
|
||||||
|
Reads the complete response from a serial connection and waits for specific lines.
|
||||||
|
'''
|
||||||
|
if wait_for_lines is None:
|
||||||
|
wait_for_lines = [] # Default to an empty list if not provided
|
||||||
|
|
||||||
|
response = bytearray()
|
||||||
|
serial_connection.timeout = timeout
|
||||||
|
end_time = time.time() + end_of_response_timeout
|
||||||
|
start_time = time.time()
|
||||||
|
|
||||||
|
while True:
|
||||||
|
elapsed_time = time.time() - start_time # Time since function start
|
||||||
|
if serial_connection.in_waiting > 0:
|
||||||
|
data = serial_connection.read(serial_connection.in_waiting)
|
||||||
|
response.extend(data)
|
||||||
|
end_time = time.time() + end_of_response_timeout # Reset timeout on new data
|
||||||
|
|
||||||
|
# Decode and check for any target line
|
||||||
|
decoded_response = response.decode('utf-8', errors='replace')
|
||||||
|
for target_line in wait_for_lines:
|
||||||
|
if target_line in decoded_response:
|
||||||
|
if debug:
|
||||||
|
print(f"[DEBUG] 🔎 Found target line: {target_line} (in {elapsed_time:.2f}s)")
|
||||||
|
return decoded_response # Return response immediately if a target line is found
|
||||||
|
elif time.time() > end_time:
|
||||||
|
if debug:
|
||||||
|
print(f"[DEBUG] Timeout reached. No more data received.")
|
||||||
|
break
|
||||||
|
time.sleep(0.1) # Short sleep to prevent busy waiting
|
||||||
|
|
||||||
|
# Final response and debug output
|
||||||
|
total_elapsed_time = time.time() - start_time
|
||||||
|
if debug:
|
||||||
|
print(f"[DEBUG] ⏱️ elapsed time: {total_elapsed_time:.2f}s. ⏱️")
|
||||||
|
# Check if the elapsed time exceeded 10 seconds
|
||||||
|
if total_elapsed_time > 10 and debug:
|
||||||
|
print(f"[ALERT] 🚨 The operation took too long 🚨")
|
||||||
|
print(f'<span style="color: red;font-weight: bold;">[ALERT] ⚠️{total_elapsed_time:.2f}s⚠️</span>')
|
||||||
|
|
||||||
|
return response.decode('utf-8', errors='replace') # Return the full response if no target line is found
|
||||||
|
|
||||||
|
try:
|
||||||
|
print('Start script')
|
||||||
|
|
||||||
|
# Increase verbosity
|
||||||
|
command = f'AT+CMEE=2\r'
|
||||||
|
ser_sara.write(command.encode('utf-8'))
|
||||||
|
response_SARA_1 = read_complete_response(ser_sara, wait_for_lines=["OK", "ERROR"])
|
||||||
|
print(response_SARA_1, end="")
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
# 1. Create SOCKET
|
||||||
|
print('➡️Create SOCKET')
|
||||||
|
command = f'AT+USOCR=17\r'
|
||||||
|
ser_sara.write(command.encode('utf-8'))
|
||||||
|
response_SARA_1 = read_complete_response(ser_sara, wait_for_lines=["OK", "ERROR"])
|
||||||
|
print(response_SARA_1, end="")
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
# 2. Retreive Socket ID
|
||||||
|
match = re.search(r'\+USOCR:\s*(\d+)', response_SARA_1)
|
||||||
|
if match:
|
||||||
|
socket_id = match.group(1)
|
||||||
|
print(f"Socket ID: {socket_id}")
|
||||||
|
else:
|
||||||
|
print("Failed to extract socket ID")
|
||||||
|
|
||||||
|
|
||||||
|
#3. Connect to UDP server
|
||||||
|
print("Connect to server:")
|
||||||
|
command = f'AT+USOCO={socket_id},"192.168.0.20",4242\r'
|
||||||
|
ser_sara.write(command.encode('utf-8'))
|
||||||
|
response_SARA_2 = read_complete_response(ser_sara, wait_for_lines=["OK", "+CME ERROR", "ERROR"], debug=False)
|
||||||
|
print(response_SARA_2)
|
||||||
|
|
||||||
|
# 4. Write data and send
|
||||||
|
print("Write data:")
|
||||||
|
command = f'AT+USOWR={socket_id},10\r'
|
||||||
|
ser_sara.write(command.encode('utf-8'))
|
||||||
|
response_SARA_2 = read_complete_response(ser_sara, wait_for_lines=["@","OK", "+CME ERROR", "ERROR"], debug=False)
|
||||||
|
print(response_SARA_2)
|
||||||
|
|
||||||
|
ser_sara.write("1234567890".encode())
|
||||||
|
response_SARA_2 = read_complete_response(ser_sara, wait_for_lines=["@","OK", "+CME ERROR", "ERROR"], debug=False)
|
||||||
|
print(response_SARA_2)
|
||||||
|
|
||||||
|
#Close socket
|
||||||
|
print("Close socket:")
|
||||||
|
command = f'AT+USOCL={socket_id}\r'
|
||||||
|
ser_sara.write(command.encode('utf-8'))
|
||||||
|
response_SARA_2 = read_complete_response(ser_sara, wait_for_lines=["OK", "+CME ERROR", "ERROR"], debug=False)
|
||||||
|
print(response_SARA_2)
|
||||||
|
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print("An error occurred:", e)
|
||||||
|
traceback.print_exc() # This prints the full traceback
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
'''
|
r'''
|
||||||
____ _ ____ _
|
____ _ ____ _
|
||||||
/ ___| / \ | _ \ / \
|
/ ___| / \ | _ \ / \
|
||||||
\___ \ / _ \ | |_) | / _ \
|
\___ \ / _ \ | |_) | / _ \
|
||||||
@@ -19,6 +19,8 @@ import sys
|
|||||||
import json
|
import json
|
||||||
import re
|
import re
|
||||||
import sqlite3
|
import sqlite3
|
||||||
|
import traceback
|
||||||
|
|
||||||
|
|
||||||
#GPIO
|
#GPIO
|
||||||
SARA_power_GPIO = 16
|
SARA_power_GPIO = 16
|
||||||
@@ -384,12 +386,12 @@ try:
|
|||||||
latitude = match.group(1)
|
latitude = match.group(1)
|
||||||
longitude = match.group(2)
|
longitude = match.group(2)
|
||||||
print(f"📍 Latitude: {latitude}, Longitude: {longitude}")
|
print(f"📍 Latitude: {latitude}, Longitude: {longitude}")
|
||||||
|
#update sqlite table
|
||||||
|
update_sqlite_config("latitude_raw", float(latitude))
|
||||||
|
update_sqlite_config("longitude_raw", float(longitude))
|
||||||
else:
|
else:
|
||||||
print("❌ Failed to extract coordinates.")
|
print("❌ Failed to extract coordinates.")
|
||||||
|
|
||||||
#update sqlite table
|
|
||||||
update_sqlite_config("latitude_raw", float(latitude))
|
|
||||||
update_sqlite_config("longitude_raw", float(longitude))
|
|
||||||
|
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
'''
|
r'''
|
||||||
____ _ ____ _
|
____ _ ____ _
|
||||||
/ ___| / \ | _ \ / \
|
/ ___| / \ | _ \ / \
|
||||||
\___ \ / _ \ | |_) | / _ \
|
\___ \ / _ \ | |_) | / _ \
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
'''
|
r'''
|
||||||
____ _ ____ _
|
____ _ ____ _
|
||||||
/ ___| / \ | _ \ / \
|
/ ___| / \ | _ \ / \
|
||||||
\___ \ / _ \ | |_) | / _ \
|
\___ \ / _ \ | |_) | / _ \
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
'''
|
r'''
|
||||||
____ _ ____ _
|
____ _ ____ _
|
||||||
/ ___| / \ | _ \ / \
|
/ ___| / \ | _ \ / \
|
||||||
\___ \ / _ \ | |_) | / _ \
|
\___ \ / _ \ | |_) | / _ \
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
'''
|
r'''
|
||||||
____ _ ____ _
|
____ _ ____ _
|
||||||
/ ___| / \ | _ \ / \
|
/ ___| / \ | _ \ / \
|
||||||
\___ \ / _ \ | |_) | / _ \
|
\___ \ / _ \ | |_) | / _ \
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
'''
|
r'''
|
||||||
____ _ ____ _
|
____ _ ____ _
|
||||||
/ ___| / \ | _ \ / \
|
/ ___| / \ | _ \ / \
|
||||||
\___ \ / _ \ | |_) | / _ \
|
\___ \ / _ \ | |_) | / _ \
|
||||||
@@ -97,7 +97,7 @@ try:
|
|||||||
command= f'AT+UHTTPC={aircarto_profile_id},1,"/ping.php","aircarto_server_response.txt"\r'
|
command= f'AT+UHTTPC={aircarto_profile_id},1,"/ping.php","aircarto_server_response.txt"\r'
|
||||||
ser_sara.write(command.encode('utf-8'))
|
ser_sara.write(command.encode('utf-8'))
|
||||||
|
|
||||||
response_SARA_3 = read_complete_response(ser_sara, timeout=5, end_of_response_timeout=120, wait_for_lines=["+UUHTTPCR", "+CME ERROR"], debug=True)
|
response_SARA_3 = read_complete_response(ser_sara, timeout=5, end_of_response_timeout=20, wait_for_lines=["+UUHTTPCR", "+CME ERROR"], debug=True)
|
||||||
|
|
||||||
print(response_SARA_3)
|
print(response_SARA_3)
|
||||||
# si on recoit la réponse UHTTPCR
|
# si on recoit la réponse UHTTPCR
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
'''
|
r'''
|
||||||
____ _ ____ _
|
____ _ ____ _
|
||||||
/ ___| / \ | _ \ / \
|
/ ___| / \ | _ \ / \
|
||||||
\___ \ / _ \ | |_) | / _ \
|
\___ \ / _ \ | |_) | / _ \
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
'''
|
r'''
|
||||||
____ _ ____ _
|
____ _ ____ _
|
||||||
/ ___| / \ | _ \ / \
|
/ ___| / \ | _ \ / \
|
||||||
\___ \ / _ \ | |_) | / _ \
|
\___ \ / _ \ | |_) | / _ \
|
||||||
|
|||||||
@@ -15,29 +15,47 @@ echo "NebuleAir pro started at $(date)"
|
|||||||
|
|
||||||
chmod -R 777 /var/www/nebuleair_pro_4g/
|
chmod -R 777 /var/www/nebuleair_pro_4g/
|
||||||
|
|
||||||
# Blink GPIO 23 and 24 five times
|
# Blink GPIO 23 and 24 five times (starts OFF, stays OFF at end)
|
||||||
for i in {1..5}; do
|
#gpioset -c gpiochip0 -t 1s,1s,1s,1s,1s,1s,1s,1s,1s,1s,0 23=0 24=0
|
||||||
# Turn GPIO 23 and 24 ON
|
|
||||||
gpioset gpiochip0 23=1 24=1
|
# Blink GPIO 23 and 24 five times (starts OFF, stays OFF at end)
|
||||||
#echo "LEDs ON"
|
python3 << 'EOF'
|
||||||
sleep 1
|
import RPi.GPIO as GPIO
|
||||||
|
import time
|
||||||
# Turn GPIO 23 and 24 OFF
|
|
||||||
gpioset gpiochip0 23=0 24=0
|
GPIO.setmode(GPIO.BCM)
|
||||||
#echo "LEDs OFF"
|
GPIO.setwarnings(False)
|
||||||
sleep 1
|
GPIO.setup(23, GPIO.OUT)
|
||||||
done
|
GPIO.setup(24, GPIO.OUT)
|
||||||
|
|
||||||
|
for _ in range(5):
|
||||||
|
GPIO.output(23, GPIO.HIGH)
|
||||||
|
GPIO.output(24, GPIO.HIGH)
|
||||||
|
time.sleep(1)
|
||||||
|
GPIO.output(23, GPIO.LOW)
|
||||||
|
GPIO.output(24, GPIO.LOW)
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
GPIO.cleanup()
|
||||||
|
EOF
|
||||||
|
|
||||||
echo "getting RPI serial number"
|
echo "getting RPI serial number"
|
||||||
# Get the last 8 characters of the serial number and write to text file
|
# Get the last 8 characters of the serial number and write to text file
|
||||||
serial_number=$(cat /proc/cpuinfo | grep Serial | awk '{print substr($3, length($3) - 7)}')
|
serial_number=$(cat /proc/cpuinfo | grep Serial | awk '{print substr($3, length($3) - 7)}')
|
||||||
|
|
||||||
# update Sqlite database
|
# update Sqlite database (only if not already set, i.e., still has default value 'XXXX')
|
||||||
echo "Updating SQLite database with device ID: $serial_number"
|
echo "Updating SQLite database with device ID: $serial_number"
|
||||||
sqlite3 /var/www/nebuleair_pro_4g/sqlite/sensors.db "UPDATE config_table SET value='$serial_number' WHERE key='deviceID';"
|
sqlite3 /var/www/nebuleair_pro_4g/sqlite/sensors.db "UPDATE config_table SET value='$serial_number' WHERE key='deviceID' AND value='XXXX';"
|
||||||
|
|
||||||
echo "id: $serial_number"
|
echo "id: $serial_number"
|
||||||
|
|
||||||
|
# Get deviceID from SQLite config_table (may be different from serial_number if manually configured)
|
||||||
|
DEVICE_ID=$(sqlite3 /var/www/nebuleair_pro_4g/sqlite/sensors.db "SELECT value FROM config_table WHERE key='deviceID'")
|
||||||
|
echo "Device ID from database: $DEVICE_ID"
|
||||||
|
|
||||||
|
# Get deviceName from SQLite config_table for use in hotspot SSID
|
||||||
|
DEVICE_NAME=$(sqlite3 /var/www/nebuleair_pro_4g/sqlite/sensors.db "SELECT value FROM config_table WHERE key='deviceName'")
|
||||||
|
echo "Device Name from database: $DEVICE_NAME"
|
||||||
|
|
||||||
# Get SSH tunnel port from SQLite config_table
|
# Get SSH tunnel port from SQLite config_table
|
||||||
SSH_TUNNEL_PORT=$(sqlite3 /var/www/nebuleair_pro_4g/sqlite/sensors.db "SELECT value FROM config_table WHERE key='sshTunnel_port'")
|
SSH_TUNNEL_PORT=$(sqlite3 /var/www/nebuleair_pro_4g/sqlite/sensors.db "SELECT value FROM config_table WHERE key='sshTunnel_port'")
|
||||||
@@ -54,9 +72,9 @@ if [ "$STATE" == "30 (disconnected)" ]; then
|
|||||||
# Perform a wifi scan and save its output to a csv file
|
# Perform a wifi scan and save its output to a csv file
|
||||||
# nmcli device wifi list
|
# nmcli device wifi list
|
||||||
nmcli -f SSID,SIGNAL,SECURITY device wifi list | awk 'BEGIN { OFS=","; print "SSID,SIGNAL,SECURITY" } NR>1 { print $1,$2,$3 }' > "$OUTPUT_FILE"
|
nmcli -f SSID,SIGNAL,SECURITY device wifi list | awk 'BEGIN { OFS=","; print "SSID,SIGNAL,SECURITY" } NR>1 { print $1,$2,$3 }' > "$OUTPUT_FILE"
|
||||||
# Start the hotspot
|
# Start the hotspot with SSID based on deviceName
|
||||||
echo "Starting hotspot..."
|
echo "Starting hotspot with SSID: $DEVICE_NAME"
|
||||||
sudo nmcli device wifi hotspot ifname wlan0 ssid nebuleair_pro password nebuleaircfg
|
sudo nmcli device wifi hotspot ifname wlan0 ssid "$DEVICE_NAME" password nebuleaircfg
|
||||||
|
|
||||||
# Update SQLite to reflect hotspot mode
|
# Update SQLite to reflect hotspot mode
|
||||||
sqlite3 /var/www/nebuleair_pro_4g/sqlite/sensors.db "UPDATE config_table SET value='hotspot' WHERE key='WIFI_status'"
|
sqlite3 /var/www/nebuleair_pro_4g/sqlite/sensors.db "UPDATE config_table SET value='hotspot' WHERE key='WIFI_status'"
|
||||||
|
|||||||
41
connexion.sh
41
connexion.sh
@@ -2,26 +2,49 @@
|
|||||||
echo "-------"
|
echo "-------"
|
||||||
echo "Start connexion shell script at $(date)"
|
echo "Start connexion shell script at $(date)"
|
||||||
|
|
||||||
|
# Get deviceName from database for hotspot SSID
|
||||||
|
DEVICE_NAME=$(sqlite3 /var/www/nebuleair_pro_4g/sqlite/sensors.db "SELECT value FROM config_table WHERE key='deviceName'")
|
||||||
|
echo "Device Name: $DEVICE_NAME"
|
||||||
|
|
||||||
#disable hotspot
|
# Find and disable any active hotspot connection
|
||||||
echo "Disable Hotspot:"
|
echo "Disable Hotspot..."
|
||||||
sudo nmcli connection down Hotspot
|
# Get all wireless connections that are currently active (excludes the target WiFi)
|
||||||
sleep 10
|
ACTIVE_HOTSPOT=$(nmcli -t -f NAME,TYPE,DEVICE connection show --active | grep ':802-11-wireless:wlan0' | cut -d: -f1)
|
||||||
|
|
||||||
|
if [ -n "$ACTIVE_HOTSPOT" ]; then
|
||||||
|
echo "Disabling hotspot connection: $ACTIVE_HOTSPOT"
|
||||||
|
sudo nmcli connection down "$ACTIVE_HOTSPOT"
|
||||||
|
else
|
||||||
|
echo "No active hotspot found"
|
||||||
|
fi
|
||||||
|
|
||||||
|
sleep 5
|
||||||
|
|
||||||
echo "Start connection with:"
|
echo "Start connection with:"
|
||||||
echo "SSID: $1"
|
echo "SSID: $1"
|
||||||
echo "Password: $2"
|
echo "Password: [HIDDEN]"
|
||||||
sudo nmcli device wifi connect "$1" password "$2"
|
sudo nmcli device wifi connect "$1" password "$2"
|
||||||
|
|
||||||
#check if connection is successfull
|
# Check if connection is successful
|
||||||
if [ $? -eq 0 ]; then
|
if [ $? -eq 0 ]; then
|
||||||
echo "Connection to $1 is successfull"
|
echo "Connection to $1 is successful"
|
||||||
|
|
||||||
|
# Update SQLite to reflect connected status
|
||||||
|
sqlite3 /var/www/nebuleair_pro_4g/sqlite/sensors.db "UPDATE config_table SET value='connected' WHERE key='WIFI_status'"
|
||||||
|
echo "Updated database: WIFI_status = connected"
|
||||||
else
|
else
|
||||||
echo "Connection to $1 failed"
|
echo "Connection to $1 failed"
|
||||||
echo "Restarting hotspot..."
|
echo "Restarting hotspot..."
|
||||||
#enable hotspot
|
|
||||||
sudo nmcli connection up Hotspot
|
# Recreate hotspot with current deviceName as SSID
|
||||||
|
sudo nmcli device wifi hotspot ifname wlan0 ssid "$DEVICE_NAME" password nebuleaircfg
|
||||||
|
|
||||||
|
# Update SQLite to reflect hotspot mode
|
||||||
|
sqlite3 /var/www/nebuleair_pro_4g/sqlite/sensors.db "UPDATE config_table SET value='hotspot' WHERE key='WIFI_status'"
|
||||||
|
echo "Updated database: WIFI_status = hotspot"
|
||||||
|
echo "Hotspot restarted with SSID: $DEVICE_NAME"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "End connexion shell script"
|
echo "End connexion shell script"
|
||||||
echo "-------"
|
echo "-------"
|
||||||
|
|
||||||
|
|||||||
0
envea/read_value_loop.py → envea/old/read_value_loop.py
Executable file → Normal file
0
envea/read_value_loop.py → envea/old/read_value_loop.py
Executable file → Normal file
0
envea/read_value_loop_json.py → envea/old/read_value_loop_json.py
Executable file → Normal file
0
envea/read_value_loop_json.py → envea/old/read_value_loop_json.py
Executable file → Normal file
@@ -1,6 +1,7 @@
|
|||||||
import serial
|
import serial
|
||||||
import time
|
import time
|
||||||
import sys
|
import sys
|
||||||
|
import re
|
||||||
|
|
||||||
parameter = sys.argv[1:] # Exclude the script name
|
parameter = sys.argv[1:] # Exclude the script name
|
||||||
#print("Parameters received:")
|
#print("Parameters received:")
|
||||||
@@ -61,8 +62,46 @@ def read_cairsens(port, baudrate=9600, parity=serial.PARITY_NONE, stopbits=seria
|
|||||||
|
|
||||||
# ASCII characters
|
# ASCII characters
|
||||||
ascii_data = ''.join(chr(b) if 0x20 <= b <= 0x7E else '.' for b in raw_bytes)
|
ascii_data = ''.join(chr(b) if 0x20 <= b <= 0x7E else '.' for b in raw_bytes)
|
||||||
print(f"Valeurs converties en ASCII : {ascii_data}")
|
sensor_type = "Unknown" # ou None, selon ton besoin
|
||||||
|
sensor_measurement = "Unknown"
|
||||||
|
sensor_range = "Unknown"
|
||||||
|
|
||||||
|
letters = re.findall(r'[A-Za-z]', ascii_data)
|
||||||
|
if len(letters) >= 1:
|
||||||
|
#print(f"First letter found: {letters[0]}")
|
||||||
|
if letters[0] == "C":
|
||||||
|
sensor_type = "Cairclip"
|
||||||
|
if len(letters) >= 2:
|
||||||
|
#print(f"Second letter found: {letters[1]}")
|
||||||
|
if letters[1] == "A":
|
||||||
|
sensor_measurement = "Ammonia(NH3)"
|
||||||
|
if letters[1] == "C":
|
||||||
|
sensor_measurement = "O3 and NO2"
|
||||||
|
if letters[1] == "G":
|
||||||
|
sensor_measurement = "CH4"
|
||||||
|
if letters[1] == "H":
|
||||||
|
sensor_measurement = "H2S"
|
||||||
|
if letters[1] == "N":
|
||||||
|
sensor_measurement = "NO2"
|
||||||
|
if len(letters) >= 3:
|
||||||
|
#print(f"Thrisd letter found: {letters[2]}")
|
||||||
|
if letters[2] == "B":
|
||||||
|
sensor_range = "0-250 ppb"
|
||||||
|
if letters[2] == "M":
|
||||||
|
sensor_range = "0-1ppm"
|
||||||
|
if letters[2] == "V":
|
||||||
|
sensor_range = "0-20 ppm"
|
||||||
|
if letters[2] == "P":
|
||||||
|
sensor_range = "PACKET data block ?"
|
||||||
|
|
||||||
|
if len(letters) < 1:
|
||||||
|
print("No letter found in the ASCII data.")
|
||||||
|
|
||||||
|
print(f"Valeurs converties en ASCII : {sensor_type} {sensor_measurement} {sensor_range}")
|
||||||
|
|
||||||
|
#print(f"Sensor type: {sensor_type}")
|
||||||
|
#print(f"Sensor measurment: {sensor_measurement}")
|
||||||
|
#print(f"Sensor range: {sensor_range}")
|
||||||
# Numeric values
|
# Numeric values
|
||||||
numeric_values = [b for b in raw_bytes]
|
numeric_values = [b for b in raw_bytes]
|
||||||
print(f"Valeurs numériques : {numeric_values}")
|
print(f"Valeurs numériques : {numeric_values}")
|
||||||
|
|||||||
224
envea/read_ref_v2.py
Normal file
224
envea/read_ref_v2.py
Normal file
@@ -0,0 +1,224 @@
|
|||||||
|
"""
|
||||||
|
_____ _ ___ _______ _
|
||||||
|
| ____| \ | \ \ / / ____| / \
|
||||||
|
| _| | \| |\ \ / /| _| / _ \
|
||||||
|
| |___| |\ | \ V / | |___ / ___ \
|
||||||
|
|_____|_| \_| \_/ |_____/_/ \_\
|
||||||
|
|
||||||
|
Gather data from envea Sensors and store them to the SQlite table
|
||||||
|
Use the RTC time for the timestamp
|
||||||
|
|
||||||
|
This script is run by a service nebuleair-envea-data.service
|
||||||
|
|
||||||
|
/usr/bin/python3 /var/www/nebuleair_pro_4g/envea/read_ref_v2.py ttyAMA4
|
||||||
|
|
||||||
|
ATTENTION --> read_ref.py fonctionne mieux
|
||||||
|
"""
|
||||||
|
|
||||||
|
import serial
|
||||||
|
import time
|
||||||
|
import sys
|
||||||
|
|
||||||
|
parameter = sys.argv[1:] # Exclude the script name
|
||||||
|
port='/dev/'+parameter[0]
|
||||||
|
|
||||||
|
# Mapping dictionaries
|
||||||
|
COMPOUND_MAP = {
|
||||||
|
'A': 'Ammonia',
|
||||||
|
'B': 'Benzene',
|
||||||
|
'C': 'Carbon Monoxide',
|
||||||
|
'D': 'Hydrogen Sulfide',
|
||||||
|
'E': 'Ethylene',
|
||||||
|
'F': 'Formaldehyde',
|
||||||
|
'G': 'Gasoline',
|
||||||
|
'H': 'Hydrogen',
|
||||||
|
'I': 'Isobutylene',
|
||||||
|
'J': 'Jet Fuel',
|
||||||
|
'K': 'Kerosene',
|
||||||
|
'L': 'Liquified Petroleum Gas',
|
||||||
|
'M': 'Methane',
|
||||||
|
'N': 'Nitrogen Dioxide',
|
||||||
|
'O': 'Ozone',
|
||||||
|
'P': 'Propane',
|
||||||
|
'Q': 'Quinoline',
|
||||||
|
'R': 'Refrigerant',
|
||||||
|
'S': 'Sulfur Dioxide',
|
||||||
|
'T': 'Toluene',
|
||||||
|
'U': 'Uranium Hexafluoride',
|
||||||
|
'V': 'Vinyl Chloride',
|
||||||
|
'W': 'Water Vapor',
|
||||||
|
'X': 'Xylene',
|
||||||
|
'Y': 'Yttrium',
|
||||||
|
'Z': 'Zinc'
|
||||||
|
}
|
||||||
|
|
||||||
|
RANGE_MAP = {
|
||||||
|
'A': '0-10 ppm',
|
||||||
|
'B': '0-250 ppb',
|
||||||
|
'C': '0-1000 ppm',
|
||||||
|
'D': '0-50 ppm',
|
||||||
|
'E': '0-100 ppm',
|
||||||
|
'F': '0-5 ppm',
|
||||||
|
'G': '0-500 ppm',
|
||||||
|
'H': '0-2000 ppm',
|
||||||
|
'I': '0-200 ppm',
|
||||||
|
'J': '0-300 ppm',
|
||||||
|
'K': '0-400 ppm',
|
||||||
|
'L': '0-600 ppm',
|
||||||
|
'M': '0-800 ppm',
|
||||||
|
'N': '0-20 ppm',
|
||||||
|
'O': '0-1 ppm',
|
||||||
|
'P': '0-5000 ppm',
|
||||||
|
'Q': '0-150 ppm',
|
||||||
|
'R': '0-750 ppm',
|
||||||
|
'S': '0-25 ppm',
|
||||||
|
'T': '0-350 ppm',
|
||||||
|
'U': '0-450 ppm',
|
||||||
|
'V': '0-550 ppm',
|
||||||
|
'W': '0-650 ppm',
|
||||||
|
'X': '0-850 ppm',
|
||||||
|
'Y': '0-950 ppm',
|
||||||
|
'Z': '0-1500 ppm'
|
||||||
|
}
|
||||||
|
|
||||||
|
INTERFACE_MAP = {
|
||||||
|
0x01: 'USB',
|
||||||
|
0x02: 'UART',
|
||||||
|
0x03: 'I2C',
|
||||||
|
0x04: 'SPI'
|
||||||
|
}
|
||||||
|
|
||||||
|
def parse_cairsens_data(hex_data):
|
||||||
|
"""
|
||||||
|
Parse the extracted hex data from CAIRSENS sensor.
|
||||||
|
|
||||||
|
:param hex_data: Hexadecimal string of extracted data (indices 11-28)
|
||||||
|
:return: Dictionary with parsed information
|
||||||
|
"""
|
||||||
|
# Convert hex to bytes for easier processing
|
||||||
|
raw_bytes = bytes.fromhex(hex_data)
|
||||||
|
|
||||||
|
# Initialize result dictionary
|
||||||
|
result = {
|
||||||
|
'device_type': 'Unknown',
|
||||||
|
'compound': 'Unknown',
|
||||||
|
'range': 'Unknown',
|
||||||
|
'interface': 'Unknown',
|
||||||
|
'raw_data': hex_data
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(raw_bytes) >= 4: # Ensure we have at least 4 bytes
|
||||||
|
# First byte: Device type check
|
||||||
|
first_char = chr(raw_bytes[0]) if 0x20 <= raw_bytes[0] <= 0x7E else '?'
|
||||||
|
if first_char == 'C':
|
||||||
|
result['device_type'] = 'CAIRCLIP'
|
||||||
|
else:
|
||||||
|
result['device_type'] = f'Unknown ({first_char})'
|
||||||
|
|
||||||
|
# Second byte: Compound mapping
|
||||||
|
second_char = chr(raw_bytes[1]) if 0x20 <= raw_bytes[1] <= 0x7E else '?'
|
||||||
|
result['compound'] = COMPOUND_MAP.get(second_char, f'Unknown ({second_char})')
|
||||||
|
|
||||||
|
# Third byte: Range mapping
|
||||||
|
third_char = chr(raw_bytes[2]) if 0x20 <= raw_bytes[2] <= 0x7E else '?'
|
||||||
|
result['range'] = RANGE_MAP.get(third_char, f'Unknown ({third_char})')
|
||||||
|
|
||||||
|
# Fourth byte: Interface (raw byte value)
|
||||||
|
interface_byte = raw_bytes[3]
|
||||||
|
result['interface'] = INTERFACE_MAP.get(interface_byte, f'Unknown (0x{interface_byte:02X})')
|
||||||
|
result['interface_raw'] = f'0x{interface_byte:02X}'
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
def read_cairsens(port, baudrate=9600, parity=serial.PARITY_NONE, stopbits=serial.STOPBITS_ONE, databits=serial.EIGHTBITS, timeout=1):
|
||||||
|
"""
|
||||||
|
Lit les données de la sonde CAIRSENS via UART.
|
||||||
|
/usr/bin/python3 /var/www/nebuleair_pro_4g/envea/read_ref_v2.py ttyAMA4
|
||||||
|
|
||||||
|
|
||||||
|
:param port: Le port série utilisé (ex: 'COM1' ou '/dev/ttyAMA0').
|
||||||
|
:param baudrate: Le débit en bauds (ex: 9600).
|
||||||
|
:param parity: Le bit de parité (serial.PARITY_NONE, serial.PARITY_EVEN, serial.PARITY_ODD).
|
||||||
|
:param stopbits: Le nombre de bits de stop (serial.STOPBITS_ONE, serial.STOPBITS_TWO).
|
||||||
|
:param databits: Le nombre de bits de données (serial.FIVEBITS, serial.SIXBITS, serial.SEVENBITS, serial.EIGHTBITS).
|
||||||
|
:param timeout: Temps d'attente maximal pour la lecture (en secondes).
|
||||||
|
:return: Les données reçues sous forme de chaîne de caractères.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Ouvrir la connexion série
|
||||||
|
ser = serial.Serial(
|
||||||
|
port=port,
|
||||||
|
baudrate=baudrate,
|
||||||
|
parity=parity,
|
||||||
|
stopbits=stopbits,
|
||||||
|
bytesize=databits,
|
||||||
|
timeout=timeout
|
||||||
|
)
|
||||||
|
print(f"Connexion ouverte sur {port} à {baudrate} bauds.")
|
||||||
|
|
||||||
|
# Attendre un instant pour stabiliser la connexion
|
||||||
|
time.sleep(2)
|
||||||
|
|
||||||
|
# Envoyer une commande à la sonde (si nécessaire)
|
||||||
|
# Adapter cette ligne selon la documentation de la sonde
|
||||||
|
#ser.write(b'\r\n')
|
||||||
|
ser.write(b'\xFF\x02\x13\x30\x01\x02\x03\x04\x05\x06\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\x1C\xD1\x61\x03')
|
||||||
|
|
||||||
|
# Lire les données reçues
|
||||||
|
data = ser.readline()
|
||||||
|
print(f"Données reçues brutes : {data}")
|
||||||
|
|
||||||
|
# Convertir les données en hexadécimal
|
||||||
|
hex_data = data.hex() # Convertit en chaîne hexadécimale
|
||||||
|
formatted_hex = ' '.join(hex_data[i:i+2] for i in range(0, len(hex_data), 2)) # Formate avec des espaces
|
||||||
|
print(f"Données reçues en hexadécimal : {formatted_hex}")
|
||||||
|
|
||||||
|
# Extraire les valeurs de l'index 11 à 28 (indices 22 à 56 en hex string)
|
||||||
|
extracted_hex = hex_data[22:56] # Each byte is 2 hex chars, so 11*2=22 to 28*2=56
|
||||||
|
print(f"Valeurs hexadécimales extraites (11 à 28) : {extracted_hex}")
|
||||||
|
|
||||||
|
# Parse the extracted data
|
||||||
|
parsed_data = parse_cairsens_data(extracted_hex)
|
||||||
|
|
||||||
|
# Display parsed information
|
||||||
|
print("\n=== CAIRSENS SENSOR INFORMATION ===")
|
||||||
|
print(f"Device Type: {parsed_data['device_type']}")
|
||||||
|
print(f"Compound: {parsed_data['compound']}")
|
||||||
|
print(f"Range: {parsed_data['range']}")
|
||||||
|
print(f"Interface: {parsed_data['interface']} ({parsed_data.get('interface_raw', 'N/A')})")
|
||||||
|
print(f"Raw Data: {parsed_data['raw_data']}")
|
||||||
|
print("=====================================")
|
||||||
|
|
||||||
|
# Convertir en ASCII et en valeurs numériques (pour debug)
|
||||||
|
if extracted_hex:
|
||||||
|
raw_bytes = bytes.fromhex(extracted_hex)
|
||||||
|
ascii_data = ''.join(chr(b) if 0x20 <= b <= 0x7E else '.' for b in raw_bytes)
|
||||||
|
print(f"Valeurs converties en ASCII : {ascii_data}")
|
||||||
|
|
||||||
|
numeric_values = [b for b in raw_bytes]
|
||||||
|
print(f"Valeurs numériques : {numeric_values}")
|
||||||
|
|
||||||
|
# Fermer la connexion
|
||||||
|
ser.close()
|
||||||
|
print("Connexion fermée.")
|
||||||
|
|
||||||
|
return parsed_data
|
||||||
|
|
||||||
|
except serial.SerialException as e:
|
||||||
|
print(f"Erreur de connexion série : {e}")
|
||||||
|
return None
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Erreur générale : {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Exemple d'utilisation
|
||||||
|
if __name__ == "__main__":
|
||||||
|
port = port # Remplacez par votre port série (ex: /dev/ttyAMA0 sur Raspberry Pi)
|
||||||
|
baudrate = 9600 # Débit en bauds (à vérifier dans la documentation)
|
||||||
|
parity = serial.PARITY_NONE # Parité (NONE, EVEN, ODD)
|
||||||
|
stopbits = serial.STOPBITS_ONE # Bits de stop (ONE, TWO)
|
||||||
|
databits = serial.EIGHTBITS # Bits de données (FIVEBITS, SIXBITS, SEVENBITS, EIGHTBITS)
|
||||||
|
|
||||||
|
data = read_cairsens(port, baudrate, parity, stopbits, databits)
|
||||||
|
if data:
|
||||||
|
print(f"\nRésultat final : {data}")
|
||||||
@@ -44,9 +44,9 @@ def read_cairsens(port, baudrate=9600, parity=serial.PARITY_NONE, stopbits=seria
|
|||||||
|
|
||||||
|
|
||||||
# Lire les données reçues
|
# Lire les données reçues
|
||||||
#data = ser.read_until(b'\n') # Lire jusqu'à la fin de ligne ou un autre délimiteur
|
data = ser.read_until(b'\n') # Lire jusqu'à la fin de ligne ou un autre délimiteur
|
||||||
data = ser.readline()
|
data = ser.readline()
|
||||||
#print(f"Données reçues brutes : {data}")
|
print(f"Données reçues brutes : {data}")
|
||||||
#print(f"Données reçues (utf-8) : {data.decode('utf-8').strip()}")
|
#print(f"Données reçues (utf-8) : {data.decode('utf-8').strip()}")
|
||||||
|
|
||||||
# Extraire le 20ème octet
|
# Extraire le 20ème octet
|
||||||
|
|||||||
@@ -8,7 +8,9 @@
|
|||||||
Gather data from envea Sensors and store them to the SQlite table
|
Gather data from envea Sensors and store them to the SQlite table
|
||||||
Use the RTC time for the timestamp
|
Use the RTC time for the timestamp
|
||||||
|
|
||||||
/usr/bin/python3 /var/www/nebuleair_pro_4g/envea/read_value_v2.py
|
This script is run by a service nebuleair-envea-data.service
|
||||||
|
|
||||||
|
/usr/bin/python3 /var/www/nebuleair_pro_4g/envea/read_value_v2.py -d
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@@ -18,23 +20,58 @@ import time
|
|||||||
import traceback
|
import traceback
|
||||||
import sqlite3
|
import sqlite3
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
import sys
|
||||||
|
|
||||||
|
# Set DEBUG to True to enable debug prints, False to disable
|
||||||
|
DEBUG = False # Change this to False to disable debug output
|
||||||
|
|
||||||
|
# You can also control debug via command line argument
|
||||||
|
if len(sys.argv) > 1 and sys.argv[1] in ['--debug', '-d']:
|
||||||
|
DEBUG = True
|
||||||
|
elif len(sys.argv) > 1 and sys.argv[1] in ['--quiet', '-q']:
|
||||||
|
DEBUG = False
|
||||||
|
|
||||||
|
def debug_print(message):
|
||||||
|
"""Print debug messages only if DEBUG is True"""
|
||||||
|
if DEBUG:
|
||||||
|
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
print(f"[{timestamp}] {message}")
|
||||||
|
|
||||||
|
debug_print("=== ENVEA Sensor Reader Started ===")
|
||||||
|
|
||||||
# Connect to the SQLite database
|
# Connect to the SQLite database
|
||||||
conn = sqlite3.connect("/var/www/nebuleair_pro_4g/sqlite/sensors.db")
|
try:
|
||||||
cursor = conn.cursor()
|
conn = sqlite3.connect("/var/www/nebuleair_pro_4g/sqlite/sensors.db")
|
||||||
|
cursor = conn.cursor()
|
||||||
|
except Exception as e:
|
||||||
|
debug_print(f"✗ Failed to connect to database: {e}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
#GET RTC TIME from SQlite
|
# GET RTC TIME from SQlite
|
||||||
cursor.execute("SELECT * FROM timestamp_table LIMIT 1")
|
try:
|
||||||
row = cursor.fetchone() # Get the first (and only) row
|
cursor.execute("SELECT * FROM timestamp_table LIMIT 1")
|
||||||
rtc_time_str = row[1] # '2025-02-07 12:30:45'
|
row = cursor.fetchone() # Get the first (and only) row
|
||||||
|
rtc_time_str = row[1] # '2025-02-07 12:30:45'
|
||||||
|
except Exception as e:
|
||||||
|
debug_print(f"✗ Failed to get RTC time: {e}")
|
||||||
|
rtc_time_str = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
debug_print(f" Using system time instead: {rtc_time_str}")
|
||||||
|
|
||||||
# Fetch connected ENVEA sondes from SQLite config table
|
# Fetch connected ENVEA sondes from SQLite config table
|
||||||
cursor.execute("SELECT port, name, coefficient FROM envea_sondes_table WHERE connected = 1")
|
try:
|
||||||
connected_envea_sondes = cursor.fetchall() # List of tuples (port, name, coefficient)
|
cursor.execute("SELECT port, name, coefficient FROM envea_sondes_table WHERE connected = 1")
|
||||||
|
connected_envea_sondes = cursor.fetchall() # List of tuples (port, name, coefficient)
|
||||||
|
debug_print(f"✓ Found {len(connected_envea_sondes)} connected ENVEA sensors")
|
||||||
|
for port, name, coefficient in connected_envea_sondes:
|
||||||
|
debug_print(f" - {name}: port={port}, coefficient={coefficient}")
|
||||||
|
except Exception as e:
|
||||||
|
debug_print(f"✗ Failed to fetch connected sensors: {e}")
|
||||||
|
connected_envea_sondes = []
|
||||||
|
|
||||||
serial_connections = {}
|
serial_connections = {}
|
||||||
|
|
||||||
if connected_envea_sondes:
|
if connected_envea_sondes:
|
||||||
|
debug_print("\n--- Opening Serial Connections ---")
|
||||||
for port, name, coefficient in connected_envea_sondes:
|
for port, name, coefficient in connected_envea_sondes:
|
||||||
try:
|
try:
|
||||||
serial_connections[name] = serial.Serial(
|
serial_connections[name] = serial.Serial(
|
||||||
@@ -45,58 +82,101 @@ if connected_envea_sondes:
|
|||||||
bytesize=serial.EIGHTBITS,
|
bytesize=serial.EIGHTBITS,
|
||||||
timeout=1
|
timeout=1
|
||||||
)
|
)
|
||||||
|
debug_print(f"✓ Opened serial port for {name} on /dev/{port}")
|
||||||
except serial.SerialException as e:
|
except serial.SerialException as e:
|
||||||
print(f"Error opening serial port for {name}: {e}")
|
debug_print(f"✗ Error opening serial port for {name}: {e}")
|
||||||
|
else:
|
||||||
|
debug_print("! No connected ENVEA sensors found in configuration")
|
||||||
|
|
||||||
global data_h2s, data_no2, data_o3
|
# Initialize sensor data variables
|
||||||
|
global data_h2s, data_no2, data_o3, data_co, data_nh3, data_so2
|
||||||
data_h2s = 0
|
data_h2s = 0
|
||||||
data_no2 = 0
|
data_no2 = 0
|
||||||
data_o3 = 0
|
data_o3 = 0
|
||||||
data_co = 0
|
data_co = 0
|
||||||
data_nh3 = 0
|
data_nh3 = 0
|
||||||
|
data_so2 = 0
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if connected_envea_sondes:
|
if connected_envea_sondes:
|
||||||
|
debug_print("\n--- Reading Sensor Data ---")
|
||||||
for port, name, coefficient in connected_envea_sondes:
|
for port, name, coefficient in connected_envea_sondes:
|
||||||
if name in serial_connections:
|
if name in serial_connections:
|
||||||
serial_connection = serial_connections[name]
|
serial_connection = serial_connections[name]
|
||||||
try:
|
try:
|
||||||
serial_connection.write(
|
debug_print(f"Reading from {name}...")
|
||||||
b"\xFF\x02\x13\x30\x01\x02\x03\x04\x05\x06\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\x12\xAF\x88\x03"
|
|
||||||
)
|
# Send command to sensor
|
||||||
|
command = b"\xFF\x02\x13\x30\x01\x02\x03\x04\x05\x06\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\x12\xAF\x88\x03"
|
||||||
|
serial_connection.write(command)
|
||||||
|
debug_print(f" → Sent command: {command.hex()}")
|
||||||
|
|
||||||
|
# Read response
|
||||||
data_envea = serial_connection.readline()
|
data_envea = serial_connection.readline()
|
||||||
|
debug_print(f" ← Received {len(data_envea)} bytes: {data_envea.hex()}")
|
||||||
|
|
||||||
if len(data_envea) >= 20:
|
if len(data_envea) >= 20:
|
||||||
byte_20 = data_envea[19] * coefficient
|
byte_20 = data_envea[19]
|
||||||
|
raw_value = byte_20
|
||||||
|
calculated_value = byte_20 * coefficient
|
||||||
|
debug_print(f" → Byte 20 value: {raw_value} (0x{raw_value:02X})")
|
||||||
|
debug_print(f" → Calculated value: {raw_value} × {coefficient} = {calculated_value}")
|
||||||
|
|
||||||
if name == "h2s":
|
if name == "h2s":
|
||||||
data_h2s = byte_20
|
data_h2s = calculated_value
|
||||||
elif name == "no2":
|
elif name == "no2":
|
||||||
data_no2 = byte_20
|
data_no2 = calculated_value
|
||||||
elif name == "o3":
|
elif name == "o3":
|
||||||
data_o3 = byte_20
|
data_o3 = calculated_value
|
||||||
|
elif name == "co":
|
||||||
|
data_co = calculated_value
|
||||||
|
elif name == "nh3":
|
||||||
|
data_nh3 = calculated_value
|
||||||
|
elif name == "so2":
|
||||||
|
data_so2 = calculated_value
|
||||||
|
|
||||||
|
debug_print(f" ✓ {name.upper()} = {calculated_value}")
|
||||||
|
else:
|
||||||
|
debug_print(f" ✗ Response too short (expected ≥20 bytes)")
|
||||||
|
|
||||||
except serial.SerialException as e:
|
except serial.SerialException as e:
|
||||||
print(f"Error communicating with {name}: {e}")
|
debug_print(f"✗ Error communicating with {name}: {e}")
|
||||||
|
else:
|
||||||
|
debug_print(f"! No serial connection available for {name}")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print("An error occurred while gathering data:", e)
|
debug_print(f"\n✗ An error occurred while gathering data: {e}")
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
|
|
||||||
|
# Display all collected data
|
||||||
|
debug_print(f"\n--- Collected Sensor Data ---")
|
||||||
|
debug_print(f"H2S: {data_h2s} ppb")
|
||||||
|
debug_print(f"NO2: {data_no2} ppb")
|
||||||
|
debug_print(f"O3: {data_o3} ppb")
|
||||||
|
debug_print(f"CO: {data_co} ppb")
|
||||||
|
debug_print(f"NH3: {data_nh3} ppb")
|
||||||
|
debug_print(f"SO2: {data_so2} ppb")
|
||||||
|
|
||||||
#print(f" H2S: {data_h2s}, NO2: {data_no2}, O3: {data_o3}")
|
# Save to sqlite database
|
||||||
|
|
||||||
#save to sqlite database
|
|
||||||
try:
|
try:
|
||||||
cursor.execute('''
|
cursor.execute('''
|
||||||
INSERT INTO data_envea (timestamp,h2s, no2, o3, co, nh3) VALUES (?,?,?,?,?,?)'''
|
INSERT INTO data_envea (timestamp, h2s, no2, o3, co, nh3, so2) VALUES (?,?,?,?,?,?,?)'''
|
||||||
, (rtc_time_str,data_h2s,data_no2,data_o3,data_co,data_nh3 ))
|
, (rtc_time_str, data_h2s, data_no2, data_o3, data_co, data_nh3, data_so2))
|
||||||
|
|
||||||
# Commit and close the connection
|
# Commit and close the connection
|
||||||
conn.commit()
|
conn.commit()
|
||||||
|
|
||||||
#print("Sensor data saved successfully!")
|
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Database error: {e}")
|
debug_print(f"✗ Database error: {e}")
|
||||||
|
traceback.print_exc()
|
||||||
|
|
||||||
|
# Close serial connections
|
||||||
|
if serial_connections:
|
||||||
|
for name, connection in serial_connections.items():
|
||||||
|
try:
|
||||||
|
connection.close()
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
conn.close()
|
conn.close()
|
||||||
|
debug_print("\n=== ENVEA Sensor Reader Finished ===\n")
|
||||||
|
|
||||||
315
html/admin.html
315
html/admin.html
@@ -91,7 +91,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-check mb-3">
|
<div class="form-check mb-3">
|
||||||
<input class="form-check-input" type="checkbox" value="" id="check_envea" onchange="update_config_sqlite('envea', this.checked)">
|
<input class="form-check-input" type="checkbox" value="" id="check_envea" onchange="update_config_sqlite('envea', this.checked);add_sondeEnveaContainer() ">
|
||||||
<label class="form-check-label" for="check_envea">
|
<label class="form-check-label" for="check_envea">
|
||||||
Send Envea sensor data
|
Send Envea sensor data
|
||||||
</label>
|
</label>
|
||||||
@@ -112,9 +112,41 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-check mb-3">
|
<div class="form-check mb-3">
|
||||||
<input class="form-check-input" type="checkbox" value="" id="check_uSpot" onchange="update_config_sqlite('send_uSpot', this.checked)" disabled>
|
<input class="form-check-input" type="checkbox" value="" id="check_NOISE" onchange="update_config_sqlite('NOISE', this.checked)">
|
||||||
|
<label class="form-check-label" for="check_NOISE">
|
||||||
|
Send Noise data
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||||
|
<span class="fw-bold">Protected Settings</span>
|
||||||
|
<button type="button" class="btn btn-sm btn-outline-primary" onclick="toggleProtectedSettings()" id="unlockBtn">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-lock-fill" viewBox="0 0 16 16">
|
||||||
|
<path d="M8 1a2 2 0 0 1 2 2v4H6V3a2 2 0 0 1 2-2zm3 6V3a3 3 0 0 0-6 0v4a2 2 0 0 0-2 2v5a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V9a2 2 0 0 0-2-2z"/>
|
||||||
|
</svg>
|
||||||
|
Unlock
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-check mb-3">
|
||||||
|
<input class="form-check-input protected-checkbox" type="checkbox" value="" id="check_aircarto" onchange="update_config_sqlite('send_aircarto', this.checked)" disabled>
|
||||||
|
<label class="form-check-label" for="check_aircarto">
|
||||||
|
Send to AirCarto (HTTP)
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-check mb-3">
|
||||||
|
<input class="form-check-input protected-checkbox" type="checkbox" value="" id="check_uSpot" onchange="update_config_sqlite('send_uSpot', this.checked)" disabled>
|
||||||
<label class="form-check-label" for="check_uSpot">
|
<label class="form-check-label" for="check_uSpot">
|
||||||
Send to uSpot
|
Send to uSpot (HTTPS)
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="form-check mb-3">
|
||||||
|
<input class="form-check-input protected-checkbox" type="checkbox" value="" id="check_miotiq" onchange="update_config_sqlite('send_miotiq', this.checked)" disabled>
|
||||||
|
<label class="form-check-label" for="check_miotiq">
|
||||||
|
Send to miotiq (UDP)
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -248,7 +280,34 @@
|
|||||||
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast" aria-label="Close"></button>
|
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast" aria-label="Close"></button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Envea Detection Modal -->
|
||||||
|
<div class="modal fade" id="enveaDetectionModal" tabindex="-1" aria-labelledby="enveaDetectionModalLabel" aria-hidden="true">
|
||||||
|
<div class="modal-dialog modal-xl">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title" id="enveaDetectionModalLabel">Envea Sondes Detection</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body" style="max-height: 70vh; overflow-y: auto;">
|
||||||
|
<div id="detectionProgress" class="text-center" style="display: none;">
|
||||||
|
<div class="spinner-border text-primary" role="status">
|
||||||
|
<span class="visually-hidden">Loading...</span>
|
||||||
|
</div>
|
||||||
|
<p class="mt-2">Scanning ports for Envea devices...</p>
|
||||||
|
</div>
|
||||||
|
<div id="detectionResults">
|
||||||
|
<p>Click "Start Detection" to scan for connected Envea devices.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
||||||
|
<button type="button" class="btn btn-primary" id="startDetectionBtn" onclick="startEnveaDetection()">Start Detection</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
</main>
|
</main>
|
||||||
@@ -261,6 +320,8 @@
|
|||||||
<script src="assets/jquery/jquery-3.7.1.min.js"></script>
|
<script src="assets/jquery/jquery-3.7.1.min.js"></script>
|
||||||
<!-- Link Bootstrap JS and Popper.js locally -->
|
<!-- Link Bootstrap JS and Popper.js locally -->
|
||||||
<script src="assets/js/bootstrap.bundle.js"></script>
|
<script src="assets/js/bootstrap.bundle.js"></script>
|
||||||
|
<!-- i18n translation system -->
|
||||||
|
<script src="assets/js/i18n.js"></script>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
document.addEventListener('DOMContentLoaded', function () {
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
@@ -331,16 +392,29 @@ window.onload = function() {
|
|||||||
const checkbox_nmp5channels = document.getElementById("check_NPM_5channels");
|
const checkbox_nmp5channels = document.getElementById("check_NPM_5channels");
|
||||||
const checkbox_wind = document.getElementById("check_WindMeter");
|
const checkbox_wind = document.getElementById("check_WindMeter");
|
||||||
const checkbox_uSpot = document.getElementById("check_uSpot");
|
const checkbox_uSpot = document.getElementById("check_uSpot");
|
||||||
|
const checkbox_aircarto = document.getElementById("check_aircarto");
|
||||||
|
const checkbox_miotiq = document.getElementById("check_miotiq");
|
||||||
|
|
||||||
const checkbox_bme = document.getElementById("check_bme280");
|
const checkbox_bme = document.getElementById("check_bme280");
|
||||||
const checkbox_envea = document.getElementById("check_envea");
|
const checkbox_envea = document.getElementById("check_envea");
|
||||||
const checkbox_solar = document.getElementById("check_solarBattery");
|
const checkbox_solar = document.getElementById("check_solarBattery");
|
||||||
|
const checkbox_noise = document.getElementById("check_NOISE");
|
||||||
|
|
||||||
checkbox_bme.checked = response["BME280"];
|
checkbox_bme.checked = response["BME280"];
|
||||||
checkbox_envea.checked = response["envea"];
|
checkbox_envea.checked = response["envea"];
|
||||||
checkbox_solar.checked = response["MPPT"];
|
checkbox_solar.checked = response["MPPT"];
|
||||||
checkbox_nmp5channels.checked = response.npm_5channel;
|
checkbox_nmp5channels.checked = response.npm_5channel;
|
||||||
checkbox_wind.checked = response["windMeter"];
|
checkbox_wind.checked = response["windMeter"];
|
||||||
|
checkbox_noise.checked = response["NOISE"];
|
||||||
|
|
||||||
checkbox_uSpot.checked = response["send_uSpot"];
|
checkbox_uSpot.checked = response["send_uSpot"];
|
||||||
|
checkbox_aircarto.checked = response["send_aircarto"];
|
||||||
|
checkbox_miotiq.checked = response["send_miotiq"];
|
||||||
|
|
||||||
|
// If envea is enabled, show the envea sondes container
|
||||||
|
if (response["envea"]) {
|
||||||
|
add_sondeEnveaContainer();
|
||||||
|
}
|
||||||
|
|
||||||
},
|
},
|
||||||
error: function(xhr, status, error) {
|
error: function(xhr, status, error) {
|
||||||
@@ -701,7 +775,7 @@ function add_sondeEnveaContainer() {
|
|||||||
$('#advanced_options').append('<div id="sondes_envea_div" class="input-group mt-4 border p-3 rounded"><legend>Sondes Envea</legend><p>Plouf</p></div>');
|
$('#advanced_options').append('<div id="sondes_envea_div" class="input-group mt-4 border p-3 rounded"><legend>Sondes Envea</legend><p>Plouf</p></div>');
|
||||||
} else {
|
} else {
|
||||||
// Clear existing content if container exists
|
// Clear existing content if container exists
|
||||||
$('#sondes_envea_div').html('<legend>Sondes Envea</legend>');
|
$('#sondes_envea_div').html('<legend>Sondes Envea <button type="button" class="btn btn-sm btn-info ms-2" onclick="detectEnveaSondes()">Detect Devices</button></legend>');
|
||||||
$('#envea_table').html('<table class="table table-striped table-bordered">'+
|
$('#envea_table').html('<table class="table table-striped table-bordered">'+
|
||||||
'<thead><tr><th scope="col">Software</th><th scope="col">Hardware (PCB)</th></tr></thead>'+
|
'<thead><tr><th scope="col">Software</th><th scope="col">Hardware (PCB)</th></tr></thead>'+
|
||||||
'<tbody>' +
|
'<tbody>' +
|
||||||
@@ -726,11 +800,14 @@ function add_sondeEnveaContainer() {
|
|||||||
onchange="updateSondeStatus(${sonde.id}, this.checked)">
|
onchange="updateSondeStatus(${sonde.id}, this.checked)">
|
||||||
</div>
|
</div>
|
||||||
<input type="text" class="form-control" placeholder="Name" value="${sonde.name}"
|
<input type="text" class="form-control" placeholder="Name" value="${sonde.name}"
|
||||||
id="${sondeId}_name" onchange="updateSondeName(${sonde.id}, this.value)">
|
id="${sondeId}_name" readonly style="background-color: #f8f9fa;">
|
||||||
<input type="text" class="form-control" placeholder="Port" value="${sonde.port}"
|
<select class="form-control" id="${sondeId}_port" onchange="updateSondePort(${sonde.id}, this.value)">
|
||||||
id="${sondeId}_port" onchange="updateSondePort(${sonde.id}, this.value)">
|
<option value="ttyAMA3" ${sonde.port === 'ttyAMA3' ? 'selected' : ''}>ttyAMA3</option>
|
||||||
|
<option value="ttyAMA4" ${sonde.port === 'ttyAMA4' ? 'selected' : ''}>ttyAMA4</option>
|
||||||
|
<option value="ttyAMA5" ${sonde.port === 'ttyAMA5' ? 'selected' : ''}>ttyAMA5</option>
|
||||||
|
</select>
|
||||||
<input type="number" class="form-control" placeholder="Coefficient" value="${sonde.coefficient}"
|
<input type="number" class="form-control" placeholder="Coefficient" value="${sonde.coefficient}"
|
||||||
id="${sondeId}_coefficient" onchange="updateSondeCoefficient(${sonde.id}, this.value)">
|
id="${sondeId}_coefficient" onchange="updateSondeCoefficientWithConfirm(${sonde.id}, this.value, this)">
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@@ -928,6 +1005,23 @@ function updateSondePort(id, port) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function updateSondeCoefficientWithConfirm(id, coefficient, inputElement) {
|
||||||
|
// Store the previous value in case user cancels
|
||||||
|
const previousValue = inputElement.getAttribute('data-previous-value') || inputElement.defaultValue;
|
||||||
|
|
||||||
|
// Show confirmation dialog
|
||||||
|
const confirmed = confirm(`Are you sure you want to change the coefficient to ${coefficient}?\n\nThis will affect sensor calibration and data accuracy.`);
|
||||||
|
|
||||||
|
if (confirmed) {
|
||||||
|
// Store the new value as previous for next time
|
||||||
|
inputElement.setAttribute('data-previous-value', coefficient);
|
||||||
|
updateSondeCoefficient(id, coefficient);
|
||||||
|
} else {
|
||||||
|
// Revert to previous value
|
||||||
|
inputElement.value = previousValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function updateSondeCoefficient(id, coefficient) {
|
function updateSondeCoefficient(id, coefficient) {
|
||||||
console.log(`Updating sonde ${id} coefficient to: ${coefficient}`);
|
console.log(`Updating sonde ${id} coefficient to: ${coefficient}`);
|
||||||
const toastLiveExample = document.getElementById('liveToast');
|
const toastLiveExample = document.getElementById('liveToast');
|
||||||
@@ -1234,6 +1328,209 @@ function toggleService(serviceName, enable) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
_____ ____ _ _ _
|
||||||
|
| ____|_ ____ _____ __ _ | _ \ ___| |_ ___ ___| |_(_) ___ _ __
|
||||||
|
| _| | '_ \ \ / / _ \/ _` | | | | |/ _ \ __/ _ \/ __| __| |/ _ \| '_ \
|
||||||
|
| |___| | | \ V / __/ (_| | | |_| | __/ || __/ (__| |_| | (_) | | | |
|
||||||
|
|_____|_| |_|\_/ \___|\__,_| |____/ \___|\__\___|\___|\__|_|\___/|_| |_|
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
function detectEnveaSondes() {
|
||||||
|
console.log("Opening Envea detection modal");
|
||||||
|
const modal = new bootstrap.Modal(document.getElementById('enveaDetectionModal'));
|
||||||
|
modal.show();
|
||||||
|
|
||||||
|
// Reset modal content
|
||||||
|
document.getElementById('detectionProgress').style.display = 'none';
|
||||||
|
document.getElementById('detectionResults').innerHTML = '<p>Click "Start Detection" to scan for connected Envea devices.</p>';
|
||||||
|
document.getElementById('startDetectionBtn').style.display = 'inline-block';
|
||||||
|
}
|
||||||
|
|
||||||
|
function startEnveaDetection() {
|
||||||
|
console.log("Starting Envea device detection");
|
||||||
|
|
||||||
|
// Show progress spinner
|
||||||
|
document.getElementById('detectionProgress').style.display = 'block';
|
||||||
|
document.getElementById('detectionResults').innerHTML = '';
|
||||||
|
document.getElementById('startDetectionBtn').style.display = 'none';
|
||||||
|
|
||||||
|
// Test the three ports: ttyAMA3, ttyAMA4, ttyAMA5
|
||||||
|
const ports = ['ttyAMA3', 'ttyAMA4', 'ttyAMA5'];
|
||||||
|
let completedTests = 0;
|
||||||
|
let results = [];
|
||||||
|
|
||||||
|
ports.forEach(function(port, index) {
|
||||||
|
$.ajax({
|
||||||
|
url: `launcher.php?type=detect_envea_device&port=${port}`,
|
||||||
|
dataType: 'json',
|
||||||
|
method: 'GET',
|
||||||
|
cache: false,
|
||||||
|
timeout: 10000, // 10 second timeout per port
|
||||||
|
success: function(response) {
|
||||||
|
console.log(`Detection result for ${port}:`, response);
|
||||||
|
|
||||||
|
results[index] = {
|
||||||
|
port: port,
|
||||||
|
success: response.success,
|
||||||
|
data: response.data || '',
|
||||||
|
error: response.error || '',
|
||||||
|
detected: response.detected || false,
|
||||||
|
device_info: response.device_info || ''
|
||||||
|
};
|
||||||
|
|
||||||
|
completedTests++;
|
||||||
|
if (completedTests === ports.length) {
|
||||||
|
displayDetectionResults(results);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: function(xhr, status, error) {
|
||||||
|
console.error(`Detection failed for ${port}:`, error);
|
||||||
|
|
||||||
|
results[index] = {
|
||||||
|
port: port,
|
||||||
|
success: false,
|
||||||
|
data: '',
|
||||||
|
error: `Request failed: ${error}`,
|
||||||
|
detected: false,
|
||||||
|
device_info: ''
|
||||||
|
};
|
||||||
|
|
||||||
|
completedTests++;
|
||||||
|
if (completedTests === ports.length) {
|
||||||
|
displayDetectionResults(results);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function displayDetectionResults(results) {
|
||||||
|
console.log("Displaying detection results:", results);
|
||||||
|
|
||||||
|
// Hide progress spinner
|
||||||
|
document.getElementById('detectionProgress').style.display = 'none';
|
||||||
|
|
||||||
|
let htmlContent = '<h6>Detection Results:</h6>';
|
||||||
|
|
||||||
|
// Create cards for each port result
|
||||||
|
results.forEach(function(result, index) {
|
||||||
|
const statusBadge = result.detected ?
|
||||||
|
'<span class="badge bg-success">Device Detected</span>' :
|
||||||
|
result.success ?
|
||||||
|
'<span class="badge bg-warning">No Device</span>' :
|
||||||
|
'<span class="badge bg-danger">Error</span>';
|
||||||
|
|
||||||
|
const deviceInfo = result.device_info || (result.detected ? 'Envea Device' : 'None');
|
||||||
|
const rawData = result.data || 'No data';
|
||||||
|
|
||||||
|
htmlContent += `
|
||||||
|
<div class="card mb-3">
|
||||||
|
<div class="card-header d-flex justify-content-between align-items-center">
|
||||||
|
<h6 class="mb-0"><strong>Port ${result.port}</strong></h6>
|
||||||
|
${statusBadge}
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12 mb-3">
|
||||||
|
<strong>Device Information:</strong>
|
||||||
|
<p class="mb-0">${deviceInfo}</p>
|
||||||
|
</div>
|
||||||
|
${result.error ? `<div class="col-12 mb-3"><div class="alert alert-danger mb-0"><strong>Error:</strong> ${result.error}</div></div>` : ''}
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||||
|
<strong>Raw Data Output:</strong>
|
||||||
|
<button class="btn btn-sm btn-outline-secondary" type="button" data-bs-toggle="collapse" data-bs-target="#rawData${index}" aria-expanded="false">
|
||||||
|
Toggle Raw Data
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="collapse" id="rawData${index}">
|
||||||
|
<pre class="bg-light p-3 rounded" style="white-space: pre-wrap; word-wrap: break-word; max-height: 300px; overflow-y: auto; font-size: 0.85rem;">${rawData}</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add summary
|
||||||
|
const detectedCount = results.filter(r => r.detected).length;
|
||||||
|
htmlContent += `<div class="alert alert-info mt-3">
|
||||||
|
<i class="bi bi-info-circle"></i> <strong>Summary:</strong> ${detectedCount} device(s) detected out of ${results.length} ports tested.
|
||||||
|
</div>`;
|
||||||
|
|
||||||
|
document.getElementById('detectionResults').innerHTML = htmlContent;
|
||||||
|
document.getElementById('startDetectionBtn').style.display = 'inline-block';
|
||||||
|
document.getElementById('startDetectionBtn').textContent = 'Scan Again';
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
____ _ _ _ ____ _ _ _
|
||||||
|
| _ \ _ __ ___ | |_ ___ ___| |_ ___ __| | / ___| ___| |_| |_(_)_ __ __ _ ___
|
||||||
|
| |_) | '__/ _ \| __/ _ \/ __| __/ _ \/ _` | \___ \ / _ \ __| __| | '_ \ / _` / __|
|
||||||
|
| __/| | | (_) | || __/ (__| || __/ (_| | ___) | __/ |_| |_| | | | | (_| \__ \
|
||||||
|
|_| |_| \___/ \__\___|\___|\__\___|\__,_| |____/ \___|\__|\__|_|_| |_|\__, |___/
|
||||||
|
|___/
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Track if protected settings are unlocked
|
||||||
|
let protectedSettingsUnlocked = false;
|
||||||
|
|
||||||
|
function toggleProtectedSettings() {
|
||||||
|
const unlockBtn = document.getElementById('unlockBtn');
|
||||||
|
const protectedCheckboxes = document.querySelectorAll('.protected-checkbox');
|
||||||
|
|
||||||
|
if (protectedSettingsUnlocked) {
|
||||||
|
// Lock the settings
|
||||||
|
protectedSettingsUnlocked = false;
|
||||||
|
protectedCheckboxes.forEach(checkbox => {
|
||||||
|
checkbox.disabled = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update button appearance
|
||||||
|
unlockBtn.classList.remove('btn-success');
|
||||||
|
unlockBtn.classList.add('btn-outline-primary');
|
||||||
|
unlockBtn.innerHTML = `
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-lock-fill" viewBox="0 0 16 16">
|
||||||
|
<path d="M8 1a2 2 0 0 1 2 2v4H6V3a2 2 0 0 1 2-2zm3 6V3a3 3 0 0 0-6 0v4a2 2 0 0 0-2 2v5a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V9a2 2 0 0 0-2-2z"/>
|
||||||
|
</svg>
|
||||||
|
Unlock
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Show toast notification
|
||||||
|
showToast('Protected settings locked', 'info');
|
||||||
|
} else {
|
||||||
|
// Prompt for password
|
||||||
|
const password = prompt('Enter admin password to unlock protected settings:');
|
||||||
|
|
||||||
|
if (password === '123plouf') {
|
||||||
|
// Correct password - unlock the settings
|
||||||
|
protectedSettingsUnlocked = true;
|
||||||
|
protectedCheckboxes.forEach(checkbox => {
|
||||||
|
checkbox.disabled = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update button appearance
|
||||||
|
unlockBtn.classList.remove('btn-outline-primary');
|
||||||
|
unlockBtn.classList.add('btn-success');
|
||||||
|
unlockBtn.innerHTML = `
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-unlock-fill" viewBox="0 0 16 16">
|
||||||
|
<path d="M11 1a2 2 0 0 0-2 2v4a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V9a2 2 0 0 1 2-2h5V3a3 3 0 0 1 6 0v4a.5.5 0 0 1-1 0V3a2 2 0 0 0-2-2z"/>
|
||||||
|
</svg>
|
||||||
|
Lock
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Show success toast
|
||||||
|
showToast('Protected settings unlocked! You can now edit the checkboxes.', 'success');
|
||||||
|
} else if (password !== null) {
|
||||||
|
// Wrong password (null means user cancelled)
|
||||||
|
showToast('Incorrect password!', 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
129
html/assets/js/i18n.js
Normal file
129
html/assets/js/i18n.js
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
/**
|
||||||
|
* NebuleAir i18n - Lightweight internationalization system
|
||||||
|
* Works offline with local JSON translation files
|
||||||
|
* Stores language preference in SQLite database
|
||||||
|
*/
|
||||||
|
|
||||||
|
const i18n = {
|
||||||
|
currentLang: 'fr', // Default language
|
||||||
|
translations: {},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize i18n system
|
||||||
|
* Loads language preference from server and applies translations
|
||||||
|
*/
|
||||||
|
async init() {
|
||||||
|
try {
|
||||||
|
// Load language preference from server (SQLite database)
|
||||||
|
const response = await fetch('launcher.php?type=get_language');
|
||||||
|
const data = await response.json();
|
||||||
|
this.currentLang = data.language || 'fr';
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Could not load language preference, using default (fr):', error);
|
||||||
|
this.currentLang = 'fr';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load translations and apply
|
||||||
|
await this.loadTranslations(this.currentLang);
|
||||||
|
this.applyTranslations();
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load translation file for specified language
|
||||||
|
* @param {string} lang - Language code (fr, en)
|
||||||
|
*/
|
||||||
|
async loadTranslations(lang) {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`lang/${lang}.json`);
|
||||||
|
this.translations = await response.json();
|
||||||
|
console.log(`Translations loaded for: ${lang}`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Failed to load translations for ${lang}:`, error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply translations to all elements with data-i18n attribute
|
||||||
|
*/
|
||||||
|
applyTranslations() {
|
||||||
|
document.querySelectorAll('[data-i18n]').forEach(element => {
|
||||||
|
const key = element.getAttribute('data-i18n');
|
||||||
|
const translation = this.get(key);
|
||||||
|
|
||||||
|
if (translation) {
|
||||||
|
// Handle different element types
|
||||||
|
if (element.tagName === 'INPUT' || element.tagName === 'TEXTAREA') {
|
||||||
|
if (element.type === 'button' || element.type === 'submit') {
|
||||||
|
element.value = translation;
|
||||||
|
} else {
|
||||||
|
element.placeholder = translation;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
element.textContent = translation;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.warn(`Translation not found for key: ${key}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update HTML lang attribute
|
||||||
|
document.documentElement.lang = this.currentLang;
|
||||||
|
|
||||||
|
// Update language switcher dropdown
|
||||||
|
const languageSwitcher = document.getElementById('languageSwitcher');
|
||||||
|
if (languageSwitcher) {
|
||||||
|
languageSwitcher.value = this.currentLang;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get translation by key (supports nested keys with dot notation)
|
||||||
|
* @param {string} key - Translation key (e.g., 'sensors.title')
|
||||||
|
* @returns {string} Translated string or key if not found
|
||||||
|
*/
|
||||||
|
get(key) {
|
||||||
|
const keys = key.split('.');
|
||||||
|
let value = this.translations;
|
||||||
|
|
||||||
|
for (const k of keys) {
|
||||||
|
if (value && typeof value === 'object' && k in value) {
|
||||||
|
value = value[k];
|
||||||
|
} else {
|
||||||
|
return key; // Return key if translation not found
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return value;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Change language and reload translations
|
||||||
|
* @param {string} lang - Language code (fr, en)
|
||||||
|
*/
|
||||||
|
async setLanguage(lang) {
|
||||||
|
if (lang === this.currentLang) return;
|
||||||
|
|
||||||
|
this.currentLang = lang;
|
||||||
|
|
||||||
|
// Save to server (SQLite database)
|
||||||
|
try {
|
||||||
|
await fetch(`launcher.php?type=set_language&language=${lang}`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to save language preference:', error);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reload translations and apply
|
||||||
|
await this.loadTranslations(lang);
|
||||||
|
this.applyTranslations();
|
||||||
|
|
||||||
|
// Emit custom event for other scripts to react to language change
|
||||||
|
document.dispatchEvent(new CustomEvent('languageChanged', { detail: { language: lang } }));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Auto-initialize when DOM is ready
|
||||||
|
if (document.readyState === 'loading') {
|
||||||
|
document.addEventListener('DOMContentLoaded', () => i18n.init());
|
||||||
|
} else {
|
||||||
|
i18n.init();
|
||||||
|
}
|
||||||
@@ -71,7 +71,10 @@
|
|||||||
<button class="btn btn-primary" onclick="get_data_sqlite('data_BME280',getSelectedLimit(),false)">Mesures Temp/Hum</button>
|
<button class="btn btn-primary" onclick="get_data_sqlite('data_BME280',getSelectedLimit(),false)">Mesures Temp/Hum</button>
|
||||||
<button class="btn btn-primary" onclick="get_data_sqlite('data_NPM_5channels',getSelectedLimit(),false)">Mesures PM (5 canaux)</button>
|
<button class="btn btn-primary" onclick="get_data_sqlite('data_NPM_5channels',getSelectedLimit(),false)">Mesures PM (5 canaux)</button>
|
||||||
<button class="btn btn-primary" onclick="get_data_sqlite('data_envea',getSelectedLimit(),false)">Sonde Cairsens</button>
|
<button class="btn btn-primary" onclick="get_data_sqlite('data_envea',getSelectedLimit(),false)">Sonde Cairsens</button>
|
||||||
|
<button class="btn btn-primary" onclick="get_data_sqlite('data_NOISE',getSelectedLimit(),false)">Sonde bruit</button>
|
||||||
|
|
||||||
<button class="btn btn-primary" onclick="get_data_sqlite('data_WIND',getSelectedLimit(),false)">Sonde Vent</button>
|
<button class="btn btn-primary" onclick="get_data_sqlite('data_WIND',getSelectedLimit(),false)">Sonde Vent</button>
|
||||||
|
<button class="btn btn-primary" onclick="get_data_sqlite('data_MPPT',getSelectedLimit(),false)">Batterie</button>
|
||||||
|
|
||||||
<button class="btn btn-warning" onclick="get_data_sqlite('timestamp_table',getSelectedLimit(),false)">Timestamp Table</button>
|
<button class="btn btn-warning" onclick="get_data_sqlite('timestamp_table',getSelectedLimit(),false)">Timestamp Table</button>
|
||||||
|
|
||||||
@@ -96,6 +99,9 @@
|
|||||||
<button class="btn btn-primary" onclick="get_data_sqlite('data_BME280',10,true, getStartDate(), getEndDate())">Mesures Temp/Hum</button>
|
<button class="btn btn-primary" onclick="get_data_sqlite('data_BME280',10,true, getStartDate(), getEndDate())">Mesures Temp/Hum</button>
|
||||||
<button class="btn btn-primary" onclick="get_data_sqlite('data_NPM_5channels',10,true, getStartDate(), getEndDate())">Mesures PM (5 canaux)</button>
|
<button class="btn btn-primary" onclick="get_data_sqlite('data_NPM_5channels',10,true, getStartDate(), getEndDate())">Mesures PM (5 canaux)</button>
|
||||||
<button class="btn btn-primary" onclick="get_data_sqlite('data_envea',10,true, getStartDate(), getEndDate())">Sonde Cairsens</button>
|
<button class="btn btn-primary" onclick="get_data_sqlite('data_envea',10,true, getStartDate(), getEndDate())">Sonde Cairsens</button>
|
||||||
|
<button class="btn btn-primary" onclick="get_data_sqlite('data_NOISE',10,true, getStartDate(), getEndDate())">Sonde Bruit</button>
|
||||||
|
|
||||||
|
<button class="btn btn-primary" onclick="get_data_sqlite('data_mppt',10,true, getStartDate(), getEndDate())">Batterie</button>
|
||||||
|
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
@@ -118,6 +124,8 @@
|
|||||||
<script src="assets/jquery/jquery-3.7.1.min.js"></script>
|
<script src="assets/jquery/jquery-3.7.1.min.js"></script>
|
||||||
<!-- Link Bootstrap JS and Popper.js locally -->
|
<!-- Link Bootstrap JS and Popper.js locally -->
|
||||||
<script src="assets/js/bootstrap.bundle.js"></script>
|
<script src="assets/js/bootstrap.bundle.js"></script>
|
||||||
|
<!-- i18n translation system -->
|
||||||
|
<script src="assets/js/i18n.js"></script>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
document.addEventListener('DOMContentLoaded', function () {
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
@@ -170,9 +178,9 @@ window.onload = function() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
//device name html page title
|
//device name html page title
|
||||||
if (response.deviceName) {
|
if (response.deviceName) {
|
||||||
document.title = response.deviceName;
|
document.title = response.deviceName;
|
||||||
}
|
}
|
||||||
|
|
||||||
},
|
},
|
||||||
error: function(xhr, status, error) {
|
error: function(xhr, status, error) {
|
||||||
@@ -280,8 +288,26 @@ function get_data_sqlite(table, limit, download , startDate = "", endDate = "")
|
|||||||
<th>speed (km/h)</th>
|
<th>speed (km/h)</th>
|
||||||
<th>Direction (V)</th>
|
<th>Direction (V)</th>
|
||||||
`;
|
`;
|
||||||
|
}else if (table === "data_MPPT") {
|
||||||
|
tableHTML += `
|
||||||
|
<th>Timestamp</th>
|
||||||
|
<th>Battery Voltage</th>
|
||||||
|
<th>Battery Current</th>
|
||||||
|
<th> solar_voltage</th>
|
||||||
|
<th> solar_power</th>
|
||||||
|
<th> charger_status</th>
|
||||||
|
|
||||||
|
`;
|
||||||
|
}else if (table === "data_NOISE") {
|
||||||
|
tableHTML += `
|
||||||
|
<th>Timestamp</th>
|
||||||
|
<th>Curent LEQ</th>
|
||||||
|
<th>DB_A_value</th>
|
||||||
|
|
||||||
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
tableHTML += `</tr></thead><tbody>`;
|
tableHTML += `</tr></thead><tbody>`;
|
||||||
|
|
||||||
// Loop through rows and create table rows
|
// Loop through rows and create table rows
|
||||||
@@ -336,6 +362,22 @@ function get_data_sqlite(table, limit, download , startDate = "", endDate = "")
|
|||||||
<td>${columns[1]}</td>
|
<td>${columns[1]}</td>
|
||||||
<td>${columns[2]}</td>
|
<td>${columns[2]}</td>
|
||||||
`;
|
`;
|
||||||
|
}else if (table === "data_MPPT") {
|
||||||
|
tableHTML += `
|
||||||
|
<td>${columns[0]}</td>
|
||||||
|
<td>${columns[1]}</td>
|
||||||
|
<td>${columns[2]}</td>
|
||||||
|
<td>${columns[3]}</td>
|
||||||
|
<td>${columns[4]}</td>
|
||||||
|
<td>${columns[5]}</td>
|
||||||
|
`;
|
||||||
|
}else if (table === "data_NOISE") {
|
||||||
|
tableHTML += `
|
||||||
|
<td>${columns[0]}</td>
|
||||||
|
<td>${columns[1]}</td>
|
||||||
|
<td>${columns[2]}</td>
|
||||||
|
|
||||||
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
tableHTML += "</tr>";
|
tableHTML += "</tr>";
|
||||||
|
|||||||
@@ -108,6 +108,8 @@
|
|||||||
<script src="assets/jquery/jquery-3.7.1.min.js"></script>
|
<script src="assets/jquery/jquery-3.7.1.min.js"></script>
|
||||||
<!-- Link Bootstrap JS and Popper.js locally -->
|
<!-- Link Bootstrap JS and Popper.js locally -->
|
||||||
<script src="assets/js/bootstrap.bundle.js"></script>
|
<script src="assets/js/bootstrap.bundle.js"></script>
|
||||||
|
<!-- i18n translation system -->
|
||||||
|
<script src="assets/js/i18n.js"></script>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
document.addEventListener('DOMContentLoaded', function () {
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
|
|||||||
247
html/lang/README.md
Normal file
247
html/lang/README.md
Normal file
@@ -0,0 +1,247 @@
|
|||||||
|
# NebuleAir i18n System
|
||||||
|
|
||||||
|
Lightweight internationalization (i18n) system for NebuleAir web interface.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- **Offline-first**: Works completely offline with local JSON translation files
|
||||||
|
- **Database-backed**: Language preference stored in SQLite `config_table`
|
||||||
|
- **Automatic**: Translations apply on page load and when language changes
|
||||||
|
- **Simple API**: Easy-to-use data attributes and JavaScript API
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
### 1. Include i18n.js in your HTML page
|
||||||
|
|
||||||
|
```html
|
||||||
|
<script src="assets/js/i18n.js"></script>
|
||||||
|
```
|
||||||
|
|
||||||
|
The i18n system will automatically initialize when the page loads.
|
||||||
|
|
||||||
|
### 2. Add translation keys to HTML elements
|
||||||
|
|
||||||
|
Use the `data-i18n` attribute to mark elements for translation:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<h1 data-i18n="page.title">Titre en français</h1>
|
||||||
|
<p data-i18n="page.description">Description en français</p>
|
||||||
|
<button data-i18n="common.submit">Soumettre</button>
|
||||||
|
```
|
||||||
|
|
||||||
|
The text content serves as a fallback if translations aren't loaded.
|
||||||
|
|
||||||
|
### 3. Add translations to JSON files
|
||||||
|
|
||||||
|
Edit `lang/fr.json` and `lang/en.json`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"page": {
|
||||||
|
"title": "Mon Titre",
|
||||||
|
"description": "Ma description"
|
||||||
|
},
|
||||||
|
"common": {
|
||||||
|
"submit": "Soumettre"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Translation keys use dot notation for nested objects.
|
||||||
|
|
||||||
|
## Translation Files
|
||||||
|
|
||||||
|
- **`fr.json`**: French translations (default)
|
||||||
|
- **`en.json`**: English translations
|
||||||
|
|
||||||
|
### File Structure Example
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"common": {
|
||||||
|
"save": "Enregistrer",
|
||||||
|
"cancel": "Annuler",
|
||||||
|
"delete": "Supprimer"
|
||||||
|
},
|
||||||
|
"navigation": {
|
||||||
|
"home": "Accueil",
|
||||||
|
"settings": "Paramètres"
|
||||||
|
},
|
||||||
|
"sensors": {
|
||||||
|
"title": "Capteurs",
|
||||||
|
"description": "Liste des capteurs"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## JavaScript API
|
||||||
|
|
||||||
|
### Get Current Language
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const currentLang = i18n.currentLang; // 'fr' or 'en'
|
||||||
|
```
|
||||||
|
|
||||||
|
### Change Language Programmatically
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
await i18n.setLanguage('en'); // Switch to English
|
||||||
|
```
|
||||||
|
|
||||||
|
### Get Translation in JavaScript
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const translation = i18n.get('sensors.title'); // Returns translated string
|
||||||
|
```
|
||||||
|
|
||||||
|
### Manual Translation Application
|
||||||
|
|
||||||
|
If you dynamically create HTML elements, call `applyTranslations()` after adding them to the DOM:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Create new element
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.setAttribute('data-i18n', 'mypage.newElement');
|
||||||
|
div.textContent = 'Fallback text';
|
||||||
|
document.body.appendChild(div);
|
||||||
|
|
||||||
|
// Apply translations
|
||||||
|
i18n.applyTranslations();
|
||||||
|
```
|
||||||
|
|
||||||
|
### Listen for Language Changes
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
document.addEventListener('languageChanged', (event) => {
|
||||||
|
console.log('Language changed to:', event.detail.language);
|
||||||
|
// Reload dynamic content, update charts, etc.
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Special Cases
|
||||||
|
|
||||||
|
### Input Placeholders
|
||||||
|
|
||||||
|
For input fields, the translation applies to the `placeholder` attribute:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<input type="text" data-i18n="form.emailPlaceholder" placeholder="Email...">
|
||||||
|
```
|
||||||
|
|
||||||
|
### Button Values
|
||||||
|
|
||||||
|
For input buttons, the translation applies to the `value` attribute:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<input type="submit" data-i18n="common.submit" value="Submit">
|
||||||
|
```
|
||||||
|
|
||||||
|
### Dynamic Content
|
||||||
|
|
||||||
|
For content created with JavaScript (like sensor cards), add `data-i18n` attributes to your template strings and call `i18n.applyTranslations()` after inserting into the DOM.
|
||||||
|
|
||||||
|
## Example: Migrating an Existing Page
|
||||||
|
|
||||||
|
### Before (French only):
|
||||||
|
|
||||||
|
```html
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Capteurs</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Liste des capteurs</h1>
|
||||||
|
<button onclick="getData()">Obtenir les données</button>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
```
|
||||||
|
|
||||||
|
### After (Multilingual):
|
||||||
|
|
||||||
|
```html
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title data-i18n="sensors.pageTitle">Capteurs</title>
|
||||||
|
<script src="assets/js/i18n.js"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1 data-i18n="sensors.title">Liste des capteurs</h1>
|
||||||
|
<button onclick="getData()" data-i18n="common.getData">Obtenir les données</button>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Add to `lang/fr.json`:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"sensors": {
|
||||||
|
"pageTitle": "Capteurs",
|
||||||
|
"title": "Liste des capteurs"
|
||||||
|
},
|
||||||
|
"common": {
|
||||||
|
"getData": "Obtenir les données"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Add to `lang/en.json`:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"sensors": {
|
||||||
|
"pageTitle": "Sensors",
|
||||||
|
"title": "Sensor List"
|
||||||
|
},
|
||||||
|
"common": {
|
||||||
|
"getData": "Get Data"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Backend Integration
|
||||||
|
|
||||||
|
### Get Language Preference
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const response = await fetch('launcher.php?type=get_language');
|
||||||
|
const data = await response.json();
|
||||||
|
console.log(data.language); // 'fr' or 'en'
|
||||||
|
```
|
||||||
|
|
||||||
|
### Set Language Preference
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const response = await fetch('launcher.php?type=set_language&language=en');
|
||||||
|
const data = await response.json();
|
||||||
|
console.log(data.success); // true
|
||||||
|
```
|
||||||
|
|
||||||
|
Language preference is stored in SQLite `config_table` with key `language`.
|
||||||
|
|
||||||
|
## Completed Pages
|
||||||
|
|
||||||
|
- ✅ **sensors.html** - Fully translated with French/English support
|
||||||
|
|
||||||
|
## TODO: Pages to Migrate
|
||||||
|
|
||||||
|
- ⏳ index.html
|
||||||
|
- ⏳ admin.html
|
||||||
|
- ⏳ wifi.html
|
||||||
|
- ⏳ saraR4.html
|
||||||
|
- ⏳ map.html
|
||||||
|
|
||||||
|
## Tips
|
||||||
|
|
||||||
|
1. **Reuse common translations**: Put frequently used strings (buttons, actions, status messages) in the `common` section
|
||||||
|
2. **Keep keys descriptive**: Use `sensors.bme280.title` instead of `s1` for maintainability
|
||||||
|
3. **Test both languages**: Always verify that both French and English translations display correctly
|
||||||
|
4. **Fallback text**: Always provide fallback text in HTML for graceful degradation
|
||||||
|
|
||||||
|
## Support
|
||||||
|
|
||||||
|
For issues or questions about the i18n system, refer to the implementation in:
|
||||||
|
- `/html/assets/js/i18n.js` - Core translation library
|
||||||
|
- `/html/lang/fr.json` - French translations
|
||||||
|
- `/html/lang/en.json` - English translations
|
||||||
|
- `/html/sensors.html` - Example implementation
|
||||||
64
html/lang/en.json
Normal file
64
html/lang/en.json
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
{
|
||||||
|
"common": {
|
||||||
|
"getData": "Get Data",
|
||||||
|
"loading": "Loading...",
|
||||||
|
"error": "Error",
|
||||||
|
"startRecording": "Start recording",
|
||||||
|
"stopRecording": "Stop recording"
|
||||||
|
},
|
||||||
|
"sensors": {
|
||||||
|
"title": "Measurement Sensors",
|
||||||
|
"description": "Your NebuleAir sensor is equipped with one or more probes that measure environmental variables. Measurements are automatic, but you can verify their operation here.",
|
||||||
|
"npm": {
|
||||||
|
"title": "NextPM",
|
||||||
|
"description": "Particulate matter sensor.",
|
||||||
|
"headerUart": "UART Port"
|
||||||
|
},
|
||||||
|
"bme280": {
|
||||||
|
"title": "BME280 Temp/Humidity Sensor",
|
||||||
|
"description": "Temperature and humidity sensor on I2C port.",
|
||||||
|
"headerI2c": "I2C Port",
|
||||||
|
"temp": "Temperature",
|
||||||
|
"hum": "Humidity",
|
||||||
|
"press": "Pressure"
|
||||||
|
},
|
||||||
|
"noise": {
|
||||||
|
"title": "Decibel Meter",
|
||||||
|
"description": "Noise sensor on I2C port.",
|
||||||
|
"headerI2c": "I2C Port"
|
||||||
|
},
|
||||||
|
"envea": {
|
||||||
|
"title": "Envea Probe",
|
||||||
|
"description": "Gas sensor."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"wifi": {
|
||||||
|
"title": "WIFI Connection",
|
||||||
|
"description": "WIFI connection is not mandatory but it allows you to perform updates and enable remote control.",
|
||||||
|
"status": "Status",
|
||||||
|
"connected": "Connected",
|
||||||
|
"hotspot": "Hotspot",
|
||||||
|
"disconnected": "Disconnected",
|
||||||
|
"scan": "Scan",
|
||||||
|
"connect": "Connect",
|
||||||
|
"enterPassword": "Enter password for"
|
||||||
|
},
|
||||||
|
"admin": {
|
||||||
|
"title": "Administration",
|
||||||
|
"parameters": "Parameters (config)",
|
||||||
|
"deviceName": "Device Name",
|
||||||
|
"deviceID": "Device ID",
|
||||||
|
"modemVersion": "Modem Version"
|
||||||
|
},
|
||||||
|
"sidebar": {
|
||||||
|
"home": "Home",
|
||||||
|
"sensors": "Sensors",
|
||||||
|
"database": "Database",
|
||||||
|
"modem4g": "4G Modem",
|
||||||
|
"wifi": "WIFI",
|
||||||
|
"logs": "Logs",
|
||||||
|
"map": "Map",
|
||||||
|
"terminal": "Terminal",
|
||||||
|
"admin": "Admin"
|
||||||
|
}
|
||||||
|
}
|
||||||
64
html/lang/fr.json
Normal file
64
html/lang/fr.json
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
{
|
||||||
|
"common": {
|
||||||
|
"getData": "Obtenir les données",
|
||||||
|
"loading": "Chargement...",
|
||||||
|
"error": "Erreur",
|
||||||
|
"startRecording": "Démarrer l'enregistrement",
|
||||||
|
"stopRecording": "Arrêter l'enregistrement"
|
||||||
|
},
|
||||||
|
"sensors": {
|
||||||
|
"title": "Les sondes de mesure",
|
||||||
|
"description": "Votre capteur NebuleAir est équipé de une ou plusieurs sondes qui permettent de mesurer certaines variables environnementales. La mesure est automatique mais vous pouvez ici vous assurer de leur bon fonctionnement.",
|
||||||
|
"npm": {
|
||||||
|
"title": "NextPM",
|
||||||
|
"description": "Capteur particules fines.",
|
||||||
|
"headerUart": "Port UART"
|
||||||
|
},
|
||||||
|
"bme280": {
|
||||||
|
"title": "Capteur Temp/Humidité BME280",
|
||||||
|
"description": "Capteur température et humidité sur le port I2C.",
|
||||||
|
"headerI2c": "Port I2C",
|
||||||
|
"temp": "Température",
|
||||||
|
"hum": "Humidité",
|
||||||
|
"press": "Pression"
|
||||||
|
},
|
||||||
|
"noise": {
|
||||||
|
"title": "Sonomètre",
|
||||||
|
"description": "Capteur bruit sur le port I2C.",
|
||||||
|
"headerI2c": "Port I2C"
|
||||||
|
},
|
||||||
|
"envea": {
|
||||||
|
"title": "Sonde Envea",
|
||||||
|
"description": "Capteur gaz."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"wifi": {
|
||||||
|
"title": "Connexion WIFI",
|
||||||
|
"description": "La connexion WIFI n'est pas obligatoire mais elle vous permet d'effectuer des mises à jour et d'activer le contrôle à distance.",
|
||||||
|
"status": "Statut",
|
||||||
|
"connected": "Connecté",
|
||||||
|
"hotspot": "Point d'accès",
|
||||||
|
"disconnected": "Déconnecté",
|
||||||
|
"scan": "Scanner",
|
||||||
|
"connect": "Se connecter",
|
||||||
|
"enterPassword": "Entrer le mot de passe pour"
|
||||||
|
},
|
||||||
|
"admin": {
|
||||||
|
"title": "Administration",
|
||||||
|
"parameters": "Paramètres (config)",
|
||||||
|
"deviceName": "Nom de l'appareil",
|
||||||
|
"deviceID": "ID de l'appareil",
|
||||||
|
"modemVersion": "Version du modem"
|
||||||
|
},
|
||||||
|
"sidebar": {
|
||||||
|
"home": "Accueil",
|
||||||
|
"sensors": "Capteurs",
|
||||||
|
"database": "Base de données",
|
||||||
|
"modem4g": "Modem 4G",
|
||||||
|
"wifi": "WIFI",
|
||||||
|
"logs": "Logs",
|
||||||
|
"map": "Carte",
|
||||||
|
"terminal": "Terminal",
|
||||||
|
"admin": "Admin"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -77,6 +77,46 @@ if ($type == "get_config_sqlite") {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GET language preference from SQLite
|
||||||
|
if ($type == "get_language") {
|
||||||
|
try {
|
||||||
|
$db = new PDO("sqlite:$database_path");
|
||||||
|
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
|
||||||
|
|
||||||
|
$stmt = $db->prepare("SELECT value FROM config_table WHERE key = 'language'");
|
||||||
|
$stmt->execute();
|
||||||
|
$result = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
$language = $result ? $result['value'] : 'fr'; // Default to French
|
||||||
|
echo json_encode(['language' => $language]);
|
||||||
|
} catch (Exception $e) {
|
||||||
|
echo json_encode(['language' => 'fr', 'error' => $e->getMessage()]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SET language preference in SQLite
|
||||||
|
if ($type == "set_language") {
|
||||||
|
$language = $_GET['language'];
|
||||||
|
|
||||||
|
// Validate language (only allow fr or en)
|
||||||
|
if (!in_array($language, ['fr', 'en'])) {
|
||||||
|
echo json_encode(['success' => false, 'error' => 'Invalid language']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$db = new PDO("sqlite:$database_path");
|
||||||
|
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
|
||||||
|
|
||||||
|
$stmt = $db->prepare("UPDATE config_table SET value = ? WHERE key = 'language'");
|
||||||
|
$stmt->execute([$language]);
|
||||||
|
|
||||||
|
echo json_encode(['success' => true, 'language' => $language]);
|
||||||
|
} catch (Exception $e) {
|
||||||
|
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|
||||||
*/
|
*/
|
||||||
@@ -343,6 +383,13 @@ if ($type == "sara_ping") {
|
|||||||
echo $output;
|
echo $output;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($type == "sara_psd_setup") {
|
||||||
|
$command = 'sudo /usr/bin/python3 /var/www/nebuleair_pro_4g/SARA/R5/setPDP.py';
|
||||||
|
$output = shell_exec($command);
|
||||||
|
echo $output;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
if ($type == "git_pull") {
|
if ($type == "git_pull") {
|
||||||
$command = 'sudo git pull';
|
$command = 'sudo git pull';
|
||||||
$output = shell_exec($command);
|
$output = shell_exec($command);
|
||||||
@@ -656,6 +703,20 @@ if ($type == "sara_connectNetwork") {
|
|||||||
echo $output;
|
echo $output;
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
#Setup Hostnmae
|
||||||
|
if ($type == "sara_setupHostname") {
|
||||||
|
$port=$_GET['port'];
|
||||||
|
$server_hostname=$_GET['networkID'];
|
||||||
|
$profileID=$_GET['profileID'];
|
||||||
|
|
||||||
|
//echo "connecting to network... please wait...";
|
||||||
|
$command = 'sudo /usr/bin/python3 /var/www/nebuleair_pro_4g/SARA/sara_setURL.py ' . $port . ' ' . $server_hostname . ' ' . $profileID;
|
||||||
|
$output = shell_exec($command);
|
||||||
|
echo $output;
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#SET THE URL for messaging (profile id 2)
|
#SET THE URL for messaging (profile id 2)
|
||||||
@@ -771,33 +832,39 @@ if ($type == "wifi_connect") {
|
|||||||
|
|
||||||
if ($type == "wifi_scan") {
|
if ($type == "wifi_scan") {
|
||||||
|
|
||||||
// Set the path to your CSV file
|
// Perform live WiFi scan instead of reading stale CSV file
|
||||||
$csvFile = '/var/www/nebuleair_pro_4g/wifi_list.csv';
|
$output = shell_exec('nmcli -f SSID,SIGNAL,SECURITY device wifi list ifname wlan0 2>/dev/null');
|
||||||
|
|
||||||
// Initialize an array to hold the JSON data
|
// Initialize an array to hold the JSON data
|
||||||
$jsonData = [];
|
$jsonData = [];
|
||||||
|
|
||||||
// Open the CSV file for reading
|
if ($output) {
|
||||||
if (($handle = fopen($csvFile, 'r')) !== false) {
|
// Split the output into lines
|
||||||
// Get the headers from the first row
|
$lines = explode("\n", trim($output));
|
||||||
$headers = fgetcsv($handle);
|
|
||||||
|
// Skip the header line and process each network
|
||||||
// Loop through the rest of the rows
|
for ($i = 1; $i < count($lines); $i++) {
|
||||||
while (($row = fgetcsv($handle)) !== false) {
|
$line = trim($lines[$i]);
|
||||||
// Combine headers with row data to create an associative array
|
if (empty($line)) continue;
|
||||||
$jsonData[] = array_combine($headers, $row);
|
|
||||||
|
// Split by multiple spaces (nmcli uses column formatting)
|
||||||
|
$parts = preg_split('/\s{2,}/', $line, 3);
|
||||||
|
|
||||||
|
if (count($parts) >= 2) {
|
||||||
|
$jsonData[] = [
|
||||||
|
'SSID' => trim($parts[0]),
|
||||||
|
'SIGNAL' => trim($parts[1]),
|
||||||
|
'SECURITY' => isset($parts[2]) ? trim($parts[2]) : '--'
|
||||||
|
];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close the file handle
|
|
||||||
fclose($handle);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set the content type to JSON
|
// Set the content type to JSON
|
||||||
header('Content-Type: application/json');
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
// Convert the array to JSON format and output it
|
// Convert the array to JSON format and output it
|
||||||
echo json_encode($jsonData, JSON_PRETTY_PRINT);
|
echo json_encode($jsonData, JSON_PRETTY_PRINT);
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1017,6 +1084,10 @@ if ($type == "get_systemd_services") {
|
|||||||
'description' => 'Tracks solar panel and battery status',
|
'description' => 'Tracks solar panel and battery status',
|
||||||
'frequency' => 'Every 2 minutes'
|
'frequency' => 'Every 2 minutes'
|
||||||
],
|
],
|
||||||
|
'nebuleair-noise-data.timer' => [
|
||||||
|
'description' => 'Get Data from noise sensor',
|
||||||
|
'frequency' => 'Every minute'
|
||||||
|
],
|
||||||
'nebuleair-db-cleanup-data.timer' => [
|
'nebuleair-db-cleanup-data.timer' => [
|
||||||
'description' => 'Cleans up old data from database',
|
'description' => 'Cleans up old data from database',
|
||||||
'frequency' => 'Daily'
|
'frequency' => 'Daily'
|
||||||
@@ -1198,3 +1269,96 @@ if ($type == "toggle_systemd_service") {
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
_____ ____ _ _ _
|
||||||
|
| ____|_ ____ _____ __ _ | _ \ ___| |_ ___ ___| |_(_) ___ _ __
|
||||||
|
| _| | '_ \ \ / / _ \/ _` | | | | |/ _ \ __/ _ \/ __| __| |/ _ \| '_ \
|
||||||
|
| |___| | | \ V / __/ (_| | | |_| | __/ || __/ (__| |_| | (_) | | | |
|
||||||
|
|_____|_| |_|\_/ \___|\__,_| |____/ \___|\__\___|\___|\__|_|\___/|_| |_|
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Detect Envea devices on specified port
|
||||||
|
if ($type == "detect_envea_device") {
|
||||||
|
$port = $_GET['port'] ?? null;
|
||||||
|
|
||||||
|
if (empty($port)) {
|
||||||
|
echo json_encode([
|
||||||
|
'success' => false,
|
||||||
|
'error' => 'No port specified'
|
||||||
|
]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate port name (security check)
|
||||||
|
$allowedPorts = ['ttyAMA2', 'ttyAMA3', 'ttyAMA4', 'ttyAMA5'];
|
||||||
|
|
||||||
|
if (!in_array($port, $allowedPorts)) {
|
||||||
|
echo json_encode([
|
||||||
|
'success' => false,
|
||||||
|
'error' => 'Invalid port name'
|
||||||
|
]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Execute the envea detection script
|
||||||
|
$command = "sudo /usr/bin/python3 /var/www/nebuleair_pro_4g/envea/read_ref.py " . escapeshellarg($port) . " 2>&1";
|
||||||
|
$output = shell_exec($command);
|
||||||
|
|
||||||
|
// Check if we got any meaningful output
|
||||||
|
$detected = false;
|
||||||
|
$device_info = '';
|
||||||
|
$raw_data = $output;
|
||||||
|
|
||||||
|
if (!empty($output)) {
|
||||||
|
// Look for indicators that a device is connected
|
||||||
|
if (strpos($output, 'Connexion ouverte') !== false) {
|
||||||
|
// Connection was successful
|
||||||
|
if (strpos($output, 'Données reçues brutes') !== false &&
|
||||||
|
strpos($output, 'b\'\'') === false) {
|
||||||
|
// We received actual data (not empty)
|
||||||
|
$detected = true;
|
||||||
|
$device_info = 'Envea CAIRSENS Device';
|
||||||
|
|
||||||
|
// Try to extract device type from ASCII data if available
|
||||||
|
if (preg_match('/Valeurs converties en ASCII : (.+)/', $output, $matches)) {
|
||||||
|
$ascii_data = trim($matches[1]);
|
||||||
|
if (!empty($ascii_data) && $ascii_data !== '........') {
|
||||||
|
$device_info = "Envea Device: " . $ascii_data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Connection successful but no data
|
||||||
|
$device_info = 'Port accessible but no Envea device detected';
|
||||||
|
}
|
||||||
|
} else if (strpos($output, 'Erreur de connexion série') !== false) {
|
||||||
|
// Serial connection error
|
||||||
|
$device_info = 'Serial connection error - port may be busy or not available';
|
||||||
|
} else {
|
||||||
|
// Other output
|
||||||
|
$device_info = 'Unexpected response from port';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// No output at all
|
||||||
|
$device_info = 'No response from port';
|
||||||
|
}
|
||||||
|
|
||||||
|
echo json_encode([
|
||||||
|
'success' => true,
|
||||||
|
'port' => $port,
|
||||||
|
'detected' => $detected,
|
||||||
|
'device_info' => $device_info,
|
||||||
|
'data' => $raw_data,
|
||||||
|
'timestamp' => date('Y-m-d H:i:s')
|
||||||
|
], JSON_PRETTY_PRINT);
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
echo json_encode([
|
||||||
|
'success' => false,
|
||||||
|
'error' => 'Script execution failed: ' . $e->getMessage(),
|
||||||
|
'port' => $port
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -90,6 +90,8 @@
|
|||||||
<script src="assets/jquery/jquery-3.7.1.min.js"></script>
|
<script src="assets/jquery/jquery-3.7.1.min.js"></script>
|
||||||
<!-- Link Bootstrap JS and Popper.js locally -->
|
<!-- Link Bootstrap JS and Popper.js locally -->
|
||||||
<script src="assets/js/bootstrap.bundle.js"></script>
|
<script src="assets/js/bootstrap.bundle.js"></script>
|
||||||
|
<!-- i18n translation system -->
|
||||||
|
<script src="assets/js/i18n.js"></script>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
document.addEventListener('DOMContentLoaded', function () {
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
|
|||||||
@@ -67,7 +67,7 @@
|
|||||||
-->
|
-->
|
||||||
<div class="row mb-3">
|
<div class="row mb-3">
|
||||||
|
|
||||||
<div class="col-sm-3">
|
<div class="col-sm-2">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
|
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
@@ -252,6 +252,34 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="col-sm-4">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body">
|
||||||
|
<p class="card-text">Setup PSD connection.</p>
|
||||||
|
<button class="btn btn-primary" onclick="PSD_setup()">Start</button>
|
||||||
|
<div id="loading_PSD" class="spinner-border spinner-border-sm" style="display: none;" role="status"></div>
|
||||||
|
<div id="response_psd_setup"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-sm-3">
|
||||||
|
<div class="card">
|
||||||
|
|
||||||
|
<div class="card-body">
|
||||||
|
<p class="card-text">Setup Server Hostname.</p>
|
||||||
|
<div class="input-group input-group-sm mb-3">
|
||||||
|
<span class="input-group-text" id="inputGroup-sizing-sm">Server name</span>
|
||||||
|
<input type="text" id="messageInput_server" class="form-control" aria-label="Sizing example input" aria-describedby="inputGroup-sizing-sm">
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-primary" onclick="setupServerHostname('ttyAMA2', document.getElementById('messageInput_server').value, 0)">Set</button>
|
||||||
|
<div id="loading_serverHostname" class="spinner-border spinner-border-sm" style="display: none;" role="status"></div>
|
||||||
|
<div id="response_serverHostname"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
@@ -329,6 +357,8 @@
|
|||||||
<script src="assets/jquery/jquery-3.7.1.min.js"></script>
|
<script src="assets/jquery/jquery-3.7.1.min.js"></script>
|
||||||
<!-- Link Bootstrap JS and Popper.js locally -->
|
<!-- Link Bootstrap JS and Popper.js locally -->
|
||||||
<script src="assets/js/bootstrap.bundle.js"></script>
|
<script src="assets/js/bootstrap.bundle.js"></script>
|
||||||
|
<!-- i18n translation system -->
|
||||||
|
<script src="assets/js/i18n.js"></script>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
document.addEventListener('DOMContentLoaded', function () {
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
@@ -453,6 +483,8 @@ function getData_saraR4(port, command, timeout){
|
|||||||
console.log(safeCommand);
|
console.log(safeCommand);
|
||||||
|
|
||||||
$("#loading_"+port+"_"+safeCommand).show();
|
$("#loading_"+port+"_"+safeCommand).show();
|
||||||
|
$("#response_"+port+"_"+safeCommand).empty();
|
||||||
|
|
||||||
|
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: 'launcher.php?type=sara&port='+port+'&command='+encodeURIComponent(command)+'&timeout='+timeout,
|
url: 'launcher.php?type=sara&port='+port+'&command='+encodeURIComponent(command)+'&timeout='+timeout,
|
||||||
@@ -558,6 +590,28 @@ function connectNetwork_saraR4(port, networkID, timeout){
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setupServerHostname(port, serverName, timeout){
|
||||||
|
console.log(" Setupt server hostname "+serverName+"):");
|
||||||
|
$("#loading_serverHostname").show();
|
||||||
|
$.ajax({
|
||||||
|
url: 'launcher.php?type=sara_setupHostname&port='+port+'&networkID='+encodeURIComponent(serverName)+'&profileID=0',
|
||||||
|
dataType:'text',
|
||||||
|
//dataType: 'json', // Specify that you expect a JSON response
|
||||||
|
method: 'GET', // Use GET or POST depending on your needs
|
||||||
|
success: function(response) {
|
||||||
|
console.log(response);
|
||||||
|
$("#loading_serverHostname").hide();
|
||||||
|
// Replace newline characters with <br> tags
|
||||||
|
const formattedResponse = response.replace(/\n/g, "<br>");
|
||||||
|
$("#response_serverHostname").html(formattedResponse);
|
||||||
|
|
||||||
|
},
|
||||||
|
error: function(xhr, status, error) {
|
||||||
|
console.error('AJAX request failed:', status, error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function mqtt_getConfig_saraR4(port, timeout){
|
function mqtt_getConfig_saraR4(port, timeout){
|
||||||
console.log("GET MQTT config (port "+port+"):");
|
console.log("GET MQTT config (port "+port+"):");
|
||||||
$("#loading_mqtt_getConfig").show();
|
$("#loading_mqtt_getConfig").show();
|
||||||
@@ -671,6 +725,7 @@ function setURL_saraR4(port, url){
|
|||||||
|
|
||||||
function ping_test(port, url){
|
function ping_test(port, url){
|
||||||
console.log("Test ping to data.nebuleair.fr:");
|
console.log("Test ping to data.nebuleair.fr:");
|
||||||
|
$("#response_ping").empty();
|
||||||
$("#loading_ping").show();
|
$("#loading_ping").show();
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: 'launcher.php?type=sara_ping',
|
url: 'launcher.php?type=sara_ping',
|
||||||
@@ -690,6 +745,27 @@ function ping_test(port, url){
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function PSD_setup(port, url){
|
||||||
|
console.log("Setup PSD connection:");
|
||||||
|
$("#loading_PSD").show();
|
||||||
|
$.ajax({
|
||||||
|
url: 'launcher.php?type=sara_psd_setup',
|
||||||
|
dataType: 'text',
|
||||||
|
//dataType: 'json', // Specify that you expect a JSON response
|
||||||
|
method: 'GET', // Use GET or POST depending on your needs
|
||||||
|
success: function(response) {
|
||||||
|
console.log(response);
|
||||||
|
$("#loading_PSD").hide();
|
||||||
|
// Replace newline characters with <br> tags
|
||||||
|
const formattedResponse = response.replace(/\n/g, "<br>");
|
||||||
|
$("#response_psd_setup").html(formattedResponse);
|
||||||
|
},
|
||||||
|
error: function(xhr, status, error) {
|
||||||
|
console.error('AJAX request failed:', status, error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function writeMessage_saraR4(port, message, type){
|
function writeMessage_saraR4(port, message, type){
|
||||||
console.log(type +" message to SARA R4 memory (port "+port+" and message "+message+"):");
|
console.log(type +" message to SARA R4 memory (port "+port+" and message "+message+"):");
|
||||||
$("#loading_"+port+"_message_write").show();
|
$("#loading_"+port+"_message_write").show();
|
||||||
|
|||||||
@@ -49,11 +49,11 @@
|
|||||||
</aside>
|
</aside>
|
||||||
<!-- Main content -->
|
<!-- Main content -->
|
||||||
<main class="col-md-10 ms-sm-auto col-lg-11 offset-md-2 offset-lg-1 px-md-4">
|
<main class="col-md-10 ms-sm-auto col-lg-11 offset-md-2 offset-lg-1 px-md-4">
|
||||||
<h1 class="mt-4">Les sondes de mesure</h1>
|
<h1 class="mt-4" data-i18n="sensors.title">Les sondes de mesure</h1>
|
||||||
<p>Votre capteur NebuleAir est équipé de une ou plusieurs sondes qui permettent de mesurer certaines variables environnementales. La mesure
|
<p data-i18n="sensors.description">Votre capteur NebuleAir est équipé de une ou plusieurs sondes qui permettent de mesurer certaines variables environnementales. La mesure
|
||||||
est automatique mais vous pouvez ici vous assurer de leur bon fonctionnement.
|
est automatique mais vous pouvez ici vous assurer de leur bon fonctionnement.
|
||||||
</p>
|
</p>
|
||||||
<div class="row mb-3" id="card-container"></div>
|
<div class="row mb-3" id="card-container"></div>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -64,6 +64,8 @@
|
|||||||
<script src="assets/jquery/jquery-3.7.1.min.js"></script>
|
<script src="assets/jquery/jquery-3.7.1.min.js"></script>
|
||||||
<!-- Link Bootstrap JS and Popper.js locally -->
|
<!-- Link Bootstrap JS and Popper.js locally -->
|
||||||
<script src="assets/js/bootstrap.bundle.js"></script>
|
<script src="assets/js/bootstrap.bundle.js"></script>
|
||||||
|
<!-- i18n translation system -->
|
||||||
|
<script src="assets/js/i18n.js"></script>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
document.addEventListener('DOMContentLoaded', function () {
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
@@ -101,7 +103,7 @@ function getNPM_values(port){
|
|||||||
|
|
||||||
$("#loading_"+port).hide();
|
$("#loading_"+port).hide();
|
||||||
// Create an array of the desired keys
|
// Create an array of the desired keys
|
||||||
const keysToShow = ["PM1", "PM25", "PM10"];
|
const keysToShow = ["PM1", "PM25", "PM10","message"];
|
||||||
// Error messages mapping
|
// Error messages mapping
|
||||||
const errorMessages = {
|
const errorMessages = {
|
||||||
"notReady": "Sensor is not ready",
|
"notReady": "Sensor is not ready",
|
||||||
@@ -273,6 +275,8 @@ function getBME280_values(){
|
|||||||
window.onload = function() {
|
window.onload = function() {
|
||||||
|
|
||||||
//NEW way to get config (SQLite)
|
//NEW way to get config (SQLite)
|
||||||
|
let mainConfig = {}; // Store main config for use in sensor card creation
|
||||||
|
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: 'launcher.php?type=get_config_sqlite',
|
url: 'launcher.php?type=get_config_sqlite',
|
||||||
dataType:'json',
|
dataType:'json',
|
||||||
@@ -282,43 +286,42 @@ $.ajax({
|
|||||||
console.log("Getting SQLite config table:");
|
console.log("Getting SQLite config table:");
|
||||||
console.log(response);
|
console.log(response);
|
||||||
|
|
||||||
|
mainConfig = response; // Store for later use
|
||||||
|
|
||||||
//device name_side bar
|
//device name_side bar
|
||||||
const elements = document.querySelectorAll('.sideBar_sensorName');
|
const elements = document.querySelectorAll('.sideBar_sensorName');
|
||||||
elements.forEach((element) => {
|
elements.forEach((element) => {
|
||||||
element.innerText = response.deviceName;
|
element.innerText = response.deviceName;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// After getting main config, create sensor cards
|
||||||
|
createSensorCards(mainConfig);
|
||||||
|
|
||||||
},
|
},
|
||||||
error: function(xhr, status, error) {
|
error: function(xhr, status, error) {
|
||||||
console.error('AJAX request failed:', status, error);
|
console.error('AJAX request failed:', status, error);
|
||||||
}
|
}
|
||||||
});//end AJAX
|
});//end AJAX
|
||||||
|
|
||||||
//getting config_scripts table
|
//Function to create sensor cards based on config
|
||||||
$.ajax({
|
function createSensorCards(config) {
|
||||||
url: 'launcher.php?type=get_config_scripts_sqlite',
|
console.log("Creating sensor cards with config:");
|
||||||
dataType:'json',
|
console.log(config);
|
||||||
//dataType: 'json', // Specify that you expect a JSON response
|
|
||||||
method: 'GET', // Use GET or POST depending on your needs
|
|
||||||
success: function(response) {
|
|
||||||
console.log("Getting SQLite config scripts table:");
|
|
||||||
console.log(response);
|
|
||||||
|
|
||||||
const container = document.getElementById('card-container'); // Conteneur des cartes
|
const container = document.getElementById('card-container'); // Conteneur des cartes
|
||||||
|
|
||||||
//creates NPM card
|
//creates NPM card (by default)
|
||||||
if (response["NPM/get_data_modbus_v3.py"]) {
|
|
||||||
const cardHTML = `
|
const cardHTML = `
|
||||||
<div class="col-sm-3">
|
<div class="col-sm-3">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-header">
|
<div class="card-header" data-i18n="sensors.npm.headerUart">
|
||||||
Port UART
|
Port UART
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h5 class="card-title">NextPM</h5>
|
<h5 class="card-title" data-i18n="sensors.npm.title">NextPM</h5>
|
||||||
<p class="card-text">Capteur particules fines.</p>
|
<p class="card-text" data-i18n="sensors.npm.description">Capteur particules fines.</p>
|
||||||
<button class="btn btn-primary" onclick="getNPM_values('ttyAMA5')">Get Data</button>
|
<button class="btn btn-primary" onclick="getNPM_values('ttyAMA5')" data-i18n="common.getData">Get Data</button>
|
||||||
<br>
|
<br>
|
||||||
<div id="loading_ttyAMA5" class="spinner-border spinner-border-sm" style="display: none;" role="status"></div>
|
<div id="loading_ttyAMA5" class="spinner-border spinner-border-sm" style="display: none;" role="status"></div>
|
||||||
<table class="table table-striped-columns">
|
<table class="table table-striped-columns">
|
||||||
@@ -327,22 +330,22 @@ error: function(xhr, status, error) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>`;
|
</div>`;
|
||||||
|
|
||||||
container.innerHTML += cardHTML; // Add the I2C card if condition is met
|
container.innerHTML += cardHTML; // Add the I2C card if condition is met
|
||||||
}
|
|
||||||
|
|
||||||
//creates i2c BME280 card
|
//creates i2c BME280 card
|
||||||
if (response["BME280/get_data_v2.py"]) {
|
if (config.BME280) {
|
||||||
const i2C_BME_HTML = `
|
const i2C_BME_HTML = `
|
||||||
<div class="col-sm-3">
|
<div class="col-sm-3">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-header">
|
<div class="card-header" data-i18n="sensors.bme280.headerI2c">
|
||||||
Port I2C
|
Port I2C
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h5 class="card-title">BME280 Temp/Hum sensor</h5>
|
<h5 class="card-title" data-i18n="sensors.bme280.title">BME280 Temp/Hum sensor</h5>
|
||||||
<p class="card-text">Capteur température et humidité sur le port I2C.</p>
|
<p class="card-text" data-i18n="sensors.bme280.description">Capteur température et humidité sur le port I2C.</p>
|
||||||
<button class="btn btn-primary mb-1" onclick="getBME280_values()">Get Data</button>
|
<button class="btn btn-primary mb-1" onclick="getBME280_values()" data-i18n="common.getData">Get Data</button>
|
||||||
<br>
|
<br>
|
||||||
<div id="loading_BME280" class="spinner-border spinner-border-sm" style="display: none;" role="status"></div>
|
<div id="loading_BME280" class="spinner-border spinner-border-sm" style="display: none;" role="status"></div>
|
||||||
<table class="table table-striped-columns">
|
<table class="table table-striped-columns">
|
||||||
@@ -351,25 +354,25 @@ error: function(xhr, status, error) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>`;
|
</div>`;
|
||||||
|
|
||||||
container.innerHTML += i2C_BME_HTML; // Add the I2C card if condition is met
|
container.innerHTML += i2C_BME_HTML; // Add the I2C card if condition is met
|
||||||
}
|
}
|
||||||
|
|
||||||
//creates i2c sound card
|
//creates i2c sound card
|
||||||
if (response.i2C_sound) {
|
if (config.NOISE) {
|
||||||
const i2C_HTML = `
|
const i2C_HTML = `
|
||||||
<div class="col-sm-3">
|
<div class="col-sm-3">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-header">
|
<div class="card-header" data-i18n="sensors.noise.headerI2c">
|
||||||
Port I2C
|
Port I2C
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h5 class="card-title">Decibel Meter</h5>
|
<h5 class="card-title" data-i18n="sensors.noise.title">Decibel Meter</h5>
|
||||||
<p class="card-text">Capteur bruit sur le port I2C.</p>
|
<p class="card-text" data-i18n="sensors.noise.description">Capteur bruit sur le port I2C.</p>
|
||||||
<button class="btn btn-primary mb-1" onclick="getNoise_values()">Get Data</button>
|
<button class="btn btn-primary mb-1" onclick="getNoise_values()" data-i18n="common.getData">Get Data</button>
|
||||||
<br>
|
<br>
|
||||||
<button class="btn btn-success" onclick="startNoise()">Start recording</button>
|
<button class="btn btn-success" onclick="startNoise()" data-i18n="common.startRecording">Start recording</button>
|
||||||
<button class="btn btn-danger" onclick="stopNoise()">Stop recording</button>
|
<button class="btn btn-danger" onclick="stopNoise()" data-i18n="common.stopRecording">Stop recording</button>
|
||||||
<div id="loading_noise" class="spinner-border spinner-border-sm" style="display: none;" role="status"></div>
|
<div id="loading_noise" class="spinner-border spinner-border-sm" style="display: none;" role="status"></div>
|
||||||
<table class="table table-striped-columns">
|
<table class="table table-striped-columns">
|
||||||
<tbody id="data-table-body_noise"></tbody>
|
<tbody id="data-table-body_noise"></tbody>
|
||||||
@@ -377,13 +380,13 @@ error: function(xhr, status, error) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>`;
|
</div>`;
|
||||||
|
|
||||||
container.innerHTML += i2C_HTML; // Add the I2C card if condition is met
|
container.innerHTML += i2C_HTML; // Add the I2C card if condition is met
|
||||||
}
|
}
|
||||||
|
|
||||||
//Si on a des SONDES ENVEA connectée il faut faire un deuxième call dans la table envea_sondes_table
|
//Si on a des SONDES ENVEA connectée il faut faire un deuxième call dans la table envea_sondes_table
|
||||||
//creates ENVEA cards
|
//creates ENVEA cards
|
||||||
if (response["envea/read_value_v2.py"]) {
|
if (config.envea) {
|
||||||
console.log("Need to display ENVEA sondes");
|
console.log("Need to display ENVEA sondes");
|
||||||
//getting config_scripts table
|
//getting config_scripts table
|
||||||
$.ajax({
|
$.ajax({
|
||||||
@@ -408,8 +411,8 @@ error: function(xhr, status, error) {
|
|||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h5 class="card-title">Sonde Envea ${name}</h5>
|
<h5 class="card-title">Sonde Envea ${name}</h5>
|
||||||
<p class="card-text">Capteur gas.</p>
|
<p class="card-text" data-i18n="sensors.envea.description">Capteur gas.</p>
|
||||||
<button class="btn btn-primary" onclick="getENVEA_values('${port}','${name}')">Get Data</button>
|
<button class="btn btn-primary" onclick="getENVEA_values('${port}','${name}')" data-i18n="common.getData">Get Data</button>
|
||||||
<div id="loading_envea${name}" class="spinner-border spinner-border-sm" style="display: none;" role="status"></div>
|
<div id="loading_envea${name}" class="spinner-border spinner-border-sm" style="display: none;" role="status"></div>
|
||||||
<table class="table table-striped-columns">
|
<table class="table table-striped-columns">
|
||||||
<tbody id="data-table-body_envea${name}"></tbody>
|
<tbody id="data-table-body_envea${name}"></tbody>
|
||||||
@@ -419,6 +422,9 @@ error: function(xhr, status, error) {
|
|||||||
</div>`;
|
</div>`;
|
||||||
container.innerHTML += cardHTML; // Ajouter la carte au conteneur
|
container.innerHTML += cardHTML; // Ajouter la carte au conteneur
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Apply translations to dynamically created Envea cards
|
||||||
|
i18n.applyTranslations();
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -429,14 +435,12 @@ error: function(xhr, status, error) {
|
|||||||
});//end AJAX envea Sondes
|
});//end AJAX envea Sondes
|
||||||
|
|
||||||
|
|
||||||
}//end if
|
}//end if envea
|
||||||
|
|
||||||
|
|
||||||
},
|
// Apply translations to all dynamically created sensor cards
|
||||||
error: function(xhr, status, error) {
|
i18n.applyTranslations();
|
||||||
console.error('AJAX request failed:', status, error);
|
|
||||||
}
|
} // end createSensorCards function
|
||||||
});//end AJAX (config_scripts)
|
|
||||||
|
|
||||||
//get local RTC
|
//get local RTC
|
||||||
$.ajax({
|
$.ajax({
|
||||||
|
|||||||
@@ -4,34 +4,34 @@
|
|||||||
<svg class="bi me-2" width="16" height="16" fill="currentColor" aria-hidden="true">
|
<svg class="bi me-2" width="16" height="16" fill="currentColor" aria-hidden="true">
|
||||||
<use xlink:href="assets/icons/bootstrap-icons.svg#house"></use>
|
<use xlink:href="assets/icons/bootstrap-icons.svg#house"></use>
|
||||||
</svg>
|
</svg>
|
||||||
Home
|
<span data-i18n="sidebar.home">Accueil</span>
|
||||||
</a>
|
</a>
|
||||||
<a class="nav-link text-white" href="sensors.html">
|
<a class="nav-link text-white" href="sensors.html">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-thermometer-sun" viewBox="0 0 16 16">
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-thermometer-sun" viewBox="0 0 16 16">
|
||||||
<path d="M5 12.5a1.5 1.5 0 1 1-2-1.415V2.5a.5.5 0 0 1 1 0v8.585A1.5 1.5 0 0 1 5 12.5"/>
|
<path d="M5 12.5a1.5 1.5 0 1 1-2-1.415V2.5a.5.5 0 0 1 1 0v8.585A1.5 1.5 0 0 1 5 12.5"/>
|
||||||
<path d="M1 2.5a2.5 2.5 0 0 1 5 0v7.55a3.5 3.5 0 1 1-5 0zM3.5 1A1.5 1.5 0 0 0 2 2.5v7.987l-.167.15a2.5 2.5 0 1 0 3.333 0L5 10.486V2.5A1.5 1.5 0 0 0 3.5 1m5 1a.5.5 0 0 1 .5.5v1a.5.5 0 0 1-1 0v-1a.5.5 0 0 1 .5-.5m4.243 1.757a.5.5 0 0 1 0 .707l-.707.708a.5.5 0 1 1-.708-.708l.708-.707a.5.5 0 0 1 .707 0M8 5.5a.5.5 0 0 1 .5-.5 3 3 0 1 1 0 6 .5.5 0 0 1 0-1 2 2 0 0 0 0-4 .5.5 0 0 1-.5-.5M12.5 8a.5.5 0 0 1 .5-.5h1a.5.5 0 1 1 0 1h-1a.5.5 0 0 1-.5-.5m-1.172 2.828a.5.5 0 0 1 .708 0l.707.708a.5.5 0 0 1-.707.707l-.708-.707a.5.5 0 0 1 0-.708M8.5 12a.5.5 0 0 1 .5.5v1a.5.5 0 0 1-1 0v-1a.5.5 0 0 1 .5-.5"/>
|
<path d="M1 2.5a2.5 2.5 0 0 1 5 0v7.55a3.5 3.5 0 1 1-5 0zM3.5 1A1.5 1.5 0 0 0 2 2.5v7.987l-.167.15a2.5 2.5 0 1 0 3.333 0L5 10.486V2.5A1.5 1.5 0 0 0 3.5 1m5 1a.5.5 0 0 1 .5.5v1a.5.5 0 0 1-1 0v-1a.5.5 0 0 1 .5-.5m4.243 1.757a.5.5 0 0 1 0 .707l-.707.708a.5.5 0 1 1-.708-.708l.708-.707a.5.5 0 0 1 .707 0M8 5.5a.5.5 0 0 1 .5-.5 3 3 0 1 1 0 6 .5.5 0 0 1 0-1 2 2 0 0 0 0-4 .5.5 0 0 1-.5-.5M12.5 8a.5.5 0 0 1 .5-.5h1a.5.5 0 1 1 0 1h-1a.5.5 0 0 1-.5-.5m-1.172 2.828a.5.5 0 0 1 .708 0l.707.708a.5.5 0 0 1-.707.707l-.708-.707a.5.5 0 0 1 0-.708M8.5 12a.5.5 0 0 1 .5.5v1a.5.5 0 0 1-1 0v-1a.5.5 0 0 1 .5-.5"/>
|
||||||
</svg>
|
</svg>
|
||||||
Capteurs
|
<span data-i18n="sidebar.sensors">Capteurs</span>
|
||||||
</a>
|
</a>
|
||||||
<a class="nav-link text-white" href="database.html">
|
<a class="nav-link text-white" href="database.html">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-database" viewBox="0 0 16 16">
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-database" viewBox="0 0 16 16">
|
||||||
<path d="M4.318 2.687C5.234 2.271 6.536 2 8 2s2.766.27 3.682.687C12.644 3.125 13 3.627 13 4c0 .374-.356.875-1.318 1.313C10.766 5.729 9.464 6 8 6s-2.766-.27-3.682-.687C3.356 4.875 3 4.373 3 4c0-.374.356-.875 1.318-1.313M13 5.698V7c0 .374-.356.875-1.318 1.313C10.766 8.729 9.464 9 8 9s-2.766-.27-3.682-.687C3.356 7.875 3 7.373 3 7V5.698c.271.202.58.378.904.525C4.978 6.711 6.427 7 8 7s3.022-.289 4.096-.777A5 5 0 0 0 13 5.698M14 4c0-1.007-.875-1.755-1.904-2.223C11.022 1.289 9.573 1 8 1s-3.022.289-4.096.777C2.875 2.245 2 2.993 2 4v9c0 1.007.875 1.755 1.904 2.223C4.978 15.71 6.427 16 8 16s3.022-.289 4.096-.777C13.125 14.755 14 14.007 14 13zm-1 4.698V10c0 .374-.356.875-1.318 1.313C10.766 11.729 9.464 12 8 12s-2.766-.27-3.682-.687C3.356 10.875 3 10.373 3 10V8.698c.271.202.58.378.904.525C4.978 9.71 6.427 10 8 10s3.022-.289 4.096-.777A5 5 0 0 0 13 8.698m0 3V13c0 .374-.356.875-1.318 1.313C10.766 14.729 9.464 15 8 15s-2.766-.27-3.682-.687C3.356 13.875 3 13.373 3 13v-1.302c.271.202.58.378.904.525C4.978 12.71 6.427 13 8 13s3.022-.289 4.096-.777c.324-.147.633-.323.904-.525"/>
|
<path d="M4.318 2.687C5.234 2.271 6.536 2 8 2s2.766.27 3.682.687C12.644 3.125 13 3.627 13 4c0 .374-.356.875-1.318 1.313C10.766 5.729 9.464 6 8 6s-2.766-.27-3.682-.687C3.356 4.875 3 4.373 3 4c0-.374.356-.875 1.318-1.313M13 5.698V7c0 .374-.356.875-1.318 1.313C10.766 8.729 9.464 9 8 9s-2.766-.27-3.682-.687C3.356 7.875 3 7.373 3 7V5.698c.271.202.58.378.904.525C4.978 6.711 6.427 7 8 7s3.022-.289 4.096-.777A5 5 0 0 0 13 5.698M14 4c0-1.007-.875-1.755-1.904-2.223C11.022 1.289 9.573 1 8 1s-3.022.289-4.096.777C2.875 2.245 2 2.993 2 4v9c0 1.007.875 1.755 1.904 2.223C4.978 15.71 6.427 16 8 16s3.022-.289 4.096-.777C13.125 14.755 14 14.007 14 13zm-1 4.698V10c0 .374-.356.875-1.318 1.313C10.766 11.729 9.464 12 8 12s-2.766-.27-3.682-.687C3.356 10.875 3 10.373 3 10V8.698c.271.202.58.378.904.525C4.978 9.71 6.427 10 8 10s3.022-.289 4.096-.777A5 5 0 0 0 13 8.698m0 3V13c0 .374-.356.875-1.318 1.313C10.766 14.729 9.464 15 8 15s-2.766-.27-3.682-.687C3.356 13.875 3 13.373 3 13v-1.302c.271.202.58.378.904.525C4.978 12.71 6.427 13 8 13s3.022-.289 4.096-.777c.324-.147.633-.323.904-.525"/>
|
||||||
</svg>
|
</svg>
|
||||||
|
|
||||||
DataBase
|
<span data-i18n="sidebar.database">Base de données</span>
|
||||||
</a>
|
</a>
|
||||||
<a class="nav-link text-white" href="saraR4.html">
|
<a class="nav-link text-white" href="saraR4.html">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-reception-4" viewBox="0 0 16 16">
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-reception-4" viewBox="0 0 16 16">
|
||||||
<path d="M0 11.5a.5.5 0 0 1 .5-.5h2a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1-.5-.5zm4-3a.5.5 0 0 1 .5-.5h2a.5.5 0 0 1 .5.5v5a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1-.5-.5zm4-3a.5.5 0 0 1 .5-.5h2a.5.5 0 0 1 .5.5v8a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1-.5-.5zm4-3a.5.5 0 0 1 .5-.5h2a.5.5 0 0 1 .5.5v11a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1-.5-.5z"/>
|
<path d="M0 11.5a.5.5 0 0 1 .5-.5h2a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1-.5-.5zm4-3a.5.5 0 0 1 .5-.5h2a.5.5 0 0 1 .5.5v5a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1-.5-.5zm4-3a.5.5 0 0 1 .5-.5h2a.5.5 0 0 1 .5.5v8a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1-.5-.5zm4-3a.5.5 0 0 1 .5-.5h2a.5.5 0 0 1 .5.5v11a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1-.5-.5z"/>
|
||||||
</svg>
|
</svg>
|
||||||
Modem 4G
|
<span data-i18n="sidebar.modem4g">Modem 4G</span>
|
||||||
</a>
|
</a>
|
||||||
<a class="nav-link text-white" href="wifi.html">
|
<a class="nav-link text-white" href="wifi.html">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-wifi" viewBox="0 0 16 16">
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-wifi" viewBox="0 0 16 16">
|
||||||
<path d="M15.384 6.115a.485.485 0 0 0-.047-.736A12.44 12.44 0 0 0 8 3C5.259 3 2.723 3.882.663 5.379a.485.485 0 0 0-.048.736.52.52 0 0 0 .668.05A11.45 11.45 0 0 1 8 4c2.507 0 4.827.802 6.716 2.164.205.148.49.13.668-.049"/>
|
<path d="M15.384 6.115a.485.485 0 0 0-.047-.736A12.44 12.44 0 0 0 8 3C5.259 3 2.723 3.882.663 5.379a.485.485 0 0 0-.048.736.52.52 0 0 0 .668.05A11.45 11.45 0 0 1 8 4c2.507 0 4.827.802 6.716 2.164.205.148.49.13.668-.049"/>
|
||||||
<path d="M13.229 8.271a.482.482 0 0 0-.063-.745A9.46 9.46 0 0 0 8 6c-1.905 0-3.68.56-5.166 1.526a.48.48 0 0 0-.063.745.525.525 0 0 0 .652.065A8.46 8.46 0 0 1 8 7a8.46 8.46 0 0 1 4.576 1.336c.206.132.48.108.653-.065m-2.183 2.183c.226-.226.185-.605-.1-.75A6.5 6.5 0 0 0 8 9c-1.06 0-2.062.254-2.946.704-.285.145-.326.524-.1.75l.015.015c.16.16.407.19.611.09A5.5 5.5 0 0 1 8 10c.868 0 1.69.201 2.42.56.203.1.45.07.61-.091zM9.06 12.44c.196-.196.198-.52-.04-.66A2 2 0 0 0 8 11.5a2 2 0 0 0-1.02.28c-.238.14-.236.464-.04.66l.706.706a.5.5 0 0 0 .707 0l.707-.707z"/>
|
<path d="M13.229 8.271a.482.482 0 0 0-.063-.745A9.46 9.46 0 0 0 8 6c-1.905 0-3.68.56-5.166 1.526a.48.48 0 0 0-.063.745.525.525 0 0 0 .652.065A8.46 8.46 0 0 1 8 7a8.46 8.46 0 0 1 4.576 1.336c.206.132.48.108.653-.065m-2.183 2.183c.226-.226.185-.605-.1-.75A6.5 6.5 0 0 0 8 9c-1.06 0-2.062.254-2.946.704-.285.145-.326.524-.1.75l.015.015c.16.16.407.19.611.09A5.5 5.5 0 0 1 8 10c.868 0 1.69.201 2.42.56.203.1.45.07.61-.091zM9.06 12.44c.196-.196.198-.52-.04-.66A2 2 0 0 0 8 11.5a2 2 0 0 0-1.02.28c-.238.14-.236.464-.04.66l.706.706a.5.5 0 0 0 .707 0l.707-.707z"/>
|
||||||
</svg>
|
</svg>
|
||||||
WIFI
|
<span data-i18n="sidebar.wifi">WIFI</span>
|
||||||
</a>
|
</a>
|
||||||
<a class="nav-link text-white" href="logs.html">
|
<a class="nav-link text-white" href="logs.html">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-journal-code" viewBox="0 0 16 16">
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-journal-code" viewBox="0 0 16 16">
|
||||||
@@ -39,25 +39,27 @@
|
|||||||
<path d="M3 0h10a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2v-1h1v1a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V2a1 1 0 0 0-1-1H3a1 1 0 0 0-1 1v1H1V2a2 2 0 0 1 2-2"/>
|
<path d="M3 0h10a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2v-1h1v1a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V2a1 1 0 0 0-1-1H3a1 1 0 0 0-1 1v1H1V2a2 2 0 0 1 2-2"/>
|
||||||
<path d="M1 5v-.5a.5.5 0 0 1 1 0V5h.5a.5.5 0 0 1 0 1h-2a.5.5 0 0 1 0-1zm0 3v-.5a.5.5 0 0 1 1 0V8h.5a.5.5 0 0 1 0 1h-2a.5.5 0 0 1 0-1zm0 3v-.5a.5.5 0 0 1 1 0v.5h.5a.5.5 0 0 1 0 1h-2a.5.5 0 0 1 0-1z"/>
|
<path d="M1 5v-.5a.5.5 0 0 1 1 0V5h.5a.5.5 0 0 1 0 1h-2a.5.5 0 0 1 0-1zm0 3v-.5a.5.5 0 0 1 1 0V8h.5a.5.5 0 0 1 0 1h-2a.5.5 0 0 1 0-1zm0 3v-.5a.5.5 0 0 1 1 0v.5h.5a.5.5 0 0 1 0 1h-2a.5.5 0 0 1 0-1z"/>
|
||||||
</svg>
|
</svg>
|
||||||
Logs
|
<span data-i18n="sidebar.logs">Logs</span>
|
||||||
</a>
|
</a>
|
||||||
|
<!-- Hidden: Not ready yet
|
||||||
<a class="nav-link text-white" href="map.html">
|
<a class="nav-link text-white" href="map.html">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-geo-alt-fill" viewBox="0 0 16 16">
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-geo-alt-fill" viewBox="0 0 16 16">
|
||||||
<path d="M8 16s6-5.686 6-10A6 6 0 0 0 2 6c0 4.314 6 10 6 10m0-7a3 3 0 1 1 0-6 3 3 0 0 1 0 6"/>
|
<path d="M8 16s6-5.686 6-10A6 6 0 0 0 2 6c0 4.314 6 10 6 10m0-7a3 3 0 1 1 0-6 3 3 0 0 1 0 6"/>
|
||||||
</svg>
|
</svg>
|
||||||
Carte
|
<span data-i18n="sidebar.map">Carte</span>
|
||||||
</a>
|
</a>
|
||||||
<a class="nav-link text-white" href="terminal.html">
|
<a class="nav-link text-white" href="terminal.html">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-terminal-fill" viewBox="0 0 16 16">
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-terminal-fill" viewBox="0 0 16 16">
|
||||||
<path d="M0 3a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V3zm9.5 5.5h-3a.5.5 0 0 0 0 1h3a.5.5 0 0 0 0-1zm-6.354-.354a.5.5 0 1 0 .708.708l2-2a.5.5 0 0 0 0-.708l-2-2a.5.5 0 1 0-.708.708L4.793 6.5 3.146 8.146z"/>
|
<path d="M0 3a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V3zm9.5 5.5h-3a.5.5 0 0 0 0 1h3a.5.5 0 0 0 0-1zm-6.354-.354a.5.5 0 1 0 .708.708l2-2a.5.5 0 0 0 0-.708l-2-2a.5.5 0 1 0-.708.708L4.793 6.5 3.146 8.146z"/>
|
||||||
</svg>
|
</svg>
|
||||||
Terminal
|
<span data-i18n="sidebar.terminal">Terminal</span>
|
||||||
</a>
|
</a>
|
||||||
|
-->
|
||||||
<a class="nav-link text-white" href="admin.html">
|
<a class="nav-link text-white" href="admin.html">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-tools" viewBox="0 0 16 16">
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-tools" viewBox="0 0 16 16">
|
||||||
<path d="M1 0 0 1l2.2 3.081a1 1 0 0 0 .815.419h.07a1 1 0 0 1 .708.293l2.675 2.675-2.617 2.654A3.003 3.003 0 0 0 0 13a3 3 0 1 0 5.878-.851l2.654-2.617.968.968-.305.914a1 1 0 0 0 .242 1.023l3.27 3.27a.997.997 0 0 0 1.414 0l1.586-1.586a.997.997 0 0 0 0-1.414l-3.27-3.27a1 1 0 0 0-1.023-.242L10.5 9.5l-.96-.96 2.68-2.643A3.005 3.005 0 0 0 16 3q0-.405-.102-.777l-2.14 2.141L12 4l-.364-1.757L13.777.102a3 3 0 0 0-3.675 3.68L7.462 6.46 4.793 3.793a1 1 0 0 1-.293-.707v-.071a1 1 0 0 0-.419-.814zm9.646 10.646a.5.5 0 0 1 .708 0l2.914 2.915a.5.5 0 0 1-.707.707l-2.915-2.914a.5.5 0 0 1 0-.708M3 11l.471.242.529.026.287.445.445.287.026.529L5 13l-.242.471-.026.529-.445.287-.287.445-.529.026L3 15l-.471-.242L2 14.732l-.287-.445L1.268 14l-.026-.529L1 13l.242-.471.026-.529.445-.287.287-.445.529-.026z"/>
|
<path d="M1 0 0 1l2.2 3.081a1 1 0 0 0 .815.419h.07a1 1 0 0 1 .708.293l2.675 2.675-2.617 2.654A3.003 3.003 0 0 0 0 13a3 3 0 1 0 5.878-.851l2.654-2.617.968.968-.305.914a1 1 0 0 0 .242 1.023l3.27 3.27a.997.997 0 0 0 1.414 0l1.586-1.586a.997.997 0 0 0 0-1.414l-3.27-3.27a1 1 0 0 0-1.023-.242L10.5 9.5l-.96-.96 2.68-2.643A3.005 3.005 0 0 0 16 3q0-.405-.102-.777l-2.14 2.141L12 4l-.364-1.757L13.777.102a3 3 0 0 0-3.675 3.68L7.462 6.46 4.793 3.793a1 1 0 0 1-.293-.707v-.071a1 1 0 0 0-.419-.814zm9.646 10.646a.5.5 0 0 1 .708 0l2.914 2.915a.5.5 0 0 1-.707.707l-2.915-2.914a.5.5 0 0 1 0-.708M3 11l.471.242.529.026.287.445.445.287.026.529L5 13l-.242.471-.026.529-.445.287-.287.445-.529.026L3 15l-.471-.242L2 14.732l-.287-.445L1.268 14l-.026-.529L1 13l.242-.471.026-.529.445-.287.287-.445.529-.026z"/>
|
||||||
</svg>
|
</svg>
|
||||||
Admin
|
<span data-i18n="sidebar.admin">Admin</span>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<!-- New content at the bottom -->
|
<!-- New content at the bottom -->
|
||||||
|
|||||||
@@ -6,12 +6,18 @@
|
|||||||
</a>
|
</a>
|
||||||
<div class="d-flex">
|
<div class="d-flex">
|
||||||
<button class="btn btn-outline-light d-md-none me-2" type="button" data-bs-toggle="offcanvas" data-bs-target="#sidebarOffcanvas" aria-controls="sidebarOffcanvas" aria-label="Toggle Sidebar">☰</button>
|
<button class="btn btn-outline-light d-md-none me-2" type="button" data-bs-toggle="offcanvas" data-bs-target="#sidebarOffcanvas" aria-controls="sidebarOffcanvas" aria-label="Toggle Sidebar">☰</button>
|
||||||
|
|
||||||
<!-- Centered text -->
|
<!-- Centered text -->
|
||||||
<!--
|
<!--
|
||||||
<span id="pageTitle_plus_ID" class="position-absolute top-50 start-50 translate-middle">Texte au milieu</span>
|
<span id="pageTitle_plus_ID" class="position-absolute top-50 start-50 translate-middle">Texte au milieu</span>
|
||||||
-->
|
-->
|
||||||
|
|
||||||
|
<!-- Language Switcher -->
|
||||||
|
<select class="form-select form-select-sm me-2" id="languageSwitcher" style="width: auto; background-color: #6c757d; color: white; border-color: white;" onchange="i18n.setLanguage(this.value)">
|
||||||
|
<option value="fr" style="background-color: #6c757d; color: white;">🇫🇷 FR</option>
|
||||||
|
<option value="en" style="background-color: #6c757d; color: white;">🇬🇧 EN</option>
|
||||||
|
</select>
|
||||||
|
|
||||||
<button type="button" class="btn btn-outline-light" id="RTC_time">loading...</button>
|
<button type="button" class="btn btn-outline-light" id="RTC_time">loading...</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -117,6 +117,8 @@
|
|||||||
<script src="assets/jquery/jquery-3.7.1.min.js"></script>
|
<script src="assets/jquery/jquery-3.7.1.min.js"></script>
|
||||||
<!-- Link Bootstrap JS and Popper.js locally -->
|
<!-- Link Bootstrap JS and Popper.js locally -->
|
||||||
<script src="assets/js/bootstrap.bundle.js"></script>
|
<script src="assets/js/bootstrap.bundle.js"></script>
|
||||||
|
<!-- i18n translation system -->
|
||||||
|
<script src="assets/js/i18n.js"></script>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
document.addEventListener('DOMContentLoaded', function () {
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
|
|||||||
@@ -23,11 +23,11 @@ fi
|
|||||||
|
|
||||||
# Update and install necessary packages
|
# Update and install necessary packages
|
||||||
info "Updating package list and installing necessary packages..."
|
info "Updating package list and installing necessary packages..."
|
||||||
sudo apt update && sudo apt install -y git gh apache2 sqlite3 php php-sqlite3 python3 python3-pip jq autossh i2c-tools python3-smbus || error "Failed to install required packages."
|
sudo apt update && sudo apt install -y git gh apache2 sqlite3 php php-sqlite3 python3 python3-pip jq autossh i2c-tools python3-smbus python3-rpi.gpio || error "Failed to install required packages."
|
||||||
|
|
||||||
# Install Python libraries
|
# Install Python libraries
|
||||||
info "Installing Python libraries..."
|
info "Installing Python libraries..."
|
||||||
sudo pip3 install pyserial requests RPi.GPIO adafruit-circuitpython-bme280 crcmod psutil gpiozero ntplib pytz --break-system-packages || error "Failed to install 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."
|
||||||
|
|
||||||
# Clone the repository (check if it exists first)
|
# Clone the repository (check if it exists first)
|
||||||
REPO_DIR="/var/www/nebuleair_pro_4g"
|
REPO_DIR="/var/www/nebuleair_pro_4g"
|
||||||
@@ -99,13 +99,48 @@ fi
|
|||||||
|
|
||||||
# Add sudo authorization (prevent duplicate entries)
|
# Add sudo authorization (prevent duplicate entries)
|
||||||
info "Setting up sudo authorization..."
|
info "Setting up sudo authorization..."
|
||||||
if ! sudo grep -q "/usr/bin/nmcli" /etc/sudoers; then
|
SUDOERS_FILE="/etc/sudoers"
|
||||||
echo -e "ALL ALL=(ALL) NOPASSWD: /usr/bin/nmcli, /usr/sbin/reboot\nwww-data ALL=(ALL) NOPASSWD: /usr/bin/git pull\nwww-data ALL=(ALL) NOPASSWD: /usr/bin/ssh\nwww-data ALL=(ALL) NOPASSWD: /usr/bin/python3 * www-data ALL=(ALL) NOPASSWD: /bin/systemctl * www-data ALL=(ALL) NOPASSWD: /var/www/nebuleair_pro_4g/*" | sudo tee -a /etc/sudoers > /dev/null
|
|
||||||
success "Sudo authorization added."
|
# First, fix any existing syntax errors
|
||||||
|
if sudo visudo -c 2>&1 | grep -q "syntax error"; then
|
||||||
|
warning "Syntax error detected in sudoers file. Attempting to fix..."
|
||||||
|
# Remove the problematic line if it exists
|
||||||
|
sudo sed -i '/www-data ALL=(ALL) NOPASSWD: \/usr\/bin\/python3 \* www-data/d' "$SUDOERS_FILE"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Add proper sudo rules (each on a separate line)
|
||||||
|
if ! sudo grep -q "/usr/bin/nmcli" "$SUDOERS_FILE"; then
|
||||||
|
# Create a temporary file with the new rules
|
||||||
|
cat <<EOF | sudo tee /tmp/sudoers_additions > /dev/null
|
||||||
|
# NebuleAir Pro 4G sudo rules
|
||||||
|
ALL ALL=(ALL) NOPASSWD: /usr/bin/nmcli, /usr/sbin/reboot
|
||||||
|
www-data ALL=(ALL) NOPASSWD: /usr/bin/git pull
|
||||||
|
www-data ALL=(ALL) NOPASSWD: /usr/bin/ssh
|
||||||
|
www-data ALL=(ALL) NOPASSWD: /usr/bin/python3 *
|
||||||
|
www-data ALL=(ALL) NOPASSWD: /bin/systemctl *
|
||||||
|
www-data ALL=(ALL) NOPASSWD: /var/www/nebuleair_pro_4g/*
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Validate the temporary file
|
||||||
|
if sudo visudo -c -f /tmp/sudoers_additions; then
|
||||||
|
# Append to sudoers if valid
|
||||||
|
sudo cat /tmp/sudoers_additions >> "$SUDOERS_FILE"
|
||||||
|
success "Sudo authorization added."
|
||||||
|
else
|
||||||
|
error "Failed to add sudo rules - syntax validation failed."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Clean up
|
||||||
|
sudo rm -f /tmp/sudoers_additions
|
||||||
else
|
else
|
||||||
warning "Sudo authorization already set. Skipping."
|
warning "Sudo authorization already set. Skipping."
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Validate sudoers file after changes
|
||||||
|
if ! sudo visudo -c; then
|
||||||
|
error "Sudoers file has syntax errors! Please fix manually with 'sudo visudo'"
|
||||||
|
fi
|
||||||
|
|
||||||
# Open all UART serial ports (avoid duplication)
|
# Open all UART serial ports (avoid duplication)
|
||||||
info "Configuring UART serial ports..."
|
info "Configuring UART serial ports..."
|
||||||
if ! grep -q "enable_uart=1" /boot/firmware/config.txt; then
|
if ! grep -q "enable_uart=1" /boot/firmware/config.txt; then
|
||||||
@@ -128,6 +163,13 @@ success "I2C ports enabled."
|
|||||||
info "Creates sqlites databases..."
|
info "Creates sqlites databases..."
|
||||||
/usr/bin/python3 /var/www/nebuleair_pro_4g/sqlite/create_db.py
|
/usr/bin/python3 /var/www/nebuleair_pro_4g/sqlite/create_db.py
|
||||||
|
|
||||||
|
# Final sudoers check
|
||||||
|
if sudo visudo -c; then
|
||||||
|
success "Sudoers file is valid."
|
||||||
|
else
|
||||||
|
error "Sudoers file has errors! System may not function correctly."
|
||||||
|
fi
|
||||||
|
|
||||||
# Completion message
|
# Completion message
|
||||||
success "Setup completed successfully!"
|
success "Setup completed successfully!"
|
||||||
info "System will reboot in 5 seconds..."
|
info "System will reboot in 5 seconds..."
|
||||||
|
|||||||
@@ -33,21 +33,21 @@ pinctrl set 16 op
|
|||||||
pinctrl set 16 dh
|
pinctrl set 16 dh
|
||||||
sleep 5
|
sleep 5
|
||||||
|
|
||||||
#Check SARA connection
|
#Check SARA connection (ATI)
|
||||||
info "Check SARA connection"
|
info "Check SARA connection (ATI)"
|
||||||
/usr/bin/python3 /var/www/nebuleair_pro_4g/SARA/sara.py ttyAMA2 ATI 2
|
/usr/bin/python3 /var/www/nebuleair_pro_4g/SARA/sara.py ttyAMA2 ATI 2
|
||||||
|
|
||||||
#set up SARA R4 APN
|
#set up SARA R4 APN
|
||||||
info "Set up Monogoto APN"
|
#info "Set up Monogoto APN"
|
||||||
/usr/bin/python3 /var/www/nebuleair_pro_4g/SARA/sara_setAPN.py ttyAMA2 data.mono 2
|
#/usr/bin/python3 /var/www/nebuleair_pro_4g/SARA/sara_setAPN.py ttyAMA2 data.mono 2
|
||||||
|
|
||||||
#activate blue network led on the SARA R4
|
#activate blue network led on the SARA R4
|
||||||
info "Activate blue LED"
|
info "Activate blue LED"
|
||||||
/usr/bin/python3 /var/www/nebuleair_pro_4g/SARA/sara.py ttyAMA2 AT+UGPIOC=16,2 2
|
/usr/bin/python3 /var/www/nebuleair_pro_4g/SARA/sara.py ttyAMA2 AT+UGPIOC=16,2 2
|
||||||
|
|
||||||
#Connect to network
|
#Connect to network
|
||||||
info "Connect SARA R4 to network"
|
#info "Connect SARA R4 to network"
|
||||||
python3 /var/www/nebuleair_pro_4g/SARA/sara_connectNetwork.py ttyAMA2 20810 60
|
#python3 /var/www/nebuleair_pro_4g/SARA/sara_connectNetwork.py ttyAMA2 20810 60
|
||||||
|
|
||||||
#Need to create the two service
|
#Need to create the two service
|
||||||
# 1. start the scripts to set-up the services
|
# 1. start the scripts to set-up the services
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,39 +1,277 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
# File: /var/www/nebuleair_pro_4g/services/check_services.sh
|
# File: /var/www/nebuleair_pro_4g/services/check_services.sh
|
||||||
# Purpose: Check status of all NebuleAir services and logs
|
# Purpose: Check status of all NebuleAir services and logs
|
||||||
# Install:
|
# Version with fixed color handling for proper table display
|
||||||
# sudo chmod +x /var/www/nebuleair_pro_4g/services/check_services.sh
|
|
||||||
# sudo /var/www/nebuleair_pro_4g/services/check_services.sh
|
|
||||||
|
|
||||||
echo "=== NebuleAir Services Status ==="
|
# Colors for output
|
||||||
echo ""
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
BLUE='\033[0;34m'
|
||||||
|
CYAN='\033[0;36m'
|
||||||
|
BOLD='\033[1m'
|
||||||
|
DIM='\033[2m'
|
||||||
|
NC='\033[0m' # No Color
|
||||||
|
|
||||||
# Check status of all timers
|
# Service list
|
||||||
echo "--- TIMER STATUS ---"
|
SERVICES=("npm" "envea" "sara" "bme280" "mppt" "db-cleanup" "noise")
|
||||||
systemctl list-timers | grep nebuleair
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# Check status of all services
|
# Function to print header
|
||||||
echo "--- SERVICE STATUS ---"
|
print_header() {
|
||||||
for service in npm envea sara bme280 mppt db-cleanup; do
|
local text="$1"
|
||||||
status=$(systemctl is-active nebuleair-$service-data.service)
|
|
||||||
timer_status=$(systemctl is-active nebuleair-$service-data.timer)
|
|
||||||
|
|
||||||
echo "nebuleair-$service-data: Service=$status, Timer=$timer_status"
|
|
||||||
done
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# Show recent logs for each service
|
|
||||||
echo "--- RECENT LOGS (last 5 entries per service) ---"
|
|
||||||
for service in npm envea sara bme280 mppt db-cleanup; do
|
|
||||||
echo "[$service service logs]"
|
|
||||||
journalctl -u nebuleair-$service-data.service -n 5 --no-pager
|
|
||||||
echo ""
|
echo ""
|
||||||
|
echo -e "${BLUE}${BOLD}=== $text ===${NC}"
|
||||||
|
echo -e "${BLUE}$(printf '%.0s=' {1..70})${NC}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Function to print section
|
||||||
|
print_section() {
|
||||||
|
local text="$1"
|
||||||
|
echo ""
|
||||||
|
echo -e "${CYAN}${BOLD}--- $text ---${NC}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Function to print a separator line
|
||||||
|
print_separator() {
|
||||||
|
echo "+--------------------------+-----------+-----------+-------------+-------------+-------------------------+"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Clear screen for clean output
|
||||||
|
clear
|
||||||
|
|
||||||
|
# Main header
|
||||||
|
print_header "NebuleAir Services Status Report"
|
||||||
|
echo -e "Generated on: $(date '+%Y-%m-%d %H:%M:%S')"
|
||||||
|
|
||||||
|
# Timer Schedule
|
||||||
|
print_section "Active Timers Schedule"
|
||||||
|
echo ""
|
||||||
|
systemctl list-timers --no-pager | head -n 1
|
||||||
|
systemctl list-timers --no-pager | grep nebuleair || echo "No active nebuleair timers found"
|
||||||
|
|
||||||
|
# Service Status Overview with fixed color handling
|
||||||
|
print_section "Service Status Overview"
|
||||||
|
echo ""
|
||||||
|
print_separator
|
||||||
|
printf "| %-24s | %-9s | %-9s | %-11s | %-11s | %-23s |\n" "Service" "Svc State" "Svc Boot" "Timer State" "Timer Boot" "Health Status"
|
||||||
|
print_separator
|
||||||
|
|
||||||
|
for service in "${SERVICES[@]}"; do
|
||||||
|
# Check the actual service and timer names (with -data suffix)
|
||||||
|
full_service_name="nebuleair-${service}-data"
|
||||||
|
|
||||||
|
# Get raw status values
|
||||||
|
service_status=$(systemctl is-active ${full_service_name}.service 2>/dev/null | tr -d '\n' || echo "not-found")
|
||||||
|
service_enabled=$(systemctl is-enabled ${full_service_name}.service 2>/dev/null | tr -d '\n' || echo "not-found")
|
||||||
|
timer_status=$(systemctl is-active ${full_service_name}.timer 2>/dev/null | tr -d '\n' || echo "not-found")
|
||||||
|
timer_enabled=$(systemctl is-enabled ${full_service_name}.timer 2>/dev/null | tr -d '\n' || echo "not-found")
|
||||||
|
|
||||||
|
# Check if files exist and override if not found
|
||||||
|
if ! systemctl list-unit-files | grep -q "^${full_service_name}.service" &>/dev/null; then
|
||||||
|
service_status="not-found"
|
||||||
|
service_enabled="not-found"
|
||||||
|
fi
|
||||||
|
if ! systemctl list-unit-files | grep -q "^${full_service_name}.timer" &>/dev/null; then
|
||||||
|
timer_status="not-found"
|
||||||
|
timer_enabled="not-found"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Create display strings without embedded colors for table cells
|
||||||
|
case $service_status in
|
||||||
|
"active") svc_st_display="active"; svc_st_color="${GREEN}" ;;
|
||||||
|
"inactive") svc_st_display="inactive"; svc_st_color="${DIM}" ;;
|
||||||
|
"activating") svc_st_display="starting"; svc_st_color="${YELLOW}" ;;
|
||||||
|
"not-found") svc_st_display="missing"; svc_st_color="${RED}" ;;
|
||||||
|
*) svc_st_display="$service_status"; svc_st_color="${RED}" ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
case $service_enabled in
|
||||||
|
"enabled"|"static") svc_en_display="enabled"; svc_en_color="${GREEN}" ;;
|
||||||
|
"disabled") svc_en_display="disabled"; svc_en_color="${YELLOW}" ;;
|
||||||
|
"not-found") svc_en_display="missing"; svc_en_color="${RED}" ;;
|
||||||
|
*) svc_en_display="$service_enabled"; svc_en_color="${YELLOW}" ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
case $timer_status in
|
||||||
|
"active") tim_st_display="active"; tim_st_color="${GREEN}" ;;
|
||||||
|
"inactive") tim_st_display="inactive"; tim_st_color="${RED}" ;;
|
||||||
|
"not-found") tim_st_display="missing"; tim_st_color="${RED}" ;;
|
||||||
|
*) tim_st_display="$timer_status"; tim_st_color="${RED}" ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
case $timer_enabled in
|
||||||
|
"enabled"|"static") tim_en_display="enabled"; tim_en_color="${GREEN}" ;;
|
||||||
|
"disabled") tim_en_display="disabled"; tim_en_color="${YELLOW}" ;;
|
||||||
|
"not-found") tim_en_display="missing"; tim_en_color="${RED}" ;;
|
||||||
|
*) tim_en_display="$timer_enabled"; tim_en_color="${YELLOW}" ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# Determine health status
|
||||||
|
if [[ "$timer_status" == "active" ]]; then
|
||||||
|
if [[ "$timer_enabled" == "enabled" || "$timer_enabled" == "static" ]]; then
|
||||||
|
health_display="✓ OK"
|
||||||
|
health_color="${GREEN}"
|
||||||
|
else
|
||||||
|
health_display="⚠ Boot disabled"
|
||||||
|
health_color="${YELLOW}"
|
||||||
|
fi
|
||||||
|
elif [[ "$timer_status" == "inactive" ]]; then
|
||||||
|
health_display="✗ Timer stopped"
|
||||||
|
health_color="${RED}"
|
||||||
|
else
|
||||||
|
health_display="✗ Timer missing"
|
||||||
|
health_color="${RED}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Print row with colors applied outside of printf formatting
|
||||||
|
printf "| %-24s | " "$full_service_name"
|
||||||
|
printf "${svc_st_color}%-9s${NC} | " "$svc_st_display"
|
||||||
|
printf "${svc_en_color}%-9s${NC} | " "$svc_en_display"
|
||||||
|
printf "${tim_st_color}%-11s${NC} | " "$tim_st_display"
|
||||||
|
printf "${tim_en_color}%-11s${NC} | " "$tim_en_display"
|
||||||
|
printf "${health_color}%-23s${NC} |\n" "$health_display"
|
||||||
|
done
|
||||||
|
print_separator
|
||||||
|
|
||||||
|
# Understanding the table
|
||||||
|
echo ""
|
||||||
|
echo -e "${DIM}Note: For timer-based services, it's normal for the service to be 'inactive' and 'disabled'.${NC}"
|
||||||
|
echo -e "${DIM} What matters is that the timer is 'active' and 'enabled'.${NC}"
|
||||||
|
|
||||||
|
# Configuration Issues
|
||||||
|
print_section "Configuration Issues"
|
||||||
|
echo ""
|
||||||
|
issues_found=false
|
||||||
|
|
||||||
|
for service in "${SERVICES[@]}"; do
|
||||||
|
full_service_name="nebuleair-${service}-data"
|
||||||
|
|
||||||
|
timer_status=$(systemctl is-active ${full_service_name}.timer 2>/dev/null | tr -d '\n' || echo "not-found")
|
||||||
|
timer_enabled=$(systemctl is-enabled ${full_service_name}.timer 2>/dev/null | tr -d '\n' || echo "not-found")
|
||||||
|
|
||||||
|
# Check if timer exists
|
||||||
|
if ! systemctl list-unit-files | grep -q "^${full_service_name}.timer" &>/dev/null; then
|
||||||
|
timer_status="not-found"
|
||||||
|
timer_enabled="not-found"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$timer_status" != "active" || ("$timer_enabled" != "enabled" && "$timer_enabled" != "static") ]]; then
|
||||||
|
issues_found=true
|
||||||
|
echo -e " ${RED}•${NC} ${BOLD}$full_service_name${NC}"
|
||||||
|
if [[ "$timer_status" == "not-found" ]]; then
|
||||||
|
echo -e " ${RED}→${NC} Timer unit file is missing"
|
||||||
|
elif [[ "$timer_status" != "active" ]]; then
|
||||||
|
echo -e " ${RED}→${NC} Timer is not running (status: $timer_status)"
|
||||||
|
fi
|
||||||
|
if [[ "$timer_enabled" == "not-found" ]]; then
|
||||||
|
echo -e " ${RED}→${NC} Timer unit file is missing"
|
||||||
|
elif [[ "$timer_enabled" != "enabled" && "$timer_enabled" != "static" ]]; then
|
||||||
|
echo -e " ${YELLOW}→${NC} Timer won't start on boot (status: $timer_enabled)"
|
||||||
|
fi
|
||||||
|
echo ""
|
||||||
|
fi
|
||||||
done
|
done
|
||||||
|
|
||||||
echo "=== End of Report ==="
|
if [[ "$issues_found" == "false" ]]; then
|
||||||
|
echo -e " ${GREEN}✓${NC} All timers are properly configured!"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Recent Executions - Simplified
|
||||||
|
print_section "Last Execution Status"
|
||||||
echo ""
|
echo ""
|
||||||
echo "For detailed logs use:"
|
printf " %-12s %-20s %s\n" "Service" "Last Run" "Status"
|
||||||
echo " sudo journalctl -u nebuleair-[service]-data.service -f"
|
printf " %-12s %-20s %s\n" "-------" "--------" "------"
|
||||||
echo "To restart a specific service timer:"
|
|
||||||
echo " sudo systemctl restart nebuleair-[service]-data.timer"
|
for service in "${SERVICES[@]}"; do
|
||||||
|
full_service_name="nebuleair-${service}-data"
|
||||||
|
|
||||||
|
# Get last execution time and status
|
||||||
|
last_log=$(journalctl -u ${full_service_name}.service -n 3 --no-pager 2>/dev/null | grep -E "(Started|Finished|Failed)" | tail -1)
|
||||||
|
|
||||||
|
if [[ -n "$last_log" ]]; then
|
||||||
|
timestamp=$(echo "$last_log" | awk '{print $1, $2, $3}')
|
||||||
|
if echo "$last_log" | grep -q "Finished"; then
|
||||||
|
status="${GREEN}✓ Success${NC}"
|
||||||
|
elif echo "$last_log" | grep -q "Failed"; then
|
||||||
|
status="${RED}✗ Failed${NC}"
|
||||||
|
elif echo "$last_log" | grep -q "Started"; then
|
||||||
|
status="${YELLOW}⟳ Running${NC}"
|
||||||
|
else
|
||||||
|
status="${DIM}- Unknown${NC}"
|
||||||
|
fi
|
||||||
|
printf " %-12s %-20s %b\n" "$service" "$timestamp" "$status"
|
||||||
|
else
|
||||||
|
printf " %-12s %-20s %b\n" "$service" "-" "${DIM}- No data${NC}"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# Summary
|
||||||
|
print_section "Summary"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
working=0
|
||||||
|
needs_attention=0
|
||||||
|
|
||||||
|
for service in "${SERVICES[@]}"; do
|
||||||
|
full_service_name="nebuleair-${service}-data"
|
||||||
|
timer_status=$(systemctl is-active ${full_service_name}.timer 2>/dev/null | tr -d '\n')
|
||||||
|
timer_enabled=$(systemctl is-enabled ${full_service_name}.timer 2>/dev/null | tr -d '\n')
|
||||||
|
|
||||||
|
if [[ "$timer_status" == "active" ]] && [[ "$timer_enabled" == "enabled" || "$timer_enabled" == "static" ]]; then
|
||||||
|
((working++))
|
||||||
|
else
|
||||||
|
((needs_attention++))
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
total=${#SERVICES[@]}
|
||||||
|
|
||||||
|
# Visual progress bar
|
||||||
|
echo -n " Overall Health: ["
|
||||||
|
for ((i=1; i<=10; i++)); do
|
||||||
|
if ((i <= working * 10 / total)); then
|
||||||
|
echo -n -e "${GREEN}▰${NC}"
|
||||||
|
else
|
||||||
|
echo -n -e "${RED}▱${NC}"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
echo -e "] ${working}/${total}"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo -e " ${GREEN}✓${NC} Working properly: ${BOLD}$working${NC} services"
|
||||||
|
echo -e " ${RED}✗${NC} Need attention: ${BOLD}$needs_attention${NC} services"
|
||||||
|
|
||||||
|
# Quick Commands
|
||||||
|
print_section "Quick Commands"
|
||||||
|
echo ""
|
||||||
|
echo -e " ${BOLD}Fix a timer that needs attention:${NC}"
|
||||||
|
echo " $ sudo systemctl enable --now nebuleair-[service]-data.timer"
|
||||||
|
echo ""
|
||||||
|
echo -e " ${BOLD}View live logs:${NC}"
|
||||||
|
echo " $ sudo journalctl -u nebuleair-[service]-data.service -f"
|
||||||
|
echo ""
|
||||||
|
echo -e " ${BOLD}Check timer details:${NC}"
|
||||||
|
echo " $ systemctl status nebuleair-[service]-data.timer"
|
||||||
|
echo ""
|
||||||
|
echo -e " ${BOLD}Run service manually:${NC}"
|
||||||
|
echo " $ sudo systemctl start nebuleair-[service]-data.service"
|
||||||
|
|
||||||
|
# Specific fixes needed
|
||||||
|
if [[ $needs_attention -gt 0 ]]; then
|
||||||
|
echo ""
|
||||||
|
echo -e "${YELLOW}${BOLD}Recommended Actions:${NC}"
|
||||||
|
for service in "${SERVICES[@]}"; do
|
||||||
|
full_service_name="nebuleair-${service}-data"
|
||||||
|
timer_status=$(systemctl is-active ${full_service_name}.timer 2>/dev/null | tr -d '\n')
|
||||||
|
timer_enabled=$(systemctl is-enabled ${full_service_name}.timer 2>/dev/null | tr -d '\n')
|
||||||
|
|
||||||
|
if [[ "$timer_status" != "active" ]] && [[ "$timer_status" != "not-found" ]]; then
|
||||||
|
echo -e " ${RED}→${NC} sudo systemctl start ${full_service_name}.timer"
|
||||||
|
fi
|
||||||
|
if [[ "$timer_enabled" != "enabled" ]] && [[ "$timer_enabled" != "static" ]] && [[ "$timer_enabled" != "not-found" ]]; then
|
||||||
|
echo -e " ${YELLOW}→${NC} sudo systemctl enable ${full_service_name}.timer"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
@@ -173,6 +173,38 @@ AccuracySec=1s
|
|||||||
WantedBy=timers.target
|
WantedBy=timers.target
|
||||||
EOL
|
EOL
|
||||||
|
|
||||||
|
# Create service and timer files for noise Data (every minutes)
|
||||||
|
cat > /etc/systemd/system/nebuleair-noise-data.service << 'EOL'
|
||||||
|
[Unit]
|
||||||
|
Description=NebuleAir noise Data Collection Service
|
||||||
|
After=network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=oneshot
|
||||||
|
ExecStart=/usr/bin/python3 /var/www/nebuleair_pro_4g/sound_meter/NSRT_mk4_get_data.py
|
||||||
|
User=root
|
||||||
|
WorkingDirectory=/var/www/nebuleair_pro_4g
|
||||||
|
StandardOutput=append:/var/www/nebuleair_pro_4g/logs/noise_service.log
|
||||||
|
StandardError=append:/var/www/nebuleair_pro_4g/logs/noise_service_errors.log
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
EOL
|
||||||
|
|
||||||
|
cat > /etc/systemd/system/nebuleair-noise-data.timer << 'EOL'
|
||||||
|
[Unit]
|
||||||
|
Description=Run NebuleAir MPPT Data Collection every 120 seconds
|
||||||
|
Requires=nebuleair-noise-data.service
|
||||||
|
|
||||||
|
[Timer]
|
||||||
|
OnBootSec=60s
|
||||||
|
OnUnitActiveSec=60s
|
||||||
|
AccuracySec=1s
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=timers.target
|
||||||
|
EOL
|
||||||
|
|
||||||
# Create service and timer files for Database Cleanup
|
# Create service and timer files for Database Cleanup
|
||||||
cat > /etc/systemd/system/nebuleair-db-cleanup-data.service << 'EOL'
|
cat > /etc/systemd/system/nebuleair-db-cleanup-data.service << 'EOL'
|
||||||
[Unit]
|
[Unit]
|
||||||
@@ -210,7 +242,7 @@ systemctl daemon-reload
|
|||||||
|
|
||||||
# Enable and start all timers
|
# Enable and start all timers
|
||||||
echo "Enabling and starting all services..."
|
echo "Enabling and starting all services..."
|
||||||
for service in npm envea sara bme280 mppt db-cleanup; do
|
for service in npm envea sara bme280 mppt db-cleanup noise; do
|
||||||
systemctl enable nebuleair-$service-data.timer
|
systemctl enable nebuleair-$service-data.timer
|
||||||
systemctl start nebuleair-$service-data.timer
|
systemctl start nebuleair-$service-data.timer
|
||||||
echo "Started nebuleair-$service-data timer"
|
echo "Started nebuleair-$service-data timer"
|
||||||
|
|||||||
55
sound_meter/NSRT_MK4_change_config.py
Normal file
55
sound_meter/NSRT_MK4_change_config.py
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
'''
|
||||||
|
____ ___ _ _ _ _ ____
|
||||||
|
/ ___| / _ \| | | | \ | | _ \
|
||||||
|
\___ \| | | | | | | \| | | | |
|
||||||
|
___) | |_| | |_| | |\ | |_| |
|
||||||
|
|____/ \___/ \___/|_| \_|____/
|
||||||
|
|
||||||
|
python3 /var/www/nebuleair_pro_4g/sound_meter/NSRT_MK4_change_config.py
|
||||||
|
|
||||||
|
1.Intervalle d'enregistrement
|
||||||
|
L'intervalle d'enregistrement définit le temps entre deux points successifs enregistrés.
|
||||||
|
Cela définit également la période d'intégration pour le LEQ, et la période d'observation pour L-min et L-max et Lpeak.
|
||||||
|
L'intervalle d'enregistrement peut être réglé de 125 ms (1/8ème) à 2 H par incréments de 125 ms.
|
||||||
|
|
||||||
|
some parameters can be changed:
|
||||||
|
write_tau(tau: float) -> time constant
|
||||||
|
write_fs(frequency: int) -> sampling freq
|
||||||
|
|
||||||
|
'''
|
||||||
|
|
||||||
|
import nsrt_mk3_dev
|
||||||
|
#from nsrt_mk3_dev import Weighting
|
||||||
|
#from nsrt_mk3_dev.nsrt_mk3_dev import NsrtMk3Dev, Weighting
|
||||||
|
from enum import Enum
|
||||||
|
|
||||||
|
class Weighting(Enum):
|
||||||
|
DB_A = 1
|
||||||
|
DB_C = 2
|
||||||
|
DB_Z = 3
|
||||||
|
|
||||||
|
nsrt = nsrt_mk3_dev.NsrtMk3Dev('/dev/ttyACM0')
|
||||||
|
|
||||||
|
#####################
|
||||||
|
#change time constant
|
||||||
|
nsrt.write_tau(1)
|
||||||
|
#####################
|
||||||
|
|
||||||
|
#####################
|
||||||
|
#change Weighting curve
|
||||||
|
# - Weighting.DB_A (A-weighting - most common for environmental noise)
|
||||||
|
# - Weighting.DB_C (C-weighting - for peak measurements)
|
||||||
|
# - Weighting.DB_Z (Z-weighting - linear/flat response)
|
||||||
|
nsrt.write_weighting(Weighting.DB_A)
|
||||||
|
#####################
|
||||||
|
|
||||||
|
freq_level = nsrt.read_fs() #current sampling frequency
|
||||||
|
time_constant = nsrt.read_tau() #reads the current time constant
|
||||||
|
leq_level = nsrt.read_leq() #current running LEQ and starts the integration of a new LEQ.
|
||||||
|
weighting = nsrt.read_weighting() #weighting curve that is currently selected
|
||||||
|
weighted_level = nsrt.read_level() #current running level in dB.
|
||||||
|
|
||||||
|
print(f'current sampling freq : {freq_level} Hz')
|
||||||
|
print(f'current time constant : {time_constant} s')
|
||||||
|
print(f'current LEQ level: {leq_level:0.2f} dB')
|
||||||
|
print(f'{weighting} value: {weighted_level:0.2f} dBA')
|
||||||
72
sound_meter/NSRT_mk4_get_data.py
Normal file
72
sound_meter/NSRT_mk4_get_data.py
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
'''
|
||||||
|
____ ___ _ _ _ _ ____
|
||||||
|
/ ___| / _ \| | | | \ | | _ \
|
||||||
|
\___ \| | | | | | | \| | | | |
|
||||||
|
___) | |_| | |_| | |\ | |_| |
|
||||||
|
|____/ \___/ \___/|_| \_|____/
|
||||||
|
|
||||||
|
python3 /var/www/nebuleair_pro_4g/sound_meter/NSRT_mk4_get_data.py
|
||||||
|
|
||||||
|
Script to get data from the NSRT_MK4 Sound Level Meter
|
||||||
|
|
||||||
|
triggered by a systemd service
|
||||||
|
sudo systemctl status nebuleair-noise-data.service
|
||||||
|
|
||||||
|
Need to install "nsrt_mk3_dev"
|
||||||
|
|
||||||
|
1.Intervalle d'enregistrement
|
||||||
|
L'intervalle d'enregistrement définit le temps entre deux points successifs enregistrés.
|
||||||
|
Cela définit également la période d'intégration pour le LEQ, et la période d'observation pour L-min et L-max et Lpeak.
|
||||||
|
L'intervalle d'enregistrement peut être réglé de 125 ms (1/8ème) à 2 H par incréments de 125 ms.
|
||||||
|
|
||||||
|
some parameters can be changed:
|
||||||
|
write_tau(tau: float) -> time constant
|
||||||
|
write_fs(frequency: int) -> sampling freq
|
||||||
|
|
||||||
|
'''
|
||||||
|
|
||||||
|
import nsrt_mk3_dev
|
||||||
|
import sqlite3
|
||||||
|
|
||||||
|
nsrt = nsrt_mk3_dev.NsrtMk3Dev('/dev/ttyACM0')
|
||||||
|
|
||||||
|
# Connect to the SQLite database
|
||||||
|
conn = sqlite3.connect("/var/www/nebuleair_pro_4g/sqlite/sensors.db")
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
#GET RTC TIME from SQlite
|
||||||
|
cursor.execute("SELECT * FROM timestamp_table LIMIT 1")
|
||||||
|
row = cursor.fetchone() # Get the first (and only) row
|
||||||
|
rtc_time_str = row[1] # '2025-02-07 12:30:45'
|
||||||
|
|
||||||
|
freq_level = nsrt.read_fs() #current sampling frequency
|
||||||
|
time_constant = nsrt.read_tau() #reads the current time constant
|
||||||
|
leq_level = nsrt.read_leq() #current running LEQ and starts the integration of a new LEQ.
|
||||||
|
weighting = nsrt.read_weighting() #weighting curve that is currently selected ( A ou C)
|
||||||
|
weighted_level = nsrt.read_level() #current running level in dB.
|
||||||
|
|
||||||
|
#print(f'current sampling freq : {freq_level} Hz')
|
||||||
|
#print(f'current time constant : {time_constant} s')
|
||||||
|
#print(f'current LEQ level: {leq_level:0.2f} dB')
|
||||||
|
#print(f'{weighting} value: {weighted_level:0.2f} dBA')
|
||||||
|
# Round values to 2 decimal places before saving
|
||||||
|
leq_level_rounded = round(leq_level, 2)
|
||||||
|
weighted_level_rounded = round(weighted_level, 2)
|
||||||
|
|
||||||
|
#save to db
|
||||||
|
#save to sqlite database
|
||||||
|
try:
|
||||||
|
cursor.execute('''
|
||||||
|
INSERT INTO data_NOISE (timestamp,current_LEQ, DB_A_value) VALUES (?,?,?)'''
|
||||||
|
, (rtc_time_str,leq_level_rounded,weighted_level_rounded))
|
||||||
|
|
||||||
|
# Commit and close the connection
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
#print("Sensor data saved successfully!")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Database error: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
conn.close()
|
||||||
0
sound_meter/sound_meter → sound_meter/old/sound_meter
Executable file → Normal file
0
sound_meter/sound_meter → sound_meter/old/sound_meter
Executable file → Normal file
0
sound_meter/sound_meter.c → sound_meter/old/sound_meter.c
Executable file → Normal file
0
sound_meter/sound_meter.c → sound_meter/old/sound_meter.c
Executable file → Normal file
0
sound_meter/sound_meter_moving_avg → sound_meter/old/sound_meter_moving_avg
Executable file → Normal file
0
sound_meter/sound_meter_moving_avg → sound_meter/old/sound_meter_moving_avg
Executable file → Normal file
0
sound_meter/sound_meter_moving_avg.c → sound_meter/old/sound_meter_moving_avg.c
Executable file → Normal file
0
sound_meter/sound_meter_moving_avg.c → sound_meter/old/sound_meter_moving_avg.c
Executable file → Normal file
0
sound_meter/sound_meter_nonStop → sound_meter/old/sound_meter_nonStop
Executable file → Normal file
0
sound_meter/sound_meter_nonStop → sound_meter/old/sound_meter_nonStop
Executable file → Normal file
0
sound_meter/sound_meter_nonStop.c → sound_meter/old/sound_meter_nonStop.c
Executable file → Normal file
0
sound_meter/sound_meter_nonStop.c → sound_meter/old/sound_meter_nonStop.c
Executable file → Normal file
@@ -1,4 +1,4 @@
|
|||||||
'''
|
r'''
|
||||||
____ ___ _ _ _
|
____ ___ _ _ _
|
||||||
/ ___| / _ \| | (_) |_ ___
|
/ ___| / _ \| | (_) |_ ___
|
||||||
\___ \| | | | | | | __/ _ \
|
\___ \| | | | | | | __/ _ \
|
||||||
@@ -89,7 +89,8 @@ CREATE TABLE IF NOT EXISTS data_envea (
|
|||||||
h2s REAL,
|
h2s REAL,
|
||||||
nh3 REAL,
|
nh3 REAL,
|
||||||
co REAL,
|
co REAL,
|
||||||
o3 REAL
|
o3 REAL,
|
||||||
|
so2 REAL
|
||||||
)
|
)
|
||||||
""")
|
""")
|
||||||
|
|
||||||
@@ -126,6 +127,14 @@ CREATE TABLE IF NOT EXISTS data_MPPT (
|
|||||||
)
|
)
|
||||||
""")
|
""")
|
||||||
|
|
||||||
|
# Create a table noise capture (NSRT mk4)
|
||||||
|
cursor.execute("""
|
||||||
|
CREATE TABLE IF NOT EXISTS data_NOISE (
|
||||||
|
timestamp TEXT,
|
||||||
|
current_LEQ REAL,
|
||||||
|
DB_A_value REAL
|
||||||
|
)
|
||||||
|
""")
|
||||||
|
|
||||||
# Commit and close the connection
|
# Commit and close the connection
|
||||||
conn.commit()
|
conn.commit()
|
||||||
|
|||||||
232
sqlite/delete.py
Normal file
232
sqlite/delete.py
Normal file
@@ -0,0 +1,232 @@
|
|||||||
|
'''
|
||||||
|
____ ___ _ _ _
|
||||||
|
/ ___| / _ \| | (_) |_ ___
|
||||||
|
\___ \| | | | | | | __/ _ \
|
||||||
|
___) | |_| | |___| | || __/
|
||||||
|
|____/ \__\_\_____|_|\__\___|
|
||||||
|
|
||||||
|
Script to delete a table from sqlite database
|
||||||
|
/usr/bin/python3 /var/www/nebuleair_pro_4g/sqlite/delete.py table_name [--confirm]
|
||||||
|
|
||||||
|
Available tables are:
|
||||||
|
data_NPM
|
||||||
|
data_NPM_5channels
|
||||||
|
data_BME280
|
||||||
|
data_envea
|
||||||
|
timestamp_table
|
||||||
|
data_MPPT
|
||||||
|
data_WIND
|
||||||
|
modem_status
|
||||||
|
config_table
|
||||||
|
envea_sondes_table
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
# Will ask for confirmation
|
||||||
|
/usr/bin/python3 /var/www/nebuleair_pro_4g/sqlite/delete.py data_NPM
|
||||||
|
|
||||||
|
# Skip confirmation prompt
|
||||||
|
/usr/bin/python3 /var/www/nebuleair_pro_4g/sqlite/delete.py data_NPM --confirm
|
||||||
|
|
||||||
|
# List all tables
|
||||||
|
/usr/bin/python3 /var/www/nebuleair_pro_4g/sqlite/delete.py --list
|
||||||
|
'''
|
||||||
|
|
||||||
|
import sqlite3
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
def list_tables(cursor):
|
||||||
|
"""List all tables in the database"""
|
||||||
|
cursor.execute("SELECT name FROM sqlite_master WHERE type='table' ORDER BY name")
|
||||||
|
tables = cursor.fetchall()
|
||||||
|
|
||||||
|
print("\n📋 Available tables:")
|
||||||
|
print("-" * 40)
|
||||||
|
for table in tables:
|
||||||
|
# Get row count for each table
|
||||||
|
cursor.execute(f"SELECT COUNT(*) FROM {table[0]}")
|
||||||
|
count = cursor.fetchone()[0]
|
||||||
|
print(f" {table[0]} ({count} rows)")
|
||||||
|
print("-" * 40)
|
||||||
|
|
||||||
|
def get_table_info(cursor, table_name):
|
||||||
|
"""Get information about a table"""
|
||||||
|
try:
|
||||||
|
# Check if table exists
|
||||||
|
cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name=?", (table_name,))
|
||||||
|
if not cursor.fetchone():
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Get row count
|
||||||
|
cursor.execute(f"SELECT COUNT(*) FROM {table_name}")
|
||||||
|
row_count = cursor.fetchone()[0]
|
||||||
|
|
||||||
|
# Get table schema
|
||||||
|
cursor.execute(f"PRAGMA table_info({table_name})")
|
||||||
|
columns = cursor.fetchall()
|
||||||
|
|
||||||
|
return {
|
||||||
|
'row_count': row_count,
|
||||||
|
'columns': columns
|
||||||
|
}
|
||||||
|
except sqlite3.Error as e:
|
||||||
|
print(f"Error getting table info: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def backup_table(cursor, table_name, db_path):
|
||||||
|
"""Create a backup of the table before deletion"""
|
||||||
|
try:
|
||||||
|
backup_dir = os.path.dirname(db_path)
|
||||||
|
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
|
||||||
|
backup_file = os.path.join(backup_dir, f"{table_name}_backup_{timestamp}.sql")
|
||||||
|
|
||||||
|
# Get table schema
|
||||||
|
cursor.execute(f"SELECT sql FROM sqlite_master WHERE type='table' AND name=?", (table_name,))
|
||||||
|
create_sql = cursor.fetchone()
|
||||||
|
|
||||||
|
if create_sql:
|
||||||
|
with open(backup_file, 'w') as f:
|
||||||
|
# Write table creation SQL
|
||||||
|
f.write(f"-- Backup of table {table_name} created on {datetime.now()}\n")
|
||||||
|
f.write(f"{create_sql[0]};\n\n")
|
||||||
|
|
||||||
|
# Write data
|
||||||
|
cursor.execute(f"SELECT * FROM {table_name}")
|
||||||
|
rows = cursor.fetchall()
|
||||||
|
|
||||||
|
if rows:
|
||||||
|
# Get column names
|
||||||
|
cursor.execute(f"PRAGMA table_info({table_name})")
|
||||||
|
columns = [col[1] for col in cursor.fetchall()]
|
||||||
|
|
||||||
|
f.write(f"-- Data for table {table_name}\n")
|
||||||
|
for row in rows:
|
||||||
|
values = []
|
||||||
|
for value in row:
|
||||||
|
if value is None:
|
||||||
|
values.append('NULL')
|
||||||
|
elif isinstance(value, str):
|
||||||
|
escaped_value = value.replace("'", "''")
|
||||||
|
values.append(f"'{escaped_value}'")
|
||||||
|
else:
|
||||||
|
values.append(str(value))
|
||||||
|
|
||||||
|
f.write(f"INSERT INTO {table_name} ({', '.join(columns)}) VALUES ({', '.join(values)});\n")
|
||||||
|
|
||||||
|
print(f"✓ Table backed up to: {backup_file}")
|
||||||
|
return backup_file
|
||||||
|
except Exception as e:
|
||||||
|
print(f"⚠️ Backup failed: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def delete_table(cursor, table_name, create_backup=True, db_path=None):
|
||||||
|
"""Delete a table from the database"""
|
||||||
|
|
||||||
|
# Get table info first
|
||||||
|
table_info = get_table_info(cursor, table_name)
|
||||||
|
if not table_info:
|
||||||
|
print(f"❌ Table '{table_name}' does not exist!")
|
||||||
|
return False
|
||||||
|
|
||||||
|
print(f"\n📊 Table Information:")
|
||||||
|
print(f" Name: {table_name}")
|
||||||
|
print(f" Rows: {table_info['row_count']}")
|
||||||
|
print(f" Columns: {len(table_info['columns'])}")
|
||||||
|
|
||||||
|
# Create backup if requested
|
||||||
|
backup_file = None
|
||||||
|
if create_backup and db_path:
|
||||||
|
print(f"\n💾 Creating backup...")
|
||||||
|
backup_file = backup_table(cursor, table_name, db_path)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Delete the table
|
||||||
|
cursor.execute(f"DROP TABLE {table_name}")
|
||||||
|
print(f"\n✅ Table '{table_name}' deleted successfully!")
|
||||||
|
|
||||||
|
if backup_file:
|
||||||
|
print(f" Backup saved: {backup_file}")
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
except sqlite3.Error as e:
|
||||||
|
print(f"❌ Error deleting table: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def main():
|
||||||
|
if len(sys.argv) < 2:
|
||||||
|
print("Usage: python3 delete_table.py <table_name> [--confirm] [--no-backup]")
|
||||||
|
print(" python3 delete_table.py --list")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
db_path = "/var/www/nebuleair_pro_4g/sqlite/sensors.db"
|
||||||
|
|
||||||
|
# Check if database exists
|
||||||
|
if not os.path.exists(db_path):
|
||||||
|
print(f"❌ Database not found: {db_path}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# Parse arguments
|
||||||
|
args = sys.argv[1:]
|
||||||
|
|
||||||
|
if '--list' in args:
|
||||||
|
# List all tables
|
||||||
|
conn = sqlite3.connect(db_path)
|
||||||
|
cursor = conn.cursor()
|
||||||
|
list_tables(cursor)
|
||||||
|
conn.close()
|
||||||
|
return
|
||||||
|
|
||||||
|
table_name = args[0]
|
||||||
|
skip_confirmation = '--confirm' in args
|
||||||
|
create_backup = '--no-backup' not in args
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Connect to database
|
||||||
|
conn = sqlite3.connect(db_path)
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
# List available tables first
|
||||||
|
list_tables(cursor)
|
||||||
|
|
||||||
|
# Check if table exists
|
||||||
|
table_info = get_table_info(cursor, table_name)
|
||||||
|
if not table_info:
|
||||||
|
print(f"\n❌ Table '{table_name}' does not exist!")
|
||||||
|
conn.close()
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# Confirmation prompt
|
||||||
|
if not skip_confirmation:
|
||||||
|
print(f"\n⚠️ WARNING: You are about to delete table '{table_name}'")
|
||||||
|
print(f" This table contains {table_info['row_count']} rows")
|
||||||
|
if create_backup:
|
||||||
|
print(f" A backup will be created before deletion")
|
||||||
|
else:
|
||||||
|
print(f" NO BACKUP will be created (--no-backup flag used)")
|
||||||
|
|
||||||
|
response = input(f"\nAre you sure you want to delete '{table_name}'? (yes/no): ").lower().strip()
|
||||||
|
|
||||||
|
if response not in ['yes', 'y']:
|
||||||
|
print("❌ Operation cancelled")
|
||||||
|
conn.close()
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
# Perform deletion
|
||||||
|
success = delete_table(cursor, table_name, create_backup, db_path)
|
||||||
|
|
||||||
|
if success:
|
||||||
|
conn.commit()
|
||||||
|
print(f"\n🎉 Operation completed successfully!")
|
||||||
|
else:
|
||||||
|
print(f"\n❌ Operation failed!")
|
||||||
|
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Error: {e}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -9,6 +9,9 @@ Script to flush (delete) data from a sqlite database
|
|||||||
|
|
||||||
/usr/bin/python3 /var/www/nebuleair_pro_4g/sqlite/flush_old_data.py
|
/usr/bin/python3 /var/www/nebuleair_pro_4g/sqlite/flush_old_data.py
|
||||||
|
|
||||||
|
Script that is triggered by a systemd
|
||||||
|
sudo systemctl status nebuleair-db-cleanup-data.service
|
||||||
|
|
||||||
Available table are
|
Available table are
|
||||||
|
|
||||||
data_NPM
|
data_NPM
|
||||||
@@ -16,56 +19,184 @@ data_NPM_5channels
|
|||||||
data_BME280
|
data_BME280
|
||||||
data_envea
|
data_envea
|
||||||
timestamp_table
|
timestamp_table
|
||||||
|
data_MPPT
|
||||||
|
data_NOISE
|
||||||
|
data_WIND
|
||||||
|
|
||||||
'''
|
'''
|
||||||
|
|
||||||
import sqlite3
|
import sqlite3
|
||||||
import datetime
|
import datetime
|
||||||
|
import sys
|
||||||
|
|
||||||
|
|
||||||
# Connect to the SQLite database
|
def table_exists(cursor, table_name):
|
||||||
conn = sqlite3.connect("/var/www/nebuleair_pro_4g/sqlite/sensors.db")
|
"""Check if a table exists in the database"""
|
||||||
cursor = conn.cursor()
|
try:
|
||||||
|
cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name=?", (table_name,))
|
||||||
#GET RTC TIME from SQlite
|
return cursor.fetchone() is not None
|
||||||
cursor.execute("SELECT * FROM timestamp_table LIMIT 1")
|
except sqlite3.Error as e:
|
||||||
row = cursor.fetchone() # Get the first (and only) row
|
print(f"[ERROR] Failed to check if table '{table_name}' exists: {e}")
|
||||||
|
return False
|
||||||
if row:
|
|
||||||
rtc_time_str = row[1] # Assuming timestamp is stored as TEXT (YYYY-MM-DD HH:MM:SS)
|
|
||||||
print(f"[INFO] Last recorded timestamp: {rtc_time_str}")
|
|
||||||
|
|
||||||
# Convert last_updated to a datetime object
|
|
||||||
last_updated = datetime.datetime.strptime(rtc_time_str, "%Y-%m-%d %H:%M:%S")
|
|
||||||
|
|
||||||
# Calculate the cutoff date (3 months before last_updated)
|
|
||||||
cutoff_date = last_updated - datetime.timedelta(days=60)
|
|
||||||
cutoff_date_str = cutoff_date.strftime("%Y-%m-%d %H:%M:%S")
|
|
||||||
|
|
||||||
print(f"[INFO] Deleting records older than: {cutoff_date_str}")
|
|
||||||
|
|
||||||
# List of tables to delete old data from
|
|
||||||
tables_to_clean = ["data_NPM", "data_NPM_5channels", "data_BME280", "data_envea","data_WIND", "data_MPPT"]
|
|
||||||
|
|
||||||
# Loop through each table and delete old data
|
|
||||||
for table in tables_to_clean:
|
|
||||||
delete_query = f"DELETE FROM {table} WHERE timestamp < ?"
|
|
||||||
cursor.execute(delete_query, (cutoff_date_str,))
|
|
||||||
print(f"[INFO] Deleted old records from {table}")
|
|
||||||
|
|
||||||
# **Commit changes before running VACUUM**
|
|
||||||
conn.commit()
|
|
||||||
print("[INFO] Changes committed successfully!")
|
|
||||||
|
|
||||||
# Now it's safe to run VACUUM
|
|
||||||
print("[INFO] Running VACUUM to optimize database space...")
|
|
||||||
cursor.execute("VACUUM")
|
|
||||||
|
|
||||||
print("[SUCCESS] Old data flushed successfully!")
|
|
||||||
|
|
||||||
else:
|
|
||||||
print("[ERROR] No timestamp found in timestamp_table.")
|
|
||||||
|
|
||||||
|
|
||||||
# Close the database connection
|
def get_table_count(cursor, table_name):
|
||||||
conn.close()
|
"""Get the number of records in a table"""
|
||||||
|
try:
|
||||||
|
cursor.execute(f"SELECT COUNT(*) FROM {table_name}")
|
||||||
|
return cursor.fetchone()[0]
|
||||||
|
except sqlite3.Error as e:
|
||||||
|
print(f"[WARNING] Could not get count for table '{table_name}': {e}")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def delete_old_records(cursor, table_name, cutoff_date_str):
|
||||||
|
"""Delete old records from a specific table"""
|
||||||
|
try:
|
||||||
|
# First check how many records will be deleted
|
||||||
|
cursor.execute(f"SELECT COUNT(*) FROM {table_name} WHERE timestamp < ?", (cutoff_date_str,))
|
||||||
|
records_to_delete = cursor.fetchone()[0]
|
||||||
|
|
||||||
|
if records_to_delete == 0:
|
||||||
|
print(f"[INFO] No old records to delete from '{table_name}'")
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Delete the records
|
||||||
|
cursor.execute(f"DELETE FROM {table_name} WHERE timestamp < ?", (cutoff_date_str,))
|
||||||
|
deleted_count = cursor.rowcount
|
||||||
|
|
||||||
|
print(f"[SUCCESS] Deleted {deleted_count} old records from '{table_name}'")
|
||||||
|
return True
|
||||||
|
|
||||||
|
except sqlite3.Error as e:
|
||||||
|
print(f"[ERROR] Failed to delete records from '{table_name}': {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
try:
|
||||||
|
# Connect to the SQLite database
|
||||||
|
print("[INFO] Connecting to database...")
|
||||||
|
conn = sqlite3.connect("/var/www/nebuleair_pro_4g/sqlite/sensors.db")
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
# Check database connection
|
||||||
|
cursor.execute("SELECT sqlite_version()")
|
||||||
|
version = cursor.fetchone()[0]
|
||||||
|
print(f"[INFO] Connected to SQLite version: {version}")
|
||||||
|
|
||||||
|
# GET RTC TIME from SQLite
|
||||||
|
print("[INFO] Getting timestamp from database...")
|
||||||
|
|
||||||
|
# First check if timestamp_table exists
|
||||||
|
if not table_exists(cursor, "timestamp_table"):
|
||||||
|
print("[ERROR] timestamp_table does not exist!")
|
||||||
|
return False
|
||||||
|
|
||||||
|
cursor.execute("SELECT * FROM timestamp_table LIMIT 1")
|
||||||
|
row = cursor.fetchone()
|
||||||
|
|
||||||
|
if not row:
|
||||||
|
print("[ERROR] No timestamp found in timestamp_table.")
|
||||||
|
return False
|
||||||
|
|
||||||
|
rtc_time_str = row[1] # Assuming timestamp is stored as TEXT (YYYY-MM-DD HH:MM:SS)
|
||||||
|
print(f"[INFO] Last recorded timestamp: {rtc_time_str}")
|
||||||
|
|
||||||
|
# Convert last_updated to a datetime object
|
||||||
|
try:
|
||||||
|
last_updated = datetime.datetime.strptime(rtc_time_str, "%Y-%m-%d %H:%M:%S")
|
||||||
|
except ValueError as e:
|
||||||
|
print(f"[ERROR] Invalid timestamp format: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Calculate the cutoff date (60 days before last_updated)
|
||||||
|
cutoff_date = last_updated - datetime.timedelta(days=60)
|
||||||
|
cutoff_date_str = cutoff_date.strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
|
||||||
|
print(f"[INFO] Deleting records older than: {cutoff_date_str}")
|
||||||
|
|
||||||
|
# List of tables to delete old data from
|
||||||
|
tables_to_clean = [
|
||||||
|
"data_NPM",
|
||||||
|
"data_NPM_5channels",
|
||||||
|
"data_BME280",
|
||||||
|
"data_envea",
|
||||||
|
"data_WIND",
|
||||||
|
"data_MPPT",
|
||||||
|
"data_NOISE"
|
||||||
|
]
|
||||||
|
|
||||||
|
# Check which tables actually exist
|
||||||
|
existing_tables = []
|
||||||
|
missing_tables = []
|
||||||
|
|
||||||
|
for table in tables_to_clean:
|
||||||
|
if table_exists(cursor, table):
|
||||||
|
existing_tables.append(table)
|
||||||
|
record_count = get_table_count(cursor, table)
|
||||||
|
print(f"[INFO] Table '{table}' exists with {record_count} records")
|
||||||
|
else:
|
||||||
|
missing_tables.append(table)
|
||||||
|
print(f"[WARNING] Table '{table}' does not exist - skipping")
|
||||||
|
|
||||||
|
if missing_tables:
|
||||||
|
print(f"[INFO] Missing tables: {', '.join(missing_tables)}")
|
||||||
|
|
||||||
|
if not existing_tables:
|
||||||
|
print("[WARNING] No tables found to clean!")
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Loop through existing tables and delete old data
|
||||||
|
successful_deletions = 0
|
||||||
|
failed_deletions = 0
|
||||||
|
|
||||||
|
for table in existing_tables:
|
||||||
|
if delete_old_records(cursor, table, cutoff_date_str):
|
||||||
|
successful_deletions += 1
|
||||||
|
else:
|
||||||
|
failed_deletions += 1
|
||||||
|
|
||||||
|
# Commit changes before running VACUUM
|
||||||
|
print("[INFO] Committing changes...")
|
||||||
|
conn.commit()
|
||||||
|
print("[SUCCESS] Changes committed successfully!")
|
||||||
|
|
||||||
|
# Only run VACUUM if at least some deletions were successful
|
||||||
|
if successful_deletions > 0:
|
||||||
|
print("[INFO] Running VACUUM to optimize database space...")
|
||||||
|
try:
|
||||||
|
cursor.execute("VACUUM")
|
||||||
|
print("[SUCCESS] Database optimized successfully!")
|
||||||
|
except sqlite3.Error as e:
|
||||||
|
print(f"[WARNING] VACUUM failed: {e}")
|
||||||
|
|
||||||
|
# Summary
|
||||||
|
print(f"\n[SUMMARY]")
|
||||||
|
print(f"Tables processed successfully: {successful_deletions}")
|
||||||
|
print(f"Tables with errors: {failed_deletions}")
|
||||||
|
print(f"Tables skipped (missing): {len(missing_tables)}")
|
||||||
|
|
||||||
|
if failed_deletions == 0:
|
||||||
|
print("[SUCCESS] Old data flushed successfully!")
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
print("[WARNING] Some operations failed - check logs above")
|
||||||
|
return False
|
||||||
|
|
||||||
|
except sqlite3.Error as e:
|
||||||
|
print(f"[ERROR] Database error: {e}")
|
||||||
|
return False
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[ERROR] Unexpected error: {e}")
|
||||||
|
return False
|
||||||
|
finally:
|
||||||
|
# Always close the database connection
|
||||||
|
if 'conn' in locals():
|
||||||
|
conn.close()
|
||||||
|
print("[INFO] Database connection closed")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
success = main()
|
||||||
|
sys.exit(0 if success else 1)
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
'''
|
r'''
|
||||||
____ ___ _ _ _
|
____ ___ _ _ _
|
||||||
/ ___| / _ \| | (_) |_ ___
|
/ ___| / _ \| | (_) |_ ___
|
||||||
\___ \| | | | | | | __/ _ \
|
\___ \| | | | | | | __/ _ \
|
||||||
@@ -41,13 +41,17 @@ config_entries = [
|
|||||||
("SARA_R4_network_status", "connected", "str"),
|
("SARA_R4_network_status", "connected", "str"),
|
||||||
("SARA_R4_neworkID", "20810", "int"),
|
("SARA_R4_neworkID", "20810", "int"),
|
||||||
("WIFI_status", "connected", "str"),
|
("WIFI_status", "connected", "str"),
|
||||||
|
("send_aircarto", "1", "bool"),
|
||||||
("send_uSpot", "0", "bool"),
|
("send_uSpot", "0", "bool"),
|
||||||
|
("send_miotiq", "0", "bool"),
|
||||||
("npm_5channel", "0", "bool"),
|
("npm_5channel", "0", "bool"),
|
||||||
("envea", "0", "bool"),
|
("envea", "0", "bool"),
|
||||||
("windMeter", "0", "bool"),
|
("windMeter", "0", "bool"),
|
||||||
("BME280", "0", "bool"),
|
("BME280", "0", "bool"),
|
||||||
("MPPT", "0", "bool"),
|
("MPPT", "0", "bool"),
|
||||||
("modem_version", "XXX", "str")
|
("NOISE", "0", "bool"),
|
||||||
|
("modem_version", "XXX", "str"),
|
||||||
|
("language", "fr", "str")
|
||||||
]
|
]
|
||||||
|
|
||||||
for key, value, value_type in config_entries:
|
for key, value, value_type in config_entries:
|
||||||
@@ -56,18 +60,47 @@ for key, value, value_type in config_entries:
|
|||||||
(key, value, value_type)
|
(key, value, value_type)
|
||||||
)
|
)
|
||||||
|
|
||||||
# Insert envea sondes
|
# Clean up duplicate envea sondes first (keep only first occurrence of each name)
|
||||||
|
print("Cleaning up duplicate envea sondes...")
|
||||||
|
cursor.execute("""
|
||||||
|
DELETE FROM envea_sondes_table
|
||||||
|
WHERE id NOT IN (
|
||||||
|
SELECT MIN(id)
|
||||||
|
FROM envea_sondes_table
|
||||||
|
GROUP BY name
|
||||||
|
)
|
||||||
|
""")
|
||||||
|
deleted_count = cursor.rowcount
|
||||||
|
if deleted_count > 0:
|
||||||
|
print(f"Deleted {deleted_count} duplicate envea sonde entries")
|
||||||
|
|
||||||
|
# Insert envea sondes (only if they don't already exist)
|
||||||
|
# Attention pour le H2S il y a plusieurs sondes
|
||||||
|
# H2S 1ppm -> coef 4
|
||||||
|
# H2S 20ppm -> coef 1
|
||||||
|
# H2S 200ppm -> coef 10
|
||||||
|
|
||||||
envea_sondes = [
|
envea_sondes = [
|
||||||
(False, "ttyAMA4", "h2s", 4),
|
(False, "ttyAMA4", "h2s", 4), #H2S
|
||||||
(False, "ttyAMA3", "no2", 1),
|
(False, "ttyAMA3", "no2", 1),
|
||||||
|
(False, "ttyAMA3", "nh3", 100),
|
||||||
|
(False, "ttyAMA3", "so2", 4),
|
||||||
(False, "ttyAMA2", "o3", 1)
|
(False, "ttyAMA2", "o3", 1)
|
||||||
]
|
]
|
||||||
|
|
||||||
for connected, port, name, coefficient in envea_sondes:
|
for connected, port, name, coefficient in envea_sondes:
|
||||||
cursor.execute(
|
# Check if sensor with this name already exists
|
||||||
"INSERT OR IGNORE INTO envea_sondes_table (connected, port, name, coefficient) VALUES (?, ?, ?, ?)",
|
cursor.execute("SELECT COUNT(*) FROM envea_sondes_table WHERE name = ?", (name,))
|
||||||
(1 if connected else 0, port, name, coefficient)
|
exists = cursor.fetchone()[0] > 0
|
||||||
)
|
|
||||||
|
if not exists:
|
||||||
|
cursor.execute(
|
||||||
|
"INSERT INTO envea_sondes_table (connected, port, name, coefficient) VALUES (?, ?, ?, ?)",
|
||||||
|
(1 if connected else 0, port, name, coefficient)
|
||||||
|
)
|
||||||
|
print(f"Added envea sonde: {name}")
|
||||||
|
else:
|
||||||
|
print(f"Envea sonde '{name}' already exists, skipping")
|
||||||
|
|
||||||
|
|
||||||
# Commit and close the connection
|
# Commit and close the connection
|
||||||
|
|||||||
39
update_firmware.sh
Normal file → Executable file
39
update_firmware.sh
Normal file → Executable file
@@ -3,6 +3,7 @@
|
|||||||
# NebuleAir Pro 4G - Comprehensive Update Script
|
# NebuleAir Pro 4G - Comprehensive Update Script
|
||||||
# This script performs a complete system update including git pull,
|
# This script performs a complete system update including git pull,
|
||||||
# config initialization, and service management
|
# config initialization, and service management
|
||||||
|
# Non-interactive version for WebUI
|
||||||
|
|
||||||
echo "======================================"
|
echo "======================================"
|
||||||
echo "NebuleAir Pro 4G - Firmware Update"
|
echo "NebuleAir Pro 4G - Firmware Update"
|
||||||
@@ -13,6 +14,9 @@ echo ""
|
|||||||
# Set working directory
|
# Set working directory
|
||||||
cd /var/www/nebuleair_pro_4g
|
cd /var/www/nebuleair_pro_4g
|
||||||
|
|
||||||
|
# Ensure this script is executable
|
||||||
|
chmod +x /var/www/nebuleair_pro_4g/update_firmware.sh
|
||||||
|
|
||||||
# Function to print status messages
|
# Function to print status messages
|
||||||
print_status() {
|
print_status() {
|
||||||
echo "[$(date '+%H:%M:%S')] $1"
|
echo "[$(date '+%H:%M:%S')] $1"
|
||||||
@@ -30,14 +34,23 @@ check_status() {
|
|||||||
|
|
||||||
# Step 1: Git operations
|
# Step 1: Git operations
|
||||||
print_status "Step 1: Updating firmware from repository..."
|
print_status "Step 1: Updating firmware from repository..."
|
||||||
|
|
||||||
|
# Disable filemode to prevent permission issues
|
||||||
|
git -C /var/www/nebuleair_pro_4g config core.fileMode false
|
||||||
|
check_status "Git fileMode disabled"
|
||||||
|
|
||||||
|
# Fetch latest changes
|
||||||
git fetch origin
|
git fetch origin
|
||||||
check_status "Git fetch"
|
check_status "Git fetch"
|
||||||
|
|
||||||
# Show current branch and any changes
|
# Show current branch
|
||||||
print_status "Current branch: $(git branch --show-current)"
|
print_status "Current branch: $(git branch --show-current)"
|
||||||
|
|
||||||
|
# Check for local changes
|
||||||
if [ -n "$(git status --porcelain)" ]; then
|
if [ -n "$(git status --porcelain)" ]; then
|
||||||
print_status "Warning: Local changes detected:"
|
print_status "Warning: Local changes detected, stashing..."
|
||||||
git status --short
|
git stash push -m "Auto-stash before update $(date)"
|
||||||
|
check_status "Git stash"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Pull latest changes
|
# Pull latest changes
|
||||||
@@ -59,6 +72,7 @@ sudo chmod 755 /var/www/nebuleair_pro_4g/NPM/*.py
|
|||||||
sudo chmod 755 /var/www/nebuleair_pro_4g/BME280/*.py
|
sudo chmod 755 /var/www/nebuleair_pro_4g/BME280/*.py
|
||||||
sudo chmod 755 /var/www/nebuleair_pro_4g/SARA/*.py
|
sudo chmod 755 /var/www/nebuleair_pro_4g/SARA/*.py
|
||||||
sudo chmod 755 /var/www/nebuleair_pro_4g/envea/*.py
|
sudo chmod 755 /var/www/nebuleair_pro_4g/envea/*.py
|
||||||
|
sudo chmod 755 /var/www/nebuleair_pro_4g/MPPT/*.py 2>/dev/null
|
||||||
check_status "File permissions update"
|
check_status "File permissions update"
|
||||||
|
|
||||||
# Step 4: Restart critical services if they exist
|
# Step 4: Restart critical services if they exist
|
||||||
@@ -72,16 +86,22 @@ services=(
|
|||||||
"nebuleair-sara-data.timer"
|
"nebuleair-sara-data.timer"
|
||||||
"nebuleair-bme280-data.timer"
|
"nebuleair-bme280-data.timer"
|
||||||
"nebuleair-mppt-data.timer"
|
"nebuleair-mppt-data.timer"
|
||||||
|
"nebuleair-noise-data.timer"
|
||||||
)
|
)
|
||||||
|
|
||||||
for service in "${services[@]}"; do
|
for service in "${services[@]}"; do
|
||||||
if systemctl list-unit-files | grep -q "$service"; then
|
if systemctl list-unit-files | grep -q "$service"; then
|
||||||
print_status "Restarting service: $service"
|
# Check if service is enabled before restarting
|
||||||
sudo systemctl restart "$service"
|
if systemctl is-enabled --quiet "$service" 2>/dev/null; then
|
||||||
if systemctl is-active --quiet "$service"; then
|
print_status "Restarting enabled service: $service"
|
||||||
print_status "✓ $service is running"
|
sudo systemctl restart "$service"
|
||||||
|
if systemctl is-active --quiet "$service"; then
|
||||||
|
print_status "✓ $service is running"
|
||||||
|
else
|
||||||
|
print_status "⚠ $service failed to start"
|
||||||
|
fi
|
||||||
else
|
else
|
||||||
print_status "⚠ $service may not be active"
|
print_status "ℹ Service $service is disabled, skipping restart"
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
print_status "ℹ Service $service not found (may not be installed)"
|
print_status "ℹ Service $service not found (may not be installed)"
|
||||||
@@ -113,6 +133,9 @@ print_status "Step 6: Cleaning up..."
|
|||||||
sudo find /var/www/nebuleair_pro_4g/logs -name "*.log" -size +10M -exec truncate -s 0 {} \;
|
sudo find /var/www/nebuleair_pro_4g/logs -name "*.log" -size +10M -exec truncate -s 0 {} \;
|
||||||
check_status "Log cleanup"
|
check_status "Log cleanup"
|
||||||
|
|
||||||
|
print_status ""
|
||||||
|
print_status "======================================"
|
||||||
print_status "Update completed successfully!"
|
print_status "Update completed successfully!"
|
||||||
|
print_status "======================================"
|
||||||
|
|
||||||
exit 0
|
exit 0
|
||||||
Reference in New Issue
Block a user