Compare commits
198 Commits
fdef8e2df0
...
v1.6.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
11585b4783 | ||
|
|
52b86dbc3d | ||
|
|
361c0d1a76 | ||
|
|
bd2e1f1eda | ||
|
|
2b4e9205c1 | ||
|
|
b3c019c27b | ||
|
|
e733cd27e8 | ||
|
|
a9db7750b2 | ||
|
|
c42656e0ae | ||
|
|
eb93ba49bd | ||
|
|
3804a52fda | ||
|
|
ee0577c504 | ||
|
|
72fbbb82a1 | ||
|
|
5b3769769d | ||
|
|
6be18b5bde | ||
|
|
7619caffc4 | ||
|
|
85596c3882 | ||
|
|
6a00ab85d9 | ||
|
|
2ff47dc877 | ||
|
|
d2a3eafaa1 | ||
|
|
6706b22f21 | ||
|
|
ffe13d3639 | ||
|
|
7b324f8ab8 | ||
|
|
ad0f83ce71 | ||
|
|
928c1a1d4e | ||
|
|
24cb96e9a9 | ||
|
|
e2f765de8a | ||
|
|
cb98e38a3e | ||
|
|
4fe79ad112 | ||
|
|
b869ac3e9e | ||
|
|
c09fa3ca72 | ||
|
|
79d9be2c85 | ||
|
|
903dcce2d7 | ||
|
|
425a89de3f | ||
|
|
8f88eae575 | ||
|
|
ffead8597a | ||
|
|
196176667f | ||
|
|
87ddb76e39 | ||
|
|
dbe6c71d33 | ||
|
|
537abb682e | ||
|
|
8d74e3e678 | ||
|
|
5849190220 | ||
|
|
408ab767e1 | ||
|
|
2949c78b56 | ||
|
|
c83f8396aa | ||
|
|
ecd59e537e | ||
|
|
83d854b596 | ||
|
|
a0f8b4b8eb | ||
|
|
8d0507852a | ||
|
|
6e17f39a2c | ||
|
|
5a2b3bb19d | ||
|
|
5bffec10a1 | ||
|
|
e0e8a4cefe | ||
|
|
d5b2e9c6c3 | ||
|
|
7ab06f3413 | ||
|
|
794b86fb9b | ||
|
|
7479344df7 | ||
|
|
98b5b43190 | ||
|
|
1298e79688 | ||
|
|
7a7d1c0c3f | ||
|
|
7c30ccd8f7 | ||
|
|
bc2aec7946 | ||
|
|
6b8d0c18c9 | ||
|
|
b65e9571dc | ||
|
|
e8cef5b593 | ||
|
|
36d4bac0a5 | ||
|
|
a208540093 | ||
|
|
02687f6d74 | ||
|
|
8c55798e34 | ||
|
|
cf502abfef | ||
|
|
e659696044 | ||
|
|
d086a440dd | ||
|
|
86c2d1eb41 | ||
|
|
aa1b90e3d5 | ||
|
|
f1d716d900 | ||
|
|
248732bac9 | ||
|
|
7b0fb0650a | ||
|
|
3e5ee9c77e | ||
|
|
8106af624f | ||
|
|
30bc04b89e | ||
|
|
198836fa13 | ||
|
|
ea2642685c | ||
|
|
f8f5300b9b | ||
|
|
b88d2bc1d9 | ||
|
|
88680f07b0 | ||
|
|
20c6a12251 | ||
|
|
e20bb0b8fc | ||
|
|
50a8cdd938 | ||
|
|
dc1739e033 | ||
|
|
544eebd715 | ||
|
|
6bdaef8c24 | ||
|
|
98cb1ea517 | ||
|
|
4ed185de0c | ||
|
|
3d61ce22d3 | ||
|
|
3a6b529cba | ||
|
|
3c8558ea1d | ||
|
|
49a4623d85 | ||
|
|
d3d72410c1 | ||
|
|
fcfbe4f2d4 | ||
|
|
53e7c77322 | ||
|
|
20ba897cde | ||
|
|
15e43513f4 | ||
|
|
a5717df182 | ||
|
|
8aaed1b93f | ||
|
|
1fa7a2d695 | ||
|
|
80bc16fb26 | ||
|
|
042b2efa93 | ||
|
|
80fcd8bf37 | ||
|
|
b60f044105 | ||
|
|
eeaaeca4a7 | ||
|
|
91a4e7c841 | ||
|
|
8291475e36 | ||
|
|
994bbf7a8d | ||
|
|
e5770b09dc | ||
|
|
79d7f61e4a | ||
|
|
d593449171 | ||
|
|
c571bbd408 | ||
|
|
5742cc7e49 | ||
|
|
4c552e4a31 | ||
|
|
5777b35770 | ||
|
|
13445d574c | ||
|
|
10f84f0c1b | ||
|
|
4f1b140a75 | ||
|
|
a3b2bef5c1 | ||
|
|
f679732591 | ||
|
|
857d590b8f | ||
|
|
141dd68716 | ||
|
|
79a9217307 | ||
|
|
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 | ||
|
|
020594e065 | ||
|
|
5a1a4e0d81 | ||
|
|
3cd5b13c25 | ||
|
|
5a0f1c0745 | ||
|
|
2516a3bd1c | ||
|
|
1b8dc54fe0 | ||
|
|
2bd74ca91a | ||
|
|
f40c105abf |
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
|
||||||
|
}
|
||||||
8
.gitignore
vendored
8
.gitignore
vendored
@@ -14,4 +14,12 @@ NPM/data/*.txt
|
|||||||
NPM/data/*.json
|
NPM/data/*.json
|
||||||
*.lock
|
*.lock
|
||||||
sqlite/*.db
|
sqlite/*.db
|
||||||
|
sqlite/*.sql
|
||||||
|
|
||||||
tests/
|
tests/
|
||||||
|
|
||||||
|
# Secrets
|
||||||
|
.env
|
||||||
|
|
||||||
|
# Claude Code local settings
|
||||||
|
.claude/settings.local.json
|
||||||
26
.update-exclude
Normal file
26
.update-exclude
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
# NebuleAir Pro 4G - Fichiers exclus lors de la mise à jour par upload
|
||||||
|
# Ce fichier est versionné dans le repo et voyage avec chaque release.
|
||||||
|
# Quand on ajoute un nouveau capteur avec du cache local, mettre à jour cette liste.
|
||||||
|
|
||||||
|
# Base de données (données capteur, config locale)
|
||||||
|
sqlite/sensors.db
|
||||||
|
sqlite/*.db-journal
|
||||||
|
sqlite/*.db-wal
|
||||||
|
|
||||||
|
# Logs applicatifs
|
||||||
|
logs/
|
||||||
|
|
||||||
|
# Historique git (pour que git pull fonctionne toujours après)
|
||||||
|
.git/
|
||||||
|
|
||||||
|
# Fichiers de configuration locale
|
||||||
|
config.json
|
||||||
|
deviceID.txt
|
||||||
|
wifi_list.csv
|
||||||
|
|
||||||
|
# Données capteurs en cache
|
||||||
|
envea/data/
|
||||||
|
NPM/data/
|
||||||
|
|
||||||
|
# Verrous
|
||||||
|
*.lock
|
||||||
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
|
||||||
53
MH-Z19/get_data.py
Normal file
53
MH-Z19/get_data.py
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
'''
|
||||||
|
Script to get CO2 values from MH-Z19 sensor
|
||||||
|
need parameter: CO2_port
|
||||||
|
/usr/bin/python3 /var/www/nebuleair_pro_4g/MH-Z19/get_data.py ttyAMA4
|
||||||
|
'''
|
||||||
|
|
||||||
|
import serial
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
|
||||||
|
parameter = sys.argv[1:]
|
||||||
|
port = '/dev/' + parameter[0]
|
||||||
|
|
||||||
|
|
||||||
|
def read_co2():
|
||||||
|
try:
|
||||||
|
ser = serial.Serial(
|
||||||
|
port=port,
|
||||||
|
baudrate=9600,
|
||||||
|
parity=serial.PARITY_NONE,
|
||||||
|
stopbits=serial.STOPBITS_ONE,
|
||||||
|
bytesize=serial.EIGHTBITS,
|
||||||
|
timeout=1
|
||||||
|
)
|
||||||
|
except serial.SerialException as e:
|
||||||
|
print(json.dumps({"error": f"Serial port error: {e}"}))
|
||||||
|
return
|
||||||
|
|
||||||
|
READ_CO2_COMMAND = b'\xFF\x01\x86\x00\x00\x00\x00\x00\x79'
|
||||||
|
|
||||||
|
try:
|
||||||
|
ser.write(READ_CO2_COMMAND)
|
||||||
|
time.sleep(2)
|
||||||
|
response = ser.read(9)
|
||||||
|
|
||||||
|
if len(response) < 9:
|
||||||
|
print(json.dumps({"error": "No data or incomplete data received from sensor"}))
|
||||||
|
return
|
||||||
|
|
||||||
|
if response[0] == 0xFF:
|
||||||
|
co2_concentration = response[2] * 256 + response[3]
|
||||||
|
print(json.dumps({"CO2": co2_concentration}))
|
||||||
|
else:
|
||||||
|
print(json.dumps({"error": "Invalid response from sensor"}))
|
||||||
|
except Exception as e:
|
||||||
|
print(json.dumps({"error": str(e)}))
|
||||||
|
finally:
|
||||||
|
ser.close()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
read_co2()
|
||||||
66
MH-Z19/write_data.py
Normal file
66
MH-Z19/write_data.py
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
'''
|
||||||
|
Script to get CO2 values from MH-Z19 sensor and write to database
|
||||||
|
/usr/bin/python3 /var/www/nebuleair_pro_4g/MH-Z19/write_data.py
|
||||||
|
'''
|
||||||
|
|
||||||
|
import serial
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
import sqlite3
|
||||||
|
|
||||||
|
conn = sqlite3.connect("/var/www/nebuleair_pro_4g/sqlite/sensors.db")
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
mh_z19_port = "/dev/ttyAMA4"
|
||||||
|
|
||||||
|
ser = serial.Serial(
|
||||||
|
port=mh_z19_port,
|
||||||
|
baudrate=9600,
|
||||||
|
parity=serial.PARITY_NONE,
|
||||||
|
stopbits=serial.STOPBITS_ONE,
|
||||||
|
bytesize=serial.EIGHTBITS,
|
||||||
|
timeout=1
|
||||||
|
)
|
||||||
|
|
||||||
|
READ_CO2_COMMAND = b'\xFF\x01\x86\x00\x00\x00\x00\x00\x79'
|
||||||
|
|
||||||
|
|
||||||
|
def read_co2():
|
||||||
|
ser.write(READ_CO2_COMMAND)
|
||||||
|
time.sleep(2)
|
||||||
|
response = ser.read(9)
|
||||||
|
if len(response) < 9:
|
||||||
|
print("Error: No data or incomplete data received from CO2 sensor.")
|
||||||
|
return None
|
||||||
|
if response[0] == 0xFF:
|
||||||
|
co2_concentration = response[2] * 256 + response[3]
|
||||||
|
return co2_concentration
|
||||||
|
else:
|
||||||
|
print("Error reading data from CO2 sensor.")
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
try:
|
||||||
|
co2 = read_co2()
|
||||||
|
if co2 is not None:
|
||||||
|
# Get RTC time from SQLite
|
||||||
|
cursor.execute("SELECT * FROM timestamp_table LIMIT 1")
|
||||||
|
row = cursor.fetchone()
|
||||||
|
rtc_time_str = row[1]
|
||||||
|
# Save to SQLite
|
||||||
|
cursor.execute('INSERT INTO data_MHZ19 (timestamp, CO2) VALUES (?, ?)', (rtc_time_str, co2))
|
||||||
|
conn.commit()
|
||||||
|
print(f"CO2: {co2} ppm (saved at {rtc_time_str})")
|
||||||
|
else:
|
||||||
|
print("Failed to get CO2 data.")
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print("Program terminated.")
|
||||||
|
finally:
|
||||||
|
ser.close()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
251
MPPT/read.py
251
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)
|
|
||||||
|
|
||||||
# Initialize data dictionary and tracking variables
|
# Clear any buffered data
|
||||||
data = {}
|
ser.reset_input_buffer()
|
||||||
start_time = time.time()
|
time.sleep(0.5)
|
||||||
|
|
||||||
while time.time() - start_time < timeout:
|
# Initialize data dictionary
|
||||||
|
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
|
lines_read += 1
|
||||||
if '\t' in line:
|
|
||||||
key, value = line.split('\t', 1)
|
# Check if this line contains tab-separated key-value pair
|
||||||
data[key] = value
|
if '\t' in line:
|
||||||
print(f"{key}: {value}")
|
parts = line.split('\t', 1)
|
||||||
else:
|
if len(parts) == 2:
|
||||||
print(f"Info: {line}")
|
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 a complete data block
|
|
||||||
if 'Checksum' in data:
|
|
||||||
# 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,
|
||||||
|
# something is wrong
|
||||||
|
log("Multiple incomplete blocks, clearing data...")
|
||||||
|
data = {}
|
||||||
|
|
||||||
# Timeout occurred
|
except UnicodeDecodeError as e:
|
||||||
print(f"Timeout on attempt {attempt+1}: Could not get complete data")
|
log(f"Decode error: {e}", "ERROR")
|
||||||
ser.close()
|
continue
|
||||||
|
except Exception as e:
|
||||||
|
log(f"Error reading line: {e}", "ERROR")
|
||||||
|
continue
|
||||||
|
|
||||||
# Add small delay between attempts
|
# Timeout reached
|
||||||
if attempt < max_attempts - 1:
|
log(f"Timeout after {timeout}s, read {lines_read} lines, saw {blocks_seen} blocks")
|
||||||
print("Waiting before next attempt...")
|
ser.close()
|
||||||
time.sleep(2)
|
|
||||||
|
|
||||||
except Exception as e:
|
# If we have some data but not all required keys, return what we have
|
||||||
print(f"Error on attempt {attempt+1}: {e}")
|
if data and len(data) >= len(required_keys) - 1:
|
||||||
try:
|
log("Returning partial data...")
|
||||||
ser.close()
|
return data
|
||||||
except:
|
|
||||||
pass
|
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,25 +218,37 @@ 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))}")
|
||||||
|
log(f"MPPT Status: {get_mppt_status(parsed_data.get('MPPT', 0))}")
|
||||||
|
log(f"Load Output: {parsed_data.get('LOAD', 'Unknown')}, {parsed_data.get('IL', 0):.2f}A")
|
||||||
|
log(f"\nToday's Energy: {parsed_data.get('H22', 0)}kWh (Max: {parsed_data.get('H21', 0)}W)")
|
||||||
|
log(f"Total Energy: {parsed_data.get('H19', 0)}kWh")
|
||||||
|
|
||||||
# Save to SQLite
|
# Validate critical values
|
||||||
cursor.execute("SELECT * FROM timestamp_table LIMIT 1")
|
battery_voltage = parsed_data.get('V', 0)
|
||||||
row = cursor.fetchone()
|
|
||||||
rtc_time_str = row[1]
|
|
||||||
|
|
||||||
# Extract values
|
if battery_voltage > 0:
|
||||||
battery_voltage = parsed_data.get('V', 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)
|
||||||
@@ -210,16 +256,27 @@ if __name__ == "__main__":
|
|||||||
(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 = {
|
||||||
time.sleep(3)
|
'PM1': 0.0,
|
||||||
exit()
|
'PM25': 0.0,
|
||||||
except:
|
'PM10': 0.0,
|
||||||
# for all other kinds of error, but not specifying which one
|
'sleep': 0,
|
||||||
print("Unknown error...")
|
'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 Exception as e:
|
||||||
|
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)
|
||||||
|
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
|
||||||
@@ -38,6 +40,9 @@ import crcmod
|
|||||||
import sqlite3
|
import sqlite3
|
||||||
import time
|
import time
|
||||||
|
|
||||||
|
# Dry-run mode: print JSON output without writing to database
|
||||||
|
dry_run = "--dry-run" in sys.argv
|
||||||
|
|
||||||
# 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()
|
||||||
@@ -59,119 +64,181 @@ 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
|
||||||
|
npm_status = 0xFF # Default: all errors (will be overwritten if read succeeds)
|
||||||
|
|
||||||
# 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])
|
if not dry_run:
|
||||||
|
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)
|
if not dry_run:
|
||||||
#print("Response:", formatted)
|
print("[ERROR] CRC check failed! Corrupted data received.")
|
||||||
|
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):
|
||||||
|
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")
|
||||||
|
|
||||||
|
# Read NPM status register (register 19 = 0x13, 1 register)
|
||||||
|
# Modbus request: slave=0x01, func=0x03, addr=0x0013, qty=0x0001
|
||||||
|
status_request = b'\x01\x03\x00\x13\x00\x01'
|
||||||
|
status_crc = crc16(status_request)
|
||||||
|
status_request += bytes([status_crc & 0xFF, (status_crc >> 8) & 0xFF])
|
||||||
|
|
||||||
|
ser.flushInput()
|
||||||
|
ser.write(status_request)
|
||||||
|
time.sleep(0.2)
|
||||||
|
|
||||||
|
# Response: addr(1) + func(1) + byte_count(1) + data(2) + crc(2) = 7 bytes
|
||||||
|
status_response = ser.read(7)
|
||||||
|
if len(status_response) == 7:
|
||||||
|
status_recv_crc = int.from_bytes(status_response[-2:], byteorder='little')
|
||||||
|
status_calc_crc = crc16(status_response[:-2])
|
||||||
|
if status_recv_crc == status_calc_crc:
|
||||||
|
npm_status = int.from_bytes(status_response[3:5], byteorder='big') & 0xFF
|
||||||
|
if not dry_run:
|
||||||
|
print(f"NPM status: 0x{npm_status:02X} ({npm_status})")
|
||||||
|
else:
|
||||||
|
if not dry_run:
|
||||||
|
print("[WARNING] NPM status CRC check failed, keeping default")
|
||||||
else:
|
else:
|
||||||
lsw = int.from_bytes(byte_data[offset:offset+2], byteorder='big')
|
if not dry_run:
|
||||||
msw = int.from_bytes(byte_data[offset+2:offset+4], byteorder='big')
|
print(f"[WARNING] NPM status incomplete response ({len(status_response)} bytes)")
|
||||||
value = (msw << 16) | lsw
|
|
||||||
|
|
||||||
value = value / scale
|
ser.close()
|
||||||
|
|
||||||
if round_to == 0:
|
except Exception as e:
|
||||||
return int(value)
|
if not dry_run:
|
||||||
elif round_to is not None:
|
print(f"[ERROR] Sensor communication failed: {e}")
|
||||||
return round(value, round_to)
|
# Variables already set to -1 at the beginning
|
||||||
|
|
||||||
|
finally:
|
||||||
|
if dry_run:
|
||||||
|
# Print JSON output without writing to database
|
||||||
|
result = {
|
||||||
|
"PM1": pm1_10s,
|
||||||
|
"PM25": pm25_10s,
|
||||||
|
"PM10": pm10_10s,
|
||||||
|
"temperature": temperature,
|
||||||
|
"humidity": relative_humidity,
|
||||||
|
"npm_status": npm_status,
|
||||||
|
"npm_status_hex": f"0x{npm_status:02X}"
|
||||||
|
}
|
||||||
|
print(json.dumps(result))
|
||||||
else:
|
else:
|
||||||
return value
|
# Always save data to database, even if all values are 0
|
||||||
|
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))
|
||||||
|
|
||||||
pm1_10s = extract_value(byte_data, 56, 1000, round_to=1)
|
cursor.execute('''
|
||||||
pm25_10s = extract_value(byte_data, 58, 1000, round_to=1)
|
INSERT INTO data_NPM (timestamp,PM1, PM25, PM10, temp_npm, hum_npm, npm_status) VALUES (?,?,?,?,?,?,?)'''
|
||||||
pm10_10s = extract_value(byte_data, 60, 1000, round_to=1)
|
, (rtc_time_str, pm1_10s, pm25_10s, pm10_10s, temperature, relative_humidity, npm_status))
|
||||||
|
|
||||||
#print("10 sec concentration:")
|
# Commit and close the connection
|
||||||
#print(f"PM1: {pm1_10s}")
|
conn.commit()
|
||||||
#print(f"PM2.5: {pm25_10s}")
|
|
||||||
#print(f"PM10: {pm10_10s}")
|
|
||||||
|
|
||||||
# Extract values for 5 channels
|
conn.close()
|
||||||
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()
|
|
||||||
169
README.md
169
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
|
||||||
@@ -59,6 +59,9 @@ 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/git pull
|
||||||
www-data ALL=(ALL) NOPASSWD: /usr/bin/ssh
|
www-data ALL=(ALL) NOPASSWD: /usr/bin/ssh
|
||||||
www-data ALL=(ALL) NOPASSWD: /usr/bin/python3 *
|
www-data ALL=(ALL) NOPASSWD: /usr/bin/python3 *
|
||||||
|
www-data ALL=(ALL) NOPASSWD: /bin/systemctl *
|
||||||
|
www-data ALL=(ALL) NOPASSWD: /usr/bin/pkill
|
||||||
|
www-data ALL=(ALL) NOPASSWD: /var/www/nebuleair_pro_4g/*
|
||||||
```
|
```
|
||||||
## Serial
|
## Serial
|
||||||
|
|
||||||
@@ -178,6 +181,146 @@ And set the base URL for Sara R4 communication:
|
|||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## UDP Payload Miotiq — Structure 100 bytes
|
||||||
|
|
||||||
|
| Bytes | Taille | Nom | Format | Description |
|
||||||
|
|-------|--------|-----|--------|-------------|
|
||||||
|
| 0-7 | 8 | device_id | ASCII | Identifiant unique du capteur |
|
||||||
|
| 8 | 1 | signal_quality | uint8 | Qualite signal modem (AT+CSQ) |
|
||||||
|
| 9 | 1 | protocol_version | uint8 | Version protocole (0x01) |
|
||||||
|
| 10-11 | 2 | pm1 | uint16 BE | PM1.0 en ug/m3 (x10) |
|
||||||
|
| 12-13 | 2 | pm25 | uint16 BE | PM2.5 en ug/m3 (x10) |
|
||||||
|
| 14-15 | 2 | pm10 | uint16 BE | PM10 en ug/m3 (x10) |
|
||||||
|
| 16-17 | 2 | temperature | int16 BE | Temperature en C (x100, signe) |
|
||||||
|
| 18-19 | 2 | humidity | uint16 BE | Humidite en % (x100) |
|
||||||
|
| 20-21 | 2 | pressure | uint16 BE | Pression en hPa |
|
||||||
|
| 22-23 | 2 | noise_cur_leq | uint16 BE | Bruit LEQ en dB(A) (x10) |
|
||||||
|
| 24-25 | 2 | noise_cur_level | uint16 BE | Bruit instantane en dB(A) (x10) |
|
||||||
|
| 26-27 | 2 | noise_max | uint16 BE | Bruit max en dB(A) (x10) |
|
||||||
|
| 28-29 | 2 | envea_no2 | uint16 BE | NO2 en ppb |
|
||||||
|
| 30-31 | 2 | envea_h2s | uint16 BE | H2S en ppb |
|
||||||
|
| 32-33 | 2 | envea_nh3 | uint16 BE | NH3 en ppb |
|
||||||
|
| 34-35 | 2 | envea_co | uint16 BE | CO en ppb |
|
||||||
|
| 36-37 | 2 | envea_o3 | uint16 BE | O3 en ppb |
|
||||||
|
| 38-39 | 2 | npm_ch1 | uint16 BE | NPM canal 1 (5-channel) |
|
||||||
|
| 40-41 | 2 | npm_ch2 | uint16 BE | NPM canal 2 (5-channel) |
|
||||||
|
| 42-43 | 2 | npm_ch3 | uint16 BE | NPM canal 3 (5-channel) |
|
||||||
|
| 44-45 | 2 | npm_ch4 | uint16 BE | NPM canal 4 (5-channel) |
|
||||||
|
| 46-47 | 2 | npm_ch5 | uint16 BE | NPM canal 5 (5-channel) |
|
||||||
|
| 48-49 | 2 | mppt_temperature | int16 BE | Temperature MPPT en C (x10, signe) |
|
||||||
|
| 50-51 | 2 | mppt_humidity | uint16 BE | Humidite MPPT en % (x10) |
|
||||||
|
| 52-53 | 2 | battery_voltage | uint16 BE | Tension batterie en V (x100) |
|
||||||
|
| 54-55 | 2 | battery_current | int16 BE | Courant batterie en A (x100, signe) |
|
||||||
|
| 56-57 | 2 | solar_voltage | uint16 BE | Tension solaire en V (x100) |
|
||||||
|
| 58-59 | 2 | solar_power | uint16 BE | Puissance solaire en W |
|
||||||
|
| 60-61 | 2 | charger_status | uint16 BE | Status chargeur MPPT |
|
||||||
|
| 62-63 | 2 | wind_speed | uint16 BE | Vitesse vent en m/s (x10) |
|
||||||
|
| 64-65 | 2 | wind_direction | uint16 BE | Direction vent en degres |
|
||||||
|
| 66 | 1 | error_flags | uint8 | Erreurs systeme (voir detail) |
|
||||||
|
| 67 | 1 | npm_status | uint8 | Registre status NextPM |
|
||||||
|
| 68 | 1 | device_status | uint8 | Etat general du boitier |
|
||||||
|
| 69 | 1 | version_major | uint8 | Version firmware major |
|
||||||
|
| 70 | 1 | version_minor | uint8 | Version firmware minor |
|
||||||
|
| 71 | 1 | version_patch | uint8 | Version firmware patch |
|
||||||
|
| 72-99 | 28 | reserved | — | Reserve (initialise a 0xFF) |
|
||||||
|
|
||||||
|
### Consommation data (UDP Miotiq uniquement)
|
||||||
|
|
||||||
|
Taille par paquet : 100 bytes payload + 8 bytes UDP header + 20 bytes IP header = **128 bytes**
|
||||||
|
|
||||||
|
| | Toutes les 60s | Toutes les 10s |
|
||||||
|
|---|---|---|
|
||||||
|
| Paquets/jour | 1 440 | 8 640 |
|
||||||
|
| Par jour | ~180 KB | ~1.08 MB |
|
||||||
|
| Par mois | ~5.3 MB | ~32.4 MB |
|
||||||
|
| Par an | ~63.6 MB | ~388.8 MB |
|
||||||
|
|
||||||
|
> Note : ces chiffres ne comptent que l'UDP vers Miotiq. Les envois HTTP (AirCarto) et HTTPS (uSpot) consomment des donnees supplementaires.
|
||||||
|
|
||||||
|
### Parser Miotiq
|
||||||
|
|
||||||
|
```
|
||||||
|
16|device_id|string|||W
|
||||||
|
2|signal_quality|hex2dec|dB||
|
||||||
|
2|version|hex2dec|||W
|
||||||
|
4|ISO_68|hex2dec|ugm3|x/10|
|
||||||
|
4|ISO_39|hex2dec|ugm3|x/10|
|
||||||
|
4|ISO_24|hex2dec|ugm3|x/10|
|
||||||
|
4|ISO_54|hex2dec|degC|x/100|
|
||||||
|
4|ISO_55|hex2dec|%|x/100|
|
||||||
|
4|ISO_53|hex2dec|hPa||
|
||||||
|
4|noise_cur_leq|hex2dec|dB|x/10|
|
||||||
|
4|noise_cur_level|hex2dec|dB|x/10|
|
||||||
|
4|max_noise|hex2dec|dB|x/10|
|
||||||
|
4|ISO_03|hex2dec|ppb||
|
||||||
|
4|ISO_05|hex2dec|ppb||
|
||||||
|
4|ISO_21|hex2dec|ppb||
|
||||||
|
4|ISO_04|hex2dec|ppb||
|
||||||
|
4|ISO_08|hex2dec|ppb||
|
||||||
|
4|npm_ch1|hex2dec|count||
|
||||||
|
4|npm_ch2|hex2dec|count||
|
||||||
|
4|npm_ch3|hex2dec|count||
|
||||||
|
4|npm_ch4|hex2dec|count||
|
||||||
|
4|npm_ch5|hex2dec|count||
|
||||||
|
4|npm_temp|hex2dec|°C|x/10|
|
||||||
|
4|npm_humidity|hex2dec|%|x/10|
|
||||||
|
4|battery_voltage|hex2dec|V|x/100|
|
||||||
|
4|battery_current|hex2dec|A|x/100|
|
||||||
|
4|solar_voltage|hex2dec|V|x/100|
|
||||||
|
4|solar_power|hex2dec|W||
|
||||||
|
4|charger_status|hex2dec|||
|
||||||
|
4|wind_speed|hex2dec|m/s|x/10|
|
||||||
|
4|wind_direction|hex2dec|degrees||
|
||||||
|
2|error_flags|hex2dec|||
|
||||||
|
2|npm_status|hex2dec|||
|
||||||
|
2|device_status|hex2dec|||
|
||||||
|
2|version_major|hex2dec|||
|
||||||
|
2|version_minor|hex2dec|||
|
||||||
|
2|version_patch|hex2dec|||
|
||||||
|
22|reserved|skip|||
|
||||||
|
```
|
||||||
|
|
||||||
|
### Byte 66 — error_flags
|
||||||
|
|
||||||
|
| Bit | Masque | Description |
|
||||||
|
|-----|--------|-------------|
|
||||||
|
| 0 | 0x01 | RTC deconnecte |
|
||||||
|
| 1 | 0x02 | RTC reset (annee 2000) |
|
||||||
|
| 2 | 0x04 | BME280 erreur |
|
||||||
|
| 3 | 0x08 | NPM erreur |
|
||||||
|
| 4 | 0x10 | Envea erreur |
|
||||||
|
| 5 | 0x20 | Bruit erreur |
|
||||||
|
| 6 | 0x40 | MPPT erreur |
|
||||||
|
| 7 | 0x80 | Vent erreur |
|
||||||
|
|
||||||
|
### Byte 67 — npm_status
|
||||||
|
|
||||||
|
| Bit | Masque | Description |
|
||||||
|
|-----|--------|-------------|
|
||||||
|
| 0 | 0x01 | Sleep mode |
|
||||||
|
| 1 | 0x02 | Degraded mode |
|
||||||
|
| 2 | 0x04 | Not ready |
|
||||||
|
| 3 | 0x08 | Heater error |
|
||||||
|
| 4 | 0x10 | THP sensor error |
|
||||||
|
| 5 | 0x20 | Fan error |
|
||||||
|
| 6 | 0x40 | Memory error |
|
||||||
|
| 7 | 0x80 | Laser error |
|
||||||
|
|
||||||
|
### Byte 68 — device_status
|
||||||
|
|
||||||
|
| Bit | Masque | Description |
|
||||||
|
|-----|--------|-------------|
|
||||||
|
| 0 | 0x01 | Modem reboot au cycle precedent |
|
||||||
|
| 1 | 0x02 | WiFi connecte |
|
||||||
|
| 2 | 0x04 | Hotspot actif |
|
||||||
|
| 3 | 0x08 | Pas de fix GPS |
|
||||||
|
| 4 | 0x10 | Batterie faible |
|
||||||
|
| 5 | 0x20 | Disque plein |
|
||||||
|
| 6 | 0x40 | Erreur base SQLite |
|
||||||
|
| 7 | 0x80 | Boot recent (uptime < 5 min) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
# Notes
|
# Notes
|
||||||
|
|
||||||
## Wifi Hotspot (AP)
|
## Wifi Hotspot (AP)
|
||||||
@@ -210,4 +353,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'''
|
||||||
____ _ ____ _
|
____ _ ____ _
|
||||||
/ ___| / \ | _ \ / \
|
/ ___| / \ | _ \ / \
|
||||||
\___ \ / _ \ | |_) | / _ \
|
\___ \ / _ \ | |_) | / _ \
|
||||||
@@ -46,6 +46,9 @@ try:
|
|||||||
timeout = timeout
|
timeout = timeout
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Flush any leftover data from previous commands or modem boot URCs
|
||||||
|
ser.reset_input_buffer()
|
||||||
|
|
||||||
ser.write((command + '\r').encode('utf-8'))
|
ser.write((command + '\r').encode('utf-8'))
|
||||||
|
|
||||||
#ser.write(b'ATI\r') #General Information
|
#ser.write(b'ATI\r') #General Information
|
||||||
|
|||||||
@@ -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,35 +15,68 @@ 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
|
|
||||||
#echo "LEDs ON"
|
|
||||||
sleep 1
|
|
||||||
|
|
||||||
# Turn GPIO 23 and 24 OFF
|
# Blink GPIO 23 and 24 five times (starts OFF, stays OFF at end)
|
||||||
gpioset gpiochip0 23=0 24=0
|
python3 << 'EOF'
|
||||||
#echo "LEDs OFF"
|
import RPi.GPIO as GPIO
|
||||||
sleep 1
|
import time
|
||||||
done
|
|
||||||
|
GPIO.setmode(GPIO.BCM)
|
||||||
|
GPIO.setwarnings(False)
|
||||||
|
GPIO.setup(23, GPIO.OUT)
|
||||||
|
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'")
|
||||||
|
|
||||||
#need to wait for the network manager to be ready
|
#need to wait for the network manager to be ready
|
||||||
sleep 20
|
sleep 20
|
||||||
|
|
||||||
|
# IMPORTANT: Always enable WiFi radio at boot (in case it was disabled by power save)
|
||||||
|
WIFI_RADIO_STATE=$(nmcli radio wifi)
|
||||||
|
echo "WiFi radio state: $WIFI_RADIO_STATE"
|
||||||
|
|
||||||
|
if [ "$WIFI_RADIO_STATE" == "disabled" ]; then
|
||||||
|
echo "WiFi radio is disabled, enabling it..."
|
||||||
|
nmcli radio wifi on
|
||||||
|
# Wait longer for NetworkManager to scan and reconnect to known networks
|
||||||
|
echo "Waiting 15 seconds for WiFi to reconnect to known networks..."
|
||||||
|
sleep 15
|
||||||
|
else
|
||||||
|
echo "WiFi radio is already enabled"
|
||||||
|
fi
|
||||||
|
|
||||||
# Get the connection state of wlan0
|
# Get the connection state of wlan0
|
||||||
STATE=$(nmcli -g GENERAL.STATE device show wlan0)
|
STATE=$(nmcli -g GENERAL.STATE device show wlan0)
|
||||||
|
|
||||||
@@ -54,9 +87,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'"
|
||||||
|
|||||||
299
changelog.json
Normal file
299
changelog.json
Normal file
@@ -0,0 +1,299 @@
|
|||||||
|
{
|
||||||
|
"versions": [
|
||||||
|
{
|
||||||
|
"version": "1.6.0",
|
||||||
|
"date": "2026-03-18",
|
||||||
|
"changes": {
|
||||||
|
"features": [
|
||||||
|
"Payload UDP Miotiq: envoi npm_status (byte 67) — registre status NextPM en temps reel"
|
||||||
|
],
|
||||||
|
"improvements": [
|
||||||
|
"npm_status lu depuis la derniere mesure en base (rowid DESC, pas de moyenne ni de timestamp)"
|
||||||
|
],
|
||||||
|
"fixes": [],
|
||||||
|
"compatibility": [
|
||||||
|
"Necessite mise a jour du parser Miotiq pour decoder le byte 67 (npm_status)"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"notes": "Le capteur envoie maintenant le registre status du NextPM dans chaque trame UDP (byte 67). La valeur est prise de la derniere mesure sans moyenne (un code erreur ne se moyenne pas). Utilise rowid pour eviter toute dependance au RTC."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"version": "1.5.2",
|
||||||
|
"date": "2026-03-18",
|
||||||
|
"changes": {
|
||||||
|
"features": [
|
||||||
|
"Page capteurs: lecture NPM via get_data_modbus_v3.py --dry-run (meme script que le timer)",
|
||||||
|
"Page capteurs: affichage temperature et humidite interne du NPM",
|
||||||
|
"Page capteurs: decodage npm_status avec flags d'erreur individuels"
|
||||||
|
],
|
||||||
|
"improvements": [
|
||||||
|
"NPM get_data_modbus_v3.py: mode --dry-run (print JSON sans ecriture en base)",
|
||||||
|
"Page capteurs: status NPM affiche en vert (OK) ou orange/rouge (erreurs decodees)"
|
||||||
|
],
|
||||||
|
"fixes": [
|
||||||
|
"Page capteurs: suppression unite ug/m3 sur le champ message/status"
|
||||||
|
],
|
||||||
|
"compatibility": []
|
||||||
|
},
|
||||||
|
"notes": "La page capteurs utilise maintenant le meme script Modbus que le timer systemd, en mode dry-run pour eviter les conflits d'ecriture SQLite. Le status NPM est decode bit par bit."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"version": "1.5.1",
|
||||||
|
"date": "2026-03-18",
|
||||||
|
"changes": {
|
||||||
|
"features": [
|
||||||
|
"Payload UDP Miotiq: bytes 69-71 firmware version (major.minor.patch)",
|
||||||
|
"README: documentation complete de la structure des 100 bytes UDP"
|
||||||
|
],
|
||||||
|
"improvements": [],
|
||||||
|
"fixes": [],
|
||||||
|
"compatibility": [
|
||||||
|
"Necessite mise a jour du parser Miotiq pour decoder les bytes 69-71 (firmware version)"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"notes": "Le capteur envoie maintenant sa version firmware dans chaque trame UDP. Cote serveur, bytes 69/70/71 = major/minor/patch. Documentation payload complete ajoutee au README."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"version": "1.5.0",
|
||||||
|
"date": "2026-03-18",
|
||||||
|
"changes": {
|
||||||
|
"features": [
|
||||||
|
"Payload UDP Miotiq: byte 66 error_flags (erreurs systeme RTC/capteurs)",
|
||||||
|
"Payload UDP Miotiq: byte 67 npm_status (registre status NextPM)",
|
||||||
|
"Payload UDP Miotiq: byte 68 device_status (etat general du boitier, specification)",
|
||||||
|
"Methodes SensorPayload: set_error_flags(), set_npm_status(), set_device_status()"
|
||||||
|
],
|
||||||
|
"improvements": [
|
||||||
|
"Initialisation bytes 66-68 a 0x00 au lieu de 0xFF pour eviter faux positifs cote serveur",
|
||||||
|
"Escalade erreur UDP: si PDP reset echoue, notification WiFi + hardware reboot + exit"
|
||||||
|
],
|
||||||
|
"fixes": [],
|
||||||
|
"compatibility": [
|
||||||
|
"Necessite mise a jour du parser Miotiq pour decoder les bytes 66-68 (error_flags, npm_status, device_status)"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"notes": "Ajout de registres d'erreur et d'etat dans la payload UDP (bytes 66-68). Les bytes de status sont initialises a 0x00 (aucune erreur) au lieu de 0xFF. Le flag RTC est implemente, les autres flags seront actives progressivement."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"version": "1.4.6",
|
||||||
|
"date": "2026-03-17",
|
||||||
|
"changes": {
|
||||||
|
"features": [
|
||||||
|
"Page Admin: comparaison RTC vs heure du navigateur (au lieu de system time)",
|
||||||
|
"Page Admin: ajout champ Browser time (UTC) dans l'onglet Clock",
|
||||||
|
"Page Admin: bloquer update firmware en mode hotspot avec message explicatif",
|
||||||
|
"Page Admin: liens Gitea pour mise a jour hors-ligne (releases + main.zip)"
|
||||||
|
],
|
||||||
|
"improvements": [
|
||||||
|
"Page Admin: RTC time mis en evidence (label bold, input large, bordure bleue)",
|
||||||
|
"Page Admin: System time replie dans un details/summary (non utilise par le capteur)",
|
||||||
|
"Page Admin: descriptions ajoutees pour System time, RTC time et Synchroniser le RTC"
|
||||||
|
],
|
||||||
|
"fixes": [
|
||||||
|
"Fix forget_wifi scan: delai 5s + rescan explicite pour remplir wifi_list.csv",
|
||||||
|
"Fix blocage navigateur: revert optimisations fetch qui saturaient la limite 6 connexions/domaine"
|
||||||
|
],
|
||||||
|
"compatibility": []
|
||||||
|
},
|
||||||
|
"notes": "L'onglet Clock compare maintenant le RTC a l'heure du navigateur, plus fiable que le system time Linux (non utilise par le capteur). L'update firmware est bloque en mode hotspot avec un message explicatif. La mise a jour hors-ligne via upload .zip reste disponible."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"version": "1.4.5",
|
||||||
|
"date": "2026-03-17",
|
||||||
|
"changes": {
|
||||||
|
"features": [
|
||||||
|
"Page WiFi: bouton Oublier le reseau pour passer en mode hotspot sans reboot",
|
||||||
|
"Page WiFi: badge Mode Hotspot visible dans la sidebar (lien vers page WiFi)",
|
||||||
|
"Page WiFi: scan des reseaux WiFi en mode hotspot via cache CSV (scan au demarrage)"
|
||||||
|
],
|
||||||
|
"improvements": [
|
||||||
|
"Page WiFi: refonte UI avec cards contextuelles (infos connexion detaillees si connecte, scan si hotspot)",
|
||||||
|
"Page WiFi: affichage SSID, signal, IP, passerelle, hostname, frequence, securite",
|
||||||
|
"Page WiFi: scan WiFi masque quand deja connecte, scan avec colonnes signal et securite",
|
||||||
|
"Page WiFi: migration de config.json vers get_config_sqlite",
|
||||||
|
"Endpoint internet enrichi: SSID, signal, frequence, securite, passerelle, hostname",
|
||||||
|
"Scan WiFi en mode hotspot: lecture du fichier wifi_list.csv avec notice explicative",
|
||||||
|
"forget_wifi.sh: scan WiFi avec rescan explicite et delai avant lancement hotspot"
|
||||||
|
],
|
||||||
|
"fixes": [
|
||||||
|
"Correction VERSION 1.4.3 -> 1.4.4",
|
||||||
|
"Fix IP hotspot: 192.168.43.1 -> 10.42.0.1 (defaut NetworkManager)",
|
||||||
|
"Fix forget_wifi.sh: appel bash explicite + disconnect wlan0 avant delete"
|
||||||
|
],
|
||||||
|
"compatibility": []
|
||||||
|
},
|
||||||
|
"notes": "Le bouton Oublier le reseau supprime la connexion WiFi sauvegardee, scanne les reseaux disponibles, puis demarre le hotspot (pas de reboot necessaire). En mode hotspot, la page WiFi affiche les reseaux scannes au demarrage via un cache CSV. Adresse hotspot: http://10.42.0.1/html/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"version": "1.4.4",
|
||||||
|
"date": "2026-03-16",
|
||||||
|
"changes": {
|
||||||
|
"features": [
|
||||||
|
"Bouton Self Test disponible sur les pages Accueil, Capteurs et Admin (en plus de Modem 4G)",
|
||||||
|
"Test du module RTC DS3231 integre dans le self-test (connexion + synchronisation horloge)"
|
||||||
|
],
|
||||||
|
"improvements": [
|
||||||
|
"Refactoring self-test : code JS et HTML des modals extraits dans des fichiers partages (selftest.js, selftest-modal.html)",
|
||||||
|
"Le modal self-test est charge dynamiquement via fetch, plus besoin de dupliquer le HTML"
|
||||||
|
],
|
||||||
|
"fixes": [],
|
||||||
|
"compatibility": []
|
||||||
|
},
|
||||||
|
"notes": "Le self-test est maintenant accessible depuis toutes les pages principales. Le test RTC verifie la connexion du module et l'ecart avec l'heure systeme."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"version": "1.4.3",
|
||||||
|
"date": "2026-03-16",
|
||||||
|
"changes": {
|
||||||
|
"features": [
|
||||||
|
"Page database: bouton telecharger toute la table (bypass filtre dates)",
|
||||||
|
"Page database: validation obligatoire des dates avant telechargement par periode"
|
||||||
|
],
|
||||||
|
"improvements": [
|
||||||
|
"Payload UDP bruit: bytes 22-23 = noise_cur_leq, 24-25 = noise_cur_level, 26-27 = max_noise (reserve)",
|
||||||
|
"Envoi des deux valeurs bruit (cur_LEQ + DB_A_value) en UDP Miotiq au lieu d'une seule"
|
||||||
|
],
|
||||||
|
"fixes": [],
|
||||||
|
"compatibility": [
|
||||||
|
"Necessite mise a jour du parser Miotiq pour decoder les nouveaux champs noise_cur_leq et noise_cur_level"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"notes": "Mise a jour structure UDP bruit pour alignement avec parser Miotiq et ameliorations page database."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"version": "1.4.2",
|
||||||
|
"date": "2026-03-14",
|
||||||
|
"changes": {
|
||||||
|
"features": [],
|
||||||
|
"improvements": [],
|
||||||
|
"fixes": [
|
||||||
|
"Fix envoi UDP Miotiq: desynchronisation serie causant l'envoi de la commande AT+USOWR comme payload au lieu des donnees capteurs",
|
||||||
|
"Ajout flush buffer serie (reset_input_buffer) avant chaque etape UDP critique",
|
||||||
|
"Verification du prompt @ du modem avant envoi des donnees binaires",
|
||||||
|
"Abort propre de l'envoi UDP si creation socket, connexion ou prompt @ echoue",
|
||||||
|
"Retry creation socket apres reset PDP reussi"
|
||||||
|
],
|
||||||
|
"compatibility": []
|
||||||
|
},
|
||||||
|
"notes": "Corrige un bug ou le modem SARA envoyait la commande AT+USOWR comme donnees UDP, causant des erreurs UNKNOWN_DEVICE sur le parser Miotiq."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"version": "1.4.1",
|
||||||
|
"date": "2026-03-12",
|
||||||
|
"changes": {
|
||||||
|
"features": [],
|
||||||
|
"improvements": [
|
||||||
|
"Migration capteur bruit de l'ancien systeme I2C vers le sonometre NSRT MK4 en USB",
|
||||||
|
"Nouveau script sound_meter/read.py pour lecture a la demande (retour JSON)",
|
||||||
|
"Page capteurs: carte USB avec affichage LEQ et dB(A) au lieu de l'ancien format texte",
|
||||||
|
"Self-test modem: parsing JSON du NSRT MK4 au lieu de texte brut"
|
||||||
|
],
|
||||||
|
"fixes": [
|
||||||
|
"Correction du self-test bruit qui affichait 'Unexpected value' avec le nouveau capteur"
|
||||||
|
],
|
||||||
|
"compatibility": []
|
||||||
|
},
|
||||||
|
"notes": "Mise a jour necessaire si le sonometre NSRT MK4 est connecte en USB. L'ancien capteur I2C n'est plus supporte sur la page capteurs."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"version": "1.4.0",
|
||||||
|
"date": "2026-03-10",
|
||||||
|
"changes": {
|
||||||
|
"features": [
|
||||||
|
"Mise a jour firmware hors-ligne par upload de fichier ZIP via l'interface web admin",
|
||||||
|
"Barre de progression pour suivre l'upload du fichier",
|
||||||
|
"Fichier .update-exclude versionne pour gerer les exclusions rsync de maniere evolutive"
|
||||||
|
],
|
||||||
|
"improvements": [
|
||||||
|
"Vidage du buffer serie avant chaque commande AT dans sara.py (evite les URCs residuelles au demarrage)"
|
||||||
|
],
|
||||||
|
"fixes": [],
|
||||||
|
"compatibility": [
|
||||||
|
"Necessite l'ajout de update_firmware_from_file.sh dans les permissions sudo de www-data",
|
||||||
|
"Necessite Apache mod_rewrite pour html/.htaccess (upload 50MB)"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"notes": "Permet la mise a jour du firmware sans connexion internet : telecharger le .zip depuis Gitea, se connecter au hotspot du capteur, et uploader via admin.html."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"version": "1.3.0",
|
||||||
|
"date": "2026-02-17",
|
||||||
|
"changes": {
|
||||||
|
"features": [
|
||||||
|
"Onglet 'Ecran' pour le controle de l'affichage HDMI (ModuleAir Pro uniquement)",
|
||||||
|
"Demarrage et arret du script d'affichage via l'interface web",
|
||||||
|
"Verification automatique du type d'appareil pour afficher l'onglet"
|
||||||
|
],
|
||||||
|
"improvements": [
|
||||||
|
"Ajout de logs console pour le debougage des commandes web",
|
||||||
|
"Traduction de l'element de menu 'Ecran'"
|
||||||
|
],
|
||||||
|
"fixes": [
|
||||||
|
"Correction des permissions d'execution des scripts python via web (sudo)",
|
||||||
|
"Correction de la visibilite des onglets du menu lateral (doublons ID)"
|
||||||
|
],
|
||||||
|
"compatibility": [
|
||||||
|
"Necessite python3-kivy installe",
|
||||||
|
"Necessite l'ajout de permissions sudo pour www-data (voir documentation)"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"notes": "Ajout de la fonctionnalite de controle d'ecran pour les demonstrations."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"version": "1.2.0",
|
||||||
|
"date": "2026-02-17",
|
||||||
|
"changes": {
|
||||||
|
"features": [
|
||||||
|
"Integration capteur CO2 MH-Z19 (scripts, base de donnees, service systemd, interface web)",
|
||||||
|
"Carte test CO2 sur la page capteurs",
|
||||||
|
"Checkbox activation CO2 sur la page admin",
|
||||||
|
"Consultation et telechargement des mesures CO2 sur la page base de donnees"
|
||||||
|
],
|
||||||
|
"improvements": [],
|
||||||
|
"fixes": [
|
||||||
|
"Logo ModuleAir Pro ne s'affichait pas (script dans innerHTML non execute)"
|
||||||
|
],
|
||||||
|
"compatibility": [
|
||||||
|
"Necessite re-execution de create_db.py, set_config.py et setup_services.sh apres mise a jour"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"notes": "Ajout du support capteur CO2 MH-Z19 pour le ModuleAir Pro. La transmission SARA sera integree dans une version ulterieure."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"version": "1.1.0",
|
||||||
|
"date": "2026-02-16",
|
||||||
|
"changes": {
|
||||||
|
"features": [
|
||||||
|
"Card informations base de donnees (taille, nombre d'entrees, dates min/max par table)",
|
||||||
|
"Telechargement CSV complet par table depuis la page base de donnees",
|
||||||
|
"Bouton version firmware NextPM sur la page capteurs",
|
||||||
|
"Tests capteurs integres dans l'auto-test modem",
|
||||||
|
"Logo dynamique selon le type d'appareil (NebuleAir/ModuleAir)"
|
||||||
|
],
|
||||||
|
"improvements": [
|
||||||
|
"Reordonnancement de l'auto-test : capteurs avant communication"
|
||||||
|
],
|
||||||
|
"fixes": [],
|
||||||
|
"compatibility": []
|
||||||
|
},
|
||||||
|
"notes": "Ameliorations de l'interface web : meilleure visibilite sur l'etat de la base de donnees et des capteurs."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"version": "1.0.0",
|
||||||
|
"date": "2026-02-11",
|
||||||
|
"changes": {
|
||||||
|
"features": [
|
||||||
|
"Support multi-device : NebuleAir Pro / ModuleAir Pro",
|
||||||
|
"Systeme de versioning firmware",
|
||||||
|
"Changelog viewer dans l'interface web"
|
||||||
|
],
|
||||||
|
"improvements": [],
|
||||||
|
"fixes": [],
|
||||||
|
"compatibility": [
|
||||||
|
"Les capteurs existants sont automatiquement configures en 'nebuleair_pro'"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"notes": "Premiere version tracee. Les capteurs anterieurs recevront device_type=nebuleair_pro par defaut lors de la mise a jour."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
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,132 @@ 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"
|
|
||||||
)
|
calculated_value = None
|
||||||
data_envea = serial_connection.readline()
|
max_retries = 3
|
||||||
if len(data_envea) >= 20:
|
|
||||||
byte_20 = data_envea[19] * coefficient
|
for attempt in range(max_retries):
|
||||||
|
# Flush input buffer to clear any stale data
|
||||||
|
serial_connection.reset_input_buffer()
|
||||||
|
|
||||||
|
# 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)
|
||||||
|
if attempt == 0:
|
||||||
|
debug_print(f" → Sent command: {command.hex()}")
|
||||||
|
|
||||||
|
# Wait for sensor response
|
||||||
|
time.sleep(0.8)
|
||||||
|
|
||||||
|
# Read all available data from buffer
|
||||||
|
bytes_available = serial_connection.in_waiting
|
||||||
|
debug_print(f" ← Attempt {attempt + 1}: {bytes_available} bytes available")
|
||||||
|
|
||||||
|
if bytes_available > 0:
|
||||||
|
data_envea = serial_connection.read(bytes_available)
|
||||||
|
else:
|
||||||
|
data_envea = serial_connection.read(32)
|
||||||
|
|
||||||
|
if len(data_envea) > 0:
|
||||||
|
debug_print(f" ← Received {len(data_envea)} bytes: {data_envea.hex()}")
|
||||||
|
|
||||||
|
# Find frame start (0xFF 0x02) in received data
|
||||||
|
frame_start = -1
|
||||||
|
for i in range(len(data_envea) - 1):
|
||||||
|
if data_envea[i] == 0xFF and data_envea[i + 1] == 0x02:
|
||||||
|
frame_start = i
|
||||||
|
break
|
||||||
|
|
||||||
|
if frame_start >= 0:
|
||||||
|
frame_data = data_envea[frame_start:]
|
||||||
|
if len(frame_data) >= 20:
|
||||||
|
byte_20 = frame_data[19]
|
||||||
|
calculated_value = byte_20 * coefficient
|
||||||
|
debug_print(f" → Found valid frame at position {frame_start}")
|
||||||
|
debug_print(f" → Byte 20 = {byte_20} × {coefficient} = {calculated_value}")
|
||||||
|
break # Success, exit retry loop
|
||||||
|
|
||||||
|
debug_print(f" ✗ Attempt {attempt + 1} failed, {'retrying...' if attempt < max_retries - 1 else 'giving up'}")
|
||||||
|
time.sleep(0.2)
|
||||||
|
|
||||||
|
if calculated_value is not None:
|
||||||
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" ✗ Failed to read {name} after {max_retries} attempts")
|
||||||
|
|
||||||
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")
|
||||||
|
|
||||||
55
forget_wifi.sh
Normal file
55
forget_wifi.sh
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
echo "-------"
|
||||||
|
echo "Start forget WiFi 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"
|
||||||
|
|
||||||
|
# Get current active WiFi connection name
|
||||||
|
ACTIVE_WIFI=$(nmcli -t -f NAME,TYPE,DEVICE connection show --active | grep ':802-11-wireless:wlan0' | cut -d: -f1)
|
||||||
|
|
||||||
|
if [ -z "$ACTIVE_WIFI" ]; then
|
||||||
|
echo "No active WiFi connection found on wlan0"
|
||||||
|
echo "End forget WiFi shell script"
|
||||||
|
echo "-------"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Forgetting WiFi connection: $ACTIVE_WIFI"
|
||||||
|
|
||||||
|
# Disconnect wlan0 first to prevent NetworkManager from auto-reconnecting
|
||||||
|
sudo nmcli device disconnect wlan0
|
||||||
|
echo "wlan0 disconnected"
|
||||||
|
|
||||||
|
# Delete (forget) the saved connection
|
||||||
|
sudo nmcli connection delete "$ACTIVE_WIFI"
|
||||||
|
|
||||||
|
if [ $? -eq 0 ]; then
|
||||||
|
echo "Connection '$ACTIVE_WIFI' deleted successfully"
|
||||||
|
else
|
||||||
|
echo "Failed to delete connection '$ACTIVE_WIFI'"
|
||||||
|
fi
|
||||||
|
|
||||||
|
sleep 5
|
||||||
|
|
||||||
|
# Scan WiFi networks BEFORE starting hotspot (scan impossible once hotspot is active)
|
||||||
|
OUTPUT_FILE="/var/www/nebuleair_pro_4g/wifi_list.csv"
|
||||||
|
echo "Scanning WiFi networks (waiting for wlan0 to be ready)..."
|
||||||
|
nmcli device wifi rescan ifname wlan0 2>/dev/null
|
||||||
|
sleep 3
|
||||||
|
nmcli -f SSID,SIGNAL,SECURITY device wifi list ifname wlan0 | awk 'BEGIN { OFS=","; print "SSID,SIGNAL,SECURITY" } NR>1 { print $1,$2,$3 }' > "$OUTPUT_FILE"
|
||||||
|
echo "WiFi scan saved to $OUTPUT_FILE"
|
||||||
|
cat "$OUTPUT_FILE"
|
||||||
|
|
||||||
|
# Start hotspot
|
||||||
|
echo "Starting hotspot with SSID: $DEVICE_NAME"
|
||||||
|
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 started with SSID: $DEVICE_NAME"
|
||||||
|
|
||||||
|
echo "End forget WiFi shell script"
|
||||||
|
echo "-------"
|
||||||
3
html/.htaccess
Normal file
3
html/.htaccess
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
php_value upload_max_filesize 50M
|
||||||
|
php_value post_max_size 55M
|
||||||
|
php_value max_execution_time 300
|
||||||
1334
html/admin.html
1334
html/admin.html
File diff suppressed because it is too large
Load Diff
66
html/assets/data/operators.json
Normal file
66
html/assets/data/operators.json
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
{
|
||||||
|
"operators": {
|
||||||
|
"20801": { "name": "Orange", "country": "France" },
|
||||||
|
"20802": { "name": "Orange", "country": "France" },
|
||||||
|
"20810": { "name": "SFR", "country": "France" },
|
||||||
|
"20811": { "name": "SFR", "country": "France" },
|
||||||
|
"20813": { "name": "SFR", "country": "France" },
|
||||||
|
"20815": { "name": "Free Mobile", "country": "France" },
|
||||||
|
"20816": { "name": "Free Mobile", "country": "France" },
|
||||||
|
"20820": { "name": "Bouygues Telecom", "country": "France" },
|
||||||
|
"20821": { "name": "Bouygues Telecom", "country": "France" },
|
||||||
|
"20826": { "name": "NRJ Mobile", "country": "France" },
|
||||||
|
"20888": { "name": "Bouygues Telecom", "country": "France" },
|
||||||
|
"22201": { "name": "TIM", "country": "Italy" },
|
||||||
|
"22210": { "name": "Vodafone", "country": "Italy" },
|
||||||
|
"22288": { "name": "WIND", "country": "Italy" },
|
||||||
|
"22299": { "name": "3 Italia", "country": "Italy" },
|
||||||
|
"23410": { "name": "O2", "country": "UK" },
|
||||||
|
"23415": { "name": "Vodafone", "country": "UK" },
|
||||||
|
"23420": { "name": "3", "country": "UK" },
|
||||||
|
"23430": { "name": "EE", "country": "UK" },
|
||||||
|
"23433": { "name": "EE", "country": "UK" },
|
||||||
|
"26201": { "name": "Telekom", "country": "Germany" },
|
||||||
|
"26202": { "name": "Vodafone", "country": "Germany" },
|
||||||
|
"26203": { "name": "O2", "country": "Germany" },
|
||||||
|
"26207": { "name": "O2", "country": "Germany" },
|
||||||
|
"21401": { "name": "Vodafone", "country": "Spain" },
|
||||||
|
"21403": { "name": "Orange", "country": "Spain" },
|
||||||
|
"21404": { "name": "Yoigo", "country": "Spain" },
|
||||||
|
"21407": { "name": "Movistar", "country": "Spain" },
|
||||||
|
"22801": { "name": "Swisscom", "country": "Switzerland" },
|
||||||
|
"22802": { "name": "Sunrise", "country": "Switzerland" },
|
||||||
|
"22803": { "name": "Salt", "country": "Switzerland" },
|
||||||
|
"20601": { "name": "Proximus", "country": "Belgium" },
|
||||||
|
"20610": { "name": "Orange", "country": "Belgium" },
|
||||||
|
"20620": { "name": "Base", "country": "Belgium" },
|
||||||
|
"20404": { "name": "Vodafone", "country": "Netherlands" },
|
||||||
|
"20408": { "name": "KPN", "country": "Netherlands" },
|
||||||
|
"20412": { "name": "T-Mobile", "country": "Netherlands" },
|
||||||
|
"20416": { "name": "T-Mobile", "country": "Netherlands" },
|
||||||
|
"26801": { "name": "Vodafone", "country": "Portugal" },
|
||||||
|
"26803": { "name": "NOS", "country": "Portugal" },
|
||||||
|
"26806": { "name": "MEO", "country": "Portugal" },
|
||||||
|
"29340": { "name": "SI Mobil", "country": "Slovenia" },
|
||||||
|
"29341": { "name": "Mobitel", "country": "Slovenia" }
|
||||||
|
},
|
||||||
|
"modes": {
|
||||||
|
"0": "Automatic",
|
||||||
|
"1": "Manual",
|
||||||
|
"2": "Deregistered",
|
||||||
|
"3": "Format only",
|
||||||
|
"4": "Manual/Automatic"
|
||||||
|
},
|
||||||
|
"accessTechnology": {
|
||||||
|
"0": "GSM",
|
||||||
|
"1": "GSM Compact",
|
||||||
|
"2": "UTRAN (3G)",
|
||||||
|
"3": "GSM/GPRS with EDGE",
|
||||||
|
"4": "UTRAN with HSDPA",
|
||||||
|
"5": "UTRAN with HSUPA",
|
||||||
|
"6": "UTRAN with HSDPA/HSUPA",
|
||||||
|
"7": "LTE (4G)",
|
||||||
|
"8": "EC-GSM-IoT",
|
||||||
|
"9": "LTE Cat-M / NB-IoT"
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
html/assets/img/logoModuleAir.png
Normal file
BIN
html/assets/img/logoModuleAir.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 233 KiB |
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();
|
||||||
|
}
|
||||||
966
html/assets/js/selftest.js
Normal file
966
html/assets/js/selftest.js
Normal file
@@ -0,0 +1,966 @@
|
|||||||
|
// ============================================
|
||||||
|
// SELF TEST FUNCTIONS (shared across pages)
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
// Cache for operators data
|
||||||
|
let operatorsDataSelfTest = null;
|
||||||
|
|
||||||
|
function loadOperatorsDataSelfTest() {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
if (operatorsDataSelfTest) {
|
||||||
|
resolve(operatorsDataSelfTest);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$.ajax({
|
||||||
|
url: 'assets/data/operators.json',
|
||||||
|
dataType: 'json',
|
||||||
|
method: 'GET',
|
||||||
|
success: function(data) {
|
||||||
|
operatorsDataSelfTest = data;
|
||||||
|
resolve(data);
|
||||||
|
},
|
||||||
|
error: function(xhr, status, error) {
|
||||||
|
console.error('Failed to load operators data:', error);
|
||||||
|
reject(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Global object to store test results for report
|
||||||
|
let selfTestReport = {
|
||||||
|
timestamp: '',
|
||||||
|
deviceId: '',
|
||||||
|
modemVersion: '',
|
||||||
|
results: {},
|
||||||
|
rawResponses: {}
|
||||||
|
};
|
||||||
|
|
||||||
|
function runSelfTest() {
|
||||||
|
console.log("Starting Self Test...");
|
||||||
|
|
||||||
|
// Reset report
|
||||||
|
selfTestReport = {
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
deviceId: document.querySelector('.sideBar_sensorName')?.textContent || 'Unknown',
|
||||||
|
modemVersion: document.getElementById('modem_version')?.textContent || 'Unknown',
|
||||||
|
results: {},
|
||||||
|
rawResponses: {}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Reset UI
|
||||||
|
resetSelfTestUI();
|
||||||
|
|
||||||
|
// Show modal
|
||||||
|
const modal = new bootstrap.Modal(document.getElementById('selfTestModal'));
|
||||||
|
modal.show();
|
||||||
|
|
||||||
|
// Disable buttons during test
|
||||||
|
document.getElementById('selfTestCloseBtn').disabled = true;
|
||||||
|
document.getElementById('selfTestDoneBtn').disabled = true;
|
||||||
|
document.getElementById('selfTestCopyBtn').disabled = true;
|
||||||
|
document.querySelectorAll('.btn_selfTest').forEach(btn => btn.disabled = true);
|
||||||
|
|
||||||
|
// Start test sequence
|
||||||
|
selfTestSequence();
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetSelfTestUI() {
|
||||||
|
// Reset status
|
||||||
|
document.getElementById('selftest_status').innerHTML = `
|
||||||
|
<div class="d-flex align-items-center text-muted">
|
||||||
|
<div class="spinner-border spinner-border-sm me-2" role="status"></div>
|
||||||
|
<span>Preparing test...</span>
|
||||||
|
</div>`;
|
||||||
|
|
||||||
|
// Reset test items
|
||||||
|
document.getElementById('test_wifi_status').className = 'badge bg-secondary';
|
||||||
|
document.getElementById('test_wifi_status').textContent = 'Pending';
|
||||||
|
document.getElementById('test_wifi_detail').textContent = 'Waiting...';
|
||||||
|
|
||||||
|
document.getElementById('test_modem_status').className = 'badge bg-secondary';
|
||||||
|
document.getElementById('test_modem_status').textContent = 'Pending';
|
||||||
|
document.getElementById('test_modem_detail').textContent = 'Waiting...';
|
||||||
|
|
||||||
|
document.getElementById('test_sim_status').className = 'badge bg-secondary';
|
||||||
|
document.getElementById('test_sim_status').textContent = 'Pending';
|
||||||
|
document.getElementById('test_sim_detail').textContent = 'Waiting...';
|
||||||
|
|
||||||
|
document.getElementById('test_signal_status').className = 'badge bg-secondary';
|
||||||
|
document.getElementById('test_signal_status').textContent = 'Pending';
|
||||||
|
document.getElementById('test_signal_detail').textContent = 'Waiting...';
|
||||||
|
|
||||||
|
document.getElementById('test_network_status').className = 'badge bg-secondary';
|
||||||
|
document.getElementById('test_network_status').textContent = 'Pending';
|
||||||
|
document.getElementById('test_network_detail').textContent = 'Waiting...';
|
||||||
|
|
||||||
|
// Reset sensor tests
|
||||||
|
document.getElementById('sensor_tests_container').innerHTML = '';
|
||||||
|
document.getElementById('comm_tests_separator').style.display = 'none';
|
||||||
|
|
||||||
|
// Reset logs
|
||||||
|
document.getElementById('selftest_logs').innerHTML = '';
|
||||||
|
|
||||||
|
// Reset summary
|
||||||
|
document.getElementById('selftest_summary').innerHTML = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function addSelfTestLog(message, isRaw = false) {
|
||||||
|
const logsEl = document.getElementById('selftest_logs');
|
||||||
|
const timestamp = new Date().toLocaleTimeString();
|
||||||
|
|
||||||
|
if (isRaw) {
|
||||||
|
// Raw AT response - format nicely
|
||||||
|
logsEl.textContent += `[${timestamp}] >>> RAW RESPONSE:\n${message}\n<<<\n`;
|
||||||
|
} else {
|
||||||
|
logsEl.textContent += `[${timestamp}] ${message}\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto-scroll to bottom
|
||||||
|
logsEl.parentElement.scrollTop = logsEl.parentElement.scrollHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateTestStatus(testId, status, detail, badge) {
|
||||||
|
document.getElementById(`test_${testId}_status`).className = `badge ${badge}`;
|
||||||
|
document.getElementById(`test_${testId}_status`).textContent = status;
|
||||||
|
document.getElementById(`test_${testId}_detail`).textContent = detail;
|
||||||
|
|
||||||
|
// Store result in report
|
||||||
|
selfTestReport.results[testId] = {
|
||||||
|
status: status,
|
||||||
|
detail: detail
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function setConfigMode(enabled) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
addSelfTestLog(`Setting modem_config_mode to ${enabled}...`);
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
url: `launcher.php?type=update_config_sqlite¶m=modem_config_mode&value=${enabled}`,
|
||||||
|
dataType: 'json',
|
||||||
|
method: 'GET',
|
||||||
|
cache: false,
|
||||||
|
success: function(response) {
|
||||||
|
if (response.success) {
|
||||||
|
addSelfTestLog(`modem_config_mode set to ${enabled}`);
|
||||||
|
// Update checkbox state if it exists on the page
|
||||||
|
const checkbox = document.getElementById('check_modem_configMode');
|
||||||
|
if (checkbox) checkbox.checked = enabled;
|
||||||
|
resolve(true);
|
||||||
|
} else {
|
||||||
|
addSelfTestLog(`Failed to set modem_config_mode: ${response.error || 'Unknown error'}`);
|
||||||
|
reject(new Error(response.error || 'Failed to set config mode'));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: function(xhr, status, error) {
|
||||||
|
addSelfTestLog(`AJAX error setting config mode: ${error}`);
|
||||||
|
reject(new Error(error));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function sendATCommand(command, timeout) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
addSelfTestLog(`Sending AT command: ${command} (timeout: ${timeout}s)`);
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
url: `launcher.php?type=sara&port=ttyAMA2&command=${encodeURIComponent(command)}&timeout=${timeout}`,
|
||||||
|
dataType: 'text',
|
||||||
|
method: 'GET',
|
||||||
|
success: function(response) {
|
||||||
|
// Store raw response in report
|
||||||
|
selfTestReport.rawResponses[command] = response;
|
||||||
|
|
||||||
|
// Log raw response
|
||||||
|
addSelfTestLog(response.trim(), true);
|
||||||
|
|
||||||
|
resolve(response);
|
||||||
|
},
|
||||||
|
error: function(xhr, status, error) {
|
||||||
|
addSelfTestLog(`AT command error: ${error}`);
|
||||||
|
selfTestReport.rawResponses[command] = `ERROR: ${error}`;
|
||||||
|
reject(new Error(error));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function delaySelfTest(ms) {
|
||||||
|
return new Promise(resolve => setTimeout(resolve, ms));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function selfTestSequence() {
|
||||||
|
let testsPassed = 0;
|
||||||
|
let testsFailed = 0;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Collect system info at the start
|
||||||
|
document.getElementById('selftest_status').innerHTML = `
|
||||||
|
<div class="d-flex align-items-center text-primary">
|
||||||
|
<div class="spinner-border spinner-border-sm me-2" role="status"></div>
|
||||||
|
<span>Collecting system information...</span>
|
||||||
|
</div>`;
|
||||||
|
|
||||||
|
// Get system info from config
|
||||||
|
try {
|
||||||
|
const configResponse = await new Promise((resolve, reject) => {
|
||||||
|
$.ajax({
|
||||||
|
url: 'launcher.php?type=get_config_sqlite',
|
||||||
|
dataType: 'json',
|
||||||
|
method: 'GET',
|
||||||
|
success: function(data) { resolve(data); },
|
||||||
|
error: function(xhr, status, error) { reject(new Error(error)); }
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Store in report
|
||||||
|
selfTestReport.deviceId = configResponse.deviceID || 'Unknown';
|
||||||
|
selfTestReport.deviceName = configResponse.deviceName || 'Unknown';
|
||||||
|
selfTestReport.modemVersion = configResponse.modem_version || 'Unknown';
|
||||||
|
selfTestReport.latitude = configResponse.latitude_raw || 'N/A';
|
||||||
|
selfTestReport.longitude = configResponse.longitude_raw || 'N/A';
|
||||||
|
selfTestReport.config = configResponse;
|
||||||
|
|
||||||
|
// Get RTC time
|
||||||
|
try {
|
||||||
|
const rtcTime = await new Promise((resolve, reject) => {
|
||||||
|
$.ajax({
|
||||||
|
url: 'launcher.php?type=RTC_time',
|
||||||
|
dataType: 'text',
|
||||||
|
method: 'GET',
|
||||||
|
success: function(data) { resolve(data.trim()); },
|
||||||
|
error: function(xhr, status, error) { resolve('N/A'); }
|
||||||
|
});
|
||||||
|
});
|
||||||
|
selfTestReport.systemTime = rtcTime;
|
||||||
|
} catch (e) {
|
||||||
|
selfTestReport.systemTime = 'N/A';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log system info
|
||||||
|
addSelfTestLog('════════════════════════════════════════════════════════');
|
||||||
|
addSelfTestLog(' NEBULEAIR PRO 4G - SELF TEST');
|
||||||
|
addSelfTestLog('════════════════════════════════════════════════════════');
|
||||||
|
addSelfTestLog(`Device ID: ${selfTestReport.deviceId}`);
|
||||||
|
addSelfTestLog(`Device Name: ${selfTestReport.deviceName}`);
|
||||||
|
addSelfTestLog(`Modem Version: ${selfTestReport.modemVersion}`);
|
||||||
|
addSelfTestLog(`RTC Time: ${selfTestReport.systemTime}`);
|
||||||
|
addSelfTestLog(`Browser Time: ${new Date().toLocaleString()}`);
|
||||||
|
addSelfTestLog(`GPS: ${selfTestReport.latitude}, ${selfTestReport.longitude}`);
|
||||||
|
addSelfTestLog('────────────────────────────────────────────────────────');
|
||||||
|
addSelfTestLog('');
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
addSelfTestLog(`Warning: Could not get system config: ${error.message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
await delaySelfTest(300);
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════
|
||||||
|
// SENSOR TESTS - Test enabled sensors based on config
|
||||||
|
// ═══════════════════════════════════════════════════════
|
||||||
|
const config = selfTestReport.config || {};
|
||||||
|
const sensorTests = [];
|
||||||
|
|
||||||
|
// NPM is always present
|
||||||
|
sensorTests.push({ id: 'npm', name: 'NextPM (Particles)', type: 'npm', port: 'ttyAMA5' });
|
||||||
|
|
||||||
|
// BME280 if enabled
|
||||||
|
if (config.BME280) {
|
||||||
|
sensorTests.push({ id: 'bme280', name: 'BME280 (Temp/Hum)', type: 'BME280' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Noise if enabled
|
||||||
|
if (config.NOISE) {
|
||||||
|
sensorTests.push({ id: 'noise', name: 'Noise Sensor', type: 'noise' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Envea if enabled
|
||||||
|
if (config.envea) {
|
||||||
|
sensorTests.push({ id: 'envea', name: 'Envea (Gas Sensors)', type: 'envea' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// RTC module is always present (DS3231)
|
||||||
|
sensorTests.push({ id: 'rtc', name: 'RTC Module (DS3231)', type: 'rtc' });
|
||||||
|
|
||||||
|
// Create sensor test UI entries dynamically
|
||||||
|
const sensorContainer = document.getElementById('sensor_tests_container');
|
||||||
|
sensorContainer.innerHTML = '';
|
||||||
|
|
||||||
|
sensorTests.forEach(sensor => {
|
||||||
|
sensorContainer.innerHTML += `
|
||||||
|
<div class="list-group-item d-flex justify-content-between align-items-center" id="test_${sensor.id}">
|
||||||
|
<div>
|
||||||
|
<strong>${sensor.name}</strong>
|
||||||
|
<div class="small text-muted" id="test_${sensor.id}_detail">Waiting...</div>
|
||||||
|
</div>
|
||||||
|
<span id="test_${sensor.id}_status" class="badge bg-secondary">Pending</span>
|
||||||
|
</div>`;
|
||||||
|
});
|
||||||
|
|
||||||
|
addSelfTestLog('');
|
||||||
|
addSelfTestLog('────────────────────────────────────────────────────────');
|
||||||
|
addSelfTestLog('SENSOR TESTS');
|
||||||
|
addSelfTestLog('────────────────────────────────────────────────────────');
|
||||||
|
|
||||||
|
// Run each sensor test
|
||||||
|
for (const sensor of sensorTests) {
|
||||||
|
await delaySelfTest(500);
|
||||||
|
|
||||||
|
document.getElementById('selftest_status').innerHTML = `
|
||||||
|
<div class="d-flex align-items-center text-primary">
|
||||||
|
<div class="spinner-border spinner-border-sm me-2" role="status"></div>
|
||||||
|
<span>Testing ${sensor.name}...</span>
|
||||||
|
</div>`;
|
||||||
|
|
||||||
|
updateTestStatus(sensor.id, 'Testing...', 'Reading sensor data...', 'bg-info');
|
||||||
|
addSelfTestLog(`Testing ${sensor.name}...`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (sensor.type === 'npm') {
|
||||||
|
// NPM sensor test (uses get_data_modbus_v3.py --dry-run)
|
||||||
|
const npmResult = await new Promise((resolve, reject) => {
|
||||||
|
$.ajax({
|
||||||
|
url: 'launcher.php?type=npm',
|
||||||
|
dataType: 'json',
|
||||||
|
method: 'GET',
|
||||||
|
timeout: 15000,
|
||||||
|
success: function(data) { resolve(data); },
|
||||||
|
error: function(xhr, status, error) { reject(new Error(error || status)); }
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
selfTestReport.rawResponses['NPM Sensor'] = JSON.stringify(npmResult, null, 2);
|
||||||
|
addSelfTestLog(`NPM response: PM1=${npmResult.PM1}, PM2.5=${npmResult.PM25}, PM10=${npmResult.PM10}, status=${npmResult.npm_status_hex}`);
|
||||||
|
|
||||||
|
// Decode npm_status flags
|
||||||
|
const status = npmResult.npm_status !== undefined ? npmResult.npm_status : 0;
|
||||||
|
|
||||||
|
if (status === 0xFF) {
|
||||||
|
// 0xFF = no response = disconnected
|
||||||
|
updateTestStatus(sensor.id, 'Failed', 'Capteur déconnecté', 'bg-danger');
|
||||||
|
testsFailed++;
|
||||||
|
} else {
|
||||||
|
const statusFlags = {
|
||||||
|
0x01: "Sleep mode",
|
||||||
|
0x02: "Degraded mode",
|
||||||
|
0x04: "Not ready",
|
||||||
|
0x08: "Heater error",
|
||||||
|
0x10: "THP sensor error",
|
||||||
|
0x20: "Fan error",
|
||||||
|
0x40: "Memory error",
|
||||||
|
0x80: "Laser error"
|
||||||
|
};
|
||||||
|
const activeErrors = [];
|
||||||
|
Object.entries(statusFlags).forEach(([mask, label]) => {
|
||||||
|
if (status & mask) activeErrors.push(label);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (activeErrors.length > 0) {
|
||||||
|
updateTestStatus(sensor.id, 'Warning', `Status ${npmResult.npm_status_hex}: ${activeErrors.join(', ')}`, 'bg-warning');
|
||||||
|
testsFailed++;
|
||||||
|
} else if (npmResult.PM1 !== undefined && npmResult.PM25 !== undefined && npmResult.PM10 !== undefined) {
|
||||||
|
updateTestStatus(sensor.id, 'Passed', `PM1: ${npmResult.PM1} | PM2.5: ${npmResult.PM25} | PM10: ${npmResult.PM10} µg/m³`, 'bg-success');
|
||||||
|
testsPassed++;
|
||||||
|
} else {
|
||||||
|
updateTestStatus(sensor.id, 'Warning', 'Incomplete data received', 'bg-warning');
|
||||||
|
testsFailed++;
|
||||||
|
}
|
||||||
|
} // end else (not 0xFF)
|
||||||
|
|
||||||
|
} else if (sensor.type === 'BME280') {
|
||||||
|
// BME280 sensor test
|
||||||
|
const bme280Result = await new Promise((resolve, reject) => {
|
||||||
|
$.ajax({
|
||||||
|
url: 'launcher.php?type=BME280',
|
||||||
|
dataType: 'text',
|
||||||
|
method: 'GET',
|
||||||
|
timeout: 15000,
|
||||||
|
success: function(data) { resolve(data); },
|
||||||
|
error: function(xhr, status, error) { reject(new Error(error || status)); }
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const bmeData = JSON.parse(bme280Result);
|
||||||
|
selfTestReport.rawResponses['BME280 Sensor'] = JSON.stringify(bmeData, null, 2);
|
||||||
|
addSelfTestLog(`BME280 response: temp=${bmeData.temp}, hum=${bmeData.hum}, press=${bmeData.press}`);
|
||||||
|
|
||||||
|
if (bmeData.temp !== undefined && bmeData.hum !== undefined && bmeData.press !== undefined) {
|
||||||
|
updateTestStatus(sensor.id, 'Passed', `${bmeData.temp}°C | ${bmeData.hum}% | ${bmeData.press} hPa`, 'bg-success');
|
||||||
|
testsPassed++;
|
||||||
|
} else {
|
||||||
|
updateTestStatus(sensor.id, 'Warning', 'Incomplete data received', 'bg-warning');
|
||||||
|
testsFailed++;
|
||||||
|
}
|
||||||
|
|
||||||
|
} else if (sensor.type === 'noise') {
|
||||||
|
// NSRT MK4 noise sensor test (returns JSON)
|
||||||
|
const noiseResult = await new Promise((resolve, reject) => {
|
||||||
|
$.ajax({
|
||||||
|
url: 'launcher.php?type=noise',
|
||||||
|
dataType: 'json',
|
||||||
|
method: 'GET',
|
||||||
|
timeout: 15000,
|
||||||
|
success: function(data) { resolve(data); },
|
||||||
|
error: function(xhr, status, error) { reject(new Error(error || status)); }
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
selfTestReport.rawResponses['Noise Sensor'] = JSON.stringify(noiseResult);
|
||||||
|
addSelfTestLog(`Noise response: ${JSON.stringify(noiseResult)}`);
|
||||||
|
|
||||||
|
if (noiseResult.error) {
|
||||||
|
updateTestStatus(sensor.id, 'Failed', noiseResult.error, 'bg-danger');
|
||||||
|
testsFailed++;
|
||||||
|
} else if (noiseResult.LEQ > 0 && noiseResult.dBA > 0) {
|
||||||
|
updateTestStatus(sensor.id, 'Passed', `LEQ: ${noiseResult.LEQ} dB | dB(A): ${noiseResult.dBA}`, 'bg-success');
|
||||||
|
testsPassed++;
|
||||||
|
} else {
|
||||||
|
updateTestStatus(sensor.id, 'Warning', `Unexpected values: LEQ=${noiseResult.LEQ}, dBA=${noiseResult.dBA}`, 'bg-warning');
|
||||||
|
testsFailed++;
|
||||||
|
}
|
||||||
|
|
||||||
|
} else if (sensor.type === 'envea') {
|
||||||
|
// Envea sensor test - use the debug endpoint for all sensors
|
||||||
|
const enveaResult = await new Promise((resolve, reject) => {
|
||||||
|
$.ajax({
|
||||||
|
url: 'launcher.php?type=envea_debug',
|
||||||
|
dataType: 'text',
|
||||||
|
method: 'GET',
|
||||||
|
timeout: 30000,
|
||||||
|
success: function(data) { resolve(data); },
|
||||||
|
error: function(xhr, status, error) { reject(new Error(error || status)); }
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
selfTestReport.rawResponses['Envea Sensors'] = enveaResult;
|
||||||
|
addSelfTestLog(`Envea response: ${enveaResult.trim().substring(0, 200)}`);
|
||||||
|
|
||||||
|
if (enveaResult.trim() !== '' && !enveaResult.toLowerCase().includes('error')) {
|
||||||
|
updateTestStatus(sensor.id, 'Passed', 'Sensors responding', 'bg-success');
|
||||||
|
testsPassed++;
|
||||||
|
} else if (enveaResult.toLowerCase().includes('error')) {
|
||||||
|
updateTestStatus(sensor.id, 'Failed', 'Sensor error detected', 'bg-danger');
|
||||||
|
testsFailed++;
|
||||||
|
} else {
|
||||||
|
updateTestStatus(sensor.id, 'Failed', 'No data received', 'bg-danger');
|
||||||
|
testsFailed++;
|
||||||
|
}
|
||||||
|
|
||||||
|
} else if (sensor.type === 'rtc') {
|
||||||
|
// RTC DS3231 module test
|
||||||
|
const rtcResult = await new Promise((resolve, reject) => {
|
||||||
|
$.ajax({
|
||||||
|
url: 'launcher.php?type=sys_RTC_module_time',
|
||||||
|
dataType: 'json',
|
||||||
|
method: 'GET',
|
||||||
|
timeout: 10000,
|
||||||
|
success: function(data) { resolve(data); },
|
||||||
|
error: function(xhr, status, error) { reject(new Error(error || status)); }
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
selfTestReport.rawResponses['RTC Module'] = JSON.stringify(rtcResult, null, 2);
|
||||||
|
addSelfTestLog(`RTC response: ${JSON.stringify(rtcResult)}`);
|
||||||
|
|
||||||
|
if (rtcResult.rtc_module_time === 'not connected') {
|
||||||
|
updateTestStatus(sensor.id, 'Failed', 'RTC module not connected', 'bg-danger');
|
||||||
|
testsFailed++;
|
||||||
|
} else if (rtcResult.rtc_module_time) {
|
||||||
|
// Compare RTC with browser time (more reliable than system time)
|
||||||
|
const rtcDate = new Date(rtcResult.rtc_module_time + ' UTC');
|
||||||
|
const browserDate = new Date();
|
||||||
|
const timeDiff = Math.abs(Math.round((browserDate - rtcDate) / 1000));
|
||||||
|
|
||||||
|
if (timeDiff <= 60) {
|
||||||
|
updateTestStatus(sensor.id, 'Passed', `${rtcResult.rtc_module_time} (sync OK vs navigateur, ecart: ${timeDiff}s)`, 'bg-success');
|
||||||
|
testsPassed++;
|
||||||
|
} else {
|
||||||
|
const minutes = Math.floor(timeDiff / 60);
|
||||||
|
const label = minutes > 0 ? `${minutes}min ${timeDiff % 60}s` : `${timeDiff}s`;
|
||||||
|
updateTestStatus(sensor.id, 'Warning', `${rtcResult.rtc_module_time} (desync vs navigateur: ${label})`, 'bg-warning');
|
||||||
|
testsFailed++;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
updateTestStatus(sensor.id, 'Warning', 'Unexpected response', 'bg-warning');
|
||||||
|
testsFailed++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
addSelfTestLog(`${sensor.name} test error: ${error.message}`);
|
||||||
|
updateTestStatus(sensor.id, 'Failed', error.message, 'bg-danger');
|
||||||
|
selfTestReport.rawResponses[`${sensor.name}`] = `ERROR: ${error.message}`;
|
||||||
|
testsFailed++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════
|
||||||
|
// COMMUNICATION TESTS - WiFi, Modem, SIM, Signal, Network
|
||||||
|
// ═══════════════════════════════════════════════════════
|
||||||
|
addSelfTestLog('');
|
||||||
|
addSelfTestLog('────────────────────────────────────────────────────────');
|
||||||
|
addSelfTestLog('COMMUNICATION TESTS');
|
||||||
|
addSelfTestLog('────────────────────────────────────────────────────────');
|
||||||
|
|
||||||
|
document.getElementById('comm_tests_separator').style.display = '';
|
||||||
|
|
||||||
|
// Check WiFi / Network status (informational, no pass/fail)
|
||||||
|
document.getElementById('selftest_status').innerHTML = `
|
||||||
|
<div class="d-flex align-items-center text-primary">
|
||||||
|
<div class="spinner-border spinner-border-sm me-2" role="status"></div>
|
||||||
|
<span>Checking network status...</span>
|
||||||
|
</div>`;
|
||||||
|
|
||||||
|
updateTestStatus('wifi', 'Checking...', 'Getting network info...', 'bg-info');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const wifiResponse = await new Promise((resolve, reject) => {
|
||||||
|
$.ajax({
|
||||||
|
url: 'launcher.php?type=wifi_status',
|
||||||
|
dataType: 'json',
|
||||||
|
method: 'GET',
|
||||||
|
success: function(data) {
|
||||||
|
addSelfTestLog(`WiFi status received`);
|
||||||
|
// Store raw response
|
||||||
|
selfTestReport.rawResponses['WiFi Status'] = JSON.stringify(data, null, 2);
|
||||||
|
resolve(data);
|
||||||
|
},
|
||||||
|
error: function(xhr, status, error) {
|
||||||
|
addSelfTestLog(`WiFi status error: ${error}`);
|
||||||
|
selfTestReport.rawResponses['WiFi Status'] = `ERROR: ${error}`;
|
||||||
|
reject(new Error(error));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Log detailed WiFi info
|
||||||
|
addSelfTestLog(`Mode: ${wifiResponse.mode}, SSID: ${wifiResponse.ssid}, IP: ${wifiResponse.ip}, Hostname: ${wifiResponse.hostname}`);
|
||||||
|
|
||||||
|
if (wifiResponse.connected) {
|
||||||
|
let modeLabel = '';
|
||||||
|
let badgeClass = 'bg-info';
|
||||||
|
|
||||||
|
if (wifiResponse.mode === 'hotspot') {
|
||||||
|
modeLabel = 'Hotspot';
|
||||||
|
badgeClass = 'bg-warning text-dark';
|
||||||
|
} else if (wifiResponse.mode === 'wifi') {
|
||||||
|
modeLabel = 'WiFi';
|
||||||
|
badgeClass = 'bg-info';
|
||||||
|
} else if (wifiResponse.mode === 'ethernet') {
|
||||||
|
modeLabel = 'Ethernet';
|
||||||
|
badgeClass = 'bg-info';
|
||||||
|
}
|
||||||
|
|
||||||
|
const detailText = `${wifiResponse.ssid} | ${wifiResponse.ip} | ${wifiResponse.hostname}.local`;
|
||||||
|
updateTestStatus('wifi', modeLabel, detailText, badgeClass);
|
||||||
|
} else {
|
||||||
|
updateTestStatus('wifi', 'Disconnected', 'No network connection', 'bg-secondary');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
updateTestStatus('wifi', 'Error', error.message, 'bg-secondary');
|
||||||
|
}
|
||||||
|
|
||||||
|
await delaySelfTest(500);
|
||||||
|
|
||||||
|
// Enable config mode
|
||||||
|
document.getElementById('selftest_status').innerHTML = `
|
||||||
|
<div class="d-flex align-items-center text-primary">
|
||||||
|
<div class="spinner-border spinner-border-sm me-2" role="status"></div>
|
||||||
|
<span>Enabling configuration mode...</span>
|
||||||
|
</div>`;
|
||||||
|
|
||||||
|
await setConfigMode(true);
|
||||||
|
|
||||||
|
// Wait for SARA script to release the port (2 seconds should be enough)
|
||||||
|
addSelfTestLog('Waiting for modem port to be available...');
|
||||||
|
await delaySelfTest(2000);
|
||||||
|
|
||||||
|
// Test Modem Connection (ATI)
|
||||||
|
document.getElementById('selftest_status').innerHTML = `
|
||||||
|
<div class="d-flex align-items-center text-primary">
|
||||||
|
<div class="spinner-border spinner-border-sm me-2" role="status"></div>
|
||||||
|
<span>Testing modem connection...</span>
|
||||||
|
</div>`;
|
||||||
|
|
||||||
|
updateTestStatus('modem', 'Testing...', 'Sending ATI command...', 'bg-info');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const modemResponse = await sendATCommand('ATI', 5);
|
||||||
|
|
||||||
|
if (modemResponse.includes('OK') && (modemResponse.toUpperCase().includes('SARA-R5') || modemResponse.toUpperCase().includes('SARA-R4'))) {
|
||||||
|
// Extract model
|
||||||
|
const modelMatch = modemResponse.match(/SARA-R[45]\d*[A-Z]*-\d+[A-Z]*-\d+/i);
|
||||||
|
const model = modelMatch ? modelMatch[0] : 'SARA module';
|
||||||
|
updateTestStatus('modem', 'Passed', `Model: ${model}`, 'bg-success');
|
||||||
|
testsPassed++;
|
||||||
|
} else if (modemResponse.includes('OK')) {
|
||||||
|
updateTestStatus('modem', 'Passed', 'Modem responding', 'bg-success');
|
||||||
|
testsPassed++;
|
||||||
|
} else {
|
||||||
|
updateTestStatus('modem', 'Failed', 'No valid response', 'bg-danger');
|
||||||
|
testsFailed++;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
updateTestStatus('modem', 'Failed', error.message, 'bg-danger');
|
||||||
|
testsFailed++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delay between AT commands
|
||||||
|
await delaySelfTest(1000);
|
||||||
|
|
||||||
|
// Test SIM Card (AT+CCID?)
|
||||||
|
document.getElementById('selftest_status').innerHTML = `
|
||||||
|
<div class="d-flex align-items-center text-primary">
|
||||||
|
<div class="spinner-border spinner-border-sm me-2" role="status"></div>
|
||||||
|
<span>Testing SIM card...</span>
|
||||||
|
</div>`;
|
||||||
|
|
||||||
|
updateTestStatus('sim', 'Testing...', 'Sending AT+CCID? command...', 'bg-info');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const simResponse = await sendATCommand('AT+CCID?', 5);
|
||||||
|
|
||||||
|
const ccidMatch = simResponse.match(/\+CCID:\s*(\d{18,22})/);
|
||||||
|
if (simResponse.includes('OK') && ccidMatch) {
|
||||||
|
const iccid = ccidMatch[1];
|
||||||
|
// Show last 4 digits only for privacy
|
||||||
|
const maskedIccid = '****' + iccid.slice(-4);
|
||||||
|
updateTestStatus('sim', 'Passed', `ICCID: ...${maskedIccid}`, 'bg-success');
|
||||||
|
testsPassed++;
|
||||||
|
} else if (simResponse.includes('ERROR')) {
|
||||||
|
updateTestStatus('sim', 'Failed', 'SIM card not detected', 'bg-danger');
|
||||||
|
testsFailed++;
|
||||||
|
} else {
|
||||||
|
updateTestStatus('sim', 'Warning', 'Unable to read ICCID', 'bg-warning');
|
||||||
|
testsFailed++;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
updateTestStatus('sim', 'Failed', error.message, 'bg-danger');
|
||||||
|
testsFailed++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delay between AT commands
|
||||||
|
await delaySelfTest(1000);
|
||||||
|
|
||||||
|
// Test Signal Strength (AT+CSQ)
|
||||||
|
document.getElementById('selftest_status').innerHTML = `
|
||||||
|
<div class="d-flex align-items-center text-primary">
|
||||||
|
<div class="spinner-border spinner-border-sm me-2" role="status"></div>
|
||||||
|
<span>Testing signal strength...</span>
|
||||||
|
</div>`;
|
||||||
|
|
||||||
|
updateTestStatus('signal', 'Testing...', 'Sending AT+CSQ command...', 'bg-info');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const signalResponse = await sendATCommand('AT+CSQ', 5);
|
||||||
|
|
||||||
|
const csqMatch = signalResponse.match(/\+CSQ:\s*(\d+),(\d+)/);
|
||||||
|
if (signalResponse.includes('OK') && csqMatch) {
|
||||||
|
const signalPower = parseInt(csqMatch[1]);
|
||||||
|
|
||||||
|
if (signalPower === 99) {
|
||||||
|
updateTestStatus('signal', 'Failed', 'No signal detected', 'bg-danger');
|
||||||
|
testsFailed++;
|
||||||
|
} else if (signalPower === 0) {
|
||||||
|
updateTestStatus('signal', 'Warning', 'Very poor signal (0/31)', 'bg-warning');
|
||||||
|
testsFailed++;
|
||||||
|
} else if (signalPower <= 24) {
|
||||||
|
updateTestStatus('signal', 'Passed', `Poor signal (${signalPower}/31)`, 'bg-success');
|
||||||
|
testsPassed++;
|
||||||
|
} else if (signalPower <= 26) {
|
||||||
|
updateTestStatus('signal', 'Passed', `Good signal (${signalPower}/31)`, 'bg-success');
|
||||||
|
testsPassed++;
|
||||||
|
} else if (signalPower <= 28) {
|
||||||
|
updateTestStatus('signal', 'Passed', `Very good signal (${signalPower}/31)`, 'bg-success');
|
||||||
|
testsPassed++;
|
||||||
|
} else {
|
||||||
|
updateTestStatus('signal', 'Passed', `Excellent signal (${signalPower}/31)`, 'bg-success');
|
||||||
|
testsPassed++;
|
||||||
|
}
|
||||||
|
} else if (signalResponse.includes('ERROR')) {
|
||||||
|
updateTestStatus('signal', 'Failed', 'Unable to read signal', 'bg-danger');
|
||||||
|
testsFailed++;
|
||||||
|
} else {
|
||||||
|
updateTestStatus('signal', 'Warning', 'Unexpected response', 'bg-warning');
|
||||||
|
testsFailed++;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
updateTestStatus('signal', 'Failed', error.message, 'bg-danger');
|
||||||
|
testsFailed++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delay between AT commands
|
||||||
|
await delaySelfTest(1000);
|
||||||
|
|
||||||
|
// Test Network Connection (AT+COPS?)
|
||||||
|
document.getElementById('selftest_status').innerHTML = `
|
||||||
|
<div class="d-flex align-items-center text-primary">
|
||||||
|
<div class="spinner-border spinner-border-sm me-2" role="status"></div>
|
||||||
|
<span>Testing network connection...</span>
|
||||||
|
</div>`;
|
||||||
|
|
||||||
|
updateTestStatus('network', 'Testing...', 'Sending AT+COPS? command...', 'bg-info');
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Load operators data for network name lookup
|
||||||
|
let opData = null;
|
||||||
|
try {
|
||||||
|
opData = await loadOperatorsDataSelfTest();
|
||||||
|
} catch (e) {
|
||||||
|
addSelfTestLog('Warning: Could not load operators data');
|
||||||
|
}
|
||||||
|
|
||||||
|
const networkResponse = await sendATCommand('AT+COPS?', 5);
|
||||||
|
|
||||||
|
const copsMatch = networkResponse.match(/\+COPS:\s*(\d+)(?:,(\d+),"?([^",]+)"?,(\d+))?/);
|
||||||
|
if (networkResponse.includes('OK') && copsMatch) {
|
||||||
|
const mode = copsMatch[1];
|
||||||
|
const oper = copsMatch[3];
|
||||||
|
const act = copsMatch[4];
|
||||||
|
|
||||||
|
if (oper) {
|
||||||
|
// Get operator name from lookup table
|
||||||
|
let operatorName = oper;
|
||||||
|
if (opData && opData.operators && opData.operators[oper]) {
|
||||||
|
operatorName = opData.operators[oper].name;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get access technology
|
||||||
|
let actDesc = 'Unknown';
|
||||||
|
if (opData && opData.accessTechnology && opData.accessTechnology[act]) {
|
||||||
|
actDesc = opData.accessTechnology[act];
|
||||||
|
}
|
||||||
|
|
||||||
|
updateTestStatus('network', 'Passed', `${operatorName} (${actDesc})`, 'bg-success');
|
||||||
|
testsPassed++;
|
||||||
|
} else {
|
||||||
|
updateTestStatus('network', 'Warning', 'Not registered to network', 'bg-warning');
|
||||||
|
testsFailed++;
|
||||||
|
}
|
||||||
|
} else if (networkResponse.includes('ERROR')) {
|
||||||
|
updateTestStatus('network', 'Failed', 'Unable to get network info', 'bg-danger');
|
||||||
|
testsFailed++;
|
||||||
|
} else {
|
||||||
|
updateTestStatus('network', 'Warning', 'Unexpected response', 'bg-warning');
|
||||||
|
testsFailed++;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
updateTestStatus('network', 'Failed', error.message, 'bg-danger');
|
||||||
|
testsFailed++;
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
addSelfTestLog(`Test sequence error: ${error.message}`);
|
||||||
|
} finally {
|
||||||
|
// Always disable config mode at the end
|
||||||
|
document.getElementById('selftest_status').innerHTML = `
|
||||||
|
<div class="d-flex align-items-center text-primary">
|
||||||
|
<div class="spinner-border spinner-border-sm me-2" role="status"></div>
|
||||||
|
<span>Disabling configuration mode...</span>
|
||||||
|
</div>`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await delaySelfTest(500);
|
||||||
|
await setConfigMode(false);
|
||||||
|
} catch (error) {
|
||||||
|
addSelfTestLog(`Warning: Failed to disable config mode: ${error.message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show final status
|
||||||
|
const totalTests = testsPassed + testsFailed;
|
||||||
|
let statusClass, statusIcon, statusText;
|
||||||
|
|
||||||
|
if (testsFailed === 0) {
|
||||||
|
statusClass = 'text-success';
|
||||||
|
statusIcon = '✓';
|
||||||
|
statusText = 'All tests passed';
|
||||||
|
} else if (testsPassed === 0) {
|
||||||
|
statusClass = 'text-danger';
|
||||||
|
statusIcon = '✗';
|
||||||
|
statusText = 'All tests failed';
|
||||||
|
} else {
|
||||||
|
statusClass = 'text-warning';
|
||||||
|
statusIcon = '!';
|
||||||
|
statusText = 'Some tests failed';
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('selftest_status').innerHTML = `
|
||||||
|
<div class="d-flex align-items-center ${statusClass}">
|
||||||
|
<span class="fs-4 me-2">${statusIcon}</span>
|
||||||
|
<span><strong>${statusText}</strong></span>
|
||||||
|
</div>`;
|
||||||
|
|
||||||
|
document.getElementById('selftest_summary').innerHTML = `
|
||||||
|
<span class="badge bg-success me-1">${testsPassed} passed</span>
|
||||||
|
<span class="badge bg-danger">${testsFailed} failed</span>`;
|
||||||
|
|
||||||
|
// Store summary in report
|
||||||
|
selfTestReport.summary = {
|
||||||
|
passed: testsPassed,
|
||||||
|
failed: testsFailed,
|
||||||
|
status: statusText
|
||||||
|
};
|
||||||
|
|
||||||
|
// Enable buttons
|
||||||
|
document.getElementById('selfTestCloseBtn').disabled = false;
|
||||||
|
document.getElementById('selfTestDoneBtn').disabled = false;
|
||||||
|
document.getElementById('selfTestCopyBtn').disabled = false;
|
||||||
|
document.querySelectorAll('.btn_selfTest').forEach(btn => btn.disabled = false);
|
||||||
|
|
||||||
|
addSelfTestLog('Self test completed.');
|
||||||
|
addSelfTestLog('Click "Copy Report" to share results with support.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateReport() {
|
||||||
|
// Build formatted report
|
||||||
|
let report = `===============================================================
|
||||||
|
NEBULEAIR PRO 4G - SELF TEST REPORT
|
||||||
|
===============================================================
|
||||||
|
|
||||||
|
DEVICE INFORMATION
|
||||||
|
------------------
|
||||||
|
Device ID: ${selfTestReport.deviceId || 'Unknown'}
|
||||||
|
Device Name: ${selfTestReport.deviceName || 'Unknown'}
|
||||||
|
Modem Version: ${selfTestReport.modemVersion || 'Unknown'}
|
||||||
|
System Time: ${selfTestReport.systemTime || 'Unknown'}
|
||||||
|
Report Date: ${selfTestReport.timestamp}
|
||||||
|
GPS Location: ${selfTestReport.latitude || 'N/A'}, ${selfTestReport.longitude || 'N/A'}
|
||||||
|
|
||||||
|
===============================================================
|
||||||
|
TEST RESULTS
|
||||||
|
===============================================================
|
||||||
|
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Add test results (sensors first, then communication)
|
||||||
|
const testNames = {
|
||||||
|
npm: 'NextPM (Particles)',
|
||||||
|
bme280: 'BME280 (Temp/Hum)',
|
||||||
|
noise: 'Noise Sensor',
|
||||||
|
envea: 'Envea (Gas Sensors)',
|
||||||
|
rtc: 'RTC Module (DS3231)',
|
||||||
|
wifi: 'WiFi/Network',
|
||||||
|
modem: 'Modem Connection',
|
||||||
|
sim: 'SIM Card',
|
||||||
|
signal: 'Signal Strength',
|
||||||
|
network: 'Network Connection'
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const [testId, name] of Object.entries(testNames)) {
|
||||||
|
if (selfTestReport.results[testId]) {
|
||||||
|
const result = selfTestReport.results[testId];
|
||||||
|
const statusIcon = result.status === 'Passed' ? '[OK]' :
|
||||||
|
result.status === 'Failed' ? '[FAIL]' :
|
||||||
|
result.status.includes('Hotspot') || result.status.includes('WiFi') || result.status.includes('Ethernet') ? '[INFO]' : '[WARN]';
|
||||||
|
report += `${statusIcon} ${name}
|
||||||
|
Status: ${result.status}
|
||||||
|
Detail: ${result.detail}
|
||||||
|
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add summary
|
||||||
|
if (selfTestReport.summary) {
|
||||||
|
report += `===============================================================
|
||||||
|
SUMMARY
|
||||||
|
===============================================================
|
||||||
|
|
||||||
|
Passed: ${selfTestReport.summary.passed}
|
||||||
|
Failed: ${selfTestReport.summary.failed}
|
||||||
|
Status: ${selfTestReport.summary.status}
|
||||||
|
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add raw AT responses
|
||||||
|
report += `===============================================================
|
||||||
|
RAW AT RESPONSES
|
||||||
|
===============================================================
|
||||||
|
|
||||||
|
`;
|
||||||
|
|
||||||
|
for (const [command, response] of Object.entries(selfTestReport.rawResponses)) {
|
||||||
|
report += `--- ${command} ---
|
||||||
|
${response}
|
||||||
|
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add full logs
|
||||||
|
report += `===============================================================
|
||||||
|
DETAILED LOGS
|
||||||
|
===============================================================
|
||||||
|
|
||||||
|
${document.getElementById('selftest_logs').textContent}
|
||||||
|
|
||||||
|
===============================================================
|
||||||
|
END OF REPORT - Generated by NebuleAir Pro 4G
|
||||||
|
===============================================================
|
||||||
|
`;
|
||||||
|
|
||||||
|
return report;
|
||||||
|
}
|
||||||
|
|
||||||
|
function openShareReportModal() {
|
||||||
|
// Generate the report
|
||||||
|
const report = generateReport();
|
||||||
|
|
||||||
|
// Put report in textarea
|
||||||
|
document.getElementById('shareReportText').value = report;
|
||||||
|
|
||||||
|
// Open the share modal
|
||||||
|
const shareModal = new bootstrap.Modal(document.getElementById('shareReportModal'));
|
||||||
|
shareModal.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectAllReportText() {
|
||||||
|
const textarea = document.getElementById('shareReportText');
|
||||||
|
textarea.select();
|
||||||
|
textarea.setSelectionRange(0, textarea.value.length); // For mobile devices
|
||||||
|
}
|
||||||
|
|
||||||
|
function downloadReport() {
|
||||||
|
const report = generateReport();
|
||||||
|
|
||||||
|
// Create filename with device ID
|
||||||
|
const deviceId = selfTestReport.deviceId || 'unknown';
|
||||||
|
const date = new Date().toISOString().slice(0, 10);
|
||||||
|
const filename = `logs_nebuleair_${deviceId}_${date}.txt`;
|
||||||
|
|
||||||
|
// Create blob and download
|
||||||
|
const blob = new Blob([report], { type: 'text/plain;charset=utf-8' });
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = url;
|
||||||
|
a.download = filename;
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
|
||||||
|
// Cleanup
|
||||||
|
setTimeout(function() {
|
||||||
|
document.body.removeChild(a);
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load the self-test modal HTML into the page
|
||||||
|
function initSelfTestModal() {
|
||||||
|
fetch('selftest-modal.html')
|
||||||
|
.then(response => response.text())
|
||||||
|
.then(html => {
|
||||||
|
// Insert modal HTML before </body>
|
||||||
|
const container = document.createElement('div');
|
||||||
|
container.id = 'selftest-modal-container';
|
||||||
|
container.innerHTML = html;
|
||||||
|
document.body.appendChild(container);
|
||||||
|
})
|
||||||
|
.catch(error => console.error('Error loading selftest modal:', error));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto-init when DOM is ready
|
||||||
|
document.addEventListener('DOMContentLoaded', initSelfTestModal);
|
||||||
58
html/assets/js/topbar-logo.js
Normal file
58
html/assets/js/topbar-logo.js
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
|
||||||
|
/**
|
||||||
|
* Global configuration handler for UI elements
|
||||||
|
* - Updates Topbar Logo based on device type
|
||||||
|
* - Shows/Hides "Screen" sidebar tab based on device type
|
||||||
|
*/
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
let config = null;
|
||||||
|
|
||||||
|
// Fetch config once
|
||||||
|
fetch('launcher.php?type=get_config_sqlite')
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
config = data;
|
||||||
|
applyConfig(); // Apply immediately if elements are ready
|
||||||
|
})
|
||||||
|
.catch(error => console.error('Error loading config:', error));
|
||||||
|
|
||||||
|
// Observe DOM changes to handle dynamically loaded elements (sidebar, topbar)
|
||||||
|
const observer = new MutationObserver(() => {
|
||||||
|
if (config) applyConfig();
|
||||||
|
});
|
||||||
|
|
||||||
|
observer.observe(document.body, { childList: true, subtree: true });
|
||||||
|
|
||||||
|
function applyConfig() {
|
||||||
|
if (!config) return;
|
||||||
|
|
||||||
|
const isModuleAirPro = (config.device_type === 'moduleair_pro' || config.type === 'moduleair_pro');
|
||||||
|
|
||||||
|
// 1. Topbar Logo Logic
|
||||||
|
const logo = document.getElementById('topbar-logo');
|
||||||
|
if (logo && isModuleAirPro) {
|
||||||
|
// prevent unnecessary re-assignments
|
||||||
|
if (!logo.src.includes('logoModuleAir.png')) {
|
||||||
|
logo.src = 'assets/img/logoModuleAir.png';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Sidebar Screen Tab Logic - Use class since ID might be duplicated (desktop/mobile)
|
||||||
|
const navScreenElements = document.querySelectorAll('.nav-screen-item');
|
||||||
|
if (navScreenElements.length > 0) {
|
||||||
|
navScreenElements.forEach(navScreen => {
|
||||||
|
if (isModuleAirPro) {
|
||||||
|
// Ensure it's visible (bootstrap nav-link usually block or flex)
|
||||||
|
if (navScreen.style.display === 'none') {
|
||||||
|
navScreen.style.display = 'flex';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Hide if not pro
|
||||||
|
if (navScreen.style.display !== 'none') {
|
||||||
|
navScreen.style.display = 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -26,6 +26,13 @@
|
|||||||
.offcanvas-backdrop {
|
.offcanvas-backdrop {
|
||||||
z-index: 1040;
|
z-index: 1040;
|
||||||
}
|
}
|
||||||
|
/* Highlight most recent data row with light green background */
|
||||||
|
.table .most-recent-row td {
|
||||||
|
background-color: #d4edda !important;
|
||||||
|
}
|
||||||
|
.table-striped .most-recent-row td {
|
||||||
|
background-color: #d4edda !important;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@@ -49,60 +56,108 @@
|
|||||||
</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">Base de données</h1>
|
<h1 class="mt-4" data-i18n="database.title">Base de données</h1>
|
||||||
<p>Le capteur enregistre en local les données de mesures. Vous pouvez ici les consulter et les télécharger.</p>
|
<p data-i18n="database.description">Le capteur enregistre en local les données de mesures. Vous pouvez ici les consulter et les télécharger.</p>
|
||||||
|
|
||||||
<div class="row mb-3">
|
<div class="row mb-3">
|
||||||
|
|
||||||
<div class="col-sm-5">
|
<div class="col-lg-4 col-md-6 mb-3">
|
||||||
<div class="card text-dark bg-light">
|
<div class="card text-dark bg-light h-100">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h5 class="card-title">Consulter la base de donnée</h5>
|
<h5 class="card-title" data-i18n="database.viewDatabase">Consulter la base de donnée</h5>
|
||||||
<!-- Dropdown to select number of records -->
|
<!-- Dropdown to select number of records -->
|
||||||
<div class="d-flex align-items-center mb-3">
|
<div class="d-flex align-items-center mb-3">
|
||||||
<label for="records_limit" class="form-label me-2">Nombre de mesures:</label>
|
<label for="records_limit" class="form-label me-2" data-i18n="database.numberOfMeasures">Nombre de mesures:</label>
|
||||||
<select id="records_limit" class="form-select w-auto">
|
<select id="records_limit" class="form-select w-auto">
|
||||||
<option value="10" selected>10 dernières</option>
|
<option value="10" selected data-i18n="database.last10">10 dernières</option>
|
||||||
<option value="20">20 dernières</option>
|
<option value="20" data-i18n="database.last20">20 dernières</option>
|
||||||
<option value="30">30 dernières</option>
|
<option value="30" data-i18n="database.last30">30 dernières</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<button class="btn btn-primary" onclick="get_data_sqlite('data_NPM',getSelectedLimit(),false)">Mesures PM</button>
|
<button class="btn btn-primary mb-2" onclick="get_data_sqlite('data_NPM',getSelectedLimit(),false)" data-i18n="database.pmMeasures">Mesures PM</button>
|
||||||
<button class="btn btn-primary" onclick="get_data_sqlite('data_BME280',getSelectedLimit(),false)">Mesures Temp/Hum</button>
|
<button class="btn btn-primary mb-2" onclick="get_data_sqlite('data_BME280',getSelectedLimit(),false)" data-i18n="database.tempHumMeasures">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 mb-2" onclick="get_data_sqlite('data_NPM_5channels',getSelectedLimit(),false)" data-i18n="database.pm5Channels">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 mb-2" onclick="get_data_sqlite('data_envea',getSelectedLimit(),false)" data-i18n="database.cairsensProbe">Sonde Cairsens</button>
|
||||||
<button class="btn btn-primary" onclick="get_data_sqlite('data_WIND',getSelectedLimit(),false)">Sonde Vent</button>
|
<button class="btn btn-primary mb-2" onclick="get_data_sqlite('data_NOISE',getSelectedLimit(),false)" data-i18n="database.noiseProbe">Sonde bruit</button>
|
||||||
|
<button class="btn btn-primary mb-2" onclick="get_data_sqlite('data_WIND',getSelectedLimit(),false)" data-i18n="database.windProbe">Sonde Vent</button>
|
||||||
<button class="btn btn-warning" onclick="get_data_sqlite('timestamp_table',getSelectedLimit(),false)">Timestamp Table</button>
|
<button class="btn btn-primary mb-2" onclick="get_data_sqlite('data_MPPT',getSelectedLimit(),false)" data-i18n="database.battery">Batterie</button>
|
||||||
|
<button class="btn btn-primary mb-2" onclick="get_data_sqlite('data_MHZ19',getSelectedLimit(),false)">Mesures CO2</button>
|
||||||
|
<button class="btn btn-warning mb-2" onclick="get_data_sqlite('timestamp_table',getSelectedLimit(),false)" data-i18n="database.timestampTable">Timestamp Table</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-sm-5">
|
<div class="col-lg-4 col-md-6 mb-3">
|
||||||
<div class="card text-dark bg-light">
|
<div class="card text-dark bg-light h-100">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h5 class="card-title">Télécharger les données</h5>
|
<h5 class="card-title" data-i18n="database.downloadData">Télécharger les données</h5>
|
||||||
<!-- Date selection for download -->
|
<!-- Date selection for download -->
|
||||||
<div class="d-flex align-items-center gap-3 mb-3">
|
<div class="d-flex align-items-center gap-3 mb-3">
|
||||||
<label for="start_date" class="form-label">Date de début:</label>
|
<label for="start_date" class="form-label" data-i18n="database.startDate">Date de début:</label>
|
||||||
<input type="date" id="start_date" class="form-control w-auto">
|
<input type="date" id="start_date" class="form-control w-auto">
|
||||||
<label for="end_date" class="form-label">Date de fin:</label>
|
<label for="end_date" class="form-label" data-i18n="database.endDate">Date de fin:</label>
|
||||||
<input type="date" id="end_date" class="form-control w-auto">
|
<input type="date" id="end_date" class="form-control w-auto">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<button class="btn btn-primary mb-2" onclick="downloadByDate('data_NPM')" data-i18n="database.pmMeasures">Mesures PM</button>
|
||||||
<button class="btn btn-primary" onclick="get_data_sqlite('data_NPM',10,true, getStartDate(), getEndDate())">Mesures PM</button>
|
<button class="btn btn-primary mb-2" onclick="downloadByDate('data_BME280')" data-i18n="database.tempHumMeasures">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 mb-2" onclick="downloadByDate('data_NPM_5channels')" data-i18n="database.pm5Channels">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 mb-2" onclick="downloadByDate('data_envea')" data-i18n="database.cairsensProbe">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 mb-2" onclick="downloadByDate('data_NOISE')" data-i18n="database.noiseProbe">Sonde Bruit</button>
|
||||||
|
<button class="btn btn-primary mb-2" onclick="downloadByDate('data_mppt')" data-i18n="database.battery">Batterie</button>
|
||||||
</table>
|
<button class="btn btn-primary mb-2" onclick="downloadByDate('data_MHZ19')">Mesures CO2</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div class="col-lg-4 col-md-6 mb-3">
|
||||||
|
<div class="card text-dark bg-light h-100">
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="card-title" data-i18n="database.downloadAll">Télécharger toute la table</h5>
|
||||||
|
<p class="text-muted small" data-i18n="database.downloadAllDesc">Télécharge l'intégralité des données sans filtre de date.</p>
|
||||||
|
<button class="btn btn-success mb-2" onclick="downloadFullTable('data_NPM')" data-i18n="database.pmMeasures">Mesures PM</button>
|
||||||
|
<button class="btn btn-success mb-2" onclick="downloadFullTable('data_BME280')" data-i18n="database.tempHumMeasures">Mesures Temp/Hum</button>
|
||||||
|
<button class="btn btn-success mb-2" onclick="downloadFullTable('data_NPM_5channels')" data-i18n="database.pm5Channels">Mesures PM (5 canaux)</button>
|
||||||
|
<button class="btn btn-success mb-2" onclick="downloadFullTable('data_envea')" data-i18n="database.cairsensProbe">Sonde Cairsens</button>
|
||||||
|
<button class="btn btn-success mb-2" onclick="downloadFullTable('data_NOISE')" data-i18n="database.noiseProbe">Sonde Bruit</button>
|
||||||
|
<button class="btn btn-success mb-2" onclick="downloadFullTable('data_MPPT')" data-i18n="database.battery">Batterie</button>
|
||||||
|
<button class="btn btn-success mb-2" onclick="downloadFullTable('data_MHZ19')">Mesures CO2</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row mb-3">
|
||||||
|
|
||||||
|
<div class="col-lg-4 col-md-6 mb-3">
|
||||||
|
<div class="card text-dark bg-light h-100">
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="card-title" data-i18n="database.statsTitle">Informations sur la base</h5>
|
||||||
|
<div id="db_stats_content">
|
||||||
|
<div class="text-center py-3">
|
||||||
|
<div class="spinner-border spinner-border-sm" role="status"></div>
|
||||||
|
<span class="ms-2" data-i18n="common.loading">Chargement...</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row mb-3">
|
||||||
|
<div class="col-lg-4 col-md-6 mb-3">
|
||||||
|
<div class="card text-white bg-danger h-100">
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="card-title" data-i18n="database.dangerZone">Zone dangereuse</h5>
|
||||||
|
<p class="card-text" data-i18n="database.dangerWarning">Attention: Cette action est irréversible!</p>
|
||||||
|
<button class="btn btn-dark btn-lg w-100 mb-2" onclick="emptySensorTables()" data-i18n="database.emptyAllTables">Vider toutes les tables de capteurs</button>
|
||||||
|
<small class="d-block mt-2" data-i18n="database.emptyTablesNote">Note: Les tables de configuration et horodatage seront préservées.</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="row mt-2">
|
<div class="row mt-2">
|
||||||
<div id="table_data"></div>
|
<div id="table_data"></div>
|
||||||
@@ -118,6 +173,9 @@
|
|||||||
<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 src="assets/js/topbar-logo.js"></script>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
document.addEventListener('DOMContentLoaded', function () {
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
@@ -129,6 +187,19 @@
|
|||||||
{ id: 'sidebar_mobile', file: 'sidebar.html' }
|
{ id: 'sidebar_mobile', file: 'sidebar.html' }
|
||||||
];
|
];
|
||||||
|
|
||||||
|
let loadedCount = 0;
|
||||||
|
const totalElements = elementsToLoad.length;
|
||||||
|
|
||||||
|
function applyTranslationsWhenReady() {
|
||||||
|
if (typeof i18n !== 'undefined' && i18n.translations && Object.keys(i18n.translations).length > 0) {
|
||||||
|
console.log("Applying translations to dynamically loaded content");
|
||||||
|
i18n.applyTranslations();
|
||||||
|
} else {
|
||||||
|
// Retry after a short delay if translations aren't loaded yet
|
||||||
|
setTimeout(applyTranslationsWhenReady, 100);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
elementsToLoad.forEach(({ id, file }) => {
|
elementsToLoad.forEach(({ id, file }) => {
|
||||||
fetch(file)
|
fetch(file)
|
||||||
.then(response => response.text())
|
.then(response => response.text())
|
||||||
@@ -137,10 +208,21 @@
|
|||||||
if (element) {
|
if (element) {
|
||||||
element.innerHTML = data;
|
element.innerHTML = data;
|
||||||
}
|
}
|
||||||
|
loadedCount++;
|
||||||
|
|
||||||
|
// Re-apply translations after all dynamic content is loaded
|
||||||
|
if (loadedCount === totalElements) {
|
||||||
|
applyTranslationsWhenReady();
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.catch(error => console.error(`Error loading ${file}:`, error));
|
.catch(error => console.error(`Error loading ${file}:`, error));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Also listen for language change events to re-apply translations
|
||||||
|
document.addEventListener('languageChanged', function() {
|
||||||
|
console.log("Language changed, re-applying translations");
|
||||||
|
i18n.applyTranslations();
|
||||||
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -170,9 +252,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) {
|
||||||
@@ -181,6 +263,9 @@ window.onload = function() {
|
|||||||
}); //end ajax
|
}); //end ajax
|
||||||
|
|
||||||
|
|
||||||
|
// Get database table stats
|
||||||
|
loadDbStats();
|
||||||
|
|
||||||
//get local RTC
|
//get local RTC
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: 'launcher.php?type=RTC_time',
|
url: 'launcher.php?type=RTC_time',
|
||||||
@@ -242,6 +327,7 @@ function get_data_sqlite(table, limit, download , startDate = "", endDate = "")
|
|||||||
<th>PM10</th>
|
<th>PM10</th>
|
||||||
<th>Temperature (°C)</th>
|
<th>Temperature (°C)</th>
|
||||||
<th>Humidity (%)</th>
|
<th>Humidity (%)</th>
|
||||||
|
<th>Status</th>
|
||||||
`;
|
`;
|
||||||
} else if (table === "data_BME280") {
|
} else if (table === "data_BME280") {
|
||||||
tableHTML += `
|
tableHTML += `
|
||||||
@@ -280,16 +366,45 @@ 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>
|
||||||
|
|
||||||
|
`;
|
||||||
|
}else if (table === "data_MHZ19") {
|
||||||
|
tableHTML += `
|
||||||
|
<th>Timestamp</th>
|
||||||
|
<th>CO2 (ppm)</th>
|
||||||
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
tableHTML += `</tr></thead><tbody>`;
|
tableHTML += `</tr></thead><tbody>`;
|
||||||
|
|
||||||
// Loop through rows and create table rows
|
// Loop through rows and create table rows
|
||||||
rows.forEach(row => {
|
rows.forEach((row, index) => {
|
||||||
let columns = row.replace(/[()]/g, "").split(", "); // Remove parentheses and split
|
let columns = row.replace(/[()]/g, "").split(", "); // Remove parentheses and split
|
||||||
tableHTML += "<tr>";
|
// Add special class to first row (most recent data)
|
||||||
|
const rowClass = index === 0 ? ' class="most-recent-row"' : '';
|
||||||
|
tableHTML += `<tr${rowClass}>`;
|
||||||
|
|
||||||
if (table === "data_NPM") {
|
if (table === "data_NPM") {
|
||||||
|
const statusVal = parseInt(columns[6]) || 0;
|
||||||
|
const statusBadge = statusVal === 0
|
||||||
|
? '<span class="badge text-bg-success">OK</span>'
|
||||||
|
: `<span class="badge text-bg-warning">0x${statusVal.toString(16).toUpperCase().padStart(2,'0')}</span>`;
|
||||||
tableHTML += `
|
tableHTML += `
|
||||||
<td>${columns[0]}</td>
|
<td>${columns[0]}</td>
|
||||||
<td>${columns[1]}</td>
|
<td>${columns[1]}</td>
|
||||||
@@ -297,6 +412,7 @@ function get_data_sqlite(table, limit, download , startDate = "", endDate = "")
|
|||||||
<td>${columns[3]}</td>
|
<td>${columns[3]}</td>
|
||||||
<td>${columns[4]}</td>
|
<td>${columns[4]}</td>
|
||||||
<td>${columns[5]}</td>
|
<td>${columns[5]}</td>
|
||||||
|
<td>${statusBadge}</td>
|
||||||
`;
|
`;
|
||||||
} else if (table === "data_BME280") {
|
} else if (table === "data_BME280") {
|
||||||
tableHTML += `
|
tableHTML += `
|
||||||
@@ -336,6 +452,27 @@ 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>
|
||||||
|
|
||||||
|
`;
|
||||||
|
}else if (table === "data_MHZ19") {
|
||||||
|
tableHTML += `
|
||||||
|
<td>${columns[0]}</td>
|
||||||
|
<td>${columns[1]}</td>
|
||||||
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
tableHTML += "</tr>";
|
tableHTML += "</tr>";
|
||||||
@@ -360,11 +497,25 @@ function getSelectedLimit() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getStartDate() {
|
function getStartDate() {
|
||||||
return document.getElementById("start_date").value || "2025-01-01"; // Default to a safe date
|
return document.getElementById("start_date").value;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getEndDate() {
|
function getEndDate() {
|
||||||
return document.getElementById("end_date").value || "2025-12-31"; // Default to a safe date
|
return document.getElementById("end_date").value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function downloadByDate(table) {
|
||||||
|
const startDate = getStartDate();
|
||||||
|
const endDate = getEndDate();
|
||||||
|
if (!startDate || !endDate) {
|
||||||
|
alert("Veuillez sélectionner une date de début et une date de fin.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
get_data_sqlite(table, 10, true, startDate, endDate);
|
||||||
|
}
|
||||||
|
|
||||||
|
function downloadFullTable(table) {
|
||||||
|
window.location.href = 'launcher.php?type=download_full_table&table=' + encodeURIComponent(table);
|
||||||
}
|
}
|
||||||
|
|
||||||
function downloadCSV(response, table) {
|
function downloadCSV(response, table) {
|
||||||
@@ -374,13 +525,16 @@ function downloadCSV(response, table) {
|
|||||||
|
|
||||||
// Add headers based on table type
|
// Add headers based on table type
|
||||||
if (table === "data_NPM") {
|
if (table === "data_NPM") {
|
||||||
csvContent += "TimestampUTC,PM1,PM2.5,PM10,Temperature_sensor,Humidity_sensor\n";
|
csvContent += "TimestampUTC,PM1,PM2.5,PM10,Temperature_sensor,Humidity_sensor,npm_status\n";
|
||||||
} else if (table === "data_BME280") {
|
} else if (table === "data_BME280") {
|
||||||
csvContent += "TimestampUTC,Temperature (°C),Humidity (%),Pressure (hPa)\n";
|
csvContent += "TimestampUTC,Temperature (°C),Humidity (%),Pressure (hPa)\n";
|
||||||
}
|
}
|
||||||
else if (table === "data_NPM_5channels") {
|
else if (table === "data_NPM_5channels") {
|
||||||
csvContent += "TimestampUTC,PM_ch1,PM_ch2,PM_ch3,PM_ch4,PM_ch5\n";
|
csvContent += "TimestampUTC,PM_ch1,PM_ch2,PM_ch3,PM_ch4,PM_ch5\n";
|
||||||
}
|
}
|
||||||
|
else if (table === "data_MHZ19") {
|
||||||
|
csvContent += "TimestampUTC,CO2_ppm\n";
|
||||||
|
}
|
||||||
|
|
||||||
// Format rows as CSV
|
// Format rows as CSV
|
||||||
rows.forEach(row => {
|
rows.forEach(row => {
|
||||||
@@ -399,6 +553,140 @@ function downloadCSV(response, table) {
|
|||||||
document.body.removeChild(a);
|
document.body.removeChild(a);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Table display names
|
||||||
|
const tableDisplayNames = {
|
||||||
|
'data_NPM': 'PM (NextPM)',
|
||||||
|
'data_NPM_5channels': 'PM 5 canaux',
|
||||||
|
'data_BME280': 'Temp/Hum (BME280)',
|
||||||
|
'data_envea': 'Gaz (Cairsens)',
|
||||||
|
'data_WIND': 'Vent',
|
||||||
|
'data_MPPT': 'Batterie (MPPT)',
|
||||||
|
'data_NOISE': 'Bruit'
|
||||||
|
};
|
||||||
|
|
||||||
|
function loadDbStats() {
|
||||||
|
$.ajax({
|
||||||
|
url: 'launcher.php?type=db_table_stats',
|
||||||
|
dataType: 'json',
|
||||||
|
method: 'GET',
|
||||||
|
success: function(response) {
|
||||||
|
if (!response.success) {
|
||||||
|
document.getElementById('db_stats_content').innerHTML =
|
||||||
|
'<div class="alert alert-danger mb-0">' + (response.error || 'Erreur') + '</div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let html = '<p class="mb-2"><strong data-i18n="database.statsDbSize">Taille totale:</strong> ' + response.size_mb + ' MB</p>';
|
||||||
|
html += '<div class="table-responsive"><table class="table table-sm table-bordered mb-0">';
|
||||||
|
html += '<thead class="table-secondary"><tr>';
|
||||||
|
html += '<th data-i18n="database.statsTable">Table</th>';
|
||||||
|
html += '<th data-i18n="database.statsCount">Entrées</th>';
|
||||||
|
html += '<th data-i18n="database.statsOldest">Plus ancienne</th>';
|
||||||
|
html += '<th data-i18n="database.statsNewest">Plus récente</th>';
|
||||||
|
html += '<th data-i18n="database.statsDownload">CSV</th>';
|
||||||
|
html += '</tr></thead><tbody>';
|
||||||
|
|
||||||
|
response.tables.forEach(function(t) {
|
||||||
|
const displayName = tableDisplayNames[t.name] || t.name;
|
||||||
|
const oldest = t.oldest ? t.oldest.substring(0, 16) : '-';
|
||||||
|
const newest = t.newest ? t.newest.substring(0, 16) : '-';
|
||||||
|
const downloadBtn = t.count > 0
|
||||||
|
? '<a href="launcher.php?type=download_full_table&table=' + t.name + '" class="btn btn-outline-primary btn-sm py-0 px-1" title="Download CSV"><svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" viewBox="0 0 16 16"><path d="M.5 9.9a.5.5 0 0 1 .5.5v2.5a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-2.5a.5.5 0 0 1 1 0v2.5a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2v-2.5a.5.5 0 0 1 .5-.5z"/><path d="M7.646 11.854a.5.5 0 0 0 .708 0l3-3a.5.5 0 0 0-.708-.708L8.5 10.293V1.5a.5.5 0 0 0-1 0v8.793L5.354 8.146a.5.5 0 1 0-.708.708l3 3z"/></svg></a>'
|
||||||
|
: '-';
|
||||||
|
html += '<tr>';
|
||||||
|
html += '<td>' + displayName + '</td>';
|
||||||
|
html += '<td>' + t.count.toLocaleString() + '</td>';
|
||||||
|
html += '<td><small>' + oldest + '</small></td>';
|
||||||
|
html += '<td><small>' + newest + '</small></td>';
|
||||||
|
html += '<td class="text-center">' + downloadBtn + '</td>';
|
||||||
|
html += '</tr>';
|
||||||
|
});
|
||||||
|
|
||||||
|
html += '</tbody></table></div>';
|
||||||
|
html += '<button class="btn btn-outline-secondary btn-sm mt-2" onclick="loadDbStats()" data-i18n="logs.refresh">Refresh</button>';
|
||||||
|
|
||||||
|
document.getElementById('db_stats_content').innerHTML = html;
|
||||||
|
|
||||||
|
// Re-apply translations if i18n is loaded
|
||||||
|
if (typeof i18n !== 'undefined' && i18n.translations && Object.keys(i18n.translations).length > 0) {
|
||||||
|
i18n.applyTranslations();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: function(xhr, status, error) {
|
||||||
|
document.getElementById('db_stats_content').innerHTML =
|
||||||
|
'<div class="alert alert-danger mb-0">Erreur: ' + error + '</div>';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to empty all sensor tables
|
||||||
|
function emptySensorTables() {
|
||||||
|
// Show confirmation dialog
|
||||||
|
const confirmed = confirm(
|
||||||
|
"WARNING: This will permanently delete ALL sensor data from the database!\n\n" +
|
||||||
|
"The following tables will be emptied:\n" +
|
||||||
|
"- data_NPM\n" +
|
||||||
|
"- data_NPM_5channels\n" +
|
||||||
|
"- data_BME280\n" +
|
||||||
|
"- data_envea\n" +
|
||||||
|
"- data_WIND\n" +
|
||||||
|
"- data_MPPT\n" +
|
||||||
|
"- data_NOISE\n\n" +
|
||||||
|
"Configuration and timestamp tables will be preserved.\n\n" +
|
||||||
|
"Are you absolutely sure you want to continue?"
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!confirmed) {
|
||||||
|
console.log("Empty sensor tables operation cancelled by user");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show loading message
|
||||||
|
const tableDataDiv = document.getElementById("table_data");
|
||||||
|
tableDataDiv.innerHTML = '<div class="alert alert-info">Emptying sensor tables... Please wait...</div>';
|
||||||
|
|
||||||
|
// Make AJAX request to empty tables
|
||||||
|
$.ajax({
|
||||||
|
url: 'launcher.php?type=empty_sensor_tables',
|
||||||
|
dataType: 'json',
|
||||||
|
method: 'GET',
|
||||||
|
success: function(response) {
|
||||||
|
console.log("Empty sensor tables response:", response);
|
||||||
|
|
||||||
|
if (response.success) {
|
||||||
|
// Show success message
|
||||||
|
let message = '<div class="alert alert-success">';
|
||||||
|
message += '<h5>Success!</h5>';
|
||||||
|
message += '<p>' + response.message + '</p>';
|
||||||
|
|
||||||
|
if (response.tables_processed && response.tables_processed.length > 0) {
|
||||||
|
message += '<p><strong>Tables emptied:</strong></p><ul>';
|
||||||
|
response.tables_processed.forEach(table => {
|
||||||
|
message += `<li>${table.name}: ${table.deleted} records deleted</li>`;
|
||||||
|
});
|
||||||
|
message += '</ul>';
|
||||||
|
}
|
||||||
|
|
||||||
|
message += '</div>';
|
||||||
|
tableDataDiv.innerHTML = message;
|
||||||
|
} else {
|
||||||
|
// Show error message
|
||||||
|
tableDataDiv.innerHTML = `<div class="alert alert-danger">
|
||||||
|
<h5>Error!</h5>
|
||||||
|
<p>${response.message || response.error || 'Unknown error occurred'}</p>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: function(xhr, status, error) {
|
||||||
|
console.error('AJAX request failed:', status, error);
|
||||||
|
tableDataDiv.innerHTML = `<div class="alert alert-danger">
|
||||||
|
<h5>Error!</h5>
|
||||||
|
<p>Failed to empty sensor tables: ${error}</p>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
749
html/index.html
749
html/index.html
@@ -1,5 +1,6 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
@@ -11,31 +12,39 @@
|
|||||||
body {
|
body {
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
#sidebar a.nav-link {
|
#sidebar a.nav-link {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
#sidebar a.nav-link:hover {
|
#sidebar a.nav-link:hover {
|
||||||
background-color: rgba(0, 0, 0, 0.5);
|
background-color: rgba(0, 0, 0, 0.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
#sidebar a.nav-link svg {
|
#sidebar a.nav-link svg {
|
||||||
margin-right: 8px; /* Add spacing between icons and text */
|
margin-right: 8px;
|
||||||
|
/* Add spacing between icons and text */
|
||||||
}
|
}
|
||||||
|
|
||||||
#sidebar {
|
#sidebar {
|
||||||
transition: transform 0.3s ease-in-out;
|
transition: transform 0.3s ease-in-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
.offcanvas-backdrop {
|
.offcanvas-backdrop {
|
||||||
z-index: 1040;
|
z-index: 1040;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<!-- Topbar -->
|
<!-- Topbar -->
|
||||||
<span id="topbar"></span>
|
<span id="topbar"></span>
|
||||||
|
|
||||||
<!-- Sidebar Offcanvas for Mobile -->
|
<!-- Sidebar Offcanvas for Mobile -->
|
||||||
<div class="offcanvas offcanvas-start text-white bg-dark" tabindex="-1" id="sidebarOffcanvas" aria-labelledby="sidebarOffcanvasLabel">
|
<div class="offcanvas offcanvas-start text-white bg-dark" tabindex="-1" id="sidebarOffcanvas"
|
||||||
|
aria-labelledby="sidebarOffcanvasLabel">
|
||||||
<div class="offcanvas-header">
|
<div class="offcanvas-header">
|
||||||
<h5 class="offcanvas-title" id="sidebarOffcanvasLabel">NebuleAir</h5>
|
<h5 class="offcanvas-title" id="sidebarOffcanvasLabel">NebuleAir</h5>
|
||||||
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="offcanvas" aria-label="Close"></button>
|
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="offcanvas" aria-label="Close"></button>
|
||||||
@@ -47,44 +56,56 @@
|
|||||||
|
|
||||||
<div class="container-fluid mt-5">
|
<div class="container-fluid mt-5">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<aside class="col-md-2 col-lg-1 d-none d-md-block vh-100 position-fixed bg-dark text-white" id="sidebar">
|
<aside class="col-md-2 col-lg-1 d-none d-md-block vh-100 position-fixed bg-dark text-white" id="sidebar">
|
||||||
</aside>
|
</aside>
|
||||||
<!-- Main content -->
|
<!-- Main content -->
|
||||||
<main class="col-md-9 ms-sm-auto col-lg-10 offset-md-3 offset-lg-2 px-md-4">
|
<main class="col-md-9 ms-sm-auto col-lg-10 offset-md-3 offset-lg-2 px-md-4">
|
||||||
<h1 class="mt-4">Votre capteur</h1>
|
<h1 class="mt-4" data-i18n="home.title">Votre capteur</h1>
|
||||||
<p>Bienvenue sur votre interface de configuration de votre capteur.</p>
|
<p data-i18n="home.welcome">Bienvenue sur votre interface de configuration de votre capteur.</p>
|
||||||
|
|
||||||
|
<button class="btn btn-success mb-3 btn_selfTest" onclick="runSelfTest()">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-check2-circle me-1" viewBox="0 0 16 16">
|
||||||
|
<path d="M2.5 8a5.5 5.5 0 1 1 11 0 5.5 5.5 0 0 1-11 0z"/>
|
||||||
|
<path d="M15.354 3.354a.5.5 0 0 0-.708-.708L8 9.293 5.354 6.646a.5.5 0 1 0-.708.708l3 3a.5.5 0 0 0 .708 0l7-7z"/>
|
||||||
|
</svg>
|
||||||
|
Run Self Test
|
||||||
|
</button>
|
||||||
|
|
||||||
<div class="row mb-3">
|
<div class="row mb-3">
|
||||||
|
|
||||||
<!-- Card NPM values -->
|
<!-- Card NPM values -->
|
||||||
<div class="col-sm-4 mt-2">
|
<div class="col-sm-4 mt-2">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h5 class="card-title">Mesures PM</h5>
|
<h5 class="card-title" data-i18n="home.pmMeasures">Mesures PM</h5>
|
||||||
<canvas id="sensorPMChart" style="width: 100%; max-width: 600px; height: 200px;"></canvas>
|
<canvas id="sensorPMChart" style="width: 100%; max-width: 600px; height: 200px;"></canvas>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Card Linux Stats -->
|
<!-- Card Linux Stats -->
|
||||||
<div class="col-sm-4 mt-2">
|
<div class="col-sm-4 mt-2">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h5 class="card-title">Linux stats</h5>
|
<h5 class="card-title" data-i18n="home.linuxStats">Statistiques Linux</h5>
|
||||||
<p class="card-text">Disk usage (total size <span id="disk_size"></span> Gb) </p>
|
<p class="card-text"><span data-i18n="home.diskUsage">Utilisation du disque (taille totale</span> <span
|
||||||
|
id="disk_size"></span> Gb) </p>
|
||||||
<div id="disk_space"></div>
|
<div id="disk_space"></div>
|
||||||
<p class="card-text">Memory usage (total size <span id="memory_size"></span> Mb) </p>
|
<p class="card-text"><span data-i18n="home.memoryUsage">Utilisation de la mémoire (taille totale</span>
|
||||||
|
<span id="memory_size"></span> Mb)
|
||||||
|
</p>
|
||||||
<div id="memory_space"></div>
|
<div id="memory_space"></div>
|
||||||
<p class="card-text"> Database size: <span id="database_size"></span> </p>
|
<p class="card-text"><span data-i18n="home.databaseSize">Taille de la base de données:</span> <span
|
||||||
|
id="database_size"></span> </p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
<div class="row mb-3">
|
<div class="row mb-3">
|
||||||
|
|
||||||
<div class="col-sm-4 mt-2">
|
<div class="col-sm-4 mt-2">
|
||||||
@@ -102,362 +123,378 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- JAVASCRIPT -->
|
<!-- JAVASCRIPT -->
|
||||||
|
|
||||||
<!-- Link Ajax locally -->
|
<!-- Link Ajax locally -->
|
||||||
<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 src="assets/js/topbar-logo.js"></script>
|
||||||
|
<script src="assets/js/selftest.js"></script>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
document.addEventListener('DOMContentLoaded', function () {
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
const elementsToLoad = [
|
const elementsToLoad = [
|
||||||
{ id: 'topbar', file: 'topbar.html' },
|
{ id: 'topbar', file: 'topbar.html' },
|
||||||
{ id: 'sidebar', file: 'sidebar.html' },
|
{ id: 'sidebar', file: 'sidebar.html' },
|
||||||
{ id: 'sidebar_mobile', file: 'sidebar.html' }
|
{ id: 'sidebar_mobile', file: 'sidebar.html' }
|
||||||
];
|
];
|
||||||
|
|
||||||
elementsToLoad.forEach(({ id, file }) => {
|
elementsToLoad.forEach(({ id, file }) => {
|
||||||
fetch(file)
|
fetch(file)
|
||||||
.then(response => response.text())
|
.then(response => response.text())
|
||||||
.then(data => {
|
.then(data => {
|
||||||
const element = document.getElementById(id);
|
const element = document.getElementById(id);
|
||||||
if (element) {
|
if (element) {
|
||||||
element.innerHTML = data;
|
element.innerHTML = data;
|
||||||
}
|
// Apply translations after loading dynamic content
|
||||||
})
|
if (window.i18n && typeof window.i18n.applyTranslations === 'function') {
|
||||||
.catch(error => console.error(`Error loading ${file}:`, error));
|
window.i18n.applyTranslations();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => console.error(`Error loading ${file}:`, error));
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
});
|
window.onload = function () {
|
||||||
|
|
||||||
|
//NEW way to get data from SQLITE
|
||||||
|
$.ajax({
|
||||||
|
url: 'launcher.php?type=get_config_sqlite',
|
||||||
|
dataType: 'json',
|
||||||
|
//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 table:");
|
||||||
|
console.log(response);
|
||||||
|
|
||||||
window.onload = function() {
|
//get device Name (for the side bar)
|
||||||
|
const deviceName = response.deviceName;
|
||||||
|
const elements = document.querySelectorAll('.sideBar_sensorName');
|
||||||
|
elements.forEach((element) => {
|
||||||
|
element.innerText = deviceName;
|
||||||
|
});
|
||||||
|
|
||||||
//NEW way to get data from SQLITE
|
//device name html page title
|
||||||
$.ajax({
|
if (response.deviceName) {
|
||||||
url: 'launcher.php?type=get_config_sqlite',
|
document.title = response.deviceName;
|
||||||
dataType:'json',
|
}
|
||||||
//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 table:");
|
|
||||||
console.log(response);
|
|
||||||
|
|
||||||
//get device Name (for the side bar)
|
// Check for device type to show Screen tab
|
||||||
const deviceName = response.deviceName;
|
// Assuming the key in config is 'device_type' or 'type'
|
||||||
const elements = document.querySelectorAll('.sideBar_sensorName');
|
if (response.device_type === 'moduleair_pro' || response.type === 'moduleair_pro') {
|
||||||
elements.forEach((element) => {
|
$('.nav-screen-item').show();
|
||||||
element.innerText = deviceName;
|
$('.nav-screen-item').css('display', 'flex'); // Ensure flex display to match others
|
||||||
});
|
}
|
||||||
|
|
||||||
//device name html page title
|
},
|
||||||
if (response.deviceName) {
|
error: function (xhr, status, error) {
|
||||||
document.title = response.deviceName;
|
console.error('AJAX request failed:', status, error);
|
||||||
}
|
|
||||||
|
|
||||||
},
|
|
||||||
error: function(xhr, status, error) {
|
|
||||||
console.error('AJAX request failed:', status, error);
|
|
||||||
}
|
|
||||||
}); //end ajax
|
|
||||||
|
|
||||||
/* OLD way of getting config data
|
|
||||||
fetch('../config.json') // Replace 'deviceID.txt' with 'config.json'
|
|
||||||
.then(response => response.json()) // Parse response as JSON
|
|
||||||
.then(data => {
|
|
||||||
console.log("Getting config file (onload)");
|
|
||||||
//get device ID
|
|
||||||
const deviceID = data.deviceID.trim().toUpperCase();
|
|
||||||
//document.getElementById('pageTitle_plus_ID').innerText = 'token: ' + deviceID;
|
|
||||||
|
|
||||||
|
|
||||||
//get device Name
|
|
||||||
const deviceName = data.deviceName;
|
|
||||||
|
|
||||||
const elements = document.querySelectorAll('.sideBar_sensorName');
|
|
||||||
elements.forEach((element) => {
|
|
||||||
element.innerText = deviceName;
|
|
||||||
});
|
|
||||||
|
|
||||||
//end fetch config
|
|
||||||
})
|
|
||||||
.catch(error => console.error('Error loading config.json:', error));
|
|
||||||
//end windows on load
|
|
||||||
*/
|
|
||||||
//get local RTC
|
|
||||||
$.ajax({
|
|
||||||
url: 'launcher.php?type=RTC_time',
|
|
||||||
dataType: 'text', // Specify that you expect a JSON response
|
|
||||||
method: 'GET', // Use GET or POST depending on your needs
|
|
||||||
success: function(response) {
|
|
||||||
console.log("Local RTC: " + response);
|
|
||||||
const RTC_Element = document.getElementById("RTC_time");
|
|
||||||
RTC_Element.textContent = response;
|
|
||||||
},
|
|
||||||
error: function(xhr, status, error) {
|
|
||||||
console.error('AJAX request failed:', status, error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
//get database size
|
|
||||||
$.ajax({
|
|
||||||
url: 'launcher.php?type=database_size',
|
|
||||||
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);
|
|
||||||
|
|
||||||
if (response.size_megabytes !== undefined) {
|
|
||||||
// Extract and format the size in MB
|
|
||||||
const databaseSizeMB = response.size_megabytes + " MB";
|
|
||||||
|
|
||||||
// Update the HTML element with the database size
|
|
||||||
const databaseSizeElement = document.getElementById("database_size");
|
|
||||||
databaseSizeElement.textContent = databaseSizeMB;
|
|
||||||
|
|
||||||
console.log("Database size:", databaseSizeMB);
|
|
||||||
} else if (response.error) {
|
|
||||||
// Handle errors from the PHP response
|
|
||||||
console.error("Error from server:", response.error);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
error: function(xhr, status, error) {
|
|
||||||
console.error('AJAX request failed:', status, error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
//get disk free space
|
|
||||||
$.ajax({
|
|
||||||
url: 'launcher.php?type=linux_disk',
|
|
||||||
dataType: 'text', // Specify that you expect a JSON response
|
|
||||||
method: 'GET', // Use GET or POST depending on your needs
|
|
||||||
success: function(response) {
|
|
||||||
console.log("Linux disk space: " + response);
|
|
||||||
//1. disk size
|
|
||||||
const disk_size = document.getElementById("disk_size");
|
|
||||||
const firstNumber = response.match(/(?<!\w)(\d+(\.\d+)?)(?=\D)/)[1];
|
|
||||||
|
|
||||||
disk_size.innerHTML = firstNumber;
|
|
||||||
//2. Free space
|
|
||||||
const match = response.match(/(\d+)%/);
|
|
||||||
const diskSpace = document.getElementById("disk_space");
|
|
||||||
const percentage = match[1];
|
|
||||||
|
|
||||||
// Create the outer div with class and attributes
|
|
||||||
const progressDiv = document.createElement('div');
|
|
||||||
progressDiv.className = 'progress mb-3';
|
|
||||||
progressDiv.setAttribute('role', 'progressbar');
|
|
||||||
progressDiv.setAttribute('aria-label', 'Example with label');
|
|
||||||
progressDiv.setAttribute('aria-valuenow', percentage);
|
|
||||||
progressDiv.setAttribute('aria-valuemin', 0);
|
|
||||||
progressDiv.setAttribute('aria-valuemax', 100);
|
|
||||||
|
|
||||||
// Create the inner progress bar div
|
|
||||||
const progressBarDiv = document.createElement('div');
|
|
||||||
progressBarDiv.className = 'progress-bar';
|
|
||||||
progressBarDiv.style.width = `${percentage}%`; // Set the width dynamically
|
|
||||||
progressBarDiv.textContent = `${percentage}%`; // Set the text dynamically
|
|
||||||
|
|
||||||
// Append the progress bar to the outer div
|
|
||||||
progressDiv.appendChild(progressBarDiv);
|
|
||||||
|
|
||||||
// Append the entire progress bar to the body (or any other container)
|
|
||||||
diskSpace.appendChild(progressDiv);
|
|
||||||
|
|
||||||
},
|
|
||||||
error: function(xhr, status, error) {
|
|
||||||
console.error('AJAX request failed:', status, error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
//get memory free space
|
|
||||||
$.ajax({
|
|
||||||
url: 'launcher.php?type=linux_memory',
|
|
||||||
dataType: 'text', // Specify that you expect a JSON response
|
|
||||||
method: 'GET', // Use GET or POST depending on your needs
|
|
||||||
success: function(response) {
|
|
||||||
console.log("Linux memory space: " + response);
|
|
||||||
//1. memory size
|
|
||||||
const memory_size = document.getElementById("memory_size");
|
|
||||||
const memorySpace = document.getElementById("memory_space");
|
|
||||||
|
|
||||||
|
|
||||||
const memLine = response.match(/Mem:\s+(\d+\.?\d*)Mi\s+(\d+\.?\d*)Mi/);
|
|
||||||
const totalMemory = parseFloat(memLine[1]); // Total memory in MiB
|
|
||||||
const usedMemory = parseFloat(memLine[2]); // Used memory in MiB
|
|
||||||
|
|
||||||
// Calculate the percentage
|
|
||||||
const percentageUsed = ((usedMemory / totalMemory) * 100).toFixed(2);
|
|
||||||
|
|
||||||
console.log(totalMemory);
|
|
||||||
|
|
||||||
memory_size.innerHTML = totalMemory;
|
|
||||||
|
|
||||||
|
|
||||||
console.log(usedMemory);
|
|
||||||
console.log(percentageUsed);
|
|
||||||
|
|
||||||
// Create the outer div with class and attributes
|
|
||||||
const progressDiv = document.createElement('div');
|
|
||||||
progressDiv.className = 'progress mb-3';
|
|
||||||
progressDiv.setAttribute('role', 'progressbar');
|
|
||||||
progressDiv.setAttribute('aria-label', 'Example with label');
|
|
||||||
progressDiv.setAttribute('aria-valuenow', percentageUsed);
|
|
||||||
progressDiv.setAttribute('aria-valuemin', 0);
|
|
||||||
progressDiv.setAttribute('aria-valuemax', 100);
|
|
||||||
|
|
||||||
// Create the inner progress bar div
|
|
||||||
const progressBarDiv = document.createElement('div');
|
|
||||||
progressBarDiv.className = 'progress-bar';
|
|
||||||
progressBarDiv.style.width = `${percentageUsed}%`; // Set the width dynamically
|
|
||||||
progressBarDiv.textContent = `${percentageUsed}%`; // Set the text dynamically
|
|
||||||
|
|
||||||
// Append the progress bar to the outer div
|
|
||||||
progressDiv.appendChild(progressBarDiv);
|
|
||||||
|
|
||||||
// Append the entire progress bar to the body (or any other container)
|
|
||||||
memorySpace.appendChild(progressDiv);
|
|
||||||
|
|
||||||
},
|
|
||||||
error: function(xhr, status, error) {
|
|
||||||
console.error('AJAX request failed:', status, error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
// GET NPM SQLite values
|
|
||||||
$.ajax({
|
|
||||||
url: 'launcher.php?type=get_npm_sqlite_data',
|
|
||||||
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);
|
|
||||||
updatePMChart(response);
|
|
||||||
},
|
|
||||||
error: function(xhr, status, error) {
|
|
||||||
console.error('AJAX request failed:', status, error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
let chart; // Store the Chart.js instance globally
|
|
||||||
|
|
||||||
function updatePMChart(data) {
|
|
||||||
const labels = data.map(d => d.timestamp);
|
|
||||||
const PM1 = data.map(d => d.PM1);
|
|
||||||
const PM25 = data.map(d => d.PM25);
|
|
||||||
const PM10 = data.map(d => d.PM10);
|
|
||||||
|
|
||||||
const ctx = document.getElementById('sensorPMChart').getContext('2d');
|
|
||||||
|
|
||||||
if (!chart) {
|
|
||||||
chart = new Chart(ctx, {
|
|
||||||
type: 'line',
|
|
||||||
data: {
|
|
||||||
labels: labels,
|
|
||||||
datasets: [
|
|
||||||
{
|
|
||||||
label: "PM1",
|
|
||||||
data: PM1,
|
|
||||||
borderColor: "rgba(0, 51, 153, 1)",
|
|
||||||
backgroundColor: "rgba(0, 51, 153, 0.2)", // Very light blue background
|
|
||||||
fill: true,
|
|
||||||
tension: 0.4, // Smooth curves
|
|
||||||
pointRadius: 2, // Larger points
|
|
||||||
pointHoverRadius: 6 // Bigger hover points
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "PM2.5",
|
|
||||||
data: PM25,
|
|
||||||
borderColor: "rgba(30, 144, 255, 1)",
|
|
||||||
backgroundColor: "rgba(30, 144, 255, 0.2)", // Very light medium blue background
|
|
||||||
fill: true,
|
|
||||||
tension: 0.4,
|
|
||||||
pointRadius: 2,
|
|
||||||
pointHoverRadius: 6
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "PM10",
|
|
||||||
data: PM10,
|
|
||||||
borderColor: "rgba(135, 206, 250, 1)",
|
|
||||||
backgroundColor: "rgba(135, 206, 250, 0.2)", // Very light blue background
|
|
||||||
fill: true,
|
|
||||||
tension: 0.4,
|
|
||||||
pointRadius: 2,
|
|
||||||
pointHoverRadius: 6
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
options: {
|
|
||||||
responsive: true,
|
|
||||||
maintainAspectRatio: true,
|
|
||||||
plugins: {
|
|
||||||
legend: {
|
|
||||||
position: 'top'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
scales: {
|
|
||||||
x: {
|
|
||||||
title: {
|
|
||||||
display: true,
|
|
||||||
text: 'Time (UTC)',
|
|
||||||
font: {
|
|
||||||
size: 16,
|
|
||||||
family: 'Arial, sans-serif'
|
|
||||||
},
|
|
||||||
color: '#4A4A4A'
|
|
||||||
},
|
|
||||||
ticks: {
|
|
||||||
autoSkip: true,
|
|
||||||
maxTicksLimit: 5,
|
|
||||||
color: '#4A4A4A',
|
|
||||||
callback: function(value, index) {
|
|
||||||
// Access the correct label from the `labels` array
|
|
||||||
const label = labels[index]; // Use the original `labels` array
|
|
||||||
if (label && typeof label === 'string' && label.includes(' ')) {
|
|
||||||
return label.split(' ')[1].slice(0, 5); // Extract "HH:MM"
|
|
||||||
}
|
|
||||||
return value; // Fallback for invalid labels
|
|
||||||
}
|
|
||||||
},
|
|
||||||
grid: {
|
|
||||||
display: false // Remove gridlines for a cleaner look
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
},
|
|
||||||
y: {
|
|
||||||
title: {
|
|
||||||
display: true,
|
|
||||||
text: 'Values (µg/m³)',
|
|
||||||
font: {
|
|
||||||
size: 16,
|
|
||||||
family: 'Arial, sans-serif'
|
|
||||||
},
|
|
||||||
color: '#4A4A4A'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
chart.data.labels = labels;
|
|
||||||
chart.data.datasets[0].data = PM1;
|
|
||||||
chart.data.datasets[1].data = PM25;
|
|
||||||
chart.data.datasets[2].data = PM10;
|
|
||||||
chart.update();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}); //end ajax
|
||||||
|
|
||||||
|
/* OLD way of getting config data
|
||||||
|
fetch('../config.json') // Replace 'deviceID.txt' with 'config.json'
|
||||||
|
.then(response => response.json()) // Parse response as JSON
|
||||||
|
.then(data => {
|
||||||
|
console.log("Getting config file (onload)");
|
||||||
|
//get device ID
|
||||||
|
const deviceID = data.deviceID.trim().toUpperCase();
|
||||||
|
//document.getElementById('pageTitle_plus_ID').innerText = 'token: ' + deviceID;
|
||||||
|
|
||||||
|
|
||||||
|
//get device Name
|
||||||
|
const deviceName = data.deviceName;
|
||||||
|
|
||||||
|
const elements = document.querySelectorAll('.sideBar_sensorName');
|
||||||
|
elements.forEach((element) => {
|
||||||
|
element.innerText = deviceName;
|
||||||
|
});
|
||||||
|
|
||||||
|
//end fetch config
|
||||||
|
})
|
||||||
|
.catch(error => console.error('Error loading config.json:', error));
|
||||||
|
//end windows on load
|
||||||
|
*/
|
||||||
|
//get local RTC
|
||||||
|
$.ajax({
|
||||||
|
url: 'launcher.php?type=RTC_time',
|
||||||
|
dataType: 'text', // Specify that you expect a JSON response
|
||||||
|
method: 'GET', // Use GET or POST depending on your needs
|
||||||
|
success: function (response) {
|
||||||
|
console.log("Local RTC: " + response);
|
||||||
|
const RTC_Element = document.getElementById("RTC_time");
|
||||||
|
RTC_Element.textContent = response;
|
||||||
|
},
|
||||||
|
error: function (xhr, status, error) {
|
||||||
|
console.error('AJAX request failed:', status, error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
//get database size
|
||||||
|
$.ajax({
|
||||||
|
url: 'launcher.php?type=database_size',
|
||||||
|
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);
|
||||||
|
|
||||||
|
if (response.size_megabytes !== undefined) {
|
||||||
|
// Extract and format the size in MB
|
||||||
|
const databaseSizeMB = response.size_megabytes + " MB";
|
||||||
|
|
||||||
|
// Update the HTML element with the database size
|
||||||
|
const databaseSizeElement = document.getElementById("database_size");
|
||||||
|
databaseSizeElement.textContent = databaseSizeMB;
|
||||||
|
|
||||||
|
console.log("Database size:", databaseSizeMB);
|
||||||
|
} else if (response.error) {
|
||||||
|
// Handle errors from the PHP response
|
||||||
|
console.error("Error from server:", response.error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: function (xhr, status, error) {
|
||||||
|
console.error('AJAX request failed:', status, error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
//get disk free space
|
||||||
|
$.ajax({
|
||||||
|
url: 'launcher.php?type=linux_disk',
|
||||||
|
dataType: 'text', // Specify that you expect a JSON response
|
||||||
|
method: 'GET', // Use GET or POST depending on your needs
|
||||||
|
success: function (response) {
|
||||||
|
console.log("Linux disk space: " + response);
|
||||||
|
//1. disk size
|
||||||
|
const disk_size = document.getElementById("disk_size");
|
||||||
|
const firstNumber = response.match(/(?<!\w)(\d+(\.\d+)?)(?=\D)/)[1];
|
||||||
|
|
||||||
|
disk_size.innerHTML = firstNumber;
|
||||||
|
//2. Free space
|
||||||
|
const match = response.match(/(\d+)%/);
|
||||||
|
const diskSpace = document.getElementById("disk_space");
|
||||||
|
const percentage = match[1];
|
||||||
|
|
||||||
|
// Create the outer div with class and attributes
|
||||||
|
const progressDiv = document.createElement('div');
|
||||||
|
progressDiv.className = 'progress mb-3';
|
||||||
|
progressDiv.setAttribute('role', 'progressbar');
|
||||||
|
progressDiv.setAttribute('aria-label', 'Example with label');
|
||||||
|
progressDiv.setAttribute('aria-valuenow', percentage);
|
||||||
|
progressDiv.setAttribute('aria-valuemin', 0);
|
||||||
|
progressDiv.setAttribute('aria-valuemax', 100);
|
||||||
|
|
||||||
|
// Create the inner progress bar div
|
||||||
|
const progressBarDiv = document.createElement('div');
|
||||||
|
progressBarDiv.className = 'progress-bar';
|
||||||
|
progressBarDiv.style.width = `${percentage}%`; // Set the width dynamically
|
||||||
|
progressBarDiv.textContent = `${percentage}%`; // Set the text dynamically
|
||||||
|
|
||||||
|
// Append the progress bar to the outer div
|
||||||
|
progressDiv.appendChild(progressBarDiv);
|
||||||
|
|
||||||
|
// Append the entire progress bar to the body (or any other container)
|
||||||
|
diskSpace.appendChild(progressDiv);
|
||||||
|
|
||||||
|
},
|
||||||
|
error: function (xhr, status, error) {
|
||||||
|
console.error('AJAX request failed:', status, error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
//get memory free space
|
||||||
|
$.ajax({
|
||||||
|
url: 'launcher.php?type=linux_memory',
|
||||||
|
dataType: 'text', // Specify that you expect a JSON response
|
||||||
|
method: 'GET', // Use GET or POST depending on your needs
|
||||||
|
success: function (response) {
|
||||||
|
console.log("Linux memory space: " + response);
|
||||||
|
//1. memory size
|
||||||
|
const memory_size = document.getElementById("memory_size");
|
||||||
|
const memorySpace = document.getElementById("memory_space");
|
||||||
|
|
||||||
|
|
||||||
|
const memLine = response.match(/Mem:\s+(\d+\.?\d*)Mi\s+(\d+\.?\d*)Mi/);
|
||||||
|
const totalMemory = parseFloat(memLine[1]); // Total memory in MiB
|
||||||
|
const usedMemory = parseFloat(memLine[2]); // Used memory in MiB
|
||||||
|
|
||||||
|
// Calculate the percentage
|
||||||
|
const percentageUsed = ((usedMemory / totalMemory) * 100).toFixed(2);
|
||||||
|
|
||||||
|
console.log(totalMemory);
|
||||||
|
|
||||||
|
memory_size.innerHTML = totalMemory;
|
||||||
|
|
||||||
|
|
||||||
|
console.log(usedMemory);
|
||||||
|
console.log(percentageUsed);
|
||||||
|
|
||||||
|
// Create the outer div with class and attributes
|
||||||
|
const progressDiv = document.createElement('div');
|
||||||
|
progressDiv.className = 'progress mb-3';
|
||||||
|
progressDiv.setAttribute('role', 'progressbar');
|
||||||
|
progressDiv.setAttribute('aria-label', 'Example with label');
|
||||||
|
progressDiv.setAttribute('aria-valuenow', percentageUsed);
|
||||||
|
progressDiv.setAttribute('aria-valuemin', 0);
|
||||||
|
progressDiv.setAttribute('aria-valuemax', 100);
|
||||||
|
|
||||||
|
// Create the inner progress bar div
|
||||||
|
const progressBarDiv = document.createElement('div');
|
||||||
|
progressBarDiv.className = 'progress-bar';
|
||||||
|
progressBarDiv.style.width = `${percentageUsed}%`; // Set the width dynamically
|
||||||
|
progressBarDiv.textContent = `${percentageUsed}%`; // Set the text dynamically
|
||||||
|
|
||||||
|
// Append the progress bar to the outer div
|
||||||
|
progressDiv.appendChild(progressBarDiv);
|
||||||
|
|
||||||
|
// Append the entire progress bar to the body (or any other container)
|
||||||
|
memorySpace.appendChild(progressDiv);
|
||||||
|
|
||||||
|
},
|
||||||
|
error: function (xhr, status, error) {
|
||||||
|
console.error('AJAX request failed:', status, error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
// GET NPM SQLite values
|
||||||
|
$.ajax({
|
||||||
|
url: 'launcher.php?type=get_npm_sqlite_data',
|
||||||
|
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);
|
||||||
|
updatePMChart(response);
|
||||||
|
},
|
||||||
|
error: function (xhr, status, error) {
|
||||||
|
console.error('AJAX request failed:', status, error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let chart; // Store the Chart.js instance globally
|
||||||
|
|
||||||
|
function updatePMChart(data) {
|
||||||
|
const labels = data.map(d => d.timestamp);
|
||||||
|
const PM1 = data.map(d => d.PM1);
|
||||||
|
const PM25 = data.map(d => d.PM25);
|
||||||
|
const PM10 = data.map(d => d.PM10);
|
||||||
|
|
||||||
|
const ctx = document.getElementById('sensorPMChart').getContext('2d');
|
||||||
|
|
||||||
|
if (!chart) {
|
||||||
|
chart = new Chart(ctx, {
|
||||||
|
type: 'line',
|
||||||
|
data: {
|
||||||
|
labels: labels,
|
||||||
|
datasets: [
|
||||||
|
{
|
||||||
|
label: "PM1",
|
||||||
|
data: PM1,
|
||||||
|
borderColor: "rgba(0, 51, 153, 1)",
|
||||||
|
backgroundColor: "rgba(0, 51, 153, 0.2)", // Very light blue background
|
||||||
|
fill: true,
|
||||||
|
tension: 0.4, // Smooth curves
|
||||||
|
pointRadius: 2, // Larger points
|
||||||
|
pointHoverRadius: 6 // Bigger hover points
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "PM2.5",
|
||||||
|
data: PM25,
|
||||||
|
borderColor: "rgba(30, 144, 255, 1)",
|
||||||
|
backgroundColor: "rgba(30, 144, 255, 0.2)", // Very light medium blue background
|
||||||
|
fill: true,
|
||||||
|
tension: 0.4,
|
||||||
|
pointRadius: 2,
|
||||||
|
pointHoverRadius: 6
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "PM10",
|
||||||
|
data: PM10,
|
||||||
|
borderColor: "rgba(135, 206, 250, 1)",
|
||||||
|
backgroundColor: "rgba(135, 206, 250, 0.2)", // Very light blue background
|
||||||
|
fill: true,
|
||||||
|
tension: 0.4,
|
||||||
|
pointRadius: 2,
|
||||||
|
pointHoverRadius: 6
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: true,
|
||||||
|
plugins: {
|
||||||
|
legend: {
|
||||||
|
position: 'top'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
scales: {
|
||||||
|
x: {
|
||||||
|
title: {
|
||||||
|
display: true,
|
||||||
|
text: 'Time (UTC)',
|
||||||
|
font: {
|
||||||
|
size: 16,
|
||||||
|
family: 'Arial, sans-serif'
|
||||||
|
},
|
||||||
|
color: '#4A4A4A'
|
||||||
|
},
|
||||||
|
ticks: {
|
||||||
|
autoSkip: true,
|
||||||
|
maxTicksLimit: 5,
|
||||||
|
color: '#4A4A4A',
|
||||||
|
callback: function (value, index) {
|
||||||
|
// Access the correct label from the `labels` array
|
||||||
|
const label = labels[index]; // Use the original `labels` array
|
||||||
|
if (label && typeof label === 'string' && label.includes(' ')) {
|
||||||
|
return label.split(' ')[1].slice(0, 5); // Extract "HH:MM"
|
||||||
|
}
|
||||||
|
return value; // Fallback for invalid labels
|
||||||
|
}
|
||||||
|
},
|
||||||
|
grid: {
|
||||||
|
display: false // Remove gridlines for a cleaner look
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
},
|
||||||
|
y: {
|
||||||
|
title: {
|
||||||
|
display: true,
|
||||||
|
text: 'Values (µg/m³)',
|
||||||
|
font: {
|
||||||
|
size: 16,
|
||||||
|
family: 'Arial, sans-serif'
|
||||||
|
},
|
||||||
|
color: '#4A4A4A'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
chart.data.labels = labels;
|
||||||
|
chart.data.datasets[0].data = PM1;
|
||||||
|
chart.data.datasets[1].data = PM25;
|
||||||
|
chart.data.datasets[2].data = PM10;
|
||||||
|
chart.update();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
</script>
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
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
|
||||||
113
html/lang/en.json
Normal file
113
html/lang/en.json
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
{
|
||||||
|
"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": "NSRT MK4",
|
||||||
|
"description": "NSRT MK4 sound level meter on USB port.",
|
||||||
|
"headerUsb": "USB 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",
|
||||||
|
"screen": "Screen",
|
||||||
|
"sensors": "Sensors",
|
||||||
|
"database": "Database",
|
||||||
|
"modem4g": "4G Modem",
|
||||||
|
"wifi": "WIFI",
|
||||||
|
"logs": "Logs",
|
||||||
|
"map": "Map",
|
||||||
|
"terminal": "Terminal",
|
||||||
|
"admin": "Admin"
|
||||||
|
},
|
||||||
|
"home": {
|
||||||
|
"title": "Your Sensor",
|
||||||
|
"welcome": "Welcome to your sensor configuration interface.",
|
||||||
|
"pmMeasures": "PM Measurements",
|
||||||
|
"linuxStats": "Linux Statistics",
|
||||||
|
"diskUsage": "Disk usage (total size",
|
||||||
|
"memoryUsage": "Memory usage (total size",
|
||||||
|
"databaseSize": "Database size:"
|
||||||
|
},
|
||||||
|
"database": {
|
||||||
|
"title": "Database",
|
||||||
|
"description": "The sensor records measurement data locally. You can view and download it here.",
|
||||||
|
"viewDatabase": "View Database",
|
||||||
|
"numberOfMeasures": "Number of measurements:",
|
||||||
|
"last10": "Last 10",
|
||||||
|
"last20": "Last 20",
|
||||||
|
"last30": "Last 30",
|
||||||
|
"pmMeasures": "PM Measurements",
|
||||||
|
"tempHumMeasures": "Temp/Hum Measurements",
|
||||||
|
"pm5Channels": "PM Measurements (5 channels)",
|
||||||
|
"cairsensProbe": "Cairsens Probe",
|
||||||
|
"noiseProbe": "Noise Probe",
|
||||||
|
"windProbe": "Wind Probe",
|
||||||
|
"battery": "Battery",
|
||||||
|
"timestampTable": "Timestamp Table",
|
||||||
|
"downloadData": "Download Data",
|
||||||
|
"startDate": "Start date:",
|
||||||
|
"endDate": "End date:",
|
||||||
|
"dangerZone": "Danger Zone",
|
||||||
|
"dangerWarning": "Warning: This action is irreversible!",
|
||||||
|
"emptyAllTables": "Empty all sensor tables",
|
||||||
|
"emptyTablesNote": "Note: Configuration and timestamp tables will be preserved.",
|
||||||
|
"statsTitle": "Database Information",
|
||||||
|
"statsDbSize": "Total size:",
|
||||||
|
"statsTable": "Table",
|
||||||
|
"statsCount": "Entries",
|
||||||
|
"statsOldest": "Oldest",
|
||||||
|
"statsNewest": "Newest",
|
||||||
|
"statsDownload": "CSV"
|
||||||
|
},
|
||||||
|
"logs": {
|
||||||
|
"title": "The Log",
|
||||||
|
"description": "The log allows you to know if the sensor processes are running correctly.",
|
||||||
|
"saraLogs": "Sara logs",
|
||||||
|
"bootLogs": "Boot logs",
|
||||||
|
"refresh": "Refresh",
|
||||||
|
"clear": "Clear"
|
||||||
|
}
|
||||||
|
}
|
||||||
113
html/lang/fr.json
Normal file
113
html/lang/fr.json
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
{
|
||||||
|
"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": "NSRT MK4",
|
||||||
|
"description": "Sonomètre NSRT MK4 sur port USB.",
|
||||||
|
"headerUsb": "Port USB"
|
||||||
|
},
|
||||||
|
"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",
|
||||||
|
"screen": "Écran",
|
||||||
|
"sensors": "Capteurs",
|
||||||
|
"database": "Base de données",
|
||||||
|
"modem4g": "Modem 4G",
|
||||||
|
"wifi": "WIFI",
|
||||||
|
"logs": "Logs",
|
||||||
|
"map": "Carte",
|
||||||
|
"terminal": "Terminal",
|
||||||
|
"admin": "Admin"
|
||||||
|
},
|
||||||
|
"home": {
|
||||||
|
"title": "Votre capteur",
|
||||||
|
"welcome": "Bienvenue sur votre interface de configuration de votre capteur.",
|
||||||
|
"pmMeasures": "Mesures PM",
|
||||||
|
"linuxStats": "Statistiques Linux",
|
||||||
|
"diskUsage": "Utilisation du disque (taille totale",
|
||||||
|
"memoryUsage": "Utilisation de la mémoire (taille totale",
|
||||||
|
"databaseSize": "Taille de la base de données:"
|
||||||
|
},
|
||||||
|
"database": {
|
||||||
|
"title": "Base de données",
|
||||||
|
"description": "Le capteur enregistre en local les données de mesures. Vous pouvez ici les consulter et les télécharger.",
|
||||||
|
"viewDatabase": "Consulter la base de donnée",
|
||||||
|
"numberOfMeasures": "Nombre de mesures:",
|
||||||
|
"last10": "10 dernières",
|
||||||
|
"last20": "20 dernières",
|
||||||
|
"last30": "30 dernières",
|
||||||
|
"pmMeasures": "Mesures PM",
|
||||||
|
"tempHumMeasures": "Mesures Temp/Hum",
|
||||||
|
"pm5Channels": "Mesures PM (5 canaux)",
|
||||||
|
"cairsensProbe": "Sonde Cairsens",
|
||||||
|
"noiseProbe": "Sonde bruit",
|
||||||
|
"windProbe": "Sonde Vent",
|
||||||
|
"battery": "Batterie",
|
||||||
|
"timestampTable": "Timestamp Table",
|
||||||
|
"downloadData": "Télécharger les données",
|
||||||
|
"startDate": "Date de début:",
|
||||||
|
"endDate": "Date de fin:",
|
||||||
|
"dangerZone": "Zone dangereuse",
|
||||||
|
"dangerWarning": "Attention: Cette action est irréversible!",
|
||||||
|
"emptyAllTables": "Vider toutes les tables de capteurs",
|
||||||
|
"emptyTablesNote": "Note: Les tables de configuration et horodatage seront préservées.",
|
||||||
|
"statsTitle": "Informations sur la base",
|
||||||
|
"statsDbSize": "Taille totale:",
|
||||||
|
"statsTable": "Table",
|
||||||
|
"statsCount": "Entrées",
|
||||||
|
"statsOldest": "Plus ancienne",
|
||||||
|
"statsNewest": "Plus récente",
|
||||||
|
"statsDownload": "CSV"
|
||||||
|
},
|
||||||
|
"logs": {
|
||||||
|
"title": "Le journal",
|
||||||
|
"description": "Le journal des logs permet de savoir si les processus du capteur se déroulent correctement.",
|
||||||
|
"saraLogs": "Sara logs",
|
||||||
|
"bootLogs": "Boot logs",
|
||||||
|
"refresh": "Refresh",
|
||||||
|
"clear": "Clear"
|
||||||
|
}
|
||||||
|
}
|
||||||
1135
html/launcher.php
1135
html/launcher.php
File diff suppressed because it is too large
Load Diff
@@ -49,16 +49,16 @@
|
|||||||
</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">Le journal</h1>
|
<h1 class="mt-4" data-i18n="logs.title">Le journal</h1>
|
||||||
<p>Le journal des logs permet de savoir si les processus du capteur se déroulent correctement.</p>
|
<p data-i18n="logs.description">Le journal des logs permet de savoir si les processus du capteur se déroulent correctement.</p>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<!-- card 1 -->
|
<!-- card 1 -->
|
||||||
<div class="col-lg-6 col-12">
|
<div class="col-lg-6 col-12">
|
||||||
<div class="card" style="height: 80vh;">
|
<div class="card" style="height: 80vh;">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
Master logs
|
<span data-i18n="logs.saraLogs">Sara logs</span>
|
||||||
<button type="submit" class="btn btn-secondary btn-sm" id="refresh-master-log">Refresh</button>
|
<button type="submit" class="btn btn-secondary btn-sm" id="refresh-master-log" data-i18n="logs.refresh">Refresh</button>
|
||||||
<button type="submit" class="btn btn-secondary btn-sm" onclick="clear_loopLogs()">Clear</button>
|
<button type="submit" class="btn btn-secondary btn-sm" onclick="clear_loopLogs()" data-i18n="logs.clear">Clear</button>
|
||||||
|
|
||||||
<span id="script_running"></span>
|
<span id="script_running"></span>
|
||||||
</div>
|
</div>
|
||||||
@@ -71,8 +71,8 @@
|
|||||||
<div class="col-lg-6 col-12">
|
<div class="col-lg-6 col-12">
|
||||||
<div class="card" style="height: 80vh;">
|
<div class="card" style="height: 80vh;">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
Boot logs
|
<span data-i18n="logs.bootLogs">Boot logs</span>
|
||||||
<button type="submit" class="btn btn-secondary btn-sm" id="refresh-boot-log">Refresh</button>
|
<button type="submit" class="btn btn-secondary btn-sm" id="refresh-boot-log" data-i18n="logs.refresh">Refresh</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body overflow-auto" id="card_boot_content">
|
<div class="card-body overflow-auto" id="card_boot_content">
|
||||||
|
|
||||||
@@ -90,6 +90,9 @@
|
|||||||
<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 src="assets/js/topbar-logo.js"></script>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
document.addEventListener('DOMContentLoaded', function () {
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
|
|||||||
@@ -117,6 +117,7 @@
|
|||||||
<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>
|
||||||
|
<script src="assets/js/topbar-logo.js"></script>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
document.addEventListener('DOMContentLoaded', function () {
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
|
|||||||
659
html/saraR4.html
659
html/saraR4.html
@@ -58,6 +58,14 @@
|
|||||||
<label class="form-check-label" for="check_modem_configMode">Mode configuration</label>
|
<label class="form-check-label" for="check_modem_configMode">Mode configuration</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<button class="btn btn-success mb-3 btn_selfTest" onclick="runSelfTest()">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-check2-circle me-1" viewBox="0 0 16 16">
|
||||||
|
<path d="M2.5 8a5.5 5.5 0 1 1 11 0 5.5 5.5 0 0 1-11 0z"/>
|
||||||
|
<path d="M15.354 3.354a.5.5 0 0 0-.708-.708L8 9.293 5.354 6.646a.5.5 0 1 0-.708.708l3 3a.5.5 0 0 0 .708 0l7-7z"/>
|
||||||
|
</svg>
|
||||||
|
Run Self Test
|
||||||
|
</button>
|
||||||
|
|
||||||
<span id="modem_status_message"></span>
|
<span id="modem_status_message"></span>
|
||||||
<!--
|
<!--
|
||||||
<h3>
|
<h3>
|
||||||
@@ -72,48 +80,73 @@
|
|||||||
|
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<p class="card-text">General information. </p>
|
<p class="card-text">General information. </p>
|
||||||
<button class="btn btn-primary" onclick="getData_saraR4('ttyAMA2', 'ATI', 1)">Get Data</button>
|
<button class="btn btn-primary" onclick="getModemInfo('ttyAMA2', 1)">Get Data</button>
|
||||||
<div id="loading_ttyAMA2_ATI" class="spinner-border spinner-border-sm" style="display: none;" role="status"></div>
|
<div id="loading_ttyAMA2_ATI" class="spinner-border spinner-border-sm" style="display: none;" role="status"></div>
|
||||||
<div id="response_ttyAMA2_ATI"></div>
|
<div id="modem_info_alert"></div>
|
||||||
|
<div class="collapse mt-2" id="modem_info_logs">
|
||||||
|
<div class="card card-body bg-light">
|
||||||
|
<small><code id="response_ttyAMA2_ATI"></code></small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-sm-2">
|
<div class="col-sm-3">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
|
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<p class="card-text">SIM card information.</p>
|
<p class="card-text">SIM card information.</p>
|
||||||
<button class="btn btn-primary" onclick="getData_saraR4('ttyAMA2', 'AT+CCID?', 1)">Get Data</button>
|
<button class="btn btn-primary me-1" onclick="getSimInfo('ttyAMA2', 1)">Get Data (ICCID)</button>
|
||||||
<div id="loading_ttyAMA2_AT_CCID_" class="spinner-border spinner-border-sm" style="display: none;" role="status"></div>
|
<div id="loading_ttyAMA2_AT_CCID_" class="spinner-border spinner-border-sm" style="display: none;" role="status"></div>
|
||||||
<div id="response_ttyAMA2_AT_CCID_"></div>
|
<div id="sim_info_alert"></div>
|
||||||
|
<div class="collapse mt-2" id="sim_info_logs">
|
||||||
|
<div class="card card-body bg-light">
|
||||||
|
<small><code id="response_ttyAMA2_AT_CCID_"></code></small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<hr>
|
||||||
|
<button class="btn btn-primary me-1" onclick="getImsiInfo('ttyAMA2', 1)">Get Data (IMSI)</button>
|
||||||
|
<div id="loading_ttyAMA2_AT_CIMI" class="spinner-border spinner-border-sm" style="display: none;" role="status"></div>
|
||||||
|
<div id="imsi_info_alert"></div>
|
||||||
|
<div class="collapse mt-2" id="imsi_info_logs">
|
||||||
|
<div class="card card-body bg-light">
|
||||||
|
<small><code id="response_ttyAMA2_AT_CIMI"></code></small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-sm-2">
|
<div class="col-sm-3">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
|
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<p class="card-text">Actual Network connection</p>
|
<p class="card-text">Actual Network connection</p>
|
||||||
<button class="btn btn-primary" onclick="getData_saraR4('ttyAMA2', 'AT+COPS?', 2)">Get Data</button>
|
<button class="btn btn-primary" onclick="getNetworkInfo('ttyAMA2', 2)">Get Data</button>
|
||||||
<div id="loading_ttyAMA2_AT_COPS_" class="spinner-border spinner-border-sm" style="display: none;" role="status"></div>
|
<div id="loading_ttyAMA2_AT_COPS_" class="spinner-border spinner-border-sm" style="display: none;" role="status"></div>
|
||||||
<div id="response_ttyAMA2_AT_COPS_"></div>
|
<div id="network_info_alert"></div>
|
||||||
|
<div class="collapse mt-2" id="network_info_logs">
|
||||||
</table>
|
<div class="card card-body bg-light">
|
||||||
|
<small><code id="response_ttyAMA2_AT_COPS_"></code></small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-sm-2">
|
<div class="col-sm-3">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<p class="card-text">Signal strength </p>
|
<p class="card-text">Signal strength </p>
|
||||||
<button class="btn btn-primary" onclick="getData_saraR4('ttyAMA2', 'AT+CSQ', 1)">Get Data</button>
|
<button class="btn btn-primary" onclick="getSignalInfo('ttyAMA2', 1)">Get Data</button>
|
||||||
<div id="loading_ttyAMA2_AT_CSQ" class="spinner-border spinner-border-sm" style="display: none;" role="status"></div>
|
<div id="loading_ttyAMA2_AT_CSQ" class="spinner-border spinner-border-sm" style="display: none;" role="status"></div>
|
||||||
<div id="response_ttyAMA2_AT_CSQ"></div>
|
<div id="signal_info_alert"></div>
|
||||||
</table>
|
<div class="collapse mt-2" id="signal_info_logs">
|
||||||
|
<div class="card card-body bg-light">
|
||||||
|
<small><code id="response_ttyAMA2_AT_CSQ"></code></small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -253,6 +286,34 @@
|
|||||||
</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 +390,10 @@
|
|||||||
<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 src="assets/js/topbar-logo.js"></script>
|
||||||
|
<script src="assets/js/selftest.js"></script>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
document.addEventListener('DOMContentLoaded', function () {
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
@@ -443,6 +508,522 @@ window.onload = function() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function getModemInfo(port, timeout) {
|
||||||
|
console.log("Getting modem info from port " + port);
|
||||||
|
|
||||||
|
$("#loading_ttyAMA2_ATI").show();
|
||||||
|
$("#modem_info_alert").empty();
|
||||||
|
$("#response_ttyAMA2_ATI").empty();
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
url: 'launcher.php?type=sara&port=' + port + '&command=' + encodeURIComponent('ATI') + '&timeout=' + timeout,
|
||||||
|
dataType: 'text',
|
||||||
|
method: 'GET',
|
||||||
|
success: function(response) {
|
||||||
|
console.log("ATI response:", response);
|
||||||
|
$("#loading_ttyAMA2_ATI").hide();
|
||||||
|
|
||||||
|
// Store raw logs
|
||||||
|
const formattedLogs = response.replace(/\n/g, "<br>");
|
||||||
|
$("#response_ttyAMA2_ATI").html(formattedLogs);
|
||||||
|
|
||||||
|
// Parse response to detect modem model
|
||||||
|
let alertHtml = '';
|
||||||
|
const responseUpper = response.toUpperCase();
|
||||||
|
|
||||||
|
if (response.includes('OK') && (responseUpper.includes('SARA-R5') || responseUpper.includes('SARA-R4'))) {
|
||||||
|
// Extract model name
|
||||||
|
let modelName = 'SARA';
|
||||||
|
const modelMatch = response.match(/SARA-R[45]\d*[A-Z]*-\d+[A-Z]*-\d+/i);
|
||||||
|
if (modelMatch) {
|
||||||
|
modelName = modelMatch[0];
|
||||||
|
} else if (responseUpper.includes('SARA-R5')) {
|
||||||
|
modelName = 'SARA-R5';
|
||||||
|
} else if (responseUpper.includes('SARA-R4')) {
|
||||||
|
modelName = 'SARA-R4';
|
||||||
|
}
|
||||||
|
|
||||||
|
alertHtml = `
|
||||||
|
<div class="alert alert-success py-2 mb-0 mt-2 d-flex justify-content-between align-items-center" role="alert">
|
||||||
|
<div>
|
||||||
|
<strong>Modem connected</strong><br>
|
||||||
|
<small>Model: ${modelName}</small>
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-sm btn-outline-secondary" type="button" data-bs-toggle="collapse" data-bs-target="#modem_info_logs" aria-expanded="false" aria-controls="modem_info_logs">
|
||||||
|
<small>+</small>
|
||||||
|
</button>
|
||||||
|
</div>`;
|
||||||
|
} else if (response.includes('ERROR') || response.trim() === '' || !response.includes('OK')) {
|
||||||
|
alertHtml = `
|
||||||
|
<div class="alert alert-danger py-2 mb-0 mt-2 d-flex justify-content-between align-items-center" role="alert">
|
||||||
|
<div>
|
||||||
|
<strong>Modem not connected</strong><br>
|
||||||
|
<small>No response from modem</small>
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-sm btn-outline-secondary" type="button" data-bs-toggle="collapse" data-bs-target="#modem_info_logs" aria-expanded="false" aria-controls="modem_info_logs">
|
||||||
|
<small>+</small>
|
||||||
|
</button>
|
||||||
|
</div>`;
|
||||||
|
} else {
|
||||||
|
// Unknown response but got something
|
||||||
|
alertHtml = `
|
||||||
|
<div class="alert alert-warning py-2 mb-0 mt-2 d-flex justify-content-between align-items-center" role="alert">
|
||||||
|
<div>
|
||||||
|
<strong>Modem detected</strong><br>
|
||||||
|
<small>Unexpected response</small>
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-sm btn-outline-secondary" type="button" data-bs-toggle="collapse" data-bs-target="#modem_info_logs" aria-expanded="false" aria-controls="modem_info_logs">
|
||||||
|
<small>+</small>
|
||||||
|
</button>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
$("#modem_info_alert").html(alertHtml);
|
||||||
|
},
|
||||||
|
error: function(xhr, status, error) {
|
||||||
|
console.error('AJAX request failed:', status, error);
|
||||||
|
$("#loading_ttyAMA2_ATI").hide();
|
||||||
|
$("#modem_info_alert").html(`
|
||||||
|
<div class="alert alert-danger py-2 mb-0 mt-2" role="alert">
|
||||||
|
<strong>Communication error</strong><br>
|
||||||
|
<small>${error}</small>
|
||||||
|
</div>`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSimInfo(port, timeout) {
|
||||||
|
console.log("Getting SIM info from port " + port);
|
||||||
|
|
||||||
|
$("#loading_ttyAMA2_AT_CCID_").show();
|
||||||
|
$("#sim_info_alert").empty();
|
||||||
|
$("#response_ttyAMA2_AT_CCID_").empty();
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
url: 'launcher.php?type=sara&port=' + port + '&command=' + encodeURIComponent('AT+CCID?') + '&timeout=' + timeout,
|
||||||
|
dataType: 'text',
|
||||||
|
method: 'GET',
|
||||||
|
success: function(response) {
|
||||||
|
console.log("CCID response:", response);
|
||||||
|
$("#loading_ttyAMA2_AT_CCID_").hide();
|
||||||
|
|
||||||
|
// Store raw logs
|
||||||
|
const formattedLogs = response.replace(/\n/g, "<br>");
|
||||||
|
$("#response_ttyAMA2_AT_CCID_").html(formattedLogs);
|
||||||
|
|
||||||
|
// Parse response to extract SIM card number
|
||||||
|
let alertHtml = '';
|
||||||
|
|
||||||
|
// Match CCID number (typically 19-20 digits)
|
||||||
|
const ccidMatch = response.match(/\+CCID:\s*(\d{18,22})/);
|
||||||
|
|
||||||
|
if (response.includes('OK') && ccidMatch) {
|
||||||
|
const simNumber = ccidMatch[1];
|
||||||
|
alertHtml = `
|
||||||
|
<div class="alert alert-success py-2 mb-0 mt-2 d-flex justify-content-between align-items-center" role="alert">
|
||||||
|
<div>
|
||||||
|
<strong>SIM card connected</strong><br>
|
||||||
|
<small>ICCID: ${simNumber}</small>
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-sm btn-outline-secondary" type="button" data-bs-toggle="collapse" data-bs-target="#sim_info_logs" aria-expanded="false" aria-controls="sim_info_logs">
|
||||||
|
<small>+</small>
|
||||||
|
</button>
|
||||||
|
</div>`;
|
||||||
|
} else if (response.includes('ERROR') || response.trim() === '' || !response.includes('OK')) {
|
||||||
|
alertHtml = `
|
||||||
|
<div class="alert alert-danger py-2 mb-0 mt-2 d-flex justify-content-between align-items-center" role="alert">
|
||||||
|
<div>
|
||||||
|
<strong>SIM card not detected</strong><br>
|
||||||
|
<small>No SIM card or read error</small>
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-sm btn-outline-secondary" type="button" data-bs-toggle="collapse" data-bs-target="#sim_info_logs" aria-expanded="false" aria-controls="sim_info_logs">
|
||||||
|
<small>+</small>
|
||||||
|
</button>
|
||||||
|
</div>`;
|
||||||
|
} else {
|
||||||
|
alertHtml = `
|
||||||
|
<div class="alert alert-warning py-2 mb-0 mt-2 d-flex justify-content-between align-items-center" role="alert">
|
||||||
|
<div>
|
||||||
|
<strong>SIM card detected</strong><br>
|
||||||
|
<small>Unable to read ICCID</small>
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-sm btn-outline-secondary" type="button" data-bs-toggle="collapse" data-bs-target="#sim_info_logs" aria-expanded="false" aria-controls="sim_info_logs">
|
||||||
|
<small>+</small>
|
||||||
|
</button>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
$("#sim_info_alert").html(alertHtml);
|
||||||
|
},
|
||||||
|
error: function(xhr, status, error) {
|
||||||
|
console.error('AJAX request failed:', status, error);
|
||||||
|
$("#loading_ttyAMA2_AT_CCID_").hide();
|
||||||
|
$("#sim_info_alert").html(`
|
||||||
|
<div class="alert alert-danger py-2 mb-0 mt-2" role="alert">
|
||||||
|
<strong>Communication error</strong><br>
|
||||||
|
<small>${error}</small>
|
||||||
|
</div>`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function getImsiInfo(port, timeout) {
|
||||||
|
console.log("Getting IMSI from port " + port);
|
||||||
|
|
||||||
|
$("#loading_ttyAMA2_AT_CIMI").show();
|
||||||
|
$("#imsi_info_alert").empty();
|
||||||
|
$("#response_ttyAMA2_AT_CIMI").empty();
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
url: 'launcher.php?type=sara&port=' + port + '&command=' + encodeURIComponent('AT+CIMI') + '&timeout=' + timeout,
|
||||||
|
dataType: 'text',
|
||||||
|
method: 'GET',
|
||||||
|
success: function(response) {
|
||||||
|
console.log("IMSI response:", response);
|
||||||
|
$("#loading_ttyAMA2_AT_CIMI").hide();
|
||||||
|
|
||||||
|
// Store raw logs
|
||||||
|
const formattedLogs = response.replace(/\n/g, "<br>");
|
||||||
|
$("#response_ttyAMA2_AT_CIMI").html(formattedLogs);
|
||||||
|
|
||||||
|
// Parse response to extract IMSI (15-digit number)
|
||||||
|
let alertHtml = '';
|
||||||
|
const imsiMatch = response.match(/(\d{15})/);
|
||||||
|
|
||||||
|
if (response.includes('OK') && imsiMatch) {
|
||||||
|
const imsiNumber = imsiMatch[1];
|
||||||
|
alertHtml = `
|
||||||
|
<div class="alert alert-success py-2 mb-0 mt-2 d-flex justify-content-between align-items-center" role="alert">
|
||||||
|
<div>
|
||||||
|
<strong>IMSI read successfully</strong><br>
|
||||||
|
<small>IMSI: ${imsiNumber}</small>
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-sm btn-outline-secondary" type="button" data-bs-toggle="collapse" data-bs-target="#imsi_info_logs" aria-expanded="false" aria-controls="imsi_info_logs">
|
||||||
|
<small>+</small>
|
||||||
|
</button>
|
||||||
|
</div>`;
|
||||||
|
} else if (response.includes('ERROR') || response.trim() === '' || !response.includes('OK')) {
|
||||||
|
alertHtml = `
|
||||||
|
<div class="alert alert-danger py-2 mb-0 mt-2 d-flex justify-content-between align-items-center" role="alert">
|
||||||
|
<div>
|
||||||
|
<strong>IMSI not available</strong><br>
|
||||||
|
<small>No SIM card or read error</small>
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-sm btn-outline-secondary" type="button" data-bs-toggle="collapse" data-bs-target="#imsi_info_logs" aria-expanded="false" aria-controls="imsi_info_logs">
|
||||||
|
<small>+</small>
|
||||||
|
</button>
|
||||||
|
</div>`;
|
||||||
|
} else {
|
||||||
|
alertHtml = `
|
||||||
|
<div class="alert alert-warning py-2 mb-0 mt-2 d-flex justify-content-between align-items-center" role="alert">
|
||||||
|
<div>
|
||||||
|
<strong>SIM card detected</strong><br>
|
||||||
|
<small>Unable to read IMSI</small>
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-sm btn-outline-secondary" type="button" data-bs-toggle="collapse" data-bs-target="#imsi_info_logs" aria-expanded="false" aria-controls="imsi_info_logs">
|
||||||
|
<small>+</small>
|
||||||
|
</button>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
$("#imsi_info_alert").html(alertHtml);
|
||||||
|
},
|
||||||
|
error: function(xhr, status, error) {
|
||||||
|
console.error('AJAX request failed:', status, error);
|
||||||
|
$("#loading_ttyAMA2_AT_CIMI").hide();
|
||||||
|
$("#imsi_info_alert").html(`
|
||||||
|
<div class="alert alert-danger py-2 mb-0 mt-2" role="alert">
|
||||||
|
<strong>Communication error</strong><br>
|
||||||
|
<small>${error}</small>
|
||||||
|
</div>`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cache for operators data
|
||||||
|
let operatorsData = null;
|
||||||
|
|
||||||
|
function loadOperatorsData() {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
if (operatorsData) {
|
||||||
|
resolve(operatorsData);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$.ajax({
|
||||||
|
url: 'assets/data/operators.json',
|
||||||
|
dataType: 'json',
|
||||||
|
method: 'GET',
|
||||||
|
success: function(data) {
|
||||||
|
operatorsData = data;
|
||||||
|
resolve(data);
|
||||||
|
},
|
||||||
|
error: function(xhr, status, error) {
|
||||||
|
console.error('Failed to load operators data:', error);
|
||||||
|
reject(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function getNetworkInfo(port, timeout) {
|
||||||
|
console.log("Getting network info from port " + port);
|
||||||
|
|
||||||
|
$("#loading_ttyAMA2_AT_COPS_").show();
|
||||||
|
$("#network_info_alert").empty();
|
||||||
|
$("#response_ttyAMA2_AT_COPS_").empty();
|
||||||
|
|
||||||
|
// Load operators data first, then query modem
|
||||||
|
loadOperatorsData().then(function(opData) {
|
||||||
|
$.ajax({
|
||||||
|
url: 'launcher.php?type=sara&port=' + port + '&command=' + encodeURIComponent('AT+COPS?') + '&timeout=' + timeout,
|
||||||
|
dataType: 'text',
|
||||||
|
method: 'GET',
|
||||||
|
success: function(response) {
|
||||||
|
console.log("COPS response:", response);
|
||||||
|
$("#loading_ttyAMA2_AT_COPS_").hide();
|
||||||
|
|
||||||
|
// Store raw logs
|
||||||
|
const formattedLogs = response.replace(/\n/g, "<br>");
|
||||||
|
$("#response_ttyAMA2_AT_COPS_").html(formattedLogs);
|
||||||
|
|
||||||
|
// Parse response: +COPS: <mode>[,<format>,<oper>[,<AcT>]]
|
||||||
|
let alertHtml = '';
|
||||||
|
const copsMatch = response.match(/\+COPS:\s*(\d+)(?:,(\d+),"?([^",]+)"?,(\d+))?/);
|
||||||
|
|
||||||
|
if (response.includes('OK') && copsMatch) {
|
||||||
|
const mode = copsMatch[1];
|
||||||
|
const format = copsMatch[2];
|
||||||
|
const oper = copsMatch[3];
|
||||||
|
const act = copsMatch[4];
|
||||||
|
|
||||||
|
// Get mode description
|
||||||
|
const modeDesc = opData.modes[mode] || 'Unknown';
|
||||||
|
|
||||||
|
// Get operator name
|
||||||
|
let operatorName = oper || 'Not registered';
|
||||||
|
let operatorCountry = '';
|
||||||
|
if (oper && opData.operators[oper]) {
|
||||||
|
operatorName = opData.operators[oper].name;
|
||||||
|
operatorCountry = opData.operators[oper].country;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get access technology
|
||||||
|
const actDesc = act ? (opData.accessTechnology[act] || 'Unknown') : 'N/A';
|
||||||
|
|
||||||
|
if (oper) {
|
||||||
|
alertHtml = `
|
||||||
|
<div class="alert alert-success py-2 mb-0 mt-2 d-flex justify-content-between align-items-center" role="alert">
|
||||||
|
<div>
|
||||||
|
<strong>Connected to network</strong><br>
|
||||||
|
<small>
|
||||||
|
Operator: ${operatorName}${operatorCountry ? ' (' + operatorCountry + ')' : ''}<br>
|
||||||
|
Technology: ${actDesc}<br>
|
||||||
|
Mode: ${modeDesc}
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-sm btn-outline-secondary" type="button" data-bs-toggle="collapse" data-bs-target="#network_info_logs" aria-expanded="false" aria-controls="network_info_logs">
|
||||||
|
<small>+</small>
|
||||||
|
</button>
|
||||||
|
</div>`;
|
||||||
|
} else {
|
||||||
|
alertHtml = `
|
||||||
|
<div class="alert alert-warning py-2 mb-0 mt-2 d-flex justify-content-between align-items-center" role="alert">
|
||||||
|
<div>
|
||||||
|
<strong>Not registered</strong><br>
|
||||||
|
<small>Mode: ${modeDesc}</small>
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-sm btn-outline-secondary" type="button" data-bs-toggle="collapse" data-bs-target="#network_info_logs" aria-expanded="false" aria-controls="network_info_logs">
|
||||||
|
<small>+</small>
|
||||||
|
</button>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
} else if (response.includes('ERROR') || response.trim() === '' || !response.includes('OK')) {
|
||||||
|
alertHtml = `
|
||||||
|
<div class="alert alert-danger py-2 mb-0 mt-2 d-flex justify-content-between align-items-center" role="alert">
|
||||||
|
<div>
|
||||||
|
<strong>Network error</strong><br>
|
||||||
|
<small>Unable to get network info</small>
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-sm btn-outline-secondary" type="button" data-bs-toggle="collapse" data-bs-target="#network_info_logs" aria-expanded="false" aria-controls="network_info_logs">
|
||||||
|
<small>+</small>
|
||||||
|
</button>
|
||||||
|
</div>`;
|
||||||
|
} else {
|
||||||
|
alertHtml = `
|
||||||
|
<div class="alert alert-warning py-2 mb-0 mt-2 d-flex justify-content-between align-items-center" role="alert">
|
||||||
|
<div>
|
||||||
|
<strong>Unknown response</strong><br>
|
||||||
|
<small>Check logs for details</small>
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-sm btn-outline-secondary" type="button" data-bs-toggle="collapse" data-bs-target="#network_info_logs" aria-expanded="false" aria-controls="network_info_logs">
|
||||||
|
<small>+</small>
|
||||||
|
</button>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
$("#network_info_alert").html(alertHtml);
|
||||||
|
},
|
||||||
|
error: function(xhr, status, error) {
|
||||||
|
console.error('AJAX request failed:', status, error);
|
||||||
|
$("#loading_ttyAMA2_AT_COPS_").hide();
|
||||||
|
$("#network_info_alert").html(`
|
||||||
|
<div class="alert alert-danger py-2 mb-0 mt-2" role="alert">
|
||||||
|
<strong>Communication error</strong><br>
|
||||||
|
<small>${error}</small>
|
||||||
|
</div>`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}).catch(function(error) {
|
||||||
|
$("#loading_ttyAMA2_AT_COPS_").hide();
|
||||||
|
$("#network_info_alert").html(`
|
||||||
|
<div class="alert alert-danger py-2 mb-0 mt-2" role="alert">
|
||||||
|
<strong>Configuration error</strong><br>
|
||||||
|
<small>Failed to load operators data</small>
|
||||||
|
</div>`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSignalInfo(port, timeout) {
|
||||||
|
console.log("Getting signal info from port " + port);
|
||||||
|
|
||||||
|
$("#loading_ttyAMA2_AT_CSQ").show();
|
||||||
|
$("#signal_info_alert").empty();
|
||||||
|
$("#response_ttyAMA2_AT_CSQ").empty();
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
url: 'launcher.php?type=sara&port=' + port + '&command=' + encodeURIComponent('AT+CSQ') + '&timeout=' + timeout,
|
||||||
|
dataType: 'text',
|
||||||
|
method: 'GET',
|
||||||
|
success: function(response) {
|
||||||
|
console.log("CSQ response:", response);
|
||||||
|
$("#loading_ttyAMA2_AT_CSQ").hide();
|
||||||
|
|
||||||
|
// Store raw logs
|
||||||
|
const formattedLogs = response.replace(/\n/g, "<br>");
|
||||||
|
$("#response_ttyAMA2_AT_CSQ").html(formattedLogs);
|
||||||
|
|
||||||
|
// Parse response: +CSQ: <signal_power>,<qual>
|
||||||
|
let alertHtml = '';
|
||||||
|
const csqMatch = response.match(/\+CSQ:\s*(\d+),(\d+)/);
|
||||||
|
|
||||||
|
if (response.includes('OK') && csqMatch) {
|
||||||
|
const signalPower = parseInt(csqMatch[1]);
|
||||||
|
const qual = parseInt(csqMatch[2]);
|
||||||
|
|
||||||
|
// Determine signal quality and color (matching Python thresholds)
|
||||||
|
let signalDesc, signalColor, signalIcon, alertClass;
|
||||||
|
|
||||||
|
if (signalPower === 99) {
|
||||||
|
signalDesc = 'No signal';
|
||||||
|
signalColor = '#333333';
|
||||||
|
signalIcon = '⚫';
|
||||||
|
alertClass = 'alert-dark';
|
||||||
|
} else if (signalPower === 0) {
|
||||||
|
signalDesc = 'Very poor';
|
||||||
|
signalColor = '#dc3545';
|
||||||
|
signalIcon = '🔴';
|
||||||
|
alertClass = 'alert-danger';
|
||||||
|
} else if (signalPower <= 24) {
|
||||||
|
signalDesc = 'Poor';
|
||||||
|
signalColor = '#fd7e14';
|
||||||
|
signalIcon = '🟠';
|
||||||
|
alertClass = 'alert-warning';
|
||||||
|
} else if (signalPower <= 26) {
|
||||||
|
signalDesc = 'Good';
|
||||||
|
signalColor = '#ffc107';
|
||||||
|
signalIcon = '🟡';
|
||||||
|
alertClass = 'alert-warning';
|
||||||
|
} else if (signalPower <= 28) {
|
||||||
|
signalDesc = 'Very good';
|
||||||
|
signalColor = '#198754';
|
||||||
|
signalIcon = '🟢';
|
||||||
|
alertClass = 'alert-success';
|
||||||
|
} else if (signalPower <= 30) {
|
||||||
|
signalDesc = 'Excellent';
|
||||||
|
signalColor = '#0d6efd';
|
||||||
|
signalIcon = '🔵';
|
||||||
|
alertClass = 'alert-primary';
|
||||||
|
} else {
|
||||||
|
signalDesc = 'Maximum';
|
||||||
|
signalColor = '#6f42c1';
|
||||||
|
signalIcon = '🟣';
|
||||||
|
alertClass = 'alert-primary';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate approximate dBm (for RSSI: -113 + 2*signalPower)
|
||||||
|
let rssiDbm = signalPower !== 99 ? (-113 + 2 * signalPower) + ' dBm' : 'N/A';
|
||||||
|
|
||||||
|
// Signal bars visualization (1-5 bars based on signal power, matching thresholds)
|
||||||
|
let bars = 0;
|
||||||
|
if (signalPower !== 99) {
|
||||||
|
if (signalPower >= 29) bars = 5; // Excellent / Very Strong
|
||||||
|
else if (signalPower >= 27) bars = 4; // Very good
|
||||||
|
else if (signalPower >= 25) bars = 3; // Good
|
||||||
|
else if (signalPower >= 10) bars = 2; // Poor (mid)
|
||||||
|
else if (signalPower >= 1) bars = 1; // Poor (low)
|
||||||
|
}
|
||||||
|
|
||||||
|
const barsHtml = `
|
||||||
|
<span style="font-size: 1.2em; letter-spacing: 2px;">
|
||||||
|
${[1,2,3,4,5].map(i =>
|
||||||
|
`<span style="color: ${i <= bars ? signalColor : '#dee2e6'};">▮</span>`
|
||||||
|
).join('')}
|
||||||
|
</span>`;
|
||||||
|
|
||||||
|
alertHtml = `
|
||||||
|
<div class="alert ${alertClass} py-2 mb-0 mt-2 d-flex justify-content-between align-items-center" role="alert">
|
||||||
|
<div>
|
||||||
|
<div class="d-flex align-items-center mb-1">
|
||||||
|
${barsHtml}
|
||||||
|
<span class="ms-2"><strong>${signalDesc}</strong></span>
|
||||||
|
</div>
|
||||||
|
<small>
|
||||||
|
Signal: ${signalPower}/31 (${rssiDbm})<br>
|
||||||
|
Quality: ${qual}/7
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-sm btn-outline-secondary" type="button" data-bs-toggle="collapse" data-bs-target="#signal_info_logs" aria-expanded="false" aria-controls="signal_info_logs">
|
||||||
|
<small>+</small>
|
||||||
|
</button>
|
||||||
|
</div>`;
|
||||||
|
} else if (response.includes('ERROR') || response.trim() === '' || !response.includes('OK')) {
|
||||||
|
alertHtml = `
|
||||||
|
<div class="alert alert-danger py-2 mb-0 mt-2 d-flex justify-content-between align-items-center" role="alert">
|
||||||
|
<div>
|
||||||
|
<strong>Signal error</strong><br>
|
||||||
|
<small>Unable to get signal info</small>
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-sm btn-outline-secondary" type="button" data-bs-toggle="collapse" data-bs-target="#signal_info_logs" aria-expanded="false" aria-controls="signal_info_logs">
|
||||||
|
<small>+</small>
|
||||||
|
</button>
|
||||||
|
</div>`;
|
||||||
|
} else {
|
||||||
|
alertHtml = `
|
||||||
|
<div class="alert alert-warning py-2 mb-0 mt-2 d-flex justify-content-between align-items-center" role="alert">
|
||||||
|
<div>
|
||||||
|
<strong>Unknown response</strong><br>
|
||||||
|
<small>Check logs for details</small>
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-sm btn-outline-secondary" type="button" data-bs-toggle="collapse" data-bs-target="#signal_info_logs" aria-expanded="false" aria-controls="signal_info_logs">
|
||||||
|
<small>+</small>
|
||||||
|
</button>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
$("#signal_info_alert").html(alertHtml);
|
||||||
|
},
|
||||||
|
error: function(xhr, status, error) {
|
||||||
|
console.error('AJAX request failed:', status, error);
|
||||||
|
$("#loading_ttyAMA2_AT_CSQ").hide();
|
||||||
|
$("#signal_info_alert").html(`
|
||||||
|
<div class="alert alert-danger py-2 mb-0 mt-2" role="alert">
|
||||||
|
<strong>Communication error</strong><br>
|
||||||
|
<small>${error}</small>
|
||||||
|
</div>`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function getData_saraR4(port, command, timeout){
|
function getData_saraR4(port, command, timeout){
|
||||||
console.log("Data from SaraR4");
|
console.log("Data from SaraR4");
|
||||||
console.log("Port: " + port );
|
console.log("Port: " + port );
|
||||||
@@ -453,6 +1034,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 +1141,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 +1276,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 +1296,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();
|
||||||
@@ -864,6 +1491,8 @@ function update_modem_configMode(param, checked){
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// Self test functions are now in assets/js/selftest.js
|
||||||
|
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
179
html/screen.html
Normal file
179
html/screen.html
Normal file
@@ -0,0 +1,179 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Screen Control</title>
|
||||||
|
<link rel="stylesheet" href="assets/css/bootstrap.min.css">
|
||||||
|
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
#sidebar a.nav-link {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
#sidebar a.nav-link:hover {
|
||||||
|
background-color: rgba(0, 0, 0, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
#sidebar a.nav-link svg {
|
||||||
|
margin-right: 8px;
|
||||||
|
/* Add spacing between icons and text */
|
||||||
|
}
|
||||||
|
|
||||||
|
#sidebar {
|
||||||
|
transition: transform 0.3s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.offcanvas-backdrop {
|
||||||
|
z-index: 1040;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<!-- Topbar -->
|
||||||
|
<span id="topbar"></span>
|
||||||
|
|
||||||
|
<!-- Sidebar Offcanvas for Mobile -->
|
||||||
|
<div class="offcanvas offcanvas-start text-white bg-dark" tabindex="-1" id="sidebarOffcanvas"
|
||||||
|
aria-labelledby="sidebarOffcanvasLabel">
|
||||||
|
<div class="offcanvas-header">
|
||||||
|
<h5 class="offcanvas-title" id="sidebarOffcanvasLabel">NebuleAir</h5>
|
||||||
|
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="offcanvas"
|
||||||
|
aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
<div class="offcanvas-body" id="sidebar_mobile">
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="container-fluid mt-5">
|
||||||
|
<div class="row">
|
||||||
|
<aside class="col-md-2 col-lg-1 d-none d-md-block vh-100 position-fixed bg-dark text-white" id="sidebar">
|
||||||
|
</aside>
|
||||||
|
<!-- Main content -->
|
||||||
|
<main class="col-md-9 ms-sm-auto col-lg-10 offset-md-3 offset-lg-2 px-md-4">
|
||||||
|
<h1 class="mt-4" data-i18n="screen.title">Contrôle de l'écran</h1>
|
||||||
|
<p data-i18n="screen.description">Gérer l'affichage sur l'écran HDMI.</p>
|
||||||
|
|
||||||
|
<div class="row mt-4">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="card-title">Actions</h5>
|
||||||
|
<p class="card-text">Démarrer ou arrêter l'application d'affichage sur l'écran HDMI.</p>
|
||||||
|
<button id="startBtn" class="btn btn-success m-2" onclick="controlScreen('start')">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor"
|
||||||
|
class="bi bi-play-fill" viewBox="0 0 16 16">
|
||||||
|
<path
|
||||||
|
d="m11.596 8.697-6.363 3.692c-.54.313-1.233-.066-1.233-.697V4.308c0-.63.692-1.01 1.233-.696l6.363 3.692a.802.802 0 0 1 0 1.393z" />
|
||||||
|
</svg>
|
||||||
|
Démarrer
|
||||||
|
</button>
|
||||||
|
<button id="stopBtn" class="btn btn-danger m-2" onclick="controlScreen('stop')">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor"
|
||||||
|
class="bi bi-stop-fill" viewBox="0 0 16 16">
|
||||||
|
<path
|
||||||
|
d="M5 3.5h6A1.5 1.5 0 0 1 12.5 5v6a1.5 1.5 0 0 1-1.5 1.5H5A1.5 1.5 0 0 1 3.5 11V5A1.5 1.5 0 0 1 5 3.5z" />
|
||||||
|
</svg>
|
||||||
|
Arrêter
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="status-message" class="mt-3 col-md-6"></div>
|
||||||
|
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- JAVASCRIPT -->
|
||||||
|
|
||||||
|
<!-- Link Ajax locally -->
|
||||||
|
<script src="assets/jquery/jquery-3.7.1.min.js"></script>
|
||||||
|
<!-- Link Bootstrap JS and Popper.js locally -->
|
||||||
|
<script src="assets/js/bootstrap.bundle.js"></script>
|
||||||
|
<!-- i18n translation system -->
|
||||||
|
<script src="assets/js/i18n.js"></script>
|
||||||
|
<script src="assets/js/topbar-logo.js"></script>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
|
const elementsToLoad = [
|
||||||
|
{ id: 'topbar', file: 'topbar.html' },
|
||||||
|
{ id: 'sidebar', file: 'sidebar.html' },
|
||||||
|
{ id: 'sidebar_mobile', file: 'sidebar.html' }
|
||||||
|
];
|
||||||
|
|
||||||
|
elementsToLoad.forEach(({ id, file }) => {
|
||||||
|
fetch(file)
|
||||||
|
.then(response => response.text())
|
||||||
|
.then(data => {
|
||||||
|
const element = document.getElementById(id);
|
||||||
|
if (element) {
|
||||||
|
element.innerHTML = data;
|
||||||
|
// Apply translations after loading dynamic content
|
||||||
|
if (window.i18n && typeof window.i18n.applyTranslations === 'function') {
|
||||||
|
window.i18n.applyTranslations();
|
||||||
|
}
|
||||||
|
// Ensure the screen tab is visible here as well
|
||||||
|
if (id.includes('sidebar')) {
|
||||||
|
setTimeout(() => {
|
||||||
|
const navScreenElements = element.querySelectorAll('.nav-screen-item');
|
||||||
|
navScreenElements.forEach(el => el.style.display = 'flex');
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => console.error(`Error loading ${file}:`, error));
|
||||||
|
});
|
||||||
|
|
||||||
|
// Translation fallback for now if keys are missing
|
||||||
|
setTimeout(() => {
|
||||||
|
if (document.querySelector('[data-i18n="screen.title"]').innerText === "screen.title") {
|
||||||
|
document.querySelector('[data-i18n="screen.title"]').innerText = "Contrôle de l'écran";
|
||||||
|
}
|
||||||
|
if (document.querySelector('[data-i18n="screen.description"]').innerText === "screen.description") {
|
||||||
|
document.querySelector('[data-i18n="screen.description"]').innerText = "Gérer l'affichage sur l'écran HDMI.";
|
||||||
|
}
|
||||||
|
}, 500);
|
||||||
|
});
|
||||||
|
|
||||||
|
function controlScreen(action) {
|
||||||
|
console.log("Sending Screen Control Action:", action);
|
||||||
|
$.ajax({
|
||||||
|
url: 'launcher.php?type=screen_control&action=' + action,
|
||||||
|
dataType: 'text',
|
||||||
|
method: 'GET',
|
||||||
|
success: function (response) {
|
||||||
|
console.log("Server Response:", response);
|
||||||
|
if (action == 'start') {
|
||||||
|
$('#startBtn').removeClass('btn-success').addClass('btn-secondary').prop('disabled', true);
|
||||||
|
$('#stopBtn').removeClass('btn-secondary').addClass('btn-danger').prop('disabled', false);
|
||||||
|
$('#status-message').html('<div class="alert alert-success">L\'écran a été démarré. Réponse: ' + response + '</div>');
|
||||||
|
} else {
|
||||||
|
$('#startBtn').removeClass('btn-secondary').addClass('btn-success').prop('disabled', false);
|
||||||
|
$('#stopBtn').removeClass('btn-danger').addClass('btn-secondary').prop('disabled', true);
|
||||||
|
$('#status-message').html('<div class="alert alert-warning">L\'écran a été arrêté. Réponse: ' + response + '</div>');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: function (xhr, status, error) {
|
||||||
|
console.error("AJAX Error:", status, error);
|
||||||
|
$('#status-message').html('<div class="alert alert-danger">Erreur: ' + error + '</div>');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
134
html/selftest-modal.html
Normal file
134
html/selftest-modal.html
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
<!-- Self Test Modal -->
|
||||||
|
<div class="modal fade" id="selfTestModal" tabindex="-1" aria-labelledby="selfTestModalLabel" aria-hidden="true" data-bs-backdrop="static" data-bs-keyboard="false">
|
||||||
|
<div class="modal-dialog modal-dialog-centered">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title" id="selfTestModalLabel">Modem Self Test</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close" id="selfTestCloseBtn" disabled></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div id="selftest_status" class="mb-3">
|
||||||
|
<div class="d-flex align-items-center text-muted">
|
||||||
|
<div class="spinner-border spinner-border-sm me-2" role="status"></div>
|
||||||
|
<span>Preparing test...</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="list-group" id="selftest_results">
|
||||||
|
<!-- Dynamic sensor test entries will be added here -->
|
||||||
|
<div id="sensor_tests_container"></div>
|
||||||
|
|
||||||
|
<!-- Separator for communication tests -->
|
||||||
|
<div id="comm_tests_separator" class="list-group-item bg-light text-center py-1" style="display:none;">
|
||||||
|
<small class="text-muted fw-bold">COMMUNICATION</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Info: WiFi Status -->
|
||||||
|
<div class="list-group-item d-flex justify-content-between align-items-center" id="test_wifi">
|
||||||
|
<div>
|
||||||
|
<strong>WiFi / Network</strong>
|
||||||
|
<div class="small text-muted" id="test_wifi_detail">Waiting...</div>
|
||||||
|
</div>
|
||||||
|
<span id="test_wifi_status" class="badge bg-secondary">Pending</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Test: Modem Connection -->
|
||||||
|
<div class="list-group-item d-flex justify-content-between align-items-center" id="test_modem">
|
||||||
|
<div>
|
||||||
|
<strong>Modem Connection</strong>
|
||||||
|
<div class="small text-muted" id="test_modem_detail">Waiting...</div>
|
||||||
|
</div>
|
||||||
|
<span id="test_modem_status" class="badge bg-secondary">Pending</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Test: SIM Card -->
|
||||||
|
<div class="list-group-item d-flex justify-content-between align-items-center" id="test_sim">
|
||||||
|
<div>
|
||||||
|
<strong>SIM Card</strong>
|
||||||
|
<div class="small text-muted" id="test_sim_detail">Waiting...</div>
|
||||||
|
</div>
|
||||||
|
<span id="test_sim_status" class="badge bg-secondary">Pending</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Test: Signal Strength -->
|
||||||
|
<div class="list-group-item d-flex justify-content-between align-items-center" id="test_signal">
|
||||||
|
<div>
|
||||||
|
<strong>Signal Strength</strong>
|
||||||
|
<div class="small text-muted" id="test_signal_detail">Waiting...</div>
|
||||||
|
</div>
|
||||||
|
<span id="test_signal_status" class="badge bg-secondary">Pending</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Test: Network Connection -->
|
||||||
|
<div class="list-group-item d-flex justify-content-between align-items-center" id="test_network">
|
||||||
|
<div>
|
||||||
|
<strong>Network Connection</strong>
|
||||||
|
<div class="small text-muted" id="test_network_detail">Waiting...</div>
|
||||||
|
</div>
|
||||||
|
<span id="test_network_status" class="badge bg-secondary">Pending</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Logs section -->
|
||||||
|
<div class="mt-3">
|
||||||
|
<button class="btn btn-sm btn-outline-secondary" type="button" data-bs-toggle="collapse" data-bs-target="#selftest_logs_collapse">
|
||||||
|
Show detailed logs
|
||||||
|
</button>
|
||||||
|
<div class="collapse mt-2" id="selftest_logs_collapse">
|
||||||
|
<div class="card card-body bg-dark text-light" style="max-height: 250px; overflow-y: auto; font-family: monospace; font-size: 0.75rem;">
|
||||||
|
<pre id="selftest_logs" style="margin: 0; white-space: pre-wrap; word-wrap: break-word;"></pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<div id="selftest_summary" class="me-auto"></div>
|
||||||
|
<button type="button" class="btn btn-primary" id="selfTestCopyBtn" onclick="openShareReportModal()" disabled>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-share me-1" viewBox="0 0 16 16">
|
||||||
|
<path d="M13.5 1a1.5 1.5 0 1 0 0 3 1.5 1.5 0 0 0 0-3zM11 2.5a2.5 2.5 0 1 1 .603 1.628l-6.718 3.12a2.499 2.499 0 0 1 0 1.504l6.718 3.12a2.5 2.5 0 1 1-.488.876l-6.718-3.12a2.5 2.5 0 1 1 0-3.256l6.718-3.12A2.5 2.5 0 0 1 11 2.5zm-8.5 4a1.5 1.5 0 1 0 0 3 1.5 1.5 0 0 0 0-3zm11 5.5a1.5 1.5 0 1 0 0 3 1.5 1.5 0 0 0 0-3z"/>
|
||||||
|
</svg>
|
||||||
|
Share Report
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal" id="selfTestDoneBtn" disabled>Close</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Share Report Modal -->
|
||||||
|
<div class="modal fade" id="shareReportModal" tabindex="-1" aria-labelledby="shareReportModalLabel" aria-hidden="true">
|
||||||
|
<div class="modal-dialog modal-lg modal-dialog-centered">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header bg-primary text-white">
|
||||||
|
<h5 class="modal-title" id="shareReportModalLabel">Share Diagnostic Report</h5>
|
||||||
|
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="alert alert-info">
|
||||||
|
<strong>Need help?</strong> You can send this diagnostic report to our support team at
|
||||||
|
<a href="mailto:contact@aircarto.fr?subject=NebuleAir%20Diagnostic%20Report" class="alert-link">contact@aircarto.fr</a>
|
||||||
|
<br><small>Select all the text below (Ctrl+A) and copy it (Ctrl+C), or use the Download button.</small>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<textarea id="shareReportText" class="form-control font-monospace" rows="15" readonly style="font-size: 0.75rem; background-color: #1e1e1e; color: #d4d4d4;"></textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-success" onclick="downloadReport()">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-download me-1" viewBox="0 0 16 16">
|
||||||
|
<path d="M.5 9.9a.5.5 0 0 1 .5.5v2.5a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-2.5a.5.5 0 0 1 1 0v2.5a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2v-2.5a.5.5 0 0 1 .5-.5z"/>
|
||||||
|
<path d="M7.646 11.854a.5.5 0 0 0 .708 0l3-3a.5.5 0 0 0-.708-.708L8.5 10.293V1.5a.5.5 0 0 0-1 0v8.793L5.354 8.146a.5.5 0 1 0-.708.708l3 3z"/>
|
||||||
|
</svg>
|
||||||
|
Download (.txt)
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-outline-primary" onclick="selectAllReportText()">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-cursor-text me-1" viewBox="0 0 16 16">
|
||||||
|
<path d="M5 2a.5.5 0 0 1 .5-.5c.862 0 1.573.287 2.06.566.174.099.321.198.44.286.119-.088.266-.187.44-.286A4.165 4.165 0 0 1 10.5 1.5a.5.5 0 0 1 0 1c-.638 0-1.177.213-1.564.434a3.49 3.49 0 0 0-.436.294V7.5H9a.5.5 0 0 1 0 1h-.5v4.272c.1.08.248.187.436.294.387.221.926.434 1.564.434a.5.5 0 0 1 0 1 4.165 4.165 0 0 1-2.06-.566A4.561 4.561 0 0 1 8 13.65a4.561 4.561 0 0 1-.44.285 4.165 4.165 0 0 1-2.06.566.5.5 0 0 1 0-1c.638 0 1.177-.213 1.564-.434.188-.107.335-.214.436-.294V8.5H7a.5.5 0 0 1 0-1h.5V3.228a3.49 3.49 0 0 0-.436-.294A3.166 3.166 0 0 0 5.5 2.5.5.5 0 0 1 5 2z"/>
|
||||||
|
</svg>
|
||||||
|
Select All
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
@@ -9,31 +10,39 @@
|
|||||||
body {
|
body {
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
#sidebar a.nav-link {
|
#sidebar a.nav-link {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
#sidebar a.nav-link:hover {
|
#sidebar a.nav-link:hover {
|
||||||
background-color: rgba(0, 0, 0, 0.5);
|
background-color: rgba(0, 0, 0, 0.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
#sidebar a.nav-link svg {
|
#sidebar a.nav-link svg {
|
||||||
margin-right: 8px; /* Add spacing between icons and text */
|
margin-right: 8px;
|
||||||
|
/* Add spacing between icons and text */
|
||||||
}
|
}
|
||||||
|
|
||||||
#sidebar {
|
#sidebar {
|
||||||
transition: transform 0.3s ease-in-out;
|
transition: transform 0.3s ease-in-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
.offcanvas-backdrop {
|
.offcanvas-backdrop {
|
||||||
z-index: 1040;
|
z-index: 1040;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<!-- Topbar -->
|
<!-- Topbar -->
|
||||||
<span id="topbar"></span>
|
<span id="topbar"></span>
|
||||||
|
|
||||||
<!-- Sidebar Offcanvas for Mobile -->
|
<!-- Sidebar Offcanvas for Mobile -->
|
||||||
<div class="offcanvas offcanvas-start text-white bg-dark" tabindex="-1" id="sidebarOffcanvas" aria-labelledby="sidebarOffcanvasLabel">
|
<div class="offcanvas offcanvas-start text-white bg-dark" tabindex="-1" id="sidebarOffcanvas"
|
||||||
|
aria-labelledby="sidebarOffcanvasLabel">
|
||||||
<div class="offcanvas-header">
|
<div class="offcanvas-header">
|
||||||
<h5 class="offcanvas-title" id="sidebarOffcanvasLabel">NebuleAir</h5>
|
<h5 class="offcanvas-title" id="sidebarOffcanvasLabel">NebuleAir</h5>
|
||||||
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="offcanvas" aria-label="Close"></button>
|
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="offcanvas" aria-label="Close"></button>
|
||||||
@@ -45,138 +54,222 @@
|
|||||||
|
|
||||||
<div class="container-fluid mt-5">
|
<div class="container-fluid mt-5">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<aside class="col-md-2 col-lg-1 d-none d-md-block vh-100 position-fixed bg-dark text-white" id="sidebar">
|
<aside class="col-md-2 col-lg-1 d-none d-md-block vh-100 position-fixed bg-dark text-white" id="sidebar">
|
||||||
</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
|
||||||
est automatique mais vous pouvez ici vous assurer de leur bon fonctionnement.
|
de mesurer certaines variables environnementales. La mesure
|
||||||
|
est automatique mais vous pouvez ici vous assurer de leur bon fonctionnement.
|
||||||
</p>
|
</p>
|
||||||
|
<button class="btn btn-success mb-3 btn_selfTest" onclick="runSelfTest()">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-check2-circle me-1" viewBox="0 0 16 16">
|
||||||
|
<path d="M2.5 8a5.5 5.5 0 1 1 11 0 5.5 5.5 0 0 1-11 0z"/>
|
||||||
|
<path d="M15.354 3.354a.5.5 0 0 0-.708-.708L8 9.293 5.354 6.646a.5.5 0 1 0-.708.708l3 3a.5.5 0 0 0 .708 0l7-7z"/>
|
||||||
|
</svg>
|
||||||
|
Run Self Test
|
||||||
|
</button>
|
||||||
|
|
||||||
<div class="row mb-3" id="card-container"></div>
|
<div class="row mb-3" id="card-container"></div>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- JAVASCRIPT -->
|
<!-- JAVASCRIPT -->
|
||||||
|
|
||||||
<!-- Link Ajax locally -->
|
<!-- Link Ajax locally -->
|
||||||
<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 src="assets/js/topbar-logo.js"></script>
|
||||||
|
<script src="assets/js/selftest.js"></script>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
document.addEventListener('DOMContentLoaded', function () {
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
const elementsToLoad = [
|
const elementsToLoad = [
|
||||||
{ id: 'topbar', file: 'topbar.html' },
|
{ id: 'topbar', file: 'topbar.html' },
|
||||||
{ id: 'sidebar', file: 'sidebar.html' },
|
{ id: 'sidebar', file: 'sidebar.html' },
|
||||||
{ id: 'sidebar_mobile', file: 'sidebar.html' }
|
{ id: 'sidebar_mobile', file: 'sidebar.html' }
|
||||||
];
|
];
|
||||||
|
|
||||||
elementsToLoad.forEach(({ id, file }) => {
|
elementsToLoad.forEach(({ id, file }) => {
|
||||||
fetch(file)
|
fetch(file)
|
||||||
.then(response => response.text())
|
.then(response => response.text())
|
||||||
.then(data => {
|
.then(data => {
|
||||||
const element = document.getElementById(id);
|
const element = document.getElementById(id);
|
||||||
if (element) {
|
if (element) {
|
||||||
element.innerHTML = data;
|
element.innerHTML = data;
|
||||||
}
|
// Apply translations after loading dynamic content
|
||||||
})
|
if (window.i18n && typeof window.i18n.applyTranslations === 'function') {
|
||||||
.catch(error => console.error(`Error loading ${file}:`, error));
|
window.i18n.applyTranslations();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => console.error(`Error loading ${file}:`, error));
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
|
||||||
function getNPM_values(port){
|
function getNPM_values(port) {
|
||||||
console.log("Data from NPM (port "+port+"):");
|
console.log("Data from NPM (port " + port + "):");
|
||||||
$("#loading_"+port).show();
|
$("#loading_" + port).show();
|
||||||
|
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: 'launcher.php?type=npm&port='+port,
|
url: 'launcher.php?type=npm',
|
||||||
dataType: 'json', // Specify that you expect a JSON response
|
dataType: 'json',
|
||||||
method: 'GET', // Use GET or POST depending on your needs
|
method: 'GET',
|
||||||
success: function(response) {
|
success: function (response) {
|
||||||
console.log(response);
|
console.log(response);
|
||||||
const tableBody = document.getElementById("data-table-body_"+port);
|
const tableBody = document.getElementById("data-table-body_" + port);
|
||||||
tableBody.innerHTML = "";
|
tableBody.innerHTML = "";
|
||||||
|
|
||||||
$("#loading_"+port).hide();
|
$("#loading_" + port).hide();
|
||||||
// Create an array of the desired keys
|
|
||||||
const keysToShow = ["PM1", "PM25", "PM10"];
|
|
||||||
// Error messages mapping
|
|
||||||
const errorMessages = {
|
|
||||||
"notReady": "Sensor is not ready",
|
|
||||||
"fanError": "Fan malfunction detected",
|
|
||||||
"laserError": "Laser malfunction detected",
|
|
||||||
"heatError": "Heating system error",
|
|
||||||
"t_rhError": "Temperature/Humidity sensor error",
|
|
||||||
"memoryError": "Memory failure detected",
|
|
||||||
"degradedState": "Sensor in degraded state"
|
|
||||||
};
|
|
||||||
// Add only the specified elements to the table
|
|
||||||
keysToShow.forEach(key => {
|
|
||||||
if (response[key] !== undefined) { // Check if the key exists in the response
|
|
||||||
const value = response[key];
|
|
||||||
$("#data-table-body_"+port).append(`
|
|
||||||
<tr>
|
|
||||||
<td>${key}</td>
|
|
||||||
<td>${value} µg/m³</td>
|
|
||||||
</tr>
|
|
||||||
`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Check for errors and add them to the table
|
// PM values
|
||||||
Object.keys(errorMessages).forEach(errorKey => {
|
const pmKeys = ["PM1", "PM25", "PM10"];
|
||||||
if (response[errorKey] === 1) {
|
pmKeys.forEach(key => {
|
||||||
$("#data-table-body_" + port).append(`
|
if (response[key] !== undefined) {
|
||||||
<tr class="error-row">
|
$("#data-table-body_" + port).append(`
|
||||||
<td><b>${errorKey}</b></td>
|
<tr>
|
||||||
<td style="color: red;">⚠ ${errorMessages[errorKey]}</td>
|
<td>${key}</td>
|
||||||
</tr>
|
<td>${response[key]} µg/m³</td>
|
||||||
`);
|
</tr>
|
||||||
}
|
`);
|
||||||
});
|
}
|
||||||
},
|
});
|
||||||
error: function(xhr, status, error) {
|
|
||||||
console.error('AJAX request failed:', status, error);
|
// Temperature & humidity
|
||||||
}
|
if (response.temperature !== undefined) {
|
||||||
});
|
$("#data-table-body_" + port).append(`
|
||||||
|
<tr><td>Temperature</td><td>${response.temperature} °C</td></tr>
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
if (response.humidity !== undefined) {
|
||||||
|
$("#data-table-body_" + port).append(`
|
||||||
|
<tr><td>Humidity</td><td>${response.humidity} %</td></tr>
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// NPM status decoded
|
||||||
|
if (response.npm_status !== undefined) {
|
||||||
|
const status = response.npm_status;
|
||||||
|
|
||||||
|
if (status === 0xFF) {
|
||||||
|
// 0xFF = no response from sensor = disconnected
|
||||||
|
$("#data-table-body_" + port).append(`
|
||||||
|
<tr>
|
||||||
|
<td>Status</td>
|
||||||
|
<td style="color: red; font-weight: bold;">Capteur déconnecté</td>
|
||||||
|
</tr>
|
||||||
|
`);
|
||||||
|
} else if (status === 0) {
|
||||||
|
$("#data-table-body_" + port).append(`
|
||||||
|
<tr>
|
||||||
|
<td>Status</td>
|
||||||
|
<td style="color: green; font-weight: bold;">OK</td>
|
||||||
|
</tr>
|
||||||
|
`);
|
||||||
|
} else {
|
||||||
|
$("#data-table-body_" + port).append(`
|
||||||
|
<tr>
|
||||||
|
<td>Status</td>
|
||||||
|
<td style="color: orange; font-weight: bold;">${response.npm_status_hex}</td>
|
||||||
|
</tr>
|
||||||
|
`);
|
||||||
|
// Decode individual error bits
|
||||||
|
const statusFlags = {
|
||||||
|
0x01: "Sleep mode",
|
||||||
|
0x02: "Degraded mode",
|
||||||
|
0x04: "Not ready",
|
||||||
|
0x08: "Heater error",
|
||||||
|
0x10: "THP sensor error",
|
||||||
|
0x20: "Fan error",
|
||||||
|
0x40: "Memory error",
|
||||||
|
0x80: "Laser error"
|
||||||
|
};
|
||||||
|
Object.entries(statusFlags).forEach(([mask, label]) => {
|
||||||
|
if (status & mask) {
|
||||||
|
$("#data-table-body_" + port).append(`
|
||||||
|
<tr class="error-row">
|
||||||
|
<td></td>
|
||||||
|
<td style="color: red;">⚠ ${label}</td>
|
||||||
|
</tr>
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: function (xhr, status, error) {
|
||||||
|
console.error('AJAX request failed:', status, error);
|
||||||
|
$("#loading_" + port).hide();
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function getENVEA_values(port, name){
|
function getNPM_firmware(port) {
|
||||||
console.log("Data from Envea " + name + " (port " + port + "):");
|
console.log("Firmware version from NPM (port " + port + "):");
|
||||||
$("#loading_envea" + name).show();
|
$("#loading_fw_" + port).show();
|
||||||
|
|
||||||
$.ajax({
|
$.ajax({
|
||||||
|
url: 'launcher.php?type=npm_firmware&port=' + port,
|
||||||
|
dataType: 'json',
|
||||||
|
method: 'GET',
|
||||||
|
success: function (response) {
|
||||||
|
console.log(response);
|
||||||
|
$("#loading_fw_" + port).hide();
|
||||||
|
const fwSpan = document.getElementById("fw_version_" + port);
|
||||||
|
if (response.firmware_version !== undefined) {
|
||||||
|
fwSpan.innerHTML = '<span class="badge bg-success">Firmware: ' + response.firmware_version + '</span>';
|
||||||
|
} else {
|
||||||
|
fwSpan.innerHTML = '<span class="badge bg-danger">Error reading firmware</span>';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: function (xhr, status, error) {
|
||||||
|
console.error('AJAX request failed:', status, error);
|
||||||
|
$("#loading_fw_" + port).hide();
|
||||||
|
const fwSpan = document.getElementById("fw_version_" + port);
|
||||||
|
fwSpan.innerHTML = '<span class="badge bg-danger">Error</span>';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function getENVEA_values(port, name) {
|
||||||
|
console.log("Data from Envea " + name + " (port " + port + "):");
|
||||||
|
$("#loading_envea" + name).show();
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
url: 'launcher.php?type=envea&port=' + port + '&name=' + name,
|
url: 'launcher.php?type=envea&port=' + port + '&name=' + name,
|
||||||
dataType: 'json',
|
dataType: 'json',
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
success: function(response) {
|
success: function (response) {
|
||||||
console.log(response);
|
console.log(response);
|
||||||
const tableBody = document.getElementById("data-table-body_envea" + name);
|
const tableBody = document.getElementById("data-table-body_envea" + name);
|
||||||
tableBody.innerHTML = "";
|
tableBody.innerHTML = "";
|
||||||
|
|
||||||
$("#loading_envea" + name).hide();
|
$("#loading_envea" + name).hide();
|
||||||
|
|
||||||
const keysToShow = [name];
|
const keysToShow = [name];
|
||||||
keysToShow.forEach(key => {
|
keysToShow.forEach(key => {
|
||||||
if (response !== undefined) {
|
if (response !== undefined) {
|
||||||
const value = response;
|
const value = response;
|
||||||
$("#data-table-body_envea" + name).append(`
|
$("#data-table-body_envea" + name).append(`
|
||||||
<tr>
|
<tr>
|
||||||
<td>${key}</td>
|
<td>${key}</td>
|
||||||
<td>${value} ppb</td>
|
<td>${value} ppb</td>
|
||||||
</tr>
|
</tr>
|
||||||
`);
|
`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
error: function(xhr, status, error) {
|
error: function (xhr, status, error) {
|
||||||
console.error('AJAX request failed:', status, error);
|
console.error('AJAX request failed:', status, error);
|
||||||
const tableBody = document.getElementById("data-table-body_envea" + name);
|
const tableBody = document.getElementById("data-table-body_envea" + name);
|
||||||
$("#loading_envea" + name).hide();
|
$("#loading_envea" + name).hide();
|
||||||
|
|
||||||
tableBody.innerHTML = `
|
tableBody.innerHTML = `
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="2" class="text-danger">
|
<td colspan="2" class="text-danger">
|
||||||
❌ Error: unable to get data from sensor.<br>
|
❌ Error: unable to get data from sensor.<br>
|
||||||
@@ -185,142 +278,247 @@ function getENVEA_values(port, name){
|
|||||||
</tr>
|
</tr>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getENVEA_debug_values() {
|
||||||
|
console.log("Getting debug data from all Envea sensors");
|
||||||
|
$("#loading_envea_debug").show();
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
url: 'launcher.php?type=envea_debug',
|
||||||
|
dataType: 'text',
|
||||||
|
method: 'GET',
|
||||||
|
success: function (response) {
|
||||||
|
console.log("Envea debug output:", response);
|
||||||
|
const outputDiv = document.getElementById("envea-debug-output");
|
||||||
|
$("#loading_envea_debug").hide();
|
||||||
|
|
||||||
|
// Display raw output in a pre block
|
||||||
|
outputDiv.innerHTML = `<pre style="background-color: #f5f5f5; padding: 10px; border-radius: 5px; max-height: 500px; overflow-y: auto; font-size: 12px;">${response}</pre>`;
|
||||||
|
},
|
||||||
|
error: function (xhr, status, error) {
|
||||||
|
console.error('AJAX request failed:', status, error);
|
||||||
|
const outputDiv = document.getElementById("envea-debug-output");
|
||||||
|
$("#loading_envea_debug").hide();
|
||||||
|
|
||||||
|
outputDiv.innerHTML = `
|
||||||
|
<div class="alert alert-danger">
|
||||||
|
❌ Error: unable to get debug data from sensors.<br>
|
||||||
|
<small>${status}: ${error}</small>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
function getNoise_values(){
|
function getNoise_values() {
|
||||||
console.log("Data from I2C Noise Sensor:");
|
console.log("Data from NSRT MK4 Noise Sensor:");
|
||||||
$("#loading_noise").show();
|
$("#loading_noise").show();
|
||||||
|
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: 'launcher.php?type=noise',
|
url: 'launcher.php?type=noise',
|
||||||
dataType: 'text',
|
dataType: 'json',
|
||||||
method: 'GET', // Use GET or POST depending on your needs
|
method: 'GET',
|
||||||
success: function(response) {
|
success: function (response) {
|
||||||
console.log(response);
|
console.log(response);
|
||||||
const tableBody = document.getElementById("data-table-body_noise");
|
const tableBody = document.getElementById("data-table-body_noise");
|
||||||
tableBody.innerHTML = "";
|
tableBody.innerHTML = "";
|
||||||
$("#loading_noise").hide();
|
$("#loading_noise").hide();
|
||||||
|
|
||||||
// Create an array of the desired keys
|
if (response.error) {
|
||||||
const keysToShow = ["Noise"];
|
$("#data-table-body_noise").append(`
|
||||||
// Add only the specified elements to the table
|
<tr><td colspan="2" class="text-danger">${response.error}</td></tr>
|
||||||
keysToShow.forEach(key => {
|
`);
|
||||||
if (response !== undefined) { // Check if the key exists in the response
|
return;
|
||||||
const value = response;
|
}
|
||||||
$("#data-table-body_noise").append(`
|
|
||||||
<tr>
|
const rows = [
|
||||||
<td>${key}</td>
|
{ label: "LEQ", value: response.LEQ + " dB" },
|
||||||
<td>${value} DB</td>
|
{ label: "dB(A)", value: response.dBA + " dBA" },
|
||||||
</tr>
|
{ label: "Weighting", value: response.weighting },
|
||||||
`);
|
{ label: "Tau", value: response.tau + " s" }
|
||||||
}
|
];
|
||||||
});
|
rows.forEach(row => {
|
||||||
},
|
$("#data-table-body_noise").append(`
|
||||||
error: function(xhr, status, error) {
|
<tr>
|
||||||
console.error('AJAX request failed:', status, error);
|
<td>${row.label}</td>
|
||||||
}
|
<td>${row.value}</td>
|
||||||
});
|
</tr>
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
error: function (xhr, status, error) {
|
||||||
|
$("#loading_noise").hide();
|
||||||
|
console.error('AJAX request failed:', status, error);
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function getBME280_values(){
|
function getMHZ19_values() {
|
||||||
|
console.log("Data from MH-Z19 CO2 sensor:");
|
||||||
|
$("#loading_mhz19").show();
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
url: 'launcher.php?type=mhz19',
|
||||||
|
dataType: 'json',
|
||||||
|
method: 'GET',
|
||||||
|
success: function (response) {
|
||||||
|
console.log(response);
|
||||||
|
const tableBody = document.getElementById("data-table-body_mhz19");
|
||||||
|
tableBody.innerHTML = "";
|
||||||
|
$("#loading_mhz19").hide();
|
||||||
|
|
||||||
|
if (response.error) {
|
||||||
|
$("#data-table-body_mhz19").append(`
|
||||||
|
<tr>
|
||||||
|
<td colspan="2" class="text-danger">
|
||||||
|
⚠ ${response.error}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
`);
|
||||||
|
} else if (response.CO2 !== undefined) {
|
||||||
|
$("#data-table-body_mhz19").append(`
|
||||||
|
<tr>
|
||||||
|
<td>CO2</td>
|
||||||
|
<td>${response.CO2} ppm</td>
|
||||||
|
</tr>
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: function (xhr, status, error) {
|
||||||
|
console.error('AJAX request failed:', status, error);
|
||||||
|
$("#loading_mhz19").hide();
|
||||||
|
const tableBody = document.getElementById("data-table-body_mhz19");
|
||||||
|
tableBody.innerHTML = `
|
||||||
|
<tr>
|
||||||
|
<td colspan="2" class="text-danger">
|
||||||
|
⚠ Erreur de communication avec le capteur
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function getBME280_values() {
|
||||||
console.log("Data from I2C BME280:");
|
console.log("Data from I2C BME280:");
|
||||||
$("#loading_BME280").show();
|
$("#loading_BME280").show();
|
||||||
|
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: 'launcher.php?type=BME280',
|
url: 'launcher.php?type=BME280',
|
||||||
dataType: 'text',
|
dataType: 'text',
|
||||||
|
|
||||||
method: 'GET', // Use GET or POST depending on your needs
|
method: 'GET', // Use GET or POST depending on your needs
|
||||||
success: function(response) {
|
success: function (response) {
|
||||||
console.log(response);
|
console.log(response);
|
||||||
|
|
||||||
const tableBody = document.getElementById("data-table-body_BME280");
|
const tableBody = document.getElementById("data-table-body_BME280");
|
||||||
tableBody.innerHTML = "";
|
tableBody.innerHTML = "";
|
||||||
$("#loading_BME280").hide();
|
$("#loading_BME280").hide();
|
||||||
|
|
||||||
// Parse the JSON response
|
// Parse the JSON response
|
||||||
const data = JSON.parse(response);
|
const data = JSON.parse(response);
|
||||||
const keysToShow = ["temp", "hum", "press"];
|
const keysToShow = ["temp", "hum", "press"];
|
||||||
|
|
||||||
|
|
||||||
// Add only the specified elements to the table
|
// Add only the specified elements to the table
|
||||||
keysToShow.forEach(key => {
|
keysToShow.forEach(key => {
|
||||||
if (response !== undefined) { // Check if the key exists in the response
|
if (response !== undefined) { // Check if the key exists in the response
|
||||||
const value = data[key];
|
const value = data[key];
|
||||||
const unit = key === "temp" ? "°C"
|
const unit = key === "temp" ? "°C"
|
||||||
: key === "hum" ? "%"
|
: key === "hum" ? "%"
|
||||||
: key === "press" ? "hPa"
|
: key === "press" ? "hPa"
|
||||||
: ""; // Add appropriate units
|
: ""; // Add appropriate units
|
||||||
|
|
||||||
$("#data-table-body_BME280").append(`
|
$("#data-table-body_BME280").append(`
|
||||||
<tr>
|
<tr>
|
||||||
<td>${key.charAt(0).toUpperCase() + key.slice(1)}</td>
|
<td>${key.charAt(0).toUpperCase() + key.slice(1)}</td>
|
||||||
<td>${value} ${unit}</td>
|
<td>${value} ${unit}</td>
|
||||||
</tr>
|
</tr>
|
||||||
`);
|
`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
error: function(xhr, status, error) {
|
error: function (xhr, status, error) {
|
||||||
console.error('AJAX request failed:', status, error);
|
console.error('AJAX request failed:', status, error);
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
window.onload = function() {
|
|
||||||
|
|
||||||
//NEW way to get config (SQLite)
|
|
||||||
$.ajax({
|
|
||||||
url: 'launcher.php?type=get_config_sqlite',
|
|
||||||
dataType:'json',
|
|
||||||
//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 table:");
|
|
||||||
console.log(response);
|
|
||||||
|
|
||||||
|
|
||||||
//device name_side bar
|
|
||||||
const elements = document.querySelectorAll('.sideBar_sensorName');
|
|
||||||
elements.forEach((element) => {
|
|
||||||
element.innerText = response.deviceName;
|
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
},
|
|
||||||
error: function(xhr, status, error) {
|
|
||||||
console.error('AJAX request failed:', status, error);
|
|
||||||
}
|
|
||||||
});//end AJAX
|
|
||||||
|
|
||||||
//getting config_scripts table
|
window.onload = function () {
|
||||||
$.ajax({
|
|
||||||
url: 'launcher.php?type=get_config_scripts_sqlite',
|
|
||||||
dataType:'json',
|
|
||||||
//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
|
//NEW way to get config (SQLite)
|
||||||
|
let mainConfig = {}; // Store main config for use in sensor card creation
|
||||||
|
|
||||||
//creates NPM card
|
$.ajax({
|
||||||
if (response["NPM/get_data_modbus_v3.py"]) {
|
url: 'launcher.php?type=get_config_sqlite',
|
||||||
const cardHTML = `
|
dataType: 'json',
|
||||||
|
//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 table:");
|
||||||
|
console.log(response);
|
||||||
|
|
||||||
|
mainConfig = response; // Store for later use
|
||||||
|
|
||||||
|
// Function to update sidebar device name
|
||||||
|
function updateSidebarDeviceName(deviceName) {
|
||||||
|
const elements = document.querySelectorAll('.sideBar_sensorName');
|
||||||
|
if (elements.length > 0) {
|
||||||
|
elements.forEach((element) => {
|
||||||
|
element.innerText = deviceName;
|
||||||
|
});
|
||||||
|
console.log("Device name updated in sidebar:", deviceName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update device name immediately and with retries to handle async sidebar loading
|
||||||
|
if (response.deviceName) {
|
||||||
|
updateSidebarDeviceName(response.deviceName);
|
||||||
|
// Retry after delays to catch async sidebar load
|
||||||
|
setTimeout(() => updateSidebarDeviceName(response.deviceName), 100);
|
||||||
|
setTimeout(() => updateSidebarDeviceName(response.deviceName), 500);
|
||||||
|
|
||||||
|
// Set page title
|
||||||
|
document.title = response.deviceName;
|
||||||
|
}
|
||||||
|
|
||||||
|
// After getting main config, create sensor cards
|
||||||
|
createSensorCards(mainConfig);
|
||||||
|
|
||||||
|
},
|
||||||
|
error: function (xhr, status, error) {
|
||||||
|
console.error('AJAX request failed:', status, error);
|
||||||
|
}
|
||||||
|
});//end AJAX
|
||||||
|
|
||||||
|
//Function to create sensor cards based on config
|
||||||
|
function createSensorCards(config) {
|
||||||
|
console.log("Creating sensor cards with config:");
|
||||||
|
console.log(config);
|
||||||
|
|
||||||
|
const container = document.getElementById('card-container'); // Conteneur des cartes
|
||||||
|
|
||||||
|
//creates NPM card (by default)
|
||||||
|
|
||||||
|
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>
|
||||||
|
<button class="btn btn-secondary" onclick="getNPM_firmware('ttyAMA5')">Firmware Version</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>
|
||||||
|
<div id="loading_fw_ttyAMA5" class="spinner-border spinner-border-sm" style="display: none;" role="status"></div>
|
||||||
|
<div id="fw_version_ttyAMA5" class="mt-1"></div>
|
||||||
<table class="table table-striped-columns">
|
<table class="table table-striped-columns">
|
||||||
<tbody id="data-table-body_ttyAMA5"></tbody>
|
<tbody id="data-table-body_ttyAMA5"></tbody>
|
||||||
</table>
|
</table>
|
||||||
@@ -328,21 +526,21 @@ error: function(xhr, status, error) {
|
|||||||
</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
|
|
||||||
if (response["BME280/get_data_v2.py"]) {
|
//creates i2c BME280 card
|
||||||
const i2C_BME_HTML = `
|
if (config.BME280) {
|
||||||
|
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">
|
||||||
@@ -352,24 +550,21 @@ error: function(xhr, status, error) {
|
|||||||
</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 NSRT MK4 noise sensor card (USB)
|
||||||
if (response.i2C_sound) {
|
if (config.NOISE) {
|
||||||
const i2C_HTML = `
|
const noiseHTML = `
|
||||||
<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.headerUsb">
|
||||||
Port I2C
|
Port USB
|
||||||
</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">NSRT MK4</h5>
|
||||||
<p class="card-text">Capteur bruit sur le port I2C.</p>
|
<p class="card-text" data-i18n="sensors.noise.description">Sonomètre NSRT MK4 sur port USB.</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>
|
|
||||||
<button class="btn btn-success" onclick="startNoise()">Start recording</button>
|
|
||||||
<button class="btn btn-danger" onclick="stopNoise()">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>
|
||||||
@@ -378,84 +573,103 @@ error: function(xhr, status, error) {
|
|||||||
</div>
|
</div>
|
||||||
</div>`;
|
</div>`;
|
||||||
|
|
||||||
container.innerHTML += i2C_HTML; // Add the I2C card if condition is met
|
container.innerHTML += noiseHTML;
|
||||||
}
|
}
|
||||||
|
|
||||||
//Si on a des SONDES ENVEA connectée il faut faire un deuxième call dans la table envea_sondes_table
|
//creates MH-Z19 CO2 card
|
||||||
//creates ENVEA cards
|
if (config.MHZ19) {
|
||||||
if (response["envea/read_value_v2.py"]) {
|
const MHZ19_HTML = `
|
||||||
console.log("Need to display ENVEA sondes");
|
<div class="col-sm-3">
|
||||||
//getting config_scripts table
|
<div class="card">
|
||||||
$.ajax({
|
<div class="card-header">
|
||||||
url: 'launcher.php?type=get_envea_sondes_table_sqlite',
|
Port UART 4
|
||||||
dataType:'json',
|
</div>
|
||||||
//dataType: 'json', // Specify that you expect a JSON response
|
<div class="card-body">
|
||||||
method: 'GET', // Use GET or POST depending on your needs
|
<h5 class="card-title">MH-Z19 CO2</h5>
|
||||||
success: function(sondes) {
|
<p class="card-text">Capteur de dioxyde de carbone.</p>
|
||||||
console.log("Getting SQLite envea sondes table:");
|
<button class="btn btn-primary mb-1" onclick="getMHZ19_values()" data-i18n="common.getData">Get Data</button>
|
||||||
console.log(sondes);
|
<div id="loading_mhz19" class="spinner-border spinner-border-sm" style="display: none;" role="status"></div>
|
||||||
const ENVEA_sensors = sondes.filter(sonde => sonde.connected); // Filter only connected sondes
|
<table class="table table-striped-columns">
|
||||||
|
<tbody id="data-table-body_mhz19"></tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>`;
|
||||||
|
|
||||||
ENVEA_sensors.forEach((sensor, index) => {
|
container.innerHTML += MHZ19_HTML;
|
||||||
const port = sensor.port; // Port from the sensor object
|
}
|
||||||
const name = sensor.name; // Port from the sensor object
|
|
||||||
const coefficient = sensor.coefficient;
|
//Si on a des SONDES ENVEA connectée il faut faire un deuxième call dans la table envea_sondes_table
|
||||||
const cardHTML = `
|
//creates ENVEA debug card
|
||||||
<div class="col-sm-3">
|
if (config.envea) {
|
||||||
|
console.log("Need to display ENVEA sondes");
|
||||||
|
//getting config_scripts table
|
||||||
|
$.ajax({
|
||||||
|
url: 'launcher.php?type=get_envea_sondes_table_sqlite',
|
||||||
|
dataType: 'json',
|
||||||
|
method: 'GET',
|
||||||
|
success: function (sondes) {
|
||||||
|
console.log("Getting SQLite envea sondes table:");
|
||||||
|
console.log(sondes);
|
||||||
|
const ENVEA_sensors = sondes.filter(sonde => sonde.connected); // Filter only connected sondes
|
||||||
|
|
||||||
|
// Only create the card if there are connected sensors
|
||||||
|
if (ENVEA_sensors.length > 0) {
|
||||||
|
// Create a single debug card for all Envea sensors
|
||||||
|
const cardHTML = `
|
||||||
|
<div class="col-sm-6">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
Port UART ${port.replace('ttyAMA', '')}
|
<strong>Sondes Envea (Debug)</strong>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h5 class="card-title">Sonde Envea ${name}</h5>
|
<h5 class="card-title" data-i18n="sensors.envea.title">Sonde Envea</h5>
|
||||||
<p class="card-text">Capteur gas.</p>
|
<p class="card-text" data-i18n="sensors.envea.description">Capteur gaz.</p>
|
||||||
<button class="btn btn-primary" onclick="getENVEA_values('${port}','${name}')">Get Data</button>
|
<p class="text-muted small">Sondes connectées: ${ENVEA_sensors.map(s => s.name).join(', ')}</p>
|
||||||
<div id="loading_envea${name}" class="spinner-border spinner-border-sm" style="display: none;" role="status"></div>
|
<button class="btn btn-primary" onclick="getENVEA_debug_values()" data-i18n="common.getData">Get Data</button>
|
||||||
<table class="table table-striped-columns">
|
<div id="loading_envea_debug" class="spinner-border spinner-border-sm" style="display: none;" role="status"></div>
|
||||||
<tbody id="data-table-body_envea${name}"></tbody>
|
<div id="envea-debug-output" class="mt-3"></div>
|
||||||
</table>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>`;
|
</div>`;
|
||||||
container.innerHTML += cardHTML; // Ajouter la carte au conteneur
|
container.innerHTML += cardHTML;
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
},
|
|
||||||
error: function(xhr, status, error) {
|
|
||||||
console.error('AJAX request failed:', status, error);
|
|
||||||
}
|
|
||||||
});//end AJAX envea Sondes
|
|
||||||
|
|
||||||
|
|
||||||
}//end if
|
|
||||||
|
|
||||||
|
|
||||||
},
|
|
||||||
error: function(xhr, status, error) {
|
|
||||||
console.error('AJAX request failed:', status, error);
|
|
||||||
}
|
|
||||||
});//end AJAX (config_scripts)
|
|
||||||
|
|
||||||
//get local RTC
|
|
||||||
$.ajax({
|
|
||||||
url: 'launcher.php?type=RTC_time',
|
|
||||||
dataType: 'text', // Specify that you expect a JSON response
|
|
||||||
method: 'GET', // Use GET or POST depending on your needs
|
|
||||||
success: function(response) {
|
|
||||||
console.log("Local RTC: " + response);
|
|
||||||
const RTC_Element = document.getElementById("RTC_time");
|
|
||||||
RTC_Element.textContent = response;
|
|
||||||
|
|
||||||
},
|
|
||||||
error: function(xhr, status, error) {
|
|
||||||
console.error('AJAX request failed:', status, error);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Apply translations to dynamically created Envea card
|
||||||
|
i18n.applyTranslations();
|
||||||
|
|
||||||
|
},
|
||||||
|
error: function (xhr, status, error) {
|
||||||
|
console.error('AJAX request failed:', status, error);
|
||||||
|
}
|
||||||
|
});//end AJAX envea Sondes
|
||||||
|
|
||||||
|
}//end if envea
|
||||||
|
|
||||||
|
// Apply translations to all dynamically created sensor cards
|
||||||
|
i18n.applyTranslations();
|
||||||
|
|
||||||
|
} // end createSensorCards function
|
||||||
|
|
||||||
|
//get local RTC
|
||||||
|
$.ajax({
|
||||||
|
url: 'launcher.php?type=RTC_time',
|
||||||
|
dataType: 'text', // Specify that you expect a JSON response
|
||||||
|
method: 'GET', // Use GET or POST depending on your needs
|
||||||
|
success: function (response) {
|
||||||
|
console.log("Local RTC: " + response);
|
||||||
|
const RTC_Element = document.getElementById("RTC_time");
|
||||||
|
RTC_Element.textContent = response;
|
||||||
|
|
||||||
|
},
|
||||||
|
error: function (xhr, status, error) {
|
||||||
|
console.error('AJAX request failed:', status, error);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
} //end windows onload
|
} //end windows onload
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
@@ -1,70 +1,109 @@
|
|||||||
<!-- Sidebar -->
|
<!-- Sidebar -->
|
||||||
<nav class="nav flex-column">
|
<nav class="nav flex-column">
|
||||||
<a class="nav-link text-white mt-4" href="index.html">
|
<a class="nav-link text-white mt-4" href="index.html">
|
||||||
<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">
|
|
||||||
<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="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>
|
|
||||||
Capteurs
|
|
||||||
</a>
|
|
||||||
<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">
|
|
||||||
<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>
|
|
||||||
|
|
||||||
DataBase
|
<!-- Screen Control (Hidden by default) -->
|
||||||
</a>
|
<a class="nav-link text-white nav-screen-item" href="screen.html" id="nav-screen" style="display: none;">
|
||||||
<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-display"
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-reception-4" viewBox="0 0 16 16">
|
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
|
||||||
</svg>
|
d="M0 4s0-2 2-2h12s2 0 2 2v6s0 2-2 2h-4c0 .667.083 1.167.25 1.5H11a.5.5 0 0 1 0 1H5a.5.5 0 0 1 0-1h.75c.167-.333.25-.833.25-1.5H2s-2 0-2-2V4zm1.398-.855a.758.758 0 0 0-.254.302A1.46 1.46 0 0 0 1 4.01V10c0 .325.078.502.145.602.07.105.17.188.302.254a1.464 1.464 0 0 0 .538.143L2.01 11H14c.325 0 .502-.078.602-.145a.758.758 0 0 0 .254-.302 1.464 1.464 0 0 0 .143-.538L15 9.99V4c0-.325-.078-.502-.145-.602a.757.757 0 0 0-.302-.254A1.46 1.46 0 0 0 13.99 3H2c-.325 0-.502.078-.602.145z" />
|
||||||
Modem 4G
|
</svg>
|
||||||
</a>
|
<span data-i18n="sidebar.screen">Screen</span>
|
||||||
<a class="nav-link text-white" href="wifi.html">
|
</a>
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-wifi" viewBox="0 0 16 16">
|
<a class="nav-link text-white" href="sensors.html">
|
||||||
<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"/>
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-thermometer-sun"
|
||||||
<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"/>
|
viewBox="0 0 16 16">
|
||||||
</svg>
|
<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" />
|
||||||
WIFI
|
<path
|
||||||
</a>
|
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" />
|
||||||
<a class="nav-link text-white" href="logs.html">
|
</svg>
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-journal-code" viewBox="0 0 16 16">
|
<span data-i18n="sidebar.sensors">Capteurs</span>
|
||||||
<path fill-rule="evenodd" d="M8.646 5.646a.5.5 0 0 1 .708 0l2 2a.5.5 0 0 1 0 .708l-2 2a.5.5 0 0 1-.708-.708L10.293 8 8.646 6.354a.5.5 0 0 1 0-.708m-1.292 0a.5.5 0 0 0-.708 0l-2 2a.5.5 0 0 0 0 .708l2 2a.5.5 0 0 0 .708-.708L5.707 8l1.647-1.646a.5.5 0 0 0 0-.708"/>
|
</a>
|
||||||
<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"/>
|
<a class="nav-link text-white" href="database.html">
|
||||||
<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 xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-database"
|
||||||
</svg>
|
viewBox="0 0 16 16">
|
||||||
Logs
|
<path
|
||||||
</a>
|
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>
|
||||||
|
|
||||||
|
<span data-i18n="sidebar.database">Base de données</span>
|
||||||
|
</a>
|
||||||
|
<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">
|
||||||
|
<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>
|
||||||
|
<span data-i18n="sidebar.modem4g">Modem 4G</span>
|
||||||
|
</a>
|
||||||
|
<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">
|
||||||
|
<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" />
|
||||||
|
</svg>
|
||||||
|
<span data-i18n="sidebar.wifi">WIFI</span>
|
||||||
|
</a>
|
||||||
|
<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">
|
||||||
|
<path fill-rule="evenodd"
|
||||||
|
d="M8.646 5.646a.5.5 0 0 1 .708 0l2 2a.5.5 0 0 1 0 .708l-2 2a.5.5 0 0 1-.708-.708L10.293 8 8.646 6.354a.5.5 0 0 1 0-.708m-1.292 0a.5.5 0 0 0-.708 0l-2 2a.5.5 0 0 0 0 .708l2 2a.5.5 0 0 0 .708-.708L5.707 8l1.647-1.646a.5.5 0 0 0 0-.708" />
|
||||||
|
<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" />
|
||||||
|
</svg>
|
||||||
|
<span data-i18n="sidebar.logs">Logs</span>
|
||||||
|
</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">
|
-->
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-tools" viewBox="0 0 16 16">
|
<a class="nav-link text-white" href="admin.html">
|
||||||
<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 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" />
|
||||||
|
</svg>
|
||||||
|
<span data-i18n="sidebar.admin">Admin</span>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<!-- New content at the bottom -->
|
||||||
|
<div class="sidebar-footer text-center text-white">
|
||||||
|
<hr>
|
||||||
|
<span class="sideBar_sensorName">NebuleAir</span>
|
||||||
|
<div class="sidebar-hotspot-badge mt-2" style="display:none;">
|
||||||
|
<a href="wifi.html" class="text-decoration-none">
|
||||||
|
<span class="badge text-bg-warning w-100 py-2" style="font-size: 0.75rem;">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" fill="currentColor" class="bi bi-broadcast me-1" viewBox="0 0 16 16">
|
||||||
|
<path d="M3.05 3.05a7 7 0 0 0 0 9.9.5.5 0 0 1-.707.707 8 8 0 0 1 0-11.314.5.5 0 0 1 .707.707m2.122 2.122a4 4 0 0 0 0 5.656.5.5 0 1 1-.708.708 5 5 0 0 1 0-7.072.5.5 0 0 1 .708.708m5.656-.708a.5.5 0 0 1 .708 0 5 5 0 0 1 0 7.072.5.5 0 1 1-.708-.708 4 4 0 0 0 0-5.656.5.5 0 0 1 0-.708m2.122-2.12a.5.5 0 0 1 .707 0 8 8 0 0 1 0 11.313.5.5 0 0 1-.707-.707 7 7 0 0 0 0-9.9.5.5 0 0 1 0-.707zM10 8a2 2 0 1 1-4 0 2 2 0 0 1 4 0"/>
|
||||||
</svg>
|
</svg>
|
||||||
Admin
|
Mode Hotspot
|
||||||
|
</span>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<!-- New content at the bottom -->
|
|
||||||
<div class="sidebar-footer text-center text-white">
|
|
||||||
<hr>
|
|
||||||
<span class="sideBar_sensorName"> NebuleAir</span>
|
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
</nav>
|
</nav>
|
||||||
@@ -175,6 +175,7 @@
|
|||||||
<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>
|
||||||
|
<script src="assets/js/topbar-logo.js"></script>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
document.addEventListener('DOMContentLoaded', function () {
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<nav class="navbar navbar-dark fixed-top" style="background-color: #8d8d8f;" id="topbar">
|
<nav class="navbar navbar-dark fixed-top" style="background-color: #8d8d8f;" id="topbar">
|
||||||
<div class="container-fluid">
|
<div class="container-fluid">
|
||||||
<a class="navbar-brand" href="#">
|
<a class="navbar-brand" href="#">
|
||||||
<img src="assets/img/LogoNebuleAir.png" alt="Logo" height="30" class="d-inline-block align-text-top">
|
<img src="assets/img/LogoNebuleAir.png" alt="Logo" height="30" class="d-inline-block align-text-top" id="topbar-logo">
|
||||||
</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>
|
||||||
@@ -12,6 +12,12 @@
|
|||||||
<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>
|
||||||
|
|||||||
528
html/wifi.html
528
html/wifi.html
@@ -54,56 +54,114 @@
|
|||||||
|
|
||||||
<h3>Status
|
<h3>Status
|
||||||
<span id="wifi-status" class="badge">Loading...</span>
|
<span id="wifi-status" class="badge">Loading...</span>
|
||||||
|
<button id="btn-forget-wifi" class="btn btn-outline-danger btn-sm ms-2" style="display:none;" onclick="wifi_forget()">Oublier le réseau</button>
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
<div class="row mb-3">
|
<div class="row mb-3">
|
||||||
|
|
||||||
<div class="col-sm-4">
|
<!-- Connection Info Card (shown when connected to WiFi) -->
|
||||||
<div class="card text-dark bg-light">
|
<div class="col-sm-6" id="card-connection-info" style="display:none;">
|
||||||
|
<div class="card border-success">
|
||||||
|
<div class="card-header bg-success text-white">
|
||||||
|
<h5 class="card-title mb-0">Connexion WiFi</h5>
|
||||||
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h5 class="card-title">WIFI / Ethernet</h5>
|
<div id="connection-info-loading" class="text-center py-3">
|
||||||
<p class="card-text">General information.</p>
|
<div class="spinner-border spinner-border-sm text-primary" role="status"></div>
|
||||||
<button class="btn btn-primary" onclick="get_internet()">Get Data</button>
|
<span class="ms-2">Chargement...</span>
|
||||||
<table class="table table-striped-columns">
|
</div>
|
||||||
<tbody id="data-table-body_internet_general"></tbody>
|
<table class="table table-sm mb-0" id="connection-info-table" style="display:none;">
|
||||||
|
<tbody>
|
||||||
|
<tr><td class="text-muted" style="width:40%">SSID</td><td><strong id="info-ssid">-</strong></td></tr>
|
||||||
|
<tr><td class="text-muted">Signal</td><td><span id="info-signal">-</span></td></tr>
|
||||||
|
<tr><td class="text-muted">Adresse IP</td><td><code id="info-ip">-</code></td></tr>
|
||||||
|
<tr><td class="text-muted">Passerelle</td><td><code id="info-gateway">-</code></td></tr>
|
||||||
|
<tr><td class="text-muted">Hostname</td><td><code id="info-hostname">-</code></td></tr>
|
||||||
|
<tr><td class="text-muted">Frequence</td><td id="info-freq">-</td></tr>
|
||||||
|
<tr><td class="text-muted">Securite</td><td id="info-security">-</td></tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<button class="btn btn-outline-primary btn-sm mt-2" onclick="get_internet()">Rafraichir</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Ethernet Card -->
|
||||||
|
<div class="col-sm-6" id="card-ethernet">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5 class="card-title mb-0">Ethernet</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<table class="table table-sm mb-0">
|
||||||
|
<tbody>
|
||||||
|
<tr><td class="text-muted" style="width:40%">Etat</td><td id="info-eth-status">-</td></tr>
|
||||||
|
<tr><td class="text-muted">Adresse IP</td><td><code id="info-eth-ip">-</code></td></tr>
|
||||||
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-sm-8">
|
<!-- Hotspot Info Card (shown when in hotspot mode) -->
|
||||||
<div class="card text-dark bg-light">
|
<div class="col-sm-6" id="card-hotspot-info" style="display:none;">
|
||||||
|
<div class="card border-warning">
|
||||||
|
<div class="card-header bg-warning">
|
||||||
|
<h5 class="card-title mb-0">Mode Hotspot</h5>
|
||||||
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h5 class="card-title">Wifi Scan</h5>
|
<p class="mb-1">Le capteur n'est connecte a aucun reseau WiFi.</p>
|
||||||
<p class="card-text">Scan des réseaux WIFI disponibles.</p>
|
<p class="text-muted mb-0">Utilisez le scan ci-dessous pour vous connecter a un reseau.</p>
|
||||||
<button class="btn btn-primary" onclick="wifi_scan()">Scan</button>
|
</div>
|
||||||
<table class="table">
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- WiFi Scan Card (hidden when connected) -->
|
||||||
|
<div class="row mb-3" id="card-wifi-scan" style="display:none;">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header d-flex justify-content-between align-items-center">
|
||||||
|
<h5 class="card-title mb-0">Reseaux WiFi disponibles</h5>
|
||||||
|
<button class="btn btn-primary btn-sm" onclick="wifi_scan()">Scan</button>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div id="wifi-scan-cache-notice" class="alert alert-info py-2 mb-2" style="display:none; font-size: 0.85rem;">
|
||||||
|
Scan effectue au demarrage du capteur (scan live indisponible en mode hotspot).
|
||||||
|
</div>
|
||||||
|
<div id="wifi-scan-empty" class="text-center text-muted py-3">
|
||||||
|
Cliquez sur "Scan" pour rechercher les reseaux WiFi.
|
||||||
|
</div>
|
||||||
|
<table class="table table-hover mb-0" id="wifi-scan-table" style="display:none;">
|
||||||
|
<thead>
|
||||||
|
<tr><th>SSID</th><th>Signal</th><th>Securite</th><th></th></tr>
|
||||||
|
</thead>
|
||||||
<tbody id="data-table-body_wifi_scan"></tbody>
|
<tbody id="data-table-body_wifi_scan"></tbody>
|
||||||
</table>
|
</table>
|
||||||
</table>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Modal WIFI PASSWORD -->
|
<!-- Modal WIFI PASSWORD -->
|
||||||
<!-- filled with JS -->
|
<div class="modal fade" id="myModal" tabindex="-1" aria-labelledby="exampleModalLabel" aria-hidden="true">
|
||||||
<div class="modal fade" id="myModal" tabindex="-1" aria-labelledby="exampleModalLabel" aria-hidden="true">
|
<div class="modal-dialog">
|
||||||
<div class="modal-dialog">
|
<div class="modal-content">
|
||||||
<div class="modal-content">
|
<div class="modal-header">
|
||||||
<div class="modal-header">
|
<h1 class="modal-title fs-5" id="myModalLabel">Modal title</h1>
|
||||||
<h1 class="modal-title fs-5" id="myModalLabel">Modal title</h1>
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
</div>
|
||||||
</div>
|
<div class="modal-body" id="myModalBody">
|
||||||
<div class="modal-body" id="myModalBody">
|
...
|
||||||
...
|
</div>
|
||||||
</div>
|
<div class="modal-footer" id="myModalFooter">
|
||||||
<div class="modal-footer" id="myModalFooter">
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
||||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
<button type="button" class="btn btn-primary">Save changes</button>
|
||||||
<button type="button" class="btn btn-primary">Save changes</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
|
|
||||||
@@ -117,6 +175,9 @@
|
|||||||
<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 src="assets/js/topbar-logo.js"></script>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
document.addEventListener('DOMContentLoaded', function () {
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
@@ -139,197 +200,280 @@
|
|||||||
})
|
})
|
||||||
.catch(error => console.error(`Error loading ${file}:`, error));
|
.catch(error => console.error(`Error loading ${file}:`, error));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function getSignalBadge(signal) {
|
||||||
|
const val = parseInt(signal, 10);
|
||||||
|
if (isNaN(val)) return '';
|
||||||
|
let color, label;
|
||||||
|
if (val >= 70) { color = 'success'; label = 'Excellent'; }
|
||||||
|
else if (val >= 50) { color = 'primary'; label = 'Bon'; }
|
||||||
|
else if (val >= 30) { color = 'warning'; label = 'Faible'; }
|
||||||
|
else { color = 'danger'; label = 'Tres faible'; }
|
||||||
|
return `<span class="badge text-bg-${color}">${val}% — ${label}</span>`;
|
||||||
|
}
|
||||||
|
|
||||||
function get_internet(){
|
function get_internet(){
|
||||||
console.log("Getting internet general infos");
|
console.log("Getting internet general infos");
|
||||||
$.ajax({
|
document.getElementById('connection-info-loading').style.display = '';
|
||||||
url: 'launcher.php?type=internet',
|
document.getElementById('connection-info-table').style.display = 'none';
|
||||||
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);
|
|
||||||
let tableBody = document.getElementById('data-table-body_internet_general');
|
|
||||||
tableBody.innerHTML = ''; // Clear existing table content
|
|
||||||
|
|
||||||
// Iterate through the data and create rows
|
$.ajax({
|
||||||
for (let key in response) {
|
url: 'launcher.php?type=internet',
|
||||||
let row = `
|
dataType: 'json',
|
||||||
<tr>
|
method: 'GET',
|
||||||
<td>${key}</td>
|
success: function(response) {
|
||||||
<td>${response[key].connection}</td>
|
console.log(response);
|
||||||
<td>${response[key].IP ? response[key].IP : "No IP"}</td>
|
const wifi = response.wifi;
|
||||||
</tr>
|
const eth = response.ethernet;
|
||||||
`;
|
|
||||||
tableBody.innerHTML += row; // Append row to table body
|
|
||||||
}
|
|
||||||
},
|
|
||||||
error: function(xhr, status, error) {
|
|
||||||
console.error('AJAX request failed:', status, error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
|
document.getElementById('info-ssid').textContent = wifi.ssid || '-';
|
||||||
|
document.getElementById('info-signal').innerHTML = wifi.signal ? getSignalBadge(wifi.signal) : '-';
|
||||||
|
document.getElementById('info-ip').textContent = wifi.IP || '-';
|
||||||
|
document.getElementById('info-gateway').textContent = wifi.gateway || '-';
|
||||||
|
document.getElementById('info-hostname').textContent = wifi.hostname || '-';
|
||||||
|
document.getElementById('info-freq').textContent = wifi.frequency ? wifi.frequency + ' MHz' : '-';
|
||||||
|
document.getElementById('info-security').textContent = wifi.security || '-';
|
||||||
|
|
||||||
function wifi_connect(SSID, PASS){
|
document.getElementById('info-eth-status').textContent = eth.connection || '-';
|
||||||
console.log("Connecting to wifi");
|
document.getElementById('info-eth-ip').textContent = eth.IP || '-';
|
||||||
console.log(SSID);
|
|
||||||
console.log(PASS);
|
|
||||||
if (typeof PASS === 'undefined') {
|
|
||||||
console.log("Need to add password");
|
|
||||||
//open bootstrap modal to ask for password
|
|
||||||
var myModal = new bootstrap.Modal(document.getElementById('myModal'));
|
|
||||||
//modifiy modal title
|
|
||||||
document.getElementById('myModalLabel').innerHTML = "Enter password for "+SSID;
|
|
||||||
//add input field to modal body
|
|
||||||
document.getElementById('myModalBody').innerHTML = "<input type='text' id='wifi_pass' class='form-control' placeholder='Password'>";
|
|
||||||
//add button to modal footer
|
|
||||||
document.getElementById('myModalFooter').innerHTML = "<button type='button' class='btn btn-secondary' data-bs-dismiss='modal'>Close</button><button type='button' class='btn btn-primary' onclick='wifi_connect(\""+SSID+"\", document.getElementById(\"wifi_pass\").value)'>Se connecter</button>";
|
|
||||||
myModal.show();
|
|
||||||
} else {
|
|
||||||
console.log("Will try to connect to "+SSID+" with password "+PASS);
|
|
||||||
console.log("Start PHP script:");
|
|
||||||
|
|
||||||
$.ajax({
|
document.getElementById('connection-info-loading').style.display = 'none';
|
||||||
url: 'launcher.php?type=wifi_connect&SSID='+SSID+'&pass='+PASS,
|
document.getElementById('connection-info-table').style.display = '';
|
||||||
dataType: 'text', // Specify that you expect a JSON response
|
},
|
||||||
method: 'GET', // Use GET or POST depending on your needs
|
error: function(xhr, status, error) {
|
||||||
success: function(response) {
|
console.error('AJAX request failed:', status, error);
|
||||||
console.log(response);
|
document.getElementById('connection-info-loading').innerHTML = '<span class="text-danger">Erreur de chargement</span>';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
},
|
function wifi_connect(SSID, PASS){
|
||||||
error: function(xhr, status, error) {
|
console.log("Connecting to wifi");
|
||||||
console.error('AJAX request failed:', status, error);
|
if (typeof PASS === 'undefined') {
|
||||||
}
|
var myModal = new bootstrap.Modal(document.getElementById('myModal'));
|
||||||
});
|
document.getElementById('myModalLabel').innerHTML = "Enter password for "+SSID;
|
||||||
|
document.getElementById('myModalBody').innerHTML = "<input type='text' id='wifi_pass' class='form-control' placeholder='Password'>";
|
||||||
|
document.getElementById('myModalFooter').innerHTML = "<button type='button' class='btn btn-secondary' data-bs-dismiss='modal'>Close</button><button type='button' class='btn btn-primary' onclick='wifi_connect(\""+SSID+"\", document.getElementById(\"wifi_pass\").value)'>Se connecter</button>";
|
||||||
|
myModal.show();
|
||||||
|
} else {
|
||||||
|
var myModal = bootstrap.Modal.getInstance(document.getElementById('myModal'));
|
||||||
|
if (myModal) { myModal.hide(); }
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
url: 'launcher.php?type=wifi_connect&SSID='+SSID+'&pass='+PASS,
|
||||||
|
dataType: 'json',
|
||||||
|
method: 'GET',
|
||||||
|
success: function(response) {
|
||||||
|
console.log(response);
|
||||||
|
showConnectionStatus(response);
|
||||||
|
},
|
||||||
|
error: function(xhr, status, error) {
|
||||||
|
console.error('AJAX request failed:', status, error);
|
||||||
|
alert('Error: Could not start connection process');
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
|
|
||||||
function wifi_scan(){
|
|
||||||
console.log("Scanning Wifi");
|
|
||||||
$.ajax({
|
|
||||||
url: 'launcher.php?type=wifi_scan',
|
|
||||||
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);
|
|
||||||
const tableBody = document.getElementById("data-table-body_wifi_scan");
|
|
||||||
|
|
||||||
// Clear the existing table body
|
|
||||||
tableBody.innerHTML = "";
|
|
||||||
|
|
||||||
// Loop through the wifiNetworks array and create rows
|
|
||||||
response.forEach(network => {
|
|
||||||
const row = document.createElement("tr");
|
|
||||||
|
|
||||||
// Create and append cells for SSID, BARS, and SIGNAL
|
|
||||||
const ssidCell = document.createElement("td");
|
|
||||||
// Truncate SSID to 25 characters
|
|
||||||
const truncatedSSID = network.SSID.length > 20 ? network.SSID.substring(0, 20) + '...' : network.SSID;
|
|
||||||
ssidCell.textContent = truncatedSSID;
|
|
||||||
row.appendChild(ssidCell);
|
|
||||||
|
|
||||||
/*
|
|
||||||
const signalCell = document.createElement("td");
|
|
||||||
signalCell.textContent = network.SIGNAL;
|
|
||||||
row.appendChild(signalCell);
|
|
||||||
*/
|
|
||||||
|
|
||||||
// Create a button
|
|
||||||
const buttonCell = document.createElement("td");
|
|
||||||
const button = document.createElement("button");
|
|
||||||
button.textContent = "Connect"; // Button text
|
|
||||||
button.classList.add("btn", "btn-primary"); // Bootstrap button classes
|
|
||||||
|
|
||||||
// Determine button color based on SIGNAL value
|
|
||||||
const signalValue = parseInt(network.SIGNAL, 10); // Assuming SIGNAL is a numeric value
|
|
||||||
// Calculate color based on the signal strength
|
|
||||||
let buttonColor;
|
|
||||||
if (signalValue >= 100) {
|
|
||||||
buttonColor = "success"; // Green for strong signal
|
|
||||||
} else if (signalValue >= 50) {
|
|
||||||
buttonColor = "warning"; // Yellow for moderate signal
|
|
||||||
} else {
|
|
||||||
buttonColor = "danger"; // Red for weak signal
|
|
||||||
}
|
|
||||||
// Add Bootstrap button classes along with color
|
|
||||||
button.classList.add("btn", `btn-${buttonColor}`);
|
|
||||||
|
|
||||||
|
|
||||||
//Trigger function as soon as the button is clicked
|
|
||||||
button.addEventListener("click", () => wifi_connect(network.SSID));
|
|
||||||
|
|
||||||
|
|
||||||
// Append the button to the button cell
|
|
||||||
buttonCell.appendChild(button);
|
|
||||||
row.appendChild(buttonCell);
|
|
||||||
|
|
||||||
// Append the row to the table body
|
|
||||||
tableBody.appendChild(row);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
error: function(xhr, status, error) {
|
|
||||||
console.error('AJAX request failed:', status, error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
function confirmSubmit() {
|
|
||||||
// You can display a simple confirmation message or customize the behavior
|
|
||||||
return confirm("Are you sure you want to connect to this Wi-Fi network?");
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function showConnectionStatus(response) {
|
||||||
|
const lang = localStorage.getItem('language') || 'fr';
|
||||||
|
const instructions = response.instructions[lang] || response.instructions['fr'];
|
||||||
|
|
||||||
|
const overlay = document.createElement('div');
|
||||||
|
overlay.id = 'connection-status-overlay';
|
||||||
|
overlay.style.cssText = 'position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.9);z-index:9999;display:flex;align-items:center;justify-content:center;color:white;';
|
||||||
|
overlay.innerHTML = `
|
||||||
|
<div style="max-width: 600px; padding: 40px; text-align: center;">
|
||||||
|
<div class="spinner-border text-primary mb-4" role="status" style="width: 4rem; height: 4rem;"><span class="visually-hidden">Loading...</span></div>
|
||||||
|
<h2 class="mb-4">${instructions.title}</h2>
|
||||||
|
<div class="alert alert-info text-start" role="alert">
|
||||||
|
<ol class="mb-0" style="padding-left: 20px;">
|
||||||
|
<li class="mb-2"><strong>${instructions.step1}</strong></li>
|
||||||
|
<li class="mb-2">${instructions.step2}</li>
|
||||||
|
<li class="mb-2">${instructions.step3}</li>
|
||||||
|
<li class="mb-2">${instructions.step4}</li>
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
<div class="alert alert-warning text-start" role="alert"><strong>Important:</strong> ${instructions.warning}</div>
|
||||||
|
<div class="mt-4">
|
||||||
|
<p class="text-muted">${lang === 'fr' ? 'Reconnexion à' : 'Reconnecting to'}: <strong class="text-white">${response.ssid}</strong></p>
|
||||||
|
<p class="text-muted">${lang === 'fr' ? 'Nom du capteur' : 'Sensor name'}: <strong class="text-white">${response.deviceName}</strong></p>
|
||||||
|
</div>
|
||||||
|
<div class="mt-4"><small class="text-muted">${lang === 'fr' ? 'Cette fenêtre va se fermer automatiquement...' : 'This window will close automatically...'}</small></div>
|
||||||
|
</div>`;
|
||||||
|
document.body.appendChild(overlay);
|
||||||
|
setTimeout(() => { const o = document.getElementById('connection-status-overlay'); if (o) o.remove(); }, 30000);
|
||||||
|
}
|
||||||
|
|
||||||
|
function wifi_forget(){
|
||||||
|
if (!confirm('Oublier le réseau WiFi actuel et passer en mode hotspot ?')) return;
|
||||||
|
$.ajax({
|
||||||
|
url: 'launcher.php?type=wifi_forget',
|
||||||
|
dataType: 'json',
|
||||||
|
method: 'GET',
|
||||||
|
success: function(response) {
|
||||||
|
console.log(response);
|
||||||
|
showForgetStatus(response);
|
||||||
|
},
|
||||||
|
error: function(xhr, status, error) {
|
||||||
|
console.error('AJAX request failed:', status, error);
|
||||||
|
alert('Error: Could not forget WiFi network');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function showForgetStatus(response) {
|
||||||
|
const lang = localStorage.getItem('language') || 'fr';
|
||||||
|
const instructions = response.instructions[lang] || response.instructions['fr'];
|
||||||
|
|
||||||
|
const overlay = document.createElement('div');
|
||||||
|
overlay.id = 'connection-status-overlay';
|
||||||
|
overlay.style.cssText = 'position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.9);z-index:9999;display:flex;align-items:center;justify-content:center;color:white;';
|
||||||
|
overlay.innerHTML = `
|
||||||
|
<div style="max-width: 600px; padding: 40px; text-align: center;">
|
||||||
|
<div class="spinner-border text-warning mb-4" role="status" style="width: 4rem; height: 4rem;"><span class="visually-hidden">Loading...</span></div>
|
||||||
|
<h2 class="mb-4">${instructions.title}</h2>
|
||||||
|
<div class="alert alert-info text-start" role="alert">
|
||||||
|
<ol class="mb-0" style="padding-left: 20px;">
|
||||||
|
<li class="mb-2"><strong>${instructions.step1}</strong></li>
|
||||||
|
<li class="mb-2">${instructions.step2}</li>
|
||||||
|
<li class="mb-2">${instructions.step3}</li>
|
||||||
|
<li class="mb-2">${instructions.step4}</li>
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
<div class="alert alert-warning text-start" role="alert"><strong>Important:</strong> ${instructions.warning}</div>
|
||||||
|
<div class="mt-4"><p class="text-muted">Hotspot: <strong class="text-white">${response.deviceName}</strong></p></div>
|
||||||
|
</div>`;
|
||||||
|
document.body.appendChild(overlay);
|
||||||
|
setTimeout(() => { const o = document.getElementById('connection-status-overlay'); if (o) o.remove(); }, 30000);
|
||||||
|
}
|
||||||
|
|
||||||
|
function wifi_scan(){
|
||||||
|
console.log("Scanning Wifi");
|
||||||
|
document.getElementById('wifi-scan-empty').innerHTML = '<div class="spinner-border spinner-border-sm text-primary" role="status"></div> Scan en cours...';
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
url: 'launcher.php?type=wifi_scan',
|
||||||
|
dataType: 'json',
|
||||||
|
method: 'GET',
|
||||||
|
success: function(response) {
|
||||||
|
console.log(response);
|
||||||
|
const tableBody = document.getElementById("data-table-body_wifi_scan");
|
||||||
|
tableBody.innerHTML = "";
|
||||||
|
|
||||||
|
if (response.length === 0) {
|
||||||
|
document.getElementById('wifi-scan-empty').textContent = 'Aucun reseau WiFi trouve.';
|
||||||
|
document.getElementById('wifi-scan-empty').style.display = '';
|
||||||
|
document.getElementById('wifi-scan-table').style.display = 'none';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show cached scan notice if in hotspot mode
|
||||||
|
const isCached = response.length > 0 && response[0].cached;
|
||||||
|
const cacheNotice = document.getElementById('wifi-scan-cache-notice');
|
||||||
|
if (cacheNotice) cacheNotice.style.display = isCached ? '' : 'none';
|
||||||
|
|
||||||
|
document.getElementById('wifi-scan-empty').style.display = 'none';
|
||||||
|
document.getElementById('wifi-scan-table').style.display = '';
|
||||||
|
|
||||||
|
response.forEach(network => {
|
||||||
|
const row = document.createElement("tr");
|
||||||
|
|
||||||
|
const ssidCell = document.createElement("td");
|
||||||
|
ssidCell.textContent = network.SSID.length > 25 ? network.SSID.substring(0, 25) + '...' : network.SSID;
|
||||||
|
row.appendChild(ssidCell);
|
||||||
|
|
||||||
|
const signalCell = document.createElement("td");
|
||||||
|
signalCell.innerHTML = getSignalBadge(network.SIGNAL);
|
||||||
|
row.appendChild(signalCell);
|
||||||
|
|
||||||
|
const securityCell = document.createElement("td");
|
||||||
|
securityCell.textContent = network.SECURITY || '--';
|
||||||
|
securityCell.classList.add('text-muted');
|
||||||
|
row.appendChild(securityCell);
|
||||||
|
|
||||||
|
const buttonCell = document.createElement("td");
|
||||||
|
buttonCell.classList.add('text-end');
|
||||||
|
const button = document.createElement("button");
|
||||||
|
button.textContent = "Connecter";
|
||||||
|
button.classList.add("btn", "btn-primary", "btn-sm");
|
||||||
|
button.addEventListener("click", () => wifi_connect(network.SSID));
|
||||||
|
buttonCell.appendChild(button);
|
||||||
|
row.appendChild(buttonCell);
|
||||||
|
|
||||||
|
tableBody.appendChild(row);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
error: function(xhr, status, error) {
|
||||||
|
console.error('AJAX request failed:', status, error);
|
||||||
|
document.getElementById('wifi-scan-empty').innerHTML = '<span class="text-danger">Erreur lors du scan</span>';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
window.onload = function() {
|
window.onload = function() {
|
||||||
fetch('../config.json') // Replace 'deviceID.txt' with 'config.json'
|
$.ajax({
|
||||||
.then(response => response.json()) // Parse response as JSON
|
url: 'launcher.php?type=get_config_sqlite',
|
||||||
.then(data => {
|
dataType: 'json',
|
||||||
console.log("Getting config file (onload)");
|
method: 'GET',
|
||||||
//get device ID
|
success: function(data) {
|
||||||
const deviceID = data.deviceID.trim().toUpperCase();
|
console.log("Getting config (onload)");
|
||||||
//document.getElementById('pageTitle_plus_ID').innerText = 'token: ' + deviceID;
|
|
||||||
|
|
||||||
//get device Name
|
|
||||||
const deviceName = data.deviceName;
|
const deviceName = data.deviceName;
|
||||||
|
|
||||||
const elements = document.querySelectorAll('.sideBar_sensorName');
|
function updateSidebarDeviceName(deviceName) {
|
||||||
elements.forEach((element) => {
|
const elements = document.querySelectorAll('.sideBar_sensorName');
|
||||||
element.innerText = deviceName;
|
if (elements.length > 0) {
|
||||||
});
|
elements.forEach((element) => {
|
||||||
|
element.innerText = deviceName;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
//device name html page title
|
if (deviceName) {
|
||||||
if (response.deviceName) {
|
updateSidebarDeviceName(deviceName);
|
||||||
document.title = response.deviceName;
|
setTimeout(() => updateSidebarDeviceName(deviceName), 100);
|
||||||
}
|
setTimeout(() => updateSidebarDeviceName(deviceName), 500);
|
||||||
|
document.title = deviceName;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
//get wifi connection status
|
|
||||||
const WIFI_statusElement = document.getElementById("wifi-status");
|
const WIFI_statusElement = document.getElementById("wifi-status");
|
||||||
console.log("WIFI is: " + data.WIFI_status);
|
|
||||||
|
|
||||||
if (data.WIFI_status === "connected") {
|
if (data.WIFI_status === "connected") {
|
||||||
WIFI_statusElement.textContent = "Connected";
|
WIFI_statusElement.textContent = "Connected";
|
||||||
WIFI_statusElement.className = "badge text-bg-success";
|
WIFI_statusElement.className = "badge text-bg-success";
|
||||||
|
document.getElementById('btn-forget-wifi').style.display = 'inline-block';
|
||||||
|
document.getElementById('card-connection-info').style.display = '';
|
||||||
|
document.getElementById('card-hotspot-info').style.display = 'none';
|
||||||
|
document.getElementById('card-wifi-scan').style.display = 'none';
|
||||||
|
get_internet();
|
||||||
} else if (data.WIFI_status === "hotspot") {
|
} else if (data.WIFI_status === "hotspot") {
|
||||||
WIFI_statusElement.textContent = "Hotspot";
|
WIFI_statusElement.textContent = "Hotspot";
|
||||||
WIFI_statusElement.className = "badge text-bg-warning";
|
WIFI_statusElement.className = "badge text-bg-warning";
|
||||||
|
document.getElementById('btn-forget-wifi').style.display = 'none';
|
||||||
|
document.getElementById('card-connection-info').style.display = 'none';
|
||||||
|
document.getElementById('card-hotspot-info').style.display = '';
|
||||||
|
document.getElementById('card-wifi-scan').style.display = '';
|
||||||
|
wifi_scan();
|
||||||
} else {
|
} else {
|
||||||
WIFI_statusElement.textContent = "Unknown";
|
WIFI_statusElement.textContent = "Unknown";
|
||||||
WIFI_statusElement.className = "badge text-bg-secondary";
|
WIFI_statusElement.className = "badge text-bg-secondary";
|
||||||
|
document.getElementById('btn-forget-wifi').style.display = 'none';
|
||||||
|
document.getElementById('card-connection-info').style.display = 'none';
|
||||||
|
document.getElementById('card-hotspot-info').style.display = 'none';
|
||||||
|
document.getElementById('card-wifi-scan').style.display = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
//get local RTC
|
// Update hotspot badge in sidebar
|
||||||
|
document.querySelectorAll('.sidebar-hotspot-badge').forEach(function(badge) {
|
||||||
|
badge.style.display = (data.WIFI_status === 'hotspot') ? '' : 'none';
|
||||||
|
});
|
||||||
|
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: 'launcher.php?type=RTC_time',
|
url: 'launcher.php?type=RTC_time',
|
||||||
dataType: 'text', // Specify that you expect a JSON response
|
dataType: 'text',
|
||||||
method: 'GET', // Use GET or POST depending on your needs
|
method: 'GET',
|
||||||
success: function(response) {
|
success: function(response) {
|
||||||
console.log("Local RTC: " + response);
|
|
||||||
const RTC_Element = document.getElementById("RTC_time");
|
const RTC_Element = document.getElementById("RTC_time");
|
||||||
RTC_Element.textContent = response;
|
RTC_Element.textContent = response;
|
||||||
},
|
},
|
||||||
@@ -338,10 +482,12 @@ function get_internet(){
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
},
|
||||||
})
|
error: function(xhr, status, error) {
|
||||||
.catch(error => console.error('Error loading config.json:', error));
|
console.error('AJAX request failed:', status, error);
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -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"
|
||||||
@@ -81,36 +81,92 @@ fi
|
|||||||
info "Set config..."
|
info "Set config..."
|
||||||
if [[ -f "$REPO_DIR/sqlite/set_config.py" ]]; then
|
if [[ -f "$REPO_DIR/sqlite/set_config.py" ]]; then
|
||||||
sudo /usr/bin/python3 "$REPO_DIR/sqlite/set_config.py" || error "Failed to set config."
|
sudo /usr/bin/python3 "$REPO_DIR/sqlite/set_config.py" || error "Failed to set config."
|
||||||
success "Databases created successfully."
|
success "Databases set configuration successfully."
|
||||||
else
|
else
|
||||||
warning "Database creation script not found."
|
warning "Database set configuration script not found."
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Configure Apache
|
# Configure Apache
|
||||||
info "Configuring Apache..."
|
info "Configuring Apache..."
|
||||||
APACHE_CONF="/etc/apache2/sites-available/000-default.conf"
|
APACHE_CONF="/etc/apache2/sites-available/000-default.conf"
|
||||||
if grep -q "DocumentRoot /var/www/nebuleair_pro_4g" "$APACHE_CONF"; then
|
if grep -q "DocumentRoot /var/www/nebuleair_pro_4g" "$APACHE_CONF"; then
|
||||||
warning "Apache configuration already set. Skipping."
|
warning "Apache DocumentRoot already set. Skipping."
|
||||||
else
|
else
|
||||||
sudo sed -i 's|DocumentRoot /var/www/html|DocumentRoot /var/www/nebuleair_pro_4g|' "$APACHE_CONF"
|
sudo sed -i 's|DocumentRoot /var/www/html|DocumentRoot /var/www/nebuleair_pro_4g|' "$APACHE_CONF"
|
||||||
sudo systemctl reload apache2
|
success "Apache DocumentRoot updated."
|
||||||
success "Apache configuration updated and reloaded."
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Enable AllowOverride for .htaccess (needed for PHP upload limits)
|
||||||
|
APACHE_MAIN_CONF="/etc/apache2/apache2.conf"
|
||||||
|
if grep -q "AllowOverride All" "$APACHE_MAIN_CONF"; then
|
||||||
|
warning "AllowOverride already configured. Skipping."
|
||||||
|
else
|
||||||
|
# Replace AllowOverride None with AllowOverride All for /var/www/
|
||||||
|
sudo sed -i '/<Directory \/var\/www\/>/,/<\/Directory>/ s/AllowOverride None/AllowOverride All/' "$APACHE_MAIN_CONF"
|
||||||
|
success "AllowOverride All enabled for /var/www/."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Also increase PHP limits directly in php.ini as fallback
|
||||||
|
PHP_INI=$(php -r "echo php_ini_loaded_file();" 2>/dev/null)
|
||||||
|
if [[ -n "$PHP_INI" ]]; then
|
||||||
|
sudo sed -i 's/upload_max_filesize = .*/upload_max_filesize = 50M/' "$PHP_INI"
|
||||||
|
sudo sed -i 's/post_max_size = .*/post_max_size = 55M/' "$PHP_INI"
|
||||||
|
success "PHP upload limits set to 50M in $PHP_INI"
|
||||||
|
fi
|
||||||
|
|
||||||
|
sudo systemctl reload apache2
|
||||||
|
success "Apache configuration updated and reloaded."
|
||||||
|
|
||||||
# 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 *" | 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: /usr/bin/pkill *
|
||||||
|
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
|
||||||
|
|
||||||
# Open all UART serial ports (avoid duplication)
|
# Validate sudoers file after changes
|
||||||
info "Configuring UART serial ports..."
|
if ! sudo visudo -c; then
|
||||||
|
error "Sudoers file has syntax errors! Please fix manually with 'sudo visudo'"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Open all UART serial ports and disable HDMI + Bluetooth (avoid duplication)
|
||||||
|
info "Configuring UART serial ports and disabling Bluetooth to save power..."
|
||||||
if ! grep -q "enable_uart=1" /boot/firmware/config.txt; then
|
if ! grep -q "enable_uart=1" /boot/firmware/config.txt; then
|
||||||
echo -e "\nenable_uart=1\ndtoverlay=uart0\ndtoverlay=uart1\ndtoverlay=uart2\ndtoverlay=uart3\ndtoverlay=uart4\ndtoverlay=uart5" | sudo tee -a /boot/firmware/config.txt > /dev/null
|
echo -e "\nenable_uart=1\ndtoverlay=uart0\ndtoverlay=uart1\ndtoverlay=uart2\ndtoverlay=uart3\ndtoverlay=uart4\ndtoverlay=uart5\n\n# Disable Bluetooth to save power (~20-30mA)\ndtoverlay=disable-bt" | sudo tee -a /boot/firmware/config.txt > /dev/null
|
||||||
success "UART configuration added."
|
success "UART configuration, HDMI and Bluetooth disable added."
|
||||||
else
|
else
|
||||||
warning "UART configuration already set. Skipping."
|
warning "UART configuration already set. Skipping."
|
||||||
fi
|
fi
|
||||||
@@ -124,9 +180,12 @@ info "Enabling I2C ports..."
|
|||||||
sudo raspi-config nonint do_i2c 0
|
sudo raspi-config nonint do_i2c 0
|
||||||
success "I2C ports enabled."
|
success "I2C ports enabled."
|
||||||
|
|
||||||
#creates databases
|
# Final sudoers check
|
||||||
info "Creates sqlites databases..."
|
if sudo visudo -c; then
|
||||||
/usr/bin/python3 /var/www/nebuleair_pro_4g/sqlite/create_db.py
|
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!"
|
||||||
|
|||||||
@@ -31,23 +31,46 @@ info "Set up the RTC"
|
|||||||
info "Wake Up SARA"
|
info "Wake Up SARA"
|
||||||
pinctrl set 16 op
|
pinctrl set 16 op
|
||||||
pinctrl set 16 dh
|
pinctrl set 16 dh
|
||||||
sleep 5
|
|
||||||
|
|
||||||
#Check SARA connection
|
info "Waiting for SARA to wake up..."
|
||||||
info "Check SARA connection"
|
for i in {1..8}; do
|
||||||
/usr/bin/python3 /var/www/nebuleair_pro_4g/SARA/sara.py ttyAMA2 ATI 2
|
echo -ne " $i/8 seconds\r"
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
echo -e "\n"
|
||||||
|
|
||||||
|
#Check SARA connection (ATI)
|
||||||
|
info "Check SARA connection (ATI)"
|
||||||
|
/usr/bin/python3 /var/www/nebuleair_pro_4g/SARA/sara.py ttyAMA2 ATI 2 || warning "SARA not detected (ATI). Continuing..."
|
||||||
|
|
||||||
|
sleep 1
|
||||||
|
|
||||||
#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 || warning "SARA LED activation failed. Continuing..."
|
||||||
|
|
||||||
|
sleep 1
|
||||||
|
|
||||||
|
#get SIM card CCID
|
||||||
|
info "Get SIM card CCID"
|
||||||
|
/usr/bin/python3 /var/www/nebuleair_pro_4g/SARA/sara.py ttyAMA2 AT+CCID? 2 || warning "SARA CCID read failed. Continuing..."
|
||||||
|
|
||||||
|
sleep 1
|
||||||
|
|
||||||
|
#get SIM card IMSI
|
||||||
|
info "Get SIM card IMSI"
|
||||||
|
imsi_output=$(/usr/bin/python3 /var/www/nebuleair_pro_4g/SARA/sara.py ttyAMA2 AT+CIMI 2 2>&1) || warning "SARA IMSI read failed. Continuing..."
|
||||||
|
echo "$imsi_output"
|
||||||
|
# Extract IMSI (15-digit numeric string)
|
||||||
|
imsi_number=$(echo "$imsi_output" | grep -oP '^\d{15}$' || echo "N/A")
|
||||||
|
|
||||||
#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
|
||||||
@@ -100,3 +123,28 @@ sudo systemctl enable rtc_save_to_db.service
|
|||||||
# Start the service immediately
|
# Start the service immediately
|
||||||
info "Starting the service..."
|
info "Starting the service..."
|
||||||
sudo systemctl start rtc_save_to_db.service
|
sudo systemctl start rtc_save_to_db.service
|
||||||
|
|
||||||
|
# Display device information
|
||||||
|
echo ""
|
||||||
|
echo "=========================================="
|
||||||
|
echo -e "${GREEN} Installation Complete!${NC}"
|
||||||
|
echo "=========================================="
|
||||||
|
|
||||||
|
# Get Raspberry Pi serial number (last 8 characters)
|
||||||
|
serial_number=$(cat /proc/cpuinfo | grep Serial | awk '{print substr($3, length($3) - 7)}' | tr '[:lower:]' '[:upper:]')
|
||||||
|
echo -e "${BLUE}Device ID (Serial):${NC} $serial_number"
|
||||||
|
|
||||||
|
# Display IMSI
|
||||||
|
echo -e "${BLUE}IMSI:${NC} ${imsi_number:-N/A}"
|
||||||
|
|
||||||
|
# Get IP address and make it a clickable link
|
||||||
|
ip_wlan0=$(ip -4 addr show wlan0 2>/dev/null | grep -oP '(?<=inet\s)\d+(\.\d+){3}' || echo "Not connected")
|
||||||
|
|
||||||
|
if [[ "$ip_wlan0" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
||||||
|
admin_url="http://${ip_wlan0}/html/admin.html"
|
||||||
|
echo -e "${BLUE}IP Address (wlan0):${NC} ${admin_url}"
|
||||||
|
else
|
||||||
|
echo -e "${BLUE}IP Address (wlan0):${NC} $ip_wlan0"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "=========================================="
|
||||||
|
|||||||
107
loop/AUDIT_SARA_send_data_v2.md
Normal file
107
loop/AUDIT_SARA_send_data_v2.md
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
# Audit SARA_send_data_v2.py
|
||||||
|
|
||||||
|
Date: 2026-03-14
|
||||||
|
|
||||||
|
## Correction deja appliquee
|
||||||
|
|
||||||
|
**Bug AT+USOWR leak dans payload UDP Miotiq** — Le device_id recu par Miotiq etait `41542b55534f5752` = `AT+USOWR` (la commande AT elle-meme).
|
||||||
|
|
||||||
|
Cause: desynchronisation serie entre le script et le modem. Le code envoyait les donnees binaires sans verifier que le modem avait bien envoye le prompt `@`.
|
||||||
|
|
||||||
|
Corrections appliquees dans la section UDP (send_miotiq):
|
||||||
|
- `ser_sara.reset_input_buffer()` avant chaque commande AT critique
|
||||||
|
- Verification que `"@" in response` avant d'envoyer les donnees binaires
|
||||||
|
- Abort propre a chaque etape via `socket_id = None` si creation socket, connexion, ou prompt `@` echoue
|
||||||
|
- Retry `AT+USOCR=17` apres un PDP reset reussi
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Bugs critiques restants
|
||||||
|
|
||||||
|
### 1. Double `\r` sur plusieurs commandes AT
|
||||||
|
|
||||||
|
Certaines commandes AT ont `\r` dans le f-string ET un `+ '\r'` lors du write, ce qui envoie `\r\r` au modem.
|
||||||
|
|
||||||
|
**Lignes concernees:**
|
||||||
|
- **Ligne 988**: `command = f'AT+CSQ\r'` puis `ser_sara.write((command + '\r')...)`
|
||||||
|
- **Lignes 588, 628, 646, 656, 666, 674, 682, 690, 698**: fonctions `reset_server_hostname` et `reset_server_hostname_https`
|
||||||
|
- **Lignes 1403, 1541, 1570, 1694, 1741**: sections AirCarto et uSpot
|
||||||
|
|
||||||
|
Le modem tolere souvent le double `\r`, mais ca peut generer des reponses parasites dans le buffer serie et contribuer a des bugs de desynchronisation.
|
||||||
|
|
||||||
|
**Correction**: retirer le `\r` du f-string OU retirer le `+ '\r'` dans le write. Choisir une convention unique.
|
||||||
|
|
||||||
|
### 2. Double guillemet dans AT+URDFILE (ligne 1402)
|
||||||
|
|
||||||
|
```python
|
||||||
|
command = f'AT+URDFILE="aircarto_server_response.txt""\r'
|
||||||
|
# ^^ double "
|
||||||
|
```
|
||||||
|
|
||||||
|
Le `"` en trop peut causer une erreur AT ou une reponse inattendue.
|
||||||
|
|
||||||
|
**Correction**: `command = f'AT+URDFILE="aircarto_server_response.txt"\r'`
|
||||||
|
|
||||||
|
### 3. Crash si table SQLite vide (lignes 781-786, 820-826, 868-880)
|
||||||
|
|
||||||
|
```python
|
||||||
|
rows = cursor.fetchall()
|
||||||
|
data_values = [row[2:] for row in rows]
|
||||||
|
averages = [round(sum(col) / len(col),1) for col in zip(*data_values)]
|
||||||
|
```
|
||||||
|
|
||||||
|
Si `data_NPM`, `data_NPM_5channels` ou `data_envea` est vide, `data_values` sera `[]` et le calcul d'average crashera (IndexError / division par zero).
|
||||||
|
|
||||||
|
**Correction**: ajouter un check `if rows:` avant le calcul, comme c'est deja fait pour BME280 (ligne 840), wind (ligne 912) et MPPT (ligne 936).
|
||||||
|
|
||||||
|
### 4. Overflow struct.pack sur valeurs negatives (class SensorPayload)
|
||||||
|
|
||||||
|
```python
|
||||||
|
def set_npm_core(self, pm1, pm25, pm10):
|
||||||
|
self.payload[10:12] = struct.pack('>H', int(pm1 * 10)) # H = unsigned 16-bit
|
||||||
|
```
|
||||||
|
|
||||||
|
Si un capteur retourne une valeur negative (erreur capteur, -1, etc.), `struct.pack('>H', -10)` leve `struct.error`. Concerne: `set_npm_core`, `set_noise`, `set_envea`, `set_npm_5channels`, `set_wind`, `set_mppt` (sauf battery_current et temperatures qui utilisent `'>h'` signe).
|
||||||
|
|
||||||
|
**Correction**: clamper les valeurs avant pack: `max(0, int(value * 10))` pour les champs unsigned, ou verifier `value >= 0` avant le pack.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Problemes importants
|
||||||
|
|
||||||
|
### 5. Port serie et SQLite jamais fermes
|
||||||
|
|
||||||
|
`ser_sara` (ligne 248) et `conn` (ligne 155) sont ouverts mais jamais fermes, meme dans le bloc `except` final (ligne 1755). Si le script crash, le port serie peut rester verrouille pour le prochain cycle.
|
||||||
|
|
||||||
|
**Correction**: ajouter un bloc `finally` apres le `except` (ligne 1757):
|
||||||
|
```python
|
||||||
|
finally:
|
||||||
|
ser_sara.close()
|
||||||
|
conn.close()
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6. Code mort dans reset_server_hostname_https (lignes 717-722)
|
||||||
|
|
||||||
|
```python
|
||||||
|
if profile_id == 1: # ligne 613
|
||||||
|
...
|
||||||
|
elif profile_id == 1: # ligne 718 — jamais atteint car meme condition
|
||||||
|
pass
|
||||||
|
```
|
||||||
|
|
||||||
|
Copie-colle de `reset_server_hostname`. Le elif est mort.
|
||||||
|
|
||||||
|
**Correction**: supprimer le bloc elif (lignes 717-722).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Resume
|
||||||
|
|
||||||
|
| # | Type | Description | Lignes |
|
||||||
|
|----|----------|------------------------------------------|----------------|
|
||||||
|
| 1 | Bug | Double \r sur commandes AT | 988, 588+, ... |
|
||||||
|
| 2 | Bug | Double guillemet AT+URDFILE | 1402 |
|
||||||
|
| 3 | Crash | Table SQLite vide -> IndexError | 781, 820, 868 |
|
||||||
|
| 4 | Crash | struct.pack overflow valeur negative | SensorPayload |
|
||||||
|
| 5 | Cleanup | Serial/SQLite jamais fermes (finally) | 248, 155 |
|
||||||
|
| 6 | Cleanup | Code mort elif profile_id==1 | 717-722 |
|
||||||
File diff suppressed because it is too large
Load Diff
322
loop/error_flags.md
Normal file
322
loop/error_flags.md
Normal file
@@ -0,0 +1,322 @@
|
|||||||
|
# Error Flags — UDP Payload Miotiq (Bytes 66-68)
|
||||||
|
|
||||||
|
## Principe
|
||||||
|
|
||||||
|
Les bytes 66, 67 et 68 de la payload UDP (100 bytes) sont utilises comme registres d'erreurs
|
||||||
|
et d'etat. Chaque bit represente un etat independant. Plusieurs flags peuvent
|
||||||
|
etre actifs simultanement.
|
||||||
|
|
||||||
|
- **Byte 66** : erreurs systeme (RTC, capteurs)
|
||||||
|
- **Byte 67** : status NextPM (registre interne du capteur)
|
||||||
|
- **Byte 68** : status device (etat general du boitier)
|
||||||
|
|
||||||
|
## Position dans la payload
|
||||||
|
|
||||||
|
```
|
||||||
|
Bytes 0-65 : donnees capteurs (existant)
|
||||||
|
Byte 66 : error_flags (erreurs systeme)
|
||||||
|
Byte 67 : npm_status (status NextPM)
|
||||||
|
Byte 68 : device_status (etat general du boitier)
|
||||||
|
Bytes 69-99 : reserved (initialises a 0xFF)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Byte 66 — Error Flags (erreurs systeme)
|
||||||
|
|
||||||
|
Chaque bit represente une erreur detectee par le script d'envoi (`SARA_send_data_v2.py`).
|
||||||
|
|
||||||
|
| Bit | Masque | Nom | Description | Source |
|
||||||
|
|-----|--------|-------------------|--------------------------------------------------|-------------------------------|
|
||||||
|
| 0 | 0x01 | RTC_DISCONNECTED | Module RTC DS3231 non detecte sur le bus I2C | timestamp_table → 'not connected' |
|
||||||
|
| 1 | 0x02 | RTC_RESET | RTC en date par defaut (annee 2000) | timestamp_table → annee 2000 |
|
||||||
|
| 2 | 0x04 | BME280_ERROR | Capteur BME280 non detecte ou erreur de lecture | data_BME280 → valeurs a 0 |
|
||||||
|
| 3 | 0x08 | NPM_ERROR | Capteur NextPM non detecte ou erreur communication| data_NPM → toutes valeurs a 0 |
|
||||||
|
| 4 | 0x10 | ENVEA_ERROR | Capteurs Envea non detectes ou erreur serie | data_envea → valeurs a 0 |
|
||||||
|
| 5 | 0x20 | NOISE_ERROR | Capteur bruit NSRT MK4 non detecte ou erreur | data_noise → valeurs a 0 |
|
||||||
|
| 6 | 0x40 | MPPT_ERROR | Chargeur solaire MPPT non detecte ou erreur | data_MPPT → valeurs a 0 |
|
||||||
|
| 7 | 0x80 | WIND_ERROR | Capteur vent non detecte ou erreur | data_windMeter → valeurs a 0 |
|
||||||
|
|
||||||
|
### Detection des erreurs
|
||||||
|
|
||||||
|
Les scripts de collecte (`get_data_modbus_v3.py`, `get_data_v2.py`, etc.) ecrivent des **0**
|
||||||
|
en base SQLite quand un capteur ne repond pas. Le script d'envoi (`SARA_send_data_v2.py`)
|
||||||
|
lit ces valeurs et peut detecter l'erreur quand toutes les valeurs d'un capteur sont a 0.
|
||||||
|
|
||||||
|
Pour le RTC, le champ `timestamp_table` contient directement `'not connected'` ou une date
|
||||||
|
en annee 2000 quand le module est deconnecte ou reinitialise.
|
||||||
|
|
||||||
|
### Exemples de valeurs
|
||||||
|
|
||||||
|
| Valeur dec | Hex | Signification |
|
||||||
|
|------------|------|---------------------------------------|
|
||||||
|
| 0 | 0x00 | Aucune erreur |
|
||||||
|
| 1 | 0x01 | RTC deconnecte |
|
||||||
|
| 2 | 0x02 | RTC reset (annee 2000) |
|
||||||
|
| 5 | 0x05 | RTC deconnecte + BME280 erreur |
|
||||||
|
| 9 | 0x09 | RTC deconnecte + NPM erreur |
|
||||||
|
| 255 | 0xFF | Toutes les erreurs (cas extreme) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Byte 67 — NPM Status (registre interne NextPM)
|
||||||
|
|
||||||
|
Le capteur NextPM possede un registre de status sur 9 bits (registre Modbus).
|
||||||
|
On stocke les 8 bits bas dans le byte 67. Ce registre est lu directement depuis
|
||||||
|
le capteur via Modbus, pas depuis SQLite.
|
||||||
|
|
||||||
|
| Bit | Masque | Nom | Description | Severite |
|
||||||
|
|-----|--------|-----------------|-----------------------------------------------------------------------|----------------|
|
||||||
|
| 0 | 0x01 | SLEEP_STATE | Capteur en veille (commande sleep). Seule la lecture status autorisee | Info |
|
||||||
|
| 1 | 0x02 | DEGRADED_STATE | Erreur mineure confirmee. Mesures possibles mais precision reduite | Warning |
|
||||||
|
| 2 | 0x04 | NOT_READY | Demarrage en cours (15s apres mise sous tension). Mesures non fiables | Info |
|
||||||
|
| 3 | 0x08 | HEAT_ERROR | Humidite relative > 60% pendant > 10 minutes | Warning |
|
||||||
|
| 4 | 0x10 | TRH_ERROR | Capteur T/RH interne hors specification | Warning |
|
||||||
|
| 5 | 0x20 | FAN_ERROR | Vitesse ventilateur hors plage (tourne encore) | Warning |
|
||||||
|
| 6 | 0x40 | MEMORY_ERROR | Acces memoire impossible, fonctions internes limitees | Warning |
|
||||||
|
| 7 | 0x80 | LASER_ERROR | Aucune particule detectee pendant > 240s, possible erreur laser | Warning |
|
||||||
|
|
||||||
|
Note : le bit 8 du registre NextPM (DEFAULT_STATE — ventilateur arrete apres 3 tentatives)
|
||||||
|
ne tient pas dans un byte. Si necessaire, il peut etre combine avec le bit 0 (SLEEP_STATE)
|
||||||
|
car les deux indiquent un capteur inactif.
|
||||||
|
|
||||||
|
### Exemples de valeurs
|
||||||
|
|
||||||
|
| Valeur dec | Hex | Signification |
|
||||||
|
|------------|------|--------------------------------------------|
|
||||||
|
| 0 | 0x00 | Capteur OK, mesures fiables |
|
||||||
|
| 4 | 0x04 | Demarrage en cours (NOT_READY) |
|
||||||
|
| 8 | 0x08 | Erreur humidite (HEAT_ERROR) |
|
||||||
|
| 32 | 0x20 | Erreur ventilateur (FAN_ERROR) |
|
||||||
|
| 128 | 0x80 | Possible erreur laser (LASER_ERROR) |
|
||||||
|
| 40 | 0x28 | HEAT_ERROR + FAN_ERROR |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Byte 68 — Device Status (etat general du boitier)
|
||||||
|
|
||||||
|
Flags d'etat du device, determines par le script d'envoi (`SARA_send_data_v2.py`).
|
||||||
|
Ces flags donnent du contexte sur l'etat general du boitier pour le diagnostic a distance.
|
||||||
|
|
||||||
|
| Bit | Masque | Nom | Description | Source |
|
||||||
|
|-----|--------|----------------------|----------------------------------------------------------------|-------------------------------------|
|
||||||
|
| 0 | 0x01 | SARA_REBOOTED | Le modem a ete reboot hardware au cycle precedent | flag fichier ou SQLite |
|
||||||
|
| 1 | 0x02 | WIFI_CONNECTED | Le device est connecte en WiFi (atelier/maintenance) | nmcli device status |
|
||||||
|
| 2 | 0x04 | HOTSPOT_ACTIVE | Le hotspot WiFi est actif (configuration en cours) | nmcli device status |
|
||||||
|
| 3 | 0x08 | GPS_NO_FIX | Pas de position GPS valide | config_table latitude/longitude |
|
||||||
|
| 4 | 0x10 | BATTERY_LOW | Tension batterie sous seuil critique | data_MPPT → battery_voltage |
|
||||||
|
| 5 | 0x20 | DISK_FULL | Espace disque critique sur la Pi (< 5%) | os.statvfs ou shutil.disk_usage |
|
||||||
|
| 6 | 0x40 | DB_ERROR | Erreur d'acces a la base SQLite | try/except sur connexion SQLite |
|
||||||
|
| 7 | 0x80 | BOOT_RECENT | Le device a redemarre recemment (uptime < 5 min) | /proc/uptime |
|
||||||
|
|
||||||
|
### Exemples de valeurs
|
||||||
|
|
||||||
|
| Valeur dec | Hex | Signification |
|
||||||
|
|------------|------|--------------------------------------------------|
|
||||||
|
| 0 | 0x00 | Tout est normal |
|
||||||
|
| 1 | 0x01 | Modem reboot au cycle precedent |
|
||||||
|
| 2 | 0x02 | WiFi connecte (probablement en atelier) |
|
||||||
|
| 6 | 0x06 | WiFi + hotspot actifs (configuration en cours) |
|
||||||
|
| 128 | 0x80 | Boot recent (uptime < 5 min) |
|
||||||
|
| 145 | 0x91 | Modem reboot + batterie faible + boot recent |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Implementation
|
||||||
|
|
||||||
|
### Etape 1 : Lire le status NPM depuis le capteur
|
||||||
|
|
||||||
|
Le script `NPM/get_data_modbus_v3.py` doit etre modifie pour :
|
||||||
|
1. Lire le registre de status du NextPM (adresse Modbus a determiner)
|
||||||
|
2. Stocker le status byte dans une nouvelle colonne SQLite (ex: `npm_status` dans `data_NPM`)
|
||||||
|
|
||||||
|
### Etape 2 : Construire les flags dans SARA_send_data_v2.py
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Constantes error_flags (byte 66)
|
||||||
|
ERR_RTC_DISCONNECTED = 0x01
|
||||||
|
ERR_RTC_RESET = 0x02
|
||||||
|
ERR_BME280 = 0x04
|
||||||
|
ERR_NPM = 0x08
|
||||||
|
ERR_ENVEA = 0x10
|
||||||
|
ERR_NOISE = 0x20
|
||||||
|
ERR_MPPT = 0x40
|
||||||
|
ERR_WIND = 0x80
|
||||||
|
|
||||||
|
# Constantes device_status (byte 68)
|
||||||
|
DEV_SARA_REBOOTED = 0x01
|
||||||
|
DEV_WIFI_CONNECTED = 0x02
|
||||||
|
DEV_HOTSPOT_ACTIVE = 0x04
|
||||||
|
DEV_GPS_NO_FIX = 0x08
|
||||||
|
DEV_BATTERY_LOW = 0x10
|
||||||
|
DEV_DISK_FULL = 0x20
|
||||||
|
DEV_DB_ERROR = 0x40
|
||||||
|
DEV_BOOT_RECENT = 0x80
|
||||||
|
|
||||||
|
# Construction byte 66
|
||||||
|
error_flags = 0x00
|
||||||
|
|
||||||
|
if rtc_status == "disconnected":
|
||||||
|
error_flags |= ERR_RTC_DISCONNECTED
|
||||||
|
if rtc_status == "reset":
|
||||||
|
error_flags |= ERR_RTC_RESET
|
||||||
|
if PM1 == 0 and PM25 == 0 and PM10 == 0:
|
||||||
|
error_flags |= ERR_NPM
|
||||||
|
# ... autres capteurs
|
||||||
|
|
||||||
|
payload.set_error_flags(error_flags)
|
||||||
|
|
||||||
|
# Construction byte 67 (lu depuis SQLite, ecrit par get_data_modbus_v3.py)
|
||||||
|
npm_status = get_npm_status_from_db() # 0-255
|
||||||
|
payload.set_npm_status(npm_status)
|
||||||
|
|
||||||
|
# Construction byte 68
|
||||||
|
device_status = 0x00
|
||||||
|
|
||||||
|
if sara_was_rebooted(): # flag fichier persistant
|
||||||
|
device_status |= DEV_SARA_REBOOTED
|
||||||
|
if check_wifi_connected(): # nmcli device status
|
||||||
|
device_status |= DEV_WIFI_CONNECTED
|
||||||
|
if check_hotspot_active(): # nmcli device status
|
||||||
|
device_status |= DEV_HOTSPOT_ACTIVE
|
||||||
|
if latitude == 0.0 and longitude == 0.0: # config_table
|
||||||
|
device_status |= DEV_GPS_NO_FIX
|
||||||
|
if battery_voltage < 11.0: # data_MPPT seuil a ajuster
|
||||||
|
device_status |= DEV_BATTERY_LOW
|
||||||
|
if check_disk_usage() > 95: # shutil.disk_usage
|
||||||
|
device_status |= DEV_DISK_FULL
|
||||||
|
# DEV_DB_ERROR: set dans le try/except de la connexion SQLite
|
||||||
|
if get_uptime_seconds() < 300: # /proc/uptime
|
||||||
|
device_status |= DEV_BOOT_RECENT
|
||||||
|
|
||||||
|
payload.set_device_status(device_status)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Etape 3 : Ajouter les methodes dans SensorPayload
|
||||||
|
|
||||||
|
```python
|
||||||
|
def set_error_flags(self, flags):
|
||||||
|
"""Set system error flags (byte 66)"""
|
||||||
|
self.payload[66] = flags & 0xFF
|
||||||
|
|
||||||
|
def set_npm_status(self, status):
|
||||||
|
"""Set NextPM status register (byte 67)"""
|
||||||
|
self.payload[67] = status & 0xFF
|
||||||
|
|
||||||
|
def set_device_status(self, status):
|
||||||
|
"""Set device status flags (byte 68)"""
|
||||||
|
self.payload[68] = status & 0xFF
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Parser Miotiq
|
||||||
|
|
||||||
|
```
|
||||||
|
16|device_id|string|||W
|
||||||
|
2|signal_quality|hex2dec|dB||
|
||||||
|
2|version|hex2dec|||W
|
||||||
|
4|ISO_68|hex2dec|ugm3|x/10|
|
||||||
|
4|ISO_39|hex2dec|ugm3|x/10|
|
||||||
|
4|ISO_24|hex2dec|ugm3|x/10|
|
||||||
|
4|ISO_54|hex2dec|degC|x/100|
|
||||||
|
4|ISO_55|hex2dec|%|x/100|
|
||||||
|
4|ISO_53|hex2dec|hPa||
|
||||||
|
4|noise_cur_leq|hex2dec|dB|x/10|
|
||||||
|
4|noise_cur_level|hex2dec|dB|x/10|
|
||||||
|
4|max_noise|hex2dec|dB|x/10|
|
||||||
|
4|ISO_03|hex2dec|ppb||
|
||||||
|
4|ISO_05|hex2dec|ppb||
|
||||||
|
4|ISO_21|hex2dec|ppb||
|
||||||
|
4|ISO_04|hex2dec|ppb||
|
||||||
|
4|ISO_08|hex2dec|ppb||
|
||||||
|
4|npm_ch1|hex2dec|count||
|
||||||
|
4|npm_ch2|hex2dec|count||
|
||||||
|
4|npm_ch3|hex2dec|count||
|
||||||
|
4|npm_ch4|hex2dec|count||
|
||||||
|
4|npm_ch5|hex2dec|count||
|
||||||
|
4|npm_temp|hex2dec|°C|x/10|
|
||||||
|
4|npm_humidity|hex2dec|%|x/10|
|
||||||
|
4|battery_voltage|hex2dec|V|x/100|
|
||||||
|
4|battery_current|hex2dec|A|x/100|
|
||||||
|
4|solar_voltage|hex2dec|V|x/100|
|
||||||
|
4|solar_power|hex2dec|W||
|
||||||
|
4|charger_status|hex2dec|||
|
||||||
|
4|wind_speed|hex2dec|m/s|x/10|
|
||||||
|
4|wind_direction|hex2dec|degrees||
|
||||||
|
2|error_flags|hex2dec|||
|
||||||
|
2|npm_status|hex2dec|||
|
||||||
|
2|device_status|hex2dec|||
|
||||||
|
2|version_major|hex2dec|||
|
||||||
|
2|version_minor|hex2dec|||
|
||||||
|
2|version_patch|hex2dec|||
|
||||||
|
22|reserved|skip|||
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Lecture cote serveur (exemple Python)
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Byte 66 — erreurs systeme
|
||||||
|
error_flags = int(parsed_error_flags)
|
||||||
|
|
||||||
|
rtc_disconnected = bool(error_flags & 0x01)
|
||||||
|
rtc_reset = bool(error_flags & 0x02)
|
||||||
|
bme280_error = bool(error_flags & 0x04)
|
||||||
|
npm_error = bool(error_flags & 0x08)
|
||||||
|
envea_error = bool(error_flags & 0x10)
|
||||||
|
noise_error = bool(error_flags & 0x20)
|
||||||
|
mppt_error = bool(error_flags & 0x40)
|
||||||
|
wind_error = bool(error_flags & 0x80)
|
||||||
|
|
||||||
|
# Byte 67 — status NextPM
|
||||||
|
npm_status = int(parsed_npm_status)
|
||||||
|
|
||||||
|
npm_sleep = bool(npm_status & 0x01)
|
||||||
|
npm_degraded = bool(npm_status & 0x02)
|
||||||
|
npm_not_ready = bool(npm_status & 0x04)
|
||||||
|
npm_heat_err = bool(npm_status & 0x08)
|
||||||
|
npm_trh_err = bool(npm_status & 0x10)
|
||||||
|
npm_fan_err = bool(npm_status & 0x20)
|
||||||
|
npm_mem_err = bool(npm_status & 0x40)
|
||||||
|
npm_laser_err = bool(npm_status & 0x80)
|
||||||
|
|
||||||
|
# Byte 68 — status device
|
||||||
|
device_status = int(parsed_device_status)
|
||||||
|
|
||||||
|
sara_rebooted = bool(device_status & 0x01)
|
||||||
|
wifi_connected = bool(device_status & 0x02)
|
||||||
|
hotspot_active = bool(device_status & 0x04)
|
||||||
|
gps_no_fix = bool(device_status & 0x08)
|
||||||
|
battery_low = bool(device_status & 0x10)
|
||||||
|
disk_full = bool(device_status & 0x20)
|
||||||
|
db_error = bool(device_status & 0x40)
|
||||||
|
boot_recent = bool(device_status & 0x80)
|
||||||
|
|
||||||
|
# Alertes
|
||||||
|
if rtc_disconnected:
|
||||||
|
alert("RTC module deconnecte — verifier pile/cables I2C")
|
||||||
|
if npm_fan_err:
|
||||||
|
alert("NextPM: ventilateur hors plage — maintenance requise")
|
||||||
|
if npm_laser_err:
|
||||||
|
alert("NextPM: possible erreur laser — verifier le capteur")
|
||||||
|
if battery_low:
|
||||||
|
alert("Batterie faible — verifier alimentation solaire")
|
||||||
|
if disk_full:
|
||||||
|
alert("Espace disque critique — verifier logs/DB")
|
||||||
|
if sara_rebooted:
|
||||||
|
alert("Modem reboot hardware au cycle precedent — instabilite reseau")
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- Les bytes 66-68 sont initialises a 0x00 dans le constructeur SensorPayload
|
||||||
|
(0x00 = aucune erreur/aucun flag). Les bytes 69-71 restent a 0xFF si le
|
||||||
|
fichier VERSION est absent ou malformed.
|
||||||
|
- Le NPM status n'est pas encore lu par `get_data_modbus_v3.py`. Il faut d'abord
|
||||||
|
ajouter la lecture du registre de status Modbus et le stocker en SQLite.
|
||||||
|
- Les flags du byte 66 sont determines par le script d'envoi en analysant les
|
||||||
|
valeurs lues depuis SQLite (toutes a 0 = capteur en erreur).
|
||||||
71
power/apply_cpu_mode_from_db.py
Normal file
71
power/apply_cpu_mode_from_db.py
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Apply CPU Power Mode from Database at Boot
|
||||||
|
|
||||||
|
This script is called by systemd at boot to apply the CPU power mode
|
||||||
|
stored in the database (cpu_power_mode config parameter).
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
/usr/bin/python3 /var/www/nebuleair_pro_4g/power/apply_cpu_mode_from_db.py
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sqlite3
|
||||||
|
import sys
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
DB_PATH = "/var/www/nebuleair_pro_4g/sqlite/sensors.db"
|
||||||
|
SET_MODE_SCRIPT = "/var/www/nebuleair_pro_4g/power/set_cpu_mode.py"
|
||||||
|
|
||||||
|
def get_cpu_mode_from_db():
|
||||||
|
"""Read cpu_power_mode from database"""
|
||||||
|
try:
|
||||||
|
conn = sqlite3.connect(DB_PATH)
|
||||||
|
cursor = conn.cursor()
|
||||||
|
cursor.execute("SELECT value FROM config_table WHERE key = 'cpu_power_mode'")
|
||||||
|
result = cursor.fetchone()
|
||||||
|
conn.close()
|
||||||
|
return result[0] if result else None
|
||||||
|
except sqlite3.Error as e:
|
||||||
|
print(f"Database error: {e}", file=sys.stderr)
|
||||||
|
return None
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""Main function"""
|
||||||
|
print("=== Applying CPU Power Mode from Database ===")
|
||||||
|
|
||||||
|
# Get mode from database
|
||||||
|
mode = get_cpu_mode_from_db()
|
||||||
|
|
||||||
|
if mode is None:
|
||||||
|
print("No cpu_power_mode found in database, using default: normal")
|
||||||
|
mode = "normal"
|
||||||
|
|
||||||
|
print(f"CPU power mode from database: {mode}")
|
||||||
|
|
||||||
|
# Call set_cpu_mode.py to apply the mode
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
["/usr/bin/python3", SET_MODE_SCRIPT, mode],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
timeout=30
|
||||||
|
)
|
||||||
|
|
||||||
|
if result.returncode == 0:
|
||||||
|
print(f"Successfully applied CPU power mode: {mode}")
|
||||||
|
print(result.stdout)
|
||||||
|
return 0
|
||||||
|
else:
|
||||||
|
print(f"Failed to apply CPU power mode: {mode}", file=sys.stderr)
|
||||||
|
print(result.stderr, file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
print("Timeout while applying CPU power mode", file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error: {e}", file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
256
power/set_cpu_mode.py
Normal file
256
power/set_cpu_mode.py
Normal file
@@ -0,0 +1,256 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
____ ____ _ _ ____ __ __ _
|
||||||
|
/ ___| _ \| | | | | _ \ _____ _____ _ _| \/ | ___ __| | ___
|
||||||
|
| | | |_) | | | | | |_) / _ \ \ /\ / / _ \ '__| |\/| |/ _ \ / _` |/ _ \
|
||||||
|
| |___| __/| |_| | | __/ (_) \ V V / __/ | | | | | (_) | (_| | __/
|
||||||
|
\____|_| \___/ |_| \___/ \_/\_/ \___|_| |_| |_|\___/ \__,_|\___|
|
||||||
|
|
||||||
|
CPU Power Mode Management Script
|
||||||
|
Switches between Normal and Power Saving CPU modes.
|
||||||
|
|
||||||
|
Modes:
|
||||||
|
- normal: CPU governor ondemand (600MHz-1500MHz dynamic)
|
||||||
|
- powersave: CPU governor powersave (600MHz fixed)
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
/usr/bin/python3 /var/www/nebuleair_pro_4g/power/set_cpu_mode.py <mode>
|
||||||
|
/usr/bin/python3 /var/www/nebuleair_pro_4g/power/set_cpu_mode.py normal
|
||||||
|
/usr/bin/python3 /var/www/nebuleair_pro_4g/power/set_cpu_mode.py powersave
|
||||||
|
|
||||||
|
Or get current mode:
|
||||||
|
/usr/bin/python3 /var/www/nebuleair_pro_4g/power/set_cpu_mode.py get
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sqlite3
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import datetime
|
||||||
|
import json
|
||||||
|
|
||||||
|
# Paths
|
||||||
|
DB_PATH = "/var/www/nebuleair_pro_4g/sqlite/sensors.db"
|
||||||
|
LOG_PATH = "/var/www/nebuleair_pro_4g/logs/app.log"
|
||||||
|
|
||||||
|
# Available modes
|
||||||
|
MODES = {
|
||||||
|
"normal": {
|
||||||
|
"governor": "ondemand",
|
||||||
|
"description": "Normal mode - CPU 600MHz-1500MHz dynamic",
|
||||||
|
"min_freq": 600000, # 600 MHz in kHz
|
||||||
|
"max_freq": 1500000 # 1500 MHz in kHz
|
||||||
|
},
|
||||||
|
"powersave": {
|
||||||
|
"governor": "powersave",
|
||||||
|
"description": "Power saving mode - CPU 600MHz fixed",
|
||||||
|
"min_freq": 600000,
|
||||||
|
"max_freq": 600000
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def log_message(message):
|
||||||
|
"""Write message to log file with timestamp"""
|
||||||
|
timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
log_entry = f"[{timestamp}] [CPU Power Mode] {message}\n"
|
||||||
|
try:
|
||||||
|
with open(LOG_PATH, "a") as log_file:
|
||||||
|
log_file.write(log_entry)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Failed to write to log: {e}", file=sys.stderr)
|
||||||
|
print(log_entry.strip())
|
||||||
|
|
||||||
|
def get_cpu_count():
|
||||||
|
"""Get number of CPU cores"""
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
["nproc"],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
timeout=5
|
||||||
|
)
|
||||||
|
return int(result.stdout.strip())
|
||||||
|
except Exception as e:
|
||||||
|
log_message(f"Failed to get CPU count: {e}")
|
||||||
|
return 4 # Default to 4 cores for CM4
|
||||||
|
|
||||||
|
def set_cpu_governor(governor, cpu_id):
|
||||||
|
"""Set CPU governor for specific CPU core"""
|
||||||
|
try:
|
||||||
|
with open(f"/sys/devices/system/cpu/cpu{cpu_id}/cpufreq/scaling_governor", "w") as f:
|
||||||
|
f.write(governor)
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
log_message(f"Failed to set governor for CPU{cpu_id}: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def set_cpu_freq(min_freq, max_freq, cpu_id):
|
||||||
|
"""Set CPU frequency limits for specific CPU core"""
|
||||||
|
try:
|
||||||
|
# Set min frequency
|
||||||
|
with open(f"/sys/devices/system/cpu/cpu{cpu_id}/cpufreq/scaling_min_freq", "w") as f:
|
||||||
|
f.write(str(min_freq))
|
||||||
|
|
||||||
|
# Set max frequency
|
||||||
|
with open(f"/sys/devices/system/cpu/cpu{cpu_id}/cpufreq/scaling_max_freq", "w") as f:
|
||||||
|
f.write(str(max_freq))
|
||||||
|
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
log_message(f"Failed to set frequency for CPU{cpu_id}: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def get_current_cpu_state():
|
||||||
|
"""Get current CPU governor and frequencies"""
|
||||||
|
try:
|
||||||
|
with open("/sys/devices/system/cpu/cpu0/cpufreq/scaling_governor", "r") as f:
|
||||||
|
governor = f.read().strip()
|
||||||
|
|
||||||
|
with open("/sys/devices/system/cpu/cpu0/cpufreq/scaling_min_freq", "r") as f:
|
||||||
|
min_freq = int(f.read().strip())
|
||||||
|
|
||||||
|
with open("/sys/devices/system/cpu/cpu0/cpufreq/scaling_max_freq", "r") as f:
|
||||||
|
max_freq = int(f.read().strip())
|
||||||
|
|
||||||
|
with open("/sys/devices/system/cpu/cpu0/cpufreq/scaling_cur_freq", "r") as f:
|
||||||
|
cur_freq = int(f.read().strip())
|
||||||
|
|
||||||
|
return {
|
||||||
|
"governor": governor,
|
||||||
|
"min_freq_khz": min_freq,
|
||||||
|
"max_freq_khz": max_freq,
|
||||||
|
"current_freq_khz": cur_freq,
|
||||||
|
"min_freq_mhz": min_freq / 1000,
|
||||||
|
"max_freq_mhz": max_freq / 1000,
|
||||||
|
"current_freq_mhz": cur_freq / 1000
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
log_message(f"Failed to get current CPU state: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def update_config_db(mode):
|
||||||
|
"""Update cpu_power_mode in database"""
|
||||||
|
try:
|
||||||
|
conn = sqlite3.connect(DB_PATH)
|
||||||
|
cursor = conn.cursor()
|
||||||
|
cursor.execute(
|
||||||
|
"UPDATE config_table SET value = ? WHERE key = 'cpu_power_mode'",
|
||||||
|
(mode,)
|
||||||
|
)
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
return True
|
||||||
|
except sqlite3.Error as e:
|
||||||
|
log_message(f"Database error: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def get_config_from_db():
|
||||||
|
"""Read cpu_power_mode from database"""
|
||||||
|
try:
|
||||||
|
conn = sqlite3.connect(DB_PATH)
|
||||||
|
cursor = conn.cursor()
|
||||||
|
cursor.execute("SELECT value FROM config_table WHERE key = 'cpu_power_mode'")
|
||||||
|
result = cursor.fetchone()
|
||||||
|
conn.close()
|
||||||
|
return result[0] if result else None
|
||||||
|
except sqlite3.Error as e:
|
||||||
|
log_message(f"Database error: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def apply_mode(mode):
|
||||||
|
"""Apply CPU power mode"""
|
||||||
|
if mode not in MODES:
|
||||||
|
log_message(f"Invalid mode: {mode}. Valid modes: {', '.join(MODES.keys())}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
mode_config = MODES[mode]
|
||||||
|
log_message(f"Applying mode: {mode} - {mode_config['description']}")
|
||||||
|
|
||||||
|
cpu_count = get_cpu_count()
|
||||||
|
log_message(f"Detected {cpu_count} CPU cores")
|
||||||
|
|
||||||
|
success = True
|
||||||
|
|
||||||
|
# Apply settings to all CPU cores
|
||||||
|
for cpu_id in range(cpu_count):
|
||||||
|
# Set frequency limits first
|
||||||
|
if not set_cpu_freq(mode_config['min_freq'], mode_config['max_freq'], cpu_id):
|
||||||
|
success = False
|
||||||
|
|
||||||
|
# Then set governor
|
||||||
|
if not set_cpu_governor(mode_config['governor'], cpu_id):
|
||||||
|
success = False
|
||||||
|
|
||||||
|
if success:
|
||||||
|
log_message(f"Successfully applied {mode} mode to all {cpu_count} cores")
|
||||||
|
|
||||||
|
# Update database
|
||||||
|
if update_config_db(mode):
|
||||||
|
log_message(f"Updated database: cpu_power_mode = {mode}")
|
||||||
|
else:
|
||||||
|
log_message("Warning: Failed to update database")
|
||||||
|
|
||||||
|
# Log current state
|
||||||
|
state = get_current_cpu_state()
|
||||||
|
if state:
|
||||||
|
log_message(f"Current CPU state: {state['governor']} governor, "
|
||||||
|
f"{state['min_freq_mhz']:.0f}-{state['max_freq_mhz']:.0f} MHz "
|
||||||
|
f"(current: {state['current_freq_mhz']:.0f} MHz)")
|
||||||
|
else:
|
||||||
|
log_message(f"Failed to apply {mode} mode completely")
|
||||||
|
|
||||||
|
return success
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""Main function"""
|
||||||
|
if len(sys.argv) < 2:
|
||||||
|
print("Usage: set_cpu_mode.py <mode>")
|
||||||
|
print("Modes: normal, powersave")
|
||||||
|
print("Or: set_cpu_mode.py get (to get current state)")
|
||||||
|
return 1
|
||||||
|
|
||||||
|
command = sys.argv[1].lower()
|
||||||
|
|
||||||
|
# Handle "get" command to return current state
|
||||||
|
if command == "get":
|
||||||
|
state = get_current_cpu_state()
|
||||||
|
db_mode = get_config_from_db()
|
||||||
|
|
||||||
|
if state:
|
||||||
|
output = {
|
||||||
|
"success": True,
|
||||||
|
"cpu_state": state,
|
||||||
|
"config_mode": db_mode
|
||||||
|
}
|
||||||
|
print(json.dumps(output))
|
||||||
|
return 0
|
||||||
|
else:
|
||||||
|
output = {
|
||||||
|
"success": False,
|
||||||
|
"error": "Failed to read CPU state"
|
||||||
|
}
|
||||||
|
print(json.dumps(output))
|
||||||
|
return 1
|
||||||
|
|
||||||
|
# Handle mode setting
|
||||||
|
log_message("=== CPU Power Mode Script Started ===")
|
||||||
|
|
||||||
|
if apply_mode(command):
|
||||||
|
log_message("=== CPU Power Mode Script Completed Successfully ===")
|
||||||
|
output = {
|
||||||
|
"success": True,
|
||||||
|
"mode": command,
|
||||||
|
"description": MODES[command]['description']
|
||||||
|
}
|
||||||
|
print(json.dumps(output))
|
||||||
|
return 0
|
||||||
|
else:
|
||||||
|
log_message("=== CPU Power Mode Script Failed ===")
|
||||||
|
output = {
|
||||||
|
"success": False,
|
||||||
|
"error": f"Failed to apply mode: {command}"
|
||||||
|
}
|
||||||
|
print(json.dumps(output))
|
||||||
|
return 1
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
40
screen_control/screen.py
Normal file
40
screen_control/screen.py
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
|
||||||
|
# Screen Control Script (Kivy)
|
||||||
|
#
|
||||||
|
# This script displays a simple "Bonjour" message on the HDMI screen using Kivy.
|
||||||
|
# It is designed to be run on a device without a desktop environment (headless/framebuffer).
|
||||||
|
#
|
||||||
|
# HOW TO RUN (CLI):
|
||||||
|
# -----------------
|
||||||
|
# 1. Ensure Kivy is installed (python3-kivy).
|
||||||
|
# 2. You may need to set the DISPLAY environment variable if running from SSH or outside a graphical session:
|
||||||
|
# export DISPLAY=:0
|
||||||
|
# 3. Run the script:
|
||||||
|
# python3 /var/www/nebuleair_pro_4g/screen_control/screen.py
|
||||||
|
#
|
||||||
|
# HOW TO RUN (BACKGROUND):
|
||||||
|
# ------------------------
|
||||||
|
# nohup python3 /var/www/nebuleair_pro_4g/screen_control/screen.py > /dev/null 2>&1 &
|
||||||
|
#
|
||||||
|
# HOW TO STOP:
|
||||||
|
# ------------
|
||||||
|
# pkill -f "screen_control/screen.py"
|
||||||
|
#
|
||||||
|
|
||||||
|
import os
|
||||||
|
os.environ['KIVY_GL_BACKEND'] = 'gl'
|
||||||
|
from kivy.app import App
|
||||||
|
from kivy.uix.label import Label
|
||||||
|
from kivy.core.window import Window
|
||||||
|
|
||||||
|
# Set background color to black (optional, but good for screens)
|
||||||
|
Window.clearcolor = (0, 0, 0, 1)
|
||||||
|
|
||||||
|
class ScreenApp(App):
|
||||||
|
def build(self):
|
||||||
|
# Create a label with large text "Bonjour" centered on the screen
|
||||||
|
label = Label(text='Bonjour', font_size='150sp', color=(1, 1, 1, 1))
|
||||||
|
return label
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
ScreenApp().run()
|
||||||
@@ -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 ""
|
||||||
|
printf " %-12s %-20s %s\n" "Service" "Last Run" "Status"
|
||||||
|
printf " %-12s %-20s %s\n" "-------" "--------" "------"
|
||||||
|
|
||||||
|
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 ""
|
echo ""
|
||||||
echo "For detailed logs use:"
|
|
||||||
echo " sudo journalctl -u nebuleair-[service]-data.service -f"
|
|
||||||
echo "To restart a specific service timer:"
|
|
||||||
echo " sudo systemctl restart nebuleair-[service]-data.timer"
|
|
||||||
15
services/nebuleair-cpu-power.service
Normal file
15
services/nebuleair-cpu-power.service
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=NebuleAir CPU Power Mode Service
|
||||||
|
After=multi-user.target
|
||||||
|
Wants=multi-user.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=oneshot
|
||||||
|
ExecStart=/usr/bin/python3 /var/www/nebuleair_pro_4g/power/apply_cpu_mode_from_db.py
|
||||||
|
User=root
|
||||||
|
StandardOutput=journal
|
||||||
|
StandardError=journal
|
||||||
|
RemainAfterExit=yes
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
14
services/nebuleair-wifi-powersave.service
Normal file
14
services/nebuleair-wifi-powersave.service
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=NebuleAir WiFi Power Save Service
|
||||||
|
After=network-online.target
|
||||||
|
Wants=network-online.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=oneshot
|
||||||
|
ExecStart=/usr/bin/python3 /var/www/nebuleair_pro_4g/wifi/power_save.py
|
||||||
|
User=root
|
||||||
|
StandardOutput=journal
|
||||||
|
StandardError=journal
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
12
services/nebuleair-wifi-powersave.timer
Normal file
12
services/nebuleair-wifi-powersave.timer
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=NebuleAir WiFi Power Save Timer (10 minutes after boot)
|
||||||
|
After=network-online.target
|
||||||
|
|
||||||
|
[Timer]
|
||||||
|
# Run 10 minutes after system boot
|
||||||
|
OnBootSec=10min
|
||||||
|
# Don't persist timer across reboots
|
||||||
|
Persistent=false
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=timers.target
|
||||||
@@ -173,6 +173,70 @@ 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 MH-Z19 CO2 Data
|
||||||
|
cat > /etc/systemd/system/nebuleair-mhz19-data.service << 'EOL'
|
||||||
|
[Unit]
|
||||||
|
Description=NebuleAir MH-Z19 CO2 Data Collection Service
|
||||||
|
After=network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=oneshot
|
||||||
|
ExecStart=/usr/bin/python3 /var/www/nebuleair_pro_4g/MH-Z19/write_data.py
|
||||||
|
User=root
|
||||||
|
WorkingDirectory=/var/www/nebuleair_pro_4g
|
||||||
|
StandardOutput=append:/var/www/nebuleair_pro_4g/logs/mhz19_service.log
|
||||||
|
StandardError=append:/var/www/nebuleair_pro_4g/logs/mhz19_service_errors.log
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
EOL
|
||||||
|
|
||||||
|
cat > /etc/systemd/system/nebuleair-mhz19-data.timer << 'EOL'
|
||||||
|
[Unit]
|
||||||
|
Description=Run NebuleAir MH-Z19 CO2 Data Collection every 120 seconds
|
||||||
|
Requires=nebuleair-mhz19-data.service
|
||||||
|
|
||||||
|
[Timer]
|
||||||
|
OnBootSec=120s
|
||||||
|
OnUnitActiveSec=120s
|
||||||
|
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]
|
||||||
@@ -205,17 +269,79 @@ AccuracySec=1h
|
|||||||
WantedBy=timers.target
|
WantedBy=timers.target
|
||||||
EOL
|
EOL
|
||||||
|
|
||||||
|
# Create service and timer files for WiFi Power Save
|
||||||
|
cat > /etc/systemd/system/nebuleair-wifi-powersave.service << 'EOL'
|
||||||
|
[Unit]
|
||||||
|
Description=NebuleAir WiFi Power Save Service
|
||||||
|
After=network-online.target
|
||||||
|
Wants=network-online.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=oneshot
|
||||||
|
ExecStart=/usr/bin/python3 /var/www/nebuleair_pro_4g/wifi/power_save.py
|
||||||
|
User=root
|
||||||
|
StandardOutput=journal
|
||||||
|
StandardError=journal
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
EOL
|
||||||
|
|
||||||
|
cat > /etc/systemd/system/nebuleair-wifi-powersave.timer << 'EOL'
|
||||||
|
[Unit]
|
||||||
|
Description=NebuleAir WiFi Power Save Timer (10 minutes after boot)
|
||||||
|
After=network-online.target
|
||||||
|
|
||||||
|
[Timer]
|
||||||
|
# Run 10 minutes after system boot
|
||||||
|
OnBootSec=10min
|
||||||
|
# Don't persist timer across reboots
|
||||||
|
Persistent=false
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=timers.target
|
||||||
|
EOL
|
||||||
|
|
||||||
|
# Create service file for CPU Power Mode (runs once at boot)
|
||||||
|
cat > /etc/systemd/system/nebuleair-cpu-power.service << 'EOL'
|
||||||
|
[Unit]
|
||||||
|
Description=NebuleAir CPU Power Mode Service
|
||||||
|
After=multi-user.target
|
||||||
|
Wants=multi-user.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=oneshot
|
||||||
|
ExecStart=/usr/bin/python3 /var/www/nebuleair_pro_4g/power/apply_cpu_mode_from_db.py
|
||||||
|
User=root
|
||||||
|
StandardOutput=journal
|
||||||
|
StandardError=journal
|
||||||
|
RemainAfterExit=yes
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
EOL
|
||||||
|
|
||||||
# Reload systemd to recognize new services
|
# Reload systemd to recognize new services
|
||||||
systemctl daemon-reload
|
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 mhz19 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"
|
||||||
done
|
done
|
||||||
|
|
||||||
|
# Enable and start WiFi power save timer (separate naming convention)
|
||||||
|
systemctl enable nebuleair-wifi-powersave.timer
|
||||||
|
systemctl start nebuleair-wifi-powersave.timer
|
||||||
|
echo "Started nebuleair-wifi-powersave timer"
|
||||||
|
|
||||||
|
# Enable and start CPU power mode service (runs once at boot)
|
||||||
|
systemctl enable nebuleair-cpu-power.service
|
||||||
|
systemctl start nebuleair-cpu-power.service
|
||||||
|
echo "Started nebuleair-cpu-power service"
|
||||||
|
|
||||||
echo "Checking status of all timers..."
|
echo "Checking status of all timers..."
|
||||||
systemctl list-timers | grep nebuleair
|
systemctl list-timers | grep nebuleair
|
||||||
|
|
||||||
|
|||||||
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
34
sound_meter/read.py
Normal file
34
sound_meter/read.py
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
'''
|
||||||
|
____ ___ _ _ _ _ ____
|
||||||
|
/ ___| / _ \| | | | \ | | _ \
|
||||||
|
\___ \| | | | | | | \| | | | |
|
||||||
|
___) | |_| | |_| | |\ | |_| |
|
||||||
|
|____/ \___/ \___/|_| \_|____/
|
||||||
|
|
||||||
|
python3 /var/www/nebuleair_pro_4g/sound_meter/read.py
|
||||||
|
|
||||||
|
Read current values from NSRT MK4 Sound Level Meter (USB /dev/ttyACM0)
|
||||||
|
Used by the web interface "Get Data" button via launcher.php
|
||||||
|
'''
|
||||||
|
|
||||||
|
import json
|
||||||
|
import nsrt_mk3_dev
|
||||||
|
|
||||||
|
try:
|
||||||
|
nsrt = nsrt_mk3_dev.NsrtMk3Dev('/dev/ttyACM0')
|
||||||
|
|
||||||
|
leq_level = nsrt.read_leq()
|
||||||
|
weighted_level = nsrt.read_level()
|
||||||
|
weighting = nsrt.read_weighting()
|
||||||
|
time_constant = nsrt.read_tau()
|
||||||
|
|
||||||
|
data = {
|
||||||
|
"LEQ": round(leq_level, 2),
|
||||||
|
"dBA": round(weighted_level, 2),
|
||||||
|
"weighting": str(weighting),
|
||||||
|
"tau": time_constant
|
||||||
|
}
|
||||||
|
print(json.dumps(data))
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(json.dumps({"error": str(e)}))
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
'''
|
r'''
|
||||||
____ ___ _ _ _
|
____ ___ _ _ _
|
||||||
/ ___| / _ \| | (_) |_ ___
|
/ ___| / _ \| | (_) |_ ___
|
||||||
\___ \| | | | | | | __/ _ \
|
\___ \| | | | | | | __/ _ \
|
||||||
@@ -27,14 +27,6 @@ CREATE TABLE IF NOT EXISTS config_table (
|
|||||||
)
|
)
|
||||||
""")
|
""")
|
||||||
|
|
||||||
#creates a config_scripts table
|
|
||||||
cursor.execute('''
|
|
||||||
CREATE TABLE IF NOT EXISTS config_scripts_table (
|
|
||||||
script_path TEXT PRIMARY KEY,
|
|
||||||
enabled INTEGER NOT NULL
|
|
||||||
)
|
|
||||||
''')
|
|
||||||
|
|
||||||
#creates a config table for envea sondes
|
#creates a config table for envea sondes
|
||||||
cursor.execute("""
|
cursor.execute("""
|
||||||
CREATE TABLE IF NOT EXISTS envea_sondes_table (
|
CREATE TABLE IF NOT EXISTS envea_sondes_table (
|
||||||
@@ -46,7 +38,6 @@ CREATE TABLE IF NOT EXISTS envea_sondes_table (
|
|||||||
)
|
)
|
||||||
""")
|
""")
|
||||||
|
|
||||||
|
|
||||||
# Create a table timer
|
# Create a table timer
|
||||||
cursor.execute("""
|
cursor.execute("""
|
||||||
CREATE TABLE IF NOT EXISTS timestamp_table (
|
CREATE TABLE IF NOT EXISTS timestamp_table (
|
||||||
@@ -76,10 +67,18 @@ CREATE TABLE IF NOT EXISTS data_NPM (
|
|||||||
PM25 REAL,
|
PM25 REAL,
|
||||||
PM10 REAL,
|
PM10 REAL,
|
||||||
temp_npm REAL,
|
temp_npm REAL,
|
||||||
hum_npm REAL
|
hum_npm REAL,
|
||||||
|
npm_status INTEGER DEFAULT 0
|
||||||
)
|
)
|
||||||
""")
|
""")
|
||||||
|
|
||||||
|
# Add npm_status column to existing databases (migration)
|
||||||
|
try:
|
||||||
|
cursor.execute("ALTER TABLE data_NPM ADD COLUMN npm_status INTEGER DEFAULT 0")
|
||||||
|
print("Added npm_status column to data_NPM")
|
||||||
|
except:
|
||||||
|
pass # Column already exists
|
||||||
|
|
||||||
# Create a table BME280
|
# Create a table BME280
|
||||||
cursor.execute("""
|
cursor.execute("""
|
||||||
CREATE TABLE IF NOT EXISTS data_BME280 (
|
CREATE TABLE IF NOT EXISTS data_BME280 (
|
||||||
@@ -98,7 +97,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
|
||||||
)
|
)
|
||||||
""")
|
""")
|
||||||
|
|
||||||
@@ -135,6 +135,22 @@ 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
|
||||||
|
)
|
||||||
|
""")
|
||||||
|
|
||||||
|
# Create a table MHZ19 (CO2 sensor)
|
||||||
|
cursor.execute("""
|
||||||
|
CREATE TABLE IF NOT EXISTS data_MHZ19 (
|
||||||
|
timestamp TEXT,
|
||||||
|
CO2 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()
|
||||||
204
sqlite/empty_sensor_tables.py
Normal file
204
sqlite/empty_sensor_tables.py
Normal file
@@ -0,0 +1,204 @@
|
|||||||
|
'''
|
||||||
|
____ ___ _ _ _
|
||||||
|
/ ___| / _ \| | (_) |_ ___
|
||||||
|
\___ \| | | | | | | __/ _ \
|
||||||
|
___) | |_| | |___| | || __/
|
||||||
|
|____/ \__\_\_____|_|\__\___|
|
||||||
|
|
||||||
|
Script to empty (delete all data from) sensor tables in the SQLite database
|
||||||
|
This script empties sensor data tables but preserves:
|
||||||
|
- timestamp_table
|
||||||
|
- config_table
|
||||||
|
- envea_sondes_table
|
||||||
|
- config_scripts_table
|
||||||
|
|
||||||
|
/usr/bin/python3 /var/www/nebuleair_pro_4g/sqlite/empty_sensor_tables.py
|
||||||
|
'''
|
||||||
|
|
||||||
|
import sqlite3
|
||||||
|
import sys
|
||||||
|
import json
|
||||||
|
|
||||||
|
def table_exists(cursor, table_name):
|
||||||
|
"""Check if a table exists in the database"""
|
||||||
|
try:
|
||||||
|
cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name=?", (table_name,))
|
||||||
|
return cursor.fetchone() is not None
|
||||||
|
except sqlite3.Error as e:
|
||||||
|
print(f"[ERROR] Failed to check if table '{table_name}' exists: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def get_table_count(cursor, table_name):
|
||||||
|
"""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 empty_table(cursor, table_name):
|
||||||
|
"""Delete all records from a specific table"""
|
||||||
|
try:
|
||||||
|
# Get record count before deletion
|
||||||
|
initial_count = get_table_count(cursor, table_name)
|
||||||
|
|
||||||
|
if initial_count == 0:
|
||||||
|
print(f"[INFO] Table '{table_name}' is already empty")
|
||||||
|
return True, 0
|
||||||
|
|
||||||
|
# Delete all records
|
||||||
|
cursor.execute(f"DELETE FROM {table_name}")
|
||||||
|
deleted_count = cursor.rowcount
|
||||||
|
|
||||||
|
print(f"[SUCCESS] Deleted {deleted_count} records from '{table_name}'")
|
||||||
|
return True, deleted_count
|
||||||
|
|
||||||
|
except sqlite3.Error as e:
|
||||||
|
print(f"[ERROR] Failed to empty table '{table_name}': {e}")
|
||||||
|
return False, 0
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
result = {
|
||||||
|
'success': False,
|
||||||
|
'message': '',
|
||||||
|
'tables_processed': [],
|
||||||
|
'total_deleted': 0
|
||||||
|
}
|
||||||
|
|
||||||
|
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}")
|
||||||
|
|
||||||
|
# List of sensor tables to empty (EXCLUDING timestamp_table and config tables)
|
||||||
|
sensor_tables = [
|
||||||
|
"data_NPM",
|
||||||
|
"data_NPM_5channels",
|
||||||
|
"data_BME280",
|
||||||
|
"data_envea",
|
||||||
|
"data_WIND",
|
||||||
|
"data_MPPT",
|
||||||
|
"data_NOISE",
|
||||||
|
"modem_status"
|
||||||
|
]
|
||||||
|
|
||||||
|
# Tables to PRESERVE (not empty)
|
||||||
|
preserved_tables = [
|
||||||
|
"timestamp_table",
|
||||||
|
"config_table",
|
||||||
|
"envea_sondes_table",
|
||||||
|
"config_scripts_table"
|
||||||
|
]
|
||||||
|
|
||||||
|
print(f"[INFO] Will empty the following sensor tables: {', '.join(sensor_tables)}")
|
||||||
|
print(f"[INFO] Will preserve the following tables: {', '.join(preserved_tables)}")
|
||||||
|
|
||||||
|
# Check which tables actually exist
|
||||||
|
existing_tables = []
|
||||||
|
missing_tables = []
|
||||||
|
|
||||||
|
for table in sensor_tables:
|
||||||
|
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:
|
||||||
|
result['success'] = True
|
||||||
|
result['message'] = "No sensor tables found to empty"
|
||||||
|
print("[WARNING] No sensor tables found to empty!")
|
||||||
|
print(json.dumps(result))
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Loop through existing tables and empty them
|
||||||
|
successful_operations = 0
|
||||||
|
failed_operations = 0
|
||||||
|
total_deleted = 0
|
||||||
|
|
||||||
|
for table in existing_tables:
|
||||||
|
success, deleted = empty_table(cursor, table)
|
||||||
|
if success:
|
||||||
|
successful_operations += 1
|
||||||
|
total_deleted += deleted
|
||||||
|
result['tables_processed'].append({
|
||||||
|
'name': table,
|
||||||
|
'deleted': deleted
|
||||||
|
})
|
||||||
|
else:
|
||||||
|
failed_operations += 1
|
||||||
|
|
||||||
|
# Commit changes
|
||||||
|
print("[INFO] Committing changes...")
|
||||||
|
conn.commit()
|
||||||
|
print("[SUCCESS] Changes committed successfully!")
|
||||||
|
|
||||||
|
# Run VACUUM to optimize database space
|
||||||
|
if total_deleted > 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 emptied successfully: {successful_operations}")
|
||||||
|
print(f"Tables with errors: {failed_operations}")
|
||||||
|
print(f"Tables skipped (missing): {len(missing_tables)}")
|
||||||
|
print(f"Total records deleted: {total_deleted}")
|
||||||
|
|
||||||
|
result['success'] = True
|
||||||
|
result['message'] = f"Successfully emptied {successful_operations} sensor tables. Total records deleted: {total_deleted}"
|
||||||
|
result['total_deleted'] = total_deleted
|
||||||
|
|
||||||
|
if failed_operations == 0:
|
||||||
|
print("[SUCCESS] All sensor tables emptied successfully!")
|
||||||
|
else:
|
||||||
|
result['message'] = f"Partial success: {successful_operations} tables emptied, {failed_operations} failed"
|
||||||
|
print("[WARNING] Some operations failed - check logs above")
|
||||||
|
|
||||||
|
# Output JSON result for web interface
|
||||||
|
print("\n[JSON_RESULT]")
|
||||||
|
print(json.dumps(result))
|
||||||
|
|
||||||
|
return failed_operations == 0
|
||||||
|
|
||||||
|
except sqlite3.Error as e:
|
||||||
|
result['message'] = f"Database error: {e}"
|
||||||
|
print(f"[ERROR] Database error: {e}")
|
||||||
|
print("\n[JSON_RESULT]")
|
||||||
|
print(json.dumps(result))
|
||||||
|
return False
|
||||||
|
except Exception as e:
|
||||||
|
result['message'] = f"Unexpected error: {e}"
|
||||||
|
print(f"[ERROR] Unexpected error: {e}")
|
||||||
|
print("\n[JSON_RESULT]")
|
||||||
|
print(json.dumps(result))
|
||||||
|
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)
|
||||||
@@ -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,186 @@ data_NPM_5channels
|
|||||||
data_BME280
|
data_BME280
|
||||||
data_envea
|
data_envea
|
||||||
timestamp_table
|
timestamp_table
|
||||||
|
data_MPPT
|
||||||
|
data_NOISE
|
||||||
|
data_WIND
|
||||||
|
data_MHZ19
|
||||||
|
|
||||||
'''
|
'''
|
||||||
|
|
||||||
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",
|
||||||
|
"data_MHZ19"
|
||||||
|
]
|
||||||
|
|
||||||
|
# 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)
|
||||||
@@ -30,19 +30,17 @@ limit_num=parameter[1]
|
|||||||
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()
|
||||||
|
|
||||||
# Retrieve the last 10 sensor readings
|
# Retrieve the last sensor readings based on insertion order (ROWID)
|
||||||
#cursor.execute("SELECT * FROM data_NPM ORDER BY timestamp DESC LIMIT 10")
|
# This ensures we get the most recently inserted data, regardless of timestamp value
|
||||||
#cursor.execute("SELECT * FROM data_BME280 ORDER BY timestamp DESC LIMIT 10")
|
|
||||||
#cursor.execute("SELECT * FROM timestamp_table")
|
|
||||||
if table_name == "timestamp_table":
|
if table_name == "timestamp_table":
|
||||||
cursor.execute("SELECT * FROM timestamp_table")
|
cursor.execute("SELECT * FROM timestamp_table")
|
||||||
else:
|
else:
|
||||||
query = f"SELECT * FROM {table_name} ORDER BY timestamp DESC LIMIT ?"
|
# Order by ROWID DESC to get most recently inserted rows first
|
||||||
|
query = f"SELECT * FROM {table_name} ORDER BY ROWID DESC LIMIT ?"
|
||||||
cursor.execute(query, (limit_num,))
|
cursor.execute(query, (limit_num,))
|
||||||
|
|
||||||
|
|
||||||
rows = cursor.fetchall()
|
rows = cursor.fetchall()
|
||||||
rows.reverse() # Reverse the order in Python (to get ascending order)
|
# Keep DESC order - most recently inserted data first
|
||||||
|
|
||||||
|
|
||||||
# Display the results
|
# Display the results
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
'''
|
r'''
|
||||||
____ ___ _ _ _
|
____ ___ _ _ _
|
||||||
/ ___| / _ \| | (_) |_ ___
|
/ ___| / _ \| | (_) |_ ___
|
||||||
\___ \| | | | | | | __/ _ \
|
\___ \| | | | | | | __/ _ \
|
||||||
@@ -20,37 +20,14 @@ cursor = conn.cursor()
|
|||||||
|
|
||||||
print(f"Connected to database")
|
print(f"Connected to database")
|
||||||
|
|
||||||
# Clear existing data (if any)
|
# Note: Using INSERT OR IGNORE to add only new configurations without overwriting existing ones
|
||||||
cursor.execute("DELETE FROM config_table")
|
print("Adding new configurations (existing ones will be preserved)")
|
||||||
cursor.execute("DELETE FROM config_scripts_table")
|
|
||||||
cursor.execute("DELETE FROM envea_sondes_table")
|
|
||||||
print("Existing data cleared")
|
|
||||||
|
|
||||||
#add values
|
|
||||||
|
|
||||||
# Insert script configurations
|
|
||||||
script_configs = [
|
|
||||||
("NPM/get_data_modbus_v3.py", True),
|
|
||||||
("loop/SARA_send_data_v2.py", True),
|
|
||||||
("RTC/save_to_db.py", True),
|
|
||||||
("BME280/get_data_v2.py", True),
|
|
||||||
("envea/read_value_v2.py", False),
|
|
||||||
("MPPT/read.py", False),
|
|
||||||
("windMeter/read.py", False),
|
|
||||||
("sqlite/flush_old_data.py", True)
|
|
||||||
]
|
|
||||||
|
|
||||||
for script_path, enabled in script_configs:
|
|
||||||
cursor.execute(
|
|
||||||
"INSERT INTO config_scripts_table (script_path, enabled) VALUES (?, ?)",
|
|
||||||
(script_path, 1 if enabled else 0)
|
|
||||||
)
|
|
||||||
|
|
||||||
# Insert general configurations
|
# Insert general configurations
|
||||||
config_entries = [
|
config_entries = [
|
||||||
("modem_config_mode", "0", "bool"),
|
("modem_config_mode", "0", "bool"),
|
||||||
("deviceID", "XXXX", "str"),
|
("deviceID", "XXXX", "str"),
|
||||||
("npm_5channel", "0", "bool"),
|
|
||||||
("latitude_raw", "0", "int"),
|
("latitude_raw", "0", "int"),
|
||||||
("longitude_raw", "0", "int"),
|
("longitude_raw", "0", "int"),
|
||||||
("latitude_precision", "0", "int"),
|
("latitude_precision", "0", "int"),
|
||||||
@@ -64,30 +41,83 @@ 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", "0", "bool"),
|
||||||
("send_uSpot", "0", "bool"),
|
("send_uSpot", "0", "bool"),
|
||||||
|
("send_miotiq", "1", "bool"),
|
||||||
|
("npm_5channel", "0", "bool"),
|
||||||
|
("envea", "0", "bool"),
|
||||||
("windMeter", "0", "bool"),
|
("windMeter", "0", "bool"),
|
||||||
("modem_version", "XXX", "str")
|
("BME280", "1", "bool"),
|
||||||
|
("MPPT", "0", "bool"),
|
||||||
|
("NOISE", "0", "bool"),
|
||||||
|
("MHZ19", "0", "bool"),
|
||||||
|
("modem_version", "XXX", "str"),
|
||||||
|
("device_type", "nebuleair_pro", "str"),
|
||||||
|
("language", "fr", "str"),
|
||||||
|
("wifi_power_saving", "0", "bool"),
|
||||||
|
("cpu_power_mode", "normal", "str")
|
||||||
]
|
]
|
||||||
|
|
||||||
for key, value, value_type in config_entries:
|
for key, value, value_type in config_entries:
|
||||||
cursor.execute(
|
cursor.execute(
|
||||||
"INSERT INTO config_table (key, value, type) VALUES (?, ?, ?)",
|
"INSERT OR IGNORE INTO config_table (key, value, type) VALUES (?, ?, ?)",
|
||||||
(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 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")
|
||||||
|
|
||||||
|
|
||||||
|
# Database migrations (add columns to existing tables)
|
||||||
|
migrations = [
|
||||||
|
("data_NPM", "npm_status", "INTEGER DEFAULT 0"),
|
||||||
|
]
|
||||||
|
|
||||||
|
for table, column, col_type in migrations:
|
||||||
|
try:
|
||||||
|
cursor.execute(f"ALTER TABLE {table} ADD COLUMN {column} {col_type}")
|
||||||
|
print(f"Migration: added column '{column}' to {table}")
|
||||||
|
except:
|
||||||
|
pass # Column already exists
|
||||||
|
|
||||||
# Commit and close the connection
|
# Commit and close the connection
|
||||||
conn.commit()
|
conn.commit()
|
||||||
|
|||||||
174
update_firmware.sh
Executable file
174
update_firmware.sh
Executable file
@@ -0,0 +1,174 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# NebuleAir Pro 4G - Comprehensive Update Script
|
||||||
|
# This script performs a complete system update including git pull,
|
||||||
|
# config initialization, and service management
|
||||||
|
# Non-interactive version for WebUI
|
||||||
|
|
||||||
|
echo "======================================"
|
||||||
|
echo "NebuleAir Pro 4G - Firmware Update"
|
||||||
|
echo "======================================"
|
||||||
|
echo "Started at: $(date)"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Set working directory
|
||||||
|
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
|
||||||
|
print_status() {
|
||||||
|
echo "[$(date '+%H:%M:%S')] $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Function to check command success
|
||||||
|
check_status() {
|
||||||
|
if [ $? -eq 0 ]; then
|
||||||
|
print_status "✓ $1 completed successfully"
|
||||||
|
else
|
||||||
|
print_status "✗ $1 failed"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Step 1: Git operations
|
||||||
|
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
|
||||||
|
check_status "Git fetch"
|
||||||
|
|
||||||
|
# Show current branch
|
||||||
|
print_status "Current branch: $(git branch --show-current)"
|
||||||
|
|
||||||
|
# Check for local changes
|
||||||
|
if [ -n "$(git status --porcelain)" ]; then
|
||||||
|
print_status "Warning: Local changes detected, stashing..."
|
||||||
|
git stash push -m "Auto-stash before update $(date)"
|
||||||
|
check_status "Git stash"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Pull latest changes
|
||||||
|
git pull origin $(git branch --show-current)
|
||||||
|
check_status "Git pull"
|
||||||
|
|
||||||
|
# Display firmware version
|
||||||
|
if [ -f "/var/www/nebuleair_pro_4g/VERSION" ]; then
|
||||||
|
print_status "Firmware version: $(cat /var/www/nebuleair_pro_4g/VERSION)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Step 2: Update database configuration
|
||||||
|
print_status ""
|
||||||
|
print_status "Step 2: Updating database configuration..."
|
||||||
|
/usr/bin/python3 /var/www/nebuleair_pro_4g/sqlite/set_config.py
|
||||||
|
check_status "Database configuration update"
|
||||||
|
|
||||||
|
# Step 3: Check and fix file permissions
|
||||||
|
print_status ""
|
||||||
|
print_status "Step 3: Checking file permissions..."
|
||||||
|
sudo chmod +x /var/www/nebuleair_pro_4g/update_firmware.sh
|
||||||
|
sudo chmod 755 /var/www/nebuleair_pro_4g/sqlite/*.py
|
||||||
|
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/SARA/*.py
|
||||||
|
sudo chmod 755 /var/www/nebuleair_pro_4g/envea/*.py
|
||||||
|
sudo chmod 755 /var/www/nebuleair_pro_4g/MPPT/*.py 2>/dev/null
|
||||||
|
sudo chmod 755 /var/www/nebuleair_pro_4g/wifi/*.py 2>/dev/null
|
||||||
|
check_status "File permissions update"
|
||||||
|
|
||||||
|
# Step 3b: Ensure Apache/PHP config allows file uploads
|
||||||
|
APACHE_MAIN_CONF="/etc/apache2/apache2.conf"
|
||||||
|
if grep -q '<Directory /var/www/>' "$APACHE_MAIN_CONF"; then
|
||||||
|
if ! sed -n '/<Directory \/var\/www\/>/,/<\/Directory>/p' "$APACHE_MAIN_CONF" | grep -q "AllowOverride All"; then
|
||||||
|
sed -i '/<Directory \/var\/www\/>/,/<\/Directory>/ s/AllowOverride None/AllowOverride All/' "$APACHE_MAIN_CONF"
|
||||||
|
print_status "✓ AllowOverride All enabled for Apache"
|
||||||
|
APACHE_CHANGED=true
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
PHP_INI=$(php -r "echo php_ini_loaded_file();" 2>/dev/null)
|
||||||
|
if [ -n "$PHP_INI" ]; then
|
||||||
|
CURRENT_UPLOAD=$(grep -oP 'upload_max_filesize = \K\d+' "$PHP_INI" 2>/dev/null)
|
||||||
|
if [ -n "$CURRENT_UPLOAD" ] && [ "$CURRENT_UPLOAD" -lt 50 ]; then
|
||||||
|
sed -i 's/upload_max_filesize = .*/upload_max_filesize = 50M/' "$PHP_INI"
|
||||||
|
sed -i 's/post_max_size = .*/post_max_size = 55M/' "$PHP_INI"
|
||||||
|
print_status "✓ PHP upload limits set to 50M"
|
||||||
|
APACHE_CHANGED=true
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "${APACHE_CHANGED:-false}" = true ]; then
|
||||||
|
systemctl reload apache2 2>/dev/null
|
||||||
|
print_status "✓ Apache reloaded"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Step 4: Restart critical services if they exist
|
||||||
|
print_status ""
|
||||||
|
print_status "Step 4: Managing system services..."
|
||||||
|
|
||||||
|
# List of services to check and restart
|
||||||
|
services=(
|
||||||
|
"nebuleair-npm-data.timer"
|
||||||
|
"nebuleair-envea-data.timer"
|
||||||
|
"nebuleair-sara-data.timer"
|
||||||
|
"nebuleair-bme280-data.timer"
|
||||||
|
"nebuleair-mppt-data.timer"
|
||||||
|
"nebuleair-noise-data.timer"
|
||||||
|
"nebuleair-wifi-powersave.timer"
|
||||||
|
)
|
||||||
|
|
||||||
|
for service in "${services[@]}"; do
|
||||||
|
if systemctl list-unit-files | grep -q "$service"; then
|
||||||
|
# Check if service is enabled before restarting
|
||||||
|
if systemctl is-enabled --quiet "$service" 2>/dev/null; then
|
||||||
|
print_status "Restarting enabled service: $service"
|
||||||
|
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
|
||||||
|
print_status "ℹ Service $service is disabled, skipping restart"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
print_status "ℹ Service $service not found (may not be installed)"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# Step 5: System health check
|
||||||
|
print_status ""
|
||||||
|
print_status "Step 5: System health check..."
|
||||||
|
|
||||||
|
# Check disk space
|
||||||
|
disk_usage=$(df / | awk 'NR==2 {print $5}' | sed 's/%//')
|
||||||
|
if [ "$disk_usage" -gt 90 ]; then
|
||||||
|
print_status "⚠ Warning: Disk usage is high ($disk_usage%)"
|
||||||
|
else
|
||||||
|
print_status "✓ Disk usage is acceptable ($disk_usage%)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check if database is accessible
|
||||||
|
if [ -f "/var/www/nebuleair_pro_4g/sqlite/sensors.db" ]; then
|
||||||
|
print_status "✓ Database file exists"
|
||||||
|
else
|
||||||
|
print_status "⚠ Warning: Database file not found"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Step 6: Final cleanup
|
||||||
|
print_status ""
|
||||||
|
print_status "Step 6: Cleaning up..."
|
||||||
|
sudo find /var/www/nebuleair_pro_4g/logs -name "*.log" -size +10M -exec truncate -s 0 {} \;
|
||||||
|
check_status "Log cleanup"
|
||||||
|
|
||||||
|
print_status ""
|
||||||
|
print_status "======================================"
|
||||||
|
print_status "Update completed successfully!"
|
||||||
|
print_status "======================================"
|
||||||
|
|
||||||
|
exit 0
|
||||||
211
update_firmware_from_file.sh
Normal file
211
update_firmware_from_file.sh
Normal file
@@ -0,0 +1,211 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# NebuleAir Pro 4G - Update from uploaded ZIP file
|
||||||
|
# Usage: sudo ./update_firmware_from_file.sh /path/to/extracted/folder
|
||||||
|
|
||||||
|
echo "======================================"
|
||||||
|
echo "NebuleAir Pro 4G - Firmware Update (File Upload)"
|
||||||
|
echo "======================================"
|
||||||
|
echo "Started at: $(date)"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Set target directory
|
||||||
|
TARGET_DIR="/var/www/nebuleair_pro_4g"
|
||||||
|
EXCLUDE_FILE="$TARGET_DIR/.update-exclude"
|
||||||
|
|
||||||
|
# Function to print status messages
|
||||||
|
print_status() {
|
||||||
|
echo "[$(date '+%H:%M:%S')] $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Function to check command success
|
||||||
|
check_status() {
|
||||||
|
if [ $? -eq 0 ]; then
|
||||||
|
print_status "✓ $1 completed successfully"
|
||||||
|
else
|
||||||
|
print_status "✗ $1 failed"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Validate arguments
|
||||||
|
if [ -z "$1" ]; then
|
||||||
|
print_status "✗ Error: No source directory provided"
|
||||||
|
print_status "Usage: $0 /path/to/extracted/folder"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
SOURCE_DIR="$1"
|
||||||
|
|
||||||
|
if [ ! -d "$SOURCE_DIR" ]; then
|
||||||
|
print_status "✗ Error: Source directory does not exist: $SOURCE_DIR"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Step 1: Validate source and compare versions
|
||||||
|
print_status "Step 1: Validating update package..."
|
||||||
|
|
||||||
|
if [ ! -f "$SOURCE_DIR/VERSION" ]; then
|
||||||
|
print_status "✗ Error: VERSION file not found in update package"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
NEW_VERSION=$(cat "$SOURCE_DIR/VERSION" | tr -d '[:space:]')
|
||||||
|
OLD_VERSION="unknown"
|
||||||
|
if [ -f "$TARGET_DIR/VERSION" ]; then
|
||||||
|
OLD_VERSION=$(cat "$TARGET_DIR/VERSION" | tr -d '[:space:]')
|
||||||
|
fi
|
||||||
|
|
||||||
|
print_status "Current version: $OLD_VERSION"
|
||||||
|
print_status "New version: $NEW_VERSION"
|
||||||
|
|
||||||
|
# Step 2: Rsync with exclusions from .update-exclude
|
||||||
|
print_status ""
|
||||||
|
print_status "Step 2: Syncing files..."
|
||||||
|
|
||||||
|
# Build exclude args: use .update-exclude from the SOURCE (new version) if available,
|
||||||
|
# otherwise fall back to the one already installed
|
||||||
|
if [ -f "$SOURCE_DIR/.update-exclude" ]; then
|
||||||
|
EXCLUDE_FILE="$SOURCE_DIR/.update-exclude"
|
||||||
|
print_status "Using .update-exclude from update package"
|
||||||
|
elif [ -f "$TARGET_DIR/.update-exclude" ]; then
|
||||||
|
EXCLUDE_FILE="$TARGET_DIR/.update-exclude"
|
||||||
|
print_status "Using .update-exclude from current installation"
|
||||||
|
else
|
||||||
|
print_status "⚠ No .update-exclude file found, using built-in defaults"
|
||||||
|
# Fallback minimal exclusions
|
||||||
|
EXCLUDE_FILE=$(mktemp)
|
||||||
|
cat > "$EXCLUDE_FILE" <<'EXCL'
|
||||||
|
sqlite/sensors.db
|
||||||
|
sqlite/*.db-journal
|
||||||
|
sqlite/*.db-wal
|
||||||
|
logs/
|
||||||
|
.git/
|
||||||
|
config.json
|
||||||
|
deviceID.txt
|
||||||
|
wifi_list.csv
|
||||||
|
envea/data/
|
||||||
|
NPM/data/
|
||||||
|
*.lock
|
||||||
|
EXCL
|
||||||
|
fi
|
||||||
|
|
||||||
|
rsync -av --delete --exclude-from="$EXCLUDE_FILE" "$SOURCE_DIR/" "$TARGET_DIR/"
|
||||||
|
check_status "File sync (rsync)"
|
||||||
|
|
||||||
|
# Fix ownership and permissions
|
||||||
|
print_status "Fixing ownership..."
|
||||||
|
chown -R www-data:www-data "$TARGET_DIR/"
|
||||||
|
check_status "Ownership fix (chown)"
|
||||||
|
|
||||||
|
# Step 3: Update database configuration
|
||||||
|
print_status ""
|
||||||
|
print_status "Step 3: Updating database configuration..."
|
||||||
|
/usr/bin/python3 "$TARGET_DIR/sqlite/set_config.py"
|
||||||
|
check_status "Database configuration update"
|
||||||
|
|
||||||
|
# Step 4: Check and fix file permissions
|
||||||
|
print_status ""
|
||||||
|
print_status "Step 4: Checking file permissions..."
|
||||||
|
chmod +x "$TARGET_DIR/update_firmware.sh"
|
||||||
|
chmod +x "$TARGET_DIR/update_firmware_from_file.sh"
|
||||||
|
chmod 755 "$TARGET_DIR/sqlite/"*.py
|
||||||
|
chmod 755 "$TARGET_DIR/NPM/"*.py
|
||||||
|
chmod 755 "$TARGET_DIR/BME280/"*.py
|
||||||
|
chmod 755 "$TARGET_DIR/SARA/"*.py
|
||||||
|
chmod 755 "$TARGET_DIR/envea/"*.py
|
||||||
|
chmod 755 "$TARGET_DIR/MPPT/"*.py 2>/dev/null
|
||||||
|
chmod 755 "$TARGET_DIR/wifi/"*.py 2>/dev/null
|
||||||
|
check_status "File permissions update"
|
||||||
|
|
||||||
|
# Step 4b: Ensure Apache/PHP config allows file uploads (.htaccess + php.ini)
|
||||||
|
APACHE_MAIN_CONF="/etc/apache2/apache2.conf"
|
||||||
|
if grep -q '<Directory /var/www/>' "$APACHE_MAIN_CONF"; then
|
||||||
|
if ! sed -n '/<Directory \/var\/www\/>/,/<\/Directory>/p' "$APACHE_MAIN_CONF" | grep -q "AllowOverride All"; then
|
||||||
|
sed -i '/<Directory \/var\/www\/>/,/<\/Directory>/ s/AllowOverride None/AllowOverride All/' "$APACHE_MAIN_CONF"
|
||||||
|
print_status "✓ AllowOverride All enabled for Apache"
|
||||||
|
APACHE_CHANGED=true
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
PHP_INI=$(php -r "echo php_ini_loaded_file();" 2>/dev/null)
|
||||||
|
if [ -n "$PHP_INI" ]; then
|
||||||
|
CURRENT_UPLOAD=$(grep -oP 'upload_max_filesize = \K\d+' "$PHP_INI" 2>/dev/null)
|
||||||
|
if [ -n "$CURRENT_UPLOAD" ] && [ "$CURRENT_UPLOAD" -lt 50 ]; then
|
||||||
|
sed -i 's/upload_max_filesize = .*/upload_max_filesize = 50M/' "$PHP_INI"
|
||||||
|
sed -i 's/post_max_size = .*/post_max_size = 55M/' "$PHP_INI"
|
||||||
|
print_status "✓ PHP upload limits set to 50M"
|
||||||
|
APACHE_CHANGED=true
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "${APACHE_CHANGED:-false}" = true ]; then
|
||||||
|
systemctl reload apache2 2>/dev/null
|
||||||
|
print_status "✓ Apache reloaded"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Step 5: Restart critical services
|
||||||
|
print_status ""
|
||||||
|
print_status "Step 5: Managing system services..."
|
||||||
|
|
||||||
|
services=(
|
||||||
|
"nebuleair-npm-data.timer"
|
||||||
|
"nebuleair-envea-data.timer"
|
||||||
|
"nebuleair-sara-data.timer"
|
||||||
|
"nebuleair-bme280-data.timer"
|
||||||
|
"nebuleair-mppt-data.timer"
|
||||||
|
"nebuleair-noise-data.timer"
|
||||||
|
"nebuleair-wifi-powersave.timer"
|
||||||
|
)
|
||||||
|
|
||||||
|
for service in "${services[@]}"; do
|
||||||
|
if systemctl list-unit-files | grep -q "$service"; then
|
||||||
|
if systemctl is-enabled --quiet "$service" 2>/dev/null; then
|
||||||
|
print_status "Restarting enabled service: $service"
|
||||||
|
systemctl restart "$service"
|
||||||
|
if systemctl is-active --quiet "$service"; then
|
||||||
|
print_status "✓ $service is running"
|
||||||
|
else
|
||||||
|
print_status "⚠ $service failed to start"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
print_status "ℹ Service $service is disabled, skipping restart"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
print_status "ℹ Service $service not found (may not be installed)"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# Step 6: System health check
|
||||||
|
print_status ""
|
||||||
|
print_status "Step 6: System health check..."
|
||||||
|
|
||||||
|
disk_usage=$(df / | awk 'NR==2 {print $5}' | sed 's/%//')
|
||||||
|
if [ "$disk_usage" -gt 90 ]; then
|
||||||
|
print_status "⚠ Warning: Disk usage is high ($disk_usage%)"
|
||||||
|
else
|
||||||
|
print_status "✓ Disk usage is acceptable ($disk_usage%)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -f "$TARGET_DIR/sqlite/sensors.db" ]; then
|
||||||
|
print_status "✓ Database file exists"
|
||||||
|
else
|
||||||
|
print_status "⚠ Warning: Database file not found"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Cleanup logs > 10MB
|
||||||
|
find "$TARGET_DIR/logs" -name "*.log" -size +10M -exec truncate -s 0 {} \;
|
||||||
|
check_status "Log cleanup"
|
||||||
|
|
||||||
|
# Step 7: Cleanup temporary files
|
||||||
|
print_status ""
|
||||||
|
print_status "Step 7: Cleaning up temporary files..."
|
||||||
|
rm -rf /tmp/nebuleair_update/
|
||||||
|
check_status "Temp cleanup"
|
||||||
|
|
||||||
|
print_status ""
|
||||||
|
print_status "======================================"
|
||||||
|
print_status "Update from $OLD_VERSION to $NEW_VERSION completed successfully!"
|
||||||
|
print_status "======================================"
|
||||||
|
|
||||||
|
exit 0
|
||||||
131
wifi/power_save.py
Executable file
131
wifi/power_save.py
Executable file
@@ -0,0 +1,131 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
r'''
|
||||||
|
__ ___ ___ ___ ____
|
||||||
|
\ \ / (_) _(_) | | _ \ _____ _____ _ __
|
||||||
|
\ \ /\ / /| | | | | | | |_) / _ \ \ /\ / / _ \ '__|
|
||||||
|
\ V V / | | | | | | | __/ (_) \ V V / __/ |
|
||||||
|
\_/\_/ |_|_| |_|_| |_| \___/ \_/\_/ \___|_|
|
||||||
|
____
|
||||||
|
/ ___| __ ___ _____
|
||||||
|
\___ \ / _` \ \ / / _ \
|
||||||
|
___) | (_| |\ V / __/
|
||||||
|
|____/ \__,_| \_/ \___|
|
||||||
|
|
||||||
|
WiFi Power Saving Script
|
||||||
|
Disables WiFi completely after 10 minutes of boot if wifi_power_saving is enabled in config.
|
||||||
|
Saves ~100-200mA of power consumption.
|
||||||
|
WiFi is automatically re-enabled at next boot (see boot_hotspot.sh).
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
/usr/bin/python3 /var/www/nebuleair_pro_4g/wifi/power_save.py
|
||||||
|
'''
|
||||||
|
|
||||||
|
import sqlite3
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import datetime
|
||||||
|
|
||||||
|
# Paths
|
||||||
|
DB_PATH = "/var/www/nebuleair_pro_4g/sqlite/sensors.db"
|
||||||
|
LOG_PATH = "/var/www/nebuleair_pro_4g/logs/app.log"
|
||||||
|
|
||||||
|
def log_message(message):
|
||||||
|
"""Write message to log file with timestamp"""
|
||||||
|
timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
log_entry = f"[{timestamp}] [WiFi Power Save] {message}\n"
|
||||||
|
try:
|
||||||
|
with open(LOG_PATH, "a") as log_file:
|
||||||
|
log_file.write(log_entry)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Failed to write to log: {e}", file=sys.stderr)
|
||||||
|
print(log_entry.strip())
|
||||||
|
|
||||||
|
def get_config_value(key):
|
||||||
|
"""Read configuration value from database"""
|
||||||
|
try:
|
||||||
|
conn = sqlite3.connect(DB_PATH)
|
||||||
|
cursor = conn.cursor()
|
||||||
|
cursor.execute("SELECT value FROM config_table WHERE key = ?", (key,))
|
||||||
|
result = cursor.fetchone()
|
||||||
|
conn.close()
|
||||||
|
return result[0] if result else None
|
||||||
|
except sqlite3.Error as e:
|
||||||
|
log_message(f"Database error: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def is_wifi_enabled():
|
||||||
|
"""Check if WiFi radio is currently enabled"""
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
["nmcli", "radio", "wifi"],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
timeout=10
|
||||||
|
)
|
||||||
|
return result.stdout.strip() == "enabled"
|
||||||
|
except Exception as e:
|
||||||
|
log_message(f"Failed to check WiFi status: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def disable_wifi():
|
||||||
|
"""Disable WiFi radio completely using nmcli"""
|
||||||
|
try:
|
||||||
|
# Disable WiFi radio completely to save maximum power
|
||||||
|
# WiFi will be re-enabled automatically at next boot by boot_hotspot.sh
|
||||||
|
result = subprocess.run(
|
||||||
|
["sudo", "nmcli", "radio", "wifi", "off"],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
timeout=10
|
||||||
|
)
|
||||||
|
if result.returncode == 0:
|
||||||
|
log_message("WiFi disabled completely - saving ~100-200mA power (will re-enable at next boot)")
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
log_message(f"Failed to disable WiFi: {result.stderr}")
|
||||||
|
return False
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
log_message("Timeout while trying to disable WiFi")
|
||||||
|
return False
|
||||||
|
except Exception as e:
|
||||||
|
log_message(f"Error disabling WiFi: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""Main function"""
|
||||||
|
log_message("WiFi power save script started")
|
||||||
|
|
||||||
|
# Check if wifi_power_saving is enabled in config
|
||||||
|
wifi_power_saving = get_config_value("wifi_power_saving")
|
||||||
|
|
||||||
|
if wifi_power_saving is None:
|
||||||
|
log_message("wifi_power_saving config not found - skipping (run migration or set_config.py)")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
if wifi_power_saving == "0":
|
||||||
|
log_message("WiFi power saving is disabled in config - WiFi will remain enabled")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
log_message("WiFi power saving is enabled in config")
|
||||||
|
|
||||||
|
# Check current WiFi status
|
||||||
|
wifi_enabled = is_wifi_enabled()
|
||||||
|
if wifi_enabled is None:
|
||||||
|
log_message("Could not determine WiFi status - aborting")
|
||||||
|
return 1
|
||||||
|
|
||||||
|
if not wifi_enabled:
|
||||||
|
log_message("WiFi is already disabled - nothing to do")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
# Disable WiFi completely after 10-minute configuration window
|
||||||
|
log_message("Disabling WiFi completely after 10-minute configuration window...")
|
||||||
|
if disable_wifi():
|
||||||
|
log_message("WiFi disabled successfully - will re-enable at next boot")
|
||||||
|
return 0
|
||||||
|
else:
|
||||||
|
log_message("WiFi disable failed")
|
||||||
|
return 1
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
Reference in New Issue
Block a user