This commit is contained in:
Your Name
2025-03-26 08:27:28 +01:00
21 changed files with 1934 additions and 539 deletions

View File

@@ -52,9 +52,7 @@ def load_config(config_file):
return {} return {}
# Load the configuration data # Load the configuration data
config_file = '/var/www/nebuleair_pro_4g/config.json' npm_solo_port = "/dev/ttyAMA5" #port du NPM solo
config = load_config(config_file)
npm_solo_port = config.get('NPM_solo_port', '') #port du NPM solo
#GET RTC TIME from SQlite #GET RTC TIME from SQlite
cursor.execute("SELECT * FROM timestamp_table LIMIT 1") cursor.execute("SELECT * FROM timestamp_table LIMIT 1")

View File

@@ -28,7 +28,7 @@ Line by line installation.
``` ```
sudo apt update sudo apt update
sudo apt install git gh apache2 php php-sqlite3 python3 python3-pip jq autossh i2c-tools python3-smbus -y sudo apt install git gh apache2 sqlite3 php php-sqlite3 python3 python3-pip jq autossh i2c-tools python3-smbus -y
sudo pip3 install pyserial requests RPi.GPIO adafruit-circuitpython-bme280 crcmod psutil ntplib pytz gpiozero adafruit-circuitpython-ads1x15 numpy --break-system-packages sudo pip3 install pyserial requests RPi.GPIO adafruit-circuitpython-bme280 crcmod psutil ntplib pytz gpiozero adafruit-circuitpython-ads1x15 numpy --break-system-packages
sudo mkdir -p /var/www/.ssh sudo mkdir -p /var/www/.ssh
sudo ssh-keygen -t rsa -b 4096 -f /var/www/.ssh/id_rsa -N "" sudo ssh-keygen -t rsa -b 4096 -f /var/www/.ssh/id_rsa -N ""

View File

@@ -25,15 +25,43 @@ 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) # Set GPIO17 as an output
import sqlite3
#get data from config # database connection
def load_config(config_file): conn = sqlite3.connect("/var/www/nebuleair_pro_4g/sqlite/sensors.db")
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: try:
with open(config_file, 'r') as file:
config_data = json.load(file) # 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 return config_data
except Exception as e: except Exception as e:
print(f"Error loading config file: {e}") print(f"Error loading config from SQLite: {e}")
return {} return {}
#Fonction pour mettre à jour le JSON de configuration #Fonction pour mettre à jour le JSON de configuration
@@ -65,10 +93,55 @@ def update_json_key(file_path, key, value):
except Exception as e: except Exception as e:
print(f"Error updating the JSON file: {e}") print(f"Error updating the JSON file: {e}")
# Define the config file path
config_file = '/var/www/nebuleair_pro_4g/config.json' def update_sqlite_config(key, value):
# Load the configuration data """
config = load_config(config_file) 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:
# Check if the key exists and get its type
cursor.execute("SELECT type FROM config_table WHERE key = ?", (key,))
result = cursor.fetchone()
if result is None:
print(f"Key '{key}' not found in the config_table.")
conn.close()
return
# Get the type of the value from the database
value_type = result[0]
# Convert the value to the appropriate string representation based on its type
if value_type == 'bool':
# Convert Python boolean or string 'true'/'false' to '1'/'0'
if isinstance(value, bool):
str_value = '1' if value else '0'
else:
str_value = '1' if str(value).lower() in ('true', '1', 'yes', 'y') else '0'
elif value_type == 'int':
str_value = str(int(value))
elif value_type == 'float':
str_value = str(float(value))
else:
str_value = str(value)
# Update the value in the database
cursor.execute("UPDATE config_table SET value = ? WHERE key = ?", (str_value, key))
# Commit the changes and close the connection
conn.commit()
print(f"💾 Updated '{key}' to '{value}' in database.")
except Exception as e:
print(f"Error updating the SQLite database: {e}")
#Load config
config = load_config_sqlite()
#config
baudrate = config.get('SaraR4_baudrate', 115200) #baudrate du sara R4 baudrate = config.get('SaraR4_baudrate', 115200) #baudrate du sara R4
device_id = config.get('deviceID', '').upper() #device ID en maj device_id = config.get('deviceID', '').upper() #device ID en maj
@@ -163,7 +236,7 @@ try:
print("⚠️ Could not identify modem model") print("⚠️ Could not identify modem model")
print(f"🔍 Model: {model}") print(f"🔍 Model: {model}")
update_json_key(config_file, "modem_version", model) update_sqlite_config("modem_version", model)
time.sleep(1) time.sleep(1)
''' '''
@@ -344,9 +417,9 @@ try:
else: else:
print("❌ Failed to extract coordinates.") print("❌ Failed to extract coordinates.")
#update config.json #update sqlite table
update_json_key(config_file, "latitude_raw", float(latitude)) update_sqlite_config("latitude_raw", float(latitude))
update_json_key(config_file, "longitude_raw", float(longitude)) update_sqlite_config("longitude_raw", float(longitude))
time.sleep(1) time.sleep(1)

View File

@@ -36,6 +36,51 @@ def load_config(config_file):
print(f"Error loading config file: {e}") print(f"Error loading config file: {e}")
return {} return {}
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
# Define the config file path # Define the config file path
config_file = '/var/www/nebuleair_pro_4g/config.json' config_file = '/var/www/nebuleair_pro_4g/config.json'
# Load the configuration data # Load the configuration data
@@ -57,17 +102,11 @@ ser.write((command + '\r').encode('utf-8'))
try: try:
# Read lines until a timeout occurs response = read_complete_response(ser, wait_for_lines=["OK", "ERROR"],timeout=5, end_of_response_timeout=120, debug=True)
response_lines = []
while True: print('<p class="text-danger-emphasis">')
line = ser.readline().decode('utf-8').strip() print(response)
if not line: print("</p>", end="")
break # Break the loop if an empty line is encountered
response_lines.append(line)
# Print the response
for line in response_lines:
print(line)
except serial.SerialException as e: except serial.SerialException as e:
print(f"Error: {e}") print(f"Error: {e}")

View File

@@ -2,6 +2,8 @@
# Script to check if wifi is connected and start hotspot if not # Script to check if wifi is connected and start hotspot if not
# will also retreive unique RPi ID and store it to deviceID.txt # will also retreive unique RPi ID and store it to deviceID.txt
# script that starts at boot:
# @reboot /var/www/nebuleair_pro_4g/boot_hotspot.sh >> /var/www/nebuleair_pro_4g/logs/app.log 2>&1
OUTPUT_FILE="/var/www/nebuleair_pro_4g/wifi_list.csv" OUTPUT_FILE="/var/www/nebuleair_pro_4g/wifi_list.csv"
JSON_FILE="/var/www/nebuleair_pro_4g/config.json" JSON_FILE="/var/www/nebuleair_pro_4g/config.json"
@@ -12,6 +14,8 @@ echo "-------------------"
echo "NebuleAir pro started at $(date)" echo "NebuleAir pro started at $(date)"
chmod -R 777 /var/www/nebuleair_pro_4g/
# Blink GPIO 23 and 24 five times # Blink GPIO 23 and 24 five times
for i in {1..5}; do for i in {1..5}; do
# Turn GPIO 23 and 24 ON # Turn GPIO 23 and 24 ON
@@ -25,15 +29,19 @@ for i in {1..5}; do
sleep 1 sleep 1
done done
echo "getting SARA R4 serial number" echo "getting RPI serial number"
# Get the last 8 characters of the serial number and write to text file # Get the last 8 characters of the serial number and write to text file
serial_number=$(cat /proc/cpuinfo | grep Serial | awk '{print substr($3, length($3) - 7)}') serial_number=$(cat /proc/cpuinfo | grep Serial | awk '{print substr($3, length($3) - 7)}')
# Use jq to update the "deviceID" in the JSON file
jq --arg serial_number "$serial_number" '.deviceID = $serial_number' "$JSON_FILE" > temp.json && mv temp.json "$JSON_FILE" # update Sqlite database
echo "Updating SQLite database with device ID: $serial_number"
sqlite3 /var/www/nebuleair_pro_4g/sqlite/sensors.db "UPDATE config_table SET value='$serial_number' WHERE key='deviceID';"
echo "id: $serial_number" echo "id: $serial_number"
#get the SSH port for tunneling
SSH_TUNNEL_PORT=$(jq -r '.sshTunnel_port' "$JSON_FILE") # Get SSH tunnel port from SQLite config_table
SSH_TUNNEL_PORT=$(sqlite3 /var/www/nebuleair_pro_4g/sqlite/sensors.db "SELECT value FROM config_table WHERE key='sshTunnel_port'")
#need to wait for the network manager to be ready #need to wait for the network manager to be ready
sleep 20 sleep 20
@@ -51,17 +59,16 @@ if [ "$STATE" == "30 (disconnected)" ]; then
echo "Starting hotspot..." echo "Starting hotspot..."
sudo nmcli device wifi hotspot ifname wlan0 ssid nebuleair_pro password nebuleaircfg sudo nmcli device wifi hotspot ifname wlan0 ssid nebuleair_pro password nebuleaircfg
# Update JSON to reflect hotspot mode # Update SQLite to reflect hotspot mode
jq --arg status "hotspot" '.WIFI_status = $status' "$JSON_FILE" > temp.json && mv temp.json "$JSON_FILE" sqlite3 /var/www/nebuleair_pro_4g/sqlite/sensors.db "UPDATE config_table SET value='hotspot' WHERE key='WIFI_status'"
else else
echo "🛜Success: wlan0 is connected!🛜" echo "🛜Success: wlan0 is connected!🛜"
CONN_SSID=$(nmcli -g GENERAL.CONNECTION device show wlan0) CONN_SSID=$(nmcli -g GENERAL.CONNECTION device show wlan0)
echo "Connection: $CONN_SSID" echo "Connection: $CONN_SSID"
#update config JSON file # Update SQLite to reflect hotspot mode
jq --arg status "connected" '.WIFI_status = $status' "$JSON_FILE" > temp.json && mv temp.json "$JSON_FILE" sqlite3 /var/www/nebuleair_pro_4g/sqlite/sensors.db "UPDATE config_table SET value='connected' WHERE key='WIFI_status'"
sudo chmod 777 "$JSON_FILE" sudo chmod 777 "$JSON_FILE"

View File

@@ -28,31 +28,14 @@ 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'
# Function to load config data # Fetch connected ENVEA sondes from SQLite config table
def load_config(config_file): cursor.execute("SELECT port, name, coefficient FROM envea_sondes_table WHERE connected = 1")
try: connected_envea_sondes = cursor.fetchall() # List of tuples (port, name, coefficient)
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)
# Initialize sensors and serial connections
envea_sondes = config.get('envea_sondes', [])
connected_envea_sondes = [sonde for sonde in envea_sondes if sonde.get('connected', False)]
serial_connections = {} serial_connections = {}
if connected_envea_sondes: if connected_envea_sondes:
for device in connected_envea_sondes: for port, name, coefficient in connected_envea_sondes:
port = device.get('port', 'Unknown')
name = device.get('name', 'Unknown')
try: try:
serial_connections[name] = serial.Serial( serial_connections[name] = serial.Serial(
port=f'/dev/{port}', port=f'/dev/{port}',
@@ -74,9 +57,7 @@ data_nh3 = 0
try: try:
if connected_envea_sondes: if connected_envea_sondes:
for device in connected_envea_sondes: for port, name, coefficient in connected_envea_sondes:
name = device.get('name', 'Unknown')
coefficient = device.get('coefficient', 1)
if name in serial_connections: if name in serial_connections:
serial_connection = serial_connections[name] serial_connection = serial_connections[name]
try: try:

View File

@@ -55,52 +55,13 @@
<div class="row mb-3"> <div class="row mb-3">
<div class="col-lg-3 col-12"> <div class="col-lg-3 col-12">
<h3 class="mt-4">Parameters</h3> <h3 class="mt-4">Parameters (config)</h3>
<form> <form>
<!--
<div class="form-check form-switch mb-2">
<input class="form-check-input" type="checkbox" role="switch" id="flex_loop" onchange="update_config('loop_activation',this.checked)">
<label class="form-check-label" for="flex_loop">Loop activation</label>
</div>
<div class="form-check form-switch mb-2">
<input class="form-check-input" type="checkbox" role="switch" id="flex_loop_log" onchange="update_config('loop_log', this.checked)">
<label class="form-check-label" for="flex_loop_log">Loop Logs</label>
</div>
<div class="form-check form-switch mb-2">
<input class="form-check-input" type="checkbox" role="switch" id="flex_start_log" onchange="update_config('boot_log', this.checked)">
<label class="form-check-label" for="flex_start_log">Boot Logs</label>
</div>
-->
<div class="form-check mb-3">
<input class="form-check-input" type="checkbox" value="" id="check_NPM_5channels" onchange="update_config('NextPM_5channels', this.checked)">
<label class="form-check-label" for="check_NPM_5channels">
Next PM 5 canaux
</label>
</div>
<div class="form-check mb-3">
<input class="form-check-input" type="checkbox" value="" id="check_bme280" onchange="update_config('BME280/get_data_v2.py', this.checked)">
<label class="form-check-label" for="check_bme280">
Sonde temp/hum (BME280)
</label>
</div>
<div class="form-check mb-3">
<input class="form-check-input" type="checkbox" value="" id="check_envea" onchange="update_config('envea/read_value_v2.py', this.checked)">
<label class="form-check-label" for="check_envea">
Sonde Envea
</label>
</div>
<div class="mb-3"> <div class="mb-3">
<label for="device_name" class="form-label">Device Name</label> <label for="device_name" class="form-label">Device Name</label>
<input type="text" class="form-control" id="device_name" onchange="update_config('deviceName', this.value)"> <input type="text" class="form-control" id="device_name" onchange="update_config_sqlite('deviceName', this.value)">
</div> </div>
<div class="mb-3"> <div class="mb-3">
@@ -108,6 +69,56 @@
<input type="text" class="form-control" id="device_ID" disabled> <input type="text" class="form-control" id="device_ID" disabled>
</div> </div>
<!-- config_scripts_table -->
<div class="form-check mb-3">
<input class="form-check-input" type="checkbox" value="" id="check_NPM" onchange="update_config_scripts_sqlite('NPM/get_data_modbus_v3.py', this.checked)">
<label class="form-check-label" for="check_NPM">
Next PM
</label>
</div>
<div class="form-check mb-3">
<input class="form-check-input" type="checkbox" value="" id="check_NPM_5channels" onchange="update_config_sqlite('npm_5channel', this.checked)">
<label class="form-check-label" for="check_NPM_5channels">
Next PM send 5 channels
</label>
</div>
<div class="form-check mb-3">
<input class="form-check-input" type="checkbox" value="" id="check_bme280" onchange="update_config_scripts_sqlite('BME280/get_data_v2.py', this.checked)">
<label class="form-check-label" for="check_bme280">
Sonde temp/hum (BME280)
</label>
</div>
<div class="form-check mb-3">
<input class="form-check-input" type="checkbox" value="" id="check_envea" onchange="update_config_scripts_sqlite('envea/read_value_v2.py', this.checked)">
<label class="form-check-label" for="check_envea">
Sonde Envea
</label>
</div>
<div class="form-check mb-3">
<input class="form-check-input" type="checkbox" value="" id="check_solarBattery" onchange="update_config_scripts_sqlite('MPPT/read.py', this.checked)">
<label class="form-check-label" for="check_solarBattery">
Solar / Battery MPPT
</label>
</div>
<div class="form-check mb-3">
<input class="form-check-input" type="checkbox" value="" id="check_WindMeter" onchange="update_config_scripts_sqlite('windMeter/read.py', this.checked)">
<label class="form-check-label" for="check_WindMeter">
Wind Meter
</label>
</div>
<div class="input-group mb-3" id="sondes_envea_div"></div>
<div id="envea_table"></div>
<!--<button type="submit" class="btn btn-primary">Submit</button>--> <!--<button type="submit" class="btn btn-primary">Submit</button>-->
</form> </form>
</div> </div>
@@ -117,13 +128,6 @@
<h3 class="mt-4">Clock</h3> <h3 class="mt-4">Clock</h3>
<div class="form-check mb-3">
<input class="form-check-input" type="checkbox" value="" id="check_RTC" onchange="update_config('i2c_RTC', this.checked)">
<label class="form-check-label" for="check_RTC">
RTC module (DS3231)
</label>
</div>
<div class="mb-3"> <div class="mb-3">
<label for="sys_local_time" class="form-label">System time (local)</label> <label for="sys_local_time" class="form-label">System time (local)</label>
<input type="text" class="form-control" id="sys_local_time" disabled> <input type="text" class="form-control" id="sys_local_time" disabled>
@@ -161,6 +165,22 @@
</div> </div>
</div> </div>
<!-- toast -->
<div class="toast-container position-fixed bottom-0 end-0 p-3">
<div id="liveToast" class="toast align-items-center text-bg-primary border-1" role="alert" aria-live="assertive" aria-atomic="true">
<div class="d-flex">
<div class="toast-body">
Hello, world! This is a toast message.
</div>
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast" aria-label="Close"></button>
</div>
</div>
</div>
</main> </main>
</div> </div>
</div> </div>
@@ -193,112 +213,290 @@
}); });
}); });
//end document.addEventListener
/*
___ _ _
/ _ \ _ __ | | ___ __ _ __| |
| | | | '_ \| | / _ \ / _` |/ _` |
| |_| | | | | |__| (_) | (_| | (_| |
\___/|_| |_|_____\___/ \__,_|\__,_|
*/
window.onload = function() { window.onload = function() {
fetch('../config.json') // Replace 'deviceID.txt' with 'config.json' //NEW way to get config (SQLite)
.then(response => response.json()) // Parse response as JSON $.ajax({
.then(data => { url: 'launcher.php?type=get_config_sqlite',
console.log("Getting config file (onload)"); dataType:'json',
//get device ID //dataType: 'json', // Specify that you expect a JSON response
const deviceID = data.deviceID.trim().toUpperCase(); method: 'GET', // Use GET or POST depending on your needs
//document.getElementById('pageTitle_plus_ID').innerText = 'token: ' + deviceID; success: function(response) {
//get device Name console.log("Getting SQLite config table:");
const deviceName = data.deviceName; console.log(response);
//device name
console.log("Device Name: " + deviceName); const deviceName = document.getElementById("device_name");
console.log("Device ID: " + deviceID); deviceName.value = response.deviceName;
//device name_side bar
const elements = document.querySelectorAll('.sideBar_sensorName');
elements.forEach((element) => {
const elements = document.querySelectorAll('.sideBar_sensorName'); element.innerText = response.deviceName;
elements.forEach((element) => { });
element.innerText = deviceName; //device ID
}); const deviceID = response.deviceID.trim().toUpperCase();
const device_ID = document.getElementById("device_ID");
//get BME check device_ID.value = response.deviceID.toUpperCase();
const checkbox = document.getElementById("check_bme280"); //nextPM send 5 channels
checkbox.checked = data["BME280/get_data_v2.py"]; const checkbox_nmp5channels = document.getElementById("check_NPM_5channels");
checkbox_nmp5channels.checked = response.npm_5channel;
//get NPM-5channels check
const checkbox_NPM_5channels = document.getElementById("check_NPM_5channels");
checkbox_NPM_5channels.checked = data["NextPM_5channels"];
//get sonde Envea check
const checkbox_envea = document.getElementById("check_envea");
checkbox_envea.checked = data["envea/read_value_v2.py"];
//get RTC check
const checkbox_RTC = document.getElementById("check_RTC");
checkbox_RTC.checked = data.i2c_RTC;
//device name },
const device_name = document.getElementById("device_name"); error: function(xhr, status, error) {
device_name.value = data.deviceName; console.error('AJAX request failed:', status, error);
}
});//end AJAX
//getting config_scripts table
$.ajax({
url: 'launcher.php?type=get_config_scripts_sqlite',
dataType:'json',
//dataType: 'json', // Specify that you expect a JSON response
method: 'GET', // Use GET or POST depending on your needs
success: function(response) {
console.log("Getting SQLite config scripts table:");
console.log(response);
//device ID const checkbox_NPM = document.getElementById("check_NPM");
const device_ID = document.getElementById("device_ID"); const checkbox_bme = document.getElementById("check_bme280");
device_ID.value = data.deviceID.toUpperCase(); const checkbox_envea = document.getElementById("check_envea");
const checkbox_solar = document.getElementById("check_solarBattery");
const checkbox_wind = document.getElementById("check_WindMeter");
//get system time and RTC module checkbox_NPM.checked = response["NPM/get_data_modbus_v3.py"];
$.ajax({ checkbox_bme.checked = response["BME280/get_data_v2.py"];
url: 'launcher.php?type=sys_RTC_module_time', checkbox_envea.checked = response["envea/read_value_v2.py"];
dataType: 'json', // Specify that you expect a JSON response checkbox_solar.checked = response["MPPT/read.py"];
method: 'GET', // Use GET or POST depending on your needs checkbox_wind.checked = response["windMeter/read.py"];
success: function(response) {
console.log(response);
// Update the input fields with the received JSON data
document.getElementById("sys_local_time").value = response.system_local_time;
document.getElementById("sys_UTC_time").value = response.system_utc_time;
document.getElementById("RTC_utc_time").value = response.rtc_module_time;
// Get the time difference //si sonde envea is true
const timeDiff = response.time_difference_seconds; if (response["envea/read_value_v2.py"]) {
add_sondeEnveaContainer();
}
},
error: function(xhr, status, error) {
console.error('AJAX request failed:', status, error);
}
});//end AJAX
// Reference to the alert container //OLD way to get config (JSON)
const alertContainer = document.getElementById("alert_container"); /*
fetch('../config.json') // Replace 'deviceID.txt' with 'config.json'
.then(response => response.json()) // Parse response as JSON
.then(data => {
console.log("Getting config file (onload)");
//get device ID
//document.getElementById('pageTitle_plus_ID').innerText = 'token: ' + deviceID;
//get device Name
//const deviceName = data.deviceName;
// Remove any previous alert //get BME check
alertContainer.innerHTML = ""; const checkbox = document.getElementById("check_bme280");
checkbox.checked = data["BME280/get_data_v2.py"];
// Add an alert based on time difference //get NPM-5channels check
if (typeof timeDiff === "number") { const checkbox_NPM_5channels = document.getElementById("check_NPM_5channels");
if (timeDiff >= 0 && timeDiff <= 10) { checkbox_NPM_5channels.checked = data["NextPM_5channels"];
alertContainer.innerHTML = `
<div class="alert alert-success" role="alert">
RTC and system time are in sync (Difference: ${timeDiff} sec).
</div>`;
} else if (timeDiff > 10) {
alertContainer.innerHTML = `
<div class="alert alert-danger" role="alert">
RTC time is out of sync! (Difference: ${timeDiff} sec).
</div>`;
}
}
},
error: function(xhr, status, error) {
console.error('AJAX request failed:', status, error);
}
});
//get local RTC //get sonde Envea check
$.ajax({ const checkbox_envea = document.getElementById("check_envea");
url: 'launcher.php?type=RTC_time', checkbox_envea.checked = data["envea/read_value_v2.py"];
dataType: 'text', // Specify that you expect a JSON response
method: 'GET', // Use GET or POST depending on your needs
success: function(response) {
console.log("Local RTC: " + response);
const RTC_Element = document.getElementById("RTC_time");
RTC_Element.textContent = response;
},
error: function(xhr, status, error) {
console.error('AJAX request failed:', status, error);
}
});
}) //device name
.catch(error => console.error('Error loading config.json:', error)); //const device_name = document.getElementById("device_name");
} //device_name.value = data.deviceName;
})
.catch(error => console.error('Error loading config.json:', error));
*/
//get system time and RTC module
$.ajax({
url: 'launcher.php?type=sys_RTC_module_time',
dataType: 'json', // Specify that you expect a JSON response
method: 'GET', // Use GET or POST depending on your needs
success: function(response) {
console.log("Getting RTC times");
console.log(response);
// Update the input fields with the received JSON data
document.getElementById("sys_local_time").value = response.system_local_time;
document.getElementById("sys_UTC_time").value = response.system_utc_time;
document.getElementById("RTC_utc_time").value = response.rtc_module_time;
// Get the time difference
const timeDiff = response.time_difference_seconds;
// Reference to the alert container
const alertContainer = document.getElementById("alert_container");
// Remove any previous alert
alertContainer.innerHTML = "";
// Add an alert based on time difference
if (typeof timeDiff === "number") {
if (timeDiff >= 0 && timeDiff <= 10) {
alertContainer.innerHTML = `
<div class="alert alert-success" role="alert">
RTC and system time are in sync (Difference: ${timeDiff} sec).
</div>`;
} else if (timeDiff > 10) {
alertContainer.innerHTML = `
<div class="alert alert-danger" role="alert">
RTC time is out of sync! (Difference: ${timeDiff} sec).
</div>`;
}
}
},
error: function(xhr, status, error) {
console.error('AJAX request failed:', status, error);
}
});//end AJAX
//get local RTC
$.ajax({
url: 'launcher.php?type=RTC_time',
dataType: 'text', // Specify that you expect a JSON response
method: 'GET', // Use GET or POST depending on your needs
success: function(response) {
//console.log("Local RTC: " + response);
const RTC_Element = document.getElementById("RTC_time");
RTC_Element.textContent = response;
},
error: function(xhr, status, error) {
console.error('AJAX request failed:', status, error);
}
}); //end AJAx
} //end window.onload
function update_config_sqlite(param, value){
console.log("Updating sqlite ",param," : ", value);
const toastLiveExample = document.getElementById('liveToast')
const toastBody = toastLiveExample.querySelector('.toast-body');
$.ajax({
url: 'launcher.php?type=update_config_sqlite&param='+param+'&value='+value,
dataType: 'json', // Specify that you expect a JSON response
method: 'GET', // Use GET or POST depending on your needs
cache: false, // Prevent AJAX from caching
success: function(response) {
console.log(response);
// Format the response nicely
let formattedMessage = '';
if (response.success) {
// Success message
toastLiveExample.classList.remove('text-bg-danger');
toastLiveExample.classList.add('text-bg-success');
formattedMessage = `
<strong>Success!</strong><br>
Parameter: ${response.param || param}<br>
Value: ${response.value || checked}<br>
${response.message || ''}
`;
} else {
// Error message
toastLiveExample.classList.remove('text-bg-success');
toastLiveExample.classList.add('text-bg-danger');
formattedMessage = `
<strong>Error!</strong><br>
${response.error || 'Unknown error'}<br>
Parameter: ${response.param || param}
`;
}
// Update the toast body with formatted content
toastBody.innerHTML = formattedMessage;
// Show the toast
const toastBootstrap = bootstrap.Toast.getOrCreateInstance(toastLiveExample)
toastBootstrap.show()
},
error: function(xhr, status, error) {
console.error('AJAX request failed:', status, error);
}
});
}
function update_config_scripts_sqlite(param, value) {
console.log("Updating scripts sqlite ", param, " : ", value);
const toastLiveExample = document.getElementById('liveToast')
const toastBody = toastLiveExample.querySelector('.toast-body');
$.ajax({
url: 'launcher.php?type=update_config_scripts_sqlite&param=' + param + '&value=' + value,
dataType: 'json',
method: 'GET',
cache: false,
success: function(response) {
console.log(response);
// Format the response nicely
let formattedMessage = '';
if (response.success) {
// Success message
toastLiveExample.classList.remove('text-bg-danger');
toastLiveExample.classList.add('text-bg-success');
formattedMessage = `
<strong>Success!</strong><br>
Parameter: ${response.script_path || param}<br>
Value: ${response.enabled !== undefined ? response.enabled : value}<br>
${response.message || ''}
`;
if (response.script_path == "envea/read_value_v2.py") {
console.log("envea sondes activated");
add_sondeEnveaContainer();
}
} else {
// Error message
toastLiveExample.classList.remove('text-bg-success');
toastLiveExample.classList.add('text-bg-danger');
formattedMessage = `
<strong>Error!</strong><br>
${response.error || 'Unknown error'}<br>
Parameter: ${response.script_path || param}
`;
}
// Update the toast body with formatted content
toastBody.innerHTML = formattedMessage;
// Show the toast
const toastBootstrap = bootstrap.Toast.getOrCreateInstance(toastLiveExample)
toastBootstrap.show()
},
error: function(xhr, status, error) {
console.error('AJAX request failed:', status, error);
}
});
}
function update_config(param, value){ function update_config(param, value){
@@ -358,7 +556,7 @@ function set_RTC_withNTP(){
error: function(xhr, status, error) { error: function(xhr, status, error) {
console.error('AJAX request failed:', status, error); console.error('AJAX request failed:', status, error);
} }
}); }); //end ajax
} }
function set_RTC_withBrowser(){ function set_RTC_withBrowser(){
@@ -386,7 +584,321 @@ function set_RTC_withBrowser(){
error: function(xhr, status, error) { error: function(xhr, status, error) {
console.error('AJAX request failed:', status, error); console.error('AJAX request failed:', status, error);
} }
}); }); //end ajax
}
/*
____ _ _____
/ ___| ___ _ __ __| | ___ ___ | ____|_ ____ _____ __ _
\___ \ / _ \| '_ \ / _` |/ _ \/ __| | _| | '_ \ \ / / _ \/ _` |
___) | (_) | | | | (_| | __/\__ \ | |___| | | \ V / __/ (_| |
|____/ \___/|_| |_|\__,_|\___||___/ |_____|_| |_|\_/ \___|\__,_|
*/
function add_sondeEnveaContainer() {
console.log("Sonde Envea is true: need to add container!");
// Getting envea_sondes_table data
$.ajax({
url: 'launcher.php?type=get_envea_sondes_table_sqlite',
dataType: 'json',
method: 'GET',
success: function(sondes) {
console.log("Getting SQLite envea sondes table:");
console.log(sondes);
// Create container div if it doesn't exist
if ($('#sondes_envea_div').length === 0) {
$('#advanced_options').append('<div id="sondes_envea_div" class="input-group mt-4 border p-3 rounded"><legend>Sondes Envea</legend><p>Plouf</p></div>');
} else {
// Clear existing content if container exists
$('#sondes_envea_div').html('<legend>Sondes Envea</legend>');
$('#envea_table').html('<table class="table table-striped table-bordered">'+
'<thead><tr><th scope="col">Software</th><th scope="col">Hardware (PCB)</th></tr></thead>'+
'<tbody>' +
'<tr><td>ttyAMA5</td><td>NPM1</td></tr>' +
'<tr><td>ttyAMA4</td><td>NPM2</td></tr>' +
'<tr><td>ttyAMA3</td><td>NPM3</td></tr>' +
'<tr><td>ttyAMA2</td><td>SARA</td></tr>' +
'</tbody></table>');
}
// Loop through each sonde and create UI elements
sondes.forEach(function(sonde) {
// Create a unique ID for this sonde
const sondeId = `sonde_${sonde.id}`;
// Create HTML for this sonde
const sondeHtml = `
<div class="input-group mb-3" id="${sondeId}_container">
<div class="input-group-text">
<input class="form-check-input mt-0" type="checkbox" id="${sondeId}_enabled"
${sonde.connected ? 'checked' : ''}
onchange="updateSondeStatus(${sonde.id}, this.checked)">
</div>
<input type="text" class="form-control" placeholder="Name" value="${sonde.name}"
id="${sondeId}_name" onchange="updateSondeName(${sonde.id}, this.value)">
<input type="text" class="form-control" placeholder="Port" value="${sonde.port}"
id="${sondeId}_port" onchange="updateSondePort(${sonde.id}, this.value)">
<input type="number" class="form-control" placeholder="Coefficient" value="${sonde.coefficient}"
id="${sondeId}_coefficient" onchange="updateSondeCoefficient(${sonde.id}, this.value)">
</div>
`;
// Append this sonde to the container
$('#sondes_envea_div').append(sondeHtml);
});
},
error: function(xhr, status, error) {
console.error('AJAX request failed:', status, error);
}
});
}
// Helper functions for updating sonde properties
function updateSondeStatus(id, connected) {
console.log(`Updating sonde ${id} connected status to: ${connected}`);
const toastLiveExample = document.getElementById('liveToast');
const toastBody = toastLiveExample.querySelector('.toast-body');
$.ajax({
url: `launcher.php?type=update_sonde&id=${id}&field=connected&value=${connected ? 1 : 0}`,
dataType: 'json',
method: 'GET',
cache: false,
success: function(response) {
console.log('Sonde status updated:', response);
// Format the response for toast
let formattedMessage = '';
if (response.success) {
// Success message
toastLiveExample.classList.remove('text-bg-danger');
toastLiveExample.classList.add('text-bg-success');
formattedMessage = `
<strong>Success!</strong><br>
Sonde ID: ${response.id}<br>
Connected: ${connected ? "Yes" : "No"}<br>
${response.message || ''}
`;
} else {
// Error message
toastLiveExample.classList.remove('text-bg-success');
toastLiveExample.classList.add('text-bg-danger');
formattedMessage = `
<strong>Error!</strong><br>
${response.error || 'Unknown error'}<br>
Sonde ID: ${id}
`;
}
// Update and show toast
toastBody.innerHTML = formattedMessage;
const toastBootstrap = bootstrap.Toast.getOrCreateInstance(toastLiveExample);
toastBootstrap.show();
},
error: function(xhr, status, error) {
console.error('Failed to update sonde status:', error);
// Show error toast
toastLiveExample.classList.remove('text-bg-success');
toastLiveExample.classList.add('text-bg-danger');
toastBody.innerHTML = `
<strong>Request Failed!</strong><br>
Error: ${error}<br>
Sonde ID: ${id}
`;
const toastBootstrap = bootstrap.Toast.getOrCreateInstance(toastLiveExample);
toastBootstrap.show();
}
});
}
function updateSondeName(id, name) {
console.log(`Updating sonde ${id} name to: ${name}`);
const toastLiveExample = document.getElementById('liveToast');
const toastBody = toastLiveExample.querySelector('.toast-body');
$.ajax({
url: `launcher.php?type=update_sonde&id=${id}&field=name&value=${encodeURIComponent(name)}`,
dataType: 'json',
method: 'GET',
cache: false,
success: function(response) {
console.log('Sonde name updated:', response);
// Format the response for toast
let formattedMessage = '';
if (response.success) {
// Success message
toastLiveExample.classList.remove('text-bg-danger');
toastLiveExample.classList.add('text-bg-success');
formattedMessage = `
<strong>Success!</strong><br>
Sonde ID: ${response.id}<br>
Name: ${name}<br>
${response.message || ''}
`;
} else {
// Error message
toastLiveExample.classList.remove('text-bg-success');
toastLiveExample.classList.add('text-bg-danger');
formattedMessage = `
<strong>Error!</strong><br>
${response.error || 'Unknown error'}<br>
Sonde ID: ${id}
`;
}
// Update and show toast
toastBody.innerHTML = formattedMessage;
const toastBootstrap = bootstrap.Toast.getOrCreateInstance(toastLiveExample);
toastBootstrap.show();
},
error: function(xhr, status, error) {
console.error('Failed to update sonde name:', error);
// Show error toast
toastLiveExample.classList.remove('text-bg-success');
toastLiveExample.classList.add('text-bg-danger');
toastBody.innerHTML = `
<strong>Request Failed!</strong><br>
Error: ${error}<br>
Sonde ID: ${id}
`;
const toastBootstrap = bootstrap.Toast.getOrCreateInstance(toastLiveExample);
toastBootstrap.show();
}
});
}
function updateSondePort(id, port) {
console.log(`Updating sonde ${id} port to: ${port}`);
const toastLiveExample = document.getElementById('liveToast');
const toastBody = toastLiveExample.querySelector('.toast-body');
$.ajax({
url: `launcher.php?type=update_sonde&id=${id}&field=port&value=${encodeURIComponent(port)}`,
dataType: 'json',
method: 'GET',
cache: false,
success: function(response) {
console.log('Sonde port updated:', response);
// Format the response for toast
let formattedMessage = '';
if (response.success) {
// Success message
toastLiveExample.classList.remove('text-bg-danger');
toastLiveExample.classList.add('text-bg-success');
formattedMessage = `
<strong>Success!</strong><br>
Sonde ID: ${response.id}<br>
Port: ${port}<br>
${response.message || ''}
`;
} else {
// Error message
toastLiveExample.classList.remove('text-bg-success');
toastLiveExample.classList.add('text-bg-danger');
formattedMessage = `
<strong>Error!</strong><br>
${response.error || 'Unknown error'}<br>
Sonde ID: ${id}
`;
}
// Update and show toast
toastBody.innerHTML = formattedMessage;
const toastBootstrap = bootstrap.Toast.getOrCreateInstance(toastLiveExample);
toastBootstrap.show();
},
error: function(xhr, status, error) {
console.error('Failed to update sonde port:', error);
// Show error toast
toastLiveExample.classList.remove('text-bg-success');
toastLiveExample.classList.add('text-bg-danger');
toastBody.innerHTML = `
<strong>Request Failed!</strong><br>
Error: ${error}<br>
Sonde ID: ${id}
`;
const toastBootstrap = bootstrap.Toast.getOrCreateInstance(toastLiveExample);
toastBootstrap.show();
}
});
}
function updateSondeCoefficient(id, coefficient) {
console.log(`Updating sonde ${id} coefficient to: ${coefficient}`);
const toastLiveExample = document.getElementById('liveToast');
const toastBody = toastLiveExample.querySelector('.toast-body');
$.ajax({
url: `launcher.php?type=update_sonde&id=${id}&field=coefficient&value=${coefficient}`,
dataType: 'json',
method: 'GET',
cache: false,
success: function(response) {
console.log('Sonde coefficient updated:', response);
// Format the response for toast
let formattedMessage = '';
if (response.success) {
// Success message
toastLiveExample.classList.remove('text-bg-danger');
toastLiveExample.classList.add('text-bg-success');
formattedMessage = `
<strong>Success!</strong><br>
Sonde ID: ${response.id}<br>
Coefficient: ${coefficient}<br>
${response.message || ''}
`;
} else {
// Error message
toastLiveExample.classList.remove('text-bg-success');
toastLiveExample.classList.add('text-bg-danger');
formattedMessage = `
<strong>Error!</strong><br>
${response.error || 'Unknown error'}<br>
Sonde ID: ${id}
`;
}
// Update and show toast
toastBody.innerHTML = formattedMessage;
const toastBootstrap = bootstrap.Toast.getOrCreateInstance(toastLiveExample);
toastBootstrap.show();
},
error: function(xhr, status, error) {
console.error('Failed to update sonde coefficient:', error);
// Show error toast
toastLiveExample.classList.remove('text-bg-success');
toastLiveExample.classList.add('text-bg-danger');
toastBody.innerHTML = `
<strong>Request Failed!</strong><br>
Error: ${error}<br>
Sonde ID: ${id}
`;
const toastBootstrap = bootstrap.Toast.getOrCreateInstance(toastLiveExample);
toastBootstrap.show();
}
});
} }

View File

@@ -135,6 +135,30 @@
window.onload = function() { window.onload = function() {
//NEW way to get data from SQLITE
$.ajax({
url: 'launcher.php?type=get_config_sqlite',
dataType:'json',
//dataType: 'json', // Specify that you expect a JSON response
method: 'GET', // Use GET or POST depending on your needs
success: function(response) {
console.log("Getting SQLite config table:");
console.log(response);
//get device Name (for the side bar)
const deviceName = response.deviceName;
const elements = document.querySelectorAll('.sideBar_sensorName');
elements.forEach((element) => {
element.innerText = deviceName;
});
},
error: function(xhr, status, error) {
console.error('AJAX request failed:', status, error);
}
});
/* OLD way of getting config data
fetch('../config.json') // Replace 'deviceID.txt' with 'config.json' fetch('../config.json') // Replace 'deviceID.txt' with 'config.json'
.then(response => response.json()) // Parse response as JSON .then(response => response.json()) // Parse response as JSON
.then(data => { .then(data => {
@@ -151,7 +175,12 @@ window.onload = function() {
elements.forEach((element) => { elements.forEach((element) => {
element.innerText = deviceName; element.innerText = deviceName;
}); });
//end fetch config
})
.catch(error => console.error('Error loading config.json:', error));
//end windows on load
*/
//get local RTC //get local RTC
$.ajax({ $.ajax({
url: 'launcher.php?type=RTC_time', url: 'launcher.php?type=RTC_time',
@@ -421,10 +450,6 @@ window.onload = function() {
//end fetch config
})
.catch(error => console.error('Error loading config.json:', error));
//end windows on load
} }
</script> </script>

View File

@@ -1,13 +1,16 @@
<?php <?php
//Prevents caching → Adds headers to ensure fresh response. //Prevents caching → Adds headers to ensure fresh response.
// to test this page http://192.168.1.127/html/launcher.php?type=get_config_scripts_sqlite
header("Content-Type: application/json"); header("Content-Type: application/json");
header("Cache-Control: no-store, no-cache, must-revalidate, max-age=0"); header("Cache-Control: no-store, no-cache, must-revalidate, max-age=0");
header("Pragma: no-cache"); header("Pragma: no-cache");
$database_path = "/var/www/nebuleair_pro_4g/sqlite/sensors.db";
$type=$_GET['type']; $type=$_GET['type'];
if ($type == "get_npm_sqlite_data") { if ($type == "get_npm_sqlite_data") {
$database_path = "/var/www/nebuleair_pro_4g/sqlite/sensors.db";
//echo "Getting data from sqlite database"; //echo "Getting data from sqlite database";
try { try {
$db = new PDO("sqlite:$database_path"); $db = new PDO("sqlite:$database_path");
@@ -25,8 +28,327 @@ if ($type == "get_npm_sqlite_data") {
} }
} }
/*
*/
//GETING data from config_table (SQLite DB)
if ($type == "get_config_sqlite") {
try {
$db = new PDO("sqlite:$database_path");
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
// Get all main configuration entries
$config_query = $db->query("SELECT key, value, type FROM config_table");
$config_data = $config_query->fetchAll(PDO::FETCH_ASSOC);
// Convert data types according to their 'type' field
$result = [];
foreach ($config_data as $item) {
$key = $item['key'];
$value = $item['value'];
$type = $item['type'];
// Convert value based on its type
switch ($type) {
case 'bool':
$parsed_value = ($value == '1' || $value == 'true') ? true : false;
break;
case 'int':
$parsed_value = intval($value);
break;
case 'float':
$parsed_value = floatval($value);
break;
default:
$parsed_value = $value;
}
$result[$key] = $parsed_value;
}
// Return JSON response
header('Content-Type: application/json');
echo json_encode($result, JSON_PRETTY_PRINT);
} catch (PDOException $e) {
// Return error as JSON
header('Content-Type: application/json');
echo json_encode(['error' => 'Database error: ' . $e->getMessage()]);
}
}
/*
*/
//GETING data from config_scrips_table (SQLite DB)
if ($type == "get_config_scripts_sqlite") {
try {
$db = new PDO("sqlite:$database_path");
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
// Get all main configuration entries
$config_query = $db->query("SELECT * FROM config_scripts_table");
$config_data = $config_query->fetchAll(PDO::FETCH_ASSOC);
// Convert data types according to their 'type' field
$result = [];
foreach ($config_data as $item) {
$script_path = $item['script_path'];
$enabled = $item['enabled'];
// Convert the enabled field to a proper boolean
$result[$script_path] = ($enabled == 1);
}
// Return JSON response
header('Content-Type: application/json');
echo json_encode($result, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
} catch (PDOException $e) {
// Return error as JSON
header('Content-Type: application/json');
echo json_encode(['error' => 'Database error: ' . $e->getMessage()]);
}
}
/*
*/
//GETING data from envea_sondes_table (SQLite DB)
if ($type == "get_envea_sondes_table_sqlite") {
try {
$db = new PDO("sqlite:$database_path");
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
// Get all entries from envea_sondes_table
$query = $db->query("SELECT id, connected, port, name, coefficient FROM envea_sondes_table");
$data = $query->fetchAll(PDO::FETCH_ASSOC);
// Convert data types appropriately
$result = [];
foreach ($data as $item) {
// Create object for each sonde with proper data types
$sonde = [
'id' => (int)$item['id'],
'connected' => $item['connected'] == 1, // Convert to boolean
'port' => $item['port'],
'name' => $item['name'],
'coefficient' => (float)$item['coefficient'] // Convert to float
];
// Add to results array
$result[] = $sonde;
}
// Return JSON response
header('Content-Type: application/json');
echo json_encode($result, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
} catch (PDOException $e) {
// Return error as JSON
header('Content-Type: application/json');
echo json_encode(['error' => 'Database error: ' . $e->getMessage()]);
}
}
//UPDATING the config_table from SQLite DB
if ($type == "update_config_sqlite") {
$param = $_GET['param'] ?? null;
$value = $_GET['value'] ?? null;
if ($param === null || $value === null) {
echo json_encode(["error" => "Missing parameter or value"]);
exit;
}
try {
$db = new PDO("sqlite:$database_path");
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
// First, check if parameter exists and get its type
$checkStmt = $db->prepare("SELECT type FROM config_table WHERE key = :param");
$checkStmt->bindParam(':param', $param);
$checkStmt->execute();
$result = $checkStmt->fetch(PDO::FETCH_ASSOC);
if ($result) {
// Parameter exists, determine type and update
$type = $result['type'];
// Convert value according to type if needed
$convertedValue = $value;
if ($type == "bool") {
// Convert various boolean representations to 0/1
$convertedValue = (filter_var($value, FILTER_VALIDATE_BOOLEAN)) ? "1" : "0";
} elseif ($type == "int") {
$convertedValue = (string)intval($value);
} elseif ($type == "float") {
$convertedValue = (string)floatval($value);
}
// Update the value
$updateStmt = $db->prepare("UPDATE config_table SET value = :value WHERE key = :param");
$updateStmt->bindParam(':value', $convertedValue);
$updateStmt->bindParam(':param', $param);
$updateStmt->execute();
echo json_encode([
"success" => true,
"message" => "Configuration updated successfully",
"param" => $param,
"value" => $convertedValue,
"type" => $type
]);
} else {
echo json_encode([
"error" => "Parameter not found in configuration",
"param" => $param
]);
}
} catch (PDOException $e) {
echo json_encode(["error" => $e->getMessage()]);
}
}
//UPDATING the config_scripts table from SQLite DB
if ($type == "update_config_scripts_sqlite") {
$script_path = $_GET['param'] ?? null;
$enabled = $_GET['value'] ?? null;
if ($script_path === null || $enabled === null) {
echo json_encode(["error" => "Missing parameter or value"]);
exit;
}
try {
$db = new PDO("sqlite:$database_path");
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
// First, check if parameter exists and get its type
$checkStmt = $db->prepare("SELECT enabled FROM config_scripts_table WHERE script_path = :script_path");
$checkStmt->bindParam(':script_path', $script_path);
$checkStmt->execute();
$result = $checkStmt->fetch(PDO::FETCH_ASSOC);
if ($result) {
// Convert enabled value to 0 or 1
$enabledValue = (filter_var($enabled, FILTER_VALIDATE_BOOLEAN)) ? 1 : 0;
// Update the enabled status
$updateStmt = $db->prepare("UPDATE config_scripts_table SET enabled = :enabled WHERE script_path = :script_path");
$updateStmt->bindParam(':enabled', $enabledValue, PDO::PARAM_INT);
$updateStmt->bindParam(':script_path', $script_path);
$updateStmt->execute();
echo json_encode([
"success" => true,
"message" => "Script configuration updated successfully",
"script_path" => $script_path,
"enabled" => (bool)$enabledValue
], JSON_UNESCAPED_SLASHES); // Prevent escaping forward slashes
} else {
echo json_encode([
"error" => "Script path not found in configuration",
"script_path" => $script_path
], JSON_UNESCAPED_SLASHES); // Prevent escaping forward slashes
}
} catch (PDOException $e) {
echo json_encode(["error" => $e->getMessage()]);
}
}
//UPDATING the envea_sondes_table table from SQLite DB
if ($type == "update_sonde") {
$id = $_GET['id'] ?? null;
$field = $_GET['field'] ?? null;
$value = $_GET['value'] ?? null;
// Validate parameters
if ($id === null || $field === null || $value === null) {
echo json_encode([
"success" => false,
"error" => "Missing required parameters (id, field, or value)"
]);
exit;
}
// Validate field name (whitelist approach for security)
$allowed_fields = ['connected', 'port', 'name', 'coefficient'];
if (!in_array($field, $allowed_fields)) {
echo json_encode([
"success" => false,
"error" => "Invalid field name: " . $field
]);
exit;
}
try {
// Connect to the database
$db = new PDO("sqlite:$database_path");
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
// Check if the sonde exists
$checkStmt = $db->prepare("SELECT id FROM envea_sondes_table WHERE id = :id");
$checkStmt->bindParam(':id', $id, PDO::PARAM_INT);
$checkStmt->execute();
if (!$checkStmt->fetch()) {
echo json_encode([
"success" => false,
"error" => "Sonde with ID $id not found"
]);
exit;
}
// Process value based on field type
if ($field == 'connected') {
// Convert to integer (0 or 1)
$processedValue = filter_var($value, FILTER_VALIDATE_BOOLEAN) ? 1 : 0;
$paramType = PDO::PARAM_INT;
} else if ($field == 'coefficient') {
// Convert to float
$processedValue = floatval($value);
$paramType = PDO::PARAM_STR; // SQLite doesn't have PARAM_FLOAT
} else {
// For text fields (port, name)
$processedValue = $value;
$paramType = PDO::PARAM_STR;
}
// Update the sonde record
$updateStmt = $db->prepare("UPDATE envea_sondes_table SET $field = :value WHERE id = :id");
$updateStmt->bindParam(':value', $processedValue, $paramType);
$updateStmt->bindParam(':id', $id, PDO::PARAM_INT);
$updateStmt->execute();
// Return success response
echo json_encode([
"success" => true,
"message" => "Sonde $id updated successfully",
"field" => $field,
"value" => $processedValue
]);
} catch (PDOException $e) {
// Return error as JSON
echo json_encode([
"success" => false,
"error" => "Database error: " . $e->getMessage()
]);
}
}
//update the config (old JSON updating)
if ($type == "update_config") { if ($type == "update_config") {
echo "updating...."; echo "updating.... ";
$param=$_GET['param']; $param=$_GET['param'];
$value=$_GET['value']; $value=$_GET['value'];
$configFile = '../config.json'; $configFile = '../config.json';
@@ -269,7 +591,7 @@ if ($type == "sara_connectNetwork") {
$timeout=$_GET['timeout']; $timeout=$_GET['timeout'];
$networkID=$_GET['networkID']; $networkID=$_GET['networkID'];
echo "updating SARA_R4_networkID in config file"; //echo "updating SARA_R4_networkID in config file";
// Convert `networkID` to an integer (or float if needed) // Convert `networkID` to an integer (or float if needed)
$networkID = is_numeric($networkID) ? (strpos($networkID, '.') !== false ? (float)$networkID : (int)$networkID) : 0; $networkID = is_numeric($networkID) ? (strpos($networkID, '.') !== false ? (float)$networkID : (int)$networkID) : 0;
#save to config.json #save to config.json
@@ -296,10 +618,10 @@ if ($type == "sara_connectNetwork") {
die("Error: Could not write to JSON file."); die("Error: Could not write to JSON file.");
} }
echo "SARA_R4_networkID updated successfully."; //echo "SARA_R4_networkID updated successfully.";
echo "connecting to network... please wait..."; //echo "connecting to network... please wait...";
$command = 'sudo /usr/bin/python3 /var/www/nebuleair_pro_4g/SARA/sara_connectNetwork.py ' . $port . ' ' . $networkID . ' ' . $timeout; $command = 'sudo /usr/bin/python3 /var/www/nebuleair_pro_4g/SARA/sara_connectNetwork.py ' . $port . ' ' . $networkID . ' ' . $timeout;
$output = shell_exec($command); $output = shell_exec($command);
echo $output; echo $output;

View File

@@ -179,40 +179,46 @@ window.onload = function() {
getModem_busy_status(); getModem_busy_status();
setInterval(getModem_busy_status, 2000); setInterval(getModem_busy_status, 2000);
fetch('../config.json') // Replace 'deviceID.txt' with 'config.json' //NEW way to get config (SQLite)
.then(response => response.json()) // Parse response as JSON $.ajax({
.then(data => { url: 'launcher.php?type=get_config_sqlite',
console.log("Getting config file (onload)"); dataType:'json',
//get device ID //dataType: 'json', // Specify that you expect a JSON response
const deviceID = data.deviceID.trim().toUpperCase(); method: 'GET', // Use GET or POST depending on your needs
// document.getElementById('pageTitle_plus_ID').innerText = 'token: ' + deviceID; success: function(response) {
//get device Name console.log("Getting SQLite config table:");
const deviceName = data.deviceName; console.log(response);
const elements = document.querySelectorAll('.sideBar_sensorName'); //device name_side bar
elements.forEach((element) => { const elements = document.querySelectorAll('.sideBar_sensorName');
element.innerText = deviceName; elements.forEach((element) => {
}); element.innerText = response.deviceName;
});
},
error: function(xhr, status, error) {
console.error('AJAX request failed:', status, error);
}
});//end AJAX
//get local RTC //get local RTC
$.ajax({ $.ajax({
url: 'launcher.php?type=RTC_time', url: 'launcher.php?type=RTC_time',
dataType: 'text', // Specify that you expect a JSON response dataType: 'text', // Specify that you expect a JSON response
method: 'GET', // Use GET or POST depending on your needs method: 'GET', // Use GET or POST depending on your needs
success: function(response) { success: function(response) {
console.log("Local RTC: " + response); console.log("Local RTC: " + response);
const RTC_Element = document.getElementById("RTC_time"); const RTC_Element = document.getElementById("RTC_time");
RTC_Element.textContent = response; RTC_Element.textContent = response;
}, },
error: function(xhr, status, error) { error: function(xhr, status, error) {
console.error('AJAX request failed:', status, error); console.error('AJAX request failed:', status, error);
} }
}); });
})
.catch(error => console.error('Error loading config.json:', error)); }//end onload
}
function clear_loopLogs(){ function clear_loopLogs(){

View File

@@ -59,11 +59,12 @@
</div> </div>
<span id="modem_status_message"></span> <span id="modem_status_message"></span>
<!--
<h3> <h3>
Status Status
<span id="modem-status" class="badge">Loading...</span> <span id="modem-status" class="badge">Loading...</span>
</h3> </h3>
-->
<div class="row mb-3"> <div class="row mb-3">
<div class="col-sm-3"> <div class="col-sm-3">
@@ -71,7 +72,7 @@
<div class="card-body"> <div class="card-body">
<p class="card-text">General information. </p> <p class="card-text">General information. </p>
<button class="btn btn-primary" onclick="getData_saraR4('ttyAMA2', 'ATI', 2)">Get Data</button> <button class="btn btn-primary" onclick="getData_saraR4('ttyAMA2', 'ATI', 1)">Get Data</button>
<div id="loading_ttyAMA2_ATI" class="spinner-border spinner-border-sm" style="display: none;" role="status"></div> <div id="loading_ttyAMA2_ATI" class="spinner-border spinner-border-sm" style="display: none;" role="status"></div>
<div id="response_ttyAMA2_ATI"></div> <div id="response_ttyAMA2_ATI"></div>
@@ -84,7 +85,7 @@
<div class="card-body"> <div class="card-body">
<p class="card-text">SIM card information.</p> <p class="card-text">SIM card information.</p>
<button class="btn btn-primary" onclick="getData_saraR4('ttyAMA2', 'AT+CCID?', 2)">Get Data</button> <button class="btn btn-primary" onclick="getData_saraR4('ttyAMA2', 'AT+CCID?', 1)">Get Data</button>
<div id="loading_ttyAMA2_AT_CCID_" class="spinner-border spinner-border-sm" style="display: none;" role="status"></div> <div id="loading_ttyAMA2_AT_CCID_" class="spinner-border spinner-border-sm" style="display: none;" role="status"></div>
<div id="response_ttyAMA2_AT_CCID_"></div> <div id="response_ttyAMA2_AT_CCID_"></div>
</div> </div>
@@ -109,7 +110,7 @@
<div class="card"> <div class="card">
<div class="card-body"> <div class="card-body">
<p class="card-text">Signal strength </p> <p class="card-text">Signal strength </p>
<button class="btn btn-primary" onclick="getData_saraR4('ttyAMA2', 'AT+CSQ', 2)">Get Data</button> <button class="btn btn-primary" onclick="getData_saraR4('ttyAMA2', 'AT+CSQ', 1)">Get Data</button>
<div id="loading_ttyAMA2_AT_CSQ" class="spinner-border spinner-border-sm" style="display: none;" role="status"></div> <div id="loading_ttyAMA2_AT_CSQ" class="spinner-border spinner-border-sm" style="display: none;" role="status"></div>
<div id="response_ttyAMA2_AT_CSQ"></div> <div id="response_ttyAMA2_AT_CSQ"></div>
</table> </table>
@@ -121,7 +122,7 @@
<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">Modem Reset </p>
<button class="btn btn-danger" onclick="getData_saraR4('ttyAMA2', 'AT+CFUN=15', 2)">Reset</button> <button class="btn btn-danger" onclick="getData_saraR4('ttyAMA2', 'AT+CFUN=15', 1)">Reset</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> </table>
@@ -304,7 +305,20 @@
</div> </div>
<!-- toast -->
<div class="toast-container position-fixed bottom-0 end-0 p-3">
<div id="liveToast" class="toast align-items-center text-bg-primary border-1" role="alert" aria-live="assertive" aria-atomic="true">
<div class="d-flex">
<div class="toast-body">
Hello, world! This is a toast message.
</div>
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast" aria-label="Close"></button>
</div>
</div>
</div>
</main> </main>
</div> </div>
</div> </div>
@@ -317,35 +331,62 @@
<script src="assets/js/bootstrap.bundle.js"></script> <script src="assets/js/bootstrap.bundle.js"></script>
<script> <script>
document.addEventListener('DOMContentLoaded', function () { document.addEventListener('DOMContentLoaded', function () {
const elementsToLoad = [ const elementsToLoad = [
{ id: 'topbar', file: 'topbar.html' }, { id: 'topbar', file: 'topbar.html' },
{ id: 'sidebar', file: 'sidebar.html' }, { id: 'sidebar', file: 'sidebar.html' },
{ id: 'sidebar_mobile', file: 'sidebar.html' } { id: 'sidebar_mobile', file: 'sidebar.html' }
]; ];
elementsToLoad.forEach(({ id, file }) => { elementsToLoad.forEach(({ id, file }) => {
fetch(file) fetch(file)
.then(response => response.text()) .then(response => response.text())
.then(data => { .then(data => {
const element = document.getElementById(id); const element = document.getElementById(id);
if (element) { if (element) {
element.innerHTML = data; element.innerHTML = data;
}
})
.catch(error => console.error(`Error loading ${file}:`, error));
});
//OLD way to retreive data from JSON
/*
fetch('../config.json') // Replace 'deviceID.txt' with 'config.json'
.then(response => response.json()) // Parse response as JSON
.then(data => {
console.log("Getting config file (onload)");
//modem config mode
const check_modem_configMode = document.getElementById("check_modem_configMode");
check_modem_configMode.checked = data.modem_config_mode;
console.log("Modem configuration: " + data.modem_config_mode);
})
*/
//NEW way to get data from SQLITE
$.ajax({
url: 'launcher.php?type=get_config_sqlite',
dataType:'json',
//dataType: 'json', // Specify that you expect a JSON response
method: 'GET', // Use GET or POST depending on your needs
success: function(response) {
console.log("Getting SQLite config table:");
console.log(response);
// Set checkbox state based on the response data
const check_modem_configMode = document.getElementById("check_modem_configMode");
if (check_modem_configMode) {
check_modem_configMode.checked = response.modem_config_mode;
console.log("Modem configuration: " + response.modem_config_mode);
} else {
console.error("Checkbox element not found");
} }
})
.catch(error => console.error(`Error loading ${file}:`, error));
});
fetch('../config.json') // Replace 'deviceID.txt' with 'config.json' },
.then(response => response.json()) // Parse response as JSON error: function(xhr, status, error) {
.then(data => { console.error('AJAX request failed:', status, error);
console.log("Getting config file (onload)"); }
//modem config mode });
const check_modem_configMode = document.getElementById("check_modem_configMode");
check_modem_configMode.checked = data.modem_config_mode;
console.log("Modem configuration: " + data.modem_config_mode);
})
}); });
@@ -430,8 +471,10 @@ function getData_saraR4(port, command, timeout){
} else{ } else{
// si c'est une commande AT normale // si c'est une commande AT normale
// Replace newline characters with <br> tags // Replace newline characters with <br> tags
const formattedResponse = response.replace(/\n/g, "<br>"); const formattedResponse = response.replace(/\n/g, "<br>")
.replace(/\b(OK)\b/g, '<span style="color: green; font-weight: bold;">$1</span>');;
$("#response_"+port+"_"+safeCommand).html(formattedResponse); $("#response_"+port+"_"+safeCommand).html(formattedResponse);
} }
}, },
error: function(xhr, status, error) { error: function(xhr, status, error) {
@@ -700,17 +743,68 @@ function getModem_busy_status() {
} }
function update_modem_configMode(param, checked){ function update_modem_configMode(param, checked){
//change ('modem_config_mode', '0', 'bool') inside SQLITE db
// response type: {"success":true,"message":"Configuration updated successfully","param":"modem_config_mode","value":"0","type":"bool"}
const toastLiveExample = document.getElementById('liveToast')
const toastBody = toastLiveExample.querySelector('.toast-body');
console.log("updating modem config mode to :" + checked); console.log("updating modem config mode to :" + checked);
$.ajax({ $.ajax({
url: 'launcher.php?type=update_config&param='+param+'&value='+checked, url: 'launcher.php?type=update_config_sqlite&param='+param+'&value='+checked,
dataType: 'text', // Specify that you expect a JSON response dataType: 'json', // Specify that you expect a JSON response
method: 'GET', // Use GET or POST depending on your needs method: 'GET', // Use GET or POST depending on your needs
cache: false, // Prevent AJAX from caching cache: false, // Prevent AJAX from caching
success: function(response) { success: function(response) {
console.log(response);
console.log("AJAX success:");
console.log(response);
// Format the response nicely
let formattedMessage = '';
if (response.success) {
// Success message
toastLiveExample.classList.remove('text-bg-danger');
toastLiveExample.classList.add('text-bg-success');
formattedMessage = `
<strong>Success!</strong><br>
Parameter: ${response.param || param}<br>
Value: ${response.value || checked}<br>
${response.message || ''}
`;
} else {
// Error message
toastLiveExample.classList.remove('text-bg-success');
toastLiveExample.classList.add('text-bg-danger');
formattedMessage = `
<strong>Error!</strong><br>
${response.error || 'Unknown error'}<br>
Parameter: ${response.param || param}
`;
}
// Update the toast body with formatted content
toastBody.innerHTML = formattedMessage;
// Show the toast
const toastBootstrap = bootstrap.Toast.getOrCreateInstance(toastLiveExample)
toastBootstrap.show()
}, },
error: function(xhr, status, error) { error: function(xhr, status, error) {
console.error('AJAX request failed:', status, error); console.error('AJAX request failed:', status, error);
// Update toast with error message
toastBody.textContent = 'Error: ' + error;
// Set toast to danger color
toastLiveExample.classList.remove('text-bg-success');
toastLiveExample.classList.add('text-bg-danger');
// Show the toast for errors too
const toastBootstrap = bootstrap.Toast.getOrCreateInstance(toastLiveExample);
toastBootstrap.show();
} }
}); });
} }
@@ -721,6 +815,7 @@ window.onload = function() {
getModem_busy_status(); getModem_busy_status();
setInterval(getModem_busy_status, 1000); setInterval(getModem_busy_status, 1000);
fetch('../config.json') // Replace 'deviceID.txt' with 'config.json' fetch('../config.json') // Replace 'deviceID.txt' with 'config.json'
.then(response => response.json()) // Parse response as JSON .then(response => response.json()) // Parse response as JSON
.then(data => { .then(data => {
@@ -744,6 +839,7 @@ window.onload = function() {
//get SARA_R4 connection status //get SARA_R4 connection status
/*
const SARA_statusElement = document.getElementById("modem-status"); const SARA_statusElement = document.getElementById("modem-status");
console.log("SARA R4 is: " + data.SARA_R4_network_status); console.log("SARA R4 is: " + data.SARA_R4_network_status);
@@ -757,7 +853,7 @@ window.onload = function() {
SARA_statusElement.textContent = "Unknown"; SARA_statusElement.textContent = "Unknown";
SARA_statusElement.className = "badge text-bg-secondary"; SARA_statusElement.className = "badge text-bg-secondary";
} }
*/
//get local RTC //get local RTC
$.ajax({ $.ajax({
url: 'launcher.php?type=RTC_time', url: 'launcher.php?type=RTC_time',
@@ -778,6 +874,7 @@ window.onload = function() {
}) })
.catch(error => console.error('Error loading config.json:', error)); .catch(error => console.error('Error loading config.json:', error));
} }
</script> </script>
</body> </body>

View File

@@ -144,40 +144,50 @@ function getNPM_values(port){
} }
function getENVEA_values(port, name){ function getENVEA_values(port, name){
console.log("Data from Envea "+ name+" (port "+port+"):"); console.log("Data from Envea " + name + " (port " + port + "):");
$("#loading_envea"+name).show(); $("#loading_envea" + name).show();
$.ajax({ $.ajax({
url: 'launcher.php?type=envea&port='+port+'&name='+name, url: 'launcher.php?type=envea&port=' + port + '&name=' + name,
dataType: 'json', // Specify that you expect a JSON response dataType: 'json',
method: 'GET', // Use GET or POST depending on your needs method: 'GET',
success: function(response) { success: function(response) {
console.log(response); console.log(response);
const tableBody = document.getElementById("data-table-body_envea"+name); const tableBody = document.getElementById("data-table-body_envea" + name);
tableBody.innerHTML = ""; tableBody.innerHTML = "";
$("#loading_envea"+name).hide(); $("#loading_envea" + name).hide();
// Create an array of the desired keys
// Create an array of the desired keys const keysToShow = [name];
const keysToShow = [name]; keysToShow.forEach(key => {
// Add only the specified elements to the table if (response !== undefined) {
keysToShow.forEach(key => { const value = response;
if (response !== undefined) { // Check if the key exists in the response $("#data-table-body_envea" + name).append(`
const value = response; <tr>
$("#data-table-body_envea"+name).append(` <td>${key}</td>
<tr> <td>${value} ppb</td>
<td>${key}</td> </tr>
<td>${value} ppb</td> `);
</tr> }
`);
}
});
},
error: function(xhr, status, error) {
console.error('AJAX request failed:', status, error);
}
}); });
},
error: function(xhr, status, error) {
console.error('AJAX request failed:', status, error);
const tableBody = document.getElementById("data-table-body_envea" + name);
$("#loading_envea" + name).hide();
tableBody.innerHTML = `
<tr>
<td colspan="2" class="text-danger">
❌ Error: unable to get data from sensor.<br>
<small>${status}: ${error}</small>
</td>
</tr>
`;
} }
});
}
function getNoise_values(){ function getNoise_values(){
console.log("Data from I2C Noise Sensor:"); console.log("Data from I2C Noise Sensor:");
@@ -261,143 +271,190 @@ function getBME280_values(){
window.onload = function() { window.onload = function() {
fetch('../config.json') // Replace 'deviceID.txt' with 'config.json'
.then(response => response.json()) // Parse response as JSON //NEW way to get config (SQLite)
.then(data => { $.ajax({
//get device ID url: 'launcher.php?type=get_config_sqlite',
const deviceID = data.deviceID.trim().toUpperCase(); dataType:'json',
//document.getElementById('pageTitle_plus_ID').innerText = 'token: ' + deviceID; //dataType: 'json', // Specify that you expect a JSON response
//get device Name method: 'GET', // Use GET or POST depending on your needs
const deviceName = data.deviceName; success: function(response) {
console.log("Getting SQLite config table:");
const elements = document.querySelectorAll('.sideBar_sensorName'); console.log(response);
elements.forEach((element) => {
element.innerText = deviceName;
}); //device name_side bar
const elements = document.querySelectorAll('.sideBar_sensorName');
elements.forEach((element) => {
element.innerText = response.deviceName;
});
},
error: function(xhr, status, error) {
console.error('AJAX request failed:', status, error);
}
});//end AJAX
//getting config_scripts table
$.ajax({
url: 'launcher.php?type=get_config_scripts_sqlite',
dataType:'json',
//dataType: 'json', // Specify that you expect a JSON response
method: 'GET', // Use GET or POST depending on your needs
success: function(response) {
console.log("Getting SQLite config scripts table:");
console.log(response);
const container = document.getElementById('card-container'); // Conteneur des cartes
//creates NPM card
if (response["NPM/get_data_modbus_v3.py"]) {
const cardHTML = `
<div class="col-sm-3">
<div class="card">
<div class="card-header">
Port UART
</div>
<div class="card-body">
<h5 class="card-title">NextPM</h5>
<p class="card-text">Capteur particules fines.</p>
<button class="btn btn-primary" onclick="getNPM_values('ttyAMA5')">Get Data</button>
<br>
<div id="loading_ttyAMA5" class="spinner-border spinner-border-sm" style="display: none;" role="status"></div>
<table class="table table-striped-columns">
<tbody id="data-table-body_ttyAMA5"></tbody>
</table>
</div>
</div>
</div>`;
container.innerHTML += cardHTML; // Add the I2C card if condition is met
}
//creates i2c BME280 card
if (response["BME280/get_data_v2.py"]) {
const i2C_BME_HTML = `
<div class="col-sm-3">
<div class="card">
<div class="card-header">
Port I2C
</div>
<div class="card-body">
<h5 class="card-title">BME280 Temp/Hum sensor</h5>
<p class="card-text">Capteur température et humidité sur le port I2C.</p>
<button class="btn btn-primary mb-1" onclick="getBME280_values()">Get Data</button>
<br>
<div id="loading_BME280" class="spinner-border spinner-border-sm" style="display: none;" role="status"></div>
<table class="table table-striped-columns">
<tbody id="data-table-body_BME280"></tbody>
</table>
</div>
</div>
</div>`;
container.innerHTML += i2C_BME_HTML; // Add the I2C card if condition is met
}
//creates i2c sound card
if (response.i2C_sound) {
const i2C_HTML = `
<div class="col-sm-3">
<div class="card">
<div class="card-header">
Port I2C
</div>
<div class="card-body">
<h5 class="card-title">Decibel Meter</h5>
<p class="card-text">Capteur bruit sur le port I2C.</p>
<button class="btn btn-primary mb-1" onclick="getNoise_values()">Get Data</button>
<br>
<button class="btn btn-success" onclick="startNoise()">Start recording</button>
<button class="btn btn-danger" onclick="stopNoise()">Stop recording</button>
<div id="loading_noise" class="spinner-border spinner-border-sm" style="display: none;" role="status"></div>
<table class="table table-striped-columns">
<tbody id="data-table-body_noise"></tbody>
</table>
</div>
</div>
</div>`;
container.innerHTML += i2C_HTML; // Add the I2C card if condition is met
}
//Si on a des SONDES ENVEA connectée il faut faire un deuxième call dans la table envea_sondes_table
//creates ENVEA cards
if (response["envea/read_value_v2.py"]) {
console.log("Need to display ENVEA sondes");
//getting config_scripts table
$.ajax({
url: 'launcher.php?type=get_envea_sondes_table_sqlite',
dataType:'json',
//dataType: 'json', // Specify that you expect a JSON response
method: 'GET', // Use GET or POST depending on your needs
success: function(sondes) {
console.log("Getting SQLite envea sondes table:");
console.log(sondes);
const ENVEA_sensors = sondes.filter(sonde => sonde.connected); // Filter only connected sondes
ENVEA_sensors.forEach((sensor, index) => {
const port = sensor.port; // Port from the sensor object
const name = sensor.name; // Port from the sensor object
const coefficient = sensor.coefficient;
const cardHTML = `
<div class="col-sm-3">
<div class="card">
<div class="card-header">
Port UART ${port.replace('ttyAMA', '')}
</div>
<div class="card-body">
<h5 class="card-title">Sonde Envea ${name}</h5>
<p class="card-text">Capteur gas.</p>
<button class="btn btn-primary" onclick="getENVEA_values('${port}','${name}')">Get Data</button>
<div id="loading_envea${name}" class="spinner-border spinner-border-sm" style="display: none;" role="status"></div>
<table class="table table-striped-columns">
<tbody id="data-table-body_envea${name}"></tbody>
</table>
</div>
</div>
</div>`;
container.innerHTML += cardHTML; // Ajouter la carte au conteneur
});
},
error: function(xhr, status, error) {
console.error('AJAX request failed:', status, error);
}
});//end AJAX envea Sondes
//get local RTC
$.ajax({
url: 'launcher.php?type=RTC_time',
dataType: 'text', // Specify that you expect a JSON response
method: 'GET', // Use GET or POST depending on your needs
success: function(response) {
console.log("Local RTC: " + response);
const RTC_Element = document.getElementById("RTC_time");
RTC_Element.textContent = response;
}, }//end if
error: function(xhr, status, error) {
console.error('AJAX request failed:', status, error);
}
});
const container = document.getElementById('card-container'); // Conteneur des cartes },
error: function(xhr, status, error) {
//creates NPM cards console.error('AJAX request failed:', status, error);
const NPM_ports = data.NextPM_ports; // Récupère les ports }
NPM_ports.forEach((port, index) => { });//end AJAX (config_scripts)
const cardHTML = `
<div class="col-sm-3">
<div class="card">
<div class="card-header">
Port UART ${port.replace('ttyAMA', '')}
</div>
<div class="card-body">
<h5 class="card-title">NextPM ${String.fromCharCode(65 + index)}</h5>
<p class="card-text">Capteur particules fines.</p>
<button class="btn btn-primary" onclick="getNPM_values('${port}')">Get Data</button>
<div id="loading_${port}" class="spinner-border spinner-border-sm" style="display: none;" role="status"></div>
<table class="table table-striped-columns">
<tbody id="data-table-body_${port}"></tbody>
</table>
</div>
</div>
</div>`;
container.innerHTML += cardHTML; // Ajouter la carte au conteneur
});
//creates ENVEA cards //get local RTC
const ENVEA_sensors = data.envea_sondes.filter(sonde => sonde.connected); // Filter only connected sondes $.ajax({
url: 'launcher.php?type=RTC_time',
dataType: 'text', // Specify that you expect a JSON response
method: 'GET', // Use GET or POST depending on your needs
success: function(response) {
console.log("Local RTC: " + response);
const RTC_Element = document.getElementById("RTC_time");
RTC_Element.textContent = response;
ENVEA_sensors.forEach((sensor, index) => { },
const port = sensor.port; // Port from the sensor object error: function(xhr, status, error) {
const name = sensor.name; // Port from the sensor object console.error('AJAX request failed:', status, error);
const coefficient = sensor.coefficient; }
const cardHTML = ` });
<div class="col-sm-3">
<div class="card">
<div class="card-header">
Port UART ${port.replace('ttyAMA', '')}
</div>
<div class="card-body">
<h5 class="card-title">Sonde Envea ${name}</h5>
<p class="card-text">Capteur gas.</p>
<button class="btn btn-primary" onclick="getENVEA_values('${port}','${name}','${coefficient}')">Get Data</button>
<div id="loading_envea${name}" class="spinner-border spinner-border-sm" style="display: none;" role="status"></div>
<table class="table table-striped-columns">
<tbody id="data-table-body_envea${name}"></tbody>
</table>
</div>
</div>
</div>`;
container.innerHTML += cardHTML; // Ajouter la carte au conteneur
});
//creates i2c BME280 card } //end windows onload
if (data["BME280/get_data_v2.py"]) {
const i2C_BME_HTML = `
<div class="col-sm-3">
<div class="card">
<div class="card-header">
Port I2C
</div>
<div class="card-body">
<h5 class="card-title">BME280 Temp/Hum sensor</h5>
<p class="card-text">Capteur température et humidité sur le port I2C.</p>
<button class="btn btn-primary mb-1" onclick="getBME280_values()">Get Data</button>
<br>
<div id="loading_BME280" class="spinner-border spinner-border-sm" style="display: none;" role="status"></div>
<table class="table table-striped-columns">
<tbody id="data-table-body_BME280"></tbody>
</table>
</div>
</div>
</div>`;
container.innerHTML += i2C_BME_HTML; // Add the I2C card if condition is met
}
//creates i2c sound card
if (data.i2C_sound) {
const i2C_HTML = `
<div class="col-sm-3">
<div class="card">
<div class="card-header">
Port I2C
</div>
<div class="card-body">
<h5 class="card-title">Decibel Meter</h5>
<p class="card-text">Capteur bruit sur le port I2C.</p>
<button class="btn btn-primary mb-1" onclick="getNoise_values()">Get Data</button>
<br>
<button class="btn btn-success" onclick="startNoise()">Start recording</button>
<button class="btn btn-danger" onclick="stopNoise()">Stop recording</button>
<div id="loading_noise" class="spinner-border spinner-border-sm" style="display: none;" role="status"></div>
<table class="table table-striped-columns">
<tbody id="data-table-body_noise"></tbody>
</table>
</div>
</div>
</div>`;
container.innerHTML += i2C_HTML; // Add the I2C card if condition is met
}
})
.catch(error => console.error('Error loading config.json:', error));
}
</script> </script>
</body> </body>

View File

@@ -23,11 +23,11 @@ fi
# Update and install necessary packages # Update and install necessary packages
info "Updating package list and installing necessary packages..." info "Updating package list and installing necessary packages..."
sudo apt update && sudo apt install -y git gh apache2 php php-sqlite3 python3 python3-pip jq autossh i2c-tools python3-smbus || error "Failed to install required packages." sudo apt update && sudo apt install -y git gh apache2 sqlite3 php php-sqlite3 python3 python3-pip jq autossh i2c-tools python3-smbus || error "Failed to install required packages."
# Install Python libraries # Install Python libraries
info "Installing Python libraries..." info "Installing Python libraries..."
sudo pip3 install pyserial requests RPi.GPIO adafruit-circuitpython-bme280 crcmod psutil ntplib pytz --break-system-packages || error "Failed to install Python libraries." sudo pip3 install pyserial requests RPi.GPIO adafruit-circuitpython-bme280 crcmod psutil gpiozero ntplib pytz --break-system-packages || error "Failed to install Python libraries."
# Ask user if they want to set up SSH keys # Ask user if they want to set up SSH keys
read -p "Do you want to set up an SSH key for /var/www? (y/n): " answer read -p "Do you want to set up an SSH key for /var/www? (y/n): " answer

View File

@@ -165,65 +165,84 @@ def blink_led(pin, blink_count, delay=1):
GPIO.output(pin, GPIO.LOW) # Ensure LED is off GPIO.output(pin, GPIO.LOW) # Ensure LED is off
print(f"LED on GPIO {pin} turned OFF (cleanup avoided)") print(f"LED on GPIO {pin} turned OFF (cleanup avoided)")
#get data from config #get config data from SQLite table
def load_config(config_file): def load_config_sqlite():
"""
Load configuration data from SQLite config table
Returns:
dict: Configuration data with proper type conversion
"""
try: try:
with open(config_file, 'r') as file:
config_data = json.load(file) # 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 return config_data
except Exception as e: except Exception as e:
print(f"Error loading config file: {e}") print(f"Error loading config from SQLite: {e}")
return {} return {}
#Fonction pour mettre à jour le JSON de configuration def load_config_scripts_sqlite():
def update_json_key(file_path, key, value):
""" """
Updates a specific key in a JSON file with a new value. Load script configuration data from SQLite config_scripts_table
:param file_path: Path to the JSON file. Returns:
:param key: The key to update in the JSON file. dict: Script paths as keys and enabled status as boolean values
:param value: The new value to assign to the key.
""" """
try: try:
# Load the existing data # Query the config_scripts_table
with open(file_path, "r") as file: cursor.execute("SELECT script_path, enabled FROM config_scripts_table")
data = json.load(file) rows = cursor.fetchall()
# Check if the key exists in the JSON file # Create config dictionary with script paths as keys and enabled status as boolean values
if key in data: scripts_config = {}
data[key] = value # Update the key with the new value for script_path, enabled in rows:
else: # Convert integer enabled value (0/1) to boolean
print(f"Key '{key}' not found in the JSON file.") scripts_config[script_path] = bool(enabled)
return
# Write the updated data back to the file return scripts_config
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: except Exception as e:
print(f"Error updating the JSON file: {e}") print(f"Error loading scripts config from SQLite: {e}")
return {}
# Define the config file path #Load config
config_file = '/var/www/nebuleair_pro_4g/config.json' config = load_config_sqlite()
#config
# Load the configuration data device_id = config.get('deviceID', 'unknown')
config = load_config(config_file) device_id = device_id.upper()
modem_config_mode = config.get('modem_config_mode', False)
device_latitude_raw = config.get('latitude_raw', 0) device_latitude_raw = config.get('latitude_raw', 0)
device_longitude_raw = config.get('longitude_raw', 0) device_longitude_raw = config.get('longitude_raw', 0)
baudrate = config.get('SaraR4_baudrate', 115200) #baudrate du sara R4 modem_version=config.get('modem_version', "")
device_id = config.get('deviceID', '').upper() #device ID en maj Sara_baudrate = config.get('SaraR4_baudrate', 115200)
bme_280_config = config.get('BME280/get_data_v2.py', False) #présence du BME280 npm_5channel = config.get('npm_5channel', False) #5 canaux du NPM
envea_cairsens= config.get('envea/read_value_v2.py', False) selected_networkID = int(config.get('SARA_R4_neworkID', 0))
mppt_charger= config.get('MPPT/read.py', False)
wind_meter= config.get('windMeter/read.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 () send_uSpot = config.get('send_uSpot', False) #envoi sur MicroSpot ()
reset_uSpot_url = False reset_uSpot_url = False
selected_networkID = int(config.get('SARA_R4_neworkID', 0))
npm_5channel = config.get('NextPM_5channels', False) #5 canaux du NPM #config_scripts
modem_version=config.get('modem_version', "") config_scripts = load_config_scripts_sqlite()
modem_config_mode = config.get('modem_config_mode', False) #modem 4G en mode configuration bme_280_config = config_scripts.get('BME280/get_data_v2.py', False)
envea_cairsens= config_scripts.get('envea/read_value_v2.py', False)
mppt_charger= config_scripts.get('MPPT/read.py', False)
wind_meter= config_scripts.get('windMeter/read.py', False)
#update device id in the payload json #update device id in the payload json
payload_json["nebuleairid"] = device_id payload_json["nebuleairid"] = device_id
@@ -235,7 +254,7 @@ if modem_config_mode:
ser_sara = serial.Serial( ser_sara = serial.Serial(
port='/dev/ttyAMA2', port='/dev/ttyAMA2',
baudrate=baudrate, #115200 ou 9600 baudrate=Sara_baudrate, #115200 ou 9600
parity=serial.PARITY_NONE, #PARITY_NONE, PARITY_EVEN or PARITY_ODD parity=serial.PARITY_NONE, #PARITY_NONE, PARITY_EVEN or PARITY_ODD
stopbits=serial.STOPBITS_ONE, stopbits=serial.STOPBITS_ONE,
bytesize=serial.EIGHTBITS, bytesize=serial.EIGHTBITS,
@@ -304,6 +323,49 @@ def extract_error_code(response):
# Return None if we couldn't find the error code # Return None if we couldn't find the error code
return None return None
def send_error_notification(device_id, error_type, additional_info=None):
"""
Send an error notification to the server when issues with the SARA module occur.
Will silently fail if there's no internet connection.
Parameters:
-----------
device_id : str
The unique identifier of the device
error_type : str
Type of error encountered (e.g., 'serial_error', 'cme_error', 'http_error', 'timeout')
additional_info : str, optional
Any additional information about the error for logging purposes
Returns:
--------
bool
True if notification was sent successfully, False otherwise
"""
# Create the alert URL with all relevant parameters
base_url = 'http://data.nebuleair.fr/pro_4G/alert.php'
alert_url = f'{base_url}?capteur_id={device_id}&error_type={error_type}'
# Add additional info if provided
if additional_info:
# Make sure to URL encode the additional info
from urllib.parse import quote
alert_url += f'&details={quote(str(additional_info))}'
# Try to send the notification, catch ALL exceptions
try:
response = requests.post(alert_url, timeout=3)
if response.status_code == 200:
print(f"✅ Alert notification sent successfully")
return True
else:
print(f"⚠️ Alert notification failed: Status code {response.status_code}")
except Exception as e:
print(f"⚠️ Alert notification couldn't be sent: {e}")
return False
def modem_complete_reboot_and_reinitialize(modem_version, aircarto_profile_id): def modem_complete_reboot_and_reinitialize(modem_version, aircarto_profile_id):
""" """
Performs a complete modem restart sequence: Performs a complete modem restart sequence:
@@ -558,9 +620,9 @@ try:
payload_csv[10] = averages[1] # envea_h2s payload_csv[10] = averages[1] # envea_h2s
payload_csv[11] = averages[2] # envea_nh3 payload_csv[11] = averages[2] # envea_nh3
#Add data to payload JSON #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[0])})
payload_json["sensordatavalues"].append({"value_type": "CAIRSENS_NO2", "value": str(averages[1])}) payload_json["sensordatavalues"].append({"value_type": "CAIRSENS_H2S", "value": str(averages[1])})
payload_json["sensordatavalues"].append({"value_type": "CAIRSENS_NH3", "value": str(averages[2])}) payload_json["sensordatavalues"].append({"value_type": "CAIRSENS_NH3", "value": str(averages[2])})
#Wind meter #Wind meter
@@ -611,23 +673,9 @@ try:
print('🛑STOP LOOP🛑') print('🛑STOP LOOP🛑')
print("<hr>") print("<hr>")
# Send notification #Send notification (WIFI)
try: send_error_notification(device_id, "serial_error")
# Format the URL with the device_id
alert_url = f'http://data.nebuleair.fr/pro_4G/alert.php?capteur_id={device_id}&error_type=serial_error'
# Send POST request with short timeout
response = requests.post(alert_url, timeout=3)
if response.status_code == 200:
print(f"Alert notification sent successfully")
else:
print(f"Alert notification failed with status code: {response.status_code}")
except Exception as e:
# Catch any exception and continue
print(f"Alert notification failed: {e}")
#end loop #end loop
sys.exit() sys.exit()
@@ -701,11 +749,15 @@ try:
#3. Send to endpoint (with device ID) #3. Send to endpoint (with device ID)
print("Send data (POST REQUEST):") print("Send data (POST REQUEST):")
command= f'AT+UHTTPC={aircarto_profile_id},4,"/pro_4G/data.php?sensor_id={device_id}&lat{device_latitude_raw}=&long={device_longitude_raw}&datetime={influx_timestamp}","aircarto_server_response.txt","sensordata_csv.json",4\r' command= f'AT+UHTTPC={aircarto_profile_id},4,"/pro_4G/data.php?sensor_id={device_id}&lat={device_latitude_raw}&long={device_longitude_raw}&datetime={influx_timestamp}","aircarto_server_response.txt","sensordata_csv.json",4\r'
print("sending:")
print('<p class="text-danger-emphasis">')
print(command)
print("</p>", end="")
ser_sara.write(command.encode('utf-8')) 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", "ERROR"], debug=True) response_SARA_3 = read_complete_response(ser_sara, timeout=5, end_of_response_timeout=120, wait_for_lines=["+UUHTTPCR", "+CME ERROR", "ERROR"], debug=True)
print("receiving:")
print('<p class="text-danger-emphasis">') print('<p class="text-danger-emphasis">')
print(response_SARA_3) print(response_SARA_3)
print("</p>", end="") print("</p>", end="")
@@ -735,8 +787,6 @@ try:
print('<span style="color: red;font-weight: bold;">ATTENTION: CME ERROR</span>') print('<span style="color: red;font-weight: bold;">ATTENTION: CME ERROR</span>')
print("error:", lines[-1]) print("error:", lines[-1])
print("*****") print("*****")
#update status
update_json_key(config_file, "SARA_R4_network_status", "disconnected")
# Gestion de l'erreur spécifique # Gestion de l'erreur spécifique
if "No connection to phone" in lines[-1]: if "No connection to phone" in lines[-1]:
@@ -775,7 +825,6 @@ try:
if len(parts) == 3 and parts[-1] == '0': # The third value indicates success if len(parts) == 3 and parts[-1] == '0': # The third value indicates success
print("*****") print("*****")
print('<span style="color: red;font-weight: bold;">⛔ATTENTION: HTTP operation failed</span>') 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("*****")
print("Blink red LED") print("Blink red LED")
# Run LED blinking in a separate thread # Run LED blinking in a separate thread
@@ -822,7 +871,6 @@ try:
# 2.2 code 1 (✅✅HHTP / UUHTTPCR succeded✅✅) # 2.2 code 1 (✅✅HHTP / UUHTTPCR succeded✅✅)
else: else:
print('<span style="font-weight: bold;">✅✅HTTP operation successful.</span>') print('<span style="font-weight: bold;">✅✅HTTP operation successful.</span>')
update_json_key(config_file, "SARA_R4_network_status", "connected")
print("Blink blue LED") print("Blink blue LED")
led_thread = Thread(target=blink_led, args=(23, 5, 0.5)) led_thread = Thread(target=blink_led, args=(23, 5, 0.5))
led_thread.start() led_thread.start()
@@ -904,7 +952,7 @@ try:
#Si non ne recoit pas de réponse UHTTPCR #Si non ne recoit pas de réponse UHTTPCR
#on a peut etre une ERROR de type "+CME ERROR: No connection to phone" ou "Operation not allowed" #on a peut etre une ERROR de type "+CME ERROR: No connection to phone" ou "Operation not allowed" ou "ERROR"
else: else:
print('<span style="color: red;font-weight: bold;">No UUHTTPCR response</span>') print('<span style="color: red;font-weight: bold;">No UUHTTPCR response</span>')
print("Blink red LED") print("Blink red LED")
@@ -952,6 +1000,9 @@ try:
if "ERROR" in line: if "ERROR" in line:
print("⛔Attention ERROR!⛔") print("⛔Attention ERROR!⛔")
#Send notification (WIFI)
send_error_notification(device_id, "sara_error")
#Software Reboot #Software Reboot
software_reboot_success = modem_complete_reboot_and_reinitialize(modem_version, aircarto_profile_id) software_reboot_success = modem_complete_reboot_and_reinitialize(modem_version, aircarto_profile_id)
if software_reboot_success: if software_reboot_success:
@@ -1086,7 +1137,6 @@ try:
print("error:", lines[-1]) print("error:", lines[-1])
print("*****") print("*****")
#update status #update status
#update_json_key(config_file, "SARA_R4_network_status", "disconnected")
# Gestion de l'erreur spécifique # Gestion de l'erreur spécifique
if "No connection to phone" in lines[-1]: if "No connection to phone" in lines[-1]:
@@ -1112,7 +1162,6 @@ try:
if len(parts) == 3 and parts[-1] == '0': # The third value indicates success if len(parts) == 3 and parts[-1] == '0': # The third value indicates success
print("*****") print("*****")
print('<span style="color: red;font-weight: bold;">⛔ATTENTION: HTTP operation failed</span>') 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("*****")
print("Blink red LED") print("Blink red LED")
# Run LED blinking in a separate thread # Run LED blinking in a separate thread
@@ -1152,7 +1201,6 @@ try:
else: else:
# Si la commande HTTP a réussi # Si la commande HTTP a réussi
print('<span style="font-weight: bold;">✅✅HTTP operation successful.</span>') print('<span style="font-weight: bold;">✅✅HTTP operation successful.</span>')
update_json_key(config_file, "SARA_R4_network_status", "connected")
print("Blink blue LED") print("Blink blue LED")
led_thread = Thread(target=blink_led, args=(23, 5, 0.5)) led_thread = Thread(target=blink_led, args=(23, 5, 0.5))
led_thread.start() led_thread.start()

104
master.py
View File

@@ -52,17 +52,73 @@ Specific scripts can be disabled with config.json
import time import time
import threading import threading
import subprocess import subprocess
import json
import os import os
import sqlite3
# Base directory where scripts are stored # Base directory where scripts are stored
SCRIPT_DIR = "/var/www/nebuleair_pro_4g/" SCRIPT_DIR = "/var/www/nebuleair_pro_4g/"
CONFIG_FILE = "/var/www/nebuleair_pro_4g/config.json" DB_PATH = "/var/www/nebuleair_pro_4g/sqlite/sensors.db"
# Lock file path for SARA script
SARA_LOCK_FILE = "/var/www/nebuleair_pro_4g/sara_script.lock"
def is_script_enabled(script_name):
"""Check if a script is enabled in the database."""
try:
conn = sqlite3.connect(DB_PATH)
cursor = conn.cursor()
cursor.execute(
"SELECT enabled FROM config_scripts_table WHERE script_path = ?",
(script_name,)
)
result = cursor.fetchone()
conn.close()
if result is None:
return True # Default to enabled if not found in database
return bool(result[0])
except Exception:
# If any database error occurs, default to enabled
return True
def create_lock_file():
"""Create a lock file for the SARA script."""
with open(SARA_LOCK_FILE, 'w') as f:
f.write(str(int(time.time())))
def remove_lock_file():
"""Remove the SARA script lock file."""
if os.path.exists(SARA_LOCK_FILE):
os.remove(SARA_LOCK_FILE)
def is_script_locked():
"""Check if the SARA script is currently locked."""
if not os.path.exists(SARA_LOCK_FILE):
return False
# Check if lock is older than 60 seconds (stale)
with open(SARA_LOCK_FILE, 'r') as f:
try:
lock_time = int(f.read().strip())
if time.time() - lock_time > 60:
# Lock is stale, remove it
remove_lock_file()
return False
except ValueError:
# Invalid lock file content
remove_lock_file()
return False
return True
def load_config():
"""Load the configuration file to determine which scripts to run."""
with open(CONFIG_FILE, "r") as f:
return json.load(f)
def run_script(script_name, interval, delay=0): def run_script(script_name, interval, delay=0):
"""Run a script in a synchronized loop with an optional start delay.""" """Run a script in a synchronized loop with an optional start delay."""
@@ -70,33 +126,41 @@ def run_script(script_name, interval, delay=0):
next_run = time.monotonic() + delay # Apply the initial delay next_run = time.monotonic() + delay # Apply the initial delay
while True: while True:
config = load_config() if is_script_enabled(script_name):
if config.get(script_name, True): # Default to True if not found # Special handling for SARA script to prevent concurrent runs
subprocess.run(["python3", script_path]) if script_name == "loop/SARA_send_data_v2.py":
if not is_script_locked():
create_lock_file()
try:
subprocess.run(["python3", script_path])
finally:
remove_lock_file()
else:
# Run other scripts normally
subprocess.run(["python3", script_path])
# Wait until the next exact interval # Wait until the next exact interval
next_run += interval next_run += interval
sleep_time = max(0, next_run - time.monotonic()) # Prevent negative sleep times sleep_time = max(0, next_run - time.monotonic()) # Prevent negative sleep times
time.sleep(sleep_time) time.sleep(sleep_time)
# Define scripts and their execution intervals (seconds) # Define scripts and their execution intervals (seconds)
SCRIPTS = [ SCRIPTS = [
#("RTC/save_to_db.py", 1, 0), # --> will run as a separated system service (rtc_save_to_db.service) # Format: (script_name, interval_in_seconds, start_delay_in_seconds)
("NPM/get_data_modbus_v3.py", 10, 0), # Get NPM data (modbus 5 channels) every 10s, with 2s delay ("NPM/get_data_modbus_v3.py", 10, 0), # Get NPM data (modbus 5 channels) every 10s
("envea/read_value_v2.py", 10, 0), # Get NPM data (modbus 5 channels) every 10s, with 2s delay ("envea/read_value_v2.py", 10, 0), # Get Envea data every 10s
("loop/SARA_send_data_v2.py", 60, 1), # Send data every 60 seconds, with 2s delay ("loop/SARA_send_data_v2.py", 60, 1), # Send data every 60 seconds, with 1s delay
("BME280/get_data_v2.py", 120, 0), # Get BME280 data every 120 seconds, no delay ("BME280/get_data_v2.py", 120, 0), # Get BME280 data every 120 seconds
("MPPT/read.py", 120, 0), # Get MPPT data every 120 seconds, no delay ("MPPT/read.py", 120, 0), # Get MPPT data every 120 seconds
#("windMeter/read.py", 60, 2), # --> will run as a separated system service () ("sqlite/flush_old_data.py", 86400, 0) # Flush old data inside db every day
("sqlite/flush_old_data.py", 86400, 0) # flush old data inside db every day ()
] ]
# Start threads for enabled scripts # Start threads for scripts
for script_name, interval, delay in SCRIPTS: for script_name, interval, delay in SCRIPTS:
thread = threading.Thread(target=run_script, args=(script_name, interval, delay), daemon=True) thread = threading.Thread(target=run_script, args=(script_name, interval, delay), daemon=True)
thread.start() thread.start()
# Keep the main script running # Keep the main script running
while True: while True:
time.sleep(1) time.sleep(1)

0
config.json.dist → old/config.json.dist Executable file → Normal file
View File

0
install_software.yaml → old/install_software.yaml Executable file → Normal file
View File

View File

@@ -18,6 +18,35 @@ import sqlite3
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()
#create a config table
cursor.execute("""
CREATE TABLE IF NOT EXISTS config_table (
key TEXT PRIMARY KEY,
value TEXT NOT NULL,
type TEXT NOT NULL
)
""")
#creates a config_scripts table
cursor.execute('''
CREATE TABLE IF NOT EXISTS config_scripts_table (
script_path TEXT PRIMARY KEY,
enabled INTEGER NOT NULL
)
''')
#creates a config table for envea sondes
cursor.execute("""
CREATE TABLE IF NOT EXISTS envea_sondes_table (
id INTEGER PRIMARY KEY AUTOINCREMENT,
connected INTEGER NOT NULL,
port TEXT NOT NULL,
name TEXT NOT NULL,
coefficient REAL NOT NULL
)
""")
# Create a table timer # Create a table timer
cursor.execute(""" cursor.execute("""
CREATE TABLE IF NOT EXISTS timestamp_table ( CREATE TABLE IF NOT EXISTS timestamp_table (

View File

@@ -36,7 +36,6 @@ cursor = conn.cursor()
#cursor.execute("SELECT * FROM timestamp_table") #cursor.execute("SELECT * FROM timestamp_table")
if table_name == "timestamp_table": if table_name == "timestamp_table":
cursor.execute("SELECT * FROM timestamp_table") cursor.execute("SELECT * FROM timestamp_table")
else: else:
query = f"SELECT * FROM {table_name} ORDER BY timestamp DESC LIMIT ?" query = f"SELECT * FROM {table_name} ORDER BY timestamp DESC LIMIT ?"
cursor.execute(query, (limit_num,)) cursor.execute(query, (limit_num,))

43
sqlite/read_config.py Normal file
View File

@@ -0,0 +1,43 @@
'''
____ ___ _ _ _
/ ___| / _ \| | (_) |_ ___
\___ \| | | | | | | __/ _ \
___) | |_| | |___| | || __/
|____/ \__\_\_____|_|\__\___|
Script to read data from a sqlite database
/usr/bin/python3 /var/www/nebuleair_pro_4g/sqlite/read_config.py config_table
Available table are
config_table
config_scripts_table
envea_sondes_table
'''
import sqlite3
import sys
parameter = sys.argv[1:] # Exclude the script name
#print("Parameters received:")
table_name=parameter[0]
# Connect to the SQLite database
conn = sqlite3.connect("/var/www/nebuleair_pro_4g/sqlite/sensors.db")
cursor = conn.cursor()
# Retrieve the data
query = f"SELECT * FROM {table_name}"
cursor.execute(query)
rows = cursor.fetchall()
rows.reverse() # Reverse the order in Python (to get ascending order)
# Display the results
for row in rows:
print(row)
# Close the database connection
conn.close()

95
sqlite/set_config.py Normal file
View File

@@ -0,0 +1,95 @@
'''
____ ___ _ _ _
/ ___| / _ \| | (_) |_ ___
\___ \| | | | | | | __/ _ \
___) | |_| | |___| | || __/
|____/ \__\_\_____|_|\__\___|
Script to set the config
/usr/bin/python3 /var/www/nebuleair_pro_4g/sqlite/set_config.py
in case of readonly error:
sudo chmod 777 /var/www/nebuleair_pro_4g/sqlite/sensors.db
'''
import sqlite3
# Connect to (or create if not existent) the database
conn = sqlite3.connect("/var/www/nebuleair_pro_4g/sqlite/sensors.db")
cursor = conn.cursor()
print(f"Connected to database")
# Clear existing data (if any)
cursor.execute("DELETE FROM config_table")
cursor.execute("DELETE FROM config_scripts_table")
cursor.execute("DELETE FROM envea_sondes_table")
print("Existing data cleared")
#add values
# Insert script configurations
script_configs = [
("NPM/get_data_modbus_v3.py", True),
("loop/SARA_send_data_v2.py", True),
("RTC/save_to_db.py", True),
("BME280/get_data_v2.py", True),
("envea/read_value_v2.py", False),
("MPPT/read.py", False),
("windMeter/read.py", False),
("sqlite/flush_old_data.py", True)
]
for script_path, enabled in script_configs:
cursor.execute(
"INSERT INTO config_scripts_table (script_path, enabled) VALUES (?, ?)",
(script_path, 1 if enabled else 0)
)
# Insert general configurations
config_entries = [
("modem_config_mode", "0", "bool"),
("deviceID", "XXXX", "str"),
("npm_5channel", "0", "bool"),
("latitude_raw", "0", "int"),
("longitude_raw", "0", "int"),
("latitude_precision", "0", "int"),
("longitude_precision", "0", "int"),
("deviceName", "NebuleAir-proXXX", "str"),
("SaraR4_baudrate", "115200", "int"),
("NPM_solo_port", "/dev/ttyAMA5", "str"),
("sshTunnel_port", "59228", "int"),
("SARA_R4_general_status", "connected", "str"),
("SARA_R4_SIM_status", "connected", "str"),
("SARA_R4_network_status", "connected", "str"),
("SARA_R4_neworkID", "20810", "int"),
("WIFI_status", "connected", "str"),
("send_uSpot", "0", "bool"),
("modem_version", "XXX", "str")
]
for key, value, value_type in config_entries:
cursor.execute(
"INSERT INTO config_table (key, value, type) VALUES (?, ?, ?)",
(key, value, value_type)
)
# Insert envea sondes
envea_sondes = [
(False, "ttyAMA4", "h2s", 4),
(False, "ttyAMA3", "no2", 1),
(False, "ttyAMA2", "o3", 1)
]
for connected, port, name, coefficient in envea_sondes:
cursor.execute(
"INSERT INTO envea_sondes_table (connected, port, name, coefficient) VALUES (?, ?, ?, ?)",
(1 if connected else 0, port, name, coefficient)
)
# Commit and close the connection
conn.commit()
conn.close()
print("Database updated successfully!")