44 Commits

Author SHA1 Message Date
Your Name
e82d75a4d6 update 2025-03-26 08:27:28 +01:00
Your Name
dc27e5f139 update 2025-03-26 08:25:42 +01:00
Your Name
4bc05091be update 2025-03-25 21:18:12 +01:00
Your Name
29f9ec445a Merge remote-tracking branch 'refs/remotes/origin/main' 2025-03-25 20:23:01 +01:00
Your Name
7b398d0d6d update 2025-03-25 20:22:42 +01:00
PaulVua
76336d0073 update 2025-03-25 16:27:01 +01:00
PaulVua
46a8e21e64 update 2025-03-25 16:20:19 +01:00
Your Name
2129d45ef6 update 2025-03-25 14:55:50 +01:00
Your Name
6312cd8d72 update 2025-03-24 18:04:19 +01:00
Your Name
7c17ec82f5 update 2025-03-24 17:57:47 +01:00
Your Name
b7a6f4c907 add sqlite config management 2025-03-24 15:19:27 +01:00
Your Name
6b3329b9b8 Merge remote-tracking branch 'refs/remotes/origin/main' 2025-03-24 10:23:37 +01:00
PaulVua
e9b1e0e88e Merge remote-tracking branch 'refs/remotes/origin/main' 2025-03-24 10:21:25 +01:00
Your Name
2db732ebb3 Merge remote-tracking branch 'refs/remotes/origin/main' 2025-03-20 17:46:09 +01:00
Your Name
d5302f78ba udpate 2025-03-20 17:45:47 +01:00
Your Name
5b7de91d50 update 2025-03-20 10:54:41 +01:00
Your Name
4d15076d4b update 2025-03-20 09:56:36 +01:00
Your Name
809742b6d5 update 2025-03-20 09:49:14 +01:00
PaulVua
bca975b0c5 Merge remote-tracking branch 'refs/remotes/origin/main' 2025-03-18 18:02:14 +01:00
PaulVua
dfba956685 update 2025-03-18 18:01:33 +01:00
Your Name
d07314262e update 2025-03-18 16:24:22 +01:00
Your Name
dffa639574 update 2025-03-18 12:08:30 +01:00
PaulVua
1fd5a3e75c update 2025-03-18 11:50:39 +01:00
Your Name
e674b21eaa update 2025-03-17 15:17:07 +01:00
Your Name
efc94ba5e1 update 2025-03-17 15:13:16 +01:00
root
26328dec99 update 2025-03-17 12:29:06 +01:00
PaulVua
ec3e81e99e update 2025-03-17 11:00:55 +01:00
Your Name
1c6af36313 update 2025-03-14 10:57:45 +01:00
Your Name
f1d6f595ac update 2025-03-14 09:28:28 +01:00
Your Name
cfc2e0c47f update 2025-03-14 08:54:35 +01:00
Your Name
1037207df3 update 2025-03-13 11:39:40 +01:00
Your Name
14044a8856 update 2025-03-12 17:55:30 +01:00
Your Name
d57a47ef68 update 2025-03-11 16:35:49 +01:00
Your Name
5e7375cd4e update 2025-03-11 15:40:22 +01:00
Your Name
c42b16ddb6 Merge remote-tracking branch 'refs/remotes/origin/main' 2025-03-10 17:48:04 +01:00
Your Name
283a46eb0b update 2025-03-10 17:44:03 +01:00
Your Name
33b24a9f53 update 2025-03-10 15:00:00 +01:00
Your Name
10c4348e54 update 2025-03-10 13:38:02 +01:00
Your Name
072f98ef95 update 2025-03-10 12:15:05 +01:00
Your Name
7b4ff011ec update 2025-03-05 16:24:15 +01:00
Your Name
ab2124f50d update 2025-03-05 16:19:49 +01:00
Your Name
b493d30a41 update 2025-03-05 16:07:22 +01:00
Your Name
659effb7c4 update 2025-03-05 15:58:40 +01:00
Your Name
ebb0fd0a2b update 2025-03-05 09:29:53 +01:00
38 changed files with 3792 additions and 699 deletions

40
GPIO/control.py Normal file
View File

@@ -0,0 +1,40 @@
'''
____ ____ ___ ___
/ ___| _ \_ _/ _ \
| | _| |_) | | | | |
| |_| | __/| | |_| |
\____|_| |___\___/
script to control GPIO output
GPIO 16 -> SARA 5V
GPIO 20 -> SARA PWR ON
option 1:
CLI tool like pinctrl
pinctrl set 17 op
pinctrl set 17 dh
pinctrl set 17 dl
option 2:
python library RPI.GPIO
/usr/bin/python3 /var/www/nebuleair_pro_4g/GPIO/control.py
'''
import RPi.GPIO as GPIO
import time
selected_GPIO = 16
GPIO.setmode(GPIO.BCM) # Use BCM numbering
GPIO.setup(selected_GPIO, GPIO.OUT) # Set GPIO17 as an output
while True:
GPIO.output(selected_GPIO, GPIO.HIGH) # Turn ON
time.sleep(1) # Wait 1 sec
GPIO.output(selected_GPIO, GPIO.LOW) # Turn OFF
time.sleep(1) # Wait 1 sec

225
MPPT/read.py Normal file
View File

@@ -0,0 +1,225 @@
'''
__ __ ____ ____ _____
| \/ | _ \| _ \_ _|
| |\/| | |_) | |_) || |
| | | | __/| __/ | |
|_| |_|_| |_| |_|
Chargeur solaire Victron MPPT interface UART
MPPT connections
5V / Rx / TX / GND
RPI connection
-- / GPIO9 / GPIO8 / GND
* pas besoin de connecter le 5V (le GND uniquement)
typical response from uart:
PID 0xA075 ->product ID
FW 164 ->firmware version
SER# HQ2249VJV9W ->serial num
V 13310 ->Battery voilatage in mV
I -130 ->Battery current in mA (negative means its discharging)
VPV 10 ->Solar Panel voltage
PPV 0 ->Solar Panel power (in W)
CS 0 ->Charger status:
0=off (no charging),
2=Bulk (Max current is being delivered to the battery),
3=Absorbtion (battery is nearly full,voltage is held constant.),
4=Float (Battery is fully charged, only maintaining charge)
MPPT 0 ->MPPT (Maximum Power Point Tracking) state: 0 = Off, 1 = Active, 2 = Not tracking
OR 0x00000001
ERR 0
LOAD ON
IL 100
H19 18 ->historical data (Total energy absorbed in kWh)
H20 0 -> Total energy discharged in kWh
H21 0
H22 9
H23 92
HSDS 19
Checksum u
sudo /usr/bin/python3 /var/www/nebuleair_pro_4g/MPPT/read.py
'''
import serial
import time
import sqlite3
# Connect to the SQLite database
conn = sqlite3.connect("/var/www/nebuleair_pro_4g/sqlite/sensors.db")
cursor = conn.cursor()
def read_vedirect(port='/dev/ttyAMA4', baudrate=19200, timeout=20, max_attempts=3):
"""
Read and parse data from Victron MPPT controller with retry logic
Returns parsed data as a dictionary or None if all attempts fail
"""
required_keys = ['V', 'I', 'VPV', 'PPV', 'CS'] # Essential keys we need
for attempt in range(max_attempts):
try:
print(f"Attempt {attempt+1} of {max_attempts}...")
ser = serial.Serial(port, baudrate, timeout=1)
# Initialize data dictionary and tracking variables
data = {}
start_time = time.time()
while time.time() - start_time < timeout:
line = ser.readline().decode('utf-8', errors='ignore').strip()
if not line:
continue
# Check if line contains a key-value pair
if '\t' in line:
key, value = line.split('\t', 1)
data[key] = value
print(f"{key}: {value}")
else:
print(f"Info: {line}")
# Check if we have a complete data block
if 'Checksum' in data:
# Check if we have all required keys
missing_keys = [key for key in required_keys if key not in data]
if not missing_keys:
ser.close()
return data
else:
print(f"Incomplete data, missing: {', '.join(missing_keys)}")
# Clear data and continue reading
data = {}
# Timeout occurred
print(f"Timeout on attempt {attempt+1}: Could not get complete data")
ser.close()
# Add small delay between attempts
if attempt < max_attempts - 1:
print("Waiting before next attempt...")
time.sleep(2)
except Exception as e:
print(f"Error on attempt {attempt+1}: {e}")
try:
ser.close()
except:
pass
print("All attempts failed")
return None
def parse_values(data):
"""Convert string values to appropriate types"""
if not data:
return None
parsed = {}
# Define conversions for each key
conversions = {
'PID': str,
'FW': int,
'SER#': str,
'V': lambda x: float(x)/1000, # Convert mV to V
'I': lambda x: float(x)/1000, # Convert mA to A
'VPV': lambda x: float(x)/1000 if x != '---' else 0, # Convert mV to V
'PPV': int,
'CS': int,
'MPPT': int,
'OR': str,
'ERR': int,
'LOAD': str,
'IL': int,
'H19': int, # Total energy absorbed in kWh
'H20': int, # Total energy discharged in kWh
'H21': int,
'H22': int,
'H23': int,
'HSDS': int
}
# Convert values according to their type
for key, value in data.items():
if key in conversions:
try:
parsed[key] = conversions[key](value)
except (ValueError, TypeError):
parsed[key] = value # Keep as string if conversion fails
else:
parsed[key] = value
return parsed
def get_charger_status(cs_value):
"""Convert CS numeric value to human-readable status"""
status_map = {
0: "Off",
1: "Low power mode",
2: "Fault",
3: "Bulk",
4: "Absorption",
5: "Float",
6: "Storage",
7: "Equalize",
9: "Inverting",
11: "Power supply",
245: "Starting-up",
247: "Repeated absorption",
252: "External control"
}
return status_map.get(cs_value, f"Unknown ({cs_value})")
if __name__ == "__main__":
# Read data (with retry logic)
raw_data = read_vedirect()
if raw_data:
# Parse data
parsed_data = parse_values(raw_data)
if parsed_data:
# Check if we have valid battery voltage
if parsed_data.get('V', 0) > 0:
print("\n===== MPPT Summary =====")
print(f"Battery: {parsed_data.get('V', 0):.2f}V, {parsed_data.get('I', 0):.2f}A")
print(f"Solar: {parsed_data.get('VPV', 0):.2f}V, {parsed_data.get('PPV', 0)}W")
print(f"Charger status: {get_charger_status(parsed_data.get('CS', 0))}")
# Save to SQLite
cursor.execute("SELECT * FROM timestamp_table LIMIT 1")
row = cursor.fetchone()
rtc_time_str = row[1]
# Extract values
battery_voltage = parsed_data.get('V', 0)
battery_current = parsed_data.get('I', 0)
solar_voltage = parsed_data.get('VPV', 0)
solar_power = parsed_data.get('PPV', 0)
charger_status = parsed_data.get('CS', 0)
try:
cursor.execute('''
INSERT INTO data_MPPT (timestamp, battery_voltage, battery_current, solar_voltage, solar_power, charger_status)
VALUES (?, ?, ?, ?, ?, ?)''',
(rtc_time_str, battery_voltage, battery_current, solar_voltage, solar_power, charger_status))
conn.commit()
print("MPPT data saved successfully!")
except Exception as e:
print(f"Database error: {e}")
else:
print("Invalid data: Battery voltage is zero or missing")
else:
print("Failed to parse data")
else:
print("No valid data received from MPPT controller")
# Always close the connection
conn.close()

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,8 +28,8 @@ 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 --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 ""
sudo ssh-copy-id -i /var/www/.ssh/id_rsa.pub -p 50221 airlab_server1@aircarto.fr sudo ssh-copy-id -i /var/www/.ssh/id_rsa.pub -p 50221 airlab_server1@aircarto.fr

View File

@@ -1,11 +1,15 @@
#!/usr/bin/python3
""" """
Script to set the RTC using an NTP server. ____ _____ ____
| _ \_ _/ ___|
| |_) || || |
| _ < | || |___
|_| \_\|_| \____|
Script to set the RTC using an NTP server (script used by web UI)
RPI needs to be connected to the internet (WIFI). RPI needs to be connected to the internet (WIFI).
Requires ntplib and pytz: Requires ntplib and pytz:
sudo pip3 install ntplib pytz --break-system-packages sudo pip3 install ntplib pytz --break-system-packages
/usr/bin/python3 /var/www/nebuleair_pro_4g/RTC/set_with_NTP.py
""" """
import smbus2 import smbus2
import time import time
@@ -49,29 +53,95 @@ def set_time(bus, year, month, day, hour, minute, second):
]) ])
def read_time(bus): def read_time(bus):
"""Read the RTC time.""" """Read the RTC time and validate the values."""
try:
data = bus.read_i2c_block_data(DS3231_ADDR, REG_TIME, 7) data = bus.read_i2c_block_data(DS3231_ADDR, REG_TIME, 7)
# Convert from BCD
second = bcd_to_dec(data[0] & 0x7F) second = bcd_to_dec(data[0] & 0x7F)
minute = bcd_to_dec(data[1]) minute = bcd_to_dec(data[1])
hour = bcd_to_dec(data[2] & 0x3F) hour = bcd_to_dec(data[2] & 0x3F)
day = bcd_to_dec(data[4]) day = bcd_to_dec(data[4])
month = bcd_to_dec(data[5]) month = bcd_to_dec(data[5])
year = bcd_to_dec(data[6]) + 2000 year = bcd_to_dec(data[6]) + 2000
# Print raw values for debugging
print(f"Raw RTC values: {data}")
print(f"Decoded values: Y:{year} M:{month} D:{day} H:{hour} M:{minute} S:{second}")
# Validate date values
if not (1 <= month <= 12):
print(f"Invalid month value: {month}, using default")
month = 1
# Check days in month (simplified)
days_in_month = [0, 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
if not (1 <= day <= days_in_month[month]):
print(f"Invalid day value: {day} for month {month}, using default")
day = 1
# Validate time values
if not (0 <= hour <= 23):
print(f"Invalid hour value: {hour}, using default")
hour = 0
if not (0 <= minute <= 59):
print(f"Invalid minute value: {minute}, using default")
minute = 0
if not (0 <= second <= 59):
print(f"Invalid second value: {second}, using default")
second = 0
return (year, month, day, hour, minute, second) return (year, month, day, hour, minute, second)
except Exception as e:
print(f"Error reading RTC: {e}")
# Return a safe default date (2023-01-01 00:00:00)
return (2023, 1, 1, 0, 0, 0)
def get_internet_time(): def get_internet_time():
"""Get the current time from an NTP server.""" """Get the current time from an NTP server."""
ntp_client = ntplib.NTPClient() ntp_client = ntplib.NTPClient()
response = ntp_client.request('pool.ntp.org') # Try multiple NTP servers in case one fails
servers = ['pool.ntp.org', 'time.google.com', 'time.windows.com', 'time.apple.com']
for server in servers:
try:
print(f"Trying NTP server: {server}")
response = ntp_client.request(server, timeout=2)
utc_time = datetime.utcfromtimestamp(response.tx_time) utc_time = datetime.utcfromtimestamp(response.tx_time)
print(f"Successfully got time from {server}")
return utc_time return utc_time
except Exception as e:
print(f"Failed to get time from {server}: {e}")
# If all servers fail, raise exception
raise Exception("All NTP servers failed")
def main(): def main():
try:
bus = smbus2.SMBus(1) bus = smbus2.SMBus(1)
# Test if RTC is accessible
try:
bus.read_byte(DS3231_ADDR)
print("RTC module is accessible")
except Exception as e:
print(f"Error accessing RTC module: {e}")
print("Please check connections and I2C configuration")
return
# Get the current time from the RTC # Get the current time from the RTC
try:
year, month, day, hours, minutes, seconds = read_time(bus) year, month, day, hours, minutes, seconds = read_time(bus)
# Create datetime object with validation to handle invalid dates
rtc_time = datetime(year, month, day, hours, minutes, seconds) rtc_time = datetime(year, month, day, hours, minutes, seconds)
print(f"Actual RTC Time : {rtc_time.strftime('%Y-%m-%d %H:%M:%S')}")
except ValueError as e:
print(f"Invalid date/time read from RTC: {e}")
print("Will proceed with setting RTC from internet time")
rtc_time = None
# Get current UTC time from an NTP server # Get current UTC time from an NTP server
try: try:
@@ -79,19 +149,35 @@ def main():
print(f"Time from Internet (UTC) : {internet_utc_time.strftime('%Y-%m-%d %H:%M:%S')}") print(f"Time from Internet (UTC) : {internet_utc_time.strftime('%Y-%m-%d %H:%M:%S')}")
except Exception as e: except Exception as e:
print(f"Error retrieving time from the internet: {e}") print(f"Error retrieving time from the internet: {e}")
if rtc_time is None:
print("Cannot proceed without either valid RTC time or internet time")
return
print("Will keep current RTC time")
return return
# Print current RTC time
print(f"Actual RTC Time : {rtc_time.strftime('%Y-%m-%d %H:%M:%S')}")
# Set the RTC to UTC time # Set the RTC to UTC time
print("Setting RTC to internet time...")
set_time(bus, internet_utc_time.year, internet_utc_time.month, internet_utc_time.day, set_time(bus, internet_utc_time.year, internet_utc_time.month, internet_utc_time.day,
internet_utc_time.hour, internet_utc_time.minute, internet_utc_time.second) internet_utc_time.hour, internet_utc_time.minute, internet_utc_time.second)
# Read and print the new time from RTC # Read and print the new time from RTC
print("Reading back new RTC time...")
year, month, day, hour, minute, second = read_time(bus) year, month, day, hour, minute, second = read_time(bus)
rtc_time_new = datetime(year, month, day, hour, minute, second) rtc_time_new = datetime(year, month, day, hour, minute, second)
print(f"New RTC Time (UTC) : {rtc_time_new.strftime('%Y-%m-%d %H:%M:%S')}") print(f"New RTC Time (UTC) : {rtc_time_new.strftime('%Y-%m-%d %H:%M:%S')}")
# Calculate difference to verify accuracy
time_diff = abs((rtc_time_new - internet_utc_time).total_seconds())
print(f"Time difference : {time_diff:.2f} seconds")
if time_diff > 5:
print("Warning: RTC time differs significantly from internet time")
print("You may need to retry or check RTC module")
else:
print("RTC successfully synchronized with internet time")
except Exception as e:
print(f"Unexpected error: {e}")
if __name__ == "__main__": if __name__ == "__main__":
main() main()

View File

@@ -1,5 +1,11 @@
""" """
Script to set the RTC using the browser time. ____ _____ ____
| _ \_ _/ ___|
| |_) || || |
| _ < | || |___
|_| \_\|_| \____|
Script to set the RTC using the browser time (script used by the web UI).
/usr/bin/python3 /var/www/nebuleair_pro_4g/RTC/set_with_browserTime.py '2024-01-30 12:48:39' /usr/bin/python3 /var/www/nebuleair_pro_4g/RTC/set_with_browserTime.py '2024-01-30 12:48:39'

166
SARA/R5/setPDP.py Normal file
View File

@@ -0,0 +1,166 @@
'''
____ _ ____ _
/ ___| / \ | _ \ / \
\___ \ / _ \ | |_) | / _ \
___) / ___ \| _ < / ___ \
|____/_/ \_\_| \_\/_/ \_\
Script to set the PDP context for the SARA R5
/usr/bin/python3 /var/www/nebuleair_pro_4g/SARA/R5/setPDP.py
'''
import serial
import time
import sys
import json
import re
#get data from config
def load_config(config_file):
try:
with open(config_file, 'r') as file:
config_data = json.load(file)
return config_data
except Exception as e:
print(f"Error loading config file: {e}")
return {}
#Fonction pour mettre à jour le JSON de configuration
def update_json_key(file_path, key, value):
"""
Updates a specific key in a JSON file with a new value.
:param file_path: Path to the JSON file.
:param key: The key to update in the JSON file.
:param value: The new value to assign to the key.
"""
try:
# Load the existing data
with open(file_path, "r") as file:
data = json.load(file)
# Check if the key exists in the JSON file
if key in data:
data[key] = value # Update the key with the new value
else:
print(f"Key '{key}' not found in the JSON file.")
return
# Write the updated data back to the file
with open(file_path, "w") as file:
json.dump(data, file, indent=2) # Use indent for pretty printing
print(f"💾 updating '{key}' to '{value}'.")
except Exception as e:
print(f"Error updating the JSON file: {e}")
# Define the config file path
config_file = '/var/www/nebuleair_pro_4g/config.json'
# Load the configuration data
config = load_config(config_file)
baudrate = config.get('SaraR4_baudrate', 115200) #baudrate du sara R4
device_id = config.get('deviceID', '').upper() #device ID en maj
ser_sara = serial.Serial(
port='/dev/ttyAMA2',
baudrate=baudrate, #115200 ou 9600
parity=serial.PARITY_NONE, #PARITY_NONE, PARITY_EVEN or PARITY_ODD
stopbits=serial.STOPBITS_ONE,
bytesize=serial.EIGHTBITS,
timeout = 2
)
def read_complete_response(serial_connection, timeout=2, end_of_response_timeout=2, wait_for_lines=None, debug=True):
'''
Fonction très importante !!!
Reads the complete response from a serial connection and waits for specific lines.
'''
if wait_for_lines is None:
wait_for_lines = [] # Default to an empty list if not provided
response = bytearray()
serial_connection.timeout = timeout
end_time = time.time() + end_of_response_timeout
start_time = time.time()
while True:
elapsed_time = time.time() - start_time # Time since function start
if serial_connection.in_waiting > 0:
data = serial_connection.read(serial_connection.in_waiting)
response.extend(data)
end_time = time.time() + end_of_response_timeout # Reset timeout on new data
# Decode and check for any target line
decoded_response = response.decode('utf-8', errors='replace')
for target_line in wait_for_lines:
if target_line in decoded_response:
if debug:
print(f"[DEBUG] 🔎 Found target line: {target_line} (in {elapsed_time:.2f}s)")
return decoded_response # Return response immediately if a target line is found
elif time.time() > end_time:
if debug:
print(f"[DEBUG] Timeout reached. No more data received.")
break
time.sleep(0.1) # Short sleep to prevent busy waiting
# Final response and debug output
total_elapsed_time = time.time() - start_time
if debug:
print(f"[DEBUG] ⏱️ elapsed time: {total_elapsed_time:.2f}s. ⏱️")
# Check if the elapsed time exceeded 10 seconds
if total_elapsed_time > 10 and debug:
print(f"[ALERT] 🚨 The operation took too long 🚨")
print(f'<span style="color: red;font-weight: bold;">[ALERT] ⚠️{total_elapsed_time:.2f}s⚠</span>')
return response.decode('utf-8', errors='replace') # Return the full response if no target line is found
try:
print('Start script')
# 1. Check connection
print('Check SARA R5 connexion')
command = f'ATI0\r'
ser_sara.write(command.encode('utf-8'))
response_SARA_1 = read_complete_response(ser_sara, wait_for_lines=["OK"])
print(response_SARA_1, end="")
time.sleep(1)
# 2. Activate PDP context 1
print('Activate PDP context 1')
command = f'AT+CGACT=1,1\r'
ser_sara.write(command.encode('utf-8'))
response_SARA_2 = read_complete_response(ser_sara, wait_for_lines=["OK"])
print(response_SARA_2, end="")
time.sleep(1)
# 2. Set the PDP type
print('Set the PDP type to IPv4 referring to the outputof the +CGDCONT read command')
command = f'AT+UPSD=0,0,0\r'
ser_sara.write(command.encode('utf-8'))
response_SARA_3 = read_complete_response(ser_sara, wait_for_lines=["OK"])
print(response_SARA_3, end="")
time.sleep(1)
# 2. Profile #0 is mapped on CID=1.
print('Profile #0 is mapped on CID=1.')
command = f'AT+UPSD=0,100,1\r'
ser_sara.write(command.encode('utf-8'))
response_SARA_3 = read_complete_response(ser_sara, wait_for_lines=["OK"])
print(response_SARA_3, end="")
time.sleep(1)
# 2. Set the PDP type
print('Activate the PSD profile #0: the IPv4 address is already assigned by the network.')
command = f'AT+UPSDA=0,3\r'
ser_sara.write(command.encode('utf-8'))
response_SARA_3 = read_complete_response(ser_sara, wait_for_lines=["OK","+UUPSDA"])
print(response_SARA_3, end="")
time.sleep(1)
except Exception as e:
print("An error occurred:", e)
traceback.print_exc() # This prints the full traceback

View File

@@ -0,0 +1,103 @@
'''
____ _ ____ _
/ ___| / \ | _ \ / \
\___ \ / _ \ | |_) | / _ \
___) / ___ \| _ < / ___ \
|____/_/ \_\_| \_\/_/ \_\
Script to Configures the network connection to a Multi GNSS Assistance (MGA) server used also per CellLocate
/usr/bin/python3 /var/www/nebuleair_pro_4g/SARA/cellLocate/server_conf.py ttyAMA2 1
AT+UGSRV="cell-live1.services.u-blox.com","cell-live2.services.u-blox.com","XkEKfGqVSbmNE1eZfBZm4Q"
'''
import serial
import time
import sys
import json
parameter = sys.argv[1:] # Exclude the script name
port='/dev/'+parameter[0] # ex: ttyAMA2
timeout = float(parameter[1]) # ex:2
#get baudrate
def load_config(config_file):
try:
with open(config_file, 'r') as file:
config_data = json.load(file)
return config_data
except Exception as e:
print(f"Error loading config file: {e}")
return {}
# Define the config file path
config_file = '/var/www/nebuleair_pro_4g/config.json'
# Load the configuration data
config = load_config(config_file)
# Access the shared variables
baudrate = config.get('SaraR4_baudrate', 115200)
ser = serial.Serial(
port=port, #USB0 or ttyS0
baudrate=baudrate, #115200 ou 9600
parity=serial.PARITY_NONE, #PARITY_NONE, PARITY_EVEN or PARITY_ODD
stopbits=serial.STOPBITS_ONE,
bytesize=serial.EIGHTBITS,
timeout = timeout
)
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
#command = f'ATI\r'
command = f'AT+UGSRV="cell-live1.services.u-blox.com","cell-live2.services.u-blox.com","XkEKfGqVSbmNE1eZfBZm4Q"\r'
ser.write((command + '\r').encode('utf-8'))
response = read_complete_response(ser, wait_for_lines=["+UULOC"])
print(response)

View File

@@ -13,19 +13,55 @@ Script that starts at the boot of the RPI (with cron)
''' '''
import serial import serial
import RPi.GPIO as GPIO
import time import time
import sys import sys
import json import json
import re import re
#get data from config #GPIO
def load_config(config_file): SARA_power_GPIO = 16
SARA_ON_GPIO = 20
GPIO.setmode(GPIO.BCM) # Use BCM numbering
GPIO.setup(SARA_power_GPIO, GPIO.OUT) # Set GPIO17 as an output
import sqlite3
# database connection
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
@@ -57,13 +93,60 @@ 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
sara_r5_DPD_setup = False
ser_sara = serial.Serial( ser_sara = serial.Serial(
port='/dev/ttyAMA2', port='/dev/ttyAMA2',
baudrate=baudrate, #115200 ou 9600 baudrate=baudrate, #115200 ou 9600
@@ -120,20 +203,46 @@ def read_complete_response(serial_connection, timeout=2, end_of_response_timeout
try: try:
print('<h3>Start reboot python script</h3>') print('<h3>Start reboot python script</h3>')
#First we need to power on the module (if connected to mosfet via gpio16)
GPIO.output(SARA_power_GPIO, GPIO.HIGH)
time.sleep(5)
#check modem status #check modem status
#Attention:
# SARA R4 response: Manufacturer: u-blox Model: SARA-R410M-02B
# SArA R5 response: SARA-R500S-01B-00
print("Check SARA Status") print("Check SARA Status")
command = f'ATI\r' command = f'ATI\r'
ser_sara.write(command.encode('utf-8')) ser_sara.write(command.encode('utf-8'))
response_SARA_ATI = read_complete_response(ser_sara, wait_for_lines=["IMEI"]) response_SARA_ATI = read_complete_response(ser_sara, wait_for_lines=["IMEI"])
print(response_SARA_ATI) print(response_SARA_ATI)
match = re.search(r"Model:\s*(.+)", response_SARA_ATI)
model = match.group(1).strip() if match else "Unknown" # Strip unwanted characters # Check for SARA model with more robust regex
print(f" Model: {model}") model = "Unknown"
update_json_key(config_file, "modem_version", model) if "SARA-R410M" in response_SARA_ATI:
model = "SARA-R410M"
print("📱 Detected SARA R4 modem")
elif "SARA-R500" in response_SARA_ATI:
model = "SARA-R500"
print("📱 Detected SARA R5 modem")
sara_r5_DPD_setup = True
else:
# Fallback to regex match if direct string match fails
match = re.search(r"Model:\s*([A-Za-z0-9\-]+)", response_SARA_ATI)
if match:
model = match.group(1).strip()
else:
model = "Unknown"
print("⚠️ Could not identify modem model")
print(f"🔍 Model: {model}")
update_sqlite_config("modem_version", model)
time.sleep(1) time.sleep(1)
'''
# 1. Set AIRCARTO URL AIRCARTO
'''
# 1. Set AIRCARTO URL (profile id = 0)
print('Set aircarto URL') print('Set aircarto URL')
aircarto_profile_id = 0 aircarto_profile_id = 0
aircarto_url="data.nebuleair.fr" aircarto_url="data.nebuleair.fr"
@@ -143,26 +252,155 @@ try:
print(response_SARA_1) print(response_SARA_1)
time.sleep(1) time.sleep(1)
#2. Set uSpot URL '''
print('Set uSpot URL') uSpot
'''
print("Set uSpot URL with SSL")
security_profile_id = 1
uSpot_profile_id = 1 uSpot_profile_id = 1
uSpot_url="api-prod.uspot.probesys.net" uSpot_url="api-prod.uspot.probesys.net"
command = f'AT+UHTTP={uSpot_profile_id},1,"{uSpot_url}"\r'
ser_sara.write(command.encode('utf-8'))
response_SARA_2 = read_complete_response(ser_sara, wait_for_lines=["OK"]) #step 1: import the certificate
print("➡️ import certificate")
certificate_name = "e6"
with open("/var/www/nebuleair_pro_4g/SARA/SSL/certificate/e6.pem", "rb") as cert_file:
certificate = cert_file.read()
size_of_string = len(certificate)
# AT+USECMNG=0,<type>,<internal_name>,<data_size>
# type-> 0 -> trusted root CA
command = f'AT+USECMNG=0,0,"{certificate_name}",{size_of_string}\r'
ser_sara.write((command + '\r').encode('utf-8'))
response_SARA_1 = read_complete_response(ser_sara)
print(response_SARA_1)
time.sleep(0.5)
print("➡️ add certificate")
ser_sara.write(certificate)
response_SARA_2 = read_complete_response(ser_sara)
print(response_SARA_2) print(response_SARA_2)
time.sleep(0.5)
# op_code: 0 -> certificate validation level
# param_val : 0 -> Level 0 No validation; 1-> Level 1 Root certificate validation
print("Set the security profile (params)")
certification_level=0
command = f'AT+USECPRF={security_profile_id},0,{certification_level}\r'
ser_sara.write((command + '\r').encode('utf-8'))
response_SARA_5b = read_complete_response(ser_sara, wait_for_lines=["OK"])
print(response_SARA_5b)
time.sleep(0.5)
# op_code: 1 -> minimum SSL/TLS version
# param_val : 0 -> any; server can use any version for the connection; 1-> LSv1.0; 2->TLSv1.1; 3->TLSv1.2;
print("Set the security profile (params)")
minimum_SSL_version = 0
command = f'AT+USECPRF={security_profile_id},1,{minimum_SSL_version}\r'
ser_sara.write((command + '\r').encode('utf-8'))
response_SARA_5bb = read_complete_response(ser_sara, wait_for_lines=["OK"])
print(response_SARA_5bb)
time.sleep(0.5)
#op_code: 2 -> legacy cipher suite selection
# 0 (factory-programmed value): a list of default cipher suites is proposed at the beginning of handshake process, and a cipher suite will be negotiated among the cipher suites proposed in the list.
print("Set cipher")
cipher_suite = 0
command = f'AT+USECPRF={security_profile_id},2,{cipher_suite}\r'
ser_sara.write((command + '\r').encode('utf-8'))
response_SARA_5cc = read_complete_response(ser_sara, wait_for_lines=["OK"])
print(response_SARA_5cc)
time.sleep(0.5)
# op_code: 3 -> trusted root certificate internal name
print("Set the security profile (choose cert)")
command = f'AT+USECPRF={security_profile_id},3,"{certificate_name}"\r'
ser_sara.write((command + '\r').encode('utf-8'))
response_SARA_5c = read_complete_response(ser_sara, wait_for_lines=["OK"])
print(response_SARA_5c)
time.sleep(0.5)
# op_code: 10 -> SNI (server name indication)
print("Set the SNI")
command = f'AT+USECPRF={security_profile_id},10,"{uSpot_url}"\r'
ser_sara.write((command + '\r').encode('utf-8'))
response_SARA_5cf = read_complete_response(ser_sara, wait_for_lines=["OK"])
print(response_SARA_5cf)
time.sleep(0.5)
#step 4: set url (op_code = 1)
print("SET URL")
command = f'AT+UHTTP={uSpot_profile_id},1,"{uSpot_url}"\r'
ser_sara.write((command + '\r').encode('utf-8'))
response_SARA_5 = read_complete_response(ser_sara, wait_for_lines=["OK"])
print(response_SARA_5)
time.sleep(1) time.sleep(1)
print("set port 81") #step 4: set PORT (op_code = 5)
command = f'AT+UHTTP={uSpot_profile_id},5,81\r' print("SET PORT")
port = 443
command = f'AT+UHTTP={uSpot_profile_id},5,{port}\r'
ser_sara.write((command + '\r').encode('utf-8')) ser_sara.write((command + '\r').encode('utf-8'))
response_SARA_55 = read_complete_response(ser_sara, wait_for_lines=["OK"]) response_SARA_55 = read_complete_response(ser_sara, wait_for_lines=["OK"])
print(response_SARA_55) print(response_SARA_55)
time.sleep(1) time.sleep(1)
#step 4: set url to SSL (op_code = 6) (http_secure = 1 for HTTPS)(USECMNG_PROFILE = 2)
print("SET SSL")
http_secure = 1
command = f'AT+UHTTP={uSpot_profile_id},6,{http_secure},{security_profile_id}\r'
ser_sara.write(command.encode('utf-8'))
response_SARA_5fg = read_complete_response(ser_sara, wait_for_lines=["OK"])
print(response_SARA_5fg)
time.sleep(1)
'''
SARA R5
'''
if sara_r5_DPD_setup:
print("SARA R5 PDP SETUP")
# 2. Activate PDP context 1
print('Activate PDP context 1')
command = f'AT+CGACT=1,1\r'
ser_sara.write(command.encode('utf-8'))
response_SARA_2 = read_complete_response(ser_sara, wait_for_lines=["OK"])
print(response_SARA_2, end="")
time.sleep(1)
# 2. Set the PDP type
print('Set the PDP type to IPv4 referring to the outputof the +CGDCONT read command')
command = f'AT+UPSD=0,0,0\r'
ser_sara.write(command.encode('utf-8'))
response_SARA_3 = read_complete_response(ser_sara, wait_for_lines=["OK"])
print(response_SARA_3, end="")
time.sleep(1)
# 2. Profile #0 is mapped on CID=1.
print('Profile #0 is mapped on CID=1.')
command = f'AT+UPSD=0,100,1\r'
ser_sara.write(command.encode('utf-8'))
response_SARA_3 = read_complete_response(ser_sara, wait_for_lines=["OK"])
print(response_SARA_3, end="")
time.sleep(1)
# 2. Set the PDP type
print('Activate the PSD profile #0: the IPv4 address is already assigned by the network.')
command = f'AT+UPSDA=0,3\r'
ser_sara.write(command.encode('utf-8'))
response_SARA_3 = read_complete_response(ser_sara, wait_for_lines=["OK","+UUPSDA"])
print(response_SARA_3, end="")
time.sleep(1)
#3. Get localisation (CellLocate) #3. Get localisation (CellLocate)
mode = 2 mode = 2 #single shot position
sensor = 2 sensor = 2 #use cellular CellLocate® location information
response_type = 0 response_type = 0
timeout_s = 2 timeout_s = 2
accuracy_m = 1 accuracy_m = 1
@@ -179,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

@@ -7,6 +7,8 @@
Script to see if the SARA-R410 is running Script to see if the SARA-R410 is running
ex: ex:
python3 /var/www/nebuleair_pro_4g/SARA/sara.py ttyAMA2 ATI 2
ex 1 (get SIM infos)
python3 /var/www/nebuleair_pro_4g/SARA/sara.py ttyAMA2 AT+CCID? 2 python3 /var/www/nebuleair_pro_4g/SARA/sara.py ttyAMA2 AT+CCID? 2
ex 2 (turn on blue light): ex 2 (turn on blue light):
python3 /var/www/nebuleair_pro_4g/SARA/sara.py ttyAMA2 AT+UGPIOC=16,2 2 python3 /var/www/nebuleair_pro_4g/SARA/sara.py ttyAMA2 AT+UGPIOC=16,2 2
@@ -14,6 +16,8 @@ ex 3 (reconnect network)
python3 /var/www/nebuleair_pro_4g/SARA/sara.py ttyAMA2 AT+COPS=1,2,20801 20 python3 /var/www/nebuleair_pro_4g/SARA/sara.py ttyAMA2 AT+COPS=1,2,20801 20
ex 4 (get HTTP Profiles) ex 4 (get HTTP Profiles)
python3 /var/www/nebuleair_pro_4g/SARA/sara.py ttyAMA2 AT+UHTTP? 2 python3 /var/www/nebuleair_pro_4g/SARA/sara.py ttyAMA2 AT+UHTTP? 2
ex 5 (get IP addr)
python3 /var/www/nebuleair_pro_4g/SARA/sara.py ttyAMA2 AT+CGPADDR=1 2
''' '''
@@ -45,6 +49,9 @@ config = load_config(config_file)
# Access the shared variables # Access the shared variables
baudrate = config.get('SaraR4_baudrate', 115200) baudrate = config.get('SaraR4_baudrate', 115200)
try:
ser = serial.Serial( ser = serial.Serial(
port=port, #USB0 or ttyS0 port=port, #USB0 or ttyS0
baudrate=baudrate, #115200 ou 9600 baudrate=baudrate, #115200 ou 9600
@@ -71,25 +78,33 @@ ser.write((command + '\r').encode('utf-8'))
#ser.write(b'AT+CMUX=?') #ser.write(b'AT+CMUX=?')
try:
# Read lines until a timeout occurs # Read lines until a timeout occurs
response_lines = [] response_lines = []
while True: start_time = time.time()
line = ser.readline().decode('utf-8').strip()
if not line: while (time.time() - start_time) < timeout:
break # Break the loop if an empty line is encountered line = ser.readline().decode('utf-8', errors='ignore').strip()
if line:
response_lines.append(line) response_lines.append(line)
# Check if we received any data
if not response_lines:
print(f"ERROR: No response received from {port} after sending command: {command}")
sys.exit(1)
# Print the response # Print the response
for line in response_lines: for line in response_lines:
print(line) print(line)
except serial.SerialException as e: except serial.SerialException as e:
print(f"Error: {e}") print(f"ERROR: Serial communication error: {e}")
sys.exit(1)
except Exception as e:
print(f"ERROR: Unexpected error: {e}")
sys.exit(1)
finally: finally:
if ser.is_open: # Close the serial port if it's open
if 'ser' in locals() and ser.is_open:
ser.close() ser.close()
#print("Serial closed")

79
SARA/sara_checkDNS.py Normal file
View File

@@ -0,0 +1,79 @@
'''
____ _ ____ _
/ ___| / \ | _ \ / \
\___ \ / _ \ | |_) | / _ \
___) / ___ \| _ < / ___ \
|____/_/ \_\_| \_\/_/ \_\
Script to resolve DNS (get IP from domain name) with AT+UDNSRN command
Ex:
/usr/bin/python3 /var/www/nebuleair_pro_4g/SARA/sara_checkDNS.py ttyAMA2 data.nebuleair.fr
To do: need to add profile id as parameter
'''
import serial
import time
import sys
import json
parameter = sys.argv[1:] # Exclude the script name
#print("Parameters received:")
port='/dev/'+parameter[0] # ex: ttyAMA2
url = parameter[1] # ex: data.mobileair.fr
#get baudrate
def load_config(config_file):
try:
with open(config_file, 'r') as file:
config_data = json.load(file)
return config_data
except Exception as e:
print(f"Error loading config file: {e}")
return {}
# Define the config file path
config_file = '/var/www/nebuleair_pro_4g/config.json'
# Load the configuration data
config = load_config(config_file)
# Access the shared variables
baudrate = config.get('SaraR4_baudrate', 115200)
ser = serial.Serial(
port=port, #USB0 or ttyS0
baudrate=baudrate, #115200 ou 9600
parity=serial.PARITY_NONE, #PARITY_NONE, PARITY_EVEN or PARITY_ODD
stopbits=serial.STOPBITS_ONE,
bytesize=serial.EIGHTBITS,
timeout = 2
)
command = f'AT+UDNSRN=0,"{url}"\r'
ser.write((command + '\r').encode('utf-8'))
print("****")
print("DNS check")
try:
# Read lines until a timeout occurs
response_lines = []
while True:
line = ser.readline().decode('utf-8').strip()
if not line:
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:
print(f"Error: {e}")
finally:
if ser.is_open:
ser.close()
print("****")
#print("Serial closed")

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:
line = ser.readline().decode('utf-8').strip()
if not line:
break # Break the loop if an empty line is encountered
response_lines.append(line)
# Print the response print('<p class="text-danger-emphasis">')
for line in response_lines: print(response)
print(line) print("</p>", end="")
except serial.SerialException as e: except serial.SerialException as e:
print(f"Error: {e}") print(f"Error: {e}")

View File

@@ -89,6 +89,24 @@ def read_complete_response(serial_connection, timeout=2, end_of_response_timeout
return response.decode('utf-8', errors='replace') # Return the full response if no target line is found return response.decode('utf-8', errors='replace') # Return the full response if no target line is found
def extract_error_code(response):
"""
Extract just the error code from AT+UHTTPER response
"""
for line in response.split('\n'):
if '+UHTTPER' in line:
try:
# Split the line and get the third value (error code)
parts = line.split(':')[1].strip().split(',')
if len(parts) >= 3:
error_code = int(parts[2])
return error_code
except:
pass
# Return None if we couldn't find the error code
return None
try: try:
#3. Send to endpoint (with device ID) #3. Send to endpoint (with device ID)
print("Send data (GET REQUEST):") print("Send data (GET REQUEST):")
@@ -111,7 +129,36 @@ try:
parts = http_response.split(',') parts = http_response.split(',')
# 2.1 code 0 (HTTP failed) ⛔⛔⛔ # 2.1 code 0 (HTTP failed) ⛔⛔⛔
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("⛔ATTENTION: HTTP operation failed") print("ATTENTION: HTTP operation failed")
#get error code
print("Getting error code (11->Server connection error, 73->Secure socket connect error)")
command = f'AT+UHTTPER={aircarto_profile_id}\r'
ser_sara.write(command.encode('utf-8'))
response_SARA_9 = read_complete_response(ser_sara, wait_for_lines=["OK"], debug=False)
print('<p class="text-danger-emphasis">')
print(response_SARA_9)
print("</p>", end="")
# Extract just the error code
error_code = extract_error_code(response_SARA_9)
if error_code is not None:
# Display interpretation based on error code
if error_code == 0:
print('<p class="text-success">No error detected</p>')
elif error_code == 4:
print('<p class="text-danger">Error 4: Invalid server Hostname</p>')
elif error_code == 11:
print('<p class="text-danger">Error 11: Server connection error</p>')
elif error_code == 22:
print('<p class="text-danger">Error 22: PSD or CSD connection not established</p>')
elif error_code == 73:
print('<p class="text-danger">Error 73: Secure socket connect error</p>')
else:
print(f'<p class="text-danger">Unknown error code: {error_code}</p>')
else:
print('<p class="text-danger">Could not extract error code from response</p>')
# 2.2 code 1 (HHTP succeded) # 2.2 code 1 (HHTP succeded)
else: else:
# Si la commande HTTP a réussi # Si la commande HTTP a réussi

View File

@@ -49,6 +49,8 @@ ser = serial.Serial(
) )
command = f'AT+CGDCONT=1,"IP","{apn_address}"\r' command = f'AT+CGDCONT=1,"IP","{apn_address}"\r'
#command = f'AT+CGDCONT=1,"IPV4V6","{apn_address}"\r'
#command = f'AT+CGDCONT=1,"IP","{apn_address}",0,0\r'
ser.write((command + '\r').encode('utf-8')) ser.write((command + '\r').encode('utf-8'))

View File

@@ -8,7 +8,6 @@
Script to set the URL for a HTTP request Script to set the URL for a HTTP request
Ex: Ex:
/usr/bin/python3 /var/www/nebuleair_pro_4g/SARA/sara_setURL.py ttyAMA2 data.nebuleair.fr 0 /usr/bin/python3 /var/www/nebuleair_pro_4g/SARA/sara_setURL.py ttyAMA2 data.nebuleair.fr 0
To do: need to add profile id as parameter
First profile id: First profile id:
AT+UHTTP=0,1,"data.nebuleair.fr" AT+UHTTP=0,1,"data.nebuleair.fr"

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

@@ -5,3 +5,6 @@
@reboot sleep 30 && /usr/bin/python3 /var/www/nebuleair_pro_4g/SARA/reboot/start.py >> /var/www/nebuleair_pro_4g/logs/app.log 2>&1 @reboot sleep 30 && /usr/bin/python3 /var/www/nebuleair_pro_4g/SARA/reboot/start.py >> /var/www/nebuleair_pro_4g/logs/app.log 2>&1
0 0 * * * > /var/www/nebuleair_pro_4g/logs/master.log 0 0 * * * > /var/www/nebuleair_pro_4g/logs/master.log
0 0 * * * > /var/www/nebuleair_pro_4g/logs/master_errors.log
0 0 * * * > /var/www/nebuleair_pro_4g/logs/app.log

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,27 +213,96 @@
}); });
}); });
//end document.addEventListener
/*
___ _ _
/ _ \ _ __ | | ___ __ _ __| |
| | | | '_ \| | / _ \ / _` |/ _` |
| |_| | | | | |__| (_) | (_| | (_| |
\___/|_| |_|_____\___/ \__,_|\__,_|
*/
window.onload = function() { window.onload = function() {
//NEW way to get config (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);
//device name
const deviceName = document.getElementById("device_name");
deviceName.value = response.deviceName;
//device name_side bar
const elements = document.querySelectorAll('.sideBar_sensorName');
elements.forEach((element) => {
element.innerText = response.deviceName;
});
//device ID
const deviceID = response.deviceID.trim().toUpperCase();
const device_ID = document.getElementById("device_ID");
device_ID.value = response.deviceID.toUpperCase();
//nextPM send 5 channels
const checkbox_nmp5channels = document.getElementById("check_NPM_5channels");
checkbox_nmp5channels.checked = response.npm_5channel;
},
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 checkbox_NPM = document.getElementById("check_NPM");
const checkbox_bme = document.getElementById("check_bme280");
const checkbox_envea = document.getElementById("check_envea");
const checkbox_solar = document.getElementById("check_solarBattery");
const checkbox_wind = document.getElementById("check_WindMeter");
checkbox_NPM.checked = response["NPM/get_data_modbus_v3.py"];
checkbox_bme.checked = response["BME280/get_data_v2.py"];
checkbox_envea.checked = response["envea/read_value_v2.py"];
checkbox_solar.checked = response["MPPT/read.py"];
checkbox_wind.checked = response["windMeter/read.py"];
//si sonde envea is true
if (response["envea/read_value_v2.py"]) {
add_sondeEnveaContainer();
}
},
error: function(xhr, status, error) {
console.error('AJAX request failed:', status, error);
}
});//end AJAX
//OLD way to get config (JSON)
/*
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 => {
console.log("Getting config file (onload)"); console.log("Getting config file (onload)");
//get device ID //get device ID
const deviceID = data.deviceID.trim().toUpperCase();
//document.getElementById('pageTitle_plus_ID').innerText = 'token: ' + deviceID; //document.getElementById('pageTitle_plus_ID').innerText = 'token: ' + deviceID;
//get device Name //get device Name
const deviceName = data.deviceName; //const deviceName = data.deviceName;
console.log("Device Name: " + deviceName);
console.log("Device ID: " + deviceID);
const elements = document.querySelectorAll('.sideBar_sensorName');
elements.forEach((element) => {
element.innerText = deviceName;
});
//get BME check //get BME check
const checkbox = document.getElementById("check_bme280"); const checkbox = document.getElementById("check_bme280");
@@ -227,18 +316,16 @@ window.onload = function() {
const checkbox_envea = document.getElementById("check_envea"); const checkbox_envea = document.getElementById("check_envea");
checkbox_envea.checked = data["envea/read_value_v2.py"]; 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 //device name
const device_name = document.getElementById("device_name"); //const device_name = document.getElementById("device_name");
device_name.value = data.deviceName; //device_name.value = data.deviceName;
})
.catch(error => console.error('Error loading config.json:', error));
*/
//device ID
const device_ID = document.getElementById("device_ID");
device_ID.value = data.deviceID.toUpperCase();
//get system time and RTC module //get system time and RTC module
$.ajax({ $.ajax({
@@ -246,6 +333,8 @@ window.onload = function() {
dataType: 'json', // 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
success: function(response) { success: function(response) {
console.log("Getting RTC times");
console.log(response); console.log(response);
// Update the input fields with the received JSON data // Update the input fields with the received JSON data
document.getElementById("sys_local_time").value = response.system_local_time; document.getElementById("sys_local_time").value = response.system_local_time;
@@ -279,7 +368,7 @@ window.onload = function() {
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
//get local RTC //get local RTC
$.ajax({ $.ajax({
@@ -287,17 +376,126 @@ window.onload = function() {
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);
} }
}); }); //end AJAx
})
.catch(error => console.error('Error loading config.json:', error)); } //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);
}
});
} }
@@ -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,6 +584,320 @@ 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 => {
@@ -152,6 +176,11 @@ window.onload = function() {
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,6 +28,325 @@ 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'];
@@ -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,21 +179,28 @@ 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);
//device name_side bar
const elements = document.querySelectorAll('.sideBar_sensorName'); const elements = document.querySelectorAll('.sideBar_sensorName');
elements.forEach((element) => { elements.forEach((element) => {
element.innerText = deviceName; 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({
@@ -210,9 +217,8 @@ window.onload = function() {
} }
}); });
})
.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>
@@ -305,6 +306,19 @@
</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>
@@ -336,6 +350,8 @@
.catch(error => console.error(`Error loading ${file}:`, error)); .catch(error => console.error(`Error loading ${file}:`, error));
}); });
//OLD way to retreive data from JSON
/*
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 => {
@@ -344,8 +360,33 @@
const check_modem_configMode = document.getElementById("check_modem_configMode"); const check_modem_configMode = document.getElementById("check_modem_configMode");
check_modem_configMode.checked = data.modem_config_mode; check_modem_configMode.checked = data.modem_config_mode;
console.log("Modem configuration: " + 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");
}
},
error: function(xhr, status, error) {
console.error('AJAX request failed:', status, error);
}
});
}); });
@@ -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("AJAX success:");
console.log(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) { 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

@@ -149,20 +149,18 @@ function getENVEA_values(port, name){
$.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];
// Add only the specified elements to the table
keysToShow.forEach(key => { keysToShow.forEach(key => {
if (response !== undefined) { // Check if the key exists in the response if (response !== undefined) {
const value = response; const value = response;
$("#data-table-body_envea" + name).append(` $("#data-table-body_envea" + name).append(`
<tr> <tr>
@@ -175,10 +173,22 @@ function getENVEA_values(port, name){
}, },
error: function(xhr, status, error) { error: function(xhr, status, error) {
console.error('AJAX request failed:', 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:");
$("#loading_noise").show(); $("#loading_noise").show();
@@ -261,92 +271,68 @@ 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
.then(data => {
//get device ID
const deviceID = data.deviceID.trim().toUpperCase();
//document.getElementById('pageTitle_plus_ID').innerText = 'token: ' + deviceID;
//get device Name
const deviceName = data.deviceName;
const elements = document.querySelectorAll('.sideBar_sensorName'); //NEW way to get config (SQLite)
elements.forEach((element) => {
element.innerText = deviceName;
});
//get local RTC
$.ajax({ $.ajax({
url: 'launcher.php?type=RTC_time', url: 'launcher.php?type=get_config_sqlite',
dataType: 'text', // Specify that you expect a JSON response dataType:'json',
//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
success: function(response) { success: function(response) {
console.log("Local RTC: " + response); console.log("Getting SQLite config table:");
const RTC_Element = document.getElementById("RTC_time"); console.log(response);
RTC_Element.textContent = response;
//device name_side bar
const elements = document.querySelectorAll('.sideBar_sensorName');
elements.forEach((element) => {
element.innerText = response.deviceName;
});
}, },
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
//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 const container = document.getElementById('card-container'); // Conteneur des cartes
//creates NPM cards //creates NPM card
const NPM_ports = data.NextPM_ports; // Récupère les ports if (response["NPM/get_data_modbus_v3.py"]) {
NPM_ports.forEach((port, index) => {
const cardHTML = ` const cardHTML = `
<div class="col-sm-3"> <div class="col-sm-3">
<div class="card"> <div class="card">
<div class="card-header"> <div class="card-header">
Port UART ${port.replace('ttyAMA', '')} Port UART
</div> </div>
<div class="card-body"> <div class="card-body">
<h5 class="card-title">NextPM ${String.fromCharCode(65 + index)}</h5> <h5 class="card-title">NextPM</h5>
<p class="card-text">Capteur particules fines.</p> <p class="card-text">Capteur particules fines.</p>
<button class="btn btn-primary" onclick="getNPM_values('${port}')">Get Data</button> <button class="btn btn-primary" onclick="getNPM_values('ttyAMA5')">Get Data</button>
<div id="loading_${port}" class="spinner-border spinner-border-sm" style="display: none;" role="status"></div> <br>
<div id="loading_ttyAMA5" class="spinner-border spinner-border-sm" style="display: none;" role="status"></div>
<table class="table table-striped-columns"> <table class="table table-striped-columns">
<tbody id="data-table-body_${port}"></tbody> <tbody id="data-table-body_ttyAMA5"></tbody>
</table> </table>
</div> </div>
</div> </div>
</div>`; </div>`;
container.innerHTML += cardHTML; // Ajouter la carte au conteneur
});
//creates ENVEA cards container.innerHTML += cardHTML; // Add the I2C card if condition is met
const ENVEA_sensors = data.envea_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}','${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 //creates i2c BME280 card
if (data["BME280/get_data_v2.py"]) { if (response["BME280/get_data_v2.py"]) {
const i2C_BME_HTML = ` const i2C_BME_HTML = `
<div class="col-sm-3"> <div class="col-sm-3">
<div class="card"> <div class="card">
@@ -370,7 +356,7 @@ window.onload = function() {
} }
//creates i2c sound card //creates i2c sound card
if (data.i2C_sound) { if (response.i2C_sound) {
const i2C_HTML = ` const i2C_HTML = `
<div class="col-sm-3"> <div class="col-sm-3">
<div class="card"> <div class="card">
@@ -395,9 +381,80 @@ window.onload = function() {
container.innerHTML += i2C_HTML; // Add the I2C card if condition is met 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
.catch(error => console.error('Error loading config.json:', error)); //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
}//end if
},
error: function(xhr, status, error) {
console.error('AJAX request failed:', status, error);
}
});//end AJAX (config_scripts)
//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 windows onload
</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

@@ -27,6 +27,10 @@ fi
info "Set up the RTC" info "Set up the RTC"
/usr/bin/python3 /var/www/nebuleair_pro_4g/RTC/set_with_NTP.py /usr/bin/python3 /var/www/nebuleair_pro_4g/RTC/set_with_NTP.py
#Check SARA R4 connection
info "Check SARA R4 connection"
/usr/bin/python3 /var/www/nebuleair_pro_4g/SARA/sara.py ttyAMA2 ATI 2
#set up SARA R4 APN #set up SARA R4 APN
info "Set up Monogoto APN" info "Set up Monogoto APN"
/usr/bin/python3 /var/www/nebuleair_pro_4g/SARA/sara_setAPN.py ttyAMA2 data.mono 2 /usr/bin/python3 /var/www/nebuleair_pro_4g/SARA/sara_setAPN.py ttyAMA2 data.mono 2
@@ -39,7 +43,11 @@ info "Activate blue LED"
info "Connect SARA R4 to network" info "Connect SARA R4 to network"
python3 /var/www/nebuleair_pro_4g/SARA/sara_connectNetwork.py ttyAMA2 20810 60 python3 /var/www/nebuleair_pro_4g/SARA/sara_connectNetwork.py ttyAMA2 20810 60
#Add master_nebuleair.service #Need to create the two service
# 1. master_nebuleair
# 2. rtc_save_to_db
#1. Add master_nebuleair.service
SERVICE_FILE="/etc/systemd/system/master_nebuleair.service" SERVICE_FILE="/etc/systemd/system/master_nebuleair.service"
info "Setting up systemd service for master_nebuleair..." info "Setting up systemd service for master_nebuleair..."
@@ -74,3 +82,41 @@ sudo systemctl enable master_nebuleair.service
# Start the service immediately # Start the service immediately
info "Starting the service..." info "Starting the service..."
sudo systemctl start master_nebuleair.service sudo systemctl start master_nebuleair.service
#2. Add rtc_save_to_db.service
SERVICE_FILE_2="/etc/systemd/system/rtc_save_to_db.service"
info "Setting up systemd service for rtc_save_to_db..."
# Create the systemd service file (overwrite if necessary)
sudo bash -c "cat > $SERVICE_FILE_2" <<EOF
[Unit]
Description=RTC Save to DB Script
After=network.target
[Service]
ExecStart=/usr/bin/python3 /var/www/nebuleair_pro_4g/RTC/save_to_db.py
Restart=always
RestartSec=1
User=root
WorkingDirectory=/var/www/nebuleair_pro_4g
StandardOutput=append:/var/www/nebuleair_pro_4g/logs/rtc_save_to_db.log
StandardError=append:/var/www/nebuleair_pro_4g/logs/rtc_save_to_db_errors.log
[Install]
WantedBy=multi-user.target
EOF
success "Systemd service file created: $SERVICE_FILE_2"
# Reload systemd to recognize the new service
info "Reloading systemd daemon..."
sudo systemctl daemon-reload
# Enable the service to start on boot
info "Enabling the service to start on boot..."
sudo systemctl enable rtc_save_to_db.service
# Start the service immediately
info "Starting the service..."
sudo systemctl start rtc_save_to_db.service

View File

@@ -49,6 +49,11 @@ CSV PAYLOAD (AirCarto Servers)
17 -> PM 5.0μm to 10μm quantity (Nb/L) 17 -> PM 5.0μm to 10μm quantity (Nb/L)
18 -> NPM temp inside 18 -> NPM temp inside
19 -> NPM hum inside 19 -> NPM hum inside
20 -> battery_voltage
21 -> battery_current
22 -> solar_voltage
23 -> solar_power
24 -> charger_status
JSON PAYLOAD (Micro-Spot Servers) JSON PAYLOAD (Micro-Spot Servers)
Same as NebuleAir wifi Same as NebuleAir wifi
@@ -94,6 +99,7 @@ import time
import busio import busio
import re import re
import os import os
import requests
import traceback import traceback
import threading import threading
import sys import sys
@@ -115,7 +121,7 @@ if uptime_seconds < 120:
sys.exit() sys.exit()
#Payload CSV to be sent to data.nebuleair.fr #Payload CSV to be sent to data.nebuleair.fr
payload_csv = [None] * 25 payload_csv = [None] * 30
#Payload JSON to be sent to uSpot #Payload JSON to be sent to uSpot
payload_json = { payload_json = {
"nebuleairid": "XXX", "nebuleairid": "XXX",
@@ -159,63 +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)
modem_version=config.get('modem_version', "")
baudrate = config.get('SaraR4_baudrate', 115200) #baudrate du sara R4 Sara_baudrate = config.get('SaraR4_baudrate', 115200)
device_id = config.get('deviceID', '').upper() #device ID en maj npm_5channel = config.get('npm_5channel', False) #5 canaux du NPM
bme_280_config = config.get('BME280/get_data_v2.py', False) #présence du BME280
envea_cairsens= config.get('envea/read_value_v2.py', False)
send_aircarto = config.get('send_aircarto', True) #envoi sur AirCarto (data.nebuleair.fr)
send_uSpot = config.get('send_uSpot', False) #envoi sur MicroSpot ()
selected_networkID = int(config.get('SARA_R4_neworkID', 0)) selected_networkID = int(config.get('SARA_R4_neworkID', 0))
npm_5channel = config.get('NextPM_5channels', False) #5 canaux du NPM send_uSpot = config.get('send_uSpot', False) #envoi sur MicroSpot ()
reset_uSpot_url = False
modem_config_mode = config.get('modem_config_mode', False) #modem 4G en mode configuration #config_scripts
config_scripts = load_config_scripts_sqlite()
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
@@ -227,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,
@@ -278,6 +305,182 @@ def read_complete_response(serial_connection, timeout=2, end_of_response_timeout
return response.decode('utf-8', errors='replace') # Return the full response if no target line is found return response.decode('utf-8', errors='replace') # Return the full response if no target line is found
def extract_error_code(response):
"""
Extract just the error code from AT+UHTTPER response
"""
for line in response.split('\n'):
if '+UHTTPER' in line:
try:
# Split the line and get the third value (error code)
parts = line.split(':')[1].strip().split(',')
if len(parts) >= 3:
error_code = int(parts[2])
return error_code
except:
pass
# Return None if we couldn't find the error code
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):
"""
Performs a complete modem restart sequence:
1. Reboots the modem using the appropriate command for its version
2. Waits for the modem to restart
3. Resets the HTTP profile
4. For SARA-R5, resets the PDP connection
Args:
modem_version (str): The modem version, e.g., 'SARA-R500' or 'SARA-R410'
aircarto_profile_id (int): The HTTP profile ID to reset
Returns:
bool: True if the complete sequence was successful, False otherwise
"""
print('<span style="color: orange;font-weight: bold;">🔄 Complete SARA reboot and reinitialize sequence 🔄</span>')
# Step 1: Reboot the modem - Integrated modem_software_reboot logic
print('<span style="color: orange;font-weight: bold;">🔄 Software SARA reboot! 🔄</span>')
# Use different commands based on modem version
if 'R5' in modem_version: # For SARA-R5 series
command = 'AT+CFUN=16\r' # Normal restart for R5
else: # For SARA-R4 series
command = 'AT+CFUN=15\r' # Factory reset for R4
ser_sara.write(command.encode('utf-8'))
response = read_complete_response(ser_sara, wait_for_lines=["OK", "ERROR"], debug=True)
print('<p class="text-danger-emphasis">')
print(response)
print("</p>", end="")
# Check if reboot command was acknowledged
reboot_success = response is not None and "OK" in response
if not reboot_success:
print("⚠️ Modem reboot command failed")
return False
# Step 2: Wait for the modem to restart (adjust time as needed)
print("Waiting for modem to restart...")
time.sleep(15) # 15 seconds should be enough for most modems to restart
# Step 3: Check if modem is responsive after reboot
print("Checking if modem is responsive...")
ser_sara.write(b'AT\r')
response_check = read_complete_response(ser_sara, wait_for_lines=["OK"], debug=True)
if response_check is None or "OK" not in response_check:
print("⚠️ Modem not responding after reboot")
return False
print("✅ Modem restarted successfully")
# Step 4: Reset the HTTP Profile
print('<span style="color: orange;font-weight: bold;">🔧 Resetting the HTTP Profile</span>')
command = f'AT+UHTTP={aircarto_profile_id},1,"data.nebuleair.fr"\r'
ser_sara.write(command.encode('utf-8'))
responseResetHTTP = read_complete_response(ser_sara, timeout=5, end_of_response_timeout=5,
wait_for_lines=["OK", "+CME ERROR"], debug=True)
print('<p class="text-danger-emphasis">')
print(responseResetHTTP)
print("</p>", end="")
http_reset_success = responseResetHTTP is not None and "OK" in responseResetHTTP
if not http_reset_success:
print("⚠️ HTTP profile reset failed")
# Continue anyway, don't return False here
# Step 5: For SARA-R5, reset the PDP connection
pdp_reset_success = True
if modem_version == "SARA-R500":
print("⚠️ Need to reset PDP connection for SARA-R500")
# Activate PDP context 1
print('➡️ Activate PDP context 1')
command = f'AT+CGACT=1,1\r'
ser_sara.write(command.encode('utf-8'))
response_pdp1 = read_complete_response(ser_sara, wait_for_lines=["OK"])
print(response_pdp1, end="")
pdp_reset_success = pdp_reset_success and (response_pdp1 is not None and "OK" in response_pdp1)
time.sleep(1)
# Set the PDP type
print('➡️ Set the PDP type to IPv4 referring to the output of the +CGDCONT read command')
command = f'AT+UPSD=0,0,0\r'
ser_sara.write(command.encode('utf-8'))
response_pdp2 = read_complete_response(ser_sara, wait_for_lines=["OK"])
print(response_pdp2, end="")
pdp_reset_success = pdp_reset_success and (response_pdp2 is not None and "OK" in response_pdp2)
time.sleep(1)
# Profile #0 is mapped on CID=1
print('➡️ Profile #0 is mapped on CID=1.')
command = f'AT+UPSD=0,100,1\r'
ser_sara.write(command.encode('utf-8'))
response_pdp3 = read_complete_response(ser_sara, wait_for_lines=["OK"])
print(response_pdp3, end="")
pdp_reset_success = pdp_reset_success and (response_pdp3 is not None and "OK" in response_pdp3)
time.sleep(1)
# Activate the PSD profile
print('➡️ Activate the PSD profile #0: the IPv4 address is already assigned by the network.')
command = f'AT+UPSDA=0,3\r'
ser_sara.write(command.encode('utf-8'))
response_pdp4 = read_complete_response(ser_sara, wait_for_lines=["OK", "+UUPSDA"])
print(response_pdp4, end="")
pdp_reset_success = pdp_reset_success and (response_pdp4 is not None and ("OK" in response_pdp4 or "+UUPSDA" in response_pdp4))
time.sleep(1)
if not pdp_reset_success:
print("⚠️ PDP connection reset had some issues")
# Return overall success
return http_reset_success and pdp_reset_success
try: try:
''' '''
_ ___ ___ ____ _ ___ ___ ____
@@ -288,25 +491,49 @@ try:
''' '''
print('<h3>START LOOP</h3>') print('<h3>START LOOP</h3>')
print(f'Modem version: {modem_version}')
#Local timestamp #Local timestamp
#ATTENTION:
# -> RTC module can be deconnected ""
# -> RTC module can be out of time like "2000-01-01T00:55:21Z"
print("Getting local timestamp") print("Getting local timestamp")
cursor.execute("SELECT * FROM timestamp_table LIMIT 1") 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' ou '2000-01-01 00:55:21' ou 'not connected'
print(rtc_time_str)
if rtc_time_str == 'not connected':
print("⛔ Atttention RTC module not connected⛔")
rtc_status = "disconnected"
influx_timestamp="rtc_disconnected"
else :
# Convert to a datetime object # Convert to a datetime object
dt_object = datetime.strptime(rtc_time_str, '%Y-%m-%d %H:%M:%S') dt_object = datetime.strptime(rtc_time_str, '%Y-%m-%d %H:%M:%S')
# Check if timestamp is reset (year 2000)
if dt_object.year == 2000:
print("⛔ Attention: RTC has been reset to default date ⛔")
rtc_status = "reset"
else:
print("✅ RTC timestamp is valid")
rtc_status = "valid"
# Always convert to InfluxDB format
# Convert to InfluxDB RFC3339 format with UTC 'Z' suffix # Convert to InfluxDB RFC3339 format with UTC 'Z' suffix
influx_timestamp = dt_object.strftime('%Y-%m-%dT%H:%M:%SZ') influx_timestamp = dt_object.strftime('%Y-%m-%dT%H:%M:%SZ')
rtc_status = "valid"
print(influx_timestamp) print(influx_timestamp)
#NEXTPM #NEXTPM
# We take the last measures (order by rowid and not by timestamp)
print("Getting NPM values (last 6 measures)") print("Getting NPM values (last 6 measures)")
#cursor.execute("SELECT * FROM data_NPM ORDER BY timestamp DESC LIMIT 1") #cursor.execute("SELECT * FROM data_NPM ORDER BY timestamp DESC LIMIT 1")
cursor.execute("SELECT * FROM data_NPM ORDER BY timestamp DESC LIMIT 6") #cursor.execute("SELECT * FROM data_NPM ORDER BY timestamp DESC LIMIT 6")
cursor.execute("SELECT rowid, * FROM data_NPM ORDER BY rowid DESC LIMIT 6")
rows = cursor.fetchall() rows = cursor.fetchall()
# Exclude the timestamp column (assuming first column is timestamp) # Exclude the timestamp column (assuming first column is timestamp)
data_values = [row[1:] for row in rows] # Exclude timestamp data_values = [row[2:] for row in rows] # Exclude timestamp
# Compute column-wise average # Compute column-wise average
num_columns = len(data_values[0]) num_columns = len(data_values[0])
averages = [round(sum(col) / len(col),1) for col in zip(*data_values)] averages = [round(sum(col) / len(col),1) for col in zip(*data_values)]
@@ -333,7 +560,7 @@ try:
#NextPM 5 channels #NextPM 5 channels
if npm_5channel: if npm_5channel:
print("Getting NextPM 5 channels values (last 6 measures)") print("Getting NextPM 5 channels values (last 6 measures)")
cursor.execute("SELECT * FROM data_NPM_5channels ORDER BY timestamp DESC LIMIT 6") cursor.execute("SELECT * FROM data_NPM_5channels ORDER BY rowid DESC LIMIT 6")
rows = cursor.fetchall() rows = cursor.fetchall()
# Exclude the timestamp column (assuming first column is timestamp) # Exclude the timestamp column (assuming first column is timestamp)
data_values = [row[1:] for row in rows] # Exclude timestamp data_values = [row[1:] for row in rows] # Exclude timestamp
@@ -351,7 +578,7 @@ try:
#BME280 #BME280
if bme_280_config: if bme_280_config:
print("Getting BME280 values") print("Getting BME280 values")
cursor.execute("SELECT * FROM data_BME280 ORDER BY timestamp DESC LIMIT 1") cursor.execute("SELECT * FROM data_BME280 ORDER BY rowid DESC LIMIT 1")
last_row = cursor.fetchone() last_row = cursor.fetchone()
if last_row: if last_row:
print("SQLite DB last available row:", last_row) print("SQLite DB last available row:", last_row)
@@ -374,7 +601,7 @@ try:
#envea #envea
if envea_cairsens: if envea_cairsens:
print("Getting envea cairsens values") print("Getting envea cairsens values")
cursor.execute("SELECT * FROM data_envea ORDER BY timestamp DESC LIMIT 6") cursor.execute("SELECT * FROM data_envea ORDER BY rowid DESC LIMIT 6")
rows = cursor.fetchall() rows = cursor.fetchall()
# Exclude the timestamp column (assuming first column is timestamp) # Exclude the timestamp column (assuming first column is timestamp)
data_values = [row[1:] for row in rows] # Exclude timestamp data_values = [row[1:] for row in rows] # Exclude timestamp
@@ -395,19 +622,76 @@ try:
#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
if wind_meter:
print("Getting wind meter values")
#MPPT charger
if mppt_charger:
print("Getting MPPT charger values")
cursor.execute("SELECT * FROM data_MPPT ORDER BY rowid DESC LIMIT 1")
last_row = cursor.fetchone()
if last_row:
print("SQLite DB last available row:", last_row)
battery_voltage = last_row[1]
battery_current = last_row[2]
solar_voltage = last_row[3]
solar_power = last_row[4]
charger_status = last_row[5]
#Add data to payload CSV
payload_csv[20] = battery_voltage
payload_csv[21] = battery_current
payload_csv[22] = solar_voltage
payload_csv[23] = solar_power
payload_csv[24] = charger_status
else:
print("No data available in the database.")
print("Verify SARA R4 connection") print("Verify SARA R4 connection")
# Getting the LTE Signal # Getting the LTE Signal
print("-> Getting LTE signal <-") print("➡️Getting LTE signal")
ser_sara.write(b'AT+CSQ\r') ser_sara.write(b'AT+CSQ\r')
response2 = read_complete_response(ser_sara, wait_for_lines=["OK"]) response2 = read_complete_response(ser_sara, wait_for_lines=["OK", "ERROR", "+CME ERROR"])
print('<p class="text-danger-emphasis">') print('<p class="text-danger-emphasis">')
print(response2) print(response2)
print("</p>") print("</p>", end="")
#Here it's possible that the SARA do not repond at all or send a error message
#-> TO DO : harware reboot
#-> send notification
#-> end loop, no need to continue
#1. No answer at all form SARA
if response2 is None or response2 == "":
print("No answer from SARA module")
print('🛑STOP LOOP🛑')
print("<hr>")
#Send notification (WIFI)
send_error_notification(device_id, "serial_error")
#end loop
sys.exit()
#2. si on a une erreur
elif "+CME ERROR" in response2:
print(f"SARA module returned error: {response2}")
print("The CSQ command is not supported by this module or in its current state")
print("ATTENTION: SARA is connected over serial but CSQ command not supported")
print('🛑STOP LOOP🛑')
#end loop
sys.exit()
else :
print("✅SARA is connected over serial")
match = re.search(r'\+CSQ:\s*(\d+),', response2) match = re.search(r'\+CSQ:\s*(\d+),', response2)
if match: if match:
signal_quality = int(match.group(1)) signal_quality = int(match.group(1))
@@ -425,7 +709,7 @@ try:
responseReconnect = read_complete_response(ser_sara, timeout=20, end_of_response_timeout=20) responseReconnect = read_complete_response(ser_sara, timeout=20, end_of_response_timeout=20)
print('<p class="text-danger-emphasis">') print('<p class="text-danger-emphasis">')
print(responseReconnect) print(responseReconnect)
print("</p>") print("</p>", end="")
print('🛑STOP LOOP🛑') print('🛑STOP LOOP🛑')
print("<hr>") print("<hr>")
@@ -448,26 +732,35 @@ try:
print("Open JSON:") print("Open JSON:")
command = f'AT+UDWNFILE="sensordata_csv.json",{size_of_string}\r' command = f'AT+UDWNFILE="sensordata_csv.json",{size_of_string}\r'
ser_sara.write(command.encode('utf-8')) ser_sara.write(command.encode('utf-8'))
response_SARA_1 = read_complete_response(ser_sara, wait_for_lines=[">"], debug=False) response_SARA_1 = read_complete_response(ser_sara, wait_for_lines=[">"], debug=True)
print('<p class="text-danger-emphasis">')
print(response_SARA_1) print(response_SARA_1)
print("</p>", end="")
time.sleep(1) time.sleep(1)
#2. Write to shell #2. Write to shell
print("Write data to memory:") print("Write data to memory:")
ser_sara.write(csv_string.encode()) ser_sara.write(csv_string.encode())
response_SARA_2 = read_complete_response(ser_sara, wait_for_lines=["OK"], debug=False) response_SARA_2 = read_complete_response(ser_sara, wait_for_lines=["OK"], debug=True)
print('<p class="text-danger-emphasis">')
print(response_SARA_2) print(response_SARA_2)
print("</p>", end="")
#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"], 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>") print("</p>", end="")
# si on recoit la réponse UHTTPCR # si on recoit la réponse UHTTPCR
if "+UUHTTPCR" in response_SARA_3: if "+UUHTTPCR" in response_SARA_3:
@@ -494,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]:
@@ -522,18 +813,18 @@ try:
led_thread.start() led_thread.start()
else: else:
# 2.Si la réponse contient une réponse HTTP valide # 2.Si la réponse contient une réponse UUHTTPCR
# Extract HTTP response code from the last line # Extract UUHTTPCR response code from the last line
# ATTENTION: lines[-1] renvoie l'avant dernière ligne et il peut y avoir un soucis avec le OK
# rechercher plutot
http_response = lines[-1] # "+UUHTTPCR: 0,4,0" http_response = lines[-1] # "+UUHTTPCR: 0,4,0"
parts = http_response.split(',') parts = http_response.split(',')
# 2.1 code 0 (HTTP failed) ⛔⛔⛔ # 2.1 code 0 (HTTP failed) ⛔⛔⛔
# -> GET error code
# -> reboot module
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
@@ -541,66 +832,127 @@ try:
led_thread.start() led_thread.start()
# Get error code # Get error code
print("Getting error code (11->Server connection error, 73->Secure socket connect error)") print("Getting error code")
command = f'AT+UHTTPER={aircarto_profile_id}\r' command = f'AT+UHTTPER={aircarto_profile_id}\r'
ser_sara.write(command.encode('utf-8')) ser_sara.write(command.encode('utf-8'))
response_SARA_9 = read_complete_response(ser_sara, wait_for_lines=["OK"], debug=False) response_SARA_9 = read_complete_response(ser_sara, wait_for_lines=["OK"], debug=False)
print('<p class="text-danger-emphasis">') print('<p class="text-danger-emphasis">')
print(response_SARA_9) print(response_SARA_9)
print("</p>") print("</p>", end="")
''' # Extract just the error code
+UHTTPER: profile_id,error_class,error_code error_code = extract_error_code(response_SARA_9)
if error_code is not None:
error_class # Display interpretation based on error code
0 OK, no error if error_code == 0:
3 HTTP Protocol error class print('<p class="text-success">No error detected</p>')
10 Wrong HTTP API USAGE elif error_code == 4:
print('<p class="text-danger">Error 4: Invalid server Hostname</p>')
error_code (for error_class 3 and 10) elif error_code == 11:
0 No error print('<p class="text-danger">Error 11: Server connection error</p>')
4 Invalid server Hostname elif error_code == 22:
11 Server connection error print('<p class="text-danger">⚠Error 22: PSD or CSD connection not established (SARA-R5 need to reset PDP conection)⚠️</p>')
73 Secure socket connect error elif error_code == 73:
''' print('<p class="text-danger">Error 73: Secure socket connect error</p>')
else:
#Essayer un reboot du SARA R4 (ne fonctionne pas) print(f'<p class="text-danger">Unknown error code: {error_code}</p>')
#print("🔄SARA reboot!🔄") else:
#command = f'AT+CFUN=15\r' print('<p class="text-danger">Could not extract error code from response</p>')
#ser_sara.write(command.encode('utf-8'))
#response_SARA_9r = read_complete_response(ser_sara, wait_for_lines=["OK"], debug=False)
#print('<p class="text-danger-emphasis">') #Software Reboot
#print(response_SARA_9r) software_reboot_success = modem_complete_reboot_and_reinitialize(modem_version, aircarto_profile_id)
#print("</p>") if software_reboot_success:
print("Modem successfully rebooted and reinitialized")
#reset l'url else:
print('<span style="color: orange;font-weight: bold;">❓Try Resetting the HTTP Profile❓</span>') print("There were issues with the modem reboot/reinitialize process")
command = f'AT+UHTTP={aircarto_profile_id},1,"data.nebuleair.fr"\r'
ser_sara.write(command.encode('utf-8'))
responseResetHTTP2_profile = read_complete_response(ser_sara, timeout=5, end_of_response_timeout=5, wait_for_lines=["OK", "+CME ERROR"], debug=True) # 2.2 code 1 (✅✅HHTP / UUHTTPCR succeded✅✅)
print('<p class="text-danger-emphasis">')
print(responseResetHTTP2_profile)
print("</p>")
# 2.2 code 1 (HHTP succeded)
else: else:
# 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()
#4. Read reply from server #4. Read reply from server
print("Reply from server:") print("Reply from server:")
ser_sara.write(b'AT+URDFILE="aircarto_server_response.txt"\r') ser_sara.write(b'AT+URDFILE="aircarto_server_response.txt"\r')
response_SARA_4 = read_complete_response(ser_sara, wait_for_lines=["OK"], debug=False) response_SARA_4 = read_complete_response(ser_sara, wait_for_lines=["OK"], debug=False)
print('<p class="text-success">') print('<p class="text-success">')
print(response_SARA_4) print(response_SARA_4)
print('</p>') print("</p>", end="")
#Parse the server datetime
# Extract just the date from the response
date_string = None
date_start = response_SARA_4.find("Date: ")
if date_start != -1:
date_end = response_SARA_4.find("\n", date_start)
date_string = response_SARA_4[date_start + 6:date_end].strip()
print(f'<div class="text-primary">Server date: {date_string}</div>', end="")
# Optionally convert to datetime object
try:
from datetime import datetime
server_datetime = datetime.strptime(
date_string,
"%a, %d %b %Y %H:%M:%S %Z"
)
#print(f'<p class="text-primary">Parsed datetime: {server_datetime}</p>')
except Exception as e:
print(f'<p class="text-warning">Error parsing date: {e}</p>')
# Get RTC time from SQLite
cursor.execute("SELECT * FROM timestamp_table LIMIT 1")
row = cursor.fetchone()
rtc_time_str = row[1] # '2025-02-07 12:30:45' or '2000-01-01 00:55:21' or 'not connected'
print(f'<div class="text-primary">RTC time: {rtc_time_str}</div>', end="")
# Compare times if both are available
if server_datetime and rtc_time_str != 'not connected':
try:
# Convert RTC time string to datetime
rtc_datetime = datetime.strptime(rtc_time_str, '%Y-%m-%d %H:%M:%S')
# Calculate time difference in seconds
time_diff = abs((server_datetime - rtc_datetime).total_seconds())
print(f'<div class="text-primary">Time difference: {time_diff:.2f} seconds</div>', end="")
# Check if difference is more than 60 seconds
# and update the RTC clock
if time_diff > 60:
print(f'<div class="text-warning"><strong>⚠️ RTC time differs from server time by {time_diff:.2f} seconds!</strong></div>', end="")
# Format server time for RTC update
server_time_formatted = server_datetime.strftime('%Y-%m-%d %H:%M:%S')
#update RTC module do not wait for answer, non blocking
#/usr/bin/python3 /var/www/nebuleair_pro_4g/RTC/set_with_browserTime.py '2024-01-30 12:48:39'
# Launch RTC update script as non-blocking subprocess
import subprocess
update_command = [
"/usr/bin/python3",
"/var/www/nebuleair_pro_4g/RTC/set_with_browserTime.py",
server_time_formatted
]
# Execute the command without waiting for result
subprocess.Popen(update_command,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL)
print(f'<div class="text-warning">➡️ Updating RTC with server time: {server_time_formatted}</div>', end="")
else:
print(f'<div class="text-success">✅ RTC time is synchronized with server time (within 60 seconds)</div>')
except Exception as e:
print(f'<p class="text-warning">Error comparing times: {e}</p>')
#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" #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")
@@ -629,7 +981,7 @@ try:
responseReconnect = read_complete_response(ser_sara, timeout=5, end_of_response_timeout=120, wait_for_lines=["OK", "+CME ERROR"], debug=True) responseReconnect = read_complete_response(ser_sara, timeout=5, end_of_response_timeout=120, wait_for_lines=["OK", "+CME ERROR"], debug=True)
print('<p class="text-danger-emphasis">') print('<p class="text-danger-emphasis">')
print(responseReconnect) print(responseReconnect)
print("</p>") print("</p>", end="")
# Handle "Operation not allowed" error # Handle "Operation not allowed" error
if error_message == "Operation not allowed": if error_message == "Operation not allowed":
print('<span style="color: orange;font-weight: bold;">❓Try Resetting the HTTP Profile❓</span>') print('<span style="color: orange;font-weight: bold;">❓Try Resetting the HTTP Profile❓</span>')
@@ -638,13 +990,39 @@ try:
responseResetHTTP_profile = read_complete_response(ser_sara, timeout=5, end_of_response_timeout=5, wait_for_lines=["OK", "+CME ERROR"], debug=True) responseResetHTTP_profile = read_complete_response(ser_sara, timeout=5, end_of_response_timeout=5, wait_for_lines=["OK", "+CME ERROR"], debug=True)
print('<p class="text-danger-emphasis">') print('<p class="text-danger-emphasis">')
print(responseResetHTTP_profile) print(responseResetHTTP_profile)
print("</p>") print("</p>", end="")
check_lines = responseResetHTTP_profile.strip().splitlines()
for line in check_lines:
if "+CME ERROR: Operation not allowed" in line:
print('<span style="color: red;font-weight: bold;">⚠ATTENTION: CME ERROR⚠</span>')
print('<span style="color: orange;font-weight: bold;">❓Try Reboot the module❓</span>')
#Software Reboot
if "ERROR" in line:
print("⛔Attention ERROR!⛔")
#Send notification (WIFI)
send_error_notification(device_id, "sara_error")
#Software Reboot
software_reboot_success = modem_complete_reboot_and_reinitialize(modem_version, aircarto_profile_id)
if software_reboot_success:
print("Modem successfully rebooted and reinitialized")
else:
print("There were issues with the modem reboot/reinitialize process")
#5. empty json #5. empty json
print("Empty SARA memory:") print("Empty SARA memory:")
ser_sara.write(b'AT+UDELFILE="sensordata_csv.json"\r') ser_sara.write(b'AT+UDELFILE="sensordata_csv.json"\r')
response_SARA_5 = read_complete_response(ser_sara, wait_for_lines=["OK"], debug=False) response_SARA_5 = read_complete_response(ser_sara, wait_for_lines=["OK","+CME ERROR"], debug=True)
print('<p class="text-danger-emphasis">')
print(response_SARA_5) print(response_SARA_5)
print("</p>", end="")
if "+CME ERROR" in response_SARA_5:
print("⛔ Attention CME ERROR ⛔")
''' '''
@@ -654,6 +1032,72 @@ try:
if send_uSpot: if send_uSpot:
print('➡️<p class="fw-bold">SEND TO uSPOT SERVERS</p>') print('➡️<p class="fw-bold">SEND TO uSPOT SERVERS</p>')
if reset_uSpot_url:
#2. Set uSpot URL (profile id = 1)
print('Set uSpot URL')
uSpot_profile_id = 1
uSpot_url="api-prod.uspot.probesys.net"
security_profile_id = 1
#step 1: import the certificate
print("****")
certificate_name = "e6"
with open("/var/www/nebuleair_pro_4g/SARA/SSL/certificate/e6.pem", "rb") as cert_file:
certificate = cert_file.read()
size_of_string = len(certificate)
print("\033[0;33m Import certificate\033[0m")
# AT+USECMNG=0,<type>,<internal_name>,<data_size>
# type-> 0 -> trusted root CA
command = f'AT+USECMNG=0,0,"{certificate_name}",{size_of_string}\r'
ser_sara.write((command + '\r').encode('utf-8'))
response_SARA_1 = read_complete_response(ser_sara)
print(response_SARA_1)
time.sleep(0.5)
print("\033[0;33mAdd certificate\033[0m")
ser_sara.write(certificate)
response_SARA_2 = read_complete_response(ser_sara)
print(response_SARA_2)
time.sleep(0.5)
# SECURITY PROFILE
# op_code: 3 -> trusted root certificate internal name
print("\033[0;33mSet the security profile (choose cert)\033[0m")
command = f'AT+USECPRF={security_profile_id},3,"{certificate_name}"\r'
ser_sara.write((command + '\r').encode('utf-8'))
response_SARA_5c = read_complete_response(ser_sara, wait_for_lines=["OK"])
print(response_SARA_5c)
time.sleep(0.5)
#step 4: set url (op_code = 1)
command = f'AT+UHTTP={uSpot_profile_id},1,"{uSpot_url}"\r'
ser_sara.write(command.encode('utf-8'))
response_SARA_2 = read_complete_response(ser_sara, wait_for_lines=["OK"])
print(response_SARA_2)
time.sleep(1)
#step 4: set PORT (op_code = 5)
print("set port 443")
command = f'AT+UHTTP={uSpot_profile_id},5,443\r'
ser_sara.write((command + '\r').encode('utf-8'))
response_SARA_55 = read_complete_response(ser_sara, wait_for_lines=["OK"])
print(response_SARA_55)
time.sleep(1)
#step 4: set url to SSL (op_code = 6) (http_secure = 1 for HTTPS)(USECMNG_PROFILE = 2)
print("\033[0;33mSET SSL\033[0m")
http_secure = 1
command = f'AT+UHTTP={uSpot_profile_id},6,{http_secure},{security_profile_id}\r'
#command = f'AT+UHTTP={profile_id},6,{http_secure}\r'
ser_sara.write(command.encode('utf-8'))
response_SARA_5 = read_complete_response(ser_sara, wait_for_lines=["OK"])
print(response_SARA_5)
time.sleep(1)
# 1. Open sensordata_json.json (with correct data size) # 1. Open sensordata_json.json (with correct data size)
print("Open JSON:") print("Open JSON:")
payload_string = json.dumps(payload_json) # Convert dict to JSON string payload_string = json.dumps(payload_json) # Convert dict to JSON string
@@ -680,7 +1124,7 @@ try:
print('<p class="text-danger-emphasis">') print('<p class="text-danger-emphasis">')
print(response_SARA_8) print(response_SARA_8)
print("</p>") print("</p>", end="")
# si on recoit la réponse UHTTPCR # si on recoit la réponse UHTTPCR
if "+UUHTTPCR" in response_SARA_8: if "+UUHTTPCR" in response_SARA_8:
@@ -693,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]:
@@ -719,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
@@ -727,28 +1169,31 @@ try:
led_thread.start() led_thread.start()
# Get error code # Get error code
print("Getting error code (4-> Invalid server Hostname, 11->Server connection error, 73->Secure socket connect error)") print("Getting error code")
command = f'AT+UHTTPER={uSpot_profile_id}\r' command = f'AT+UHTTPER={uSpot_profile_id}\r'
ser_sara.write(command.encode('utf-8')) ser_sara.write(command.encode('utf-8'))
response_SARA_9b = read_complete_response(ser_sara, wait_for_lines=["OK"], debug=False) response_SARA_9b = read_complete_response(ser_sara, wait_for_lines=["OK"], debug=False)
print('<p class="text-danger-emphasis">') print('<p class="text-danger-emphasis">')
print(response_SARA_9b) print(response_SARA_9b)
print("</p>") print("</p>", end="")
# Extract just the error code
''' error_code = extract_error_code(response_SARA_9b)
+UHTTPER: profile_id,error_class,error_code if error_code is not None:
# Display interpretation based on error code
error_class if error_code == 0:
0 OK, no error print('<p class="text-success">No error detected</p>')
3 HTTP Protocol error class elif error_code == 4:
10 Wrong HTTP API USAGE print('<p class="text-danger">Error 4: Invalid server Hostname</p>')
elif error_code == 11:
error_code (for error_class 3) print('<p class="text-danger">Error 11: Server connection error</p>')
0 No error elif error_code == 22:
4 Invalid server Hostname print('<p class="text-danger">Error 22: PSD or CSD connection not established</p>')
11 Server connection error elif error_code == 73:
73 Secure socket connect error print('<p class="text-danger">Error 73: Secure socket connect error</p>')
''' else:
print(f'<p class="text-danger">Unknown error code: {error_code}</p>')
else:
print('<p class="text-danger">Could not extract error code from response</p>')
#Pas forcément un moyen de résoudre le soucis #Pas forcément un moyen de résoudre le soucis
@@ -756,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()
@@ -766,7 +1210,7 @@ try:
response_SARA_4b = read_complete_response(ser_sara, wait_for_lines=["OK"], debug=False) response_SARA_4b = read_complete_response(ser_sara, wait_for_lines=["OK"], debug=False)
print('<p class="text-success">') print('<p class="text-success">')
print(response_SARA_4b) print(response_SARA_4b)
print('</p>') print("</p>", end="")

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,8 +126,17 @@ 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
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]) subprocess.run(["python3", script_path])
# Wait until the next exact interval # Wait until the next exact interval
@@ -79,17 +144,19 @@ def run_script(script_name, interval, delay=0):
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), # SAVE RTC time every 1 second, no delay # 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
("sqlite/flush_old_data.py", 86400, 0) # flush old data inside db every day () ("MPPT/read.py", 120, 0), # Get MPPT data every 120 seconds
("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()
@@ -97,4 +164,3 @@ for script_name, interval, delay in SCRIPTS:
# Keep the main script running # Keep the main script running
while True: while True:
time.sleep(1) time.sleep(1)

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

@@ -5,8 +5,11 @@
"RTC/save_to_db.py": true, "RTC/save_to_db.py": true,
"BME280/get_data_v2.py": true, "BME280/get_data_v2.py": true,
"envea/read_value_v2.py": false, "envea/read_value_v2.py": false,
"MPPT/read.py": false,
"windMeter/read.py": false,
"sqlite/flush_old_data.py": true, "sqlite/flush_old_data.py": true,
"deviceID": "XXXX", "deviceID": "XXXX",
"npm_5channel": false,
"latitude_raw": 0, "latitude_raw": 0,
"longitude_raw":0, "longitude_raw":0,
"latitude_precision": 0, "latitude_precision": 0,
@@ -25,7 +28,7 @@
"SARA_R4_general_status": "connected", "SARA_R4_general_status": "connected",
"SARA_R4_SIM_status": "connected", "SARA_R4_SIM_status": "connected",
"SARA_R4_network_status": "connected", "SARA_R4_network_status": "connected",
"SARA_R4_neworkID": 0, "SARA_R4_neworkID": 20810,
"WIFI_status": "connected", "WIFI_status": "connected",
"MQTT_GUI": false, "MQTT_GUI": false,
"send_aircarto": true, "send_aircarto": true,

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 (
@@ -30,7 +59,14 @@ cursor.execute("""
VALUES (1, CURRENT_TIMESTAMP); VALUES (1, CURRENT_TIMESTAMP);
""") """)
#create a modem status table
cursor.execute("""
CREATE TABLE IF NOT EXISTS modem_status (
id INTEGER PRIMARY KEY AUTOINCREMENT,
timestamp TEXT,
status TEXT
)
""")
# Create a table NPM # Create a table NPM
cursor.execute(""" cursor.execute("""
@@ -78,6 +114,26 @@ CREATE TABLE IF NOT EXISTS data_NPM_5channels (
) )
""") """)
# Create a table WIND
cursor.execute("""
CREATE TABLE IF NOT EXISTS data_WIND (
timestamp TEXT,
wind_speed REAL,
wind_direction REAL
)
""")
# Create a table MPPT
cursor.execute("""
CREATE TABLE IF NOT EXISTS data_MPPT (
timestamp TEXT,
battery_voltage REAL,
battery_current REAL,
solar_voltage REAL,
solar_power REAL,
charger_status INTEGER
)
""")
# Commit and close the connection # Commit and close the connection

View File

@@ -14,6 +14,8 @@ data_NPM_5channels
data_BME280 data_BME280
data_envea data_envea
timestamp_table timestamp_table
data_MPPT
data_WIND
''' '''
@@ -34,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!")

27
windMeter/ads115.py Normal file
View File

@@ -0,0 +1,27 @@
'''
Script to test the abs115 an analog-to-digital converter
sudo /usr/bin/python3 /var/www/nebuleair_pro_4g/windMeter/ads115.py
'''
import time
import board
import busio
import adafruit_ads1x15.ads1115 as ADS
from adafruit_ads1x15.analog_in import AnalogIn
i2c = busio.I2C(board.SCL, board.SDA)
ads = ADS.ADS1115(i2c)
channel = AnalogIn(ads, ADS.P0)
print("Testing ADS1115 readings...")
readings = []
for i in range(5):
voltage = channel.voltage
readings.append(voltage)
print(f"Voltage: {voltage:.6f}V")
time.sleep(1)
# Calculate and display the mean
mean_voltage = sum(readings) / len(readings)
print(f"\nMean voltage: {mean_voltage:.6f}V")

108
windMeter/read.py Normal file
View File

@@ -0,0 +1,108 @@
'''
__ _____ _ _ ____
\ \ / /_ _| \ | | _ \
\ \ /\ / / | || \| | | | |
\ V V / | || |\ | |_| |
\_/\_/ |___|_| \_|____/
Script to read wind speed from a Davis Anémomètre-girouette Vantage Pro (6410)
https://www.shapemaker.io/blog/wind-speed-measurements-with-anemometer-and-a-raspberry-pi
Connexion:
black (wind speed ) -> gpio21
green (wind direction) -> ADS1115 (module I2C)
Yellow -> 5v
RED -> GND
Attention: The Raspberry Pi doesn't have analog inputs, so we need an analog-to-digital converter (ADC) to read the wind direction.
sudo /usr/bin/python3 /var/www/nebuleair_pro_4g/windMeter/read.py
'''
#!/usr/bin/python3
import time
import sqlite3
import board
import busio
import numpy as np
import threading
import adafruit_ads1x15.ads1115 as ADS
from adafruit_ads1x15.analog_in import AnalogIn
from gpiozero import Button
from datetime import datetime
# Constants
DB_PATH = "/var/www/nebuleair_pro_4g/sqlite/sensors.db"
# Initialize I2C & ADS1115
i2c = busio.I2C(board.SCL, board.SDA)
ads = ADS.ADS1115(i2c)
channel = AnalogIn(ads, ADS.P0) # Connect to A0 on the ADS1115
# Wind speed sensor setup
wind_speed_sensor = Button(21)
wind_count = 0
wind_lock = threading.Lock()
def spin():
global wind_count
with wind_lock:
wind_count += 1
def reset_wind():
global wind_count
with wind_lock:
wind_count = 0
wind_speed_sensor.when_activated = spin # More reliable
def calc_speed(spins, interval):
return spins * (2.25 / interval) * 1.60934 # Convert MPH to km/h
def get_wind_direction():
voltage = channel.voltage
return voltage
def save_to_database(wind_speed, wind_direction, spin_count):
"""Save wind data to SQLite database."""
try:
conn = sqlite3.connect(DB_PATH)
cursor = conn.cursor()
cursor.execute("SELECT * FROM timestamp_table LIMIT 1")
row = cursor.fetchone()
rtc_time_str = row[1] if row else datetime.now().strftime("%Y-%m-%d %H:%M:%S")
cursor.execute('''
INSERT INTO data_wind (timestamp, wind_speed, wind_direction)
VALUES (?, ?, ?)
''', (rtc_time_str, round(wind_speed, 2), round(wind_direction, 2)))
conn.commit()
conn.close()
print(f"Saved: {rtc_time_str}, {wind_speed:.2f} km/h, {wind_direction:.2f}V, Spins: {spin_count}")
except Exception as e:
print(f"Database error: {e}")
def main():
print("Wind monitoring started...")
try:
while True:
reset_wind()
print("Measuring for 60 seconds...")
time.sleep(60)
wind_speed_kmh = calc_speed(wind_count, 60)
wind_direction = get_wind_direction()
save_to_database(wind_speed_kmh, wind_direction, wind_count)
except KeyboardInterrupt:
print("\nMonitoring stopped.")
except Exception as e:
print(f"Error: {e}")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,84 @@
'''
__ _____ _ _ ____
\ \ / /_ _| \ | | _ \
\ \ /\ / / | || \| | | | |
\ V V / | || |\ | |_| |
\_/\_/ |___|_| \_|____/
Script to read wind speed from a Davis Anémomètre-girouette Vantage Pro (6410)
https://www.shapemaker.io/blog/wind-speed-measurements-with-anemometer-and-a-raspberry-pi
Connexion:
black (wind speed ) -> gpio21
green (wind direction) -> ADS1115 (module I2C)
Yellow -> 5v
RED -> GND
Attention: The Raspberry Pi doesn't have analog inputs, so we need an analog-to-digital converter (ADC) to read the wind direction.
sudo /usr/bin/python3 /var/www/nebuleair_pro_4g/windMeter/read_wind_direction.py
'''
import time
import board
import busio
import adafruit_ads1x15.ads1115 as ADS
from adafruit_ads1x15.analog_in import AnalogIn
# Create the I2C bus and ADC object
i2c = busio.I2C(board.SCL, board.SDA)
ads = ADS.ADS1115(i2c)
# Connect to the channel with your Davis wind vane
wind_dir_sensor = AnalogIn(ads, ADS.P0)
# Check the current voltage range
min_voltage = 9999
max_voltage = -9999
def get_wind_direction():
"""Get wind direction angle from Davis Vantage Pro2 wind vane"""
global min_voltage, max_voltage
# Read voltage from ADS1115
voltage = wind_dir_sensor.voltage
# Update min/max for calibration
if voltage < min_voltage:
min_voltage = voltage
if voltage > max_voltage:
max_voltage = voltage
# We'll use a safer mapping approach
# Assuming the Davis sensor is linear from 0° to 360°
estimated_max = 3.859 # Initial estimate, will refine
# Calculate angle with bounds checking
angle = (voltage / estimated_max) * 360.0
# Ensure angle is in 0-360 range
angle = angle % 360
return voltage, angle
# Main loop
try:
print("Reading wind direction. Press Ctrl+C to exit.")
print("Voltage, Angle, Min Voltage, Max Voltage")
while True:
voltage, angle = get_wind_direction()
print(f"{voltage:.3f}V, {angle:.1f}°, {min_voltage:.3f}V, {max_voltage:.3f}V")
time.sleep(1)
except KeyboardInterrupt:
print("\nProgram stopped")
print(f"Observed voltage range: {min_voltage:.3f}V to {max_voltage:.3f}V")
# Suggest calibration if we have enough data
if max_voltage > min_voltage:
print("\nSuggested calibration for your setup:")
print(f"max_voltage = {max_voltage:.3f}")
print(f"def get_wind_direction():")
print(f" voltage = wind_dir_sensor.voltage")
print(f" angle = (voltage / {max_voltage:.3f}) * 360.0")
print(f" return angle % 360")

View File

@@ -0,0 +1,67 @@
'''
__ _____ _ _ ____
\ \ / /_ _| \ | | _ \
\ \ /\ / / | || \| | | | |
\ V V / | || |\ | |_| |
\_/\_/ |___|_| \_|____/
Script to read wind speed from a Davis Anémomètre-girouette Vantage Pro (6410)
https://www.shapemaker.io/blog/wind-speed-measurements-with-anemometer-and-a-raspberry-pi
Connexion:
black (wind speed ) -> gpio21
green (wind direction) -> ADS1115 (module I2C)
Yellow -> 5v
RED -> GND
Attention: The Raspberry Pi doesn't have analog inputs, so we need an analog-to-digital converter (ADC) to read the wind direction.
sudo /usr/bin/python3 /var/www/nebuleair_pro_4g/windMeter/read_wind_speed.py
'''
import time
from gpiozero import Button
from signal import pause
# Setup wind speed sensor on GPIO pin 21 (instead of 5)
wind_speed_sensor = Button(21)
wind_count = 0
def spin():
global wind_count
wind_count = wind_count + 1
def calc_speed(spins, interval):
# Davis anemometer formula: V = P*(2.25/T) in MPH
# P = pulses per sample period, T = sample period in seconds
wind_speed_mph = spins * (2.25 / interval)
return wind_speed_mph
def reset_wind():
global wind_count
wind_count = 0
# Register the event handler for the sensor
wind_speed_sensor.when_pressed = spin
try:
print("Wind speed measurement started. Press Ctrl+C to exit.")
while True:
# Reset the counter
reset_wind()
# Wait for 3 seconds and count rotations
print("Measuring for 3 seconds...")
time.sleep(3)
# Calculate and display wind speed
wind_speed = calc_speed(wind_count, 3)
print(f"Wind count: {wind_count} spins")
print(f"Wind speed: {wind_speed:.2f} mph ({wind_speed * 1.60934:.2f} km/h)")
except KeyboardInterrupt:
print("\nMeasurement stopped by user")