''' _ _ ____ __ __ | \ | | _ \| \/ | | \| | |_) | |\/| | | |\ | __/| | | | |_| \_|_| |_| |_| 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()