Add password protection for critical transmission settings
- Add unlock/lock button for protected settings in admin panel - Protect AirCarto, uSpot, and miotiq transmission checkboxes - Require password '123plouf' to enable editing protected checkboxes - Visual feedback with lock/unlock icons and toast notifications - Add CLAUDE.md documentation file for development guidance 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
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
|
||||||
@@ -118,15 +118,25 @@
|
|||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||||
|
<span class="fw-bold">Protected Settings</span>
|
||||||
|
<button type="button" class="btn btn-sm btn-outline-primary" onclick="toggleProtectedSettings()" id="unlockBtn">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-lock-fill" viewBox="0 0 16 16">
|
||||||
|
<path d="M8 1a2 2 0 0 1 2 2v4H6V3a2 2 0 0 1 2-2zm3 6V3a3 3 0 0 0-6 0v4a2 2 0 0 0-2 2v5a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V9a2 2 0 0 0-2-2z"/>
|
||||||
|
</svg>
|
||||||
|
Unlock
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="form-check mb-3">
|
<div class="form-check mb-3">
|
||||||
<input class="form-check-input" type="checkbox" value="" id="check_aircarto" onchange="update_config_sqlite('send_aircarto', this.checked)" disabled>
|
<input class="form-check-input protected-checkbox" type="checkbox" value="" id="check_aircarto" onchange="update_config_sqlite('send_aircarto', this.checked)" disabled>
|
||||||
<label class="form-check-label" for="check_aircarto">
|
<label class="form-check-label" for="check_aircarto">
|
||||||
Send to AirCarto (HTTP)
|
Send to AirCarto (HTTP)
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-check mb-3">
|
<div class="form-check mb-3">
|
||||||
<input class="form-check-input" type="checkbox" value="" id="check_uSpot" onchange="update_config_sqlite('send_uSpot', this.checked)" disabled>
|
<input class="form-check-input protected-checkbox" type="checkbox" value="" id="check_uSpot" onchange="update_config_sqlite('send_uSpot', this.checked)" disabled>
|
||||||
<label class="form-check-label" for="check_uSpot">
|
<label class="form-check-label" for="check_uSpot">
|
||||||
Send to uSpot (HTTPS)
|
Send to uSpot (HTTPS)
|
||||||
</label>
|
</label>
|
||||||
@@ -134,7 +144,7 @@
|
|||||||
|
|
||||||
|
|
||||||
<div class="form-check mb-3">
|
<div class="form-check mb-3">
|
||||||
<input class="form-check-input" type="checkbox" value="" id="check_miotiq" onchange="update_config_sqlite('send_miotiq', this.checked)" disabled>
|
<input class="form-check-input protected-checkbox" type="checkbox" value="" id="check_miotiq" onchange="update_config_sqlite('send_miotiq', this.checked)" disabled>
|
||||||
<label class="form-check-label" for="check_miotiq">
|
<label class="form-check-label" for="check_miotiq">
|
||||||
Send to miotiq (UDP)
|
Send to miotiq (UDP)
|
||||||
</label>
|
</label>
|
||||||
@@ -1454,6 +1464,71 @@ function displayDetectionResults(results) {
|
|||||||
document.getElementById('startDetectionBtn').textContent = 'Scan Again';
|
document.getElementById('startDetectionBtn').textContent = 'Scan Again';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
____ _ _ _ ____ _ _ _
|
||||||
|
| _ \ _ __ ___ | |_ ___ ___| |_ ___ __| | / ___| ___| |_| |_(_)_ __ __ _ ___
|
||||||
|
| |_) | '__/ _ \| __/ _ \/ __| __/ _ \/ _` | \___ \ / _ \ __| __| | '_ \ / _` / __|
|
||||||
|
| __/| | | (_) | || __/ (__| || __/ (_| | ___) | __/ |_| |_| | | | | (_| \__ \
|
||||||
|
|_| |_| \___/ \__\___|\___|\__\___|\__,_| |____/ \___|\__|\__|_|_| |_|\__, |___/
|
||||||
|
|___/
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Track if protected settings are unlocked
|
||||||
|
let protectedSettingsUnlocked = false;
|
||||||
|
|
||||||
|
function toggleProtectedSettings() {
|
||||||
|
const unlockBtn = document.getElementById('unlockBtn');
|
||||||
|
const protectedCheckboxes = document.querySelectorAll('.protected-checkbox');
|
||||||
|
|
||||||
|
if (protectedSettingsUnlocked) {
|
||||||
|
// Lock the settings
|
||||||
|
protectedSettingsUnlocked = false;
|
||||||
|
protectedCheckboxes.forEach(checkbox => {
|
||||||
|
checkbox.disabled = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update button appearance
|
||||||
|
unlockBtn.classList.remove('btn-success');
|
||||||
|
unlockBtn.classList.add('btn-outline-primary');
|
||||||
|
unlockBtn.innerHTML = `
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-lock-fill" viewBox="0 0 16 16">
|
||||||
|
<path d="M8 1a2 2 0 0 1 2 2v4H6V3a2 2 0 0 1 2-2zm3 6V3a3 3 0 0 0-6 0v4a2 2 0 0 0-2 2v5a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V9a2 2 0 0 0-2-2z"/>
|
||||||
|
</svg>
|
||||||
|
Unlock
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Show toast notification
|
||||||
|
showToast('Protected settings locked', 'info');
|
||||||
|
} else {
|
||||||
|
// Prompt for password
|
||||||
|
const password = prompt('Enter admin password to unlock protected settings:');
|
||||||
|
|
||||||
|
if (password === '123plouf') {
|
||||||
|
// Correct password - unlock the settings
|
||||||
|
protectedSettingsUnlocked = true;
|
||||||
|
protectedCheckboxes.forEach(checkbox => {
|
||||||
|
checkbox.disabled = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update button appearance
|
||||||
|
unlockBtn.classList.remove('btn-outline-primary');
|
||||||
|
unlockBtn.classList.add('btn-success');
|
||||||
|
unlockBtn.innerHTML = `
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-unlock-fill" viewBox="0 0 16 16">
|
||||||
|
<path d="M11 1a2 2 0 0 0-2 2v4a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V9a2 2 0 0 1 2-2h5V3a3 3 0 0 1 6 0v4a.5.5 0 0 1-1 0V3a2 2 0 0 0-2-2z"/>
|
||||||
|
</svg>
|
||||||
|
Lock
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Show success toast
|
||||||
|
showToast('Protected settings unlocked! You can now edit the checkboxes.', 'success');
|
||||||
|
} else if (password !== null) {
|
||||||
|
// Wrong password (null means user cancelled)
|
||||||
|
showToast('Incorrect password!', 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user