update
This commit is contained in:
702
loop/1_NPM/send_data.py
Normal file
702
loop/1_NPM/send_data.py
Normal file
@@ -0,0 +1,702 @@
|
||||
"""
|
||||
Main loop to gather data from sensor:
|
||||
* NPM
|
||||
* Envea
|
||||
* I2C BME280
|
||||
* Noise sensor
|
||||
and send it to AirCarto servers via SARA R4 HTTP post requests
|
||||
|
||||
CSV PAYLOAD (AirCarto Servers)
|
||||
Endpoint:
|
||||
data.nebuleair.fr
|
||||
/pro_4G/data.php?sensor_id={device_id}
|
||||
|
||||
ATTENTION : do not change order !
|
||||
CSV size: 18
|
||||
{PM1},{PM25},{PM10},{temp},{hum},{press},{avg_noise},{max_noise},{min_noise},{envea_no2},{envea_h2s},{envea_o3},{4g_signal_quality}
|
||||
0 -> PM1 (μg/m3)
|
||||
1 -> PM25 (μg/m3)
|
||||
2 -> PM10 (μg/m3)
|
||||
3 -> temp
|
||||
4 -> hum
|
||||
5 -> press
|
||||
6 -> avg_noise
|
||||
7 -> max_noise
|
||||
8 -> min_noise
|
||||
9 -> envea_no2
|
||||
10 -> envea_h2s
|
||||
11 -> envea_o3
|
||||
12 -> 4G signal quality,
|
||||
13 -> PM 0.2μm to 0.5μm quantity (Nb/L)
|
||||
14 -> PM 0.5μm to 1.0μm quantity (Nb/L)
|
||||
15 -> PM 1.0μm to 2.5μm quantity (Nb/L)
|
||||
16 -> PM 2.5μm to 5.0μm quantity (Nb/L)
|
||||
17 -> PM 5.0μm to 10μm quantity (Nb/L)
|
||||
|
||||
JSON PAYLOAD (Micro-Spot Servers)
|
||||
Same as NebuleAir wifi
|
||||
Endpoint:
|
||||
api-prod.uspot.probesys.net
|
||||
nebuleair?token=2AFF6dQk68daFZ
|
||||
port 443
|
||||
|
||||
{"nebuleairid": "82D25549434",
|
||||
"software_version": "ModuleAirV2-V1-042022",
|
||||
"sensordatavalues":
|
||||
[
|
||||
{"value_type":"NPM_P0","value":"1.54"},
|
||||
{"value_type":"NPM_P1","value":"1.54"},
|
||||
{"value_type":"NPM_P2","value":"1.54"},
|
||||
{"value_type":"NPM_N1","value":"0.02"},
|
||||
{"value_type":"NPM_N10","value":"0.02"},
|
||||
{"value_type":"NPM_N25","value":"0.02"},
|
||||
{"value_type":"MHZ16_CO2","value":"793.00"},
|
||||
{"value_type":"SGP40_VOC","value":"29915.00"},
|
||||
{"value_type":"samples","value":"134400"},
|
||||
{"value_type":"min_micro","value":"137"},
|
||||
{"value_type":"max_micro","value":"155030"},
|
||||
{"value_type":"interval","value":"145000"},
|
||||
{"value_type":"signal","value":"-80"},
|
||||
{"value_type":"latitude","value":"43.2964"},
|
||||
{"value_type":"longitude","value":"5.36978"},
|
||||
{"value_type":"state_npm","value":"State: 00000000"},
|
||||
{"value_type":"BME280_temperature","value":"28.47"},
|
||||
{"value_type":"BME280_humidity","value":"28.47"},
|
||||
{"value_type":"BME280_pressure","value":"28.47"},
|
||||
{"value_type":"CAIRSENS_NO2","value":"54"},
|
||||
{"value_type":"CAIRSENS_H2S","value":"54"},
|
||||
{"value_type":"CAIRSENS_O3","value":"54"}
|
||||
]
|
||||
}
|
||||
"""
|
||||
import board
|
||||
import json
|
||||
import serial
|
||||
import time
|
||||
import busio
|
||||
import re
|
||||
import os
|
||||
import traceback
|
||||
import sys
|
||||
import RPi.GPIO as GPIO
|
||||
from threading import Thread
|
||||
from adafruit_bme280 import basic as adafruit_bme280
|
||||
|
||||
# Record the start time of the script
|
||||
start_time_script = time.time()
|
||||
|
||||
# Check system uptime
|
||||
with open('/proc/uptime', 'r') as f:
|
||||
uptime_seconds = float(f.readline().split()[0])
|
||||
|
||||
# Skip execution if uptime is less than 2 minutes (120 seconds)
|
||||
if uptime_seconds < 120:
|
||||
print(f"System just booted ({uptime_seconds:.2f} seconds uptime), skipping execution.")
|
||||
sys.exit()
|
||||
|
||||
payload_csv = [None] * 20
|
||||
payload_json = {
|
||||
"nebuleairid": "82D25549434",
|
||||
"software_version": "ModuleAirV2-V1-042022",
|
||||
"sensordatavalues": [] # Empty list to start with
|
||||
}
|
||||
|
||||
aircarto_profile_id = 0
|
||||
uSpot_profile_id = 1
|
||||
|
||||
def blink_led(pin, blink_count, delay=1):
|
||||
"""
|
||||
Blink an LED on a specified GPIO pin.
|
||||
|
||||
Args:
|
||||
pin (int): GPIO pin number (BCM mode) to which the LED is connected.
|
||||
blink_count (int): Number of times the LED should blink.
|
||||
delay (float): Time in seconds for the LED to stay ON or OFF (default is 1 second).
|
||||
"""
|
||||
# GPIO setup
|
||||
GPIO.setwarnings(False)
|
||||
GPIO.setmode(GPIO.BCM) # Use BCM numbering
|
||||
GPIO.setup(pin, GPIO.OUT) # Set the specified pin as an output
|
||||
|
||||
try:
|
||||
for _ in range(blink_count):
|
||||
GPIO.output(pin, GPIO.HIGH) # Turn the LED on
|
||||
#print(f"LED on GPIO {pin} is ON")
|
||||
time.sleep(delay) # Wait for the specified delay
|
||||
GPIO.output(pin, GPIO.LOW) # Turn the LED off
|
||||
#print(f"LED on GPIO {pin} is OFF")
|
||||
time.sleep(delay) # Wait for the specified delay
|
||||
finally:
|
||||
GPIO.cleanup(pin) # Clean up the specific pin to reset its state
|
||||
print(f"GPIO {pin} cleaned up")
|
||||
|
||||
#get data from config
|
||||
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 {}
|
||||
|
||||
#Fonction pour mettre à jour le JSON de configuration
|
||||
def update_json_key(file_path, key, value):
|
||||
"""
|
||||
Updates a specific key in a JSON file with a new value.
|
||||
|
||||
:param file_path: Path to the JSON file.
|
||||
:param key: The key to update in the JSON file.
|
||||
:param value: The new value to assign to the key.
|
||||
"""
|
||||
try:
|
||||
# Load the existing data
|
||||
with open(file_path, "r") as file:
|
||||
data = json.load(file)
|
||||
|
||||
# Check if the key exists in the JSON file
|
||||
if key in data:
|
||||
data[key] = value # Update the key with the new value
|
||||
else:
|
||||
print(f"Key '{key}' not found in the JSON file.")
|
||||
return
|
||||
|
||||
# Write the updated data back to the file
|
||||
with open(file_path, "w") as file:
|
||||
json.dump(data, file, indent=2) # Use indent for pretty printing
|
||||
|
||||
print(f"updating '{key}' to '{value}'.")
|
||||
except Exception as e:
|
||||
print(f"Error updating the JSON file: {e}")
|
||||
|
||||
# Define the config file path
|
||||
config_file = '/var/www/nebuleair_pro_4g/config.json'
|
||||
|
||||
# Load the configuration data
|
||||
config = load_config(config_file)
|
||||
loop_activation = config.get('loop_activation', True) #activation de la loop principale
|
||||
baudrate = config.get('SaraR4_baudrate', 115200) #baudrate du sara R4
|
||||
device_id = config.get('deviceID', '').upper() #device ID en maj
|
||||
need_to_log = config.get('loop_log', False) #inscription des logs
|
||||
bme_280_config = config.get('i2c_BME', False) #présence du BME280
|
||||
i2C_sound_config = config.get('i2C_sound', False) #présence du capteur son
|
||||
send_aircarto = config.get('send_aircarto', True) #envoi sur AirCarto (data.nebuleair.fr)
|
||||
send_uSpot = config.get('send_uSpot', False) #envoi sur MicroSpot ()
|
||||
npm_5channel = config.get('NextPM_5channels', False) #5 canaux du NPM
|
||||
|
||||
envea_sondes = config.get('envea_sondes', [])
|
||||
connected_envea_sondes = [sonde for sonde in envea_sondes if sonde.get('connected', False)]
|
||||
selected_networkID = config.get('SARA_R4_neworkID', '')
|
||||
|
||||
#update device id in the payload json
|
||||
payload_json["nebuleairid"] = device_id
|
||||
|
||||
if loop_activation == False:
|
||||
print("Loop activation is False , skipping execution.")
|
||||
sys.exit()
|
||||
|
||||
ser_sara = serial.Serial(
|
||||
port='/dev/ttyAMA2',
|
||||
baudrate=baudrate, #115200 ou 9600
|
||||
parity=serial.PARITY_NONE, #PARITY_NONE, PARITY_EVEN or PARITY_ODD
|
||||
stopbits=serial.STOPBITS_ONE,
|
||||
bytesize=serial.EIGHTBITS,
|
||||
timeout = 2
|
||||
)
|
||||
|
||||
ser_NPM = serial.Serial(
|
||||
port='/dev/ttyAMA5',
|
||||
baudrate=115200,
|
||||
parity=serial.PARITY_EVEN,
|
||||
stopbits=serial.STOPBITS_ONE,
|
||||
bytesize=serial.EIGHTBITS,
|
||||
timeout = 1
|
||||
)
|
||||
|
||||
serial_connections = {}
|
||||
|
||||
# Sondes Envea
|
||||
if connected_envea_sondes:
|
||||
# Pour chacune des sondes
|
||||
for device in connected_envea_sondes:
|
||||
port = device.get('port', 'Unknown')
|
||||
name = device.get('name', 'Unknown')
|
||||
connected = device.get('connected', False)
|
||||
|
||||
serial_connections[name] = serial.Serial(
|
||||
port=f'/dev/{port}', # Format the port string
|
||||
baudrate=9600,
|
||||
parity=serial.PARITY_NONE,
|
||||
stopbits=serial.STOPBITS_ONE,
|
||||
bytesize=serial.EIGHTBITS,
|
||||
timeout = 1
|
||||
)
|
||||
|
||||
|
||||
|
||||
def read_complete_response(serial_connection, timeout=2, end_of_response_timeout=2, wait_for_line=None, debug=True):
|
||||
response = bytearray()
|
||||
serial_connection.timeout = timeout
|
||||
end_time = time.time() + end_of_response_timeout
|
||||
start_time = time.time()
|
||||
|
||||
while True:
|
||||
elapsed_time = time.time() - start_time # Time since function start
|
||||
if serial_connection.in_waiting > 0:
|
||||
data = serial_connection.read(serial_connection.in_waiting)
|
||||
response.extend(data)
|
||||
end_time = time.time() + end_of_response_timeout # Reset timeout on new data
|
||||
|
||||
# Decode and check for the specific line
|
||||
if wait_for_line:
|
||||
decoded_response = response.decode('utf-8', errors='replace')
|
||||
if wait_for_line in decoded_response:
|
||||
if debug: print(f"[DEBUG] 🔎Found target line: {wait_for_line}")
|
||||
break
|
||||
elif time.time() > end_time:
|
||||
if debug: print(f"[DEBUG] Timeout reached. No more data received.")
|
||||
break
|
||||
time.sleep(0.1) # Short sleep to prevent busy waiting
|
||||
# Final response and debug output
|
||||
total_elapsed_time = time.time() - start_time
|
||||
if debug: print(f"[DEBUG] ⏱️ elapsed time: {total_elapsed_time:.2f}s. ⏱️")
|
||||
# Check if the elapsed time exceeded 10 seconds
|
||||
if total_elapsed_time > 10 and debug:
|
||||
print(f"[ALERT] 🚨 The operation took too long🚨")
|
||||
print(f'<span style="color: red;font-weight: bold;">[ALERT] ⚠️{total_elapsed_time:.2f}s⚠️</span>')
|
||||
|
||||
return response.decode('utf-8', errors='replace')
|
||||
|
||||
# Open and read the JSON file
|
||||
try:
|
||||
# Send the command to request data (e.g., data for 60 seconds)
|
||||
print('<h3>START LOOP</h3>')
|
||||
print("Getting NPM values")
|
||||
ser_NPM.write(b'\x81\x12\x6D')
|
||||
|
||||
# Read the response
|
||||
byte_data = ser_NPM.readline()
|
||||
|
||||
#if npm is disconnected byte_data is empty
|
||||
|
||||
# Extract the state byte and PM data from the response
|
||||
state_byte = int.from_bytes(byte_data[2:3], byteorder='big')
|
||||
state_bits = [int(bit) for bit in bin(state_byte)[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
|
||||
|
||||
#Add data to payload CSV
|
||||
payload_csv[0] = PM1
|
||||
payload_csv[1] = PM25
|
||||
payload_csv[2] = PM10
|
||||
#Add data to payload JSON
|
||||
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)})
|
||||
|
||||
# Sonde BME280 connected
|
||||
if bme_280_config:
|
||||
print("Getting BME280 values")
|
||||
#on récupère les infos du BME280 et on les ajoute au payload_csv
|
||||
i2c = busio.I2C(board.SCL, board.SDA)
|
||||
bme280 = adafruit_bme280.Adafruit_BME280_I2C(i2c, address=0x76)
|
||||
bme280.sea_level_pressure = 1013.25 # Update this value for your location
|
||||
|
||||
payload_csv[3] = round(bme280.temperature, 2)
|
||||
payload_csv[4] = round(bme280.humidity, 2)
|
||||
payload_csv[5] = round(bme280.pressure, 2)
|
||||
|
||||
payload_json["sensordatavalues"].append({"value_type": "BME280_temperature", "value": f"{round(bme280.temperature, 2)}"})
|
||||
payload_json["sensordatavalues"].append({"value_type": "BME280_humidity", "value": f"{round(bme280.humidity, 2)}"})
|
||||
payload_json["sensordatavalues"].append({"value_type": "BME280_pressure", "value": f"{round(bme280.pressure, 2)}"})
|
||||
|
||||
# NPM sur 5 cannaux
|
||||
if npm_5channel:
|
||||
print("Getting NPM 5 Channels")
|
||||
# Define the path to the JSON file
|
||||
json_file_path_npm = "/var/www/nebuleair_pro_4g/NPM/data/data.json"
|
||||
# Read the JSON file
|
||||
try:
|
||||
with open(json_file_path_npm, "r") as file:
|
||||
data = json.load(file) # Load JSON into a dictionary
|
||||
|
||||
# Extract values
|
||||
channel_1 = data.get("channel_1", 0)
|
||||
channel_2 = data.get("channel_2", 0)
|
||||
channel_3 = data.get("channel_3", 0)
|
||||
channel_4 = data.get("channel_4", 0)
|
||||
channel_5 = data.get("channel_5", 0)
|
||||
|
||||
# Print extracted values
|
||||
print(f"Channel 1: {channel_1}")
|
||||
print(f"Channel 2: {channel_2}")
|
||||
print(f"Channel 3: {channel_3}")
|
||||
print(f"Channel 4: {channel_4}")
|
||||
print(f"Channel 5: {channel_5}")
|
||||
|
||||
#add to CSV
|
||||
payload_csv[13] = channel_1
|
||||
payload_csv[14] = channel_2
|
||||
payload_csv[15] = channel_3
|
||||
payload_csv[16] = channel_4
|
||||
payload_csv[17] = channel_5
|
||||
|
||||
|
||||
except FileNotFoundError:
|
||||
print(f"Error: JSON file not found at {json_file_path_npm}")
|
||||
except json.JSONDecodeError:
|
||||
print("Error: JSON file is not formatted correctly")
|
||||
except Exception as e:
|
||||
print(f"Unexpected error: {e}")
|
||||
|
||||
# Sonde Bruit connected
|
||||
if i2C_sound_config:
|
||||
#on récupère les infos de sound_metermoving et on les ajoute au message
|
||||
file_path_data_noise = "/var/www/nebuleair_pro_4g/sound_meter/moving_avg_minute.txt"
|
||||
# Read the file and extract the numbers
|
||||
try:
|
||||
with open(file_path_data_noise, "r") as file:
|
||||
content = file.read().strip()
|
||||
avg_noise, max_noise, min_noise = map(int, content.split())
|
||||
|
||||
# Append the variables to the payload_csv
|
||||
payload_csv[6] = avg_noise
|
||||
payload_csv[7] = max_noise
|
||||
payload_csv[8] = min_noise
|
||||
|
||||
except FileNotFoundError:
|
||||
print(f"Error: File {file_path_data_noise} not found.")
|
||||
except ValueError:
|
||||
print("Error: File content is not valid numbers.")
|
||||
|
||||
# Sondes Envea
|
||||
if connected_envea_sondes:
|
||||
print("Getting Envea values")
|
||||
# Define the path to the JSON file
|
||||
json_file_path_envea = "/var/www/nebuleair_pro_4g/envea/data/data.json"
|
||||
# Read the JSON file
|
||||
try:
|
||||
with open(json_file_path_envea, "r") as file:
|
||||
data = json.load(file) # Load JSON into a dictionary
|
||||
|
||||
# Extract values
|
||||
h2s = data.get("h2s", 0)
|
||||
no2 = data.get("no2", 0)
|
||||
o3 = data.get("o3", 0)
|
||||
|
||||
# Print extracted values
|
||||
print(f"h2s : {h2s}")
|
||||
print(f"no2 : {no2}")
|
||||
print(f"o3: {o3}")
|
||||
|
||||
#add to CSV
|
||||
payload_csv[10] = h2s
|
||||
payload_csv[9] = no2
|
||||
payload_csv[11] = o3
|
||||
|
||||
#add to JSON
|
||||
payload_json["sensordatavalues"].append({"value_type": "CAIRSENS_H2S", "value": str(h2s)})
|
||||
payload_json["sensordatavalues"].append({"value_type": "CAIRSENS_NO2", "value": str(no2)})
|
||||
payload_json["sensordatavalues"].append({"value_type": "CAIRSENS_O3", "value": str(o3)})
|
||||
|
||||
|
||||
except FileNotFoundError:
|
||||
print(f"Error: JSON file not found at {json_file_path_envea}")
|
||||
except json.JSONDecodeError:
|
||||
print("Error: JSON file is not formatted correctly")
|
||||
except Exception as e:
|
||||
print(f"Unexpected error: {e}")
|
||||
|
||||
# Getting the LTE Signal
|
||||
print("-> Getting LTE signal <-")
|
||||
ser_sara.write(b'AT+CSQ\r')
|
||||
response2 = read_complete_response(ser_sara, wait_for_line="OK")
|
||||
print('<p class="text-danger-emphasis">')
|
||||
print(response2)
|
||||
print("</p>")
|
||||
match = re.search(r'\+CSQ:\s*(\d+),', response2)
|
||||
if match:
|
||||
signal_quality = int(match.group(1))
|
||||
payload_csv[12]=signal_quality
|
||||
time.sleep(1)
|
||||
|
||||
# On vérifie si le signal n'est pas à 99 pour déconnexion
|
||||
# si c'est le cas on essaie de se reconnecter
|
||||
if signal_quality == 99:
|
||||
print('<span style="color: red;font-weight: bold;">⚠️ATTENTION: Signal Quality indicates no signal (99)⚠️</span>')
|
||||
print("TRY TO RECONNECT:")
|
||||
command = f'AT+COPS=1,2,"{selected_networkID}"\r'
|
||||
ser_sara.write(command.encode('utf-8'))
|
||||
responseReconnect = read_complete_response(ser_sara, timeout=20, end_of_response_timeout=20)
|
||||
print('<p class="text-danger-emphasis">')
|
||||
print(responseReconnect)
|
||||
print("</p>")
|
||||
|
||||
print('🛑STOP LOOP🛑')
|
||||
print("<hr>")
|
||||
|
||||
#on arrete le script pas besoin de continuer
|
||||
sys.exit()
|
||||
else:
|
||||
print("Signal Quality:", signal_quality)
|
||||
|
||||
|
||||
#print(payload_json)
|
||||
|
||||
'''
|
||||
SEND TO AIRCARTO
|
||||
'''
|
||||
|
||||
# Write Data to saraR4
|
||||
# 1. Open sensordata_csv.json (with correct data size)
|
||||
csv_string = ','.join(str(value) if value is not None else '' for value in payload_csv)
|
||||
size_of_string = len(csv_string)
|
||||
print("Open JSON:")
|
||||
command = f'AT+UDWNFILE="sensordata_csv.json",{size_of_string}\r'
|
||||
ser_sara.write(command.encode('utf-8'))
|
||||
response_SARA_1 = read_complete_response(ser_sara, wait_for_line=">", debug=False)
|
||||
print(response_SARA_1)
|
||||
time.sleep(1)
|
||||
|
||||
#2. Write to shell
|
||||
print("Write data to memory:")
|
||||
ser_sara.write(csv_string.encode())
|
||||
response_SARA_2 = read_complete_response(ser_sara, wait_for_line="OK", debug=False)
|
||||
print(response_SARA_2)
|
||||
|
||||
#3. Send to endpoint (with device ID)
|
||||
print("Send data (POST REQUEST):")
|
||||
command= f'AT+UHTTPC={aircarto_profile_id},4,"/pro_4G/data.php?sensor_id={device_id}","server_response.txt","sensordata_csv.json",4\r'
|
||||
ser_sara.write(command.encode('utf-8'))
|
||||
|
||||
response_SARA_3 = read_complete_response(ser_sara, timeout=5, end_of_response_timeout=120, wait_for_line="+UUHTTPCR")
|
||||
|
||||
print('<p class="text-danger-emphasis">')
|
||||
print(response_SARA_3)
|
||||
print("</p>")
|
||||
|
||||
# si on recoit la réponse UHTTPCR
|
||||
if "+UUHTTPCR" in response_SARA_3:
|
||||
print("✅ Received +UUHTTPCR response.")
|
||||
|
||||
# Les types de réponse
|
||||
|
||||
# 1.La commande n'a pas fonctionné
|
||||
# +CME ERROR: No connection to phone
|
||||
# +CME ERROR: Operation not allowed
|
||||
|
||||
# 2.La commande fonctionne: elle renvoie un code
|
||||
# +UUHTTPCR: <profile_id>,<http_command>,<http_result>
|
||||
# <http_result>: 1 pour sucess et 0 pour fail
|
||||
# +UUHTTPCR: 0,4,1 -> OK
|
||||
# +UUHTTPCR: 0,4,0 -> error
|
||||
|
||||
# Split response into lines
|
||||
lines = response_SARA_3.strip().splitlines()
|
||||
|
||||
# 1.Vérifier si la réponse contient un message d'erreur CME
|
||||
if "+CME ERROR" in lines[-1]:
|
||||
print("*****")
|
||||
print('<span style="color: red;font-weight: bold;">ATTENTION: CME ERROR</span>')
|
||||
print("error:", lines[-1])
|
||||
print("*****")
|
||||
#update status
|
||||
update_json_key(config_file, "SARA_R4_network_status", "disconnected")
|
||||
|
||||
# Gestion de l'erreur spécifique
|
||||
if "No connection to phone" in lines[-1]:
|
||||
print("No connection to the phone. Retrying or reset may be required.")
|
||||
# Actions spécifiques pour ce type d'erreur (par exemple, réinitialiser ou tenter de reconnecter)
|
||||
# need to reconnect to network
|
||||
# and reset HTTP profile (AT+UHTTP=0) -> ne fonctionne pas..
|
||||
# tester un reset avec CFUN 15
|
||||
# 1.Reconnexion au réseau (AT+COPS)
|
||||
command = f'AT+COPS=1,2,"{selected_networkID}"\r'
|
||||
ser_sara.write(command.encode('utf-8'))
|
||||
responseReconnect = read_complete_response(ser_sara)
|
||||
print("Response reconnect:")
|
||||
print(responseReconnect)
|
||||
print("End response reconnect")
|
||||
|
||||
elif "Operation not allowed" in lines[-1]:
|
||||
print("Operation not allowed. This may require a different configuration.")
|
||||
# Actions spécifiques pour ce type d'erreur
|
||||
|
||||
# Clignotement LED rouge en cas d'erreur
|
||||
led_thread = Thread(target=blink_led, args=(24, 5, 0.5))
|
||||
led_thread.start()
|
||||
|
||||
else:
|
||||
# 2.Si la réponse contient une réponse HTTP valide
|
||||
# Extract HTTP response code from the last line
|
||||
# ATTENTION: lines[-1] renvoie l'avant dernière ligne et il peut y avoir un soucis avec le OK
|
||||
# rechercher plutot
|
||||
http_response = lines[-1] # "+UUHTTPCR: 0,4,0"
|
||||
parts = http_response.split(',')
|
||||
|
||||
# 2.1 code 0 (HTTP failed)
|
||||
if len(parts) == 3 and parts[-1] == '0': # The third value indicates success
|
||||
print("*****")
|
||||
print('<span style="color: red;font-weight: bold;">ATTENTION: HTTP operation failed</span>')
|
||||
update_json_key(config_file, "SARA_R4_network_status", "disconnected")
|
||||
print("*****")
|
||||
print("Blink red LED")
|
||||
# Run LED blinking in a separate thread
|
||||
led_thread = Thread(target=blink_led, args=(24, 5, 0.5))
|
||||
led_thread.start()
|
||||
|
||||
# Get error code
|
||||
print("Getting error code (11->Server connection error, 73->Secure socket connect error)")
|
||||
command = f'AT+UHTTPER={aircarto_profile_id}\r'
|
||||
ser_sara.write(command.encode('utf-8'))
|
||||
response_SARA_9 = read_complete_response(ser_sara, wait_for_line="OK", debug=False)
|
||||
print('<p class="text-danger-emphasis">')
|
||||
print(response_SARA_9)
|
||||
print("</p>")
|
||||
|
||||
'''
|
||||
+UHTTPER: profile_id,error_class,error_code
|
||||
|
||||
error_class
|
||||
0 OK, no error
|
||||
3 HTTP Protocol error class
|
||||
10 Wrong HTTP API USAGE
|
||||
|
||||
error_code (for error_class 3)
|
||||
0 No error
|
||||
11 Server connection error
|
||||
73 Secure socket connect error
|
||||
'''
|
||||
|
||||
#Pas forcément un moyen de résoudre le soucis
|
||||
#print("resetting the URL (domain name):")
|
||||
#command = f'AT+UHTTP={aircarto_profile_id},1,"{url_nebuleair}"\r'
|
||||
#ser_sara.write(command.encode('utf-8'))
|
||||
#response_SARA_31 = read_complete_response(ser_sara)
|
||||
#print(response_SARA_31)
|
||||
|
||||
# 2.2 code 1 (HHTP succeded)
|
||||
else:
|
||||
# Si la commande HTTP a réussi
|
||||
print('<span class="badge text-bg-success">HTTP operation successful.</span>')
|
||||
update_json_key(config_file, "SARA_R4_network_status", "connected")
|
||||
print("Blink blue LED")
|
||||
led_thread = Thread(target=blink_led, args=(23, 5, 0.5))
|
||||
led_thread.start()
|
||||
#4. Read reply from server
|
||||
print("Reply from server:")
|
||||
ser_sara.write(b'AT+URDFILE="server_response.txt"\r')
|
||||
response_SARA_4 = read_complete_response(ser_sara, wait_for_line="OK", debug=False)
|
||||
print('<p class="text-success">')
|
||||
print(response_SARA_4)
|
||||
print('</p>')
|
||||
else:
|
||||
print('<span style="color: red;font-weight: bold;">No UUHTTPCR response</span>')
|
||||
print("Blink red LED")
|
||||
# Run LED blinking in a separate thread
|
||||
led_thread = Thread(target=blink_led, args=(24, 5, 0.5))
|
||||
led_thread.start()
|
||||
|
||||
|
||||
#5. empty json
|
||||
print("Empty SARA memory:")
|
||||
ser_sara.write(b'AT+UDELFILE="sensordata_csv.json"\r')
|
||||
response_SARA_5 = read_complete_response(ser_sara, wait_for_line="OK", debug=False)
|
||||
print(response_SARA_5)
|
||||
|
||||
'''
|
||||
SEND TO MICRO SPOT
|
||||
'''
|
||||
if send_uSpot:
|
||||
print(">>>>>>>>")
|
||||
print(">>>>>>>>")
|
||||
print(">>>>>>>>")
|
||||
print("SEND TO MICRO SPOT (HTTP):")
|
||||
|
||||
#step 4: set url (op_code = 1)
|
||||
print("****")
|
||||
print("SET URL")
|
||||
command = f'AT+UHTTP={uSpot_profile_id},1,"api-prod.uspot.probesys.net"\r'
|
||||
ser_sara.write((command + '\r').encode('utf-8'))
|
||||
response_SARA_5 = read_complete_response(ser_sara, wait_for_line="OK", debug=False)
|
||||
print(response_SARA_5)
|
||||
time.sleep(1)
|
||||
|
||||
#step 4: set url to SSL (op_code = 6) (http_secure = 1 for HTTPS)(USECMNG_PROFILE = 2)
|
||||
print("****")
|
||||
print("SET SSL")
|
||||
command = f'AT+UHTTP={uSpot_profile_id},6,0\r'
|
||||
ser_sara.write(command.encode('utf-8'))
|
||||
response_SARA_5 = read_complete_response(ser_sara, wait_for_line="OK", debug=False)
|
||||
print(response_SARA_5)
|
||||
time.sleep(1)
|
||||
|
||||
#step 4: set PORT (op_code = 5)
|
||||
print("****")
|
||||
print("SET PORT")
|
||||
command = f'AT+UHTTP={uSpot_profile_id},5,81\r'
|
||||
ser_sara.write((command + '\r').encode('utf-8'))
|
||||
response_SARA_55 = read_complete_response(ser_sara, wait_for_line="OK", debug=False)
|
||||
print(response_SARA_55)
|
||||
time.sleep(1)
|
||||
|
||||
|
||||
# Write Data to saraR4
|
||||
|
||||
|
||||
# 1. Open sensordata_json.json (with correct data size)
|
||||
print("Open JSON:")
|
||||
payload_string = json.dumps(payload_json) # Convert dict to JSON string
|
||||
size_of_string = len(payload_string)
|
||||
command = f'AT+UDWNFILE="sensordata_json.json",{size_of_string}\r'
|
||||
ser_sara.write((command + '\r').encode('utf-8'))
|
||||
response_SARA_1 = read_complete_response(ser_sara, wait_for_line=">", debug=False)
|
||||
print(response_SARA_1)
|
||||
time.sleep(1)
|
||||
|
||||
#2. Write to shell
|
||||
print("Write to memory:")
|
||||
ser_sara.write(payload_string.encode())
|
||||
response_SARA_2 = read_complete_response(ser_sara, wait_for_line="OK", debug=False)
|
||||
print(response_SARA_2)
|
||||
|
||||
#step 4: trigger the request (http_command=1 for GET and http_command=1 for POST)
|
||||
print("****")
|
||||
print("Trigger POST REQUEST")
|
||||
command = f'AT+UHTTPC={uSpot_profile_id},4,"/nebuleair?token=2AFF6dQk68daFZ","http.resp","sensordata_json.json",4\r'
|
||||
ser_sara.write(command.encode('utf-8'))
|
||||
|
||||
|
||||
response_SARA_3 = read_complete_response(ser_sara, timeout=5, end_of_response_timeout=30, wait_for_line="+UUHTTPCR")
|
||||
print('<p class="text-danger-emphasis">')
|
||||
print(response_SARA_3)
|
||||
print("</p>")
|
||||
|
||||
#READ REPLY
|
||||
print("****")
|
||||
print("Read reply from server")
|
||||
ser_sara.write(b'AT+URDFILE="http.resp"\r')
|
||||
response_SARA_7 = read_complete_response(ser_sara, wait_for_line="OK", debug=False)
|
||||
print('<p class="text-success">')
|
||||
print(response_SARA_7)
|
||||
print('</p>')
|
||||
|
||||
|
||||
#5. empty json
|
||||
print("Empty SARA memory:")
|
||||
ser_sara.write(b'AT+UDELFILE="sensordata_json.json"\r')
|
||||
response_SARA_8 = read_complete_response(ser_sara, wait_for_line="OK", debug=False)
|
||||
print(response_SARA_8)
|
||||
|
||||
|
||||
# Calculate and print the elapsed time
|
||||
elapsed_time = time.time() - start_time_script
|
||||
print(f"Elapsed time: {elapsed_time:.2f} seconds")
|
||||
print("<hr>")
|
||||
|
||||
|
||||
except Exception as e:
|
||||
print("An error occurred:", e)
|
||||
traceback.print_exc() # This prints the full traceback
|
||||
312
loop/1_NPM/send_data_mqtt.py
Normal file
312
loop/1_NPM/send_data_mqtt.py
Normal file
@@ -0,0 +1,312 @@
|
||||
"""
|
||||
Main loop to gather data from sensor:
|
||||
* NPM
|
||||
* I2C BME280
|
||||
* Noise sensor
|
||||
and send it to AirCarto servers via SARA R4 MQTT requests
|
||||
"""
|
||||
import board
|
||||
import json
|
||||
import serial
|
||||
import time
|
||||
import busio
|
||||
|
||||
import RPi.GPIO as GPIO
|
||||
from adafruit_bme280 import basic as adafruit_bme280
|
||||
|
||||
# Record the start time of the script
|
||||
start_time = time.time()
|
||||
|
||||
url="data.nebuleair.fr"
|
||||
|
||||
# Set up GPIO mode (for Blue LED: network status)
|
||||
GPIO.setwarnings(False)
|
||||
GPIO.setmode(GPIO.BCM) # Use Broadcom pin numbering
|
||||
GPIO.setup(23, GPIO.OUT) # Set GPIO23 as an output pin
|
||||
|
||||
# Add yellow color to the output
|
||||
yellow_color = "\033[33m" # ANSI escape code for yellow
|
||||
red_color = "\033[31m" # ANSI escape code for red
|
||||
reset_color = "\033[0m" # Reset color to default
|
||||
green_color = "\033[32m" # Green
|
||||
|
||||
#get data from config
|
||||
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 {}
|
||||
|
||||
def update_json_key(file_path, key, value):
|
||||
"""
|
||||
Updates a specific key in a JSON file with a new value.
|
||||
|
||||
:param file_path: Path to the JSON file.
|
||||
:param key: The key to update in the JSON file.
|
||||
:param value: The new value to assign to the key.
|
||||
"""
|
||||
try:
|
||||
# Load the existing data
|
||||
with open(file_path, "r") as file:
|
||||
data = json.load(file)
|
||||
|
||||
# Check if the key exists in the JSON file
|
||||
if key in data:
|
||||
data[key] = value # Update the key with the new value
|
||||
else:
|
||||
print(f"Key '{key}' not found in the JSON file.")
|
||||
return
|
||||
|
||||
# Write the updated data back to the file
|
||||
with open(file_path, "w") as file:
|
||||
json.dump(data, file, indent=2) # Use indent for pretty printing
|
||||
|
||||
print(f"Successfully updated '{key}' to '{value}'.")
|
||||
except Exception as e:
|
||||
print(f"Error updating the JSON file: {e}")
|
||||
|
||||
# Define the config file path
|
||||
config_file = '/var/www/nebuleair_pro_4g/config.json'
|
||||
|
||||
# Load the configuration data
|
||||
config = load_config(config_file)
|
||||
# Access the shared variables
|
||||
baudrate = config.get('SaraR4_baudrate', 115200) #baudrate du sara R4
|
||||
device_id = config.get('deviceID', '').upper() #device ID en maj
|
||||
need_to_log = config.get('loop_log', False) #inscription des logs
|
||||
bme_280_config = config.get('i2c_BME', False) #présence du BME280
|
||||
i2C_sound_config = config.get('i2C_sound', False) #présence du BME280
|
||||
|
||||
ser = serial.Serial(
|
||||
port='/dev/ttyAMA2',
|
||||
baudrate=baudrate, #115200 ou 9600
|
||||
parity=serial.PARITY_NONE, #PARITY_NONE, PARITY_EVEN or PARITY_ODD
|
||||
stopbits=serial.STOPBITS_ONE,
|
||||
bytesize=serial.EIGHTBITS,
|
||||
timeout = 2
|
||||
)
|
||||
|
||||
ser_NPM = serial.Serial(
|
||||
port='/dev/ttyAMA5',
|
||||
baudrate=115200,
|
||||
parity=serial.PARITY_EVEN,
|
||||
stopbits=serial.STOPBITS_ONE,
|
||||
bytesize=serial.EIGHTBITS,
|
||||
timeout = 1
|
||||
)
|
||||
|
||||
def read_complete_response(serial_connection, timeout=2, end_of_response_timeout=2):
|
||||
response = bytearray()
|
||||
serial_connection.timeout = timeout
|
||||
end_time = time.time() + end_of_response_timeout
|
||||
|
||||
while True:
|
||||
if serial_connection.in_waiting > 0:
|
||||
data = serial_connection.read(serial_connection.in_waiting)
|
||||
response.extend(data)
|
||||
end_time = time.time() + end_of_response_timeout # Reset timeout on new data
|
||||
elif time.time() > end_time:
|
||||
break
|
||||
time.sleep(0.1) # Short sleep to prevent busy waiting
|
||||
|
||||
# Decode the response and filter out empty lines
|
||||
decoded_response = response.decode('utf-8')
|
||||
non_empty_lines = "\n".join(line for line in decoded_response.splitlines() if line.strip())
|
||||
|
||||
# Add yellow color to the output
|
||||
|
||||
colored_output = f"{yellow_color}{non_empty_lines}\n{reset_color}"
|
||||
|
||||
return colored_output
|
||||
|
||||
# Open and read the JSON file
|
||||
try:
|
||||
# Send the command to request data (e.g., data for 60 seconds)
|
||||
ser_NPM.write(b'\x81\x12\x6D')
|
||||
|
||||
# Read the response
|
||||
byte_data = ser_NPM.readline()
|
||||
|
||||
#if npm is disconnected byte_data is empty
|
||||
|
||||
# Extract the state byte and PM data from the response
|
||||
state_byte = int.from_bytes(byte_data[2:3], byteorder='big')
|
||||
state_bits = [int(bit) for bit in bin(state_byte)[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
|
||||
|
||||
# Create a dictionary with the parsed data
|
||||
data = {
|
||||
'sondeID': device_id,
|
||||
'PM1': PM1,
|
||||
'PM25': PM25,
|
||||
'PM10': PM10
|
||||
}
|
||||
|
||||
message = f"{data['PM1']},{data['PM25']},{data['PM10']}"
|
||||
|
||||
if bme_280_config:
|
||||
#on récupère les infos du BME280 et on les ajoute au message
|
||||
i2c = busio.I2C(board.SCL, board.SDA)
|
||||
bme280 = adafruit_bme280.Adafruit_BME280_I2C(i2c, address=0x76)
|
||||
bme280.sea_level_pressure = 1013.25 # Update this value for your location
|
||||
data['temp'] = round(bme280.temperature, 2)
|
||||
data['hum'] = round(bme280.humidity, 2)
|
||||
data['press'] = round(bme280.pressure, 2)
|
||||
message += f",{data['temp']},{data['hum']},{data['press']}"
|
||||
|
||||
if i2C_sound_config:
|
||||
#on récupère les infos de sound_metermoving et on les ajoute au message
|
||||
file_path_data_noise = "/var/www/nebuleair_pro_4g/sound_meter/moving_avg_minute.txt"
|
||||
# Read the file and extract the numbers
|
||||
try:
|
||||
with open(file_path_data_noise, "r") as file:
|
||||
content = file.read().strip()
|
||||
avg_noise, max_noise, min_noise = map(int, content.split())
|
||||
|
||||
# Append the variables to the JSON and to the message
|
||||
data['avg_noise'] = avg_noise
|
||||
data['max_noise'] = max_noise
|
||||
data['min_noise'] = min_noise
|
||||
|
||||
#get BME280 data (SAFE: it returns none if the key do not exist)
|
||||
|
||||
message = f"{data.get('PM1', '')},{data.get('PM25', '')},{data.get('PM10', '')},{data.get('temp', '')},{data.get('hum', '')},{data.get('press', '')},{avg_noise},{max_noise},{min_noise}"
|
||||
|
||||
print(message) # Display the message or send it further
|
||||
|
||||
except FileNotFoundError:
|
||||
print(f"Error: File {file_path} not found.")
|
||||
except ValueError:
|
||||
print("Error: File content is not valid numbers.")
|
||||
|
||||
# Print the content of the JSON file
|
||||
if need_to_log:
|
||||
print("Data from sensors:")
|
||||
print(json.dumps(data, indent=4)) # Pretty print the JSON data
|
||||
|
||||
|
||||
|
||||
|
||||
#Write Data to saraR4
|
||||
|
||||
#1. MQTT profile configuration
|
||||
# Note: you need to logout first to change the config
|
||||
print("")
|
||||
|
||||
#print("1.PROFILE CONFIG")
|
||||
#print(" 1.A. READ CONFIG")
|
||||
#command = f'AT+UMQTT?\r'
|
||||
#ser.write((command + '\r').encode('utf-8'))
|
||||
#response_SARA_1 = read_complete_response(ser)
|
||||
#if need_to_log:
|
||||
# print(response_SARA_1)
|
||||
|
||||
# La config s'efface à chaque redémarrage!
|
||||
need_to_update_config = False
|
||||
if need_to_update_config:
|
||||
print("1.B. SET CONFIG")
|
||||
#command = f'AT+UMQTT=1,1883\r' #MQTT local TCP port number
|
||||
command = f'AT+UMQTT=2,"aircarto.fr"\r' #MQTT server name
|
||||
#command = f'AT+UMQTT=3,"193.252.54.10"\r' # MQTT server IP address
|
||||
#command = f'AT+UMQTT=12,1\r' # MQTT clean session
|
||||
ser.write((command + '\r').encode('utf-8'))
|
||||
response_SARA_1 = read_complete_response(ser)
|
||||
if need_to_log:
|
||||
print(response_SARA_1)
|
||||
lines = response_SARA_1.strip().splitlines()
|
||||
for line in lines:
|
||||
if line.startswith("+UMQTT"):
|
||||
# Split the line by commas and get the last number
|
||||
parts = line.split(',')
|
||||
last_number = parts[-1].strip() # Get the last part and strip any whitespace
|
||||
|
||||
if int(last_number) == 1:
|
||||
print(f"{green_color}MQTT profile configuration SUCCEDED{reset_color}")
|
||||
else:
|
||||
print(f"{green_color}ERROR: MQTT profile configuration fail{reset_color}")
|
||||
|
||||
|
||||
#2. MQTT login
|
||||
need_to_update_login = False
|
||||
if need_to_update_login:
|
||||
print("")
|
||||
print("2.MQTT LOGIN")
|
||||
#command = f'AT+UMQTTC=1\r' #MQTT login
|
||||
command = f'AT+UMQTTC=0\r' #MQTT logout
|
||||
|
||||
ser.write((command + '\r').encode('utf-8'))
|
||||
response_SARA_2 = read_complete_response(ser, 8, 8)
|
||||
if need_to_log:
|
||||
print(response_SARA_2)
|
||||
lines = response_SARA_2.strip().splitlines()
|
||||
for line in lines:
|
||||
|
||||
if line.startswith("+UMQTTC"):
|
||||
parts = line.split(',')
|
||||
first_number = parts[0].replace("+UMQTTC:", "").strip()
|
||||
last_number = parts[-1].strip() # Get the last part and strip any whitespace
|
||||
#print(f"Last number: {last_number}")
|
||||
if int(first_number) == 0:
|
||||
print("MQTT logout command ->")
|
||||
if int(first_number) == 1:
|
||||
print("MQTT login command ->")
|
||||
if int(last_number) == 1:
|
||||
print(f"{green_color}SUCCESS{reset_color}")
|
||||
else:
|
||||
print(f"{red_color}FAIL{reset_color}")
|
||||
|
||||
if line.startswith("+UUMQTTC"):
|
||||
parts = line.split(',')
|
||||
first_number = parts[0].replace("+UUMQTTC:", "").strip()
|
||||
last_number = parts[-1].strip() # Get the last part and strip any whitespace
|
||||
if int(first_number) == 1:
|
||||
print("MQTT login result ->")
|
||||
if int(last_number) == 0:
|
||||
print(f"{green_color}connection accepted{reset_color}")
|
||||
if int(last_number) == 1:
|
||||
print(f"{green_color}the server does not support the level of the MQTT protocol requested by the Client{reset_color}")
|
||||
if int(last_number) == 2:
|
||||
print(f"{green_color} the client identifier is correct UTF-8 but not allowed by the Server{reset_color}")
|
||||
if int(last_number) == 3:
|
||||
print(f"{green_color} the network connection has been made but the MQTT service is unavailable{reset_color}")
|
||||
|
||||
|
||||
#3. MQTT publish
|
||||
print("")
|
||||
print("3.MQTT PUBLISH")
|
||||
command = f'AT+UMQTTC=2,0,0,"nebuleair/pro/{device_id}/data","{message}"\r'
|
||||
ser.write((command + '\r').encode('utf-8'))
|
||||
response_SARA_3 = read_complete_response(ser)
|
||||
if need_to_log:
|
||||
print(response_SARA_3)
|
||||
lines = response_SARA_3.strip().splitlines()
|
||||
for line in lines:
|
||||
if line.startswith("+UMQTTC"):
|
||||
parts = line.split(',')
|
||||
first_number = parts[0].replace("+UMQTTC:", "").strip()
|
||||
last_number = parts[-1].strip() # Get the last part and strip any whitespace
|
||||
if int(first_number) == 2:
|
||||
print("MQTT Publish ->")
|
||||
if int(last_number) == 1:
|
||||
print(f"{green_color}SUCCESS{reset_color}")
|
||||
else:
|
||||
print(f"{red_color}FAIL{reset_color}")
|
||||
|
||||
|
||||
# Calculate and print the elapsed time
|
||||
elapsed_time = time.time() - start_time
|
||||
if need_to_log:
|
||||
print("")
|
||||
print(f"Elapsed time: {elapsed_time:.2f} seconds")
|
||||
print("----------------------------------------")
|
||||
print("----------------------------------------")
|
||||
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error reading the JSON file: {e}")
|
||||
6
loop/3_NPM/data.json
Normal file
6
loop/3_NPM/data.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"sondeID": "Average_USB2_USB3",
|
||||
"PM1": 0.0,
|
||||
"PM25": 0.0,
|
||||
"PM10": 0.0
|
||||
}
|
||||
101
loop/3_NPM/get_data.py
Normal file
101
loop/3_NPM/get_data.py
Normal file
@@ -0,0 +1,101 @@
|
||||
import serial
|
||||
import json
|
||||
import time
|
||||
|
||||
# Initialize serial ports for the three sensors
|
||||
ser3 = serial.Serial(
|
||||
port='/dev/ttyAMA3',
|
||||
baudrate=115200,
|
||||
parity=serial.PARITY_EVEN,
|
||||
stopbits=serial.STOPBITS_ONE,
|
||||
bytesize=serial.EIGHTBITS,
|
||||
timeout = 1
|
||||
)
|
||||
|
||||
ser4 = serial.Serial(
|
||||
port='/dev/ttyAMA4',
|
||||
baudrate=115200,
|
||||
parity=serial.PARITY_EVEN,
|
||||
stopbits=serial.STOPBITS_ONE,
|
||||
bytesize=serial.EIGHTBITS,
|
||||
timeout = 1
|
||||
)
|
||||
|
||||
ser5 = serial.Serial(
|
||||
port='/dev/ttyAMA5',
|
||||
baudrate=115200,
|
||||
parity=serial.PARITY_EVEN,
|
||||
stopbits=serial.STOPBITS_ONE,
|
||||
bytesize=serial.EIGHTBITS,
|
||||
timeout = 1
|
||||
)
|
||||
|
||||
# Function to read and parse sensor data
|
||||
def read_sensor_data(ser, sonde_id):
|
||||
try:
|
||||
# Send the command to request data (e.g., data for 60 seconds)
|
||||
ser.write(b'\x81\x12\x6D')
|
||||
|
||||
# Read the response
|
||||
byte_data = ser.readline()
|
||||
|
||||
# Extract the state byte and PM data from the response
|
||||
state_byte = int.from_bytes(byte_data[2:3], byteorder='big')
|
||||
state_bits = [int(bit) for bit in bin(state_byte)[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
|
||||
|
||||
# Create a dictionary with the parsed data
|
||||
data = {
|
||||
'sondeID': sonde_id,
|
||||
'PM1': PM1,
|
||||
'PM25': PM25,
|
||||
'PM10': PM10
|
||||
}
|
||||
|
||||
return data
|
||||
except Exception as e:
|
||||
print(f"Error reading from sensor {sonde_id}: {e}")
|
||||
return None
|
||||
|
||||
# Function to create a JSON object with all sensor data
|
||||
def collect_all_sensor_data():
|
||||
all_data = {}
|
||||
|
||||
# Read data from each sensor and add to the all_data dictionary
|
||||
sensor_data_3 = read_sensor_data(ser3, 'USB2')
|
||||
sensor_data_4 = read_sensor_data(ser4, 'USB3')
|
||||
sensor_data_5 = read_sensor_data(ser5, 'USB4')
|
||||
|
||||
# Store the data for each sensor in the dictionary
|
||||
if sensor_data_3:
|
||||
all_data['sensor_3'] = sensor_data_3
|
||||
if sensor_data_4:
|
||||
all_data['sensor_4'] = sensor_data_4
|
||||
if sensor_data_5:
|
||||
all_data['sensor_5'] = sensor_data_5
|
||||
|
||||
return all_data
|
||||
|
||||
# Main script to run once
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
# Collect data from all sensors
|
||||
data = collect_all_sensor_data()
|
||||
|
||||
# Convert data to JSON
|
||||
json_data = json.dumps(data, indent=4)
|
||||
|
||||
# Define the output file path
|
||||
output_file = "/var/www/nebuleair_pro_4g/loop/data.json" # Change this to your desired file path
|
||||
|
||||
# Write the JSON data to the file
|
||||
with open(output_file, 'w') as file:
|
||||
file.write(json_data)
|
||||
|
||||
print(f"Data successfully written to {output_file}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error: {e}")
|
||||
182
loop/3_NPM/get_data_closest_pair.py
Normal file
182
loop/3_NPM/get_data_closest_pair.py
Normal file
@@ -0,0 +1,182 @@
|
||||
import serial
|
||||
import json
|
||||
import time
|
||||
import math
|
||||
|
||||
# Record the start time of the script
|
||||
start_time = time.time()
|
||||
|
||||
#get config
|
||||
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 {}
|
||||
|
||||
# Define the config file path
|
||||
config_file = '/var/www/nebuleair_pro_4g/config.json'
|
||||
# Load the configuration data
|
||||
config = load_config(config_file)
|
||||
# Access the shared variables
|
||||
need_to_log = config.get('loop_log', False)
|
||||
|
||||
# Initialize serial ports for the three sensors
|
||||
ser3 = serial.Serial(
|
||||
port='/dev/ttyAMA3',
|
||||
baudrate=115200,
|
||||
parity=serial.PARITY_EVEN,
|
||||
stopbits=serial.STOPBITS_ONE,
|
||||
bytesize=serial.EIGHTBITS,
|
||||
timeout=1
|
||||
)
|
||||
|
||||
ser4 = serial.Serial(
|
||||
port='/dev/ttyAMA4',
|
||||
baudrate=115200,
|
||||
parity=serial.PARITY_EVEN,
|
||||
stopbits=serial.STOPBITS_ONE,
|
||||
bytesize=serial.EIGHTBITS,
|
||||
timeout=1
|
||||
)
|
||||
|
||||
ser5 = serial.Serial(
|
||||
port='/dev/ttyAMA5',
|
||||
baudrate=115200,
|
||||
parity=serial.PARITY_EVEN,
|
||||
stopbits=serial.STOPBITS_ONE,
|
||||
bytesize=serial.EIGHTBITS,
|
||||
timeout=1
|
||||
)
|
||||
|
||||
# Function to read and parse sensor data
|
||||
def read_sensor_data(ser, sonde_id):
|
||||
try:
|
||||
# Send the command to request data (e.g., data for 60 seconds)
|
||||
ser.write(b'\x81\x12\x6D')
|
||||
|
||||
# Read the response
|
||||
byte_data = ser.readline()
|
||||
|
||||
# Extract the state byte and PM data from the response
|
||||
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
|
||||
|
||||
# Create a dictionary with the parsed data
|
||||
data = {
|
||||
'sondeID': sonde_id,
|
||||
'PM1': PM1,
|
||||
'PM25': PM25,
|
||||
'PM10': PM10
|
||||
}
|
||||
|
||||
return data
|
||||
except Exception as e:
|
||||
print(f"Error reading from sensor {sonde_id}: {e}")
|
||||
return None
|
||||
|
||||
# Function to calculate the Euclidean distance between two sensor readings
|
||||
def calculate_distance(sensor1, sensor2):
|
||||
PM1_diff = sensor1['PM1'] - sensor2['PM1']
|
||||
PM25_diff = sensor1['PM25'] - sensor2['PM25']
|
||||
PM10_diff = sensor1['PM10'] - sensor2['PM10']
|
||||
return math.sqrt(PM1_diff**2 + PM25_diff**2 + PM10_diff**2)
|
||||
|
||||
# Function to select the closest pair of sensors and average their data
|
||||
def average_closest_pair(data):
|
||||
# List of sensor names and their data
|
||||
sensors = list(data.items())
|
||||
|
||||
# Variable to keep track of the smallest distance and corresponding pair
|
||||
min_distance = float('inf')
|
||||
closest_pair = None
|
||||
|
||||
# Compare each pair of sensors to find the closest one
|
||||
for i in range(len(sensors)):
|
||||
for j in range(i + 1, len(sensors)):
|
||||
sensor1 = sensors[i][1]
|
||||
sensor2 = sensors[j][1]
|
||||
|
||||
# Calculate the distance between the two sensors
|
||||
distance = calculate_distance(sensor1, sensor2)
|
||||
|
||||
# Update the closest pair if a smaller distance is found
|
||||
if distance < min_distance:
|
||||
min_distance = distance
|
||||
closest_pair = (sensor1, sensor2)
|
||||
|
||||
# If a closest pair is found, average their values
|
||||
if closest_pair:
|
||||
sensor1, sensor2 = closest_pair
|
||||
averaged_data = {
|
||||
'sondeID': f"Average_{sensor1['sondeID']}_{sensor2['sondeID']}",
|
||||
'PM1': round((sensor1['PM1'] + sensor2['PM1']) / 2, 2),
|
||||
'PM25': round((sensor1['PM25'] + sensor2['PM25']) / 2, 2),
|
||||
'PM10': round((sensor1['PM10'] + sensor2['PM10']) / 2, 2)
|
||||
}
|
||||
return averaged_data
|
||||
else:
|
||||
return None
|
||||
|
||||
# Function to create a JSON object with all sensor data
|
||||
def collect_all_sensor_data():
|
||||
all_data = {}
|
||||
|
||||
# Read data from each sensor and add to the all_data dictionary
|
||||
sensor_data_3 = read_sensor_data(ser3, 'USB2')
|
||||
sensor_data_4 = read_sensor_data(ser4, 'USB3')
|
||||
sensor_data_5 = read_sensor_data(ser5, 'USB4')
|
||||
|
||||
# Store the data for each sensor in the dictionary
|
||||
if sensor_data_3:
|
||||
all_data['sensor_3'] = sensor_data_3
|
||||
if sensor_data_4:
|
||||
all_data['sensor_4'] = sensor_data_4
|
||||
if sensor_data_5:
|
||||
all_data['sensor_5'] = sensor_data_5
|
||||
|
||||
return all_data
|
||||
|
||||
# Main script to run once and average data for the closest sensors
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
# Collect data from all sensors
|
||||
data = collect_all_sensor_data()
|
||||
if need_to_log:
|
||||
print("Getting Data from all sensors:")
|
||||
print(data)
|
||||
|
||||
# Average the closest pair of sensors
|
||||
averaged_data = average_closest_pair(data)
|
||||
if need_to_log:
|
||||
print("Average the closest pair of sensors:")
|
||||
print(averaged_data)
|
||||
|
||||
|
||||
if averaged_data:
|
||||
# Convert the averaged data to JSON
|
||||
json_data = json.dumps(averaged_data, indent=4)
|
||||
|
||||
# Define the output file path
|
||||
output_file = "/var/www/nebuleair_pro_4g/loop/data.json" # Change this to your desired file path
|
||||
|
||||
# Write the JSON data to the file
|
||||
with open(output_file, 'w') as file:
|
||||
file.write(json_data)
|
||||
|
||||
if need_to_log:
|
||||
print(f"Data successfully written to {output_file}")
|
||||
else:
|
||||
print("No closest pair found to average.")
|
||||
|
||||
# Calculate and print the elapsed time
|
||||
elapsed_time = time.time() - start_time
|
||||
if need_to_log:
|
||||
print(f"Elapsed time: {elapsed_time:.2f} seconds")
|
||||
print("-----------------")
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error: {e}")
|
||||
142
loop/3_NPM/send_data.py
Normal file
142
loop/3_NPM/send_data.py
Normal file
@@ -0,0 +1,142 @@
|
||||
import json
|
||||
import serial
|
||||
import time
|
||||
|
||||
# Record the start time of the script
|
||||
start_time = time.time()
|
||||
|
||||
# Define the path to the JSON file
|
||||
file_path = "/var/www/nebuleair_pro_4g/loop/data.json" # Replace with your actual file path
|
||||
|
||||
url="data.nebuleair.fr"
|
||||
|
||||
#get config
|
||||
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 {}
|
||||
|
||||
# Define the config file path
|
||||
config_file = '/var/www/nebuleair_pro_4g/config.json'
|
||||
# Load the configuration data
|
||||
config = load_config(config_file)
|
||||
# Access the shared variables
|
||||
baudrate = config.get('SaraR4_baudrate', 115200)
|
||||
device_id = config.get('deviceID', '').upper()
|
||||
need_to_log = config.get('loop_log', False)
|
||||
|
||||
ser = serial.Serial(
|
||||
port='/dev/ttyAMA2',
|
||||
baudrate=baudrate, #115200 ou 9600
|
||||
parity=serial.PARITY_NONE, #PARITY_NONE, PARITY_EVEN or PARITY_ODD
|
||||
stopbits=serial.STOPBITS_ONE,
|
||||
bytesize=serial.EIGHTBITS,
|
||||
timeout = 2
|
||||
)
|
||||
|
||||
def read_complete_response(serial_connection, timeout=2, end_of_response_timeout=2):
|
||||
response = bytearray()
|
||||
serial_connection.timeout = timeout
|
||||
end_time = time.time() + end_of_response_timeout
|
||||
|
||||
while True:
|
||||
if serial_connection.in_waiting > 0:
|
||||
data = serial_connection.read(serial_connection.in_waiting)
|
||||
response.extend(data)
|
||||
end_time = time.time() + end_of_response_timeout # Reset timeout on new data
|
||||
elif time.time() > end_time:
|
||||
break
|
||||
time.sleep(0.1) # Short sleep to prevent busy waiting
|
||||
|
||||
return response.decode('utf-8')
|
||||
|
||||
# Open and read the JSON file
|
||||
try:
|
||||
with open(file_path, 'r') as file:
|
||||
# Load the data from the file
|
||||
data = json.load(file)
|
||||
|
||||
# Print the content of the JSON file
|
||||
if need_to_log:
|
||||
print("Data from JSON file:")
|
||||
print(json.dumps(data, indent=4)) # Pretty print the JSON data
|
||||
|
||||
message = f"{data['PM1']},{data['PM25']},{data['PM10']}"
|
||||
|
||||
#Write Data to saraR4
|
||||
#1. Open sensordata.json (with correct data size)
|
||||
size_of_string = len(message)
|
||||
command = f'AT+UDWNFILE="sensordata.json",{size_of_string}\r'
|
||||
ser.write((command + '\r').encode('utf-8'))
|
||||
response_SARA_1 = read_complete_response(ser)
|
||||
if need_to_log:
|
||||
print("Open JSON:")
|
||||
print(response_SARA_1)
|
||||
time.sleep(1)
|
||||
#2. Write to shell
|
||||
ser.write(message.encode())
|
||||
response_SARA_2 = read_complete_response(ser)
|
||||
if need_to_log:
|
||||
print("Write to memory:")
|
||||
print(response_SARA_2)
|
||||
#3. Send to endpoint (with device ID)
|
||||
command= f'AT+UHTTPC=0,4,"/pro_4G/data.php?sensor_id={device_id}","server_response.txt","sensordata.json",4\r'
|
||||
ser.write((command + '\r').encode('utf-8'))
|
||||
response_SARA_3 = read_complete_response(ser)
|
||||
if need_to_log:
|
||||
print("Send data:")
|
||||
print(response_SARA_3)
|
||||
# Split response into lines
|
||||
lines = response_SARA_3.strip().splitlines()
|
||||
# +UUHTTPCR: <profile_id>,<http_command>,<http_result>
|
||||
# <http_result>: 1 pour sucess et 0 pour fail
|
||||
# +UUHTTPCR: 0,4,1 -> OK
|
||||
# +UUHTTPCR: 0,4,0 -> error
|
||||
|
||||
# Extract HTTP response code from the last line
|
||||
http_response = lines[-1] # "+UUHTTPCR: 0,4,0"
|
||||
parts = http_response.split(',')
|
||||
|
||||
# Check HTTP result
|
||||
if len(parts) == 3 and parts[-1] == '0': # The third value indicates success
|
||||
print("*****")
|
||||
print("!ATTENTION!")
|
||||
print("error: HTTP operation failed.")
|
||||
print("*****")
|
||||
print("resetting the URL (domain name):")
|
||||
command = f'AT+UHTTP=0,1,"{url}"\r'
|
||||
ser.write((command + '\r').encode('utf-8'))
|
||||
response_SARA_31 = read_complete_response(ser)
|
||||
if need_to_log:
|
||||
print(response_SARA_31)
|
||||
|
||||
else:
|
||||
print("HTTP operation successful.")
|
||||
#4. Read reply from server
|
||||
ser.write(b'AT+URDFILE="server_response.txt"\r')
|
||||
response_SARA_4 = read_complete_response(ser)
|
||||
if need_to_log:
|
||||
print("Reply from server:")
|
||||
print(response_SARA_4)
|
||||
|
||||
#5. empty json
|
||||
ser.write(b'AT+UDELFILE="sensordata.json"\r')
|
||||
response_SARA_5 = read_complete_response(ser)
|
||||
if need_to_log:
|
||||
print("Empty JSON:")
|
||||
print(response_SARA_5)
|
||||
|
||||
|
||||
|
||||
# Calculate and print the elapsed time
|
||||
elapsed_time = time.time() - start_time
|
||||
if need_to_log:
|
||||
print(f"Elapsed time: {elapsed_time:.2f} seconds")
|
||||
print("-----------------")
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error reading the JSON file: {e}")
|
||||
639
loop/SARA_send_data_v2.py
Normal file
639
loop/SARA_send_data_v2.py
Normal file
@@ -0,0 +1,639 @@
|
||||
"""
|
||||
____ _ ____ _ ____ _ ____ _
|
||||
/ ___| / \ | _ \ / \ / ___| ___ _ __ __| | | _ \ __ _| |_ __ _
|
||||
\___ \ / _ \ | |_) | / _ \ \___ \ / _ \ '_ \ / _` | | | | |/ _` | __/ _` |
|
||||
___) / ___ \| _ < / ___ \ ___) | __/ | | | (_| | | |_| | (_| | || (_| |
|
||||
|____/_/ \_\_| \_\/_/ \_\ |____/ \___|_| |_|\__,_| |____/ \__,_|\__\__,_|
|
||||
|
||||
Main loop to gather data from sensor inside SQLite database:
|
||||
|
||||
* NPM
|
||||
* Envea
|
||||
* I2C BME280
|
||||
* Noise sensor
|
||||
|
||||
and send it to AirCarto servers via SARA R4 HTTP post requests
|
||||
also send the timestamp (already stored inside the DB) !
|
||||
|
||||
/usr/bin/python3 /var/www/nebuleair_pro_4g/loop/SARA_send_data_v2.py
|
||||
|
||||
|
||||
ATTENTION:
|
||||
# This script is triggered every minutes by /var/www/nebuleair_pro_4g/master.py (as a service)
|
||||
|
||||
CSV PAYLOAD (AirCarto Servers)
|
||||
Endpoint:
|
||||
data.nebuleair.fr
|
||||
/pro_4G/data.php?sensor_id={device_id}×tamp={rtc_module_time}
|
||||
|
||||
ATTENTION : do not change order !
|
||||
CSV size: 18
|
||||
{PM1},{PM25},{PM10},{temp},{hum},{press},{avg_noise},{max_noise},{min_noise},{envea_no2},{envea_h2s},{envea_o3},{4g_signal_quality}
|
||||
0 -> PM1 (μg/m3)
|
||||
1 -> PM25 (μg/m3)
|
||||
2 -> PM10 (μg/m3)
|
||||
3 -> temp
|
||||
4 -> hum
|
||||
5 -> press
|
||||
6 -> avg_noise
|
||||
7 -> max_noise
|
||||
8 -> min_noise
|
||||
9 -> envea_no2
|
||||
10 -> envea_h2s
|
||||
11 -> envea_nh3
|
||||
12 -> 4G signal quality,
|
||||
13 -> PM 0.2μm to 0.5μm quantity (Nb/L)
|
||||
14 -> PM 0.5μm to 1.0μm quantity (Nb/L)
|
||||
15 -> PM 1.0μm to 2.5μm quantity (Nb/L)
|
||||
16 -> PM 2.5μm to 5.0μm quantity (Nb/L)
|
||||
17 -> PM 5.0μm to 10μm quantity (Nb/L)
|
||||
18 -> NPM temp inside
|
||||
19 -> NPM hum inside
|
||||
|
||||
JSON PAYLOAD (Micro-Spot Servers)
|
||||
Same as NebuleAir wifi
|
||||
Endpoint:
|
||||
api-prod.uspot.probesys.net
|
||||
nebuleair?token=2AFF6dQk68daFZ
|
||||
port 443
|
||||
|
||||
{"nebuleairid": "82D25549434",
|
||||
"software_version": "ModuleAirV2-V1-042022",
|
||||
"sensordatavalues":
|
||||
[
|
||||
{"value_type":"NPM_P0","value":"1.54"},
|
||||
{"value_type":"NPM_P1","value":"1.54"},
|
||||
{"value_type":"NPM_P2","value":"1.54"},
|
||||
{"value_type":"NPM_N1","value":"0.02"},
|
||||
{"value_type":"NPM_N10","value":"0.02"},
|
||||
{"value_type":"NPM_N25","value":"0.02"},
|
||||
{"value_type":"MHZ16_CO2","value":"793.00"},
|
||||
{"value_type":"SGP40_VOC","value":"29915.00"},
|
||||
{"value_type":"samples","value":"134400"},
|
||||
{"value_type":"min_micro","value":"137"},
|
||||
{"value_type":"max_micro","value":"155030"},
|
||||
{"value_type":"interval","value":"145000"},
|
||||
{"value_type":"signal","value":"-80"},
|
||||
{"value_type":"latitude","value":"43.2964"},
|
||||
{"value_type":"longitude","value":"5.36978"},
|
||||
{"value_type":"state_npm","value":"State: 00000000"},
|
||||
{"value_type":"BME280_temperature","value":"28.47"},
|
||||
{"value_type":"BME280_humidity","value":"28.47"},
|
||||
{"value_type":"BME280_pressure","value":"28.47"},
|
||||
{"value_type":"CAIRSENS_NO2","value":"54"},
|
||||
{"value_type":"CAIRSENS_H2S","value":"54"},
|
||||
{"value_type":"CAIRSENS_O3","value":"54"}
|
||||
]
|
||||
}
|
||||
"""
|
||||
|
||||
import board
|
||||
import json
|
||||
import serial
|
||||
import time
|
||||
import busio
|
||||
import re
|
||||
import os
|
||||
import traceback
|
||||
import sys
|
||||
import sqlite3
|
||||
import RPi.GPIO as GPIO
|
||||
from threading import Thread
|
||||
from datetime import datetime
|
||||
|
||||
# Record the start time of the script
|
||||
start_time_script = time.time()
|
||||
|
||||
# Check system uptime
|
||||
with open('/proc/uptime', 'r') as f:
|
||||
uptime_seconds = float(f.readline().split()[0])
|
||||
|
||||
# Skip execution if uptime is less than 2 minutes (120 seconds)
|
||||
if uptime_seconds < 120:
|
||||
print(f"System just booted ({uptime_seconds:.2f} seconds uptime), skipping execution.")
|
||||
sys.exit()
|
||||
|
||||
#Payload CSV to be sent to data.nebuleair.fr
|
||||
payload_csv = [None] * 25
|
||||
#Payload JSON to be sent to uSpot
|
||||
payload_json = {
|
||||
"nebuleairid": "XXX",
|
||||
"software_version": "ModuleAirV2-V1-042022",
|
||||
"sensordatavalues": [] # Empty list to start with
|
||||
}
|
||||
|
||||
# SARA R4 UHTTPC profile IDs
|
||||
aircarto_profile_id = 0
|
||||
uSpot_profile_id = 1
|
||||
|
||||
# database connection
|
||||
conn = sqlite3.connect("/var/www/nebuleair_pro_4g/sqlite/sensors.db")
|
||||
cursor = conn.cursor()
|
||||
|
||||
def blink_led(pin, blink_count, delay=1):
|
||||
"""
|
||||
Blink an LED on a specified GPIO pin.
|
||||
|
||||
Args:
|
||||
pin (int): GPIO pin number (BCM mode) to which the LED is connected.
|
||||
blink_count (int): Number of times the LED should blink.
|
||||
delay (float): Time in seconds for the LED to stay ON or OFF (default is 1 second).
|
||||
"""
|
||||
# GPIO setup
|
||||
GPIO.setwarnings(False)
|
||||
GPIO.setmode(GPIO.BCM) # Use BCM numbering
|
||||
GPIO.setup(pin, GPIO.OUT) # Set the specified pin as an output
|
||||
|
||||
try:
|
||||
for _ in range(blink_count):
|
||||
GPIO.output(pin, GPIO.HIGH) # Turn the LED on
|
||||
#print(f"LED on GPIO {pin} is ON")
|
||||
time.sleep(delay) # Wait for the specified delay
|
||||
GPIO.output(pin, GPIO.LOW) # Turn the LED off
|
||||
#print(f"LED on GPIO {pin} is OFF")
|
||||
time.sleep(delay) # Wait for the specified delay
|
||||
finally:
|
||||
GPIO.cleanup(pin) # Clean up the specific pin to reset its state
|
||||
print(f"GPIO {pin} cleaned up")
|
||||
|
||||
#get data from config
|
||||
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 {}
|
||||
|
||||
#Fonction pour mettre à jour le JSON de configuration
|
||||
def update_json_key(file_path, key, value):
|
||||
"""
|
||||
Updates a specific key in a JSON file with a new value.
|
||||
|
||||
:param file_path: Path to the JSON file.
|
||||
:param key: The key to update in the JSON file.
|
||||
:param value: The new value to assign to the key.
|
||||
"""
|
||||
try:
|
||||
# Load the existing data
|
||||
with open(file_path, "r") as file:
|
||||
data = json.load(file)
|
||||
|
||||
# Check if the key exists in the JSON file
|
||||
if key in data:
|
||||
data[key] = value # Update the key with the new value
|
||||
else:
|
||||
print(f"Key '{key}' not found in the JSON file.")
|
||||
return
|
||||
|
||||
# Write the updated data back to the file
|
||||
with open(file_path, "w") as file:
|
||||
json.dump(data, file, indent=2) # Use indent for pretty printing
|
||||
|
||||
print(f"updating '{key}' to '{value}'.")
|
||||
except Exception as e:
|
||||
print(f"Error updating the JSON file: {e}")
|
||||
|
||||
# Define the config file path
|
||||
config_file = '/var/www/nebuleair_pro_4g/config.json'
|
||||
|
||||
# Load the configuration data
|
||||
config = load_config(config_file)
|
||||
baudrate = config.get('SaraR4_baudrate', 115200) #baudrate du sara R4
|
||||
device_id = config.get('deviceID', '').upper() #device ID en maj
|
||||
bme_280_config = config.get('BME280/get_data_v2.py', False) #présence du BME280
|
||||
envea_cairsens= config.get('envea/read_value_v2.py', False)
|
||||
send_aircarto = config.get('send_aircarto', True) #envoi sur AirCarto (data.nebuleair.fr)
|
||||
send_uSpot = config.get('send_uSpot', False) #envoi sur MicroSpot ()
|
||||
selected_networkID = config.get('SARA_R4_neworkID', '')
|
||||
npm_5channel = config.get('NextPM_5channels', False) #5 canaux du NPM
|
||||
|
||||
modem_config_mode = config.get('modem_config_mode', False) #modem 4G en mode configuration
|
||||
|
||||
#update device id in the payload json
|
||||
payload_json["nebuleairid"] = device_id
|
||||
|
||||
# Skip execution if modem_config_mode is true
|
||||
if modem_config_mode:
|
||||
print("Modem 4G (SARA R4) is in config mode -> EXIT")
|
||||
sys.exit()
|
||||
|
||||
ser_sara = serial.Serial(
|
||||
port='/dev/ttyAMA2',
|
||||
baudrate=baudrate, #115200 ou 9600
|
||||
parity=serial.PARITY_NONE, #PARITY_NONE, PARITY_EVEN or PARITY_ODD
|
||||
stopbits=serial.STOPBITS_ONE,
|
||||
bytesize=serial.EIGHTBITS,
|
||||
timeout = 2
|
||||
)
|
||||
|
||||
def read_complete_response(serial_connection, timeout=2, end_of_response_timeout=2, wait_for_lines=None, debug=True):
|
||||
'''
|
||||
Fonction très importante !!!
|
||||
Reads the complete response from a serial connection and waits for specific lines.
|
||||
'''
|
||||
if wait_for_lines is None:
|
||||
wait_for_lines = [] # Default to an empty list if not provided
|
||||
|
||||
response = bytearray()
|
||||
serial_connection.timeout = timeout
|
||||
end_time = time.time() + end_of_response_timeout
|
||||
start_time = time.time()
|
||||
|
||||
while True:
|
||||
elapsed_time = time.time() - start_time # Time since function start
|
||||
if serial_connection.in_waiting > 0:
|
||||
data = serial_connection.read(serial_connection.in_waiting)
|
||||
response.extend(data)
|
||||
end_time = time.time() + end_of_response_timeout # Reset timeout on new data
|
||||
|
||||
# Decode and check for any target line
|
||||
decoded_response = response.decode('utf-8', errors='replace')
|
||||
for target_line in wait_for_lines:
|
||||
if target_line in decoded_response:
|
||||
if debug:
|
||||
print(f"[DEBUG] 🔎 Found target line: {target_line} (in {elapsed_time:.2f}s)")
|
||||
return decoded_response # Return response immediately if a target line is found
|
||||
elif time.time() > end_time:
|
||||
if debug:
|
||||
print(f"[DEBUG] Timeout reached. No more data received.")
|
||||
break
|
||||
time.sleep(0.1) # Short sleep to prevent busy waiting
|
||||
|
||||
# Final response and debug output
|
||||
total_elapsed_time = time.time() - start_time
|
||||
if debug:
|
||||
print(f"[DEBUG] ⏱️ elapsed time: {total_elapsed_time:.2f}s. ⏱️")
|
||||
# Check if the elapsed time exceeded 10 seconds
|
||||
if total_elapsed_time > 10 and debug:
|
||||
print(f"[ALERT] 🚨 The operation took too long 🚨")
|
||||
print(f'<span style="color: red;font-weight: bold;">[ALERT] ⚠️{total_elapsed_time:.2f}s⚠️</span>')
|
||||
|
||||
return response.decode('utf-8', errors='replace') # Return the full response if no target line is found
|
||||
|
||||
try:
|
||||
'''
|
||||
_ ___ ___ ____
|
||||
| | / _ \ / _ \| _ \
|
||||
| | | | | | | | | |_) |
|
||||
| |__| |_| | |_| | __/
|
||||
|_____\___/ \___/|_|
|
||||
|
||||
'''
|
||||
print('<h3>START LOOP</h3>')
|
||||
|
||||
#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(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)
|
||||
|
||||
#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),1) 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
|
||||
payload_csv[1] = PM25
|
||||
payload_csv[2] = PM10
|
||||
payload_csv[18] = npm_temp
|
||||
payload_csv[19] = npm_hum
|
||||
|
||||
#Add data to payload JSON
|
||||
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)})
|
||||
|
||||
|
||||
#NextPM 5 channels
|
||||
if npm_5channel:
|
||||
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)
|
||||
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)]
|
||||
|
||||
# Store averages in specific indices
|
||||
payload_csv[13] = averages[0] # Channel 1
|
||||
payload_csv[14] = averages[1] # Channel 2
|
||||
payload_csv[15] = averages[2] # Channel 3
|
||||
payload_csv[16] = averages[3] # Channel 4
|
||||
payload_csv[17] = averages[4] # Channel 5
|
||||
|
||||
#BME280
|
||||
if bme_280_config:
|
||||
print("➡️Getting BME280 values")
|
||||
cursor.execute("SELECT * FROM data_BME280 ORDER BY timestamp DESC LIMIT 1")
|
||||
last_row = cursor.fetchone()
|
||||
if last_row:
|
||||
print("SQLite DB last available row:", last_row)
|
||||
BME280_temperature = last_row[1]
|
||||
BME280_humidity = last_row[2]
|
||||
BME280_pressure = last_row[3]
|
||||
|
||||
#Add data to payload CSV
|
||||
payload_csv[3] = BME280_temperature
|
||||
payload_csv[4] = BME280_humidity
|
||||
payload_csv[5] = BME280_pressure
|
||||
|
||||
#Add data to payload JSON
|
||||
payload_json["sensordatavalues"].append({"value_type": "BME280_temperature", "value": str(BME280_temperature)})
|
||||
payload_json["sensordatavalues"].append({"value_type": "BME280_humidity", "value": str(BME280_humidity)})
|
||||
payload_json["sensordatavalues"].append({"value_type": "BME280_pressure", "value": str(BME280_pressure)})
|
||||
else:
|
||||
print("No data available in the database.")
|
||||
|
||||
#envea
|
||||
if envea_cairsens:
|
||||
print("➡️Getting envea cairsens values")
|
||||
cursor.execute("SELECT * FROM data_envea 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, ignoring 0 values
|
||||
averages = []
|
||||
for col in zip(*data_values): # Iterate column-wise
|
||||
filtered_values = [val for val in col if val != 0] # Remove zeros
|
||||
if filtered_values:
|
||||
avg = round(sum(filtered_values) / len(filtered_values)) # Compute average
|
||||
else:
|
||||
avg = 0 # If all values were zero, store 0
|
||||
averages.append(avg)
|
||||
|
||||
# Store averages in specific indices
|
||||
payload_csv[9] = averages[0] # envea_no2
|
||||
payload_csv[10] = averages[1] # envea_h2s
|
||||
payload_csv[11] = averages[2] # envea_nh3
|
||||
|
||||
#Add data to payload JSON
|
||||
payload_json["sensordatavalues"].append({"value_type": "CAIRSENS_NO2", "value": str(averages[0])})
|
||||
payload_json["sensordatavalues"].append({"value_type": "CAIRSENS_NO2", "value": str(averages[1])})
|
||||
payload_json["sensordatavalues"].append({"value_type": "CAIRSENS_NH3", "value": str(averages[2])})
|
||||
|
||||
|
||||
print("Verify SARA R4 connection")
|
||||
|
||||
# Getting the LTE Signal
|
||||
print("-> Getting LTE signal <-")
|
||||
ser_sara.write(b'AT+CSQ\r')
|
||||
response2 = read_complete_response(ser_sara, wait_for_lines=["OK"])
|
||||
print('<p class="text-danger-emphasis">')
|
||||
print(response2)
|
||||
print("</p>")
|
||||
match = re.search(r'\+CSQ:\s*(\d+),', response2)
|
||||
if match:
|
||||
signal_quality = int(match.group(1))
|
||||
payload_csv[12]=signal_quality
|
||||
time.sleep(0.1)
|
||||
|
||||
# On vérifie si le signal n'est pas à 99 pour déconnexion
|
||||
# si c'est le cas on essaie de se reconnecter
|
||||
if signal_quality == 99:
|
||||
print('<span style="color: red;font-weight: bold;">⚠️ATTENTION: Signal Quality indicates no signal (99)⚠️</span>')
|
||||
print("TRY TO RECONNECT:")
|
||||
command = f'AT+COPS=1,2,"{selected_networkID}"\r'
|
||||
ser_sara.write(command.encode('utf-8'))
|
||||
responseReconnect = read_complete_response(ser_sara, timeout=20, end_of_response_timeout=20)
|
||||
print('<p class="text-danger-emphasis">')
|
||||
print(responseReconnect)
|
||||
print("</p>")
|
||||
|
||||
print('🛑STOP LOOP🛑')
|
||||
print("<hr>")
|
||||
|
||||
#on arrete le script pas besoin de continuer
|
||||
sys.exit()
|
||||
else:
|
||||
print("Signal Quality:", signal_quality)
|
||||
|
||||
|
||||
'''
|
||||
SEND TO AIRCARTO
|
||||
'''
|
||||
# Write Data to saraR4
|
||||
# 1. Open sensordata_csv.json (with correct data size)
|
||||
csv_string = ','.join(str(value) if value is not None else '' for value in payload_csv)
|
||||
size_of_string = len(csv_string)
|
||||
print("Open JSON:")
|
||||
command = f'AT+UDWNFILE="sensordata_csv.json",{size_of_string}\r'
|
||||
ser_sara.write(command.encode('utf-8'))
|
||||
response_SARA_1 = read_complete_response(ser_sara, wait_for_lines=[">"], debug=False)
|
||||
print(response_SARA_1)
|
||||
time.sleep(1)
|
||||
|
||||
#2. Write to shell
|
||||
print("Write data to memory:")
|
||||
ser_sara.write(csv_string.encode())
|
||||
response_SARA_2 = read_complete_response(ser_sara, wait_for_lines=["OK"], debug=False)
|
||||
print(response_SARA_2)
|
||||
|
||||
#3. Send to endpoint (with device ID)
|
||||
print("Send data (POST REQUEST):")
|
||||
command= f'AT+UHTTPC={aircarto_profile_id},4,"/pro_4G/data.php?sensor_id={device_id}&datetime={influx_timestamp}","server_response.txt","sensordata_csv.json",4\r'
|
||||
ser_sara.write(command.encode('utf-8'))
|
||||
|
||||
response_SARA_3 = read_complete_response(ser_sara, timeout=5, end_of_response_timeout=120, wait_for_lines=["+UUHTTPCR", "+CME ERROR"], debug=True)
|
||||
|
||||
print('<p class="text-danger-emphasis">')
|
||||
print(response_SARA_3)
|
||||
print("</p>")
|
||||
|
||||
# si on recoit la réponse UHTTPCR
|
||||
if "+UUHTTPCR" in response_SARA_3:
|
||||
print("✅ Received +UUHTTPCR response.")
|
||||
|
||||
# Les types de réponse
|
||||
|
||||
# 1.La commande n'a pas fonctionné
|
||||
# +CME ERROR: No connection to phone
|
||||
# +CME ERROR: Operation not allowed
|
||||
|
||||
# 2.La commande fonctionne: elle renvoie un code
|
||||
# +UUHTTPCR: <profile_id>,<http_command>,<http_result>
|
||||
# <http_result>: 1 pour sucess et 0 pour fail
|
||||
# +UUHTTPCR: 0,4,1 -> OK
|
||||
# +UUHTTPCR: 0,4,0 -> error
|
||||
|
||||
# Split response into lines
|
||||
lines = response_SARA_3.strip().splitlines()
|
||||
|
||||
# 1.Vérifier si la réponse contient un message d'erreur CME
|
||||
if "+CME ERROR" in lines[-1]:
|
||||
print("*****")
|
||||
print('<span style="color: red;font-weight: bold;">ATTENTION: CME ERROR</span>')
|
||||
print("error:", lines[-1])
|
||||
print("*****")
|
||||
#update status
|
||||
update_json_key(config_file, "SARA_R4_network_status", "disconnected")
|
||||
|
||||
# Gestion de l'erreur spécifique
|
||||
if "No connection to phone" in lines[-1]:
|
||||
print("No connection to the phone. Retrying or reset may be required.")
|
||||
# Actions spécifiques pour ce type d'erreur (par exemple, réinitialiser ou tenter de reconnecter)
|
||||
# need to reconnect to network
|
||||
# and reset HTTP profile (AT+UHTTP=0) -> ne fonctionne pas..
|
||||
# tester un reset avec CFUN 15
|
||||
# 1.Reconnexion au réseau (AT+COPS)
|
||||
command = f'AT+COPS=1,2,"{selected_networkID}"\r'
|
||||
ser_sara.write(command.encode('utf-8'))
|
||||
responseReconnect = read_complete_response(ser_sara)
|
||||
print("Response reconnect:")
|
||||
print(responseReconnect)
|
||||
print("End response reconnect")
|
||||
|
||||
elif "Operation not allowed" in lines[-1]:
|
||||
print("Operation not allowed. This may require a different configuration.")
|
||||
# Actions spécifiques pour ce type d'erreur
|
||||
|
||||
# Clignotement LED rouge en cas d'erreur
|
||||
led_thread = Thread(target=blink_led, args=(24, 5, 0.5))
|
||||
led_thread.start()
|
||||
|
||||
else:
|
||||
# 2.Si la réponse contient une réponse HTTP valide
|
||||
# Extract HTTP response code from the last line
|
||||
# ATTENTION: lines[-1] renvoie l'avant dernière ligne et il peut y avoir un soucis avec le OK
|
||||
# rechercher plutot
|
||||
http_response = lines[-1] # "+UUHTTPCR: 0,4,0"
|
||||
parts = http_response.split(',')
|
||||
|
||||
# 2.1 code 0 (HTTP failed)
|
||||
if len(parts) == 3 and parts[-1] == '0': # The third value indicates success
|
||||
print("*****")
|
||||
print('<span style="color: red;font-weight: bold;">ATTENTION: HTTP operation failed</span>')
|
||||
update_json_key(config_file, "SARA_R4_network_status", "disconnected")
|
||||
print("*****")
|
||||
print("Blink red LED")
|
||||
# Run LED blinking in a separate thread
|
||||
led_thread = Thread(target=blink_led, args=(24, 5, 0.5))
|
||||
led_thread.start()
|
||||
|
||||
# Get error code
|
||||
print("Getting error code (11->Server connection error, 73->Secure socket connect error)")
|
||||
command = f'AT+UHTTPER={aircarto_profile_id}\r'
|
||||
ser_sara.write(command.encode('utf-8'))
|
||||
response_SARA_9 = read_complete_response(ser_sara, wait_for_lines=["OK"], debug=False)
|
||||
print('<p class="text-danger-emphasis">')
|
||||
print(response_SARA_9)
|
||||
print("</p>")
|
||||
|
||||
'''
|
||||
+UHTTPER: profile_id,error_class,error_code
|
||||
|
||||
error_class
|
||||
0 OK, no error
|
||||
3 HTTP Protocol error class
|
||||
10 Wrong HTTP API USAGE
|
||||
|
||||
error_code (for error_class 3)
|
||||
0 No error
|
||||
11 Server connection error
|
||||
73 Secure socket connect error
|
||||
'''
|
||||
|
||||
#Pas forcément un moyen de résoudre le soucis
|
||||
#print("resetting the URL (domain name):")
|
||||
#command = f'AT+UHTTP={aircarto_profile_id},1,"{url_nebuleair}"\r'
|
||||
#ser_sara.write(command.encode('utf-8'))
|
||||
#response_SARA_31 = read_complete_response(ser_sara)
|
||||
#print(response_SARA_31)
|
||||
|
||||
# 2.2 code 1 (HHTP succeded)
|
||||
else:
|
||||
# Si la commande HTTP a réussi
|
||||
print('<span class="badge text-bg-success">HTTP operation successful.</span>')
|
||||
update_json_key(config_file, "SARA_R4_network_status", "connected")
|
||||
print("Blink blue LED")
|
||||
led_thread = Thread(target=blink_led, args=(23, 5, 0.5))
|
||||
led_thread.start()
|
||||
#4. Read reply from server
|
||||
print("Reply from server:")
|
||||
ser_sara.write(b'AT+URDFILE="server_response.txt"\r')
|
||||
response_SARA_4 = read_complete_response(ser_sara, wait_for_lines=["OK"], debug=False)
|
||||
print('<p class="text-success">')
|
||||
print(response_SARA_4)
|
||||
print('</p>')
|
||||
|
||||
#Si non ne recoit pas de réponse UHTTPCR
|
||||
#on a peut etre une ERROR de type "+CME ERROR: No connection to phone"
|
||||
else:
|
||||
print('<span style="color: red;font-weight: bold;">No UUHTTPCR response</span>')
|
||||
print("Blink red LED")
|
||||
# Run LED blinking in a separate thread
|
||||
led_thread = Thread(target=blink_led, args=(24, 5, 0.5))
|
||||
led_thread.start()
|
||||
#Vérification de l'erreur
|
||||
print("Getting type of error")
|
||||
# Split the response into lines and search for "+CME ERROR:"
|
||||
lines2 = response_SARA_3.strip().splitlines()
|
||||
for line in lines2:
|
||||
if "+CME ERROR" in line:
|
||||
error_message = line.split("+CME ERROR:")[1].strip()
|
||||
print("*****")
|
||||
print('<span style="color: red;font-weight: bold;">⚠️ATTENTION: CME ERROR⚠️</span>')
|
||||
print(f"Error type: {error_message}")
|
||||
print("*****")
|
||||
# Handle "No connection to phone" error
|
||||
if error_message == "No connection to phone":
|
||||
print('<span style="color: orange;font-weight: bold;">📞Try reconnect to network📞</span>')
|
||||
#IMPORTANT!
|
||||
# Reconnexion au réseau (AT+COPS)
|
||||
#command = f'AT+COPS=1,2,{selected_networkID}\r'
|
||||
command = f'AT+COPS=0\r'
|
||||
ser_sara.write(command.encode('utf-8'))
|
||||
responseReconnect = read_complete_response(ser_sara, timeout=5, end_of_response_timeout=120, wait_for_lines=["OK", "+CME ERROR"], debug=True)
|
||||
print('<p class="text-danger-emphasis">')
|
||||
print(responseReconnect)
|
||||
print("</p>")
|
||||
# Handle "Operation not allowed" error
|
||||
if error_message == "Operation not allowed":
|
||||
print('<span style="color: orange;font-weight: bold;">❓Try Resetting the HTTP Profile❓</span>')
|
||||
command = f'AT+UHTTP={aircarto_profile_id},1,"data.nebuleair.fr"\r'
|
||||
ser_sara.write(command.encode('utf-8'))
|
||||
responseResetHTTP_profile = read_complete_response(ser_sara, timeout=5, end_of_response_timeout=5, wait_for_lines=["OK", "+CME ERROR"], debug=True)
|
||||
print('<p class="text-danger-emphasis">')
|
||||
print(responseResetHTTP_profile)
|
||||
print("</p>")
|
||||
|
||||
|
||||
|
||||
#5. empty json
|
||||
print("Empty SARA memory:")
|
||||
ser_sara.write(b'AT+UDELFILE="sensordata_csv.json"\r')
|
||||
response_SARA_5 = read_complete_response(ser_sara, wait_for_lines=["OK"], debug=False)
|
||||
print(response_SARA_5)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# Calculate and print the elapsed time
|
||||
elapsed_time = time.time() - start_time_script
|
||||
print(f"Elapsed time: {elapsed_time:.2f} seconds")
|
||||
print("<hr>")
|
||||
|
||||
except Exception as e:
|
||||
print("An error occurred:", e)
|
||||
traceback.print_exc() # This prints the full traceback
|
||||
Reference in New Issue
Block a user