This commit is contained in:
PaulVua
2025-02-18 12:05:43 +01:00
parent 9774215e7c
commit 6622be14ad
8 changed files with 390 additions and 49 deletions

View File

@@ -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 (?,?,?,?,?,?)'''

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

View File

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

View File

@@ -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,

View File

@@ -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(`
<tr class="error-row">
<td><b>${errorKey}</b></td>
<td style="color: red;">⚠ ${errorMessages[errorKey]}</td>
</tr>
`);
}
});
},
error: function(xhr, status, error) {
console.error('AJAX request failed:', status, error);

View File

@@ -283,25 +283,33 @@ try:
'''
print('<h3>START LOOP</h3>')
#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)
datetime_measure_PM = last_row[0] #on récupère le datetime de la table data_NPM
#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(datetime_measure_PM, '%Y-%m-%d %H:%M:%S')
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)
PM1 = last_row[1]
PM25 = last_row[2]
PM10 = last_row[3]
npm_temp = last_row[4]
npm_hum = last_row[5]
#NEXTPM
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)]
PM1 = averages[0]
PM25 = averages[1]
PM10 = averages[2]
npm_temp = averages[3]
npm_hum = averages[4]
#Add data to payload CSV
payload_csv[0] = PM1
@@ -314,12 +322,11 @@ try:
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.")
#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)

View File

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