Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a68af89612 | ||
|
|
7045adc7a6 | ||
|
|
c062263b24 | ||
|
|
9f76e3b2de | ||
|
|
0ed18dd5c1 | ||
|
|
cf10d20db5 | ||
|
|
3f7d0c0816 | ||
|
|
47d76be5df |
72
SARA/reboot/hardware_reboot.py
Normal file
72
SARA/reboot/hardware_reboot.py
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
'''
|
||||||
|
____ _ ____ _
|
||||||
|
/ ___| / \ | _ \ / \
|
||||||
|
\___ \ / _ \ | |_) | / _ \
|
||||||
|
___) / ___ \| _ < / ___ \
|
||||||
|
|____/_/ \_\_| \_\/_/ \_\
|
||||||
|
|
||||||
|
sudo /usr/bin/python3 /var/www/nebuleair_pro_4g/SARA/reboot/hardware_reboot.py
|
||||||
|
|
||||||
|
Hardware reboot of the SARA R5 modem using GPIO 16 (GND control via transistor).
|
||||||
|
Cuts power for 3 seconds, then verifies modem is responsive with ATI command.
|
||||||
|
Returns JSON result for web interface.
|
||||||
|
'''
|
||||||
|
|
||||||
|
import RPi.GPIO as GPIO
|
||||||
|
import serial
|
||||||
|
import time
|
||||||
|
import json
|
||||||
|
import sqlite3
|
||||||
|
|
||||||
|
SARA_GND_GPIO = 16
|
||||||
|
|
||||||
|
# Load baudrate from config
|
||||||
|
conn = sqlite3.connect("/var/www/nebuleair_pro_4g/sqlite/sensors.db")
|
||||||
|
cursor = conn.cursor()
|
||||||
|
cursor.execute("SELECT value FROM config_table WHERE key='SaraR4_baudrate'")
|
||||||
|
row = cursor.fetchone()
|
||||||
|
baudrate = int(row[0]) if row else 115200
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
result = {
|
||||||
|
"reboot": False,
|
||||||
|
"modem_response": None,
|
||||||
|
"error": None
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Step 1: Cut GND (modem off)
|
||||||
|
GPIO.setmode(GPIO.BCM)
|
||||||
|
GPIO.setup(SARA_GND_GPIO, GPIO.OUT)
|
||||||
|
GPIO.output(SARA_GND_GPIO, GPIO.LOW)
|
||||||
|
time.sleep(3)
|
||||||
|
|
||||||
|
# Step 2: Restore GND (modem on)
|
||||||
|
GPIO.output(SARA_GND_GPIO, GPIO.HIGH)
|
||||||
|
time.sleep(5) # wait for modem boot
|
||||||
|
|
||||||
|
# Step 3: Check modem with ATI
|
||||||
|
ser = serial.Serial('/dev/ttyAMA2', baudrate=baudrate, timeout=3)
|
||||||
|
ser.reset_input_buffer()
|
||||||
|
|
||||||
|
for attempt in range(5):
|
||||||
|
ser.write(b'ATI\r')
|
||||||
|
time.sleep(1)
|
||||||
|
response = ser.read(ser.in_waiting or 1).decode('utf-8', errors='replace')
|
||||||
|
if "OK" in response:
|
||||||
|
result["reboot"] = True
|
||||||
|
result["modem_response"] = response.strip()
|
||||||
|
break
|
||||||
|
time.sleep(2)
|
||||||
|
else:
|
||||||
|
result["error"] = "Modem ne repond pas apres le redemarrage"
|
||||||
|
|
||||||
|
ser.close()
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
result["error"] = str(e)
|
||||||
|
|
||||||
|
finally:
|
||||||
|
GPIO.cleanup(SARA_GND_GPIO)
|
||||||
|
|
||||||
|
print(json.dumps(result))
|
||||||
@@ -11,12 +11,17 @@ Script that starts at the boot of the RPI (with cron)
|
|||||||
|
|
||||||
/usr/bin/python3 /var/www/nebuleair_pro_4g/SARA/reboot/start.py
|
/usr/bin/python3 /var/www/nebuleair_pro_4g/SARA/reboot/start.py
|
||||||
|
|
||||||
|
Roles:
|
||||||
|
1. Reset modem_config_mode to 0 (boot safety)
|
||||||
|
2. Power on SARA modem via GPIO 16
|
||||||
|
3. Detect modem model (SARA R4 or R5) and save to SQLite
|
||||||
|
|
||||||
|
All other configuration (AirCarto URL, uSpot HTTPS, PDP setup, geolocation)
|
||||||
|
is handled by the main loop script: loop/SARA_send_data_v2.py
|
||||||
'''
|
'''
|
||||||
import serial
|
import serial
|
||||||
import RPi.GPIO as GPIO
|
import RPi.GPIO as GPIO
|
||||||
import time
|
import time
|
||||||
import sys
|
|
||||||
import json
|
|
||||||
import re
|
import re
|
||||||
import sqlite3
|
import sqlite3
|
||||||
import traceback
|
import traceback
|
||||||
@@ -24,72 +29,29 @@ import traceback
|
|||||||
|
|
||||||
#GPIO
|
#GPIO
|
||||||
SARA_power_GPIO = 16
|
SARA_power_GPIO = 16
|
||||||
SARA_ON_GPIO = 20
|
|
||||||
|
|
||||||
GPIO.setmode(GPIO.BCM) # Use BCM numbering
|
GPIO.setmode(GPIO.BCM) # Use BCM numbering
|
||||||
GPIO.setup(SARA_power_GPIO, GPIO.OUT) # Set GPIO17 as an output
|
GPIO.setup(SARA_power_GPIO, GPIO.OUT)
|
||||||
|
|
||||||
# database connection
|
# database connection
|
||||||
conn = sqlite3.connect("/var/www/nebuleair_pro_4g/sqlite/sensors.db")
|
conn = sqlite3.connect("/var/www/nebuleair_pro_4g/sqlite/sensors.db")
|
||||||
cursor = conn.cursor()
|
cursor = conn.cursor()
|
||||||
|
|
||||||
#get config data from SQLite table
|
|
||||||
def load_config_sqlite():
|
|
||||||
"""
|
|
||||||
Load configuration data from SQLite config table
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
dict: Configuration data with proper type conversion
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
|
|
||||||
# Query the config table
|
|
||||||
cursor.execute("SELECT key, value, type FROM config_table")
|
|
||||||
rows = cursor.fetchall()
|
|
||||||
|
|
||||||
# Create config dictionary
|
|
||||||
config_data = {}
|
|
||||||
for key, value, type_name in rows:
|
|
||||||
# Convert value based on its type
|
|
||||||
if type_name == 'bool':
|
|
||||||
config_data[key] = value == '1' or value == 'true'
|
|
||||||
elif type_name == 'int':
|
|
||||||
config_data[key] = int(value)
|
|
||||||
elif type_name == 'float':
|
|
||||||
config_data[key] = float(value)
|
|
||||||
else:
|
|
||||||
config_data[key] = value
|
|
||||||
|
|
||||||
return config_data
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Error loading config from SQLite: {e}")
|
|
||||||
return {}
|
|
||||||
|
|
||||||
def update_sqlite_config(key, value):
|
def update_sqlite_config(key, value):
|
||||||
"""
|
"""
|
||||||
Updates a specific key in the SQLite config_table with a new value.
|
Updates a specific key in the SQLite config_table with a new value.
|
||||||
|
|
||||||
:param key: The key to update in the config_table.
|
|
||||||
:param value: The new value to assign to the key.
|
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
|
|
||||||
# Check if the key exists and get its type
|
|
||||||
cursor.execute("SELECT type FROM config_table WHERE key = ?", (key,))
|
cursor.execute("SELECT type FROM config_table WHERE key = ?", (key,))
|
||||||
result = cursor.fetchone()
|
result = cursor.fetchone()
|
||||||
|
|
||||||
if result is None:
|
if result is None:
|
||||||
print(f"Key '{key}' not found in the config_table.")
|
print(f"Key '{key}' not found in the config_table.")
|
||||||
conn.close()
|
|
||||||
return
|
return
|
||||||
|
|
||||||
# Get the type of the value from the database
|
|
||||||
value_type = result[0]
|
value_type = result[0]
|
||||||
|
|
||||||
# Convert the value to the appropriate string representation based on its type
|
|
||||||
if value_type == 'bool':
|
if value_type == 'bool':
|
||||||
# Convert Python boolean or string 'true'/'false' to '1'/'0'
|
|
||||||
if isinstance(value, bool):
|
if isinstance(value, bool):
|
||||||
str_value = '1' if value else '0'
|
str_value = '1' if value else '0'
|
||||||
else:
|
else:
|
||||||
@@ -101,28 +63,22 @@ def update_sqlite_config(key, value):
|
|||||||
else:
|
else:
|
||||||
str_value = str(value)
|
str_value = str(value)
|
||||||
|
|
||||||
# Update the value in the database
|
|
||||||
cursor.execute("UPDATE config_table SET value = ? WHERE key = ?", (str_value, key))
|
cursor.execute("UPDATE config_table SET value = ? WHERE key = ?", (str_value, key))
|
||||||
|
|
||||||
# Commit the changes and close the connection
|
|
||||||
conn.commit()
|
conn.commit()
|
||||||
|
|
||||||
print(f"💾 Updated '{key}' to '{value}' in database.")
|
print(f"Updated '{key}' to '{value}' in database.")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error updating the SQLite database: {e}")
|
print(f"Error updating the SQLite database: {e}")
|
||||||
|
|
||||||
#Load config
|
# Load baudrate from config
|
||||||
config = load_config_sqlite()
|
cursor.execute("SELECT value FROM config_table WHERE key = 'SaraR4_baudrate'")
|
||||||
#config
|
row = cursor.fetchone()
|
||||||
baudrate = config.get('SaraR4_baudrate', 115200) #baudrate du sara R4
|
baudrate = int(row[0]) if row else 115200
|
||||||
device_id = config.get('deviceID', '').upper() #device ID en maj
|
|
||||||
|
|
||||||
sara_r5_DPD_setup = False
|
|
||||||
|
|
||||||
ser_sara = serial.Serial(
|
ser_sara = serial.Serial(
|
||||||
port='/dev/ttyAMA2',
|
port='/dev/ttyAMA2',
|
||||||
baudrate=baudrate, #115200 ou 9600
|
baudrate=baudrate,
|
||||||
parity=serial.PARITY_NONE, #PARITY_NONE, PARITY_EVEN or PARITY_ODD
|
parity=serial.PARITY_NONE,
|
||||||
stopbits=serial.STOPBITS_ONE,
|
stopbits=serial.STOPBITS_ONE,
|
||||||
bytesize=serial.EIGHTBITS,
|
bytesize=serial.EIGHTBITS,
|
||||||
timeout = 2
|
timeout = 2
|
||||||
@@ -130,11 +86,10 @@ ser_sara = serial.Serial(
|
|||||||
|
|
||||||
def read_complete_response(serial_connection, timeout=2, end_of_response_timeout=2, wait_for_lines=None, debug=True):
|
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.
|
Reads the complete response from a serial connection and waits for specific lines.
|
||||||
'''
|
'''
|
||||||
if wait_for_lines is None:
|
if wait_for_lines is None:
|
||||||
wait_for_lines = [] # Default to an empty list if not provided
|
wait_for_lines = []
|
||||||
|
|
||||||
response = bytearray()
|
response = bytearray()
|
||||||
serial_connection.timeout = timeout
|
serial_connection.timeout = timeout
|
||||||
@@ -142,259 +97,72 @@ def read_complete_response(serial_connection, timeout=2, end_of_response_timeout
|
|||||||
start_time = time.time()
|
start_time = time.time()
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
elapsed_time = time.time() - start_time # Time since function start
|
elapsed_time = time.time() - start_time
|
||||||
if serial_connection.in_waiting > 0:
|
if serial_connection.in_waiting > 0:
|
||||||
data = serial_connection.read(serial_connection.in_waiting)
|
data = serial_connection.read(serial_connection.in_waiting)
|
||||||
response.extend(data)
|
response.extend(data)
|
||||||
end_time = time.time() + end_of_response_timeout # Reset timeout on new data
|
end_time = time.time() + end_of_response_timeout
|
||||||
|
|
||||||
# Decode and check for any target line
|
|
||||||
decoded_response = response.decode('utf-8', errors='replace')
|
decoded_response = response.decode('utf-8', errors='replace')
|
||||||
for target_line in wait_for_lines:
|
for target_line in wait_for_lines:
|
||||||
if target_line in decoded_response:
|
if target_line in decoded_response:
|
||||||
if debug:
|
if debug:
|
||||||
print(f"[DEBUG] 🔎 Found target line: {target_line} (in {elapsed_time:.2f}s)")
|
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
|
return decoded_response
|
||||||
elif time.time() > end_time:
|
elif time.time() > end_time:
|
||||||
if debug:
|
if debug:
|
||||||
print(f"[DEBUG] Timeout reached. No more data received.")
|
print(f"[DEBUG] Timeout reached. No more data received.")
|
||||||
break
|
break
|
||||||
time.sleep(0.1) # Short sleep to prevent busy waiting
|
time.sleep(0.1)
|
||||||
|
|
||||||
# Final response and debug output
|
|
||||||
total_elapsed_time = time.time() - start_time
|
total_elapsed_time = time.time() - start_time
|
||||||
if debug:
|
if debug:
|
||||||
print(f"[DEBUG] ⏱️ elapsed time: {total_elapsed_time:.2f}s. ⏱️")
|
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:
|
if total_elapsed_time > 10 and debug:
|
||||||
print(f"[ALERT] 🚨 The operation took too long 🚨")
|
print(f"[ALERT] The operation took too long ({total_elapsed_time:.2f}s)")
|
||||||
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
|
return response.decode('utf-8', errors='replace')
|
||||||
|
|
||||||
try:
|
try:
|
||||||
print('<h3>Start reboot python script</h3>')
|
print('<h3>Start reboot python script</h3>')
|
||||||
|
|
||||||
#First we need to power on the module (if connected to mosfet via gpio16)
|
# 1. Reset modem_config_mode at boot to prevent capteur from staying stuck in config mode
|
||||||
|
cursor.execute("UPDATE config_table SET value = '0' WHERE key = 'modem_config_mode'")
|
||||||
|
conn.commit()
|
||||||
|
print("modem_config_mode reset to 0 (boot safety)")
|
||||||
|
|
||||||
|
# 2. Power on the module (MOSFET via GPIO 16)
|
||||||
GPIO.output(SARA_power_GPIO, GPIO.HIGH)
|
GPIO.output(SARA_power_GPIO, GPIO.HIGH)
|
||||||
time.sleep(5)
|
time.sleep(5)
|
||||||
|
|
||||||
#check modem status
|
# 3. Detect modem model
|
||||||
#Attention:
|
# SARA R4 response: Manufacturer: u-blox Model: SARA-R410M-02B
|
||||||
# SARA R4 response: Manufacturer: u-blox Model: SARA-R410M-02B
|
# SARA R5 response: SARA-R500S-01B-00
|
||||||
# SArA R5 response: SARA-R500S-01B-00
|
print("Check SARA Status")
|
||||||
print("⚙️Check SARA Status")
|
|
||||||
command = f'ATI\r'
|
command = f'ATI\r'
|
||||||
ser_sara.write(command.encode('utf-8'))
|
ser_sara.write(command.encode('utf-8'))
|
||||||
response_SARA_ATI = read_complete_response(ser_sara, wait_for_lines=["IMEI"])
|
response_SARA_ATI = read_complete_response(ser_sara, wait_for_lines=["IMEI"])
|
||||||
print(response_SARA_ATI)
|
print(response_SARA_ATI)
|
||||||
|
|
||||||
# Check for SARA model with more robust regex
|
|
||||||
model = "Unknown"
|
model = "Unknown"
|
||||||
if "SARA-R410M" in response_SARA_ATI:
|
if "SARA-R410M" in response_SARA_ATI:
|
||||||
model = "SARA-R410M"
|
model = "SARA-R410M"
|
||||||
print("📱 Detected SARA R4 modem")
|
print("Detected SARA R4 modem")
|
||||||
elif "SARA-R500" in response_SARA_ATI:
|
elif "SARA-R500" in response_SARA_ATI:
|
||||||
model = "SARA-R500"
|
model = "SARA-R500"
|
||||||
print("📱 Detected SARA R5 modem")
|
print("Detected SARA R5 modem")
|
||||||
sara_r5_DPD_setup = True
|
|
||||||
else:
|
else:
|
||||||
# Fallback to regex match if direct string match fails
|
|
||||||
match = re.search(r"Model:\s*([A-Za-z0-9\-]+)", response_SARA_ATI)
|
match = re.search(r"Model:\s*([A-Za-z0-9\-]+)", response_SARA_ATI)
|
||||||
if match:
|
if match:
|
||||||
model = match.group(1).strip()
|
model = match.group(1).strip()
|
||||||
else:
|
else:
|
||||||
model = "Unknown"
|
print("Could not identify modem model")
|
||||||
print("⚠️ Could not identify modem model")
|
|
||||||
|
|
||||||
print(f"🔍 Model: {model}")
|
print(f"Model: {model}")
|
||||||
update_sqlite_config("modem_version", model)
|
update_sqlite_config("modem_version", model)
|
||||||
time.sleep(1)
|
|
||||||
|
|
||||||
'''
|
print('<h3>Boot script complete. Modem ready for main loop.</h3>')
|
||||||
AIRCARTO
|
|
||||||
'''
|
|
||||||
# 1. Set AIRCARTO URL (profile id = 0)
|
|
||||||
print('➡️Set aircarto URL')
|
|
||||||
aircarto_profile_id = 0
|
|
||||||
aircarto_url="data.nebuleair.fr"
|
|
||||||
command = f'AT+UHTTP={aircarto_profile_id},1,"{aircarto_url}"\r'
|
|
||||||
ser_sara.write(command.encode('utf-8'))
|
|
||||||
response_SARA_1 = read_complete_response(ser_sara, wait_for_lines=["OK"])
|
|
||||||
print(response_SARA_1)
|
|
||||||
time.sleep(1)
|
|
||||||
|
|
||||||
'''
|
|
||||||
uSpot
|
|
||||||
'''
|
|
||||||
print("➡️➡️Set uSpot URL with SSL")
|
|
||||||
|
|
||||||
security_profile_id = 1
|
|
||||||
uSpot_profile_id = 1
|
|
||||||
uSpot_url="api-prod.uspot.probesys.net"
|
|
||||||
|
|
||||||
|
|
||||||
#step 1: import the certificate
|
|
||||||
print("➡️ import certificate")
|
|
||||||
certificate_name = "e6"
|
|
||||||
with open("/var/www/nebuleair_pro_4g/SARA/SSL/certificate/e6.pem", "rb") as cert_file:
|
|
||||||
certificate = cert_file.read()
|
|
||||||
size_of_string = len(certificate)
|
|
||||||
|
|
||||||
# AT+USECMNG=0,<type>,<internal_name>,<data_size>
|
|
||||||
# type-> 0 -> trusted root CA
|
|
||||||
command = f'AT+USECMNG=0,0,"{certificate_name}",{size_of_string}\r'
|
|
||||||
ser_sara.write((command + '\r').encode('utf-8'))
|
|
||||||
response_SARA_1 = read_complete_response(ser_sara)
|
|
||||||
print(response_SARA_1)
|
|
||||||
|
|
||||||
time.sleep(0.5)
|
|
||||||
|
|
||||||
print("➡️ add certificate")
|
|
||||||
ser_sara.write(certificate)
|
|
||||||
response_SARA_2 = read_complete_response(ser_sara)
|
|
||||||
print(response_SARA_2)
|
|
||||||
|
|
||||||
time.sleep(0.5)
|
|
||||||
|
|
||||||
# op_code: 0 -> certificate validation level
|
|
||||||
# param_val : 0 -> Level 0 No validation; 1-> Level 1 Root certificate validation
|
|
||||||
print("➡️Set the security profile (params)")
|
|
||||||
certification_level=0
|
|
||||||
command = f'AT+USECPRF={security_profile_id},0,{certification_level}\r'
|
|
||||||
ser_sara.write((command + '\r').encode('utf-8'))
|
|
||||||
response_SARA_5b = read_complete_response(ser_sara, wait_for_lines=["OK"])
|
|
||||||
print(response_SARA_5b)
|
|
||||||
time.sleep(0.5)
|
|
||||||
|
|
||||||
# op_code: 1 -> minimum SSL/TLS version
|
|
||||||
# param_val : 0 -> any; server can use any version for the connection; 1-> LSv1.0; 2->TLSv1.1; 3->TLSv1.2;
|
|
||||||
print("➡️Set the security profile (params)")
|
|
||||||
minimum_SSL_version = 0
|
|
||||||
command = f'AT+USECPRF={security_profile_id},1,{minimum_SSL_version}\r'
|
|
||||||
ser_sara.write((command + '\r').encode('utf-8'))
|
|
||||||
response_SARA_5bb = read_complete_response(ser_sara, wait_for_lines=["OK"])
|
|
||||||
print(response_SARA_5bb)
|
|
||||||
time.sleep(0.5)
|
|
||||||
|
|
||||||
#op_code: 2 -> legacy cipher suite selection
|
|
||||||
# 0 (factory-programmed value): a list of default cipher suites is proposed at the beginning of handshake process, and a cipher suite will be negotiated among the cipher suites proposed in the list.
|
|
||||||
print("➡️Set cipher")
|
|
||||||
cipher_suite = 0
|
|
||||||
command = f'AT+USECPRF={security_profile_id},2,{cipher_suite}\r'
|
|
||||||
ser_sara.write((command + '\r').encode('utf-8'))
|
|
||||||
response_SARA_5cc = read_complete_response(ser_sara, wait_for_lines=["OK"])
|
|
||||||
print(response_SARA_5cc)
|
|
||||||
time.sleep(0.5)
|
|
||||||
|
|
||||||
# op_code: 3 -> trusted root certificate internal name
|
|
||||||
print("➡️Set the security profile (choose cert)")
|
|
||||||
command = f'AT+USECPRF={security_profile_id},3,"{certificate_name}"\r'
|
|
||||||
ser_sara.write((command + '\r').encode('utf-8'))
|
|
||||||
response_SARA_5c = read_complete_response(ser_sara, wait_for_lines=["OK"])
|
|
||||||
print(response_SARA_5c)
|
|
||||||
time.sleep(0.5)
|
|
||||||
|
|
||||||
# op_code: 10 -> SNI (server name indication)
|
|
||||||
print("➡️Set the SNI")
|
|
||||||
command = f'AT+USECPRF={security_profile_id},10,"{uSpot_url}"\r'
|
|
||||||
ser_sara.write((command + '\r').encode('utf-8'))
|
|
||||||
response_SARA_5cf = read_complete_response(ser_sara, wait_for_lines=["OK"])
|
|
||||||
print(response_SARA_5cf)
|
|
||||||
time.sleep(0.5)
|
|
||||||
|
|
||||||
#step 4: set url (op_code = 1)
|
|
||||||
print("➡️SET URL")
|
|
||||||
command = f'AT+UHTTP={uSpot_profile_id},1,"{uSpot_url}"\r'
|
|
||||||
ser_sara.write((command + '\r').encode('utf-8'))
|
|
||||||
response_SARA_5 = read_complete_response(ser_sara, wait_for_lines=["OK"])
|
|
||||||
print(response_SARA_5)
|
|
||||||
time.sleep(1)
|
|
||||||
|
|
||||||
#step 4: set PORT (op_code = 5)
|
|
||||||
print("➡️SET PORT")
|
|
||||||
port = 443
|
|
||||||
command = f'AT+UHTTP={uSpot_profile_id},5,{port}\r'
|
|
||||||
ser_sara.write((command + '\r').encode('utf-8'))
|
|
||||||
response_SARA_55 = read_complete_response(ser_sara, wait_for_lines=["OK"])
|
|
||||||
print(response_SARA_55)
|
|
||||||
time.sleep(1)
|
|
||||||
|
|
||||||
#step 4: set url to SSL (op_code = 6) (http_secure = 1 for HTTPS)(USECMNG_PROFILE = 2)
|
|
||||||
print("➡️SET SSL")
|
|
||||||
http_secure = 1
|
|
||||||
command = f'AT+UHTTP={uSpot_profile_id},6,{http_secure},{security_profile_id}\r'
|
|
||||||
|
|
||||||
ser_sara.write(command.encode('utf-8'))
|
|
||||||
response_SARA_5fg = read_complete_response(ser_sara, wait_for_lines=["OK"])
|
|
||||||
print(response_SARA_5fg)
|
|
||||||
time.sleep(1)
|
|
||||||
|
|
||||||
|
|
||||||
'''
|
|
||||||
SARA R5
|
|
||||||
'''
|
|
||||||
|
|
||||||
if sara_r5_DPD_setup:
|
|
||||||
print("➡️➡️SARA R5 PDP SETUP")
|
|
||||||
# 2. Activate PDP context 1
|
|
||||||
print('➡️Activate PDP context 1')
|
|
||||||
command = f'AT+CGACT=1,1\r'
|
|
||||||
ser_sara.write(command.encode('utf-8'))
|
|
||||||
response_SARA_2 = read_complete_response(ser_sara, wait_for_lines=["OK"])
|
|
||||||
print(response_SARA_2, end="")
|
|
||||||
time.sleep(1)
|
|
||||||
|
|
||||||
# 2. Set the PDP type
|
|
||||||
print('➡️Set the PDP type to IPv4 referring to the outputof the +CGDCONT read command')
|
|
||||||
command = f'AT+UPSD=0,0,0\r'
|
|
||||||
ser_sara.write(command.encode('utf-8'))
|
|
||||||
response_SARA_3 = read_complete_response(ser_sara, wait_for_lines=["OK"])
|
|
||||||
print(response_SARA_3, end="")
|
|
||||||
time.sleep(1)
|
|
||||||
|
|
||||||
# 2. Profile #0 is mapped on CID=1.
|
|
||||||
print('➡️Profile #0 is mapped on CID=1.')
|
|
||||||
command = f'AT+UPSD=0,100,1\r'
|
|
||||||
ser_sara.write(command.encode('utf-8'))
|
|
||||||
response_SARA_3 = read_complete_response(ser_sara, wait_for_lines=["OK"])
|
|
||||||
print(response_SARA_3, end="")
|
|
||||||
time.sleep(1)
|
|
||||||
|
|
||||||
# 2. Set the PDP type
|
|
||||||
print('➡️Activate the PSD profile #0: the IPv4 address is already assigned by the network.')
|
|
||||||
command = f'AT+UPSDA=0,3\r'
|
|
||||||
ser_sara.write(command.encode('utf-8'))
|
|
||||||
response_SARA_3 = read_complete_response(ser_sara, wait_for_lines=["OK","+UUPSDA"])
|
|
||||||
print(response_SARA_3, end="")
|
|
||||||
time.sleep(1)
|
|
||||||
|
|
||||||
|
|
||||||
#3. Get localisation (CellLocate)
|
|
||||||
mode = 2 #single shot position
|
|
||||||
sensor = 2 #use cellular CellLocate® location information
|
|
||||||
response_type = 0
|
|
||||||
timeout_s = 2
|
|
||||||
accuracy_m = 1
|
|
||||||
command = f'AT+ULOC={mode},{sensor},{response_type},{timeout_s},{accuracy_m}\r'
|
|
||||||
ser_sara.write((command + '\r').encode('utf-8'))
|
|
||||||
response_SARA_3 = read_complete_response(ser_sara, wait_for_lines=["+UULOC"])
|
|
||||||
print(response_SARA_3)
|
|
||||||
|
|
||||||
match = re.search(r"\+UULOC: \d{2}/\d{2}/\d{4},\d{2}:\d{2}:\d{2}\.\d{3},([-+]?\d+\.\d+),([-+]?\d+\.\d+)", response_SARA_3)
|
|
||||||
if match:
|
|
||||||
latitude = match.group(1)
|
|
||||||
longitude = match.group(2)
|
|
||||||
print(f"📍 Latitude: {latitude}, Longitude: {longitude}")
|
|
||||||
#update sqlite table
|
|
||||||
update_sqlite_config("latitude_raw", float(latitude))
|
|
||||||
update_sqlite_config("longitude_raw", float(longitude))
|
|
||||||
else:
|
|
||||||
print("❌ Failed to extract coordinates.")
|
|
||||||
|
|
||||||
|
|
||||||
time.sleep(1)
|
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print("An error occurred:", e)
|
print("An error occurred:", e)
|
||||||
traceback.print_exc() # This prints the full traceback
|
traceback.print_exc()
|
||||||
@@ -1,5 +1,73 @@
|
|||||||
{
|
{
|
||||||
"versions": [
|
"versions": [
|
||||||
|
{
|
||||||
|
"version": "1.6.4",
|
||||||
|
"date": "2026-04-02",
|
||||||
|
"changes": {
|
||||||
|
"features": [
|
||||||
|
"Page modem: boutons Activer/Desactiver LED status connexion PCB (AT+UGPIOC=16,2 / AT+UGPIOC=16,255)"
|
||||||
|
],
|
||||||
|
"improvements": [
|
||||||
|
"Page modem: messages de progression en 3 etapes pendant le reset hardware (coupure, redemarrage, test connexion)",
|
||||||
|
"Page modem: bouton reset hardware desactive pendant l'operation pour eviter les doubles clics"
|
||||||
|
],
|
||||||
|
"fixes": [],
|
||||||
|
"compatibility": []
|
||||||
|
},
|
||||||
|
"notes": "Le reset hardware affiche maintenant les etapes en temps reel (~20s). Deux nouveaux boutons permettent de controler la LED bleue du PCB qui indique l'etat de la connexion reseau du modem."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"version": "1.6.3",
|
||||||
|
"date": "2026-04-01",
|
||||||
|
"changes": {
|
||||||
|
"features": [
|
||||||
|
"Page logs: bouton Auto-refresh pour suivre les logs SARA en temps reel (polling 3s)"
|
||||||
|
],
|
||||||
|
"improvements": [
|
||||||
|
"Service SARA: ajout flag python3 -u (unbuffered) pour ecriture immediate des logs dans le fichier"
|
||||||
|
],
|
||||||
|
"fixes": [],
|
||||||
|
"compatibility": [
|
||||||
|
"Necessite re-execution de setup_services.sh pour activer le mode unbuffered (optionnel, pas d'impact si non fait)"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"notes": "Les logs SARA sont maintenant visibles en temps reel sur la page logs grace au mode unbuffered Python et au rafraichissement automatique. Aucun impact sur les anciennes installations qui ne relancent pas setup_services.sh."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"version": "1.6.2",
|
||||||
|
"date": "2026-03-27",
|
||||||
|
"changes": {
|
||||||
|
"features": [],
|
||||||
|
"improvements": [
|
||||||
|
"Simplification du script de boot SARA (start.py): suppression config AirCarto, uSpot/SSL, PDP et geolocalisation",
|
||||||
|
"La configuration modem est desormais entierement geree par le script principal (SARA_send_data_v2.py)"
|
||||||
|
],
|
||||||
|
"fixes": [],
|
||||||
|
"compatibility": []
|
||||||
|
},
|
||||||
|
"notes": "Le script de boot ne fait plus que 3 choses: reset modem_config_mode, alimentation modem GPIO 16, detection modele R4/R5. Toute la configuration (URLs, certificats, PDP, geolocalisation) est deja geree par le script principal qui tourne chaque minute avec gestion d'erreur et retry."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"version": "1.6.1",
|
||||||
|
"date": "2026-03-19",
|
||||||
|
"changes": {
|
||||||
|
"features": [
|
||||||
|
"Sonometre NSRT MK4: detection deconnexion avec message explicite (page capteurs + self-test)",
|
||||||
|
"Colonne noise_status dans data_NOISE (0x00=OK, 0xFF=deconnecte)",
|
||||||
|
"ERR_NOISE (bit 5, byte 66) dans error_flags UDP quand sonometre deconnecte"
|
||||||
|
],
|
||||||
|
"improvements": [
|
||||||
|
"Script NSRT_mk4_get_data.py ecrit en base meme si capteur deconnecte (valeurs a 0, noise_status=0xFF)",
|
||||||
|
"Script read.py: message d'erreur clair au lieu de l'exception Python brute",
|
||||||
|
"Self-test: affiche 'Capteur deconnecte — verifiez le cablage USB' au lieu de l'erreur technique"
|
||||||
|
],
|
||||||
|
"fixes": [],
|
||||||
|
"compatibility": [
|
||||||
|
"Migration automatique: colonne noise_status ajoutee via set_config.py lors du firmware update"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"notes": "Gestion de la deconnexion du sonometre NSRT MK4 alignee sur le modele NPM: ecriture en base avec status d'erreur, flag ERR_NOISE dans la payload UDP, et messages utilisateur explicites sur l'interface web."
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"version": "1.6.0",
|
"version": "1.6.0",
|
||||||
"date": "2026-03-18",
|
"date": "2026-03-18",
|
||||||
|
|||||||
@@ -412,7 +412,10 @@ async function selfTestSequence() {
|
|||||||
addSelfTestLog(`Noise response: ${JSON.stringify(noiseResult)}`);
|
addSelfTestLog(`Noise response: ${JSON.stringify(noiseResult)}`);
|
||||||
|
|
||||||
if (noiseResult.error) {
|
if (noiseResult.error) {
|
||||||
updateTestStatus(sensor.id, 'Failed', noiseResult.error, 'bg-danger');
|
const noiseMsg = noiseResult.disconnected
|
||||||
|
? 'Capteur déconnecté — vérifiez le câblage USB'
|
||||||
|
: noiseResult.error;
|
||||||
|
updateTestStatus(sensor.id, 'Failed', noiseMsg, 'bg-danger');
|
||||||
testsFailed++;
|
testsFailed++;
|
||||||
} else if (noiseResult.LEQ > 0 && noiseResult.dBA > 0) {
|
} else if (noiseResult.LEQ > 0 && noiseResult.dBA > 0) {
|
||||||
updateTestStatus(sensor.id, 'Passed', `LEQ: ${noiseResult.LEQ} dB | dB(A): ${noiseResult.dBA}`, 'bg-success');
|
updateTestStatus(sensor.id, 'Passed', `LEQ: ${noiseResult.LEQ} dB | dB(A): ${noiseResult.dBA}`, 'bg-success');
|
||||||
|
|||||||
@@ -381,6 +381,7 @@ function get_data_sqlite(table, limit, download , startDate = "", endDate = "")
|
|||||||
<th>Timestamp</th>
|
<th>Timestamp</th>
|
||||||
<th>Curent LEQ</th>
|
<th>Curent LEQ</th>
|
||||||
<th>DB_A_value</th>
|
<th>DB_A_value</th>
|
||||||
|
<th>Status</th>
|
||||||
|
|
||||||
`;
|
`;
|
||||||
}else if (table === "data_MHZ19") {
|
}else if (table === "data_MHZ19") {
|
||||||
@@ -462,10 +463,13 @@ function get_data_sqlite(table, limit, download , startDate = "", endDate = "")
|
|||||||
<td>${columns[5]}</td>
|
<td>${columns[5]}</td>
|
||||||
`;
|
`;
|
||||||
}else if (table === "data_NOISE") {
|
}else if (table === "data_NOISE") {
|
||||||
|
const nStatus = parseInt(columns[3]) || 0;
|
||||||
|
const nStatusLabel = nStatus === 255 ? '❌ Déconnecté' : '✅ OK';
|
||||||
tableHTML += `
|
tableHTML += `
|
||||||
<td>${columns[0]}</td>
|
<td>${columns[0]}</td>
|
||||||
<td>${columns[1]}</td>
|
<td>${columns[1]}</td>
|
||||||
<td>${columns[2]}</td>
|
<td>${columns[2]}</td>
|
||||||
|
<td>${nStatusLabel}</td>
|
||||||
|
|
||||||
`;
|
`;
|
||||||
}else if (table === "data_MHZ19") {
|
}else if (table === "data_MHZ19") {
|
||||||
@@ -532,6 +536,9 @@ function downloadCSV(response, table) {
|
|||||||
else if (table === "data_NPM_5channels") {
|
else if (table === "data_NPM_5channels") {
|
||||||
csvContent += "TimestampUTC,PM_ch1,PM_ch2,PM_ch3,PM_ch4,PM_ch5\n";
|
csvContent += "TimestampUTC,PM_ch1,PM_ch2,PM_ch3,PM_ch4,PM_ch5\n";
|
||||||
}
|
}
|
||||||
|
else if (table === "data_NOISE") {
|
||||||
|
csvContent += "TimestampUTC,Current_LEQ,DB_A_value,noise_status\n";
|
||||||
|
}
|
||||||
else if (table === "data_MHZ19") {
|
else if (table === "data_MHZ19") {
|
||||||
csvContent += "TimestampUTC,CO2_ppm\n";
|
csvContent += "TimestampUTC,CO2_ppm\n";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -880,6 +880,13 @@ if ($type == "sara") {
|
|||||||
echo $output;
|
echo $output;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# SARA HARDWARE REBOOT (GPIO 16)
|
||||||
|
if ($type == "sara_hardware_reboot") {
|
||||||
|
$command = 'sudo /usr/bin/python3 /var/www/nebuleair_pro_4g/SARA/reboot/hardware_reboot.py';
|
||||||
|
$output = shell_exec($command);
|
||||||
|
echo $output;
|
||||||
|
}
|
||||||
|
|
||||||
# SARA R4 COMMANDS (MQTT)
|
# SARA R4 COMMANDS (MQTT)
|
||||||
if ($type == "sara_getMQTT_config") {
|
if ($type == "sara_getMQTT_config") {
|
||||||
$port=$_GET['port'];
|
$port=$_GET['port'];
|
||||||
|
|||||||
@@ -58,6 +58,9 @@
|
|||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<span data-i18n="logs.saraLogs">Sara logs</span>
|
<span data-i18n="logs.saraLogs">Sara logs</span>
|
||||||
<button type="submit" class="btn btn-secondary btn-sm" id="refresh-master-log" data-i18n="logs.refresh">Refresh</button>
|
<button type="submit" class="btn btn-secondary btn-sm" id="refresh-master-log" data-i18n="logs.refresh">Refresh</button>
|
||||||
|
<button type="button" class="btn btn-sm" id="auto-refresh-toggle" onclick="toggleAutoRefresh()">
|
||||||
|
<span id="auto-refresh-icon">▶</span> Auto
|
||||||
|
</button>
|
||||||
<button type="submit" class="btn btn-secondary btn-sm" onclick="clear_loopLogs()" data-i18n="logs.clear">Clear</button>
|
<button type="submit" class="btn btn-secondary btn-sm" onclick="clear_loopLogs()" data-i18n="logs.clear">Clear</button>
|
||||||
|
|
||||||
<span id="script_running"></span>
|
<span id="script_running"></span>
|
||||||
@@ -274,6 +277,32 @@ function clear_loopLogs(){
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Auto-refresh for SARA logs
|
||||||
|
let autoRefreshInterval = null;
|
||||||
|
const AUTO_REFRESH_MS = 3000;
|
||||||
|
|
||||||
|
function toggleAutoRefresh() {
|
||||||
|
const btn = document.getElementById('auto-refresh-toggle');
|
||||||
|
const icon = document.getElementById('auto-refresh-icon');
|
||||||
|
|
||||||
|
if (autoRefreshInterval) {
|
||||||
|
clearInterval(autoRefreshInterval);
|
||||||
|
autoRefreshInterval = null;
|
||||||
|
icon.textContent = '▶';
|
||||||
|
btn.classList.remove('btn-success');
|
||||||
|
btn.classList.add('btn-secondary');
|
||||||
|
} else {
|
||||||
|
// Refresh immediately, then every N seconds
|
||||||
|
displayLogFile('../logs/sara_service.log', document.getElementById('card_loop_content'), true, 1000);
|
||||||
|
autoRefreshInterval = setInterval(() => {
|
||||||
|
displayLogFile('../logs/sara_service.log', document.getElementById('card_loop_content'), true, 1000);
|
||||||
|
}, AUTO_REFRESH_MS);
|
||||||
|
icon.textContent = '⏸';
|
||||||
|
btn.classList.remove('btn-secondary');
|
||||||
|
btn.classList.add('btn-success');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function getModem_busy_status() {
|
function getModem_busy_status() {
|
||||||
//console.log("Getting modem busy status");
|
//console.log("Getting modem busy status");
|
||||||
|
|
||||||
|
|||||||
143
html/saraR4.html
143
html/saraR4.html
@@ -58,6 +58,16 @@
|
|||||||
<label class="form-check-label" for="check_modem_configMode">Mode configuration</label>
|
<label class="form-check-label" for="check_modem_configMode">Mode configuration</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div id="configmode_alert" class="alert alert-danger align-items-center py-3 mb-3" role="alert" style="display:none;">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" class="bi bi-exclamation-triangle-fill me-3 flex-shrink-0" viewBox="0 0 16 16">
|
||||||
|
<path d="M8.982 1.566a1.13 1.13 0 0 0-1.96 0L.165 13.233c-.457.778.091 1.767.98 1.767h13.713c.889 0 1.438-.99.98-1.767zM8 5c.535 0 .954.462.9.995l-.35 3.507a.552.552 0 0 1-1.1 0L7.1 5.995A.905.905 0 0 1 8 5m.002 6a1 1 0 1 1 0 2 1 1 0 0 1 0-2"/>
|
||||||
|
</svg>
|
||||||
|
<div>
|
||||||
|
<strong>Mode configuration actif — le capteur n'envoie aucune donnée !</strong><br>
|
||||||
|
<small>Le script de transmission (SARA) est désactivé tant que ce mode est actif. Pensez à le désactiver une fois la configuration terminée. Ce mode sera automatiquement désactivé au prochain redémarrage du capteur.</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<button class="btn btn-success mb-3 btn_selfTest" onclick="runSelfTest()">
|
<button class="btn btn-success mb-3 btn_selfTest" onclick="runSelfTest()">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-check2-circle me-1" viewBox="0 0 16 16">
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-check2-circle me-1" viewBox="0 0 16 16">
|
||||||
<path d="M2.5 8a5.5 5.5 0 1 1 11 0 5.5 5.5 0 0 1-11 0z"/>
|
<path d="M2.5 8a5.5 5.5 0 1 1 11 0 5.5 5.5 0 0 1-11 0z"/>
|
||||||
@@ -151,14 +161,35 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-sm-2">
|
<div class="col-sm-3">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<p class="card-text">Modem Reset </p>
|
<p class="card-text"><strong>Modem Reset</strong></p>
|
||||||
<button class="btn btn-danger" onclick="getData_saraR4('ttyAMA2', 'AT+CFUN=15', 1)">Reset</button>
|
|
||||||
|
<p class="text-muted small mb-2">Reset software (AT+CFUN=15) : redémarre le firmware du modem.</p>
|
||||||
|
<button class="btn btn-warning mb-2" onclick="getData_saraR4('ttyAMA2', 'AT+CFUN=15', 1)">Reset Software</button>
|
||||||
<div id="loading_ttyAMA2_AT_CFUN_15" class="spinner-border spinner-border-sm" style="display: none;" role="status"></div>
|
<div id="loading_ttyAMA2_AT_CFUN_15" class="spinner-border spinner-border-sm" style="display: none;" role="status"></div>
|
||||||
<div id="response_ttyAMA2_AT_CFUN_15"></div>
|
<div id="response_ttyAMA2_AT_CFUN_15"></div>
|
||||||
</table>
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<p class="text-muted small mb-2">Reset hardware (GPIO 16) : coupe et rétablit l'alimentation du modem, puis teste la connexion avec le modem (~20 secondes).</p>
|
||||||
|
<button class="btn btn-danger mb-2" id="btn_hw_reboot" onclick="hardwareRebootSara()">Reset Hardware</button>
|
||||||
|
<div id="loading_hw_reboot" class="spinner-border spinner-border-sm" style="display: none;" role="status"></div>
|
||||||
|
<div id="response_hw_reboot"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-sm-3">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body">
|
||||||
|
<p class="card-text"><strong>LED status connexion (PCB)</strong></p>
|
||||||
|
<p class="text-muted small mb-2">Active la LED bleue du PCB qui indique l'état de la connexion réseau (GPIO 16 du modem).</p>
|
||||||
|
<button class="btn btn-primary mb-2" onclick="sendLedCommand('AT+UGPIOC=16,2', 'Activer')">Activer LED</button>
|
||||||
|
<button class="btn btn-secondary mb-2" onclick="sendLedCommand('AT+UGPIOC=16,255', 'Désactiver')">Désactiver LED</button>
|
||||||
|
<div id="loading_led" class="spinner-border spinner-border-sm" style="display: none;" role="status"></div>
|
||||||
|
<div id="response_led"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -446,6 +477,9 @@ document.addEventListener('DOMContentLoaded', function () {
|
|||||||
if (check_modem_configMode) {
|
if (check_modem_configMode) {
|
||||||
check_modem_configMode.checked = response.modem_config_mode;
|
check_modem_configMode.checked = response.modem_config_mode;
|
||||||
console.log("Modem configuration: " + response.modem_config_mode);
|
console.log("Modem configuration: " + response.modem_config_mode);
|
||||||
|
// Show/hide config mode alert banner
|
||||||
|
const alertEl = document.getElementById("configmode_alert");
|
||||||
|
if (alertEl) alertEl.style.display = response.modem_config_mode ? "flex" : "none";
|
||||||
} else {
|
} else {
|
||||||
console.error("Checkbox element not found");
|
console.error("Checkbox element not found");
|
||||||
}
|
}
|
||||||
@@ -1024,6 +1058,103 @@ function getSignalInfo(port, timeout) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function hardwareRebootSara() {
|
||||||
|
if (!confirm("Couper l'alimentation du modem SARA via GPIO 16 ?\nLe modem sera éteint pendant ~3 secondes puis redémarré.\nLa connexion avec le modem sera ensuite testée (~20 secondes au total).")) return;
|
||||||
|
|
||||||
|
console.log("Hardware reboot SARA via GPIO 16");
|
||||||
|
$("#btn_hw_reboot").prop("disabled", true);
|
||||||
|
$("#loading_hw_reboot").show();
|
||||||
|
$("#response_hw_reboot").html(`
|
||||||
|
<div class="alert alert-info py-2 mt-2" id="hw_reboot_progress">
|
||||||
|
<small><strong>Étape 1/3</strong> — Coupure alimentation du modem...</small>
|
||||||
|
</div>`);
|
||||||
|
|
||||||
|
// Progress messages
|
||||||
|
var step2Timeout = setTimeout(function() {
|
||||||
|
$("#hw_reboot_progress small").html('<strong>Étape 2/3</strong> — Attente du redémarrage du modem...');
|
||||||
|
}, 4000);
|
||||||
|
|
||||||
|
var step3Timeout = setTimeout(function() {
|
||||||
|
$("#hw_reboot_progress small").html('<strong>Étape 3/3</strong> — Test de la connexion avec le modem (ATI)...');
|
||||||
|
}, 9000);
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
url: 'launcher.php?type=sara_hardware_reboot',
|
||||||
|
dataType: 'json',
|
||||||
|
method: 'GET',
|
||||||
|
timeout: 45000,
|
||||||
|
success: function(response) {
|
||||||
|
console.log(response);
|
||||||
|
clearTimeout(step2Timeout);
|
||||||
|
clearTimeout(step3Timeout);
|
||||||
|
$("#loading_hw_reboot").hide();
|
||||||
|
$("#btn_hw_reboot").prop("disabled", false);
|
||||||
|
|
||||||
|
if (response.reboot) {
|
||||||
|
$("#response_hw_reboot").html(`
|
||||||
|
<div class="alert alert-success py-2 mt-2">
|
||||||
|
<strong>Modem redémarré et connexion vérifiée</strong><br>
|
||||||
|
<small><code>${response.modem_response}</code></small>
|
||||||
|
</div>`);
|
||||||
|
} else {
|
||||||
|
$("#response_hw_reboot").html(`
|
||||||
|
<div class="alert alert-danger py-2 mt-2">
|
||||||
|
<strong>Echec</strong><br>
|
||||||
|
<small>${response.error || 'Modem ne répond pas après le redémarrage'}</small>
|
||||||
|
</div>`);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: function(xhr, status, error) {
|
||||||
|
console.error('Hardware reboot failed:', status, error);
|
||||||
|
clearTimeout(step2Timeout);
|
||||||
|
clearTimeout(step3Timeout);
|
||||||
|
$("#loading_hw_reboot").hide();
|
||||||
|
$("#btn_hw_reboot").prop("disabled", false);
|
||||||
|
$("#response_hw_reboot").html(`
|
||||||
|
<div class="alert alert-danger py-2 mt-2">
|
||||||
|
<strong>Erreur de communication</strong><br>
|
||||||
|
<small>${error}</small>
|
||||||
|
</div>`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function sendLedCommand(command, label) {
|
||||||
|
$("#loading_led").show();
|
||||||
|
$("#response_led").empty();
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
url: 'launcher.php?type=sara&port=ttyAMA2&command=' + encodeURIComponent(command) + '&timeout=5',
|
||||||
|
dataType: 'text',
|
||||||
|
method: 'GET',
|
||||||
|
timeout: 10000,
|
||||||
|
success: function(response) {
|
||||||
|
$("#loading_led").hide();
|
||||||
|
if (response.indexOf("OK") !== -1) {
|
||||||
|
$("#response_led").html(`
|
||||||
|
<div class="alert alert-success py-2 mt-2">
|
||||||
|
<strong>${label} — OK</strong><br>
|
||||||
|
<small>Commande <code>${command}</code> exécutée avec succès.</small>
|
||||||
|
</div>`);
|
||||||
|
} else {
|
||||||
|
$("#response_led").html(`
|
||||||
|
<div class="alert alert-warning py-2 mt-2">
|
||||||
|
<strong>${label} — Réponse inattendue</strong><br>
|
||||||
|
<small><code>${response}</code></small>
|
||||||
|
</div>`);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: function(xhr, status, error) {
|
||||||
|
$("#loading_led").hide();
|
||||||
|
$("#response_led").html(`
|
||||||
|
<div class="alert alert-danger py-2 mt-2">
|
||||||
|
<strong>Erreur de connexion avec le modem</strong><br>
|
||||||
|
<small>${error || 'Pas de réponse'}</small>
|
||||||
|
</div>`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function getData_saraR4(port, command, timeout){
|
function getData_saraR4(port, command, timeout){
|
||||||
console.log("Data from SaraR4");
|
console.log("Data from SaraR4");
|
||||||
console.log("Port: " + port );
|
console.log("Port: " + port );
|
||||||
@@ -1429,6 +1560,10 @@ function update_modem_configMode(param, checked){
|
|||||||
const toastBody = toastLiveExample.querySelector('.toast-body');
|
const toastBody = toastLiveExample.querySelector('.toast-body');
|
||||||
|
|
||||||
console.log("updating modem config mode to :" + checked);
|
console.log("updating modem config mode to :" + checked);
|
||||||
|
// Toggle alert banner immediately
|
||||||
|
const alertEl = document.getElementById("configmode_alert");
|
||||||
|
if (alertEl) alertEl.style.display = checked ? "flex" : "none";
|
||||||
|
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: 'launcher.php?type=update_config_sqlite¶m='+param+'&value='+checked,
|
url: 'launcher.php?type=update_config_sqlite¶m='+param+'&value='+checked,
|
||||||
dataType: 'json', // Specify that you expect a JSON response
|
dataType: 'json', // Specify that you expect a JSON response
|
||||||
|
|||||||
@@ -1015,6 +1015,7 @@ try:
|
|||||||
print("No data available in the database.")
|
print("No data available in the database.")
|
||||||
|
|
||||||
# NOISE sensor
|
# NOISE sensor
|
||||||
|
noise_disconnected = False
|
||||||
if NOISE_sensor:
|
if NOISE_sensor:
|
||||||
print("➡️Getting NOISE sensor values")
|
print("➡️Getting NOISE sensor values")
|
||||||
cursor.execute("SELECT * FROM data_NOISE ORDER BY rowid DESC LIMIT 1")
|
cursor.execute("SELECT * FROM data_NOISE ORDER BY rowid DESC LIMIT 1")
|
||||||
@@ -1024,6 +1025,14 @@ try:
|
|||||||
cur_LEQ = last_row[1]
|
cur_LEQ = last_row[1]
|
||||||
cur_level = last_row[2]
|
cur_level = last_row[2]
|
||||||
|
|
||||||
|
# noise_status: 0xFF = disconnected (column index 3)
|
||||||
|
noise_status_value = last_row[3] if len(last_row) > 3 and last_row[3] is not None else 0x00
|
||||||
|
if noise_status_value == 0xFF:
|
||||||
|
noise_disconnected = True
|
||||||
|
print("Noise status: 0xFF (disconnected)")
|
||||||
|
else:
|
||||||
|
print(f"Noise status: 0x{noise_status_value:02X}")
|
||||||
|
|
||||||
#Add data to payload CSV
|
#Add data to payload CSV
|
||||||
payload_csv[6] = cur_level
|
payload_csv[6] = cur_level
|
||||||
|
|
||||||
@@ -1149,6 +1158,8 @@ try:
|
|||||||
error_flags |= ERR_RTC_RESET
|
error_flags |= ERR_RTC_RESET
|
||||||
if npm_disconnected:
|
if npm_disconnected:
|
||||||
error_flags |= ERR_NPM
|
error_flags |= ERR_NPM
|
||||||
|
if noise_disconnected:
|
||||||
|
error_flags |= ERR_NOISE
|
||||||
payload.set_error_flags(error_flags)
|
payload.set_error_flags(error_flags)
|
||||||
|
|
||||||
# ---- Firmware version (bytes 69-71) ----
|
# ---- Firmware version (bytes 69-71) ----
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ After=network.target
|
|||||||
|
|
||||||
[Service]
|
[Service]
|
||||||
Type=oneshot
|
Type=oneshot
|
||||||
ExecStart=/usr/bin/python3 /var/www/nebuleair_pro_4g/loop/SARA_send_data_v2.py
|
ExecStart=/usr/bin/python3 -u /var/www/nebuleair_pro_4g/loop/SARA_send_data_v2.py
|
||||||
User=root
|
User=root
|
||||||
WorkingDirectory=/var/www/nebuleair_pro_4g
|
WorkingDirectory=/var/www/nebuleair_pro_4g
|
||||||
StandardOutput=append:/var/www/nebuleair_pro_4g/logs/sara_service.log
|
StandardOutput=append:/var/www/nebuleair_pro_4g/logs/sara_service.log
|
||||||
|
|||||||
@@ -28,7 +28,31 @@ write_fs(frequency: int) -> sampling freq
|
|||||||
import nsrt_mk3_dev
|
import nsrt_mk3_dev
|
||||||
import sqlite3
|
import sqlite3
|
||||||
|
|
||||||
nsrt = nsrt_mk3_dev.NsrtMk3Dev('/dev/ttyACM0')
|
# noise_status: 0x00 = OK, 0xFF = disconnected
|
||||||
|
noise_status = 0x00
|
||||||
|
leq_level_rounded = 0
|
||||||
|
weighted_level_rounded = 0
|
||||||
|
|
||||||
|
try:
|
||||||
|
nsrt = nsrt_mk3_dev.NsrtMk3Dev('/dev/ttyACM0')
|
||||||
|
|
||||||
|
freq_level = nsrt.read_fs() #current sampling frequency
|
||||||
|
time_constant = nsrt.read_tau() #reads the current time constant
|
||||||
|
leq_level = nsrt.read_leq() #current running LEQ and starts the integration of a new LEQ.
|
||||||
|
weighting = nsrt.read_weighting() #weighting curve that is currently selected ( A ou C)
|
||||||
|
weighted_level = nsrt.read_level() #current running level in dB.
|
||||||
|
|
||||||
|
leq_level_rounded = round(leq_level, 2)
|
||||||
|
weighted_level_rounded = round(weighted_level, 2)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
err_msg = str(e)
|
||||||
|
if "No such file or directory" in err_msg or "could not open port" in err_msg:
|
||||||
|
print(f"NSRT MK4 disconnected: {err_msg}")
|
||||||
|
noise_status = 0xFF
|
||||||
|
else:
|
||||||
|
print(f"NSRT MK4 error: {err_msg}")
|
||||||
|
noise_status = 0xFF
|
||||||
|
|
||||||
# Connect to the SQLite database
|
# Connect to the SQLite database
|
||||||
conn = sqlite3.connect("/var/www/nebuleair_pro_4g/sqlite/sensors.db")
|
conn = sqlite3.connect("/var/www/nebuleair_pro_4g/sqlite/sensors.db")
|
||||||
@@ -39,34 +63,15 @@ cursor.execute("SELECT * FROM timestamp_table LIMIT 1")
|
|||||||
row = cursor.fetchone() # Get the first (and only) row
|
row = cursor.fetchone() # Get the first (and only) row
|
||||||
rtc_time_str = row[1] # '2025-02-07 12:30:45'
|
rtc_time_str = row[1] # '2025-02-07 12:30:45'
|
||||||
|
|
||||||
freq_level = nsrt.read_fs() #current sampling frequency
|
|
||||||
time_constant = nsrt.read_tau() #reads the current time constant
|
|
||||||
leq_level = nsrt.read_leq() #current running LEQ and starts the integration of a new LEQ.
|
|
||||||
weighting = nsrt.read_weighting() #weighting curve that is currently selected ( A ou C)
|
|
||||||
weighted_level = nsrt.read_level() #current running level in dB.
|
|
||||||
|
|
||||||
#print(f'current sampling freq : {freq_level} Hz')
|
|
||||||
#print(f'current time constant : {time_constant} s')
|
|
||||||
#print(f'current LEQ level: {leq_level:0.2f} dB')
|
|
||||||
#print(f'{weighting} value: {weighted_level:0.2f} dBA')
|
|
||||||
# Round values to 2 decimal places before saving
|
|
||||||
leq_level_rounded = round(leq_level, 2)
|
|
||||||
weighted_level_rounded = round(weighted_level, 2)
|
|
||||||
|
|
||||||
#save to db
|
|
||||||
#save to sqlite database
|
#save to sqlite database
|
||||||
try:
|
try:
|
||||||
cursor.execute('''
|
cursor.execute('''
|
||||||
INSERT INTO data_NOISE (timestamp,current_LEQ, DB_A_value) VALUES (?,?,?)'''
|
INSERT INTO data_NOISE (timestamp, current_LEQ, DB_A_value, noise_status) VALUES (?,?,?,?)'''
|
||||||
, (rtc_time_str,leq_level_rounded,weighted_level_rounded))
|
, (rtc_time_str, leq_level_rounded, weighted_level_rounded, noise_status))
|
||||||
|
|
||||||
# Commit and close the connection
|
|
||||||
conn.commit()
|
conn.commit()
|
||||||
|
|
||||||
#print("Sensor data saved successfully!")
|
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Database error: {e}")
|
print(f"Database error: {e}")
|
||||||
|
|
||||||
|
|
||||||
conn.close()
|
conn.close()
|
||||||
@@ -31,4 +31,10 @@ try:
|
|||||||
print(json.dumps(data))
|
print(json.dumps(data))
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(json.dumps({"error": str(e)}))
|
err_msg = str(e)
|
||||||
|
if "No such file or directory" in err_msg or "could not open port" in err_msg:
|
||||||
|
print(json.dumps({"error": "Capteur déconnecté — vérifiez le câblage USB du sonomètre NSRT MK4 (/dev/ttyACM0)", "disconnected": True}))
|
||||||
|
elif "Permission denied" in err_msg:
|
||||||
|
print(json.dumps({"error": "Permission refusée sur /dev/ttyACM0 — exécutez: sudo chmod 777 /dev/ttyACM0", "disconnected": False}))
|
||||||
|
else:
|
||||||
|
print(json.dumps({"error": err_msg}))
|
||||||
|
|||||||
@@ -140,10 +140,18 @@ cursor.execute("""
|
|||||||
CREATE TABLE IF NOT EXISTS data_NOISE (
|
CREATE TABLE IF NOT EXISTS data_NOISE (
|
||||||
timestamp TEXT,
|
timestamp TEXT,
|
||||||
current_LEQ REAL,
|
current_LEQ REAL,
|
||||||
DB_A_value REAL
|
DB_A_value REAL,
|
||||||
|
noise_status INTEGER DEFAULT 0
|
||||||
)
|
)
|
||||||
""")
|
""")
|
||||||
|
|
||||||
|
# Add noise_status column to existing databases (migration)
|
||||||
|
try:
|
||||||
|
cursor.execute("ALTER TABLE data_NOISE ADD COLUMN noise_status INTEGER DEFAULT 0")
|
||||||
|
print("Added noise_status column to data_NOISE")
|
||||||
|
except:
|
||||||
|
pass # Column already exists
|
||||||
|
|
||||||
# Create a table MHZ19 (CO2 sensor)
|
# Create a table MHZ19 (CO2 sensor)
|
||||||
cursor.execute("""
|
cursor.execute("""
|
||||||
CREATE TABLE IF NOT EXISTS data_MHZ19 (
|
CREATE TABLE IF NOT EXISTS data_MHZ19 (
|
||||||
|
|||||||
@@ -110,6 +110,7 @@ for connected, port, name, coefficient in envea_sondes:
|
|||||||
# Database migrations (add columns to existing tables)
|
# Database migrations (add columns to existing tables)
|
||||||
migrations = [
|
migrations = [
|
||||||
("data_NPM", "npm_status", "INTEGER DEFAULT 0"),
|
("data_NPM", "npm_status", "INTEGER DEFAULT 0"),
|
||||||
|
("data_NOISE", "noise_status", "INTEGER DEFAULT 0"),
|
||||||
]
|
]
|
||||||
|
|
||||||
for table, column, col_type in migrations:
|
for table, column, col_type in migrations:
|
||||||
|
|||||||
Reference in New Issue
Block a user