Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
11585b4783 | ||
|
|
52b86dbc3d | ||
|
|
361c0d1a76 | ||
|
|
bd2e1f1eda | ||
|
|
2b4e9205c1 | ||
|
|
b3c019c27b | ||
|
|
e733cd27e8 | ||
|
|
a9db7750b2 | ||
|
|
c42656e0ae |
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()
|
||||||
@@ -110,6 +113,7 @@ try:
|
|||||||
|
|
||||||
# Validate response length
|
# Validate response length
|
||||||
if len(byte_data) < response_length:
|
if len(byte_data) < response_length:
|
||||||
|
if not dry_run:
|
||||||
print(f"[ERROR] Incomplete response received: {byte_data.hex()}")
|
print(f"[ERROR] Incomplete response received: {byte_data.hex()}")
|
||||||
raise Exception("Incomplete response")
|
raise Exception("Incomplete response")
|
||||||
|
|
||||||
@@ -118,6 +122,7 @@ try:
|
|||||||
calculated_crc = crc16(byte_data[:-2])
|
calculated_crc = crc16(byte_data[:-2])
|
||||||
|
|
||||||
if received_crc != calculated_crc:
|
if received_crc != calculated_crc:
|
||||||
|
if not dry_run:
|
||||||
print("[ERROR] CRC check failed! Corrupted data received.")
|
print("[ERROR] CRC check failed! Corrupted data received.")
|
||||||
raise Exception("CRC check failed")
|
raise Exception("CRC check failed")
|
||||||
|
|
||||||
@@ -194,20 +199,37 @@ try:
|
|||||||
status_calc_crc = crc16(status_response[:-2])
|
status_calc_crc = crc16(status_response[:-2])
|
||||||
if status_recv_crc == status_calc_crc:
|
if status_recv_crc == status_calc_crc:
|
||||||
npm_status = int.from_bytes(status_response[3:5], byteorder='big') & 0xFF
|
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})")
|
print(f"NPM status: 0x{npm_status:02X} ({npm_status})")
|
||||||
else:
|
else:
|
||||||
|
if not dry_run:
|
||||||
print("[WARNING] NPM status CRC check failed, keeping default")
|
print("[WARNING] NPM status CRC check failed, keeping default")
|
||||||
else:
|
else:
|
||||||
|
if not dry_run:
|
||||||
print(f"[WARNING] NPM status incomplete response ({len(status_response)} bytes)")
|
print(f"[WARNING] NPM status incomplete response ({len(status_response)} bytes)")
|
||||||
|
|
||||||
ser.close()
|
ser.close()
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
if not dry_run:
|
||||||
print(f"[ERROR] Sensor communication failed: {e}")
|
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:
|
||||||
|
# Print JSON output without writing to database
|
||||||
|
result = {
|
||||||
|
"PM1": pm1_10s,
|
||||||
|
"PM25": pm25_10s,
|
||||||
|
"PM10": pm10_10s,
|
||||||
|
"temperature": temperature,
|
||||||
|
"humidity": relative_humidity,
|
||||||
|
"npm_status": npm_status,
|
||||||
|
"npm_status_hex": f"0x{npm_status:02X}"
|
||||||
|
}
|
||||||
|
print(json.dumps(result))
|
||||||
|
else:
|
||||||
|
# Always save data to database, even if all values are 0
|
||||||
cursor.execute('''
|
cursor.execute('''
|
||||||
INSERT INTO data_NPM_5channels (timestamp,PM_ch1, PM_ch2, PM_ch3, PM_ch4, PM_ch5) VALUES (?,?,?,?,?,?)'''
|
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))
|
, (rtc_time_str, channel_1, channel_2, channel_3, channel_4, channel_5))
|
||||||
@@ -218,4 +240,5 @@ finally:
|
|||||||
|
|
||||||
# Commit and close the connection
|
# Commit and close the connection
|
||||||
conn.commit()
|
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,58 @@
|
|||||||
{
|
{
|
||||||
"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",
|
"version": "1.5.0",
|
||||||
"date": "2026-03-18",
|
"date": "2026-03-18",
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -805,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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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(`
|
||||||
|
<tr>
|
||||||
|
<td>Status</td>
|
||||||
|
<td style="color: red; font-weight: bold;">Capteur déconnecté</td>
|
||||||
|
</tr>
|
||||||
|
`);
|
||||||
|
} else if (status === 0) {
|
||||||
|
$("#data-table-body_" + port).append(`
|
||||||
|
<tr>
|
||||||
|
<td>Status</td>
|
||||||
|
<td style="color: green; font-weight: bold;">OK</td>
|
||||||
|
</tr>
|
||||||
|
`);
|
||||||
|
} else {
|
||||||
|
$("#data-table-body_" + port).append(`
|
||||||
|
<tr>
|
||||||
|
<td>Status</td>
|
||||||
|
<td style="color: orange; font-weight: bold;">${response.npm_status_hex}</td>
|
||||||
|
</tr>
|
||||||
|
`);
|
||||||
|
// Decode individual error bits
|
||||||
|
const statusFlags = {
|
||||||
|
0x01: "Sleep mode",
|
||||||
|
0x02: "Degraded mode",
|
||||||
|
0x04: "Not ready",
|
||||||
|
0x08: "Heater error",
|
||||||
|
0x10: "THP sensor error",
|
||||||
|
0x20: "Fan error",
|
||||||
|
0x40: "Memory error",
|
||||||
|
0x80: "Laser error"
|
||||||
|
};
|
||||||
|
Object.entries(statusFlags).forEach(([mask, label]) => {
|
||||||
|
if (status & mask) {
|
||||||
$("#data-table-body_" + port).append(`
|
$("#data-table-body_" + port).append(`
|
||||||
<tr class="error-row">
|
<tr class="error-row">
|
||||||
<td><b>${errorKey}</b></td>
|
<td></td>
|
||||||
<td style="color: red;">⚠ ${errorMessages[errorKey]}</td>
|
<td style="color: red;">⚠ ${label}</td>
|
||||||
</tr>
|
</tr>
|
||||||
`);
|
`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
error: function (xhr, status, error) {
|
error: function (xhr, status, error) {
|
||||||
console.error('AJAX request failed:', status, error);
|
console.error('AJAX request failed:', status, error);
|
||||||
|
$("#loading_" + port).hide();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -386,6 +386,16 @@ class SensorPayload:
|
|||||||
"""Set device status flags (byte 68)"""
|
"""Set device status flags (byte 68)"""
|
||||||
self.payload[68] = status & 0xFF
|
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)
|
||||||
@@ -836,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)
|
||||||
@@ -1124,8 +1147,17 @@ try:
|
|||||||
error_flags |= ERR_RTC_DISCONNECTED
|
error_flags |= ERR_RTC_DISCONNECTED
|
||||||
if rtc_status == "reset":
|
if rtc_status == "reset":
|
||||||
error_flags |= ERR_RTC_RESET
|
error_flags |= ERR_RTC_RESET
|
||||||
|
if npm_disconnected:
|
||||||
|
error_flags |= ERR_NPM
|
||||||
payload.set_error_flags(error_flags)
|
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="")
|
||||||
|
|
||||||
|
|||||||
@@ -247,7 +247,10 @@ def set_device_status(self, status):
|
|||||||
2|error_flags|hex2dec|||
|
2|error_flags|hex2dec|||
|
||||||
2|npm_status|hex2dec|||
|
2|npm_status|hex2dec|||
|
||||||
2|device_status|hex2dec|||
|
2|device_status|hex2dec|||
|
||||||
28|reserved|skip|||
|
2|version_major|hex2dec|||
|
||||||
|
2|version_minor|hex2dec|||
|
||||||
|
2|version_patch|hex2dec|||
|
||||||
|
22|reserved|skip|||
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -310,9 +313,9 @@ if sara_rebooted:
|
|||||||
|
|
||||||
## Notes
|
## Notes
|
||||||
|
|
||||||
- La payload est initialisee a 0xFF (tous bytes a 255). Le script doit explicitement
|
- Les bytes 66-68 sont initialises a 0x00 dans le constructeur SensorPayload
|
||||||
ecrire 0x00 dans les bytes 66-67 quand tout va bien, sinon Miotiq interpretera
|
(0x00 = aucune erreur/aucun flag). Les bytes 69-71 restent a 0xFF si le
|
||||||
255 = toutes les erreurs.
|
fichier VERSION est absent ou malformed.
|
||||||
- Le NPM status n'est pas encore lu par `get_data_modbus_v3.py`. Il faut d'abord
|
- 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.
|
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
|
- Les flags du byte 66 sont determines par le script d'envoi en analysant les
|
||||||
|
|||||||
Reference in New Issue
Block a user