This commit is contained in:
root
2025-02-20 15:07:49 +01:00
commit 59133628af
228 changed files with 282657 additions and 0 deletions

64
NPM/firmware_version.py Normal file
View File

@@ -0,0 +1,64 @@
'''
Script to get NPM firmware version
need parameter: port
/usr/bin/python3 /var/www/nebuleair_pro_4g/NPM/firmware_version.py ttyAMA5
'''
import serial
import requests
import json
import sys
parameter = sys.argv[1:] # Exclude the script name
#print("Parameters received:")
port='/dev/'+parameter[0]
ser = serial.Serial(
port=port,
baudrate=115200,
parity=serial.PARITY_EVEN,
stopbits=serial.STOPBITS_ONE,
bytesize=serial.EIGHTBITS,
timeout = 1
)
ser.write(b'\x81\x17\x68') #firmware version
while True:
try:
byte_data = ser.readline()
formatted = ''.join(f'\\x{byte:02x}' for byte in byte_data)
#print(formatted)
'''
la réponse est de type
\x81\x17\x00\x10\x46\x12
avec
\x81 address
\x17 command code
\x00 state
\x10\x46 firmware version
\x12 checksum
'''
# Extract byte 4 and byte 5
byte4 = byte_data[3] # 4th byte (index 3)
byte5 = byte_data[4] # 5th byte (index 4)
firmware_version = int(f"{byte4:02x}{byte5:02x}")
data = {
'firmware_version': firmware_version,
}
json_data = json.dumps(data)
print(json_data)
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()

75
NPM/get_data.py Normal file
View File

@@ -0,0 +1,75 @@
'''
_ _ ____ __ __
| \ | | _ \| \/ |
| \| | |_) | |\/| |
| |\ | __/| | | |
|_| \_|_| |_| |_|
Script to get NPM values
need parameter: port
/usr/bin/python3 /var/www/nebuleair_pro_4g/NPM/get_data.py ttyAMA5
'''
import serial
import requests
import json
import sys
parameter = sys.argv[1:] # Exclude the script name
#print("Parameters received:")
port='/dev/'+parameter[0]
ser = serial.Serial(
port=port,
baudrate=115200,
parity=serial.PARITY_EVEN,
stopbits=serial.STOPBITS_ONE,
bytesize=serial.EIGHTBITS,
timeout = 1
)
ser.write(b'\x81\x11\x6E') #data10s
#ser.write(b'\x81\x12\x6D') #data60s
while True:
try:
byte_data = ser.readline()
#print(byte_data)
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
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]
}
json_data = json.dumps(data)
print(json_data)
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()

137
NPM/get_data_modbus.py Normal file
View File

@@ -0,0 +1,137 @@
'''
_ _ ____ __ __
| \ | | _ \| \/ |
| \| | |_) | |\/| |
| |\ | __/| | | |
|_| \_|_| |_| |_|
Script to get NPM data via Modbus
need parameter: port
/usr/bin/python3 /var/www/nebuleair_pro_4g/NPM/get_data_modbus.py
Modbus RTU
[Slave Address][Function Code][Starting Address][Quantity of Registers][CRC]
Pour récupérer les 5 cannaux (a partir du registre 0x80)
Donnée actualisée toutes les 10 secondes
Request
\x01\x03\x00\x80\x00\x0A\xE4\x1E
\x01 Slave Address (slave device address)
\x03 Function code (read multiple holding registers)
\x00\x80 Starting Address (The request starts reading from holding register address 0x80 or 128)
\x00\x0A Quantity of Registers (Requests to read 0x0A or 10 consecutive registers starting from address 128)
\xE4\x1E 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\x80\x00\x0A'
# 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)
# Extract LSW (first 2 bytes) and MSW (last 2 bytes)
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='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='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='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='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}")
cursor.execute('''
INSERT INTO data_NPM_5channels (timestamp,PM_ch1, PM_ch2, PM_ch3, PM_ch4, PM_ch5) VALUES (?,?,?,?,?,?)'''
, (rtc_time_str,raw_value_channel1,raw_value_channel2,raw_value_channel3,raw_value_channel4,raw_value_channel5))
# 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()

188
NPM/get_data_modbus_v2.py Normal file
View File

@@ -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 = 2
)
# 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()

179
NPM/get_data_modbus_v3.py Normal file
View File

@@ -0,0 +1,179 @@
'''
_ _ ____ __ __
| \ | | _ \| \/ |
| \| | |_) | |\/| |
| |\ | __/| | | |
|_| \_|_| |_| |_|
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
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
#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()

52
NPM/get_data_temp_hum.py Normal file
View File

@@ -0,0 +1,52 @@
'''
_ _ ____ __ __
| \ | | _ \| \/ |
| \| | |_) | |\/| |
| |\ | __/| | | |
|_| \_|_| |_| |_|
Script to get NPM values: ONLY temp and hum
need parameter: port
/usr/bin/python3 /var/www/nebuleair_pro_4g/NPM/get_data_temp_hum.py ttyAMA5
'''
import serial
import requests
import json
import sys
parameter = sys.argv[1:] # Exclude the script name
#print("Parameters received:")
port='/dev/'+parameter[0]
ser = serial.Serial(
port=port,
baudrate=115200,
parity=serial.PARITY_EVEN,
stopbits=serial.STOPBITS_ONE,
bytesize=serial.EIGHTBITS,
timeout = 1
)
ser.write(b'\x81\x14\x6B') # Temp and humidity command
while True:
try:
byte_data_temp_hum = ser.readline()
# Decode temperature and humidity values
temperature = int.from_bytes(byte_data_temp_hum[3:5], byteorder='big') / 100.0
humidity = int.from_bytes(byte_data_temp_hum[5:7], byteorder='big') / 100.0
print(f"temp: {temperature}")
print(f"hum: {humidity}")
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()

104
NPM/get_data_v2.py Normal file
View File

@@ -0,0 +1,104 @@
'''
_ _ ____ __ __
| \ | | _ \| \/ |
| \| | |_) | |\/| |
| |\ | __/| | | |
|_| \_|_| |_| |_|
Script to get NPM values (PM1, PM2.5 and PM10)
PM and the sensor temp/hum
And store them inside sqlite database
Uses RTC module for timing (from SQLite db)
/usr/bin/python3 /var/www/nebuleair_pro_4g/NPM/get_data_v2.py
'''
import serial
import requests
import json
import sys
import sqlite3
import smbus2
import time
from datetime import datetime
# 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
)
# 1⃣ Request PM Data (PM1, PM2.5, PM10)
#ser.write(b'\x81\x11\x6E') #data10s
ser.write(b'\x81\x12\x6D') #data60s
time.sleep(0.5) # Small delay to allow the sensor to process the request
#print("Start get_data_v2.py script")
byte_data = ser.readline()
#print(byte_data)
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
# 2⃣ Request Temperature & Humidity
ser.write(b'\x81\x14\x6B') # Temp and humidity command
time.sleep(0.5) # Small delay to allow the sensor to process the request
byte_data_temp_hum = ser.readline()
# Decode temperature and humidity values
temperature = int.from_bytes(byte_data_temp_hum[3:5], byteorder='big') / 100.0
humidity = int.from_bytes(byte_data_temp_hum[5:7], byteorder='big') / 100.0
#print(f"State: {Statebits}")
#print(f"PM1: {PM1}")
#print(f"PM25: {PM25}")
#print(f"PM10: {PM10}")
#print(f"temp: {temperature}")
#print(f"hum: {humidity}")
#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'
#save to sqlite database
try:
cursor.execute('''
INSERT INTO data_NPM (timestamp,PM1, PM25, PM10, temp_npm, hum_npm) VALUES (?,?,?,?,?,?)'''
, (rtc_time_str,PM1,PM25,PM10,temperature,humidity ))
# Commit and close the connection
conn.commit()
#print("Sensor data saved successfully!")
except Exception as e:
print(f"Database error: {e}")
conn.close()

View File

@@ -0,0 +1,172 @@
'''
Loop to run every minutes
* * * * * /usr/bin/python3 /var/www/nebuleair_pro_4g/NPM/get_data_modbus_loop.py ttyAMA5
saves data (avaerage) to a json file /var/www/nebuleair_pro_4g/NPM/data/data.json
saves data (all) to a sqlite database
first time running the script?
sudo mkdir /var/www/nebuleair_pro_4g/NPM/data
sudo touch /var/www/nebuleair_pro_4g/NPM/data/data.json
'''
import serial
import sys
import crcmod
import time
import json
import os
import sqlite3
import smbus2 # For RTC DS3231
from datetime import datetime
# Ensure a port argument is provided
if len(sys.argv) < 2:
print("Usage: python3 get_data_modbus.py <serial_port>")
sys.exit(1)
port = '/dev/' + sys.argv[1]
# Initialize serial communication
try:
ser = serial.Serial(
port=port,
baudrate=115200,
parity=serial.PARITY_EVEN,
stopbits=serial.STOPBITS_ONE,
bytesize=serial.EIGHTBITS,
timeout=0.5
)
except Exception as e:
print(f"Error opening serial port {port}: {e}")
sys.exit(1)
# Define Modbus CRC-16 function
crc16 = crcmod.predefined.mkPredefinedCrcFun('modbus')
# Request frame without CRC
data = b'\x01\x03\x00\x80\x00\x0A'
# 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])
# Log request frame
print(f"Request frame: {request.hex()}")
# Initialize SQLite database
conn = sqlite3.connect("/var/www/nebuleair_pro_4g/sqlite/sensors.db")
cursor = conn.cursor()
# RTC Module (DS3231) Setup
RTC_I2C_ADDR = 0x68 # DS3231 I2C Address
bus = smbus2.SMBus(1)
def bcd_to_dec(bcd):
return (bcd // 16 * 10) + (bcd % 16)
def get_rtc_time():
"""Reads time from RTC module (DS3231)"""
try:
data = bus.read_i2c_block_data(RTC_I2C_ADDR, 0x00, 7)
seconds = bcd_to_dec(data[0] & 0x7F)
minutes = bcd_to_dec(data[1])
hours = bcd_to_dec(data[2] & 0x3F)
day = bcd_to_dec(data[4])
month = bcd_to_dec(data[5])
year = bcd_to_dec(data[6]) + 2000
return datetime(year, month, day, hours, minutes, seconds).strftime("%Y-%m-%d %H:%M:%S")
except Exception as e:
print(f"RTC Read Error: {e}")
return "N/A"
# Initialize storage for averaging
num_samples = 6
channel_sums = [0] * 5 # 5 channels
#do not start immediately to prevent multiple write/read over NPM serial port
time.sleep(3)
# Loop 6 times to collect data every 10 seconds
for i in range(num_samples):
print(f"\nIteration {i+1}/{num_samples}")
ser.write(request)
rtc_timestamp = get_rtc_time()
try:
byte_data = ser.readline()
formatted = ''.join(f'\\x{byte:02x}' for byte in byte_data)
print(f"Raw Response: {formatted}")
if len(byte_data) < 23:
print("Incomplete response, skipping this sample.")
time.sleep(9)
continue
# Extract and process the 5 channels
channels = []
for j in range(5):
lsw = int.from_bytes(byte_data[3 + j*4 : 5 + j*4], byteorder='little')
msw = int.from_bytes(byte_data[5 + j*4 : 7 + j*4], byteorder='little')
raw_value = (msw << 16) | lsw
channels.append(raw_value)
# Accumulate sum for each channel
for j in range(5):
channel_sums[j] += channels[j]
# Print collected values
print(f"Timestamp (RTC): {rtc_timestamp}")
for j in range(5):
print(f"Channel {j+1}: {channels[j]}")
# Save the individual reading to the database
cursor.execute('''
INSERT INTO data (timestamp, PM_ch1, PM_ch2, PM_ch3, PM_ch4, PM_ch5)
VALUES (?, ?, ?, ?, ?, ?)
''', (rtc_timestamp, channels[0], channels[1], channels[2], channels[3], channels[4]))
conn.commit()
except Exception as e:
print(f"Error reading data: {e}")
# Wait 10 seconds before next reading
time.sleep(10)
# Compute the average values
channel_means = [int(s / num_samples) for s in channel_sums]
# Create JSON structure
data_json = {
"channel_1": channel_means[0],
"channel_2": channel_means[1],
"channel_3": channel_means[2],
"channel_4": channel_means[3],
"channel_5": channel_means[4]
}
# Print final JSON data
print("\nFinal JSON Data (Averaged over 6 readings):")
print(json.dumps(data_json, indent=4))
# Define JSON file path
output_file = "/var/www/nebuleair_pro_4g/NPM/data/data.json"
# Write results to a JSON file
try:
with open(output_file, "w") as f:
json.dump(data_json, f, indent=4)
print(f"\nAveraged JSON data saved to {output_file}")
except Exception as e:
print(f"Error writing to file: {e}")
# Close serial connection
ser.close()

126
NPM/old/test_modbus.py Normal file
View File

@@ -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()