421 lines
16 KiB
Python
Executable File
421 lines
16 KiB
Python
Executable File
"""
|
|
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
|
|
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
|
|
Same as NebuleAir wifi
|
|
{"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":"th_npm","value":"28.47 / 37.54"}
|
|
]}
|
|
|
|
|
|
"""
|
|
import board
|
|
import json
|
|
import serial
|
|
import time
|
|
import busio
|
|
import re
|
|
|
|
|
|
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"
|
|
payload = [None] * 20
|
|
|
|
# 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 {}
|
|
|
|
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)
|
|
|
|
# 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 capteur son
|
|
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', '')
|
|
|
|
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
|
|
)
|
|
|
|
ser_envea = serial.Serial(
|
|
port='/dev/ttyAMA4',
|
|
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):
|
|
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:
|
|
# 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']}"
|
|
payload[0] = data['PM1']
|
|
payload[1] = data['PM25']
|
|
payload[2] = data['PM10']
|
|
|
|
# Sonde BME280 connected
|
|
if bme_280_config:
|
|
#on récupère les infos du BME280 et on les ajoute au payload
|
|
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']}"
|
|
payload[3] = data['temp']
|
|
payload[4] = data['hum']
|
|
payload[5] = data['press']
|
|
|
|
# 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 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}"
|
|
payload[6] = data['avg_noise']
|
|
payload[7] = data['max_noise']
|
|
payload[8] = data['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.")
|
|
|
|
# Sondes Envea
|
|
if connected_envea_sondes:
|
|
# Pour chacune des sondes
|
|
for device in connected_envea_sondes:
|
|
print(f"Connected envea Sonde: {device.get('name', 'Unknown')} on port {device.get('port', 'Unknown')} and coefficient {device.get('coefficient', 'Unknown')} ")
|
|
ser_envea.write(b'\xFF\x02\x13\x30\x01\x02\x03\x04\x05\x06\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\x12\xAF\x88\x03')
|
|
data_envea = ser_envea.readline()
|
|
coefficient = device.get('coefficient', 'Unknown')
|
|
if len(data_envea) >= 20:
|
|
byte_20 = data_envea[19]
|
|
byte_20 = byte_20 * coefficient
|
|
payload[10] = byte_20
|
|
print(f"Data from envea {byte_20}")
|
|
else:
|
|
print("Données reçues insuffisantes pour extraire le 20ème octet.")
|
|
|
|
# Getting the LTE Signal
|
|
print("-> Getting signal <-")
|
|
ser_sara.write(b'AT+CSQ\r')
|
|
response2 = read_complete_response(ser_sara)
|
|
print("Response:")
|
|
print(response2)
|
|
print("<----")
|
|
match = re.search(r'\+CSQ:\s*(\d+),', response2)
|
|
if match:
|
|
signal_quality = match.group(1)
|
|
print("Signal Quality:", signal_quality)
|
|
payload[12]=signal_quality
|
|
time.sleep(1)
|
|
|
|
|
|
#Write Data to saraR4
|
|
#1. Open sensordata.json (with correct data size)
|
|
csv_string = ','.join(str(value) if value is not None else '' for value in payload)
|
|
size_of_string = len(csv_string)
|
|
command = f'AT+UDWNFILE="sensordata.json",{size_of_string}\r'
|
|
ser_sara.write((command + '\r').encode('utf-8'))
|
|
response_SARA_1 = read_complete_response(ser_sara)
|
|
#if need_to_log:
|
|
#print("Open JSON:")
|
|
#print(response_SARA_1)
|
|
time.sleep(1)
|
|
#2. Write to shell
|
|
ser_sara.write(csv_string.encode())
|
|
response_SARA_2 = read_complete_response(ser_sara)
|
|
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_sara.write((command + '\r').encode('utf-8'))
|
|
response_SARA_3 = read_complete_response(ser_sara)
|
|
|
|
print("Send data:")
|
|
print(response_SARA_3)
|
|
|
|
# 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 + '\r').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}"\r'
|
|
ser_sara.write((command + '\r').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
|
|
ser_sara.write(b'AT+URDFILE="server_response.txt"\r')
|
|
response_SARA_4 = read_complete_response(ser_sara)
|
|
if need_to_log:
|
|
print("Reply from server:")
|
|
print('<p class="text-success">')
|
|
print(response_SARA_4)
|
|
print('</p>')
|
|
|
|
#5. empty json
|
|
ser_sara.write(b'AT+UDELFILE="sensordata.json"\r')
|
|
response_SARA_5 = read_complete_response(ser_sara)
|
|
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("----------------------------------------")
|
|
print("----------------------------------------")
|
|
|
|
|
|
except Exception as e:
|
|
print(f"Error reading the JSON file: {e}") |