Files
nebuleair_pro_4g/loop/1_NPM/send_data.py
Your Name 838d6d7357 update
2025-01-23 11:35:51 +01:00

629 lines
24 KiB
Python
Executable File
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
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 !
{PM1},{PM25},{PM10},{temp},{hum},{press},{avg_noise},{max_noise},{min_noise},{envea_no2},{envea_h2s},{envea_o3},{4g_signal_quality}
0 -> PM1
1 -> PM25
2 -> PM10
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
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 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()
url_nebuleair="data.nebuleair.fr"
payload_csv = [None] * 20
payload_json = {
"nebuleairid": "82D25549434",
"software_version": "ModuleAirV2-V1-042022",
"sensordatavalues": [] # Empty list to start with
}
# 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
#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
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 ()
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
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):
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:
print(f"[DEBUG] 🔎Found target line: {wait_for_line}")
break
elif time.time() > end_time:
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
print(f"[DEBUG] ⏱️ elapsed time: {total_elapsed_time:.2f}s. ⏱️")
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)}"})
# 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} not found.")
except ValueError:
print("Error: File content is not valid numbers.")
# 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')
coefficient = device.get('coefficient', 'Unknown')
print(f"Connected envea Sonde: {name} on port {port} and coefficient {coefficient} ")
if name in serial_connections:
serial_connection = serial_connections[name]
try:
# Write data to the device
serial_connection.write(
b"\xFF\x02\x13\x30\x01\x02\x03\x04\x05\x06\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\x12\xAF\x88\x03"
)
# Read data from the device
data_envea = serial_connection.readline()
if len(data_envea) >= 20:
byte_20 = data_envea[19]
byte_20 = byte_20 * coefficient
# Update payload CSV based on device type
if name == "h2s":
payload_csv[10] = byte_20
payload_json["sensordatavalues"].append({"value_type": "CAIRSENS_H2S", "value": str(byte_20)})
if name == "no2":
payload_csv[9] = byte_20
payload_json["sensordatavalues"].append({"value_type": "CAIRSENS_NO2", "value": str(byte_20)})
if name == "o3":
payload_csv[11] = byte_20
payload_json["sensordatavalues"].append({"value_type": "CAIRSENS_O3", "value": str(byte_20)})
print(f"Data from envea {name}: {byte_20}")
else:
print(f"Données reçues insuffisantes pour {name} pour extraire le 20ème octet.")
except serial.SerialException as e:
print(f"Error communicating with {name}: {e}")
else:
print(f"No serial connection for {name}")
# 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=">")
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")
print(response_SARA_2)
#3. Send to endpoint (with device ID)
print("Send data (POST REQUEST):")
command= f'AT+UHTTPC=0,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=45, wait_for_line="+UUHTTPCR")
print('<p class="text-danger-emphasis">')
print(response_SARA_3)
print("</p>")
# Wait for the +UUHTTPCR response
#print("Waiting for +UUHTTPCR response...")
#response_received = False
#while not response_received:
# response_SARA_3 = read_complete_response(ser_sara, timeout=5)
# print(response_SARA_3.strip())
# if "+UUHTTPCR" in response_SARA_3:
# response_received = True
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 en cas d'erreur
GPIO.output(23, GPIO.LOW) # Éteindre la LED définitivement
for _ in range(4):
GPIO.output(23, GPIO.HIGH) # Allumer la LED
time.sleep(0.1)
GPIO.output(23, GPIO.LOW) # Éteindre la LED
time.sleep(0.1)
GPIO.output(23, GPIO.LOW) # Turn off the LED
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("resetting the URL (domain name):")
print("Turning off the blue LED...")
for _ in range(4): # Faire clignoter 4 fois
GPIO.output(23, GPIO.HIGH) # Allumer la LED
time.sleep(0.1) # Attendre 100 ms
GPIO.output(23, GPIO.LOW) # Éteindre la LED
time.sleep(0.1) # Attendre 100 ms
GPIO.output(23, GPIO.LOW) # Turn off the LED
command = f'AT+UHTTP=0,1,"{url_nebuleair}"\r'
ser_sara.write(command.encode('utf-8'))
response_SARA_31 = read_complete_response(ser_sara)
if need_to_log:
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("Turning on the blue LED...")
for _ in range(4): # Faire clignoter 4 fois
GPIO.output(23, GPIO.HIGH) # Allumer la LED
time.sleep(0.1) # Attendre 100 ms
GPIO.output(23, GPIO.LOW) # Éteindre la LED
time.sleep(0.1) # Attendre 100 ms
GPIO.output(23, GPIO.HIGH) # Turn on the LED
#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")
print('<p class="text-success">')
print(response_SARA_4)
print('</p>')
else:
print('<span style="color: red;font-weight: bold;">No UUHTTPCR response</span>')
#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")
print(response_SARA_5)
'''
SEND TO MICRO SPOT
'''
if send_uSpot:
print(">>>>>>>>")
print(">>>>>>>>")
print(">>>>>>>>")
print("SEND TO MICRO SPOT (HTTP):")
profile_id = 1
#step 4: set url (op_code = 1)
print("****")
print("SET URL")
command = f'AT+UHTTP={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")
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={profile_id},6,0\r'
ser_sara.write(command.encode('utf-8'))
response_SARA_5 = read_complete_response(ser_sara, wait_for_line="OK")
print(response_SARA_5)
time.sleep(1)
#step 4: set PORT (op_code = 5)
print("****")
print("SET PORT")
command = f'AT+UHTTP={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")
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=">")
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")
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={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")
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")
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