This commit is contained in:
Your Name
2026-01-05 15:50:35 +00:00
parent 62ef47aa67
commit a38ce79555
4 changed files with 365 additions and 117 deletions

View File

@@ -14,9 +14,9 @@ import serial
import requests import requests
import json import json
import sys import sys
import time
parameter = sys.argv[1:] # Exclude the script name parameter = sys.argv[1:] # Exclude the script name
#print("Parameters received:")
port='/dev/'+parameter[0] port='/dev/'+parameter[0]
ser = serial.Serial( ser = serial.Serial(
@@ -34,42 +34,93 @@ ser.write(b'\x81\x11\x6E') #data10s
while True: while True:
try: try:
byte_data = ser.readline() byte_data = ser.readline()
#print(byte_data)
# Convert raw data to hex string for debugging
raw_hex = byte_data.hex() if byte_data else ""
# Check if we received data
if not byte_data or len(byte_data) < 15:
data = {
'PM1': 0.0,
'PM25': 0.0,
'PM10': 0.0,
'sleep': 0,
'degradedState': 0,
'notReady': 0,
'heatError': 0,
't_rhError': 0,
'fanError': 0,
'memoryError': 0,
'laserError': 0,
'raw': raw_hex,
'message': f"No data received or incomplete frame (length: {len(byte_data)})"
}
json_data = json.dumps(data)
print(json_data)
break
stateByte = int.from_bytes(byte_data[2:3], byteorder='big') stateByte = int.from_bytes(byte_data[2:3], byteorder='big')
Statebits = [int(bit) for bit in bin(stateByte)[2:].zfill(8)] Statebits = [int(bit) for bit in bin(stateByte)[2:].zfill(8)]
PM1 = int.from_bytes(byte_data[9:11], byteorder='big')/10 PM1 = int.from_bytes(byte_data[9:11], byteorder='big')/10
PM25 = int.from_bytes(byte_data[11:13], byteorder='big')/10 PM25 = int.from_bytes(byte_data[11:13], byteorder='big')/10
PM10 = int.from_bytes(byte_data[13:15], byteorder='big')/10 PM10 = int.from_bytes(byte_data[13:15], byteorder='big')/10
#print(f"State: {Statebits}")
#print(f"PM1: {PM1}") # Create JSON with raw data and status message
#print(f"PM25: {PM25}")
#print(f"PM10: {PM10}")
#create JSON
data = { data = {
'capteurID': 'nebuleairpro1',
'sondeID':'USB2',
'PM1': PM1, 'PM1': PM1,
'PM25': PM25, 'PM25': PM25,
'PM10': PM10, 'PM10': PM10,
'sleep' : Statebits[0], 'sleep': Statebits[0],
'degradedState' : Statebits[1], 'degradedState': Statebits[1],
'notReady' : Statebits[2], 'notReady': Statebits[2],
'heatError' : Statebits[3], 'heatError': Statebits[3],
't_rhError' : Statebits[4], 't_rhError': Statebits[4],
'fanError' : Statebits[5], 'fanError': Statebits[5],
'memoryError' : Statebits[6], 'memoryError': Statebits[6],
'laserError' : Statebits[7] 'laserError': Statebits[7],
'raw': raw_hex,
'message': 'OK' if sum(Statebits[1:]) == 0 else 'Sensor error detected'
} }
json_data = json.dumps(data) json_data = json.dumps(data)
print(json_data) print(json_data)
break break
except KeyboardInterrupt: except KeyboardInterrupt:
print("User interrupt encountered. Exiting...") data = {
time.sleep(3) 'PM1': 0.0,
exit() 'PM25': 0.0,
except: 'PM10': 0.0,
# for all other kinds of error, but not specifying which one 'sleep': 0,
print("Unknown error...") 'degradedState': 0,
'notReady': 0,
'heatError': 0,
't_rhError': 0,
'fanError': 0,
'memoryError': 0,
'laserError': 0,
'raw': '',
'message': 'User interrupt encountered'
}
print(json.dumps(data))
time.sleep(3) time.sleep(3)
exit() exit()
except Exception as e:
data = {
'PM1': 0.0,
'PM25': 0.0,
'PM10': 0.0,
'sleep': 0,
'degradedState': 0,
'notReady': 0,
'heatError': 0,
't_rhError': 0,
'fanError': 0,
'memoryError': 0,
'laserError': 0,
'raw': '',
'message': f'Error: {str(e)}'
}
print(json.dumps(data))
time.sleep(3)
exit()

177
NPM/get_data_modbus_v2_1.py Normal file
View File

@@ -0,0 +1,177 @@
'''
_ _ ____ __ __
| \ | | _ \| \/ |
| \| | |_) | |\/| |
| |\ | __/| | | |
|_| \_|_| |_| |_|
Script to get NPM data via Modbus
Improved version with data stream lenght check
/usr/bin/python3 /var/www/nebuleair_pro_4g/NPM/get_data_modbus_v3.py
Modbus RTU
[Slave Address][Function Code][Starting Address][Quantity of Registers][CRC]
Pour récupérer
les concentrations en PM1, PM10 et PM2.5 (a partir du registre 0x38)
les 5 cannaux
la température et l'humidité à l'intérieur du capteur
Donnée actualisée toutes les 10 secondes
Request
\x01\x03\x00\x38\x00\x55\...\...
\x01 Slave Address (slave device address)
\x03 Function code (read multiple holding registers)
\x00\x38 Starting Address (The request starts reading from holding register address x38 or 56)
\x00\x55 Quantity of Registers (Requests to read x55 or 85 consecutive registers starting from address 56)
\...\... Cyclic Redundancy Check (checksum )
'''
import serial
import requests
import json
import sys
import crcmod
import sqlite3
import time
# Connect to the SQLite database
conn = sqlite3.connect("/var/www/nebuleair_pro_4g/sqlite/sensors.db")
cursor = conn.cursor()
def load_config(config_file):
try:
with open(config_file, 'r') as file:
config_data = json.load(file)
return config_data
except Exception as e:
print(f"Error loading config file: {e}")
return {}
# Load the configuration data
npm_solo_port = "/dev/ttyAMA5" #port du NPM solo
#GET RTC TIME from SQlite
cursor.execute("SELECT * FROM timestamp_table LIMIT 1")
row = cursor.fetchone() # Get the first (and only) row
rtc_time_str = row[1] # '2025-02-07 12:30:45'
# Initialize serial port
ser = serial.Serial(
port=npm_solo_port,
baudrate=115200,
parity=serial.PARITY_EVEN,
stopbits=serial.STOPBITS_ONE,
bytesize=serial.EIGHTBITS,
timeout=2
)
# Define Modbus CRC-16 function
crc16 = crcmod.predefined.mkPredefinedCrcFun('modbus')
# Request frame without CRC
data = b'\x01\x03\x00\x38\x00\x55'
# Calculate and append CRC
crc = crc16(data)
crc_low = crc & 0xFF
crc_high = (crc >> 8) & 0xFF
request = data + bytes([crc_low, crc_high])
# Clear serial buffer before sending
ser.flushInput()
# Send request
ser.write(request)
time.sleep(0.2) # Wait for sensor to respond
# Read response
response_length = 2 + 1 + (2 * 85) + 2 # Address + Function + Data + CRC
byte_data = ser.read(response_length)
# Validate response length
if len(byte_data) < response_length:
print("[ERROR] Incomplete response received:", byte_data.hex())
exit()
# Verify CRC
received_crc = int.from_bytes(byte_data[-2:], byteorder='little')
calculated_crc = crc16(byte_data[:-2])
if received_crc != calculated_crc:
print("[ERROR] CRC check failed! Corrupted data received.")
exit()
# Convert response to hex for debugging
formatted = ''.join(f'\\x{byte:02x}' for byte in byte_data)
#print("Response:", formatted)
# Extract and print PM values
def extract_value(byte_data, register, scale=1, single_register=False, round_to=None):
REGISTER_START = 56
offset = (register - REGISTER_START) * 2 + 3
if single_register:
value = int.from_bytes(byte_data[offset:offset+2], byteorder='big')
else:
lsw = int.from_bytes(byte_data[offset:offset+2], byteorder='big')
msw = int.from_bytes(byte_data[offset+2:offset+4], byteorder='big')
value = (msw << 16) | lsw
value = value / scale
if round_to == 0:
return int(value)
elif round_to is not None:
return round(value, round_to)
else:
return value
pm1_10s = extract_value(byte_data, 56, 1000, round_to=1)
pm25_10s = extract_value(byte_data, 58, 1000, round_to=1)
pm10_10s = extract_value(byte_data, 60, 1000, round_to=1)
#print("10 sec concentration:")
#print(f"PM1: {pm1_10s}")
#print(f"PM2.5: {pm25_10s}")
#print(f"PM10: {pm10_10s}")
# Extract values for 5 channels
channel_1 = extract_value(byte_data, 128, round_to=0) # 0.2 - 0.5μm
channel_2 = extract_value(byte_data, 130, round_to=0) # 0.5 - 1.0μm
channel_3 = extract_value(byte_data, 132, round_to=0) # 1.0 - 2.5μm
channel_4 = extract_value(byte_data, 134, round_to=0) # 2.5 - 5.0μm
channel_5 = extract_value(byte_data, 136, round_to=0) # 5.0 - 10.0μm
#print(f"Channel 1 (0.2->0.5): {channel_1}")
#print(f"Channel 2 (0.5->1.0): {channel_2}")
#print(f"Channel 3 (1.0->2.5): {channel_3}")
#print(f"Channel 4 (2.5->5.0): {channel_4}")
#print(f"Channel 5 (5.0->10.): {channel_5}")
# Retrieve relative humidity from register 106 (0x6A)
relative_humidity = extract_value(byte_data, 106, 100, single_register=True)
# Retrieve temperature from register 106 (0x6A)
temperature = extract_value(byte_data, 107, 100, single_register=True)
#print(f"Internal Relative Humidity: {relative_humidity} %")
#print(f"Internal temperature: {temperature} °C")
cursor.execute('''
INSERT INTO data_NPM_5channels (timestamp,PM_ch1, PM_ch2, PM_ch3, PM_ch4, PM_ch5) VALUES (?,?,?,?,?,?)'''
, (rtc_time_str,channel_1,channel_2,channel_3,channel_4,channel_5))
cursor.execute('''
INSERT INTO data_NPM (timestamp,PM1, PM25, PM10, temp_npm, hum_npm) VALUES (?,?,?,?,?,?)'''
, (rtc_time_str,pm1_10s,pm25_10s,pm10_10s,temperature,relative_humidity ))
# Commit and close the connection
conn.commit()
conn.close()

View File

@@ -29,6 +29,8 @@ Request
\x00\x55 Quantity of Registers (Requests to read x55 or 85 consecutive registers starting from address 56) \x00\x55 Quantity of Registers (Requests to read x55 or 85 consecutive registers starting from address 56)
\...\... Cyclic Redundancy Check (checksum ) \...\... Cyclic Redundancy Check (checksum )
MAJ 2026 --> renvoie des 0 si pas de réponse du NPM
''' '''
import serial import serial
import requests import requests
@@ -59,119 +61,137 @@ cursor.execute("SELECT * FROM timestamp_table LIMIT 1")
row = cursor.fetchone() # Get the first (and only) row row = cursor.fetchone() # Get the first (and only) row
rtc_time_str = row[1] # '2025-02-07 12:30:45' rtc_time_str = row[1] # '2025-02-07 12:30:45'
# Initialize serial port # Initialize default error values
ser = serial.Serial( pm1_10s = 0
port=npm_solo_port, pm25_10s = 0
baudrate=115200, pm10_10s = 0
parity=serial.PARITY_EVEN, channel_1 = 0
stopbits=serial.STOPBITS_ONE, channel_2 = 0
bytesize=serial.EIGHTBITS, channel_3 = 0
timeout=2 channel_4 = 0
) channel_5 = 0
relative_humidity = 0
temperature = 0
# Define Modbus CRC-16 function try:
crc16 = crcmod.predefined.mkPredefinedCrcFun('modbus') # Initialize serial port
ser = serial.Serial(
port=npm_solo_port,
baudrate=115200,
parity=serial.PARITY_EVEN,
stopbits=serial.STOPBITS_ONE,
bytesize=serial.EIGHTBITS,
timeout=2
)
# Request frame without CRC # Define Modbus CRC-16 function
data = b'\x01\x03\x00\x38\x00\x55' crc16 = crcmod.predefined.mkPredefinedCrcFun('modbus')
# Calculate and append CRC # Request frame without CRC
crc = crc16(data) data = b'\x01\x03\x00\x38\x00\x55'
crc_low = crc & 0xFF
crc_high = (crc >> 8) & 0xFF
request = data + bytes([crc_low, crc_high])
# Clear serial buffer before sending # Calculate and append CRC
ser.flushInput() crc = crc16(data)
crc_low = crc & 0xFF
crc_high = (crc >> 8) & 0xFF
request = data + bytes([crc_low, crc_high])
# Send request # Clear serial buffer before sending
ser.write(request) ser.flushInput()
time.sleep(0.2) # Wait for sensor to respond
# Read response # Send request
response_length = 2 + 1 + (2 * 85) + 2 # Address + Function + Data + CRC ser.write(request)
byte_data = ser.read(response_length) time.sleep(0.2) # Wait for sensor to respond
# Validate response length # Read response
if len(byte_data) < response_length: response_length = 2 + 1 + (2 * 85) + 2 # Address + Function + Data + CRC
print("[ERROR] Incomplete response received:", byte_data.hex()) byte_data = ser.read(response_length)
exit()
# Verify CRC # Validate response length
received_crc = int.from_bytes(byte_data[-2:], byteorder='little') if len(byte_data) < response_length:
calculated_crc = crc16(byte_data[:-2]) print(f"[ERROR] Incomplete response received: {byte_data.hex()}")
raise Exception("Incomplete response")
if received_crc != calculated_crc: # Verify CRC
print("[ERROR] CRC check failed! Corrupted data received.") received_crc = int.from_bytes(byte_data[-2:], byteorder='little')
exit() calculated_crc = crc16(byte_data[:-2])
# Convert response to hex for debugging if received_crc != calculated_crc:
formatted = ''.join(f'\\x{byte:02x}' for byte in byte_data) print("[ERROR] CRC check failed! Corrupted data received.")
#print("Response:", formatted) raise Exception("CRC check failed")
# Extract and print PM values # Convert response to hex for debugging
def extract_value(byte_data, register, scale=1, single_register=False, round_to=None): formatted = ''.join(f'\\x{byte:02x}' for byte in byte_data)
REGISTER_START = 56 #print("Response:", formatted)
offset = (register - REGISTER_START) * 2 + 3
if single_register: # Extract and print PM values
value = int.from_bytes(byte_data[offset:offset+2], byteorder='big') def extract_value(byte_data, register, scale=1, single_register=False, round_to=None):
else: REGISTER_START = 56
lsw = int.from_bytes(byte_data[offset:offset+2], byteorder='big') offset = (register - REGISTER_START) * 2 + 3
msw = int.from_bytes(byte_data[offset+2:offset+4], byteorder='big')
value = (msw << 16) | lsw
value = value / scale if single_register:
value = int.from_bytes(byte_data[offset:offset+2], byteorder='big')
else:
lsw = int.from_bytes(byte_data[offset:offset+2], byteorder='big')
msw = int.from_bytes(byte_data[offset+2:offset+4], byteorder='big')
value = (msw << 16) | lsw
if round_to == 0: value = value / scale
return int(value)
elif round_to is not None:
return round(value, round_to)
else:
return value
pm1_10s = extract_value(byte_data, 56, 1000, round_to=1) if round_to == 0:
pm25_10s = extract_value(byte_data, 58, 1000, round_to=1) return int(value)
pm10_10s = extract_value(byte_data, 60, 1000, round_to=1) elif round_to is not None:
return round(value, round_to)
else:
return value
#print("10 sec concentration:") pm1_10s = extract_value(byte_data, 56, 1000, round_to=1)
#print(f"PM1: {pm1_10s}") pm25_10s = extract_value(byte_data, 58, 1000, round_to=1)
#print(f"PM2.5: {pm25_10s}") pm10_10s = extract_value(byte_data, 60, 1000, round_to=1)
#print(f"PM10: {pm10_10s}")
# Extract values for 5 channels #print("10 sec concentration:")
channel_1 = extract_value(byte_data, 128, round_to=0) # 0.2 - 0.5μm #print(f"PM1: {pm1_10s}")
channel_2 = extract_value(byte_data, 130, round_to=0) # 0.5 - 1.0μm #print(f"PM2.5: {pm25_10s}")
channel_3 = extract_value(byte_data, 132, round_to=0) # 1.0 - 2.5μm #print(f"PM10: {pm10_10s}")
channel_4 = extract_value(byte_data, 134, round_to=0) # 2.5 - 5.0μm
channel_5 = extract_value(byte_data, 136, round_to=0) # 5.0 - 10.0μm
#print(f"Channel 1 (0.2->0.5): {channel_1}") # Extract values for 5 channels
#print(f"Channel 2 (0.5->1.0): {channel_2}") channel_1 = extract_value(byte_data, 128, round_to=0) # 0.2 - 0.5μm
#print(f"Channel 3 (1.0->2.5): {channel_3}") channel_2 = extract_value(byte_data, 130, round_to=0) # 0.5 - 1.0μm
#print(f"Channel 4 (2.5->5.0): {channel_4}") channel_3 = extract_value(byte_data, 132, round_to=0) # 1.0 - 2.5μm
#print(f"Channel 5 (5.0->10.): {channel_5}") channel_4 = extract_value(byte_data, 134, round_to=0) # 2.5 - 5.0μm
channel_5 = extract_value(byte_data, 136, round_to=0) # 5.0 - 10.0μm
#print(f"Channel 1 (0.2->0.5): {channel_1}")
#print(f"Channel 2 (0.5->1.0): {channel_2}")
#print(f"Channel 3 (1.0->2.5): {channel_3}")
#print(f"Channel 4 (2.5->5.0): {channel_4}")
#print(f"Channel 5 (5.0->10.): {channel_5}")
# Retrieve relative humidity from register 106 (0x6A) # Retrieve relative humidity from register 106 (0x6A)
relative_humidity = extract_value(byte_data, 106, 100, single_register=True) relative_humidity = extract_value(byte_data, 106, 100, single_register=True)
# Retrieve temperature from register 106 (0x6A) # Retrieve temperature from register 106 (0x6A)
temperature = extract_value(byte_data, 107, 100, single_register=True) temperature = extract_value(byte_data, 107, 100, single_register=True)
#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")
ser.close()
except Exception as e:
print(f"[ERROR] Sensor communication failed: {e}")
# Variables already set to -1 at the beginning
cursor.execute(''' finally:
INSERT INTO data_NPM_5channels (timestamp,PM_ch1, PM_ch2, PM_ch3, PM_ch4, PM_ch5) VALUES (?,?,?,?,?,?)''' # Always save data to database, even if all values are -1
, (rtc_time_str,channel_1,channel_2,channel_3,channel_4,channel_5)) 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) 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))
# Commit and close the connection # Commit and close the connection
conn.commit() conn.commit()
conn.close()
conn.close()

View File

@@ -101,7 +101,7 @@ function getNPM_values(port){
$("#loading_"+port).hide(); $("#loading_"+port).hide();
// Create an array of the desired keys // Create an array of the desired keys
const keysToShow = ["PM1", "PM25", "PM10"]; const keysToShow = ["PM1", "PM25", "PM10","message"];
// Error messages mapping // Error messages mapping
const errorMessages = { const errorMessages = {
"notReady": "Sensor is not ready", "notReady": "Sensor is not ready",
@@ -307,8 +307,8 @@ error: function(xhr, status, error) {
const container = document.getElementById('card-container'); // Conteneur des cartes const container = document.getElementById('card-container'); // Conteneur des cartes
//creates NPM card //creates NPM card (by default)
if (response["NPM/get_data_modbus_v3.py"]) {
const cardHTML = ` const cardHTML = `
<div class="col-sm-3"> <div class="col-sm-3">
<div class="card"> <div class="card">
@@ -329,7 +329,7 @@ error: function(xhr, status, error) {
</div>`; </div>`;
container.innerHTML += cardHTML; // Add the I2C card if condition is met container.innerHTML += cardHTML; // Add the I2C card if condition is met
}
//creates i2c BME280 card //creates i2c BME280 card
if (response["BME280/get_data_v2.py"]) { if (response["BME280/get_data_v2.py"]) {