Compare commits
147 Commits
ef2bd6b895
...
ai_branch_
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
020594e065 | ||
|
|
5a1a4e0d81 | ||
|
|
3cd5b13c25 | ||
|
|
5a0f1c0745 | ||
|
|
2516a3bd1c | ||
|
|
1b8dc54fe0 | ||
|
|
2bd74ca91a | ||
|
|
f40c105abf | ||
|
|
fdef8e2df0 | ||
|
|
386ad6fb03 | ||
|
|
a7c138e93f | ||
|
|
4e4832b128 | ||
|
|
11463b175c | ||
|
|
c06741b11d | ||
|
|
b1352261e7 | ||
|
|
376ff454bf | ||
|
|
932fdf83a2 | ||
|
|
1ca3e2ada2 | ||
|
|
fd1d32a62b | ||
|
|
61b302fe35 | ||
|
|
2aaa229e82 | ||
|
|
fd28069b0c | ||
|
|
b17c996f2f | ||
|
|
8273307cab | ||
|
|
a73eb30d32 | ||
|
|
ba889feee9 | ||
|
|
12c7a0b6af | ||
|
|
08c5ed8841 | ||
|
|
7f5eb7608c | ||
|
|
44f44c3361 | ||
|
|
a8350332ac | ||
|
|
6c6eed1ad6 | ||
|
|
ee71c28d33 | ||
|
|
6d3220665e | ||
|
|
98e5a239f5 | ||
|
|
17f4ce46dd | ||
|
|
338b8a049f | ||
|
|
1e9e80ae55 | ||
|
|
9d280c6e37 | ||
|
|
d4c1178b3d | ||
|
|
f7f6fccd60 | ||
|
|
afceb34c1b | ||
|
|
7a958d5c8e | ||
|
|
8fd76001f2 | ||
|
|
e320a3bc2b | ||
|
|
8a4e184699 | ||
|
|
e61b0a76da | ||
|
|
970a36598c | ||
|
|
e75caff929 | ||
|
|
e82d75a4d6 | ||
|
|
dc27e5f139 | ||
|
|
4bc05091be | ||
|
|
29f9ec445a | ||
|
|
7b398d0d6d | ||
|
|
76336d0073 | ||
|
|
46a8e21e64 | ||
|
|
2129d45ef6 | ||
|
|
6312cd8d72 | ||
|
|
7c17ec82f5 | ||
|
|
b7a6f4c907 | ||
|
|
6b3329b9b8 | ||
|
|
e9b1e0e88e | ||
|
|
2db732ebb3 | ||
|
|
d5302f78ba | ||
|
|
5b7de91d50 | ||
|
|
4d15076d4b | ||
|
|
809742b6d5 | ||
|
|
bca975b0c5 | ||
|
|
dfba956685 | ||
|
|
d07314262e | ||
|
|
dffa639574 | ||
|
|
1fd5a3e75c | ||
|
|
e674b21eaa | ||
|
|
efc94ba5e1 | ||
|
|
26328dec99 | ||
|
|
ec3e81e99e | ||
|
|
1c6af36313 | ||
|
|
f1d6f595ac | ||
|
|
cfc2e0c47f | ||
|
|
1037207df3 | ||
|
|
14044a8856 | ||
|
|
d57a47ef68 | ||
|
|
5e7375cd4e | ||
|
|
c42b16ddb6 | ||
|
|
283a46eb0b | ||
|
|
33b24a9f53 | ||
|
|
10c4348e54 | ||
|
|
072f98ef95 | ||
|
|
7b4ff011ec | ||
|
|
ab2124f50d | ||
|
|
b493d30a41 | ||
|
|
659effb7c4 | ||
|
|
ebb0fd0a2b | ||
|
|
5d121761e7 | ||
|
|
d90fb14c90 | ||
|
|
3f329e0afa | ||
|
|
cd030a9e14 | ||
|
|
709cad6981 | ||
|
|
c59246e320 | ||
|
|
1a15d70aa7 | ||
|
|
bf9ece8589 | ||
|
|
3d507ae659 | ||
|
|
700de9c9f4 | ||
|
|
ec6fbf6bb2 | ||
|
|
8fecde5d56 | ||
|
|
49e93ab3ad | ||
|
|
e0d7614ad8 | ||
|
|
516f6367fa | ||
|
|
0549471669 | ||
|
|
20a0786380 | ||
|
|
92ec2a0bb9 | ||
|
|
c7fb474f66 | ||
|
|
8c5d831878 | ||
|
|
b3f5ee9795 | ||
|
|
4f7a704779 | ||
|
|
b5aeafeb56 | ||
|
|
144d904813 | ||
|
|
e3607143a1 | ||
|
|
7e8bf1294c | ||
|
|
eea1acd701 | ||
|
|
accfd3e371 | ||
|
|
dfbae99ba5 | ||
|
|
4c4c6ce77e | ||
|
|
c4fb7aed72 | ||
|
|
cee6c7f79b | ||
|
|
8fb1882864 | ||
|
|
67fcd78aac | ||
|
|
727fa4cfeb | ||
|
|
6622be14ad | ||
|
|
9774215e7c | ||
|
|
ecd61f765a | ||
|
|
62c729b63b | ||
|
|
e609c38ca0 | ||
|
|
1cb1b05b51 | ||
|
|
7cac769795 | ||
|
|
fb44b57ac1 | ||
|
|
d98eb48535 | ||
|
|
46303b9c19 | ||
|
|
49be391eb3 | ||
|
|
268a0586b8 | ||
|
|
7de382a43d | ||
|
|
c3e2866fab | ||
|
|
a90552148c | ||
|
|
c6073b49b9 | ||
|
|
aaeb20aece | ||
|
|
10cc46a079 | ||
|
|
b8150535e8 |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -1,4 +1,4 @@
|
||||
logs/app.log
|
||||
logs/*.log
|
||||
logs/loop.log
|
||||
deviceID.txt
|
||||
loop/loop.log
|
||||
@@ -14,3 +14,4 @@ NPM/data/*.txt
|
||||
NPM/data/*.json
|
||||
*.lock
|
||||
sqlite/*.db
|
||||
tests/
|
||||
74
BME280/get_data_v2.py
Executable file
74
BME280/get_data_v2.py
Executable file
@@ -0,0 +1,74 @@
|
||||
'''
|
||||
____ __ __ _____ ____ ___ ___
|
||||
| __ )| \/ | ____|___ \( _ ) / _ \
|
||||
| _ \| |\/| | _| __) / _ \| | | |
|
||||
| |_) | | | | |___ / __/ (_) | |_| |
|
||||
|____/|_| |_|_____|_____\___/ \___/
|
||||
|
||||
Script to read data from BME280
|
||||
Sensor connected to i2c on address 76 (use sudo i2cdetect -y 1 to get the address )
|
||||
-> save data to database (table data_BME280 )
|
||||
sudo python3 /var/www/nebuleair_pro_4g/BME280/get_data_v2.py
|
||||
|
||||
'''
|
||||
|
||||
import board
|
||||
import busio
|
||||
import json
|
||||
import sqlite3
|
||||
from adafruit_bme280 import basic as adafruit_bme280
|
||||
|
||||
# Connect to the SQLite database
|
||||
conn = sqlite3.connect("/var/www/nebuleair_pro_4g/sqlite/sensors.db")
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Create I2C bus
|
||||
i2c = busio.I2C(board.SCL, board.SDA)
|
||||
bme280 = adafruit_bme280.Adafruit_BME280_I2C(i2c, address=0x76)
|
||||
|
||||
# Configure settings
|
||||
bme280.sea_level_pressure = 1013.25 # Update this value for your location
|
||||
|
||||
# Read sensor data
|
||||
|
||||
#print(f"Temperature: {bme280.temperature:.2f} °C")
|
||||
#print(f"Humidity: {bme280.humidity:.2f} %")
|
||||
#print(f"Pressure: {bme280.pressure:.2f} hPa")
|
||||
#print(f"Altitude: {bme280.altitude:.2f} m")
|
||||
|
||||
temperature = round(bme280.temperature, 2)
|
||||
humidity = round(bme280.humidity, 2)
|
||||
pressure = round(bme280.pressure, 2)
|
||||
|
||||
sensor_data = {
|
||||
"temp": temperature, # Temperature in °C
|
||||
"hum": humidity, # Humidity in %
|
||||
"press": pressure # Pressure in hPa
|
||||
}
|
||||
|
||||
#GET RTC TIME from SQlite
|
||||
cursor.execute("SELECT * FROM timestamp_table LIMIT 1")
|
||||
row = cursor.fetchone() # Get the first (and only) row
|
||||
rtc_time_str = row[1] # '2025-02-07 12:30:45'
|
||||
|
||||
|
||||
# Convert to JSON and print
|
||||
#print(json.dumps(sensor_data, indent=4))
|
||||
|
||||
|
||||
#save to sqlite database
|
||||
try:
|
||||
cursor.execute('''
|
||||
INSERT INTO data_BME280 (timestamp,temperature, humidity, pressure) VALUES (?,?,?,?)'''
|
||||
, (rtc_time_str,temperature,humidity,pressure))
|
||||
|
||||
# Commit and close the connection
|
||||
conn.commit()
|
||||
|
||||
#print("Sensor data saved successfully!")
|
||||
|
||||
except Exception as e:
|
||||
print(f"Database error: {e}")
|
||||
|
||||
|
||||
conn.close()
|
||||
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 16 op
|
||||
pinctrl set 16 dh
|
||||
pinctrl set 16 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()
|
||||
@@ -1,4 +1,10 @@
|
||||
'''
|
||||
_ _ ____ __ __
|
||||
| \ | | _ \| \/ |
|
||||
| \| | |_) | |\/| |
|
||||
| |\ | __/| | | |
|
||||
|_| \_|_| |_| |_|
|
||||
|
||||
Script to get NPM values
|
||||
need parameter: port
|
||||
/usr/bin/python3 /var/www/nebuleair_pro_4g/NPM/get_data.py ttyAMA5
|
||||
|
||||
@@ -1,12 +1,19 @@
|
||||
'''
|
||||
_ _ ____ __ __
|
||||
| \ | | _ \| \/ |
|
||||
| \| | |_) | |\/| |
|
||||
| |\ | __/| | | |
|
||||
|_| \_|_| |_| |_|
|
||||
|
||||
Script to get NPM data via Modbus
|
||||
need parameter: port
|
||||
/usr/bin/python3 /var/www/nebuleair_pro_4g/NPM/get_data_modbus.py ttyAMA5
|
||||
/usr/bin/python3 /var/www/nebuleair_pro_4g/NPM/get_data_modbus.py
|
||||
|
||||
Modbus RTU
|
||||
[Slave Address][Function Code][Starting Address][Quantity of Registers][CRC]
|
||||
|
||||
Pour récupérer les 5 cannaux (a partir du registre 0x80)
|
||||
Donnée actualisée toutes les 10 secondes
|
||||
|
||||
Request
|
||||
\x01\x03\x00\x80\x00\x0A\xE4\x1E
|
||||
@@ -24,13 +31,28 @@ import requests
|
||||
import json
|
||||
import sys
|
||||
import crcmod
|
||||
import sqlite3
|
||||
|
||||
parameter = sys.argv[1:] # Exclude the script name
|
||||
#print("Parameters received:")
|
||||
port='/dev/'+parameter[0]
|
||||
# Connect to the SQLite database
|
||||
conn = sqlite3.connect("/var/www/nebuleair_pro_4g/sqlite/sensors.db")
|
||||
cursor = conn.cursor()
|
||||
|
||||
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 {}
|
||||
|
||||
# Load the configuration data
|
||||
config_file = '/var/www/nebuleair_pro_4g/config.json'
|
||||
config = load_config(config_file)
|
||||
npm_solo_port = config.get('NPM_solo_port', '') #port du NPM solo
|
||||
|
||||
ser = serial.Serial(
|
||||
port=port,
|
||||
port=npm_solo_port,
|
||||
baudrate=115200,
|
||||
parity=serial.PARITY_EVEN,
|
||||
stopbits=serial.STOPBITS_ONE,
|
||||
@@ -51,35 +73,40 @@ crc_high = (crc >> 8) & 0xFF
|
||||
|
||||
# Append CRC to the frame
|
||||
request = data + bytes([crc_low, crc_high])
|
||||
print(f"Request frame: {request.hex()}")
|
||||
#print(f"Request frame: {request.hex()}")
|
||||
|
||||
ser.write(request)
|
||||
|
||||
#GET RTC TIME from SQlite
|
||||
cursor.execute("SELECT * FROM timestamp_table LIMIT 1")
|
||||
row = cursor.fetchone() # Get the first (and only) row
|
||||
rtc_time_str = row[1] # '2025-02-07 12:30:45'
|
||||
|
||||
while True:
|
||||
try:
|
||||
byte_data = ser.readline()
|
||||
formatted = ''.join(f'\\x{byte:02x}' for byte in byte_data)
|
||||
print(formatted)
|
||||
#print(formatted)
|
||||
|
||||
# Extract LSW (first 2 bytes) and MSW (last 2 bytes)
|
||||
lsw_channel1 = int.from_bytes(byte_data[3:5], byteorder='little')
|
||||
msw_chanel1 = int.from_bytes(byte_data[5:7], byteorder='little')
|
||||
lsw_channel1 = int.from_bytes(byte_data[3:5], byteorder='big')
|
||||
msw_chanel1 = int.from_bytes(byte_data[5:7], byteorder='big')
|
||||
raw_value_channel1 = (msw_chanel1 << 16) | lsw_channel1
|
||||
|
||||
lsw_channel2 = int.from_bytes(byte_data[7:9], byteorder='little')
|
||||
msw_chanel2 = int.from_bytes(byte_data[9:11], byteorder='little')
|
||||
lsw_channel2 = int.from_bytes(byte_data[7:9], byteorder='big')
|
||||
msw_chanel2 = int.from_bytes(byte_data[9:11], byteorder='big')
|
||||
raw_value_channel2 = (msw_chanel2 << 16) | lsw_channel2
|
||||
|
||||
lsw_channel3 = int.from_bytes(byte_data[11:13], byteorder='little')
|
||||
msw_chanel3 = int.from_bytes(byte_data[13:15], byteorder='little')
|
||||
lsw_channel3 = int.from_bytes(byte_data[11:13], byteorder='big')
|
||||
msw_chanel3 = int.from_bytes(byte_data[13:15], byteorder='big')
|
||||
raw_value_channel3 = (msw_chanel3 << 16) | lsw_channel3
|
||||
|
||||
lsw_channel4 = int.from_bytes(byte_data[15:17], byteorder='little')
|
||||
msw_chanel4 = int.from_bytes(byte_data[17:19], byteorder='little')
|
||||
lsw_channel4 = int.from_bytes(byte_data[15:17], byteorder='big')
|
||||
msw_chanel4 = int.from_bytes(byte_data[17:19], byteorder='big')
|
||||
raw_value_channel4 = (msw_chanel1 << 16) | lsw_channel4
|
||||
|
||||
lsw_channel5 = int.from_bytes(byte_data[19:21], byteorder='little')
|
||||
msw_chanel5 = int.from_bytes(byte_data[21:23], byteorder='little')
|
||||
lsw_channel5 = int.from_bytes(byte_data[19:21], byteorder='big')
|
||||
msw_chanel5 = int.from_bytes(byte_data[21:23], byteorder='big')
|
||||
raw_value_channel5 = (msw_chanel5 << 16) | lsw_channel5
|
||||
|
||||
print(f"Channel 1 (0.2->0.5): {raw_value_channel1}")
|
||||
@@ -88,6 +115,12 @@ while True:
|
||||
print(f"Channel 4 (2.5->5.0): {raw_value_channel4}")
|
||||
print(f"Channel 5 (5.0->10.): {raw_value_channel5}")
|
||||
|
||||
cursor.execute('''
|
||||
INSERT INTO data_NPM_5channels (timestamp,PM_ch1, PM_ch2, PM_ch3, PM_ch4, PM_ch5) VALUES (?,?,?,?,?,?)'''
|
||||
, (rtc_time_str,raw_value_channel1,raw_value_channel2,raw_value_channel3,raw_value_channel4,raw_value_channel5))
|
||||
|
||||
# Commit and close the connection
|
||||
conn.commit()
|
||||
|
||||
|
||||
break
|
||||
@@ -101,3 +134,4 @@ while True:
|
||||
time.sleep(3)
|
||||
exit()
|
||||
|
||||
conn.close()
|
||||
|
||||
188
NPM/get_data_modbus_v2.py
Executable file
188
NPM/get_data_modbus_v2.py
Executable file
@@ -0,0 +1,188 @@
|
||||
'''
|
||||
_ _ ____ __ __
|
||||
| \ | | _ \| \/ |
|
||||
| \| | |_) | |\/| |
|
||||
| |\ | __/| | | |
|
||||
|_| \_|_| |_| |_|
|
||||
|
||||
Script to get NPM data via Modbus
|
||||
need parameter: port
|
||||
/usr/bin/python3 /var/www/nebuleair_pro_4g/NPM/get_data_modbus_v2.py
|
||||
|
||||
Modbus RTU
|
||||
[Slave Address][Function Code][Starting Address][Quantity of Registers][CRC]
|
||||
|
||||
Pour récupérer
|
||||
les concentrations en PM1, PM10 et PM2.5 (a partir du registre 0x38)
|
||||
les 5 cannaux
|
||||
la température et l'humidité à l'intérieur du capteur
|
||||
Donnée actualisée toutes les 10 secondes
|
||||
|
||||
Request
|
||||
\x01\x03\x00\x38\x00\x55\...\...
|
||||
|
||||
\x01 Slave Address (slave device address)
|
||||
\x03 Function code (read multiple holding registers)
|
||||
\x00\x38 Starting Address (The request starts reading from holding register address x38 or 56)
|
||||
\x00\x55 Quantity of Registers (Requests to read x55 or 85 consecutive registers starting from address 56)
|
||||
\...\... Cyclic Redundancy Check (checksum )
|
||||
|
||||
'''
|
||||
|
||||
import serial
|
||||
import requests
|
||||
import json
|
||||
import sys
|
||||
import crcmod
|
||||
import sqlite3
|
||||
|
||||
# Connect to the SQLite database
|
||||
conn = sqlite3.connect("/var/www/nebuleair_pro_4g/sqlite/sensors.db")
|
||||
cursor = conn.cursor()
|
||||
|
||||
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 {}
|
||||
|
||||
# Load the configuration data
|
||||
config_file = '/var/www/nebuleair_pro_4g/config.json'
|
||||
config = load_config(config_file)
|
||||
npm_solo_port = config.get('NPM_solo_port', '') #port du NPM solo
|
||||
|
||||
ser = serial.Serial(
|
||||
port=npm_solo_port,
|
||||
baudrate=115200,
|
||||
parity=serial.PARITY_EVEN,
|
||||
stopbits=serial.STOPBITS_ONE,
|
||||
bytesize=serial.EIGHTBITS,
|
||||
timeout = 2
|
||||
)
|
||||
|
||||
# Define Modbus CRC-16 function
|
||||
crc16 = crcmod.predefined.mkPredefinedCrcFun('modbus')
|
||||
|
||||
# Request frame without CRC
|
||||
data = b'\x01\x03\x00\x38\x00\x55'
|
||||
|
||||
# Calculate CRC
|
||||
crc = crc16(data)
|
||||
crc_low = crc & 0xFF
|
||||
crc_high = (crc >> 8) & 0xFF
|
||||
|
||||
# Append CRC to the frame
|
||||
request = data + bytes([crc_low, crc_high])
|
||||
#print(f"Request frame: {request.hex()}")
|
||||
|
||||
ser.write(request)
|
||||
|
||||
#GET RTC TIME from SQlite
|
||||
cursor.execute("SELECT * FROM timestamp_table LIMIT 1")
|
||||
row = cursor.fetchone() # Get the first (and only) row
|
||||
rtc_time_str = row[1] # '2025-02-07 12:30:45'
|
||||
|
||||
while True:
|
||||
try:
|
||||
byte_data = ser.readline()
|
||||
formatted = ''.join(f'\\x{byte:02x}' for byte in byte_data)
|
||||
print(formatted)
|
||||
|
||||
# Register base (56 = 0x38)
|
||||
REGISTER_START = 56
|
||||
|
||||
# Function to extract 32-bit values from Modbus response
|
||||
def extract_value(byte_data, register, scale=1, single_register=False, round_to=None):
|
||||
"""Extracts a value from Modbus response.
|
||||
|
||||
- `register`: Modbus register to read.
|
||||
- `scale`: Value is divided by this (e.g., `1000` for PM values).
|
||||
- `single_register`: If `True`, only reads 16 bits (one register).
|
||||
"""
|
||||
offset = (register - REGISTER_START) * 2 + 3 # Calculate byte offset
|
||||
|
||||
if single_register:
|
||||
value = int.from_bytes(byte_data[offset:offset+2], byteorder='big')
|
||||
else:
|
||||
lsw = int.from_bytes(byte_data[offset:offset+2], byteorder='big')
|
||||
msw = int.from_bytes(byte_data[offset+2:offset+4], byteorder='big')
|
||||
value = (msw << 16) | lsw # 32-bit value
|
||||
|
||||
value = value / scale # Apply scaling
|
||||
|
||||
if round_to == 0:
|
||||
return int(value) # Convert to integer to remove .0
|
||||
elif round_to is not None:
|
||||
return round(value, round_to) # Apply normal rounding
|
||||
else:
|
||||
return value # No rounding if round_to is None
|
||||
|
||||
# 10-sec PM Concentration (PM1, PM2.5, PM10)
|
||||
pm1_10s = extract_value(byte_data, 56, 1000, round_to=1)
|
||||
pm25_10s = extract_value(byte_data, 58, 1000, round_to=1)
|
||||
pm10_10s = extract_value(byte_data, 60, 1000, round_to=1)
|
||||
|
||||
print("10 sec concentration:")
|
||||
print(f"PM1: {pm1_10s}")
|
||||
print(f"PM2.5: {pm25_10s}")
|
||||
print(f"PM10: {pm10_10s}")
|
||||
|
||||
# 1-min PM Concentration
|
||||
pm1_1min = extract_value(byte_data, 68, 1000, round_to=1)
|
||||
pm25_1min = extract_value(byte_data, 70, 1000, round_to=1)
|
||||
pm10_1min = extract_value(byte_data, 72, 1000, round_to=1)
|
||||
|
||||
#print("1 min concentration:")
|
||||
#print(f"PM1: {pm1_1min}")
|
||||
#print(f"PM2.5: {pm25_1min}")
|
||||
#print(f"PM10: {pm10_1min}")
|
||||
|
||||
# Extract values for 5 channels
|
||||
channel_1 = extract_value(byte_data, 128, round_to=0) # 0.2 - 0.5μm
|
||||
channel_2 = extract_value(byte_data, 130, round_to=0) # 0.5 - 1.0μm
|
||||
channel_3 = extract_value(byte_data, 132, round_to=0) # 1.0 - 2.5μm
|
||||
channel_4 = extract_value(byte_data, 134, round_to=0) # 2.5 - 5.0μm
|
||||
channel_5 = extract_value(byte_data, 136, round_to=0) # 5.0 - 10.0μm
|
||||
|
||||
print(f"Channel 1 (0.2->0.5): {channel_1}")
|
||||
print(f"Channel 2 (0.5->1.0): {channel_2}")
|
||||
print(f"Channel 3 (1.0->2.5): {channel_3}")
|
||||
print(f"Channel 4 (2.5->5.0): {channel_4}")
|
||||
print(f"Channel 5 (5.0->10.): {channel_5}")
|
||||
|
||||
|
||||
# Retrieve relative humidity from register 106 (0x6A)
|
||||
relative_humidity = extract_value(byte_data, 106, 100, single_register=True)
|
||||
#print(f"Internal Relative Humidity: {relative_humidity} %")
|
||||
# Retrieve temperature from register 106 (0x6A)
|
||||
temperature = extract_value(byte_data, 107, 100, single_register=True)
|
||||
#print(f"Internal temperature: {temperature} °C")
|
||||
|
||||
cursor.execute('''
|
||||
INSERT INTO data_NPM_5channels (timestamp,PM_ch1, PM_ch2, PM_ch3, PM_ch4, PM_ch5) VALUES (?,?,?,?,?,?)'''
|
||||
, (rtc_time_str,channel_1,channel_2,channel_3,channel_4,channel_5))
|
||||
|
||||
cursor.execute('''
|
||||
INSERT INTO data_NPM (timestamp,PM1, PM25, PM10, temp_npm, hum_npm) VALUES (?,?,?,?,?,?)'''
|
||||
, (rtc_time_str,pm1_10s,pm25_10s,pm10_10s,temperature,relative_humidity ))
|
||||
|
||||
|
||||
# Commit and close the connection
|
||||
conn.commit()
|
||||
|
||||
|
||||
break
|
||||
except KeyboardInterrupt:
|
||||
print("User interrupt encountered. Exiting...")
|
||||
time.sleep(3)
|
||||
exit()
|
||||
except:
|
||||
# for all other kinds of error, but not specifying which one
|
||||
print("Unknown error...")
|
||||
time.sleep(3)
|
||||
exit()
|
||||
|
||||
conn.close()
|
||||
177
NPM/get_data_modbus_v3.py
Executable file
177
NPM/get_data_modbus_v3.py
Executable file
@@ -0,0 +1,177 @@
|
||||
'''
|
||||
_ _ ____ __ __
|
||||
| \ | | _ \| \/ |
|
||||
| \| | |_) | |\/| |
|
||||
| |\ | __/| | | |
|
||||
|_| \_|_| |_| |_|
|
||||
|
||||
Script to get NPM data via Modbus
|
||||
|
||||
Improved version with data stream lenght check
|
||||
|
||||
/usr/bin/python3 /var/www/nebuleair_pro_4g/NPM/get_data_modbus_v3.py
|
||||
|
||||
Modbus RTU
|
||||
[Slave Address][Function Code][Starting Address][Quantity of Registers][CRC]
|
||||
|
||||
Pour récupérer
|
||||
les concentrations en PM1, PM10 et PM2.5 (a partir du registre 0x38)
|
||||
les 5 cannaux
|
||||
la température et l'humidité à l'intérieur du capteur
|
||||
Donnée actualisée toutes les 10 secondes
|
||||
|
||||
Request
|
||||
\x01\x03\x00\x38\x00\x55\...\...
|
||||
|
||||
\x01 Slave Address (slave device address)
|
||||
\x03 Function code (read multiple holding registers)
|
||||
\x00\x38 Starting Address (The request starts reading from holding register address x38 or 56)
|
||||
\x00\x55 Quantity of Registers (Requests to read x55 or 85 consecutive registers starting from address 56)
|
||||
\...\... Cyclic Redundancy Check (checksum )
|
||||
|
||||
'''
|
||||
import serial
|
||||
import requests
|
||||
import json
|
||||
import sys
|
||||
import crcmod
|
||||
import sqlite3
|
||||
import time
|
||||
|
||||
# Connect to the SQLite database
|
||||
conn = sqlite3.connect("/var/www/nebuleair_pro_4g/sqlite/sensors.db")
|
||||
cursor = conn.cursor()
|
||||
|
||||
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 {}
|
||||
|
||||
# Load the configuration data
|
||||
npm_solo_port = "/dev/ttyAMA5" #port du NPM solo
|
||||
|
||||
#GET RTC TIME from SQlite
|
||||
cursor.execute("SELECT * FROM timestamp_table LIMIT 1")
|
||||
row = cursor.fetchone() # Get the first (and only) row
|
||||
rtc_time_str = row[1] # '2025-02-07 12:30:45'
|
||||
|
||||
# Initialize serial port
|
||||
ser = serial.Serial(
|
||||
port=npm_solo_port,
|
||||
baudrate=115200,
|
||||
parity=serial.PARITY_EVEN,
|
||||
stopbits=serial.STOPBITS_ONE,
|
||||
bytesize=serial.EIGHTBITS,
|
||||
timeout=2
|
||||
)
|
||||
|
||||
# Define Modbus CRC-16 function
|
||||
crc16 = crcmod.predefined.mkPredefinedCrcFun('modbus')
|
||||
|
||||
# Request frame without CRC
|
||||
data = b'\x01\x03\x00\x38\x00\x55'
|
||||
|
||||
# Calculate and append CRC
|
||||
crc = crc16(data)
|
||||
crc_low = crc & 0xFF
|
||||
crc_high = (crc >> 8) & 0xFF
|
||||
request = data + bytes([crc_low, crc_high])
|
||||
|
||||
# Clear serial buffer before sending
|
||||
ser.flushInput()
|
||||
|
||||
# Send request
|
||||
ser.write(request)
|
||||
time.sleep(0.2) # Wait for sensor to respond
|
||||
|
||||
# Read response
|
||||
response_length = 2 + 1 + (2 * 85) + 2 # Address + Function + Data + CRC
|
||||
byte_data = ser.read(response_length)
|
||||
|
||||
# Validate response length
|
||||
if len(byte_data) < response_length:
|
||||
print("[ERROR] Incomplete response received:", byte_data.hex())
|
||||
exit()
|
||||
|
||||
# Verify CRC
|
||||
received_crc = int.from_bytes(byte_data[-2:], byteorder='little')
|
||||
calculated_crc = crc16(byte_data[:-2])
|
||||
|
||||
if received_crc != calculated_crc:
|
||||
print("[ERROR] CRC check failed! Corrupted data received.")
|
||||
exit()
|
||||
|
||||
# Convert response to hex for debugging
|
||||
formatted = ''.join(f'\\x{byte:02x}' for byte in byte_data)
|
||||
#print("Response:", formatted)
|
||||
|
||||
# Extract and print PM values
|
||||
def extract_value(byte_data, register, scale=1, single_register=False, round_to=None):
|
||||
REGISTER_START = 56
|
||||
offset = (register - REGISTER_START) * 2 + 3
|
||||
|
||||
if single_register:
|
||||
value = int.from_bytes(byte_data[offset:offset+2], byteorder='big')
|
||||
else:
|
||||
lsw = int.from_bytes(byte_data[offset:offset+2], byteorder='big')
|
||||
msw = int.from_bytes(byte_data[offset+2:offset+4], byteorder='big')
|
||||
value = (msw << 16) | lsw
|
||||
|
||||
value = value / scale
|
||||
|
||||
if round_to == 0:
|
||||
return int(value)
|
||||
elif round_to is not None:
|
||||
return round(value, round_to)
|
||||
else:
|
||||
return value
|
||||
|
||||
pm1_10s = extract_value(byte_data, 56, 1000, round_to=1)
|
||||
pm25_10s = extract_value(byte_data, 58, 1000, round_to=1)
|
||||
pm10_10s = extract_value(byte_data, 60, 1000, round_to=1)
|
||||
|
||||
#print("10 sec concentration:")
|
||||
#print(f"PM1: {pm1_10s}")
|
||||
#print(f"PM2.5: {pm25_10s}")
|
||||
#print(f"PM10: {pm10_10s}")
|
||||
|
||||
# Extract values for 5 channels
|
||||
channel_1 = extract_value(byte_data, 128, round_to=0) # 0.2 - 0.5μm
|
||||
channel_2 = extract_value(byte_data, 130, round_to=0) # 0.5 - 1.0μm
|
||||
channel_3 = extract_value(byte_data, 132, round_to=0) # 1.0 - 2.5μm
|
||||
channel_4 = extract_value(byte_data, 134, round_to=0) # 2.5 - 5.0μm
|
||||
channel_5 = extract_value(byte_data, 136, round_to=0) # 5.0 - 10.0μm
|
||||
|
||||
#print(f"Channel 1 (0.2->0.5): {channel_1}")
|
||||
#print(f"Channel 2 (0.5->1.0): {channel_2}")
|
||||
#print(f"Channel 3 (1.0->2.5): {channel_3}")
|
||||
#print(f"Channel 4 (2.5->5.0): {channel_4}")
|
||||
#print(f"Channel 5 (5.0->10.): {channel_5}")
|
||||
|
||||
|
||||
# Retrieve relative humidity from register 106 (0x6A)
|
||||
relative_humidity = extract_value(byte_data, 106, 100, single_register=True)
|
||||
# Retrieve temperature from register 106 (0x6A)
|
||||
temperature = extract_value(byte_data, 107, 100, single_register=True)
|
||||
|
||||
#print(f"Internal Relative Humidity: {relative_humidity} %")
|
||||
#print(f"Internal temperature: {temperature} °C")
|
||||
|
||||
|
||||
|
||||
cursor.execute('''
|
||||
INSERT INTO data_NPM_5channels (timestamp,PM_ch1, PM_ch2, PM_ch3, PM_ch4, PM_ch5) VALUES (?,?,?,?,?,?)'''
|
||||
, (rtc_time_str,channel_1,channel_2,channel_3,channel_4,channel_5))
|
||||
|
||||
cursor.execute('''
|
||||
INSERT INTO data_NPM (timestamp,PM1, PM25, PM10, temp_npm, hum_npm) VALUES (?,?,?,?,?,?)'''
|
||||
, (rtc_time_str,pm1_10s,pm25_10s,pm10_10s,temperature,relative_humidity ))
|
||||
|
||||
# Commit and close the connection
|
||||
conn.commit()
|
||||
|
||||
conn.close()
|
||||
52
NPM/get_data_temp_hum.py
Executable file
52
NPM/get_data_temp_hum.py
Executable file
@@ -0,0 +1,52 @@
|
||||
'''
|
||||
_ _ ____ __ __
|
||||
| \ | | _ \| \/ |
|
||||
| \| | |_) | |\/| |
|
||||
| |\ | __/| | | |
|
||||
|_| \_|_| |_| |_|
|
||||
|
||||
Script to get NPM values: ONLY temp and hum
|
||||
need parameter: port
|
||||
/usr/bin/python3 /var/www/nebuleair_pro_4g/NPM/get_data_temp_hum.py ttyAMA5
|
||||
'''
|
||||
|
||||
import serial
|
||||
import requests
|
||||
import json
|
||||
import sys
|
||||
|
||||
parameter = sys.argv[1:] # Exclude the script name
|
||||
#print("Parameters received:")
|
||||
port='/dev/'+parameter[0]
|
||||
|
||||
ser = serial.Serial(
|
||||
port=port,
|
||||
baudrate=115200,
|
||||
parity=serial.PARITY_EVEN,
|
||||
stopbits=serial.STOPBITS_ONE,
|
||||
bytesize=serial.EIGHTBITS,
|
||||
timeout = 1
|
||||
)
|
||||
|
||||
ser.write(b'\x81\x14\x6B') # Temp and humidity command
|
||||
|
||||
while True:
|
||||
try:
|
||||
byte_data_temp_hum = ser.readline()
|
||||
# Decode temperature and humidity values
|
||||
temperature = int.from_bytes(byte_data_temp_hum[3:5], byteorder='big') / 100.0
|
||||
humidity = int.from_bytes(byte_data_temp_hum[5:7], byteorder='big') / 100.0
|
||||
|
||||
print(f"temp: {temperature}")
|
||||
print(f"hum: {humidity}")
|
||||
break
|
||||
except KeyboardInterrupt:
|
||||
print("User interrupt encountered. Exiting...")
|
||||
time.sleep(3)
|
||||
exit()
|
||||
except:
|
||||
# for all other kinds of error, but not specifying which one
|
||||
print("Unknown error...")
|
||||
time.sleep(3)
|
||||
exit()
|
||||
|
||||
104
NPM/get_data_v2.py
Executable file
104
NPM/get_data_v2.py
Executable file
@@ -0,0 +1,104 @@
|
||||
'''
|
||||
_ _ ____ __ __
|
||||
| \ | | _ \| \/ |
|
||||
| \| | |_) | |\/| |
|
||||
| |\ | __/| | | |
|
||||
|_| \_|_| |_| |_|
|
||||
|
||||
Script to get NPM values (PM1, PM2.5 and PM10)
|
||||
PM and the sensor temp/hum
|
||||
And store them inside sqlite database
|
||||
Uses RTC module for timing (from SQLite db)
|
||||
/usr/bin/python3 /var/www/nebuleair_pro_4g/NPM/get_data_v2.py
|
||||
'''
|
||||
|
||||
import serial
|
||||
import requests
|
||||
import json
|
||||
import sys
|
||||
import sqlite3
|
||||
import smbus2
|
||||
import time
|
||||
from datetime import datetime
|
||||
|
||||
# Connect to the SQLite database
|
||||
conn = sqlite3.connect("/var/www/nebuleair_pro_4g/sqlite/sensors.db")
|
||||
cursor = conn.cursor()
|
||||
|
||||
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 {}
|
||||
|
||||
# Load the configuration data
|
||||
config_file = '/var/www/nebuleair_pro_4g/config.json'
|
||||
config = load_config(config_file)
|
||||
npm_solo_port = config.get('NPM_solo_port', '') #port du NPM solo
|
||||
|
||||
ser = serial.Serial(
|
||||
port=npm_solo_port,
|
||||
baudrate=115200,
|
||||
parity=serial.PARITY_EVEN,
|
||||
stopbits=serial.STOPBITS_ONE,
|
||||
bytesize=serial.EIGHTBITS,
|
||||
timeout = 0.5
|
||||
)
|
||||
|
||||
# 1️⃣ Request PM Data (PM1, PM2.5, PM10)
|
||||
|
||||
#ser.write(b'\x81\x11\x6E') #data10s
|
||||
ser.write(b'\x81\x12\x6D') #data60s
|
||||
time.sleep(0.5) # Small delay to allow the sensor to process the request
|
||||
|
||||
#print("Start get_data_v2.py script")
|
||||
byte_data = ser.readline()
|
||||
#print(byte_data)
|
||||
stateByte = int.from_bytes(byte_data[2:3], byteorder='big')
|
||||
Statebits = [int(bit) for bit in bin(stateByte)[2:].zfill(8)]
|
||||
PM1 = int.from_bytes(byte_data[9:11], byteorder='big')/10
|
||||
PM25 = int.from_bytes(byte_data[11:13], byteorder='big')/10
|
||||
PM10 = int.from_bytes(byte_data[13:15], byteorder='big')/10
|
||||
|
||||
# 2️⃣ Request Temperature & Humidity
|
||||
ser.write(b'\x81\x14\x6B') # Temp and humidity command
|
||||
time.sleep(0.5) # Small delay to allow the sensor to process the request
|
||||
byte_data_temp_hum = ser.readline()
|
||||
|
||||
# Decode temperature and humidity values
|
||||
temperature = int.from_bytes(byte_data_temp_hum[3:5], byteorder='big') / 100.0
|
||||
humidity = int.from_bytes(byte_data_temp_hum[5:7], byteorder='big') / 100.0
|
||||
|
||||
#print(f"State: {Statebits}")
|
||||
#print(f"PM1: {PM1}")
|
||||
#print(f"PM25: {PM25}")
|
||||
#print(f"PM10: {PM10}")
|
||||
#print(f"temp: {temperature}")
|
||||
#print(f"hum: {humidity}")
|
||||
|
||||
#GET RTC TIME from SQlite
|
||||
cursor.execute("SELECT * FROM timestamp_table LIMIT 1")
|
||||
row = cursor.fetchone() # Get the first (and only) row
|
||||
rtc_time_str = row[1] # '2025-02-07 12:30:45'
|
||||
|
||||
#save to sqlite database
|
||||
try:
|
||||
cursor.execute('''
|
||||
INSERT INTO data_NPM (timestamp,PM1, PM25, PM10, temp_npm, hum_npm) VALUES (?,?,?,?,?,?)'''
|
||||
, (rtc_time_str,PM1,PM25,PM10,temperature,humidity ))
|
||||
|
||||
# Commit and close the connection
|
||||
conn.commit()
|
||||
|
||||
#print("Sensor data saved successfully!")
|
||||
|
||||
except Exception as e:
|
||||
print(f"Database error: {e}")
|
||||
|
||||
|
||||
conn.close()
|
||||
|
||||
|
||||
126
NPM/old/test_modbus.py
Executable file
126
NPM/old/test_modbus.py
Executable file
@@ -0,0 +1,126 @@
|
||||
'''
|
||||
_ _ ____ __ __
|
||||
| \ | | _ \| \/ |
|
||||
| \| | |_) | |\/| |
|
||||
| |\ | __/| | | |
|
||||
|_| \_|_| |_| |_|
|
||||
|
||||
Script to get NPM data via Modbus
|
||||
need parameter: port
|
||||
/usr/bin/python3 /var/www/nebuleair_pro_4g/NPM/old/test_modbus.py
|
||||
|
||||
Modbus RTU
|
||||
[Slave Address][Function Code][Starting Address][Quantity of Registers][CRC]
|
||||
|
||||
Pour récupérer
|
||||
les concentrations en PM1, PM10 et PM2.5 (a partir du registre 0x38)
|
||||
les 5 cannaux
|
||||
Donnée actualisée toutes les 10 secondes
|
||||
|
||||
Request
|
||||
\x01\x03\x00\x38\x00\x55\...\...
|
||||
|
||||
\x01 Slave Address (slave device address)
|
||||
\x03 Function code (read multiple holding registers)
|
||||
\x00\x38 Starting Address (The request starts reading from holding register address x38 or 56)
|
||||
\x00\x55 Quantity of Registers (Requests to read x55 or 85 consecutive registers starting from address 56)
|
||||
\...\... Cyclic Redundancy Check (checksum )
|
||||
|
||||
'''
|
||||
|
||||
import serial
|
||||
import requests
|
||||
import json
|
||||
import sys
|
||||
import crcmod
|
||||
import sqlite3
|
||||
|
||||
# Connect to the SQLite database
|
||||
conn = sqlite3.connect("/var/www/nebuleair_pro_4g/sqlite/sensors.db")
|
||||
cursor = conn.cursor()
|
||||
|
||||
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 {}
|
||||
|
||||
# Load the configuration data
|
||||
config_file = '/var/www/nebuleair_pro_4g/config.json'
|
||||
config = load_config(config_file)
|
||||
npm_solo_port = config.get('NPM_solo_port', '') #port du NPM solo
|
||||
|
||||
ser = serial.Serial(
|
||||
port=npm_solo_port,
|
||||
baudrate=115200,
|
||||
parity=serial.PARITY_EVEN,
|
||||
stopbits=serial.STOPBITS_ONE,
|
||||
bytesize=serial.EIGHTBITS,
|
||||
timeout = 0.5
|
||||
)
|
||||
|
||||
# Define Modbus CRC-16 function
|
||||
crc16 = crcmod.predefined.mkPredefinedCrcFun('modbus')
|
||||
|
||||
# Request frame without CRC
|
||||
data = b'\x01\x03\x00\x44\x00\x06'
|
||||
|
||||
# Calculate CRC
|
||||
crc = crc16(data)
|
||||
crc_low = crc & 0xFF
|
||||
crc_high = (crc >> 8) & 0xFF
|
||||
|
||||
# Append CRC to the frame
|
||||
request = data + bytes([crc_low, crc_high])
|
||||
#print(f"Request frame: {request.hex()}")
|
||||
|
||||
ser.write(request)
|
||||
|
||||
while True:
|
||||
try:
|
||||
byte_data = ser.readline()
|
||||
formatted = ''.join(f'\\x{byte:02x}' for byte in byte_data)
|
||||
print(formatted)
|
||||
|
||||
if len(byte_data) < 14:
|
||||
print(f"Error: Received {len(byte_data)} bytes, expected 14!")
|
||||
continue
|
||||
|
||||
#10 secs concentration
|
||||
|
||||
lsw_pm1 = int.from_bytes(byte_data[3:5], byteorder='big')
|
||||
msw_pm1 = int.from_bytes(byte_data[5:7], byteorder='big')
|
||||
raw_value_pm1 = (msw_pm1 << 16) | lsw_pm1
|
||||
raw_value_pm1 = raw_value_pm1 / 1000
|
||||
|
||||
lsw_pm25 = int.from_bytes(byte_data[7:9], byteorder='big')
|
||||
msw_pm25 = int.from_bytes(byte_data[9:11], byteorder='big')
|
||||
raw_value_pm25 = (msw_pm25 << 16) | lsw_pm25
|
||||
raw_value_pm25 = raw_value_pm25 / 1000
|
||||
|
||||
|
||||
lsw_pm10 = int.from_bytes(byte_data[11:13], byteorder='big')
|
||||
msw_pm10 = int.from_bytes(byte_data[13:15], byteorder='big')
|
||||
raw_value_pm10 = (msw_pm10 << 16) | lsw_pm10
|
||||
raw_value_pm10 = raw_value_pm10 / 1000
|
||||
|
||||
print("1 min")
|
||||
print(f"PM1: {raw_value_pm1}")
|
||||
print(f"PM2.5: {raw_value_pm25}")
|
||||
print(f"PM10: {raw_value_pm10}")
|
||||
|
||||
break
|
||||
except KeyboardInterrupt:
|
||||
print("User interrupt encountered. Exiting...")
|
||||
time.sleep(3)
|
||||
exit()
|
||||
except:
|
||||
# for all other kinds of error, but not specifying which one
|
||||
print("Unknown error...")
|
||||
time.sleep(3)
|
||||
exit()
|
||||
|
||||
conn.close()
|
||||
70
README.md
70
README.md
@@ -4,30 +4,46 @@ Based on the Rpi4 or CM4.
|
||||
|
||||
# Installation
|
||||
|
||||
Installation can be made with Ansible or the classic way.
|
||||
# Express
|
||||
|
||||
You can download the `installation_part1.sh` and run it:
|
||||
```
|
||||
wget http://gitea.aircarto.fr/PaulVua/nebuleair_pro_4g/raw/branch/main/installation_part1.sh
|
||||
chmod +x installation_part1.sh
|
||||
sudo ./installation_part1.sh
|
||||
```
|
||||
|
||||
After reboot you can do the same with part 2.
|
||||
|
||||
```
|
||||
wget http://gitea.aircarto.fr/PaulVua/nebuleair_pro_4g/raw/branch/main/installation_part2.sh
|
||||
chmod +x installation_part2.sh
|
||||
sudo ./installation_part2.sh
|
||||
```
|
||||
|
||||
## Ansible (WORK IN PROGRESS)
|
||||
Installation with Ansible will use a playbook `install_software.yml`.
|
||||
|
||||
## General
|
||||
|
||||
See `installation.sh`
|
||||
Line by line installation.
|
||||
|
||||
```
|
||||
sudo apt update
|
||||
sudo apt install git gh apache2 php python3 python3-pip jq autossh i2c-tools python3-smbus -y
|
||||
sudo pip3 install pyserial requests RPi.GPIO adafruit-circuitpython-bme280 crcmod --break-system-packages
|
||||
sudo apt install git gh apache2 sqlite3 php php-sqlite3 python3 python3-pip jq autossh i2c-tools python3-smbus -y
|
||||
sudo pip3 install pyserial requests RPi.GPIO adafruit-circuitpython-bme280 crcmod psutil ntplib pytz gpiozero adafruit-circuitpython-ads1x15 numpy --break-system-packages
|
||||
sudo mkdir -p /var/www/.ssh
|
||||
sudo ssh-keygen -t rsa -b 4096 -f /var/www/.ssh/id_rsa -N ""
|
||||
sudo ssh-copy-id -i /var/www/.ssh/id_rsa.pub -p 50221 airlab_server1@aircarto.fr
|
||||
sudo git clone http://gitea.aircarto.fr/PaulVua/nebuleair_pro_4g.git /var/www/nebuleair_pro_4g
|
||||
sudo mkdir /var/www/nebuleair_pro_4g/logs
|
||||
sudo touch /var/www/nebuleair_pro_4g/logs/app.log /var/www/nebuleair_pro_4g/logs/loop.log /var/www/nebuleair_pro_4g/wifi_list.csv
|
||||
sudo cp /var/www/nebuleair_pro_4g/config.json.dist /var/www/nebuleair_pro_4g/config.json
|
||||
/usr/bin/python3 /var/www/nebuleair_pro_4g/sqlite/create_db.py
|
||||
/usr/bin/python3 /var/www/nebuleair_pro_4g/sqlite/set_config.py
|
||||
sudo chmod -R 777 /var/www/nebuleair_pro_4g/
|
||||
git config --global core.fileMode false
|
||||
git -C /var/www/nebuleair_pro_4g config core.fileMode false
|
||||
git config --global --add safe.directory /var/www/nebuleair_pro_4g
|
||||
sudo crontab /var/www/nebuleair_pro_4g/cron_jobs
|
||||
sudo /usr/bin/python3 /var/www/nebuleair_pro_4g/sqlite/create_db.py
|
||||
```
|
||||
## Apache
|
||||
Configuration of Apache to redirect to the html homepage project
|
||||
@@ -42,7 +58,9 @@ To make things simpler we will allow all users to use "nmcli" as sudo without en
|
||||
ALL ALL=(ALL) NOPASSWD: /usr/bin/nmcli, /usr/sbin/reboot
|
||||
www-data ALL=(ALL) NOPASSWD: /usr/bin/git pull
|
||||
www-data ALL=(ALL) NOPASSWD: /usr/bin/ssh
|
||||
|
||||
www-data ALL=(ALL) NOPASSWD: /usr/bin/python3 *
|
||||
www-data ALL=(ALL) NOPASSWD: /bin/systemctl *
|
||||
www-data ALL=(ALL) NOPASSWD: /var/www/nebuleair_pro_4g/*
|
||||
```
|
||||
## Serial
|
||||
|
||||
@@ -161,42 +179,6 @@ And set the base URL for Sara R4 communication:
|
||||
@reboot sleep 30 && /usr/bin/python3 /var/www/nebuleair_pro_4g/SARA/sara_setURL.py ttyAMA2 data.nebuleair.fr >> /var/www/nebuleair_pro_4g/logs/app.log 2>&1
|
||||
```
|
||||
|
||||
### With only 1 NPM
|
||||
|
||||
Loop every minutes to get the PM values and send it to the server (we use flock to be sure the previous script is over before start the new one):
|
||||
|
||||
```
|
||||
* * * * * flock -n /var/www/nebuleair_pro_4g/loop/1_NPM/send_data.lock /usr/bin/python3 /var/www/nebuleair_pro_4g/loop/1_NPM/send_data.py >> /var/www/nebuleair_pro_4g/logs/loop.log 2>&1
|
||||
```
|
||||
|
||||
All in one:
|
||||
|
||||
```
|
||||
@reboot chmod 777 /dev/ttyAMA*
|
||||
@reboot /var/www/nebuleair_pro_4g/boot_hotspot.sh >> /var/www/nebuleair_pro_4g/logs/app.log 2>&1
|
||||
@reboot sleep 30 && /usr/bin/python3 /var/www/nebuleair_pro_4g/SARA/sara_setURL.py ttyAMA2 data.nebuleair.fr >> /var/www/nebuleair_pro_4g/logs/app.log 2>&1
|
||||
* * * * * flock -n /var/www/nebuleair_pro_4g/loop/1_NPM/send_data.lock /usr/bin/python3 /var/www/nebuleair_pro_4g/loop/1_NPM/send_data.py >> /var/www/nebuleair_pro_4g/logs/loop.log 2>&1
|
||||
0 0 */2 * * > /var/www/nebuleair_pro_4g/logs/loop.log
|
||||
```
|
||||
|
||||
### With 3 NPM
|
||||
Loop every minutes to get the PM values and send it to the server:
|
||||
|
||||
```
|
||||
* * * * * /usr/bin/python3 /var/www/nebuleair_pro_4g/loop/3_NPM/get_data_closest_pair.py >> /var/www/nebuleair_pro_4g/logs/loop.log 2>&1
|
||||
* * * * * sleep 5 && /usr/bin/python3 /var/www/nebuleair_pro_4g/loop/3_NPM/send_data.py >> /var/www/nebuleair_pro_4g/logs/loop.log 2>&1
|
||||
```
|
||||
|
||||
All in one:
|
||||
|
||||
```
|
||||
@reboot chmod 777 /dev/ttyAMA* /dev/i2c-1
|
||||
@reboot /var/www/nebuleair_pro_4g/boot_hotspot.sh >> /var/www/nebuleair_pro_4g/logs/app.log 2>&1
|
||||
@reboot sleep 30 && /usr/bin/python3 /var/www/nebuleair_pro_4g/SARA/sara_setURL.py ttyAMA2 data.nebuleair.fr >> /var/www/nebuleair_pro_4g/logs/app.log 2>&1
|
||||
* * * * * /usr/bin/python3 /var/www/nebuleair_pro_4g/loop/3_NPM/get_data_closest_pair.py >> /var/www/nebuleair_pro_4g/logs/loop.log 2>&1
|
||||
* * * * * sleep 5 && /usr/bin/python3 /var/www/nebuleair_pro_4g/loop/3_NPM/send_data.py >> /var/www/nebuleair_pro_4g/logs/loop.log 2>&1
|
||||
0 0 */2 * * > /var/www/nebuleair_pro_4g/logs/loop.log
|
||||
```
|
||||
|
||||
# Notes
|
||||
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
'''
|
||||
____ _____ ____
|
||||
| _ \_ _/ ___|
|
||||
| |_) || || |
|
||||
| _ < | || |___
|
||||
|_| \_\|_| \____|
|
||||
|
||||
Script to read time from RTC module
|
||||
I2C connection
|
||||
Address 0x68
|
||||
@@ -9,7 +15,6 @@ import time
|
||||
import json
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
# DS3231 I2C address
|
||||
DS3231_ADDR = 0x68
|
||||
|
||||
@@ -52,7 +57,6 @@ def main():
|
||||
rtc_time_str = "not connected"
|
||||
time_difference = "N/A" # Not applicable
|
||||
|
||||
|
||||
# Print both times
|
||||
#print(f"RTC module Time: {rtc_time.strftime('%Y-%m-%d %H:%M:%S')}")
|
||||
#print(f"Sys local Time: {system_time.strftime('%Y-%m-%d %H:%M:%S')}")
|
||||
|
||||
129
RTC/save_to_db.py
Executable file
129
RTC/save_to_db.py
Executable file
@@ -0,0 +1,129 @@
|
||||
'''
|
||||
____ _____ ____
|
||||
| _ \_ _/ ___|
|
||||
| |_) || || |
|
||||
| _ < | || |___
|
||||
|_| \_\|_| \____|
|
||||
|
||||
Script to read time from RTC module and save it to DB
|
||||
I2C connection
|
||||
Address 0x68
|
||||
/usr/bin/python3 /var/www/nebuleair_pro_4g/RTC/save_to_db.py
|
||||
|
||||
This need to be run as a system service
|
||||
|
||||
--> sudo nano /etc/systemd/system/rtc_save_to_db.service
|
||||
|
||||
⬇️
|
||||
[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
|
||||
⬆️
|
||||
|
||||
|
||||
sudo systemctl daemon-reload
|
||||
sudo systemctl enable rtc_save_to_db.service
|
||||
|
||||
sudo systemctl start rtc_save_to_db.service
|
||||
|
||||
sudo systemctl status rtc_save_to_db.service
|
||||
|
||||
'''
|
||||
import smbus2
|
||||
import time
|
||||
import json
|
||||
from datetime import datetime
|
||||
import sqlite3
|
||||
|
||||
# DS3231 I2C address
|
||||
DS3231_ADDR = 0x68
|
||||
|
||||
# Registers for DS3231
|
||||
REG_TIME = 0x00
|
||||
|
||||
# Connect to (or create if not existent) the database
|
||||
DB_PATH = "/var/www/nebuleair_pro_4g/sqlite/sensors.db"
|
||||
|
||||
def bcd_to_dec(bcd):
|
||||
return (bcd // 16 * 10) + (bcd % 16)
|
||||
|
||||
def read_time(bus):
|
||||
"""Try to read and decode time from the RTC module (DS3231)."""
|
||||
try:
|
||||
data = bus.read_i2c_block_data(DS3231_ADDR, REG_TIME, 7)
|
||||
seconds = bcd_to_dec(data[0] & 0x7F)
|
||||
minutes = bcd_to_dec(data[1])
|
||||
hours = bcd_to_dec(data[2] & 0x3F)
|
||||
day = bcd_to_dec(data[4])
|
||||
month = bcd_to_dec(data[5])
|
||||
year = bcd_to_dec(data[6]) + 2000
|
||||
return datetime(year, month, day, hours, minutes, seconds)
|
||||
except OSError:
|
||||
return None # RTC module not connected
|
||||
|
||||
def main():
|
||||
# Read RTC time
|
||||
bus = smbus2.SMBus(1)
|
||||
|
||||
while True:
|
||||
# Open a new database connection inside the loop to prevent connection loss
|
||||
conn = sqlite3.connect(DB_PATH)
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Try to read RTC time
|
||||
rtc_time = read_time(bus)
|
||||
# Get current system time
|
||||
system_time = datetime.now() #local
|
||||
utc_time = datetime.utcnow() #UTC
|
||||
|
||||
# If RTC is not connected, set default message
|
||||
# Calculate time difference (in seconds) if RTC is connected
|
||||
if rtc_time:
|
||||
rtc_time_str = rtc_time.strftime('%Y-%m-%d %H:%M:%S')
|
||||
time_difference = int((utc_time - rtc_time).total_seconds()) # Convert to int
|
||||
else:
|
||||
rtc_time_str = "not connected"
|
||||
time_difference = "N/A" # Not applicable
|
||||
|
||||
# Print both times
|
||||
#print(f"RTC module Time: {rtc_time.strftime('%Y-%m-%d %H:%M:%S')}")
|
||||
#print(f"Sys local Time: {system_time.strftime('%Y-%m-%d %H:%M:%S')}")
|
||||
#print(f"Sys UTC Time: {utc_time.strftime('%Y-%m-%d %H:%M:%S')}")
|
||||
|
||||
# Create JSON output
|
||||
time_data = {
|
||||
"rtc_module_time":rtc_time_str,
|
||||
"system_local_time": system_time.strftime('%Y-%m-%d %H:%M:%S'),
|
||||
"system_utc_time": utc_time.strftime('%Y-%m-%d %H:%M:%S'),
|
||||
"time_difference_seconds": time_difference
|
||||
}
|
||||
|
||||
#print(json.dumps(time_data, indent=4))
|
||||
|
||||
# Save to database
|
||||
try:
|
||||
cursor.execute("UPDATE timestamp_table SET last_updated = ? WHERE id = 1", (rtc_time_str,))
|
||||
conn.commit()
|
||||
#print("Sensor data saved successfully!")
|
||||
|
||||
except sqlite3.Error as e:
|
||||
print(f"Database error: {e}")
|
||||
|
||||
conn.close() # Close connection to avoid database locking issues
|
||||
time.sleep(1) # Wait for 1 second before reading again
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,11 +1,15 @@
|
||||
#!/usr/bin/python3
|
||||
"""
|
||||
Script to set the RTC using an NTP server.
|
||||
____ _____ ____
|
||||
| _ \_ _/ ___|
|
||||
| |_) || || |
|
||||
| _ < | || |___
|
||||
|_| \_\|_| \____|
|
||||
|
||||
Script to set the RTC using an NTP server (script used by web UI)
|
||||
RPI needs to be connected to the internet (WIFI).
|
||||
Requires ntplib and pytz:
|
||||
sudo pip3 install ntplib pytz --break-system-packages
|
||||
|
||||
/usr/bin/python3 /var/www/nebuleair_pro_4g/RTC/set_with_NTP.py
|
||||
|
||||
"""
|
||||
import smbus2
|
||||
import time
|
||||
@@ -49,29 +53,95 @@ def set_time(bus, year, month, day, hour, minute, second):
|
||||
])
|
||||
|
||||
def read_time(bus):
|
||||
"""Read the RTC time."""
|
||||
"""Read the RTC time and validate the values."""
|
||||
try:
|
||||
data = bus.read_i2c_block_data(DS3231_ADDR, REG_TIME, 7)
|
||||
|
||||
# Convert from BCD
|
||||
second = bcd_to_dec(data[0] & 0x7F)
|
||||
minute = bcd_to_dec(data[1])
|
||||
hour = bcd_to_dec(data[2] & 0x3F)
|
||||
day = bcd_to_dec(data[4])
|
||||
month = bcd_to_dec(data[5])
|
||||
year = bcd_to_dec(data[6]) + 2000
|
||||
|
||||
# Print raw values for debugging
|
||||
print(f"Raw RTC values: {data}")
|
||||
print(f"Decoded values: Y:{year} M:{month} D:{day} H:{hour} M:{minute} S:{second}")
|
||||
|
||||
# Validate date values
|
||||
if not (1 <= month <= 12):
|
||||
print(f"Invalid month value: {month}, using default")
|
||||
month = 1
|
||||
|
||||
# Check days in month (simplified)
|
||||
days_in_month = [0, 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
|
||||
if not (1 <= day <= days_in_month[month]):
|
||||
print(f"Invalid day value: {day} for month {month}, using default")
|
||||
day = 1
|
||||
|
||||
# Validate time values
|
||||
if not (0 <= hour <= 23):
|
||||
print(f"Invalid hour value: {hour}, using default")
|
||||
hour = 0
|
||||
|
||||
if not (0 <= minute <= 59):
|
||||
print(f"Invalid minute value: {minute}, using default")
|
||||
minute = 0
|
||||
|
||||
if not (0 <= second <= 59):
|
||||
print(f"Invalid second value: {second}, using default")
|
||||
second = 0
|
||||
|
||||
return (year, month, day, hour, minute, second)
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error reading RTC: {e}")
|
||||
# Return a safe default date (2023-01-01 00:00:00)
|
||||
return (2023, 1, 1, 0, 0, 0)
|
||||
|
||||
def get_internet_time():
|
||||
"""Get the current time from an NTP server."""
|
||||
ntp_client = ntplib.NTPClient()
|
||||
response = ntp_client.request('pool.ntp.org')
|
||||
# Try multiple NTP servers in case one fails
|
||||
servers = ['pool.ntp.org', 'time.google.com', 'time.windows.com', 'time.apple.com']
|
||||
|
||||
for server in servers:
|
||||
try:
|
||||
print(f"Trying NTP server: {server}")
|
||||
response = ntp_client.request(server, timeout=2)
|
||||
utc_time = datetime.utcfromtimestamp(response.tx_time)
|
||||
print(f"Successfully got time from {server}")
|
||||
return utc_time
|
||||
except Exception as e:
|
||||
print(f"Failed to get time from {server}: {e}")
|
||||
|
||||
# If all servers fail, raise exception
|
||||
raise Exception("All NTP servers failed")
|
||||
|
||||
def main():
|
||||
try:
|
||||
bus = smbus2.SMBus(1)
|
||||
|
||||
# Test if RTC is accessible
|
||||
try:
|
||||
bus.read_byte(DS3231_ADDR)
|
||||
print("RTC module is accessible")
|
||||
except Exception as e:
|
||||
print(f"Error accessing RTC module: {e}")
|
||||
print("Please check connections and I2C configuration")
|
||||
return
|
||||
|
||||
# Get the current time from the RTC
|
||||
try:
|
||||
year, month, day, hours, minutes, seconds = read_time(bus)
|
||||
# Create datetime object with validation to handle invalid dates
|
||||
rtc_time = datetime(year, month, day, hours, minutes, seconds)
|
||||
print(f"Actual RTC Time : {rtc_time.strftime('%Y-%m-%d %H:%M:%S')}")
|
||||
except ValueError as e:
|
||||
print(f"Invalid date/time read from RTC: {e}")
|
||||
print("Will proceed with setting RTC from internet time")
|
||||
rtc_time = None
|
||||
|
||||
# Get current UTC time from an NTP server
|
||||
try:
|
||||
@@ -79,19 +149,35 @@ def main():
|
||||
print(f"Time from Internet (UTC) : {internet_utc_time.strftime('%Y-%m-%d %H:%M:%S')}")
|
||||
except Exception as e:
|
||||
print(f"Error retrieving time from the internet: {e}")
|
||||
if rtc_time is None:
|
||||
print("Cannot proceed without either valid RTC time or internet time")
|
||||
return
|
||||
print("Will keep current RTC time")
|
||||
return
|
||||
|
||||
# Print current RTC time
|
||||
print(f"Actual RTC Time : {rtc_time.strftime('%Y-%m-%d %H:%M:%S')}")
|
||||
|
||||
# Set the RTC to UTC time
|
||||
print("Setting RTC to internet time...")
|
||||
set_time(bus, internet_utc_time.year, internet_utc_time.month, internet_utc_time.day,
|
||||
internet_utc_time.hour, internet_utc_time.minute, internet_utc_time.second)
|
||||
|
||||
# Read and print the new time from RTC
|
||||
print("Reading back new RTC time...")
|
||||
year, month, day, hour, minute, second = read_time(bus)
|
||||
rtc_time_new = datetime(year, month, day, hour, minute, second)
|
||||
print(f"New RTC Time (UTC) : {rtc_time_new.strftime('%Y-%m-%d %H:%M:%S')}")
|
||||
|
||||
# Calculate difference to verify accuracy
|
||||
time_diff = abs((rtc_time_new - internet_utc_time).total_seconds())
|
||||
print(f"Time difference : {time_diff:.2f} seconds")
|
||||
|
||||
if time_diff > 5:
|
||||
print("Warning: RTC time differs significantly from internet time")
|
||||
print("You may need to retry or check RTC module")
|
||||
else:
|
||||
print("RTC successfully synchronized with internet time")
|
||||
|
||||
except Exception as e:
|
||||
print(f"Unexpected error: {e}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,5 +1,11 @@
|
||||
"""
|
||||
Script to set the RTC using the browser time.
|
||||
____ _____ ____
|
||||
| _ \_ _/ ___|
|
||||
| |_) || || |
|
||||
| _ < | || |___
|
||||
|_| \_\|_| \____|
|
||||
|
||||
Script to set the RTC using the browser time (script used by the web UI).
|
||||
|
||||
/usr/bin/python3 /var/www/nebuleair_pro_4g/RTC/set_with_browserTime.py '2024-01-30 12:48:39'
|
||||
|
||||
|
||||
166
SARA/R5/setPDP.py
Normal file
166
SARA/R5/setPDP.py
Normal file
@@ -0,0 +1,166 @@
|
||||
'''
|
||||
____ _ ____ _
|
||||
/ ___| / \ | _ \ / \
|
||||
\___ \ / _ \ | |_) | / _ \
|
||||
___) / ___ \| _ < / ___ \
|
||||
|____/_/ \_\_| \_\/_/ \_\
|
||||
|
||||
Script to set the PDP context for the SARA R5
|
||||
|
||||
|
||||
/usr/bin/python3 /var/www/nebuleair_pro_4g/SARA/R5/setPDP.py
|
||||
|
||||
'''
|
||||
import serial
|
||||
import time
|
||||
import sys
|
||||
import json
|
||||
import re
|
||||
|
||||
#get data from config
|
||||
def load_config(config_file):
|
||||
try:
|
||||
with open(config_file, 'r') as file:
|
||||
config_data = json.load(file)
|
||||
return config_data
|
||||
except Exception as e:
|
||||
print(f"Error loading config file: {e}")
|
||||
return {}
|
||||
|
||||
#Fonction pour mettre à jour le JSON de configuration
|
||||
def update_json_key(file_path, key, value):
|
||||
"""
|
||||
Updates a specific key in a JSON file with a new value.
|
||||
|
||||
:param file_path: Path to the JSON file.
|
||||
:param key: The key to update in the JSON file.
|
||||
:param value: The new value to assign to the key.
|
||||
"""
|
||||
try:
|
||||
# Load the existing data
|
||||
with open(file_path, "r") as file:
|
||||
data = json.load(file)
|
||||
|
||||
# Check if the key exists in the JSON file
|
||||
if key in data:
|
||||
data[key] = value # Update the key with the new value
|
||||
else:
|
||||
print(f"Key '{key}' not found in the JSON file.")
|
||||
return
|
||||
|
||||
# Write the updated data back to the file
|
||||
with open(file_path, "w") as file:
|
||||
json.dump(data, file, indent=2) # Use indent for pretty printing
|
||||
|
||||
print(f"💾 updating '{key}' to '{value}'.")
|
||||
except Exception as e:
|
||||
print(f"Error updating the JSON file: {e}")
|
||||
|
||||
# Define the config file path
|
||||
config_file = '/var/www/nebuleair_pro_4g/config.json'
|
||||
# Load the configuration data
|
||||
config = load_config(config_file)
|
||||
baudrate = config.get('SaraR4_baudrate', 115200) #baudrate du sara R4
|
||||
device_id = config.get('deviceID', '').upper() #device ID en maj
|
||||
|
||||
ser_sara = serial.Serial(
|
||||
port='/dev/ttyAMA2',
|
||||
baudrate=baudrate, #115200 ou 9600
|
||||
parity=serial.PARITY_NONE, #PARITY_NONE, PARITY_EVEN or PARITY_ODD
|
||||
stopbits=serial.STOPBITS_ONE,
|
||||
bytesize=serial.EIGHTBITS,
|
||||
timeout = 2
|
||||
)
|
||||
|
||||
def read_complete_response(serial_connection, timeout=2, end_of_response_timeout=2, wait_for_lines=None, debug=True):
|
||||
'''
|
||||
Fonction très importante !!!
|
||||
Reads the complete response from a serial connection and waits for specific lines.
|
||||
'''
|
||||
if wait_for_lines is None:
|
||||
wait_for_lines = [] # Default to an empty list if not provided
|
||||
|
||||
response = bytearray()
|
||||
serial_connection.timeout = timeout
|
||||
end_time = time.time() + end_of_response_timeout
|
||||
start_time = time.time()
|
||||
|
||||
while True:
|
||||
elapsed_time = time.time() - start_time # Time since function start
|
||||
if serial_connection.in_waiting > 0:
|
||||
data = serial_connection.read(serial_connection.in_waiting)
|
||||
response.extend(data)
|
||||
end_time = time.time() + end_of_response_timeout # Reset timeout on new data
|
||||
|
||||
# Decode and check for any target line
|
||||
decoded_response = response.decode('utf-8', errors='replace')
|
||||
for target_line in wait_for_lines:
|
||||
if target_line in decoded_response:
|
||||
if debug:
|
||||
print(f"[DEBUG] 🔎 Found target line: {target_line} (in {elapsed_time:.2f}s)")
|
||||
return decoded_response # Return response immediately if a target line is found
|
||||
elif time.time() > end_time:
|
||||
if debug:
|
||||
print(f"[DEBUG] Timeout reached. No more data received.")
|
||||
break
|
||||
time.sleep(0.1) # Short sleep to prevent busy waiting
|
||||
|
||||
# Final response and debug output
|
||||
total_elapsed_time = time.time() - start_time
|
||||
if debug:
|
||||
print(f"[DEBUG] ⏱️ elapsed time: {total_elapsed_time:.2f}s. ⏱️")
|
||||
# Check if the elapsed time exceeded 10 seconds
|
||||
if total_elapsed_time > 10 and debug:
|
||||
print(f"[ALERT] 🚨 The operation took too long 🚨")
|
||||
print(f'<span style="color: red;font-weight: bold;">[ALERT] ⚠️{total_elapsed_time:.2f}s⚠️</span>')
|
||||
|
||||
return response.decode('utf-8', errors='replace') # Return the full response if no target line is found
|
||||
|
||||
try:
|
||||
print('Start script')
|
||||
|
||||
# 1. Check connection
|
||||
print('➡️Check SARA R5 connexion')
|
||||
command = f'ATI0\r'
|
||||
ser_sara.write(command.encode('utf-8'))
|
||||
response_SARA_1 = read_complete_response(ser_sara, wait_for_lines=["OK"])
|
||||
print(response_SARA_1, end="")
|
||||
time.sleep(1)
|
||||
|
||||
# 2. Activate PDP context 1
|
||||
print('➡️Activate PDP context 1')
|
||||
command = f'AT+CGACT=1,1\r'
|
||||
ser_sara.write(command.encode('utf-8'))
|
||||
response_SARA_2 = read_complete_response(ser_sara, wait_for_lines=["OK"])
|
||||
print(response_SARA_2, end="")
|
||||
time.sleep(1)
|
||||
|
||||
# 2. Set the PDP type
|
||||
print('➡️Set the PDP type to IPv4 referring to the outputof the +CGDCONT read command')
|
||||
command = f'AT+UPSD=0,0,0\r'
|
||||
ser_sara.write(command.encode('utf-8'))
|
||||
response_SARA_3 = read_complete_response(ser_sara, wait_for_lines=["OK"])
|
||||
print(response_SARA_3, end="")
|
||||
time.sleep(1)
|
||||
|
||||
# 2. Profile #0 is mapped on CID=1.
|
||||
print('➡️Profile #0 is mapped on CID=1.')
|
||||
command = f'AT+UPSD=0,100,1\r'
|
||||
ser_sara.write(command.encode('utf-8'))
|
||||
response_SARA_3 = read_complete_response(ser_sara, wait_for_lines=["OK"])
|
||||
print(response_SARA_3, end="")
|
||||
time.sleep(1)
|
||||
|
||||
# 2. Set the PDP type
|
||||
print('➡️Activate the PSD profile #0: the IPv4 address is already assigned by the network.')
|
||||
command = f'AT+UPSDA=0,3\r'
|
||||
ser_sara.write(command.encode('utf-8'))
|
||||
response_SARA_3 = read_complete_response(ser_sara, wait_for_lines=["OK","+UUPSDA"])
|
||||
print(response_SARA_3, end="")
|
||||
time.sleep(1)
|
||||
|
||||
|
||||
|
||||
except Exception as e:
|
||||
print("An error occurred:", e)
|
||||
traceback.print_exc() # This prints the full traceback
|
||||
@@ -25,38 +25,35 @@ url = parameter[1] # ex: data.mobileair.fr
|
||||
|
||||
profile_id = 3
|
||||
|
||||
#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 {}
|
||||
baudrate = 115200
|
||||
send_uSpot = False
|
||||
|
||||
# Define the config file path
|
||||
config_file = '/var/www/nebuleair_pro_4g/config.json'
|
||||
# Load the configuration data
|
||||
config = load_config(config_file)
|
||||
# Access the shared variables
|
||||
baudrate = config.get('SaraR4_baudrate', 115200)
|
||||
send_uSpot = config.get('send_uSpot', False)
|
||||
|
||||
def read_complete_response(serial_connection, timeout=2, end_of_response_timeout=2):
|
||||
def read_complete_response(serial_connection, timeout=2, end_of_response_timeout=2, wait_for_line=None):
|
||||
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 the specific line
|
||||
if wait_for_line:
|
||||
decoded_response = response.decode('utf-8', errors='replace')
|
||||
if wait_for_line in decoded_response:
|
||||
print(f"[DEBUG] 🔎Found target line: {wait_for_line}")
|
||||
break
|
||||
elif time.time() > end_time:
|
||||
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
|
||||
print(f"[DEBUG] ⏱️ elapsed time: {total_elapsed_time:.2f}s. ⏱️")
|
||||
return response.decode('utf-8', errors='replace')
|
||||
|
||||
ser_sara = serial.Serial(
|
||||
@@ -97,7 +94,8 @@ try:
|
||||
print("Trigger POST REQUEST")
|
||||
command = f'AT+UHTTPC={profile_id},1,"/pro_4G/test.php","http.resp"\r'
|
||||
ser_sara.write((command + '\r').encode('utf-8'))
|
||||
response_SARA_6 = read_complete_response(ser_sara)
|
||||
response_SARA_6 = read_complete_response(ser_sara, timeout=5, end_of_response_timeout=50, wait_for_line="+UUHTTPCR")
|
||||
|
||||
print(response_SARA_6)
|
||||
time.sleep(1)
|
||||
|
||||
|
||||
@@ -26,23 +26,8 @@ url = parameter[1] # ex: data.mobileair.fr
|
||||
|
||||
profile_id = 3
|
||||
|
||||
#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)
|
||||
send_uSpot = config.get('send_uSpot', False)
|
||||
baudrate = 115200
|
||||
send_uSpot = False
|
||||
|
||||
def read_complete_response(serial_connection, timeout=2, end_of_response_timeout=2):
|
||||
response = bytearray()
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
Script to set the URL for a HTTP request and trigger the POST Request
|
||||
Ex:
|
||||
/usr/bin/python3 /var/www/nebuleair_pro_4g/SARA/SSL/full_test_HTTPS_POST.py ttyAMA2 api-prod.uspot.probesys.net /nebuleair?token=2AFF6dQk68daFZ
|
||||
/usr/bin/python3 /var/www/nebuleair_pro_4g/SARA/SSL/full_test_HTTPS_POST.py ttyAMA2 webhook.site /13502b8b-201a-41ea-ae33-983516074de5
|
||||
/usr/bin/python3 /var/www/nebuleair_pro_4g/SARA/SSL/full_test_HTTPS_POST.py ttyAMA2 webhook.site /0904d7b1-2558-43b9-8b35-df5bc40df967
|
||||
/usr/bin/python3 /var/www/nebuleair_pro_4g/SARA/SSL/full_test_HTTPS_POST.py ttyAMA2 aircarto.fr /tests/test.php
|
||||
/usr/bin/python3 /var/www/nebuleair_pro_4g/SARA/SSL/full_test_HTTPS_POST.py ttyAMA2 ssl.aircarto.fr /test.php
|
||||
|
||||
@@ -28,23 +28,8 @@ url = parameter[1] # ex: data.mobileair.fr
|
||||
endpoint = parameter[2]
|
||||
profile_id = 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)
|
||||
send_uSpot = config.get('send_uSpot', False)
|
||||
baudrate = 115200
|
||||
send_uSpot = False
|
||||
|
||||
def color_text(text, color):
|
||||
colors = {
|
||||
@@ -100,15 +85,15 @@ ser_sara = serial.Serial(
|
||||
try:
|
||||
#step 1: import the certificate
|
||||
print("****")
|
||||
with open("/var/www/nebuleair_pro_4g/SARA/SSL/certificate/e6.der", "rb") as cert_file:
|
||||
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,"e6",{size_of_string}\r'
|
||||
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)
|
||||
@@ -149,15 +134,15 @@ try:
|
||||
# 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("\033[0;33mSet the security profile (params)\033[0m")
|
||||
minimum_SSL_version = 3
|
||||
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_line="OK")
|
||||
print(response_SARA_5bb)
|
||||
time.sleep(0.5)
|
||||
|
||||
#op_code: 2 -> cipher suite
|
||||
# 0 (factory-programmed value): (0x0000) Automatic the cipher suite will be negotiated in the handshake process
|
||||
#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("\033[0;33mSet cipher \033[0m")
|
||||
cipher_suite = 0
|
||||
command = f'AT+USECPRF={security_profile_id},2,{cipher_suite}\r'
|
||||
@@ -168,7 +153,7 @@ try:
|
||||
|
||||
# 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,"e6"\r'
|
||||
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_line="OK")
|
||||
print(response_SARA_5c)
|
||||
@@ -290,7 +275,7 @@ try:
|
||||
# Wait for the +UUHTTPCR response
|
||||
print("Waiting for +UUHTTPCR response...")
|
||||
|
||||
response_SARA_3 = read_complete_response(ser_sara, timeout=5, end_of_response_timeout=30, wait_for_line="+UUHTTPCR")
|
||||
response_SARA_3 = read_complete_response(ser_sara, timeout=5, end_of_response_timeout=50, wait_for_line="+UUHTTPCR")
|
||||
|
||||
print("\033[0;34m")
|
||||
print(response_SARA_3)
|
||||
@@ -326,7 +311,7 @@ try:
|
||||
print(response_SARA_8)
|
||||
|
||||
# Get error code
|
||||
print("\033[0;33mEmpty Memory\033[0m")
|
||||
print("\033[0;33mGet error code\033[0m")
|
||||
command = f'AT+UHTTPER={profile_id}\r'
|
||||
ser_sara.write(command.encode('utf-8'))
|
||||
response_SARA_9 = read_complete_response(ser_sara, wait_for_line="OK")
|
||||
@@ -340,9 +325,10 @@ try:
|
||||
3 HTTP Protocol error class
|
||||
10 Wrong HTTP API USAGE
|
||||
|
||||
error_code (for error_class 3)
|
||||
error_code (for error_class 3 or 10)
|
||||
0 No error
|
||||
11 Server connection error
|
||||
22 PSD or CSD connection not established
|
||||
73 Secure socket connect error
|
||||
'''
|
||||
|
||||
|
||||
@@ -31,23 +31,8 @@ endpoint = parameter[2]
|
||||
|
||||
profile_id = 3
|
||||
|
||||
#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)
|
||||
send_uSpot = config.get('send_uSpot', False)
|
||||
baudrate = 115200
|
||||
send_uSpot = False
|
||||
|
||||
def read_complete_response(serial_connection, timeout=2, end_of_response_timeout=2):
|
||||
response = bytearray()
|
||||
|
||||
@@ -21,23 +21,8 @@ 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)
|
||||
send_uSpot = config.get('send_uSpot', False)
|
||||
baudrate = 115200
|
||||
send_uSpot = False
|
||||
|
||||
def read_complete_response(serial_connection, timeout=2, end_of_response_timeout=2):
|
||||
response = bytearray()
|
||||
|
||||
@@ -23,24 +23,8 @@ parameter = sys.argv[1:] # Exclude the script name
|
||||
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)
|
||||
send_uSpot = config.get('send_uSpot', False)
|
||||
baudrate = 115200
|
||||
send_uSpot = False
|
||||
|
||||
def read_complete_response(serial_connection, timeout=2, end_of_response_timeout=2):
|
||||
response = bytearray()
|
||||
|
||||
@@ -14,19 +14,7 @@ parameter = sys.argv[1:] # Exclude the script name
|
||||
port = '/dev/' + parameter[0] # e.g., ttyAMA2
|
||||
timeout = float(parameter[1]) # e.g., 2 seconds
|
||||
|
||||
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'
|
||||
config = load_config(config_file)
|
||||
baudrate = config.get('SaraR4_baudrate', 115200)
|
||||
baudrate = 115200
|
||||
|
||||
def read_complete_response(serial_connection, timeout=2, end_of_response_timeout=2):
|
||||
response = bytearray()
|
||||
|
||||
109
SARA/cellLocate/get_loc.py
Normal file
109
SARA/cellLocate/get_loc.py
Normal file
@@ -0,0 +1,109 @@
|
||||
'''
|
||||
____ _ ____ _
|
||||
/ ___| / \ | _ \ / \
|
||||
\___ \ / _ \ | |_) | / _ \
|
||||
___) / ___ \| _ < / ___ \
|
||||
|____/_/ \_\_| \_\/_/ \_\
|
||||
|
||||
Script to get Location from GSM
|
||||
|
||||
/usr/bin/python3 /var/www/nebuleair_pro_4g/SARA/cellLocate/get_loc.py ttyAMA2 1
|
||||
|
||||
AT+ULOC=
|
||||
<mode>, ->2 -> single shot position
|
||||
<sensor>, ->2 -> use cellulare CellLocate
|
||||
<response_type>, ->0 -> standard
|
||||
<timeout>, ->2 -> seconds
|
||||
<accuracy> ->1 -> in meters
|
||||
[,<num_hypothesis>]
|
||||
|
||||
exemple: AT+ULOC=2,2,0,2,1
|
||||
'''
|
||||
|
||||
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+ULOC=2,2,0,2,1\r'
|
||||
ser.write((command + '\r').encode('utf-8'))
|
||||
|
||||
response = read_complete_response(ser, wait_for_lines=["+UULOC"])
|
||||
print(response)
|
||||
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)
|
||||
27
SARA/check_running.py
Executable file
27
SARA/check_running.py
Executable file
@@ -0,0 +1,27 @@
|
||||
'''
|
||||
Check if the main loop is running
|
||||
/usr/bin/python3 /var/www/nebuleair_pro_4g/tests/check_running.py
|
||||
'''
|
||||
import psutil
|
||||
import json
|
||||
|
||||
def is_script_running(script_name):
|
||||
"""Check if a given Python script is currently running."""
|
||||
for process in psutil.process_iter(['pid', 'cmdline']):
|
||||
if process.info['cmdline'] and script_name in " ".join(process.info['cmdline']):
|
||||
return True # Script is running
|
||||
return False # Script is not running
|
||||
|
||||
script_to_check = "/var/www/nebuleair_pro_4g/loop/SARA_send_data_v2.py"
|
||||
|
||||
# Determine script status
|
||||
is_running = is_script_running(script_to_check)
|
||||
|
||||
# Create JSON response
|
||||
response = {
|
||||
"message": "The script is still running.❌❌❌" if is_running else "The script is NOT running.✅✅✅",
|
||||
"running": is_running
|
||||
}
|
||||
|
||||
# Print JSON output
|
||||
print(json.dumps(response, indent=4)) # Pretty print for readability
|
||||
398
SARA/reboot/start.py
Normal file
398
SARA/reboot/start.py
Normal file
@@ -0,0 +1,398 @@
|
||||
'''
|
||||
____ _ ____ _
|
||||
/ ___| / \ | _ \ / \
|
||||
\___ \ / _ \ | |_) | / _ \
|
||||
___) / ___ \| _ < / ___ \
|
||||
|____/_/ \_\_| \_\/_/ \_\
|
||||
|
||||
Script that starts at the boot of the RPI (with cron)
|
||||
|
||||
@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
|
||||
|
||||
/usr/bin/python3 /var/www/nebuleair_pro_4g/SARA/reboot/start.py
|
||||
|
||||
'''
|
||||
import serial
|
||||
import RPi.GPIO as GPIO
|
||||
import time
|
||||
import sys
|
||||
import json
|
||||
import re
|
||||
import sqlite3
|
||||
|
||||
#GPIO
|
||||
SARA_power_GPIO = 16
|
||||
SARA_ON_GPIO = 20
|
||||
|
||||
GPIO.setmode(GPIO.BCM) # Use BCM numbering
|
||||
GPIO.setup(SARA_power_GPIO, GPIO.OUT) # Set GPIO17 as an output
|
||||
|
||||
# database connection
|
||||
conn = sqlite3.connect("/var/www/nebuleair_pro_4g/sqlite/sensors.db")
|
||||
cursor = conn.cursor()
|
||||
|
||||
#get config data from SQLite table
|
||||
def load_config_sqlite():
|
||||
"""
|
||||
Load configuration data from SQLite config table
|
||||
|
||||
Returns:
|
||||
dict: Configuration data with proper type conversion
|
||||
"""
|
||||
try:
|
||||
|
||||
# Query the config table
|
||||
cursor.execute("SELECT key, value, type FROM config_table")
|
||||
rows = cursor.fetchall()
|
||||
|
||||
# Create config dictionary
|
||||
config_data = {}
|
||||
for key, value, type_name in rows:
|
||||
# Convert value based on its type
|
||||
if type_name == 'bool':
|
||||
config_data[key] = value == '1' or value == 'true'
|
||||
elif type_name == 'int':
|
||||
config_data[key] = int(value)
|
||||
elif type_name == 'float':
|
||||
config_data[key] = float(value)
|
||||
else:
|
||||
config_data[key] = value
|
||||
|
||||
return config_data
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error loading config from SQLite: {e}")
|
||||
return {}
|
||||
|
||||
def update_sqlite_config(key, value):
|
||||
"""
|
||||
Updates a specific key in the SQLite config_table with a new value.
|
||||
|
||||
:param key: The key to update in the config_table.
|
||||
:param value: The new value to assign to the key.
|
||||
"""
|
||||
try:
|
||||
|
||||
# Check if the key exists and get its type
|
||||
cursor.execute("SELECT type FROM config_table WHERE key = ?", (key,))
|
||||
result = cursor.fetchone()
|
||||
|
||||
if result is None:
|
||||
print(f"Key '{key}' not found in the config_table.")
|
||||
conn.close()
|
||||
return
|
||||
|
||||
# Get the type of the value from the database
|
||||
value_type = result[0]
|
||||
|
||||
# Convert the value to the appropriate string representation based on its type
|
||||
if value_type == 'bool':
|
||||
# Convert Python boolean or string 'true'/'false' to '1'/'0'
|
||||
if isinstance(value, bool):
|
||||
str_value = '1' if value else '0'
|
||||
else:
|
||||
str_value = '1' if str(value).lower() in ('true', '1', 'yes', 'y') else '0'
|
||||
elif value_type == 'int':
|
||||
str_value = str(int(value))
|
||||
elif value_type == 'float':
|
||||
str_value = str(float(value))
|
||||
else:
|
||||
str_value = str(value)
|
||||
|
||||
# Update the value in the database
|
||||
cursor.execute("UPDATE config_table SET value = ? WHERE key = ?", (str_value, key))
|
||||
|
||||
# Commit the changes and close the connection
|
||||
conn.commit()
|
||||
|
||||
print(f"💾 Updated '{key}' to '{value}' in database.")
|
||||
except Exception as e:
|
||||
print(f"Error updating the SQLite database: {e}")
|
||||
|
||||
#Load config
|
||||
config = load_config_sqlite()
|
||||
#config
|
||||
baudrate = config.get('SaraR4_baudrate', 115200) #baudrate du sara R4
|
||||
device_id = config.get('deviceID', '').upper() #device ID en maj
|
||||
|
||||
sara_r5_DPD_setup = False
|
||||
|
||||
ser_sara = serial.Serial(
|
||||
port='/dev/ttyAMA2',
|
||||
baudrate=baudrate, #115200 ou 9600
|
||||
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('<h3>Start reboot python script</h3>')
|
||||
|
||||
#First we need to power on the module (if connected to mosfet via gpio16)
|
||||
GPIO.output(SARA_power_GPIO, GPIO.HIGH)
|
||||
time.sleep(5)
|
||||
|
||||
#check modem status
|
||||
#Attention:
|
||||
# SARA R4 response: Manufacturer: u-blox Model: SARA-R410M-02B
|
||||
# SArA R5 response: SARA-R500S-01B-00
|
||||
print("⚙️Check SARA Status")
|
||||
command = f'ATI\r'
|
||||
ser_sara.write(command.encode('utf-8'))
|
||||
response_SARA_ATI = read_complete_response(ser_sara, wait_for_lines=["IMEI"])
|
||||
print(response_SARA_ATI)
|
||||
|
||||
# Check for SARA model with more robust regex
|
||||
model = "Unknown"
|
||||
if "SARA-R410M" in response_SARA_ATI:
|
||||
model = "SARA-R410M"
|
||||
print("📱 Detected SARA R4 modem")
|
||||
elif "SARA-R500" in response_SARA_ATI:
|
||||
model = "SARA-R500"
|
||||
print("📱 Detected SARA R5 modem")
|
||||
sara_r5_DPD_setup = True
|
||||
else:
|
||||
# Fallback to regex match if direct string match fails
|
||||
match = re.search(r"Model:\s*([A-Za-z0-9\-]+)", response_SARA_ATI)
|
||||
if match:
|
||||
model = match.group(1).strip()
|
||||
else:
|
||||
model = "Unknown"
|
||||
print("⚠️ Could not identify modem model")
|
||||
|
||||
print(f"🔍 Model: {model}")
|
||||
update_sqlite_config("modem_version", model)
|
||||
time.sleep(1)
|
||||
|
||||
'''
|
||||
AIRCARTO
|
||||
'''
|
||||
# 1. Set AIRCARTO URL (profile id = 0)
|
||||
print('➡️Set aircarto URL')
|
||||
aircarto_profile_id = 0
|
||||
aircarto_url="data.nebuleair.fr"
|
||||
command = f'AT+UHTTP={aircarto_profile_id},1,"{aircarto_url}"\r'
|
||||
ser_sara.write(command.encode('utf-8'))
|
||||
response_SARA_1 = read_complete_response(ser_sara, wait_for_lines=["OK"])
|
||||
print(response_SARA_1)
|
||||
time.sleep(1)
|
||||
|
||||
'''
|
||||
uSpot
|
||||
'''
|
||||
print("➡️➡️Set uSpot URL with SSL")
|
||||
|
||||
security_profile_id = 1
|
||||
uSpot_profile_id = 1
|
||||
uSpot_url="api-prod.uspot.probesys.net"
|
||||
|
||||
|
||||
#step 1: import the certificate
|
||||
print("➡️ import certificate")
|
||||
certificate_name = "e6"
|
||||
with open("/var/www/nebuleair_pro_4g/SARA/SSL/certificate/e6.pem", "rb") as cert_file:
|
||||
certificate = cert_file.read()
|
||||
size_of_string = len(certificate)
|
||||
|
||||
# AT+USECMNG=0,<type>,<internal_name>,<data_size>
|
||||
# type-> 0 -> trusted root CA
|
||||
command = f'AT+USECMNG=0,0,"{certificate_name}",{size_of_string}\r'
|
||||
ser_sara.write((command + '\r').encode('utf-8'))
|
||||
response_SARA_1 = read_complete_response(ser_sara)
|
||||
print(response_SARA_1)
|
||||
|
||||
time.sleep(0.5)
|
||||
|
||||
print("➡️ add certificate")
|
||||
ser_sara.write(certificate)
|
||||
response_SARA_2 = read_complete_response(ser_sara)
|
||||
print(response_SARA_2)
|
||||
|
||||
time.sleep(0.5)
|
||||
|
||||
# op_code: 0 -> certificate validation level
|
||||
# param_val : 0 -> Level 0 No validation; 1-> Level 1 Root certificate validation
|
||||
print("➡️Set the security profile (params)")
|
||||
certification_level=0
|
||||
command = f'AT+USECPRF={security_profile_id},0,{certification_level}\r'
|
||||
ser_sara.write((command + '\r').encode('utf-8'))
|
||||
response_SARA_5b = read_complete_response(ser_sara, wait_for_lines=["OK"])
|
||||
print(response_SARA_5b)
|
||||
time.sleep(0.5)
|
||||
|
||||
# op_code: 1 -> minimum SSL/TLS version
|
||||
# param_val : 0 -> any; server can use any version for the connection; 1-> LSv1.0; 2->TLSv1.1; 3->TLSv1.2;
|
||||
print("➡️Set the security profile (params)")
|
||||
minimum_SSL_version = 0
|
||||
command = f'AT+USECPRF={security_profile_id},1,{minimum_SSL_version}\r'
|
||||
ser_sara.write((command + '\r').encode('utf-8'))
|
||||
response_SARA_5bb = read_complete_response(ser_sara, wait_for_lines=["OK"])
|
||||
print(response_SARA_5bb)
|
||||
time.sleep(0.5)
|
||||
|
||||
#op_code: 2 -> legacy cipher suite selection
|
||||
# 0 (factory-programmed value): a list of default cipher suites is proposed at the beginning of handshake process, and a cipher suite will be negotiated among the cipher suites proposed in the list.
|
||||
print("➡️Set cipher")
|
||||
cipher_suite = 0
|
||||
command = f'AT+USECPRF={security_profile_id},2,{cipher_suite}\r'
|
||||
ser_sara.write((command + '\r').encode('utf-8'))
|
||||
response_SARA_5cc = read_complete_response(ser_sara, wait_for_lines=["OK"])
|
||||
print(response_SARA_5cc)
|
||||
time.sleep(0.5)
|
||||
|
||||
# op_code: 3 -> trusted root certificate internal name
|
||||
print("➡️Set the security profile (choose cert)")
|
||||
command = f'AT+USECPRF={security_profile_id},3,"{certificate_name}"\r'
|
||||
ser_sara.write((command + '\r').encode('utf-8'))
|
||||
response_SARA_5c = read_complete_response(ser_sara, wait_for_lines=["OK"])
|
||||
print(response_SARA_5c)
|
||||
time.sleep(0.5)
|
||||
|
||||
# op_code: 10 -> SNI (server name indication)
|
||||
print("➡️Set the SNI")
|
||||
command = f'AT+USECPRF={security_profile_id},10,"{uSpot_url}"\r'
|
||||
ser_sara.write((command + '\r').encode('utf-8'))
|
||||
response_SARA_5cf = read_complete_response(ser_sara, wait_for_lines=["OK"])
|
||||
print(response_SARA_5cf)
|
||||
time.sleep(0.5)
|
||||
|
||||
#step 4: set url (op_code = 1)
|
||||
print("➡️SET URL")
|
||||
command = f'AT+UHTTP={uSpot_profile_id},1,"{uSpot_url}"\r'
|
||||
ser_sara.write((command + '\r').encode('utf-8'))
|
||||
response_SARA_5 = read_complete_response(ser_sara, wait_for_lines=["OK"])
|
||||
print(response_SARA_5)
|
||||
time.sleep(1)
|
||||
|
||||
#step 4: set PORT (op_code = 5)
|
||||
print("➡️SET PORT")
|
||||
port = 443
|
||||
command = f'AT+UHTTP={uSpot_profile_id},5,{port}\r'
|
||||
ser_sara.write((command + '\r').encode('utf-8'))
|
||||
response_SARA_55 = read_complete_response(ser_sara, wait_for_lines=["OK"])
|
||||
print(response_SARA_55)
|
||||
time.sleep(1)
|
||||
|
||||
#step 4: set url to SSL (op_code = 6) (http_secure = 1 for HTTPS)(USECMNG_PROFILE = 2)
|
||||
print("➡️SET SSL")
|
||||
http_secure = 1
|
||||
command = f'AT+UHTTP={uSpot_profile_id},6,{http_secure},{security_profile_id}\r'
|
||||
|
||||
ser_sara.write(command.encode('utf-8'))
|
||||
response_SARA_5fg = read_complete_response(ser_sara, wait_for_lines=["OK"])
|
||||
print(response_SARA_5fg)
|
||||
time.sleep(1)
|
||||
|
||||
|
||||
'''
|
||||
SARA R5
|
||||
'''
|
||||
|
||||
if sara_r5_DPD_setup:
|
||||
print("➡️➡️SARA R5 PDP SETUP")
|
||||
# 2. Activate PDP context 1
|
||||
print('➡️Activate PDP context 1')
|
||||
command = f'AT+CGACT=1,1\r'
|
||||
ser_sara.write(command.encode('utf-8'))
|
||||
response_SARA_2 = read_complete_response(ser_sara, wait_for_lines=["OK"])
|
||||
print(response_SARA_2, end="")
|
||||
time.sleep(1)
|
||||
|
||||
# 2. Set the PDP type
|
||||
print('➡️Set the PDP type to IPv4 referring to the outputof the +CGDCONT read command')
|
||||
command = f'AT+UPSD=0,0,0\r'
|
||||
ser_sara.write(command.encode('utf-8'))
|
||||
response_SARA_3 = read_complete_response(ser_sara, wait_for_lines=["OK"])
|
||||
print(response_SARA_3, end="")
|
||||
time.sleep(1)
|
||||
|
||||
# 2. Profile #0 is mapped on CID=1.
|
||||
print('➡️Profile #0 is mapped on CID=1.')
|
||||
command = f'AT+UPSD=0,100,1\r'
|
||||
ser_sara.write(command.encode('utf-8'))
|
||||
response_SARA_3 = read_complete_response(ser_sara, wait_for_lines=["OK"])
|
||||
print(response_SARA_3, end="")
|
||||
time.sleep(1)
|
||||
|
||||
# 2. Set the PDP type
|
||||
print('➡️Activate the PSD profile #0: the IPv4 address is already assigned by the network.')
|
||||
command = f'AT+UPSDA=0,3\r'
|
||||
ser_sara.write(command.encode('utf-8'))
|
||||
response_SARA_3 = read_complete_response(ser_sara, wait_for_lines=["OK","+UUPSDA"])
|
||||
print(response_SARA_3, end="")
|
||||
time.sleep(1)
|
||||
|
||||
|
||||
#3. Get localisation (CellLocate)
|
||||
mode = 2 #single shot position
|
||||
sensor = 2 #use cellular CellLocate® location information
|
||||
response_type = 0
|
||||
timeout_s = 2
|
||||
accuracy_m = 1
|
||||
command = f'AT+ULOC={mode},{sensor},{response_type},{timeout_s},{accuracy_m}\r'
|
||||
ser_sara.write((command + '\r').encode('utf-8'))
|
||||
response_SARA_3 = read_complete_response(ser_sara, wait_for_lines=["+UULOC"])
|
||||
print(response_SARA_3)
|
||||
|
||||
match = re.search(r"\+UULOC: \d{2}/\d{2}/\d{4},\d{2}:\d{2}:\d{2}\.\d{3},([-+]?\d+\.\d+),([-+]?\d+\.\d+)", response_SARA_3)
|
||||
if match:
|
||||
latitude = match.group(1)
|
||||
longitude = match.group(2)
|
||||
print(f"📍 Latitude: {latitude}, Longitude: {longitude}")
|
||||
else:
|
||||
print("❌ Failed to extract coordinates.")
|
||||
|
||||
#update sqlite table
|
||||
update_sqlite_config("latitude_raw", float(latitude))
|
||||
update_sqlite_config("longitude_raw", float(longitude))
|
||||
|
||||
time.sleep(1)
|
||||
|
||||
except Exception as e:
|
||||
print("An error occurred:", e)
|
||||
traceback.print_exc() # This prints the full traceback
|
||||
90
SARA/sara.py
90
SARA/sara.py
@@ -1,11 +1,23 @@
|
||||
'''
|
||||
____ _ ____ _
|
||||
/ ___| / \ | _ \ / \
|
||||
\___ \ / _ \ | |_) | / _ \
|
||||
___) / ___ \| _ < / ___ \
|
||||
|____/_/ \_\_| \_\/_/ \_\
|
||||
|
||||
Script to see if the SARA-R410 is running
|
||||
ex:
|
||||
python3 /var/www/nebuleair_pro_4g/SARA/sara.py ttyAMA2 ATI 2
|
||||
ex 1 (get SIM infos)
|
||||
python3 /var/www/nebuleair_pro_4g/SARA/sara.py ttyAMA2 AT+CCID? 2
|
||||
ex 2 (turn on blue light):
|
||||
python3 /var/www/nebuleair_pro_4g/SARA/sara.py ttyAMA2 AT+UGPIOC=16,2 2
|
||||
ex 3 (reconnect network)
|
||||
python3 /var/www/nebuleair_pro_4g/SARA/sara.py ttyAMA2 AT+COPS=1,2,20801 20
|
||||
ex 4 (get HTTP Profiles)
|
||||
python3 /var/www/nebuleair_pro_4g/SARA/sara.py ttyAMA2 AT+UHTTP? 2
|
||||
ex 5 (get IP addr)
|
||||
python3 /var/www/nebuleair_pro_4g/SARA/sara.py ttyAMA2 AT+CGPADDR=1 2
|
||||
|
||||
'''
|
||||
|
||||
@@ -20,68 +32,64 @@ port='/dev/'+parameter[0] # ex: ttyAMA2
|
||||
command = parameter[1] # ex: AT+CCID?
|
||||
timeout = float(parameter[2]) # ex:2
|
||||
|
||||
#get baudrate
|
||||
def load_config(config_file):
|
||||
try:
|
||||
with open(config_file, 'r') as file:
|
||||
config_data = json.load(file)
|
||||
return config_data
|
||||
except Exception as e:
|
||||
print(f"Error loading config file: {e}")
|
||||
return {}
|
||||
|
||||
# Define the config file path
|
||||
config_file = '/var/www/nebuleair_pro_4g/config.json'
|
||||
# Load the configuration data
|
||||
config = load_config(config_file)
|
||||
# Access the shared variables
|
||||
baudrate = config.get('SaraR4_baudrate', 115200)
|
||||
baudrate = 115200
|
||||
|
||||
ser = serial.Serial(
|
||||
try:
|
||||
|
||||
ser = serial.Serial(
|
||||
port=port, #USB0 or ttyS0
|
||||
baudrate=baudrate, #115200 ou 9600
|
||||
parity=serial.PARITY_NONE, #PARITY_NONE, PARITY_EVEN or PARITY_ODD
|
||||
stopbits=serial.STOPBITS_ONE,
|
||||
bytesize=serial.EIGHTBITS,
|
||||
timeout = timeout
|
||||
)
|
||||
)
|
||||
|
||||
ser.write((command + '\r').encode('utf-8'))
|
||||
ser.write((command + '\r').encode('utf-8'))
|
||||
|
||||
#ser.write(b'ATI\r') #General Information
|
||||
#ser.write(b'AT+CCID?\r') #SIM card number
|
||||
#ser.write(b'AT+CPIN?\r') #Check the status of the SIM card
|
||||
#ser.write(b'AT+CIND?\r') #Indication state (last number is SIM detection: 0 no SIM detection, 1 SIM detected, 2 not available)
|
||||
#ser.write(b'AT+UGPIOR=?\r') #Reads the current value of the specified GPIO pin
|
||||
#ser.write(b'AT+UGPIOC?\r') #GPIO select configuration
|
||||
#ser.write(b'AT+COPS=?\r') #Check the network and cellular technology the modem is currently using
|
||||
#ser.write(b'AT+COPS=1,2,20801') #connext to orange
|
||||
#ser.write(b'AT+CFUN=?\r') #Selects/read the level of functionality
|
||||
#ser.write(b'AT+URAT=?\r') #Radio Access Technology
|
||||
#ser.write(b'AT+USIMSTAT?')
|
||||
#ser.write(b'AT+IPR=115200') #Check/Define baud rate
|
||||
#ser.write(b'AT+CMUX=?')
|
||||
#ser.write(b'ATI\r') #General Information
|
||||
#ser.write(b'AT+CCID?\r') #SIM card number
|
||||
#ser.write(b'AT+CPIN?\r') #Check the status of the SIM card
|
||||
#ser.write(b'AT+CIND?\r') #Indication state (last number is SIM detection: 0 no SIM detection, 1 SIM detected, 2 not available)
|
||||
#ser.write(b'AT+UGPIOR=?\r') #Reads the current value of the specified GPIO pin
|
||||
#ser.write(b'AT+UGPIOC?\r') #GPIO select configuration
|
||||
#ser.write(b'AT+COPS=?\r') #Check the network and cellular technology the modem is currently using
|
||||
#ser.write(b'AT+COPS=1,2,20801') #connext to orange
|
||||
#ser.write(b'AT+CFUN=?\r') #Selects/read the level of functionality
|
||||
#ser.write(b'AT+URAT=?\r') #Radio Access Technology
|
||||
#ser.write(b'AT+USIMSTAT?')
|
||||
#ser.write(b'AT+IPR=115200') #Check/Define baud rate
|
||||
#ser.write(b'AT+CMUX=?')
|
||||
|
||||
|
||||
|
||||
try:
|
||||
# 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
|
||||
start_time = time.time()
|
||||
|
||||
while (time.time() - start_time) < timeout:
|
||||
line = ser.readline().decode('utf-8', errors='ignore').strip()
|
||||
if line:
|
||||
response_lines.append(line)
|
||||
|
||||
# Check if we received any data
|
||||
if not response_lines:
|
||||
print(f"ERROR: No response received from {port} after sending command: {command}")
|
||||
sys.exit(1)
|
||||
|
||||
# Print the response
|
||||
for line in response_lines:
|
||||
print(line)
|
||||
|
||||
except serial.SerialException as e:
|
||||
print(f"Error: {e}")
|
||||
|
||||
print(f"ERROR: Serial communication error: {e}")
|
||||
sys.exit(1)
|
||||
except Exception as e:
|
||||
print(f"ERROR: Unexpected error: {e}")
|
||||
sys.exit(1)
|
||||
finally:
|
||||
if ser.is_open:
|
||||
# Close the serial port if it's open
|
||||
if 'ser' in locals() and ser.is_open:
|
||||
ser.close()
|
||||
#print("Serial closed")
|
||||
|
||||
|
||||
|
||||
63
SARA/sara_checkDNS.py
Normal file
63
SARA/sara_checkDNS.py
Normal file
@@ -0,0 +1,63 @@
|
||||
'''
|
||||
____ _ ____ _
|
||||
/ ___| / \ | _ \ / \
|
||||
\___ \ / _ \ | |_) | / _ \
|
||||
___) / ___ \| _ < / ___ \
|
||||
|____/_/ \_\_| \_\/_/ \_\
|
||||
|
||||
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
|
||||
|
||||
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")
|
||||
|
||||
@@ -1,11 +1,18 @@
|
||||
'''
|
||||
____ _ ____ _
|
||||
/ ___| / \ | _ \ / \
|
||||
\___ \ / _ \ | |_) | / _ \
|
||||
___) / ___ \| _ < / ___ \
|
||||
|____/_/ \_\_| \_\/_/ \_\
|
||||
|
||||
Script to connect SARA-R410 to network SARA-R410
|
||||
python3 /var/www/nebuleair_pro_4g/SARA/sara_connectNetwork.py ttyAMA2 20801 10
|
||||
|
||||
AT+COPS=1,2,20801
|
||||
mode->1 pour manual
|
||||
format->2 pour numeric
|
||||
operator->20801 pour orange
|
||||
operator->20801 pour orange, 20810 pour SFR
|
||||
|
||||
'''
|
||||
|
||||
import serial
|
||||
@@ -19,22 +26,54 @@ networkID = parameter[1] # ex: 20801
|
||||
timeout = float(parameter[2]) # ex:2
|
||||
|
||||
|
||||
#get baudrate
|
||||
def load_config(config_file):
|
||||
try:
|
||||
with open(config_file, 'r') as file:
|
||||
config_data = json.load(file)
|
||||
return config_data
|
||||
except Exception as e:
|
||||
print(f"Error loading config file: {e}")
|
||||
return {}
|
||||
|
||||
# Define the config file path
|
||||
config_file = '/var/www/nebuleair_pro_4g/config.json'
|
||||
# Load the configuration data
|
||||
config = load_config(config_file)
|
||||
# Access the shared variables
|
||||
baudrate = config.get('SaraR4_baudrate', 115200)
|
||||
|
||||
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
|
||||
|
||||
|
||||
baudrate = 115200
|
||||
|
||||
ser = serial.Serial(
|
||||
port=port, #USB0 or ttyS0
|
||||
@@ -50,17 +89,11 @@ ser.write((command + '\r').encode('utf-8'))
|
||||
|
||||
|
||||
try:
|
||||
# Read lines until a timeout occurs
|
||||
response_lines = []
|
||||
while True:
|
||||
line = ser.readline().decode('utf-8').strip()
|
||||
if not line:
|
||||
break # Break the loop if an empty line is encountered
|
||||
response_lines.append(line)
|
||||
response = read_complete_response(ser, wait_for_lines=["OK", "ERROR"],timeout=5, end_of_response_timeout=120, debug=True)
|
||||
|
||||
# Print the response
|
||||
for line in response_lines:
|
||||
print(line)
|
||||
print('<p class="text-danger-emphasis">')
|
||||
print(response)
|
||||
print("</p>", end="")
|
||||
|
||||
except serial.SerialException as e:
|
||||
print(f"Error: {e}")
|
||||
|
||||
@@ -11,22 +11,7 @@ parameter = sys.argv[1:] # Exclude the script name
|
||||
port='/dev/'+parameter[0] # ex: ttyAMA2
|
||||
message = parameter[1] # ex: Hello
|
||||
|
||||
#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)
|
||||
baudrate = 115200
|
||||
|
||||
ser = serial.Serial(
|
||||
port=port, #USB0 or ttyS0
|
||||
|
||||
58
SARA/sara_google_ping.py
Normal file
58
SARA/sara_google_ping.py
Normal file
@@ -0,0 +1,58 @@
|
||||
'''
|
||||
____ _ ____ _
|
||||
/ ___| / \ | _ \ / \
|
||||
\___ \ / _ \ | |_) | / _ \
|
||||
___) / ___ \| _ < / ___ \
|
||||
|____/_/ \_\_| \_\/_/ \_\
|
||||
|
||||
Script to set the URL for a HTTP request
|
||||
Ex:
|
||||
/usr/bin/python3 /var/www/nebuleair_pro_4g/SARA/sara_google_ping.py
|
||||
|
||||
|
||||
'''
|
||||
|
||||
import serial
|
||||
import time
|
||||
import sys
|
||||
import json
|
||||
|
||||
|
||||
baudrate = 115200
|
||||
|
||||
ser = 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
|
||||
)
|
||||
url="www.google.com"
|
||||
command = f'AT+UPING="{url}"\r'
|
||||
ser.write((command + '\r').encode('utf-8'))
|
||||
|
||||
|
||||
|
||||
try:
|
||||
# Read lines until a timeout occurs
|
||||
response_lines = []
|
||||
while True:
|
||||
line = ser.readline().decode('utf-8').strip()
|
||||
if not line:
|
||||
break # Break the loop if an empty line is encountered
|
||||
response_lines.append(line)
|
||||
|
||||
# Print the response
|
||||
for line in response_lines:
|
||||
print(line)
|
||||
|
||||
except serial.SerialException as e:
|
||||
print(f"Error: {e}")
|
||||
|
||||
finally:
|
||||
if ser.is_open:
|
||||
ser.close()
|
||||
print("****")
|
||||
#print("Serial closed")
|
||||
|
||||
165
SARA/sara_ping.py
Normal file
165
SARA/sara_ping.py
Normal file
@@ -0,0 +1,165 @@
|
||||
'''
|
||||
____ _ ____ _
|
||||
/ ___| / \ | _ \ / \
|
||||
\___ \ / _ \ | |_) | / _ \
|
||||
___) / ___ \| _ < / ___ \
|
||||
|____/_/ \_\_| \_\/_/ \_\
|
||||
|
||||
Script to do a ping request to data.nebuleair.fr/ping.php
|
||||
python3 /var/www/nebuleair_pro_4g/SARA/sara_ping.py
|
||||
'''
|
||||
|
||||
import serial
|
||||
import time
|
||||
import sys
|
||||
import json
|
||||
|
||||
# SARA R4 UHTTPC profile IDs
|
||||
aircarto_profile_id = 0
|
||||
|
||||
baudrate = 115200
|
||||
|
||||
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
|
||||
|
||||
|
||||
def extract_error_code(response):
|
||||
"""
|
||||
Extract just the error code from AT+UHTTPER response
|
||||
"""
|
||||
for line in response.split('\n'):
|
||||
if '+UHTTPER' in line:
|
||||
try:
|
||||
# Split the line and get the third value (error code)
|
||||
parts = line.split(':')[1].strip().split(',')
|
||||
if len(parts) >= 3:
|
||||
error_code = int(parts[2])
|
||||
return error_code
|
||||
except:
|
||||
pass
|
||||
|
||||
# Return None if we couldn't find the error code
|
||||
return None
|
||||
|
||||
try:
|
||||
#3. Send to endpoint (with device ID)
|
||||
print("Send data (GET REQUEST):")
|
||||
command= f'AT+UHTTPC={aircarto_profile_id},1,"/ping.php","aircarto_server_response.txt"\r'
|
||||
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)
|
||||
|
||||
print(response_SARA_3)
|
||||
# si on recoit la réponse UHTTPCR
|
||||
if "+UUHTTPCR" in response_SARA_3:
|
||||
print("✅ Received +UUHTTPCR response.")
|
||||
# Split response into lines
|
||||
lines = response_SARA_3.strip().splitlines()
|
||||
# 1.Vérifier si la réponse contient un message d'erreur CME
|
||||
if "+CME ERROR" in lines[-1]:
|
||||
print("error ⛔")
|
||||
else:
|
||||
http_response = lines[-1] # "+UUHTTPCR: 0,4,0"
|
||||
parts = http_response.split(',')
|
||||
# 2.1 code 0 (HTTP failed) ⛔⛔⛔
|
||||
if len(parts) == 3 and parts[-1] == '0': # The third value indicates success
|
||||
print("⛔⛔ATTENTION: HTTP operation failed")
|
||||
#get error code
|
||||
print("Getting error code (11->Server connection error, 73->Secure socket connect error)")
|
||||
command = f'AT+UHTTPER={aircarto_profile_id}\r'
|
||||
ser_sara.write(command.encode('utf-8'))
|
||||
response_SARA_9 = read_complete_response(ser_sara, wait_for_lines=["OK"], debug=False)
|
||||
print('<p class="text-danger-emphasis">')
|
||||
print(response_SARA_9)
|
||||
print("</p>", end="")
|
||||
# Extract just the error code
|
||||
error_code = extract_error_code(response_SARA_9)
|
||||
if error_code is not None:
|
||||
# Display interpretation based on error code
|
||||
if error_code == 0:
|
||||
print('<p class="text-success">No error detected</p>')
|
||||
elif error_code == 4:
|
||||
print('<p class="text-danger">Error 4: Invalid server Hostname</p>')
|
||||
elif error_code == 11:
|
||||
print('<p class="text-danger">Error 11: Server connection error</p>')
|
||||
elif error_code == 22:
|
||||
print('<p class="text-danger">Error 22: PSD or CSD connection not established</p>')
|
||||
elif error_code == 73:
|
||||
print('<p class="text-danger">Error 73: Secure socket connect error</p>')
|
||||
else:
|
||||
print(f'<p class="text-danger">Unknown error code: {error_code}</p>')
|
||||
else:
|
||||
print('<p class="text-danger">Could not extract error code from response</p>')
|
||||
|
||||
|
||||
|
||||
# 2.2 code 1 (HHTP succeded)
|
||||
else:
|
||||
# Si la commande HTTP a réussi
|
||||
print("✅✅HTTP operation successful")
|
||||
#4. Read reply from server
|
||||
print("Reply from server:")
|
||||
ser_sara.write(b'AT+URDFILE="aircarto_server_response.txt"\r')
|
||||
response_SARA_4 = read_complete_response(ser_sara, wait_for_lines=["OK"], debug=False)
|
||||
print(response_SARA_4)
|
||||
|
||||
|
||||
|
||||
except serial.SerialException as e:
|
||||
print(f"Error: {e}")
|
||||
|
||||
finally:
|
||||
if ser_sara.is_open:
|
||||
ser_sara.close()
|
||||
#print("Serial closed")
|
||||
|
||||
@@ -11,22 +11,7 @@ parameter = sys.argv[1:] # Exclude the script name
|
||||
port='/dev/'+parameter[0] # ex: ttyAMA2
|
||||
message = parameter[1] # ex: Hello
|
||||
|
||||
#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)
|
||||
baudrate = 115200
|
||||
|
||||
ser = serial.Serial(
|
||||
port=port, #USB0 or ttyS0
|
||||
|
||||
@@ -12,22 +12,7 @@ port='/dev/'+parameter[0] # ex: ttyAMA2
|
||||
endpoint = parameter[1] # ex: /pro_4G/notif_message.php
|
||||
profile_id = parameter[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)
|
||||
baudrate = 115200
|
||||
|
||||
ser = serial.Serial(
|
||||
port=port, #USB0 or ttyS0
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
'''
|
||||
____ _ ____ _
|
||||
/ ___| / \ | _ \ / \
|
||||
\___ \ / _ \ | |_) | / _ \
|
||||
___) / ___ \| _ < / ___ \
|
||||
|____/_/ \_\_| \_\/_/ \_\
|
||||
|
||||
Script to connect SARA-R410 to APN
|
||||
AT+CGDCONT=1,"IP","data.mono"
|
||||
|
||||
@@ -15,23 +21,7 @@ port='/dev/'+parameter[0] # ex: ttyAMA2
|
||||
apn_address = parameter[1] # ex: data.mono
|
||||
timeout = float(parameter[2]) # ex:2
|
||||
|
||||
|
||||
#get baudrate
|
||||
def load_config(config_file):
|
||||
try:
|
||||
with open(config_file, 'r') as file:
|
||||
config_data = json.load(file)
|
||||
return config_data
|
||||
except Exception as e:
|
||||
print(f"Error loading config file: {e}")
|
||||
return {}
|
||||
|
||||
# Define the config file path
|
||||
config_file = '/var/www/nebuleair_pro_4g/config.json'
|
||||
# Load the configuration data
|
||||
config = load_config(config_file)
|
||||
# Access the shared variables
|
||||
baudrate = config.get('SaraR4_baudrate', 115200)
|
||||
baudrate = 115200
|
||||
|
||||
ser = serial.Serial(
|
||||
port=port, #USB0 or ttyS0
|
||||
@@ -43,6 +33,8 @@ ser = serial.Serial(
|
||||
)
|
||||
|
||||
command = f'AT+CGDCONT=1,"IP","{apn_address}"\r'
|
||||
#command = f'AT+CGDCONT=1,"IPV4V6","{apn_address}"\r'
|
||||
#command = f'AT+CGDCONT=1,"IP","{apn_address}",0,0\r'
|
||||
ser.write((command + '\r').encode('utf-8'))
|
||||
|
||||
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
'''
|
||||
____ _ ____ _
|
||||
/ ___| / \ | _ \ / \
|
||||
\___ \ / _ \ | |_) | / _ \
|
||||
___) / ___ \| _ < / ___ \
|
||||
|____/_/ \_\_| \_\/_/ \_\
|
||||
|
||||
Script to set the URL for a HTTP request
|
||||
Ex:
|
||||
/usr/bin/python3 /var/www/nebuleair_pro_4g/SARA/sara_setURL.py ttyAMA2 data.nebuleair.fr 0
|
||||
To do: need to add profile id as parameter
|
||||
|
||||
First profile id:
|
||||
AT+UHTTP=0,1,"data.nebuleair.fr"
|
||||
@@ -22,22 +27,7 @@ url = parameter[1] # ex: data.mobileair.fr
|
||||
profile_id = parameter[2] #ex: 0
|
||||
|
||||
|
||||
#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)
|
||||
baudrate = 115200
|
||||
|
||||
ser = serial.Serial(
|
||||
port=port, #USB0 or ttyS0
|
||||
|
||||
@@ -40,22 +40,7 @@ def read_complete_response(serial_connection, timeout=2, end_of_response_timeout
|
||||
|
||||
return response.decode('utf-8', errors='replace')
|
||||
|
||||
#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)
|
||||
baudrate = 115200
|
||||
|
||||
ser_sara = serial.Serial(
|
||||
port=port, #USB0 or ttyS0
|
||||
|
||||
@@ -12,21 +12,7 @@ port='/dev/'+parameter[0] # ex: ttyAMA2
|
||||
message = parameter[1] # ex: Hello
|
||||
|
||||
#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)
|
||||
baudrate = 115200
|
||||
|
||||
ser = serial.Serial(
|
||||
port=port, #USB0 or ttyS0
|
||||
|
||||
@@ -2,9 +2,10 @@
|
||||
|
||||
# Script to check if wifi is connected and start hotspot if not
|
||||
# will also retreive unique RPi ID and store it to deviceID.txt
|
||||
OUTPUT_FILE="/var/www/nebuleair_pro_4g/wifi_list.csv"
|
||||
JSON_FILE="/var/www/nebuleair_pro_4g/config.json"
|
||||
# script that starts at boot:
|
||||
# @reboot /var/www/nebuleair_pro_4g/boot_hotspot.sh >> /var/www/nebuleair_pro_4g/logs/app.log 2>&1
|
||||
|
||||
OUTPUT_FILE="/var/www/nebuleair_pro_4g/wifi_list.csv"
|
||||
|
||||
|
||||
echo "-------------------"
|
||||
@@ -12,6 +13,8 @@ echo "-------------------"
|
||||
|
||||
echo "NebuleAir pro started at $(date)"
|
||||
|
||||
chmod -R 777 /var/www/nebuleair_pro_4g/
|
||||
|
||||
# Blink GPIO 23 and 24 five times
|
||||
for i in {1..5}; do
|
||||
# Turn GPIO 23 and 24 ON
|
||||
@@ -25,15 +28,19 @@ for i in {1..5}; do
|
||||
sleep 1
|
||||
done
|
||||
|
||||
echo "getting SARA R4 serial number"
|
||||
echo "getting RPI serial number"
|
||||
# Get the last 8 characters of the serial number and write to text file
|
||||
serial_number=$(cat /proc/cpuinfo | grep Serial | awk '{print substr($3, length($3) - 7)}')
|
||||
# Define the JSON file path
|
||||
# Use jq to update the "deviceID" in the JSON file
|
||||
jq --arg serial_number "$serial_number" '.deviceID = $serial_number' "$JSON_FILE" > temp.json && mv temp.json "$JSON_FILE"
|
||||
|
||||
# update Sqlite database
|
||||
echo "Updating SQLite database with device ID: $serial_number"
|
||||
sqlite3 /var/www/nebuleair_pro_4g/sqlite/sensors.db "UPDATE config_table SET value='$serial_number' WHERE key='deviceID';"
|
||||
|
||||
echo "id: $serial_number"
|
||||
#get the SSH port for tunneling
|
||||
SSH_TUNNEL_PORT=$(jq -r '.sshTunnel_port' "$JSON_FILE")
|
||||
|
||||
|
||||
# Get SSH tunnel port from SQLite config_table
|
||||
SSH_TUNNEL_PORT=$(sqlite3 /var/www/nebuleair_pro_4g/sqlite/sensors.db "SELECT value FROM config_table WHERE key='sshTunnel_port'")
|
||||
|
||||
#need to wait for the network manager to be ready
|
||||
sleep 20
|
||||
@@ -51,42 +58,39 @@ if [ "$STATE" == "30 (disconnected)" ]; then
|
||||
echo "Starting hotspot..."
|
||||
sudo nmcli device wifi hotspot ifname wlan0 ssid nebuleair_pro password nebuleaircfg
|
||||
|
||||
# Update JSON to reflect hotspot mode
|
||||
jq --arg status "hotspot" '.WIFI_status = $status' "$JSON_FILE" > temp.json && mv temp.json "$JSON_FILE"
|
||||
|
||||
# Update SQLite to reflect hotspot mode
|
||||
sqlite3 /var/www/nebuleair_pro_4g/sqlite/sensors.db "UPDATE config_table SET value='hotspot' WHERE key='WIFI_status'"
|
||||
|
||||
else
|
||||
echo "Success: wlan0 is connected!"
|
||||
echo "🛜Success: wlan0 is connected!🛜"
|
||||
CONN_SSID=$(nmcli -g GENERAL.CONNECTION device show wlan0)
|
||||
echo "Connection: $CONN_SSID"
|
||||
|
||||
#update config JSON file
|
||||
jq --arg status "connected" '.WIFI_status = $status' "$JSON_FILE" > temp.json && mv temp.json "$JSON_FILE"
|
||||
|
||||
sudo chmod 777 "$JSON_FILE"
|
||||
# Update SQLite to reflect hotspot mode
|
||||
sqlite3 /var/www/nebuleair_pro_4g/sqlite/sensors.db "UPDATE config_table SET value='connected' WHERE key='WIFI_status'"
|
||||
|
||||
# Lancer le tunnel SSH
|
||||
echo "Démarrage du tunnel SSH sur le port $SSH_TUNNEL_PORT..."
|
||||
#echo "Démarrage du tunnel SSH sur le port $SSH_TUNNEL_PORT..."
|
||||
# Start the SSH agent if it's not already running
|
||||
eval "$(ssh-agent -s)"
|
||||
#eval "$(ssh-agent -s)"
|
||||
# Add your SSH private key
|
||||
ssh-add /home/airlab/.ssh/id_rsa
|
||||
#ssh-add /home/airlab/.ssh/id_rsa
|
||||
#connections details
|
||||
REMOTE_USER="airlab_server1" # Remplacez par votre nom d'utilisateur distant
|
||||
REMOTE_SERVER="aircarto.fr" # Remplacez par l'adresse de votre serveur
|
||||
LOCAL_PORT=22 # Port local à rediriger
|
||||
MONITOR_PORT=0 # Désactive la surveillance de connexion autossh
|
||||
#REMOTE_USER="airlab_server1" # Remplacez par votre nom d'utilisateur distant
|
||||
#REMOTE_SERVER="aircarto.fr" # Remplacez par l'adresse de votre serveur
|
||||
#LOCAL_PORT=22 # Port local à rediriger
|
||||
#MONITOR_PORT=0 # Désactive la surveillance de connexion autossh
|
||||
|
||||
#autossh -M "$MONITOR_PORT" -f -N -R "$SSH_TUNNEL_PORT:localhost:$LOCAL_PORT" "$REMOTE_USER@$REMOTE_SERVER" -p 50221
|
||||
# ssh -f -N -R 52221:localhost:22 -p 50221 airlab_server1@aircarto.fr
|
||||
ssh -i /var/www/.ssh/id_rsa -f -N -R "$SSH_TUNNEL_PORT:localhost:$LOCAL_PORT" -p 50221 -o StrictHostKeyChecking=no "$REMOTE_USER@$REMOTE_SERVER"
|
||||
#ssh -i /var/www/.ssh/id_rsa -f -N -R "$SSH_TUNNEL_PORT:localhost:$LOCAL_PORT" -p 50221 -o StrictHostKeyChecking=no "$REMOTE_USER@$REMOTE_SERVER"
|
||||
|
||||
#Check if the tunnel was created successfully
|
||||
if [ $? -eq 0 ]; then
|
||||
echo "Tunnel started successfully!"
|
||||
else
|
||||
echo "Error: Unable to start the tunnel!"
|
||||
exit 1
|
||||
fi
|
||||
#if [ $? -eq 0 ]; then
|
||||
# echo "Tunnel started successfully!"
|
||||
#else
|
||||
# echo "Error: Unable to start the tunnel!"
|
||||
# exit 1
|
||||
#fi
|
||||
fi
|
||||
echo "-------------------"
|
||||
|
||||
10
cron_jobs
10
cron_jobs
@@ -2,12 +2,12 @@
|
||||
|
||||
@reboot /var/www/nebuleair_pro_4g/boot_hotspot.sh >> /var/www/nebuleair_pro_4g/logs/app.log 2>&1
|
||||
|
||||
@reboot sleep 30 && /usr/bin/python3 /var/www/nebuleair_pro_4g/SARA/sara_setURL.py ttyAMA2 data.nebuleair.fr 0 >> /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
|
||||
|
||||
#@reboot sleep 45 && /usr/bin/python3 /var/www/nebuleair_pro_4g/SARA/SSL/prepareUspotProfile.py ttyAMA2 api-prod.uspot.probesys.net >> /var/www/nebuleair_pro_4g/logs/app.log 2>&1
|
||||
#0 0 * * * > /var/www/nebuleair_pro_4g/logs/master.log
|
||||
#0 0 * * * > /var/www/nebuleair_pro_4g/logs/master_errors.log
|
||||
#0 0 * * * > /var/www/nebuleair_pro_4g/logs/app.log
|
||||
|
||||
#* * * * * /usr/bin/python3 /var/www/nebuleair_pro_4g/loop/1_NPM/send_data.py >> /var/www/nebuleair_pro_4g/logs/loop.log 2>&1
|
||||
0 0 * * * find /var/www/nebuleair_pro_4g/logs -name "*.log" -type f -exec truncate -s 0 {} \;
|
||||
|
||||
* * * * * flock -n /var/www/nebuleair_pro_4g/loop/1_NPM/send_data.lock /usr/bin/python3 /var/www/nebuleair_pro_4g/loop/1_NPM/send_data.py >> /var/www/nebuleair_pro_4g/logs/loop.log 2>&1
|
||||
|
||||
0 0 * * * > /var/www/nebuleair_pro_4g/logs/loop.log
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
"""
|
||||
_____ _ ___ _______ _
|
||||
| ____| \ | \ \ / / ____| / \
|
||||
| _| | \| |\ \ / /| _| / _ \
|
||||
| |___| |\ | \ V / | |___ / ___ \
|
||||
|_____|_| \_| \_/ |_____/_/ \_\
|
||||
|
||||
Main loop to gather data from envea Sensors
|
||||
Need to run every minutes
|
||||
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
"""
|
||||
_____ _ ___ _______ _
|
||||
| ____| \ | \ \ / / ____| / \
|
||||
| _| | \| |\ \ / /| _| / _ \
|
||||
| |___| |\ | \ V / | |___ / ___ \
|
||||
|_____|_| \_| \_/ |_____/_/ \_\
|
||||
|
||||
Main loop to gather data from Envea Sensors
|
||||
|
||||
Runs every minute via cron:
|
||||
|
||||
102
envea/read_value_v2.py
Executable file
102
envea/read_value_v2.py
Executable file
@@ -0,0 +1,102 @@
|
||||
"""
|
||||
_____ _ ___ _______ _
|
||||
| ____| \ | \ \ / / ____| / \
|
||||
| _| | \| |\ \ / /| _| / _ \
|
||||
| |___| |\ | \ V / | |___ / ___ \
|
||||
|_____|_| \_| \_/ |_____/_/ \_\
|
||||
|
||||
Gather data from envea Sensors and store them to the SQlite table
|
||||
Use the RTC time for the timestamp
|
||||
|
||||
/usr/bin/python3 /var/www/nebuleair_pro_4g/envea/read_value_v2.py
|
||||
|
||||
"""
|
||||
|
||||
import json
|
||||
import serial
|
||||
import time
|
||||
import traceback
|
||||
import sqlite3
|
||||
from datetime import datetime
|
||||
|
||||
# Connect to the SQLite database
|
||||
conn = sqlite3.connect("/var/www/nebuleair_pro_4g/sqlite/sensors.db")
|
||||
cursor = conn.cursor()
|
||||
|
||||
#GET RTC TIME from SQlite
|
||||
cursor.execute("SELECT * FROM timestamp_table LIMIT 1")
|
||||
row = cursor.fetchone() # Get the first (and only) row
|
||||
rtc_time_str = row[1] # '2025-02-07 12:30:45'
|
||||
|
||||
# Fetch connected ENVEA sondes from SQLite config table
|
||||
cursor.execute("SELECT port, name, coefficient FROM envea_sondes_table WHERE connected = 1")
|
||||
connected_envea_sondes = cursor.fetchall() # List of tuples (port, name, coefficient)
|
||||
|
||||
serial_connections = {}
|
||||
|
||||
if connected_envea_sondes:
|
||||
for port, name, coefficient in connected_envea_sondes:
|
||||
try:
|
||||
serial_connections[name] = serial.Serial(
|
||||
port=f'/dev/{port}',
|
||||
baudrate=9600,
|
||||
parity=serial.PARITY_NONE,
|
||||
stopbits=serial.STOPBITS_ONE,
|
||||
bytesize=serial.EIGHTBITS,
|
||||
timeout=1
|
||||
)
|
||||
except serial.SerialException as e:
|
||||
print(f"Error opening serial port for {name}: {e}")
|
||||
|
||||
global data_h2s, data_no2, data_o3
|
||||
data_h2s = 0
|
||||
data_no2 = 0
|
||||
data_o3 = 0
|
||||
data_co = 0
|
||||
data_nh3 = 0
|
||||
|
||||
try:
|
||||
if connected_envea_sondes:
|
||||
for port, name, coefficient in connected_envea_sondes:
|
||||
if name in serial_connections:
|
||||
serial_connection = serial_connections[name]
|
||||
try:
|
||||
serial_connection.write(
|
||||
b"\xFF\x02\x13\x30\x01\x02\x03\x04\x05\x06\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\x12\xAF\x88\x03"
|
||||
)
|
||||
data_envea = serial_connection.readline()
|
||||
if len(data_envea) >= 20:
|
||||
byte_20 = data_envea[19] * coefficient
|
||||
if name == "h2s":
|
||||
data_h2s = byte_20
|
||||
elif name == "no2":
|
||||
data_no2 = byte_20
|
||||
elif name == "o3":
|
||||
data_o3 = byte_20
|
||||
except serial.SerialException as e:
|
||||
print(f"Error communicating with {name}: {e}")
|
||||
except Exception as e:
|
||||
print("An error occurred while gathering data:", e)
|
||||
traceback.print_exc()
|
||||
|
||||
|
||||
#print(f" H2S: {data_h2s}, NO2: {data_no2}, O3: {data_o3}")
|
||||
|
||||
#save to sqlite database
|
||||
try:
|
||||
cursor.execute('''
|
||||
INSERT INTO data_envea (timestamp,h2s, no2, o3, co, nh3) VALUES (?,?,?,?,?,?)'''
|
||||
, (rtc_time_str,data_h2s,data_no2,data_o3,data_co,data_nh3 ))
|
||||
|
||||
# Commit and close the connection
|
||||
conn.commit()
|
||||
|
||||
#print("Sensor data saved successfully!")
|
||||
|
||||
except Exception as e:
|
||||
print(f"Database error: {e}")
|
||||
|
||||
|
||||
conn.close()
|
||||
|
||||
|
||||
1016
html/admin.html
1016
html/admin.html
File diff suppressed because it is too large
Load Diff
20
html/assets/js/chart.js
Executable file
20
html/assets/js/chart.js
Executable file
File diff suppressed because one or more lines are too long
BIN
html/assets/leaflet/images/layers-2x.png
Executable file
BIN
html/assets/leaflet/images/layers-2x.png
Executable file
Binary file not shown.
|
After Width: | Height: | Size: 1.2 KiB |
BIN
html/assets/leaflet/images/layers.png
Executable file
BIN
html/assets/leaflet/images/layers.png
Executable file
Binary file not shown.
|
After Width: | Height: | Size: 696 B |
BIN
html/assets/leaflet/images/marker-icon-2x.png
Executable file
BIN
html/assets/leaflet/images/marker-icon-2x.png
Executable file
Binary file not shown.
|
After Width: | Height: | Size: 2.4 KiB |
BIN
html/assets/leaflet/images/marker-icon.png
Executable file
BIN
html/assets/leaflet/images/marker-icon.png
Executable file
Binary file not shown.
|
After Width: | Height: | Size: 1.4 KiB |
BIN
html/assets/leaflet/images/marker-shadow.png
Executable file
BIN
html/assets/leaflet/images/marker-shadow.png
Executable file
Binary file not shown.
|
After Width: | Height: | Size: 618 B |
14419
html/assets/leaflet/leaflet-src.esm.js
Executable file
14419
html/assets/leaflet/leaflet-src.esm.js
Executable file
File diff suppressed because it is too large
Load Diff
1
html/assets/leaflet/leaflet-src.esm.js.map
Executable file
1
html/assets/leaflet/leaflet-src.esm.js.map
Executable file
File diff suppressed because one or more lines are too long
14512
html/assets/leaflet/leaflet-src.js
Executable file
14512
html/assets/leaflet/leaflet-src.js
Executable file
File diff suppressed because it is too large
Load Diff
1
html/assets/leaflet/leaflet-src.js.map
Executable file
1
html/assets/leaflet/leaflet-src.js.map
Executable file
File diff suppressed because one or more lines are too long
661
html/assets/leaflet/leaflet.css
Executable file
661
html/assets/leaflet/leaflet.css
Executable file
@@ -0,0 +1,661 @@
|
||||
/* required styles */
|
||||
|
||||
.leaflet-pane,
|
||||
.leaflet-tile,
|
||||
.leaflet-marker-icon,
|
||||
.leaflet-marker-shadow,
|
||||
.leaflet-tile-container,
|
||||
.leaflet-pane > svg,
|
||||
.leaflet-pane > canvas,
|
||||
.leaflet-zoom-box,
|
||||
.leaflet-image-layer,
|
||||
.leaflet-layer {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
}
|
||||
.leaflet-container {
|
||||
overflow: hidden;
|
||||
}
|
||||
.leaflet-tile,
|
||||
.leaflet-marker-icon,
|
||||
.leaflet-marker-shadow {
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
user-select: none;
|
||||
-webkit-user-drag: none;
|
||||
}
|
||||
/* Prevents IE11 from highlighting tiles in blue */
|
||||
.leaflet-tile::selection {
|
||||
background: transparent;
|
||||
}
|
||||
/* Safari renders non-retina tile on retina better with this, but Chrome is worse */
|
||||
.leaflet-safari .leaflet-tile {
|
||||
image-rendering: -webkit-optimize-contrast;
|
||||
}
|
||||
/* hack that prevents hw layers "stretching" when loading new tiles */
|
||||
.leaflet-safari .leaflet-tile-container {
|
||||
width: 1600px;
|
||||
height: 1600px;
|
||||
-webkit-transform-origin: 0 0;
|
||||
}
|
||||
.leaflet-marker-icon,
|
||||
.leaflet-marker-shadow {
|
||||
display: block;
|
||||
}
|
||||
/* .leaflet-container svg: reset svg max-width decleration shipped in Joomla! (joomla.org) 3.x */
|
||||
/* .leaflet-container img: map is broken in FF if you have max-width: 100% on tiles */
|
||||
.leaflet-container .leaflet-overlay-pane svg {
|
||||
max-width: none !important;
|
||||
max-height: none !important;
|
||||
}
|
||||
.leaflet-container .leaflet-marker-pane img,
|
||||
.leaflet-container .leaflet-shadow-pane img,
|
||||
.leaflet-container .leaflet-tile-pane img,
|
||||
.leaflet-container img.leaflet-image-layer,
|
||||
.leaflet-container .leaflet-tile {
|
||||
max-width: none !important;
|
||||
max-height: none !important;
|
||||
width: auto;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.leaflet-container img.leaflet-tile {
|
||||
/* See: https://bugs.chromium.org/p/chromium/issues/detail?id=600120 */
|
||||
mix-blend-mode: plus-lighter;
|
||||
}
|
||||
|
||||
.leaflet-container.leaflet-touch-zoom {
|
||||
-ms-touch-action: pan-x pan-y;
|
||||
touch-action: pan-x pan-y;
|
||||
}
|
||||
.leaflet-container.leaflet-touch-drag {
|
||||
-ms-touch-action: pinch-zoom;
|
||||
/* Fallback for FF which doesn't support pinch-zoom */
|
||||
touch-action: none;
|
||||
touch-action: pinch-zoom;
|
||||
}
|
||||
.leaflet-container.leaflet-touch-drag.leaflet-touch-zoom {
|
||||
-ms-touch-action: none;
|
||||
touch-action: none;
|
||||
}
|
||||
.leaflet-container {
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
.leaflet-container a {
|
||||
-webkit-tap-highlight-color: rgba(51, 181, 229, 0.4);
|
||||
}
|
||||
.leaflet-tile {
|
||||
filter: inherit;
|
||||
visibility: hidden;
|
||||
}
|
||||
.leaflet-tile-loaded {
|
||||
visibility: inherit;
|
||||
}
|
||||
.leaflet-zoom-box {
|
||||
width: 0;
|
||||
height: 0;
|
||||
-moz-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
z-index: 800;
|
||||
}
|
||||
/* workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=888319 */
|
||||
.leaflet-overlay-pane svg {
|
||||
-moz-user-select: none;
|
||||
}
|
||||
|
||||
.leaflet-pane { z-index: 400; }
|
||||
|
||||
.leaflet-tile-pane { z-index: 200; }
|
||||
.leaflet-overlay-pane { z-index: 400; }
|
||||
.leaflet-shadow-pane { z-index: 500; }
|
||||
.leaflet-marker-pane { z-index: 600; }
|
||||
.leaflet-tooltip-pane { z-index: 650; }
|
||||
.leaflet-popup-pane { z-index: 700; }
|
||||
|
||||
.leaflet-map-pane canvas { z-index: 100; }
|
||||
.leaflet-map-pane svg { z-index: 200; }
|
||||
|
||||
.leaflet-vml-shape {
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
}
|
||||
.lvml {
|
||||
behavior: url(#default#VML);
|
||||
display: inline-block;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
|
||||
/* control positioning */
|
||||
|
||||
.leaflet-control {
|
||||
position: relative;
|
||||
z-index: 800;
|
||||
pointer-events: visiblePainted; /* IE 9-10 doesn't have auto */
|
||||
pointer-events: auto;
|
||||
}
|
||||
.leaflet-top,
|
||||
.leaflet-bottom {
|
||||
position: absolute;
|
||||
z-index: 1000;
|
||||
pointer-events: none;
|
||||
}
|
||||
.leaflet-top {
|
||||
top: 0;
|
||||
}
|
||||
.leaflet-right {
|
||||
right: 0;
|
||||
}
|
||||
.leaflet-bottom {
|
||||
bottom: 0;
|
||||
}
|
||||
.leaflet-left {
|
||||
left: 0;
|
||||
}
|
||||
.leaflet-control {
|
||||
float: left;
|
||||
clear: both;
|
||||
}
|
||||
.leaflet-right .leaflet-control {
|
||||
float: right;
|
||||
}
|
||||
.leaflet-top .leaflet-control {
|
||||
margin-top: 10px;
|
||||
}
|
||||
.leaflet-bottom .leaflet-control {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.leaflet-left .leaflet-control {
|
||||
margin-left: 10px;
|
||||
}
|
||||
.leaflet-right .leaflet-control {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
|
||||
/* zoom and fade animations */
|
||||
|
||||
.leaflet-fade-anim .leaflet-popup {
|
||||
opacity: 0;
|
||||
-webkit-transition: opacity 0.2s linear;
|
||||
-moz-transition: opacity 0.2s linear;
|
||||
transition: opacity 0.2s linear;
|
||||
}
|
||||
.leaflet-fade-anim .leaflet-map-pane .leaflet-popup {
|
||||
opacity: 1;
|
||||
}
|
||||
.leaflet-zoom-animated {
|
||||
-webkit-transform-origin: 0 0;
|
||||
-ms-transform-origin: 0 0;
|
||||
transform-origin: 0 0;
|
||||
}
|
||||
svg.leaflet-zoom-animated {
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
.leaflet-zoom-anim .leaflet-zoom-animated {
|
||||
-webkit-transition: -webkit-transform 0.25s cubic-bezier(0,0,0.25,1);
|
||||
-moz-transition: -moz-transform 0.25s cubic-bezier(0,0,0.25,1);
|
||||
transition: transform 0.25s cubic-bezier(0,0,0.25,1);
|
||||
}
|
||||
.leaflet-zoom-anim .leaflet-tile,
|
||||
.leaflet-pan-anim .leaflet-tile {
|
||||
-webkit-transition: none;
|
||||
-moz-transition: none;
|
||||
transition: none;
|
||||
}
|
||||
|
||||
.leaflet-zoom-anim .leaflet-zoom-hide {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
|
||||
/* cursors */
|
||||
|
||||
.leaflet-interactive {
|
||||
cursor: pointer;
|
||||
}
|
||||
.leaflet-grab {
|
||||
cursor: -webkit-grab;
|
||||
cursor: -moz-grab;
|
||||
cursor: grab;
|
||||
}
|
||||
.leaflet-crosshair,
|
||||
.leaflet-crosshair .leaflet-interactive {
|
||||
cursor: crosshair;
|
||||
}
|
||||
.leaflet-popup-pane,
|
||||
.leaflet-control {
|
||||
cursor: auto;
|
||||
}
|
||||
.leaflet-dragging .leaflet-grab,
|
||||
.leaflet-dragging .leaflet-grab .leaflet-interactive,
|
||||
.leaflet-dragging .leaflet-marker-draggable {
|
||||
cursor: move;
|
||||
cursor: -webkit-grabbing;
|
||||
cursor: -moz-grabbing;
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
/* marker & overlays interactivity */
|
||||
.leaflet-marker-icon,
|
||||
.leaflet-marker-shadow,
|
||||
.leaflet-image-layer,
|
||||
.leaflet-pane > svg path,
|
||||
.leaflet-tile-container {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.leaflet-marker-icon.leaflet-interactive,
|
||||
.leaflet-image-layer.leaflet-interactive,
|
||||
.leaflet-pane > svg path.leaflet-interactive,
|
||||
svg.leaflet-image-layer.leaflet-interactive path {
|
||||
pointer-events: visiblePainted; /* IE 9-10 doesn't have auto */
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
/* visual tweaks */
|
||||
|
||||
.leaflet-container {
|
||||
background: #ddd;
|
||||
outline-offset: 1px;
|
||||
}
|
||||
.leaflet-container a {
|
||||
color: #0078A8;
|
||||
}
|
||||
.leaflet-zoom-box {
|
||||
border: 2px dotted #38f;
|
||||
background: rgba(255,255,255,0.5);
|
||||
}
|
||||
|
||||
|
||||
/* general typography */
|
||||
.leaflet-container {
|
||||
font-family: "Helvetica Neue", Arial, Helvetica, sans-serif;
|
||||
font-size: 12px;
|
||||
font-size: 0.75rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
|
||||
/* general toolbar styles */
|
||||
|
||||
.leaflet-bar {
|
||||
box-shadow: 0 1px 5px rgba(0,0,0,0.65);
|
||||
border-radius: 4px;
|
||||
}
|
||||
.leaflet-bar a {
|
||||
background-color: #fff;
|
||||
border-bottom: 1px solid #ccc;
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
line-height: 26px;
|
||||
display: block;
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
color: black;
|
||||
}
|
||||
.leaflet-bar a,
|
||||
.leaflet-control-layers-toggle {
|
||||
background-position: 50% 50%;
|
||||
background-repeat: no-repeat;
|
||||
display: block;
|
||||
}
|
||||
.leaflet-bar a:hover,
|
||||
.leaflet-bar a:focus {
|
||||
background-color: #f4f4f4;
|
||||
}
|
||||
.leaflet-bar a:first-child {
|
||||
border-top-left-radius: 4px;
|
||||
border-top-right-radius: 4px;
|
||||
}
|
||||
.leaflet-bar a:last-child {
|
||||
border-bottom-left-radius: 4px;
|
||||
border-bottom-right-radius: 4px;
|
||||
border-bottom: none;
|
||||
}
|
||||
.leaflet-bar a.leaflet-disabled {
|
||||
cursor: default;
|
||||
background-color: #f4f4f4;
|
||||
color: #bbb;
|
||||
}
|
||||
|
||||
.leaflet-touch .leaflet-bar a {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
line-height: 30px;
|
||||
}
|
||||
.leaflet-touch .leaflet-bar a:first-child {
|
||||
border-top-left-radius: 2px;
|
||||
border-top-right-radius: 2px;
|
||||
}
|
||||
.leaflet-touch .leaflet-bar a:last-child {
|
||||
border-bottom-left-radius: 2px;
|
||||
border-bottom-right-radius: 2px;
|
||||
}
|
||||
|
||||
/* zoom control */
|
||||
|
||||
.leaflet-control-zoom-in,
|
||||
.leaflet-control-zoom-out {
|
||||
font: bold 18px 'Lucida Console', Monaco, monospace;
|
||||
text-indent: 1px;
|
||||
}
|
||||
|
||||
.leaflet-touch .leaflet-control-zoom-in, .leaflet-touch .leaflet-control-zoom-out {
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
|
||||
/* layers control */
|
||||
|
||||
.leaflet-control-layers {
|
||||
box-shadow: 0 1px 5px rgba(0,0,0,0.4);
|
||||
background: #fff;
|
||||
border-radius: 5px;
|
||||
}
|
||||
.leaflet-control-layers-toggle {
|
||||
background-image: url(images/layers.png);
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
}
|
||||
.leaflet-retina .leaflet-control-layers-toggle {
|
||||
background-image: url(images/layers-2x.png);
|
||||
background-size: 26px 26px;
|
||||
}
|
||||
.leaflet-touch .leaflet-control-layers-toggle {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
}
|
||||
.leaflet-control-layers .leaflet-control-layers-list,
|
||||
.leaflet-control-layers-expanded .leaflet-control-layers-toggle {
|
||||
display: none;
|
||||
}
|
||||
.leaflet-control-layers-expanded .leaflet-control-layers-list {
|
||||
display: block;
|
||||
position: relative;
|
||||
}
|
||||
.leaflet-control-layers-expanded {
|
||||
padding: 6px 10px 6px 6px;
|
||||
color: #333;
|
||||
background: #fff;
|
||||
}
|
||||
.leaflet-control-layers-scrollbar {
|
||||
overflow-y: scroll;
|
||||
overflow-x: hidden;
|
||||
padding-right: 5px;
|
||||
}
|
||||
.leaflet-control-layers-selector {
|
||||
margin-top: 2px;
|
||||
position: relative;
|
||||
top: 1px;
|
||||
}
|
||||
.leaflet-control-layers label {
|
||||
display: block;
|
||||
font-size: 13px;
|
||||
font-size: 1.08333em;
|
||||
}
|
||||
.leaflet-control-layers-separator {
|
||||
height: 0;
|
||||
border-top: 1px solid #ddd;
|
||||
margin: 5px -10px 5px -6px;
|
||||
}
|
||||
|
||||
/* Default icon URLs */
|
||||
.leaflet-default-icon-path { /* used only in path-guessing heuristic, see L.Icon.Default */
|
||||
background-image: url(images/marker-icon.png);
|
||||
}
|
||||
|
||||
|
||||
/* attribution and scale controls */
|
||||
|
||||
.leaflet-container .leaflet-control-attribution {
|
||||
background: #fff;
|
||||
background: rgba(255, 255, 255, 0.8);
|
||||
margin: 0;
|
||||
}
|
||||
.leaflet-control-attribution,
|
||||
.leaflet-control-scale-line {
|
||||
padding: 0 5px;
|
||||
color: #333;
|
||||
line-height: 1.4;
|
||||
}
|
||||
.leaflet-control-attribution a {
|
||||
text-decoration: none;
|
||||
}
|
||||
.leaflet-control-attribution a:hover,
|
||||
.leaflet-control-attribution a:focus {
|
||||
text-decoration: underline;
|
||||
}
|
||||
.leaflet-attribution-flag {
|
||||
display: inline !important;
|
||||
vertical-align: baseline !important;
|
||||
width: 1em;
|
||||
height: 0.6669em;
|
||||
}
|
||||
.leaflet-left .leaflet-control-scale {
|
||||
margin-left: 5px;
|
||||
}
|
||||
.leaflet-bottom .leaflet-control-scale {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
.leaflet-control-scale-line {
|
||||
border: 2px solid #777;
|
||||
border-top: none;
|
||||
line-height: 1.1;
|
||||
padding: 2px 5px 1px;
|
||||
white-space: nowrap;
|
||||
-moz-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
background: rgba(255, 255, 255, 0.8);
|
||||
text-shadow: 1px 1px #fff;
|
||||
}
|
||||
.leaflet-control-scale-line:not(:first-child) {
|
||||
border-top: 2px solid #777;
|
||||
border-bottom: none;
|
||||
margin-top: -2px;
|
||||
}
|
||||
.leaflet-control-scale-line:not(:first-child):not(:last-child) {
|
||||
border-bottom: 2px solid #777;
|
||||
}
|
||||
|
||||
.leaflet-touch .leaflet-control-attribution,
|
||||
.leaflet-touch .leaflet-control-layers,
|
||||
.leaflet-touch .leaflet-bar {
|
||||
box-shadow: none;
|
||||
}
|
||||
.leaflet-touch .leaflet-control-layers,
|
||||
.leaflet-touch .leaflet-bar {
|
||||
border: 2px solid rgba(0,0,0,0.2);
|
||||
background-clip: padding-box;
|
||||
}
|
||||
|
||||
|
||||
/* popup */
|
||||
|
||||
.leaflet-popup {
|
||||
position: absolute;
|
||||
text-align: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.leaflet-popup-content-wrapper {
|
||||
padding: 1px;
|
||||
text-align: left;
|
||||
border-radius: 12px;
|
||||
}
|
||||
.leaflet-popup-content {
|
||||
margin: 13px 24px 13px 20px;
|
||||
line-height: 1.3;
|
||||
font-size: 13px;
|
||||
font-size: 1.08333em;
|
||||
min-height: 1px;
|
||||
}
|
||||
.leaflet-popup-content p {
|
||||
margin: 17px 0;
|
||||
margin: 1.3em 0;
|
||||
}
|
||||
.leaflet-popup-tip-container {
|
||||
width: 40px;
|
||||
height: 20px;
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
margin-top: -1px;
|
||||
margin-left: -20px;
|
||||
overflow: hidden;
|
||||
pointer-events: none;
|
||||
}
|
||||
.leaflet-popup-tip {
|
||||
width: 17px;
|
||||
height: 17px;
|
||||
padding: 1px;
|
||||
|
||||
margin: -10px auto 0;
|
||||
pointer-events: auto;
|
||||
|
||||
-webkit-transform: rotate(45deg);
|
||||
-moz-transform: rotate(45deg);
|
||||
-ms-transform: rotate(45deg);
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
.leaflet-popup-content-wrapper,
|
||||
.leaflet-popup-tip {
|
||||
background: white;
|
||||
color: #333;
|
||||
box-shadow: 0 3px 14px rgba(0,0,0,0.4);
|
||||
}
|
||||
.leaflet-container a.leaflet-popup-close-button {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
border: none;
|
||||
text-align: center;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
font: 16px/24px Tahoma, Verdana, sans-serif;
|
||||
color: #757575;
|
||||
text-decoration: none;
|
||||
background: transparent;
|
||||
}
|
||||
.leaflet-container a.leaflet-popup-close-button:hover,
|
||||
.leaflet-container a.leaflet-popup-close-button:focus {
|
||||
color: #585858;
|
||||
}
|
||||
.leaflet-popup-scrolled {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.leaflet-oldie .leaflet-popup-content-wrapper {
|
||||
-ms-zoom: 1;
|
||||
}
|
||||
.leaflet-oldie .leaflet-popup-tip {
|
||||
width: 24px;
|
||||
margin: 0 auto;
|
||||
|
||||
-ms-filter: "progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678)";
|
||||
filter: progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678);
|
||||
}
|
||||
|
||||
.leaflet-oldie .leaflet-control-zoom,
|
||||
.leaflet-oldie .leaflet-control-layers,
|
||||
.leaflet-oldie .leaflet-popup-content-wrapper,
|
||||
.leaflet-oldie .leaflet-popup-tip {
|
||||
border: 1px solid #999;
|
||||
}
|
||||
|
||||
|
||||
/* div icon */
|
||||
|
||||
.leaflet-div-icon {
|
||||
background: #fff;
|
||||
border: 1px solid #666;
|
||||
}
|
||||
|
||||
|
||||
/* Tooltip */
|
||||
/* Base styles for the element that has a tooltip */
|
||||
.leaflet-tooltip {
|
||||
position: absolute;
|
||||
padding: 6px;
|
||||
background-color: #fff;
|
||||
border: 1px solid #fff;
|
||||
border-radius: 3px;
|
||||
color: #222;
|
||||
white-space: nowrap;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
pointer-events: none;
|
||||
box-shadow: 0 1px 3px rgba(0,0,0,0.4);
|
||||
}
|
||||
.leaflet-tooltip.leaflet-interactive {
|
||||
cursor: pointer;
|
||||
pointer-events: auto;
|
||||
}
|
||||
.leaflet-tooltip-top:before,
|
||||
.leaflet-tooltip-bottom:before,
|
||||
.leaflet-tooltip-left:before,
|
||||
.leaflet-tooltip-right:before {
|
||||
position: absolute;
|
||||
pointer-events: none;
|
||||
border: 6px solid transparent;
|
||||
background: transparent;
|
||||
content: "";
|
||||
}
|
||||
|
||||
/* Directions */
|
||||
|
||||
.leaflet-tooltip-bottom {
|
||||
margin-top: 6px;
|
||||
}
|
||||
.leaflet-tooltip-top {
|
||||
margin-top: -6px;
|
||||
}
|
||||
.leaflet-tooltip-bottom:before,
|
||||
.leaflet-tooltip-top:before {
|
||||
left: 50%;
|
||||
margin-left: -6px;
|
||||
}
|
||||
.leaflet-tooltip-top:before {
|
||||
bottom: 0;
|
||||
margin-bottom: -12px;
|
||||
border-top-color: #fff;
|
||||
}
|
||||
.leaflet-tooltip-bottom:before {
|
||||
top: 0;
|
||||
margin-top: -12px;
|
||||
margin-left: -6px;
|
||||
border-bottom-color: #fff;
|
||||
}
|
||||
.leaflet-tooltip-left {
|
||||
margin-left: -6px;
|
||||
}
|
||||
.leaflet-tooltip-right {
|
||||
margin-left: 6px;
|
||||
}
|
||||
.leaflet-tooltip-left:before,
|
||||
.leaflet-tooltip-right:before {
|
||||
top: 50%;
|
||||
margin-top: -6px;
|
||||
}
|
||||
.leaflet-tooltip-left:before {
|
||||
right: 0;
|
||||
margin-right: -12px;
|
||||
border-left-color: #fff;
|
||||
}
|
||||
.leaflet-tooltip-right:before {
|
||||
left: 0;
|
||||
margin-left: -12px;
|
||||
border-right-color: #fff;
|
||||
}
|
||||
|
||||
/* Printing */
|
||||
|
||||
@media print {
|
||||
/* Prevent printers from removing background-images of controls. */
|
||||
.leaflet-control {
|
||||
-webkit-print-color-adjust: exact;
|
||||
print-color-adjust: exact;
|
||||
}
|
||||
}
|
||||
6
html/assets/leaflet/leaflet.js
Executable file
6
html/assets/leaflet/leaflet.js
Executable file
File diff suppressed because one or more lines are too long
1
html/assets/leaflet/leaflet.js.map
Executable file
1
html/assets/leaflet/leaflet.js.map
Executable file
File diff suppressed because one or more lines are too long
406
html/database.html
Executable file
406
html/database.html
Executable file
@@ -0,0 +1,406 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>NebuleAir</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;
|
||||
}
|
||||
</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">
|
||||
<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">Base de données</h1>
|
||||
<p>Le capteur enregistre en local les données de mesures. Vous pouvez ici les consulter et les télécharger.</p>
|
||||
|
||||
<div class="row mb-3">
|
||||
|
||||
<div class="col-sm-5">
|
||||
<div class="card text-dark bg-light">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Consulter la base de donnée</h5>
|
||||
<!-- Dropdown to select number of records -->
|
||||
<div class="d-flex align-items-center mb-3">
|
||||
<label for="records_limit" class="form-label me-2">Nombre de mesures:</label>
|
||||
<select id="records_limit" class="form-select w-auto">
|
||||
<option value="10" selected>10 dernières</option>
|
||||
<option value="20">20 dernières</option>
|
||||
<option value="30">30 dernières</option>
|
||||
</select>
|
||||
</div>
|
||||
<button class="btn btn-primary" onclick="get_data_sqlite('data_NPM',getSelectedLimit(),false)">Mesures PM</button>
|
||||
<button class="btn btn-primary" onclick="get_data_sqlite('data_BME280',getSelectedLimit(),false)">Mesures Temp/Hum</button>
|
||||
<button class="btn btn-primary" onclick="get_data_sqlite('data_NPM_5channels',getSelectedLimit(),false)">Mesures PM (5 canaux)</button>
|
||||
<button class="btn btn-primary" onclick="get_data_sqlite('data_envea',getSelectedLimit(),false)">Sonde Cairsens</button>
|
||||
<button class="btn btn-primary" onclick="get_data_sqlite('data_WIND',getSelectedLimit(),false)">Sonde Vent</button>
|
||||
|
||||
<button class="btn btn-warning" onclick="get_data_sqlite('timestamp_table',getSelectedLimit(),false)">Timestamp Table</button>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-5">
|
||||
<div class="card text-dark bg-light">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Télécharger les données</h5>
|
||||
<!-- Date selection for download -->
|
||||
<div class="d-flex align-items-center gap-3 mb-3">
|
||||
<label for="start_date" class="form-label">Date de début:</label>
|
||||
<input type="date" id="start_date" class="form-control w-auto">
|
||||
<label for="end_date" class="form-label">Date de fin:</label>
|
||||
<input type="date" id="end_date" class="form-control w-auto">
|
||||
</div>
|
||||
|
||||
|
||||
<button class="btn btn-primary" onclick="get_data_sqlite('data_NPM',10,true, getStartDate(), getEndDate())">Mesures PM</button>
|
||||
<button class="btn btn-primary" onclick="get_data_sqlite('data_BME280',10,true, getStartDate(), getEndDate())">Mesures Temp/Hum</button>
|
||||
<button class="btn btn-primary" onclick="get_data_sqlite('data_NPM_5channels',10,true, getStartDate(), getEndDate())">Mesures PM (5 canaux)</button>
|
||||
<button class="btn btn-primary" onclick="get_data_sqlite('data_envea',10,true, getStartDate(), getEndDate())">Sonde Cairsens</button>
|
||||
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
|
||||
<div class="row mt-2">
|
||||
<div id="table_data"></div>
|
||||
</div>
|
||||
|
||||
</main>
|
||||
</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 () {
|
||||
console.log("DOMContentLoaded");
|
||||
|
||||
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));
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
window.onload = function() {
|
||||
|
||||
|
||||
//NEW way to get data from SQLITE
|
||||
$.ajax({
|
||||
url: 'launcher.php?type=get_config_sqlite',
|
||||
dataType:'json',
|
||||
//dataType: 'json', // Specify that you expect a JSON response
|
||||
method: 'GET', // Use GET or POST depending on your needs
|
||||
success: function(response) {
|
||||
console.log("Getting SQLite config table:");
|
||||
console.log(response);
|
||||
|
||||
//get device Name (for the side bar)
|
||||
const deviceName = response.deviceName;
|
||||
const elements = document.querySelectorAll('.sideBar_sensorName');
|
||||
elements.forEach((element) => {
|
||||
element.innerText = deviceName;
|
||||
});
|
||||
|
||||
//device name html page title
|
||||
if (response.deviceName) {
|
||||
document.title = response.deviceName;
|
||||
}
|
||||
|
||||
},
|
||||
error: function(xhr, status, error) {
|
||||
console.error('AJAX request failed:', status, error);
|
||||
}
|
||||
}); //end ajax
|
||||
|
||||
|
||||
//get local RTC
|
||||
$.ajax({
|
||||
url: 'launcher.php?type=RTC_time',
|
||||
dataType: 'text', // Specify that you expect a JSON response
|
||||
method: 'GET', // Use GET or POST depending on your needs
|
||||
success: function(response) {
|
||||
console.log("Local RTC: " + response);
|
||||
const RTC_Element = document.getElementById("RTC_time");
|
||||
RTC_Element.textContent = response;
|
||||
},
|
||||
error: function(xhr, status, error) {
|
||||
console.error('AJAX request failed:', status, error);
|
||||
}
|
||||
}); //end AJAX
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
// TABLE PM
|
||||
function get_data_sqlite(table, limit, download , startDate = "", endDate = "") {
|
||||
console.log(`Getting data for table: ${table}, limit: ${limit}, download: ${download}, start: ${startDate}, end: ${endDate}`);
|
||||
// Construct URL parameters dynamically
|
||||
let url = `launcher.php?type=table_mesure&table=${table}&limit=${limit}&download=${download}`;
|
||||
|
||||
// Add date parameters if downloading
|
||||
if (download) {
|
||||
url += `&start_date=${startDate}&end_date=${endDate}`;
|
||||
}
|
||||
|
||||
console.log(url);
|
||||
|
||||
$.ajax({
|
||||
url: url,
|
||||
dataType: 'text', // Specify that you expect a JSON response
|
||||
method: 'GET', // Use GET or POST depending on your needs
|
||||
success: function(response) {
|
||||
console.log(response);
|
||||
|
||||
// If download is true, generate and trigger CSV download
|
||||
if (download) {
|
||||
downloadCSV(response, table);
|
||||
return; // Exit function after triggering download
|
||||
}
|
||||
|
||||
let rows = response.trim().split("\n");
|
||||
// Generate Bootstrap table
|
||||
|
||||
let tableHTML = `<table class="table table-striped table-bordered">
|
||||
<thead class="table-dark"><tr>`;
|
||||
|
||||
// Define column headers dynamically based on the table type
|
||||
if (table === "data_NPM") {
|
||||
tableHTML += `
|
||||
<th>Timestamp</th>
|
||||
<th>PM1</th>
|
||||
<th>PM2.5</th>
|
||||
<th>PM10</th>
|
||||
<th>Temperature (°C)</th>
|
||||
<th>Humidity (%)</th>
|
||||
`;
|
||||
} else if (table === "data_BME280") {
|
||||
tableHTML += `
|
||||
<th>Timestamp</th>
|
||||
<th>Temperature (°C)</th>
|
||||
<th>Humidity (%)</th>
|
||||
<th>Pressure (hPa)</th>
|
||||
`;
|
||||
} else if (table === "data_NPM_5channels") {
|
||||
tableHTML += `
|
||||
<th>Timestamp</th>
|
||||
<th>PM_ch1 (nb/L)</th>
|
||||
<th>PM_ch2 (nb/L)</th>
|
||||
<th>PM_ch3 (nb/L)</th>
|
||||
<th>PM_ch4 (nb/L)</th>
|
||||
<th>PM_ch5 (nb/L)</th>
|
||||
|
||||
`;
|
||||
}else if (table === "data_envea") {
|
||||
tableHTML += `
|
||||
<th>Timestamp</th>
|
||||
<th>NO2</th>
|
||||
<th>H2S</th>
|
||||
<th>NH3</th>
|
||||
<th>CO</th>
|
||||
<th>O3</th>
|
||||
|
||||
`;
|
||||
}else if (table === "timestamp_table") {
|
||||
tableHTML += `
|
||||
<th>Timestamp</th>
|
||||
`;
|
||||
}else if (table === "data_WIND") {
|
||||
tableHTML += `
|
||||
<th>Timestamp</th>
|
||||
<th>speed (km/h)</th>
|
||||
<th>Direction (V)</th>
|
||||
`;
|
||||
}
|
||||
|
||||
tableHTML += `</tr></thead><tbody>`;
|
||||
|
||||
// Loop through rows and create table rows
|
||||
rows.forEach(row => {
|
||||
let columns = row.replace(/[()]/g, "").split(", "); // Remove parentheses and split
|
||||
tableHTML += "<tr>";
|
||||
|
||||
if (table === "data_NPM") {
|
||||
tableHTML += `
|
||||
<td>${columns[0]}</td>
|
||||
<td>${columns[1]}</td>
|
||||
<td>${columns[2]}</td>
|
||||
<td>${columns[3]}</td>
|
||||
<td>${columns[4]}</td>
|
||||
<td>${columns[5]}</td>
|
||||
`;
|
||||
} else if (table === "data_BME280") {
|
||||
tableHTML += `
|
||||
<td>${columns[0]}</td>
|
||||
<td>${columns[1]}</td>
|
||||
<td>${columns[2]}</td>
|
||||
<td>${columns[3]}</td>
|
||||
`;
|
||||
}
|
||||
else if (table === "data_NPM_5channels") {
|
||||
tableHTML += `
|
||||
<td>${columns[0]}</td>
|
||||
<td>${columns[1]}</td>
|
||||
<td>${columns[2]}</td>
|
||||
<td>${columns[3]}</td>
|
||||
<td>${columns[4]}</td>
|
||||
<td>${columns[5]}</td>
|
||||
|
||||
`;
|
||||
} else if (table === "data_envea") {
|
||||
tableHTML += `
|
||||
<td>${columns[0]}</td>
|
||||
<td>${columns[1]}</td>
|
||||
<td>${columns[2]}</td>
|
||||
<td>${columns[3]}</td>
|
||||
<td>${columns[4]}</td>
|
||||
<td>${columns[5]}</td>
|
||||
|
||||
`;
|
||||
}else if (table === "timestamp_table") {
|
||||
tableHTML += `
|
||||
<td>${columns[1]}</td>
|
||||
`;
|
||||
}else if (table === "data_WIND") {
|
||||
tableHTML += `
|
||||
<td>${columns[0]}</td>
|
||||
<td>${columns[1]}</td>
|
||||
<td>${columns[2]}</td>
|
||||
`;
|
||||
}
|
||||
|
||||
tableHTML += "</tr>";
|
||||
});
|
||||
|
||||
tableHTML += `</tbody></table>`;
|
||||
|
||||
// Update the #table_data div with the generated table
|
||||
document.getElementById("table_data").innerHTML = tableHTML;
|
||||
},
|
||||
error: function(xhr, status, error) {
|
||||
console.error('AJAX request failed:', status, error);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
function getSelectedLimit() {
|
||||
return document.getElementById("records_limit").value;
|
||||
}
|
||||
|
||||
function getStartDate() {
|
||||
return document.getElementById("start_date").value || "2025-01-01"; // Default to a safe date
|
||||
}
|
||||
|
||||
function getEndDate() {
|
||||
return document.getElementById("end_date").value || "2025-12-31"; // Default to a safe date
|
||||
}
|
||||
|
||||
function downloadCSV(response, table) {
|
||||
let rows = response.trim().split("\n");
|
||||
|
||||
let csvContent = "";
|
||||
|
||||
// Add headers based on table type
|
||||
if (table === "data_NPM") {
|
||||
csvContent += "TimestampUTC,PM1,PM2.5,PM10,Temperature_sensor,Humidity_sensor\n";
|
||||
} else if (table === "data_BME280") {
|
||||
csvContent += "TimestampUTC,Temperature (°C),Humidity (%),Pressure (hPa)\n";
|
||||
}
|
||||
else if (table === "data_NPM_5channels") {
|
||||
csvContent += "TimestampUTC,PM_ch1,PM_ch2,PM_ch3,PM_ch4,PM_ch5\n";
|
||||
}
|
||||
|
||||
// Format rows as CSV
|
||||
rows.forEach(row => {
|
||||
let columns = row.replace(/[()]/g, "").split(", ");
|
||||
csvContent += columns.join(",") + "\n";
|
||||
});
|
||||
|
||||
// Create a downloadable file
|
||||
let blob = new Blob([csvContent], { type: "text/csv" });
|
||||
let url = window.URL.createObjectURL(blob);
|
||||
let a = document.createElement("a");
|
||||
a.href = url;
|
||||
a.download = table + "_data.csv"; // File name
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
}
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
226
html/index.html
226
html/index.html
@@ -5,6 +5,8 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>NebuleAir</title>
|
||||
<link rel="stylesheet" href="assets/css/bootstrap.min.css">
|
||||
<script src="assets/js/chart.js"></script> <!-- Local Chart.js -->
|
||||
|
||||
<style>
|
||||
body {
|
||||
overflow-x: hidden;
|
||||
@@ -51,25 +53,51 @@
|
||||
<main class="col-md-9 ms-sm-auto col-lg-10 offset-md-3 offset-lg-2 px-md-4">
|
||||
<h1 class="mt-4">Votre capteur</h1>
|
||||
<p>Bienvenue sur votre interface de configuration de votre capteur.</p>
|
||||
|
||||
<div class="row mb-3">
|
||||
|
||||
<div class="col-sm-4">
|
||||
<!-- Card NPM values -->
|
||||
<div class="col-sm-4 mt-2">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Mesures PM</h5>
|
||||
<canvas id="sensorPMChart" style="width: 100%; max-width: 600px; height: 200px;"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Card Linux Stats -->
|
||||
<div class="col-sm-4 mt-2">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Linux stats</h5>
|
||||
|
||||
<p class="card-text">Disk usage (total size <span id="disk_size"></span> Gb) </p>
|
||||
<div id="disk_space"></div>
|
||||
|
||||
<p class="card-text">Memory usage (total size <span id="memory_size"></span> Mb) </p>
|
||||
<div id="memory_space"></div>
|
||||
<p class="card-text"> Database size: <span id="database_size"></span> </p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
<!--
|
||||
<div class="row mb-3">
|
||||
|
||||
<div class="col-sm-4 mt-2">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Mesures Temperature</h5>
|
||||
<canvas id="sensorBME_temp" style="width: 100%; max-width: 600px; height: 200px;"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
-->
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
@@ -107,6 +135,35 @@
|
||||
|
||||
window.onload = function() {
|
||||
|
||||
//NEW way to get data from SQLITE
|
||||
$.ajax({
|
||||
url: 'launcher.php?type=get_config_sqlite',
|
||||
dataType:'json',
|
||||
//dataType: 'json', // Specify that you expect a JSON response
|
||||
method: 'GET', // Use GET or POST depending on your needs
|
||||
success: function(response) {
|
||||
console.log("Getting SQLite config table:");
|
||||
console.log(response);
|
||||
|
||||
//get device Name (for the side bar)
|
||||
const deviceName = response.deviceName;
|
||||
const elements = document.querySelectorAll('.sideBar_sensorName');
|
||||
elements.forEach((element) => {
|
||||
element.innerText = deviceName;
|
||||
});
|
||||
|
||||
//device name html page title
|
||||
if (response.deviceName) {
|
||||
document.title = response.deviceName;
|
||||
}
|
||||
|
||||
},
|
||||
error: function(xhr, status, error) {
|
||||
console.error('AJAX request failed:', status, error);
|
||||
}
|
||||
}); //end ajax
|
||||
|
||||
/* OLD way of getting config data
|
||||
fetch('../config.json') // Replace 'deviceID.txt' with 'config.json'
|
||||
.then(response => response.json()) // Parse response as JSON
|
||||
.then(data => {
|
||||
@@ -124,6 +181,11 @@ window.onload = function() {
|
||||
element.innerText = deviceName;
|
||||
});
|
||||
|
||||
//end fetch config
|
||||
})
|
||||
.catch(error => console.error('Error loading config.json:', error));
|
||||
//end windows on load
|
||||
*/
|
||||
//get local RTC
|
||||
$.ajax({
|
||||
url: 'launcher.php?type=RTC_time',
|
||||
@@ -139,6 +201,34 @@ window.onload = function() {
|
||||
}
|
||||
});
|
||||
|
||||
//get database size
|
||||
$.ajax({
|
||||
url: 'launcher.php?type=database_size',
|
||||
dataType: 'json', // Specify that you expect a JSON response
|
||||
method: 'GET', // Use GET or POST depending on your needs
|
||||
success: function(response) {
|
||||
console.log(response);
|
||||
|
||||
if (response.size_megabytes !== undefined) {
|
||||
// Extract and format the size in MB
|
||||
const databaseSizeMB = response.size_megabytes + " MB";
|
||||
|
||||
// Update the HTML element with the database size
|
||||
const databaseSizeElement = document.getElementById("database_size");
|
||||
databaseSizeElement.textContent = databaseSizeMB;
|
||||
|
||||
console.log("Database size:", databaseSizeMB);
|
||||
} else if (response.error) {
|
||||
// Handle errors from the PHP response
|
||||
console.error("Error from server:", response.error);
|
||||
}
|
||||
},
|
||||
error: function(xhr, status, error) {
|
||||
console.error('AJAX request failed:', status, error);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
//get disk free space
|
||||
$.ajax({
|
||||
url: 'launcher.php?type=linux_disk',
|
||||
@@ -210,8 +300,6 @@ window.onload = function() {
|
||||
console.log(usedMemory);
|
||||
console.log(percentageUsed);
|
||||
|
||||
|
||||
|
||||
// Create the outer div with class and attributes
|
||||
const progressDiv = document.createElement('div');
|
||||
progressDiv.className = 'progress mb-3';
|
||||
@@ -240,9 +328,133 @@ window.onload = function() {
|
||||
});
|
||||
|
||||
|
||||
// GET NPM SQLite values
|
||||
$.ajax({
|
||||
url: 'launcher.php?type=get_npm_sqlite_data',
|
||||
dataType: 'json', // Specify that you expect a JSON response
|
||||
method: 'GET', // Use GET or POST depending on your needs
|
||||
success: function(response) {
|
||||
console.log(response);
|
||||
updatePMChart(response);
|
||||
},
|
||||
error: function(xhr, status, error) {
|
||||
console.error('AJAX request failed:', status, error);
|
||||
}
|
||||
});
|
||||
|
||||
let chart; // Store the Chart.js instance globally
|
||||
|
||||
function updatePMChart(data) {
|
||||
const labels = data.map(d => d.timestamp);
|
||||
const PM1 = data.map(d => d.PM1);
|
||||
const PM25 = data.map(d => d.PM25);
|
||||
const PM10 = data.map(d => d.PM10);
|
||||
|
||||
const ctx = document.getElementById('sensorPMChart').getContext('2d');
|
||||
|
||||
if (!chart) {
|
||||
chart = new Chart(ctx, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: labels,
|
||||
datasets: [
|
||||
{
|
||||
label: "PM1",
|
||||
data: PM1,
|
||||
borderColor: "rgba(0, 51, 153, 1)",
|
||||
backgroundColor: "rgba(0, 51, 153, 0.2)", // Very light blue background
|
||||
fill: true,
|
||||
tension: 0.4, // Smooth curves
|
||||
pointRadius: 2, // Larger points
|
||||
pointHoverRadius: 6 // Bigger hover points
|
||||
},
|
||||
{
|
||||
label: "PM2.5",
|
||||
data: PM25,
|
||||
borderColor: "rgba(30, 144, 255, 1)",
|
||||
backgroundColor: "rgba(30, 144, 255, 0.2)", // Very light medium blue background
|
||||
fill: true,
|
||||
tension: 0.4,
|
||||
pointRadius: 2,
|
||||
pointHoverRadius: 6
|
||||
},
|
||||
{
|
||||
label: "PM10",
|
||||
data: PM10,
|
||||
borderColor: "rgba(135, 206, 250, 1)",
|
||||
backgroundColor: "rgba(135, 206, 250, 0.2)", // Very light blue background
|
||||
fill: true,
|
||||
tension: 0.4,
|
||||
pointRadius: 2,
|
||||
pointHoverRadius: 6
|
||||
}
|
||||
]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: true,
|
||||
plugins: {
|
||||
legend: {
|
||||
position: 'top'
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Time (UTC)',
|
||||
font: {
|
||||
size: 16,
|
||||
family: 'Arial, sans-serif'
|
||||
},
|
||||
color: '#4A4A4A'
|
||||
},
|
||||
ticks: {
|
||||
autoSkip: true,
|
||||
maxTicksLimit: 5,
|
||||
color: '#4A4A4A',
|
||||
callback: function(value, index) {
|
||||
// Access the correct label from the `labels` array
|
||||
const label = labels[index]; // Use the original `labels` array
|
||||
if (label && typeof label === 'string' && label.includes(' ')) {
|
||||
return label.split(' ')[1].slice(0, 5); // Extract "HH:MM"
|
||||
}
|
||||
return value; // Fallback for invalid labels
|
||||
}
|
||||
},
|
||||
grid: {
|
||||
display: false // Remove gridlines for a cleaner look
|
||||
}
|
||||
|
||||
|
||||
},
|
||||
y: {
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Values (µg/m³)',
|
||||
font: {
|
||||
size: 16,
|
||||
family: 'Arial, sans-serif'
|
||||
},
|
||||
color: '#4A4A4A'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
});
|
||||
} else {
|
||||
chart.data.labels = labels;
|
||||
chart.data.datasets[0].data = PM1;
|
||||
chart.data.datasets[1].data = PM25;
|
||||
chart.data.datasets[2].data = PM10;
|
||||
chart.update();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
})
|
||||
.catch(error => console.error('Error loading config.json:', error));
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
198
html/logs.html
198
html/logs.html
@@ -56,8 +56,11 @@
|
||||
<div class="col-lg-6 col-12">
|
||||
<div class="card" style="height: 80vh;">
|
||||
<div class="card-header">
|
||||
Loop logs <button type="submit" class="btn btn-secondary btn-sm" onclick="clear_loopLogs()">Clear</button>
|
||||
Sara logs
|
||||
<button type="submit" class="btn btn-secondary btn-sm" id="refresh-master-log">Refresh</button>
|
||||
<button type="submit" class="btn btn-secondary btn-sm" onclick="clear_loopLogs()">Clear</button>
|
||||
|
||||
<span id="script_running"></span>
|
||||
</div>
|
||||
<div class="card-body overflow-auto" id="card_loop_content">
|
||||
|
||||
@@ -69,6 +72,7 @@
|
||||
<div class="card" style="height: 80vh;">
|
||||
<div class="card-header">
|
||||
Boot logs
|
||||
<button type="submit" class="btn btn-secondary btn-sm" id="refresh-boot-log">Refresh</button>
|
||||
</div>
|
||||
<div class="card-body overflow-auto" id="card_boot_content">
|
||||
|
||||
@@ -110,77 +114,54 @@
|
||||
const loop_card_content = document.getElementById('card_loop_content');
|
||||
const boot_card_content = document.getElementById('card_boot_content');
|
||||
|
||||
fetch('../logs/loop.log')
|
||||
.then((response) => {
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch the log file.');
|
||||
}
|
||||
return response.text();
|
||||
})
|
||||
.then((data) => {
|
||||
const lines = data.split('\n');
|
||||
//Getting Master logs
|
||||
console.log("Getting SARA logs");
|
||||
displayLogFile('../logs/sara_service.log', loop_card_content, true, 1000);
|
||||
|
||||
// Format log content
|
||||
const formattedLog = lines
|
||||
.map((line) => line.trim()) // Remove extra whitespace
|
||||
.filter((line) => line) // Remove empty lines
|
||||
.join('<br>'); // Join formatted lines with line breaks
|
||||
console.log("Getting app/boot logs");
|
||||
displayLogFile('../logs/app.log', boot_card_content, true, 1000);
|
||||
|
||||
loop_card_content.innerHTML = `<pre style="white-space: pre-wrap; word-wrap: break-word; margin: 0;">${formattedLog}</pre>`;
|
||||
loop_card_content.scrollTop = loop_card_content.scrollHeight; // Scroll to the bottom
|
||||
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
loop_card_content.textContent = 'Error loading log file.';
|
||||
});
|
||||
|
||||
fetch('../logs/app.log')
|
||||
.then((response) => {
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch the log file.');
|
||||
}
|
||||
return response.text();
|
||||
})
|
||||
.then((data) => {
|
||||
const lines = data.split('\n');
|
||||
|
||||
// Format log content
|
||||
const formattedLog = lines
|
||||
.map((line) => line.trim()) // Remove extra whitespace
|
||||
.filter((line) => line) // Remove empty lines
|
||||
.join('<br>'); // Join formatted lines with line breaks
|
||||
|
||||
boot_card_content.innerHTML = `<pre style="white-space: pre-wrap; word-wrap: break-word; margin: 0;">${formattedLog}</pre>`;
|
||||
boot_card_content.scrollTop = loop_card_content.scrollHeight; // Scroll to the bottom
|
||||
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
boot_card_content.textContent = 'Error loading log file.';
|
||||
});
|
||||
// Setup master log with refresh button
|
||||
setupLogRefreshButton('refresh-master-log', '../logs/sara_service.log', 'card_loop_content', 3000);
|
||||
|
||||
// Setup boot log with refresh button
|
||||
setupLogRefreshButton('refresh-boot-log', '../logs/app.log', 'card_boot_content', 300);
|
||||
|
||||
|
||||
});
|
||||
|
||||
|
||||
window.onload = function() {
|
||||
fetch('../config.json') // Replace 'deviceID.txt' with 'config.json'
|
||||
.then(response => response.json()) // Parse response as JSON
|
||||
.then(data => {
|
||||
console.log("Getting config file (onload)");
|
||||
//get device ID
|
||||
const deviceID = data.deviceID.trim().toUpperCase();
|
||||
// document.getElementById('pageTitle_plus_ID').innerText = 'token: ' + deviceID;
|
||||
//get device Name
|
||||
const deviceName = data.deviceName;
|
||||
getModem_busy_status();
|
||||
setInterval(getModem_busy_status, 2000);
|
||||
|
||||
//NEW way to get config (SQLite)
|
||||
$.ajax({
|
||||
url: 'launcher.php?type=get_config_sqlite',
|
||||
dataType:'json',
|
||||
//dataType: 'json', // Specify that you expect a JSON response
|
||||
method: 'GET', // Use GET or POST depending on your needs
|
||||
success: function(response) {
|
||||
console.log("Getting SQLite config table:");
|
||||
console.log(response);
|
||||
|
||||
//device name_side bar
|
||||
const elements = document.querySelectorAll('.sideBar_sensorName');
|
||||
elements.forEach((element) => {
|
||||
element.innerText = deviceName;
|
||||
element.innerText = response.deviceName;
|
||||
});
|
||||
|
||||
//device name html page title
|
||||
if (response.deviceName) {
|
||||
document.title = response.deviceName;
|
||||
}
|
||||
|
||||
},
|
||||
error: function(xhr, status, error) {
|
||||
console.error('AJAX request failed:', status, error);
|
||||
}
|
||||
});//end AJAX
|
||||
|
||||
|
||||
//get local RTC
|
||||
$.ajax({
|
||||
@@ -197,10 +178,78 @@ window.onload = function() {
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
}//end onload
|
||||
|
||||
function displayLogFile(logFilePath, containerElement, scrollToBottom = true, maxLines = 0) {
|
||||
// Show loading indicator
|
||||
containerElement.innerHTML = '<div class="text-center"><i>Loading log file...</i></div>';
|
||||
|
||||
return fetch(logFilePath)
|
||||
.then((response) => {
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch the log file: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
return response.text();
|
||||
})
|
||||
.catch(error => console.error('Error loading config.json:', error));
|
||||
.then((data) => {
|
||||
// Split the log into lines
|
||||
let lines = data.split('\n');
|
||||
|
||||
// Apply max lines limit if specified
|
||||
if (maxLines > 0 && lines.length > maxLines) {
|
||||
lines = lines.slice(-maxLines); // Get only the last N lines
|
||||
}
|
||||
|
||||
// Format log content
|
||||
const formattedLog = lines
|
||||
.map((line) => line.trim()) // Remove extra whitespace
|
||||
.filter((line) => line) // Remove empty lines
|
||||
.join('<br>'); // Join formatted lines with line breaks
|
||||
|
||||
// Display the formatted log
|
||||
containerElement.innerHTML = `<pre style="white-space: pre-wrap; word-wrap: break-word; margin: 0;">${formattedLog}</pre>`;
|
||||
|
||||
// Scroll to bottom if requested
|
||||
if (scrollToBottom) {
|
||||
containerElement.scrollTop = containerElement.scrollHeight;
|
||||
}
|
||||
|
||||
return formattedLog; // Return the formatted log in case the caller needs it
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(`Error loading log file ${logFilePath}:`, error);
|
||||
containerElement.innerHTML = `<div class="text-danger">Error loading log file: ${error.message}</div>`;
|
||||
throw error; // Re-throw the error for the caller to handle if needed
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up a refresh button for a log file
|
||||
* @param {string} buttonId - ID of the button element
|
||||
* @param {string} logFilePath - Path to the log file
|
||||
* @param {string} containerId - ID of the container to display the log in
|
||||
* @param {number} maxLines - Maximum number of lines to display (0 for all)
|
||||
*/
|
||||
function setupLogRefreshButton(buttonId, logFilePath, containerId, maxLines = 0) {
|
||||
console.log("Refreshing logs");
|
||||
|
||||
const button = document.getElementById(buttonId);
|
||||
const container = document.getElementById(containerId);
|
||||
|
||||
if (!button || !container) {
|
||||
console.error('Button or container element not found');
|
||||
return;
|
||||
}
|
||||
|
||||
// Initial load
|
||||
displayLogFile(logFilePath, container, true, maxLines);
|
||||
|
||||
// Set up button click handler
|
||||
button.addEventListener('click', () => {
|
||||
displayLogFile(logFilePath, container, true, maxLines);
|
||||
});
|
||||
}
|
||||
|
||||
function clear_loopLogs(){
|
||||
console.log("Clearing loop logs");
|
||||
@@ -221,6 +270,37 @@ function clear_loopLogs(){
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function getModem_busy_status() {
|
||||
//console.log("Getting modem busy status");
|
||||
|
||||
const script_is_running = document.getElementById("script_running");
|
||||
|
||||
$.ajax({
|
||||
url: 'launcher.php?type=getModem_busy',
|
||||
dataType: 'json', // Expecting JSON response
|
||||
method: 'GET',
|
||||
success: function(response) {
|
||||
//console.log(response);
|
||||
|
||||
if (response.running) {
|
||||
// Script is running → Show the Bootstrap spinner
|
||||
script_is_running.innerHTML = `
|
||||
<div class="spinner-border spinner-border-sm text-danger" role="status">
|
||||
<span class="visually-hidden">Modem is busy...</span>
|
||||
</div>
|
||||
`;
|
||||
} else {
|
||||
// Script is NOT running → Show a success message (no spinner)
|
||||
script_is_running.innerHTML = ``;
|
||||
}
|
||||
},
|
||||
error: function(xhr, status, error) {
|
||||
console.error('AJAX request failed:', status, error);
|
||||
script_is_running.innerHTML = `<span class="text-warning">Error checking status ⚠️</span>`;
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
</body>
|
||||
|
||||
265
html/map.html
Executable file
265
html/map.html
Executable file
@@ -0,0 +1,265 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>NebuleAir</title>
|
||||
<link rel="stylesheet" href="assets/css/bootstrap.min.css">
|
||||
<link rel="stylesheet" href="assets/leaflet/leaflet.css" />
|
||||
<script src="assets/leaflet/leaflet.js"></script>
|
||||
|
||||
|
||||
<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;
|
||||
}
|
||||
</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">
|
||||
<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">Localisation</h1>
|
||||
|
||||
<div class="row">
|
||||
|
||||
|
||||
<div class="col-sm-6 mb-2">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h3 class="mt-1">Zone localisation du capteur</h3>
|
||||
<p class="card-text">Mis à jour automatiquement par le capteur. </p>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-6 mb-3">
|
||||
<label for="device_name" class="form-label">Latitude</label>
|
||||
<input type="text" class="form-control" id="device_latitude_raw" disabled>
|
||||
</div>
|
||||
<div class="col-6 mb-3">
|
||||
<label for="device_name" class="form-label">Longitude</label>
|
||||
<input type="text" class="form-control" id="device_longitude_raw" disabled>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="col-sm-6 mb-2">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h3 class="mt-1">Point précis</h3>
|
||||
<p class="card-text">Mis à jour manuellement (sur aircarto.fr ou sur cette interface si le capteur est connecté au WIFI) </p>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-6 mb-3">
|
||||
<label for="device_name" class="form-label">Latitude</label>
|
||||
<input type="text" class="form-control" id="device_latitude_precision" onchange="update_config('latitude_precision', this.value)">
|
||||
</div>
|
||||
<div class="col-6 mb-3">
|
||||
<label for="device_name" class="form-label">Longitude</label>
|
||||
<input type="text" class="form-control" id="device_longitude_precision" onchange="update_config('longitude_precision', this.value)">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div class="row">
|
||||
<div id="map" style="height: 70vh;"></div>
|
||||
</div>
|
||||
|
||||
</main>
|
||||
</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));
|
||||
});
|
||||
|
||||
|
||||
|
||||
});
|
||||
|
||||
let map;
|
||||
let marker;
|
||||
|
||||
// Function to load and update map
|
||||
function loadConfigAndUpdateMap() {
|
||||
fetch('../config.json')
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
console.log("Getting config file (update)");
|
||||
|
||||
// Get device details
|
||||
const deviceID = data.deviceID.trim().toUpperCase();
|
||||
const deviceName = data.deviceName;
|
||||
const device_latitude_precision = parseFloat(data.latitude_precision);
|
||||
const device_longitude_precision = parseFloat(data.longitude_precision);
|
||||
const device_latitude_raw = parseFloat(data.latitude_raw);
|
||||
const device_longitude_raw = parseFloat(data.longitude_raw);
|
||||
|
||||
console.log("Latitude (precision): " + device_latitude_precision);
|
||||
console.log("Longitude (precision): " + device_longitude_precision);
|
||||
|
||||
// Update input fields
|
||||
document.getElementById("device_latitude_precision").value = device_latitude_precision;
|
||||
document.getElementById("device_longitude_precision").value = device_longitude_precision;
|
||||
document.getElementById("device_latitude_raw").value = device_latitude_raw;
|
||||
document.getElementById("device_longitude_raw").value = device_longitude_raw;
|
||||
|
||||
// If map is not initialized, create it
|
||||
if (!map) {
|
||||
map = L.map('map').setView([device_latitude_precision, device_longitude_precision], 15);
|
||||
|
||||
L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||
maxZoom: 19,
|
||||
attribution: '© <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>'
|
||||
}).addTo(map);
|
||||
|
||||
// Add draggable marker (point precision)
|
||||
marker = L.marker([device_latitude_precision, device_longitude_precision], { draggable: true }).addTo(map);
|
||||
|
||||
//add a circle
|
||||
var circle = L.circle([device_latitude_raw, device_longitude_raw], {
|
||||
color: 'blue',
|
||||
fillColor: '#3399FF',
|
||||
fillOpacity: 0.3,
|
||||
radius: 500
|
||||
}).addTo(map);
|
||||
|
||||
// Event listener when marker is moved
|
||||
marker.on('dragend', function (event) {
|
||||
let newLatLng = marker.getLatLng();
|
||||
console.log("Marker moved to:", newLatLng.lat, newLatLng.lng);
|
||||
|
||||
// Update the input fields with new values
|
||||
document.getElementById("device_latitude_precision").value = newLatLng.lat;
|
||||
document.getElementById("device_longitude_precision").value = newLatLng.lng;
|
||||
|
||||
// Call update function to save new values
|
||||
update_config('latitude_precision', newLatLng.lat);
|
||||
|
||||
setTimeout(() => { update_config('longitude_precision', newLatLng.lng); }, 750);
|
||||
});
|
||||
|
||||
} else {
|
||||
// If the map already exists, update position
|
||||
map.setView([device_latitude, device_longitude], 9);
|
||||
|
||||
// Move marker
|
||||
marker.setLatLng([device_latitude, device_longitude]);
|
||||
}
|
||||
|
||||
// Update device name in sidebar
|
||||
document.querySelectorAll('.sideBar_sensorName').forEach((element) => {
|
||||
element.innerText = deviceName;
|
||||
});
|
||||
|
||||
// Get local RTC time
|
||||
$.ajax({
|
||||
url: 'launcher.php?type=RTC_time',
|
||||
dataType: 'text',
|
||||
method: 'GET',
|
||||
success: function(response) {
|
||||
console.log("Local RTC: " + response);
|
||||
document.getElementById("RTC_time").textContent = response;
|
||||
},
|
||||
error: function(xhr, status, error) {
|
||||
console.error('AJAX request failed:', status, error);
|
||||
}
|
||||
});
|
||||
})
|
||||
.catch(error => console.error('Error loading config.json:', error));
|
||||
}
|
||||
|
||||
// Function to update config and refresh the map
|
||||
function update_config(param, value) {
|
||||
console.log("Updating ", param, " : ", value);
|
||||
$.ajax({
|
||||
url: 'launcher.php?type=update_config¶m=' + param + '&value=' + value,
|
||||
dataType: 'text',
|
||||
method: 'GET',
|
||||
cache: false,
|
||||
success: function(response) {
|
||||
console.log(response);
|
||||
},
|
||||
error: function(xhr, status, error) {
|
||||
console.error('AJAX request failed:', status, error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Load config and initialize map on page load
|
||||
window.onload = function () {
|
||||
loadConfigAndUpdateMap();
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
348
html/saraR4.html
348
html/saraR4.html
@@ -50,12 +50,21 @@
|
||||
<!-- 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">Modem 4G</h1>
|
||||
<h4 id="modem_version"></h4>
|
||||
<p>Votre capteur est équipé d'un modem 4G et d'une carte SIM afin d'envoyer les mesures sur internet.</p>
|
||||
|
||||
<div class="form-check form-switch mb-2">
|
||||
<input class="form-check-input" type="checkbox" role="switch" id="check_modem_configMode" onchange="update_modem_configMode('modem_config_mode',this.checked)">
|
||||
<label class="form-check-label" for="check_modem_configMode">Mode configuration</label>
|
||||
</div>
|
||||
|
||||
<span id="modem_status_message"></span>
|
||||
<!--
|
||||
<h3>
|
||||
Status
|
||||
<span id="modem-status" class="badge">Loading...</span>
|
||||
</h3>
|
||||
|
||||
-->
|
||||
<div class="row mb-3">
|
||||
|
||||
<div class="col-sm-3">
|
||||
@@ -63,7 +72,7 @@
|
||||
|
||||
<div class="card-body">
|
||||
<p class="card-text">General information. </p>
|
||||
<button class="btn btn-primary" onclick="getData_saraR4('ttyAMA2', 'ATI', 2)">Get Data</button>
|
||||
<button class="btn btn-primary" onclick="getData_saraR4('ttyAMA2', 'ATI', 1)">Get Data</button>
|
||||
<div id="loading_ttyAMA2_ATI" class="spinner-border spinner-border-sm" style="display: none;" role="status"></div>
|
||||
<div id="response_ttyAMA2_ATI"></div>
|
||||
|
||||
@@ -71,19 +80,19 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-3">
|
||||
<div class="col-sm-2">
|
||||
<div class="card">
|
||||
|
||||
<div class="card-body">
|
||||
<p class="card-text">SIM card information.</p>
|
||||
<button class="btn btn-primary" onclick="getData_saraR4('ttyAMA2', 'AT+CCID?', 2)">Get Data</button>
|
||||
<button class="btn btn-primary" onclick="getData_saraR4('ttyAMA2', 'AT+CCID?', 1)">Get Data</button>
|
||||
<div id="loading_ttyAMA2_AT_CCID_" class="spinner-border spinner-border-sm" style="display: none;" role="status"></div>
|
||||
<div id="response_ttyAMA2_AT_CCID_"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-3">
|
||||
<div class="col-sm-2">
|
||||
<div class="card">
|
||||
|
||||
<div class="card-body">
|
||||
@@ -97,11 +106,11 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-3">
|
||||
<div class="col-sm-2">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<p class="card-text">Signal strength </p>
|
||||
<button class="btn btn-primary" onclick="getData_saraR4('ttyAMA2', 'AT+CSQ', 2)">Get Data</button>
|
||||
<button class="btn btn-primary" onclick="getData_saraR4('ttyAMA2', 'AT+CSQ', 1)">Get Data</button>
|
||||
<div id="loading_ttyAMA2_AT_CSQ" class="spinner-border spinner-border-sm" style="display: none;" role="status"></div>
|
||||
<div id="response_ttyAMA2_AT_CSQ"></div>
|
||||
</table>
|
||||
@@ -109,6 +118,18 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-2">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<p class="card-text">Modem Reset </p>
|
||||
<button class="btn btn-danger" onclick="getData_saraR4('ttyAMA2', 'AT+CFUN=15', 1)">Reset</button>
|
||||
<div id="loading_ttyAMA2_AT_CFUN_15" class="spinner-border spinner-border-sm" style="display: none;" role="status"></div>
|
||||
<div id="response_ttyAMA2_AT_CFUN_15"></div>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<h3>Connexion 4G Network</h3>
|
||||
@@ -217,6 +238,24 @@
|
||||
</div>
|
||||
</div>
|
||||
-->
|
||||
<h3>Test HTTP server comm.</h3>
|
||||
<div class="row mb-3">
|
||||
<!-- SET URL -->
|
||||
|
||||
<div class="col-sm-4">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<p class="card-text">Test communication with the server.</p>
|
||||
<button class="btn btn-primary" onclick="ping_test()">Test</button>
|
||||
<div id="loading_ping" class="spinner-border spinner-border-sm" style="display: none;" role="status"></div>
|
||||
<div id="response_ping"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
<h3>Send message (test)</h3>
|
||||
<div class="row mb-3">
|
||||
<!-- SET URL -->
|
||||
@@ -267,6 +306,19 @@
|
||||
|
||||
</div>
|
||||
|
||||
<!-- toast -->
|
||||
|
||||
<div class="toast-container position-fixed bottom-0 end-0 p-3">
|
||||
<div id="liveToast" class="toast align-items-center text-bg-primary border-1" role="alert" aria-live="assertive" aria-atomic="true">
|
||||
<div class="d-flex">
|
||||
<div class="toast-body">
|
||||
Hello, world! This is a toast message.
|
||||
</div>
|
||||
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast" aria-label="Close"></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
@@ -279,7 +331,7 @@
|
||||
<script src="assets/js/bootstrap.bundle.js"></script>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
const elementsToLoad = [
|
||||
{ id: 'topbar', file: 'topbar.html' },
|
||||
{ id: 'sidebar', file: 'sidebar.html' },
|
||||
@@ -297,8 +349,100 @@
|
||||
})
|
||||
.catch(error => console.error(`Error loading ${file}:`, error));
|
||||
});
|
||||
|
||||
//OLD way to retreive data from JSON
|
||||
/*
|
||||
fetch('../config.json') // Replace 'deviceID.txt' with 'config.json'
|
||||
.then(response => response.json()) // Parse response as JSON
|
||||
.then(data => {
|
||||
console.log("Getting config file (onload)");
|
||||
//modem config mode
|
||||
const check_modem_configMode = document.getElementById("check_modem_configMode");
|
||||
check_modem_configMode.checked = data.modem_config_mode;
|
||||
console.log("Modem configuration: " + data.modem_config_mode);
|
||||
})
|
||||
*/
|
||||
|
||||
//NEW way to get data from SQLITE
|
||||
$.ajax({
|
||||
url: 'launcher.php?type=get_config_sqlite',
|
||||
dataType:'json',
|
||||
//dataType: 'json', // Specify that you expect a JSON response
|
||||
method: 'GET', // Use GET or POST depending on your needs
|
||||
success: function(response) {
|
||||
console.log("Getting SQLite config table:");
|
||||
console.log(response);
|
||||
//modem_version
|
||||
const modem_version_html = document.getElementById("modem_version");
|
||||
modem_version_html.innerText = response.modem_version;
|
||||
|
||||
// Set checkbox state based on the response data
|
||||
const check_modem_configMode = document.getElementById("check_modem_configMode");
|
||||
if (check_modem_configMode) {
|
||||
check_modem_configMode.checked = response.modem_config_mode;
|
||||
console.log("Modem configuration: " + response.modem_config_mode);
|
||||
} else {
|
||||
console.error("Checkbox element not found");
|
||||
}
|
||||
|
||||
},
|
||||
error: function(xhr, status, error) {
|
||||
console.error('AJAX request failed:', status, error);
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
window.onload = function() {
|
||||
getModem_busy_status();
|
||||
setInterval(getModem_busy_status, 1000);
|
||||
|
||||
//NEW way to get config (SQLite)
|
||||
$.ajax({
|
||||
url: 'launcher.php?type=get_config_sqlite',
|
||||
dataType:'json',
|
||||
//dataType: 'json', // Specify that you expect a JSON response
|
||||
method: 'GET', // Use GET or POST depending on your needs
|
||||
success: function(response) {
|
||||
console.log("Getting SQLite config table:");
|
||||
console.log(response);
|
||||
//device name_side bar
|
||||
const elements = document.querySelectorAll('.sideBar_sensorName');
|
||||
elements.forEach((element) => {
|
||||
element.innerText = response.deviceName;
|
||||
});
|
||||
//device name html page title
|
||||
if (response.deviceName) {
|
||||
document.title = response.deviceName;
|
||||
}
|
||||
},
|
||||
error: function(xhr, status, error) {
|
||||
console.error('AJAX request failed:', status, error);
|
||||
}
|
||||
});//end AJAX
|
||||
|
||||
|
||||
//get local RTC
|
||||
$.ajax({
|
||||
url: 'launcher.php?type=RTC_time',
|
||||
dataType: 'text', // Specify that you expect a JSON response
|
||||
method: 'GET', // Use GET or POST depending on your needs
|
||||
success: function(response) {
|
||||
console.log("Local RTC: " + response);
|
||||
const RTC_Element = document.getElementById("RTC_time");
|
||||
RTC_Element.textContent = response;
|
||||
|
||||
},
|
||||
error: function(xhr, status, error) {
|
||||
console.error('AJAX request failed:', status, error);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
function getData_saraR4(port, command, timeout){
|
||||
console.log("Data from SaraR4");
|
||||
console.log("Port: " + port );
|
||||
@@ -312,6 +456,7 @@ function getData_saraR4(port, command, timeout){
|
||||
|
||||
$.ajax({
|
||||
url: 'launcher.php?type=sara&port='+port+'&command='+encodeURIComponent(command)+'&timeout='+timeout,
|
||||
dataType:'text',
|
||||
//dataType: 'json', // Specify that you expect a JSON response
|
||||
method: 'GET', // Use GET or POST depending on your needs
|
||||
success: function(response) {
|
||||
@@ -379,8 +524,10 @@ function getData_saraR4(port, command, timeout){
|
||||
} else{
|
||||
// si c'est une commande AT normale
|
||||
// Replace newline characters with <br> tags
|
||||
const formattedResponse = response.replace(/\n/g, "<br>");
|
||||
const formattedResponse = response.replace(/\n/g, "<br>")
|
||||
.replace(/\b(OK)\b/g, '<span style="color: green; font-weight: bold;">$1</span>');;
|
||||
$("#response_"+port+"_"+safeCommand).html(formattedResponse);
|
||||
|
||||
}
|
||||
},
|
||||
error: function(xhr, status, error) {
|
||||
@@ -389,11 +536,12 @@ function getData_saraR4(port, command, timeout){
|
||||
});
|
||||
}
|
||||
|
||||
function connectNetwork_saraR4(port, networkID, timeout){
|
||||
function connectNetwork_saraR4(port, networkID, timeout){
|
||||
console.log(" Connect to network (port "+port+" and network id "+networkID+"):");
|
||||
$("#loading_"+port+"_AT_COPS_Connect").show();
|
||||
$.ajax({
|
||||
url: 'launcher.php?type=sara_connectNetwork&port='+port+'&networkID='+encodeURIComponent(networkID)+'&timeout='+timeout,
|
||||
dataType:'text',
|
||||
//dataType: 'json', // Specify that you expect a JSON response
|
||||
method: 'GET', // Use GET or POST depending on your needs
|
||||
success: function(response) {
|
||||
@@ -410,11 +558,12 @@ function getData_saraR4(port, command, timeout){
|
||||
});
|
||||
}
|
||||
|
||||
function mqtt_getConfig_saraR4(port, timeout){
|
||||
function mqtt_getConfig_saraR4(port, timeout){
|
||||
console.log("GET MQTT config (port "+port+"):");
|
||||
$("#loading_mqtt_getConfig").show();
|
||||
$.ajax({
|
||||
url: 'launcher.php?type=sara_getMQTT_config&port='+port+'&timeout='+timeout,
|
||||
dataType:'text',
|
||||
//dataType: 'json', // Specify that you expect a JSON response
|
||||
method: 'GET', // Use GET or POST depending on your needs
|
||||
success: function(response) {
|
||||
@@ -430,11 +579,12 @@ function getData_saraR4(port, command, timeout){
|
||||
});
|
||||
}
|
||||
|
||||
function mqtt_login_logout(port, login_logout, timeout){
|
||||
function mqtt_login_logout(port, login_logout, timeout){
|
||||
console.log("GET MQTT login / logout (port "+port+"):");
|
||||
$("#loading_mqtt_login_logout").show();
|
||||
$.ajax({
|
||||
url: 'launcher.php?type=sara_getMQTT_login_logout&port='+port+'&login_logout='+login_logout+'&timeout='+timeout,
|
||||
dataType:'text',
|
||||
//dataType: 'json', // Specify that you expect a JSON response
|
||||
method: 'GET', // Use GET or POST depending on your needs
|
||||
success: function(response) {
|
||||
@@ -468,7 +618,7 @@ function getData_saraR4(port, command, timeout){
|
||||
|
||||
} else {
|
||||
console.log("No matching line found");
|
||||
}
|
||||
}
|
||||
|
||||
},
|
||||
error: function(xhr, status, error) {
|
||||
@@ -477,11 +627,12 @@ function getData_saraR4(port, command, timeout){
|
||||
});
|
||||
}
|
||||
|
||||
function mqtt_publish(port, message, timeout){
|
||||
function mqtt_publish(port, message, timeout){
|
||||
console.log(" MQTT publish (port "+port+"):");
|
||||
$("#loading_mqtt_publish").show();
|
||||
$.ajax({
|
||||
url: 'launcher.php?type=sara_MQTT_publish&port='+port+'&timeout='+timeout+'&message='+message,
|
||||
dataType: 'text',
|
||||
//dataType: 'json', // Specify that you expect a JSON response
|
||||
method: 'GET', // Use GET or POST depending on your needs
|
||||
success: function(response) {
|
||||
@@ -497,11 +648,12 @@ function getData_saraR4(port, command, timeout){
|
||||
});
|
||||
}
|
||||
|
||||
function setURL_saraR4(port, url){
|
||||
function setURL_saraR4(port, url){
|
||||
console.log("Set URL for HTTP (port "+port+" and URL "+url+"):");
|
||||
$("#loading_"+port+"_setURL").show();
|
||||
$.ajax({
|
||||
url: 'launcher.php?type=sara_setURL&port='+port+'&url='+encodeURIComponent(url),
|
||||
dataType: 'text',
|
||||
//dataType: 'json', // Specify that you expect a JSON response
|
||||
method: 'GET', // Use GET or POST depending on your needs
|
||||
success: function(response) {
|
||||
@@ -517,11 +669,33 @@ function getData_saraR4(port, command, timeout){
|
||||
});
|
||||
}
|
||||
|
||||
function writeMessage_saraR4(port, message, type){
|
||||
function ping_test(port, url){
|
||||
console.log("Test ping to data.nebuleair.fr:");
|
||||
$("#loading_ping").show();
|
||||
$.ajax({
|
||||
url: 'launcher.php?type=sara_ping',
|
||||
dataType: 'text',
|
||||
//dataType: 'json', // Specify that you expect a JSON response
|
||||
method: 'GET', // Use GET or POST depending on your needs
|
||||
success: function(response) {
|
||||
console.log(response);
|
||||
$("#loading_ping").hide();
|
||||
// Replace newline characters with <br> tags
|
||||
const formattedResponse = response.replace(/\n/g, "<br>");
|
||||
$("#response_ping").html(formattedResponse);
|
||||
},
|
||||
error: function(xhr, status, error) {
|
||||
console.error('AJAX request failed:', status, error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function writeMessage_saraR4(port, message, type){
|
||||
console.log(type +" message to SARA R4 memory (port "+port+" and message "+message+"):");
|
||||
$("#loading_"+port+"_message_write").show();
|
||||
$.ajax({
|
||||
url: 'launcher.php?type=sara_writeMessage&port='+port+'&message='+encodeURIComponent(message)+'&type2='+type,
|
||||
dataType: 'text',
|
||||
//dataType: 'json', // Specify that you expect a JSON response
|
||||
method: 'GET', // Use GET or POST depending on your needs
|
||||
success: function(response) {
|
||||
@@ -537,7 +711,7 @@ function getData_saraR4(port, command, timeout){
|
||||
});
|
||||
}
|
||||
|
||||
function sendMessage_saraR4(port, endpoint){
|
||||
function sendMessage_saraR4(port, endpoint){
|
||||
|
||||
console.log("Send message from SaraR4 (port "+port+" and endpoint "+endpoint+"):");
|
||||
|
||||
@@ -545,6 +719,7 @@ function getData_saraR4(port, command, timeout){
|
||||
|
||||
$.ajax({
|
||||
url: 'launcher.php?type=sara_sendMessage&port='+port+'&endpoint='+encodeURIComponent(endpoint),
|
||||
dataType: 'text',
|
||||
//dataType: 'json', // Specify that you expect a JSON response
|
||||
method: 'GET', // Use GET or POST depending on your needs
|
||||
success: function(response) {
|
||||
@@ -560,7 +735,7 @@ function getData_saraR4(port, command, timeout){
|
||||
});
|
||||
}
|
||||
|
||||
function connectAPN_saraR4(port, APN_address, timeout){
|
||||
function connectAPN_saraR4(port, APN_address, timeout){
|
||||
|
||||
console.log(" Set APN (port "+port+" and adress "+APN_address+"):");
|
||||
|
||||
@@ -569,6 +744,7 @@ function getData_saraR4(port, command, timeout){
|
||||
$.ajax({
|
||||
url: 'launcher.php?type=sara_APN&port='+port+'&APN_address='+encodeURIComponent(APN_address)+'&timeout='+timeout,
|
||||
//dataType: 'json', // Specify that you expect a JSON response
|
||||
dataType: 'text',
|
||||
method: 'GET', // Use GET or POST depending on your needs
|
||||
success: function(response) {
|
||||
console.log(response);
|
||||
@@ -582,61 +758,113 @@ function getData_saraR4(port, command, timeout){
|
||||
console.error('AJAX request failed:', status, error);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function getModem_busy_status() {
|
||||
//console.log("Getting modem busy status");
|
||||
|
||||
window.onload = function() {
|
||||
fetch('../config.json') // Replace 'deviceID.txt' with 'config.json'
|
||||
.then(response => response.json()) // Parse response as JSON
|
||||
.then(data => {
|
||||
//get device ID
|
||||
const deviceID = data.deviceID.trim().toUpperCase();
|
||||
//document.getElementById('pageTitle_plus_ID').innerText = 'token: ' + deviceID;
|
||||
const SARA_busy_message = document.getElementById("modem_status_message");
|
||||
|
||||
//get device Name
|
||||
const deviceName = data.deviceName;
|
||||
|
||||
const elements = document.querySelectorAll('.sideBar_sensorName');
|
||||
elements.forEach((element) => {
|
||||
element.innerText = deviceName;
|
||||
});
|
||||
|
||||
|
||||
//get SARA_R4 connection status
|
||||
const SARA_statusElement = document.getElementById("modem-status");
|
||||
console.log("SARA R4 is: " + data.SARA_R4_network_status);
|
||||
|
||||
if (data.SARA_R4_network_status === "connected") {
|
||||
SARA_statusElement.textContent = "Connected";
|
||||
SARA_statusElement.className = "badge text-bg-success";
|
||||
} else if (data.SARA_R4_network_status === "disconnected") {
|
||||
SARA_statusElement.textContent = "Disconnected";
|
||||
SARA_statusElement.className = "badge text-bg-danger";
|
||||
} else {
|
||||
SARA_statusElement.textContent = "Unknown";
|
||||
SARA_statusElement.className = "badge text-bg-secondary";
|
||||
}
|
||||
|
||||
//get local RTC
|
||||
$.ajax({
|
||||
url: 'launcher.php?type=RTC_time',
|
||||
dataType: 'text', // Specify that you expect a JSON response
|
||||
method: 'GET', // Use GET or POST depending on your needs
|
||||
url: 'launcher.php?type=getModem_busy',
|
||||
dataType: 'json', // Expecting JSON response
|
||||
method: 'GET',
|
||||
success: function(response) {
|
||||
console.log("Local RTC: " + response);
|
||||
const RTC_Element = document.getElementById("RTC_time");
|
||||
RTC_Element.textContent = response;
|
||||
//console.log(response);
|
||||
|
||||
if (response.running) {
|
||||
// Script is running → Red button, "Modem is busy"
|
||||
|
||||
SARA_busy_message.innerHTML= ` <div class="alert alert-warning" role="alert">
|
||||
Le modem 4G est en cours d'utilisation! L'utilisation des boutons ci-dessous peut entrainer des erreurs. Veuillez mettre le modem en mode configuration.
|
||||
</div>`
|
||||
} else {
|
||||
// Script is NOT running → Green button, "Modem is available"
|
||||
|
||||
SARA_busy_message.innerHTML= ` <div class="alert alert-primary" role="alert">
|
||||
Veuillez vous assurer de mettre le modem en mode configuration avant de cliquer sur les boutons ci-dessous. <br>
|
||||
Une fois terminé veillez à bien désactiver le mode configuration.
|
||||
</div>`
|
||||
}
|
||||
},
|
||||
error: function(xhr, status, error) {
|
||||
console.error('AJAX request failed:', status, error);
|
||||
SARA_busy_status.textContent = "Error checking status";
|
||||
SARA_busy_status.className = "btn text-bg-warning"; // Yellow button for errors
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function update_modem_configMode(param, checked){
|
||||
//change ('modem_config_mode', '0', 'bool') inside SQLITE db
|
||||
// response type: {"success":true,"message":"Configuration updated successfully","param":"modem_config_mode","value":"0","type":"bool"}
|
||||
const toastLiveExample = document.getElementById('liveToast')
|
||||
const toastBody = toastLiveExample.querySelector('.toast-body');
|
||||
|
||||
console.log("updating modem config mode to :" + checked);
|
||||
$.ajax({
|
||||
url: 'launcher.php?type=update_config_sqlite¶m='+param+'&value='+checked,
|
||||
dataType: 'json', // Specify that you expect a JSON response
|
||||
method: 'GET', // Use GET or POST depending on your needs
|
||||
cache: false, // Prevent AJAX from caching
|
||||
success: function(response) {
|
||||
|
||||
console.log("AJAX success:");
|
||||
console.log(response);
|
||||
|
||||
// Format the response nicely
|
||||
let formattedMessage = '';
|
||||
|
||||
if (response.success) {
|
||||
// Success message
|
||||
toastLiveExample.classList.remove('text-bg-danger');
|
||||
toastLiveExample.classList.add('text-bg-success');
|
||||
|
||||
formattedMessage = `
|
||||
<strong>Success!</strong><br>
|
||||
Parameter: ${response.param || param}<br>
|
||||
Value: ${response.value || checked}<br>
|
||||
${response.message || ''}
|
||||
`;
|
||||
} else {
|
||||
// Error message
|
||||
toastLiveExample.classList.remove('text-bg-success');
|
||||
toastLiveExample.classList.add('text-bg-danger');
|
||||
|
||||
formattedMessage = `
|
||||
<strong>Error!</strong><br>
|
||||
${response.error || 'Unknown error'}<br>
|
||||
Parameter: ${response.param || param}
|
||||
`;
|
||||
}
|
||||
|
||||
// Update the toast body with formatted content
|
||||
toastBody.innerHTML = formattedMessage;
|
||||
// Show the toast
|
||||
const toastBootstrap = bootstrap.Toast.getOrCreateInstance(toastLiveExample)
|
||||
toastBootstrap.show()
|
||||
|
||||
|
||||
},
|
||||
error: function(xhr, status, error) {
|
||||
console.error('AJAX request failed:', status, error);
|
||||
// Update toast with error message
|
||||
toastBody.textContent = 'Error: ' + error;
|
||||
|
||||
// Set toast to danger color
|
||||
toastLiveExample.classList.remove('text-bg-success');
|
||||
toastLiveExample.classList.add('text-bg-danger');
|
||||
|
||||
// Show the toast for errors too
|
||||
const toastBootstrap = bootstrap.Toast.getOrCreateInstance(toastLiveExample);
|
||||
toastBootstrap.show();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
})
|
||||
.catch(error => console.error('Error loading config.json:', error));
|
||||
}
|
||||
</script>
|
||||
|
||||
</body>
|
||||
|
||||
@@ -102,6 +102,16 @@ function getNPM_values(port){
|
||||
$("#loading_"+port).hide();
|
||||
// Create an array of the desired keys
|
||||
const keysToShow = ["PM1", "PM25", "PM10"];
|
||||
// Error messages mapping
|
||||
const errorMessages = {
|
||||
"notReady": "Sensor is not ready",
|
||||
"fanError": "Fan malfunction detected",
|
||||
"laserError": "Laser malfunction detected",
|
||||
"heatError": "Heating system error",
|
||||
"t_rhError": "Temperature/Humidity sensor error",
|
||||
"memoryError": "Memory failure detected",
|
||||
"degradedState": "Sensor in degraded state"
|
||||
};
|
||||
// Add only the specified elements to the table
|
||||
keysToShow.forEach(key => {
|
||||
if (response[key] !== undefined) { // Check if the key exists in the response
|
||||
@@ -114,38 +124,14 @@ function getNPM_values(port){
|
||||
`);
|
||||
}
|
||||
});
|
||||
},
|
||||
error: function(xhr, status, error) {
|
||||
console.error('AJAX request failed:', status, error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function getENVEA_values(port, name){
|
||||
console.log("Data from Envea "+ name+" (port "+port+"):");
|
||||
$("#loading_envea"+name).show();
|
||||
|
||||
$.ajax({
|
||||
url: 'launcher.php?type=envea&port='+port+'&name='+name,
|
||||
dataType: 'json', // Specify that you expect a JSON response
|
||||
method: 'GET', // Use GET or POST depending on your needs
|
||||
success: function(response) {
|
||||
console.log(response);
|
||||
const tableBody = document.getElementById("data-table-body_envea"+name);
|
||||
tableBody.innerHTML = "";
|
||||
|
||||
$("#loading_envea"+name).hide();
|
||||
// Create an array of the desired keys
|
||||
// Create an array of the desired keys
|
||||
const keysToShow = [name];
|
||||
// Add only the specified elements to the table
|
||||
keysToShow.forEach(key => {
|
||||
if (response !== undefined) { // Check if the key exists in the response
|
||||
const value = response;
|
||||
$("#data-table-body_envea"+name).append(`
|
||||
<tr>
|
||||
<td>${key}</td>
|
||||
<td>${value} ppb</td>
|
||||
// Check for errors and add them to the table
|
||||
Object.keys(errorMessages).forEach(errorKey => {
|
||||
if (response[errorKey] === 1) {
|
||||
$("#data-table-body_" + port).append(`
|
||||
<tr class="error-row">
|
||||
<td><b>${errorKey}</b></td>
|
||||
<td style="color: red;">⚠ ${errorMessages[errorKey]}</td>
|
||||
</tr>
|
||||
`);
|
||||
}
|
||||
@@ -157,12 +143,59 @@ function getENVEA_values(port, name){
|
||||
});
|
||||
}
|
||||
|
||||
function getENVEA_values(port, name){
|
||||
console.log("Data from Envea " + name + " (port " + port + "):");
|
||||
$("#loading_envea" + name).show();
|
||||
|
||||
$.ajax({
|
||||
url: 'launcher.php?type=envea&port=' + port + '&name=' + name,
|
||||
dataType: 'json',
|
||||
method: 'GET',
|
||||
success: function(response) {
|
||||
console.log(response);
|
||||
const tableBody = document.getElementById("data-table-body_envea" + name);
|
||||
tableBody.innerHTML = "";
|
||||
|
||||
$("#loading_envea" + name).hide();
|
||||
|
||||
const keysToShow = [name];
|
||||
keysToShow.forEach(key => {
|
||||
if (response !== undefined) {
|
||||
const value = response;
|
||||
$("#data-table-body_envea" + name).append(`
|
||||
<tr>
|
||||
<td>${key}</td>
|
||||
<td>${value} ppb</td>
|
||||
</tr>
|
||||
`);
|
||||
}
|
||||
});
|
||||
},
|
||||
error: function(xhr, status, error) {
|
||||
console.error('AJAX request failed:', status, error);
|
||||
const tableBody = document.getElementById("data-table-body_envea" + name);
|
||||
$("#loading_envea" + name).hide();
|
||||
|
||||
tableBody.innerHTML = `
|
||||
<tr>
|
||||
<td colspan="2" class="text-danger">
|
||||
❌ Error: unable to get data from sensor.<br>
|
||||
<small>${status}: ${error}</small>
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function getNoise_values(){
|
||||
console.log("Data from I2C Noise Sensor:");
|
||||
$("#loading_noise").show();
|
||||
|
||||
$.ajax({
|
||||
url: 'launcher.php?type=noise',
|
||||
dataType: 'text',
|
||||
method: 'GET', // Use GET or POST depending on your needs
|
||||
success: function(response) {
|
||||
console.log(response);
|
||||
@@ -197,6 +230,8 @@ function getBME280_values(){
|
||||
|
||||
$.ajax({
|
||||
url: 'launcher.php?type=BME280',
|
||||
dataType: 'text',
|
||||
|
||||
method: 'GET', // Use GET or POST depending on your needs
|
||||
success: function(response) {
|
||||
console.log(response);
|
||||
@@ -236,92 +271,68 @@ function getBME280_values(){
|
||||
|
||||
|
||||
window.onload = function() {
|
||||
fetch('../config.json') // Replace 'deviceID.txt' with 'config.json'
|
||||
.then(response => response.json()) // Parse response as JSON
|
||||
.then(data => {
|
||||
//get device ID
|
||||
const deviceID = data.deviceID.trim().toUpperCase();
|
||||
//document.getElementById('pageTitle_plus_ID').innerText = 'token: ' + deviceID;
|
||||
//get device Name
|
||||
const deviceName = data.deviceName;
|
||||
|
||||
const elements = document.querySelectorAll('.sideBar_sensorName');
|
||||
elements.forEach((element) => {
|
||||
element.innerText = deviceName;
|
||||
});
|
||||
|
||||
|
||||
|
||||
//get local RTC
|
||||
$.ajax({
|
||||
url: 'launcher.php?type=RTC_time',
|
||||
dataType: 'text', // Specify that you expect a JSON response
|
||||
//NEW way to get config (SQLite)
|
||||
$.ajax({
|
||||
url: 'launcher.php?type=get_config_sqlite',
|
||||
dataType:'json',
|
||||
//dataType: 'json', // Specify that you expect a JSON response
|
||||
method: 'GET', // Use GET or POST depending on your needs
|
||||
success: function(response) {
|
||||
console.log("Local RTC: " + response);
|
||||
const RTC_Element = document.getElementById("RTC_time");
|
||||
RTC_Element.textContent = response;
|
||||
console.log("Getting SQLite config table:");
|
||||
console.log(response);
|
||||
|
||||
|
||||
//device name_side bar
|
||||
const elements = document.querySelectorAll('.sideBar_sensorName');
|
||||
elements.forEach((element) => {
|
||||
element.innerText = response.deviceName;
|
||||
});
|
||||
|
||||
},
|
||||
error: function(xhr, status, error) {
|
||||
error: function(xhr, status, error) {
|
||||
console.error('AJAX request failed:', status, error);
|
||||
}
|
||||
});
|
||||
}
|
||||
});//end AJAX
|
||||
|
||||
//getting config_scripts table
|
||||
$.ajax({
|
||||
url: 'launcher.php?type=get_config_scripts_sqlite',
|
||||
dataType:'json',
|
||||
//dataType: 'json', // Specify that you expect a JSON response
|
||||
method: 'GET', // Use GET or POST depending on your needs
|
||||
success: function(response) {
|
||||
console.log("Getting SQLite config scripts table:");
|
||||
console.log(response);
|
||||
|
||||
const container = document.getElementById('card-container'); // Conteneur des cartes
|
||||
|
||||
//creates NPM cards
|
||||
const NPM_ports = data.NextPM_ports; // Récupère les ports
|
||||
NPM_ports.forEach((port, index) => {
|
||||
//creates NPM card
|
||||
if (response["NPM/get_data_modbus_v3.py"]) {
|
||||
const cardHTML = `
|
||||
<div class="col-sm-3">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
Port UART ${port.replace('ttyAMA', '')}
|
||||
Port UART
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">NextPM ${String.fromCharCode(65 + index)}</h5>
|
||||
<h5 class="card-title">NextPM</h5>
|
||||
<p class="card-text">Capteur particules fines.</p>
|
||||
<button class="btn btn-primary" onclick="getNPM_values('${port}')">Get Data</button>
|
||||
<div id="loading_${port}" class="spinner-border spinner-border-sm" style="display: none;" role="status"></div>
|
||||
<button class="btn btn-primary" onclick="getNPM_values('ttyAMA5')">Get Data</button>
|
||||
<br>
|
||||
<div id="loading_ttyAMA5" class="spinner-border spinner-border-sm" style="display: none;" role="status"></div>
|
||||
<table class="table table-striped-columns">
|
||||
<tbody id="data-table-body_${port}"></tbody>
|
||||
<tbody id="data-table-body_ttyAMA5"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
container.innerHTML += cardHTML; // Ajouter la carte au conteneur
|
||||
});
|
||||
|
||||
//creates ENVEA cards
|
||||
const ENVEA_sensors = data.envea_sondes.filter(sonde => sonde.connected); // Filter only connected sondes
|
||||
|
||||
ENVEA_sensors.forEach((sensor, index) => {
|
||||
const port = sensor.port; // Port from the sensor object
|
||||
const name = sensor.name; // Port from the sensor object
|
||||
const coefficient = sensor.coefficient;
|
||||
const cardHTML = `
|
||||
<div class="col-sm-3">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
Port UART ${port.replace('ttyAMA', '')}
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Sonde Envea ${name}</h5>
|
||||
<p class="card-text">Capteur gas.</p>
|
||||
<button class="btn btn-primary" onclick="getENVEA_values('${port}','${name}','${coefficient}')">Get Data</button>
|
||||
<div id="loading_envea${name}" class="spinner-border spinner-border-sm" style="display: none;" role="status"></div>
|
||||
<table class="table table-striped-columns">
|
||||
<tbody id="data-table-body_envea${name}"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
container.innerHTML += cardHTML; // Ajouter la carte au conteneur
|
||||
});
|
||||
container.innerHTML += cardHTML; // Add the I2C card if condition is met
|
||||
}
|
||||
|
||||
//creates i2c BME280 card
|
||||
if (data.i2c_BME) {
|
||||
if (response["BME280/get_data_v2.py"]) {
|
||||
const i2C_BME_HTML = `
|
||||
<div class="col-sm-3">
|
||||
<div class="card">
|
||||
@@ -345,7 +356,7 @@ window.onload = function() {
|
||||
}
|
||||
|
||||
//creates i2c sound card
|
||||
if (data.i2C_sound) {
|
||||
if (response.i2C_sound) {
|
||||
const i2C_HTML = `
|
||||
<div class="col-sm-3">
|
||||
<div class="card">
|
||||
@@ -370,9 +381,80 @@ window.onload = function() {
|
||||
container.innerHTML += i2C_HTML; // Add the I2C card if condition is met
|
||||
}
|
||||
|
||||
})
|
||||
.catch(error => console.error('Error loading config.json:', error));
|
||||
//Si on a des SONDES ENVEA connectée il faut faire un deuxième call dans la table envea_sondes_table
|
||||
//creates ENVEA cards
|
||||
if (response["envea/read_value_v2.py"]) {
|
||||
console.log("Need to display ENVEA sondes");
|
||||
//getting config_scripts table
|
||||
$.ajax({
|
||||
url: 'launcher.php?type=get_envea_sondes_table_sqlite',
|
||||
dataType:'json',
|
||||
//dataType: 'json', // Specify that you expect a JSON response
|
||||
method: 'GET', // Use GET or POST depending on your needs
|
||||
success: function(sondes) {
|
||||
console.log("Getting SQLite envea sondes table:");
|
||||
console.log(sondes);
|
||||
const ENVEA_sensors = sondes.filter(sonde => sonde.connected); // Filter only connected sondes
|
||||
|
||||
ENVEA_sensors.forEach((sensor, index) => {
|
||||
const port = sensor.port; // Port from the sensor object
|
||||
const name = sensor.name; // Port from the sensor object
|
||||
const coefficient = sensor.coefficient;
|
||||
const cardHTML = `
|
||||
<div class="col-sm-3">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
Port UART ${port.replace('ttyAMA', '')}
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Sonde Envea ${name}</h5>
|
||||
<p class="card-text">Capteur gas.</p>
|
||||
<button class="btn btn-primary" onclick="getENVEA_values('${port}','${name}')">Get Data</button>
|
||||
<div id="loading_envea${name}" class="spinner-border spinner-border-sm" style="display: none;" role="status"></div>
|
||||
<table class="table table-striped-columns">
|
||||
<tbody id="data-table-body_envea${name}"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
container.innerHTML += cardHTML; // Ajouter la carte au conteneur
|
||||
});
|
||||
|
||||
|
||||
|
||||
},
|
||||
error: function(xhr, status, error) {
|
||||
console.error('AJAX request failed:', status, error);
|
||||
}
|
||||
});//end AJAX envea Sondes
|
||||
|
||||
|
||||
}//end if
|
||||
|
||||
|
||||
},
|
||||
error: function(xhr, status, error) {
|
||||
console.error('AJAX request failed:', status, error);
|
||||
}
|
||||
});//end AJAX (config_scripts)
|
||||
|
||||
//get local RTC
|
||||
$.ajax({
|
||||
url: 'launcher.php?type=RTC_time',
|
||||
dataType: 'text', // Specify that you expect a JSON response
|
||||
method: 'GET', // Use GET or POST depending on your needs
|
||||
success: function(response) {
|
||||
console.log("Local RTC: " + response);
|
||||
const RTC_Element = document.getElementById("RTC_time");
|
||||
RTC_Element.textContent = response;
|
||||
|
||||
},
|
||||
error: function(xhr, status, error) {
|
||||
console.error('AJAX request failed:', status, error);
|
||||
}
|
||||
});
|
||||
|
||||
} //end windows onload
|
||||
</script>
|
||||
|
||||
</body>
|
||||
|
||||
@@ -13,6 +13,13 @@
|
||||
</svg>
|
||||
Capteurs
|
||||
</a>
|
||||
<a class="nav-link text-white" href="database.html">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-database" viewBox="0 0 16 16">
|
||||
<path d="M4.318 2.687C5.234 2.271 6.536 2 8 2s2.766.27 3.682.687C12.644 3.125 13 3.627 13 4c0 .374-.356.875-1.318 1.313C10.766 5.729 9.464 6 8 6s-2.766-.27-3.682-.687C3.356 4.875 3 4.373 3 4c0-.374.356-.875 1.318-1.313M13 5.698V7c0 .374-.356.875-1.318 1.313C10.766 8.729 9.464 9 8 9s-2.766-.27-3.682-.687C3.356 7.875 3 7.373 3 7V5.698c.271.202.58.378.904.525C4.978 6.711 6.427 7 8 7s3.022-.289 4.096-.777A5 5 0 0 0 13 5.698M14 4c0-1.007-.875-1.755-1.904-2.223C11.022 1.289 9.573 1 8 1s-3.022.289-4.096.777C2.875 2.245 2 2.993 2 4v9c0 1.007.875 1.755 1.904 2.223C4.978 15.71 6.427 16 8 16s3.022-.289 4.096-.777C13.125 14.755 14 14.007 14 13zm-1 4.698V10c0 .374-.356.875-1.318 1.313C10.766 11.729 9.464 12 8 12s-2.766-.27-3.682-.687C3.356 10.875 3 10.373 3 10V8.698c.271.202.58.378.904.525C4.978 9.71 6.427 10 8 10s3.022-.289 4.096-.777A5 5 0 0 0 13 8.698m0 3V13c0 .374-.356.875-1.318 1.313C10.766 14.729 9.464 15 8 15s-2.766-.27-3.682-.687C3.356 13.875 3 13.373 3 13v-1.302c.271.202.58.378.904.525C4.978 12.71 6.427 13 8 13s3.022-.289 4.096-.777c.324-.147.633-.323.904-.525"/>
|
||||
</svg>
|
||||
|
||||
DataBase
|
||||
</a>
|
||||
<a class="nav-link text-white" href="saraR4.html">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-reception-4" viewBox="0 0 16 16">
|
||||
<path d="M0 11.5a.5.5 0 0 1 .5-.5h2a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1-.5-.5zm4-3a.5.5 0 0 1 .5-.5h2a.5.5 0 0 1 .5.5v5a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1-.5-.5zm4-3a.5.5 0 0 1 .5-.5h2a.5.5 0 0 1 .5.5v8a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1-.5-.5zm4-3a.5.5 0 0 1 .5-.5h2a.5.5 0 0 1 .5.5v11a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1-.5-.5z"/>
|
||||
@@ -34,6 +41,18 @@
|
||||
</svg>
|
||||
Logs
|
||||
</a>
|
||||
<a class="nav-link text-white" href="map.html">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-geo-alt-fill" viewBox="0 0 16 16">
|
||||
<path d="M8 16s6-5.686 6-10A6 6 0 0 0 2 6c0 4.314 6 10 6 10m0-7a3 3 0 1 1 0-6 3 3 0 0 1 0 6"/>
|
||||
</svg>
|
||||
Carte
|
||||
</a>
|
||||
<a class="nav-link text-white" href="terminal.html">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-terminal-fill" viewBox="0 0 16 16">
|
||||
<path d="M0 3a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V3zm9.5 5.5h-3a.5.5 0 0 0 0 1h3a.5.5 0 0 0 0-1zm-6.354-.354a.5.5 0 1 0 .708.708l2-2a.5.5 0 0 0 0-.708l-2-2a.5.5 0 1 0-.708.708L4.793 6.5 3.146 8.146z"/>
|
||||
</svg>
|
||||
Terminal
|
||||
</a>
|
||||
<a class="nav-link text-white" href="admin.html">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-tools" viewBox="0 0 16 16">
|
||||
<path d="M1 0 0 1l2.2 3.081a1 1 0 0 0 .815.419h.07a1 1 0 0 1 .708.293l2.675 2.675-2.617 2.654A3.003 3.003 0 0 0 0 13a3 3 0 1 0 5.878-.851l2.654-2.617.968.968-.305.914a1 1 0 0 0 .242 1.023l3.27 3.27a.997.997 0 0 0 1.414 0l1.586-1.586a.997.997 0 0 0 0-1.414l-3.27-3.27a1 1 0 0 0-1.023-.242L10.5 9.5l-.96-.96 2.68-2.643A3.005 3.005 0 0 0 16 3q0-.405-.102-.777l-2.14 2.141L12 4l-.364-1.757L13.777.102a3 3 0 0 0-3.675 3.68L7.462 6.46 4.793 3.793a1 1 0 0 1-.293-.707v-.071a1 1 0 0 0-.419-.814zm9.646 10.646a.5.5 0 0 1 .708 0l2.914 2.915a.5.5 0 0 1-.707.707l-2.915-2.914a.5.5 0 0 1 0-.708M3 11l.471.242.529.026.287.445.445.287.026.529L5 13l-.242.471-.026.529-.445.287-.287.445-.529.026L3 15l-.471-.242L2 14.732l-.287-.445L1.268 14l-.026-.529L1 13l.242-.471.026-.529.445-.287.287-.445.529-.026z"/>
|
||||
|
||||
413
html/terminal.html
Normal file
413
html/terminal.html
Normal file
@@ -0,0 +1,413 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>NebuleAir - Terminal</title>
|
||||
<link rel="stylesheet" href="assets/css/bootstrap.min.css">
|
||||
<style>
|
||||
body {
|
||||
overflow-x: hidden;
|
||||
}
|
||||
#sidebar a.nav-link {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
#sidebar a.nav-link:hover {
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
#sidebar a.nav-link svg {
|
||||
margin-right: 8px; /* Add spacing between icons and text */
|
||||
}
|
||||
#sidebar {
|
||||
transition: transform 0.3s ease-in-out;
|
||||
}
|
||||
.offcanvas-backdrop {
|
||||
z-index: 1040;
|
||||
}
|
||||
#terminal {
|
||||
width: 100%;
|
||||
min-height: 400px;
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
background-color: #000;
|
||||
color: #00ff00;
|
||||
font-family: monospace;
|
||||
padding: 10px;
|
||||
border-radius: 5px;
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
#cmdLine {
|
||||
width: 100%;
|
||||
background-color: #000;
|
||||
color: #00ff00;
|
||||
font-family: monospace;
|
||||
padding: 10px;
|
||||
border: none;
|
||||
border-radius: 0 0 5px 5px;
|
||||
margin-top: -1px;
|
||||
}
|
||||
#cmdLine:focus {
|
||||
outline: none;
|
||||
}
|
||||
.command-container {
|
||||
display: none;
|
||||
}
|
||||
.password-popup {
|
||||
display: none;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
z-index: 2000;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
.password-container {
|
||||
background-color: white;
|
||||
padding: 20px;
|
||||
border-radius: 5px;
|
||||
box-shadow: 0 0 10px rgba(0, 0, 0, 0.3);
|
||||
width: 300px;
|
||||
}
|
||||
.limited-commands {
|
||||
background-color: #f8f9fa;
|
||||
padding: 15px;
|
||||
border-radius: 5px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
.limited-commands code {
|
||||
white-space: nowrap;
|
||||
margin-right: 10px;
|
||||
margin-bottom: 5px;
|
||||
display: inline-block;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<!-- Topbar -->
|
||||
<span id="topbar"></span>
|
||||
|
||||
<!-- Sidebar Offcanvas for Mobile -->
|
||||
<div class="offcanvas offcanvas-start text-white bg-dark" tabindex="-1" id="sidebarOffcanvas" aria-labelledby="sidebarOffcanvasLabel">
|
||||
<div class="offcanvas-header">
|
||||
<h5 class="offcanvas-title" id="sidebarOffcanvasLabel">NebuleAir</h5>
|
||||
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="offcanvas" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="offcanvas-body" id="sidebar_mobile">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="container-fluid mt-5">
|
||||
<div class="row">
|
||||
<!-- Side bar -->
|
||||
<aside class="col-md-2 col-lg-1 d-none d-md-block vh-100 position-fixed bg-dark text-white" id="sidebar">
|
||||
</aside>
|
||||
<!-- Main content -->
|
||||
<main class="col-md-10 ms-sm-auto col-lg-11 offset-md-2 offset-lg-1 px-md-4">
|
||||
<h1 class="mt-4">Terminal Console</h1>
|
||||
|
||||
<div class="row mb-3">
|
||||
<div class="col-12">
|
||||
<div class="alert alert-warning">
|
||||
<strong>Warning:</strong> This terminal provides direct access to system commands.
|
||||
Use with caution as improper commands may affect system functionality.
|
||||
</div>
|
||||
|
||||
<div class="limited-commands">
|
||||
<h5>Quick Commands:</h5>
|
||||
<div>
|
||||
<code onclick="insertCommand('ls -la')">ls -la</code>
|
||||
<code onclick="insertCommand('df -h')">df -h</code>
|
||||
<code onclick="insertCommand('free -h')">free -h</code>
|
||||
<code onclick="insertCommand('uptime')">uptime</code>
|
||||
<code onclick="insertCommand('systemctl status master_nebuleair.service')">service status</code>
|
||||
<code onclick="insertCommand('cat /var/www/nebuleair_pro_4g/config.json')">view config</code>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<h5 class="mb-0">Command Console</h5>
|
||||
<div>
|
||||
<button id="accessBtn" class="btn btn-primary me-2">Access Terminal</button>
|
||||
<button id="clearBtn" class="btn btn-secondary" disabled>Clear</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<div class="command-container" id="commandContainer">
|
||||
<div id="terminal">Welcome to NebuleAir Terminal Console
|
||||
Type your commands below. Type 'help' for a list of commands.
|
||||
</div>
|
||||
<input type="text" id="cmdLine" placeholder="Enter command..." disabled>
|
||||
</div>
|
||||
<div id="errorMsg" class="alert alert-danger m-3" style="display:none;"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Password Modal -->
|
||||
<div class="password-popup" id="passwordModal">
|
||||
<div class="password-container">
|
||||
<h5>Authentication Required</h5>
|
||||
<p>Please enter the admin password to access the terminal:</p>
|
||||
<div class="mb-3">
|
||||
<input type="password" class="form-control" id="adminPassword" placeholder="Password">
|
||||
</div>
|
||||
<div class="mb-3 d-flex justify-content-between">
|
||||
<button class="btn btn-secondary" id="cancelPasswordBtn">Cancel</button>
|
||||
<button class="btn btn-primary" id="submitPasswordBtn">Submit</button>
|
||||
</div>
|
||||
<div id="passwordError" class="text-danger mt-2" style="display:none;"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- JAVASCRIPT -->
|
||||
<!-- Link Ajax locally -->
|
||||
<script src="assets/jquery/jquery-3.7.1.min.js"></script>
|
||||
<!-- Link Bootstrap JS and Popper.js locally -->
|
||||
<script src="assets/js/bootstrap.bundle.js"></script>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
const elementsToLoad = [
|
||||
{ id: 'topbar', file: 'topbar.html' },
|
||||
{ id: 'sidebar', file: 'sidebar.html' },
|
||||
{ id: 'sidebar_mobile', file: 'sidebar.html' }
|
||||
];
|
||||
|
||||
elementsToLoad.forEach(({ id, file }) => {
|
||||
fetch(file)
|
||||
.then(response => response.text())
|
||||
.then(data => {
|
||||
const element = document.getElementById(id);
|
||||
if (element) {
|
||||
element.innerHTML = data;
|
||||
}
|
||||
})
|
||||
.catch(error => console.error(`Error loading ${file}:`, error));
|
||||
});
|
||||
|
||||
// Initialize elements
|
||||
initializeElements();
|
||||
});
|
||||
|
||||
window.onload = function() {
|
||||
|
||||
//NEW way to get config (SQLite)
|
||||
$.ajax({
|
||||
url: 'launcher.php?type=get_config_sqlite',
|
||||
dataType:'json',
|
||||
//dataType: 'json', // Specify that you expect a JSON response
|
||||
method: 'GET', // Use GET or POST depending on your needs
|
||||
success: function(response) {
|
||||
console.log("Getting SQLite config table:");
|
||||
console.log(response);
|
||||
|
||||
|
||||
//device name_side bar
|
||||
const elements = document.querySelectorAll('.sideBar_sensorName');
|
||||
elements.forEach((element) => {
|
||||
element.innerText = response.deviceName;
|
||||
});
|
||||
|
||||
},
|
||||
error: function(xhr, status, error) {
|
||||
console.error('AJAX request failed:', status, error);
|
||||
}
|
||||
});//end AJAX
|
||||
}
|
||||
|
||||
// Add admin password (should be changed to something more secure)
|
||||
const ADMIN_PASSWORD = "123plouf";
|
||||
|
||||
// Global variables
|
||||
let terminal;
|
||||
let cmdLine;
|
||||
let commandContainer;
|
||||
let accessBtn;
|
||||
let clearBtn;
|
||||
let passwordModal;
|
||||
let adminPassword;
|
||||
let submitPasswordBtn;
|
||||
let cancelPasswordBtn;
|
||||
let passwordError;
|
||||
let errorMsg;
|
||||
let commandHistory = [];
|
||||
let historyIndex = -1;
|
||||
|
||||
// Initialize DOM references after document is loaded
|
||||
function initializeElements() {
|
||||
terminal = document.getElementById('terminal');
|
||||
cmdLine = document.getElementById('cmdLine');
|
||||
commandContainer = document.getElementById('commandContainer');
|
||||
accessBtn = document.getElementById('accessBtn');
|
||||
clearBtn = document.getElementById('clearBtn');
|
||||
passwordModal = document.getElementById('passwordModal');
|
||||
adminPassword = document.getElementById('adminPassword');
|
||||
submitPasswordBtn = document.getElementById('submitPasswordBtn');
|
||||
cancelPasswordBtn = document.getElementById('cancelPasswordBtn');
|
||||
passwordError = document.getElementById('passwordError');
|
||||
errorMsg = document.getElementById('errorMsg');
|
||||
|
||||
// Set up event listeners
|
||||
accessBtn.addEventListener('click', function() {
|
||||
passwordModal.style.display = 'flex';
|
||||
adminPassword.value = ''; // Clear password field
|
||||
passwordError.style.display = 'none';
|
||||
adminPassword.focus();
|
||||
});
|
||||
|
||||
// Password submit button
|
||||
submitPasswordBtn.addEventListener('click', function() {
|
||||
if (adminPassword.value === ADMIN_PASSWORD) {
|
||||
passwordModal.style.display = 'none';
|
||||
enableTerminal();
|
||||
} else {
|
||||
passwordError.textContent = 'Invalid password';
|
||||
passwordError.style.display = 'block';
|
||||
}
|
||||
});
|
||||
|
||||
// Enter key for password
|
||||
adminPassword.addEventListener('keypress', function(e) {
|
||||
if (e.key === 'Enter') {
|
||||
submitPasswordBtn.click();
|
||||
}
|
||||
});
|
||||
|
||||
// Cancel password button
|
||||
cancelPasswordBtn.addEventListener('click', function() {
|
||||
passwordModal.style.display = 'none';
|
||||
});
|
||||
|
||||
// Clear button
|
||||
clearBtn.addEventListener('click', function() {
|
||||
terminal.innerHTML = 'Terminal cleared.\n';
|
||||
});
|
||||
|
||||
// Command line input events
|
||||
cmdLine.addEventListener('keypress', function(e) {
|
||||
if (e.key === 'Enter') {
|
||||
const command = cmdLine.value.trim();
|
||||
if (command) {
|
||||
executeCommand(command);
|
||||
commandHistory.push(command);
|
||||
historyIndex = commandHistory.length;
|
||||
cmdLine.value = '';
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Command history navigation with arrow keys
|
||||
cmdLine.addEventListener('keydown', function(e) {
|
||||
if (e.key === 'ArrowUp') {
|
||||
if (historyIndex > 0) {
|
||||
historyIndex--;
|
||||
cmdLine.value = commandHistory[historyIndex];
|
||||
}
|
||||
e.preventDefault();
|
||||
} else if (e.key === 'ArrowDown') {
|
||||
if (historyIndex < commandHistory.length - 1) {
|
||||
historyIndex++;
|
||||
cmdLine.value = commandHistory[historyIndex];
|
||||
} else {
|
||||
historyIndex = commandHistory.length;
|
||||
cmdLine.value = '';
|
||||
}
|
||||
e.preventDefault();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Enable terminal access
|
||||
function enableTerminal() {
|
||||
commandContainer.style.display = 'block';
|
||||
cmdLine.disabled = false;
|
||||
clearBtn.disabled = false;
|
||||
accessBtn.textContent = 'Authenticated';
|
||||
accessBtn.classList.remove('btn-primary');
|
||||
accessBtn.classList.add('btn-success');
|
||||
accessBtn.disabled = true;
|
||||
cmdLine.focus();
|
||||
}
|
||||
|
||||
// Insert a predefined command
|
||||
function insertCommand(cmd) {
|
||||
// Only allow insertion if terminal is enabled
|
||||
if (cmdLine.disabled === false) {
|
||||
cmdLine.value = cmd;
|
||||
cmdLine.focus();
|
||||
} else {
|
||||
// Alert user that they need to authenticate first
|
||||
alert('Please access the terminal first by clicking "Access Terminal" and entering the password.');
|
||||
}
|
||||
}
|
||||
|
||||
// Execute a command
|
||||
function executeCommand(command) {
|
||||
// Add command to terminal with user prefix
|
||||
terminal.innerHTML += `<span style="color: cyan;">user@nebuleair</span>:<span style="color: yellow;">~</span>$ ${command}\n`;
|
||||
terminal.scrollTop = terminal.scrollHeight;
|
||||
|
||||
// Handle special commands
|
||||
if (command === 'clear') {
|
||||
terminal.innerHTML = 'Terminal cleared.\n';
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Filter dangerous commands
|
||||
const dangerousCommands = [
|
||||
'rm -rf /', 'rm -rf /*', 'rm -rf ~', 'rm -rf ~/*',
|
||||
'mkfs', 'dd if=/dev/zero', 'dd if=/dev/random',
|
||||
'>>', '>', '|', ';', '&&', '||',
|
||||
'wget', 'curl', 'ssh', 'scp', 'nc',
|
||||
'chmod -R', 'chown -R'
|
||||
];
|
||||
|
||||
// Check for dangerous commands or command chaining
|
||||
const hasDangerousCommand = dangerousCommands.some(cmd => command.includes(cmd));
|
||||
if (hasDangerousCommand || command.includes('&') || command.includes(';') || command.includes('|')) {
|
||||
terminal.innerHTML += '<span style="color: red;">Error: This command is not allowed for security reasons.</span>\n';
|
||||
terminal.scrollTop = terminal.scrollHeight;
|
||||
return;
|
||||
}
|
||||
|
||||
// Execute the command via AJAX
|
||||
$.ajax({
|
||||
url: 'launcher.php?type=execute_command',
|
||||
method: 'POST',
|
||||
dataType:'json',
|
||||
data: {
|
||||
type: 'execute_command',
|
||||
command: command
|
||||
},
|
||||
success: function(response) {
|
||||
console.log(response);
|
||||
|
||||
if (response.success) {
|
||||
// Add command output to terminal
|
||||
terminal.innerHTML += `<span style="color: #00ff00;">${response.output}</span>\n`;
|
||||
} else {
|
||||
terminal.innerHTML += `<span style="color: red;">Error: ${response.message}</span>\n`;
|
||||
}
|
||||
terminal.scrollTop = terminal.scrollHeight;
|
||||
},
|
||||
error: function(xhr, status, error) {
|
||||
terminal.innerHTML += `<span style="color: red;">Error executing command: ${error}</span>\n`;
|
||||
terminal.scrollTop = terminal.scrollHeight;
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -302,6 +302,11 @@ function get_internet(){
|
||||
element.innerText = deviceName;
|
||||
});
|
||||
|
||||
//device name html page title
|
||||
if (response.deviceName) {
|
||||
document.title = response.deviceName;
|
||||
}
|
||||
|
||||
|
||||
//get wifi connection status
|
||||
const WIFI_statusElement = document.getElementById("wifi-status");
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Exit immediately if a command exits with a non-zero status
|
||||
set -e
|
||||
|
||||
# Update and install necessary packages
|
||||
echo "Updating package list and installing necessary packages..."
|
||||
sudo apt update
|
||||
sudo apt install -y git gh apache2 php python3 python3-pip jq autossh i2c-tools python3-smbus
|
||||
|
||||
# Install Python libraries
|
||||
echo "Installing Python libraries..."
|
||||
sudo pip3 install pyserial requests RPi.GPIO adafruit-circuitpython-bme280 --break-system-packages
|
||||
|
||||
# Set up SSH for /var/www
|
||||
echo "Setting up SSH keys..."
|
||||
sudo mkdir -p /var/www/.ssh
|
||||
sudo ssh-keygen -t rsa -b 4096 -f /var/www/.ssh/id_rsa -N ""
|
||||
sudo ssh-copy-id -i /var/www/.ssh/id_rsa.pub -p 50221 airlab_server1@aircarto.fr
|
||||
|
||||
|
||||
# Clone the repository
|
||||
echo "Cloning the NebuleAir Pro 4G repository..."
|
||||
sudo git clone http://gitea.aircarto.fr/PaulVua/nebuleair_pro_4g.git /var/www/nebuleair_pro_4g
|
||||
|
||||
# Set up repository files and permissions
|
||||
echo "Setting up repository files and permissions..."
|
||||
sudo mkdir -p /var/www/nebuleair_pro_4g/logs
|
||||
sudo touch /var/www/nebuleair_pro_4g/logs/app.log /var/www/nebuleair_pro_4g/logs/loop.log
|
||||
sudo cp /var/www/nebuleair_pro_4g/config.json.dist /var/www/nebuleair_pro_4g/config.json
|
||||
sudo chmod -R 777 /var/www/nebuleair_pro_4g/
|
||||
git config core.fileMode false
|
||||
|
||||
# Set up cron jobs
|
||||
echo "Setting up cron jobs..."
|
||||
sudo crontab /var/www/nebuleair_pro_4g/cron_jobs
|
||||
|
||||
echo "Setup completed successfully!"
|
||||
135
installation_part1.sh
Normal file
135
installation_part1.sh
Normal file
@@ -0,0 +1,135 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Exit on error, unset variable usage, and error in a pipeline
|
||||
set -euo pipefail
|
||||
|
||||
# Define color variables
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m' # No color
|
||||
|
||||
# Function to print messages in color
|
||||
info() { echo -e "${BLUE}[INFO]${NC} $1"; }
|
||||
success() { echo -e "${GREEN}[SUCCESS]${NC} $1"; }
|
||||
warning() { echo -e "${YELLOW}[WARNING]${NC} $1"; }
|
||||
error() { echo -e "${RED}[ERROR]${NC} $1"; exit 1; }
|
||||
|
||||
# Check for root privileges
|
||||
if [[ "$EUID" -ne 0 ]]; then
|
||||
error "This script must be run as root. Use 'sudo ./installation.sh'"
|
||||
fi
|
||||
|
||||
# Update and install necessary packages
|
||||
info "Updating package list and installing necessary packages..."
|
||||
sudo apt update && sudo apt install -y git gh apache2 sqlite3 php php-sqlite3 python3 python3-pip jq autossh i2c-tools python3-smbus || error "Failed to install required packages."
|
||||
|
||||
# Install Python libraries
|
||||
info "Installing Python libraries..."
|
||||
sudo pip3 install pyserial requests RPi.GPIO adafruit-circuitpython-bme280 crcmod psutil gpiozero ntplib pytz --break-system-packages || error "Failed to install Python libraries."
|
||||
|
||||
# Clone the repository (check if it exists first)
|
||||
REPO_DIR="/var/www/nebuleair_pro_4g"
|
||||
if [[ -d "$REPO_DIR" ]]; then
|
||||
warning "Repository already exists. Will update instead of clone."
|
||||
# Save current directory
|
||||
local current_dir=$(pwd)
|
||||
# Navigate to repository directory
|
||||
cd "$REPO_DIR"
|
||||
# Stash any local changes
|
||||
sudo git stash || warning "Failed to stash local changes"
|
||||
# Pull latest changes
|
||||
sudo git pull || error "Failed to pull latest changes"
|
||||
# Return to original directory
|
||||
cd "$current_dir"
|
||||
success "Repository updated successfully!"
|
||||
else
|
||||
info "Cloning the NebuleAir Pro 4G repository..."
|
||||
sudo git clone http://gitea.aircarto.fr/PaulVua/nebuleair_pro_4g.git "$REPO_DIR" || error "Failed to clone repository."
|
||||
fi
|
||||
|
||||
# Set up repository files and permissions
|
||||
info "Setting up repository files and permissions..."
|
||||
sudo mkdir -p "$REPO_DIR/logs"
|
||||
sudo touch "$REPO_DIR/logs/app.log" "$REPO_DIR/logs/master.log" "$REPO_DIR/logs/master_errors.log" "$REPO_DIR/wifi_list.csv"
|
||||
sudo chmod -R 755 "$REPO_DIR/"
|
||||
sudo chown -R www-data:www-data "$REPO_DIR/"
|
||||
sudo git config --global core.fileMode false
|
||||
#sudo git -C /var/www/nebuleair_pro_4g config core.fileMode false
|
||||
sudo git config --global --add safe.directory "$REPO_DIR"
|
||||
|
||||
# Set up cron jobs (ensure file exists first)
|
||||
info "Setting up cron jobs..."
|
||||
if [[ -f "$REPO_DIR/cron_jobs" ]]; then
|
||||
sudo crontab "$REPO_DIR/cron_jobs"
|
||||
success "Cron jobs set up successfully."
|
||||
else
|
||||
warning "Cron jobs file not found. Skipping."
|
||||
fi
|
||||
|
||||
# Create databases
|
||||
info "Creating databases..."
|
||||
if [[ -f "$REPO_DIR/sqlite/create_db.py" ]]; then
|
||||
sudo /usr/bin/python3 "$REPO_DIR/sqlite/create_db.py" || error "Failed to create databases."
|
||||
success "Databases created successfully."
|
||||
else
|
||||
warning "Database creation script not found."
|
||||
fi
|
||||
|
||||
# Set config
|
||||
info "Set config..."
|
||||
if [[ -f "$REPO_DIR/sqlite/set_config.py" ]]; then
|
||||
sudo /usr/bin/python3 "$REPO_DIR/sqlite/set_config.py" || error "Failed to set config."
|
||||
success "Databases created successfully."
|
||||
else
|
||||
warning "Database creation script not found."
|
||||
fi
|
||||
|
||||
# Configure Apache
|
||||
info "Configuring Apache..."
|
||||
APACHE_CONF="/etc/apache2/sites-available/000-default.conf"
|
||||
if grep -q "DocumentRoot /var/www/nebuleair_pro_4g" "$APACHE_CONF"; then
|
||||
warning "Apache configuration already set. Skipping."
|
||||
else
|
||||
sudo sed -i 's|DocumentRoot /var/www/html|DocumentRoot /var/www/nebuleair_pro_4g|' "$APACHE_CONF"
|
||||
sudo systemctl reload apache2
|
||||
success "Apache configuration updated and reloaded."
|
||||
fi
|
||||
|
||||
# Add sudo authorization (prevent duplicate entries)
|
||||
info "Setting up sudo authorization..."
|
||||
if ! sudo grep -q "/usr/bin/nmcli" /etc/sudoers; then
|
||||
echo -e "ALL ALL=(ALL) NOPASSWD: /usr/bin/nmcli, /usr/sbin/reboot\nwww-data ALL=(ALL) NOPASSWD: /usr/bin/git pull\nwww-data ALL=(ALL) NOPASSWD: /usr/bin/ssh\nwww-data ALL=(ALL) NOPASSWD: /usr/bin/python3 * www-data ALL=(ALL) NOPASSWD: /bin/systemctl * www-data ALL=(ALL) NOPASSWD: /var/www/nebuleair_pro_4g/*" | sudo tee -a /etc/sudoers > /dev/null
|
||||
success "Sudo authorization added."
|
||||
else
|
||||
warning "Sudo authorization already set. Skipping."
|
||||
fi
|
||||
|
||||
# Open all UART serial ports (avoid duplication)
|
||||
info "Configuring UART serial ports..."
|
||||
if ! grep -q "enable_uart=1" /boot/firmware/config.txt; then
|
||||
echo -e "\nenable_uart=1\ndtoverlay=uart0\ndtoverlay=uart1\ndtoverlay=uart2\ndtoverlay=uart3\ndtoverlay=uart4\ndtoverlay=uart5" | sudo tee -a /boot/firmware/config.txt > /dev/null
|
||||
success "UART configuration added."
|
||||
else
|
||||
warning "UART configuration already set. Skipping."
|
||||
fi
|
||||
|
||||
# Ensure correct permissions for serial devices
|
||||
info "Setting permissions for serial devices..."
|
||||
sudo chmod 666 /dev/ttyAMA* || warning "Failed to set permissions for /dev/ttyAMA*"
|
||||
|
||||
# Enable I2C ports
|
||||
info "Enabling I2C ports..."
|
||||
sudo raspi-config nonint do_i2c 0
|
||||
success "I2C ports enabled."
|
||||
|
||||
#creates databases
|
||||
info "Creates sqlites databases..."
|
||||
/usr/bin/python3 /var/www/nebuleair_pro_4g/sqlite/create_db.py
|
||||
|
||||
# Completion message
|
||||
success "Setup completed successfully!"
|
||||
info "System will reboot in 5 seconds..."
|
||||
sleep 5
|
||||
sudo reboot
|
||||
102
installation_part2.sh
Normal file
102
installation_part2.sh
Normal file
@@ -0,0 +1,102 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Script to set up the App after rebooting
|
||||
|
||||
# Exit on error, unset variable usage, and error in a pipeline
|
||||
set -euo pipefail
|
||||
|
||||
# Define color variables
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m' # No color
|
||||
|
||||
# Function to print messages in color
|
||||
info() { echo -e "${BLUE}[INFO]${NC} $1"; }
|
||||
success() { echo -e "${GREEN}[SUCCESS]${NC} $1"; }
|
||||
warning() { echo -e "${YELLOW}[WARNING]${NC} $1"; }
|
||||
error() { echo -e "${RED}[ERROR]${NC} $1"; exit 1; }
|
||||
|
||||
# Check for root privileges
|
||||
if [[ "$EUID" -ne 0 ]]; then
|
||||
error "This script must be run as root. Use 'sudo ./installation.sh'"
|
||||
fi
|
||||
REPO_DIR="/var/www/nebuleair_pro_4g"
|
||||
#set up the RTC
|
||||
info "Set up the RTC"
|
||||
/usr/bin/python3 /var/www/nebuleair_pro_4g/RTC/set_with_NTP.py
|
||||
|
||||
#Wake up SARA
|
||||
info "Wake Up SARA"
|
||||
pinctrl set 16 op
|
||||
pinctrl set 16 dh
|
||||
sleep 5
|
||||
|
||||
#Check SARA connection
|
||||
info "Check SARA connection"
|
||||
/usr/bin/python3 /var/www/nebuleair_pro_4g/SARA/sara.py ttyAMA2 ATI 2
|
||||
|
||||
#set up SARA R4 APN
|
||||
info "Set up Monogoto APN"
|
||||
/usr/bin/python3 /var/www/nebuleair_pro_4g/SARA/sara_setAPN.py ttyAMA2 data.mono 2
|
||||
|
||||
#activate blue network led on the SARA R4
|
||||
info "Activate blue LED"
|
||||
/usr/bin/python3 /var/www/nebuleair_pro_4g/SARA/sara.py ttyAMA2 AT+UGPIOC=16,2 2
|
||||
|
||||
#Connect to network
|
||||
info "Connect SARA R4 to network"
|
||||
python3 /var/www/nebuleair_pro_4g/SARA/sara_connectNetwork.py ttyAMA2 20810 60
|
||||
|
||||
#Need to create the two service
|
||||
# 1. start the scripts to set-up the services
|
||||
# 2. rtc_save_to_db
|
||||
|
||||
#1. set-up the services (SARA, NPM, BME280, etc)
|
||||
info "Setting up systemd services..."
|
||||
|
||||
if [[ -f "$REPO_DIR/services/setup_services.sh" ]]; then
|
||||
sudo chmod +x "$REPO_DIR/services/setup_services.sh"
|
||||
sudo "$REPO_DIR/services/setup_services.sh" || warning "Failed to set up systemd services"
|
||||
success "Systemd services set up successfully."
|
||||
else
|
||||
warning "Systemd services setup script not found."
|
||||
fi
|
||||
|
||||
#2. Add rtc_save_to_db.service
|
||||
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
|
||||
@@ -78,9 +78,8 @@ import re
|
||||
import os
|
||||
import traceback
|
||||
import sys
|
||||
from threading import Thread
|
||||
|
||||
import RPi.GPIO as GPIO
|
||||
from threading import Thread
|
||||
from adafruit_bme280 import basic as adafruit_bme280
|
||||
|
||||
# Record the start time of the script
|
||||
@@ -95,7 +94,6 @@ if uptime_seconds < 120:
|
||||
print(f"System just booted ({uptime_seconds:.2f} seconds uptime), skipping execution.")
|
||||
sys.exit()
|
||||
|
||||
url_nebuleair="data.nebuleair.fr"
|
||||
payload_csv = [None] * 20
|
||||
payload_json = {
|
||||
"nebuleairid": "82D25549434",
|
||||
@@ -185,7 +183,6 @@ i2C_sound_config = config.get('i2C_sound', False) #présence du capteur son
|
||||
send_aircarto = config.get('send_aircarto', True) #envoi sur AirCarto (data.nebuleair.fr)
|
||||
send_uSpot = config.get('send_uSpot', False) #envoi sur MicroSpot ()
|
||||
npm_5channel = config.get('NextPM_5channels', False) #5 canaux du NPM
|
||||
local_storage = config.get('local_storage', False) #enregistrement en local des data
|
||||
|
||||
envea_sondes = config.get('envea_sondes', [])
|
||||
connected_envea_sondes = [sonde for sonde in envea_sondes if sonde.get('connected', False)]
|
||||
|
||||
1442
loop/SARA_send_data_v2.py
Executable file
1442
loop/SARA_send_data_v2.py
Executable file
File diff suppressed because it is too large
Load Diff
23
config.json.dist → old/config.json.dist
Executable file → Normal file
23
config.json.dist → old/config.json.dist
Executable file → Normal file
@@ -1,16 +1,26 @@
|
||||
{
|
||||
"loop_activation": true,
|
||||
"loop_log": true,
|
||||
"boot_log": true,
|
||||
"modem_config_mode": false,
|
||||
"NPM/get_data_modbus_v3.py":true,
|
||||
"loop/SARA_send_data_v2.py": true,
|
||||
"RTC/save_to_db.py": true,
|
||||
"BME280/get_data_v2.py": true,
|
||||
"envea/read_value_v2.py": false,
|
||||
"MPPT/read.py": false,
|
||||
"windMeter/read.py": false,
|
||||
"sqlite/flush_old_data.py": true,
|
||||
"deviceID": "XXXX",
|
||||
"npm_5channel": false,
|
||||
"latitude_raw": 0,
|
||||
"longitude_raw":0,
|
||||
"latitude_precision": 0,
|
||||
"longitude_precision": 0,
|
||||
"deviceName": "NebuleAir-proXXX",
|
||||
"SaraR4_baudrate": 115200,
|
||||
"NPM_solo_port": "/dev/ttyAMA5",
|
||||
"NextPM_ports": [
|
||||
"ttyAMA5"
|
||||
],
|
||||
"NextPM_5channels": false,
|
||||
"i2C_sound": false,
|
||||
"i2c_BME": false,
|
||||
"i2c_RTC": false,
|
||||
"local_storage": false,
|
||||
"sshTunnel_port": 59228,
|
||||
@@ -18,11 +28,12 @@
|
||||
"SARA_R4_general_status": "connected",
|
||||
"SARA_R4_SIM_status": "connected",
|
||||
"SARA_R4_network_status": "connected",
|
||||
"SARA_R4_neworkID": 0,
|
||||
"SARA_R4_neworkID": 20810,
|
||||
"WIFI_status": "connected",
|
||||
"MQTT_GUI": false,
|
||||
"send_aircarto": true,
|
||||
"send_uSpot": false,
|
||||
"modem_version": "XXX",
|
||||
"envea_sondes": [
|
||||
{
|
||||
"connected": false,
|
||||
0
install_software.yaml → old/install_software.yaml
Executable file → Normal file
0
install_software.yaml → old/install_software.yaml
Executable file → Normal file
166
old/master.py
Executable file
166
old/master.py
Executable file
@@ -0,0 +1,166 @@
|
||||
'''
|
||||
__ __ _
|
||||
| \/ | __ _ ___| |_ ___ _ __
|
||||
| |\/| |/ _` / __| __/ _ \ '__|
|
||||
| | | | (_| \__ \ || __/ |
|
||||
|_| |_|\__,_|___/\__\___|_|
|
||||
|
||||
Master Python script that will trigger other scripts at every chosen time pace
|
||||
This script is triggered as a systemd service used as an alternative to cronjobs
|
||||
|
||||
Attention:
|
||||
to do -> prevent SARA R4 Script to run again if it's taking more than 60 secs to finish (using a lock file ??)
|
||||
|
||||
First time: need to create the service file
|
||||
|
||||
--> sudo nano /etc/systemd/system/master_nebuleair.service
|
||||
|
||||
⬇️
|
||||
[Unit]
|
||||
Description=Master manager for the Python loop scripts
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
ExecStart=/usr/bin/python3 /var/www/nebuleair_pro_4g/master.py
|
||||
Restart=always
|
||||
User=root
|
||||
WorkingDirectory=/var/www/nebuleair_pro_4g
|
||||
StandardOutput=append:/var/www/nebuleair_pro_4g/logs/master.log
|
||||
StandardError=append:/var/www/nebuleair_pro_4g/logs/master_errors.log
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
⬆️
|
||||
|
||||
Reload systemd (first time after creating the service):
|
||||
sudo systemctl daemon-reload
|
||||
|
||||
Enable (once), start (once and after stopping) and restart (after modification)systemd:
|
||||
sudo systemctl enable master_nebuleair.service
|
||||
sudo systemctl start master_nebuleair.service
|
||||
sudo systemctl restart master_nebuleair.service
|
||||
|
||||
Check the service status:
|
||||
sudo systemctl status master_nebuleair.service
|
||||
|
||||
|
||||
Specific scripts can be disabled with config.json
|
||||
Exemple: stop gathering data from NPM
|
||||
Exemple: stop sending data with SARA R4
|
||||
|
||||
'''
|
||||
import time
|
||||
import threading
|
||||
import subprocess
|
||||
import os
|
||||
import sqlite3
|
||||
|
||||
|
||||
# Base directory where scripts are stored
|
||||
SCRIPT_DIR = "/var/www/nebuleair_pro_4g/"
|
||||
DB_PATH = "/var/www/nebuleair_pro_4g/sqlite/sensors.db"
|
||||
|
||||
# Lock file path for SARA script
|
||||
SARA_LOCK_FILE = "/var/www/nebuleair_pro_4g/sara_script.lock"
|
||||
|
||||
|
||||
def is_script_enabled(script_name):
|
||||
"""Check if a script is enabled in the database."""
|
||||
try:
|
||||
conn = sqlite3.connect(DB_PATH)
|
||||
cursor = conn.cursor()
|
||||
|
||||
cursor.execute(
|
||||
"SELECT enabled FROM config_scripts_table WHERE script_path = ?",
|
||||
(script_name,)
|
||||
)
|
||||
|
||||
result = cursor.fetchone()
|
||||
conn.close()
|
||||
|
||||
if result is None:
|
||||
return True # Default to enabled if not found in database
|
||||
|
||||
return bool(result[0])
|
||||
except Exception:
|
||||
# If any database error occurs, default to enabled
|
||||
return True
|
||||
|
||||
|
||||
def create_lock_file():
|
||||
"""Create a lock file for the SARA script."""
|
||||
with open(SARA_LOCK_FILE, 'w') as f:
|
||||
f.write(str(int(time.time())))
|
||||
|
||||
|
||||
def remove_lock_file():
|
||||
"""Remove the SARA script lock file."""
|
||||
if os.path.exists(SARA_LOCK_FILE):
|
||||
os.remove(SARA_LOCK_FILE)
|
||||
|
||||
|
||||
def is_script_locked():
|
||||
"""Check if the SARA script is currently locked."""
|
||||
if not os.path.exists(SARA_LOCK_FILE):
|
||||
return False
|
||||
|
||||
# Check if lock is older than 60 seconds (stale)
|
||||
with open(SARA_LOCK_FILE, 'r') as f:
|
||||
try:
|
||||
lock_time = int(f.read().strip())
|
||||
if time.time() - lock_time > 60:
|
||||
# Lock is stale, remove it
|
||||
remove_lock_file()
|
||||
return False
|
||||
except ValueError:
|
||||
# Invalid lock file content
|
||||
remove_lock_file()
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def run_script(script_name, interval, delay=0):
|
||||
"""Run a script in a synchronized loop with an optional start delay."""
|
||||
script_path = os.path.join(SCRIPT_DIR, script_name) # Build full path
|
||||
next_run = time.monotonic() + delay # Apply the initial delay
|
||||
|
||||
while True:
|
||||
if is_script_enabled(script_name):
|
||||
# Special handling for SARA script to prevent concurrent runs
|
||||
if script_name == "loop/SARA_send_data_v2.py":
|
||||
if not is_script_locked():
|
||||
create_lock_file()
|
||||
try:
|
||||
subprocess.run(["python3", script_path], timeout=200)
|
||||
finally:
|
||||
remove_lock_file()
|
||||
else:
|
||||
# Run other scripts normally
|
||||
subprocess.run(["python3", script_path])
|
||||
|
||||
# Wait until the next exact interval
|
||||
next_run += interval
|
||||
sleep_time = max(0, next_run - time.monotonic()) # Prevent negative sleep times
|
||||
time.sleep(sleep_time)
|
||||
|
||||
|
||||
# Define scripts and their execution intervals (seconds)
|
||||
SCRIPTS = [
|
||||
# Format: (script_name, interval_in_seconds, start_delay_in_seconds)
|
||||
("NPM/get_data_modbus_v3.py", 10, 0), # Get NPM data (modbus 5 channels) every 10s
|
||||
("envea/read_value_v2.py", 10, 0), # Get Envea data every 10s
|
||||
("loop/SARA_send_data_v2.py", 60, 1), # Send data every 60 seconds, with 1s delay
|
||||
("BME280/get_data_v2.py", 120, 0), # Get BME280 data every 120 seconds
|
||||
("MPPT/read.py", 120, 0), # Get MPPT data every 120 seconds
|
||||
("sqlite/flush_old_data.py", 86400, 0) # Flush old data inside db every day
|
||||
]
|
||||
|
||||
# Start threads for scripts
|
||||
for script_name, interval, delay in SCRIPTS:
|
||||
thread = threading.Thread(target=run_script, args=(script_name, interval, delay), daemon=True)
|
||||
thread.start()
|
||||
|
||||
# Keep the main script running
|
||||
while True:
|
||||
time.sleep(1)
|
||||
18
services/README.md
Normal file
18
services/README.md
Normal file
@@ -0,0 +1,18 @@
|
||||
# NebuleAir Pro Services
|
||||
|
||||
Les scripts importants tournent à l'aide d'un service et d'un timer associé.
|
||||
|
||||
Pour les installer:
|
||||
|
||||
sudo chmod +x /var/www/nebuleair_pro_4g/services/setup_services.sh
|
||||
sudo /var/www/nebuleair_pro_4g/services/setup_services.sh
|
||||
|
||||
Supprimer l'ancien master:
|
||||
sudo systemctl stop master_nebuleair.service
|
||||
sudo systemctl disable master_nebuleair.service
|
||||
|
||||
# Check les services
|
||||
|
||||
SARA:
|
||||
sudo systemctl status nebuleair-sara-data.service
|
||||
|
||||
39
services/check_services.sh
Normal file
39
services/check_services.sh
Normal file
@@ -0,0 +1,39 @@
|
||||
#!/bin/bash
|
||||
# File: /var/www/nebuleair_pro_4g/services/check_services.sh
|
||||
# Purpose: Check status of all NebuleAir services and logs
|
||||
# Install:
|
||||
# sudo chmod +x /var/www/nebuleair_pro_4g/services/check_services.sh
|
||||
# sudo /var/www/nebuleair_pro_4g/services/check_services.sh
|
||||
|
||||
echo "=== NebuleAir Services Status ==="
|
||||
echo ""
|
||||
|
||||
# Check status of all timers
|
||||
echo "--- TIMER STATUS ---"
|
||||
systemctl list-timers | grep nebuleair
|
||||
echo ""
|
||||
|
||||
# Check status of all services
|
||||
echo "--- SERVICE STATUS ---"
|
||||
for service in npm envea sara bme280 mppt db-cleanup; do
|
||||
status=$(systemctl is-active nebuleair-$service-data.service)
|
||||
timer_status=$(systemctl is-active nebuleair-$service-data.timer)
|
||||
|
||||
echo "nebuleair-$service-data: Service=$status, Timer=$timer_status"
|
||||
done
|
||||
echo ""
|
||||
|
||||
# Show recent logs for each service
|
||||
echo "--- RECENT LOGS (last 5 entries per service) ---"
|
||||
for service in npm envea sara bme280 mppt db-cleanup; do
|
||||
echo "[$service service logs]"
|
||||
journalctl -u nebuleair-$service-data.service -n 5 --no-pager
|
||||
echo ""
|
||||
done
|
||||
|
||||
echo "=== End of Report ==="
|
||||
echo ""
|
||||
echo "For detailed logs use:"
|
||||
echo " sudo journalctl -u nebuleair-[service]-data.service -f"
|
||||
echo "To restart a specific service timer:"
|
||||
echo " sudo systemctl restart nebuleair-[service]-data.timer"
|
||||
228
services/setup_services.sh
Normal file
228
services/setup_services.sh
Normal file
@@ -0,0 +1,228 @@
|
||||
#!/bin/bash
|
||||
# File: /var/www/nebuleair_pro_4g/services/setup_services.sh
|
||||
# Purpose: Set up all systemd services for NebuleAir data collection
|
||||
# to install:
|
||||
# sudo chmod +x /var/www/nebuleair_pro_4g/services/setup_services.sh
|
||||
# sudo /var/www/nebuleair_pro_4g/services/setup_services.sh
|
||||
|
||||
echo "Setting up NebuleAir systemd services and timers..."
|
||||
|
||||
# Create directory for logs if it doesn't exist
|
||||
mkdir -p /var/www/nebuleair_pro_4g/logs
|
||||
|
||||
# Create service and timer files for NPM Data
|
||||
cat > /etc/systemd/system/nebuleair-npm-data.service << 'EOL'
|
||||
[Unit]
|
||||
Description=NebuleAir NPM Data Collection Service
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
ExecStart=/usr/bin/python3 /var/www/nebuleair_pro_4g/NPM/get_data_modbus_v3.py
|
||||
User=root
|
||||
WorkingDirectory=/var/www/nebuleair_pro_4g
|
||||
StandardOutput=append:/var/www/nebuleair_pro_4g/logs/npm_service.log
|
||||
StandardError=append:/var/www/nebuleair_pro_4g/logs/npm_service_errors.log
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOL
|
||||
|
||||
cat > /etc/systemd/system/nebuleair-npm-data.timer << 'EOL'
|
||||
[Unit]
|
||||
Description=Run NebuleAir NPM Data Collection every 10 seconds
|
||||
Requires=nebuleair-npm-data.service
|
||||
|
||||
[Timer]
|
||||
OnBootSec=10s
|
||||
OnUnitActiveSec=10s
|
||||
AccuracySec=1s
|
||||
|
||||
[Install]
|
||||
WantedBy=timers.target
|
||||
EOL
|
||||
|
||||
# Create service and timer files for Envea Data
|
||||
cat > /etc/systemd/system/nebuleair-envea-data.service << 'EOL'
|
||||
[Unit]
|
||||
Description=NebuleAir Envea Data Collection Service
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
ExecStart=/usr/bin/python3 /var/www/nebuleair_pro_4g/envea/read_value_v2.py
|
||||
User=root
|
||||
WorkingDirectory=/var/www/nebuleair_pro_4g
|
||||
StandardOutput=append:/var/www/nebuleair_pro_4g/logs/envea_service.log
|
||||
StandardError=append:/var/www/nebuleair_pro_4g/logs/envea_service_errors.log
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOL
|
||||
|
||||
cat > /etc/systemd/system/nebuleair-envea-data.timer << 'EOL'
|
||||
[Unit]
|
||||
Description=Run NebuleAir Envea Data Collection every 10 seconds
|
||||
Requires=nebuleair-envea-data.service
|
||||
|
||||
[Timer]
|
||||
OnBootSec=10s
|
||||
OnUnitActiveSec=10s
|
||||
AccuracySec=1s
|
||||
|
||||
[Install]
|
||||
WantedBy=timers.target
|
||||
EOL
|
||||
|
||||
# Create service and timer files for SARA Data (No Lock File Needed)
|
||||
cat > /etc/systemd/system/nebuleair-sara-data.service << 'EOL'
|
||||
[Unit]
|
||||
Description=NebuleAir SARA Data Transmission Service
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
ExecStart=/usr/bin/python3 /var/www/nebuleair_pro_4g/loop/SARA_send_data_v2.py
|
||||
User=root
|
||||
WorkingDirectory=/var/www/nebuleair_pro_4g
|
||||
StandardOutput=append:/var/www/nebuleair_pro_4g/logs/sara_service.log
|
||||
StandardError=append:/var/www/nebuleair_pro_4g/logs/sara_service_errors.log
|
||||
RuntimeMaxSec=200s
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOL
|
||||
|
||||
cat > /etc/systemd/system/nebuleair-sara-data.timer << 'EOL'
|
||||
[Unit]
|
||||
Description=Run NebuleAir SARA Data Transmission every 60 seconds
|
||||
Requires=nebuleair-sara-data.service
|
||||
|
||||
[Timer]
|
||||
OnBootSec=60s
|
||||
OnUnitActiveSec=60s
|
||||
AccuracySec=1s
|
||||
# This is the key setting that prevents overlap
|
||||
Persistent=true
|
||||
|
||||
[Install]
|
||||
WantedBy=timers.target
|
||||
EOL
|
||||
|
||||
# Create service and timer files for BME280 Data
|
||||
cat > /etc/systemd/system/nebuleair-bme280-data.service << 'EOL'
|
||||
[Unit]
|
||||
Description=NebuleAir BME280 Data Collection Service
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
ExecStart=/usr/bin/python3 /var/www/nebuleair_pro_4g/BME280/get_data_v2.py
|
||||
User=root
|
||||
WorkingDirectory=/var/www/nebuleair_pro_4g
|
||||
StandardOutput=append:/var/www/nebuleair_pro_4g/logs/bme280_service.log
|
||||
StandardError=append:/var/www/nebuleair_pro_4g/logs/bme280_service_errors.log
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOL
|
||||
|
||||
cat > /etc/systemd/system/nebuleair-bme280-data.timer << 'EOL'
|
||||
[Unit]
|
||||
Description=Run NebuleAir BME280 Data Collection every 120 seconds
|
||||
Requires=nebuleair-bme280-data.service
|
||||
|
||||
[Timer]
|
||||
OnBootSec=120s
|
||||
OnUnitActiveSec=120s
|
||||
AccuracySec=1s
|
||||
|
||||
[Install]
|
||||
WantedBy=timers.target
|
||||
EOL
|
||||
|
||||
# Create service and timer files for MPPT Data
|
||||
cat > /etc/systemd/system/nebuleair-mppt-data.service << 'EOL'
|
||||
[Unit]
|
||||
Description=NebuleAir MPPT Data Collection Service
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
ExecStart=/usr/bin/python3 /var/www/nebuleair_pro_4g/MPPT/read.py
|
||||
User=root
|
||||
WorkingDirectory=/var/www/nebuleair_pro_4g
|
||||
StandardOutput=append:/var/www/nebuleair_pro_4g/logs/mppt_service.log
|
||||
StandardError=append:/var/www/nebuleair_pro_4g/logs/mppt_service_errors.log
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOL
|
||||
|
||||
cat > /etc/systemd/system/nebuleair-mppt-data.timer << 'EOL'
|
||||
[Unit]
|
||||
Description=Run NebuleAir MPPT Data Collection every 120 seconds
|
||||
Requires=nebuleair-mppt-data.service
|
||||
|
||||
[Timer]
|
||||
OnBootSec=120s
|
||||
OnUnitActiveSec=120s
|
||||
AccuracySec=1s
|
||||
|
||||
[Install]
|
||||
WantedBy=timers.target
|
||||
EOL
|
||||
|
||||
# Create service and timer files for Database Cleanup
|
||||
cat > /etc/systemd/system/nebuleair-db-cleanup-data.service << 'EOL'
|
||||
[Unit]
|
||||
Description=NebuleAir Database Cleanup Service
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
ExecStart=/usr/bin/python3 /var/www/nebuleair_pro_4g/sqlite/flush_old_data.py
|
||||
User=root
|
||||
WorkingDirectory=/var/www/nebuleair_pro_4g
|
||||
StandardOutput=append:/var/www/nebuleair_pro_4g/logs/db_cleanup_service.log
|
||||
StandardError=append:/var/www/nebuleair_pro_4g/logs/db_cleanup_service_errors.log
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOL
|
||||
|
||||
cat > /etc/systemd/system/nebuleair-db-cleanup-data.timer << 'EOL'
|
||||
[Unit]
|
||||
Description=Run NebuleAir Database Cleanup daily
|
||||
Requires=nebuleair-db-cleanup-data.service
|
||||
|
||||
[Timer]
|
||||
OnBootSec=1h
|
||||
OnUnitActiveSec=24h
|
||||
AccuracySec=1h
|
||||
|
||||
[Install]
|
||||
WantedBy=timers.target
|
||||
EOL
|
||||
|
||||
# Reload systemd to recognize new services
|
||||
systemctl daemon-reload
|
||||
|
||||
# Enable and start all timers
|
||||
echo "Enabling and starting all services..."
|
||||
for service in npm envea sara bme280 mppt db-cleanup; do
|
||||
systemctl enable nebuleair-$service-data.timer
|
||||
systemctl start nebuleair-$service-data.timer
|
||||
echo "Started nebuleair-$service-data timer"
|
||||
done
|
||||
|
||||
echo "Checking status of all timers..."
|
||||
systemctl list-timers | grep nebuleair
|
||||
|
||||
echo "Setup complete. All NebuleAir services are now running."
|
||||
echo "To check the status of a specific service:"
|
||||
echo " sudo systemctl status nebuleair-npm-data.service"
|
||||
echo "To view logs for a specific service:"
|
||||
echo " sudo journalctl -u nebuleair-npm-data.service"
|
||||
echo "To restart a specific timer:"
|
||||
echo " sudo systemctl restart nebuleair-npm-data.timer"
|
||||
@@ -1,31 +1,102 @@
|
||||
'''
|
||||
____ ___ _ _ _
|
||||
/ ___| / _ \| | (_) |_ ___
|
||||
\___ \| | | | | | | __/ _ \
|
||||
___) | |_| | |___| | || __/
|
||||
|____/ \__\_\_____|_|\__\___|
|
||||
|
||||
Script to create a sqlite database
|
||||
/usr/bin/python3 /var/www/nebuleair_pro_4g/sqlite/create_db.py
|
||||
|
||||
|
||||
|
||||
in case of readonly error:
|
||||
sudo chmod 777 /var/www/nebuleair_pro_4g/sqlite/sensors.db
|
||||
'''
|
||||
|
||||
import sqlite3
|
||||
|
||||
# Connect to (or create) the database
|
||||
# Connect to (or create if not existent) the database
|
||||
conn = sqlite3.connect("/var/www/nebuleair_pro_4g/sqlite/sensors.db")
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Create a table for storing sensor data
|
||||
#create a config table
|
||||
cursor.execute("""
|
||||
CREATE TABLE IF NOT EXISTS data (
|
||||
CREATE TABLE IF NOT EXISTS config_table (
|
||||
key TEXT PRIMARY KEY,
|
||||
value TEXT NOT NULL,
|
||||
type TEXT NOT NULL
|
||||
)
|
||||
""")
|
||||
|
||||
#creates a config table for envea sondes
|
||||
cursor.execute("""
|
||||
CREATE TABLE IF NOT EXISTS envea_sondes_table (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
connected INTEGER NOT NULL,
|
||||
port TEXT NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
coefficient REAL NOT NULL
|
||||
)
|
||||
""")
|
||||
|
||||
# Create a table timer
|
||||
cursor.execute("""
|
||||
CREATE TABLE IF NOT EXISTS timestamp_table (
|
||||
id INTEGER PRIMARY KEY CHECK (id = 1), -- Enforce single row by using fixed ID
|
||||
last_updated DATETIME NOT NULL
|
||||
)
|
||||
""")
|
||||
cursor.execute("""
|
||||
INSERT OR REPLACE INTO timestamp_table (id, last_updated)
|
||||
VALUES (1, CURRENT_TIMESTAMP);
|
||||
""")
|
||||
|
||||
#create a modem status table
|
||||
cursor.execute("""
|
||||
CREATE TABLE IF NOT EXISTS modem_status (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
timestamp TEXT,
|
||||
status TEXT
|
||||
)
|
||||
""")
|
||||
|
||||
# Create a table NPM
|
||||
cursor.execute("""
|
||||
CREATE TABLE IF NOT EXISTS data_NPM (
|
||||
timestamp TEXT,
|
||||
sensor_id TEXT,
|
||||
PM1 REAL,
|
||||
PM25 REAL,
|
||||
PM10 REAL,
|
||||
temp REAL,
|
||||
hum REAL,
|
||||
press REAL,
|
||||
temp_npm REAL,
|
||||
hum_npm REAL
|
||||
)
|
||||
""")
|
||||
|
||||
# Create a table BME280
|
||||
cursor.execute("""
|
||||
CREATE TABLE IF NOT EXISTS data_BME280 (
|
||||
timestamp TEXT,
|
||||
temperature REAL,
|
||||
humidity REAL,
|
||||
pressure REAL
|
||||
)
|
||||
""")
|
||||
|
||||
# Create a table cairsens
|
||||
cursor.execute("""
|
||||
CREATE TABLE IF NOT EXISTS data_envea (
|
||||
timestamp TEXT,
|
||||
no2 REAL,
|
||||
h2s REAL,
|
||||
o3 REAL,
|
||||
nh3 REAL,
|
||||
co REAL,
|
||||
o3 REAL
|
||||
)
|
||||
""")
|
||||
|
||||
# Create a table NPM_5ch
|
||||
cursor.execute("""
|
||||
CREATE TABLE IF NOT EXISTS data_NPM_5channels (
|
||||
timestamp TEXT,
|
||||
PM_ch1 INTEGER,
|
||||
PM_ch2 INTEGER,
|
||||
PM_ch3 INTEGER,
|
||||
@@ -34,6 +105,28 @@ CREATE TABLE IF NOT EXISTS data (
|
||||
)
|
||||
""")
|
||||
|
||||
# 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
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
71
sqlite/flush_old_data.py
Executable file
71
sqlite/flush_old_data.py
Executable file
@@ -0,0 +1,71 @@
|
||||
'''
|
||||
____ ___ _ _ _
|
||||
/ ___| / _ \| | (_) |_ ___
|
||||
\___ \| | | | | | | __/ _ \
|
||||
___) | |_| | |___| | || __/
|
||||
|____/ \__\_\_____|_|\__\___|
|
||||
|
||||
Script to flush (delete) data from a sqlite database
|
||||
|
||||
/usr/bin/python3 /var/www/nebuleair_pro_4g/sqlite/flush_old_data.py
|
||||
|
||||
Available table are
|
||||
|
||||
data_NPM
|
||||
data_NPM_5channels
|
||||
data_BME280
|
||||
data_envea
|
||||
timestamp_table
|
||||
|
||||
'''
|
||||
|
||||
import sqlite3
|
||||
import datetime
|
||||
|
||||
|
||||
# Connect to the SQLite database
|
||||
conn = sqlite3.connect("/var/www/nebuleair_pro_4g/sqlite/sensors.db")
|
||||
cursor = conn.cursor()
|
||||
|
||||
#GET RTC TIME from SQlite
|
||||
cursor.execute("SELECT * FROM timestamp_table LIMIT 1")
|
||||
row = cursor.fetchone() # Get the first (and only) row
|
||||
|
||||
if row:
|
||||
rtc_time_str = row[1] # Assuming timestamp is stored as TEXT (YYYY-MM-DD HH:MM:SS)
|
||||
print(f"[INFO] Last recorded timestamp: {rtc_time_str}")
|
||||
|
||||
# Convert last_updated to a datetime object
|
||||
last_updated = datetime.datetime.strptime(rtc_time_str, "%Y-%m-%d %H:%M:%S")
|
||||
|
||||
# Calculate the cutoff date (3 months before last_updated)
|
||||
cutoff_date = last_updated - datetime.timedelta(days=60)
|
||||
cutoff_date_str = cutoff_date.strftime("%Y-%m-%d %H:%M:%S")
|
||||
|
||||
print(f"[INFO] Deleting records older than: {cutoff_date_str}")
|
||||
|
||||
# List of tables to delete old data from
|
||||
tables_to_clean = ["data_NPM", "data_NPM_5channels", "data_BME280", "data_envea","data_WIND", "data_MPPT"]
|
||||
|
||||
# Loop through each table and delete old data
|
||||
for table in tables_to_clean:
|
||||
delete_query = f"DELETE FROM {table} WHERE timestamp < ?"
|
||||
cursor.execute(delete_query, (cutoff_date_str,))
|
||||
print(f"[INFO] Deleted old records from {table}")
|
||||
|
||||
# **Commit changes before running VACUUM**
|
||||
conn.commit()
|
||||
print("[INFO] Changes committed successfully!")
|
||||
|
||||
# Now it's safe to run VACUUM
|
||||
print("[INFO] Running VACUUM to optimize database space...")
|
||||
cursor.execute("VACUUM")
|
||||
|
||||
print("[SUCCESS] Old data flushed successfully!")
|
||||
|
||||
else:
|
||||
print("[ERROR] No timestamp found in timestamp_table.")
|
||||
|
||||
|
||||
# Close the database connection
|
||||
conn.close()
|
||||
@@ -1,19 +1,49 @@
|
||||
'''
|
||||
____ ___ _ _ _
|
||||
/ ___| / _ \| | (_) |_ ___
|
||||
\___ \| | | | | | | __/ _ \
|
||||
___) | |_| | |___| | || __/
|
||||
|____/ \__\_\_____|_|\__\___|
|
||||
|
||||
Script to read data from a sqlite database
|
||||
/usr/bin/python3 /var/www/nebuleair_pro_4g/sqlite/read.py
|
||||
/usr/bin/python3 /var/www/nebuleair_pro_4g/sqlite/read.py data_NPM 10
|
||||
|
||||
Available table are
|
||||
data_NPM
|
||||
data_NPM_5channels
|
||||
data_BME280
|
||||
data_envea
|
||||
timestamp_table
|
||||
data_MPPT
|
||||
data_WIND
|
||||
|
||||
'''
|
||||
|
||||
import sqlite3
|
||||
import sys
|
||||
parameter = sys.argv[1:] # Exclude the script name
|
||||
#print("Parameters received:")
|
||||
table_name=parameter[0]
|
||||
limit_num=parameter[1]
|
||||
|
||||
# Connect to the SQLite database
|
||||
conn = sqlite3.connect("/var/www/nebuleair_pro_4g/sqlite/sensors.db")
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Retrieve the last 10 sensor readings
|
||||
cursor.execute("SELECT * FROM data ORDER BY timestamp DESC LIMIT 10")
|
||||
#cursor.execute("SELECT * FROM data_NPM ORDER BY timestamp DESC LIMIT 10")
|
||||
#cursor.execute("SELECT * FROM data_BME280 ORDER BY timestamp DESC LIMIT 10")
|
||||
#cursor.execute("SELECT * FROM timestamp_table")
|
||||
if table_name == "timestamp_table":
|
||||
cursor.execute("SELECT * FROM timestamp_table")
|
||||
else:
|
||||
query = f"SELECT * FROM {table_name} ORDER BY timestamp DESC LIMIT ?"
|
||||
cursor.execute(query, (limit_num,))
|
||||
|
||||
|
||||
rows = cursor.fetchall()
|
||||
rows.reverse() # Reverse the order in Python (to get ascending order)
|
||||
|
||||
|
||||
# Display the results
|
||||
for row in rows:
|
||||
|
||||
43
sqlite/read_config.py
Normal file
43
sqlite/read_config.py
Normal file
@@ -0,0 +1,43 @@
|
||||
'''
|
||||
____ ___ _ _ _
|
||||
/ ___| / _ \| | (_) |_ ___
|
||||
\___ \| | | | | | | __/ _ \
|
||||
___) | |_| | |___| | || __/
|
||||
|____/ \__\_\_____|_|\__\___|
|
||||
|
||||
Script to read data from a sqlite database
|
||||
/usr/bin/python3 /var/www/nebuleair_pro_4g/sqlite/read_config.py config_table
|
||||
|
||||
Available table are
|
||||
config_table
|
||||
config_scripts_table
|
||||
envea_sondes_table
|
||||
|
||||
'''
|
||||
|
||||
import sqlite3
|
||||
import sys
|
||||
parameter = sys.argv[1:] # Exclude the script name
|
||||
#print("Parameters received:")
|
||||
table_name=parameter[0]
|
||||
|
||||
|
||||
# Connect to the SQLite database
|
||||
conn = sqlite3.connect("/var/www/nebuleair_pro_4g/sqlite/sensors.db")
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Retrieve the data
|
||||
query = f"SELECT * FROM {table_name}"
|
||||
cursor.execute(query)
|
||||
|
||||
|
||||
rows = cursor.fetchall()
|
||||
rows.reverse() # Reverse the order in Python (to get ascending order)
|
||||
|
||||
|
||||
# Display the results
|
||||
for row in rows:
|
||||
print(row)
|
||||
|
||||
# Close the database connection
|
||||
conn.close()
|
||||
59
sqlite/read_select_date.py
Executable file
59
sqlite/read_select_date.py
Executable file
@@ -0,0 +1,59 @@
|
||||
'''
|
||||
____ ___ _ _ _
|
||||
/ ___| / _ \| | (_) |_ ___
|
||||
\___ \| | | | | | | __/ _ \
|
||||
___) | |_| | |___| | || __/
|
||||
|____/ \__\_\_____|_|\__\___|
|
||||
|
||||
Script to read data from a sqlite database using start date and end date
|
||||
|
||||
/usr/bin/python3 /var/www/nebuleair_pro_4g/sqlite/read_select_date.py data_NPM 2025-02-09 2025-02-11
|
||||
|
||||
Available table are
|
||||
data_NPM
|
||||
data_NPM_5channels
|
||||
data_BME280
|
||||
data_envea
|
||||
timestamp_table
|
||||
|
||||
'''
|
||||
|
||||
import sqlite3
|
||||
import sys
|
||||
|
||||
parameter = sys.argv[1:] # Exclude the script name
|
||||
#print("Parameters received:")
|
||||
table_name=parameter[0]
|
||||
start_date=parameter[1]
|
||||
end_date=parameter[2]
|
||||
|
||||
# Convert to full timestamp range
|
||||
start_timestamp = f"{start_date} 00:00:00"
|
||||
end_timestamp = f"{end_date} 23:59:59"
|
||||
|
||||
# Connect to the SQLite database
|
||||
conn = sqlite3.connect("/var/www/nebuleair_pro_4g/sqlite/sensors.db")
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Retrieve the last 10 sensor readings
|
||||
#cursor.execute("SELECT * FROM data_NPM ORDER BY timestamp DESC LIMIT 10")
|
||||
#cursor.execute("SELECT * FROM data_BME280 ORDER BY timestamp DESC LIMIT 10")
|
||||
#cursor.execute("SELECT * FROM timestamp_table")
|
||||
if table_name == "timestamp_table":
|
||||
cursor.execute("SELECT * FROM timestamp_table")
|
||||
|
||||
else:
|
||||
query = f"SELECT * FROM {table_name} WHERE timestamp BETWEEN ? AND ? ORDER BY timestamp ASC"
|
||||
cursor.execute(query, (start_timestamp, end_timestamp))
|
||||
|
||||
|
||||
rows = cursor.fetchall()
|
||||
rows.reverse() # Reverse the order in Python (to get ascending order)
|
||||
|
||||
|
||||
# Display the results
|
||||
for row in rows:
|
||||
print(row)
|
||||
|
||||
# Close the database connection
|
||||
conn.close()
|
||||
77
sqlite/set_config.py
Normal file
77
sqlite/set_config.py
Normal file
@@ -0,0 +1,77 @@
|
||||
'''
|
||||
____ ___ _ _ _
|
||||
/ ___| / _ \| | (_) |_ ___
|
||||
\___ \| | | | | | | __/ _ \
|
||||
___) | |_| | |___| | || __/
|
||||
|____/ \__\_\_____|_|\__\___|
|
||||
|
||||
Script to set the config
|
||||
/usr/bin/python3 /var/www/nebuleair_pro_4g/sqlite/set_config.py
|
||||
|
||||
in case of readonly error:
|
||||
sudo chmod 777 /var/www/nebuleair_pro_4g/sqlite/sensors.db
|
||||
'''
|
||||
|
||||
import sqlite3
|
||||
|
||||
# Connect to (or create if not existent) the database
|
||||
conn = sqlite3.connect("/var/www/nebuleair_pro_4g/sqlite/sensors.db")
|
||||
cursor = conn.cursor()
|
||||
|
||||
print(f"Connected to database")
|
||||
|
||||
# Note: Using INSERT OR IGNORE to add only new configurations without overwriting existing ones
|
||||
print("Adding new configurations (existing ones will be preserved)")
|
||||
|
||||
|
||||
# Insert general configurations
|
||||
config_entries = [
|
||||
("modem_config_mode", "0", "bool"),
|
||||
("deviceID", "XXXX", "str"),
|
||||
("latitude_raw", "0", "int"),
|
||||
("longitude_raw", "0", "int"),
|
||||
("latitude_precision", "0", "int"),
|
||||
("longitude_precision", "0", "int"),
|
||||
("deviceName", "NebuleAir-proXXX", "str"),
|
||||
("SaraR4_baudrate", "115200", "int"),
|
||||
("NPM_solo_port", "/dev/ttyAMA5", "str"),
|
||||
("sshTunnel_port", "59228", "int"),
|
||||
("SARA_R4_general_status", "connected", "str"),
|
||||
("SARA_R4_SIM_status", "connected", "str"),
|
||||
("SARA_R4_network_status", "connected", "str"),
|
||||
("SARA_R4_neworkID", "20810", "int"),
|
||||
("WIFI_status", "connected", "str"),
|
||||
("send_uSpot", "0", "bool"),
|
||||
("npm_5channel", "0", "bool"),
|
||||
("envea", "0", "bool"),
|
||||
("windMeter", "0", "bool"),
|
||||
("BME280", "0", "bool"),
|
||||
("MPPT", "0", "bool"),
|
||||
("modem_version", "XXX", "str")
|
||||
]
|
||||
|
||||
for key, value, value_type in config_entries:
|
||||
cursor.execute(
|
||||
"INSERT OR IGNORE INTO config_table (key, value, type) VALUES (?, ?, ?)",
|
||||
(key, value, value_type)
|
||||
)
|
||||
|
||||
# Insert envea sondes
|
||||
envea_sondes = [
|
||||
(False, "ttyAMA4", "h2s", 4),
|
||||
(False, "ttyAMA3", "no2", 1),
|
||||
(False, "ttyAMA2", "o3", 1)
|
||||
]
|
||||
|
||||
for connected, port, name, coefficient in envea_sondes:
|
||||
cursor.execute(
|
||||
"INSERT OR IGNORE INTO envea_sondes_table (connected, port, name, coefficient) VALUES (?, ?, ?, ?)",
|
||||
(1 if connected else 0, port, name, coefficient)
|
||||
)
|
||||
|
||||
|
||||
# Commit and close the connection
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
print("Database updated successfully!")
|
||||
@@ -1,4 +1,10 @@
|
||||
'''
|
||||
____ ___ _ _ _
|
||||
/ ___| / _ \| | (_) |_ ___
|
||||
\___ \| | | | | | | __/ _ \
|
||||
___) | |_| | |___| | || __/
|
||||
|____/ \__\_\_____|_|\__\___|
|
||||
|
||||
Script to write data to a sqlite database
|
||||
/usr/bin/python3 /var/www/nebuleair_pro_4g/sqlite/write.py
|
||||
|
||||
@@ -12,14 +18,13 @@ cursor = conn.cursor()
|
||||
|
||||
# Insert a sample temperature reading
|
||||
timestamp = "2025-10-11"
|
||||
sensor_name = "NebuleAir-pro020"
|
||||
PM1 = 25.3
|
||||
PM25 = 18.3
|
||||
PM10 = 9.3
|
||||
|
||||
cursor.execute('''
|
||||
INSERT INTO data (timestamp, sensor_id, PM1, PM25, PM10) VALUES (?,?,?,?,?)'''
|
||||
, (timestamp, sensor_name,PM1,PM25,PM10))
|
||||
INSERT INTO data (timestamp, PM1, PM25, PM10) VALUES (?,?,?,?,?)'''
|
||||
, (timestamp,PM1,PM25,PM10))
|
||||
|
||||
# Commit and close the connection
|
||||
conn.commit()
|
||||
|
||||
118
update_firmware.sh
Normal file
118
update_firmware.sh
Normal file
@@ -0,0 +1,118 @@
|
||||
#!/bin/bash
|
||||
|
||||
# NebuleAir Pro 4G - Comprehensive Update Script
|
||||
# This script performs a complete system update including git pull,
|
||||
# config initialization, and service management
|
||||
|
||||
echo "======================================"
|
||||
echo "NebuleAir Pro 4G - Firmware Update"
|
||||
echo "======================================"
|
||||
echo "Started at: $(date)"
|
||||
echo ""
|
||||
|
||||
# Set working directory
|
||||
cd /var/www/nebuleair_pro_4g
|
||||
|
||||
# Function to print status messages
|
||||
print_status() {
|
||||
echo "[$(date '+%H:%M:%S')] $1"
|
||||
}
|
||||
|
||||
# Function to check command success
|
||||
check_status() {
|
||||
if [ $? -eq 0 ]; then
|
||||
print_status "✓ $1 completed successfully"
|
||||
else
|
||||
print_status "✗ $1 failed"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Step 1: Git operations
|
||||
print_status "Step 1: Updating firmware from repository..."
|
||||
git fetch origin
|
||||
check_status "Git fetch"
|
||||
|
||||
# Show current branch and any changes
|
||||
print_status "Current branch: $(git branch --show-current)"
|
||||
if [ -n "$(git status --porcelain)" ]; then
|
||||
print_status "Warning: Local changes detected:"
|
||||
git status --short
|
||||
fi
|
||||
|
||||
# Pull latest changes
|
||||
git pull origin $(git branch --show-current)
|
||||
check_status "Git pull"
|
||||
|
||||
# Step 2: Update database configuration
|
||||
print_status ""
|
||||
print_status "Step 2: Updating database configuration..."
|
||||
/usr/bin/python3 /var/www/nebuleair_pro_4g/sqlite/set_config.py
|
||||
check_status "Database configuration update"
|
||||
|
||||
# Step 3: Check and fix file permissions
|
||||
print_status ""
|
||||
print_status "Step 3: Checking file permissions..."
|
||||
sudo chmod +x /var/www/nebuleair_pro_4g/update_firmware.sh
|
||||
sudo chmod 755 /var/www/nebuleair_pro_4g/sqlite/*.py
|
||||
sudo chmod 755 /var/www/nebuleair_pro_4g/NPM/*.py
|
||||
sudo chmod 755 /var/www/nebuleair_pro_4g/BME280/*.py
|
||||
sudo chmod 755 /var/www/nebuleair_pro_4g/SARA/*.py
|
||||
sudo chmod 755 /var/www/nebuleair_pro_4g/envea/*.py
|
||||
check_status "File permissions update"
|
||||
|
||||
# Step 4: Restart critical services if they exist
|
||||
print_status ""
|
||||
print_status "Step 4: Managing system services..."
|
||||
|
||||
# List of services to check and restart
|
||||
services=(
|
||||
"nebuleair-npm-data.timer"
|
||||
"nebuleair-envea-data.timer"
|
||||
"nebuleair-sara-data.timer"
|
||||
"nebuleair-bme280-data.timer"
|
||||
"nebuleair-mppt-data.timer"
|
||||
)
|
||||
|
||||
for service in "${services[@]}"; do
|
||||
if systemctl list-unit-files | grep -q "$service"; then
|
||||
print_status "Restarting service: $service"
|
||||
sudo systemctl restart "$service"
|
||||
if systemctl is-active --quiet "$service"; then
|
||||
print_status "✓ $service is running"
|
||||
else
|
||||
print_status "⚠ $service may not be active"
|
||||
fi
|
||||
else
|
||||
print_status "ℹ Service $service not found (may not be installed)"
|
||||
fi
|
||||
done
|
||||
|
||||
# Step 5: System health check
|
||||
print_status ""
|
||||
print_status "Step 5: System health check..."
|
||||
|
||||
# Check disk space
|
||||
disk_usage=$(df / | awk 'NR==2 {print $5}' | sed 's/%//')
|
||||
if [ "$disk_usage" -gt 90 ]; then
|
||||
print_status "⚠ Warning: Disk usage is high ($disk_usage%)"
|
||||
else
|
||||
print_status "✓ Disk usage is acceptable ($disk_usage%)"
|
||||
fi
|
||||
|
||||
# Check if database is accessible
|
||||
if [ -f "/var/www/nebuleair_pro_4g/sqlite/sensors.db" ]; then
|
||||
print_status "✓ Database file exists"
|
||||
else
|
||||
print_status "⚠ Warning: Database file not found"
|
||||
fi
|
||||
|
||||
# Step 6: Final cleanup
|
||||
print_status ""
|
||||
print_status "Step 6: Cleaning up..."
|
||||
sudo find /var/www/nebuleair_pro_4g/logs -name "*.log" -size +10M -exec truncate -s 0 {} \;
|
||||
check_status "Log cleanup"
|
||||
|
||||
print_status "Update completed successfully!"
|
||||
|
||||
exit 0
|
||||
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")
|
||||
140
windMeter/read.py
Normal file
140
windMeter/read.py
Normal file
@@ -0,0 +1,140 @@
|
||||
'''
|
||||
__ _____ _ _ ____
|
||||
\ \ / /_ _| \ | | _ \
|
||||
\ \ /\ / / | || \| | | | |
|
||||
\ 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
|
||||
|
||||
this need to run as a service
|
||||
|
||||
--> sudo nano /etc/systemd/system/windMeter.service
|
||||
|
||||
⬇️
|
||||
[Unit]
|
||||
Description=Master manager for the Python wind meter scripts
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
ExecStart=/usr/bin/python3 /var/www/nebuleair_pro_4g/windMeter/read.py
|
||||
Restart=always
|
||||
User=root
|
||||
WorkingDirectory=/var/www/nebuleair_pro_4g
|
||||
StandardOutput=append:/var/www/nebuleair_pro_4g/logs/wind.log
|
||||
StandardError=append:/var/www/nebuleair_pro_4g/logs/wind_errors.log
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
⬆️
|
||||
|
||||
Reload systemd (first time after creating the service):
|
||||
sudo systemctl daemon-reload
|
||||
|
||||
Enable (once), start (once and after stopping) and restart (after modification)systemd:
|
||||
sudo systemctl enable windMeter.service
|
||||
sudo systemctl start windMeter.service
|
||||
sudo systemctl restart windMeter.service
|
||||
|
||||
Check the service status:
|
||||
sudo systemctl status windMeter.service
|
||||
|
||||
'''
|
||||
#!/usr/bin/python3
|
||||
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