Compare commits
44 Commits
ai_branch
...
e82d75a4d6
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e82d75a4d6 | ||
|
|
dc27e5f139 | ||
|
|
4bc05091be | ||
|
|
29f9ec445a | ||
|
|
7b398d0d6d | ||
|
|
76336d0073 | ||
|
|
46a8e21e64 | ||
|
|
2129d45ef6 | ||
|
|
6312cd8d72 | ||
|
|
7c17ec82f5 | ||
|
|
b7a6f4c907 | ||
|
|
6b3329b9b8 | ||
|
|
e9b1e0e88e | ||
|
|
2db732ebb3 | ||
|
|
d5302f78ba | ||
|
|
5b7de91d50 | ||
|
|
4d15076d4b | ||
|
|
809742b6d5 | ||
|
|
bca975b0c5 | ||
|
|
dfba956685 | ||
|
|
d07314262e | ||
|
|
dffa639574 | ||
|
|
1fd5a3e75c | ||
|
|
e674b21eaa | ||
|
|
efc94ba5e1 | ||
|
|
26328dec99 | ||
|
|
ec3e81e99e | ||
|
|
1c6af36313 | ||
|
|
f1d6f595ac | ||
|
|
cfc2e0c47f | ||
|
|
1037207df3 | ||
|
|
14044a8856 | ||
|
|
d57a47ef68 | ||
|
|
5e7375cd4e | ||
|
|
c42b16ddb6 | ||
|
|
283a46eb0b | ||
|
|
33b24a9f53 | ||
|
|
10c4348e54 | ||
|
|
072f98ef95 | ||
|
|
7b4ff011ec | ||
|
|
ab2124f50d | ||
|
|
b493d30a41 | ||
|
|
659effb7c4 | ||
|
|
ebb0fd0a2b |
40
GPIO/control.py
Normal file
40
GPIO/control.py
Normal 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
225
MPPT/read.py
Normal 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()
|
||||
@@ -52,9 +52,7 @@ def load_config(config_file):
|
||||
return {}
|
||||
|
||||
# Load the configuration data
|
||||
config_file = '/var/www/nebuleair_pro_4g/config.json'
|
||||
config = load_config(config_file)
|
||||
npm_solo_port = config.get('NPM_solo_port', '') #port du NPM solo
|
||||
npm_solo_port = "/dev/ttyAMA5" #port du NPM solo
|
||||
|
||||
#GET RTC TIME from SQlite
|
||||
cursor.execute("SELECT * FROM timestamp_table LIMIT 1")
|
||||
|
||||
@@ -28,8 +28,8 @@ Line by line installation.
|
||||
|
||||
```
|
||||
sudo apt update
|
||||
sudo apt install git gh apache2 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 apt install git gh apache2 sqlite3 php php-sqlite3 python3 python3-pip jq autossh i2c-tools python3-smbus -y
|
||||
sudo pip3 install pyserial requests RPi.GPIO adafruit-circuitpython-bme280 crcmod psutil ntplib pytz gpiozero adafruit-circuitpython-ads1x15 numpy --break-system-packages
|
||||
sudo mkdir -p /var/www/.ssh
|
||||
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
|
||||
|
||||
@@ -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).
|
||||
Requires ntplib and pytz:
|
||||
sudo pip3 install ntplib pytz --break-system-packages
|
||||
|
||||
/usr/bin/python3 /var/www/nebuleair_pro_4g/RTC/set_with_NTP.py
|
||||
|
||||
"""
|
||||
import smbus2
|
||||
import time
|
||||
@@ -49,49 +53,131 @@ def set_time(bus, year, month, day, hour, minute, second):
|
||||
])
|
||||
|
||||
def read_time(bus):
|
||||
"""Read the RTC time."""
|
||||
data = bus.read_i2c_block_data(DS3231_ADDR, REG_TIME, 7)
|
||||
second = bcd_to_dec(data[0] & 0x7F)
|
||||
minute = bcd_to_dec(data[1])
|
||||
hour = bcd_to_dec(data[2] & 0x3F)
|
||||
day = bcd_to_dec(data[4])
|
||||
month = bcd_to_dec(data[5])
|
||||
year = bcd_to_dec(data[6]) + 2000
|
||||
return (year, month, day, hour, minute, second)
|
||||
"""Read the RTC time and validate the values."""
|
||||
try:
|
||||
data = bus.read_i2c_block_data(DS3231_ADDR, REG_TIME, 7)
|
||||
|
||||
# Convert from BCD
|
||||
second = bcd_to_dec(data[0] & 0x7F)
|
||||
minute = bcd_to_dec(data[1])
|
||||
hour = bcd_to_dec(data[2] & 0x3F)
|
||||
day = bcd_to_dec(data[4])
|
||||
month = bcd_to_dec(data[5])
|
||||
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)
|
||||
|
||||
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():
|
||||
"""Get the current time from an NTP server."""
|
||||
ntp_client = ntplib.NTPClient()
|
||||
response = ntp_client.request('pool.ntp.org')
|
||||
utc_time = datetime.utcfromtimestamp(response.tx_time)
|
||||
return utc_time
|
||||
# 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)
|
||||
print(f"Successfully got time from {server}")
|
||||
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():
|
||||
bus = smbus2.SMBus(1)
|
||||
|
||||
# Get the current time from the RTC
|
||||
year, month, day, hours, minutes, seconds = read_time(bus)
|
||||
rtc_time = datetime(year, month, day, hours, minutes, seconds)
|
||||
|
||||
# Get current UTC time from an NTP server
|
||||
try:
|
||||
internet_utc_time = get_internet_time()
|
||||
print(f"Time from Internet (UTC) : {internet_utc_time.strftime('%Y-%m-%d %H:%M:%S')}")
|
||||
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
|
||||
try:
|
||||
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)
|
||||
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
|
||||
try:
|
||||
internet_utc_time = get_internet_time()
|
||||
print(f"Time from Internet (UTC) : {internet_utc_time.strftime('%Y-%m-%d %H:%M:%S')}")
|
||||
except Exception as 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
|
||||
|
||||
# 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,
|
||||
internet_utc_time.hour, internet_utc_time.minute, internet_utc_time.second)
|
||||
|
||||
# Read and print the new time from RTC
|
||||
print("Reading back new RTC time...")
|
||||
year, month, day, hour, minute, second = read_time(bus)
|
||||
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')}")
|
||||
|
||||
# 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"Error retrieving time from the internet: {e}")
|
||||
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_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)
|
||||
|
||||
# Read and print the new time from RTC
|
||||
year, month, day, hour, minute, second = read_time(bus)
|
||||
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"Unexpected error: {e}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
main()
|
||||
@@ -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'
|
||||
|
||||
|
||||
166
SARA/R5/setPDP.py
Normal file
166
SARA/R5/setPDP.py
Normal 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
|
||||
103
SARA/cellLocate/server_conf.py
Normal file
103
SARA/cellLocate/server_conf.py
Normal 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)
|
||||
@@ -13,19 +13,55 @@ Script that starts at the boot of the RPI (with cron)
|
||||
|
||||
'''
|
||||
import serial
|
||||
import RPi.GPIO as GPIO
|
||||
import time
|
||||
import sys
|
||||
import json
|
||||
import re
|
||||
|
||||
#get data from config
|
||||
def load_config(config_file):
|
||||
#GPIO
|
||||
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:
|
||||
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
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error loading config file: {e}")
|
||||
print(f"Error loading config from SQLite: {e}")
|
||||
return {}
|
||||
|
||||
#Fonction pour mettre à jour le JSON de configuration
|
||||
@@ -57,13 +93,60 @@ def update_json_key(file_path, key, 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)
|
||||
|
||||
def update_sqlite_config(key, value):
|
||||
"""
|
||||
Updates a specific key in the SQLite config_table with a new value.
|
||||
|
||||
:param key: The key to update in the config_table.
|
||||
:param value: The new value to assign to the key.
|
||||
"""
|
||||
try:
|
||||
|
||||
# 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
|
||||
device_id = config.get('deviceID', '').upper() #device ID en maj
|
||||
|
||||
sara_r5_DPD_setup = False
|
||||
|
||||
ser_sara = serial.Serial(
|
||||
port='/dev/ttyAMA2',
|
||||
baudrate=baudrate, #115200 ou 9600
|
||||
@@ -120,20 +203,46 @@ def read_complete_response(serial_connection, timeout=2, end_of_response_timeout
|
||||
try:
|
||||
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
|
||||
#Attention:
|
||||
# SARA R4 response: Manufacturer: u-blox Model: SARA-R410M-02B
|
||||
# SArA R5 response: SARA-R500S-01B-00
|
||||
print("⚙️Check SARA Status")
|
||||
command = f'ATI\r'
|
||||
ser_sara.write(command.encode('utf-8'))
|
||||
response_SARA_ATI = read_complete_response(ser_sara, wait_for_lines=["IMEI"])
|
||||
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
|
||||
print(f" Model: {model}")
|
||||
update_json_key(config_file, "modem_version", model)
|
||||
|
||||
# Check for SARA model with more robust regex
|
||||
model = "Unknown"
|
||||
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)
|
||||
|
||||
|
||||
# 1. Set AIRCARTO URL
|
||||
'''
|
||||
AIRCARTO
|
||||
'''
|
||||
# 1. Set AIRCARTO URL (profile id = 0)
|
||||
print('➡️Set aircarto URL')
|
||||
aircarto_profile_id = 0
|
||||
aircarto_url="data.nebuleair.fr"
|
||||
@@ -143,26 +252,155 @@ try:
|
||||
print(response_SARA_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_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)
|
||||
|
||||
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)
|
||||
|
||||
print("set port 81")
|
||||
command = f'AT+UHTTP={uSpot_profile_id},5,81\r'
|
||||
#step 4: set PORT (op_code = 5)
|
||||
print("➡️SET PORT")
|
||||
port = 443
|
||||
command = f'AT+UHTTP={uSpot_profile_id},5,{port}\r'
|
||||
ser_sara.write((command + '\r').encode('utf-8'))
|
||||
response_SARA_55 = read_complete_response(ser_sara, wait_for_lines=["OK"])
|
||||
print(response_SARA_55)
|
||||
time.sleep(1)
|
||||
|
||||
#step 4: set url to SSL (op_code = 6) (http_secure = 1 for HTTPS)(USECMNG_PROFILE = 2)
|
||||
print("➡️SET SSL")
|
||||
http_secure = 1
|
||||
command = f'AT+UHTTP={uSpot_profile_id},6,{http_secure},{security_profile_id}\r'
|
||||
|
||||
ser_sara.write(command.encode('utf-8'))
|
||||
response_SARA_5fg = read_complete_response(ser_sara, wait_for_lines=["OK"])
|
||||
print(response_SARA_5fg)
|
||||
time.sleep(1)
|
||||
|
||||
|
||||
'''
|
||||
SARA R5
|
||||
'''
|
||||
|
||||
if sara_r5_DPD_setup:
|
||||
print("➡️➡️SARA R5 PDP SETUP")
|
||||
# 2. Activate PDP context 1
|
||||
print('➡️Activate PDP context 1')
|
||||
command = f'AT+CGACT=1,1\r'
|
||||
ser_sara.write(command.encode('utf-8'))
|
||||
response_SARA_2 = read_complete_response(ser_sara, wait_for_lines=["OK"])
|
||||
print(response_SARA_2, end="")
|
||||
time.sleep(1)
|
||||
|
||||
# 2. Set the PDP type
|
||||
print('➡️Set the PDP type to IPv4 referring to the outputof the +CGDCONT read command')
|
||||
command = f'AT+UPSD=0,0,0\r'
|
||||
ser_sara.write(command.encode('utf-8'))
|
||||
response_SARA_3 = read_complete_response(ser_sara, wait_for_lines=["OK"])
|
||||
print(response_SARA_3, end="")
|
||||
time.sleep(1)
|
||||
|
||||
# 2. Profile #0 is mapped on CID=1.
|
||||
print('➡️Profile #0 is mapped on CID=1.')
|
||||
command = f'AT+UPSD=0,100,1\r'
|
||||
ser_sara.write(command.encode('utf-8'))
|
||||
response_SARA_3 = read_complete_response(ser_sara, wait_for_lines=["OK"])
|
||||
print(response_SARA_3, end="")
|
||||
time.sleep(1)
|
||||
|
||||
# 2. Set the PDP type
|
||||
print('➡️Activate the PSD profile #0: the IPv4 address is already assigned by the network.')
|
||||
command = f'AT+UPSDA=0,3\r'
|
||||
ser_sara.write(command.encode('utf-8'))
|
||||
response_SARA_3 = read_complete_response(ser_sara, wait_for_lines=["OK","+UUPSDA"])
|
||||
print(response_SARA_3, end="")
|
||||
time.sleep(1)
|
||||
|
||||
|
||||
#3. Get localisation (CellLocate)
|
||||
mode = 2
|
||||
sensor = 2
|
||||
mode = 2 #single shot position
|
||||
sensor = 2 #use cellular CellLocate® location information
|
||||
response_type = 0
|
||||
timeout_s = 2
|
||||
accuracy_m = 1
|
||||
@@ -179,9 +417,9 @@ try:
|
||||
else:
|
||||
print("❌ Failed to extract coordinates.")
|
||||
|
||||
#update config.json
|
||||
update_json_key(config_file, "latitude_raw", float(latitude))
|
||||
update_json_key(config_file, "longitude_raw", float(longitude))
|
||||
#update sqlite table
|
||||
update_sqlite_config("latitude_raw", float(latitude))
|
||||
update_sqlite_config("longitude_raw", float(longitude))
|
||||
|
||||
time.sleep(1)
|
||||
|
||||
|
||||
87
SARA/sara.py
87
SARA/sara.py
@@ -6,7 +6,9 @@
|
||||
|____/_/ \_\_| \_\/_/ \_\
|
||||
|
||||
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
|
||||
ex 2 (turn on blue light):
|
||||
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
|
||||
ex 4 (get HTTP Profiles)
|
||||
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,51 +49,62 @@ 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
|
||||
)
|
||||
|
||||
ser.write((command + '\r').encode('utf-8'))
|
||||
|
||||
#ser.write(b'ATI\r') #General Information
|
||||
#ser.write(b'AT+CCID?\r') #SIM card number
|
||||
#ser.write(b'AT+CPIN?\r') #Check the status of the SIM card
|
||||
#ser.write(b'AT+CIND?\r') #Indication state (last number is SIM detection: 0 no SIM detection, 1 SIM detected, 2 not available)
|
||||
#ser.write(b'AT+UGPIOR=?\r') #Reads the current value of the specified GPIO pin
|
||||
#ser.write(b'AT+UGPIOC?\r') #GPIO select configuration
|
||||
#ser.write(b'AT+COPS=?\r') #Check the network and cellular technology the modem is currently using
|
||||
#ser.write(b'AT+COPS=1,2,20801') #connext to orange
|
||||
#ser.write(b'AT+CFUN=?\r') #Selects/read the level of functionality
|
||||
#ser.write(b'AT+URAT=?\r') #Radio Access Technology
|
||||
#ser.write(b'AT+USIMSTAT?')
|
||||
#ser.write(b'AT+IPR=115200') #Check/Define baud rate
|
||||
#ser.write(b'AT+CMUX=?')
|
||||
|
||||
|
||||
|
||||
try:
|
||||
|
||||
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
|
||||
)
|
||||
|
||||
ser.write((command + '\r').encode('utf-8'))
|
||||
|
||||
#ser.write(b'ATI\r') #General Information
|
||||
#ser.write(b'AT+CCID?\r') #SIM card number
|
||||
#ser.write(b'AT+CPIN?\r') #Check the status of the SIM card
|
||||
#ser.write(b'AT+CIND?\r') #Indication state (last number is SIM detection: 0 no SIM detection, 1 SIM detected, 2 not available)
|
||||
#ser.write(b'AT+UGPIOR=?\r') #Reads the current value of the specified GPIO pin
|
||||
#ser.write(b'AT+UGPIOC?\r') #GPIO select configuration
|
||||
#ser.write(b'AT+COPS=?\r') #Check the network and cellular technology the modem is currently using
|
||||
#ser.write(b'AT+COPS=1,2,20801') #connext to orange
|
||||
#ser.write(b'AT+CFUN=?\r') #Selects/read the level of functionality
|
||||
#ser.write(b'AT+URAT=?\r') #Radio Access Technology
|
||||
#ser.write(b'AT+USIMSTAT?')
|
||||
#ser.write(b'AT+IPR=115200') #Check/Define baud rate
|
||||
#ser.write(b'AT+CMUX=?')
|
||||
|
||||
|
||||
# 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)
|
||||
start_time = time.time()
|
||||
|
||||
while (time.time() - start_time) < timeout:
|
||||
line = ser.readline().decode('utf-8', errors='ignore').strip()
|
||||
if 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
|
||||
for line in response_lines:
|
||||
print(line)
|
||||
|
||||
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:
|
||||
if ser.is_open:
|
||||
# Close the serial port if it's open
|
||||
if 'ser' in locals() and ser.is_open:
|
||||
ser.close()
|
||||
#print("Serial closed")
|
||||
|
||||
|
||||
|
||||
79
SARA/sara_checkDNS.py
Normal file
79
SARA/sara_checkDNS.py
Normal 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")
|
||||
|
||||
@@ -36,6 +36,51 @@ def load_config(config_file):
|
||||
print(f"Error loading config file: {e}")
|
||||
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
|
||||
config_file = '/var/www/nebuleair_pro_4g/config.json'
|
||||
# Load the configuration data
|
||||
@@ -57,17 +102,11 @@ ser.write((command + '\r').encode('utf-8'))
|
||||
|
||||
|
||||
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)
|
||||
response = read_complete_response(ser, wait_for_lines=["OK", "ERROR"],timeout=5, end_of_response_timeout=120, debug=True)
|
||||
|
||||
print('<p class="text-danger-emphasis">')
|
||||
print(response)
|
||||
print("</p>", end="")
|
||||
|
||||
except serial.SerialException as e:
|
||||
print(f"Error: {e}")
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
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:
|
||||
#3. Send to endpoint (with device ID)
|
||||
print("Send data (GET REQUEST):")
|
||||
@@ -111,7 +129,36 @@ try:
|
||||
parts = http_response.split(',')
|
||||
# 2.1 code 0 (HTTP failed) ⛔⛔⛔
|
||||
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)
|
||||
else:
|
||||
# Si la commande HTTP a réussi
|
||||
|
||||
@@ -49,6 +49,8 @@ ser = serial.Serial(
|
||||
)
|
||||
|
||||
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'))
|
||||
|
||||
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
Script to set the URL for a HTTP request
|
||||
Ex:
|
||||
/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:
|
||||
AT+UHTTP=0,1,"data.nebuleair.fr"
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
# Script to check if wifi is connected and start hotspot if not
|
||||
# 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"
|
||||
JSON_FILE="/var/www/nebuleair_pro_4g/config.json"
|
||||
@@ -12,6 +14,8 @@ echo "-------------------"
|
||||
|
||||
echo "NebuleAir pro started at $(date)"
|
||||
|
||||
chmod -R 777 /var/www/nebuleair_pro_4g/
|
||||
|
||||
# Blink GPIO 23 and 24 five times
|
||||
for i in {1..5}; do
|
||||
# Turn GPIO 23 and 24 ON
|
||||
@@ -25,15 +29,19 @@ for i in {1..5}; do
|
||||
sleep 1
|
||||
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
|
||||
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"
|
||||
|
||||
#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
|
||||
sleep 20
|
||||
@@ -51,17 +59,16 @@ if [ "$STATE" == "30 (disconnected)" ]; then
|
||||
echo "Starting hotspot..."
|
||||
sudo nmcli device wifi hotspot ifname wlan0 ssid nebuleair_pro password nebuleaircfg
|
||||
|
||||
# Update JSON to reflect hotspot mode
|
||||
jq --arg status "hotspot" '.WIFI_status = $status' "$JSON_FILE" > temp.json && mv temp.json "$JSON_FILE"
|
||||
|
||||
# Update SQLite to reflect hotspot mode
|
||||
sqlite3 /var/www/nebuleair_pro_4g/sqlite/sensors.db "UPDATE config_table SET value='hotspot' WHERE key='WIFI_status'"
|
||||
|
||||
else
|
||||
echo "🛜Success: wlan0 is connected!🛜"
|
||||
CONN_SSID=$(nmcli -g GENERAL.CONNECTION device show wlan0)
|
||||
echo "Connection: $CONN_SSID"
|
||||
|
||||
#update config JSON file
|
||||
jq --arg status "connected" '.WIFI_status = $status' "$JSON_FILE" > temp.json && mv temp.json "$JSON_FILE"
|
||||
# Update SQLite to reflect hotspot mode
|
||||
sqlite3 /var/www/nebuleair_pro_4g/sqlite/sensors.db "UPDATE config_table SET value='connected' WHERE key='WIFI_status'"
|
||||
|
||||
sudo chmod 777 "$JSON_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
|
||||
|
||||
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
|
||||
|
||||
|
||||
@@ -28,31 +28,14 @@ cursor.execute("SELECT * FROM timestamp_table LIMIT 1")
|
||||
row = cursor.fetchone() # Get the first (and only) row
|
||||
rtc_time_str = row[1] # '2025-02-07 12:30:45'
|
||||
|
||||
# Function to load config data
|
||||
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 {}
|
||||
# Fetch connected ENVEA sondes from SQLite config table
|
||||
cursor.execute("SELECT port, name, coefficient FROM envea_sondes_table WHERE connected = 1")
|
||||
connected_envea_sondes = cursor.fetchall() # List of tuples (port, name, coefficient)
|
||||
|
||||
# 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 = {}
|
||||
|
||||
if connected_envea_sondes:
|
||||
for device in connected_envea_sondes:
|
||||
port = device.get('port', 'Unknown')
|
||||
name = device.get('name', 'Unknown')
|
||||
for port, name, coefficient in connected_envea_sondes:
|
||||
try:
|
||||
serial_connections[name] = serial.Serial(
|
||||
port=f'/dev/{port}',
|
||||
@@ -74,9 +57,7 @@ data_nh3 = 0
|
||||
|
||||
try:
|
||||
if connected_envea_sondes:
|
||||
for device in connected_envea_sondes:
|
||||
name = device.get('name', 'Unknown')
|
||||
coefficient = device.get('coefficient', 1)
|
||||
for port, name, coefficient in connected_envea_sondes:
|
||||
if name in serial_connections:
|
||||
serial_connection = serial_connections[name]
|
||||
try:
|
||||
|
||||
800
html/admin.html
800
html/admin.html
@@ -55,52 +55,13 @@
|
||||
<div class="row mb-3">
|
||||
|
||||
<div class="col-lg-3 col-12">
|
||||
<h3 class="mt-4">Parameters</h3>
|
||||
<h3 class="mt-4">Parameters (config)</h3>
|
||||
|
||||
<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">
|
||||
<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 class="mb-3">
|
||||
@@ -108,6 +69,56 @@
|
||||
<input type="text" class="form-control" id="device_ID" disabled>
|
||||
</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>-->
|
||||
</form>
|
||||
</div>
|
||||
@@ -117,13 +128,6 @@
|
||||
|
||||
<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">
|
||||
<label for="sys_local_time" class="form-label">System time (local)</label>
|
||||
<input type="text" class="form-control" id="sys_local_time" disabled>
|
||||
@@ -161,6 +165,22 @@
|
||||
</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>
|
||||
</div>
|
||||
</div>
|
||||
@@ -193,112 +213,290 @@
|
||||
});
|
||||
});
|
||||
|
||||
//end document.addEventListener
|
||||
|
||||
|
||||
/*
|
||||
___ _ _
|
||||
/ _ \ _ __ | | ___ __ _ __| |
|
||||
| | | | '_ \| | / _ \ / _` |/ _` |
|
||||
| |_| | | | | |__| (_) | (_| | (_| |
|
||||
\___/|_| |_|_____\___/ \__,_|\__,_|
|
||||
|
||||
*/
|
||||
|
||||
window.onload = function() {
|
||||
|
||||
fetch('../config.json') // Replace 'deviceID.txt' with 'config.json'
|
||||
.then(response => response.json()) // Parse response as JSON
|
||||
.then(data => {
|
||||
console.log("Getting config file (onload)");
|
||||
//get device ID
|
||||
const deviceID = data.deviceID.trim().toUpperCase();
|
||||
//document.getElementById('pageTitle_plus_ID').innerText = 'token: ' + deviceID;
|
||||
//get device Name
|
||||
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
|
||||
const checkbox = document.getElementById("check_bme280");
|
||||
checkbox.checked = data["BME280/get_data_v2.py"];
|
||||
|
||||
//get NPM-5channels check
|
||||
const checkbox_NPM_5channels = document.getElementById("check_NPM_5channels");
|
||||
checkbox_NPM_5channels.checked = data["NextPM_5channels"];
|
||||
|
||||
//get sonde Envea check
|
||||
const checkbox_envea = document.getElementById("check_envea");
|
||||
checkbox_envea.checked = data["envea/read_value_v2.py"];
|
||||
|
||||
//get RTC check
|
||||
const checkbox_RTC = document.getElementById("check_RTC");
|
||||
checkbox_RTC.checked = data.i2c_RTC;
|
||||
//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;
|
||||
|
||||
|
||||
//device name
|
||||
const device_name = document.getElementById("device_name");
|
||||
device_name.value = data.deviceName;
|
||||
},
|
||||
error: function(xhr, status, error) {
|
||||
console.error('AJAX request failed:', status, error);
|
||||
}
|
||||
});//end AJAX
|
||||
|
||||
//getting config_scripts table
|
||||
$.ajax({
|
||||
url: 'launcher.php?type=get_config_scripts_sqlite',
|
||||
dataType:'json',
|
||||
//dataType: 'json', // Specify that you expect a JSON response
|
||||
method: 'GET', // Use GET or POST depending on your needs
|
||||
success: function(response) {
|
||||
console.log("Getting SQLite config scripts table:");
|
||||
console.log(response);
|
||||
|
||||
//device ID
|
||||
const device_ID = document.getElementById("device_ID");
|
||||
device_ID.value = data.deviceID.toUpperCase();
|
||||
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");
|
||||
|
||||
//get system time and RTC module
|
||||
$.ajax({
|
||||
url: 'launcher.php?type=sys_RTC_module_time',
|
||||
dataType: 'json', // Specify that you expect a JSON response
|
||||
method: 'GET', // Use GET or POST depending on your needs
|
||||
success: function(response) {
|
||||
console.log(response);
|
||||
// Update the input fields with the received JSON data
|
||||
document.getElementById("sys_local_time").value = response.system_local_time;
|
||||
document.getElementById("sys_UTC_time").value = response.system_utc_time;
|
||||
document.getElementById("RTC_utc_time").value = response.rtc_module_time;
|
||||
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"];
|
||||
|
||||
// Get the time difference
|
||||
const timeDiff = response.time_difference_seconds;
|
||||
//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
|
||||
|
||||
|
||||
// Reference to the alert container
|
||||
const alertContainer = document.getElementById("alert_container");
|
||||
//OLD way to get config (JSON)
|
||||
/*
|
||||
fetch('../config.json') // Replace 'deviceID.txt' with 'config.json'
|
||||
.then(response => response.json()) // Parse response as JSON
|
||||
.then(data => {
|
||||
console.log("Getting config file (onload)");
|
||||
//get device ID
|
||||
//document.getElementById('pageTitle_plus_ID').innerText = 'token: ' + deviceID;
|
||||
//get device Name
|
||||
//const deviceName = data.deviceName;
|
||||
|
||||
// Remove any previous alert
|
||||
alertContainer.innerHTML = "";
|
||||
//get BME check
|
||||
const checkbox = document.getElementById("check_bme280");
|
||||
checkbox.checked = data["BME280/get_data_v2.py"];
|
||||
|
||||
// Add an alert based on time difference
|
||||
if (typeof timeDiff === "number") {
|
||||
if (timeDiff >= 0 && timeDiff <= 10) {
|
||||
alertContainer.innerHTML = `
|
||||
<div class="alert alert-success" role="alert">
|
||||
RTC and system time are in sync (Difference: ${timeDiff} sec).
|
||||
</div>`;
|
||||
} else if (timeDiff > 10) {
|
||||
alertContainer.innerHTML = `
|
||||
<div class="alert alert-danger" role="alert">
|
||||
RTC time is out of sync! (Difference: ${timeDiff} sec).
|
||||
</div>`;
|
||||
}
|
||||
}
|
||||
},
|
||||
error: function(xhr, status, error) {
|
||||
console.error('AJAX request failed:', status, error);
|
||||
}
|
||||
});
|
||||
//get NPM-5channels check
|
||||
const checkbox_NPM_5channels = document.getElementById("check_NPM_5channels");
|
||||
checkbox_NPM_5channels.checked = data["NextPM_5channels"];
|
||||
|
||||
//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);
|
||||
}
|
||||
});
|
||||
//get sonde Envea check
|
||||
const checkbox_envea = document.getElementById("check_envea");
|
||||
checkbox_envea.checked = data["envea/read_value_v2.py"];
|
||||
|
||||
})
|
||||
.catch(error => console.error('Error loading config.json:', error));
|
||||
}
|
||||
//device name
|
||||
//const device_name = document.getElementById("device_name");
|
||||
//device_name.value = data.deviceName;
|
||||
|
||||
|
||||
})
|
||||
.catch(error => console.error('Error loading config.json:', error));
|
||||
*/
|
||||
|
||||
|
||||
|
||||
//get system time and RTC module
|
||||
$.ajax({
|
||||
url: 'launcher.php?type=sys_RTC_module_time',
|
||||
dataType: 'json', // Specify that you expect a JSON response
|
||||
method: 'GET', // Use GET or POST depending on your needs
|
||||
success: function(response) {
|
||||
console.log("Getting RTC times");
|
||||
|
||||
console.log(response);
|
||||
// Update the input fields with the received JSON data
|
||||
document.getElementById("sys_local_time").value = response.system_local_time;
|
||||
document.getElementById("sys_UTC_time").value = response.system_utc_time;
|
||||
document.getElementById("RTC_utc_time").value = response.rtc_module_time;
|
||||
|
||||
// Get the time difference
|
||||
const timeDiff = response.time_difference_seconds;
|
||||
|
||||
// Reference to the alert container
|
||||
const alertContainer = document.getElementById("alert_container");
|
||||
|
||||
// Remove any previous alert
|
||||
alertContainer.innerHTML = "";
|
||||
|
||||
// Add an alert based on time difference
|
||||
if (typeof timeDiff === "number") {
|
||||
if (timeDiff >= 0 && timeDiff <= 10) {
|
||||
alertContainer.innerHTML = `
|
||||
<div class="alert alert-success" role="alert">
|
||||
RTC and system time are in sync (Difference: ${timeDiff} sec).
|
||||
</div>`;
|
||||
} else if (timeDiff > 10) {
|
||||
alertContainer.innerHTML = `
|
||||
<div class="alert alert-danger" role="alert">
|
||||
RTC time is out of sync! (Difference: ${timeDiff} sec).
|
||||
</div>`;
|
||||
}
|
||||
}
|
||||
},
|
||||
error: function(xhr, status, error) {
|
||||
console.error('AJAX request failed:', status, error);
|
||||
}
|
||||
});//end AJAX
|
||||
|
||||
//get local RTC
|
||||
$.ajax({
|
||||
url: 'launcher.php?type=RTC_time',
|
||||
dataType: 'text', // Specify that you expect a JSON response
|
||||
method: 'GET', // Use GET or POST depending on your needs
|
||||
success: function(response) {
|
||||
//console.log("Local RTC: " + response);
|
||||
const RTC_Element = document.getElementById("RTC_time");
|
||||
RTC_Element.textContent = response;
|
||||
},
|
||||
error: function(xhr, status, error) {
|
||||
console.error('AJAX request failed:', status, error);
|
||||
}
|
||||
}); //end AJAx
|
||||
|
||||
|
||||
} //end window.onload
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
function update_config_sqlite(param, value){
|
||||
console.log("Updating sqlite ",param," : ", value);
|
||||
const toastLiveExample = document.getElementById('liveToast')
|
||||
const toastBody = toastLiveExample.querySelector('.toast-body');
|
||||
$.ajax({
|
||||
url: 'launcher.php?type=update_config_sqlite¶m='+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¶m=' + param + '&value=' + value,
|
||||
dataType: 'json',
|
||||
method: 'GET',
|
||||
cache: false,
|
||||
success: function(response) {
|
||||
console.log(response);
|
||||
// Format the response nicely
|
||||
let formattedMessage = '';
|
||||
|
||||
if (response.success) {
|
||||
// Success message
|
||||
toastLiveExample.classList.remove('text-bg-danger');
|
||||
toastLiveExample.classList.add('text-bg-success');
|
||||
|
||||
formattedMessage = `
|
||||
<strong>Success!</strong><br>
|
||||
Parameter: ${response.script_path || param}<br>
|
||||
Value: ${response.enabled !== undefined ? response.enabled : value}<br>
|
||||
${response.message || ''}
|
||||
`;
|
||||
|
||||
if (response.script_path == "envea/read_value_v2.py") {
|
||||
console.log("envea sondes activated");
|
||||
add_sondeEnveaContainer();
|
||||
|
||||
}
|
||||
} else {
|
||||
// Error message
|
||||
toastLiveExample.classList.remove('text-bg-success');
|
||||
toastLiveExample.classList.add('text-bg-danger');
|
||||
|
||||
formattedMessage = `
|
||||
<strong>Error!</strong><br>
|
||||
${response.error || 'Unknown error'}<br>
|
||||
Parameter: ${response.script_path || param}
|
||||
`;
|
||||
}
|
||||
|
||||
// Update the toast body with formatted content
|
||||
toastBody.innerHTML = formattedMessage;
|
||||
// Show the toast
|
||||
const toastBootstrap = bootstrap.Toast.getOrCreateInstance(toastLiveExample)
|
||||
toastBootstrap.show()
|
||||
},
|
||||
error: function(xhr, status, error) {
|
||||
console.error('AJAX request failed:', status, error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function update_config(param, value){
|
||||
@@ -358,7 +556,7 @@ function set_RTC_withNTP(){
|
||||
error: function(xhr, status, error) {
|
||||
console.error('AJAX request failed:', status, error);
|
||||
}
|
||||
});
|
||||
}); //end ajax
|
||||
}
|
||||
|
||||
function set_RTC_withBrowser(){
|
||||
@@ -386,7 +584,321 @@ function set_RTC_withBrowser(){
|
||||
error: function(xhr, 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();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -135,6 +135,30 @@
|
||||
|
||||
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'
|
||||
.then(response => response.json()) // Parse response as JSON
|
||||
.then(data => {
|
||||
@@ -151,7 +175,12 @@ window.onload = function() {
|
||||
elements.forEach((element) => {
|
||||
element.innerText = deviceName;
|
||||
});
|
||||
|
||||
|
||||
//end fetch config
|
||||
})
|
||||
.catch(error => console.error('Error loading config.json:', error));
|
||||
//end windows on load
|
||||
*/
|
||||
//get local RTC
|
||||
$.ajax({
|
||||
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>
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
<?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("Cache-Control: no-store, no-cache, must-revalidate, max-age=0");
|
||||
header("Pragma: no-cache");
|
||||
|
||||
$database_path = "/var/www/nebuleair_pro_4g/sqlite/sensors.db";
|
||||
|
||||
|
||||
$type=$_GET['type'];
|
||||
|
||||
if ($type == "get_npm_sqlite_data") {
|
||||
$database_path = "/var/www/nebuleair_pro_4g/sqlite/sensors.db";
|
||||
//echo "Getting data from sqlite database";
|
||||
try {
|
||||
$db = new PDO("sqlite:$database_path");
|
||||
@@ -25,8 +28,327 @@ if ($type == "get_npm_sqlite_data") {
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
*/
|
||||
//GETING data from config_table (SQLite DB)
|
||||
if ($type == "get_config_sqlite") {
|
||||
try {
|
||||
$db = new PDO("sqlite:$database_path");
|
||||
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
|
||||
|
||||
// Get all main configuration entries
|
||||
$config_query = $db->query("SELECT key, value, type FROM config_table");
|
||||
$config_data = $config_query->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
// Convert data types according to their 'type' field
|
||||
$result = [];
|
||||
foreach ($config_data as $item) {
|
||||
$key = $item['key'];
|
||||
$value = $item['value'];
|
||||
$type = $item['type'];
|
||||
|
||||
// Convert value based on its type
|
||||
switch ($type) {
|
||||
case 'bool':
|
||||
$parsed_value = ($value == '1' || $value == 'true') ? true : false;
|
||||
break;
|
||||
case 'int':
|
||||
$parsed_value = intval($value);
|
||||
break;
|
||||
case 'float':
|
||||
$parsed_value = floatval($value);
|
||||
break;
|
||||
default:
|
||||
$parsed_value = $value;
|
||||
}
|
||||
|
||||
$result[$key] = $parsed_value;
|
||||
}
|
||||
|
||||
// Return JSON response
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode($result, JSON_PRETTY_PRINT);
|
||||
|
||||
} catch (PDOException $e) {
|
||||
// Return error as JSON
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode(['error' => 'Database error: ' . $e->getMessage()]);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
*/
|
||||
//GETING data from config_scrips_table (SQLite DB)
|
||||
if ($type == "get_config_scripts_sqlite") {
|
||||
|
||||
try {
|
||||
$db = new PDO("sqlite:$database_path");
|
||||
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
|
||||
|
||||
// Get all main configuration entries
|
||||
$config_query = $db->query("SELECT * FROM config_scripts_table");
|
||||
$config_data = $config_query->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
// Convert data types according to their 'type' field
|
||||
$result = [];
|
||||
foreach ($config_data as $item) {
|
||||
$script_path = $item['script_path'];
|
||||
$enabled = $item['enabled'];
|
||||
|
||||
// Convert the enabled field to a proper boolean
|
||||
$result[$script_path] = ($enabled == 1);
|
||||
}
|
||||
|
||||
// Return JSON response
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode($result, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
|
||||
|
||||
} catch (PDOException $e) {
|
||||
// Return error as JSON
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode(['error' => 'Database error: ' . $e->getMessage()]);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
*/
|
||||
//GETING data from envea_sondes_table (SQLite DB)
|
||||
if ($type == "get_envea_sondes_table_sqlite") {
|
||||
|
||||
try {
|
||||
$db = new PDO("sqlite:$database_path");
|
||||
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
|
||||
|
||||
// Get all entries from envea_sondes_table
|
||||
$query = $db->query("SELECT id, connected, port, name, coefficient FROM envea_sondes_table");
|
||||
$data = $query->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
// Convert data types appropriately
|
||||
$result = [];
|
||||
foreach ($data as $item) {
|
||||
// Create object for each sonde with proper data types
|
||||
$sonde = [
|
||||
'id' => (int)$item['id'],
|
||||
'connected' => $item['connected'] == 1, // Convert to boolean
|
||||
'port' => $item['port'],
|
||||
'name' => $item['name'],
|
||||
'coefficient' => (float)$item['coefficient'] // Convert to float
|
||||
];
|
||||
|
||||
// Add to results array
|
||||
$result[] = $sonde;
|
||||
}
|
||||
|
||||
|
||||
// Return JSON response
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode($result, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
|
||||
|
||||
} catch (PDOException $e) {
|
||||
// Return error as JSON
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode(['error' => 'Database error: ' . $e->getMessage()]);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
//UPDATING the config_table from SQLite DB
|
||||
if ($type == "update_config_sqlite") {
|
||||
$param = $_GET['param'] ?? null;
|
||||
$value = $_GET['value'] ?? null;
|
||||
|
||||
if ($param === null || $value === null) {
|
||||
echo json_encode(["error" => "Missing parameter or value"]);
|
||||
exit;
|
||||
}
|
||||
|
||||
try {
|
||||
$db = new PDO("sqlite:$database_path");
|
||||
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
|
||||
|
||||
// First, check if parameter exists and get its type
|
||||
$checkStmt = $db->prepare("SELECT type FROM config_table WHERE key = :param");
|
||||
$checkStmt->bindParam(':param', $param);
|
||||
$checkStmt->execute();
|
||||
$result = $checkStmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if ($result) {
|
||||
// Parameter exists, determine type and update
|
||||
$type = $result['type'];
|
||||
|
||||
// Convert value according to type if needed
|
||||
$convertedValue = $value;
|
||||
if ($type == "bool") {
|
||||
// Convert various boolean representations to 0/1
|
||||
$convertedValue = (filter_var($value, FILTER_VALIDATE_BOOLEAN)) ? "1" : "0";
|
||||
} elseif ($type == "int") {
|
||||
$convertedValue = (string)intval($value);
|
||||
} elseif ($type == "float") {
|
||||
$convertedValue = (string)floatval($value);
|
||||
}
|
||||
|
||||
// Update the value
|
||||
$updateStmt = $db->prepare("UPDATE config_table SET value = :value WHERE key = :param");
|
||||
$updateStmt->bindParam(':value', $convertedValue);
|
||||
$updateStmt->bindParam(':param', $param);
|
||||
$updateStmt->execute();
|
||||
|
||||
echo json_encode([
|
||||
"success" => true,
|
||||
"message" => "Configuration updated successfully",
|
||||
"param" => $param,
|
||||
"value" => $convertedValue,
|
||||
"type" => $type
|
||||
]);
|
||||
} else {
|
||||
echo json_encode([
|
||||
"error" => "Parameter not found in configuration",
|
||||
"param" => $param
|
||||
]);
|
||||
}
|
||||
} catch (PDOException $e) {
|
||||
echo json_encode(["error" => $e->getMessage()]);
|
||||
}
|
||||
}
|
||||
|
||||
//UPDATING the config_scripts table from SQLite DB
|
||||
if ($type == "update_config_scripts_sqlite") {
|
||||
$script_path = $_GET['param'] ?? null;
|
||||
$enabled = $_GET['value'] ?? null;
|
||||
|
||||
if ($script_path === null || $enabled === null) {
|
||||
echo json_encode(["error" => "Missing parameter or value"]);
|
||||
exit;
|
||||
}
|
||||
|
||||
try {
|
||||
$db = new PDO("sqlite:$database_path");
|
||||
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
|
||||
|
||||
// First, check if parameter exists and get its type
|
||||
$checkStmt = $db->prepare("SELECT enabled FROM config_scripts_table WHERE script_path = :script_path");
|
||||
$checkStmt->bindParam(':script_path', $script_path);
|
||||
$checkStmt->execute();
|
||||
$result = $checkStmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if ($result) {
|
||||
// Convert enabled value to 0 or 1
|
||||
$enabledValue = (filter_var($enabled, FILTER_VALIDATE_BOOLEAN)) ? 1 : 0;
|
||||
|
||||
// Update the enabled status
|
||||
$updateStmt = $db->prepare("UPDATE config_scripts_table SET enabled = :enabled WHERE script_path = :script_path");
|
||||
$updateStmt->bindParam(':enabled', $enabledValue, PDO::PARAM_INT);
|
||||
$updateStmt->bindParam(':script_path', $script_path);
|
||||
$updateStmt->execute();
|
||||
|
||||
echo json_encode([
|
||||
"success" => true,
|
||||
"message" => "Script configuration updated successfully",
|
||||
"script_path" => $script_path,
|
||||
"enabled" => (bool)$enabledValue
|
||||
], JSON_UNESCAPED_SLASHES); // Prevent escaping forward slashes
|
||||
} else {
|
||||
echo json_encode([
|
||||
"error" => "Script path not found in configuration",
|
||||
"script_path" => $script_path
|
||||
], JSON_UNESCAPED_SLASHES); // Prevent escaping forward slashes
|
||||
}
|
||||
} catch (PDOException $e) {
|
||||
echo json_encode(["error" => $e->getMessage()]);
|
||||
}
|
||||
}
|
||||
|
||||
//UPDATING the envea_sondes_table table from SQLite DB
|
||||
if ($type == "update_sonde") {
|
||||
$id = $_GET['id'] ?? null;
|
||||
$field = $_GET['field'] ?? null;
|
||||
$value = $_GET['value'] ?? null;
|
||||
|
||||
// Validate parameters
|
||||
if ($id === null || $field === null || $value === null) {
|
||||
echo json_encode([
|
||||
"success" => false,
|
||||
"error" => "Missing required parameters (id, field, or value)"
|
||||
]);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Validate field name (whitelist approach for security)
|
||||
$allowed_fields = ['connected', 'port', 'name', 'coefficient'];
|
||||
if (!in_array($field, $allowed_fields)) {
|
||||
echo json_encode([
|
||||
"success" => false,
|
||||
"error" => "Invalid field name: " . $field
|
||||
]);
|
||||
exit;
|
||||
}
|
||||
|
||||
try {
|
||||
// Connect to the database
|
||||
$db = new PDO("sqlite:$database_path");
|
||||
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
|
||||
|
||||
// Check if the sonde exists
|
||||
$checkStmt = $db->prepare("SELECT id FROM envea_sondes_table WHERE id = :id");
|
||||
$checkStmt->bindParam(':id', $id, PDO::PARAM_INT);
|
||||
$checkStmt->execute();
|
||||
|
||||
if (!$checkStmt->fetch()) {
|
||||
echo json_encode([
|
||||
"success" => false,
|
||||
"error" => "Sonde with ID $id not found"
|
||||
]);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Process value based on field type
|
||||
if ($field == 'connected') {
|
||||
// Convert to integer (0 or 1)
|
||||
$processedValue = filter_var($value, FILTER_VALIDATE_BOOLEAN) ? 1 : 0;
|
||||
$paramType = PDO::PARAM_INT;
|
||||
} else if ($field == 'coefficient') {
|
||||
// Convert to float
|
||||
$processedValue = floatval($value);
|
||||
$paramType = PDO::PARAM_STR; // SQLite doesn't have PARAM_FLOAT
|
||||
} else {
|
||||
// For text fields (port, name)
|
||||
$processedValue = $value;
|
||||
$paramType = PDO::PARAM_STR;
|
||||
}
|
||||
|
||||
// Update the sonde record
|
||||
$updateStmt = $db->prepare("UPDATE envea_sondes_table SET $field = :value WHERE id = :id");
|
||||
$updateStmt->bindParam(':value', $processedValue, $paramType);
|
||||
$updateStmt->bindParam(':id', $id, PDO::PARAM_INT);
|
||||
$updateStmt->execute();
|
||||
|
||||
// Return success response
|
||||
echo json_encode([
|
||||
"success" => true,
|
||||
"message" => "Sonde $id updated successfully",
|
||||
"field" => $field,
|
||||
"value" => $processedValue
|
||||
]);
|
||||
|
||||
} catch (PDOException $e) {
|
||||
// Return error as JSON
|
||||
echo json_encode([
|
||||
"success" => false,
|
||||
"error" => "Database error: " . $e->getMessage()
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
//update the config (old JSON updating)
|
||||
if ($type == "update_config") {
|
||||
echo "updating....";
|
||||
echo "updating.... ";
|
||||
$param=$_GET['param'];
|
||||
$value=$_GET['value'];
|
||||
$configFile = '../config.json';
|
||||
@@ -269,7 +591,7 @@ if ($type == "sara_connectNetwork") {
|
||||
$timeout=$_GET['timeout'];
|
||||
$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)
|
||||
$networkID = is_numeric($networkID) ? (strpos($networkID, '.') !== false ? (float)$networkID : (int)$networkID) : 0;
|
||||
#save to config.json
|
||||
@@ -296,10 +618,10 @@ if ($type == "sara_connectNetwork") {
|
||||
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;
|
||||
$output = shell_exec($command);
|
||||
echo $output;
|
||||
|
||||
@@ -179,40 +179,46 @@ window.onload = function() {
|
||||
getModem_busy_status();
|
||||
setInterval(getModem_busy_status, 2000);
|
||||
|
||||
fetch('../config.json') // Replace 'deviceID.txt' with 'config.json'
|
||||
.then(response => response.json()) // Parse response as JSON
|
||||
.then(data => {
|
||||
console.log("Getting config file (onload)");
|
||||
//get device ID
|
||||
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');
|
||||
elements.forEach((element) => {
|
||||
element.innerText = deviceName;
|
||||
});
|
||||
//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_side bar
|
||||
const elements = document.querySelectorAll('.sideBar_sensorName');
|
||||
elements.forEach((element) => {
|
||||
element.innerText = response.deviceName;
|
||||
});
|
||||
|
||||
},
|
||||
error: function(xhr, status, error) {
|
||||
console.error('AJAX request failed:', status, error);
|
||||
}
|
||||
});//end AJAX
|
||||
|
||||
|
||||
//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);
|
||||
}
|
||||
});
|
||||
//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);
|
||||
}
|
||||
});
|
||||
|
||||
})
|
||||
.catch(error => console.error('Error loading config.json:', error));
|
||||
}
|
||||
|
||||
}//end onload
|
||||
|
||||
|
||||
function clear_loopLogs(){
|
||||
|
||||
171
html/saraR4.html
171
html/saraR4.html
@@ -59,11 +59,12 @@
|
||||
</div>
|
||||
|
||||
<span id="modem_status_message"></span>
|
||||
<!--
|
||||
<h3>
|
||||
Status
|
||||
<span id="modem-status" class="badge">Loading...</span>
|
||||
</h3>
|
||||
|
||||
-->
|
||||
<div class="row mb-3">
|
||||
|
||||
<div class="col-sm-3">
|
||||
@@ -71,7 +72,7 @@
|
||||
|
||||
<div class="card-body">
|
||||
<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="response_ttyAMA2_ATI"></div>
|
||||
|
||||
@@ -84,7 +85,7 @@
|
||||
|
||||
<div class="card-body">
|
||||
<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="response_ttyAMA2_AT_CCID_"></div>
|
||||
</div>
|
||||
@@ -109,7 +110,7 @@
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<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="response_ttyAMA2_AT_CSQ"></div>
|
||||
</table>
|
||||
@@ -121,7 +122,7 @@
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<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="response_ttyAMA2_AT_CFUN_15"></div>
|
||||
</table>
|
||||
@@ -304,7 +305,20 @@
|
||||
|
||||
|
||||
</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>
|
||||
</div>
|
||||
</div>
|
||||
@@ -317,35 +331,62 @@
|
||||
<script src="assets/js/bootstrap.bundle.js"></script>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
const elementsToLoad = [
|
||||
{ id: 'topbar', file: 'topbar.html' },
|
||||
{ id: 'sidebar', file: 'sidebar.html' },
|
||||
{ id: 'sidebar_mobile', file: 'sidebar.html' }
|
||||
];
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
const elementsToLoad = [
|
||||
{ id: 'topbar', file: 'topbar.html' },
|
||||
{ id: 'sidebar', file: 'sidebar.html' },
|
||||
{ id: 'sidebar_mobile', file: 'sidebar.html' }
|
||||
];
|
||||
|
||||
elementsToLoad.forEach(({ id, file }) => {
|
||||
fetch(file)
|
||||
.then(response => response.text())
|
||||
.then(data => {
|
||||
const element = document.getElementById(id);
|
||||
if (element) {
|
||||
element.innerHTML = data;
|
||||
elementsToLoad.forEach(({ id, file }) => {
|
||||
fetch(file)
|
||||
.then(response => response.text())
|
||||
.then(data => {
|
||||
const element = document.getElementById(id);
|
||||
if (element) {
|
||||
element.innerHTML = data;
|
||||
}
|
||||
})
|
||||
.catch(error => console.error(`Error loading ${file}:`, error));
|
||||
});
|
||||
|
||||
//OLD way to retreive data from JSON
|
||||
/*
|
||||
fetch('../config.json') // Replace 'deviceID.txt' with 'config.json'
|
||||
.then(response => response.json()) // Parse response as JSON
|
||||
.then(data => {
|
||||
console.log("Getting config file (onload)");
|
||||
//modem config mode
|
||||
const check_modem_configMode = document.getElementById("check_modem_configMode");
|
||||
check_modem_configMode.checked = data.modem_config_mode;
|
||||
console.log("Modem configuration: " + data.modem_config_mode);
|
||||
})
|
||||
*/
|
||||
|
||||
//NEW way to get data from SQLITE
|
||||
$.ajax({
|
||||
url: 'launcher.php?type=get_config_sqlite',
|
||||
dataType:'json',
|
||||
//dataType: 'json', // Specify that you expect a JSON response
|
||||
method: 'GET', // Use GET or POST depending on your needs
|
||||
success: function(response) {
|
||||
console.log("Getting SQLite config table:");
|
||||
console.log(response);
|
||||
// Set checkbox state based on the response data
|
||||
const check_modem_configMode = document.getElementById("check_modem_configMode");
|
||||
if (check_modem_configMode) {
|
||||
check_modem_configMode.checked = response.modem_config_mode;
|
||||
console.log("Modem configuration: " + response.modem_config_mode);
|
||||
} else {
|
||||
console.error("Checkbox element not found");
|
||||
}
|
||||
})
|
||||
.catch(error => console.error(`Error loading ${file}:`, error));
|
||||
});
|
||||
|
||||
fetch('../config.json') // Replace 'deviceID.txt' with 'config.json'
|
||||
.then(response => response.json()) // Parse response as JSON
|
||||
.then(data => {
|
||||
console.log("Getting config file (onload)");
|
||||
//modem config mode
|
||||
const check_modem_configMode = document.getElementById("check_modem_configMode");
|
||||
check_modem_configMode.checked = data.modem_config_mode;
|
||||
console.log("Modem configuration: " + data.modem_config_mode);
|
||||
|
||||
})
|
||||
},
|
||||
error: function(xhr, status, error) {
|
||||
console.error('AJAX request failed:', status, error);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
|
||||
@@ -430,8 +471,10 @@ function getData_saraR4(port, command, timeout){
|
||||
} else{
|
||||
// si c'est une commande AT normale
|
||||
// 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);
|
||||
|
||||
}
|
||||
},
|
||||
error: function(xhr, status, error) {
|
||||
@@ -700,17 +743,68 @@ function getModem_busy_status() {
|
||||
}
|
||||
|
||||
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);
|
||||
$.ajax({
|
||||
url: 'launcher.php?type=update_config¶m='+param+'&value='+checked,
|
||||
dataType: 'text', // Specify that you expect a JSON response
|
||||
url: 'launcher.php?type=update_config_sqlite¶m='+param+'&value='+checked,
|
||||
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);
|
||||
|
||||
console.log("AJAX success:");
|
||||
console.log(response);
|
||||
|
||||
// Format the response nicely
|
||||
let formattedMessage = '';
|
||||
|
||||
if (response.success) {
|
||||
// Success message
|
||||
toastLiveExample.classList.remove('text-bg-danger');
|
||||
toastLiveExample.classList.add('text-bg-success');
|
||||
|
||||
formattedMessage = `
|
||||
<strong>Success!</strong><br>
|
||||
Parameter: ${response.param || param}<br>
|
||||
Value: ${response.value || checked}<br>
|
||||
${response.message || ''}
|
||||
`;
|
||||
} else {
|
||||
// Error message
|
||||
toastLiveExample.classList.remove('text-bg-success');
|
||||
toastLiveExample.classList.add('text-bg-danger');
|
||||
|
||||
formattedMessage = `
|
||||
<strong>Error!</strong><br>
|
||||
${response.error || 'Unknown error'}<br>
|
||||
Parameter: ${response.param || param}
|
||||
`;
|
||||
}
|
||||
|
||||
// Update the toast body with formatted content
|
||||
toastBody.innerHTML = formattedMessage;
|
||||
// Show the toast
|
||||
const toastBootstrap = bootstrap.Toast.getOrCreateInstance(toastLiveExample)
|
||||
toastBootstrap.show()
|
||||
|
||||
|
||||
},
|
||||
error: function(xhr, status, error) {
|
||||
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();
|
||||
setInterval(getModem_busy_status, 1000);
|
||||
|
||||
|
||||
fetch('../config.json') // Replace 'deviceID.txt' with 'config.json'
|
||||
.then(response => response.json()) // Parse response as JSON
|
||||
.then(data => {
|
||||
@@ -744,6 +839,7 @@ window.onload = function() {
|
||||
|
||||
|
||||
//get SARA_R4 connection status
|
||||
/*
|
||||
const SARA_statusElement = document.getElementById("modem-status");
|
||||
console.log("SARA R4 is: " + data.SARA_R4_network_status);
|
||||
|
||||
@@ -757,7 +853,7 @@ window.onload = function() {
|
||||
SARA_statusElement.textContent = "Unknown";
|
||||
SARA_statusElement.className = "badge text-bg-secondary";
|
||||
}
|
||||
|
||||
*/
|
||||
//get local RTC
|
||||
$.ajax({
|
||||
url: 'launcher.php?type=RTC_time',
|
||||
@@ -778,6 +874,7 @@ window.onload = function() {
|
||||
})
|
||||
.catch(error => console.error('Error loading config.json:', error));
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
</body>
|
||||
|
||||
@@ -144,40 +144,50 @@ function getNPM_values(port){
|
||||
}
|
||||
|
||||
function getENVEA_values(port, name){
|
||||
console.log("Data from Envea "+ name+" (port "+port+"):");
|
||||
$("#loading_envea"+name).show();
|
||||
console.log("Data from Envea " + name + " (port " + port + "):");
|
||||
$("#loading_envea" + name).show();
|
||||
|
||||
$.ajax({
|
||||
url: 'launcher.php?type=envea&port='+port+'&name='+name,
|
||||
dataType: 'json', // Specify that you expect a JSON response
|
||||
method: 'GET', // Use GET or POST depending on your needs
|
||||
success: function(response) {
|
||||
console.log(response);
|
||||
const tableBody = document.getElementById("data-table-body_envea"+name);
|
||||
tableBody.innerHTML = "";
|
||||
$.ajax({
|
||||
url: 'launcher.php?type=envea&port=' + port + '&name=' + name,
|
||||
dataType: 'json',
|
||||
method: 'GET',
|
||||
success: function(response) {
|
||||
console.log(response);
|
||||
const tableBody = document.getElementById("data-table-body_envea" + name);
|
||||
tableBody.innerHTML = "";
|
||||
|
||||
$("#loading_envea"+name).hide();
|
||||
// Create an array of the desired keys
|
||||
// Create an array of the desired keys
|
||||
const keysToShow = [name];
|
||||
// Add only the specified elements to the table
|
||||
keysToShow.forEach(key => {
|
||||
if (response !== undefined) { // Check if the key exists in the response
|
||||
const value = response;
|
||||
$("#data-table-body_envea"+name).append(`
|
||||
<tr>
|
||||
<td>${key}</td>
|
||||
<td>${value} ppb</td>
|
||||
</tr>
|
||||
`);
|
||||
}
|
||||
});
|
||||
},
|
||||
error: function(xhr, status, error) {
|
||||
console.error('AJAX request failed:', status, error);
|
||||
}
|
||||
$("#loading_envea" + name).hide();
|
||||
|
||||
const keysToShow = [name];
|
||||
keysToShow.forEach(key => {
|
||||
if (response !== undefined) {
|
||||
const value = response;
|
||||
$("#data-table-body_envea" + name).append(`
|
||||
<tr>
|
||||
<td>${key}</td>
|
||||
<td>${value} ppb</td>
|
||||
</tr>
|
||||
`);
|
||||
}
|
||||
});
|
||||
},
|
||||
error: function(xhr, status, error) {
|
||||
console.error('AJAX request failed:', status, error);
|
||||
const tableBody = document.getElementById("data-table-body_envea" + name);
|
||||
$("#loading_envea" + name).hide();
|
||||
|
||||
tableBody.innerHTML = `
|
||||
<tr>
|
||||
<td colspan="2" class="text-danger">
|
||||
❌ Error: unable to get data from sensor.<br>
|
||||
<small>${status}: ${error}</small>
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function getNoise_values(){
|
||||
console.log("Data from I2C Noise Sensor:");
|
||||
@@ -261,143 +271,190 @@ function getBME280_values(){
|
||||
|
||||
|
||||
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');
|
||||
elements.forEach((element) => {
|
||||
element.innerText = deviceName;
|
||||
});
|
||||
|
||||
//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_side bar
|
||||
const elements = document.querySelectorAll('.sideBar_sensorName');
|
||||
elements.forEach((element) => {
|
||||
element.innerText = response.deviceName;
|
||||
});
|
||||
|
||||
},
|
||||
error: function(xhr, status, error) {
|
||||
console.error('AJAX request failed:', status, error);
|
||||
}
|
||||
});//end AJAX
|
||||
|
||||
//getting config_scripts table
|
||||
$.ajax({
|
||||
url: 'launcher.php?type=get_config_scripts_sqlite',
|
||||
dataType:'json',
|
||||
//dataType: 'json', // Specify that you expect a JSON response
|
||||
method: 'GET', // Use GET or POST depending on your needs
|
||||
success: function(response) {
|
||||
console.log("Getting SQLite config scripts table:");
|
||||
console.log(response);
|
||||
|
||||
const container = document.getElementById('card-container'); // Conteneur des cartes
|
||||
|
||||
//creates NPM card
|
||||
if (response["NPM/get_data_modbus_v3.py"]) {
|
||||
const cardHTML = `
|
||||
<div class="col-sm-3">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
Port UART
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">NextPM</h5>
|
||||
<p class="card-text">Capteur particules fines.</p>
|
||||
<button class="btn btn-primary" onclick="getNPM_values('ttyAMA5')">Get Data</button>
|
||||
<br>
|
||||
<div id="loading_ttyAMA5" class="spinner-border spinner-border-sm" style="display: none;" role="status"></div>
|
||||
<table class="table table-striped-columns">
|
||||
<tbody id="data-table-body_ttyAMA5"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
container.innerHTML += cardHTML; // Add the I2C card if condition is met
|
||||
}
|
||||
|
||||
//creates i2c BME280 card
|
||||
if (response["BME280/get_data_v2.py"]) {
|
||||
const i2C_BME_HTML = `
|
||||
<div class="col-sm-3">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
Port I2C
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">BME280 Temp/Hum sensor</h5>
|
||||
<p class="card-text">Capteur température et humidité sur le port I2C.</p>
|
||||
<button class="btn btn-primary mb-1" onclick="getBME280_values()">Get Data</button>
|
||||
<br>
|
||||
<div id="loading_BME280" class="spinner-border spinner-border-sm" style="display: none;" role="status"></div>
|
||||
<table class="table table-striped-columns">
|
||||
<tbody id="data-table-body_BME280"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
container.innerHTML += i2C_BME_HTML; // Add the I2C card if condition is met
|
||||
}
|
||||
|
||||
//creates i2c sound card
|
||||
if (response.i2C_sound) {
|
||||
const i2C_HTML = `
|
||||
<div class="col-sm-3">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
Port I2C
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Decibel Meter</h5>
|
||||
<p class="card-text">Capteur bruit sur le port I2C.</p>
|
||||
<button class="btn btn-primary mb-1" onclick="getNoise_values()">Get Data</button>
|
||||
<br>
|
||||
<button class="btn btn-success" onclick="startNoise()">Start recording</button>
|
||||
<button class="btn btn-danger" onclick="stopNoise()">Stop recording</button>
|
||||
<div id="loading_noise" class="spinner-border spinner-border-sm" style="display: none;" role="status"></div>
|
||||
<table class="table table-striped-columns">
|
||||
<tbody id="data-table-body_noise"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
container.innerHTML += i2C_HTML; // Add the I2C card if condition is met
|
||||
}
|
||||
|
||||
//Si on a des SONDES ENVEA connectée il faut faire un deuxième call dans la table envea_sondes_table
|
||||
//creates ENVEA cards
|
||||
if (response["envea/read_value_v2.py"]) {
|
||||
console.log("Need to display ENVEA sondes");
|
||||
//getting config_scripts table
|
||||
$.ajax({
|
||||
url: 'launcher.php?type=get_envea_sondes_table_sqlite',
|
||||
dataType:'json',
|
||||
//dataType: 'json', // Specify that you expect a JSON response
|
||||
method: 'GET', // Use GET or POST depending on your needs
|
||||
success: function(sondes) {
|
||||
console.log("Getting SQLite envea sondes table:");
|
||||
console.log(sondes);
|
||||
const ENVEA_sensors = sondes.filter(sonde => sonde.connected); // Filter only connected sondes
|
||||
|
||||
ENVEA_sensors.forEach((sensor, index) => {
|
||||
const port = sensor.port; // Port from the sensor object
|
||||
const name = sensor.name; // Port from the sensor object
|
||||
const coefficient = sensor.coefficient;
|
||||
const cardHTML = `
|
||||
<div class="col-sm-3">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
Port UART ${port.replace('ttyAMA', '')}
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Sonde Envea ${name}</h5>
|
||||
<p class="card-text">Capteur gas.</p>
|
||||
<button class="btn btn-primary" onclick="getENVEA_values('${port}','${name}')">Get Data</button>
|
||||
<div id="loading_envea${name}" class="spinner-border spinner-border-sm" style="display: none;" role="status"></div>
|
||||
<table class="table table-striped-columns">
|
||||
<tbody id="data-table-body_envea${name}"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
container.innerHTML += cardHTML; // Ajouter la carte au conteneur
|
||||
});
|
||||
|
||||
|
||||
|
||||
},
|
||||
error: function(xhr, status, error) {
|
||||
console.error('AJAX request failed:', status, error);
|
||||
}
|
||||
});//end AJAX envea Sondes
|
||||
|
||||
//get local RTC
|
||||
$.ajax({
|
||||
url: 'launcher.php?type=RTC_time',
|
||||
dataType: 'text', // Specify that you expect a JSON response
|
||||
method: 'GET', // Use GET or POST depending on your needs
|
||||
success: function(response) {
|
||||
console.log("Local RTC: " + response);
|
||||
const RTC_Element = document.getElementById("RTC_time");
|
||||
RTC_Element.textContent = response;
|
||||
|
||||
},
|
||||
error: function(xhr, status, error) {
|
||||
console.error('AJAX request failed:', status, error);
|
||||
}
|
||||
});
|
||||
}//end if
|
||||
|
||||
|
||||
const container = document.getElementById('card-container'); // Conteneur des cartes
|
||||
|
||||
//creates NPM cards
|
||||
const NPM_ports = data.NextPM_ports; // Récupère les ports
|
||||
NPM_ports.forEach((port, index) => {
|
||||
const cardHTML = `
|
||||
<div class="col-sm-3">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
Port UART ${port.replace('ttyAMA', '')}
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">NextPM ${String.fromCharCode(65 + index)}</h5>
|
||||
<p class="card-text">Capteur particules fines.</p>
|
||||
<button class="btn btn-primary" onclick="getNPM_values('${port}')">Get Data</button>
|
||||
<div id="loading_${port}" class="spinner-border spinner-border-sm" style="display: none;" role="status"></div>
|
||||
<table class="table table-striped-columns">
|
||||
<tbody id="data-table-body_${port}"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
container.innerHTML += cardHTML; // Ajouter la carte au conteneur
|
||||
});
|
||||
},
|
||||
error: function(xhr, status, error) {
|
||||
console.error('AJAX request failed:', status, error);
|
||||
}
|
||||
});//end AJAX (config_scripts)
|
||||
|
||||
//creates ENVEA cards
|
||||
const ENVEA_sensors = data.envea_sondes.filter(sonde => sonde.connected); // Filter only connected sondes
|
||||
//get local RTC
|
||||
$.ajax({
|
||||
url: 'launcher.php?type=RTC_time',
|
||||
dataType: 'text', // Specify that you expect a JSON response
|
||||
method: 'GET', // Use GET or POST depending on your needs
|
||||
success: function(response) {
|
||||
console.log("Local RTC: " + response);
|
||||
const RTC_Element = document.getElementById("RTC_time");
|
||||
RTC_Element.textContent = response;
|
||||
|
||||
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
|
||||
});
|
||||
},
|
||||
error: function(xhr, status, error) {
|
||||
console.error('AJAX request failed:', status, error);
|
||||
}
|
||||
});
|
||||
|
||||
//creates i2c BME280 card
|
||||
if (data["BME280/get_data_v2.py"]) {
|
||||
const i2C_BME_HTML = `
|
||||
<div class="col-sm-3">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
Port I2C
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">BME280 Temp/Hum sensor</h5>
|
||||
<p class="card-text">Capteur température et humidité sur le port I2C.</p>
|
||||
<button class="btn btn-primary mb-1" onclick="getBME280_values()">Get Data</button>
|
||||
<br>
|
||||
<div id="loading_BME280" class="spinner-border spinner-border-sm" style="display: none;" role="status"></div>
|
||||
<table class="table table-striped-columns">
|
||||
<tbody id="data-table-body_BME280"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
container.innerHTML += i2C_BME_HTML; // Add the I2C card if condition is met
|
||||
}
|
||||
|
||||
//creates i2c sound card
|
||||
if (data.i2C_sound) {
|
||||
const i2C_HTML = `
|
||||
<div class="col-sm-3">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
Port I2C
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Decibel Meter</h5>
|
||||
<p class="card-text">Capteur bruit sur le port I2C.</p>
|
||||
<button class="btn btn-primary mb-1" onclick="getNoise_values()">Get Data</button>
|
||||
<br>
|
||||
<button class="btn btn-success" onclick="startNoise()">Start recording</button>
|
||||
<button class="btn btn-danger" onclick="stopNoise()">Stop recording</button>
|
||||
<div id="loading_noise" class="spinner-border spinner-border-sm" style="display: none;" role="status"></div>
|
||||
<table class="table table-striped-columns">
|
||||
<tbody id="data-table-body_noise"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
container.innerHTML += i2C_HTML; // Add the I2C card if condition is met
|
||||
}
|
||||
|
||||
})
|
||||
.catch(error => console.error('Error loading config.json:', error));
|
||||
}
|
||||
} //end windows onload
|
||||
</script>
|
||||
|
||||
</body>
|
||||
|
||||
@@ -23,11 +23,11 @@ fi
|
||||
|
||||
# Update and install 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
|
||||
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
|
||||
read -p "Do you want to set up an SSH key for /var/www? (y/n): " answer
|
||||
|
||||
@@ -27,6 +27,10 @@ fi
|
||||
info "Set up the RTC"
|
||||
/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
|
||||
info "Set up Monogoto APN"
|
||||
/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"
|
||||
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"
|
||||
info "Setting up systemd service for master_nebuleair..."
|
||||
|
||||
@@ -73,4 +81,42 @@ sudo systemctl enable master_nebuleair.service
|
||||
|
||||
# Start the service immediately
|
||||
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
|
||||
|
||||
@@ -49,6 +49,11 @@ CSV PAYLOAD (AirCarto Servers)
|
||||
17 -> PM 5.0μm to 10μm quantity (Nb/L)
|
||||
18 -> NPM temp 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)
|
||||
Same as NebuleAir wifi
|
||||
@@ -94,6 +99,7 @@ import time
|
||||
import busio
|
||||
import re
|
||||
import os
|
||||
import requests
|
||||
import traceback
|
||||
import threading
|
||||
import sys
|
||||
@@ -115,7 +121,7 @@ if uptime_seconds < 120:
|
||||
sys.exit()
|
||||
|
||||
#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 = {
|
||||
"nebuleairid": "XXX",
|
||||
@@ -159,63 +165,84 @@ def blink_led(pin, blink_count, delay=1):
|
||||
GPIO.output(pin, GPIO.LOW) # Ensure LED is off
|
||||
print(f"LED on GPIO {pin} turned OFF (cleanup avoided)")
|
||||
|
||||
#get data from config
|
||||
def load_config(config_file):
|
||||
#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:
|
||||
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
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error loading config file: {e}")
|
||||
print(f"Error loading config from SQLite: {e}")
|
||||
return {}
|
||||
|
||||
#Fonction pour mettre à jour le JSON de configuration
|
||||
def update_json_key(file_path, key, value):
|
||||
def load_config_scripts_sqlite():
|
||||
"""
|
||||
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.
|
||||
Load script configuration data from SQLite config_scripts_table
|
||||
|
||||
Returns:
|
||||
dict: Script paths as keys and enabled status as boolean values
|
||||
"""
|
||||
try:
|
||||
# Load the existing data
|
||||
with open(file_path, "r") as file:
|
||||
data = json.load(file)
|
||||
# Query the config_scripts_table
|
||||
cursor.execute("SELECT script_path, enabled FROM config_scripts_table")
|
||||
rows = cursor.fetchall()
|
||||
|
||||
# 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
|
||||
# Create config dictionary with script paths as keys and enabled status as boolean values
|
||||
scripts_config = {}
|
||||
for script_path, enabled in rows:
|
||||
# Convert integer enabled value (0/1) to boolean
|
||||
scripts_config[script_path] = bool(enabled)
|
||||
|
||||
# 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
|
||||
return scripts_config
|
||||
|
||||
print(f"💾updating '{key}' to '{value}'.")
|
||||
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
|
||||
config_file = '/var/www/nebuleair_pro_4g/config.json'
|
||||
|
||||
# Load the configuration data
|
||||
config = load_config(config_file)
|
||||
#Load config
|
||||
config = load_config_sqlite()
|
||||
#config
|
||||
device_id = config.get('deviceID', 'unknown')
|
||||
device_id = device_id.upper()
|
||||
modem_config_mode = config.get('modem_config_mode', False)
|
||||
device_latitude_raw = config.get('latitude_raw', 0)
|
||||
device_longitude_raw = config.get('longitude_raw', 0)
|
||||
|
||||
baudrate = config.get('SaraR4_baudrate', 115200) #baudrate du sara R4
|
||||
device_id = config.get('deviceID', '').upper() #device ID en maj
|
||||
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 ()
|
||||
modem_version=config.get('modem_version', "")
|
||||
Sara_baudrate = config.get('SaraR4_baudrate', 115200)
|
||||
npm_5channel = config.get('npm_5channel', False) #5 canaux du NPM
|
||||
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
|
||||
payload_json["nebuleairid"] = device_id
|
||||
@@ -227,7 +254,7 @@ if modem_config_mode:
|
||||
|
||||
ser_sara = serial.Serial(
|
||||
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
|
||||
stopbits=serial.STOPBITS_ONE,
|
||||
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
|
||||
|
||||
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:
|
||||
'''
|
||||
_ ___ ___ ____
|
||||
@@ -288,25 +491,49 @@ try:
|
||||
|
||||
'''
|
||||
print('<h3>START LOOP</h3>')
|
||||
print(f'Modem version: {modem_version}')
|
||||
|
||||
#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")
|
||||
cursor.execute("SELECT * FROM timestamp_table LIMIT 1")
|
||||
row = cursor.fetchone() # Get the first (and only) row
|
||||
rtc_time_str = row[1] # '2025-02-07 12:30:45'
|
||||
# Convert to a datetime object
|
||||
dt_object = datetime.strptime(rtc_time_str, '%Y-%m-%d %H:%M:%S')
|
||||
# Convert to InfluxDB RFC3339 format with UTC 'Z' suffix
|
||||
influx_timestamp = dt_object.strftime('%Y-%m-%dT%H:%M:%SZ')
|
||||
print(influx_timestamp)
|
||||
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
|
||||
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
|
||||
influx_timestamp = dt_object.strftime('%Y-%m-%dT%H:%M:%SZ')
|
||||
rtc_status = "valid"
|
||||
print(influx_timestamp)
|
||||
|
||||
#NEXTPM
|
||||
# We take the last measures (order by rowid and not by timestamp)
|
||||
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 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()
|
||||
# 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
|
||||
num_columns = len(data_values[0])
|
||||
averages = [round(sum(col) / len(col),1) for col in zip(*data_values)]
|
||||
@@ -333,7 +560,7 @@ try:
|
||||
#NextPM 5 channels
|
||||
if npm_5channel:
|
||||
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()
|
||||
# Exclude the timestamp column (assuming first column is timestamp)
|
||||
data_values = [row[1:] for row in rows] # Exclude timestamp
|
||||
@@ -351,7 +578,7 @@ try:
|
||||
#BME280
|
||||
if bme_280_config:
|
||||
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()
|
||||
if last_row:
|
||||
print("SQLite DB last available row:", last_row)
|
||||
@@ -374,7 +601,7 @@ try:
|
||||
#envea
|
||||
if envea_cairsens:
|
||||
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()
|
||||
# Exclude the timestamp column (assuming first column is timestamp)
|
||||
data_values = [row[1:] for row in rows] # Exclude timestamp
|
||||
@@ -393,21 +620,78 @@ try:
|
||||
payload_csv[10] = averages[1] # envea_h2s
|
||||
payload_csv[11] = averages[2] # envea_nh3
|
||||
|
||||
#Add data to payload JSON
|
||||
#Add data to payload JSON
|
||||
payload_json["sensordatavalues"].append({"value_type": "CAIRSENS_NO2", "value": str(averages[0])})
|
||||
payload_json["sensordatavalues"].append({"value_type": "CAIRSENS_NO2", "value": str(averages[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])})
|
||||
|
||||
#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")
|
||||
|
||||
# Getting the LTE Signal
|
||||
print("-> Getting LTE signal <-")
|
||||
print("➡️Getting LTE signal")
|
||||
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(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)
|
||||
if match:
|
||||
signal_quality = int(match.group(1))
|
||||
@@ -425,7 +709,7 @@ try:
|
||||
responseReconnect = read_complete_response(ser_sara, timeout=20, end_of_response_timeout=20)
|
||||
print('<p class="text-danger-emphasis">')
|
||||
print(responseReconnect)
|
||||
print("</p>")
|
||||
print("</p>", end="")
|
||||
|
||||
print('🛑STOP LOOP🛑')
|
||||
print("<hr>")
|
||||
@@ -448,26 +732,35 @@ try:
|
||||
print("Open JSON:")
|
||||
command = f'AT+UDWNFILE="sensordata_csv.json",{size_of_string}\r'
|
||||
ser_sara.write(command.encode('utf-8'))
|
||||
response_SARA_1 = read_complete_response(ser_sara, wait_for_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("</p>", end="")
|
||||
|
||||
time.sleep(1)
|
||||
|
||||
#2. Write to shell
|
||||
print("Write data to memory:")
|
||||
ser_sara.write(csv_string.encode())
|
||||
response_SARA_2 = read_complete_response(ser_sara, wait_for_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("</p>", end="")
|
||||
|
||||
#3. Send to endpoint (with device ID)
|
||||
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'))
|
||||
|
||||
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(response_SARA_3)
|
||||
print("</p>")
|
||||
print("</p>", end="")
|
||||
|
||||
# si on recoit la réponse UHTTPCR
|
||||
if "+UUHTTPCR" in response_SARA_3:
|
||||
@@ -494,8 +787,6 @@ try:
|
||||
print('<span style="color: red;font-weight: bold;">ATTENTION: CME ERROR</span>')
|
||||
print("error:", lines[-1])
|
||||
print("*****")
|
||||
#update status
|
||||
update_json_key(config_file, "SARA_R4_network_status", "disconnected")
|
||||
|
||||
# Gestion de l'erreur spécifique
|
||||
if "No connection to phone" in lines[-1]:
|
||||
@@ -522,18 +813,18 @@ try:
|
||||
led_thread.start()
|
||||
|
||||
else:
|
||||
# 2.Si la réponse contient une réponse HTTP valide
|
||||
# Extract HTTP response code from the last line
|
||||
# ATTENTION: lines[-1] renvoie l'avant dernière ligne et il peut y avoir un soucis avec le OK
|
||||
# rechercher plutot
|
||||
# 2.Si la réponse contient une réponse UUHTTPCR
|
||||
# Extract UUHTTPCR response code from the last line
|
||||
|
||||
http_response = lines[-1] # "+UUHTTPCR: 0,4,0"
|
||||
parts = http_response.split(',')
|
||||
|
||||
# 2.1 code 0 (HTTP failed) ⛔⛔⛔
|
||||
# -> GET error code
|
||||
# -> reboot module
|
||||
if len(parts) == 3 and parts[-1] == '0': # The third value indicates success
|
||||
print("*****")
|
||||
print('<span style="color: red;font-weight: bold;">⛔ATTENTION: HTTP operation failed</span>')
|
||||
update_json_key(config_file, "SARA_R4_network_status", "disconnected")
|
||||
print("*****")
|
||||
print("Blink red LED")
|
||||
# Run LED blinking in a separate thread
|
||||
@@ -541,66 +832,127 @@ try:
|
||||
led_thread.start()
|
||||
|
||||
# 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'
|
||||
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>")
|
||||
print("</p>", end="")
|
||||
|
||||
'''
|
||||
+UHTTPER: profile_id,error_class,error_code
|
||||
# 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 (SARA-R5 need to reset PDP conection)⚠️</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>')
|
||||
|
||||
|
||||
error_class
|
||||
0 OK, no error
|
||||
3 HTTP Protocol error class
|
||||
10 Wrong HTTP API USAGE
|
||||
|
||||
error_code (for error_class 3 and 10)
|
||||
0 No error
|
||||
4 Invalid server Hostname
|
||||
11 Server connection error
|
||||
73 Secure socket connect error
|
||||
'''
|
||||
|
||||
#Essayer un reboot du SARA R4 (ne fonctionne pas)
|
||||
#print("🔄SARA reboot!🔄")
|
||||
#command = f'AT+CFUN=15\r'
|
||||
#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">')
|
||||
#print(response_SARA_9r)
|
||||
#print("</p>")
|
||||
|
||||
#reset l'url
|
||||
print('<span style="color: orange;font-weight: bold;">❓Try Resetting the HTTP Profile❓</span>')
|
||||
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)
|
||||
print('<p class="text-danger-emphasis">')
|
||||
print(responseResetHTTP2_profile)
|
||||
print("</p>")
|
||||
#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")
|
||||
|
||||
|
||||
# 2.2 code 1 (HHTP succeded)
|
||||
# 2.2 code 1 (✅✅HHTP / UUHTTPCR succeded✅✅)
|
||||
else:
|
||||
# Si la commande HTTP a réussi
|
||||
print('<span style="font-weight: bold;">✅✅HTTP operation successful.</span>')
|
||||
update_json_key(config_file, "SARA_R4_network_status", "connected")
|
||||
print("Blink blue LED")
|
||||
led_thread = Thread(target=blink_led, args=(23, 5, 0.5))
|
||||
led_thread.start()
|
||||
|
||||
#4. Read reply from server
|
||||
print("Reply from server:")
|
||||
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)
|
||||
print('<p class="text-success">')
|
||||
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
|
||||
#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:
|
||||
print('<span style="color: red;font-weight: bold;">No UUHTTPCR response</span>')
|
||||
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)
|
||||
print('<p class="text-danger-emphasis">')
|
||||
print(responseReconnect)
|
||||
print("</p>")
|
||||
print("</p>", end="")
|
||||
# Handle "Operation not allowed" error
|
||||
if error_message == "Operation not allowed":
|
||||
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)
|
||||
print('<p class="text-danger-emphasis">')
|
||||
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
|
||||
print("Empty SARA memory:")
|
||||
ser_sara.write(b'AT+UDELFILE="sensordata_csv.json"\r')
|
||||
response_SARA_5 = read_complete_response(ser_sara, wait_for_lines=["OK"], debug=False)
|
||||
print(response_SARA_5)
|
||||
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("</p>", end="")
|
||||
|
||||
if "+CME ERROR" in response_SARA_5:
|
||||
print("⛔ Attention CME ERROR ⛔")
|
||||
|
||||
|
||||
|
||||
|
||||
'''
|
||||
@@ -654,6 +1032,72 @@ try:
|
||||
if send_uSpot:
|
||||
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)
|
||||
print("Open JSON:")
|
||||
payload_string = json.dumps(payload_json) # Convert dict to JSON string
|
||||
@@ -680,7 +1124,7 @@ try:
|
||||
|
||||
print('<p class="text-danger-emphasis">')
|
||||
print(response_SARA_8)
|
||||
print("</p>")
|
||||
print("</p>", end="")
|
||||
|
||||
# si on recoit la réponse UHTTPCR
|
||||
if "+UUHTTPCR" in response_SARA_8:
|
||||
@@ -693,7 +1137,6 @@ try:
|
||||
print("error:", lines[-1])
|
||||
print("*****")
|
||||
#update status
|
||||
#update_json_key(config_file, "SARA_R4_network_status", "disconnected")
|
||||
|
||||
# Gestion de l'erreur spécifique
|
||||
if "No connection to phone" in lines[-1]:
|
||||
@@ -719,7 +1162,6 @@ try:
|
||||
if len(parts) == 3 and parts[-1] == '0': # The third value indicates success
|
||||
print("*****")
|
||||
print('<span style="color: red;font-weight: bold;">⛔ATTENTION: HTTP operation failed</span>')
|
||||
update_json_key(config_file, "SARA_R4_network_status", "disconnected")
|
||||
print("*****")
|
||||
print("Blink red LED")
|
||||
# Run LED blinking in a separate thread
|
||||
@@ -727,28 +1169,31 @@ try:
|
||||
led_thread.start()
|
||||
|
||||
# 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'
|
||||
ser_sara.write(command.encode('utf-8'))
|
||||
response_SARA_9b = read_complete_response(ser_sara, wait_for_lines=["OK"], debug=False)
|
||||
print('<p class="text-danger-emphasis">')
|
||||
print(response_SARA_9b)
|
||||
print("</p>")
|
||||
|
||||
'''
|
||||
+UHTTPER: profile_id,error_class,error_code
|
||||
|
||||
error_class
|
||||
0 OK, no error
|
||||
3 HTTP Protocol error class
|
||||
10 Wrong HTTP API USAGE
|
||||
|
||||
error_code (for error_class 3)
|
||||
0 No error
|
||||
4 Invalid server Hostname
|
||||
11 Server connection error
|
||||
73 Secure socket connect error
|
||||
'''
|
||||
print("</p>", end="")
|
||||
# Extract just the error code
|
||||
error_code = extract_error_code(response_SARA_9b)
|
||||
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>')
|
||||
|
||||
#Pas forcément un moyen de résoudre le soucis
|
||||
|
||||
@@ -756,7 +1201,6 @@ try:
|
||||
else:
|
||||
# Si la commande HTTP a réussi
|
||||
print('<span style="font-weight: bold;">✅✅HTTP operation successful.</span>')
|
||||
update_json_key(config_file, "SARA_R4_network_status", "connected")
|
||||
print("Blink blue LED")
|
||||
led_thread = Thread(target=blink_led, args=(23, 5, 0.5))
|
||||
led_thread.start()
|
||||
@@ -766,7 +1210,7 @@ try:
|
||||
response_SARA_4b = read_complete_response(ser_sara, wait_for_lines=["OK"], debug=False)
|
||||
print('<p class="text-success">')
|
||||
print(response_SARA_4b)
|
||||
print('</p>')
|
||||
print("</p>", end="")
|
||||
|
||||
|
||||
|
||||
|
||||
102
master.py
102
master.py
@@ -52,17 +52,73 @@ Specific scripts can be disabled with config.json
|
||||
import time
|
||||
import threading
|
||||
import subprocess
|
||||
import json
|
||||
import os
|
||||
import sqlite3
|
||||
|
||||
|
||||
# Base directory where scripts are stored
|
||||
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):
|
||||
"""Run a script in a synchronized loop with an optional start delay."""
|
||||
@@ -70,31 +126,41 @@ def run_script(script_name, interval, delay=0):
|
||||
next_run = time.monotonic() + delay # Apply the initial delay
|
||||
|
||||
while True:
|
||||
config = load_config()
|
||||
if config.get(script_name, True): # Default to True if not found
|
||||
subprocess.run(["python3", script_path])
|
||||
if is_script_enabled(script_name):
|
||||
# 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])
|
||||
|
||||
# Wait until the next exact interval
|
||||
next_run += interval
|
||||
sleep_time = max(0, next_run - time.monotonic()) # Prevent negative sleep times
|
||||
time.sleep(sleep_time)
|
||||
|
||||
|
||||
# Define scripts and their execution intervals (seconds)
|
||||
SCRIPTS = [
|
||||
#("RTC/save_to_db.py", 1, 0), # SAVE RTC time every 1 second, no delay
|
||||
("NPM/get_data_modbus_v3.py", 10, 0), # Get NPM data (modbus 5 channels) every 10s, with 2s delay
|
||||
("envea/read_value_v2.py", 10, 0), # Get NPM data (modbus 5 channels) every 10s, with 2s delay
|
||||
("loop/SARA_send_data_v2.py", 60, 1), # Send data every 60 seconds, with 2s delay
|
||||
("BME280/get_data_v2.py", 120, 0), # Get BME280 data every 120 seconds, no delay
|
||||
("sqlite/flush_old_data.py", 86400, 0) # flush old data inside db every day ()
|
||||
# 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
|
||||
("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 1s delay
|
||||
("BME280/get_data_v2.py", 120, 0), # Get BME280 data every 120 seconds
|
||||
("MPPT/read.py", 120, 0), # Get MPPT data every 120 seconds
|
||||
("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:
|
||||
thread = threading.Thread(target=run_script, args=(script_name, interval, delay), daemon=True)
|
||||
thread.start()
|
||||
|
||||
# Keep the main script running
|
||||
while True:
|
||||
time.sleep(1)
|
||||
|
||||
time.sleep(1)
|
||||
5
config.json.dist → old/config.json.dist
Executable file → Normal file
5
config.json.dist → old/config.json.dist
Executable file → Normal file
@@ -5,8 +5,11 @@
|
||||
"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,
|
||||
"deviceID": "XXXX",
|
||||
"npm_5channel": false,
|
||||
"latitude_raw": 0,
|
||||
"longitude_raw":0,
|
||||
"latitude_precision": 0,
|
||||
@@ -25,7 +28,7 @@
|
||||
"SARA_R4_general_status": "connected",
|
||||
"SARA_R4_SIM_status": "connected",
|
||||
"SARA_R4_network_status": "connected",
|
||||
"SARA_R4_neworkID": 0,
|
||||
"SARA_R4_neworkID": 20810,
|
||||
"WIFI_status": "connected",
|
||||
"MQTT_GUI": false,
|
||||
"send_aircarto": true,
|
||||
0
install_software.yaml → old/install_software.yaml
Executable file → Normal file
0
install_software.yaml → old/install_software.yaml
Executable file → Normal file
@@ -18,6 +18,35 @@ import sqlite3
|
||||
conn = sqlite3.connect("/var/www/nebuleair_pro_4g/sqlite/sensors.db")
|
||||
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
|
||||
cursor.execute("""
|
||||
CREATE TABLE IF NOT EXISTS timestamp_table (
|
||||
@@ -30,7 +59,14 @@ cursor.execute("""
|
||||
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
|
||||
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
|
||||
|
||||
@@ -14,6 +14,8 @@ data_NPM_5channels
|
||||
data_BME280
|
||||
data_envea
|
||||
timestamp_table
|
||||
data_MPPT
|
||||
data_WIND
|
||||
|
||||
'''
|
||||
|
||||
@@ -34,7 +36,6 @@ cursor = conn.cursor()
|
||||
#cursor.execute("SELECT * FROM timestamp_table")
|
||||
if table_name == "timestamp_table":
|
||||
cursor.execute("SELECT * FROM timestamp_table")
|
||||
|
||||
else:
|
||||
query = f"SELECT * FROM {table_name} ORDER BY timestamp DESC LIMIT ?"
|
||||
cursor.execute(query, (limit_num,))
|
||||
|
||||
43
sqlite/read_config.py
Normal file
43
sqlite/read_config.py
Normal 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
95
sqlite/set_config.py
Normal 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
27
windMeter/ads115.py
Normal 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
108
windMeter/read.py
Normal 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()
|
||||
84
windMeter/read_wind_direction.py
Normal file
84
windMeter/read_wind_direction.py
Normal 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")
|
||||
67
windMeter/read_wind_speed.py
Normal file
67
windMeter/read_wind_speed.py
Normal 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")
|
||||
Reference in New Issue
Block a user