diff --git a/NPM/get_data.py b/NPM/get_data.py index a694cf1..72fcc15 100755 --- a/NPM/get_data.py +++ b/NPM/get_data.py @@ -14,9 +14,9 @@ import serial import requests import json import sys +import time parameter = sys.argv[1:] # Exclude the script name -#print("Parameters received:") port='/dev/'+parameter[0] ser = serial.Serial( @@ -34,42 +34,93 @@ ser.write(b'\x81\x11\x6E') #data10s while True: try: 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') Statebits = [int(bit) for bit in bin(stateByte)[2:].zfill(8)] PM1 = int.from_bytes(byte_data[9:11], 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 - #print(f"State: {Statebits}") - #print(f"PM1: {PM1}") - #print(f"PM25: {PM25}") - #print(f"PM10: {PM10}") - #create JSON + + # Create JSON with raw data and status message data = { - 'capteurID': 'nebuleairpro1', - 'sondeID':'USB2', 'PM1': PM1, 'PM25': PM25, 'PM10': PM10, - 'sleep' : Statebits[0], - 'degradedState' : Statebits[1], - 'notReady' : Statebits[2], - 'heatError' : Statebits[3], - 't_rhError' : Statebits[4], - 'fanError' : Statebits[5], - 'memoryError' : Statebits[6], - 'laserError' : Statebits[7] + 'sleep': Statebits[0], + 'degradedState': Statebits[1], + 'notReady': Statebits[2], + 'heatError': Statebits[3], + 't_rhError': Statebits[4], + 'fanError': Statebits[5], + 'memoryError': Statebits[6], + 'laserError': Statebits[7], + 'raw': raw_hex, + 'message': 'OK' if sum(Statebits[1:]) == 0 else 'Sensor error detected' } json_data = json.dumps(data) print(json_data) break + except KeyboardInterrupt: - print("User interrupt encountered. Exiting...") + 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': 'User interrupt encountered' + } + print(json.dumps(data)) time.sleep(3) exit() - except: - # for all other kinds of error, but not specifying which one - print("Unknown error...") + + 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() - + exit() \ No newline at end of file diff --git a/NPM/get_data_modbus_v2_1.py b/NPM/get_data_modbus_v2_1.py new file mode 100644 index 0000000..0c9c141 --- /dev/null +++ b/NPM/get_data_modbus_v2_1.py @@ -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() diff --git a/NPM/get_data_modbus_v3.py b/NPM/get_data_modbus_v3.py index 0c9c141..eacc2e0 100755 --- a/NPM/get_data_modbus_v3.py +++ b/NPM/get_data_modbus_v3.py @@ -29,6 +29,8 @@ Request \x00\x55 Quantity of Registers (Requests to read x55 or 85 consecutive registers starting from address 56) \...\... Cyclic Redundancy Check (checksum ) +MAJ 2026 --> renvoie des 0 si pas de réponse du NPM + ''' import serial import requests @@ -59,119 +61,137 @@ 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 -) +# Initialize default error values +pm1_10s = 0 +pm25_10s = 0 +pm10_10s = 0 +channel_1 = 0 +channel_2 = 0 +channel_3 = 0 +channel_4 = 0 +channel_5 = 0 +relative_humidity = 0 +temperature = 0 -# Define Modbus CRC-16 function -crc16 = crcmod.predefined.mkPredefinedCrcFun('modbus') +try: + # 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 -data = b'\x01\x03\x00\x38\x00\x55' + # Define Modbus CRC-16 function + crc16 = crcmod.predefined.mkPredefinedCrcFun('modbus') -# Calculate and append CRC -crc = crc16(data) -crc_low = crc & 0xFF -crc_high = (crc >> 8) & 0xFF -request = data + bytes([crc_low, crc_high]) + # Request frame without CRC + data = b'\x01\x03\x00\x38\x00\x55' -# Clear serial buffer before sending -ser.flushInput() + # Calculate and append CRC + crc = crc16(data) + crc_low = crc & 0xFF + crc_high = (crc >> 8) & 0xFF + request = data + bytes([crc_low, crc_high]) -# Send request -ser.write(request) -time.sleep(0.2) # Wait for sensor to respond + # Clear serial buffer before sending + ser.flushInput() -# Read response -response_length = 2 + 1 + (2 * 85) + 2 # Address + Function + Data + CRC -byte_data = ser.read(response_length) + # Send request + ser.write(request) + time.sleep(0.2) # Wait for sensor to respond -# Validate response length -if len(byte_data) < response_length: - print("[ERROR] Incomplete response received:", byte_data.hex()) - exit() + # Read response + response_length = 2 + 1 + (2 * 85) + 2 # Address + Function + Data + CRC + byte_data = ser.read(response_length) -# Verify CRC -received_crc = int.from_bytes(byte_data[-2:], byteorder='little') -calculated_crc = crc16(byte_data[:-2]) + # Validate response length + if len(byte_data) < response_length: + print(f"[ERROR] Incomplete response received: {byte_data.hex()}") + raise Exception("Incomplete response") -if received_crc != calculated_crc: - print("[ERROR] CRC check failed! Corrupted data received.") - exit() + # Verify CRC + received_crc = int.from_bytes(byte_data[-2:], byteorder='little') + calculated_crc = crc16(byte_data[:-2]) -# Convert response to hex for debugging -formatted = ''.join(f'\\x{byte:02x}' for byte in byte_data) -#print("Response:", formatted) + if received_crc != calculated_crc: + print("[ERROR] CRC check failed! Corrupted data received.") + raise Exception("CRC check failed") -# 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 + # Convert response to hex for debugging + formatted = ''.join(f'\\x{byte:02x}' for byte in byte_data) + #print("Response:", formatted) - 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 + # 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 - 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: - return int(value) - elif round_to is not None: - return round(value, round_to) - else: - return value + value = value / scale -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) + if round_to == 0: + return int(value) + elif round_to is not None: + return round(value, round_to) + else: + return value -#print("10 sec concentration:") -#print(f"PM1: {pm1_10s}") -#print(f"PM2.5: {pm25_10s}") -#print(f"PM10: {pm10_10s}") + 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) -# 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("10 sec concentration:") + #print(f"PM1: {pm1_10s}") + #print(f"PM2.5: {pm25_10s}") + #print(f"PM10: {pm10_10s}") -#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}") + # 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 - -# 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"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}") -#print(f"Internal Relative Humidity: {relative_humidity} %") -#print(f"Internal temperature: {temperature} °C") + + # 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") + ser.close() -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)) +except Exception as e: + print(f"[ERROR] Sensor communication failed: {e}") + # Variables already set to -1 at the beginning -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 )) +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)) -# Commit and close the connection -conn.commit() + 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)) -conn.close() + # Commit and close the connection + conn.commit() + conn.close() \ No newline at end of file diff --git a/html/sensors.html b/html/sensors.html index 9484850..b2ff69b 100755 --- a/html/sensors.html +++ b/html/sensors.html @@ -101,7 +101,7 @@ function getNPM_values(port){ $("#loading_"+port).hide(); // Create an array of the desired keys - const keysToShow = ["PM1", "PM25", "PM10"]; + const keysToShow = ["PM1", "PM25", "PM10","message"]; // Error messages mapping const errorMessages = { "notReady": "Sensor is not ready", @@ -307,8 +307,8 @@ error: function(xhr, status, error) { const container = document.getElementById('card-container'); // Conteneur des cartes - //creates NPM card - if (response["NPM/get_data_modbus_v3.py"]) { + //creates NPM card (by default) + const cardHTML = `
@@ -329,7 +329,7 @@ error: function(xhr, status, error) {
`; container.innerHTML += cardHTML; // Add the I2C card if condition is met - } + //creates i2c BME280 card if (response["BME280/get_data_v2.py"]) {