''' _ _ ____ __ __ | \ | | _ \| \/ | | \| | |_) | |\/| | | |\ | __/| | | | |_| \_|_| |_| |_| 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 ) MAJ 2026 --> renvoie des 0 si pas de réponse du NPM ''' import serial import requests import json import sys 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() 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 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 npm_status = 0xFF # Default: all errors (will be overwritten if read succeeds) 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 ) # 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: if not dry_run: print(f"[ERROR] Incomplete response received: {byte_data.hex()}") raise Exception("Incomplete response") # Verify CRC received_crc = int.from_bytes(byte_data[-2:], byteorder='little') calculated_crc = crc16(byte_data[:-2]) if received_crc != calculated_crc: if not dry_run: print("[ERROR] CRC check failed! Corrupted data received.") raise Exception("CRC check failed") # 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") # Read NPM status register (register 19 = 0x13, 1 register) # Modbus request: slave=0x01, func=0x03, addr=0x0013, qty=0x0001 status_request = b'\x01\x03\x00\x13\x00\x01' status_crc = crc16(status_request) status_request += bytes([status_crc & 0xFF, (status_crc >> 8) & 0xFF]) ser.flushInput() ser.write(status_request) time.sleep(0.2) # Response: addr(1) + func(1) + byte_count(1) + data(2) + crc(2) = 7 bytes status_response = ser.read(7) if len(status_response) == 7: status_recv_crc = int.from_bytes(status_response[-2:], byteorder='little') 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 if not dry_run: print(f"NPM status: 0x{npm_status:02X} ({npm_status})") else: if not dry_run: print("[WARNING] NPM status CRC check failed, keeping default") else: if not dry_run: print(f"[WARNING] NPM status incomplete response ({len(status_response)} bytes)") ser.close() except Exception as e: if not dry_run: print(f"[ERROR] Sensor communication failed: {e}") # Variables already set to -1 at the beginning finally: 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)) # Commit and close the connection conn.commit() conn.close()