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/
|
||||
|
||||
# Secrets
|
||||
.env
|
||||
|
||||
# Claude Code local settings
|
||||
.claude/settings.local.json
|
||||
@@ -40,6 +40,9 @@ import crcmod
|
||||
import sqlite3
|
||||
import time
|
||||
|
||||
# Dry-run mode: print JSON output without writing to database
|
||||
dry_run = "--dry-run" in sys.argv
|
||||
|
||||
# Connect to the SQLite database
|
||||
conn = sqlite3.connect("/var/www/nebuleair_pro_4g/sqlite/sensors.db")
|
||||
cursor = conn.cursor()
|
||||
@@ -110,7 +113,8 @@ try:
|
||||
|
||||
# Validate 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")
|
||||
|
||||
# Verify CRC
|
||||
@@ -118,7 +122,8 @@ try:
|
||||
calculated_crc = crc16(byte_data[:-2])
|
||||
|
||||
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")
|
||||
|
||||
# Convert response to hex for debugging
|
||||
@@ -194,28 +199,46 @@ try:
|
||||
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
|
||||
print(f"NPM status: 0x{npm_status:02X} ({npm_status})")
|
||||
if not dry_run:
|
||||
print(f"NPM status: 0x{npm_status:02X} ({npm_status})")
|
||||
else:
|
||||
print("[WARNING] NPM status CRC check failed, keeping default")
|
||||
if not dry_run:
|
||||
print("[WARNING] NPM status CRC check failed, keeping default")
|
||||
else:
|
||||
print(f"[WARNING] NPM status incomplete response ({len(status_response)} bytes)")
|
||||
if not dry_run:
|
||||
print(f"[WARNING] NPM status incomplete response ({len(status_response)} bytes)")
|
||||
|
||||
ser.close()
|
||||
|
||||
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
|
||||
|
||||
finally:
|
||||
# Always save data to database, even if all values are -1
|
||||
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))
|
||||
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('''
|
||||
INSERT INTO data_NPM_5channels (timestamp,PM_ch1, PM_ch2, PM_ch3, PM_ch4, PM_ch5) VALUES (?,?,?,?,?,?)'''
|
||||
, (rtc_time_str, channel_1, channel_2, channel_3, channel_4, channel_5))
|
||||
|
||||
cursor.execute('''
|
||||
INSERT INTO data_NPM (timestamp,PM1, PM25, PM10, temp_npm, hum_npm, npm_status) VALUES (?,?,?,?,?,?,?)'''
|
||||
, (rtc_time_str, pm1_10s, pm25_10s, pm10_10s, temperature, relative_humidity, npm_status))
|
||||
cursor.execute('''
|
||||
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, npm_status))
|
||||
|
||||
# Commit and close the connection
|
||||
conn.commit()
|
||||
|
||||
# Commit and close the connection
|
||||
conn.commit()
|
||||
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
|
||||
|
||||
## Wifi Hotspot (AP)
|
||||
|
||||
@@ -1,5 +1,58 @@
|
||||
{
|
||||
"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",
|
||||
|
||||
@@ -320,10 +320,10 @@ async function selfTestSequence() {
|
||||
|
||||
try {
|
||||
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) => {
|
||||
$.ajax({
|
||||
url: 'launcher.php?type=npm&port=' + sensor.port,
|
||||
url: 'launcher.php?type=npm',
|
||||
dataType: 'json',
|
||||
method: 'GET',
|
||||
timeout: 15000,
|
||||
@@ -333,22 +333,42 @@ async function selfTestSequence() {
|
||||
});
|
||||
|
||||
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
|
||||
const npmErrors = ['notReady', 'fanError', 'laserError', 'heatError', 't_rhError', 'memoryError', 'degradedState'];
|
||||
const activeErrors = npmErrors.filter(e => npmResult[e] === 1);
|
||||
// Decode npm_status flags
|
||||
const status = npmResult.npm_status !== undefined ? npmResult.npm_status : 0;
|
||||
|
||||
if (status === 0xFF) {
|
||||
// 0xFF = no response = disconnected
|
||||
updateTestStatus(sensor.id, 'Failed', 'Capteur déconnecté', 'bg-danger');
|
||||
testsFailed++;
|
||||
} else {
|
||||
const statusFlags = {
|
||||
0x01: "Sleep mode",
|
||||
0x02: "Degraded mode",
|
||||
0x04: "Not ready",
|
||||
0x08: "Heater error",
|
||||
0x10: "THP sensor error",
|
||||
0x20: "Fan error",
|
||||
0x40: "Memory error",
|
||||
0x80: "Laser error"
|
||||
};
|
||||
const activeErrors = [];
|
||||
Object.entries(statusFlags).forEach(([mask, label]) => {
|
||||
if (status & mask) activeErrors.push(label);
|
||||
});
|
||||
|
||||
if (activeErrors.length > 0) {
|
||||
updateTestStatus(sensor.id, 'Warning', `Errors: ${activeErrors.join(', ')}`, 'bg-warning');
|
||||
updateTestStatus(sensor.id, 'Warning', `Status ${npmResult.npm_status_hex}: ${activeErrors.join(', ')}`, 'bg-warning');
|
||||
testsFailed++;
|
||||
} else if (npmResult.PM1 !== undefined && npmResult.PM25 !== undefined && npmResult.PM10 !== undefined) {
|
||||
updateTestStatus(sensor.id, 'Passed', `PM1: ${npmResult.PM1} | PM2.5: ${npmResult.PM25} | PM10: ${npmResult.PM10} ug/m3`, 'bg-success');
|
||||
updateTestStatus(sensor.id, 'Passed', `PM1: ${npmResult.PM1} | PM2.5: ${npmResult.PM25} | PM10: ${npmResult.PM10} µg/m³`, 'bg-success');
|
||||
testsPassed++;
|
||||
} else {
|
||||
updateTestStatus(sensor.id, 'Warning', 'Incomplete data received', 'bg-warning');
|
||||
testsFailed++;
|
||||
}
|
||||
} // end else (not 0xFF)
|
||||
|
||||
} else if (sensor.type === 'BME280') {
|
||||
// BME280 sensor test
|
||||
|
||||
@@ -805,8 +805,7 @@ if ($type == "reboot") {
|
||||
}
|
||||
|
||||
if ($type == "npm") {
|
||||
$port=$_GET['port'];
|
||||
$command = 'sudo /usr/bin/python3 /var/www/nebuleair_pro_4g/NPM/get_data.py ' . $port;
|
||||
$command = 'sudo /usr/bin/python3 /var/www/nebuleair_pro_4g/NPM/get_data_modbus_v3.py --dry-run';
|
||||
$output = shell_exec($command);
|
||||
echo $output;
|
||||
}
|
||||
|
||||
@@ -117,54 +117,94 @@
|
||||
$("#loading_" + port).show();
|
||||
|
||||
$.ajax({
|
||||
url: 'launcher.php?type=npm&port=' + port,
|
||||
dataType: 'json', // Specify that you expect a JSON response
|
||||
method: 'GET', // Use GET or POST depending on your needs
|
||||
url: 'launcher.php?type=npm',
|
||||
dataType: 'json',
|
||||
method: 'GET',
|
||||
success: function (response) {
|
||||
console.log(response);
|
||||
const tableBody = document.getElementById("data-table-body_" + port);
|
||||
tableBody.innerHTML = "";
|
||||
|
||||
$("#loading_" + port).hide();
|
||||
// Create an array of the desired keys
|
||||
const keysToShow = ["PM1", "PM25", "PM10", "message"];
|
||||
// Error messages mapping
|
||||
const errorMessages = {
|
||||
"notReady": "Sensor is not ready",
|
||||
"fanError": "Fan malfunction detected",
|
||||
"laserError": "Laser malfunction detected",
|
||||
"heatError": "Heating system error",
|
||||
"t_rhError": "Temperature/Humidity sensor error",
|
||||
"memoryError": "Memory failure detected",
|
||||
"degradedState": "Sensor in degraded state"
|
||||
};
|
||||
// Add only the specified elements to the table
|
||||
keysToShow.forEach(key => {
|
||||
if (response[key] !== undefined) { // Check if the key exists in the response
|
||||
const value = response[key];
|
||||
|
||||
// PM values
|
||||
const pmKeys = ["PM1", "PM25", "PM10"];
|
||||
pmKeys.forEach(key => {
|
||||
if (response[key] !== undefined) {
|
||||
$("#data-table-body_" + port).append(`
|
||||
<tr>
|
||||
<td>${key}</td>
|
||||
<td>${value} µg/m³</td>
|
||||
</tr>
|
||||
`);
|
||||
<tr>
|
||||
<td>${key}</td>
|
||||
<td>${response[key]} µg/m³</td>
|
||||
</tr>
|
||||
`);
|
||||
}
|
||||
});
|
||||
|
||||
// Check for errors and add them to the table
|
||||
Object.keys(errorMessages).forEach(errorKey => {
|
||||
if (response[errorKey] === 1) {
|
||||
// Temperature & humidity
|
||||
if (response.temperature !== undefined) {
|
||||
$("#data-table-body_" + port).append(`
|
||||
<tr><td>Temperature</td><td>${response.temperature} °C</td></tr>
|
||||
`);
|
||||
}
|
||||
if (response.humidity !== undefined) {
|
||||
$("#data-table-body_" + port).append(`
|
||||
<tr><td>Humidity</td><td>${response.humidity} %</td></tr>
|
||||
`);
|
||||
}
|
||||
|
||||
// NPM status decoded
|
||||
if (response.npm_status !== undefined) {
|
||||
const status = response.npm_status;
|
||||
|
||||
if (status === 0xFF) {
|
||||
// 0xFF = no response from sensor = disconnected
|
||||
$("#data-table-body_" + port).append(`
|
||||
<tr class="error-row">
|
||||
<td><b>${errorKey}</b></td>
|
||||
<td style="color: red;">⚠ ${errorMessages[errorKey]}</td>
|
||||
</tr>
|
||||
`);
|
||||
<tr>
|
||||
<td>Status</td>
|
||||
<td style="color: red; font-weight: bold;">Capteur déconnecté</td>
|
||||
</tr>
|
||||
`);
|
||||
} else if (status === 0) {
|
||||
$("#data-table-body_" + port).append(`
|
||||
<tr>
|
||||
<td>Status</td>
|
||||
<td style="color: green; font-weight: bold;">OK</td>
|
||||
</tr>
|
||||
`);
|
||||
} else {
|
||||
$("#data-table-body_" + port).append(`
|
||||
<tr>
|
||||
<td>Status</td>
|
||||
<td style="color: orange; font-weight: bold;">${response.npm_status_hex}</td>
|
||||
</tr>
|
||||
`);
|
||||
// Decode individual error bits
|
||||
const statusFlags = {
|
||||
0x01: "Sleep mode",
|
||||
0x02: "Degraded mode",
|
||||
0x04: "Not ready",
|
||||
0x08: "Heater error",
|
||||
0x10: "THP sensor error",
|
||||
0x20: "Fan error",
|
||||
0x40: "Memory error",
|
||||
0x80: "Laser error"
|
||||
};
|
||||
Object.entries(statusFlags).forEach(([mask, label]) => {
|
||||
if (status & mask) {
|
||||
$("#data-table-body_" + port).append(`
|
||||
<tr class="error-row">
|
||||
<td></td>
|
||||
<td style="color: red;">⚠ ${label}</td>
|
||||
</tr>
|
||||
`);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
error: function (xhr, status, error) {
|
||||
console.error('AJAX request failed:', status, error);
|
||||
$("#loading_" + port).hide();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -386,6 +386,16 @@ class SensorPayload:
|
||||
"""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):
|
||||
"""Get the complete 100-byte payload"""
|
||||
return bytes(self.payload)
|
||||
@@ -836,6 +846,19 @@ try:
|
||||
payload_csv[18] = npm_temp
|
||||
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
|
||||
payload.set_npm_core(PM1, PM25, PM10)
|
||||
payload.set_npm_internal(npm_temp, npm_hum)
|
||||
@@ -1124,8 +1147,17 @@ try:
|
||||
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:
|
||||
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|npm_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
|
||||
|
||||
- La payload est initialisee a 0xFF (tous bytes a 255). Le script doit explicitement
|
||||
ecrire 0x00 dans les bytes 66-67 quand tout va bien, sinon Miotiq interpretera
|
||||
255 = toutes les erreurs.
|
||||
- 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
|
||||
|
||||
Reference in New Issue
Block a user