Files
nebuleair_pro_4g/NPM/get_data_modbus_v3.py
PaulVua 5b3769769d NPM: lecture registre status Modbus (reg 19) + colonne npm_status
- get_data_modbus_v3.py: requete Modbus separee pour lire le registre
  status (0x13) du NextPM apres les donnees. Stocke dans npm_status.
- create_db.py: ajout colonne npm_status (INTEGER DEFAULT 0) dans
  data_NPM + migration ALTER TABLE pour bases existantes.
- En cas d'erreur de lecture status, garde 0xFF (toutes erreurs).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 20:27:03 +01:00

221 lines
7.2 KiB
Python
Executable File

'''
_ _ ____ __ __
| \ | | _ \| \/ |
| \| | |_) | |\/| |
| |\ | __/| | | |
|_| \_|_| |_| |_|
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
# 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:
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:
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
print(f"NPM status: 0x{npm_status:02X} ({npm_status})")
else:
print("[WARNING] NPM status CRC check failed, keeping default")
else:
print(f"[WARNING] NPM status incomplete response ({len(status_response)} bytes)")
ser.close()
except Exception as e:
print(f"[ERROR] Sensor communication failed: {e}")
# Variables already set to -1 at the beginning
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))
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()