Compare commits
27 Commits
| 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 |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -18,5 +18,8 @@ sqlite/*.sql
|
|||||||
|
|
||||||
tests/
|
tests/
|
||||||
|
|
||||||
|
# Secrets
|
||||||
|
.env
|
||||||
|
|
||||||
# Claude Code local settings
|
# Claude Code local settings
|
||||||
.claude/settings.local.json
|
.claude/settings.local.json
|
||||||
@@ -40,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()
|
||||||
@@ -72,6 +75,7 @@ channel_4 = 0
|
|||||||
channel_5 = 0
|
channel_5 = 0
|
||||||
relative_humidity = 0
|
relative_humidity = 0
|
||||||
temperature = 0
|
temperature = 0
|
||||||
|
npm_status = 0xFF # Default: all errors (will be overwritten if read succeeds)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Initialize serial port
|
# Initialize serial port
|
||||||
@@ -109,7 +113,8 @@ try:
|
|||||||
|
|
||||||
# Validate response length
|
# Validate response length
|
||||||
if len(byte_data) < response_length:
|
if len(byte_data) < response_length:
|
||||||
print(f"[ERROR] Incomplete response received: {byte_data.hex()}")
|
if not dry_run:
|
||||||
|
print(f"[ERROR] Incomplete response received: {byte_data.hex()}")
|
||||||
raise Exception("Incomplete response")
|
raise Exception("Incomplete response")
|
||||||
|
|
||||||
# Verify CRC
|
# Verify CRC
|
||||||
@@ -117,7 +122,8 @@ try:
|
|||||||
calculated_crc = crc16(byte_data[:-2])
|
calculated_crc = crc16(byte_data[:-2])
|
||||||
|
|
||||||
if received_crc != calculated_crc:
|
if received_crc != calculated_crc:
|
||||||
print("[ERROR] CRC check failed! Corrupted data received.")
|
if not dry_run:
|
||||||
|
print("[ERROR] CRC check failed! Corrupted data received.")
|
||||||
raise Exception("CRC check failed")
|
raise Exception("CRC check failed")
|
||||||
|
|
||||||
# Convert response to hex for debugging
|
# Convert response to hex for debugging
|
||||||
@@ -176,22 +182,63 @@ try:
|
|||||||
#print(f"Internal Relative Humidity: {relative_humidity} %")
|
#print(f"Internal Relative Humidity: {relative_humidity} %")
|
||||||
#print(f"Internal temperature: {temperature} °C")
|
#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:
|
||||||
|
if not dry_run:
|
||||||
|
print(f"[WARNING] NPM status incomplete response ({len(status_response)} bytes)")
|
||||||
|
|
||||||
ser.close()
|
ser.close()
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"[ERROR] Sensor communication failed: {e}")
|
if not dry_run:
|
||||||
|
print(f"[ERROR] Sensor communication failed: {e}")
|
||||||
# Variables already set to -1 at the beginning
|
# Variables already set to -1 at the beginning
|
||||||
|
|
||||||
finally:
|
finally:
|
||||||
# Always save data to database, even if all values are -1
|
if dry_run:
|
||||||
cursor.execute('''
|
# Print JSON output without writing to database
|
||||||
INSERT INTO data_NPM_5channels (timestamp,PM_ch1, PM_ch2, PM_ch3, PM_ch4, PM_ch5) VALUES (?,?,?,?,?,?)'''
|
result = {
|
||||||
, (rtc_time_str, channel_1, channel_2, channel_3, channel_4, channel_5))
|
"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:
|
||||||
|
# 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))
|
||||||
|
|
||||||
cursor.execute('''
|
cursor.execute('''
|
||||||
INSERT INTO data_NPM (timestamp,PM1, PM25, PM10, temp_npm, hum_npm) VALUES (?,?,?,?,?,?)'''
|
INSERT INTO data_NPM (timestamp,PM1, PM25, PM10, temp_npm, hum_npm, npm_status) VALUES (?,?,?,?,?,?,?)'''
|
||||||
, (rtc_time_str, pm1_10s, pm25_10s, pm10_10s, temperature, relative_humidity))
|
, (rtc_time_str, pm1_10s, pm25_10s, pm10_10s, temperature, relative_humidity, npm_status))
|
||||||
|
|
||||||
|
# Commit and close the connection
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
# Commit and close the connection
|
|
||||||
conn.commit()
|
|
||||||
conn.close()
|
conn.close()
|
||||||
140
README.md
140
README.md
@@ -181,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)
|
||||||
|
|||||||
@@ -1,5 +1,102 @@
|
|||||||
{
|
{
|
||||||
"versions": [
|
"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",
|
"version": "1.4.5",
|
||||||
"date": "2026-03-17",
|
"date": "2026-03-17",
|
||||||
|
|||||||
@@ -211,24 +211,38 @@
|
|||||||
<h3 class="mt-4">Clock</h3>
|
<h3 class="mt-4">Clock</h3>
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="sys_local_time" class="form-label">System time (local)</label>
|
<label for="RTC_utc_time" class="form-label fw-bold fs-5">RTC time (UTC)</label>
|
||||||
<input type="text" class="form-control" id="sys_local_time" disabled>
|
<input type="text" class="form-control form-control-lg border-primary" id="RTC_utc_time" disabled>
|
||||||
|
<small class="text-muted">Module DS3231 avec pile de sauvegarde. Garde l'heure meme hors tension. Horloge de reference du capteur.</small>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="sys_UTC_time" class="form-label">System time (UTC)</label>
|
<label for="browser_utc_time" class="form-label">Browser time (UTC)</label>
|
||||||
<input type="text" class="form-control" id="sys_UTC_time" disabled>
|
<input type="text" class="form-control" id="browser_utc_time" disabled>
|
||||||
|
<small class="text-muted">Heure de votre appareil (PC/Mac/tablette). Reference pour verifier le RTC.</small>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-3">
|
<hr>
|
||||||
<label for="RTC_utc_time" class="form-label">RTC time (UTC)</label>
|
<details class="mb-3">
|
||||||
<input type="text" class="form-control" id="RTC_utc_time" disabled>
|
<summary class="text-muted" style="cursor:pointer;">System time (non utilise par le capteur)</summary>
|
||||||
</div>
|
<div class="mt-2">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="sys_local_time" class="form-label">System time (local)</label>
|
||||||
|
<input type="text" class="form-control form-control-sm" id="sys_local_time" disabled>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="sys_UTC_time" class="form-label">System time (UTC)</label>
|
||||||
|
<input type="text" class="form-control form-control-sm" id="sys_UTC_time" disabled>
|
||||||
|
</div>
|
||||||
|
<small class="text-muted">Horloge Linux du Raspberry Pi. Se synchronise via internet (NTP). Non utilisee par le capteur.</small>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
|
||||||
|
|
||||||
<div id="alert_container"></div>
|
<div id="alert_container"></div>
|
||||||
|
|
||||||
<h5 class="mt-4">Set RTC</h5>
|
<h5 class="mt-4">Synchroniser le RTC</h5>
|
||||||
|
<small class="text-muted d-block mb-2">Met a jour l'horloge RTC pour qu'elle reste precise sans internet.</small>
|
||||||
|
|
||||||
<button type="submit" class="btn btn-primary mb-1" onclick="set_RTC_withNTP()">WiFi (NTP) </button>
|
<button type="submit" class="btn btn-primary mb-1" onclick="set_RTC_withNTP()">WiFi (NTP) </button>
|
||||||
<button type="submit" class="btn btn-primary mb-1" onclick="set_RTC_withBrowser()">Browser time </button>
|
<button type="submit" class="btn btn-primary mb-1" onclick="set_RTC_withBrowser()">Browser time </button>
|
||||||
@@ -585,29 +599,54 @@ window.onload = function() {
|
|||||||
document.getElementById("sys_UTC_time").value = response.system_utc_time;
|
document.getElementById("sys_UTC_time").value = response.system_utc_time;
|
||||||
document.getElementById("RTC_utc_time").value = response.rtc_module_time;
|
document.getElementById("RTC_utc_time").value = response.rtc_module_time;
|
||||||
|
|
||||||
// Get the time difference
|
// Display browser time in UTC
|
||||||
const timeDiff = response.time_difference_seconds;
|
const browserDate = new Date();
|
||||||
|
const browserUTC = browserDate.getUTCFullYear() + '-' +
|
||||||
|
String(browserDate.getUTCMonth() + 1).padStart(2, '0') + '-' +
|
||||||
|
String(browserDate.getUTCDate()).padStart(2, '0') + ' ' +
|
||||||
|
String(browserDate.getUTCHours()).padStart(2, '0') + ':' +
|
||||||
|
String(browserDate.getUTCMinutes()).padStart(2, '0') + ':' +
|
||||||
|
String(browserDate.getUTCSeconds()).padStart(2, '0');
|
||||||
|
document.getElementById("browser_utc_time").value = browserUTC;
|
||||||
|
|
||||||
// Reference to the alert container
|
// Compare RTC time with browser time
|
||||||
const alertContainer = document.getElementById("alert_container");
|
const alertContainer = document.getElementById("alert_container");
|
||||||
|
|
||||||
// Remove any previous alert
|
|
||||||
alertContainer.innerHTML = "";
|
alertContainer.innerHTML = "";
|
||||||
|
const rtcInput = document.getElementById("RTC_utc_time");
|
||||||
|
|
||||||
// Add an alert based on time difference
|
if (response.rtc_module_time === 'not connected' || !response.rtc_module_time) {
|
||||||
if (typeof timeDiff === "number") {
|
// RTC module disconnected
|
||||||
if (timeDiff >= 0 && timeDiff <= 10) {
|
rtcInput.classList.add('border-danger', 'text-danger');
|
||||||
|
rtcInput.classList.remove('border-primary');
|
||||||
|
alertContainer.innerHTML = `
|
||||||
|
<div class="alert alert-danger d-flex align-items-center" role="alert">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" class="bi bi-exclamation-triangle-fill me-2 flex-shrink-0" viewBox="0 0 16 16">
|
||||||
|
<path d="M8.982 1.566a1.13 1.13 0 0 0-1.96 0L.165 13.233c-.457.778.091 1.767.98 1.767h13.713c.889 0 1.436-.99.98-1.767L8.982 1.566zM8 5c.535 0 .954.462.9.995l-.35 3.507a.552.552 0 0 1-1.1 0L7.1 5.995A.905.905 0 0 1 8 5zm.002 6a1 1 0 1 1 0 2 1 1 0 0 1 0-2z"/>
|
||||||
|
</svg>
|
||||||
|
<div>
|
||||||
|
<strong>Module RTC deconnecte !</strong><br>
|
||||||
|
Verifiez la pile du module DS3231 et les cables I2C.
|
||||||
|
</div>
|
||||||
|
</div>`;
|
||||||
|
} else {
|
||||||
|
const rtcDate = new Date(response.rtc_module_time + ' UTC');
|
||||||
|
const timeDiff = Math.abs(Math.round((browserDate - rtcDate) / 1000));
|
||||||
|
|
||||||
|
if (timeDiff <= 30) {
|
||||||
alertContainer.innerHTML = `
|
alertContainer.innerHTML = `
|
||||||
<div class="alert alert-success" role="alert">
|
<div class="alert alert-success" role="alert">
|
||||||
RTC and system time are in sync (Difference: ${timeDiff} sec).
|
RTC synchronise avec l'heure du navigateur (ecart: ${timeDiff} sec).
|
||||||
</div>`;
|
</div>`;
|
||||||
} else if (timeDiff > 10) {
|
} else {
|
||||||
|
const minutes = Math.floor(timeDiff / 60);
|
||||||
|
const label = minutes > 0 ? `${minutes} min ${timeDiff % 60} sec` : `${timeDiff} sec`;
|
||||||
alertContainer.innerHTML = `
|
alertContainer.innerHTML = `
|
||||||
<div class="alert alert-danger" role="alert">
|
<div class="alert alert-danger" role="alert">
|
||||||
RTC time is out of sync! (Difference: ${timeDiff} sec).
|
RTC desynchronise ! Ecart avec le navigateur: ${label}.
|
||||||
|
Utilisez "Synchroniser le RTC" ci-dessous.
|
||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
error: function(xhr, status, error) {
|
error: function(xhr, status, error) {
|
||||||
console.error('AJAX request failed:', status, error);
|
console.error('AJAX request failed:', status, error);
|
||||||
|
|||||||
@@ -246,7 +246,7 @@ async function selfTestSequence() {
|
|||||||
addSelfTestLog(`Device ID: ${selfTestReport.deviceId}`);
|
addSelfTestLog(`Device ID: ${selfTestReport.deviceId}`);
|
||||||
addSelfTestLog(`Device Name: ${selfTestReport.deviceName}`);
|
addSelfTestLog(`Device Name: ${selfTestReport.deviceName}`);
|
||||||
addSelfTestLog(`Modem Version: ${selfTestReport.modemVersion}`);
|
addSelfTestLog(`Modem Version: ${selfTestReport.modemVersion}`);
|
||||||
addSelfTestLog(`System Time (RTC): ${selfTestReport.systemTime}`);
|
addSelfTestLog(`RTC Time: ${selfTestReport.systemTime}`);
|
||||||
addSelfTestLog(`Browser Time: ${new Date().toLocaleString()}`);
|
addSelfTestLog(`Browser Time: ${new Date().toLocaleString()}`);
|
||||||
addSelfTestLog(`GPS: ${selfTestReport.latitude}, ${selfTestReport.longitude}`);
|
addSelfTestLog(`GPS: ${selfTestReport.latitude}, ${selfTestReport.longitude}`);
|
||||||
addSelfTestLog('────────────────────────────────────────────────────────');
|
addSelfTestLog('────────────────────────────────────────────────────────');
|
||||||
@@ -320,10 +320,10 @@ async function selfTestSequence() {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
if (sensor.type === 'npm') {
|
if (sensor.type === 'npm') {
|
||||||
// NPM sensor test
|
// NPM sensor test (uses get_data_modbus_v3.py --dry-run)
|
||||||
const npmResult = await new Promise((resolve, reject) => {
|
const npmResult = await new Promise((resolve, reject) => {
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: 'launcher.php?type=npm&port=' + sensor.port,
|
url: 'launcher.php?type=npm',
|
||||||
dataType: 'json',
|
dataType: 'json',
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
timeout: 15000,
|
timeout: 15000,
|
||||||
@@ -333,22 +333,42 @@ async function selfTestSequence() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
selfTestReport.rawResponses['NPM Sensor'] = JSON.stringify(npmResult, null, 2);
|
selfTestReport.rawResponses['NPM Sensor'] = JSON.stringify(npmResult, null, 2);
|
||||||
addSelfTestLog(`NPM response: PM1=${npmResult.PM1}, PM2.5=${npmResult.PM25}, PM10=${npmResult.PM10}`);
|
addSelfTestLog(`NPM response: PM1=${npmResult.PM1}, PM2.5=${npmResult.PM25}, PM10=${npmResult.PM10}, status=${npmResult.npm_status_hex}`);
|
||||||
|
|
||||||
// Check for errors
|
// Decode npm_status flags
|
||||||
const npmErrors = ['notReady', 'fanError', 'laserError', 'heatError', 't_rhError', 'memoryError', 'degradedState'];
|
const status = npmResult.npm_status !== undefined ? npmResult.npm_status : 0;
|
||||||
const activeErrors = npmErrors.filter(e => npmResult[e] === 1);
|
|
||||||
|
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) {
|
if (activeErrors.length > 0) {
|
||||||
updateTestStatus(sensor.id, 'Warning', `Errors: ${activeErrors.join(', ')}`, 'bg-warning');
|
updateTestStatus(sensor.id, 'Warning', `Status ${npmResult.npm_status_hex}: ${activeErrors.join(', ')}`, 'bg-warning');
|
||||||
testsFailed++;
|
testsFailed++;
|
||||||
} else if (npmResult.PM1 !== undefined && npmResult.PM25 !== undefined && npmResult.PM10 !== undefined) {
|
} 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} ug/m3`, 'bg-success');
|
updateTestStatus(sensor.id, 'Passed', `PM1: ${npmResult.PM1} | PM2.5: ${npmResult.PM25} | PM10: ${npmResult.PM10} µg/m³`, 'bg-success');
|
||||||
testsPassed++;
|
testsPassed++;
|
||||||
} else {
|
} else {
|
||||||
updateTestStatus(sensor.id, 'Warning', 'Incomplete data received', 'bg-warning');
|
updateTestStatus(sensor.id, 'Warning', 'Incomplete data received', 'bg-warning');
|
||||||
testsFailed++;
|
testsFailed++;
|
||||||
}
|
}
|
||||||
|
} // end else (not 0xFF)
|
||||||
|
|
||||||
} else if (sensor.type === 'BME280') {
|
} else if (sensor.type === 'BME280') {
|
||||||
// BME280 sensor test
|
// BME280 sensor test
|
||||||
@@ -449,16 +469,19 @@ async function selfTestSequence() {
|
|||||||
updateTestStatus(sensor.id, 'Failed', 'RTC module not connected', 'bg-danger');
|
updateTestStatus(sensor.id, 'Failed', 'RTC module not connected', 'bg-danger');
|
||||||
testsFailed++;
|
testsFailed++;
|
||||||
} else if (rtcResult.rtc_module_time) {
|
} else if (rtcResult.rtc_module_time) {
|
||||||
const timeDiff = rtcResult.time_difference_seconds;
|
// Compare RTC with browser time (more reliable than system time)
|
||||||
if (typeof timeDiff === 'number' && Math.abs(timeDiff) <= 60) {
|
const rtcDate = new Date(rtcResult.rtc_module_time + ' UTC');
|
||||||
updateTestStatus(sensor.id, 'Passed', `${rtcResult.rtc_module_time} (sync OK, diff: ${timeDiff}s)`, 'bg-success');
|
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++;
|
testsPassed++;
|
||||||
} else if (typeof timeDiff === 'number') {
|
|
||||||
updateTestStatus(sensor.id, 'Warning', `${rtcResult.rtc_module_time} (out of sync: ${timeDiff}s)`, 'bg-warning');
|
|
||||||
testsFailed++;
|
|
||||||
} else {
|
} else {
|
||||||
updateTestStatus(sensor.id, 'Passed', `${rtcResult.rtc_module_time}`, 'bg-success');
|
const minutes = Math.floor(timeDiff / 60);
|
||||||
testsPassed++;
|
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 {
|
} else {
|
||||||
updateTestStatus(sensor.id, 'Warning', 'Unexpected response', 'bg-warning');
|
updateTestStatus(sensor.id, 'Warning', 'Unexpected response', 'bg-warning');
|
||||||
|
|||||||
@@ -327,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 += `
|
||||||
@@ -400,6 +401,10 @@ function get_data_sqlite(table, limit, download , startDate = "", endDate = "")
|
|||||||
tableHTML += `<tr${rowClass}>`;
|
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>
|
||||||
@@ -407,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 += `
|
||||||
@@ -519,7 +525,7 @@ 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";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -419,8 +419,9 @@ if ($type == "upload_firmware") {
|
|||||||
|
|
||||||
// Check file upload
|
// Check file upload
|
||||||
if (!isset($_FILES['firmware_file']) || $_FILES['firmware_file']['error'] !== UPLOAD_ERR_OK) {
|
if (!isset($_FILES['firmware_file']) || $_FILES['firmware_file']['error'] !== UPLOAD_ERR_OK) {
|
||||||
|
$max_upload = ini_get('upload_max_filesize');
|
||||||
$upload_errors = [
|
$upload_errors = [
|
||||||
UPLOAD_ERR_INI_SIZE => 'File exceeds server upload limit',
|
UPLOAD_ERR_INI_SIZE => "Le fichier depasse la limite serveur (actuellement $max_upload). Effectuez d'abord une mise a jour via WiFi (bouton Update firmware) pour debloquer l'upload hors-ligne.",
|
||||||
UPLOAD_ERR_FORM_SIZE => 'File exceeds form upload limit',
|
UPLOAD_ERR_FORM_SIZE => 'File exceeds form upload limit',
|
||||||
UPLOAD_ERR_PARTIAL => 'File was only partially uploaded',
|
UPLOAD_ERR_PARTIAL => 'File was only partially uploaded',
|
||||||
UPLOAD_ERR_NO_FILE => 'No file was uploaded',
|
UPLOAD_ERR_NO_FILE => 'No file was uploaded',
|
||||||
@@ -804,8 +805,7 @@ if ($type == "reboot") {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ($type == "npm") {
|
if ($type == "npm") {
|
||||||
$port=$_GET['port'];
|
$command = 'sudo /usr/bin/python3 /var/www/nebuleair_pro_4g/NPM/get_data_modbus_v3.py --dry-run';
|
||||||
$command = 'sudo /usr/bin/python3 /var/www/nebuleair_pro_4g/NPM/get_data.py ' . $port;
|
|
||||||
$output = shell_exec($command);
|
$output = shell_exec($command);
|
||||||
echo $output;
|
echo $output;
|
||||||
}
|
}
|
||||||
@@ -1108,7 +1108,7 @@ if ($type == "wifi_connect") {
|
|||||||
$SSID=$_GET['SSID'];
|
$SSID=$_GET['SSID'];
|
||||||
$PASS=$_GET['pass'];
|
$PASS=$_GET['pass'];
|
||||||
|
|
||||||
// Get device name from database for instructions
|
// Get device name and hostname for instructions
|
||||||
try {
|
try {
|
||||||
$db = new PDO("sqlite:$database_path");
|
$db = new PDO("sqlite:$database_path");
|
||||||
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
|
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
|
||||||
@@ -1120,6 +1120,7 @@ if ($type == "wifi_connect") {
|
|||||||
} catch (PDOException $e) {
|
} catch (PDOException $e) {
|
||||||
$deviceName = 'NebuleAir';
|
$deviceName = 'NebuleAir';
|
||||||
}
|
}
|
||||||
|
$hostname = trim(shell_exec('hostname 2>/dev/null')) ?: 'aircarto';
|
||||||
|
|
||||||
// Launch connection script in background
|
// Launch connection script in background
|
||||||
$script_path = '/var/www/nebuleair_pro_4g/connexion.sh';
|
$script_path = '/var/www/nebuleair_pro_4g/connexion.sh';
|
||||||
@@ -1132,6 +1133,7 @@ if ($type == "wifi_connect") {
|
|||||||
'success' => true,
|
'success' => true,
|
||||||
'ssid' => $SSID,
|
'ssid' => $SSID,
|
||||||
'deviceName' => $deviceName,
|
'deviceName' => $deviceName,
|
||||||
|
'hostname' => $hostname,
|
||||||
'message' => 'Connection attempt started',
|
'message' => 'Connection attempt started',
|
||||||
'instructions' => [
|
'instructions' => [
|
||||||
'fr' => [
|
'fr' => [
|
||||||
@@ -1139,7 +1141,7 @@ if ($type == "wifi_connect") {
|
|||||||
'step1' => "Le capteur tente de se connecter au réseau « $SSID »",
|
'step1' => "Le capteur tente de se connecter au réseau « $SSID »",
|
||||||
'step2' => "Vous allez être déconnecté du hotspot dans quelques secondes",
|
'step2' => "Vous allez être déconnecté du hotspot dans quelques secondes",
|
||||||
'step3' => "Reconnectez-vous au WiFi « $SSID » sur votre appareil",
|
'step3' => "Reconnectez-vous au WiFi « $SSID » sur votre appareil",
|
||||||
'step4' => "Accédez au capteur via http://$deviceName.local ou cherchez son IP dans votre routeur",
|
'step4' => "Accédez au capteur via http://$hostname.local/html/ ou cherchez son IP dans votre routeur",
|
||||||
'warning' => "Si la connexion échoue, le capteur recréera automatiquement le hotspot"
|
'warning' => "Si la connexion échoue, le capteur recréera automatiquement le hotspot"
|
||||||
],
|
],
|
||||||
'en' => [
|
'en' => [
|
||||||
@@ -1147,7 +1149,7 @@ if ($type == "wifi_connect") {
|
|||||||
'step1' => "The sensor is attempting to connect to network « $SSID »",
|
'step1' => "The sensor is attempting to connect to network « $SSID »",
|
||||||
'step2' => "You will be disconnected from the hotspot in a few seconds",
|
'step2' => "You will be disconnected from the hotspot in a few seconds",
|
||||||
'step3' => "Reconnect your device to WiFi « $SSID »",
|
'step3' => "Reconnect your device to WiFi « $SSID »",
|
||||||
'step4' => "Access the sensor via http://$deviceName.local or find its IP in your router",
|
'step4' => "Access the sensor via http://$hostname.local/html/ or find its IP in your router",
|
||||||
'warning' => "If connection fails, the sensor will automatically recreate the hotspot"
|
'warning' => "If connection fails, the sensor will automatically recreate the hotspot"
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -117,54 +117,94 @@
|
|||||||
$("#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", "message"];
|
// PM values
|
||||||
// Error messages mapping
|
const pmKeys = ["PM1", "PM25", "PM10"];
|
||||||
const errorMessages = {
|
pmKeys.forEach(key => {
|
||||||
"notReady": "Sensor is not ready",
|
if (response[key] !== undefined) {
|
||||||
"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(`
|
$("#data-table-body_" + port).append(`
|
||||||
<tr>
|
<tr>
|
||||||
<td>${key}</td>
|
<td>${key}</td>
|
||||||
<td>${value} µg/m³</td>
|
<td>${response[key]} µg/m³</td>
|
||||||
</tr>
|
</tr>
|
||||||
`);
|
`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Check for errors and add them to the table
|
// Temperature & humidity
|
||||||
Object.keys(errorMessages).forEach(errorKey => {
|
if (response.temperature !== undefined) {
|
||||||
if (response[errorKey] === 1) {
|
$("#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(`
|
$("#data-table-body_" + port).append(`
|
||||||
<tr class="error-row">
|
<tr>
|
||||||
<td><b>${errorKey}</b></td>
|
<td>Status</td>
|
||||||
<td style="color: red;">⚠ ${errorMessages[errorKey]}</td>
|
<td style="color: red; font-weight: bold;">Capteur déconnecté</td>
|
||||||
</tr>
|
</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) {
|
error: function (xhr, status, error) {
|
||||||
console.error('AJAX request failed:', status, error);
|
console.error('AJAX request failed:', status, error);
|
||||||
|
$("#loading_" + port).hide();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -90,13 +90,33 @@ fi
|
|||||||
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..."
|
||||||
SUDOERS_FILE="/etc/sudoers"
|
SUDOERS_FILE="/etc/sudoers"
|
||||||
|
|||||||
@@ -151,6 +151,16 @@ payload_json = {
|
|||||||
aircarto_profile_id = 0
|
aircarto_profile_id = 0
|
||||||
uSpot_profile_id = 1
|
uSpot_profile_id = 1
|
||||||
|
|
||||||
|
# Error flags constants (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
|
||||||
|
|
||||||
# database connection
|
# database connection
|
||||||
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()
|
||||||
@@ -271,7 +281,13 @@ class SensorPayload:
|
|||||||
#device_id_bytes = bytes.fromhex(device_id)[:8].ljust(8, b'\x00')
|
#device_id_bytes = bytes.fromhex(device_id)[:8].ljust(8, b'\x00')
|
||||||
|
|
||||||
self.payload[0:8] = device_id_bytes
|
self.payload[0:8] = device_id_bytes
|
||||||
|
|
||||||
|
# Status/error bytes default to 0x00 (no error)
|
||||||
|
# 0xFF = "no data" for sensor values, but for status bytes
|
||||||
|
# 0x00 = "no error/no flag" is the safe default
|
||||||
|
self.payload[66] = 0x00 # error_flags
|
||||||
|
self.payload[67] = 0x00 # npm_status
|
||||||
|
self.payload[68] = 0x00 # device_status
|
||||||
# Set protocol version (byte 9)
|
# Set protocol version (byte 9)
|
||||||
self.payload[9] = 0x01
|
self.payload[9] = 0x01
|
||||||
|
|
||||||
@@ -358,6 +374,28 @@ class SensorPayload:
|
|||||||
if direction is not None:
|
if direction is not None:
|
||||||
self.payload[64:66] = struct.pack('>H', int(direction))
|
self.payload[64:66] = struct.pack('>H', int(direction))
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
def set_firmware_version(self, version_str):
|
||||||
|
"""Set firmware version bytes 69-71 (major.minor.patch)"""
|
||||||
|
try:
|
||||||
|
parts = version_str.strip().split('.')
|
||||||
|
self.payload[69] = int(parts[0]) & 0xFF
|
||||||
|
self.payload[70] = int(parts[1]) & 0xFF
|
||||||
|
self.payload[71] = int(parts[2]) & 0xFF
|
||||||
|
except (IndexError, ValueError):
|
||||||
|
pass # leave as 0xFF if VERSION file is malformed
|
||||||
|
|
||||||
def get_bytes(self):
|
def get_bytes(self):
|
||||||
"""Get the complete 100-byte payload"""
|
"""Get the complete 100-byte payload"""
|
||||||
return bytes(self.payload)
|
return bytes(self.payload)
|
||||||
@@ -808,6 +846,19 @@ try:
|
|||||||
payload_csv[18] = npm_temp
|
payload_csv[18] = npm_temp
|
||||||
payload_csv[19] = npm_hum
|
payload_csv[19] = npm_hum
|
||||||
|
|
||||||
|
# npm_status: last value only (no average), use rowid (not timestamp)
|
||||||
|
npm_status_value = rows[0][7] if rows and rows[0][7] is not None else 0xFF
|
||||||
|
|
||||||
|
npm_disconnected = False
|
||||||
|
if npm_status_value == 0xFF:
|
||||||
|
# 0xFF = NPM disconnected/no response → will set ERR_NPM in error_flags
|
||||||
|
npm_disconnected = True
|
||||||
|
print("NPM status: 0xFF (disconnected)")
|
||||||
|
else:
|
||||||
|
# Valid status from NPM → send as byte 67
|
||||||
|
payload.set_npm_status(npm_status_value)
|
||||||
|
print(f"NPM status: 0x{npm_status_value:02X}")
|
||||||
|
|
||||||
#add data to payload UDP
|
#add data to payload UDP
|
||||||
payload.set_npm_core(PM1, PM25, PM10)
|
payload.set_npm_core(PM1, PM25, PM10)
|
||||||
payload.set_npm_internal(npm_temp, npm_hum)
|
payload.set_npm_internal(npm_temp, npm_hum)
|
||||||
@@ -1090,6 +1141,23 @@ try:
|
|||||||
|
|
||||||
'''
|
'''
|
||||||
|
|
||||||
|
# ---- Build error_flags (byte 66) ----
|
||||||
|
error_flags = 0x00
|
||||||
|
if rtc_status == "disconnected":
|
||||||
|
error_flags |= ERR_RTC_DISCONNECTED
|
||||||
|
if rtc_status == "reset":
|
||||||
|
error_flags |= ERR_RTC_RESET
|
||||||
|
if npm_disconnected:
|
||||||
|
error_flags |= ERR_NPM
|
||||||
|
payload.set_error_flags(error_flags)
|
||||||
|
|
||||||
|
# ---- Firmware version (bytes 69-71) ----
|
||||||
|
try:
|
||||||
|
with open("/var/www/nebuleair_pro_4g/VERSION", "r") as f:
|
||||||
|
payload.set_firmware_version(f.read())
|
||||||
|
except FileNotFoundError:
|
||||||
|
pass
|
||||||
|
|
||||||
if send_miotiq:
|
if send_miotiq:
|
||||||
print('<p class="fw-bold">➡️SEND TO MIOTIQ</p>', end="")
|
print('<p class="fw-bold">➡️SEND TO MIOTIQ</p>', end="")
|
||||||
|
|
||||||
@@ -1125,9 +1193,20 @@ try:
|
|||||||
print(response_SARA_1)
|
print(response_SARA_1)
|
||||||
else:
|
else:
|
||||||
print("⛔There were issues with the modem CSD PSD reinitialize process")
|
print("⛔There were issues with the modem CSD PSD reinitialize process")
|
||||||
|
print("🔄 PDP reset failed → escalating to hardware reboot")
|
||||||
# Clignotement LED rouge en cas d'erreur
|
# Clignotement LED rouge en cas d'erreur
|
||||||
led_thread = Thread(target=blink_led, args=(24, 5, 0.5))
|
led_thread = Thread(target=blink_led, args=(24, 5, 0.5))
|
||||||
led_thread.start()
|
led_thread.start()
|
||||||
|
#Send notification (WIFI)
|
||||||
|
send_error_notification(device_id, "UDP socket creation failed + PDP reset failed -> hardware reboot")
|
||||||
|
#Hardware Reboot
|
||||||
|
hardware_reboot_success = modem_hardware_reboot()
|
||||||
|
if hardware_reboot_success:
|
||||||
|
print("✅Modem successfully rebooted and reinitialized")
|
||||||
|
else:
|
||||||
|
print("⛔There were issues with the modem reboot/reinitialize process")
|
||||||
|
#end loop
|
||||||
|
sys.exit()
|
||||||
|
|
||||||
#Retreive Socket ID
|
#Retreive Socket ID
|
||||||
socket_id = None
|
socket_id = None
|
||||||
|
|||||||
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).
|
||||||
@@ -67,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 (
|
||||||
|
|||||||
@@ -107,6 +107,18 @@ for connected, port, name, coefficient in envea_sondes:
|
|||||||
print(f"Envea sonde '{name}' already exists, skipping")
|
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()
|
||||||
conn.close()
|
conn.close()
|
||||||
|
|||||||
@@ -81,6 +81,32 @@ 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
|
sudo chmod 755 /var/www/nebuleair_pro_4g/wifi/*.py 2>/dev/null
|
||||||
check_status "File permissions update"
|
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
|
# Step 4: Restart critical services if they exist
|
||||||
print_status ""
|
print_status ""
|
||||||
print_status "Step 4: Managing system services..."
|
print_status "Step 4: Managing system services..."
|
||||||
|
|||||||
@@ -118,6 +118,32 @@ chmod 755 "$TARGET_DIR/MPPT/"*.py 2>/dev/null
|
|||||||
chmod 755 "$TARGET_DIR/wifi/"*.py 2>/dev/null
|
chmod 755 "$TARGET_DIR/wifi/"*.py 2>/dev/null
|
||||||
check_status "File permissions update"
|
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
|
# Step 5: Restart critical services
|
||||||
print_status ""
|
print_status ""
|
||||||
print_status "Step 5: Managing system services..."
|
print_status "Step 5: Managing system services..."
|
||||||
|
|||||||
Reference in New Issue
Block a user