8 Commits

Author SHA1 Message Date
Your Name
33b24a9f53 update 2025-03-10 15:00:00 +01:00
Your Name
10c4348e54 update 2025-03-10 13:38:02 +01:00
Your Name
072f98ef95 update 2025-03-10 12:15:05 +01:00
Your Name
7b4ff011ec update 2025-03-05 16:24:15 +01:00
Your Name
ab2124f50d update 2025-03-05 16:19:49 +01:00
Your Name
b493d30a41 update 2025-03-05 16:07:22 +01:00
Your Name
659effb7c4 update 2025-03-05 15:58:40 +01:00
Your Name
ebb0fd0a2b update 2025-03-05 09:29:53 +01:00
15 changed files with 566 additions and 1047 deletions

View File

@@ -1,24 +0,0 @@
# NebuleAir Pro 4G Development Guidelines
## Commands
- `sudo systemctl restart master_nebuleair.service` - Restart main service
- `sudo systemctl status master_nebuleair.service` - Check service status
- Manual testing: Run individual Python scripts (e.g., `sudo python3 NPM/get_data_modbus_v3.py`)
- Installation: `sudo ./installation_part1.sh` followed by `sudo ./installation_part2.sh`
## Code Style
- **Language:** Python 3 with HTML/JS/CSS for web interface
- **Structure:** Organized by component (BME280, NPM, RTC, SARA, etc.)
- **Naming:** snake_case for variables/functions, version suffix for iterations (e.g., `_v2.py`)
- **Documentation:** Include docstrings with script purpose and usage instructions
- **Error Handling:** Use try/except blocks for I/O operations, print errors to logs
- **Configuration:** All settings in `config.json`, avoid hardcoding values
- **Web Components:** Follow Bootstrap patterns, use fetch() for AJAX
## Best Practices
- Check if features are enabled in config before execution
- Close database connections after use
- Round sensor readings to appropriate precision
- Keep web interface mobile-responsive
- Include error handling for network operations
- Follow existing patterns when adding new functionality

View File

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

View File

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

View File

@@ -0,0 +1,103 @@
'''
____ _ ____ _
/ ___| / \ | _ \ / \
\___ \ / _ \ | |_) | / _ \
___) / ___ \| _ < / ___ \
|____/_/ \_\_| \_\/_/ \_\
Script to Configures the network connection to a Multi GNSS Assistance (MGA) server used also per CellLocate
/usr/bin/python3 /var/www/nebuleair_pro_4g/SARA/cellLocate/server_conf.py ttyAMA2 1
AT+UGSRV="cell-live1.services.u-blox.com","cell-live2.services.u-blox.com","XkEKfGqVSbmNE1eZfBZm4Q"
'''
import serial
import time
import sys
import json
parameter = sys.argv[1:] # Exclude the script name
port='/dev/'+parameter[0] # ex: ttyAMA2
timeout = float(parameter[1]) # ex:2
#get baudrate
def load_config(config_file):
try:
with open(config_file, 'r') as file:
config_data = json.load(file)
return config_data
except Exception as e:
print(f"Error loading config file: {e}")
return {}
# Define the config file path
config_file = '/var/www/nebuleair_pro_4g/config.json'
# Load the configuration data
config = load_config(config_file)
# Access the shared variables
baudrate = config.get('SaraR4_baudrate', 115200)
ser = serial.Serial(
port=port, #USB0 or ttyS0
baudrate=baudrate, #115200 ou 9600
parity=serial.PARITY_NONE, #PARITY_NONE, PARITY_EVEN or PARITY_ODD
stopbits=serial.STOPBITS_ONE,
bytesize=serial.EIGHTBITS,
timeout = timeout
)
def read_complete_response(serial_connection, timeout=2, end_of_response_timeout=2, wait_for_lines=None, debug=True):
'''
Fonction très importante !!!
Reads the complete response from a serial connection and waits for specific lines.
'''
if wait_for_lines is None:
wait_for_lines = [] # Default to an empty list if not provided
response = bytearray()
serial_connection.timeout = timeout
end_time = time.time() + end_of_response_timeout
start_time = time.time()
while True:
elapsed_time = time.time() - start_time # Time since function start
if serial_connection.in_waiting > 0:
data = serial_connection.read(serial_connection.in_waiting)
response.extend(data)
end_time = time.time() + end_of_response_timeout # Reset timeout on new data
# Decode and check for any target line
decoded_response = response.decode('utf-8', errors='replace')
for target_line in wait_for_lines:
if target_line in decoded_response:
if debug:
print(f"[DEBUG] 🔎 Found target line: {target_line} (in {elapsed_time:.2f}s)")
return decoded_response # Return response immediately if a target line is found
elif time.time() > end_time:
if debug:
print(f"[DEBUG] Timeout reached. No more data received.")
break
time.sleep(0.1) # Short sleep to prevent busy waiting
# Final response and debug output
total_elapsed_time = time.time() - start_time
if debug:
print(f"[DEBUG] ⏱️ elapsed time: {total_elapsed_time:.2f}s. ⏱️")
# Check if the elapsed time exceeded 10 seconds
if total_elapsed_time > 10 and debug:
print(f"[ALERT] 🚨 The operation took too long 🚨")
print(f'<span style="color: red;font-weight: bold;">[ALERT] ⚠️{total_elapsed_time:.2f}s⚠</span>')
return response.decode('utf-8', errors='replace') # Return the full response if no target line is found
#command = f'ATI\r'
command = f'AT+UGSRV="cell-live1.services.u-blox.com","cell-live2.services.u-blox.com","XkEKfGqVSbmNE1eZfBZm4Q"\r'
ser.write((command + '\r').encode('utf-8'))
response = read_complete_response(ser, wait_for_lines=["+UULOC"])
print(response)

View File

@@ -121,19 +121,38 @@ try:
print('<h3>Start reboot python script</h3>')
#check modem status
#Attention:
# SARA R4 response: Manufacturer: u-blox Model: SARA-R410M-02B
# SArA R5 response: SARA-R500S-01B-00
print("Check SARA Status")
command = f'ATI\r'
ser_sara.write(command.encode('utf-8'))
response_SARA_ATI = read_complete_response(ser_sara, wait_for_lines=["IMEI"])
print(response_SARA_ATI)
match = re.search(r"Model:\s*(.+)", response_SARA_ATI)
model = match.group(1).strip() if match else "Unknown" # Strip unwanted characters
print(f" Model: {model}")
# Check for SARA model with more robust regex
model = "Unknown"
if "SARA-R410M" in response_SARA_ATI:
model = "SARA-R410M"
print("📱 Detected SARA R4 modem")
elif "SARA-R500" in response_SARA_ATI:
model = "SARA-R500"
print("📱 Detected SARA R5 modem")
else:
# Fallback to regex match if direct string match fails
match = re.search(r"Model:\s*([A-Za-z0-9\-]+)", response_SARA_ATI)
if match:
model = match.group(1).strip()
else:
model = "Unknown"
print("⚠️ Could not identify modem model")
print(f"🔍 Model: {model}")
update_json_key(config_file, "modem_version", model)
time.sleep(1)
# 1. Set AIRCARTO URL
# 1. Set AIRCARTO URL (profile id = 0)
print('Set aircarto URL')
aircarto_profile_id = 0
aircarto_url="data.nebuleair.fr"
@@ -143,26 +162,74 @@ try:
print(response_SARA_1)
time.sleep(1)
#2. Set 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)
print("set port 81")
command = f'AT+UHTTP={uSpot_profile_id},5,81\r'
#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)
#3. Get localisation (CellLocate)
mode = 2
sensor = 2
mode = 2 #single shot position
sensor = 2 #use cellular CellLocate® location information
response_type = 0
timeout_s = 2
accuracy_m = 1

View File

@@ -7,6 +7,8 @@
Script to see if the SARA-R410 is running
ex:
python3 /var/www/nebuleair_pro_4g/SARA/sara.py ttyAMA2 ATI 2
ex 1 (get SIM infos)
python3 /var/www/nebuleair_pro_4g/SARA/sara.py ttyAMA2 AT+CCID? 2
ex 2 (turn on blue light):
python3 /var/www/nebuleair_pro_4g/SARA/sara.py ttyAMA2 AT+UGPIOC=16,2 2
@@ -14,6 +16,8 @@ ex 3 (reconnect network)
python3 /var/www/nebuleair_pro_4g/SARA/sara.py ttyAMA2 AT+COPS=1,2,20801 20
ex 4 (get HTTP Profiles)
python3 /var/www/nebuleair_pro_4g/SARA/sara.py ttyAMA2 AT+UHTTP? 2
ex 5 (get IP addr)
python3 /var/www/nebuleair_pro_4g/SARA/sara.py ttyAMA2 AT+CGPADDR=1 2
'''

79
SARA/sara_checkDNS.py Normal file
View File

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

View File

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

View File

@@ -7,6 +7,7 @@
"envea/read_value_v2.py": false,
"sqlite/flush_old_data.py": true,
"deviceID": "XXXX",
"npm_5channel": false,
"latitude_raw": 0,
"longitude_raw":0,
"latitude_precision": 0,
@@ -25,7 +26,7 @@
"SARA_R4_general_status": "connected",
"SARA_R4_SIM_status": "connected",
"SARA_R4_network_status": "connected",
"SARA_R4_neworkID": 0,
"SARA_R4_neworkID": 20810,
"WIFI_status": "connected",
"MQTT_GUI": false,
"send_aircarto": true,

View File

@@ -1,344 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>NebuleAir - Config Editor</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;
}
#jsonEditor {
width: 100%;
min-height: 400px;
font-family: monospace;
font-size: 14px;
border: 1px solid #ccc;
padding: 10px;
white-space: pre;
}
.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;
}
</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">Configuration Editor</h1>
<div class="row mb-3">
<div class="col-12">
<div class="alert alert-warning">
<strong>Warning:</strong> Editing the configuration file directly can affect system functionality.
Make changes carefully and ensure valid JSON format.
</div>
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="mb-0">config.json</h5>
<div>
<button id="editBtn" class="btn btn-primary me-2">Edit</button>
<button id="saveBtn" class="btn btn-success me-2" disabled>Save</button>
<button id="cancelBtn" class="btn btn-secondary" disabled>Cancel</button>
</div>
</div>
<div class="card-body">
<div id="jsonEditor" class="mb-3" readonly></div>
<div id="errorMsg" class="alert alert-danger" 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 edit configuration:</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));
});
});
// Add admin password (should be changed to something more secure)
const ADMIN_PASSWORD = "nebuleair123";
// Global variables for editor
let originalConfig = '';
let jsonEditor;
let editBtn;
let saveBtn;
let cancelBtn;
let passwordModal;
let adminPassword;
let submitPasswordBtn;
let cancelPasswordBtn;
let passwordError;
let errorMsg;
// Initialize DOM references after document is loaded
function initializeElements() {
jsonEditor = document.getElementById('jsonEditor');
editBtn = document.getElementById('editBtn');
saveBtn = document.getElementById('saveBtn');
cancelBtn = document.getElementById('cancelBtn');
passwordModal = document.getElementById('passwordModal');
adminPassword = document.getElementById('adminPassword');
submitPasswordBtn = document.getElementById('submitPasswordBtn');
cancelPasswordBtn = document.getElementById('cancelPasswordBtn');
passwordError = document.getElementById('passwordError');
errorMsg = document.getElementById('errorMsg');
}
// Load config file
function loadConfigFile() {
fetch('../config.json')
.then(response => response.text())
.then(data => {
originalConfig = data;
// Format JSON for display with proper indentation
try {
const jsonObj = JSON.parse(data);
const formattedJSON = JSON.stringify(jsonObj, null, 2);
jsonEditor.textContent = formattedJSON;
} catch (e) {
jsonEditor.textContent = data;
console.error("Error parsing JSON:", e);
}
})
.catch(error => {
console.error('Error loading config.json:', error);
jsonEditor.textContent = "Error loading configuration file.";
});
}
// Event listeners
document.addEventListener('DOMContentLoaded', function() {
// Initialize DOM elements
initializeElements();
// Load config file
loadConfigFile();
// Edit button
editBtn.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';
enableEditing();
} 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';
});
// Save button
saveBtn.addEventListener('click', function() {
saveConfig();
});
// Cancel button
cancelBtn.addEventListener('click', function() {
cancelEditing();
});
});
// Enable editing mode
function enableEditing() {
jsonEditor.setAttribute('contenteditable', 'true');
jsonEditor.focus();
jsonEditor.classList.add('border-primary');
editBtn.disabled = true;
saveBtn.disabled = false;
cancelBtn.disabled = false;
}
// Cancel editing
function cancelEditing() {
jsonEditor.setAttribute('contenteditable', 'false');
jsonEditor.classList.remove('border-primary');
jsonEditor.textContent = originalConfig;
// Reformat JSON
try {
const jsonObj = JSON.parse(originalConfig);
const formattedJSON = JSON.stringify(jsonObj, null, 2);
jsonEditor.textContent = formattedJSON;
} catch (e) {
jsonEditor.textContent = originalConfig;
}
editBtn.disabled = false;
saveBtn.disabled = true;
cancelBtn.disabled = true;
errorMsg.style.display = 'none';
}
// Save config
function saveConfig() {
const newConfig = jsonEditor.textContent;
// Validate JSON
try {
JSON.parse(newConfig);
// Send to server
$.ajax({
url: 'launcher.php',
method: 'POST',
data: {
type: 'save_config',
config: newConfig
},
dataType: 'json',
success: function(response) {
if (response.success) {
originalConfig = newConfig;
jsonEditor.setAttribute('contenteditable', 'false');
jsonEditor.classList.remove('border-primary');
editBtn.disabled = false;
saveBtn.disabled = true;
cancelBtn.disabled = true;
// Show success message
errorMsg.textContent = 'Configuration saved successfully!';
errorMsg.classList.remove('alert-danger');
errorMsg.classList.add('alert-success');
errorMsg.style.display = 'block';
// Hide success message after 3 seconds
setTimeout(() => {
errorMsg.style.display = 'none';
}, 3000);
} else {
errorMsg.textContent = 'Error saving configuration: ' + response.message;
errorMsg.classList.remove('alert-success');
errorMsg.classList.add('alert-danger');
errorMsg.style.display = 'block';
}
},
error: function(xhr, status, error) {
errorMsg.textContent = 'Error saving configuration: ' + error;
errorMsg.classList.remove('alert-success');
errorMsg.classList.add('alert-danger');
errorMsg.style.display = 'block';
}
});
} catch (e) {
errorMsg.textContent = 'Invalid JSON format: ' + e.message;
errorMsg.classList.remove('alert-success');
errorMsg.classList.add('alert-danger');
errorMsg.style.display = 'block';
}
}
</script>
</body>
</html>

View File

@@ -4,8 +4,7 @@ header("Content-Type: application/json");
header("Cache-Control: no-store, no-cache, must-revalidate, max-age=0");
header("Pragma: no-cache");
// Get request type from GET or POST parameters
$type = isset($_GET['type']) ? $_GET['type'] : (isset($_POST['type']) ? $_POST['type'] : '');
$type=$_GET['type'];
if ($type == "get_npm_sqlite_data") {
$database_path = "/var/www/nebuleair_pro_4g/sqlite/sensors.db";
@@ -485,193 +484,3 @@ if ($type == "wifi_scan_old") {
echo $json_data;
}
// Save config.json with password protection
if ($type == "save_config") {
// 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 config content from POST data
$config = isset($_POST['config']) ? $_POST['config'] : '';
if (empty($config)) {
echo json_encode(['success' => false, 'message' => 'No configuration data provided']);
exit;
}
// Validate that the content is valid JSON
$decodedConfig = json_decode($config);
if (json_last_error() !== JSON_ERROR_NONE) {
echo json_encode([
'success' => false,
'message' => 'Invalid JSON format: ' . json_last_error_msg()
]);
exit;
}
// Path to the configuration file
$configFile = '/var/www/nebuleair_pro_4g/config.json';
// Create a backup of the current config
$backupFile = '/var/www/nebuleair_pro_4g/config.json.backup-' . date('Y-m-d-H-i-s');
if (file_exists($configFile)) {
copy($configFile, $backupFile);
}
// Write the updated configuration to the file
$result = file_put_contents($configFile, $config);
if ($result === false) {
echo json_encode([
'success' => false,
'message' => 'Failed to write configuration file. Check permissions.'
]);
} else {
echo json_encode([
'success' => true,
'message' => 'Configuration saved successfully',
'bytes_written' => $result
]);
}
}
// 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'
];
// 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'
]);
}
}

View File

@@ -47,18 +47,6 @@
</svg>
Carte
</a>
<a class="nav-link text-white" href="config.html">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-gear-fill" viewBox="0 0 16 16">
<path d="M9.405 1.05c-.413-1.4-2.397-1.4-2.81 0l-.1.34a1.464 1.464 0 0 1-2.105.872l-.31-.17c-1.283-.698-2.686.705-1.987 1.987l.169.311c.446.82.023 1.841-.872 2.105l-.34.1c-1.4.413-1.4 2.397 0 2.81l.34.1a1.464 1.464 0 0 1 .872 2.105l-.17.31c-.698 1.283.705 2.686 1.987 1.987l.311-.169a1.464 1.464 0 0 1 2.105.872l.1.34c.413 1.4 2.397 1.4 2.81 0l.1-.34a1.464 1.464 0 0 1 2.105-.872l.31.17c1.283.698 2.686-.705 1.987-1.987l-.169-.311a1.464 1.464 0 0 1 .872-2.105l.34-.1c1.4-.413 1.4-2.397 0-2.81l-.34-.1a1.464 1.464 0 0 1-.872-2.105l.17-.31c.698-1.283-.705-2.686-1.987-1.987l-.311.169a1.464 1.464 0 0 1-2.105-.872l-.1-.34zM8 10.93a2.929 2.929 0 1 1 0-5.86 2.929 2.929 0 0 1 0 5.858z"/>
</svg>
Config
</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">
<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"/>

View File

@@ -1,401 +0,0 @@
<!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();
});
// Add admin password (should be changed to something more secure)
const ADMIN_PASSWORD = "nebuleair123";
// 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;
}
if (command === 'help') {
terminal.innerHTML += `
Available commands:
help - Show this help message
clear - Clear the terminal
ls [options] - List directory contents
df -h - Show disk usage
free -h - Show memory usage
cat [file] - Display file contents
systemctl - Control system services
ifconfig - Show network configuration
reboot - Reboot the system (use with caution)
[Any other Linux command]\n`;
terminal.scrollTop = terminal.scrollHeight;
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',
method: 'POST',
data: {
type: 'execute_command',
command: command
},
success: function(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

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

View File

@@ -290,23 +290,45 @@ try:
print('<h3>START LOOP</h3>')
#Local timestamp
#ATTENTION:
# -> RTC module can be deconnected ""
# -> RTC module can be out of time like "2000-01-01T00:55:21Z"
print("Getting local timestamp")
cursor.execute("SELECT * FROM timestamp_table LIMIT 1")
row = cursor.fetchone() # Get the first (and only) row
rtc_time_str = row[1] # '2025-02-07 12:30:45'
rtc_time_str = row[1] # '2025-02-07 12:30:45' ou '2000-01-01 00:55:21' ou 'not connected'
print(rtc_time_str)
if rtc_time_str == 'not connected':
print("⛔ Atttention RTC module not connected⛔")
rtc_status = "disconnected"
influx_timestamp="rtc_disconnected"
else :
# Convert to a datetime object
dt_object = datetime.strptime(rtc_time_str, '%Y-%m-%d %H:%M:%S')
# Check if timestamp is reset (year 2000)
if dt_object.year == 2000:
print("⛔ Attention: RTC has been reset to default date ⛔")
rtc_status = "reset"
else:
print("✅ RTC timestamp is valid")
rtc_status = "valid"
# Always convert to InfluxDB format
# Convert to InfluxDB RFC3339 format with UTC 'Z' suffix
influx_timestamp = dt_object.strftime('%Y-%m-%dT%H:%M:%SZ')
rtc_status = "valid"
print(influx_timestamp)
#NEXTPM
# We take the last measures (order by rowid and not by timestamp)
print("Getting NPM values (last 6 measures)")
#cursor.execute("SELECT * FROM data_NPM ORDER BY timestamp DESC LIMIT 1")
cursor.execute("SELECT * FROM data_NPM ORDER BY timestamp DESC LIMIT 6")
#cursor.execute("SELECT * FROM data_NPM ORDER BY timestamp DESC LIMIT 6")
cursor.execute("SELECT rowid, * FROM data_NPM ORDER BY rowid DESC LIMIT 6")
rows = cursor.fetchall()
# Exclude the timestamp column (assuming first column is timestamp)
data_values = [row[1:] for row in rows] # Exclude timestamp
data_values = [row[2:] for row in rows] # Exclude timestamp
# Compute column-wise average
num_columns = len(data_values[0])
averages = [round(sum(col) / len(col),1) for col in zip(*data_values)]
@@ -407,7 +429,7 @@ try:
response2 = read_complete_response(ser_sara, wait_for_lines=["OK"])
print('<p class="text-danger-emphasis">')
print(response2)
print("</p>")
print("</p>", end="")
match = re.search(r'\+CSQ:\s*(\d+),', response2)
if match:
signal_quality = int(match.group(1))
@@ -425,7 +447,7 @@ try:
responseReconnect = read_complete_response(ser_sara, timeout=20, end_of_response_timeout=20)
print('<p class="text-danger-emphasis">')
print(responseReconnect)
print("</p>")
print("</p>", end="")
print('🛑STOP LOOP🛑')
print("<hr>")
@@ -467,7 +489,7 @@ try:
print('<p class="text-danger-emphasis">')
print(response_SARA_3)
print("</p>")
print("</p>", end="")
# si on recoit la réponse UHTTPCR
if "+UUHTTPCR" in response_SARA_3:
@@ -547,7 +569,7 @@ try:
response_SARA_9 = read_complete_response(ser_sara, wait_for_lines=["OK"], debug=False)
print('<p class="text-danger-emphasis">')
print(response_SARA_9)
print("</p>")
print("</p>", end="")
'''
+UHTTPER: profile_id,error_class,error_code
@@ -580,7 +602,7 @@ try:
responseResetHTTP2_profile = read_complete_response(ser_sara, timeout=5, end_of_response_timeout=5, wait_for_lines=["OK", "+CME ERROR"], debug=True)
print('<p class="text-danger-emphasis">')
print(responseResetHTTP2_profile)
print("</p>")
print("</p>", end="")
# 2.2 code 1 (HHTP succeded)
@@ -597,10 +619,78 @@ try:
response_SARA_4 = read_complete_response(ser_sara, wait_for_lines=["OK"], debug=False)
print('<p class="text-success">')
print(response_SARA_4)
print('</p>')
print("</p>", end="")
#Parse the server datetime
# Extract just the date from the response
date_string = None
date_start = response_SARA_4.find("Date: ")
if date_start != -1:
date_end = response_SARA_4.find("\n", date_start)
date_string = response_SARA_4[date_start + 6:date_end].strip()
print(f'<div class="text-primary">Server date: {date_string}</div>', end="")
# Optionally convert to datetime object
try:
from datetime import datetime
server_datetime = datetime.strptime(
date_string,
"%a, %d %b %Y %H:%M:%S %Z"
)
#print(f'<p class="text-primary">Parsed datetime: {server_datetime}</p>')
except Exception as e:
print(f'<p class="text-warning">Error parsing date: {e}</p>')
# Get RTC time from SQLite
cursor.execute("SELECT * FROM timestamp_table LIMIT 1")
row = cursor.fetchone()
rtc_time_str = row[1] # '2025-02-07 12:30:45' or '2000-01-01 00:55:21' or 'not connected'
print(f'<div class="text-primary">RTC time: {rtc_time_str}</div>', end="")
# Compare times if both are available
if server_datetime and rtc_time_str != 'not connected':
try:
# Convert RTC time string to datetime
rtc_datetime = datetime.strptime(rtc_time_str, '%Y-%m-%d %H:%M:%S')
# Calculate time difference in seconds
time_diff = abs((server_datetime - rtc_datetime).total_seconds())
print(f'<div class="text-primary">Time difference: {time_diff:.2f} seconds</div>', end="")
# Check if difference is more than 10 seconds
# and update the RTC clock
if time_diff > 10:
print(f'<div class="text-warning"><strong>⚠️ RTC time differs from server time by {time_diff:.2f} seconds!</strong></div>', end="")
# Format server time for RTC update
server_time_formatted = server_datetime.strftime('%Y-%m-%d %H:%M:%S')
#update RTC module do not wait for answer, non blocking
#/usr/bin/python3 /var/www/nebuleair_pro_4g/RTC/set_with_browserTime.py '2024-01-30 12:48:39'
# Launch RTC update script as non-blocking subprocess
import subprocess
update_command = [
"/usr/bin/python3",
"/var/www/nebuleair_pro_4g/RTC/set_with_browserTime.py",
server_time_formatted
]
# Execute the command without waiting for result
subprocess.Popen(update_command,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL)
print(f'<div class="text-warning">➡️ Updating RTC with server time: {server_time_formatted}</div>', end="")
else:
print(f'<div class="text-success">✅ RTC time is synchronized with server time (within 10 seconds)</div>')
except Exception as e:
print(f'<p class="text-warning">Error comparing times: {e}</p>')
#Si non ne recoit pas de réponse UHTTPCR
#on a peut etre une ERROR de type "+CME ERROR: No connection to phone"
#on a peut etre une ERROR de type "+CME ERROR: No connection to phone" ou "Operation not allowed"
else:
print('<span style="color: red;font-weight: bold;">No UUHTTPCR response</span>')
print("Blink red LED")
@@ -629,7 +719,7 @@ try:
responseReconnect = read_complete_response(ser_sara, timeout=5, end_of_response_timeout=120, wait_for_lines=["OK", "+CME ERROR"], debug=True)
print('<p class="text-danger-emphasis">')
print(responseReconnect)
print("</p>")
print("</p>", end="")
# Handle "Operation not allowed" error
if error_message == "Operation not allowed":
print('<span style="color: orange;font-weight: bold;">❓Try Resetting the HTTP Profile❓</span>')
@@ -638,7 +728,14 @@ try:
responseResetHTTP_profile = read_complete_response(ser_sara, timeout=5, end_of_response_timeout=5, wait_for_lines=["OK", "+CME ERROR"], debug=True)
print('<p class="text-danger-emphasis">')
print(responseResetHTTP_profile)
print("</p>")
print("</p>", end="")
check_lines = responseResetHTTP_profile.strip().splitlines()
for line in check_lines:
if "+CME ERROR: Operation not allowed" in line:
print('<span style="color: red;font-weight: bold;">⚠ATTENTION: CME ERROR⚠</span>')
print('<span style="color: orange;font-weight: bold;">❓Try Reboot the module❓</span>')
#5. empty json
print("Empty SARA memory:")
@@ -680,7 +777,7 @@ try:
print('<p class="text-danger-emphasis">')
print(response_SARA_8)
print("</p>")
print("</p>", end="")
# si on recoit la réponse UHTTPCR
if "+UUHTTPCR" in response_SARA_8:
@@ -733,7 +830,7 @@ try:
response_SARA_9b = read_complete_response(ser_sara, wait_for_lines=["OK"], debug=False)
print('<p class="text-danger-emphasis">')
print(response_SARA_9b)
print("</p>")
print("</p>", end="")
'''
+UHTTPER: profile_id,error_class,error_code
@@ -766,7 +863,7 @@ try:
response_SARA_4b = read_complete_response(ser_sara, wait_for_lines=["OK"], debug=False)
print('<p class="text-success">')
print(response_SARA_4b)
print('</p>')
print("</p>", end="")