From 6622be14ad32f4d081da97d10346e5751f1cc91d Mon Sep 17 00:00:00 2001 From: PaulVua Date: Tue, 18 Feb 2025 12:05:43 +0100 Subject: [PATCH] update --- NPM/get_data_modbus.py | 30 ++-- NPM/get_data_modbus_v2.py | 188 ++++++++++++++++++++++++++ NPM/{ => old}/get_data_modbus_loop.py | 0 NPM/old/test_modbus.py | 126 +++++++++++++++++ config.json.dist | 3 +- html/sensors.html | 22 +++ loop/SARA_send_data_v2.py | 67 +++++---- master.py | 3 +- 8 files changed, 390 insertions(+), 49 deletions(-) create mode 100644 NPM/get_data_modbus_v2.py rename NPM/{ => old}/get_data_modbus_loop.py (100%) mode change 100755 => 100644 create mode 100644 NPM/old/test_modbus.py diff --git a/NPM/get_data_modbus.py b/NPM/get_data_modbus.py index db30a25..c82ef59 100755 --- a/NPM/get_data_modbus.py +++ b/NPM/get_data_modbus.py @@ -89,31 +89,31 @@ while True: #print(formatted) # Extract LSW (first 2 bytes) and MSW (last 2 bytes) - lsw_channel1 = int.from_bytes(byte_data[3:5], byteorder='little') - msw_chanel1 = int.from_bytes(byte_data[5:7], byteorder='little') + lsw_channel1 = int.from_bytes(byte_data[3:5], byteorder='big') + msw_chanel1 = int.from_bytes(byte_data[5:7], byteorder='big') raw_value_channel1 = (msw_chanel1 << 16) | lsw_channel1 - lsw_channel2 = int.from_bytes(byte_data[7:9], byteorder='little') - msw_chanel2 = int.from_bytes(byte_data[9:11], byteorder='little') + lsw_channel2 = int.from_bytes(byte_data[7:9], byteorder='big') + msw_chanel2 = int.from_bytes(byte_data[9:11], byteorder='big') raw_value_channel2 = (msw_chanel2 << 16) | lsw_channel2 - lsw_channel3 = int.from_bytes(byte_data[11:13], byteorder='little') - msw_chanel3 = int.from_bytes(byte_data[13:15], byteorder='little') + lsw_channel3 = int.from_bytes(byte_data[11:13], byteorder='big') + msw_chanel3 = int.from_bytes(byte_data[13:15], byteorder='big') raw_value_channel3 = (msw_chanel3 << 16) | lsw_channel3 - lsw_channel4 = int.from_bytes(byte_data[15:17], byteorder='little') - msw_chanel4 = int.from_bytes(byte_data[17:19], byteorder='little') + lsw_channel4 = int.from_bytes(byte_data[15:17], byteorder='big') + msw_chanel4 = int.from_bytes(byte_data[17:19], byteorder='big') raw_value_channel4 = (msw_chanel1 << 16) | lsw_channel4 - lsw_channel5 = int.from_bytes(byte_data[19:21], byteorder='little') - msw_chanel5 = int.from_bytes(byte_data[21:23], byteorder='little') + lsw_channel5 = int.from_bytes(byte_data[19:21], byteorder='big') + msw_chanel5 = int.from_bytes(byte_data[21:23], byteorder='big') raw_value_channel5 = (msw_chanel5 << 16) | lsw_channel5 - #print(f"Channel 1 (0.2->0.5): {raw_value_channel1}") - #print(f"Channel 2 (0.5->1.0): {raw_value_channel2}") - #print(f"Channel 3 (1.0->2.5): {raw_value_channel3}") - #print(f"Channel 4 (2.5->5.0): {raw_value_channel4}") - #print(f"Channel 5 (5.0->10.): {raw_value_channel5}") + print(f"Channel 1 (0.2->0.5): {raw_value_channel1}") + print(f"Channel 2 (0.5->1.0): {raw_value_channel2}") + print(f"Channel 3 (1.0->2.5): {raw_value_channel3}") + print(f"Channel 4 (2.5->5.0): {raw_value_channel4}") + print(f"Channel 5 (5.0->10.): {raw_value_channel5}") cursor.execute(''' INSERT INTO data_NPM_5channels (timestamp,PM_ch1, PM_ch2, PM_ch3, PM_ch4, PM_ch5) VALUES (?,?,?,?,?,?)''' diff --git a/NPM/get_data_modbus_v2.py b/NPM/get_data_modbus_v2.py new file mode 100644 index 0000000..556bcae --- /dev/null +++ b/NPM/get_data_modbus_v2.py @@ -0,0 +1,188 @@ +''' + _ _ ____ __ __ + | \ | | _ \| \/ | + | \| | |_) | |\/| | + | |\ | __/| | | | + |_| \_|_| |_| |_| + +Script to get NPM data via Modbus +need parameter: port +/usr/bin/python3 /var/www/nebuleair_pro_4g/NPM/get_data_modbus_v2.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 + +# 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 +config_file = '/var/www/nebuleair_pro_4g/config.json' +config = load_config(config_file) +npm_solo_port = config.get('NPM_solo_port', '') #port du NPM solo + +ser = serial.Serial( + port=npm_solo_port, + baudrate=115200, + parity=serial.PARITY_EVEN, + stopbits=serial.STOPBITS_ONE, + bytesize=serial.EIGHTBITS, + timeout = 0.5 +) + +# Define Modbus CRC-16 function +crc16 = crcmod.predefined.mkPredefinedCrcFun('modbus') + +# Request frame without CRC +data = b'\x01\x03\x00\x38\x00\x55' + +# Calculate CRC +crc = crc16(data) +crc_low = crc & 0xFF +crc_high = (crc >> 8) & 0xFF + +# Append CRC to the frame +request = data + bytes([crc_low, crc_high]) +#print(f"Request frame: {request.hex()}") + +ser.write(request) + +#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' + +while True: + try: + byte_data = ser.readline() + formatted = ''.join(f'\\x{byte:02x}' for byte in byte_data) + #print(formatted) + + # Register base (56 = 0x38) + REGISTER_START = 56 + + # Function to extract 32-bit values from Modbus response + def extract_value(byte_data, register, scale=1, single_register=False, round_to=None): + """Extracts a value from Modbus response. + + - `register`: Modbus register to read. + - `scale`: Value is divided by this (e.g., `1000` for PM values). + - `single_register`: If `True`, only reads 16 bits (one register). + """ + offset = (register - REGISTER_START) * 2 + 3 # Calculate byte offset + + 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 # 32-bit value + + value = value / scale # Apply scaling + + if round_to == 0: + return int(value) # Convert to integer to remove .0 + elif round_to is not None: + return round(value, round_to) # Apply normal rounding + else: + return value # No rounding if round_to is None + + # 10-sec PM Concentration (PM1, PM2.5, PM10) + 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}") + + # 1-min PM Concentration + pm1_1min = extract_value(byte_data, 68, 1000, round_to=1) + pm25_1min = extract_value(byte_data, 70, 1000, round_to=1) + pm10_1min = extract_value(byte_data, 72, 1000, round_to=1) + + #print("1 min concentration:") + #print(f"PM1: {pm1_1min}") + #print(f"PM2.5: {pm25_1min}") + #print(f"PM10: {pm10_1min}") + + # 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) + #print(f"Internal Relative Humidity: {relative_humidity} %") + # Retrieve temperature from register 106 (0x6A) + temperature = extract_value(byte_data, 107, 100, single_register=True) + #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() + + + break + except KeyboardInterrupt: + print("User interrupt encountered. Exiting...") + time.sleep(3) + exit() + except: + # for all other kinds of error, but not specifying which one + print("Unknown error...") + time.sleep(3) + exit() + +conn.close() diff --git a/NPM/get_data_modbus_loop.py b/NPM/old/get_data_modbus_loop.py old mode 100755 new mode 100644 similarity index 100% rename from NPM/get_data_modbus_loop.py rename to NPM/old/get_data_modbus_loop.py diff --git a/NPM/old/test_modbus.py b/NPM/old/test_modbus.py new file mode 100644 index 0000000..b8883e7 --- /dev/null +++ b/NPM/old/test_modbus.py @@ -0,0 +1,126 @@ +''' + _ _ ____ __ __ + | \ | | _ \| \/ | + | \| | |_) | |\/| | + | |\ | __/| | | | + |_| \_|_| |_| |_| + +Script to get NPM data via Modbus +need parameter: port +/usr/bin/python3 /var/www/nebuleair_pro_4g/NPM/old/test_modbus.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 +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 + +# 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 +config_file = '/var/www/nebuleair_pro_4g/config.json' +config = load_config(config_file) +npm_solo_port = config.get('NPM_solo_port', '') #port du NPM solo + +ser = serial.Serial( + port=npm_solo_port, + baudrate=115200, + parity=serial.PARITY_EVEN, + stopbits=serial.STOPBITS_ONE, + bytesize=serial.EIGHTBITS, + timeout = 0.5 +) + +# Define Modbus CRC-16 function +crc16 = crcmod.predefined.mkPredefinedCrcFun('modbus') + +# Request frame without CRC +data = b'\x01\x03\x00\x44\x00\x06' + +# Calculate CRC +crc = crc16(data) +crc_low = crc & 0xFF +crc_high = (crc >> 8) & 0xFF + +# Append CRC to the frame +request = data + bytes([crc_low, crc_high]) +#print(f"Request frame: {request.hex()}") + +ser.write(request) + +while True: + try: + byte_data = ser.readline() + formatted = ''.join(f'\\x{byte:02x}' for byte in byte_data) + print(formatted) + + if len(byte_data) < 14: + print(f"Error: Received {len(byte_data)} bytes, expected 14!") + continue + + #10 secs concentration + + lsw_pm1 = int.from_bytes(byte_data[3:5], byteorder='big') + msw_pm1 = int.from_bytes(byte_data[5:7], byteorder='big') + raw_value_pm1 = (msw_pm1 << 16) | lsw_pm1 + raw_value_pm1 = raw_value_pm1 / 1000 + + lsw_pm25 = int.from_bytes(byte_data[7:9], byteorder='big') + msw_pm25 = int.from_bytes(byte_data[9:11], byteorder='big') + raw_value_pm25 = (msw_pm25 << 16) | lsw_pm25 + raw_value_pm25 = raw_value_pm25 / 1000 + + + lsw_pm10 = int.from_bytes(byte_data[11:13], byteorder='big') + msw_pm10 = int.from_bytes(byte_data[13:15], byteorder='big') + raw_value_pm10 = (msw_pm10 << 16) | lsw_pm10 + raw_value_pm10 = raw_value_pm10 / 1000 + + print("1 min") + print(f"PM1: {raw_value_pm1}") + print(f"PM2.5: {raw_value_pm25}") + print(f"PM10: {raw_value_pm10}") + + break + except KeyboardInterrupt: + print("User interrupt encountered. Exiting...") + time.sleep(3) + exit() + except: + # for all other kinds of error, but not specifying which one + print("Unknown error...") + time.sleep(3) + exit() + +conn.close() diff --git a/config.json.dist b/config.json.dist index fd44607..f1999d5 100755 --- a/config.json.dist +++ b/config.json.dist @@ -3,8 +3,7 @@ "loop_log": true, "boot_log": true, "modem_config_mode": false, - "NPM/get_data_v2.py": true, - "NPM/get_data_modbus.py":false, + "NPM/get_data_modbus_v2.py":false, "loop/SARA_send_data_v2.py": true, "RTC/save_to_db.py": true, "BME280/get_data_v2.py": true, diff --git a/html/sensors.html b/html/sensors.html index a6e8ce8..dc4bb17 100755 --- a/html/sensors.html +++ b/html/sensors.html @@ -102,6 +102,16 @@ function getNPM_values(port){ $("#loading_"+port).hide(); // Create an array of the desired keys const keysToShow = ["PM1", "PM25", "PM10"]; + // 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 @@ -114,6 +124,18 @@ function getNPM_values(port){ `); } }); + + // Check for errors and add them to the table + Object.keys(errorMessages).forEach(errorKey => { + if (response[errorKey] === 1) { + $("#data-table-body_" + port).append(` + + ${errorKey} + ⚠ ${errorMessages[errorKey]} + + `); + } + }); }, error: function(xhr, status, error) { console.error('AJAX request failed:', status, error); diff --git a/loop/SARA_send_data_v2.py b/loop/SARA_send_data_v2.py index 0a8adb7..184c9b1 100644 --- a/loop/SARA_send_data_v2.py +++ b/loop/SARA_send_data_v2.py @@ -283,43 +283,50 @@ try: ''' print('

START LOOP

') + #Local timestamp + print("➡️Getting local timestamp") + 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' + # Convert to a datetime object + dt_object = datetime.strptime(rtc_time_str, '%Y-%m-%d %H:%M:%S') + # Convert to InfluxDB RFC3339 format with UTC 'Z' suffix + influx_timestamp = dt_object.strftime('%Y-%m-%dT%H:%M:%SZ') + print(influx_timestamp) + #NEXTPM - print("➡️Getting NPM values") - cursor.execute("SELECT * FROM data_NPM ORDER BY timestamp DESC LIMIT 1") - last_row = cursor.fetchone() - # Display the result - if last_row: - print("SQLite DB last available row:", last_row) + print("➡️Getting NPM values (last 6 measures)") + #cursor.execute("SELECT * FROM data_NPM ORDER BY timestamp DESC LIMIT 1") + cursor.execute("SELECT * FROM data_NPM ORDER BY timestamp DESC LIMIT 6") + rows = cursor.fetchall() + # Exclude the timestamp column (assuming first column is timestamp) + data_values = [row[1:] for row in rows] # Exclude timestamp + # Compute column-wise average + num_columns = len(data_values[0]) + averages = [round(sum(col) / len(col)) for col in zip(*data_values)] - datetime_measure_PM = last_row[0] #on récupère le datetime de la table data_NPM - # Convert to a datetime object - dt_object = datetime.strptime(datetime_measure_PM, '%Y-%m-%d %H:%M:%S') - # Convert to InfluxDB RFC3339 format with UTC 'Z' suffix - influx_timestamp = dt_object.strftime('%Y-%m-%dT%H:%M:%SZ') + PM1 = averages[0] + PM25 = averages[1] + PM10 = averages[2] + npm_temp = averages[3] + npm_hum = averages[4] - PM1 = last_row[1] - PM25 = last_row[2] - PM10 = last_row[3] - npm_temp = last_row[4] - npm_hum = last_row[5] + #Add data to payload CSV + payload_csv[0] = PM1 + payload_csv[1] = PM25 + payload_csv[2] = PM10 + payload_csv[18] = npm_temp + payload_csv[19] = npm_hum - #Add data to payload CSV - payload_csv[0] = PM1 - payload_csv[1] = PM25 - payload_csv[2] = PM10 - payload_csv[18] = npm_temp - payload_csv[19] = npm_hum - - #Add data to payload JSON - payload_json["sensordatavalues"].append({"value_type": "NPM_P0", "value": str(PM1)}) - payload_json["sensordatavalues"].append({"value_type": "NPM_P1", "value": str(PM10)}) - payload_json["sensordatavalues"].append({"value_type": "NPM_P2", "value": str(PM25)}) - else: - print("No data available in the database.") + #Add data to payload JSON + payload_json["sensordatavalues"].append({"value_type": "NPM_P0", "value": str(PM1)}) + payload_json["sensordatavalues"].append({"value_type": "NPM_P1", "value": str(PM10)}) + payload_json["sensordatavalues"].append({"value_type": "NPM_P2", "value": str(PM25)}) + #NextPM 5 channels if npm_5channel: - print("➡️Getting NextPM 5 channels values") + print("➡️Getting NextPM 5 channels values (last 6 measures)") cursor.execute("SELECT * FROM data_NPM_5channels ORDER BY timestamp DESC LIMIT 6") rows = cursor.fetchall() # Exclude the timestamp column (assuming first column is timestamp) diff --git a/master.py b/master.py index b4b4d6d..f3e0ad4 100644 --- a/master.py +++ b/master.py @@ -82,8 +82,7 @@ def run_script(script_name, interval, delay=0): # Define scripts and their execution intervals (seconds) SCRIPTS = [ ("RTC/save_to_db.py", 1, 0), # SAVE RTC time every 1 second, no delay - ("NPM/get_data_v2.py", 60, 0), # Get NPM data every 60s, no delay - ("NPM/get_data_modbus.py", 10, 2), # Get NPM data (modbus 5 channels) every 10s, with 2s delay + ("NPM/get_data_modbus_v2.py", 10, 0), # Get NPM data (modbus 5 channels) every 10s, with 2s delay ("envea/read_value_v2.py", 10, 0), # Get NPM data (modbus 5 channels) every 10s, with 2s delay ("loop/SARA_send_data_v2.py", 60, 1), # Send data every 60 seconds, with 2s delay ("BME280/get_data_v2.py", 120, 0) # Get BME280 data every 120 seconds, no delay