Compare commits
30 Commits
ai_branch
...
e9b1e0e88e
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e9b1e0e88e | ||
|
|
5b7de91d50 | ||
|
|
4d15076d4b | ||
|
|
809742b6d5 | ||
|
|
bca975b0c5 | ||
|
|
dfba956685 | ||
|
|
d07314262e | ||
|
|
dffa639574 | ||
|
|
1fd5a3e75c | ||
|
|
e674b21eaa | ||
|
|
efc94ba5e1 | ||
|
|
26328dec99 | ||
|
|
ec3e81e99e | ||
|
|
1c6af36313 | ||
|
|
f1d6f595ac | ||
|
|
cfc2e0c47f | ||
|
|
1037207df3 | ||
|
|
14044a8856 | ||
|
|
d57a47ef68 | ||
|
|
5e7375cd4e | ||
|
|
c42b16ddb6 | ||
|
|
283a46eb0b | ||
|
|
33b24a9f53 | ||
|
|
10c4348e54 | ||
|
|
072f98ef95 | ||
|
|
7b4ff011ec | ||
|
|
ab2124f50d | ||
|
|
b493d30a41 | ||
|
|
659effb7c4 | ||
|
|
ebb0fd0a2b |
24
CLAUDE.md
24
CLAUDE.md
@@ -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
|
|
||||||
40
GPIO/control.py
Normal file
40
GPIO/control.py
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
'''
|
||||||
|
____ ____ ___ ___
|
||||||
|
/ ___| _ \_ _/ _ \
|
||||||
|
| | _| |_) | | | | |
|
||||||
|
| |_| | __/| | |_| |
|
||||||
|
\____|_| |___\___/
|
||||||
|
|
||||||
|
script to control GPIO output
|
||||||
|
|
||||||
|
GPIO 16 -> SARA 5V
|
||||||
|
GPIO 20 -> SARA PWR ON
|
||||||
|
|
||||||
|
option 1:
|
||||||
|
CLI tool like pinctrl
|
||||||
|
pinctrl set 17 op
|
||||||
|
pinctrl set 17 dh
|
||||||
|
pinctrl set 17 dl
|
||||||
|
|
||||||
|
option 2:
|
||||||
|
python library RPI.GPIO
|
||||||
|
|
||||||
|
|
||||||
|
/usr/bin/python3 /var/www/nebuleair_pro_4g/GPIO/control.py
|
||||||
|
'''
|
||||||
|
|
||||||
|
import RPi.GPIO as GPIO
|
||||||
|
import time
|
||||||
|
|
||||||
|
selected_GPIO = 16
|
||||||
|
|
||||||
|
GPIO.setmode(GPIO.BCM) # Use BCM numbering
|
||||||
|
GPIO.setup(selected_GPIO, GPIO.OUT) # Set GPIO17 as an output
|
||||||
|
|
||||||
|
while True:
|
||||||
|
GPIO.output(selected_GPIO, GPIO.HIGH) # Turn ON
|
||||||
|
time.sleep(1) # Wait 1 sec
|
||||||
|
GPIO.output(selected_GPIO, GPIO.LOW) # Turn OFF
|
||||||
|
time.sleep(1) # Wait 1 sec
|
||||||
|
|
||||||
|
|
||||||
225
MPPT/read.py
Normal file
225
MPPT/read.py
Normal file
@@ -0,0 +1,225 @@
|
|||||||
|
'''
|
||||||
|
__ __ ____ ____ _____
|
||||||
|
| \/ | _ \| _ \_ _|
|
||||||
|
| |\/| | |_) | |_) || |
|
||||||
|
| | | | __/| __/ | |
|
||||||
|
|_| |_|_| |_| |_|
|
||||||
|
|
||||||
|
Chargeur solaire Victron MPPT interface UART
|
||||||
|
|
||||||
|
MPPT connections
|
||||||
|
5V / Rx / TX / GND
|
||||||
|
RPI connection
|
||||||
|
-- / GPIO9 / GPIO8 / GND
|
||||||
|
* pas besoin de connecter le 5V (le GND uniquement)
|
||||||
|
|
||||||
|
typical response from uart:
|
||||||
|
|
||||||
|
PID 0xA075 ->product ID
|
||||||
|
FW 164 ->firmware version
|
||||||
|
SER# HQ2249VJV9W ->serial num
|
||||||
|
|
||||||
|
V 13310 ->Battery voilatage in mV
|
||||||
|
I -130 ->Battery current in mA (negative means its discharging)
|
||||||
|
VPV 10 ->Solar Panel voltage
|
||||||
|
PPV 0 ->Solar Panel power (in W)
|
||||||
|
CS 0 ->Charger status:
|
||||||
|
0=off (no charging),
|
||||||
|
2=Bulk (Max current is being delivered to the battery),
|
||||||
|
3=Absorbtion (battery is nearly full,voltage is held constant.),
|
||||||
|
4=Float (Battery is fully charged, only maintaining charge)
|
||||||
|
MPPT 0 ->MPPT (Maximum Power Point Tracking) state: 0 = Off, 1 = Active, 2 = Not tracking
|
||||||
|
OR 0x00000001
|
||||||
|
ERR 0
|
||||||
|
LOAD ON
|
||||||
|
IL 100
|
||||||
|
H19 18 ->historical data (Total energy absorbed in kWh)
|
||||||
|
H20 0 -> Total energy discharged in kWh
|
||||||
|
H21 0
|
||||||
|
H22 9
|
||||||
|
H23 92
|
||||||
|
HSDS 19
|
||||||
|
Checksum u
|
||||||
|
|
||||||
|
sudo /usr/bin/python3 /var/www/nebuleair_pro_4g/MPPT/read.py
|
||||||
|
|
||||||
|
'''
|
||||||
|
import serial
|
||||||
|
import time
|
||||||
|
import sqlite3
|
||||||
|
|
||||||
|
# Connect to the SQLite database
|
||||||
|
conn = sqlite3.connect("/var/www/nebuleair_pro_4g/sqlite/sensors.db")
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
|
||||||
|
def read_vedirect(port='/dev/ttyAMA4', baudrate=19200, timeout=20, max_attempts=3):
|
||||||
|
"""
|
||||||
|
Read and parse data from Victron MPPT controller with retry logic
|
||||||
|
Returns parsed data as a dictionary or None if all attempts fail
|
||||||
|
"""
|
||||||
|
required_keys = ['V', 'I', 'VPV', 'PPV', 'CS'] # Essential keys we need
|
||||||
|
|
||||||
|
for attempt in range(max_attempts):
|
||||||
|
try:
|
||||||
|
print(f"Attempt {attempt+1} of {max_attempts}...")
|
||||||
|
ser = serial.Serial(port, baudrate, timeout=1)
|
||||||
|
|
||||||
|
# Initialize data dictionary and tracking variables
|
||||||
|
data = {}
|
||||||
|
start_time = time.time()
|
||||||
|
|
||||||
|
while time.time() - start_time < timeout:
|
||||||
|
line = ser.readline().decode('utf-8', errors='ignore').strip()
|
||||||
|
|
||||||
|
if not line:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Check if line contains a key-value pair
|
||||||
|
if '\t' in line:
|
||||||
|
key, value = line.split('\t', 1)
|
||||||
|
data[key] = value
|
||||||
|
print(f"{key}: {value}")
|
||||||
|
else:
|
||||||
|
print(f"Info: {line}")
|
||||||
|
|
||||||
|
# Check if we have a complete data block
|
||||||
|
if 'Checksum' in data:
|
||||||
|
# Check if we have all required keys
|
||||||
|
missing_keys = [key for key in required_keys if key not in data]
|
||||||
|
if not missing_keys:
|
||||||
|
ser.close()
|
||||||
|
return data
|
||||||
|
else:
|
||||||
|
print(f"Incomplete data, missing: {', '.join(missing_keys)}")
|
||||||
|
# Clear data and continue reading
|
||||||
|
data = {}
|
||||||
|
|
||||||
|
# Timeout occurred
|
||||||
|
print(f"Timeout on attempt {attempt+1}: Could not get complete data")
|
||||||
|
ser.close()
|
||||||
|
|
||||||
|
# Add small delay between attempts
|
||||||
|
if attempt < max_attempts - 1:
|
||||||
|
print("Waiting before next attempt...")
|
||||||
|
time.sleep(2)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error on attempt {attempt+1}: {e}")
|
||||||
|
try:
|
||||||
|
ser.close()
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
print("All attempts failed")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def parse_values(data):
|
||||||
|
"""Convert string values to appropriate types"""
|
||||||
|
if not data:
|
||||||
|
return None
|
||||||
|
|
||||||
|
parsed = {}
|
||||||
|
|
||||||
|
# Define conversions for each key
|
||||||
|
conversions = {
|
||||||
|
'PID': str,
|
||||||
|
'FW': int,
|
||||||
|
'SER#': str,
|
||||||
|
'V': lambda x: float(x)/1000, # Convert mV to V
|
||||||
|
'I': lambda x: float(x)/1000, # Convert mA to A
|
||||||
|
'VPV': lambda x: float(x)/1000 if x != '---' else 0, # Convert mV to V
|
||||||
|
'PPV': int,
|
||||||
|
'CS': int,
|
||||||
|
'MPPT': int,
|
||||||
|
'OR': str,
|
||||||
|
'ERR': int,
|
||||||
|
'LOAD': str,
|
||||||
|
'IL': int,
|
||||||
|
'H19': int, # Total energy absorbed in kWh
|
||||||
|
'H20': int, # Total energy discharged in kWh
|
||||||
|
'H21': int,
|
||||||
|
'H22': int,
|
||||||
|
'H23': int,
|
||||||
|
'HSDS': int
|
||||||
|
}
|
||||||
|
|
||||||
|
# Convert values according to their type
|
||||||
|
for key, value in data.items():
|
||||||
|
if key in conversions:
|
||||||
|
try:
|
||||||
|
parsed[key] = conversions[key](value)
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
parsed[key] = value # Keep as string if conversion fails
|
||||||
|
else:
|
||||||
|
parsed[key] = value
|
||||||
|
|
||||||
|
return parsed
|
||||||
|
|
||||||
|
def get_charger_status(cs_value):
|
||||||
|
"""Convert CS numeric value to human-readable status"""
|
||||||
|
status_map = {
|
||||||
|
0: "Off",
|
||||||
|
1: "Low power mode",
|
||||||
|
2: "Fault",
|
||||||
|
3: "Bulk",
|
||||||
|
4: "Absorption",
|
||||||
|
5: "Float",
|
||||||
|
6: "Storage",
|
||||||
|
7: "Equalize",
|
||||||
|
9: "Inverting",
|
||||||
|
11: "Power supply",
|
||||||
|
245: "Starting-up",
|
||||||
|
247: "Repeated absorption",
|
||||||
|
252: "External control"
|
||||||
|
}
|
||||||
|
return status_map.get(cs_value, f"Unknown ({cs_value})")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
# Read data (with retry logic)
|
||||||
|
raw_data = read_vedirect()
|
||||||
|
|
||||||
|
if raw_data:
|
||||||
|
# Parse data
|
||||||
|
parsed_data = parse_values(raw_data)
|
||||||
|
|
||||||
|
if parsed_data:
|
||||||
|
# Check if we have valid battery voltage
|
||||||
|
if parsed_data.get('V', 0) > 0:
|
||||||
|
print("\n===== MPPT Summary =====")
|
||||||
|
print(f"Battery: {parsed_data.get('V', 0):.2f}V, {parsed_data.get('I', 0):.2f}A")
|
||||||
|
print(f"Solar: {parsed_data.get('VPV', 0):.2f}V, {parsed_data.get('PPV', 0)}W")
|
||||||
|
print(f"Charger status: {get_charger_status(parsed_data.get('CS', 0))}")
|
||||||
|
|
||||||
|
# Save to SQLite
|
||||||
|
cursor.execute("SELECT * FROM timestamp_table LIMIT 1")
|
||||||
|
row = cursor.fetchone()
|
||||||
|
rtc_time_str = row[1]
|
||||||
|
|
||||||
|
# Extract values
|
||||||
|
battery_voltage = parsed_data.get('V', 0)
|
||||||
|
battery_current = parsed_data.get('I', 0)
|
||||||
|
solar_voltage = parsed_data.get('VPV', 0)
|
||||||
|
solar_power = parsed_data.get('PPV', 0)
|
||||||
|
charger_status = parsed_data.get('CS', 0)
|
||||||
|
|
||||||
|
try:
|
||||||
|
cursor.execute('''
|
||||||
|
INSERT INTO data_MPPT (timestamp, battery_voltage, battery_current, solar_voltage, solar_power, charger_status)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?)''',
|
||||||
|
(rtc_time_str, battery_voltage, battery_current, solar_voltage, solar_power, charger_status))
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
print("MPPT data saved successfully!")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Database error: {e}")
|
||||||
|
else:
|
||||||
|
print("Invalid data: Battery voltage is zero or missing")
|
||||||
|
else:
|
||||||
|
print("Failed to parse data")
|
||||||
|
else:
|
||||||
|
print("No valid data received from MPPT controller")
|
||||||
|
|
||||||
|
# Always close the connection
|
||||||
|
conn.close()
|
||||||
@@ -29,7 +29,7 @@ Line by line installation.
|
|||||||
```
|
```
|
||||||
sudo apt update
|
sudo apt update
|
||||||
sudo apt install git gh apache2 php php-sqlite3 python3 python3-pip jq autossh i2c-tools python3-smbus -y
|
sudo apt install git gh apache2 php php-sqlite3 python3 python3-pip jq autossh i2c-tools python3-smbus -y
|
||||||
sudo pip3 install pyserial requests RPi.GPIO adafruit-circuitpython-bme280 crcmod psutil ntplib pytz --break-system-packages
|
sudo pip3 install pyserial requests RPi.GPIO adafruit-circuitpython-bme280 crcmod psutil ntplib pytz gpiozero adafruit-circuitpython-ads1x15 numpy --break-system-packages
|
||||||
sudo mkdir -p /var/www/.ssh
|
sudo mkdir -p /var/www/.ssh
|
||||||
sudo ssh-keygen -t rsa -b 4096 -f /var/www/.ssh/id_rsa -N ""
|
sudo ssh-keygen -t rsa -b 4096 -f /var/www/.ssh/id_rsa -N ""
|
||||||
sudo ssh-copy-id -i /var/www/.ssh/id_rsa.pub -p 50221 airlab_server1@aircarto.fr
|
sudo ssh-copy-id -i /var/www/.ssh/id_rsa.pub -p 50221 airlab_server1@aircarto.fr
|
||||||
|
|||||||
@@ -1,11 +1,15 @@
|
|||||||
|
#!/usr/bin/python3
|
||||||
"""
|
"""
|
||||||
Script to set the RTC using an NTP server.
|
____ _____ ____
|
||||||
|
| _ \_ _/ ___|
|
||||||
|
| |_) || || |
|
||||||
|
| _ < | || |___
|
||||||
|
|_| \_\|_| \____|
|
||||||
|
|
||||||
|
Script to set the RTC using an NTP server (script used by web UI)
|
||||||
RPI needs to be connected to the internet (WIFI).
|
RPI needs to be connected to the internet (WIFI).
|
||||||
Requires ntplib and pytz:
|
Requires ntplib and pytz:
|
||||||
sudo pip3 install ntplib pytz --break-system-packages
|
sudo pip3 install ntplib pytz --break-system-packages
|
||||||
|
|
||||||
/usr/bin/python3 /var/www/nebuleair_pro_4g/RTC/set_with_NTP.py
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
import smbus2
|
import smbus2
|
||||||
import time
|
import time
|
||||||
@@ -49,29 +53,95 @@ def set_time(bus, year, month, day, hour, minute, second):
|
|||||||
])
|
])
|
||||||
|
|
||||||
def read_time(bus):
|
def read_time(bus):
|
||||||
"""Read the RTC time."""
|
"""Read the RTC time and validate the values."""
|
||||||
|
try:
|
||||||
data = bus.read_i2c_block_data(DS3231_ADDR, REG_TIME, 7)
|
data = bus.read_i2c_block_data(DS3231_ADDR, REG_TIME, 7)
|
||||||
|
|
||||||
|
# Convert from BCD
|
||||||
second = bcd_to_dec(data[0] & 0x7F)
|
second = bcd_to_dec(data[0] & 0x7F)
|
||||||
minute = bcd_to_dec(data[1])
|
minute = bcd_to_dec(data[1])
|
||||||
hour = bcd_to_dec(data[2] & 0x3F)
|
hour = bcd_to_dec(data[2] & 0x3F)
|
||||||
day = bcd_to_dec(data[4])
|
day = bcd_to_dec(data[4])
|
||||||
month = bcd_to_dec(data[5])
|
month = bcd_to_dec(data[5])
|
||||||
year = bcd_to_dec(data[6]) + 2000
|
year = bcd_to_dec(data[6]) + 2000
|
||||||
|
|
||||||
|
# Print raw values for debugging
|
||||||
|
print(f"Raw RTC values: {data}")
|
||||||
|
print(f"Decoded values: Y:{year} M:{month} D:{day} H:{hour} M:{minute} S:{second}")
|
||||||
|
|
||||||
|
# Validate date values
|
||||||
|
if not (1 <= month <= 12):
|
||||||
|
print(f"Invalid month value: {month}, using default")
|
||||||
|
month = 1
|
||||||
|
|
||||||
|
# Check days in month (simplified)
|
||||||
|
days_in_month = [0, 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
|
||||||
|
if not (1 <= day <= days_in_month[month]):
|
||||||
|
print(f"Invalid day value: {day} for month {month}, using default")
|
||||||
|
day = 1
|
||||||
|
|
||||||
|
# Validate time values
|
||||||
|
if not (0 <= hour <= 23):
|
||||||
|
print(f"Invalid hour value: {hour}, using default")
|
||||||
|
hour = 0
|
||||||
|
|
||||||
|
if not (0 <= minute <= 59):
|
||||||
|
print(f"Invalid minute value: {minute}, using default")
|
||||||
|
minute = 0
|
||||||
|
|
||||||
|
if not (0 <= second <= 59):
|
||||||
|
print(f"Invalid second value: {second}, using default")
|
||||||
|
second = 0
|
||||||
|
|
||||||
return (year, month, day, hour, minute, second)
|
return (year, month, day, hour, minute, second)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error reading RTC: {e}")
|
||||||
|
# Return a safe default date (2023-01-01 00:00:00)
|
||||||
|
return (2023, 1, 1, 0, 0, 0)
|
||||||
|
|
||||||
def get_internet_time():
|
def get_internet_time():
|
||||||
"""Get the current time from an NTP server."""
|
"""Get the current time from an NTP server."""
|
||||||
ntp_client = ntplib.NTPClient()
|
ntp_client = ntplib.NTPClient()
|
||||||
response = ntp_client.request('pool.ntp.org')
|
# Try multiple NTP servers in case one fails
|
||||||
|
servers = ['pool.ntp.org', 'time.google.com', 'time.windows.com', 'time.apple.com']
|
||||||
|
|
||||||
|
for server in servers:
|
||||||
|
try:
|
||||||
|
print(f"Trying NTP server: {server}")
|
||||||
|
response = ntp_client.request(server, timeout=2)
|
||||||
utc_time = datetime.utcfromtimestamp(response.tx_time)
|
utc_time = datetime.utcfromtimestamp(response.tx_time)
|
||||||
|
print(f"Successfully got time from {server}")
|
||||||
return utc_time
|
return utc_time
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Failed to get time from {server}: {e}")
|
||||||
|
|
||||||
|
# If all servers fail, raise exception
|
||||||
|
raise Exception("All NTP servers failed")
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
|
try:
|
||||||
bus = smbus2.SMBus(1)
|
bus = smbus2.SMBus(1)
|
||||||
|
|
||||||
|
# Test if RTC is accessible
|
||||||
|
try:
|
||||||
|
bus.read_byte(DS3231_ADDR)
|
||||||
|
print("RTC module is accessible")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error accessing RTC module: {e}")
|
||||||
|
print("Please check connections and I2C configuration")
|
||||||
|
return
|
||||||
|
|
||||||
# Get the current time from the RTC
|
# Get the current time from the RTC
|
||||||
|
try:
|
||||||
year, month, day, hours, minutes, seconds = read_time(bus)
|
year, month, day, hours, minutes, seconds = read_time(bus)
|
||||||
|
# Create datetime object with validation to handle invalid dates
|
||||||
rtc_time = datetime(year, month, day, hours, minutes, seconds)
|
rtc_time = datetime(year, month, day, hours, minutes, seconds)
|
||||||
|
print(f"Actual RTC Time : {rtc_time.strftime('%Y-%m-%d %H:%M:%S')}")
|
||||||
|
except ValueError as e:
|
||||||
|
print(f"Invalid date/time read from RTC: {e}")
|
||||||
|
print("Will proceed with setting RTC from internet time")
|
||||||
|
rtc_time = None
|
||||||
|
|
||||||
# Get current UTC time from an NTP server
|
# Get current UTC time from an NTP server
|
||||||
try:
|
try:
|
||||||
@@ -79,19 +149,35 @@ def main():
|
|||||||
print(f"Time from Internet (UTC) : {internet_utc_time.strftime('%Y-%m-%d %H:%M:%S')}")
|
print(f"Time from Internet (UTC) : {internet_utc_time.strftime('%Y-%m-%d %H:%M:%S')}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error retrieving time from the internet: {e}")
|
print(f"Error retrieving time from the internet: {e}")
|
||||||
|
if rtc_time is None:
|
||||||
|
print("Cannot proceed without either valid RTC time or internet time")
|
||||||
|
return
|
||||||
|
print("Will keep current RTC time")
|
||||||
return
|
return
|
||||||
|
|
||||||
# Print current RTC time
|
|
||||||
print(f"Actual RTC Time : {rtc_time.strftime('%Y-%m-%d %H:%M:%S')}")
|
|
||||||
|
|
||||||
# Set the RTC to UTC time
|
# Set the RTC to UTC time
|
||||||
|
print("Setting RTC to internet time...")
|
||||||
set_time(bus, internet_utc_time.year, internet_utc_time.month, internet_utc_time.day,
|
set_time(bus, internet_utc_time.year, internet_utc_time.month, internet_utc_time.day,
|
||||||
internet_utc_time.hour, internet_utc_time.minute, internet_utc_time.second)
|
internet_utc_time.hour, internet_utc_time.minute, internet_utc_time.second)
|
||||||
|
|
||||||
# Read and print the new time from RTC
|
# Read and print the new time from RTC
|
||||||
|
print("Reading back new RTC time...")
|
||||||
year, month, day, hour, minute, second = read_time(bus)
|
year, month, day, hour, minute, second = read_time(bus)
|
||||||
rtc_time_new = datetime(year, month, day, hour, minute, second)
|
rtc_time_new = datetime(year, month, day, hour, minute, second)
|
||||||
print(f"New RTC Time (UTC) : {rtc_time_new.strftime('%Y-%m-%d %H:%M:%S')}")
|
print(f"New RTC Time (UTC) : {rtc_time_new.strftime('%Y-%m-%d %H:%M:%S')}")
|
||||||
|
|
||||||
|
# Calculate difference to verify accuracy
|
||||||
|
time_diff = abs((rtc_time_new - internet_utc_time).total_seconds())
|
||||||
|
print(f"Time difference : {time_diff:.2f} seconds")
|
||||||
|
|
||||||
|
if time_diff > 5:
|
||||||
|
print("Warning: RTC time differs significantly from internet time")
|
||||||
|
print("You may need to retry or check RTC module")
|
||||||
|
else:
|
||||||
|
print("RTC successfully synchronized with internet time")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Unexpected error: {e}")
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
@@ -1,5 +1,11 @@
|
|||||||
"""
|
"""
|
||||||
Script to set the RTC using the browser time.
|
____ _____ ____
|
||||||
|
| _ \_ _/ ___|
|
||||||
|
| |_) || || |
|
||||||
|
| _ < | || |___
|
||||||
|
|_| \_\|_| \____|
|
||||||
|
|
||||||
|
Script to set the RTC using the browser time (script used by the web UI).
|
||||||
|
|
||||||
/usr/bin/python3 /var/www/nebuleair_pro_4g/RTC/set_with_browserTime.py '2024-01-30 12:48:39'
|
/usr/bin/python3 /var/www/nebuleair_pro_4g/RTC/set_with_browserTime.py '2024-01-30 12:48:39'
|
||||||
|
|
||||||
|
|||||||
166
SARA/R5/setPDP.py
Normal file
166
SARA/R5/setPDP.py
Normal file
@@ -0,0 +1,166 @@
|
|||||||
|
'''
|
||||||
|
____ _ ____ _
|
||||||
|
/ ___| / \ | _ \ / \
|
||||||
|
\___ \ / _ \ | |_) | / _ \
|
||||||
|
___) / ___ \| _ < / ___ \
|
||||||
|
|____/_/ \_\_| \_\/_/ \_\
|
||||||
|
|
||||||
|
Script to set the PDP context for the SARA R5
|
||||||
|
|
||||||
|
|
||||||
|
/usr/bin/python3 /var/www/nebuleair_pro_4g/SARA/R5/setPDP.py
|
||||||
|
|
||||||
|
'''
|
||||||
|
import serial
|
||||||
|
import time
|
||||||
|
import sys
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
|
||||||
|
#get data from config
|
||||||
|
def load_config(config_file):
|
||||||
|
try:
|
||||||
|
with open(config_file, 'r') as file:
|
||||||
|
config_data = json.load(file)
|
||||||
|
return config_data
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error loading config file: {e}")
|
||||||
|
return {}
|
||||||
|
|
||||||
|
#Fonction pour mettre à jour le JSON de configuration
|
||||||
|
def update_json_key(file_path, key, value):
|
||||||
|
"""
|
||||||
|
Updates a specific key in a JSON file with a new value.
|
||||||
|
|
||||||
|
:param file_path: Path to the JSON file.
|
||||||
|
:param key: The key to update in the JSON file.
|
||||||
|
:param value: The new value to assign to the key.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Load the existing data
|
||||||
|
with open(file_path, "r") as file:
|
||||||
|
data = json.load(file)
|
||||||
|
|
||||||
|
# Check if the key exists in the JSON file
|
||||||
|
if key in data:
|
||||||
|
data[key] = value # Update the key with the new value
|
||||||
|
else:
|
||||||
|
print(f"Key '{key}' not found in the JSON file.")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Write the updated data back to the file
|
||||||
|
with open(file_path, "w") as file:
|
||||||
|
json.dump(data, file, indent=2) # Use indent for pretty printing
|
||||||
|
|
||||||
|
print(f"💾 updating '{key}' to '{value}'.")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error updating the JSON file: {e}")
|
||||||
|
|
||||||
|
# Define the config file path
|
||||||
|
config_file = '/var/www/nebuleair_pro_4g/config.json'
|
||||||
|
# Load the configuration data
|
||||||
|
config = load_config(config_file)
|
||||||
|
baudrate = config.get('SaraR4_baudrate', 115200) #baudrate du sara R4
|
||||||
|
device_id = config.get('deviceID', '').upper() #device ID en maj
|
||||||
|
|
||||||
|
ser_sara = serial.Serial(
|
||||||
|
port='/dev/ttyAMA2',
|
||||||
|
baudrate=baudrate, #115200 ou 9600
|
||||||
|
parity=serial.PARITY_NONE, #PARITY_NONE, PARITY_EVEN or PARITY_ODD
|
||||||
|
stopbits=serial.STOPBITS_ONE,
|
||||||
|
bytesize=serial.EIGHTBITS,
|
||||||
|
timeout = 2
|
||||||
|
)
|
||||||
|
|
||||||
|
def read_complete_response(serial_connection, timeout=2, end_of_response_timeout=2, wait_for_lines=None, debug=True):
|
||||||
|
'''
|
||||||
|
Fonction très importante !!!
|
||||||
|
Reads the complete response from a serial connection and waits for specific lines.
|
||||||
|
'''
|
||||||
|
if wait_for_lines is None:
|
||||||
|
wait_for_lines = [] # Default to an empty list if not provided
|
||||||
|
|
||||||
|
response = bytearray()
|
||||||
|
serial_connection.timeout = timeout
|
||||||
|
end_time = time.time() + end_of_response_timeout
|
||||||
|
start_time = time.time()
|
||||||
|
|
||||||
|
while True:
|
||||||
|
elapsed_time = time.time() - start_time # Time since function start
|
||||||
|
if serial_connection.in_waiting > 0:
|
||||||
|
data = serial_connection.read(serial_connection.in_waiting)
|
||||||
|
response.extend(data)
|
||||||
|
end_time = time.time() + end_of_response_timeout # Reset timeout on new data
|
||||||
|
|
||||||
|
# Decode and check for any target line
|
||||||
|
decoded_response = response.decode('utf-8', errors='replace')
|
||||||
|
for target_line in wait_for_lines:
|
||||||
|
if target_line in decoded_response:
|
||||||
|
if debug:
|
||||||
|
print(f"[DEBUG] 🔎 Found target line: {target_line} (in {elapsed_time:.2f}s)")
|
||||||
|
return decoded_response # Return response immediately if a target line is found
|
||||||
|
elif time.time() > end_time:
|
||||||
|
if debug:
|
||||||
|
print(f"[DEBUG] Timeout reached. No more data received.")
|
||||||
|
break
|
||||||
|
time.sleep(0.1) # Short sleep to prevent busy waiting
|
||||||
|
|
||||||
|
# Final response and debug output
|
||||||
|
total_elapsed_time = time.time() - start_time
|
||||||
|
if debug:
|
||||||
|
print(f"[DEBUG] ⏱️ elapsed time: {total_elapsed_time:.2f}s. ⏱️")
|
||||||
|
# Check if the elapsed time exceeded 10 seconds
|
||||||
|
if total_elapsed_time > 10 and debug:
|
||||||
|
print(f"[ALERT] 🚨 The operation took too long 🚨")
|
||||||
|
print(f'<span style="color: red;font-weight: bold;">[ALERT] ⚠️{total_elapsed_time:.2f}s⚠️</span>')
|
||||||
|
|
||||||
|
return response.decode('utf-8', errors='replace') # Return the full response if no target line is found
|
||||||
|
|
||||||
|
try:
|
||||||
|
print('Start script')
|
||||||
|
|
||||||
|
# 1. Check connection
|
||||||
|
print('➡️Check SARA R5 connexion')
|
||||||
|
command = f'ATI0\r'
|
||||||
|
ser_sara.write(command.encode('utf-8'))
|
||||||
|
response_SARA_1 = read_complete_response(ser_sara, wait_for_lines=["OK"])
|
||||||
|
print(response_SARA_1, end="")
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
# 2. Activate PDP context 1
|
||||||
|
print('➡️Activate PDP context 1')
|
||||||
|
command = f'AT+CGACT=1,1\r'
|
||||||
|
ser_sara.write(command.encode('utf-8'))
|
||||||
|
response_SARA_2 = read_complete_response(ser_sara, wait_for_lines=["OK"])
|
||||||
|
print(response_SARA_2, end="")
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
# 2. Set the PDP type
|
||||||
|
print('➡️Set the PDP type to IPv4 referring to the outputof the +CGDCONT read command')
|
||||||
|
command = f'AT+UPSD=0,0,0\r'
|
||||||
|
ser_sara.write(command.encode('utf-8'))
|
||||||
|
response_SARA_3 = read_complete_response(ser_sara, wait_for_lines=["OK"])
|
||||||
|
print(response_SARA_3, end="")
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
# 2. Profile #0 is mapped on CID=1.
|
||||||
|
print('➡️Profile #0 is mapped on CID=1.')
|
||||||
|
command = f'AT+UPSD=0,100,1\r'
|
||||||
|
ser_sara.write(command.encode('utf-8'))
|
||||||
|
response_SARA_3 = read_complete_response(ser_sara, wait_for_lines=["OK"])
|
||||||
|
print(response_SARA_3, end="")
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
# 2. Set the PDP type
|
||||||
|
print('➡️Activate the PSD profile #0: the IPv4 address is already assigned by the network.')
|
||||||
|
command = f'AT+UPSDA=0,3\r'
|
||||||
|
ser_sara.write(command.encode('utf-8'))
|
||||||
|
response_SARA_3 = read_complete_response(ser_sara, wait_for_lines=["OK","+UUPSDA"])
|
||||||
|
print(response_SARA_3, end="")
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print("An error occurred:", e)
|
||||||
|
traceback.print_exc() # This prints the full traceback
|
||||||
103
SARA/cellLocate/server_conf.py
Normal file
103
SARA/cellLocate/server_conf.py
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
'''
|
||||||
|
____ _ ____ _
|
||||||
|
/ ___| / \ | _ \ / \
|
||||||
|
\___ \ / _ \ | |_) | / _ \
|
||||||
|
___) / ___ \| _ < / ___ \
|
||||||
|
|____/_/ \_\_| \_\/_/ \_\
|
||||||
|
|
||||||
|
Script to Configures the network connection to a Multi GNSS Assistance (MGA) server used also per CellLocate
|
||||||
|
|
||||||
|
/usr/bin/python3 /var/www/nebuleair_pro_4g/SARA/cellLocate/server_conf.py ttyAMA2 1
|
||||||
|
|
||||||
|
AT+UGSRV="cell-live1.services.u-blox.com","cell-live2.services.u-blox.com","XkEKfGqVSbmNE1eZfBZm4Q"
|
||||||
|
|
||||||
|
|
||||||
|
'''
|
||||||
|
|
||||||
|
import serial
|
||||||
|
import time
|
||||||
|
import sys
|
||||||
|
import json
|
||||||
|
|
||||||
|
parameter = sys.argv[1:] # Exclude the script name
|
||||||
|
port='/dev/'+parameter[0] # ex: ttyAMA2
|
||||||
|
timeout = float(parameter[1]) # ex:2
|
||||||
|
|
||||||
|
|
||||||
|
#get baudrate
|
||||||
|
def load_config(config_file):
|
||||||
|
try:
|
||||||
|
with open(config_file, 'r') as file:
|
||||||
|
config_data = json.load(file)
|
||||||
|
return config_data
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error loading config file: {e}")
|
||||||
|
return {}
|
||||||
|
|
||||||
|
# Define the config file path
|
||||||
|
config_file = '/var/www/nebuleair_pro_4g/config.json'
|
||||||
|
# Load the configuration data
|
||||||
|
config = load_config(config_file)
|
||||||
|
# Access the shared variables
|
||||||
|
baudrate = config.get('SaraR4_baudrate', 115200)
|
||||||
|
|
||||||
|
ser = serial.Serial(
|
||||||
|
port=port, #USB0 or ttyS0
|
||||||
|
baudrate=baudrate, #115200 ou 9600
|
||||||
|
parity=serial.PARITY_NONE, #PARITY_NONE, PARITY_EVEN or PARITY_ODD
|
||||||
|
stopbits=serial.STOPBITS_ONE,
|
||||||
|
bytesize=serial.EIGHTBITS,
|
||||||
|
timeout = timeout
|
||||||
|
)
|
||||||
|
|
||||||
|
def read_complete_response(serial_connection, timeout=2, end_of_response_timeout=2, wait_for_lines=None, debug=True):
|
||||||
|
'''
|
||||||
|
Fonction très importante !!!
|
||||||
|
Reads the complete response from a serial connection and waits for specific lines.
|
||||||
|
'''
|
||||||
|
if wait_for_lines is None:
|
||||||
|
wait_for_lines = [] # Default to an empty list if not provided
|
||||||
|
|
||||||
|
response = bytearray()
|
||||||
|
serial_connection.timeout = timeout
|
||||||
|
end_time = time.time() + end_of_response_timeout
|
||||||
|
start_time = time.time()
|
||||||
|
|
||||||
|
while True:
|
||||||
|
elapsed_time = time.time() - start_time # Time since function start
|
||||||
|
if serial_connection.in_waiting > 0:
|
||||||
|
data = serial_connection.read(serial_connection.in_waiting)
|
||||||
|
response.extend(data)
|
||||||
|
end_time = time.time() + end_of_response_timeout # Reset timeout on new data
|
||||||
|
|
||||||
|
# Decode and check for any target line
|
||||||
|
decoded_response = response.decode('utf-8', errors='replace')
|
||||||
|
for target_line in wait_for_lines:
|
||||||
|
if target_line in decoded_response:
|
||||||
|
if debug:
|
||||||
|
print(f"[DEBUG] 🔎 Found target line: {target_line} (in {elapsed_time:.2f}s)")
|
||||||
|
return decoded_response # Return response immediately if a target line is found
|
||||||
|
elif time.time() > end_time:
|
||||||
|
if debug:
|
||||||
|
print(f"[DEBUG] Timeout reached. No more data received.")
|
||||||
|
break
|
||||||
|
time.sleep(0.1) # Short sleep to prevent busy waiting
|
||||||
|
|
||||||
|
# Final response and debug output
|
||||||
|
total_elapsed_time = time.time() - start_time
|
||||||
|
if debug:
|
||||||
|
print(f"[DEBUG] ⏱️ elapsed time: {total_elapsed_time:.2f}s. ⏱️")
|
||||||
|
# Check if the elapsed time exceeded 10 seconds
|
||||||
|
if total_elapsed_time > 10 and debug:
|
||||||
|
print(f"[ALERT] 🚨 The operation took too long 🚨")
|
||||||
|
print(f'<span style="color: red;font-weight: bold;">[ALERT] ⚠️{total_elapsed_time:.2f}s⚠️</span>')
|
||||||
|
|
||||||
|
return response.decode('utf-8', errors='replace') # Return the full response if no target line is found
|
||||||
|
|
||||||
|
|
||||||
|
#command = f'ATI\r'
|
||||||
|
command = f'AT+UGSRV="cell-live1.services.u-blox.com","cell-live2.services.u-blox.com","XkEKfGqVSbmNE1eZfBZm4Q"\r'
|
||||||
|
ser.write((command + '\r').encode('utf-8'))
|
||||||
|
|
||||||
|
response = read_complete_response(ser, wait_for_lines=["+UULOC"])
|
||||||
|
print(response)
|
||||||
@@ -64,6 +64,8 @@ config = load_config(config_file)
|
|||||||
baudrate = config.get('SaraR4_baudrate', 115200) #baudrate du sara R4
|
baudrate = config.get('SaraR4_baudrate', 115200) #baudrate du sara R4
|
||||||
device_id = config.get('deviceID', '').upper() #device ID en maj
|
device_id = config.get('deviceID', '').upper() #device ID en maj
|
||||||
|
|
||||||
|
sara_r5_DPD_setup = False
|
||||||
|
|
||||||
ser_sara = serial.Serial(
|
ser_sara = serial.Serial(
|
||||||
port='/dev/ttyAMA2',
|
port='/dev/ttyAMA2',
|
||||||
baudrate=baudrate, #115200 ou 9600
|
baudrate=baudrate, #115200 ou 9600
|
||||||
@@ -121,19 +123,41 @@ try:
|
|||||||
print('<h3>Start reboot python script</h3>')
|
print('<h3>Start reboot python script</h3>')
|
||||||
|
|
||||||
#check modem status
|
#check modem status
|
||||||
|
#Attention:
|
||||||
|
# SARA R4 response: Manufacturer: u-blox Model: SARA-R410M-02B
|
||||||
|
# SArA R5 response: SARA-R500S-01B-00
|
||||||
print("⚙️Check SARA Status")
|
print("⚙️Check SARA Status")
|
||||||
command = f'ATI\r'
|
command = f'ATI\r'
|
||||||
ser_sara.write(command.encode('utf-8'))
|
ser_sara.write(command.encode('utf-8'))
|
||||||
response_SARA_ATI = read_complete_response(ser_sara, wait_for_lines=["IMEI"])
|
response_SARA_ATI = read_complete_response(ser_sara, wait_for_lines=["IMEI"])
|
||||||
print(response_SARA_ATI)
|
print(response_SARA_ATI)
|
||||||
match = re.search(r"Model:\s*(.+)", response_SARA_ATI)
|
|
||||||
model = match.group(1).strip() if match else "Unknown" # Strip unwanted characters
|
# Check for SARA model with more robust regex
|
||||||
print(f" Model: {model}")
|
model = "Unknown"
|
||||||
|
if "SARA-R410M" in response_SARA_ATI:
|
||||||
|
model = "SARA-R410M"
|
||||||
|
print("📱 Detected SARA R4 modem")
|
||||||
|
elif "SARA-R500" in response_SARA_ATI:
|
||||||
|
model = "SARA-R500"
|
||||||
|
print("📱 Detected SARA R5 modem")
|
||||||
|
sara_r5_DPD_setup = True
|
||||||
|
else:
|
||||||
|
# Fallback to regex match if direct string match fails
|
||||||
|
match = re.search(r"Model:\s*([A-Za-z0-9\-]+)", response_SARA_ATI)
|
||||||
|
if match:
|
||||||
|
model = match.group(1).strip()
|
||||||
|
else:
|
||||||
|
model = "Unknown"
|
||||||
|
print("⚠️ Could not identify modem model")
|
||||||
|
|
||||||
|
print(f"🔍 Model: {model}")
|
||||||
update_json_key(config_file, "modem_version", model)
|
update_json_key(config_file, "modem_version", model)
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
|
|
||||||
|
'''
|
||||||
# 1. Set AIRCARTO URL
|
AIRCARTO
|
||||||
|
'''
|
||||||
|
# 1. Set AIRCARTO URL (profile id = 0)
|
||||||
print('➡️Set aircarto URL')
|
print('➡️Set aircarto URL')
|
||||||
aircarto_profile_id = 0
|
aircarto_profile_id = 0
|
||||||
aircarto_url="data.nebuleair.fr"
|
aircarto_url="data.nebuleair.fr"
|
||||||
@@ -143,26 +167,155 @@ try:
|
|||||||
print(response_SARA_1)
|
print(response_SARA_1)
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
|
|
||||||
#2. Set uSpot URL
|
'''
|
||||||
print('➡️Set uSpot URL')
|
uSpot
|
||||||
|
'''
|
||||||
|
print("➡️➡️Set uSpot URL with SSL")
|
||||||
|
|
||||||
|
security_profile_id = 1
|
||||||
uSpot_profile_id = 1
|
uSpot_profile_id = 1
|
||||||
uSpot_url="api-prod.uspot.probesys.net"
|
uSpot_url="api-prod.uspot.probesys.net"
|
||||||
command = f'AT+UHTTP={uSpot_profile_id},1,"{uSpot_url}"\r'
|
|
||||||
ser_sara.write(command.encode('utf-8'))
|
|
||||||
response_SARA_2 = read_complete_response(ser_sara, wait_for_lines=["OK"])
|
#step 1: import the certificate
|
||||||
|
print("➡️ import certificate")
|
||||||
|
certificate_name = "e6"
|
||||||
|
with open("/var/www/nebuleair_pro_4g/SARA/SSL/certificate/e6.pem", "rb") as cert_file:
|
||||||
|
certificate = cert_file.read()
|
||||||
|
size_of_string = len(certificate)
|
||||||
|
|
||||||
|
# AT+USECMNG=0,<type>,<internal_name>,<data_size>
|
||||||
|
# type-> 0 -> trusted root CA
|
||||||
|
command = f'AT+USECMNG=0,0,"{certificate_name}",{size_of_string}\r'
|
||||||
|
ser_sara.write((command + '\r').encode('utf-8'))
|
||||||
|
response_SARA_1 = read_complete_response(ser_sara)
|
||||||
|
print(response_SARA_1)
|
||||||
|
|
||||||
|
time.sleep(0.5)
|
||||||
|
|
||||||
|
print("➡️ add certificate")
|
||||||
|
ser_sara.write(certificate)
|
||||||
|
response_SARA_2 = read_complete_response(ser_sara)
|
||||||
print(response_SARA_2)
|
print(response_SARA_2)
|
||||||
|
|
||||||
|
time.sleep(0.5)
|
||||||
|
|
||||||
|
# op_code: 0 -> certificate validation level
|
||||||
|
# param_val : 0 -> Level 0 No validation; 1-> Level 1 Root certificate validation
|
||||||
|
print("➡️Set the security profile (params)")
|
||||||
|
certification_level=0
|
||||||
|
command = f'AT+USECPRF={security_profile_id},0,{certification_level}\r'
|
||||||
|
ser_sara.write((command + '\r').encode('utf-8'))
|
||||||
|
response_SARA_5b = read_complete_response(ser_sara, wait_for_lines=["OK"])
|
||||||
|
print(response_SARA_5b)
|
||||||
|
time.sleep(0.5)
|
||||||
|
|
||||||
|
# op_code: 1 -> minimum SSL/TLS version
|
||||||
|
# param_val : 0 -> any; server can use any version for the connection; 1-> LSv1.0; 2->TLSv1.1; 3->TLSv1.2;
|
||||||
|
print("➡️Set the security profile (params)")
|
||||||
|
minimum_SSL_version = 0
|
||||||
|
command = f'AT+USECPRF={security_profile_id},1,{minimum_SSL_version}\r'
|
||||||
|
ser_sara.write((command + '\r').encode('utf-8'))
|
||||||
|
response_SARA_5bb = read_complete_response(ser_sara, wait_for_lines=["OK"])
|
||||||
|
print(response_SARA_5bb)
|
||||||
|
time.sleep(0.5)
|
||||||
|
|
||||||
|
#op_code: 2 -> legacy cipher suite selection
|
||||||
|
# 0 (factory-programmed value): a list of default cipher suites is proposed at the beginning of handshake process, and a cipher suite will be negotiated among the cipher suites proposed in the list.
|
||||||
|
print("➡️Set cipher")
|
||||||
|
cipher_suite = 0
|
||||||
|
command = f'AT+USECPRF={security_profile_id},2,{cipher_suite}\r'
|
||||||
|
ser_sara.write((command + '\r').encode('utf-8'))
|
||||||
|
response_SARA_5cc = read_complete_response(ser_sara, wait_for_lines=["OK"])
|
||||||
|
print(response_SARA_5cc)
|
||||||
|
time.sleep(0.5)
|
||||||
|
|
||||||
|
# op_code: 3 -> trusted root certificate internal name
|
||||||
|
print("➡️Set the security profile (choose cert)")
|
||||||
|
command = f'AT+USECPRF={security_profile_id},3,"{certificate_name}"\r'
|
||||||
|
ser_sara.write((command + '\r').encode('utf-8'))
|
||||||
|
response_SARA_5c = read_complete_response(ser_sara, wait_for_lines=["OK"])
|
||||||
|
print(response_SARA_5c)
|
||||||
|
time.sleep(0.5)
|
||||||
|
|
||||||
|
# op_code: 10 -> SNI (server name indication)
|
||||||
|
print("➡️Set the SNI")
|
||||||
|
command = f'AT+USECPRF={security_profile_id},10,"{uSpot_url}"\r'
|
||||||
|
ser_sara.write((command + '\r').encode('utf-8'))
|
||||||
|
response_SARA_5cf = read_complete_response(ser_sara, wait_for_lines=["OK"])
|
||||||
|
print(response_SARA_5cf)
|
||||||
|
time.sleep(0.5)
|
||||||
|
|
||||||
|
#step 4: set url (op_code = 1)
|
||||||
|
print("➡️SET URL")
|
||||||
|
command = f'AT+UHTTP={uSpot_profile_id},1,"{uSpot_url}"\r'
|
||||||
|
ser_sara.write((command + '\r').encode('utf-8'))
|
||||||
|
response_SARA_5 = read_complete_response(ser_sara, wait_for_lines=["OK"])
|
||||||
|
print(response_SARA_5)
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
|
|
||||||
print("set port 81")
|
#step 4: set PORT (op_code = 5)
|
||||||
command = f'AT+UHTTP={uSpot_profile_id},5,81\r'
|
print("➡️SET PORT")
|
||||||
|
port = 443
|
||||||
|
command = f'AT+UHTTP={uSpot_profile_id},5,{port}\r'
|
||||||
ser_sara.write((command + '\r').encode('utf-8'))
|
ser_sara.write((command + '\r').encode('utf-8'))
|
||||||
response_SARA_55 = read_complete_response(ser_sara, wait_for_lines=["OK"])
|
response_SARA_55 = read_complete_response(ser_sara, wait_for_lines=["OK"])
|
||||||
print(response_SARA_55)
|
print(response_SARA_55)
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
|
|
||||||
|
#step 4: set url to SSL (op_code = 6) (http_secure = 1 for HTTPS)(USECMNG_PROFILE = 2)
|
||||||
|
print("➡️SET SSL")
|
||||||
|
http_secure = 1
|
||||||
|
command = f'AT+UHTTP={uSpot_profile_id},6,{http_secure},{security_profile_id}\r'
|
||||||
|
|
||||||
|
ser_sara.write(command.encode('utf-8'))
|
||||||
|
response_SARA_5fg = read_complete_response(ser_sara, wait_for_lines=["OK"])
|
||||||
|
print(response_SARA_5fg)
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
|
||||||
|
'''
|
||||||
|
SARA R5
|
||||||
|
'''
|
||||||
|
|
||||||
|
if sara_r5_DPD_setup:
|
||||||
|
print("➡️➡️SARA R5 PDP SETUP")
|
||||||
|
# 2. Activate PDP context 1
|
||||||
|
print('➡️Activate PDP context 1')
|
||||||
|
command = f'AT+CGACT=1,1\r'
|
||||||
|
ser_sara.write(command.encode('utf-8'))
|
||||||
|
response_SARA_2 = read_complete_response(ser_sara, wait_for_lines=["OK"])
|
||||||
|
print(response_SARA_2, end="")
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
# 2. Set the PDP type
|
||||||
|
print('➡️Set the PDP type to IPv4 referring to the outputof the +CGDCONT read command')
|
||||||
|
command = f'AT+UPSD=0,0,0\r'
|
||||||
|
ser_sara.write(command.encode('utf-8'))
|
||||||
|
response_SARA_3 = read_complete_response(ser_sara, wait_for_lines=["OK"])
|
||||||
|
print(response_SARA_3, end="")
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
# 2. Profile #0 is mapped on CID=1.
|
||||||
|
print('➡️Profile #0 is mapped on CID=1.')
|
||||||
|
command = f'AT+UPSD=0,100,1\r'
|
||||||
|
ser_sara.write(command.encode('utf-8'))
|
||||||
|
response_SARA_3 = read_complete_response(ser_sara, wait_for_lines=["OK"])
|
||||||
|
print(response_SARA_3, end="")
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
# 2. Set the PDP type
|
||||||
|
print('➡️Activate the PSD profile #0: the IPv4 address is already assigned by the network.')
|
||||||
|
command = f'AT+UPSDA=0,3\r'
|
||||||
|
ser_sara.write(command.encode('utf-8'))
|
||||||
|
response_SARA_3 = read_complete_response(ser_sara, wait_for_lines=["OK","+UUPSDA"])
|
||||||
|
print(response_SARA_3, end="")
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
|
||||||
#3. Get localisation (CellLocate)
|
#3. Get localisation (CellLocate)
|
||||||
mode = 2
|
mode = 2 #single shot position
|
||||||
sensor = 2
|
sensor = 2 #use cellular CellLocate® location information
|
||||||
response_type = 0
|
response_type = 0
|
||||||
timeout_s = 2
|
timeout_s = 2
|
||||||
accuracy_m = 1
|
accuracy_m = 1
|
||||||
|
|||||||
35
SARA/sara.py
35
SARA/sara.py
@@ -7,6 +7,8 @@
|
|||||||
|
|
||||||
Script to see if the SARA-R410 is running
|
Script to see if the SARA-R410 is running
|
||||||
ex:
|
ex:
|
||||||
|
python3 /var/www/nebuleair_pro_4g/SARA/sara.py ttyAMA2 ATI 2
|
||||||
|
ex 1 (get SIM infos)
|
||||||
python3 /var/www/nebuleair_pro_4g/SARA/sara.py ttyAMA2 AT+CCID? 2
|
python3 /var/www/nebuleair_pro_4g/SARA/sara.py ttyAMA2 AT+CCID? 2
|
||||||
ex 2 (turn on blue light):
|
ex 2 (turn on blue light):
|
||||||
python3 /var/www/nebuleair_pro_4g/SARA/sara.py ttyAMA2 AT+UGPIOC=16,2 2
|
python3 /var/www/nebuleair_pro_4g/SARA/sara.py ttyAMA2 AT+UGPIOC=16,2 2
|
||||||
@@ -14,6 +16,8 @@ ex 3 (reconnect network)
|
|||||||
python3 /var/www/nebuleair_pro_4g/SARA/sara.py ttyAMA2 AT+COPS=1,2,20801 20
|
python3 /var/www/nebuleair_pro_4g/SARA/sara.py ttyAMA2 AT+COPS=1,2,20801 20
|
||||||
ex 4 (get HTTP Profiles)
|
ex 4 (get HTTP Profiles)
|
||||||
python3 /var/www/nebuleair_pro_4g/SARA/sara.py ttyAMA2 AT+UHTTP? 2
|
python3 /var/www/nebuleair_pro_4g/SARA/sara.py ttyAMA2 AT+UHTTP? 2
|
||||||
|
ex 5 (get IP addr)
|
||||||
|
python3 /var/www/nebuleair_pro_4g/SARA/sara.py ttyAMA2 AT+CGPADDR=1 2
|
||||||
|
|
||||||
'''
|
'''
|
||||||
|
|
||||||
@@ -45,6 +49,9 @@ config = load_config(config_file)
|
|||||||
# Access the shared variables
|
# Access the shared variables
|
||||||
baudrate = config.get('SaraR4_baudrate', 115200)
|
baudrate = config.get('SaraR4_baudrate', 115200)
|
||||||
|
|
||||||
|
|
||||||
|
try:
|
||||||
|
|
||||||
ser = serial.Serial(
|
ser = serial.Serial(
|
||||||
port=port, #USB0 or ttyS0
|
port=port, #USB0 or ttyS0
|
||||||
baudrate=baudrate, #115200 ou 9600
|
baudrate=baudrate, #115200 ou 9600
|
||||||
@@ -71,25 +78,33 @@ ser.write((command + '\r').encode('utf-8'))
|
|||||||
#ser.write(b'AT+CMUX=?')
|
#ser.write(b'AT+CMUX=?')
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
try:
|
|
||||||
# Read lines until a timeout occurs
|
# Read lines until a timeout occurs
|
||||||
response_lines = []
|
response_lines = []
|
||||||
while True:
|
start_time = time.time()
|
||||||
line = ser.readline().decode('utf-8').strip()
|
|
||||||
if not line:
|
while (time.time() - start_time) < timeout:
|
||||||
break # Break the loop if an empty line is encountered
|
line = ser.readline().decode('utf-8', errors='ignore').strip()
|
||||||
|
if line:
|
||||||
response_lines.append(line)
|
response_lines.append(line)
|
||||||
|
|
||||||
|
# Check if we received any data
|
||||||
|
if not response_lines:
|
||||||
|
print(f"ERROR: No response received from {port} after sending command: {command}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
# Print the response
|
# Print the response
|
||||||
for line in response_lines:
|
for line in response_lines:
|
||||||
print(line)
|
print(line)
|
||||||
|
|
||||||
except serial.SerialException as e:
|
except serial.SerialException as e:
|
||||||
print(f"Error: {e}")
|
print(f"ERROR: Serial communication error: {e}")
|
||||||
|
sys.exit(1)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"ERROR: Unexpected error: {e}")
|
||||||
|
sys.exit(1)
|
||||||
finally:
|
finally:
|
||||||
if ser.is_open:
|
# Close the serial port if it's open
|
||||||
|
if 'ser' in locals() and ser.is_open:
|
||||||
ser.close()
|
ser.close()
|
||||||
#print("Serial closed")
|
|
||||||
|
|
||||||
|
|||||||
79
SARA/sara_checkDNS.py
Normal file
79
SARA/sara_checkDNS.py
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
'''
|
||||||
|
____ _ ____ _
|
||||||
|
/ ___| / \ | _ \ / \
|
||||||
|
\___ \ / _ \ | |_) | / _ \
|
||||||
|
___) / ___ \| _ < / ___ \
|
||||||
|
|____/_/ \_\_| \_\/_/ \_\
|
||||||
|
|
||||||
|
Script to resolve DNS (get IP from domain name) with AT+UDNSRN command
|
||||||
|
Ex:
|
||||||
|
/usr/bin/python3 /var/www/nebuleair_pro_4g/SARA/sara_checkDNS.py ttyAMA2 data.nebuleair.fr
|
||||||
|
To do: need to add profile id as parameter
|
||||||
|
|
||||||
|
'''
|
||||||
|
|
||||||
|
import serial
|
||||||
|
import time
|
||||||
|
import sys
|
||||||
|
import json
|
||||||
|
|
||||||
|
parameter = sys.argv[1:] # Exclude the script name
|
||||||
|
#print("Parameters received:")
|
||||||
|
port='/dev/'+parameter[0] # ex: ttyAMA2
|
||||||
|
url = parameter[1] # ex: data.mobileair.fr
|
||||||
|
|
||||||
|
|
||||||
|
#get baudrate
|
||||||
|
def load_config(config_file):
|
||||||
|
try:
|
||||||
|
with open(config_file, 'r') as file:
|
||||||
|
config_data = json.load(file)
|
||||||
|
return config_data
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error loading config file: {e}")
|
||||||
|
return {}
|
||||||
|
|
||||||
|
# Define the config file path
|
||||||
|
config_file = '/var/www/nebuleair_pro_4g/config.json'
|
||||||
|
# Load the configuration data
|
||||||
|
config = load_config(config_file)
|
||||||
|
# Access the shared variables
|
||||||
|
baudrate = config.get('SaraR4_baudrate', 115200)
|
||||||
|
|
||||||
|
ser = serial.Serial(
|
||||||
|
port=port, #USB0 or ttyS0
|
||||||
|
baudrate=baudrate, #115200 ou 9600
|
||||||
|
parity=serial.PARITY_NONE, #PARITY_NONE, PARITY_EVEN or PARITY_ODD
|
||||||
|
stopbits=serial.STOPBITS_ONE,
|
||||||
|
bytesize=serial.EIGHTBITS,
|
||||||
|
timeout = 2
|
||||||
|
)
|
||||||
|
|
||||||
|
command = f'AT+UDNSRN=0,"{url}"\r'
|
||||||
|
ser.write((command + '\r').encode('utf-8'))
|
||||||
|
|
||||||
|
print("****")
|
||||||
|
print("DNS check")
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Read lines until a timeout occurs
|
||||||
|
response_lines = []
|
||||||
|
while True:
|
||||||
|
line = ser.readline().decode('utf-8').strip()
|
||||||
|
if not line:
|
||||||
|
break # Break the loop if an empty line is encountered
|
||||||
|
response_lines.append(line)
|
||||||
|
|
||||||
|
# Print the response
|
||||||
|
for line in response_lines:
|
||||||
|
print(line)
|
||||||
|
|
||||||
|
except serial.SerialException as e:
|
||||||
|
print(f"Error: {e}")
|
||||||
|
|
||||||
|
finally:
|
||||||
|
if ser.is_open:
|
||||||
|
ser.close()
|
||||||
|
print("****")
|
||||||
|
#print("Serial closed")
|
||||||
|
|
||||||
@@ -89,6 +89,24 @@ def read_complete_response(serial_connection, timeout=2, end_of_response_timeout
|
|||||||
return response.decode('utf-8', errors='replace') # Return the full response if no target line is found
|
return response.decode('utf-8', errors='replace') # Return the full response if no target line is found
|
||||||
|
|
||||||
|
|
||||||
|
def extract_error_code(response):
|
||||||
|
"""
|
||||||
|
Extract just the error code from AT+UHTTPER response
|
||||||
|
"""
|
||||||
|
for line in response.split('\n'):
|
||||||
|
if '+UHTTPER' in line:
|
||||||
|
try:
|
||||||
|
# Split the line and get the third value (error code)
|
||||||
|
parts = line.split(':')[1].strip().split(',')
|
||||||
|
if len(parts) >= 3:
|
||||||
|
error_code = int(parts[2])
|
||||||
|
return error_code
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Return None if we couldn't find the error code
|
||||||
|
return None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
#3. Send to endpoint (with device ID)
|
#3. Send to endpoint (with device ID)
|
||||||
print("Send data (GET REQUEST):")
|
print("Send data (GET REQUEST):")
|
||||||
@@ -111,7 +129,36 @@ try:
|
|||||||
parts = http_response.split(',')
|
parts = http_response.split(',')
|
||||||
# 2.1 code 0 (HTTP failed) ⛔⛔⛔
|
# 2.1 code 0 (HTTP failed) ⛔⛔⛔
|
||||||
if len(parts) == 3 and parts[-1] == '0': # The third value indicates success
|
if len(parts) == 3 and parts[-1] == '0': # The third value indicates success
|
||||||
print("⛔ATTENTION: HTTP operation failed")
|
print("⛔⛔ATTENTION: HTTP operation failed")
|
||||||
|
#get error code
|
||||||
|
print("Getting error code (11->Server connection error, 73->Secure socket connect error)")
|
||||||
|
command = f'AT+UHTTPER={aircarto_profile_id}\r'
|
||||||
|
ser_sara.write(command.encode('utf-8'))
|
||||||
|
response_SARA_9 = read_complete_response(ser_sara, wait_for_lines=["OK"], debug=False)
|
||||||
|
print('<p class="text-danger-emphasis">')
|
||||||
|
print(response_SARA_9)
|
||||||
|
print("</p>", end="")
|
||||||
|
# Extract just the error code
|
||||||
|
error_code = extract_error_code(response_SARA_9)
|
||||||
|
if error_code is not None:
|
||||||
|
# Display interpretation based on error code
|
||||||
|
if error_code == 0:
|
||||||
|
print('<p class="text-success">No error detected</p>')
|
||||||
|
elif error_code == 4:
|
||||||
|
print('<p class="text-danger">Error 4: Invalid server Hostname</p>')
|
||||||
|
elif error_code == 11:
|
||||||
|
print('<p class="text-danger">Error 11: Server connection error</p>')
|
||||||
|
elif error_code == 22:
|
||||||
|
print('<p class="text-danger">Error 22: PSD or CSD connection not established</p>')
|
||||||
|
elif error_code == 73:
|
||||||
|
print('<p class="text-danger">Error 73: Secure socket connect error</p>')
|
||||||
|
else:
|
||||||
|
print(f'<p class="text-danger">Unknown error code: {error_code}</p>')
|
||||||
|
else:
|
||||||
|
print('<p class="text-danger">Could not extract error code from response</p>')
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# 2.2 code 1 (HHTP succeded)
|
# 2.2 code 1 (HHTP succeded)
|
||||||
else:
|
else:
|
||||||
# Si la commande HTTP a réussi
|
# Si la commande HTTP a réussi
|
||||||
|
|||||||
@@ -49,6 +49,8 @@ ser = serial.Serial(
|
|||||||
)
|
)
|
||||||
|
|
||||||
command = f'AT+CGDCONT=1,"IP","{apn_address}"\r'
|
command = f'AT+CGDCONT=1,"IP","{apn_address}"\r'
|
||||||
|
#command = f'AT+CGDCONT=1,"IPV4V6","{apn_address}"\r'
|
||||||
|
#command = f'AT+CGDCONT=1,"IP","{apn_address}",0,0\r'
|
||||||
ser.write((command + '\r').encode('utf-8'))
|
ser.write((command + '\r').encode('utf-8'))
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,6 @@
|
|||||||
Script to set the URL for a HTTP request
|
Script to set the URL for a HTTP request
|
||||||
Ex:
|
Ex:
|
||||||
/usr/bin/python3 /var/www/nebuleair_pro_4g/SARA/sara_setURL.py ttyAMA2 data.nebuleair.fr 0
|
/usr/bin/python3 /var/www/nebuleair_pro_4g/SARA/sara_setURL.py ttyAMA2 data.nebuleair.fr 0
|
||||||
To do: need to add profile id as parameter
|
|
||||||
|
|
||||||
First profile id:
|
First profile id:
|
||||||
AT+UHTTP=0,1,"data.nebuleair.fr"
|
AT+UHTTP=0,1,"data.nebuleair.fr"
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ echo "-------------------"
|
|||||||
|
|
||||||
echo "NebuleAir pro started at $(date)"
|
echo "NebuleAir pro started at $(date)"
|
||||||
|
|
||||||
|
chmod -R 777 /var/www/nebuleair_pro_4g/
|
||||||
|
|
||||||
# Blink GPIO 23 and 24 five times
|
# Blink GPIO 23 and 24 five times
|
||||||
for i in {1..5}; do
|
for i in {1..5}; do
|
||||||
# Turn GPIO 23 and 24 ON
|
# Turn GPIO 23 and 24 ON
|
||||||
|
|||||||
@@ -5,8 +5,11 @@
|
|||||||
"RTC/save_to_db.py": true,
|
"RTC/save_to_db.py": true,
|
||||||
"BME280/get_data_v2.py": true,
|
"BME280/get_data_v2.py": true,
|
||||||
"envea/read_value_v2.py": false,
|
"envea/read_value_v2.py": false,
|
||||||
|
"MPPT/read.py": false,
|
||||||
|
"windMeter/read.py": false,
|
||||||
"sqlite/flush_old_data.py": true,
|
"sqlite/flush_old_data.py": true,
|
||||||
"deviceID": "XXXX",
|
"deviceID": "XXXX",
|
||||||
|
"npm_5channel": false,
|
||||||
"latitude_raw": 0,
|
"latitude_raw": 0,
|
||||||
"longitude_raw":0,
|
"longitude_raw":0,
|
||||||
"latitude_precision": 0,
|
"latitude_precision": 0,
|
||||||
@@ -25,7 +28,7 @@
|
|||||||
"SARA_R4_general_status": "connected",
|
"SARA_R4_general_status": "connected",
|
||||||
"SARA_R4_SIM_status": "connected",
|
"SARA_R4_SIM_status": "connected",
|
||||||
"SARA_R4_network_status": "connected",
|
"SARA_R4_network_status": "connected",
|
||||||
"SARA_R4_neworkID": 0,
|
"SARA_R4_neworkID": 20810,
|
||||||
"WIFI_status": "connected",
|
"WIFI_status": "connected",
|
||||||
"MQTT_GUI": false,
|
"MQTT_GUI": false,
|
||||||
"send_aircarto": true,
|
"send_aircarto": true,
|
||||||
|
|||||||
@@ -5,3 +5,6 @@
|
|||||||
@reboot sleep 30 && /usr/bin/python3 /var/www/nebuleair_pro_4g/SARA/reboot/start.py >> /var/www/nebuleair_pro_4g/logs/app.log 2>&1
|
@reboot sleep 30 && /usr/bin/python3 /var/www/nebuleair_pro_4g/SARA/reboot/start.py >> /var/www/nebuleair_pro_4g/logs/app.log 2>&1
|
||||||
|
|
||||||
0 0 * * * > /var/www/nebuleair_pro_4g/logs/master.log
|
0 0 * * * > /var/www/nebuleair_pro_4g/logs/master.log
|
||||||
|
0 0 * * * > /var/www/nebuleair_pro_4g/logs/master_errors.log
|
||||||
|
0 0 * * * > /var/www/nebuleair_pro_4g/logs/app.log
|
||||||
|
|
||||||
|
|||||||
344
html/config.html
344
html/config.html
@@ -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>
|
|
||||||
@@ -4,8 +4,7 @@ header("Content-Type: application/json");
|
|||||||
header("Cache-Control: no-store, no-cache, must-revalidate, max-age=0");
|
header("Cache-Control: no-store, no-cache, must-revalidate, max-age=0");
|
||||||
header("Pragma: no-cache");
|
header("Pragma: no-cache");
|
||||||
|
|
||||||
// Get request type from GET or POST parameters
|
$type=$_GET['type'];
|
||||||
$type = isset($_GET['type']) ? $_GET['type'] : (isset($_POST['type']) ? $_POST['type'] : '');
|
|
||||||
|
|
||||||
if ($type == "get_npm_sqlite_data") {
|
if ($type == "get_npm_sqlite_data") {
|
||||||
$database_path = "/var/www/nebuleair_pro_4g/sqlite/sensors.db";
|
$database_path = "/var/www/nebuleair_pro_4g/sqlite/sensors.db";
|
||||||
@@ -485,193 +484,3 @@ if ($type == "wifi_scan_old") {
|
|||||||
echo $json_data;
|
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'
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -47,18 +47,6 @@
|
|||||||
</svg>
|
</svg>
|
||||||
Carte
|
Carte
|
||||||
</a>
|
</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">
|
<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"/>
|
||||||
|
|||||||
@@ -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>
|
|
||||||
@@ -27,6 +27,10 @@ fi
|
|||||||
info "Set up the RTC"
|
info "Set up the RTC"
|
||||||
/usr/bin/python3 /var/www/nebuleair_pro_4g/RTC/set_with_NTP.py
|
/usr/bin/python3 /var/www/nebuleair_pro_4g/RTC/set_with_NTP.py
|
||||||
|
|
||||||
|
#Check SARA R4 connection
|
||||||
|
info "Check SARA R4 connection"
|
||||||
|
/usr/bin/python3 /var/www/nebuleair_pro_4g/SARA/sara.py ttyAMA2 ATI 2
|
||||||
|
|
||||||
#set up SARA R4 APN
|
#set up SARA R4 APN
|
||||||
info "Set up Monogoto APN"
|
info "Set up Monogoto APN"
|
||||||
/usr/bin/python3 /var/www/nebuleair_pro_4g/SARA/sara_setAPN.py ttyAMA2 data.mono 2
|
/usr/bin/python3 /var/www/nebuleair_pro_4g/SARA/sara_setAPN.py ttyAMA2 data.mono 2
|
||||||
@@ -39,7 +43,11 @@ info "Activate blue LED"
|
|||||||
info "Connect SARA R4 to network"
|
info "Connect SARA R4 to network"
|
||||||
python3 /var/www/nebuleair_pro_4g/SARA/sara_connectNetwork.py ttyAMA2 20810 60
|
python3 /var/www/nebuleair_pro_4g/SARA/sara_connectNetwork.py ttyAMA2 20810 60
|
||||||
|
|
||||||
#Add master_nebuleair.service
|
#Need to create the two service
|
||||||
|
# 1. master_nebuleair
|
||||||
|
# 2. rtc_save_to_db
|
||||||
|
|
||||||
|
#1. Add master_nebuleair.service
|
||||||
SERVICE_FILE="/etc/systemd/system/master_nebuleair.service"
|
SERVICE_FILE="/etc/systemd/system/master_nebuleair.service"
|
||||||
info "Setting up systemd service for master_nebuleair..."
|
info "Setting up systemd service for master_nebuleair..."
|
||||||
|
|
||||||
@@ -74,3 +82,41 @@ sudo systemctl enable master_nebuleair.service
|
|||||||
# Start the service immediately
|
# Start the service immediately
|
||||||
info "Starting the service..."
|
info "Starting the service..."
|
||||||
sudo systemctl start master_nebuleair.service
|
sudo systemctl start master_nebuleair.service
|
||||||
|
|
||||||
|
|
||||||
|
#2. Add rtc_save_to_db.service
|
||||||
|
SERVICE_FILE_2="/etc/systemd/system/rtc_save_to_db.service"
|
||||||
|
info "Setting up systemd service for rtc_save_to_db..."
|
||||||
|
|
||||||
|
# Create the systemd service file (overwrite if necessary)
|
||||||
|
sudo bash -c "cat > $SERVICE_FILE_2" <<EOF
|
||||||
|
[Unit]
|
||||||
|
Description=RTC Save to DB Script
|
||||||
|
After=network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
ExecStart=/usr/bin/python3 /var/www/nebuleair_pro_4g/RTC/save_to_db.py
|
||||||
|
Restart=always
|
||||||
|
RestartSec=1
|
||||||
|
User=root
|
||||||
|
WorkingDirectory=/var/www/nebuleair_pro_4g
|
||||||
|
StandardOutput=append:/var/www/nebuleair_pro_4g/logs/rtc_save_to_db.log
|
||||||
|
StandardError=append:/var/www/nebuleair_pro_4g/logs/rtc_save_to_db_errors.log
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
EOF
|
||||||
|
|
||||||
|
success "Systemd service file created: $SERVICE_FILE_2"
|
||||||
|
|
||||||
|
# Reload systemd to recognize the new service
|
||||||
|
info "Reloading systemd daemon..."
|
||||||
|
sudo systemctl daemon-reload
|
||||||
|
|
||||||
|
# Enable the service to start on boot
|
||||||
|
info "Enabling the service to start on boot..."
|
||||||
|
sudo systemctl enable rtc_save_to_db.service
|
||||||
|
|
||||||
|
# Start the service immediately
|
||||||
|
info "Starting the service..."
|
||||||
|
sudo systemctl start rtc_save_to_db.service
|
||||||
|
|||||||
@@ -49,6 +49,11 @@ CSV PAYLOAD (AirCarto Servers)
|
|||||||
17 -> PM 5.0μm to 10μm quantity (Nb/L)
|
17 -> PM 5.0μm to 10μm quantity (Nb/L)
|
||||||
18 -> NPM temp inside
|
18 -> NPM temp inside
|
||||||
19 -> NPM hum inside
|
19 -> NPM hum inside
|
||||||
|
20 -> battery_voltage
|
||||||
|
21 -> battery_current
|
||||||
|
22 -> solar_voltage
|
||||||
|
23 -> solar_power
|
||||||
|
24 -> charger_status
|
||||||
|
|
||||||
JSON PAYLOAD (Micro-Spot Servers)
|
JSON PAYLOAD (Micro-Spot Servers)
|
||||||
Same as NebuleAir wifi
|
Same as NebuleAir wifi
|
||||||
@@ -94,6 +99,7 @@ import time
|
|||||||
import busio
|
import busio
|
||||||
import re
|
import re
|
||||||
import os
|
import os
|
||||||
|
import requests
|
||||||
import traceback
|
import traceback
|
||||||
import threading
|
import threading
|
||||||
import sys
|
import sys
|
||||||
@@ -115,7 +121,7 @@ if uptime_seconds < 120:
|
|||||||
sys.exit()
|
sys.exit()
|
||||||
|
|
||||||
#Payload CSV to be sent to data.nebuleair.fr
|
#Payload CSV to be sent to data.nebuleair.fr
|
||||||
payload_csv = [None] * 25
|
payload_csv = [None] * 30
|
||||||
#Payload JSON to be sent to uSpot
|
#Payload JSON to be sent to uSpot
|
||||||
payload_json = {
|
payload_json = {
|
||||||
"nebuleairid": "XXX",
|
"nebuleairid": "XXX",
|
||||||
@@ -205,16 +211,18 @@ config_file = '/var/www/nebuleair_pro_4g/config.json'
|
|||||||
config = load_config(config_file)
|
config = load_config(config_file)
|
||||||
device_latitude_raw = config.get('latitude_raw', 0)
|
device_latitude_raw = config.get('latitude_raw', 0)
|
||||||
device_longitude_raw = config.get('longitude_raw', 0)
|
device_longitude_raw = config.get('longitude_raw', 0)
|
||||||
|
|
||||||
baudrate = config.get('SaraR4_baudrate', 115200) #baudrate du sara R4
|
baudrate = config.get('SaraR4_baudrate', 115200) #baudrate du sara R4
|
||||||
device_id = config.get('deviceID', '').upper() #device ID en maj
|
device_id = config.get('deviceID', '').upper() #device ID en maj
|
||||||
bme_280_config = config.get('BME280/get_data_v2.py', False) #présence du BME280
|
bme_280_config = config.get('BME280/get_data_v2.py', False) #présence du BME280
|
||||||
envea_cairsens= config.get('envea/read_value_v2.py', False)
|
envea_cairsens= config.get('envea/read_value_v2.py', False)
|
||||||
|
mppt_charger= config.get('MPPT/read.py', False)
|
||||||
|
wind_meter= config.get('windMeter/read.py', False)
|
||||||
send_aircarto = config.get('send_aircarto', True) #envoi sur AirCarto (data.nebuleair.fr)
|
send_aircarto = config.get('send_aircarto', True) #envoi sur AirCarto (data.nebuleair.fr)
|
||||||
send_uSpot = config.get('send_uSpot', False) #envoi sur MicroSpot ()
|
send_uSpot = config.get('send_uSpot', False) #envoi sur MicroSpot ()
|
||||||
|
reset_uSpot_url = False
|
||||||
selected_networkID = int(config.get('SARA_R4_neworkID', 0))
|
selected_networkID = int(config.get('SARA_R4_neworkID', 0))
|
||||||
npm_5channel = config.get('NextPM_5channels', False) #5 canaux du NPM
|
npm_5channel = config.get('NextPM_5channels', False) #5 canaux du NPM
|
||||||
|
modem_version=config.get('modem_version', "")
|
||||||
modem_config_mode = config.get('modem_config_mode', False) #modem 4G en mode configuration
|
modem_config_mode = config.get('modem_config_mode', False) #modem 4G en mode configuration
|
||||||
|
|
||||||
#update device id in the payload json
|
#update device id in the payload json
|
||||||
@@ -278,6 +286,139 @@ def read_complete_response(serial_connection, timeout=2, end_of_response_timeout
|
|||||||
|
|
||||||
return response.decode('utf-8', errors='replace') # Return the full response if no target line is found
|
return response.decode('utf-8', errors='replace') # Return the full response if no target line is found
|
||||||
|
|
||||||
|
def extract_error_code(response):
|
||||||
|
"""
|
||||||
|
Extract just the error code from AT+UHTTPER response
|
||||||
|
"""
|
||||||
|
for line in response.split('\n'):
|
||||||
|
if '+UHTTPER' in line:
|
||||||
|
try:
|
||||||
|
# Split the line and get the third value (error code)
|
||||||
|
parts = line.split(':')[1].strip().split(',')
|
||||||
|
if len(parts) >= 3:
|
||||||
|
error_code = int(parts[2])
|
||||||
|
return error_code
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Return None if we couldn't find the error code
|
||||||
|
return None
|
||||||
|
|
||||||
|
def modem_complete_reboot_and_reinitialize(modem_version, aircarto_profile_id):
|
||||||
|
"""
|
||||||
|
Performs a complete modem restart sequence:
|
||||||
|
1. Reboots the modem using the appropriate command for its version
|
||||||
|
2. Waits for the modem to restart
|
||||||
|
3. Resets the HTTP profile
|
||||||
|
4. For SARA-R5, resets the PDP connection
|
||||||
|
|
||||||
|
Args:
|
||||||
|
modem_version (str): The modem version, e.g., 'SARA-R500' or 'SARA-R410'
|
||||||
|
aircarto_profile_id (int): The HTTP profile ID to reset
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True if the complete sequence was successful, False otherwise
|
||||||
|
"""
|
||||||
|
print('<span style="color: orange;font-weight: bold;">🔄 Complete SARA reboot and reinitialize sequence 🔄</span>')
|
||||||
|
|
||||||
|
# Step 1: Reboot the modem - Integrated modem_software_reboot logic
|
||||||
|
print('<span style="color: orange;font-weight: bold;">🔄 Software SARA reboot! 🔄</span>')
|
||||||
|
|
||||||
|
# Use different commands based on modem version
|
||||||
|
if 'R5' in modem_version: # For SARA-R5 series
|
||||||
|
command = 'AT+CFUN=16\r' # Normal restart for R5
|
||||||
|
else: # For SARA-R4 series
|
||||||
|
command = 'AT+CFUN=15\r' # Factory reset for R4
|
||||||
|
|
||||||
|
ser_sara.write(command.encode('utf-8'))
|
||||||
|
response = read_complete_response(ser_sara, wait_for_lines=["OK", "ERROR"], debug=True)
|
||||||
|
|
||||||
|
print('<p class="text-danger-emphasis">')
|
||||||
|
print(response)
|
||||||
|
print("</p>", end="")
|
||||||
|
|
||||||
|
# Check if reboot command was acknowledged
|
||||||
|
reboot_success = response is not None and "OK" in response
|
||||||
|
if not reboot_success:
|
||||||
|
print("⚠️ Modem reboot command failed")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Step 2: Wait for the modem to restart (adjust time as needed)
|
||||||
|
print("Waiting for modem to restart...")
|
||||||
|
time.sleep(15) # 15 seconds should be enough for most modems to restart
|
||||||
|
|
||||||
|
# Step 3: Check if modem is responsive after reboot
|
||||||
|
print("Checking if modem is responsive...")
|
||||||
|
ser_sara.write(b'AT\r')
|
||||||
|
response_check = read_complete_response(ser_sara, wait_for_lines=["OK"], debug=True)
|
||||||
|
if response_check is None or "OK" not in response_check:
|
||||||
|
print("⚠️ Modem not responding after reboot")
|
||||||
|
return False
|
||||||
|
|
||||||
|
print("✅ Modem restarted successfully")
|
||||||
|
|
||||||
|
# Step 4: Reset the HTTP Profile
|
||||||
|
print('<span style="color: orange;font-weight: bold;">🔧 Resetting the HTTP Profile</span>')
|
||||||
|
command = f'AT+UHTTP={aircarto_profile_id},1,"data.nebuleair.fr"\r'
|
||||||
|
ser_sara.write(command.encode('utf-8'))
|
||||||
|
responseResetHTTP = read_complete_response(ser_sara, timeout=5, end_of_response_timeout=5,
|
||||||
|
wait_for_lines=["OK", "+CME ERROR"], debug=True)
|
||||||
|
print('<p class="text-danger-emphasis">')
|
||||||
|
print(responseResetHTTP)
|
||||||
|
print("</p>", end="")
|
||||||
|
|
||||||
|
http_reset_success = responseResetHTTP is not None and "OK" in responseResetHTTP
|
||||||
|
if not http_reset_success:
|
||||||
|
print("⚠️ HTTP profile reset failed")
|
||||||
|
# Continue anyway, don't return False here
|
||||||
|
|
||||||
|
# Step 5: For SARA-R5, reset the PDP connection
|
||||||
|
pdp_reset_success = True
|
||||||
|
if modem_version == "SARA-R500":
|
||||||
|
print("⚠️ Need to reset PDP connection for SARA-R500")
|
||||||
|
|
||||||
|
# Activate PDP context 1
|
||||||
|
print('➡️ Activate PDP context 1')
|
||||||
|
command = f'AT+CGACT=1,1\r'
|
||||||
|
ser_sara.write(command.encode('utf-8'))
|
||||||
|
response_pdp1 = read_complete_response(ser_sara, wait_for_lines=["OK"])
|
||||||
|
print(response_pdp1, end="")
|
||||||
|
pdp_reset_success = pdp_reset_success and (response_pdp1 is not None and "OK" in response_pdp1)
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
# Set the PDP type
|
||||||
|
print('➡️ Set the PDP type to IPv4 referring to the output of the +CGDCONT read command')
|
||||||
|
command = f'AT+UPSD=0,0,0\r'
|
||||||
|
ser_sara.write(command.encode('utf-8'))
|
||||||
|
response_pdp2 = read_complete_response(ser_sara, wait_for_lines=["OK"])
|
||||||
|
print(response_pdp2, end="")
|
||||||
|
pdp_reset_success = pdp_reset_success and (response_pdp2 is not None and "OK" in response_pdp2)
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
# Profile #0 is mapped on CID=1
|
||||||
|
print('➡️ Profile #0 is mapped on CID=1.')
|
||||||
|
command = f'AT+UPSD=0,100,1\r'
|
||||||
|
ser_sara.write(command.encode('utf-8'))
|
||||||
|
response_pdp3 = read_complete_response(ser_sara, wait_for_lines=["OK"])
|
||||||
|
print(response_pdp3, end="")
|
||||||
|
pdp_reset_success = pdp_reset_success and (response_pdp3 is not None and "OK" in response_pdp3)
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
# Activate the PSD profile
|
||||||
|
print('➡️ Activate the PSD profile #0: the IPv4 address is already assigned by the network.')
|
||||||
|
command = f'AT+UPSDA=0,3\r'
|
||||||
|
ser_sara.write(command.encode('utf-8'))
|
||||||
|
response_pdp4 = read_complete_response(ser_sara, wait_for_lines=["OK", "+UUPSDA"])
|
||||||
|
print(response_pdp4, end="")
|
||||||
|
pdp_reset_success = pdp_reset_success and (response_pdp4 is not None and ("OK" in response_pdp4 or "+UUPSDA" in response_pdp4))
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
if not pdp_reset_success:
|
||||||
|
print("⚠️ PDP connection reset had some issues")
|
||||||
|
|
||||||
|
# Return overall success
|
||||||
|
return http_reset_success and pdp_reset_success
|
||||||
|
|
||||||
try:
|
try:
|
||||||
'''
|
'''
|
||||||
_ ___ ___ ____
|
_ ___ ___ ____
|
||||||
@@ -288,25 +429,49 @@ try:
|
|||||||
|
|
||||||
'''
|
'''
|
||||||
print('<h3>START LOOP</h3>')
|
print('<h3>START LOOP</h3>')
|
||||||
|
print(f'Modem version: {modem_version}')
|
||||||
|
|
||||||
#Local timestamp
|
#Local timestamp
|
||||||
|
#ATTENTION:
|
||||||
|
# -> RTC module can be deconnected ""
|
||||||
|
# -> RTC module can be out of time like "2000-01-01T00:55:21Z"
|
||||||
print("➡️Getting local timestamp")
|
print("➡️Getting local timestamp")
|
||||||
cursor.execute("SELECT * FROM timestamp_table LIMIT 1")
|
cursor.execute("SELECT * FROM timestamp_table LIMIT 1")
|
||||||
row = cursor.fetchone() # Get the first (and only) row
|
row = cursor.fetchone() # Get the first (and only) row
|
||||||
rtc_time_str = row[1] # '2025-02-07 12:30:45'
|
rtc_time_str = row[1] # '2025-02-07 12:30:45' ou '2000-01-01 00:55:21' ou 'not connected'
|
||||||
|
print(rtc_time_str)
|
||||||
|
|
||||||
|
if rtc_time_str == 'not connected':
|
||||||
|
print("⛔ Atttention RTC module not connected⛔")
|
||||||
|
rtc_status = "disconnected"
|
||||||
|
influx_timestamp="rtc_disconnected"
|
||||||
|
else :
|
||||||
# Convert to a datetime object
|
# Convert to a datetime object
|
||||||
dt_object = datetime.strptime(rtc_time_str, '%Y-%m-%d %H:%M:%S')
|
dt_object = datetime.strptime(rtc_time_str, '%Y-%m-%d %H:%M:%S')
|
||||||
|
# Check if timestamp is reset (year 2000)
|
||||||
|
if dt_object.year == 2000:
|
||||||
|
print("⛔ Attention: RTC has been reset to default date ⛔")
|
||||||
|
rtc_status = "reset"
|
||||||
|
else:
|
||||||
|
print("✅ RTC timestamp is valid")
|
||||||
|
rtc_status = "valid"
|
||||||
|
|
||||||
|
# Always convert to InfluxDB format
|
||||||
# Convert to InfluxDB RFC3339 format with UTC 'Z' suffix
|
# Convert to InfluxDB RFC3339 format with UTC 'Z' suffix
|
||||||
influx_timestamp = dt_object.strftime('%Y-%m-%dT%H:%M:%SZ')
|
influx_timestamp = dt_object.strftime('%Y-%m-%dT%H:%M:%SZ')
|
||||||
|
rtc_status = "valid"
|
||||||
print(influx_timestamp)
|
print(influx_timestamp)
|
||||||
|
|
||||||
#NEXTPM
|
#NEXTPM
|
||||||
|
# We take the last measures (order by rowid and not by timestamp)
|
||||||
print("➡️Getting NPM values (last 6 measures)")
|
print("➡️Getting NPM values (last 6 measures)")
|
||||||
#cursor.execute("SELECT * FROM data_NPM ORDER BY timestamp DESC LIMIT 1")
|
#cursor.execute("SELECT * FROM data_NPM ORDER BY timestamp DESC LIMIT 1")
|
||||||
cursor.execute("SELECT * FROM data_NPM ORDER BY timestamp DESC LIMIT 6")
|
#cursor.execute("SELECT * FROM data_NPM ORDER BY timestamp DESC LIMIT 6")
|
||||||
|
cursor.execute("SELECT rowid, * FROM data_NPM ORDER BY rowid DESC LIMIT 6")
|
||||||
|
|
||||||
rows = cursor.fetchall()
|
rows = cursor.fetchall()
|
||||||
# Exclude the timestamp column (assuming first column is timestamp)
|
# Exclude the timestamp column (assuming first column is timestamp)
|
||||||
data_values = [row[1:] for row in rows] # Exclude timestamp
|
data_values = [row[2:] for row in rows] # Exclude timestamp
|
||||||
# Compute column-wise average
|
# Compute column-wise average
|
||||||
num_columns = len(data_values[0])
|
num_columns = len(data_values[0])
|
||||||
averages = [round(sum(col) / len(col),1) for col in zip(*data_values)]
|
averages = [round(sum(col) / len(col),1) for col in zip(*data_values)]
|
||||||
@@ -333,7 +498,7 @@ try:
|
|||||||
#NextPM 5 channels
|
#NextPM 5 channels
|
||||||
if npm_5channel:
|
if npm_5channel:
|
||||||
print("➡️Getting NextPM 5 channels values (last 6 measures)")
|
print("➡️Getting NextPM 5 channels values (last 6 measures)")
|
||||||
cursor.execute("SELECT * FROM data_NPM_5channels ORDER BY timestamp DESC LIMIT 6")
|
cursor.execute("SELECT * FROM data_NPM_5channels ORDER BY rowid DESC LIMIT 6")
|
||||||
rows = cursor.fetchall()
|
rows = cursor.fetchall()
|
||||||
# Exclude the timestamp column (assuming first column is timestamp)
|
# Exclude the timestamp column (assuming first column is timestamp)
|
||||||
data_values = [row[1:] for row in rows] # Exclude timestamp
|
data_values = [row[1:] for row in rows] # Exclude timestamp
|
||||||
@@ -351,7 +516,7 @@ try:
|
|||||||
#BME280
|
#BME280
|
||||||
if bme_280_config:
|
if bme_280_config:
|
||||||
print("➡️Getting BME280 values")
|
print("➡️Getting BME280 values")
|
||||||
cursor.execute("SELECT * FROM data_BME280 ORDER BY timestamp DESC LIMIT 1")
|
cursor.execute("SELECT * FROM data_BME280 ORDER BY rowid DESC LIMIT 1")
|
||||||
last_row = cursor.fetchone()
|
last_row = cursor.fetchone()
|
||||||
if last_row:
|
if last_row:
|
||||||
print("SQLite DB last available row:", last_row)
|
print("SQLite DB last available row:", last_row)
|
||||||
@@ -374,7 +539,7 @@ try:
|
|||||||
#envea
|
#envea
|
||||||
if envea_cairsens:
|
if envea_cairsens:
|
||||||
print("➡️Getting envea cairsens values")
|
print("➡️Getting envea cairsens values")
|
||||||
cursor.execute("SELECT * FROM data_envea ORDER BY timestamp DESC LIMIT 6")
|
cursor.execute("SELECT * FROM data_envea ORDER BY rowid DESC LIMIT 6")
|
||||||
rows = cursor.fetchall()
|
rows = cursor.fetchall()
|
||||||
# Exclude the timestamp column (assuming first column is timestamp)
|
# Exclude the timestamp column (assuming first column is timestamp)
|
||||||
data_values = [row[1:] for row in rows] # Exclude timestamp
|
data_values = [row[1:] for row in rows] # Exclude timestamp
|
||||||
@@ -398,16 +563,81 @@ try:
|
|||||||
payload_json["sensordatavalues"].append({"value_type": "CAIRSENS_NO2", "value": str(averages[1])})
|
payload_json["sensordatavalues"].append({"value_type": "CAIRSENS_NO2", "value": str(averages[1])})
|
||||||
payload_json["sensordatavalues"].append({"value_type": "CAIRSENS_NH3", "value": str(averages[2])})
|
payload_json["sensordatavalues"].append({"value_type": "CAIRSENS_NH3", "value": str(averages[2])})
|
||||||
|
|
||||||
|
#Wind meter
|
||||||
|
if wind_meter:
|
||||||
|
print("➡️Getting wind meter values")
|
||||||
|
|
||||||
|
#MPPT charger
|
||||||
|
if mppt_charger:
|
||||||
|
print("➡️Getting MPPT charger values")
|
||||||
|
cursor.execute("SELECT * FROM data_MPPT ORDER BY rowid DESC LIMIT 1")
|
||||||
|
last_row = cursor.fetchone()
|
||||||
|
if last_row:
|
||||||
|
print("SQLite DB last available row:", last_row)
|
||||||
|
battery_voltage = last_row[1]
|
||||||
|
battery_current = last_row[2]
|
||||||
|
solar_voltage = last_row[3]
|
||||||
|
solar_power = last_row[4]
|
||||||
|
charger_status = last_row[5]
|
||||||
|
|
||||||
|
#Add data to payload CSV
|
||||||
|
payload_csv[20] = battery_voltage
|
||||||
|
payload_csv[21] = battery_current
|
||||||
|
payload_csv[22] = solar_voltage
|
||||||
|
payload_csv[23] = solar_power
|
||||||
|
payload_csv[24] = charger_status
|
||||||
|
else:
|
||||||
|
print("No data available in the database.")
|
||||||
|
|
||||||
print("Verify SARA R4 connection")
|
print("Verify SARA R4 connection")
|
||||||
|
|
||||||
# Getting the LTE Signal
|
# Getting the LTE Signal
|
||||||
print("-> Getting LTE signal <-")
|
print("➡️Getting LTE signal")
|
||||||
ser_sara.write(b'AT+CSQ\r')
|
ser_sara.write(b'AT+CSQ\r')
|
||||||
response2 = read_complete_response(ser_sara, wait_for_lines=["OK"])
|
response2 = read_complete_response(ser_sara, wait_for_lines=["OK", "ERROR", "+CME ERROR"])
|
||||||
print('<p class="text-danger-emphasis">')
|
print('<p class="text-danger-emphasis">')
|
||||||
print(response2)
|
print(response2)
|
||||||
print("</p>")
|
print("</p>", end="")
|
||||||
|
|
||||||
|
|
||||||
|
#Here it's possible that the SARA do not repond at all or send a error message
|
||||||
|
#-> TO DO : harware reboot
|
||||||
|
#-> send notification
|
||||||
|
#-> end loop, no need to continue
|
||||||
|
|
||||||
|
#1. No answer at all form SARA
|
||||||
|
if response2 is None or response2 == "":
|
||||||
|
print("No answer from SARA module")
|
||||||
|
print('🛑STOP LOOP🛑')
|
||||||
|
print("<hr>")
|
||||||
|
|
||||||
|
# Send notification
|
||||||
|
try:
|
||||||
|
alert_url = f'http://data.nebuleair.fr/pro_4G/alert.php?capteur_id={device_id}&error_type=serial_error'
|
||||||
|
response = requests.post(alert_url, timeout=3)
|
||||||
|
if response.status_code == 200:
|
||||||
|
print(f"Alert notification sent successfully")
|
||||||
|
else:
|
||||||
|
print(f"Alert notification failed with status code: {response.status_code}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Alert notification failed: {e}")
|
||||||
|
|
||||||
|
#end loop
|
||||||
|
sys.exit()
|
||||||
|
|
||||||
|
#2. si on a une erreur
|
||||||
|
elif "+CME ERROR" in response2:
|
||||||
|
print(f"SARA module returned error: {response2}")
|
||||||
|
print("The CSQ command is not supported by this module or in its current state")
|
||||||
|
print("⚠️ATTENTION: SARA is connected over serial but CSQ command not supported")
|
||||||
|
print('🛑STOP LOOP🛑')
|
||||||
|
#end loop
|
||||||
|
sys.exit()
|
||||||
|
|
||||||
|
else :
|
||||||
|
print("✅SARA is connected over serial")
|
||||||
|
|
||||||
|
|
||||||
match = re.search(r'\+CSQ:\s*(\d+),', response2)
|
match = re.search(r'\+CSQ:\s*(\d+),', response2)
|
||||||
if match:
|
if match:
|
||||||
signal_quality = int(match.group(1))
|
signal_quality = int(match.group(1))
|
||||||
@@ -425,7 +655,7 @@ try:
|
|||||||
responseReconnect = read_complete_response(ser_sara, timeout=20, end_of_response_timeout=20)
|
responseReconnect = read_complete_response(ser_sara, timeout=20, end_of_response_timeout=20)
|
||||||
print('<p class="text-danger-emphasis">')
|
print('<p class="text-danger-emphasis">')
|
||||||
print(responseReconnect)
|
print(responseReconnect)
|
||||||
print("</p>")
|
print("</p>", end="")
|
||||||
|
|
||||||
print('🛑STOP LOOP🛑')
|
print('🛑STOP LOOP🛑')
|
||||||
print("<hr>")
|
print("<hr>")
|
||||||
@@ -448,26 +678,31 @@ try:
|
|||||||
print("Open JSON:")
|
print("Open JSON:")
|
||||||
command = f'AT+UDWNFILE="sensordata_csv.json",{size_of_string}\r'
|
command = f'AT+UDWNFILE="sensordata_csv.json",{size_of_string}\r'
|
||||||
ser_sara.write(command.encode('utf-8'))
|
ser_sara.write(command.encode('utf-8'))
|
||||||
response_SARA_1 = read_complete_response(ser_sara, wait_for_lines=[">"], debug=False)
|
response_SARA_1 = read_complete_response(ser_sara, wait_for_lines=[">"], debug=True)
|
||||||
|
print('<p class="text-danger-emphasis">')
|
||||||
print(response_SARA_1)
|
print(response_SARA_1)
|
||||||
|
print("</p>", end="")
|
||||||
|
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
|
|
||||||
#2. Write to shell
|
#2. Write to shell
|
||||||
print("Write data to memory:")
|
print("Write data to memory:")
|
||||||
ser_sara.write(csv_string.encode())
|
ser_sara.write(csv_string.encode())
|
||||||
response_SARA_2 = read_complete_response(ser_sara, wait_for_lines=["OK"], debug=False)
|
response_SARA_2 = read_complete_response(ser_sara, wait_for_lines=["OK"], debug=True)
|
||||||
|
print('<p class="text-danger-emphasis">')
|
||||||
print(response_SARA_2)
|
print(response_SARA_2)
|
||||||
|
print("</p>", end="")
|
||||||
|
|
||||||
#3. Send to endpoint (with device ID)
|
#3. Send to endpoint (with device ID)
|
||||||
print("Send data (POST REQUEST):")
|
print("Send data (POST REQUEST):")
|
||||||
command= f'AT+UHTTPC={aircarto_profile_id},4,"/pro_4G/data.php?sensor_id={device_id}&lat{device_latitude_raw}=&long={device_longitude_raw}&datetime={influx_timestamp}","aircarto_server_response.txt","sensordata_csv.json",4\r'
|
command= f'AT+UHTTPC={aircarto_profile_id},4,"/pro_4G/data.php?sensor_id={device_id}&lat{device_latitude_raw}=&long={device_longitude_raw}&datetime={influx_timestamp}","aircarto_server_response.txt","sensordata_csv.json",4\r'
|
||||||
ser_sara.write(command.encode('utf-8'))
|
ser_sara.write(command.encode('utf-8'))
|
||||||
|
|
||||||
response_SARA_3 = read_complete_response(ser_sara, timeout=5, end_of_response_timeout=120, wait_for_lines=["+UUHTTPCR", "+CME ERROR"], debug=True)
|
response_SARA_3 = read_complete_response(ser_sara, timeout=5, end_of_response_timeout=120, wait_for_lines=["+UUHTTPCR", "+CME ERROR", "ERROR"], debug=True)
|
||||||
|
|
||||||
print('<p class="text-danger-emphasis">')
|
print('<p class="text-danger-emphasis">')
|
||||||
print(response_SARA_3)
|
print(response_SARA_3)
|
||||||
print("</p>")
|
print("</p>", end="")
|
||||||
|
|
||||||
# si on recoit la réponse UHTTPCR
|
# si on recoit la réponse UHTTPCR
|
||||||
if "+UUHTTPCR" in response_SARA_3:
|
if "+UUHTTPCR" in response_SARA_3:
|
||||||
@@ -522,14 +757,15 @@ try:
|
|||||||
led_thread.start()
|
led_thread.start()
|
||||||
|
|
||||||
else:
|
else:
|
||||||
# 2.Si la réponse contient une réponse HTTP valide
|
# 2.Si la réponse contient une réponse UUHTTPCR
|
||||||
# Extract HTTP response code from the last line
|
# Extract UUHTTPCR response code from the last line
|
||||||
# ATTENTION: lines[-1] renvoie l'avant dernière ligne et il peut y avoir un soucis avec le OK
|
|
||||||
# rechercher plutot
|
|
||||||
http_response = lines[-1] # "+UUHTTPCR: 0,4,0"
|
http_response = lines[-1] # "+UUHTTPCR: 0,4,0"
|
||||||
parts = http_response.split(',')
|
parts = http_response.split(',')
|
||||||
|
|
||||||
# 2.1 code 0 (HTTP failed) ⛔⛔⛔
|
# 2.1 code 0 (HTTP failed) ⛔⛔⛔
|
||||||
|
# -> GET error code
|
||||||
|
# -> reboot module
|
||||||
if len(parts) == 3 and parts[-1] == '0': # The third value indicates success
|
if len(parts) == 3 and parts[-1] == '0': # The third value indicates success
|
||||||
print("*****")
|
print("*****")
|
||||||
print('<span style="color: red;font-weight: bold;">⛔ATTENTION: HTTP operation failed</span>')
|
print('<span style="color: red;font-weight: bold;">⛔ATTENTION: HTTP operation failed</span>')
|
||||||
@@ -541,66 +777,128 @@ try:
|
|||||||
led_thread.start()
|
led_thread.start()
|
||||||
|
|
||||||
# Get error code
|
# Get error code
|
||||||
print("Getting error code (11->Server connection error, 73->Secure socket connect error)")
|
print("Getting error code")
|
||||||
command = f'AT+UHTTPER={aircarto_profile_id}\r'
|
command = f'AT+UHTTPER={aircarto_profile_id}\r'
|
||||||
ser_sara.write(command.encode('utf-8'))
|
ser_sara.write(command.encode('utf-8'))
|
||||||
response_SARA_9 = read_complete_response(ser_sara, wait_for_lines=["OK"], debug=False)
|
response_SARA_9 = read_complete_response(ser_sara, wait_for_lines=["OK"], debug=False)
|
||||||
print('<p class="text-danger-emphasis">')
|
print('<p class="text-danger-emphasis">')
|
||||||
print(response_SARA_9)
|
print(response_SARA_9)
|
||||||
print("</p>")
|
print("</p>", end="")
|
||||||
|
|
||||||
'''
|
# Extract just the error code
|
||||||
+UHTTPER: profile_id,error_class,error_code
|
error_code = extract_error_code(response_SARA_9)
|
||||||
|
if error_code is not None:
|
||||||
error_class
|
# Display interpretation based on error code
|
||||||
0 OK, no error
|
if error_code == 0:
|
||||||
3 HTTP Protocol error class
|
print('<p class="text-success">No error detected</p>')
|
||||||
10 Wrong HTTP API USAGE
|
elif error_code == 4:
|
||||||
|
print('<p class="text-danger">Error 4: Invalid server Hostname</p>')
|
||||||
error_code (for error_class 3 and 10)
|
elif error_code == 11:
|
||||||
0 No error
|
print('<p class="text-danger">Error 11: Server connection error</p>')
|
||||||
4 Invalid server Hostname
|
elif error_code == 22:
|
||||||
11 Server connection error
|
print('<p class="text-danger">⚠️Error 22: PSD or CSD connection not established (SARA-R5 need to reset PDP conection)⚠️</p>')
|
||||||
73 Secure socket connect error
|
elif error_code == 73:
|
||||||
'''
|
print('<p class="text-danger">Error 73: Secure socket connect error</p>')
|
||||||
|
else:
|
||||||
#Essayer un reboot du SARA R4 (ne fonctionne pas)
|
print(f'<p class="text-danger">Unknown error code: {error_code}</p>')
|
||||||
#print("🔄SARA reboot!🔄")
|
else:
|
||||||
#command = f'AT+CFUN=15\r'
|
print('<p class="text-danger">Could not extract error code from response</p>')
|
||||||
#ser_sara.write(command.encode('utf-8'))
|
|
||||||
#response_SARA_9r = read_complete_response(ser_sara, wait_for_lines=["OK"], debug=False)
|
|
||||||
#print('<p class="text-danger-emphasis">')
|
#Software Reboot
|
||||||
#print(response_SARA_9r)
|
software_reboot_success = modem_complete_reboot_and_reinitialize(modem_version, aircarto_profile_id)
|
||||||
#print("</p>")
|
if software_reboot_success:
|
||||||
|
print("Modem successfully rebooted and reinitialized")
|
||||||
#reset l'url
|
else:
|
||||||
print('<span style="color: orange;font-weight: bold;">❓Try Resetting the HTTP Profile❓</span>')
|
print("There were issues with the modem reboot/reinitialize process")
|
||||||
command = f'AT+UHTTP={aircarto_profile_id},1,"data.nebuleair.fr"\r'
|
|
||||||
ser_sara.write(command.encode('utf-8'))
|
|
||||||
responseResetHTTP2_profile = read_complete_response(ser_sara, timeout=5, end_of_response_timeout=5, wait_for_lines=["OK", "+CME ERROR"], debug=True)
|
# 2.2 code 1 (✅✅HHTP / UUHTTPCR succeded✅✅)
|
||||||
print('<p class="text-danger-emphasis">')
|
|
||||||
print(responseResetHTTP2_profile)
|
|
||||||
print("</p>")
|
|
||||||
|
|
||||||
|
|
||||||
# 2.2 code 1 (HHTP succeded)
|
|
||||||
else:
|
else:
|
||||||
# Si la commande HTTP a réussi
|
|
||||||
print('<span style="font-weight: bold;">✅✅HTTP operation successful.</span>')
|
print('<span style="font-weight: bold;">✅✅HTTP operation successful.</span>')
|
||||||
update_json_key(config_file, "SARA_R4_network_status", "connected")
|
update_json_key(config_file, "SARA_R4_network_status", "connected")
|
||||||
print("Blink blue LED")
|
print("Blink blue LED")
|
||||||
led_thread = Thread(target=blink_led, args=(23, 5, 0.5))
|
led_thread = Thread(target=blink_led, args=(23, 5, 0.5))
|
||||||
led_thread.start()
|
led_thread.start()
|
||||||
|
|
||||||
#4. Read reply from server
|
#4. Read reply from server
|
||||||
print("Reply from server:")
|
print("Reply from server:")
|
||||||
ser_sara.write(b'AT+URDFILE="aircarto_server_response.txt"\r')
|
ser_sara.write(b'AT+URDFILE="aircarto_server_response.txt"\r')
|
||||||
response_SARA_4 = read_complete_response(ser_sara, wait_for_lines=["OK"], debug=False)
|
response_SARA_4 = read_complete_response(ser_sara, wait_for_lines=["OK"], debug=False)
|
||||||
print('<p class="text-success">')
|
print('<p class="text-success">')
|
||||||
print(response_SARA_4)
|
print(response_SARA_4)
|
||||||
print('</p>')
|
print("</p>", end="")
|
||||||
|
|
||||||
|
#Parse the server datetime
|
||||||
|
# Extract just the date from the response
|
||||||
|
date_string = None
|
||||||
|
date_start = response_SARA_4.find("Date: ")
|
||||||
|
if date_start != -1:
|
||||||
|
date_end = response_SARA_4.find("\n", date_start)
|
||||||
|
date_string = response_SARA_4[date_start + 6:date_end].strip()
|
||||||
|
print(f'<div class="text-primary">Server date: {date_string}</div>', end="")
|
||||||
|
|
||||||
|
# Optionally convert to datetime object
|
||||||
|
try:
|
||||||
|
from datetime import datetime
|
||||||
|
server_datetime = datetime.strptime(
|
||||||
|
date_string,
|
||||||
|
"%a, %d %b %Y %H:%M:%S %Z"
|
||||||
|
)
|
||||||
|
#print(f'<p class="text-primary">Parsed datetime: {server_datetime}</p>')
|
||||||
|
except Exception as e:
|
||||||
|
print(f'<p class="text-warning">Error parsing date: {e}</p>')
|
||||||
|
|
||||||
|
# Get RTC time from SQLite
|
||||||
|
cursor.execute("SELECT * FROM timestamp_table LIMIT 1")
|
||||||
|
row = cursor.fetchone()
|
||||||
|
rtc_time_str = row[1] # '2025-02-07 12:30:45' or '2000-01-01 00:55:21' or 'not connected'
|
||||||
|
print(f'<div class="text-primary">RTC time: {rtc_time_str}</div>', end="")
|
||||||
|
|
||||||
|
# Compare times if both are available
|
||||||
|
if server_datetime and rtc_time_str != 'not connected':
|
||||||
|
try:
|
||||||
|
# Convert RTC time string to datetime
|
||||||
|
rtc_datetime = datetime.strptime(rtc_time_str, '%Y-%m-%d %H:%M:%S')
|
||||||
|
|
||||||
|
# Calculate time difference in seconds
|
||||||
|
time_diff = abs((server_datetime - rtc_datetime).total_seconds())
|
||||||
|
|
||||||
|
print(f'<div class="text-primary">Time difference: {time_diff:.2f} seconds</div>', end="")
|
||||||
|
|
||||||
|
# Check if difference is more than 60 seconds
|
||||||
|
# and update the RTC clock
|
||||||
|
if time_diff > 60:
|
||||||
|
print(f'<div class="text-warning"><strong>⚠️ RTC time differs from server time by {time_diff:.2f} seconds!</strong></div>', end="")
|
||||||
|
# Format server time for RTC update
|
||||||
|
server_time_formatted = server_datetime.strftime('%Y-%m-%d %H:%M:%S')
|
||||||
|
|
||||||
|
#update RTC module do not wait for answer, non blocking
|
||||||
|
#/usr/bin/python3 /var/www/nebuleair_pro_4g/RTC/set_with_browserTime.py '2024-01-30 12:48:39'
|
||||||
|
# Launch RTC update script as non-blocking subprocess
|
||||||
|
import subprocess
|
||||||
|
update_command = [
|
||||||
|
"/usr/bin/python3",
|
||||||
|
"/var/www/nebuleair_pro_4g/RTC/set_with_browserTime.py",
|
||||||
|
server_time_formatted
|
||||||
|
]
|
||||||
|
|
||||||
|
# Execute the command without waiting for result
|
||||||
|
subprocess.Popen(update_command,
|
||||||
|
stdout=subprocess.DEVNULL,
|
||||||
|
stderr=subprocess.DEVNULL)
|
||||||
|
|
||||||
|
print(f'<div class="text-warning">➡️ Updating RTC with server time: {server_time_formatted}</div>', end="")
|
||||||
|
|
||||||
|
else:
|
||||||
|
print(f'<div class="text-success">✅ RTC time is synchronized with server time (within 60 seconds)</div>')
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f'<p class="text-warning">Error comparing times: {e}</p>')
|
||||||
|
|
||||||
|
|
||||||
#Si non ne recoit pas de réponse UHTTPCR
|
#Si non ne recoit pas de réponse UHTTPCR
|
||||||
#on a peut etre une ERROR de type "+CME ERROR: No connection to phone"
|
#on a peut etre une ERROR de type "+CME ERROR: No connection to phone" ou "Operation not allowed"
|
||||||
else:
|
else:
|
||||||
print('<span style="color: red;font-weight: bold;">No UUHTTPCR response</span>')
|
print('<span style="color: red;font-weight: bold;">No UUHTTPCR response</span>')
|
||||||
print("Blink red LED")
|
print("Blink red LED")
|
||||||
@@ -629,7 +927,7 @@ try:
|
|||||||
responseReconnect = read_complete_response(ser_sara, timeout=5, end_of_response_timeout=120, wait_for_lines=["OK", "+CME ERROR"], debug=True)
|
responseReconnect = read_complete_response(ser_sara, timeout=5, end_of_response_timeout=120, wait_for_lines=["OK", "+CME ERROR"], debug=True)
|
||||||
print('<p class="text-danger-emphasis">')
|
print('<p class="text-danger-emphasis">')
|
||||||
print(responseReconnect)
|
print(responseReconnect)
|
||||||
print("</p>")
|
print("</p>", end="")
|
||||||
# Handle "Operation not allowed" error
|
# Handle "Operation not allowed" error
|
||||||
if error_message == "Operation not allowed":
|
if error_message == "Operation not allowed":
|
||||||
print('<span style="color: orange;font-weight: bold;">❓Try Resetting the HTTP Profile❓</span>')
|
print('<span style="color: orange;font-weight: bold;">❓Try Resetting the HTTP Profile❓</span>')
|
||||||
@@ -638,13 +936,36 @@ try:
|
|||||||
responseResetHTTP_profile = read_complete_response(ser_sara, timeout=5, end_of_response_timeout=5, wait_for_lines=["OK", "+CME ERROR"], debug=True)
|
responseResetHTTP_profile = read_complete_response(ser_sara, timeout=5, end_of_response_timeout=5, wait_for_lines=["OK", "+CME ERROR"], debug=True)
|
||||||
print('<p class="text-danger-emphasis">')
|
print('<p class="text-danger-emphasis">')
|
||||||
print(responseResetHTTP_profile)
|
print(responseResetHTTP_profile)
|
||||||
print("</p>")
|
print("</p>", end="")
|
||||||
|
check_lines = responseResetHTTP_profile.strip().splitlines()
|
||||||
|
for line in check_lines:
|
||||||
|
if "+CME ERROR: Operation not allowed" in line:
|
||||||
|
print('<span style="color: red;font-weight: bold;">⚠️ATTENTION: CME ERROR⚠️</span>')
|
||||||
|
print('<span style="color: orange;font-weight: bold;">❓Try Reboot the module❓</span>')
|
||||||
|
#Software Reboot
|
||||||
|
|
||||||
|
if "ERROR" in line:
|
||||||
|
print("⛔Attention ERROR!⛔")
|
||||||
|
#Software Reboot
|
||||||
|
software_reboot_success = modem_complete_reboot_and_reinitialize(modem_version, aircarto_profile_id)
|
||||||
|
if software_reboot_success:
|
||||||
|
print("Modem successfully rebooted and reinitialized")
|
||||||
|
else:
|
||||||
|
print("There were issues with the modem reboot/reinitialize process")
|
||||||
|
|
||||||
|
|
||||||
#5. empty json
|
#5. empty json
|
||||||
print("Empty SARA memory:")
|
print("Empty SARA memory:")
|
||||||
ser_sara.write(b'AT+UDELFILE="sensordata_csv.json"\r')
|
ser_sara.write(b'AT+UDELFILE="sensordata_csv.json"\r')
|
||||||
response_SARA_5 = read_complete_response(ser_sara, wait_for_lines=["OK"], debug=False)
|
response_SARA_5 = read_complete_response(ser_sara, wait_for_lines=["OK","+CME ERROR"], debug=True)
|
||||||
|
print('<p class="text-danger-emphasis">')
|
||||||
print(response_SARA_5)
|
print(response_SARA_5)
|
||||||
|
print("</p>", end="")
|
||||||
|
|
||||||
|
if "+CME ERROR" in response_SARA_5:
|
||||||
|
print("⛔ Attention CME ERROR ⛔")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
'''
|
'''
|
||||||
@@ -654,6 +975,72 @@ try:
|
|||||||
if send_uSpot:
|
if send_uSpot:
|
||||||
print('➡️<p class="fw-bold">SEND TO uSPOT SERVERS</p>')
|
print('➡️<p class="fw-bold">SEND TO uSPOT SERVERS</p>')
|
||||||
|
|
||||||
|
if reset_uSpot_url:
|
||||||
|
#2. Set uSpot URL (profile id = 1)
|
||||||
|
print('➡️Set uSpot URL')
|
||||||
|
uSpot_profile_id = 1
|
||||||
|
uSpot_url="api-prod.uspot.probesys.net"
|
||||||
|
security_profile_id = 1
|
||||||
|
|
||||||
|
#step 1: import the certificate
|
||||||
|
print("****")
|
||||||
|
certificate_name = "e6"
|
||||||
|
with open("/var/www/nebuleair_pro_4g/SARA/SSL/certificate/e6.pem", "rb") as cert_file:
|
||||||
|
certificate = cert_file.read()
|
||||||
|
size_of_string = len(certificate)
|
||||||
|
|
||||||
|
print("\033[0;33m Import certificate\033[0m")
|
||||||
|
# AT+USECMNG=0,<type>,<internal_name>,<data_size>
|
||||||
|
# type-> 0 -> trusted root CA
|
||||||
|
command = f'AT+USECMNG=0,0,"{certificate_name}",{size_of_string}\r'
|
||||||
|
ser_sara.write((command + '\r').encode('utf-8'))
|
||||||
|
response_SARA_1 = read_complete_response(ser_sara)
|
||||||
|
print(response_SARA_1)
|
||||||
|
|
||||||
|
time.sleep(0.5)
|
||||||
|
|
||||||
|
print("\033[0;33mAdd certificate\033[0m")
|
||||||
|
ser_sara.write(certificate)
|
||||||
|
response_SARA_2 = read_complete_response(ser_sara)
|
||||||
|
print(response_SARA_2)
|
||||||
|
|
||||||
|
time.sleep(0.5)
|
||||||
|
|
||||||
|
# SECURITY PROFILE
|
||||||
|
# op_code: 3 -> trusted root certificate internal name
|
||||||
|
print("\033[0;33mSet the security profile (choose cert)\033[0m")
|
||||||
|
command = f'AT+USECPRF={security_profile_id},3,"{certificate_name}"\r'
|
||||||
|
ser_sara.write((command + '\r').encode('utf-8'))
|
||||||
|
response_SARA_5c = read_complete_response(ser_sara, wait_for_lines=["OK"])
|
||||||
|
print(response_SARA_5c)
|
||||||
|
time.sleep(0.5)
|
||||||
|
|
||||||
|
#step 4: set url (op_code = 1)
|
||||||
|
command = f'AT+UHTTP={uSpot_profile_id},1,"{uSpot_url}"\r'
|
||||||
|
ser_sara.write(command.encode('utf-8'))
|
||||||
|
response_SARA_2 = read_complete_response(ser_sara, wait_for_lines=["OK"])
|
||||||
|
print(response_SARA_2)
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
#step 4: set PORT (op_code = 5)
|
||||||
|
print("set port 443")
|
||||||
|
command = f'AT+UHTTP={uSpot_profile_id},5,443\r'
|
||||||
|
ser_sara.write((command + '\r').encode('utf-8'))
|
||||||
|
response_SARA_55 = read_complete_response(ser_sara, wait_for_lines=["OK"])
|
||||||
|
print(response_SARA_55)
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
#step 4: set url to SSL (op_code = 6) (http_secure = 1 for HTTPS)(USECMNG_PROFILE = 2)
|
||||||
|
print("\033[0;33mSET SSL\033[0m")
|
||||||
|
http_secure = 1
|
||||||
|
command = f'AT+UHTTP={uSpot_profile_id},6,{http_secure},{security_profile_id}\r'
|
||||||
|
#command = f'AT+UHTTP={profile_id},6,{http_secure}\r'
|
||||||
|
|
||||||
|
ser_sara.write(command.encode('utf-8'))
|
||||||
|
response_SARA_5 = read_complete_response(ser_sara, wait_for_lines=["OK"])
|
||||||
|
print(response_SARA_5)
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
# 1. Open sensordata_json.json (with correct data size)
|
# 1. Open sensordata_json.json (with correct data size)
|
||||||
print("Open JSON:")
|
print("Open JSON:")
|
||||||
payload_string = json.dumps(payload_json) # Convert dict to JSON string
|
payload_string = json.dumps(payload_json) # Convert dict to JSON string
|
||||||
@@ -680,7 +1067,7 @@ try:
|
|||||||
|
|
||||||
print('<p class="text-danger-emphasis">')
|
print('<p class="text-danger-emphasis">')
|
||||||
print(response_SARA_8)
|
print(response_SARA_8)
|
||||||
print("</p>")
|
print("</p>", end="")
|
||||||
|
|
||||||
# si on recoit la réponse UHTTPCR
|
# si on recoit la réponse UHTTPCR
|
||||||
if "+UUHTTPCR" in response_SARA_8:
|
if "+UUHTTPCR" in response_SARA_8:
|
||||||
@@ -727,28 +1114,31 @@ try:
|
|||||||
led_thread.start()
|
led_thread.start()
|
||||||
|
|
||||||
# Get error code
|
# Get error code
|
||||||
print("Getting error code (4-> Invalid server Hostname, 11->Server connection error, 73->Secure socket connect error)")
|
print("Getting error code")
|
||||||
command = f'AT+UHTTPER={uSpot_profile_id}\r'
|
command = f'AT+UHTTPER={uSpot_profile_id}\r'
|
||||||
ser_sara.write(command.encode('utf-8'))
|
ser_sara.write(command.encode('utf-8'))
|
||||||
response_SARA_9b = read_complete_response(ser_sara, wait_for_lines=["OK"], debug=False)
|
response_SARA_9b = read_complete_response(ser_sara, wait_for_lines=["OK"], debug=False)
|
||||||
print('<p class="text-danger-emphasis">')
|
print('<p class="text-danger-emphasis">')
|
||||||
print(response_SARA_9b)
|
print(response_SARA_9b)
|
||||||
print("</p>")
|
print("</p>", end="")
|
||||||
|
# Extract just the error code
|
||||||
'''
|
error_code = extract_error_code(response_SARA_9b)
|
||||||
+UHTTPER: profile_id,error_class,error_code
|
if error_code is not None:
|
||||||
|
# Display interpretation based on error code
|
||||||
error_class
|
if error_code == 0:
|
||||||
0 OK, no error
|
print('<p class="text-success">No error detected</p>')
|
||||||
3 HTTP Protocol error class
|
elif error_code == 4:
|
||||||
10 Wrong HTTP API USAGE
|
print('<p class="text-danger">Error 4: Invalid server Hostname</p>')
|
||||||
|
elif error_code == 11:
|
||||||
error_code (for error_class 3)
|
print('<p class="text-danger">Error 11: Server connection error</p>')
|
||||||
0 No error
|
elif error_code == 22:
|
||||||
4 Invalid server Hostname
|
print('<p class="text-danger">Error 22: PSD or CSD connection not established</p>')
|
||||||
11 Server connection error
|
elif error_code == 73:
|
||||||
73 Secure socket connect error
|
print('<p class="text-danger">Error 73: Secure socket connect error</p>')
|
||||||
'''
|
else:
|
||||||
|
print(f'<p class="text-danger">Unknown error code: {error_code}</p>')
|
||||||
|
else:
|
||||||
|
print('<p class="text-danger">Could not extract error code from response</p>')
|
||||||
|
|
||||||
#Pas forcément un moyen de résoudre le soucis
|
#Pas forcément un moyen de résoudre le soucis
|
||||||
|
|
||||||
@@ -766,7 +1156,7 @@ try:
|
|||||||
response_SARA_4b = read_complete_response(ser_sara, wait_for_lines=["OK"], debug=False)
|
response_SARA_4b = read_complete_response(ser_sara, wait_for_lines=["OK"], debug=False)
|
||||||
print('<p class="text-success">')
|
print('<p class="text-success">')
|
||||||
print(response_SARA_4b)
|
print(response_SARA_4b)
|
||||||
print('</p>')
|
print("</p>", end="")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -81,11 +81,13 @@ def run_script(script_name, interval, delay=0):
|
|||||||
|
|
||||||
# Define scripts and their execution intervals (seconds)
|
# Define scripts and their execution intervals (seconds)
|
||||||
SCRIPTS = [
|
SCRIPTS = [
|
||||||
#("RTC/save_to_db.py", 1, 0), # SAVE RTC time every 1 second, no delay
|
#("RTC/save_to_db.py", 1, 0), # --> will run as a separated system service (rtc_save_to_db.service)
|
||||||
("NPM/get_data_modbus_v3.py", 10, 0), # Get NPM data (modbus 5 channels) every 10s, with 2s delay
|
("NPM/get_data_modbus_v3.py", 10, 0), # Get NPM data (modbus 5 channels) every 10s, with 2s delay
|
||||||
("envea/read_value_v2.py", 10, 0), # Get NPM data (modbus 5 channels) every 10s, with 2s delay
|
("envea/read_value_v2.py", 10, 0), # Get NPM data (modbus 5 channels) every 10s, with 2s delay
|
||||||
("loop/SARA_send_data_v2.py", 60, 1), # Send data every 60 seconds, with 2s delay
|
("loop/SARA_send_data_v2.py", 60, 1), # Send data every 60 seconds, with 2s delay
|
||||||
("BME280/get_data_v2.py", 120, 0), # Get BME280 data every 120 seconds, no delay
|
("BME280/get_data_v2.py", 120, 0), # Get BME280 data every 120 seconds, no delay
|
||||||
|
("MPPT/read.py", 120, 0), # Get MPPT data every 120 seconds, no delay
|
||||||
|
#("windMeter/read.py", 60, 2), # --> will run as a separated system service ()
|
||||||
("sqlite/flush_old_data.py", 86400, 0) # flush old data inside db every day ()
|
("sqlite/flush_old_data.py", 86400, 0) # flush old data inside db every day ()
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -30,7 +30,14 @@ cursor.execute("""
|
|||||||
VALUES (1, CURRENT_TIMESTAMP);
|
VALUES (1, CURRENT_TIMESTAMP);
|
||||||
""")
|
""")
|
||||||
|
|
||||||
|
#create a modem status table
|
||||||
|
cursor.execute("""
|
||||||
|
CREATE TABLE IF NOT EXISTS modem_status (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
timestamp TEXT,
|
||||||
|
status TEXT
|
||||||
|
)
|
||||||
|
""")
|
||||||
|
|
||||||
# Create a table NPM
|
# Create a table NPM
|
||||||
cursor.execute("""
|
cursor.execute("""
|
||||||
@@ -78,6 +85,26 @@ CREATE TABLE IF NOT EXISTS data_NPM_5channels (
|
|||||||
)
|
)
|
||||||
""")
|
""")
|
||||||
|
|
||||||
|
# Create a table WIND
|
||||||
|
cursor.execute("""
|
||||||
|
CREATE TABLE IF NOT EXISTS data_WIND (
|
||||||
|
timestamp TEXT,
|
||||||
|
wind_speed REAL,
|
||||||
|
wind_direction REAL
|
||||||
|
)
|
||||||
|
""")
|
||||||
|
|
||||||
|
# Create a table MPPT
|
||||||
|
cursor.execute("""
|
||||||
|
CREATE TABLE IF NOT EXISTS data_MPPT (
|
||||||
|
timestamp TEXT,
|
||||||
|
battery_voltage REAL,
|
||||||
|
battery_current REAL,
|
||||||
|
solar_voltage REAL,
|
||||||
|
solar_power REAL,
|
||||||
|
charger_status INTEGER
|
||||||
|
)
|
||||||
|
""")
|
||||||
|
|
||||||
|
|
||||||
# Commit and close the connection
|
# Commit and close the connection
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ data_NPM_5channels
|
|||||||
data_BME280
|
data_BME280
|
||||||
data_envea
|
data_envea
|
||||||
timestamp_table
|
timestamp_table
|
||||||
|
data_MPPT
|
||||||
|
data_WIND
|
||||||
|
|
||||||
'''
|
'''
|
||||||
|
|
||||||
|
|||||||
27
windMeter/ads115.py
Normal file
27
windMeter/ads115.py
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
'''
|
||||||
|
Script to test the abs115 an analog-to-digital converter
|
||||||
|
sudo /usr/bin/python3 /var/www/nebuleair_pro_4g/windMeter/ads115.py
|
||||||
|
|
||||||
|
'''
|
||||||
|
import time
|
||||||
|
import board
|
||||||
|
import busio
|
||||||
|
import adafruit_ads1x15.ads1115 as ADS
|
||||||
|
from adafruit_ads1x15.analog_in import AnalogIn
|
||||||
|
|
||||||
|
i2c = busio.I2C(board.SCL, board.SDA)
|
||||||
|
ads = ADS.ADS1115(i2c)
|
||||||
|
channel = AnalogIn(ads, ADS.P0)
|
||||||
|
|
||||||
|
print("Testing ADS1115 readings...")
|
||||||
|
readings = []
|
||||||
|
|
||||||
|
for i in range(5):
|
||||||
|
voltage = channel.voltage
|
||||||
|
readings.append(voltage)
|
||||||
|
print(f"Voltage: {voltage:.6f}V")
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
# Calculate and display the mean
|
||||||
|
mean_voltage = sum(readings) / len(readings)
|
||||||
|
print(f"\nMean voltage: {mean_voltage:.6f}V")
|
||||||
108
windMeter/read.py
Normal file
108
windMeter/read.py
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
'''
|
||||||
|
__ _____ _ _ ____
|
||||||
|
\ \ / /_ _| \ | | _ \
|
||||||
|
\ \ /\ / / | || \| | | | |
|
||||||
|
\ V V / | || |\ | |_| |
|
||||||
|
\_/\_/ |___|_| \_|____/
|
||||||
|
|
||||||
|
Script to read wind speed from a Davis Anémomètre-girouette Vantage Pro (6410)
|
||||||
|
https://www.shapemaker.io/blog/wind-speed-measurements-with-anemometer-and-a-raspberry-pi
|
||||||
|
|
||||||
|
Connexion:
|
||||||
|
black (wind speed ) -> gpio21
|
||||||
|
green (wind direction) -> ADS1115 (module I2C)
|
||||||
|
Yellow -> 5v
|
||||||
|
RED -> GND
|
||||||
|
|
||||||
|
Attention: The Raspberry Pi doesn't have analog inputs, so we need an analog-to-digital converter (ADC) to read the wind direction.
|
||||||
|
|
||||||
|
sudo /usr/bin/python3 /var/www/nebuleair_pro_4g/windMeter/read.py
|
||||||
|
|
||||||
|
'''
|
||||||
|
#!/usr/bin/python3
|
||||||
|
import time
|
||||||
|
import sqlite3
|
||||||
|
import board
|
||||||
|
import busio
|
||||||
|
import numpy as np
|
||||||
|
import threading
|
||||||
|
import adafruit_ads1x15.ads1115 as ADS
|
||||||
|
from adafruit_ads1x15.analog_in import AnalogIn
|
||||||
|
from gpiozero import Button
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
# Constants
|
||||||
|
DB_PATH = "/var/www/nebuleair_pro_4g/sqlite/sensors.db"
|
||||||
|
|
||||||
|
# Initialize I2C & ADS1115
|
||||||
|
i2c = busio.I2C(board.SCL, board.SDA)
|
||||||
|
ads = ADS.ADS1115(i2c)
|
||||||
|
channel = AnalogIn(ads, ADS.P0) # Connect to A0 on the ADS1115
|
||||||
|
|
||||||
|
# Wind speed sensor setup
|
||||||
|
wind_speed_sensor = Button(21)
|
||||||
|
wind_count = 0
|
||||||
|
wind_lock = threading.Lock()
|
||||||
|
|
||||||
|
def spin():
|
||||||
|
global wind_count
|
||||||
|
with wind_lock:
|
||||||
|
wind_count += 1
|
||||||
|
|
||||||
|
def reset_wind():
|
||||||
|
global wind_count
|
||||||
|
with wind_lock:
|
||||||
|
wind_count = 0
|
||||||
|
|
||||||
|
wind_speed_sensor.when_activated = spin # More reliable
|
||||||
|
|
||||||
|
def calc_speed(spins, interval):
|
||||||
|
return spins * (2.25 / interval) * 1.60934 # Convert MPH to km/h
|
||||||
|
|
||||||
|
def get_wind_direction():
|
||||||
|
voltage = channel.voltage
|
||||||
|
return voltage
|
||||||
|
|
||||||
|
def save_to_database(wind_speed, wind_direction, spin_count):
|
||||||
|
"""Save wind data to SQLite database."""
|
||||||
|
try:
|
||||||
|
conn = sqlite3.connect(DB_PATH)
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
cursor.execute("SELECT * FROM timestamp_table LIMIT 1")
|
||||||
|
row = cursor.fetchone()
|
||||||
|
rtc_time_str = row[1] if row else datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
|
||||||
|
cursor.execute('''
|
||||||
|
INSERT INTO data_wind (timestamp, wind_speed, wind_direction)
|
||||||
|
VALUES (?, ?, ?)
|
||||||
|
''', (rtc_time_str, round(wind_speed, 2), round(wind_direction, 2)))
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
print(f"Saved: {rtc_time_str}, {wind_speed:.2f} km/h, {wind_direction:.2f}V, Spins: {spin_count}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Database error: {e}")
|
||||||
|
|
||||||
|
def main():
|
||||||
|
print("Wind monitoring started...")
|
||||||
|
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
reset_wind()
|
||||||
|
print("Measuring for 60 seconds...")
|
||||||
|
time.sleep(60)
|
||||||
|
|
||||||
|
wind_speed_kmh = calc_speed(wind_count, 60)
|
||||||
|
wind_direction = get_wind_direction()
|
||||||
|
|
||||||
|
save_to_database(wind_speed_kmh, wind_direction, wind_count)
|
||||||
|
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print("\nMonitoring stopped.")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error: {e}")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
84
windMeter/read_wind_direction.py
Normal file
84
windMeter/read_wind_direction.py
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
'''
|
||||||
|
__ _____ _ _ ____
|
||||||
|
\ \ / /_ _| \ | | _ \
|
||||||
|
\ \ /\ / / | || \| | | | |
|
||||||
|
\ V V / | || |\ | |_| |
|
||||||
|
\_/\_/ |___|_| \_|____/
|
||||||
|
|
||||||
|
|
||||||
|
Script to read wind speed from a Davis Anémomètre-girouette Vantage Pro (6410)
|
||||||
|
https://www.shapemaker.io/blog/wind-speed-measurements-with-anemometer-and-a-raspberry-pi
|
||||||
|
|
||||||
|
Connexion:
|
||||||
|
black (wind speed ) -> gpio21
|
||||||
|
green (wind direction) -> ADS1115 (module I2C)
|
||||||
|
Yellow -> 5v
|
||||||
|
RED -> GND
|
||||||
|
|
||||||
|
Attention: The Raspberry Pi doesn't have analog inputs, so we need an analog-to-digital converter (ADC) to read the wind direction.
|
||||||
|
|
||||||
|
sudo /usr/bin/python3 /var/www/nebuleair_pro_4g/windMeter/read_wind_direction.py
|
||||||
|
|
||||||
|
'''
|
||||||
|
import time
|
||||||
|
import board
|
||||||
|
import busio
|
||||||
|
import adafruit_ads1x15.ads1115 as ADS
|
||||||
|
from adafruit_ads1x15.analog_in import AnalogIn
|
||||||
|
|
||||||
|
# Create the I2C bus and ADC object
|
||||||
|
i2c = busio.I2C(board.SCL, board.SDA)
|
||||||
|
ads = ADS.ADS1115(i2c)
|
||||||
|
|
||||||
|
# Connect to the channel with your Davis wind vane
|
||||||
|
wind_dir_sensor = AnalogIn(ads, ADS.P0)
|
||||||
|
|
||||||
|
# Check the current voltage range
|
||||||
|
min_voltage = 9999
|
||||||
|
max_voltage = -9999
|
||||||
|
|
||||||
|
def get_wind_direction():
|
||||||
|
"""Get wind direction angle from Davis Vantage Pro2 wind vane"""
|
||||||
|
global min_voltage, max_voltage
|
||||||
|
|
||||||
|
# Read voltage from ADS1115
|
||||||
|
voltage = wind_dir_sensor.voltage
|
||||||
|
|
||||||
|
# Update min/max for calibration
|
||||||
|
if voltage < min_voltage:
|
||||||
|
min_voltage = voltage
|
||||||
|
if voltage > max_voltage:
|
||||||
|
max_voltage = voltage
|
||||||
|
|
||||||
|
# We'll use a safer mapping approach
|
||||||
|
# Assuming the Davis sensor is linear from 0° to 360°
|
||||||
|
estimated_max = 3.859 # Initial estimate, will refine
|
||||||
|
|
||||||
|
# Calculate angle with bounds checking
|
||||||
|
angle = (voltage / estimated_max) * 360.0
|
||||||
|
|
||||||
|
# Ensure angle is in 0-360 range
|
||||||
|
angle = angle % 360
|
||||||
|
|
||||||
|
return voltage, angle
|
||||||
|
|
||||||
|
# Main loop
|
||||||
|
try:
|
||||||
|
print("Reading wind direction. Press Ctrl+C to exit.")
|
||||||
|
print("Voltage, Angle, Min Voltage, Max Voltage")
|
||||||
|
while True:
|
||||||
|
voltage, angle = get_wind_direction()
|
||||||
|
print(f"{voltage:.3f}V, {angle:.1f}°, {min_voltage:.3f}V, {max_voltage:.3f}V")
|
||||||
|
time.sleep(1)
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print("\nProgram stopped")
|
||||||
|
print(f"Observed voltage range: {min_voltage:.3f}V to {max_voltage:.3f}V")
|
||||||
|
|
||||||
|
# Suggest calibration if we have enough data
|
||||||
|
if max_voltage > min_voltage:
|
||||||
|
print("\nSuggested calibration for your setup:")
|
||||||
|
print(f"max_voltage = {max_voltage:.3f}")
|
||||||
|
print(f"def get_wind_direction():")
|
||||||
|
print(f" voltage = wind_dir_sensor.voltage")
|
||||||
|
print(f" angle = (voltage / {max_voltage:.3f}) * 360.0")
|
||||||
|
print(f" return angle % 360")
|
||||||
67
windMeter/read_wind_speed.py
Normal file
67
windMeter/read_wind_speed.py
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
'''
|
||||||
|
__ _____ _ _ ____
|
||||||
|
\ \ / /_ _| \ | | _ \
|
||||||
|
\ \ /\ / / | || \| | | | |
|
||||||
|
\ V V / | || |\ | |_| |
|
||||||
|
\_/\_/ |___|_| \_|____/
|
||||||
|
|
||||||
|
|
||||||
|
Script to read wind speed from a Davis Anémomètre-girouette Vantage Pro (6410)
|
||||||
|
https://www.shapemaker.io/blog/wind-speed-measurements-with-anemometer-and-a-raspberry-pi
|
||||||
|
|
||||||
|
Connexion:
|
||||||
|
black (wind speed ) -> gpio21
|
||||||
|
green (wind direction) -> ADS1115 (module I2C)
|
||||||
|
Yellow -> 5v
|
||||||
|
RED -> GND
|
||||||
|
|
||||||
|
Attention: The Raspberry Pi doesn't have analog inputs, so we need an analog-to-digital converter (ADC) to read the wind direction.
|
||||||
|
|
||||||
|
sudo /usr/bin/python3 /var/www/nebuleair_pro_4g/windMeter/read_wind_speed.py
|
||||||
|
|
||||||
|
'''
|
||||||
|
|
||||||
|
|
||||||
|
import time
|
||||||
|
from gpiozero import Button
|
||||||
|
from signal import pause
|
||||||
|
|
||||||
|
# Setup wind speed sensor on GPIO pin 21 (instead of 5)
|
||||||
|
wind_speed_sensor = Button(21)
|
||||||
|
wind_count = 0
|
||||||
|
|
||||||
|
def spin():
|
||||||
|
global wind_count
|
||||||
|
wind_count = wind_count + 1
|
||||||
|
|
||||||
|
def calc_speed(spins, interval):
|
||||||
|
# Davis anemometer formula: V = P*(2.25/T) in MPH
|
||||||
|
# P = pulses per sample period, T = sample period in seconds
|
||||||
|
wind_speed_mph = spins * (2.25 / interval)
|
||||||
|
return wind_speed_mph
|
||||||
|
|
||||||
|
def reset_wind():
|
||||||
|
global wind_count
|
||||||
|
wind_count = 0
|
||||||
|
|
||||||
|
# Register the event handler for the sensor
|
||||||
|
wind_speed_sensor.when_pressed = spin
|
||||||
|
|
||||||
|
try:
|
||||||
|
print("Wind speed measurement started. Press Ctrl+C to exit.")
|
||||||
|
|
||||||
|
while True:
|
||||||
|
# Reset the counter
|
||||||
|
reset_wind()
|
||||||
|
|
||||||
|
# Wait for 3 seconds and count rotations
|
||||||
|
print("Measuring for 3 seconds...")
|
||||||
|
time.sleep(3)
|
||||||
|
|
||||||
|
# Calculate and display wind speed
|
||||||
|
wind_speed = calc_speed(wind_count, 3)
|
||||||
|
print(f"Wind count: {wind_count} spins")
|
||||||
|
print(f"Wind speed: {wind_speed:.2f} mph ({wind_speed * 1.60934:.2f} km/h)")
|
||||||
|
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print("\nMeasurement stopped by user")
|
||||||
Reference in New Issue
Block a user