52 Commits

Author SHA1 Message Date
Your Name
020594e065 updates 2025-05-23 15:09:22 +02:00
PaulVua
5a1a4e0d81 updates 2025-05-23 14:38:32 +02:00
PaulVua
3cd5b13c25 updates 2025-05-23 14:31:23 +02:00
PaulVua
5a0f1c0745 updates 2025-05-23 14:30:18 +02:00
PaulVua
2516a3bd1c updates 2025-05-23 14:08:21 +02:00
PaulVua
1b8dc54fe0 updates 2025-05-23 14:03:57 +02:00
PaulVua
2bd74ca91a updates 2025-05-23 11:02:06 +02:00
PaulVua
f40c105abf updates 2025-05-23 10:48:41 +02:00
Your Name
fdef8e2df0 update 2025-05-20 11:57:55 +02:00
Your Name
386ad6fb03 update 2025-05-20 11:24:38 +02:00
Your Name
a7c138e93f update 2025-05-20 11:23:43 +02:00
Your Name
4e4832b128 update 2025-05-20 10:43:27 +02:00
Your Name
11463b175c update 2025-05-20 09:50:15 +02:00
Your Name
c06741b11d Merge remote-tracking branch 'refs/remotes/origin/main' 2025-05-20 09:46:46 +02:00
Your Name
b1352261e7 update 2025-05-20 09:45:59 +02:00
Your Name
376ff454bf update 2025-05-20 09:33:14 +02:00
Your Name
932fdf83a2 update 2025-05-20 09:21:34 +02:00
Your Name
1ca3e2ada2 update 2025-05-20 09:14:25 +02:00
Your Name
fd1d32a62b udpate 2025-05-19 10:26:27 +02:00
Your Name
61b302fe35 update 2025-05-16 11:08:23 +02:00
Your Name
2aaa229e82 update 2025-05-14 17:37:53 +02:00
Your Name
fd28069b0c update 2025-05-14 17:24:01 +02:00
Your Name
b17c996f2f update 2025-05-13 17:14:29 +02:00
Your Name
8273307cab update 2025-05-07 18:48:47 +02:00
Your Name
a73eb30d32 update 2025-05-07 18:25:30 +02:00
Your Name
ba889feee9 update 2025-05-06 17:30:14 +02:00
Your Name
12c7a0b6af update 2025-05-01 11:01:29 +02:00
Your Name
08c5ed8841 update 2025-04-16 09:53:54 +02:00
Your Name
7f5eb7608c update 2025-04-16 09:46:12 +02:00
Your Name
44f44c3361 update 2025-04-07 15:57:58 +02:00
Your Name
a8350332ac update 2025-04-07 11:47:21 +02:00
Your Name
6c6eed1ad6 update 2025-04-03 10:49:55 +02:00
Your Name
ee71c28d33 update 2025-04-03 09:30:54 +02:00
Your Name
6d3220665e update 2025-04-03 09:07:20 +02:00
Your Name
98e5a239f5 update 2025-04-02 16:10:27 +02:00
Your Name
17f4ce46dd update 2025-04-02 15:09:10 +02:00
Your Name
338b8a049f update 2025-04-02 14:43:18 +02:00
Your Name
1e9e80ae55 update 2025-04-01 17:35:35 +02:00
Your Name
9d280c6e37 update 2025-04-01 11:57:39 +02:00
Your Name
d4c1178b3d update 2025-03-28 14:29:51 +01:00
Your Name
f7f6fccd60 update 2025-03-27 17:13:40 +01:00
Your Name
afceb34c1b update 2025-03-27 16:43:55 +01:00
Your Name
7a958d5c8e update 2025-03-27 16:40:24 +01:00
Your Name
8fd76001f2 update 2025-03-27 16:02:56 +01:00
Your Name
e320a3bc2b update 2025-03-27 16:02:05 +01:00
Your Name
8a4e184699 update 2025-03-27 15:58:22 +01:00
Your Name
e61b0a76da update 2025-03-27 15:50:59 +01:00
Your Name
970a36598c update 2025-03-26 10:30:24 +01:00
Your Name
e75caff929 update 2025-03-26 08:29:24 +01:00
Your Name
e82d75a4d6 update 2025-03-26 08:27:28 +01:00
Your Name
dc27e5f139 update 2025-03-26 08:25:42 +01:00
Your Name
4bc05091be update 2025-03-25 21:18:12 +01:00
45 changed files with 2505 additions and 1015 deletions

View File

@@ -12,9 +12,9 @@ GPIO 20 -> SARA PWR ON
option 1: option 1:
CLI tool like pinctrl CLI tool like pinctrl
pinctrl set 17 op pinctrl set 16 op
pinctrl set 17 dh pinctrl set 16 dh
pinctrl set 17 dl pinctrl set 16 dl
option 2: option 2:
python library RPI.GPIO python library RPI.GPIO

View File

@@ -36,9 +36,11 @@ sudo ssh-copy-id -i /var/www/.ssh/id_rsa.pub -p 50221 airlab_server1@aircarto.fr
sudo git clone http://gitea.aircarto.fr/PaulVua/nebuleair_pro_4g.git /var/www/nebuleair_pro_4g sudo git clone http://gitea.aircarto.fr/PaulVua/nebuleair_pro_4g.git /var/www/nebuleair_pro_4g
sudo mkdir /var/www/nebuleair_pro_4g/logs sudo mkdir /var/www/nebuleair_pro_4g/logs
sudo touch /var/www/nebuleair_pro_4g/logs/app.log /var/www/nebuleair_pro_4g/logs/loop.log /var/www/nebuleair_pro_4g/wifi_list.csv sudo touch /var/www/nebuleair_pro_4g/logs/app.log /var/www/nebuleair_pro_4g/logs/loop.log /var/www/nebuleair_pro_4g/wifi_list.csv
sudo cp /var/www/nebuleair_pro_4g/config.json.dist /var/www/nebuleair_pro_4g/config.json /usr/bin/python3 /var/www/nebuleair_pro_4g/sqlite/create_db.py
/usr/bin/python3 /var/www/nebuleair_pro_4g/sqlite/set_config.py
sudo chmod -R 777 /var/www/nebuleair_pro_4g/ sudo chmod -R 777 /var/www/nebuleair_pro_4g/
git config --global core.fileMode false git config --global core.fileMode false
git -C /var/www/nebuleair_pro_4g config core.fileMode false
git config --global --add safe.directory /var/www/nebuleair_pro_4g git config --global --add safe.directory /var/www/nebuleair_pro_4g
sudo crontab /var/www/nebuleair_pro_4g/cron_jobs sudo crontab /var/www/nebuleair_pro_4g/cron_jobs
sudo /usr/bin/python3 /var/www/nebuleair_pro_4g/sqlite/create_db.py sudo /usr/bin/python3 /var/www/nebuleair_pro_4g/sqlite/create_db.py
@@ -57,6 +59,8 @@ ALL ALL=(ALL) NOPASSWD: /usr/bin/nmcli, /usr/sbin/reboot
www-data ALL=(ALL) NOPASSWD: /usr/bin/git pull www-data ALL=(ALL) NOPASSWD: /usr/bin/git pull
www-data ALL=(ALL) NOPASSWD: /usr/bin/ssh www-data ALL=(ALL) NOPASSWD: /usr/bin/ssh
www-data ALL=(ALL) NOPASSWD: /usr/bin/python3 * www-data ALL=(ALL) NOPASSWD: /usr/bin/python3 *
www-data ALL=(ALL) NOPASSWD: /bin/systemctl *
www-data ALL=(ALL) NOPASSWD: /var/www/nebuleair_pro_4g/*
``` ```
## Serial ## Serial

View File

@@ -25,23 +25,8 @@ url = parameter[1] # ex: data.mobileair.fr
profile_id = 3 profile_id = 3
#get baudrate baudrate = 115200
def load_config(config_file): send_uSpot = False
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)
send_uSpot = config.get('send_uSpot', False)
def read_complete_response(serial_connection, timeout=2, end_of_response_timeout=2, wait_for_line=None): def read_complete_response(serial_connection, timeout=2, end_of_response_timeout=2, wait_for_line=None):
response = bytearray() response = bytearray()

View File

@@ -26,23 +26,8 @@ url = parameter[1] # ex: data.mobileair.fr
profile_id = 3 profile_id = 3
#get baudrate baudrate = 115200
def load_config(config_file): send_uSpot = False
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)
send_uSpot = config.get('send_uSpot', False)
def read_complete_response(serial_connection, timeout=2, end_of_response_timeout=2): def read_complete_response(serial_connection, timeout=2, end_of_response_timeout=2):
response = bytearray() response = bytearray()

View File

@@ -28,23 +28,8 @@ url = parameter[1] # ex: data.mobileair.fr
endpoint = parameter[2] endpoint = parameter[2]
profile_id = 2 profile_id = 2
#get baudrate baudrate = 115200
def load_config(config_file): send_uSpot = False
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)
send_uSpot = config.get('send_uSpot', False)
def color_text(text, color): def color_text(text, color):
colors = { colors = {

View File

@@ -31,23 +31,8 @@ endpoint = parameter[2]
profile_id = 3 profile_id = 3
#get baudrate baudrate = 115200
def load_config(config_file): send_uSpot = False
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)
send_uSpot = config.get('send_uSpot', False)
def read_complete_response(serial_connection, timeout=2, end_of_response_timeout=2): def read_complete_response(serial_connection, timeout=2, end_of_response_timeout=2):
response = bytearray() response = bytearray()

View File

@@ -21,23 +21,8 @@ port='/dev/'+parameter[0] # ex: ttyAMA2
url = parameter[1] # ex: data.mobileair.fr url = parameter[1] # ex: data.mobileair.fr
#get baudrate baudrate = 115200
def load_config(config_file): send_uSpot = False
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)
send_uSpot = config.get('send_uSpot', False)
def read_complete_response(serial_connection, timeout=2, end_of_response_timeout=2): def read_complete_response(serial_connection, timeout=2, end_of_response_timeout=2):
response = bytearray() response = bytearray()

View File

@@ -23,24 +23,8 @@ parameter = sys.argv[1:] # Exclude the script name
port='/dev/'+parameter[0] # ex: ttyAMA2 port='/dev/'+parameter[0] # ex: ttyAMA2
url = parameter[1] # ex: data.mobileair.fr url = parameter[1] # ex: data.mobileair.fr
baudrate = 115200
#get baudrate send_uSpot = False
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)
send_uSpot = config.get('send_uSpot', False)
def read_complete_response(serial_connection, timeout=2, end_of_response_timeout=2): def read_complete_response(serial_connection, timeout=2, end_of_response_timeout=2):
response = bytearray() response = bytearray()

View File

@@ -14,19 +14,7 @@ parameter = sys.argv[1:] # Exclude the script name
port = '/dev/' + parameter[0] # e.g., ttyAMA2 port = '/dev/' + parameter[0] # e.g., ttyAMA2
timeout = float(parameter[1]) # e.g., 2 seconds timeout = float(parameter[1]) # e.g., 2 seconds
def load_config(config_file): baudrate = 115200
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'
config = load_config(config_file)
baudrate = config.get('SaraR4_baudrate', 115200)
def read_complete_response(serial_connection, timeout=2, end_of_response_timeout=2): def read_complete_response(serial_connection, timeout=2, end_of_response_timeout=2):
response = bytearray() response = bytearray()

View File

@@ -13,12 +13,20 @@ Script that starts at the boot of the RPI (with cron)
''' '''
import serial import serial
import RPi.GPIO as GPIO
import time import time
import sys import sys
import json import json
import re import re
import sqlite3 import sqlite3
#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
# database connection # database connection
conn = sqlite3.connect("/var/www/nebuleair_pro_4g/sqlite/sensors.db") conn = sqlite3.connect("/var/www/nebuleair_pro_4g/sqlite/sensors.db")
cursor = conn.cursor() cursor = conn.cursor()
@@ -56,36 +64,6 @@ def load_config_sqlite():
print(f"Error loading config from SQLite: {e}") print(f"Error loading config from SQLite: {e}")
return {} 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}")
def update_sqlite_config(key, value): def update_sqlite_config(key, value):
""" """
Updates a specific key in the SQLite config_table with a new value. Updates a specific key in the SQLite config_table with a new value.
@@ -195,6 +173,10 @@ def read_complete_response(serial_connection, timeout=2, end_of_response_timeout
try: try:
print('<h3>Start reboot python script</h3>') print('<h3>Start reboot python script</h3>')
#First we need to power on the module (if connected to mosfet via gpio16)
GPIO.output(SARA_power_GPIO, GPIO.HIGH)
time.sleep(5)
#check modem status #check modem status
#Attention: #Attention:
# SARA R4 response: Manufacturer: u-blox Model: SARA-R410M-02B # SARA R4 response: Manufacturer: u-blox Model: SARA-R410M-02B
@@ -319,7 +301,7 @@ try:
print(response_SARA_5cf) print(response_SARA_5cf)
time.sleep(0.5) time.sleep(0.5)
#step 4: set url (op_code = 1) #step 4: set url (op_code = 1)
print("SET URL") print("SET URL")
command = f'AT+UHTTP={uSpot_profile_id},1,"{uSpot_url}"\r' command = f'AT+UHTTP={uSpot_profile_id},1,"{uSpot_url}"\r'
ser_sara.write((command + '\r').encode('utf-8')) ser_sara.write((command + '\r').encode('utf-8'))

View File

@@ -32,23 +32,8 @@ port='/dev/'+parameter[0] # ex: ttyAMA2
command = parameter[1] # ex: AT+CCID? command = parameter[1] # ex: AT+CCID?
timeout = float(parameter[2]) # ex:2 timeout = float(parameter[2]) # 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 # Access the shared variables
baudrate = config.get('SaraR4_baudrate', 115200) baudrate = 115200
try: try:

View File

@@ -22,23 +22,7 @@ parameter = sys.argv[1:] # Exclude the script name
port='/dev/'+parameter[0] # ex: ttyAMA2 port='/dev/'+parameter[0] # ex: ttyAMA2
url = parameter[1] # ex: data.mobileair.fr url = parameter[1] # ex: data.mobileair.fr
baudrate = 115200
#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( ser = serial.Serial(
port=port, #USB0 or ttyS0 port=port, #USB0 or ttyS0

View File

@@ -26,15 +26,7 @@ networkID = parameter[1] # ex: 20801
timeout = float(parameter[2]) # ex:2 timeout = float(parameter[2]) # 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 {}
def read_complete_response(serial_connection, timeout=2, end_of_response_timeout=2, wait_for_lines=None, debug=True): def read_complete_response(serial_connection, timeout=2, end_of_response_timeout=2, wait_for_lines=None, debug=True):
''' '''
@@ -81,12 +73,7 @@ def read_complete_response(serial_connection, timeout=2, end_of_response_timeout
return response.decode('utf-8', errors='replace') # Return the full response if no target line is found return response.decode('utf-8', errors='replace') # Return the full response if no target line is found
# Define the config file path baudrate = 115200
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( ser = serial.Serial(
port=port, #USB0 or ttyS0 port=port, #USB0 or ttyS0

View File

@@ -11,22 +11,7 @@ parameter = sys.argv[1:] # Exclude the script name
port='/dev/'+parameter[0] # ex: ttyAMA2 port='/dev/'+parameter[0] # ex: ttyAMA2
message = parameter[1] # ex: Hello message = parameter[1] # ex: Hello
#get baudrate baudrate = 115200
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( ser = serial.Serial(
port=port, #USB0 or ttyS0 port=port, #USB0 or ttyS0

View File

@@ -18,24 +18,7 @@ import sys
import json import json
baudrate = 115200
#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( ser = serial.Serial(
port='/dev/ttyAMA2', port='/dev/ttyAMA2',

View File

@@ -17,23 +17,7 @@ import json
# SARA R4 UHTTPC profile IDs # SARA R4 UHTTPC profile IDs
aircarto_profile_id = 0 aircarto_profile_id = 0
baudrate = 115200
#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_sara = serial.Serial( ser_sara = serial.Serial(
port='/dev/ttyAMA2', port='/dev/ttyAMA2',

View File

@@ -11,22 +11,7 @@ parameter = sys.argv[1:] # Exclude the script name
port='/dev/'+parameter[0] # ex: ttyAMA2 port='/dev/'+parameter[0] # ex: ttyAMA2
message = parameter[1] # ex: Hello message = parameter[1] # ex: Hello
#get baudrate baudrate = 115200
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( ser = serial.Serial(
port=port, #USB0 or ttyS0 port=port, #USB0 or ttyS0

View File

@@ -12,22 +12,7 @@ port='/dev/'+parameter[0] # ex: ttyAMA2
endpoint = parameter[1] # ex: /pro_4G/notif_message.php endpoint = parameter[1] # ex: /pro_4G/notif_message.php
profile_id = parameter[2] profile_id = parameter[2]
#get baudrate baudrate = 115200
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( ser = serial.Serial(
port=port, #USB0 or ttyS0 port=port, #USB0 or ttyS0

View File

@@ -21,23 +21,7 @@ port='/dev/'+parameter[0] # ex: ttyAMA2
apn_address = parameter[1] # ex: data.mono apn_address = parameter[1] # ex: data.mono
timeout = float(parameter[2]) # ex:2 timeout = float(parameter[2]) # ex:2
baudrate = 115200
#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( ser = serial.Serial(
port=port, #USB0 or ttyS0 port=port, #USB0 or ttyS0

View File

@@ -27,22 +27,7 @@ url = parameter[1] # ex: data.mobileair.fr
profile_id = parameter[2] #ex: 0 profile_id = parameter[2] #ex: 0
#get baudrate baudrate = 115200
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( ser = serial.Serial(
port=port, #USB0 or ttyS0 port=port, #USB0 or ttyS0

View File

@@ -40,22 +40,7 @@ def read_complete_response(serial_connection, timeout=2, end_of_response_timeout
return response.decode('utf-8', errors='replace') return response.decode('utf-8', errors='replace')
#get baudrate baudrate = 115200
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_sara = serial.Serial( ser_sara = serial.Serial(
port=port, #USB0 or ttyS0 port=port, #USB0 or ttyS0

View File

@@ -12,21 +12,7 @@ port='/dev/'+parameter[0] # ex: ttyAMA2
message = parameter[1] # ex: Hello message = parameter[1] # ex: Hello
#get baudrate #get baudrate
def load_config(config_file): baudrate = 115200
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( ser = serial.Serial(
port=port, #USB0 or ttyS0 port=port, #USB0 or ttyS0

View File

@@ -6,7 +6,6 @@
# @reboot /var/www/nebuleair_pro_4g/boot_hotspot.sh >> /var/www/nebuleair_pro_4g/logs/app.log 2>&1 # @reboot /var/www/nebuleair_pro_4g/boot_hotspot.sh >> /var/www/nebuleair_pro_4g/logs/app.log 2>&1
OUTPUT_FILE="/var/www/nebuleair_pro_4g/wifi_list.csv" OUTPUT_FILE="/var/www/nebuleair_pro_4g/wifi_list.csv"
JSON_FILE="/var/www/nebuleair_pro_4g/config.json"
echo "-------------------" echo "-------------------"
@@ -70,8 +69,6 @@ else
# Update SQLite to reflect hotspot mode # 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'" sqlite3 /var/www/nebuleair_pro_4g/sqlite/sensors.db "UPDATE config_table SET value='connected' WHERE key='WIFI_status'"
sudo chmod 777 "$JSON_FILE"
# Lancer le tunnel SSH # Lancer le tunnel SSH
#echo "Démarrage du tunnel SSH sur le port $SSH_TUNNEL_PORT..." #echo "Démarrage du tunnel SSH sur le port $SSH_TUNNEL_PORT..."
# Start the SSH agent if it's not already running # Start the SSH agent if it's not already running

View File

@@ -4,7 +4,10 @@
@reboot sleep 30 && /usr/bin/python3 /var/www/nebuleair_pro_4g/SARA/reboot/start.py >> /var/www/nebuleair_pro_4g/logs/app.log 2>&1 @reboot sleep 30 && /usr/bin/python3 /var/www/nebuleair_pro_4g/SARA/reboot/start.py >> /var/www/nebuleair_pro_4g/logs/app.log 2>&1
0 0 * * * > /var/www/nebuleair_pro_4g/logs/master.log #0 0 * * * > /var/www/nebuleair_pro_4g/logs/master.log
0 0 * * * > /var/www/nebuleair_pro_4g/logs/master_errors.log #0 0 * * * > /var/www/nebuleair_pro_4g/logs/master_errors.log
0 0 * * * > /var/www/nebuleair_pro_4g/logs/app.log #0 0 * * * > /var/www/nebuleair_pro_4g/logs/app.log
0 0 * * * find /var/www/nebuleair_pro_4g/logs -name "*.log" -type f -exec truncate -s 0 {} \;

View File

@@ -69,47 +69,52 @@
<input type="text" class="form-control" id="device_ID" disabled> <input type="text" class="form-control" id="device_ID" disabled>
</div> </div>
<!-- config_scripts_table --> <div class="mb-3">
<label for="modem_version" class="form-label">Modem Version</label>
<div class="form-check mb-3"> <input type="text" class="form-control" id="modem_version" disabled>
<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>
<!-- config_scripts_table -->
<div class="form-check mb-3"> <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)"> <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"> <label class="form-check-label" for="check_NPM_5channels">
Next PM send 5 channels Send Next PM 5 channels data
</label> </label>
</div> </div>
<div class="form-check mb-3"> <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)"> <input class="form-check-input" type="checkbox" value="" id="check_bme280" onchange="update_config_sqlite('BME280', this.checked)">
<label class="form-check-label" for="check_bme280"> <label class="form-check-label" for="check_bme280">
Sonde temp/hum (BME280) Send temp/hum data (BME280)
</label> </label>
</div> </div>
<div class="form-check mb-3"> <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)"> <input class="form-check-input" type="checkbox" value="" id="check_envea" onchange="update_config_sqlite('envea', this.checked)">
<label class="form-check-label" for="check_envea"> <label class="form-check-label" for="check_envea">
Sonde Envea Send Envea sensor data
</label> </label>
</div> </div>
<div class="form-check mb-3"> <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)"> <input class="form-check-input" type="checkbox" value="" id="check_solarBattery" onchange="update_config_sqlite('MPPT', this.checked)">
<label class="form-check-label" for="check_solarBattery"> <label class="form-check-label" for="check_solarBattery">
Solar / Battery MPPT Send Solar / Battery MPPT data
</label> </label>
</div> </div>
<div class="form-check mb-3"> <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)"> <input class="form-check-input" type="checkbox" value="" id="check_WindMeter" onchange="update_config_sqlite('windMeter', this.checked)">
<label class="form-check-label" for="check_WindMeter"> <label class="form-check-label" for="check_WindMeter">
Wind Meter Send Wind Meter data
</label>
</div>
<div class="form-check mb-3">
<input class="form-check-input" type="checkbox" value="" id="check_uSpot" onchange="update_config_sqlite('send_uSpot', this.checked)" disabled>
<label class="form-check-label" for="check_uSpot">
Send to uSpot
</label> </label>
</div> </div>
@@ -160,12 +165,77 @@
<div class="col-lg-4 col-12"> <div class="col-lg-4 col-12">
<h3 class="mt-4">Updates</h3> <h3 class="mt-4">Updates</h3>
<button type="submit" class="btn btn-primary" onclick="updateGitPull()">Update firmware</button> <button type="submit" class="btn btn-primary" onclick="updateFirmware()" id="updateBtn">
<span id="updateBtnText">Update firmware</span>
<span id="updateSpinner" class="spinner-border spinner-border-sm ms-2" style="display: none;"></span>
</button>
<!-- Update Output Console -->
<div id="updateOutput" class="mt-3" style="display: none;">
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<span class="fw-bold">Update Log</span>
<div>
<button type="button" class="btn btn-sm btn-success me-2" onclick="location.reload()" id="reloadBtn" style="display: none;">
<svg width="14" height="14" fill="currentColor" class="bi bi-arrow-clockwise me-1" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M8 3a5 5 0 1 0 4.546 2.914.5.5 0 0 1 .908-.417A6 6 0 1 1 8 2v1z"/>
<path d="M8 4.466V.534a.25.25 0 0 1 .41-.192l2.36 1.966c.12.1.12.284 0 .384L8.41 4.658A.25.25 0 0 1 8 4.466z"/>
</svg>
Reload Page
</button>
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="clearUpdateOutput()">
Clear
</button>
</div>
</div>
<div class="card-body">
<pre id="updateOutputContent" class="mb-0" style="max-height: 400px; overflow-y: auto; font-size: 0.85rem; background-color: #f8f9fa; padding: 1rem; border-radius: 0.375rem;"></pre>
</div>
</div>
</div>
</div> </div>
</div> </div>
<!-- SYSTEMD SERVICES SECTION -->
<div class="row mb-3">
<div class="col-lg-8 col-12">
<h4 class="mt-4">SystemD Services</h4>
<div id="services-table" class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<span class="fw-bold">Service Status</span>
<button type="button" class="btn btn-sm btn-outline-primary" onclick="refreshServices()">
<svg width="14" height="14" fill="currentColor" class="bi bi-arrow-clockwise" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M8 3a5 5 0 1 0 4.546 2.914.5.5 0 0 1 .908-.417A6 6 0 1 1 8 2v1z"/>
<path d="M8 4.466V.534a.25.25 0 0 1 .41-.192l2.36 1.966c.12.1.12.284 0 .384L8.41 4.658A.25.25 0 0 1 8 4.466z"/>
</svg>
Refresh
</button>
</div>
<div class="card-body p-0">
<table class="table table-sm table-hover mb-0">
<thead class="table-light">
<tr>
<th style="width: 20%">Service</th>
<th style="width: 25%">Description</th>
<th style="width: 15%">Frequency</th>
<th style="width: 10%">Status</th>
<th style="width: 10%">Enabled</th>
<th style="width: 20%">Actions</th>
</tr>
</thead>
<tbody id="services-tbody">
<tr>
<td colspan="6" class="text-center py-3">Loading services...</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
<!-- toast --> <!-- toast -->
@@ -244,48 +314,34 @@ window.onload = function() {
elements.forEach((element) => { elements.forEach((element) => {
element.innerText = response.deviceName; element.innerText = response.deviceName;
}); });
//device name html page title
if (response.deviceName) {
document.title = response.deviceName;
}
//device ID //device ID
const deviceID = response.deviceID.trim().toUpperCase(); const deviceID = response.deviceID.trim().toUpperCase();
const device_ID = document.getElementById("device_ID"); const device_ID = document.getElementById("device_ID");
device_ID.value = response.deviceID.toUpperCase(); device_ID.value = response.deviceID.toUpperCase();
//nextPM send 5 channels //modem_version
const modem_version = document.getElementById("modem_version");
modem_version.value = response.modem_version;
const checkbox_nmp5channels = document.getElementById("check_NPM_5channels"); const checkbox_nmp5channels = document.getElementById("check_NPM_5channels");
checkbox_nmp5channels.checked = response.npm_5channel; const checkbox_wind = document.getElementById("check_WindMeter");
const checkbox_uSpot = document.getElementById("check_uSpot");
},
error: function(xhr, status, error) {
console.error('AJAX request failed:', status, error);
}
});//end AJAX
//getting config_scripts table
$.ajax({
url: 'launcher.php?type=get_config_scripts_sqlite',
dataType:'json',
//dataType: 'json', // Specify that you expect a JSON response
method: 'GET', // Use GET or POST depending on your needs
success: function(response) {
console.log("Getting SQLite config scripts table:");
console.log(response);
const checkbox_NPM = document.getElementById("check_NPM");
const checkbox_bme = document.getElementById("check_bme280"); const checkbox_bme = document.getElementById("check_bme280");
const checkbox_envea = document.getElementById("check_envea"); const checkbox_envea = document.getElementById("check_envea");
const checkbox_solar = document.getElementById("check_solarBattery"); const checkbox_solar = document.getElementById("check_solarBattery");
const checkbox_wind = document.getElementById("check_WindMeter");
checkbox_NPM.checked = response["NPM/get_data_modbus_v3.py"]; checkbox_bme.checked = response["BME280"];
checkbox_bme.checked = response["BME280/get_data_v2.py"]; checkbox_envea.checked = response["envea"];
checkbox_envea.checked = response["envea/read_value_v2.py"]; checkbox_solar.checked = response["MPPT"];
checkbox_solar.checked = response["MPPT/read.py"]; checkbox_nmp5channels.checked = response.npm_5channel;
checkbox_wind.checked = response["windMeter/read.py"]; checkbox_wind.checked = response["windMeter"];
checkbox_uSpot.checked = response["send_uSpot"];
//si sonde envea is true
if (response["envea/read_value_v2.py"]) {
add_sondeEnveaContainer();
}
}, },
error: function(xhr, status, error) { error: function(xhr, status, error) {
console.error('AJAX request failed:', status, error); console.error('AJAX request failed:', status, error);
@@ -293,6 +349,8 @@ window.onload = function() {
});//end AJAX });//end AJAX
//OLD way to get config (JSON) //OLD way to get config (JSON)
/* /*
fetch('../config.json') // Replace 'deviceID.txt' with 'config.json' fetch('../config.json') // Replace 'deviceID.txt' with 'config.json'
@@ -385,6 +443,8 @@ window.onload = function() {
} }
}); //end AJAx }); //end AJAx
// Load services on page load
refreshServices();
} //end window.onload } //end window.onload
@@ -442,62 +502,6 @@ function update_config_sqlite(param, value){
}); });
} }
function update_config_scripts_sqlite(param, value) {
console.log("Updating scripts sqlite ", param, " : ", value);
const toastLiveExample = document.getElementById('liveToast')
const toastBody = toastLiveExample.querySelector('.toast-body');
$.ajax({
url: 'launcher.php?type=update_config_scripts_sqlite&param=' + param + '&value=' + value,
dataType: 'json',
method: 'GET',
cache: false,
success: function(response) {
console.log(response);
// Format the response nicely
let formattedMessage = '';
if (response.success) {
// Success message
toastLiveExample.classList.remove('text-bg-danger');
toastLiveExample.classList.add('text-bg-success');
formattedMessage = `
<strong>Success!</strong><br>
Parameter: ${response.script_path || param}<br>
Value: ${response.enabled !== undefined ? response.enabled : value}<br>
${response.message || ''}
`;
if (response.script_path == "envea/read_value_v2.py") {
console.log("envea sondes activated");
add_sondeEnveaContainer();
}
} else {
// Error message
toastLiveExample.classList.remove('text-bg-success');
toastLiveExample.classList.add('text-bg-danger');
formattedMessage = `
<strong>Error!</strong><br>
${response.error || 'Unknown error'}<br>
Parameter: ${response.script_path || param}
`;
}
// Update the toast body with formatted content
toastBody.innerHTML = formattedMessage;
// Show the toast
const toastBootstrap = bootstrap.Toast.getOrCreateInstance(toastLiveExample)
toastBootstrap.show()
},
error: function(xhr, status, error) {
console.error('AJAX request failed:', status, error);
}
});
}
function update_config(param, value){ function update_config(param, value){
console.log("Updating ",param," : ", value); console.log("Updating ",param," : ", value);
@@ -515,28 +519,112 @@ function update_config(param, value){
}); });
} }
function updateGitPull(){ function updateFirmware() {
console.log("Updating device (git pull)"); console.log("Starting comprehensive firmware update...");
// Show loading state
const updateBtn = document.getElementById('updateBtn');
const updateBtnText = document.getElementById('updateBtnText');
const updateSpinner = document.getElementById('updateSpinner');
const updateOutput = document.getElementById('updateOutput');
const updateOutputContent = document.getElementById('updateOutputContent');
// Disable button and show spinner
updateBtn.disabled = true;
updateBtnText.textContent = 'Updating...';
updateSpinner.style.display = 'inline-block';
// Show output console
updateOutput.style.display = 'block';
updateOutputContent.textContent = 'Starting update process...\n';
$.ajax({ $.ajax({
url: 'launcher.php?type=git_pull', url: 'launcher.php?type=update_firmware',
method: 'GET', // Use GET or POST depending on your needs method: 'GET',
dataType: 'text', // Specify that you expect a JSON response dataType: 'json',
timeout: 120000, // 2 minutes timeout
success: function(response) { success: function(response) {
// Handle success response if needed console.log('Update completed:', response);
console.log(response);
alert(response);
// Reload the page after the device update
location.reload(); // This will reload the page
// Display formatted output
if (response.success && response.output) {
// Format the output for better readability
const formattedOutput = response.output
.replace(/\[\d{2}:\d{2}:\d{2}\]/g, function(match) {
return `<span style="color: #007bff; font-weight: bold;">${match}</span>`;
})
.replace(/✓/g, '<span style="color: #28a745;">✓</span>')
.replace(/✗/g, '<span style="color: #dc3545;">✗</span>')
.replace(/⚠/g, '<span style="color: #ffc107;">⚠</span>')
.replace(//g, '<span style="color: #17a2b8;"></span>');
updateOutputContent.innerHTML = formattedOutput;
// Show success toast and reload button
showToast('Update completed successfully!', 'success');
document.getElementById('reloadBtn').style.display = 'inline-block';
} else {
updateOutputContent.textContent = 'Update completed but no output received.';
showToast('Update may have completed with issues', 'warning');
}
}, },
error: function(xhr, status, error) { error: function(xhr, status, error) {
console.error('AJAX request failed:', status, error); console.error('Update failed:', status, error);
updateOutputContent.textContent = `Update failed: ${error}\n\nStatus: ${status}\nResponse: ${xhr.responseText || 'No response'}`;
showToast('Update failed! Check the output for details.', 'error');
},
complete: function() {
// Reset button state
updateBtn.disabled = false;
updateBtnText.textContent = 'Update firmware';
updateSpinner.style.display = 'none';
} }
}); });
} }
function clearUpdateOutput() {
const updateOutput = document.getElementById('updateOutput');
const updateOutputContent = document.getElementById('updateOutputContent');
const reloadBtn = document.getElementById('reloadBtn');
updateOutputContent.textContent = '';
updateOutput.style.display = 'none';
reloadBtn.style.display = 'none';
}
function showToast(message, type) {
const toastLiveExample = document.getElementById('liveToast');
const toastBody = toastLiveExample.querySelector('.toast-body');
// Set toast color based on type
toastLiveExample.classList.remove('text-bg-primary', 'text-bg-success', 'text-bg-danger', 'text-bg-warning');
switch(type) {
case 'success':
toastLiveExample.classList.add('text-bg-success');
break;
case 'error':
toastLiveExample.classList.add('text-bg-danger');
break;
case 'warning':
toastLiveExample.classList.add('text-bg-warning');
break;
default:
toastLiveExample.classList.add('text-bg-primary');
}
toastBody.textContent = message;
const toastBootstrap = bootstrap.Toast.getOrCreateInstance(toastLiveExample);
toastBootstrap.show();
}
// Legacy function for backward compatibility
function updateGitPull() {
updateFirmware();
}
function set_RTC_withNTP(){ function set_RTC_withNTP(){
console.log("Set RTC module with WIFI (NTP server)"); console.log("Set RTC module with WIFI (NTP server)");
@@ -901,6 +989,251 @@ function updateSondeCoefficient(id, coefficient) {
}); });
} }
/*
____ _ __ __ _
/ ___| ___ _ ____ _(_) ___ ___| \/ | __ _ _ __ __ _ __ _ ___ _ __ ___ ___ _ __ | |_
\___ \ / _ \ '__\ \ / / |/ __/ _ \ |\/| |/ _` | '_ \ / _` |/ _` |/ _ \ '_ ` _ \ / _ \ '_ \| __|
___) | __/ | \ V /| | (_| __/ | | | (_| | | | | (_| | (_| | __/ | | | | | __/ | | | |_
|____/ \___|_| \_/ |_|\___\___|_| |_|\__,_|_| |_|\__,_|\__, |\___|_| |_| |_|\___|_| |_|\__|
|___/
*/
function refreshServices() {
console.log("Refreshing services status");
$.ajax({
url: 'launcher.php?type=get_systemd_services',
dataType: 'json',
method: 'GET',
cache: false,
success: function(response) {
console.log("Services data:", response);
if (response.success) {
displayServices(response.services);
} else {
showServiceError("Failed to load services: " + response.error);
}
},
error: function(xhr, status, error) {
console.error('Failed to load services:', error);
showServiceError("Failed to load services: " + error);
}
});
}
function displayServices(services) {
const tbody = document.getElementById('services-tbody');
tbody.innerHTML = '';
services.forEach(function(service) {
const row = document.createElement('tr');
// Service name
const nameCell = document.createElement('td');
nameCell.textContent = service.display_name || service.name;
row.appendChild(nameCell);
// Description
const descCell = document.createElement('td');
descCell.textContent = service.description || 'No description available';
descCell.className = 'text-muted small';
row.appendChild(descCell);
// Frequency
const freqCell = document.createElement('td');
freqCell.textContent = service.frequency || 'Unknown';
freqCell.className = 'text-primary small fw-bold';
row.appendChild(freqCell);
// Status
const statusCell = document.createElement('td');
const statusBadge = document.createElement('span');
statusBadge.className = `badge ${service.active ? 'bg-success' : 'bg-danger'}`;
statusBadge.textContent = service.active ? 'Running' : 'Stopped';
statusCell.appendChild(statusBadge);
row.appendChild(statusCell);
// Enabled
const enabledCell = document.createElement('td');
const enabledBadge = document.createElement('span');
enabledBadge.className = `badge ${service.enabled ? 'bg-info' : 'bg-secondary'}`;
enabledBadge.textContent = service.enabled ? 'Enabled' : 'Disabled';
enabledCell.appendChild(enabledBadge);
row.appendChild(enabledCell);
// Actions
const actionsCell = document.createElement('td');
// Restart button
const restartBtn = document.createElement('button');
restartBtn.className = 'btn btn-sm btn-warning me-2';
restartBtn.innerHTML = '<i class="bi bi-arrow-clockwise"></i> Restart';
restartBtn.onclick = function() {
restartService(service.name);
};
actionsCell.appendChild(restartBtn);
// Enable/Disable button
const toggleBtn = document.createElement('button');
toggleBtn.className = `btn btn-sm ${service.enabled ? 'btn-danger' : 'btn-success'}`;
toggleBtn.innerHTML = service.enabled ? '<i class="bi bi-stop"></i> Disable' : '<i class="bi bi-play"></i> Enable';
toggleBtn.onclick = function() {
toggleService(service.name, !service.enabled);
};
actionsCell.appendChild(toggleBtn);
row.appendChild(actionsCell);
tbody.appendChild(row);
});
}
function showServiceError(message) {
const tbody = document.getElementById('services-tbody');
tbody.innerHTML = `
<tr>
<td colspan="6" class="text-center text-danger">
<i class="bi bi-exclamation-triangle"></i> ${message}
</td>
</tr>
`;
}
function restartService(serviceName) {
console.log(`Restarting service: ${serviceName}`);
if (!confirm(`Are you sure you want to restart ${serviceName}?`)) {
return;
}
const toastLiveExample = document.getElementById('liveToast');
const toastBody = toastLiveExample.querySelector('.toast-body');
$.ajax({
url: 'launcher.php?type=restart_systemd_service&service=' + encodeURIComponent(serviceName),
dataType: 'json',
method: 'GET',
cache: false,
success: function(response) {
console.log('Service restart response:', response);
let formattedMessage = '';
if (response.success) {
// Success message
toastLiveExample.classList.remove('text-bg-danger');
toastLiveExample.classList.add('text-bg-success');
formattedMessage = `
<strong>Success!</strong><br>
Service: ${serviceName}<br>
${response.message || 'Service restarted successfully'}
`;
// Refresh services after a short delay
setTimeout(refreshServices, 2000);
} else {
// Error message
toastLiveExample.classList.remove('text-bg-success');
toastLiveExample.classList.add('text-bg-danger');
formattedMessage = `
<strong>Error!</strong><br>
Service: ${serviceName}<br>
${response.error || 'Unknown error occurred'}
`;
}
// Update and show toast
toastBody.innerHTML = formattedMessage;
const toastBootstrap = bootstrap.Toast.getOrCreateInstance(toastLiveExample);
toastBootstrap.show();
},
error: function(xhr, status, error) {
console.error('Failed to restart service:', error);
// Show error toast
toastLiveExample.classList.remove('text-bg-success');
toastLiveExample.classList.add('text-bg-danger');
toastBody.innerHTML = `
<strong>Request Failed!</strong><br>
Service: ${serviceName}<br>
Error: ${error}
`;
const toastBootstrap = bootstrap.Toast.getOrCreateInstance(toastLiveExample);
toastBootstrap.show();
}
});
}
function toggleService(serviceName, enable) {
const action = enable ? 'enable' : 'disable';
console.log(`${action} service: ${serviceName}`);
if (!confirm(`Are you sure you want to ${action} ${serviceName}?`)) {
return;
}
const toastLiveExample = document.getElementById('liveToast');
const toastBody = toastLiveExample.querySelector('.toast-body');
$.ajax({
url: 'launcher.php?type=toggle_systemd_service&service=' + encodeURIComponent(serviceName) + '&enable=' + enable,
dataType: 'json',
method: 'GET',
cache: false,
success: function(response) {
console.log('Service toggle response:', response);
let formattedMessage = '';
if (response.success) {
// Success message
toastLiveExample.classList.remove('text-bg-danger');
toastLiveExample.classList.add('text-bg-success');
formattedMessage = `
<strong>Success!</strong><br>
Service: ${serviceName}<br>
${response.message || `Service ${action}d successfully`}
`;
// Refresh services after a short delay
setTimeout(refreshServices, 2000);
} else {
// Error message
toastLiveExample.classList.remove('text-bg-success');
toastLiveExample.classList.add('text-bg-danger');
formattedMessage = `
<strong>Error!</strong><br>
Service: ${serviceName}<br>
${response.error || 'Unknown error occurred'}
`;
}
// Update and show toast
toastBody.innerHTML = formattedMessage;
const toastBootstrap = bootstrap.Toast.getOrCreateInstance(toastLiveExample);
toastBootstrap.show();
},
error: function(xhr, status, error) {
console.error('Failed to toggle service:', error);
// Show error toast
toastLiveExample.classList.remove('text-bg-success');
toastLiveExample.classList.add('text-bg-danger');
toastBody.innerHTML = `
<strong>Request Failed!</strong><br>
Service: ${serviceName}<br>
Error: ${error}
`;
const toastBootstrap = bootstrap.Toast.getOrCreateInstance(toastLiveExample);
toastBootstrap.show();
}
});
}
</script> </script>

View File

@@ -71,6 +71,8 @@
<button class="btn btn-primary" onclick="get_data_sqlite('data_BME280',getSelectedLimit(),false)">Mesures Temp/Hum</button> <button class="btn btn-primary" onclick="get_data_sqlite('data_BME280',getSelectedLimit(),false)">Mesures Temp/Hum</button>
<button class="btn btn-primary" onclick="get_data_sqlite('data_NPM_5channels',getSelectedLimit(),false)">Mesures PM (5 canaux)</button> <button class="btn btn-primary" onclick="get_data_sqlite('data_NPM_5channels',getSelectedLimit(),false)">Mesures PM (5 canaux)</button>
<button class="btn btn-primary" onclick="get_data_sqlite('data_envea',getSelectedLimit(),false)">Sonde Cairsens</button> <button class="btn btn-primary" onclick="get_data_sqlite('data_envea',getSelectedLimit(),false)">Sonde Cairsens</button>
<button class="btn btn-primary" onclick="get_data_sqlite('data_WIND',getSelectedLimit(),false)">Sonde Vent</button>
<button class="btn btn-warning" onclick="get_data_sqlite('timestamp_table',getSelectedLimit(),false)">Timestamp Table</button> <button class="btn btn-warning" onclick="get_data_sqlite('timestamp_table',getSelectedLimit(),false)">Timestamp Table</button>
</div> </div>
@@ -147,42 +149,55 @@
window.onload = function() { window.onload = function() {
fetch('../config.json') // Replace 'deviceID.txt' with 'config.json'
.then(response => response.json()) // Parse response as JSON
.then(data => {
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;
});
//get local RTC //NEW way to get data from SQLITE
$.ajax({ $.ajax({
url: 'launcher.php?type=RTC_time', url: 'launcher.php?type=get_config_sqlite',
dataType: 'text', // Specify that you expect a JSON response dataType:'json',
method: 'GET', // Use GET or POST depending on your needs //dataType: 'json', // Specify that you expect a JSON response
success: function(response) { method: 'GET', // Use GET or POST depending on your needs
console.log("Local RTC: " + response); success: function(response) {
const RTC_Element = document.getElementById("RTC_time"); console.log("Getting SQLite config table:");
RTC_Element.textContent = response; console.log(response);
},
error: function(xhr, status, error) { //get device Name (for the side bar)
console.error('AJAX request failed:', status, error); const deviceName = response.deviceName;
} const elements = document.querySelectorAll('.sideBar_sensorName');
elements.forEach((element) => {
element.innerText = deviceName;
}); });
}) //device name html page title
.catch(error => console.error('Error loading config.json:', error)); if (response.deviceName) {
} document.title = 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);
}
}); //end AJAX
}
@@ -199,7 +214,6 @@ function get_data_sqlite(table, limit, download , startDate = "", endDate = "")
console.log(url); console.log(url);
$.ajax({ $.ajax({
url: url, url: url,
dataType: 'text', // Specify that you expect a JSON response dataType: 'text', // Specify that you expect a JSON response
@@ -260,6 +274,12 @@ function get_data_sqlite(table, limit, download , startDate = "", endDate = "")
tableHTML += ` tableHTML += `
<th>Timestamp</th> <th>Timestamp</th>
`; `;
}else if (table === "data_WIND") {
tableHTML += `
<th>Timestamp</th>
<th>speed (km/h)</th>
<th>Direction (V)</th>
`;
} }
tableHTML += `</tr></thead><tbody>`; tableHTML += `</tr></thead><tbody>`;
@@ -310,6 +330,12 @@ function get_data_sqlite(table, limit, download , startDate = "", endDate = "")
tableHTML += ` tableHTML += `
<td>${columns[1]}</td> <td>${columns[1]}</td>
`; `;
}else if (table === "data_WIND") {
tableHTML += `
<td>${columns[0]}</td>
<td>${columns[1]}</td>
<td>${columns[2]}</td>
`;
} }
tableHTML += "</tr>"; tableHTML += "</tr>";

View File

@@ -152,11 +152,16 @@ window.onload = function() {
element.innerText = deviceName; element.innerText = deviceName;
}); });
//device name html page title
if (response.deviceName) {
document.title = response.deviceName;
}
}, },
error: function(xhr, status, error) { error: function(xhr, status, error) {
console.error('AJAX request failed:', status, error); console.error('AJAX request failed:', status, error);
} }
}); }); //end ajax
/* OLD way of getting config data /* OLD way of getting config data
fetch('../config.json') // Replace 'deviceID.txt' with 'config.json' fetch('../config.json') // Replace 'deviceID.txt' with 'config.json'

View File

@@ -218,52 +218,6 @@ if ($type == "update_config_sqlite") {
} }
} }
//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 //UPDATING the envea_sondes_table table from SQLite DB
if ($type == "update_sonde") { if ($type == "update_sonde") {
@@ -395,6 +349,20 @@ if ($type == "git_pull") {
echo $output; echo $output;
} }
if ($type == "update_firmware") {
// Execute the comprehensive update script
$command = 'sudo /var/www/nebuleair_pro_4g/update_firmware.sh 2>&1';
$output = shell_exec($command);
// Return the output as JSON for better web display
header('Content-Type: application/json');
echo json_encode([
'success' => true,
'output' => $output,
'timestamp' => date('Y-m-d H:i:s')
]);
}
if ($type == "set_RTC_withNTP") { if ($type == "set_RTC_withNTP") {
$command = 'sudo /usr/bin/python3 /var/www/nebuleair_pro_4g/RTC/set_with_NTP.py'; $command = 'sudo /usr/bin/python3 /var/www/nebuleair_pro_4g/RTC/set_with_NTP.py';
$output = shell_exec($command); $output = shell_exec($command);
@@ -423,9 +391,50 @@ if ($type == "set_RTC_withBrowser") {
if ($type == "clear_loopLogs") { if ($type == "clear_loopLogs") {
$command = 'truncate -s 0 /var/www/nebuleair_pro_4g/logs/loop.log'; $response = array();
$output = shell_exec($command);
echo $output; try {
$logPath = '/var/www/nebuleair_pro_4g/logs/master.log';
// Check if file exists and is writable
if (!file_exists($logPath)) {
throw new Exception("Log file does not exist");
}
if (!is_writable($logPath)) {
throw new Exception("Log file is not writable");
}
// Execute the command
$command = 'truncate -s 0 ' . escapeshellarg($logPath);
$output = shell_exec($command . ' 2>&1');
// Check if there was any error output
if (!empty($output)) {
throw new Exception("Command error: " . $output);
}
// Success response
$response = array(
'status' => 'success',
'message' => 'Logs cleared successfully',
'timestamp' => date('Y-m-d H:i:s')
);
} catch (Exception $e) {
// Error response
$response = array(
'status' => 'error',
'message' => $e->getMessage(),
'timestamp' => date('Y-m-d H:i:s')
);
}
// Set content type to JSON
header('Content-Type: application/json');
// Return the JSON response
echo json_encode($response);
exit;
} }
if ($type == "database_size") { if ($type == "database_size") {
@@ -590,36 +599,56 @@ if ($type == "sara_connectNetwork") {
$port=$_GET['port']; $port=$_GET['port'];
$timeout=$_GET['timeout']; $timeout=$_GET['timeout'];
$networkID=$_GET['networkID']; $networkID=$_GET['networkID'];
$param="SARA_R4_neworkID";
//echo "updating SARA_R4_networkID in config file"; //echo "updating SARA_R4_networkID in config file";
//OLD way to store data (JSON file)
// Convert `networkID` to an integer (or float if needed) // Convert `networkID` to an integer (or float if needed)
$networkID = is_numeric($networkID) ? (strpos($networkID, '.') !== false ? (float)$networkID : (int)$networkID) : 0; //$networkID = is_numeric($networkID) ? (strpos($networkID, '.') !== false ? (float)$networkID : (int)$networkID) : 0;
#save to config.json #save to config.json
$configFile = '/var/www/nebuleair_pro_4g/config.json'; //$configFile = '/var/www/nebuleair_pro_4g/config.json';
// Read the JSON file // Read the JSON file
$jsonData = file_get_contents($configFile); //$jsonData = file_get_contents($configFile);
// Decode JSON data into an associative array // Decode JSON data into an associative array
$config = json_decode($jsonData, true); //$config = json_decode($jsonData, true);
// Check if decoding was successful // Check if decoding was successful
if ($config === null) { //if ($config === null) {
die("Error: Could not decode JSON file."); // die("Error: Could not decode JSON file.");
} //}
// Update the value of SARA_R4_networkID // Update the value of SARA_R4_networkID
$config['SARA_R4_neworkID'] = $networkID; // Replace 42 with the desired value //$config['SARA_R4_neworkID'] = $networkID; // Replace 42 with the desired value
// Encode the array back to JSON with pretty printing // Encode the array back to JSON with pretty printing
$newJsonData = json_encode($config, JSON_PRETTY_PRINT); //$newJsonData = json_encode($config, JSON_PRETTY_PRINT);
// Check if encoding was successful // Check if encoding was successful
if ($newJsonData === false) { //if ($newJsonData === false) {
die("Error: Could not encode JSON data."); // die("Error: Could not encode JSON data.");
} //}
// Write the updated JSON back to the file // Write the updated JSON back to the file
if (file_put_contents($configFile, $newJsonData) === false) { //if (file_put_contents($configFile, $newJsonData) === false) {
die("Error: Could not write to JSON file."); // die("Error: Could not write to JSON file.");
} //}
//echo "SARA_R4_networkID updated successfully."; //echo "SARA_R4_networkID updated successfully.";
//NEW way to store data -> use SQLITE
try {
$db = new PDO("sqlite:$database_path");
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$updateStmt = $db->prepare("UPDATE config_table SET value = :value WHERE key = :param");
$updateStmt->bindParam(':value', $networkID);
$updateStmt->bindParam(':param', $param);
$updateStmt->execute();
echo "SARA_R4_networkID updated successfully.";
} catch (PDOException $e) {
// Return error as JSON
header('Content-Type: application/json');
echo json_encode(['error' => 'Database error: ' . $e->getMessage()]);
}
//echo "connecting to network... please wait..."; //echo "connecting to network... please wait...";
$command = 'sudo /usr/bin/python3 /var/www/nebuleair_pro_4g/SARA/sara_connectNetwork.py ' . $port . ' ' . $networkID . ' ' . $timeout; $command = 'sudo /usr/bin/python3 /var/www/nebuleair_pro_4g/SARA/sara_connectNetwork.py ' . $port . ' ' . $networkID . ' ' . $timeout;
@@ -806,3 +835,366 @@ if ($type == "wifi_scan_old") {
echo $json_data; echo $json_data;
} }
/*
_____ _ _
|_ _|__ _ __ _ __ ___ (_)_ __ __ _| |
| |/ _ \ '__| '_ ` _ \| | '_ \ / _` | |
| | __/ | | | | | | | | | | | (_| | |
|_|\___|_| |_| |_| |_|_|_| |_|\__,_|_|
*/
// Execute shell command with security restrictions
if ($type == "execute_command") {
// Verify that the request is using POST method
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
echo json_encode(['success' => false, 'message' => 'Invalid request method']);
exit;
}
// Get the command from POST data
$command = isset($_POST['command']) ? $_POST['command'] : '';
if (empty($command)) {
echo json_encode(['success' => false, 'message' => 'No command provided']);
exit;
}
// List of allowed commands (prefixes)
$allowedCommands = [
'ls', 'cat', 'cd', 'pwd', 'df', 'free', 'ifconfig', 'ip', 'ps', 'date', 'uptime',
'systemctl status', 'whoami', 'hostname', 'uname', 'grep', 'tail', 'head', 'find',
'less', 'more', 'du', 'echo', 'git'
];
// Check if command is allowed
$allowed = false;
foreach ($allowedCommands as $allowedCmd) {
if (strpos($command, $allowedCmd) === 0) {
$allowed = true;
break;
}
}
// Special case for systemctl restart and reboot
if (strpos($command, 'systemctl restart') === 0 || $command === 'reboot') {
// These commands don't return output through shell_exec since they change process state
// We'll just acknowledge them
if ($command === 'reboot') {
// Execute the command with exec to avoid waiting for output
exec('sudo reboot > /dev/null 2>&1 &');
echo json_encode([
'success' => true,
'output' => 'System is rebooting...'
]);
} else {
// For systemctl restart, execute it and acknowledge
$serviceName = str_replace('systemctl restart ', '', $command);
exec('sudo systemctl restart ' . escapeshellarg($serviceName) . ' > /dev/null 2>&1 &');
echo json_encode([
'success' => true,
'output' => 'Service ' . $serviceName . ' is restarting...'
]);
}
exit;
}
// Check for prohibited patterns
$prohibitedPatterns = [
'sudo rm', ';', '&&', '||', '|', '>', '>>', '&',
'wget', 'curl', 'nc', 'ssh', 'scp', 'ftp', 'telnet',
'iptables', 'passwd', 'chown', 'chmod', 'mkfs', ' dd ',
'mount', 'umount', 'kill', 'killall'
];
foreach ($prohibitedPatterns as $pattern) {
if (strpos($command, $pattern) !== false) {
echo json_encode([
'success' => false,
'message' => 'Command contains prohibited operation: ' . $pattern
]);
exit;
}
}
if (!$allowed) {
echo json_encode([
'success' => false,
'message' => 'Command not allowed for security reasons'
]);
exit;
}
// Execute the command with timeout protection
$descriptorspec = [
0 => ["pipe", "r"], // stdin
1 => ["pipe", "w"], // stdout
2 => ["pipe", "w"] // stderr
];
// Escape the command to prevent shell injection
$escapedCommand = escapeshellcmd($command);
// Add timeout of 5 seconds to prevent long-running commands
$process = proc_open("timeout 5 $escapedCommand", $descriptorspec, $pipes);
if (is_resource($process)) {
// Close stdin pipe
fclose($pipes[0]);
// Get output from stdout
$output = stream_get_contents($pipes[1]);
fclose($pipes[1]);
// Get any errors
$errors = stream_get_contents($pipes[2]);
fclose($pipes[2]);
// Close the process
$returnValue = proc_close($process);
// Check for errors
if ($returnValue !== 0) {
// If there was an error, but we have output, consider it a partial success
if (!empty($output)) {
echo json_encode([
'success' => true,
'output' => $output . "\n" . $errors . "\nCommand exited with code $returnValue"
]);
} else {
echo json_encode([
'success' => false,
'message' => empty($errors) ? "Command failed with exit code $returnValue" : $errors
]);
}
} else {
// Success
echo json_encode([
'success' => true,
'output' => $output
]);
}
} else {
echo json_encode([
'success' => false,
'message' => 'Failed to execute command'
]);
}
}
/*
____ _ ____ _ __ __ _
/ ___| _ _ ___| |_ ___ _ __ ___ | _ \ / ___| ___ _ ____ _(_) ___ ___| \/ | __ _ _ __ __ _ __ _ ___ _ __ ___ ___ _ __ | |_
\___ \| | | / __| __/ _ \ '_ ` _ \| | | | \___ \ / _ \ '__\ \ / / |/ __/ _ \ |\/| |/ _` | '_ \ / _` |/ _` |/ _ \ '_ ` _ \ / _ \ '_ \| __|
___) | |_| \__ \ || __/ | | | | | |_| | ___) | __/ | \ V /| | (_| __/ | | | (_| | | | | (_| | (_| | __/ | | | | | __/ | | | |_
|____/ \__, |___/\__\___|_| |_| |_|____/ |____/ \___|_| \_/ |_|\___\___|_| |_|\__,_|_| |_|\__,_|\__, |\___|_| |_| |_|\___|_| |_|\__|
|___/ |___/
*/
// Get systemd services status
if ($type == "get_systemd_services") {
try {
// List of NebuleAir services to monitor with descriptions and frequencies
$services = [
'nebuleair-npm-data.timer' => [
'description' => 'Collects particulate matter data from NextPM sensor',
'frequency' => 'Every 10 seconds'
],
'nebuleair-envea-data.timer' => [
'description' => 'Reads environmental data from Envea sensors',
'frequency' => 'Every 10 seconds'
],
'nebuleair-sara-data.timer' => [
'description' => 'Transmits collected data via 4G cellular modem',
'frequency' => 'Every 60 seconds'
],
'nebuleair-bme280-data.timer' => [
'description' => 'Monitors temperature and humidity (BME280)',
'frequency' => 'Every 2 minutes'
],
'nebuleair-mppt-data.timer' => [
'description' => 'Tracks solar panel and battery status',
'frequency' => 'Every 2 minutes'
],
'nebuleair-db-cleanup-data.timer' => [
'description' => 'Cleans up old data from database',
'frequency' => 'Daily'
]
];
$serviceStatus = [];
foreach ($services as $service => $serviceInfo) {
// Get service active status
$activeCmd = "systemctl is-active " . escapeshellarg($service) . " 2>/dev/null";
$activeStatus = trim(shell_exec($activeCmd));
$isActive = ($activeStatus === 'active');
// Get service enabled status
$enabledCmd = "systemctl is-enabled " . escapeshellarg($service) . " 2>/dev/null";
$enabledStatus = trim(shell_exec($enabledCmd));
$isEnabled = ($enabledStatus === 'enabled');
// Clean up service name for display
$displayName = str_replace(['.timer', 'nebuleair-', '-data'], '', $service);
$displayName = ucfirst(str_replace('-', ' ', $displayName));
$serviceStatus[] = [
'name' => $service,
'display_name' => $displayName,
'description' => $serviceInfo['description'],
'frequency' => $serviceInfo['frequency'],
'active' => $isActive,
'enabled' => $isEnabled
];
}
echo json_encode([
'success' => true,
'services' => $serviceStatus
], JSON_PRETTY_PRINT);
} catch (Exception $e) {
echo json_encode([
'success' => false,
'error' => $e->getMessage()
]);
}
}
// Restart a systemd service
if ($type == "restart_systemd_service") {
$service = $_GET['service'] ?? null;
if (empty($service)) {
echo json_encode([
'success' => false,
'error' => 'No service specified'
]);
exit;
}
// Validate service name (security check)
$allowedServices = [
'nebuleair-npm-data.timer',
'nebuleair-envea-data.timer',
'nebuleair-sara-data.timer',
'nebuleair-bme280-data.timer',
'nebuleair-mppt-data.timer',
'nebuleair-db-cleanup-data.timer'
];
if (!in_array($service, $allowedServices)) {
echo json_encode([
'success' => false,
'error' => 'Invalid service name'
]);
exit;
}
try {
// Restart the service
$command = "sudo systemctl restart " . escapeshellarg($service) . " 2>&1";
$output = shell_exec($command);
// Check if restart was successful
$statusCmd = "systemctl is-active " . escapeshellarg($service) . " 2>/dev/null";
$status = trim(shell_exec($statusCmd));
if ($status === 'active' || empty($output)) {
echo json_encode([
'success' => true,
'message' => "Service $service restarted successfully"
]);
} else {
echo json_encode([
'success' => false,
'error' => "Failed to restart service: $output"
]);
}
} catch (Exception $e) {
echo json_encode([
'success' => false,
'error' => $e->getMessage()
]);
}
}
// Enable/disable a systemd service
if ($type == "toggle_systemd_service") {
$service = $_GET['service'] ?? null;
$enable = $_GET['enable'] ?? null;
if (empty($service) || $enable === null) {
echo json_encode([
'success' => false,
'error' => 'Missing service name or enable parameter'
]);
exit;
}
// Validate service name (security check)
$allowedServices = [
'nebuleair-npm-data.timer',
'nebuleair-envea-data.timer',
'nebuleair-sara-data.timer',
'nebuleair-bme280-data.timer',
'nebuleair-mppt-data.timer',
'nebuleair-db-cleanup-data.timer'
];
if (!in_array($service, $allowedServices)) {
echo json_encode([
'success' => false,
'error' => 'Invalid service name'
]);
exit;
}
try {
$enable = filter_var($enable, FILTER_VALIDATE_BOOLEAN);
$action = $enable ? 'enable' : 'disable';
// Enable/disable the service
$command = "sudo systemctl $action " . escapeshellarg($service) . " 2>&1";
$output = shell_exec($command);
// If disabling, also stop the service
if (!$enable) {
$stopCommand = "sudo systemctl stop " . escapeshellarg($service) . " 2>&1";
$stopOutput = shell_exec($stopCommand);
}
// If enabling, also start the service
if ($enable) {
$startCommand = "sudo systemctl start " . escapeshellarg($service) . " 2>&1";
$startOutput = shell_exec($startCommand);
}
// Check if the operation was successful
$statusCmd = "systemctl is-enabled " . escapeshellarg($service) . " 2>/dev/null";
$status = trim(shell_exec($statusCmd));
$expectedStatus = $enable ? 'enabled' : 'disabled';
if ($status === $expectedStatus) {
echo json_encode([
'success' => true,
'message' => "Service $service " . ($enable ? 'enabled and started' : 'disabled and stopped') . " successfully"
]);
} else {
echo json_encode([
'success' => false,
'error' => "Failed to $action service: $output"
]);
}
} catch (Exception $e) {
echo json_encode([
'success' => false,
'error' => $e->getMessage()
]);
}
}

View File

@@ -56,7 +56,10 @@
<div class="col-lg-6 col-12"> <div class="col-lg-6 col-12">
<div class="card" style="height: 80vh;"> <div class="card" style="height: 80vh;">
<div class="card-header"> <div class="card-header">
Master logs <button type="submit" class="btn btn-secondary btn-sm" onclick="clear_loopLogs()">Clear</button> Sara logs
<button type="submit" class="btn btn-secondary btn-sm" id="refresh-master-log">Refresh</button>
<button type="submit" class="btn btn-secondary btn-sm" onclick="clear_loopLogs()">Clear</button>
<span id="script_running"></span> <span id="script_running"></span>
</div> </div>
<div class="card-body overflow-auto" id="card_loop_content"> <div class="card-body overflow-auto" id="card_loop_content">
@@ -69,6 +72,7 @@
<div class="card" style="height: 80vh;"> <div class="card" style="height: 80vh;">
<div class="card-header"> <div class="card-header">
Boot logs Boot logs
<button type="submit" class="btn btn-secondary btn-sm" id="refresh-boot-log">Refresh</button>
</div> </div>
<div class="card-body overflow-auto" id="card_boot_content"> <div class="card-body overflow-auto" id="card_boot_content">
@@ -111,65 +115,17 @@
const boot_card_content = document.getElementById('card_boot_content'); const boot_card_content = document.getElementById('card_boot_content');
//Getting Master logs //Getting Master logs
console.log("Getting master logs"); console.log("Getting SARA logs");
displayLogFile('../logs/sara_service.log', loop_card_content, true, 1000);
fetch('../logs/master.log') console.log("Getting app/boot logs");
.then((response) => { displayLogFile('../logs/app.log', boot_card_content, true, 1000);
console.log("OK");
if (!response.ok) { // Setup master log with refresh button
throw new Error('Failed to fetch the log file.'); setupLogRefreshButton('refresh-master-log', '../logs/sara_service.log', 'card_loop_content', 3000);
}
return response.text();
})
.then((data) => {
const lines = data.split('\n');
// Format log content
const formattedLog = lines
.map((line) => line.trim()) // Remove extra whitespace
.filter((line) => line) // Remove empty lines
.join('<br>'); // Join formatted lines with line breaks
loop_card_content.innerHTML = `<pre style="white-space: pre-wrap; word-wrap: break-word; margin: 0;">${formattedLog}</pre>`;
loop_card_content.scrollTop = loop_card_content.scrollHeight; // Scroll to the bottom
})
.catch((error) => {
console.error(error);
loop_card_content.textContent = 'Error loading log file.';
});
console.log("Getting app/boot logs");
//Getting App logs
fetch('../logs/app.log')
.then((response) => {
console.log("OK");
if (!response.ok) {
throw new Error('Failed to fetch the log file.');
}
return response.text();
})
.then((data) => {
const lines = data.split('\n');
// Format log content
const formattedLog = lines
.map((line) => line.trim()) // Remove extra whitespace
.filter((line) => line) // Remove empty lines
.join('<br>'); // Join formatted lines with line breaks
boot_card_content.innerHTML = `<pre style="white-space: pre-wrap; word-wrap: break-word; margin: 0;">${formattedLog}</pre>`;
boot_card_content.scrollTop = loop_card_content.scrollHeight; // Scroll to the bottom
})
.catch((error) => {
console.error(error);
boot_card_content.textContent = 'Error loading log file.';
});
// Setup boot log with refresh button
setupLogRefreshButton('refresh-boot-log', '../logs/app.log', 'card_boot_content', 300);
}); });
@@ -195,6 +151,11 @@ window.onload = function() {
element.innerText = response.deviceName; element.innerText = response.deviceName;
}); });
//device name html page title
if (response.deviceName) {
document.title = response.deviceName;
}
}, },
error: function(xhr, status, error) { error: function(xhr, status, error) {
console.error('AJAX request failed:', status, error); console.error('AJAX request failed:', status, error);
@@ -220,6 +181,75 @@ window.onload = function() {
}//end onload }//end onload
function displayLogFile(logFilePath, containerElement, scrollToBottom = true, maxLines = 0) {
// Show loading indicator
containerElement.innerHTML = '<div class="text-center"><i>Loading log file...</i></div>';
return fetch(logFilePath)
.then((response) => {
if (!response.ok) {
throw new Error(`Failed to fetch the log file: ${response.status} ${response.statusText}`);
}
return response.text();
})
.then((data) => {
// Split the log into lines
let lines = data.split('\n');
// Apply max lines limit if specified
if (maxLines > 0 && lines.length > maxLines) {
lines = lines.slice(-maxLines); // Get only the last N lines
}
// Format log content
const formattedLog = lines
.map((line) => line.trim()) // Remove extra whitespace
.filter((line) => line) // Remove empty lines
.join('<br>'); // Join formatted lines with line breaks
// Display the formatted log
containerElement.innerHTML = `<pre style="white-space: pre-wrap; word-wrap: break-word; margin: 0;">${formattedLog}</pre>`;
// Scroll to bottom if requested
if (scrollToBottom) {
containerElement.scrollTop = containerElement.scrollHeight;
}
return formattedLog; // Return the formatted log in case the caller needs it
})
.catch((error) => {
console.error(`Error loading log file ${logFilePath}:`, error);
containerElement.innerHTML = `<div class="text-danger">Error loading log file: ${error.message}</div>`;
throw error; // Re-throw the error for the caller to handle if needed
});
}
/**
* Set up a refresh button for a log file
* @param {string} buttonId - ID of the button element
* @param {string} logFilePath - Path to the log file
* @param {string} containerId - ID of the container to display the log in
* @param {number} maxLines - Maximum number of lines to display (0 for all)
*/
function setupLogRefreshButton(buttonId, logFilePath, containerId, maxLines = 0) {
console.log("Refreshing logs");
const button = document.getElementById(buttonId);
const container = document.getElementById(containerId);
if (!button || !container) {
console.error('Button or container element not found');
return;
}
// Initial load
displayLogFile(logFilePath, container, true, maxLines);
// Set up button click handler
button.addEventListener('click', () => {
displayLogFile(logFilePath, container, true, maxLines);
});
}
function clear_loopLogs(){ function clear_loopLogs(){
console.log("Clearing loop logs"); console.log("Clearing loop logs");

View File

@@ -372,6 +372,10 @@ document.addEventListener('DOMContentLoaded', function () {
success: function(response) { success: function(response) {
console.log("Getting SQLite config table:"); console.log("Getting SQLite config table:");
console.log(response); console.log(response);
//modem_version
const modem_version_html = document.getElementById("modem_version");
modem_version_html.innerText = response.modem_version;
// Set checkbox state based on the response data // Set checkbox state based on the response data
const check_modem_configMode = document.getElementById("check_modem_configMode"); const check_modem_configMode = document.getElementById("check_modem_configMode");
if (check_modem_configMode) { if (check_modem_configMode) {
@@ -387,9 +391,58 @@ document.addEventListener('DOMContentLoaded', function () {
} }
}); });
}); });
window.onload = function() {
getModem_busy_status();
setInterval(getModem_busy_status, 1000);
//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;
});
//device name html page title
if (response.deviceName) {
document.title = 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);
}
});
}
function getData_saraR4(port, command, timeout){ function getData_saraR4(port, command, timeout){
console.log("Data from SaraR4"); console.log("Data from SaraR4");
console.log("Port: " + port ); console.log("Port: " + port );
@@ -811,69 +864,6 @@ function update_modem_configMode(param, checked){
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 => {
//get device ID
const deviceID = data.deviceID.trim().toUpperCase();
//document.getElementById('pageTitle_plus_ID').innerText = 'token: ' + deviceID;
//get device Name
const deviceName = data.deviceName;
//get modem version
const modem_version = data.modem_version;
const modem_version_html = document.getElementById("modem_version");
modem_version_html.textContent = modem_version;
const elements = document.querySelectorAll('.sideBar_sensorName');
elements.forEach((element) => {
element.innerText = deviceName;
});
//get SARA_R4 connection status
/*
const SARA_statusElement = document.getElementById("modem-status");
console.log("SARA R4 is: " + data.SARA_R4_network_status);
if (data.SARA_R4_network_status === "connected") {
SARA_statusElement.textContent = "Connected";
SARA_statusElement.className = "badge text-bg-success";
} else if (data.SARA_R4_network_status === "disconnected") {
SARA_statusElement.textContent = "Disconnected";
SARA_statusElement.className = "badge text-bg-danger";
} else {
SARA_statusElement.textContent = "Unknown";
SARA_statusElement.className = "badge text-bg-secondary";
}
*/
//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));
}
</script> </script>

View File

@@ -47,6 +47,12 @@
</svg> </svg>
Carte Carte
</a> </a>
<a class="nav-link text-white" href="terminal.html">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-terminal-fill" viewBox="0 0 16 16">
<path d="M0 3a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V3zm9.5 5.5h-3a.5.5 0 0 0 0 1h3a.5.5 0 0 0 0-1zm-6.354-.354a.5.5 0 1 0 .708.708l2-2a.5.5 0 0 0 0-.708l-2-2a.5.5 0 1 0-.708.708L4.793 6.5 3.146 8.146z"/>
</svg>
Terminal
</a>
<a class="nav-link text-white" href="admin.html"> <a class="nav-link text-white" href="admin.html">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-tools" viewBox="0 0 16 16"> <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-tools" viewBox="0 0 16 16">
<path d="M1 0 0 1l2.2 3.081a1 1 0 0 0 .815.419h.07a1 1 0 0 1 .708.293l2.675 2.675-2.617 2.654A3.003 3.003 0 0 0 0 13a3 3 0 1 0 5.878-.851l2.654-2.617.968.968-.305.914a1 1 0 0 0 .242 1.023l3.27 3.27a.997.997 0 0 0 1.414 0l1.586-1.586a.997.997 0 0 0 0-1.414l-3.27-3.27a1 1 0 0 0-1.023-.242L10.5 9.5l-.96-.96 2.68-2.643A3.005 3.005 0 0 0 16 3q0-.405-.102-.777l-2.14 2.141L12 4l-.364-1.757L13.777.102a3 3 0 0 0-3.675 3.68L7.462 6.46 4.793 3.793a1 1 0 0 1-.293-.707v-.071a1 1 0 0 0-.419-.814zm9.646 10.646a.5.5 0 0 1 .708 0l2.914 2.915a.5.5 0 0 1-.707.707l-2.915-2.914a.5.5 0 0 1 0-.708M3 11l.471.242.529.026.287.445.445.287.026.529L5 13l-.242.471-.026.529-.445.287-.287.445-.529.026L3 15l-.471-.242L2 14.732l-.287-.445L1.268 14l-.026-.529L1 13l.242-.471.026-.529.445-.287.287-.445.529-.026z"/> <path d="M1 0 0 1l2.2 3.081a1 1 0 0 0 .815.419h.07a1 1 0 0 1 .708.293l2.675 2.675-2.617 2.654A3.003 3.003 0 0 0 0 13a3 3 0 1 0 5.878-.851l2.654-2.617.968.968-.305.914a1 1 0 0 0 .242 1.023l3.27 3.27a.997.997 0 0 0 1.414 0l1.586-1.586a.997.997 0 0 0 0-1.414l-3.27-3.27a1 1 0 0 0-1.023-.242L10.5 9.5l-.96-.96 2.68-2.643A3.005 3.005 0 0 0 16 3q0-.405-.102-.777l-2.14 2.141L12 4l-.364-1.757L13.777.102a3 3 0 0 0-3.675 3.68L7.462 6.46 4.793 3.793a1 1 0 0 1-.293-.707v-.071a1 1 0 0 0-.419-.814zm9.646 10.646a.5.5 0 0 1 .708 0l2.914 2.915a.5.5 0 0 1-.707.707l-2.915-2.914a.5.5 0 0 1 0-.708M3 11l.471.242.529.026.287.445.445.287.026.529L5 13l-.242.471-.026.529-.445.287-.287.445-.529.026L3 15l-.471-.242L2 14.732l-.287-.445L1.268 14l-.026-.529L1 13l.242-.471.026-.529.445-.287.287-.445.529-.026z"/>

413
html/terminal.html Normal file
View File

@@ -0,0 +1,413 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>NebuleAir - Terminal</title>
<link rel="stylesheet" href="assets/css/bootstrap.min.css">
<style>
body {
overflow-x: hidden;
}
#sidebar a.nav-link {
position: relative;
display: flex;
align-items: center;
}
#sidebar a.nav-link:hover {
background-color: rgba(0, 0, 0, 0.5);
}
#sidebar a.nav-link svg {
margin-right: 8px; /* Add spacing between icons and text */
}
#sidebar {
transition: transform 0.3s ease-in-out;
}
.offcanvas-backdrop {
z-index: 1040;
}
#terminal {
width: 100%;
min-height: 400px;
max-height: 400px;
overflow-y: auto;
background-color: #000;
color: #00ff00;
font-family: monospace;
padding: 10px;
border-radius: 5px;
white-space: pre-wrap;
word-wrap: break-word;
}
#cmdLine {
width: 100%;
background-color: #000;
color: #00ff00;
font-family: monospace;
padding: 10px;
border: none;
border-radius: 0 0 5px 5px;
margin-top: -1px;
}
#cmdLine:focus {
outline: none;
}
.command-container {
display: none;
}
.password-popup {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
z-index: 2000;
justify-content: center;
align-items: center;
}
.password-container {
background-color: white;
padding: 20px;
border-radius: 5px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.3);
width: 300px;
}
.limited-commands {
background-color: #f8f9fa;
padding: 15px;
border-radius: 5px;
margin-bottom: 15px;
}
.limited-commands code {
white-space: nowrap;
margin-right: 10px;
margin-bottom: 5px;
display: inline-block;
}
</style>
</head>
<body>
<!-- Topbar -->
<span id="topbar"></span>
<!-- Sidebar Offcanvas for Mobile -->
<div class="offcanvas offcanvas-start text-white bg-dark" tabindex="-1" id="sidebarOffcanvas" aria-labelledby="sidebarOffcanvasLabel">
<div class="offcanvas-header">
<h5 class="offcanvas-title" id="sidebarOffcanvasLabel">NebuleAir</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="offcanvas" aria-label="Close"></button>
</div>
<div class="offcanvas-body" id="sidebar_mobile">
</div>
</div>
<div class="container-fluid mt-5">
<div class="row">
<!-- Side bar -->
<aside class="col-md-2 col-lg-1 d-none d-md-block vh-100 position-fixed bg-dark text-white" id="sidebar">
</aside>
<!-- Main content -->
<main class="col-md-10 ms-sm-auto col-lg-11 offset-md-2 offset-lg-1 px-md-4">
<h1 class="mt-4">Terminal Console</h1>
<div class="row mb-3">
<div class="col-12">
<div class="alert alert-warning">
<strong>Warning:</strong> This terminal provides direct access to system commands.
Use with caution as improper commands may affect system functionality.
</div>
<div class="limited-commands">
<h5>Quick Commands:</h5>
<div>
<code onclick="insertCommand('ls -la')">ls -la</code>
<code onclick="insertCommand('df -h')">df -h</code>
<code onclick="insertCommand('free -h')">free -h</code>
<code onclick="insertCommand('uptime')">uptime</code>
<code onclick="insertCommand('systemctl status master_nebuleair.service')">service status</code>
<code onclick="insertCommand('cat /var/www/nebuleair_pro_4g/config.json')">view config</code>
</div>
</div>
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="mb-0">Command Console</h5>
<div>
<button id="accessBtn" class="btn btn-primary me-2">Access Terminal</button>
<button id="clearBtn" class="btn btn-secondary" disabled>Clear</button>
</div>
</div>
<div class="card-body p-0">
<div class="command-container" id="commandContainer">
<div id="terminal">Welcome to NebuleAir Terminal Console
Type your commands below. Type 'help' for a list of commands.
</div>
<input type="text" id="cmdLine" placeholder="Enter command..." disabled>
</div>
<div id="errorMsg" class="alert alert-danger m-3" style="display:none;"></div>
</div>
</div>
</div>
</div>
</main>
</div>
</div>
<!-- Password Modal -->
<div class="password-popup" id="passwordModal">
<div class="password-container">
<h5>Authentication Required</h5>
<p>Please enter the admin password to access the terminal:</p>
<div class="mb-3">
<input type="password" class="form-control" id="adminPassword" placeholder="Password">
</div>
<div class="mb-3 d-flex justify-content-between">
<button class="btn btn-secondary" id="cancelPasswordBtn">Cancel</button>
<button class="btn btn-primary" id="submitPasswordBtn">Submit</button>
</div>
<div id="passwordError" class="text-danger mt-2" style="display:none;"></div>
</div>
</div>
<!-- JAVASCRIPT -->
<!-- Link Ajax locally -->
<script src="assets/jquery/jquery-3.7.1.min.js"></script>
<!-- Link Bootstrap JS and Popper.js locally -->
<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' }
];
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));
});
// Initialize elements
initializeElements();
});
window.onload = function() {
//NEW way to get config (SQLite)
$.ajax({
url: 'launcher.php?type=get_config_sqlite',
dataType:'json',
//dataType: 'json', // Specify that you expect a JSON response
method: 'GET', // Use GET or POST depending on your needs
success: function(response) {
console.log("Getting SQLite config table:");
console.log(response);
//device name_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
}
// Add admin password (should be changed to something more secure)
const ADMIN_PASSWORD = "123plouf";
// Global variables
let terminal;
let cmdLine;
let commandContainer;
let accessBtn;
let clearBtn;
let passwordModal;
let adminPassword;
let submitPasswordBtn;
let cancelPasswordBtn;
let passwordError;
let errorMsg;
let commandHistory = [];
let historyIndex = -1;
// Initialize DOM references after document is loaded
function initializeElements() {
terminal = document.getElementById('terminal');
cmdLine = document.getElementById('cmdLine');
commandContainer = document.getElementById('commandContainer');
accessBtn = document.getElementById('accessBtn');
clearBtn = document.getElementById('clearBtn');
passwordModal = document.getElementById('passwordModal');
adminPassword = document.getElementById('adminPassword');
submitPasswordBtn = document.getElementById('submitPasswordBtn');
cancelPasswordBtn = document.getElementById('cancelPasswordBtn');
passwordError = document.getElementById('passwordError');
errorMsg = document.getElementById('errorMsg');
// Set up event listeners
accessBtn.addEventListener('click', function() {
passwordModal.style.display = 'flex';
adminPassword.value = ''; // Clear password field
passwordError.style.display = 'none';
adminPassword.focus();
});
// Password submit button
submitPasswordBtn.addEventListener('click', function() {
if (adminPassword.value === ADMIN_PASSWORD) {
passwordModal.style.display = 'none';
enableTerminal();
} else {
passwordError.textContent = 'Invalid password';
passwordError.style.display = 'block';
}
});
// Enter key for password
adminPassword.addEventListener('keypress', function(e) {
if (e.key === 'Enter') {
submitPasswordBtn.click();
}
});
// Cancel password button
cancelPasswordBtn.addEventListener('click', function() {
passwordModal.style.display = 'none';
});
// Clear button
clearBtn.addEventListener('click', function() {
terminal.innerHTML = 'Terminal cleared.\n';
});
// Command line input events
cmdLine.addEventListener('keypress', function(e) {
if (e.key === 'Enter') {
const command = cmdLine.value.trim();
if (command) {
executeCommand(command);
commandHistory.push(command);
historyIndex = commandHistory.length;
cmdLine.value = '';
}
}
});
// Command history navigation with arrow keys
cmdLine.addEventListener('keydown', function(e) {
if (e.key === 'ArrowUp') {
if (historyIndex > 0) {
historyIndex--;
cmdLine.value = commandHistory[historyIndex];
}
e.preventDefault();
} else if (e.key === 'ArrowDown') {
if (historyIndex < commandHistory.length - 1) {
historyIndex++;
cmdLine.value = commandHistory[historyIndex];
} else {
historyIndex = commandHistory.length;
cmdLine.value = '';
}
e.preventDefault();
}
});
}
// Enable terminal access
function enableTerminal() {
commandContainer.style.display = 'block';
cmdLine.disabled = false;
clearBtn.disabled = false;
accessBtn.textContent = 'Authenticated';
accessBtn.classList.remove('btn-primary');
accessBtn.classList.add('btn-success');
accessBtn.disabled = true;
cmdLine.focus();
}
// Insert a predefined command
function insertCommand(cmd) {
// Only allow insertion if terminal is enabled
if (cmdLine.disabled === false) {
cmdLine.value = cmd;
cmdLine.focus();
} else {
// Alert user that they need to authenticate first
alert('Please access the terminal first by clicking "Access Terminal" and entering the password.');
}
}
// Execute a command
function executeCommand(command) {
// Add command to terminal with user prefix
terminal.innerHTML += `<span style="color: cyan;">user@nebuleair</span>:<span style="color: yellow;">~</span>$ ${command}\n`;
terminal.scrollTop = terminal.scrollHeight;
// Handle special commands
if (command === 'clear') {
terminal.innerHTML = 'Terminal cleared.\n';
return;
}
// Filter dangerous commands
const dangerousCommands = [
'rm -rf /', 'rm -rf /*', 'rm -rf ~', 'rm -rf ~/*',
'mkfs', 'dd if=/dev/zero', 'dd if=/dev/random',
'>>', '>', '|', ';', '&&', '||',
'wget', 'curl', 'ssh', 'scp', 'nc',
'chmod -R', 'chown -R'
];
// Check for dangerous commands or command chaining
const hasDangerousCommand = dangerousCommands.some(cmd => command.includes(cmd));
if (hasDangerousCommand || command.includes('&') || command.includes(';') || command.includes('|')) {
terminal.innerHTML += '<span style="color: red;">Error: This command is not allowed for security reasons.</span>\n';
terminal.scrollTop = terminal.scrollHeight;
return;
}
// Execute the command via AJAX
$.ajax({
url: 'launcher.php?type=execute_command',
method: 'POST',
dataType:'json',
data: {
type: 'execute_command',
command: command
},
success: function(response) {
console.log(response);
if (response.success) {
// Add command output to terminal
terminal.innerHTML += `<span style="color: #00ff00;">${response.output}</span>\n`;
} else {
terminal.innerHTML += `<span style="color: red;">Error: ${response.message}</span>\n`;
}
terminal.scrollTop = terminal.scrollHeight;
},
error: function(xhr, status, error) {
terminal.innerHTML += `<span style="color: red;">Error executing command: ${error}</span>\n`;
terminal.scrollTop = terminal.scrollHeight;
}
});
}
</script>
</body>
</html>

View File

@@ -302,6 +302,11 @@ function get_internet(){
element.innerText = deviceName; element.innerText = deviceName;
}); });
//device name html page title
if (response.deviceName) {
document.title = response.deviceName;
}
//get wifi connection status //get wifi connection status
const WIFI_statusElement = document.getElementById("wifi-status"); const WIFI_statusElement = document.getElementById("wifi-status");

View File

@@ -27,36 +27,23 @@ sudo apt update && sudo apt install -y git gh apache2 sqlite3 php php-sqlite3 py
# Install Python libraries # Install Python libraries
info "Installing Python libraries..." info "Installing Python libraries..."
sudo pip3 install pyserial requests RPi.GPIO adafruit-circuitpython-bme280 crcmod psutil ntplib pytz --break-system-packages || error "Failed to install Python libraries." sudo pip3 install pyserial requests RPi.GPIO adafruit-circuitpython-bme280 crcmod psutil gpiozero ntplib pytz --break-system-packages || error "Failed to install Python libraries."
# Ask user if they want to set up SSH keys
read -p "Do you want to set up an SSH key for /var/www? (y/n): " answer
answer=${answer,,} # Convert to lowercase
if [[ "$answer" == "y" ]]; then
info "Setting up SSH keys..."
sudo mkdir -p /var/www/.ssh
sudo chmod 700 /var/www/.ssh
if [[ ! -f /var/www/.ssh/id_rsa ]]; then
sudo ssh-keygen -t rsa -b 4096 -f /var/www/.ssh/id_rsa -N ""
success "SSH key generated successfully."
else
warning "SSH key already exists. Skipping key generation."
fi
sudo ssh-copy-id -i /var/www/.ssh/id_rsa.pub -p 50221 airlab_server1@aircarto.fr || warning "Failed to copy SSH key. Please check the server connection."
success "SSH setup complete!"
else
warning "Skipping SSH key setup."
fi
# Clone the repository (check if it exists first) # Clone the repository (check if it exists first)
REPO_DIR="/var/www/nebuleair_pro_4g" REPO_DIR="/var/www/nebuleair_pro_4g"
if [[ -d "$REPO_DIR" ]]; then if [[ -d "$REPO_DIR" ]]; then
warning "Repository already exists. Skipping clone." warning "Repository already exists. Will update instead of clone."
# Save current directory
local current_dir=$(pwd)
# Navigate to repository directory
cd "$REPO_DIR"
# Stash any local changes
sudo git stash || warning "Failed to stash local changes"
# Pull latest changes
sudo git pull || error "Failed to pull latest changes"
# Return to original directory
cd "$current_dir"
success "Repository updated successfully!"
else else
info "Cloning the NebuleAir Pro 4G repository..." info "Cloning the NebuleAir Pro 4G repository..."
sudo git clone http://gitea.aircarto.fr/PaulVua/nebuleair_pro_4g.git "$REPO_DIR" || error "Failed to clone repository." sudo git clone http://gitea.aircarto.fr/PaulVua/nebuleair_pro_4g.git "$REPO_DIR" || error "Failed to clone repository."
@@ -66,7 +53,6 @@ fi
info "Setting up repository files and permissions..." info "Setting up repository files and permissions..."
sudo mkdir -p "$REPO_DIR/logs" sudo mkdir -p "$REPO_DIR/logs"
sudo touch "$REPO_DIR/logs/app.log" "$REPO_DIR/logs/master.log" "$REPO_DIR/logs/master_errors.log" "$REPO_DIR/wifi_list.csv" sudo touch "$REPO_DIR/logs/app.log" "$REPO_DIR/logs/master.log" "$REPO_DIR/logs/master_errors.log" "$REPO_DIR/wifi_list.csv"
sudo cp "$REPO_DIR/config.json.dist" "$REPO_DIR/config.json"
sudo chmod -R 755 "$REPO_DIR/" sudo chmod -R 755 "$REPO_DIR/"
sudo chown -R www-data:www-data "$REPO_DIR/" sudo chown -R www-data:www-data "$REPO_DIR/"
sudo git config --global core.fileMode false sudo git config --global core.fileMode false
@@ -91,6 +77,15 @@ else
warning "Database creation script not found." warning "Database creation script not found."
fi fi
# Set config
info "Set config..."
if [[ -f "$REPO_DIR/sqlite/set_config.py" ]]; then
sudo /usr/bin/python3 "$REPO_DIR/sqlite/set_config.py" || error "Failed to set config."
success "Databases created successfully."
else
warning "Database creation script not found."
fi
# Configure Apache # Configure Apache
info "Configuring Apache..." info "Configuring Apache..."
APACHE_CONF="/etc/apache2/sites-available/000-default.conf" APACHE_CONF="/etc/apache2/sites-available/000-default.conf"
@@ -105,7 +100,7 @@ fi
# Add sudo authorization (prevent duplicate entries) # Add sudo authorization (prevent duplicate entries)
info "Setting up sudo authorization..." info "Setting up sudo authorization..."
if ! sudo grep -q "/usr/bin/nmcli" /etc/sudoers; then if ! sudo grep -q "/usr/bin/nmcli" /etc/sudoers; then
echo -e "ALL ALL=(ALL) NOPASSWD: /usr/bin/nmcli, /usr/sbin/reboot\nwww-data ALL=(ALL) NOPASSWD: /usr/bin/git pull\nwww-data ALL=(ALL) NOPASSWD: /usr/bin/ssh\nwww-data ALL=(ALL) NOPASSWD: /usr/bin/python3 *" | sudo tee -a /etc/sudoers > /dev/null echo -e "ALL ALL=(ALL) NOPASSWD: /usr/bin/nmcli, /usr/sbin/reboot\nwww-data ALL=(ALL) NOPASSWD: /usr/bin/git pull\nwww-data ALL=(ALL) NOPASSWD: /usr/bin/ssh\nwww-data ALL=(ALL) NOPASSWD: /usr/bin/python3 * www-data ALL=(ALL) NOPASSWD: /bin/systemctl * www-data ALL=(ALL) NOPASSWD: /var/www/nebuleair_pro_4g/*" | sudo tee -a /etc/sudoers > /dev/null
success "Sudo authorization added." success "Sudo authorization added."
else else
warning "Sudo authorization already set. Skipping." warning "Sudo authorization already set. Skipping."
@@ -133,7 +128,6 @@ success "I2C ports enabled."
info "Creates sqlites databases..." info "Creates sqlites databases..."
/usr/bin/python3 /var/www/nebuleair_pro_4g/sqlite/create_db.py /usr/bin/python3 /var/www/nebuleair_pro_4g/sqlite/create_db.py
# Completion message # Completion message
success "Setup completed successfully!" success "Setup completed successfully!"
info "System will reboot in 5 seconds..." info "System will reboot in 5 seconds..."

View File

@@ -22,13 +22,19 @@ error() { echo -e "${RED}[ERROR]${NC} $1"; exit 1; }
if [[ "$EUID" -ne 0 ]]; then if [[ "$EUID" -ne 0 ]]; then
error "This script must be run as root. Use 'sudo ./installation.sh'" error "This script must be run as root. Use 'sudo ./installation.sh'"
fi fi
REPO_DIR="/var/www/nebuleair_pro_4g"
#set up the RTC #set up the RTC
info "Set up the RTC" info "Set up the RTC"
/usr/bin/python3 /var/www/nebuleair_pro_4g/RTC/set_with_NTP.py /usr/bin/python3 /var/www/nebuleair_pro_4g/RTC/set_with_NTP.py
#Check SARA R4 connection #Wake up SARA
info "Check SARA R4 connection" info "Wake Up SARA"
pinctrl set 16 op
pinctrl set 16 dh
sleep 5
#Check SARA connection
info "Check SARA connection"
/usr/bin/python3 /var/www/nebuleair_pro_4g/SARA/sara.py ttyAMA2 ATI 2 /usr/bin/python3 /var/www/nebuleair_pro_4g/SARA/sara.py ttyAMA2 ATI 2
#set up SARA R4 APN #set up SARA R4 APN
@@ -44,45 +50,19 @@ info "Connect SARA R4 to network"
python3 /var/www/nebuleair_pro_4g/SARA/sara_connectNetwork.py ttyAMA2 20810 60 python3 /var/www/nebuleair_pro_4g/SARA/sara_connectNetwork.py ttyAMA2 20810 60
#Need to create the two service #Need to create the two service
# 1. master_nebuleair # 1. start the scripts to set-up the services
# 2. rtc_save_to_db # 2. rtc_save_to_db
#1. Add master_nebuleair.service #1. set-up the services (SARA, NPM, BME280, etc)
SERVICE_FILE="/etc/systemd/system/master_nebuleair.service" info "Setting up systemd services..."
info "Setting up systemd service for master_nebuleair..."
# Create the systemd service file (overwrite if necessary)
sudo bash -c "cat > $SERVICE_FILE" <<EOF
[Unit]
Description=Master manager for the Python loop scripts
After=network.target
[Service]
ExecStart=/usr/bin/python3 /var/www/nebuleair_pro_4g/master.py
Restart=always
User=root
WorkingDirectory=/var/www/nebuleair_pro_4g
StandardOutput=append:/var/www/nebuleair_pro_4g/logs/master.log
StandardError=append:/var/www/nebuleair_pro_4g/logs/master_errors.log
[Install]
WantedBy=multi-user.target
EOF
success "Systemd service file created: $SERVICE_FILE"
# 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 master_nebuleair.service
# Start the service immediately
info "Starting the service..."
sudo systemctl start master_nebuleair.service
if [[ -f "$REPO_DIR/services/setup_services.sh" ]]; then
sudo chmod +x "$REPO_DIR/services/setup_services.sh"
sudo "$REPO_DIR/services/setup_services.sh" || warning "Failed to set up systemd services"
success "Systemd services set up successfully."
else
warning "Systemd services setup script not found."
fi
#2. Add rtc_save_to_db.service #2. Add rtc_save_to_db.service
SERVICE_FILE_2="/etc/systemd/system/rtc_save_to_db.service" SERVICE_FILE_2="/etc/systemd/system/rtc_save_to_db.service"

View File

@@ -54,6 +54,8 @@ CSV PAYLOAD (AirCarto Servers)
22 -> solar_voltage 22 -> solar_voltage
23 -> solar_power 23 -> solar_power
24 -> charger_status 24 -> charger_status
25 -> Wind speed
26 -> Wind direction
JSON PAYLOAD (Micro-Spot Servers) JSON PAYLOAD (Micro-Spot Servers)
Same as NebuleAir wifi Same as NebuleAir wifi
@@ -163,7 +165,7 @@ def blink_led(pin, blink_count, delay=1):
finally: finally:
GPIO.output(pin, GPIO.LOW) # Ensure LED is off GPIO.output(pin, GPIO.LOW) # Ensure LED is off
print(f"LED on GPIO {pin} turned OFF (cleanup avoided)") #print(f"LED on GPIO {pin} turned OFF (cleanup avoided)")
#get config data from SQLite table #get config data from SQLite table
def load_config_sqlite(): def load_config_sqlite():
@@ -198,30 +200,6 @@ def load_config_sqlite():
print(f"Error loading config from SQLite: {e}") print(f"Error loading config from SQLite: {e}")
return {} return {}
def load_config_scripts_sqlite():
"""
Load script configuration data from SQLite config_scripts_table
Returns:
dict: Script paths as keys and enabled status as boolean values
"""
try:
# Query the config_scripts_table
cursor.execute("SELECT script_path, enabled FROM config_scripts_table")
rows = cursor.fetchall()
# 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)
return scripts_config
except Exception as e:
print(f"Error loading scripts config from SQLite: {e}")
return {}
#Load config #Load config
config = load_config_sqlite() config = load_config_sqlite()
#config #config
@@ -232,17 +210,13 @@ device_latitude_raw = config.get('latitude_raw', 0)
device_longitude_raw = config.get('longitude_raw', 0) device_longitude_raw = config.get('longitude_raw', 0)
modem_version=config.get('modem_version', "") modem_version=config.get('modem_version', "")
Sara_baudrate = config.get('SaraR4_baudrate', 115200) Sara_baudrate = config.get('SaraR4_baudrate', 115200)
npm_5channel = config.get('NextPM_5channels', False) #5 canaux du NPM
selected_networkID = int(config.get('SARA_R4_neworkID', 0)) selected_networkID = int(config.get('SARA_R4_neworkID', 0))
send_uSpot = config.get('send_uSpot', False) #envoi sur MicroSpot () send_uSpot = config.get('send_uSpot', False) #envoi sur MicroSpot ()
reset_uSpot_url = False npm_5channel = config.get('npm_5channel', False) #5 canaux du NPM
envea_cairsens= config.get('envea', False)
#config_scripts wind_meter= config.get('windMeter', False)
config_scripts = load_config_scripts_sqlite() bme_280_config = config.get('BME280', False)
bme_280_config = config_scripts.get('BME280/get_data_v2.py', False) mppt_charger= config.get('MPPT', False)
envea_cairsens= config_scripts.get('envea/read_value_v2.py', False)
mppt_charger= config_scripts.get('MPPT/read.py', False)
wind_meter= config_scripts.get('windMeter/read.py', False)
#update device id in the payload json #update device id in the payload json
payload_json["nebuleairid"] = device_id payload_json["nebuleairid"] = device_id
@@ -265,6 +239,9 @@ def read_complete_response(serial_connection, timeout=2, end_of_response_timeout
''' '''
Fonction très importante !!! Fonction très importante !!!
Reads the complete response from a serial connection and waits for specific lines. Reads the complete response from a serial connection and waits for specific lines.
timeout -> temps d'attente de la réponse de la première ligne (assez rapide car le SARA répond direct avec la commande recue)
end_of_response_timeout -> le temps d'inactivité entre deux lignes imprimées (plus long dans certain cas: le SARA mouline avant de finir vraiment)
wait_for_lines -> si on rencontre la string la fonction s'arrete
''' '''
if wait_for_lines is None: if wait_for_lines is None:
wait_for_lines = [] # Default to an empty list if not provided wait_for_lines = [] # Default to an empty list if not provided
@@ -366,6 +343,116 @@ def send_error_notification(device_id, error_type, additional_info=None):
return False return False
def modem_hardware_reboot():
"""
Performs a hardware reboot using transistors connected to pin 16 and 20:
pin 16 set to SARA GND
pin 20 set to SARA ON (not used)
LOW -> cut the current
HIGH -> current flow
"""
print('<span style="color: orange;font-weight: bold;">🔄 Hardware SARA reboot 🔄</span>')
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
GPIO.output(SARA_power_GPIO, GPIO.LOW)
time.sleep(2)
GPIO.output(SARA_power_GPIO, GPIO.HIGH)
time.sleep(2)
print("Checking if modem is responsive...")
for attempt in range(5):
ser_sara.write(b'AT\r')
response_check = read_complete_response(ser_sara, wait_for_lines=["OK"], debug=True)
if response_check and "OK" in response_check:
print("✅ Modem is responsive after reboot.")
return True
print(f"⏳ Waiting for modem... attempt {attempt + 1}")
time.sleep(2)
else:
print("❌ Modem not responding after reboot.")
return False
def reset_PSD_CSD_connection():
"""
Function that reset the PSD CSD connection for the SARA R5
returns true or false
"""
print("Reseting PDP connection ")
pdp_reset_success = True
# 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 pdp_reset_success
def reset_server_hostname(profile_id):
"""
Function that reset server hostname (URL) connection for the SARA R5
returns true or false
"""
print("Reseting Server Hostname connection ")
http_reset_success = False # Default fallback
if profile_id == 0:
print('<span style="color: orange;font-weight: bold;">🔧 Resetting AirCarto HTTP Profile</span>')
command = f'AT+UHTTP={profile_id},1,"data.nebuleair.fr"\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)
http_reset_success = response_SARA_5 is not None and "OK" in response_SARA_5
if not http_reset_success:
print("⚠️ AirCarto HTTP profile reset failed")
elif profile_id ==1:
pass # TODO: implement handling for profile 1
else:
print(f"❌ Unsupported profile ID: {profile_id}")
http_reset_success = False
return http_reset_success
def modem_complete_reboot_and_reinitialize(modem_version, aircarto_profile_id): def modem_complete_reboot_and_reinitialize(modem_version, aircarto_profile_id):
""" """
Performs a complete modem restart sequence: Performs a complete modem restart sequence:
@@ -384,7 +471,7 @@ def modem_complete_reboot_and_reinitialize(modem_version, aircarto_profile_id):
print('<span style="color: orange;font-weight: bold;">🔄 Complete SARA reboot and reinitialize sequence 🔄</span>') 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 # Step 1: Reboot the modem - Integrated modem_software_reboot logic
print('<span style="color: orange;font-weight: bold;">🔄 Software SARA reboot! 🔄</span>') print('<span style="color: orange;font-weight: bold;">🔄 Software SARA reboot (CFUN)! 🔄</span>')
# Use different commands based on modem version # Use different commands based on modem version
if 'R5' in modem_version: # For SARA-R5 series if 'R5' in modem_version: # For SARA-R5 series
@@ -392,6 +479,8 @@ def modem_complete_reboot_and_reinitialize(modem_version, aircarto_profile_id):
else: # For SARA-R4 series else: # For SARA-R4 series
command = 'AT+CFUN=15\r' # Factory reset for R4 command = 'AT+CFUN=15\r' # Factory reset for R4
#ATTENTION : AT+CFUN=16 sometimes causes the modem to reset before replying OK
ser_sara.write(command.encode('utf-8')) ser_sara.write(command.encode('utf-8'))
response = read_complete_response(ser_sara, wait_for_lines=["OK", "ERROR"], debug=True) response = read_complete_response(ser_sara, wait_for_lines=["OK", "ERROR"], debug=True)
@@ -400,84 +489,155 @@ def modem_complete_reboot_and_reinitialize(modem_version, aircarto_profile_id):
print("</p>", end="") print("</p>", end="")
# Check if reboot command was acknowledged # Check if reboot command was acknowledged
reboot_success = response is not None and "OK" in response if response is None or ("OK" not in response and "ERROR" in response):
if not reboot_success: print("⚠️ Reboot command may have failed or modem restarted before responding.")
print("⚠️ Modem reboot command failed") # Still continue, as the modem may have rebooted correctly
return False else:
print("✅ Modem acknowledged reboot command.")
# Step 2: Wait for the modem to restart (adjust time as needed) # Step 2: Wait for the modem to restart (adjust time as needed)
print("Waiting for modem to restart...") print("Waiting for modem to restart...")
time.sleep(15) # 15 seconds should be enough for most modems to restart time.sleep(7) # 7 seconds should be enough for most modems to restart
# Step 3: Check if modem is responsive after reboot # Step 3: Check if modem is responsive after reboot
print("Checking if modem is responsive...") 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) for attempt in range(5):
if response_check is None or "OK" not in response_check: ser_sara.write(b'AT\r')
print("⚠️ Modem not responding after reboot") response_check = read_complete_response(ser_sara, wait_for_lines=["OK"], debug=True)
if response_check and "OK" in response_check:
print("✅ Modem is responsive after reboot.")
break
print(f"⏳ Waiting for modem... attempt {attempt + 1}")
time.sleep(2)
else:
print("❌ Modem not responding after reboot.")
return False return False
print("✅ Modem restarted successfully") # Step 4: Reset AirCarto HTTP Profile
print('<span style="color: orange;font-weight: bold;">🔧 Resetting AirCarto 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="")
# Step 4: Reset the HTTP Profile print("SET URL")
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' command = f'AT+UHTTP={aircarto_profile_id},1,"data.nebuleair.fr"\r'
ser_sara.write(command.encode('utf-8')) ser_sara.write((command + '\r').encode('utf-8'))
responseResetHTTP = read_complete_response(ser_sara, timeout=5, end_of_response_timeout=5, response_SARA_5 = read_complete_response(ser_sara, wait_for_lines=["OK"])
wait_for_lines=["OK", "+CME ERROR"], debug=True) print(response_SARA_5)
print('<p class="text-danger-emphasis">') time.sleep(1)
print(responseResetHTTP)
print("</p>", end="")
http_reset_success = responseResetHTTP is not None and "OK" in responseResetHTTP
http_reset_success = response_SARA_5 is not None and "OK" in response_SARA_5
if not http_reset_success: if not http_reset_success:
print("⚠️ HTTP profile reset failed") print("⚠️ AirCarto HTTP profile reset failed")
# Continue anyway, don't return False here # Continue anyway, don't return False here
# Step 5: For SARA-R5, reset the PDP connection if send_uSpot:
pdp_reset_success = True print('<span style="color: orange;font-weight: bold;">🔧 Resetting uSpot HTTP Profile</span>')
if modem_version == "SARA-R500": uSpot_profile_id = 1
print("⚠️ Need to reset PDP connection for SARA-R500") uSpot_url="api-prod.uspot.probesys.net"
security_profile_id = 1
# Activate PDP context 1 #step 1: import the certificate
print('➡️ Activate PDP context 1') print("➡️ import certificate")
command = f'AT+CGACT=1,1\r' certificate_name = "e6"
ser_sara.write(command.encode('utf-8')) with open("/var/www/nebuleair_pro_4g/SARA/SSL/certificate/e6.pem", "rb") as cert_file:
response_pdp1 = read_complete_response(ser_sara, wait_for_lines=["OK"]) certificate = cert_file.read()
print(response_pdp1, end="") size_of_string = len(certificate)
pdp_reset_success = pdp_reset_success and (response_pdp1 is not None and "OK" in response_pdp1)
# 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, wait_for_lines=[">"])
print(response_SARA_1)
time.sleep(0.5)
print("➡️ add certificate")
ser_sara.write(certificate)
response_SARA_2 = read_complete_response(ser_sara, wait_for_lines=["OK"])
print(response_SARA_2)
time.sleep(0.5)
# op_code: 0 -> certificate validation level
# param_val : 0 -> Level 0 No validation; 1-> Level 1 Root certificate validation
print("Set the security profile (params)")
certification_level=0
command = f'AT+USECPRF={security_profile_id},0,{certification_level}\r'
ser_sara.write((command + '\r').encode('utf-8'))
response_SARA_5b = read_complete_response(ser_sara, wait_for_lines=["OK"])
print(response_SARA_5b)
time.sleep(0.5)
# op_code: 1 -> minimum SSL/TLS version
# param_val : 0 -> any; server can use any version for the connection; 1-> LSv1.0; 2->TLSv1.1; 3->TLSv1.2;
print("Set the security profile (params)")
minimum_SSL_version = 0
command = f'AT+USECPRF={security_profile_id},1,{minimum_SSL_version}\r'
ser_sara.write((command + '\r').encode('utf-8'))
response_SARA_5bb = read_complete_response(ser_sara, wait_for_lines=["OK"])
print(response_SARA_5bb)
time.sleep(0.5)
#op_code: 2 -> legacy cipher suite selection
# 0 (factory-programmed value): a list of default cipher suites is proposed at the beginning of handshake process, and a cipher suite will be negotiated among the cipher suites proposed in the list.
print("Set cipher")
cipher_suite = 0
command = f'AT+USECPRF={security_profile_id},2,{cipher_suite}\r'
ser_sara.write((command + '\r').encode('utf-8'))
response_SARA_5cc = read_complete_response(ser_sara, wait_for_lines=["OK"])
print(response_SARA_5cc)
time.sleep(0.5)
# op_code: 3 -> trusted root certificate internal name
print("Set the security profile (choose cert)")
command = f'AT+USECPRF={security_profile_id},3,"{certificate_name}"\r'
ser_sara.write((command + '\r').encode('utf-8'))
response_SARA_5c = read_complete_response(ser_sara, wait_for_lines=["OK"])
print(response_SARA_5c)
time.sleep(0.5)
# op_code: 10 -> SNI (server name indication)
print("Set the SNI")
command = f'AT+USECPRF={security_profile_id},10,"{uSpot_url}"\r'
ser_sara.write((command + '\r').encode('utf-8'))
response_SARA_5cf = read_complete_response(ser_sara, wait_for_lines=["OK"])
print(response_SARA_5cf)
time.sleep(0.5)
#step 4: set url (op_code = 1)
print("SET URL")
command = f'AT+UHTTP={uSpot_profile_id},1,"{uSpot_url}"\r'
ser_sara.write((command + '\r').encode('utf-8'))
response_SARA_5 = read_complete_response(ser_sara, wait_for_lines=["OK"])
print(response_SARA_5)
time.sleep(1) time.sleep(1)
# Set the PDP type #step 4: set PORT (op_code = 5)
print('➡️ Set the PDP type to IPv4 referring to the output of the +CGDCONT read command') print("SET PORT")
command = f'AT+UPSD=0,0,0\r' port = 443
ser_sara.write(command.encode('utf-8')) command = f'AT+UHTTP={uSpot_profile_id},5,{port}\r'
response_pdp2 = read_complete_response(ser_sara, wait_for_lines=["OK"]) ser_sara.write((command + '\r').encode('utf-8'))
print(response_pdp2, end="") response_SARA_55 = read_complete_response(ser_sara, wait_for_lines=["OK"])
pdp_reset_success = pdp_reset_success and (response_pdp2 is not None and "OK" in response_pdp2) print(response_SARA_55)
time.sleep(1) time.sleep(1)
# Profile #0 is mapped on CID=1 #step 4: set url to SSL (op_code = 6) (http_secure = 1 for HTTPS)(USECMNG_PROFILE = 2)
print('➡️ Profile #0 is mapped on CID=1.') print("SET SSL")
command = f'AT+UPSD=0,100,1\r' http_secure = 1
ser_sara.write(command.encode('utf-8')) command = f'AT+UHTTP={uSpot_profile_id},6,{http_secure},{security_profile_id}\r'
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')) ser_sara.write(command.encode('utf-8'))
response_pdp4 = read_complete_response(ser_sara, wait_for_lines=["OK", "+UUPSDA"]) response_SARA_5fg = read_complete_response(ser_sara, wait_for_lines=["OK"])
print(response_pdp4, end="") print(response_SARA_5fg)
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) time.sleep(1)
if not pdp_reset_success:
print("⚠️ PDP connection reset had some issues")
# Return overall success # Return overall success
return http_reset_success and pdp_reset_success return http_reset_success and pdp_reset_success
@@ -491,17 +651,17 @@ try:
''' '''
print('<h3>START LOOP</h3>') print('<h3>START LOOP</h3>')
print(f'Modem version: {modem_version}') #print(f'Modem version: {modem_version}')
#Local timestamp #Local timestamp
#ATTENTION: #ATTENTION:
# -> RTC module can be deconnected "" # -> RTC module can be deconnected ""
# -> RTC module can be out of time like "2000-01-01T00:55:21Z" # -> 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") cursor.execute("SELECT * FROM timestamp_table LIMIT 1")
row = cursor.fetchone() # Get the first (and only) row row = cursor.fetchone() # Get the first (and only) row
rtc_time_str = row[1] # '2025-02-07 12:30:45' ou '2000-01-01 00:55:21' ou 'not connected' 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) print(f"Getting local timestamp: {rtc_time_str}")
if rtc_time_str == 'not connected': if rtc_time_str == 'not connected':
print("⛔ Atttention RTC module not connected⛔") print("⛔ Atttention RTC module not connected⛔")
@@ -515,14 +675,14 @@ try:
print("⛔ Attention: RTC has been reset to default date ⛔") print("⛔ Attention: RTC has been reset to default date ⛔")
rtc_status = "reset" rtc_status = "reset"
else: else:
print("✅ RTC timestamp is valid") #print("✅ RTC timestamp is valid")
rtc_status = "valid" rtc_status = "valid"
# Always convert to InfluxDB format # Always convert to InfluxDB format
# Convert to InfluxDB RFC3339 format with UTC 'Z' suffix # Convert to InfluxDB RFC3339 format with UTC 'Z' suffix
influx_timestamp = dt_object.strftime('%Y-%m-%dT%H:%M:%SZ') influx_timestamp = dt_object.strftime('%Y-%m-%dT%H:%M:%SZ')
rtc_status = "valid" rtc_status = "valid"
print(influx_timestamp) #print(influx_timestamp)
#NEXTPM #NEXTPM
# We take the last measures (order by rowid and not by timestamp) # We take the last measures (order by rowid and not by timestamp)
@@ -628,6 +788,20 @@ try:
#Wind meter #Wind meter
if wind_meter: if wind_meter:
print("Getting wind meter values") print("Getting wind meter values")
cursor.execute("SELECT * FROM data_WIND ORDER BY rowid DESC LIMIT 1")
last_row = cursor.fetchone()
if last_row:
print("SQLite DB last available row:", last_row)
wind_speed = last_row[1]
wind_direction = last_row[2]
#Add data to payload CSV
payload_csv[25] = wind_speed
payload_csv[26] = wind_direction
else:
print("No data available in the database.")
#MPPT charger #MPPT charger
if mppt_charger: if mppt_charger:
@@ -651,40 +825,67 @@ try:
else: else:
print("No data available in the database.") print("No data available in the database.")
print("Verify SARA R4 connection") #print("Verify SARA connection (AT)")
# Getting the LTE Signal (AT+CSQ)
print("Getting SARA LTE signal")
command = f'AT+CSQ\r'
ser_sara.write((command + '\r').encode('utf-8'))
response2 = read_complete_response(ser_sara, wait_for_lines=["OK", "ERROR", "+CME ERROR","Socket:bind"])
# Getting the LTE Signal
print("Getting LTE signal")
ser_sara.write(b'AT+CSQ\r')
response2 = read_complete_response(ser_sara, wait_for_lines=["OK", "ERROR", "+CME ERROR"])
print('<p class="text-danger-emphasis">') print('<p class="text-danger-emphasis">')
print(response2) print(response2)
print("</p>", end="") print("</p>", end="")
#Here it's possible that the SARA do not repond at all or send a error message #Here it's possible that the SARA do not repond at all or send a error message
#-> TO DO : harware reboot
#-> send notification #-> send notification
#-> hardware reboot
#-> end loop, no need to continue #-> end loop, no need to continue
#1. No answer at all form SARA #1. No answer at all form SARA
if response2 is None or response2 == "": if response2 is None or response2 == "":
print("No answer from SARA module") print("ATTENTION: No answer from SARA module")
print('🛑STOP LOOP🛑') print('🛑STOP LOOP🛑')
print("<hr>") print("<hr>")
#Send notification (WIFI) #Send notification (WIFI)
send_error_notification(device_id, "serial_error") send_error_notification(device_id, "SERIAL ISSUE ->no answer from sara")
#Hardware Reboot
hardware_reboot_success = modem_hardware_reboot()
if hardware_reboot_success:
print("✅Modem successfully rebooted and reinitialized")
else:
print("⛔There were issues with the modem reboot/reinitialize process")
#end loop #end loop
sys.exit() sys.exit()
#2. si on a une erreur #2. si on a une reponse du SARA mais c'est une erreur
elif "+CME ERROR" in response2: elif "+CME ERROR" in response2:
print(f"SARA module returned error: {response2}") print(f"SARA module returned error: {response2}")
print("The CSQ command is not supported by this module or in its current state") 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("ATTENTION: SARA is connected over serial but CSQ command not supported")
print('🛑STOP LOOP🛑') print('🛑STOP LOOP🛑')
print("<hr>")
#end loop
sys.exit()
#3. On peut avoir une erreur de type "Socket:bind: Treck error 222 : Invalid argument"
elif "Socket:bind: Treck error" in response2:
print(f"SARA module returned error: {response2}")
print("ATTENTION: low-level error from the Treck TCP/IP stack")
print('🛑STOP LOOP🛑')
print("<hr>")
#Send notification (WIFI)
send_error_notification(device_id, "SERIAL ISSUE -> Treck TCP/IP stack error")
#hardware reboot
hardware_reboot_success = modem_hardware_reboot()
if hardware_reboot_success:
print("✅Modem successfully rebooted and reinitialized")
else:
print("⛔There were issues with the modem reboot/reinitialize process")
#end loop #end loop
sys.exit() sys.exit()
@@ -706,7 +907,7 @@ try:
command = f'AT+COPS=1,2,{selected_networkID}\r' command = f'AT+COPS=1,2,{selected_networkID}\r'
#command = f'AT+COPS=0\r' #command = f'AT+COPS=0\r'
ser_sara.write(command.encode('utf-8')) ser_sara.write(command.encode('utf-8'))
responseReconnect = read_complete_response(ser_sara, timeout=20, end_of_response_timeout=20) responseReconnect = read_complete_response(ser_sara, timeout=20, end_of_response_timeout=20, wait_for_lines=["OK", "+CME ERROR", "ERROR"], debug=True)
print('<p class="text-danger-emphasis">') print('<p class="text-danger-emphasis">')
print(responseReconnect) print(responseReconnect)
print("</p>", end="") print("</p>", end="")
@@ -721,10 +922,15 @@ try:
''' '''
SEND TO AIRCARTO ____ _____ _ _ ____ _ ___ ____ ____ _ ____ _____ ___
/ ___|| ____| \ | | _ \ / \ |_ _| _ \ / ___| / \ | _ \_ _/ _ \
\___ \| _| | \| | | | | / _ \ | || |_) | | / _ \ | |_) || || | | |
___) | |___| |\ | |_| | / ___ \ | || _ <| |___ / ___ \| _ < | || |_| |
|____/|_____|_| \_|____/ /_/ \_\___|_| \_\\____/_/ \_\_| \_\|_| \___/
''' '''
print('➡️<p class="fw-bold">SEND TO AIRCARTO SERVERS</p>') print('<p class="fw-bold">➡️SEND TO AIRCARTO SERVERS</p>', end="")
# Write Data to saraR4 # Write Data to saraR4
# 1. Open sensordata_csv.json (with correct data size) # 1. Open sensordata_csv.json (with correct data size)
csv_string = ','.join(str(value) if value is not None else '' for value in payload_csv) csv_string = ','.join(str(value) if value is not None else '' for value in payload_csv)
@@ -732,7 +938,7 @@ try:
print("Open JSON:") print("Open JSON:")
command = f'AT+UDWNFILE="sensordata_csv.json",{size_of_string}\r' command = f'AT+UDWNFILE="sensordata_csv.json",{size_of_string}\r'
ser_sara.write(command.encode('utf-8')) ser_sara.write(command.encode('utf-8'))
response_SARA_1 = read_complete_response(ser_sara, wait_for_lines=[">"], debug=True) response_SARA_1 = read_complete_response(ser_sara, wait_for_lines=[">"], debug=False)
print('<p class="text-danger-emphasis">') print('<p class="text-danger-emphasis">')
print(response_SARA_1) print(response_SARA_1)
print("</p>", end="") print("</p>", end="")
@@ -742,22 +948,21 @@ try:
#2. Write to shell #2. Write to shell
print("Write data to memory:") print("Write data to memory:")
ser_sara.write(csv_string.encode()) ser_sara.write(csv_string.encode())
response_SARA_2 = read_complete_response(ser_sara, wait_for_lines=["OK"], debug=True) response_SARA_2 = read_complete_response(ser_sara, wait_for_lines=["OK"], debug=False)
print('<p class="text-danger-emphasis">') print(f'<p class="text-danger-emphasis">{response_SARA_2.strip()}</p>', end="")
print(response_SARA_2)
print("</p>", end="")
#3. Send to endpoint (with device ID) #3. Send to endpoint (with device ID)
print("Send data (POST REQUEST):") print("Send data (POST REQUEST):")
command= f'AT+UHTTPC={aircarto_profile_id},4,"/pro_4G/data.php?sensor_id={device_id}&lat={device_latitude_raw}&long={device_longitude_raw}&datetime={influx_timestamp}","aircarto_server_response.txt","sensordata_csv.json",4\r' command= f'AT+UHTTPC={aircarto_profile_id},4,"/pro_4G/data.php?sensor_id={device_id}&lat={device_latitude_raw}&long={device_longitude_raw}&datetime={influx_timestamp}","aircarto_server_response.txt","sensordata_csv.json",4\r'
print("sending:") #print("sending:")
print('<p class="text-danger-emphasis">') #print('<p class="text-danger-emphasis">')
print(command) #print(command)
print("</p>", end="") #print("</p>", end="")
ser_sara.write(command.encode('utf-8')) ser_sara.write(command.encode('utf-8'))
response_SARA_3 = read_complete_response(ser_sara, timeout=5, end_of_response_timeout=120, wait_for_lines=["+UUHTTPCR", "+CME ERROR", "ERROR"], debug=True) response_SARA_3 = read_complete_response(ser_sara, timeout=5, end_of_response_timeout=120, wait_for_lines=["+UUHTTPCR", "+CME ERROR", "ERROR"], debug=True)
print("receiving:") #print("receiving:")
print('<p class="text-danger-emphasis">') print('<p class="text-danger-emphasis">')
print(response_SARA_3) print(response_SARA_3)
print("</p>", end="") print("</p>", end="")
@@ -842,16 +1047,36 @@ try:
# Extract just the error code # Extract just the error code
error_code = extract_error_code(response_SARA_9) error_code = extract_error_code(response_SARA_9)
if error_code is not None: if error_code is not None:
# Display interpretation based on error code # Display interpretation based on error code
if error_code == 0: if error_code == 0:
print('<p class="text-success">No error detected</p>') print('<p class="text-success">No error detected</p>')
elif error_code == 4: elif error_code == 4:
print('<p class="text-danger">Error 4: Invalid server Hostname</p>') print('<p class="text-danger">Error 4: Invalid server Hostname</p>')
send_error_notification(device_id, "UHTTPER (error n°4) -> Invalid Server Hostname")
server_hostname_resets = reset_server_hostname(aircarto_profile_id)
if server_hostname_resets:
print("✅server hostname reset successfully")
else:
print("⛔There were issues with the modem server hostname reinitialize process")
elif error_code == 11: elif error_code == 11:
print('<p class="text-danger">Error 11: Server connection error</p>') print('<p class="text-danger">Error 11: Server connection error</p>')
elif error_code == 22: 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>') print('<p class="text-danger">⚠Error 22: PSD or CSD connection not established (SARA-R5 need to reset PDP conection)⚠️</p>')
send_error_notification(device_id, "UHTTPER (error n°22) -> PSD or CSD connection not established")
psd_csd_resets = reset_PSD_CSD_connection()
if psd_csd_resets:
print("✅PSD CSD connection reset successfully")
else:
print("⛔There were issues with the modem CSD PSD reinitialize process")
elif error_code == 26:
print('<p class="text-danger">Error 26: Connection timed out</p>')
send_error_notification(device_id, "UHTTPER (error n°26) -> Connection timed out")
elif error_code == 44:
print('<p class="text-danger">Error 44: Connection lost</p>')
send_error_notification(device_id, "UHTTPER (error n°44) -> Connection lost")
elif error_code == 73: elif error_code == 73:
print('<p class="text-danger">Error 73: Secure socket connect error</p>') print('<p class="text-danger">Error 73: Secure socket connect error</p>')
else: else:
@@ -861,11 +1086,11 @@ try:
#Software Reboot #Software Reboot
software_reboot_success = modem_complete_reboot_and_reinitialize(modem_version, aircarto_profile_id) #software_reboot_success = modem_complete_reboot_and_reinitialize(modem_version, aircarto_profile_id)
if software_reboot_success: #if software_reboot_success:
print("Modem successfully rebooted and reinitialized") # print("✅Modem successfully rebooted and reinitialized")
else: #else:
print("There were issues with the modem reboot/reinitialize process") # print("⛔There were issues with the modem reboot/reinitialize process")
# 2.2 code 1 (✅✅HHTP / UUHTTPCR succeded✅✅) # 2.2 code 1 (✅✅HHTP / UUHTTPCR succeded✅✅)
@@ -877,7 +1102,8 @@ try:
#4. Read reply from server #4. Read reply from server
print("Reply from server:") print("Reply from server:")
ser_sara.write(b'AT+URDFILE="aircarto_server_response.txt"\r') command = f'AT+URDFILE="aircarto_server_response.txt""\r'
ser_sara.write((command + '\r').encode('utf-8'))
response_SARA_4 = read_complete_response(ser_sara, wait_for_lines=["OK"], debug=False) response_SARA_4 = read_complete_response(ser_sara, wait_for_lines=["OK"], debug=False)
print('<p class="text-success">') print('<p class="text-success">')
print(response_SARA_4) print(response_SARA_4)
@@ -886,6 +1112,7 @@ try:
#Parse the server datetime #Parse the server datetime
# Extract just the date from the response # Extract just the date from the response
date_string = None date_string = None
server_datetime = ""
date_start = response_SARA_4.find("Date: ") date_start = response_SARA_4.find("Date: ")
if date_start != -1: if date_start != -1:
date_end = response_SARA_4.find("\n", date_start) date_end = response_SARA_4.find("\n", date_start)
@@ -952,7 +1179,7 @@ try:
#Si non ne recoit pas de réponse UHTTPCR #Si non ne recoit pas de réponse UHTTPCR
#on a peut etre une ERROR de type "+CME ERROR: No connection to phone" ou "Operation not allowed" ou "ERROR" #on a peut être une ERROR de type "+CME ERROR: No connection to phone" ou "Operation not allowed" ou "ERROR"
else: else:
print('<span style="color: red;font-weight: bold;">No UUHTTPCR response</span>') print('<span style="color: red;font-weight: bold;">No UUHTTPCR response</span>')
print("Blink red LED") print("Blink red LED")
@@ -1001,7 +1228,7 @@ try:
if "ERROR" in line: if "ERROR" in line:
print("⛔Attention ERROR!⛔") print("⛔Attention ERROR!⛔")
#Send notification (WIFI) #Send notification (WIFI)
send_error_notification(device_id, "sara_error") send_error_notification(device_id, "SARA CME ERROR")
#Software Reboot #Software Reboot
software_reboot_success = modem_complete_reboot_and_reinitialize(modem_version, aircarto_profile_id) software_reboot_success = modem_complete_reboot_and_reinitialize(modem_version, aircarto_profile_id)
@@ -1013,7 +1240,8 @@ try:
#5. empty json #5. empty json
print("Empty SARA memory:") print("Empty SARA memory:")
ser_sara.write(b'AT+UDELFILE="sensordata_csv.json"\r') command = f'AT+UDELFILE="sensordata_csv.json"\r'
ser_sara.write((command + '\r').encode('utf-8'))
response_SARA_5 = read_complete_response(ser_sara, wait_for_lines=["OK","+CME ERROR"], debug=True) response_SARA_5 = read_complete_response(ser_sara, wait_for_lines=["OK","+CME ERROR"], debug=True)
print('<p class="text-danger-emphasis">') print('<p class="text-danger-emphasis">')
print(response_SARA_5) print(response_SARA_5)
@@ -1026,77 +1254,16 @@ try:
''' '''
SEND TO uSPOT _ ____ _
___ ___ _ __ __| | _ _/ ___| _ __ ___ | |_
/ __|/ _ \ '_ \ / _` | | | | \___ \| '_ \ / _ \| __|
\__ \ __/ | | | (_| | | |_| |___) | |_) | (_) | |_
|___/\___|_| |_|\__,_| \__,_|____/| .__/ \___/ \__|
|_|
''' '''
if send_uSpot: if send_uSpot:
print('➡️<p class="fw-bold">SEND TO uSPOT SERVERS</p>') print('<p class="fw-bold">➡️SEND TO uSPOT SERVERS</p>', end="")
if reset_uSpot_url:
#2. Set uSpot URL (profile id = 1)
print('Set uSpot URL')
uSpot_profile_id = 1
uSpot_url="api-prod.uspot.probesys.net"
security_profile_id = 1
#step 1: import the certificate
print("****")
certificate_name = "e6"
with open("/var/www/nebuleair_pro_4g/SARA/SSL/certificate/e6.pem", "rb") as cert_file:
certificate = cert_file.read()
size_of_string = len(certificate)
print("\033[0;33m Import certificate\033[0m")
# AT+USECMNG=0,<type>,<internal_name>,<data_size>
# type-> 0 -> trusted root CA
command = f'AT+USECMNG=0,0,"{certificate_name}",{size_of_string}\r'
ser_sara.write((command + '\r').encode('utf-8'))
response_SARA_1 = read_complete_response(ser_sara)
print(response_SARA_1)
time.sleep(0.5)
print("\033[0;33mAdd certificate\033[0m")
ser_sara.write(certificate)
response_SARA_2 = read_complete_response(ser_sara)
print(response_SARA_2)
time.sleep(0.5)
# SECURITY PROFILE
# op_code: 3 -> trusted root certificate internal name
print("\033[0;33mSet the security profile (choose cert)\033[0m")
command = f'AT+USECPRF={security_profile_id},3,"{certificate_name}"\r'
ser_sara.write((command + '\r').encode('utf-8'))
response_SARA_5c = read_complete_response(ser_sara, wait_for_lines=["OK"])
print(response_SARA_5c)
time.sleep(0.5)
#step 4: set url (op_code = 1)
command = f'AT+UHTTP={uSpot_profile_id},1,"{uSpot_url}"\r'
ser_sara.write(command.encode('utf-8'))
response_SARA_2 = read_complete_response(ser_sara, wait_for_lines=["OK"])
print(response_SARA_2)
time.sleep(1)
#step 4: set PORT (op_code = 5)
print("set port 443")
command = f'AT+UHTTP={uSpot_profile_id},5,443\r'
ser_sara.write((command + '\r').encode('utf-8'))
response_SARA_55 = read_complete_response(ser_sara, wait_for_lines=["OK"])
print(response_SARA_55)
time.sleep(1)
#step 4: set url to SSL (op_code = 6) (http_secure = 1 for HTTPS)(USECMNG_PROFILE = 2)
print("\033[0;33mSET SSL\033[0m")
http_secure = 1
command = f'AT+UHTTP={uSpot_profile_id},6,{http_secure},{security_profile_id}\r'
#command = f'AT+UHTTP={profile_id},6,{http_secure}\r'
ser_sara.write(command.encode('utf-8'))
response_SARA_5 = read_complete_response(ser_sara, wait_for_lines=["OK"])
print(response_SARA_5)
time.sleep(1)
# 1. Open sensordata_json.json (with correct data size) # 1. Open sensordata_json.json (with correct data size)
print("Open JSON:") print("Open JSON:")
@@ -1169,7 +1336,7 @@ try:
led_thread.start() led_thread.start()
# Get error code # Get error code
print("Getting error code") print("Getting error code", end="")
command = f'AT+UHTTPER={uSpot_profile_id}\r' command = f'AT+UHTTPER={uSpot_profile_id}\r'
ser_sara.write(command.encode('utf-8')) ser_sara.write(command.encode('utf-8'))
response_SARA_9b = read_complete_response(ser_sara, wait_for_lines=["OK"], debug=False) response_SARA_9b = read_complete_response(ser_sara, wait_for_lines=["OK"], debug=False)
@@ -1183,40 +1350,80 @@ try:
if error_code == 0: if error_code == 0:
print('<p class="text-success">No error detected</p>') print('<p class="text-success">No error detected</p>')
elif error_code == 4: elif error_code == 4:
print('<p class="text-danger">Error 4: Invalid server Hostname</p>') print('<p class="text-danger">Error 4: Invalid server Hostname</p>', end="")
send_error_notification(device_id, "UHTTPER (4) uSpot Invalid server Hostname")
elif error_code == 11: elif error_code == 11:
print('<p class="text-danger">Error 11: Server connection error</p>') print('<p class="text-danger">Error 11: Server connection error</p>', end="")
elif error_code == 22: elif error_code == 22:
print('<p class="text-danger">Error 22: PSD or CSD connection not established</p>') print('<p class="text-danger">Error 22: PSD or CSD connection not established</p>', end="")
elif error_code == 26:
print('<p class="text-danger">Error 26: Connection timed out</p>')
elif error_code == 44:
print('<p class="text-danger">Error 44: Connection lost</p>')
elif error_code == 73: elif error_code == 73:
print('<p class="text-danger">Error 73: Secure socket connect error</p>') print('<p class="text-danger">Error 73: Secure socket connect error</p>', end="")
send_error_notification(device_id, "uSpot - Secure socket connect error")
#Software Reboot ??
else: else:
print(f'<p class="text-danger">Unknown error code: {error_code}</p>') print(f'<p class="text-danger">Unknown error code: {error_code}</p>',end="")
else: else:
print('<p class="text-danger">Could not extract error code from response</p>') print('<p class="text-danger">Could not extract error code from response</p>', end="")
#Pas forcément un moyen de résoudre le soucis #Pas forcément un moyen de résoudre le soucis
# 2.2 code 1 (HHTP succeded) # 2.2 code 1 (✅✅HHTP / UUHTTPCR succeded✅✅)
else: else:
# Si la commande HTTP a réussi # Si la commande HTTP a réussi
print('<span style="font-weight: bold;">✅✅HTTP operation successful.</span>') print('<span style="font-weight: bold;">✅✅HTTP operation successful.</span>')
print("Blink blue LED") print("Blink blue LED")
led_thread = Thread(target=blink_led, args=(23, 5, 0.5)) led_thread = Thread(target=blink_led, args=(23, 5, 0.5))
led_thread.start() led_thread.start()
#4. Read reply from server #4. Read reply from server
print("Reply from server:") print("Reply from server:")
ser_sara.write(b'AT+URDFILE="uSpot_server_response.txt"\r') command = f'AT+URDFILE="uSpot_server_response.txt"\r'
ser_sara.write((command + '\r').encode('utf-8'))
response_SARA_4b = read_complete_response(ser_sara, wait_for_lines=["OK"], debug=False) response_SARA_4b = read_complete_response(ser_sara, wait_for_lines=["OK"], debug=False)
print('<p class="text-success">') print('<p class="text-success">')
print(response_SARA_4b) print(response_SARA_4b)
print("</p>", end="") print("</p>", end="")
# Initialize http_response_code to 0 as a default value
http_response_code = 0
# Safely extract HTTP code
try:
http_prefix = "HTTP/"
# response_SARA_4b is a string, not a function - use .find() method
http_pos = response_SARA_4b.find(http_prefix)
if http_pos != -1:
# Find the space after the HTTP version
space_pos = response_SARA_4b.find(" ", http_pos)
if space_pos != -1:
# Extract the code after the space
code_start = space_pos + 1
code_end = response_SARA_4b.find(" ", code_start)
if code_end != -1:
# Extract and convert to integer
http_code_str = response_SARA_4b[code_start:code_end]
http_response_code = int(http_code_str)
print(f"HTTP response code: {http_response_code}")
if http_response_code == 201:
print('<span style="font-weight: bold;">✅✅HTTP 201 ressource created.</span>')
except Exception as e:
# If any error occurs during parsing, keep the default value
print(f"Error parsing HTTP code: {e}")
#5. empty json #5. empty json
print("Empty SARA memory:") print("Empty SARA memory:")
ser_sara.write(b'AT+UDELFILE="sensordata_json.json"\r') command = f'AT+UDELFILE="sensordata_json.json"\r'
ser_sara.write((command + '\r').encode('utf-8'))
response_SARA_9t = read_complete_response(ser_sara, wait_for_lines=["OK"], debug=False) response_SARA_9t = read_complete_response(ser_sara, wait_for_lines=["OK"], debug=False)
print(response_SARA_9t) print(response_SARA_9t)

View File

@@ -132,7 +132,7 @@ def run_script(script_name, interval, delay=0):
if not is_script_locked(): if not is_script_locked():
create_lock_file() create_lock_file()
try: try:
subprocess.run(["python3", script_path]) subprocess.run(["python3", script_path], timeout=200)
finally: finally:
remove_lock_file() remove_lock_file()
else: else:

18
services/README.md Normal file
View File

@@ -0,0 +1,18 @@
# NebuleAir Pro Services
Les scripts importants tournent à l'aide d'un service et d'un timer associé.
Pour les installer:
sudo chmod +x /var/www/nebuleair_pro_4g/services/setup_services.sh
sudo /var/www/nebuleair_pro_4g/services/setup_services.sh
Supprimer l'ancien master:
sudo systemctl stop master_nebuleair.service
sudo systemctl disable master_nebuleair.service
# Check les services
SARA:
sudo systemctl status nebuleair-sara-data.service

View File

@@ -0,0 +1,39 @@
#!/bin/bash
# File: /var/www/nebuleair_pro_4g/services/check_services.sh
# Purpose: Check status of all NebuleAir services and logs
# Install:
# sudo chmod +x /var/www/nebuleair_pro_4g/services/check_services.sh
# sudo /var/www/nebuleair_pro_4g/services/check_services.sh
echo "=== NebuleAir Services Status ==="
echo ""
# Check status of all timers
echo "--- TIMER STATUS ---"
systemctl list-timers | grep nebuleair
echo ""
# Check status of all services
echo "--- SERVICE STATUS ---"
for service in npm envea sara bme280 mppt db-cleanup; do
status=$(systemctl is-active nebuleair-$service-data.service)
timer_status=$(systemctl is-active nebuleair-$service-data.timer)
echo "nebuleair-$service-data: Service=$status, Timer=$timer_status"
done
echo ""
# Show recent logs for each service
echo "--- RECENT LOGS (last 5 entries per service) ---"
for service in npm envea sara bme280 mppt db-cleanup; do
echo "[$service service logs]"
journalctl -u nebuleair-$service-data.service -n 5 --no-pager
echo ""
done
echo "=== End of Report ==="
echo ""
echo "For detailed logs use:"
echo " sudo journalctl -u nebuleair-[service]-data.service -f"
echo "To restart a specific service timer:"
echo " sudo systemctl restart nebuleair-[service]-data.timer"

228
services/setup_services.sh Normal file
View File

@@ -0,0 +1,228 @@
#!/bin/bash
# File: /var/www/nebuleair_pro_4g/services/setup_services.sh
# Purpose: Set up all systemd services for NebuleAir data collection
# to install:
# sudo chmod +x /var/www/nebuleair_pro_4g/services/setup_services.sh
# sudo /var/www/nebuleair_pro_4g/services/setup_services.sh
echo "Setting up NebuleAir systemd services and timers..."
# Create directory for logs if it doesn't exist
mkdir -p /var/www/nebuleair_pro_4g/logs
# Create service and timer files for NPM Data
cat > /etc/systemd/system/nebuleair-npm-data.service << 'EOL'
[Unit]
Description=NebuleAir NPM Data Collection Service
After=network.target
[Service]
Type=oneshot
ExecStart=/usr/bin/python3 /var/www/nebuleair_pro_4g/NPM/get_data_modbus_v3.py
User=root
WorkingDirectory=/var/www/nebuleair_pro_4g
StandardOutput=append:/var/www/nebuleair_pro_4g/logs/npm_service.log
StandardError=append:/var/www/nebuleair_pro_4g/logs/npm_service_errors.log
[Install]
WantedBy=multi-user.target
EOL
cat > /etc/systemd/system/nebuleair-npm-data.timer << 'EOL'
[Unit]
Description=Run NebuleAir NPM Data Collection every 10 seconds
Requires=nebuleair-npm-data.service
[Timer]
OnBootSec=10s
OnUnitActiveSec=10s
AccuracySec=1s
[Install]
WantedBy=timers.target
EOL
# Create service and timer files for Envea Data
cat > /etc/systemd/system/nebuleair-envea-data.service << 'EOL'
[Unit]
Description=NebuleAir Envea Data Collection Service
After=network.target
[Service]
Type=oneshot
ExecStart=/usr/bin/python3 /var/www/nebuleair_pro_4g/envea/read_value_v2.py
User=root
WorkingDirectory=/var/www/nebuleair_pro_4g
StandardOutput=append:/var/www/nebuleair_pro_4g/logs/envea_service.log
StandardError=append:/var/www/nebuleair_pro_4g/logs/envea_service_errors.log
[Install]
WantedBy=multi-user.target
EOL
cat > /etc/systemd/system/nebuleair-envea-data.timer << 'EOL'
[Unit]
Description=Run NebuleAir Envea Data Collection every 10 seconds
Requires=nebuleair-envea-data.service
[Timer]
OnBootSec=10s
OnUnitActiveSec=10s
AccuracySec=1s
[Install]
WantedBy=timers.target
EOL
# Create service and timer files for SARA Data (No Lock File Needed)
cat > /etc/systemd/system/nebuleair-sara-data.service << 'EOL'
[Unit]
Description=NebuleAir SARA Data Transmission Service
After=network.target
[Service]
Type=oneshot
ExecStart=/usr/bin/python3 /var/www/nebuleair_pro_4g/loop/SARA_send_data_v2.py
User=root
WorkingDirectory=/var/www/nebuleair_pro_4g
StandardOutput=append:/var/www/nebuleair_pro_4g/logs/sara_service.log
StandardError=append:/var/www/nebuleair_pro_4g/logs/sara_service_errors.log
RuntimeMaxSec=200s
[Install]
WantedBy=multi-user.target
EOL
cat > /etc/systemd/system/nebuleair-sara-data.timer << 'EOL'
[Unit]
Description=Run NebuleAir SARA Data Transmission every 60 seconds
Requires=nebuleair-sara-data.service
[Timer]
OnBootSec=60s
OnUnitActiveSec=60s
AccuracySec=1s
# This is the key setting that prevents overlap
Persistent=true
[Install]
WantedBy=timers.target
EOL
# Create service and timer files for BME280 Data
cat > /etc/systemd/system/nebuleair-bme280-data.service << 'EOL'
[Unit]
Description=NebuleAir BME280 Data Collection Service
After=network.target
[Service]
Type=oneshot
ExecStart=/usr/bin/python3 /var/www/nebuleair_pro_4g/BME280/get_data_v2.py
User=root
WorkingDirectory=/var/www/nebuleair_pro_4g
StandardOutput=append:/var/www/nebuleair_pro_4g/logs/bme280_service.log
StandardError=append:/var/www/nebuleair_pro_4g/logs/bme280_service_errors.log
[Install]
WantedBy=multi-user.target
EOL
cat > /etc/systemd/system/nebuleair-bme280-data.timer << 'EOL'
[Unit]
Description=Run NebuleAir BME280 Data Collection every 120 seconds
Requires=nebuleair-bme280-data.service
[Timer]
OnBootSec=120s
OnUnitActiveSec=120s
AccuracySec=1s
[Install]
WantedBy=timers.target
EOL
# Create service and timer files for MPPT Data
cat > /etc/systemd/system/nebuleair-mppt-data.service << 'EOL'
[Unit]
Description=NebuleAir MPPT Data Collection Service
After=network.target
[Service]
Type=oneshot
ExecStart=/usr/bin/python3 /var/www/nebuleair_pro_4g/MPPT/read.py
User=root
WorkingDirectory=/var/www/nebuleair_pro_4g
StandardOutput=append:/var/www/nebuleair_pro_4g/logs/mppt_service.log
StandardError=append:/var/www/nebuleair_pro_4g/logs/mppt_service_errors.log
[Install]
WantedBy=multi-user.target
EOL
cat > /etc/systemd/system/nebuleair-mppt-data.timer << 'EOL'
[Unit]
Description=Run NebuleAir MPPT Data Collection every 120 seconds
Requires=nebuleair-mppt-data.service
[Timer]
OnBootSec=120s
OnUnitActiveSec=120s
AccuracySec=1s
[Install]
WantedBy=timers.target
EOL
# Create service and timer files for Database Cleanup
cat > /etc/systemd/system/nebuleair-db-cleanup-data.service << 'EOL'
[Unit]
Description=NebuleAir Database Cleanup Service
After=network.target
[Service]
Type=oneshot
ExecStart=/usr/bin/python3 /var/www/nebuleair_pro_4g/sqlite/flush_old_data.py
User=root
WorkingDirectory=/var/www/nebuleair_pro_4g
StandardOutput=append:/var/www/nebuleair_pro_4g/logs/db_cleanup_service.log
StandardError=append:/var/www/nebuleair_pro_4g/logs/db_cleanup_service_errors.log
[Install]
WantedBy=multi-user.target
EOL
cat > /etc/systemd/system/nebuleair-db-cleanup-data.timer << 'EOL'
[Unit]
Description=Run NebuleAir Database Cleanup daily
Requires=nebuleair-db-cleanup-data.service
[Timer]
OnBootSec=1h
OnUnitActiveSec=24h
AccuracySec=1h
[Install]
WantedBy=timers.target
EOL
# Reload systemd to recognize new services
systemctl daemon-reload
# Enable and start all timers
echo "Enabling and starting all services..."
for service in npm envea sara bme280 mppt db-cleanup; do
systemctl enable nebuleair-$service-data.timer
systemctl start nebuleair-$service-data.timer
echo "Started nebuleair-$service-data timer"
done
echo "Checking status of all timers..."
systemctl list-timers | grep nebuleair
echo "Setup complete. All NebuleAir services are now running."
echo "To check the status of a specific service:"
echo " sudo systemctl status nebuleair-npm-data.service"
echo "To view logs for a specific service:"
echo " sudo journalctl -u nebuleair-npm-data.service"
echo "To restart a specific timer:"
echo " sudo systemctl restart nebuleair-npm-data.timer"

View File

@@ -27,14 +27,6 @@ CREATE TABLE IF NOT EXISTS config_table (
) )
""") """)
#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 #creates a config table for envea sondes
cursor.execute(""" cursor.execute("""
CREATE TABLE IF NOT EXISTS envea_sondes_table ( CREATE TABLE IF NOT EXISTS envea_sondes_table (
@@ -46,7 +38,6 @@ CREATE TABLE IF NOT EXISTS envea_sondes_table (
) )
""") """)
# Create a table timer # Create a table timer
cursor.execute(""" cursor.execute("""
CREATE TABLE IF NOT EXISTS timestamp_table ( CREATE TABLE IF NOT EXISTS timestamp_table (

View File

@@ -45,7 +45,7 @@ if row:
print(f"[INFO] Deleting records older than: {cutoff_date_str}") print(f"[INFO] Deleting records older than: {cutoff_date_str}")
# List of tables to delete old data from # List of tables to delete old data from
tables_to_clean = ["data_NPM", "data_NPM_5channels", "data_BME280", "data_envea"] tables_to_clean = ["data_NPM", "data_NPM_5channels", "data_BME280", "data_envea","data_WIND", "data_MPPT"]
# Loop through each table and delete old data # Loop through each table and delete old data
for table in tables_to_clean: for table in tables_to_clean:

View File

@@ -20,37 +20,14 @@ cursor = conn.cursor()
print(f"Connected to database") print(f"Connected to database")
# Clear existing data (if any) # Note: Using INSERT OR IGNORE to add only new configurations without overwriting existing ones
cursor.execute("DELETE FROM config_table") print("Adding new configurations (existing ones will be preserved)")
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 # Insert general configurations
config_entries = [ config_entries = [
("modem_config_mode", "0", "bool"), ("modem_config_mode", "0", "bool"),
("deviceID", "XXXX", "str"), ("deviceID", "XXXX", "str"),
("npm_5channel", "0", "bool"),
("latitude_raw", "0", "int"), ("latitude_raw", "0", "int"),
("longitude_raw", "0", "int"), ("longitude_raw", "0", "int"),
("latitude_precision", "0", "int"), ("latitude_precision", "0", "int"),
@@ -65,12 +42,17 @@ config_entries = [
("SARA_R4_neworkID", "20810", "int"), ("SARA_R4_neworkID", "20810", "int"),
("WIFI_status", "connected", "str"), ("WIFI_status", "connected", "str"),
("send_uSpot", "0", "bool"), ("send_uSpot", "0", "bool"),
("npm_5channel", "0", "bool"),
("envea", "0", "bool"),
("windMeter", "0", "bool"),
("BME280", "0", "bool"),
("MPPT", "0", "bool"),
("modem_version", "XXX", "str") ("modem_version", "XXX", "str")
] ]
for key, value, value_type in config_entries: for key, value, value_type in config_entries:
cursor.execute( cursor.execute(
"INSERT INTO config_table (key, value, type) VALUES (?, ?, ?)", "INSERT OR IGNORE INTO config_table (key, value, type) VALUES (?, ?, ?)",
(key, value, value_type) (key, value, value_type)
) )
@@ -83,7 +65,7 @@ envea_sondes = [
for connected, port, name, coefficient in envea_sondes: for connected, port, name, coefficient in envea_sondes:
cursor.execute( cursor.execute(
"INSERT INTO envea_sondes_table (connected, port, name, coefficient) VALUES (?, ?, ?, ?)", "INSERT OR IGNORE INTO envea_sondes_table (connected, port, name, coefficient) VALUES (?, ?, ?, ?)",
(1 if connected else 0, port, name, coefficient) (1 if connected else 0, port, name, coefficient)
) )

118
update_firmware.sh Normal file
View File

@@ -0,0 +1,118 @@
#!/bin/bash
# NebuleAir Pro 4G - Comprehensive Update Script
# This script performs a complete system update including git pull,
# config initialization, and service management
echo "======================================"
echo "NebuleAir Pro 4G - Firmware Update"
echo "======================================"
echo "Started at: $(date)"
echo ""
# Set working directory
cd /var/www/nebuleair_pro_4g
# Function to print status messages
print_status() {
echo "[$(date '+%H:%M:%S')] $1"
}
# Function to check command success
check_status() {
if [ $? -eq 0 ]; then
print_status "$1 completed successfully"
else
print_status "$1 failed"
return 1
fi
}
# Step 1: Git operations
print_status "Step 1: Updating firmware from repository..."
git fetch origin
check_status "Git fetch"
# Show current branch and any changes
print_status "Current branch: $(git branch --show-current)"
if [ -n "$(git status --porcelain)" ]; then
print_status "Warning: Local changes detected:"
git status --short
fi
# Pull latest changes
git pull origin $(git branch --show-current)
check_status "Git pull"
# Step 2: Update database configuration
print_status ""
print_status "Step 2: Updating database configuration..."
/usr/bin/python3 /var/www/nebuleair_pro_4g/sqlite/set_config.py
check_status "Database configuration update"
# Step 3: Check and fix file permissions
print_status ""
print_status "Step 3: Checking file permissions..."
sudo chmod +x /var/www/nebuleair_pro_4g/update_firmware.sh
sudo chmod 755 /var/www/nebuleair_pro_4g/sqlite/*.py
sudo chmod 755 /var/www/nebuleair_pro_4g/NPM/*.py
sudo chmod 755 /var/www/nebuleair_pro_4g/BME280/*.py
sudo chmod 755 /var/www/nebuleair_pro_4g/SARA/*.py
sudo chmod 755 /var/www/nebuleair_pro_4g/envea/*.py
check_status "File permissions update"
# Step 4: Restart critical services if they exist
print_status ""
print_status "Step 4: Managing system services..."
# List of services to check and restart
services=(
"nebuleair-npm-data.timer"
"nebuleair-envea-data.timer"
"nebuleair-sara-data.timer"
"nebuleair-bme280-data.timer"
"nebuleair-mppt-data.timer"
)
for service in "${services[@]}"; do
if systemctl list-unit-files | grep -q "$service"; then
print_status "Restarting service: $service"
sudo systemctl restart "$service"
if systemctl is-active --quiet "$service"; then
print_status "$service is running"
else
print_status "$service may not be active"
fi
else
print_status " Service $service not found (may not be installed)"
fi
done
# Step 5: System health check
print_status ""
print_status "Step 5: System health check..."
# Check disk space
disk_usage=$(df / | awk 'NR==2 {print $5}' | sed 's/%//')
if [ "$disk_usage" -gt 90 ]; then
print_status "⚠ Warning: Disk usage is high ($disk_usage%)"
else
print_status "✓ Disk usage is acceptable ($disk_usage%)"
fi
# Check if database is accessible
if [ -f "/var/www/nebuleair_pro_4g/sqlite/sensors.db" ]; then
print_status "✓ Database file exists"
else
print_status "⚠ Warning: Database file not found"
fi
# Step 6: Final cleanup
print_status ""
print_status "Step 6: Cleaning up..."
sudo find /var/www/nebuleair_pro_4g/logs -name "*.log" -size +10M -exec truncate -s 0 {} \;
check_status "Log cleanup"
print_status "Update completed successfully!"
exit 0

View File

@@ -18,6 +18,38 @@ Attention: The Raspberry Pi doesn't have analog inputs, so we need an analog-to
sudo /usr/bin/python3 /var/www/nebuleair_pro_4g/windMeter/read.py sudo /usr/bin/python3 /var/www/nebuleair_pro_4g/windMeter/read.py
this need to run as a service
--> sudo nano /etc/systemd/system/windMeter.service
⬇️
[Unit]
Description=Master manager for the Python wind meter scripts
After=network.target
[Service]
ExecStart=/usr/bin/python3 /var/www/nebuleair_pro_4g/windMeter/read.py
Restart=always
User=root
WorkingDirectory=/var/www/nebuleair_pro_4g
StandardOutput=append:/var/www/nebuleair_pro_4g/logs/wind.log
StandardError=append:/var/www/nebuleair_pro_4g/logs/wind_errors.log
[Install]
WantedBy=multi-user.target
⬆️
Reload systemd (first time after creating the service):
sudo systemctl daemon-reload
Enable (once), start (once and after stopping) and restart (after modification)systemd:
sudo systemctl enable windMeter.service
sudo systemctl start windMeter.service
sudo systemctl restart windMeter.service
Check the service status:
sudo systemctl status windMeter.service
''' '''
#!/usr/bin/python3 #!/usr/bin/python3
import time import time