147 Commits

Author SHA1 Message Date
Your Name
020594e065 updates 2025-05-23 15:09:22 +02:00
PaulVua
5a1a4e0d81 updates 2025-05-23 14:38:32 +02:00
PaulVua
3cd5b13c25 updates 2025-05-23 14:31:23 +02:00
PaulVua
5a0f1c0745 updates 2025-05-23 14:30:18 +02:00
PaulVua
2516a3bd1c updates 2025-05-23 14:08:21 +02:00
PaulVua
1b8dc54fe0 updates 2025-05-23 14:03:57 +02:00
PaulVua
2bd74ca91a updates 2025-05-23 11:02:06 +02:00
PaulVua
f40c105abf updates 2025-05-23 10:48:41 +02:00
Your Name
fdef8e2df0 update 2025-05-20 11:57:55 +02:00
Your Name
386ad6fb03 update 2025-05-20 11:24:38 +02:00
Your Name
a7c138e93f update 2025-05-20 11:23:43 +02:00
Your Name
4e4832b128 update 2025-05-20 10:43:27 +02:00
Your Name
11463b175c update 2025-05-20 09:50:15 +02:00
Your Name
c06741b11d Merge remote-tracking branch 'refs/remotes/origin/main' 2025-05-20 09:46:46 +02:00
Your Name
b1352261e7 update 2025-05-20 09:45:59 +02:00
Your Name
376ff454bf update 2025-05-20 09:33:14 +02:00
Your Name
932fdf83a2 update 2025-05-20 09:21:34 +02:00
Your Name
1ca3e2ada2 update 2025-05-20 09:14:25 +02:00
Your Name
fd1d32a62b udpate 2025-05-19 10:26:27 +02:00
Your Name
61b302fe35 update 2025-05-16 11:08:23 +02:00
Your Name
2aaa229e82 update 2025-05-14 17:37:53 +02:00
Your Name
fd28069b0c update 2025-05-14 17:24:01 +02:00
Your Name
b17c996f2f update 2025-05-13 17:14:29 +02:00
Your Name
8273307cab update 2025-05-07 18:48:47 +02:00
Your Name
a73eb30d32 update 2025-05-07 18:25:30 +02:00
Your Name
ba889feee9 update 2025-05-06 17:30:14 +02:00
Your Name
12c7a0b6af update 2025-05-01 11:01:29 +02:00
Your Name
08c5ed8841 update 2025-04-16 09:53:54 +02:00
Your Name
7f5eb7608c update 2025-04-16 09:46:12 +02:00
Your Name
44f44c3361 update 2025-04-07 15:57:58 +02:00
Your Name
a8350332ac update 2025-04-07 11:47:21 +02:00
Your Name
6c6eed1ad6 update 2025-04-03 10:49:55 +02:00
Your Name
ee71c28d33 update 2025-04-03 09:30:54 +02:00
Your Name
6d3220665e update 2025-04-03 09:07:20 +02:00
Your Name
98e5a239f5 update 2025-04-02 16:10:27 +02:00
Your Name
17f4ce46dd update 2025-04-02 15:09:10 +02:00
Your Name
338b8a049f update 2025-04-02 14:43:18 +02:00
Your Name
1e9e80ae55 update 2025-04-01 17:35:35 +02:00
Your Name
9d280c6e37 update 2025-04-01 11:57:39 +02:00
Your Name
d4c1178b3d update 2025-03-28 14:29:51 +01:00
Your Name
f7f6fccd60 update 2025-03-27 17:13:40 +01:00
Your Name
afceb34c1b update 2025-03-27 16:43:55 +01:00
Your Name
7a958d5c8e update 2025-03-27 16:40:24 +01:00
Your Name
8fd76001f2 update 2025-03-27 16:02:56 +01:00
Your Name
e320a3bc2b update 2025-03-27 16:02:05 +01:00
Your Name
8a4e184699 update 2025-03-27 15:58:22 +01:00
Your Name
e61b0a76da update 2025-03-27 15:50:59 +01:00
Your Name
970a36598c update 2025-03-26 10:30:24 +01:00
Your Name
e75caff929 update 2025-03-26 08:29:24 +01:00
Your Name
e82d75a4d6 update 2025-03-26 08:27:28 +01:00
Your Name
dc27e5f139 update 2025-03-26 08:25:42 +01:00
Your Name
4bc05091be update 2025-03-25 21:18:12 +01:00
Your Name
29f9ec445a Merge remote-tracking branch 'refs/remotes/origin/main' 2025-03-25 20:23:01 +01:00
Your Name
7b398d0d6d update 2025-03-25 20:22:42 +01:00
PaulVua
76336d0073 update 2025-03-25 16:27:01 +01:00
PaulVua
46a8e21e64 update 2025-03-25 16:20:19 +01:00
Your Name
2129d45ef6 update 2025-03-25 14:55:50 +01:00
Your Name
6312cd8d72 update 2025-03-24 18:04:19 +01:00
Your Name
7c17ec82f5 update 2025-03-24 17:57:47 +01:00
Your Name
b7a6f4c907 add sqlite config management 2025-03-24 15:19:27 +01:00
Your Name
6b3329b9b8 Merge remote-tracking branch 'refs/remotes/origin/main' 2025-03-24 10:23:37 +01:00
PaulVua
e9b1e0e88e Merge remote-tracking branch 'refs/remotes/origin/main' 2025-03-24 10:21:25 +01:00
Your Name
2db732ebb3 Merge remote-tracking branch 'refs/remotes/origin/main' 2025-03-20 17:46:09 +01:00
Your Name
d5302f78ba udpate 2025-03-20 17:45:47 +01:00
Your Name
5b7de91d50 update 2025-03-20 10:54:41 +01:00
Your Name
4d15076d4b update 2025-03-20 09:56:36 +01:00
Your Name
809742b6d5 update 2025-03-20 09:49:14 +01:00
PaulVua
bca975b0c5 Merge remote-tracking branch 'refs/remotes/origin/main' 2025-03-18 18:02:14 +01:00
PaulVua
dfba956685 update 2025-03-18 18:01:33 +01:00
Your Name
d07314262e update 2025-03-18 16:24:22 +01:00
Your Name
dffa639574 update 2025-03-18 12:08:30 +01:00
PaulVua
1fd5a3e75c update 2025-03-18 11:50:39 +01:00
Your Name
e674b21eaa update 2025-03-17 15:17:07 +01:00
Your Name
efc94ba5e1 update 2025-03-17 15:13:16 +01:00
root
26328dec99 update 2025-03-17 12:29:06 +01:00
PaulVua
ec3e81e99e update 2025-03-17 11:00:55 +01:00
Your Name
1c6af36313 update 2025-03-14 10:57:45 +01:00
Your Name
f1d6f595ac update 2025-03-14 09:28:28 +01:00
Your Name
cfc2e0c47f update 2025-03-14 08:54:35 +01:00
Your Name
1037207df3 update 2025-03-13 11:39:40 +01:00
Your Name
14044a8856 update 2025-03-12 17:55:30 +01:00
Your Name
d57a47ef68 update 2025-03-11 16:35:49 +01:00
Your Name
5e7375cd4e update 2025-03-11 15:40:22 +01:00
Your Name
c42b16ddb6 Merge remote-tracking branch 'refs/remotes/origin/main' 2025-03-10 17:48:04 +01:00
Your Name
283a46eb0b update 2025-03-10 17:44:03 +01:00
Your Name
33b24a9f53 update 2025-03-10 15:00:00 +01:00
Your Name
10c4348e54 update 2025-03-10 13:38:02 +01:00
Your Name
072f98ef95 update 2025-03-10 12:15:05 +01:00
Your Name
7b4ff011ec update 2025-03-05 16:24:15 +01:00
Your Name
ab2124f50d update 2025-03-05 16:19:49 +01:00
Your Name
b493d30a41 update 2025-03-05 16:07:22 +01:00
Your Name
659effb7c4 update 2025-03-05 15:58:40 +01:00
Your Name
ebb0fd0a2b update 2025-03-05 09:29:53 +01:00
Your Name
5d121761e7 update 2025-03-04 13:29:02 +01:00
Your Name
d90fb14c90 update 2025-03-04 13:23:26 +01:00
Your Name
3f329e0afa update 2025-03-04 10:16:24 +01:00
Your Name
cd030a9e14 update 2025-03-04 10:06:26 +01:00
Your Name
709cad6981 update 2025-02-27 14:30:22 +01:00
Your Name
c59246e320 update 2025-02-27 12:17:13 +01:00
Your Name
1a15d70aa7 udpate 2025-02-27 12:08:54 +01:00
Your Name
bf9ece8589 update 2025-02-27 11:54:53 +01:00
Your Name
3d507ae659 update 2025-02-27 11:48:42 +01:00
Your Name
700de9c9f4 update 2025-02-27 11:07:52 +01:00
Your Name
ec6fbf6bb2 update 2025-02-26 10:01:56 +01:00
Your Name
8fecde5d56 update 2025-02-26 09:48:56 +01:00
Your Name
49e93ab3ad update 2025-02-25 15:51:43 +01:00
Your Name
e0d7614ad8 update 2025-02-25 15:41:17 +01:00
Your Name
516f6367fa update 2025-02-25 15:33:13 +01:00
Your Name
0549471669 update 2025-02-25 15:25:21 +01:00
Your Name
20a0786380 update 2025-02-25 15:15:29 +01:00
Your Name
92ec2a0bb9 update 2025-02-25 14:57:29 +01:00
Your Name
c7fb474f66 update 2025-02-25 14:40:44 +01:00
Your Name
8c5d831878 update 2025-02-25 14:32:10 +01:00
Your Name
b3f5ee9795 update 2025-02-25 13:02:34 +01:00
Your Name
4f7a704779 update 2025-02-25 12:27:48 +01:00
Your Name
b5aeafeb56 update 2025-02-25 11:59:57 +01:00
Your Name
144d904813 update 2025-02-25 11:55:44 +01:00
Your Name
e3607143a1 update 2025-02-24 17:00:12 +01:00
Your Name
7e8bf1294c update 2025-02-24 16:17:13 +01:00
Your Name
eea1acd701 update 2025-02-20 13:34:36 +01:00
Your Name
accfd3e371 update 2025-02-20 13:13:25 +01:00
PaulVua
dfbae99ba5 update 2025-02-20 09:58:40 +01:00
PaulVua
4c4c6ce77e update 2025-02-20 09:45:26 +01:00
PaulVua
c4fb7aed72 update 2025-02-20 09:41:56 +01:00
PaulVua
cee6c7f79b update 2025-02-19 13:29:53 +01:00
Your Name
8fb1882864 update 2025-02-18 13:46:55 +01:00
PaulVua
67fcd78aac update 2025-02-18 12:16:53 +01:00
PaulVua
727fa4cfeb update 2025-02-18 12:12:48 +01:00
PaulVua
6622be14ad update 2025-02-18 12:05:43 +01:00
PaulVua
9774215e7c update 2025-02-11 14:55:10 +01:00
PaulVua
ecd61f765a update 2025-02-11 10:06:44 +01:00
PaulVua
62c729b63b update 2025-02-10 17:25:34 +01:00
PaulVua
e609c38ca0 update 2025-02-10 15:41:52 +01:00
PaulVua
1cb1b05b51 update 2025-02-10 10:06:16 +01:00
PaulVua
7cac769795 update 2025-02-07 15:04:11 +01:00
PaulVua
fb44b57ac1 update 2025-02-07 13:31:07 +01:00
PaulVua
d98eb48535 update 2025-02-05 17:37:27 +01:00
PaulVua
46303b9c19 update 2025-02-05 16:54:59 +01:00
PaulVua
49be391eb3 database 2025-02-05 15:09:15 +01:00
PaulVua
268a0586b8 update 2025-02-05 10:51:20 +01:00
PaulVua
7de382a43d update 2025-02-04 18:48:42 +01:00
PaulVua
c3e2866fab update 2025-02-04 18:36:58 +01:00
PaulVua
a90552148c update 2025-02-04 18:00:01 +01:00
Your Name
c6073b49b9 update 2025-02-03 13:27:21 +01:00
PaulVua
aaeb20aece update 2025-02-03 10:50:03 +01:00
Your Name
10cc46a079 Merge remote-tracking branch 'refs/remotes/origin/main' 2025-01-31 17:35:12 +01:00
Your Name
b8150535e8 update 2025-01-31 17:34:48 +01:00
93 changed files with 39463 additions and 1234 deletions

5
.gitignore vendored
View File

@@ -1,4 +1,4 @@
logs/app.log
logs/*.log
logs/loop.log
deviceID.txt
loop/loop.log
@@ -13,4 +13,5 @@ envea/data/*.json
NPM/data/*.txt
NPM/data/*.json
*.lock
sqlite/*.db
sqlite/*.db
tests/

74
BME280/get_data_v2.py Executable file
View 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
View 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
View 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()

View File

@@ -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

View File

@@ -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)
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
View 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
View 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
View 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
View 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
View 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()

View File

@@ -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

View File

@@ -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
@@ -44,7 +49,7 @@ def main():
utc_time = datetime.utcnow() #UTC
# If RTC is not connected, set default message
# Calculate time difference (in seconds) if RTC is connected
# 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
@@ -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
View 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()

View File

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

View File

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

166
SARA/R5/setPDP.py Normal file
View 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

View File

@@ -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)

View File

@@ -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()

View File

@@ -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
'''

View File

@@ -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()

View File

@@ -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()

View File

@@ -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()

View File

@@ -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
View 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)

View File

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

27
SARA/check_running.py Executable file
View 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
View 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

View File

@@ -1,11 +1,23 @@
'''
____ _ ____ _
/ ___| / \ | _ \ / \
\___ \ / _ \ | |_) | / _ \
___) / ___ \| _ < / ___ \
|____/_/ \_\_| \_\/_/ \_\
Script to see if the SARA-R410 is running
ex:
ex:
python3 /var/www/nebuleair_pro_4g/SARA/sara.py ttyAMA2 ATI 2
ex 1 (get SIM infos)
python3 /var/www/nebuleair_pro_4g/SARA/sara.py ttyAMA2 AT+CCID? 2
ex 2 (turn on blue light):
python3 /var/www/nebuleair_pro_4g/SARA/sara.py ttyAMA2 AT+UGPIOC=16,2 2
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)
ser = serial.Serial(
port=port, #USB0 or ttyS0
baudrate=baudrate, #115200 ou 9600
parity=serial.PARITY_NONE, #PARITY_NONE, PARITY_EVEN or PARITY_ODD
stopbits=serial.STOPBITS_ONE,
bytesize=serial.EIGHTBITS,
timeout = timeout
)
ser.write((command + '\r').encode('utf-8'))
#ser.write(b'ATI\r') #General Information
#ser.write(b'AT+CCID?\r') #SIM card number
#ser.write(b'AT+CPIN?\r') #Check the status of the SIM card
#ser.write(b'AT+CIND?\r') #Indication state (last number is SIM detection: 0 no SIM detection, 1 SIM detected, 2 not available)
#ser.write(b'AT+UGPIOR=?\r') #Reads the current value of the specified GPIO pin
#ser.write(b'AT+UGPIOC?\r') #GPIO select configuration
#ser.write(b'AT+COPS=?\r') #Check the network and cellular technology the modem is currently using
#ser.write(b'AT+COPS=1,2,20801') #connext to orange
#ser.write(b'AT+CFUN=?\r') #Selects/read the level of functionality
#ser.write(b'AT+URAT=?\r') #Radio Access Technology
#ser.write(b'AT+USIMSTAT?')
#ser.write(b'AT+IPR=115200') #Check/Define baud rate
#ser.write(b'AT+CMUX=?')
baudrate = 115200
try:
ser = serial.Serial(
port=port, #USB0 or ttyS0
baudrate=baudrate, #115200 ou 9600
parity=serial.PARITY_NONE, #PARITY_NONE, PARITY_EVEN or PARITY_ODD
stopbits=serial.STOPBITS_ONE,
bytesize=serial.EIGHTBITS,
timeout = timeout
)
ser.write((command + '\r').encode('utf-8'))
#ser.write(b'ATI\r') #General Information
#ser.write(b'AT+CCID?\r') #SIM card number
#ser.write(b'AT+CPIN?\r') #Check the status of the SIM card
#ser.write(b'AT+CIND?\r') #Indication state (last number is SIM detection: 0 no SIM detection, 1 SIM detected, 2 not available)
#ser.write(b'AT+UGPIOR=?\r') #Reads the current value of the specified GPIO pin
#ser.write(b'AT+UGPIOC?\r') #GPIO select configuration
#ser.write(b'AT+COPS=?\r') #Check the network and cellular technology the modem is currently using
#ser.write(b'AT+COPS=1,2,20801') #connext to orange
#ser.write(b'AT+CFUN=?\r') #Selects/read the level of functionality
#ser.write(b'AT+URAT=?\r') #Radio Access Technology
#ser.write(b'AT+USIMSTAT?')
#ser.write(b'AT+IPR=115200') #Check/Define baud rate
#ser.write(b'AT+CMUX=?')
# Read lines until a timeout occurs
response_lines = []
while True:
line = ser.readline().decode('utf-8').strip()
if not line:
break # Break the loop if an empty line is encountered
response_lines.append(line)
start_time = time.time()
while (time.time() - start_time) < timeout:
line = ser.readline().decode('utf-8', errors='ignore').strip()
if line:
response_lines.append(line)
# Check if we received any data
if not response_lines:
print(f"ERROR: No response received from {port} after sending command: {command}")
sys.exit(1)
# Print the response
for line in response_lines:
print(line)
except serial.SerialException as e:
print(f"Error: {e}")
print(f"ERROR: Serial communication error: {e}")
sys.exit(1)
except Exception as e:
print(f"ERROR: Unexpected error: {e}")
sys.exit(1)
finally:
if ser.is_open:
# Close the serial port if it's open
if 'ser' in locals() and ser.is_open:
ser.close()
#print("Serial closed")

63
SARA/sara_checkDNS.py Normal file
View 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")

View File

@@ -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)
# Print the response
for line in response_lines:
print(line)
response = read_complete_response(ser, wait_for_lines=["OK", "ERROR"],timeout=5, end_of_response_timeout=120, debug=True)
print('<p class="text-danger-emphasis">')
print(response)
print("</p>", end="")
except serial.SerialException as e:
print(f"Error: {e}")

View File

@@ -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
View 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
View 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")

View File

@@ -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

View File

@@ -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

View File

@@ -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'))

View File

@@ -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

View File

@@ -40,22 +40,7 @@ def read_complete_response(serial_connection, timeout=2, end_of_response_timeout
return response.decode('utf-8', errors='replace')
#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

View File

@@ -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

View File

@@ -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 "-------------------"

View File

@@ -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

View File

@@ -1,4 +1,10 @@
"""
_____ _ ___ _______ _
| ____| \ | \ \ / / ____| / \
| _| | \| |\ \ / /| _| / _ \
| |___| |\ | \ V / | |___ / ___ \
|_____|_| \_| \_/ |_____/_/ \_\
Main loop to gather data from envea Sensors
Need to run every minutes

View File

@@ -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
View 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()

File diff suppressed because it is too large Load Diff

20
html/assets/js/chart.js Executable file

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 696 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 618 B

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

14512
html/assets/leaflet/leaflet-src.js Executable file

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

661
html/assets/leaflet/leaflet.css Executable file
View 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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

406
html/database.html Executable file
View 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>

View File

@@ -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">
<div class="card">
<!-- 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 => {
@@ -123,7 +180,12 @@ window.onload = function() {
elements.forEach((element) => {
element.innerText = deviceName;
});
//end fetch config
})
.catch(error => console.error('Error loading config.json:', error));
//end windows on load
*/
//get local RTC
$.ajax({
url: 'launcher.php?type=RTC_time',
@@ -138,6 +200,34 @@ window.onload = function() {
console.error('AJAX request failed:', status, error);
}
});
//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({
@@ -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

View File

@@ -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,97 +114,142 @@
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);
console.log("Getting app/boot logs");
displayLogFile('../logs/app.log', boot_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
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;
const elements = document.querySelectorAll('.sideBar_sensorName');
elements.forEach((element) => {
element.innerText = 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 = 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);
}
});
//get local RTC
$.ajax({
url: 'launcher.php?type=RTC_time',
dataType: 'text', // Specify that you expect a JSON response
method: 'GET', // Use GET or POST depending on your needs
success: function(response) {
console.log("Local RTC: " + response);
const RTC_Element = document.getElementById("RTC_time");
RTC_Element.textContent = response;
},
error: function(xhr, status, error) {
console.error('AJAX request failed:', status, error);
}
});
})
.catch(error => console.error('Error loading config.json:', error));
}
}//end onload
function displayLogFile(logFilePath, containerElement, scrollToBottom = true, maxLines = 0) {
// Show loading indicator
containerElement.innerHTML = '<div class="text-center"><i>Loading log file...</i></div>';
return fetch(logFilePath)
.then((response) => {
if (!response.ok) {
throw new Error(`Failed to fetch the log file: ${response.status} ${response.statusText}`);
}
return response.text();
})
.then((data) => {
// Split the log into lines
let lines = data.split('\n');
// Apply max lines limit if specified
if (maxLines > 0 && lines.length > maxLines) {
lines = lines.slice(-maxLines); // Get only the last N lines
}
// Format log content
const formattedLog = lines
.map((line) => line.trim()) // Remove extra whitespace
.filter((line) => line) // Remove empty lines
.join('<br>'); // Join formatted lines with line breaks
// Display the formatted log
containerElement.innerHTML = `<pre style="white-space: pre-wrap; word-wrap: break-word; margin: 0;">${formattedLog}</pre>`;
// Scroll to bottom if requested
if (scrollToBottom) {
containerElement.scrollTop = containerElement.scrollHeight;
}
return formattedLog; // Return the formatted log in case the caller needs it
})
.catch((error) => {
console.error(`Error loading log file ${logFilePath}:`, error);
containerElement.innerHTML = `<div class="text-danger">Error loading log file: ${error.message}</div>`;
throw error; // Re-throw the error for the caller to handle if needed
});
}
/**
* Set up a refresh button for a log file
* @param {string} buttonId - ID of the button element
* @param {string} logFilePath - Path to the log file
* @param {string} containerId - ID of the container to display the log in
* @param {number} maxLines - Maximum number of lines to display (0 for all)
*/
function setupLogRefreshButton(buttonId, logFilePath, containerId, maxLines = 0) {
console.log("Refreshing logs");
const button = document.getElementById(buttonId);
const container = document.getElementById(containerId);
if (!button || !container) {
console.error('Button or container element not found');
return;
}
// Initial load
displayLogFile(logFilePath, container, true, maxLines);
// Set up button click handler
button.addEventListener('click', () => {
displayLogFile(logFilePath, container, true, maxLines);
});
}
function clear_loopLogs(){
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
View 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: '&copy; <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&param=' + 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>

View File

@@ -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,17 +106,29 @@
</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>
</div>
</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>
@@ -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 -->
@@ -266,7 +305,20 @@
</div>
<!-- toast -->
<div class="toast-container position-fixed bottom-0 end-0 p-3">
<div id="liveToast" class="toast align-items-center text-bg-primary border-1" role="alert" aria-live="assertive" aria-atomic="true">
<div class="d-flex">
<div class="toast-body">
Hello, world! This is a toast message.
</div>
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast" aria-label="Close"></button>
</div>
</div>
</div>
</main>
</div>
</div>
@@ -279,26 +331,118 @@
<script src="assets/js/bootstrap.bundle.js"></script>
<script>
document.addEventListener('DOMContentLoaded', function () {
const elementsToLoad = [
{ id: 'topbar', file: 'topbar.html' },
{ id: 'sidebar', file: 'sidebar.html' },
{ id: 'sidebar_mobile', file: 'sidebar.html' }
];
document.addEventListener('DOMContentLoaded', function () {
const elementsToLoad = [
{ id: 'topbar', file: 'topbar.html' },
{ id: 'sidebar', file: 'sidebar.html' },
{ id: 'sidebar_mobile', file: 'sidebar.html' }
];
elementsToLoad.forEach(({ id, file }) => {
fetch(file)
.then(response => response.text())
.then(data => {
const element = document.getElementById(id);
if (element) {
element.innerHTML = data;
elementsToLoad.forEach(({ id, file }) => {
fetch(file)
.then(response => response.text())
.then(data => {
const element = document.getElementById(id);
if (element) {
element.innerHTML = data;
}
})
.catch(error => console.error(`Error loading ${file}:`, error));
});
//OLD way to retreive data from JSON
/*
fetch('../config.json') // Replace 'deviceID.txt' with 'config.json'
.then(response => response.json()) // Parse response as JSON
.then(data => {
console.log("Getting config file (onload)");
//modem config mode
const check_modem_configMode = document.getElementById("check_modem_configMode");
check_modem_configMode.checked = data.modem_config_mode;
console.log("Modem configuration: " + data.modem_config_mode);
})
*/
//NEW way to get data from SQLITE
$.ajax({
url: 'launcher.php?type=get_config_sqlite',
dataType:'json',
//dataType: 'json', // Specify that you expect a JSON response
method: 'GET', // Use GET or POST depending on your needs
success: function(response) {
console.log("Getting SQLite config table:");
console.log(response);
//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");
}
})
.catch(error => console.error(`Error loading ${file}:`, error));
});
},
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,254 +536,335 @@ function getData_saraR4(port, command, 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: '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_"+port+"_AT_COPS_Connect").hide();
// Replace newline characters with <br> tags
const formattedResponse = response.replace(/\n/g, "<br>");
$("#response_"+port+"_AT_COPS_Connect").html(formattedResponse);
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) {
console.log(response);
$("#loading_"+port+"_AT_COPS_Connect").hide();
// Replace newline characters with <br> tags
const formattedResponse = response.replace(/\n/g, "<br>");
$("#response_"+port+"_AT_COPS_Connect").html(formattedResponse);
},
error: function(xhr, status, error) {
console.error('AJAX request failed:', status, error);
}
});
}
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: '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_mqtt_getConfig").hide();
// Replace newline characters with <br> tags
const formattedResponse = response.replace(/\n/g, "<br>");
$("#response_mqtt_getConfig").html(formattedResponse);
},
error: function(xhr, status, error) {
console.error('AJAX request failed:', status, error);
}
});
}
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: '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_mqtt_login_logout").hide();
const formattedResponse = response.replace(/\n/g, "<br>");
$("#response_mqtt_login_logout").html(formattedResponse);
const regex = /^\+UMQTTC:\s*(\d+),(\d+)/m; // Match "+UMQTTC:", followed by two numbers separated by a comma
const match = response.match(regex);
if (match) {
const firstNumber = match[1]; // The first number after ":"
const secondNumber = match[2]; // The second number after the ","
if (firstNumber == 0) {
console.log("MQTT LOGOUT:");
$("#response_mqtt_login_logout").append("<p>logout</p>");
}
if (firstNumber == 1) {
console.log("MQTT LOGIN:");
$("#response_mqtt_login_logout").append("<p>login</p>");
}
if (secondNumber == 0) {
console.log("ERROR");
$("#response_mqtt_login_logout").append("<p>error</p>");
}
if (secondNumber == 1) {
console.log("SUCCESS");
$("#response_mqtt_login_logout").append("<p>success</p>");
}
} else {
console.log("No matching line found");
}
},
error: function(xhr, status, error) {
console.error('AJAX request failed:', status, error);
}
});
}
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: '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_mqtt_publish").hide();
// Replace newline characters with <br> tags
const formattedResponse = response.replace(/\n/g, "<br>");
$("#response_mqtt_publish").html(formattedResponse);
},
error: function(xhr, status, error) {
console.error('AJAX request failed:', status, error);
}
});
}
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: '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_"+port+"_setURL").hide();
// Replace newline characters with <br> tags
const formattedResponse = response.replace(/\n/g, "<br>");
$("#response_"+port+"_setURL").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: '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_"+port+"_message_write").hide();
// Replace newline characters with <br> tags
const formattedResponse = response.replace(/\n/g, "<br>");
$("#response_"+port+"_message_write").html(formattedResponse);
},
error: function(xhr, status, error) {
console.error('AJAX request failed:', status, error);
}
});
}
function sendMessage_saraR4(port, endpoint){
console.log("Send message from SaraR4 (port "+port+" and endpoint "+endpoint+"):");
$("#loading_"+port+"_message_send").show();
$.ajax({
url: 'launcher.php?type=sara_sendMessage&port='+port+'&endpoint='+encodeURIComponent(endpoint),
//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_"+port+"_message_send").hide();
// Replace newline characters with <br> tags
const formattedResponse = response.replace(/\n/g, "<br>");
$("#response_"+port+"_message_send").html(formattedResponse);
},
error: function(xhr, status, error) {
console.error('AJAX request failed:', status, error);
}
});
}
function connectAPN_saraR4(port, APN_address, timeout){
console.log(" Set APN (port "+port+" and adress "+APN_address+"):");
$("#loading_"+port+"_APN").show();
$.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
method: 'GET', // Use GET or POST depending on your needs
success: function(response) {
console.log(response);
$("#loading_"+port+"_APN").hide();
// Replace newline characters with <br> tags
const formattedResponse = response.replace(/\n/g, "<br>");
$("#response_"+port+"_APN").html(formattedResponse);
},
error: function(xhr, status, error) {
console.error('AJAX request failed:', status, error);
}
});
}
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 SARA_R4 connection status
const SARA_statusElement = document.getElementById("modem-status");
console.log("SARA R4 is: " + data.SARA_R4_network_status);
if (data.SARA_R4_network_status === "connected") {
SARA_statusElement.textContent = "Connected";
SARA_statusElement.className = "badge text-bg-success";
} else if (data.SARA_R4_network_status === "disconnected") {
SARA_statusElement.textContent = "Disconnected";
SARA_statusElement.className = "badge text-bg-danger";
} else {
SARA_statusElement.textContent = "Unknown";
SARA_statusElement.className = "badge text-bg-secondary";
}
//get local RTC
$.ajax({
url: 'launcher.php?type=RTC_time',
dataType: 'text', // Specify that you expect a JSON response
method: 'GET', // Use GET or POST depending on your needs
success: function(response) {
console.log("Local RTC: " + response);
const RTC_Element = document.getElementById("RTC_time");
RTC_Element.textContent = response;
},
error: function(xhr, status, error) {
console.error('AJAX request failed:', status, error);
}
});
})
.catch(error => console.error('Error loading config.json:', error));
},
error: function(xhr, status, error) {
console.error('AJAX request failed:', status, error);
}
});
}
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) {
console.log(response);
$("#loading_mqtt_getConfig").hide();
// Replace newline characters with <br> tags
const formattedResponse = response.replace(/\n/g, "<br>");
$("#response_mqtt_getConfig").html(formattedResponse);
},
error: function(xhr, status, error) {
console.error('AJAX request failed:', status, error);
}
});
}
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) {
console.log(response);
$("#loading_mqtt_login_logout").hide();
const formattedResponse = response.replace(/\n/g, "<br>");
$("#response_mqtt_login_logout").html(formattedResponse);
const regex = /^\+UMQTTC:\s*(\d+),(\d+)/m; // Match "+UMQTTC:", followed by two numbers separated by a comma
const match = response.match(regex);
if (match) {
const firstNumber = match[1]; // The first number after ":"
const secondNumber = match[2]; // The second number after the ","
if (firstNumber == 0) {
console.log("MQTT LOGOUT:");
$("#response_mqtt_login_logout").append("<p>logout</p>");
}
if (firstNumber == 1) {
console.log("MQTT LOGIN:");
$("#response_mqtt_login_logout").append("<p>login</p>");
}
if (secondNumber == 0) {
console.log("ERROR");
$("#response_mqtt_login_logout").append("<p>error</p>");
}
if (secondNumber == 1) {
console.log("SUCCESS");
$("#response_mqtt_login_logout").append("<p>success</p>");
}
} else {
console.log("No matching line found");
}
},
error: function(xhr, status, error) {
console.error('AJAX request failed:', status, error);
}
});
}
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) {
console.log(response);
$("#loading_mqtt_publish").hide();
// Replace newline characters with <br> tags
const formattedResponse = response.replace(/\n/g, "<br>");
$("#response_mqtt_publish").html(formattedResponse);
},
error: function(xhr, status, error) {
console.error('AJAX request failed:', status, error);
}
});
}
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) {
console.log(response);
$("#loading_"+port+"_setURL").hide();
// Replace newline characters with <br> tags
const formattedResponse = response.replace(/\n/g, "<br>");
$("#response_"+port+"_setURL").html(formattedResponse);
},
error: function(xhr, status, error) {
console.error('AJAX request failed:', status, error);
}
});
}
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) {
console.log(response);
$("#loading_"+port+"_message_write").hide();
// Replace newline characters with <br> tags
const formattedResponse = response.replace(/\n/g, "<br>");
$("#response_"+port+"_message_write").html(formattedResponse);
},
error: function(xhr, status, error) {
console.error('AJAX request failed:', status, error);
}
});
}
function sendMessage_saraR4(port, endpoint){
console.log("Send message from SaraR4 (port "+port+" and endpoint "+endpoint+"):");
$("#loading_"+port+"_message_send").show();
$.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) {
console.log(response);
$("#loading_"+port+"_message_send").hide();
// Replace newline characters with <br> tags
const formattedResponse = response.replace(/\n/g, "<br>");
$("#response_"+port+"_message_send").html(formattedResponse);
},
error: function(xhr, status, error) {
console.error('AJAX request failed:', status, error);
}
});
}
function connectAPN_saraR4(port, APN_address, timeout){
console.log(" Set APN (port "+port+" and adress "+APN_address+"):");
$("#loading_"+port+"_APN").show();
$.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);
$("#loading_"+port+"_APN").hide();
// Replace newline characters with <br> tags
const formattedResponse = response.replace(/\n/g, "<br>");
$("#response_"+port+"_APN").html(formattedResponse);
},
error: function(xhr, status, error) {
console.error('AJAX request failed:', status, error);
}
});
}
function getModem_busy_status() {
//console.log("Getting modem busy status");
const SARA_busy_message = document.getElementById("modem_status_message");
$.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 → 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&param='+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();
}
});
}
</script>
</body>

View File

@@ -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>
`);
}
@@ -156,6 +142,52 @@ 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:");
@@ -163,6 +195,7 @@ function getNoise_values(){
$.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,143 +271,190 @@ function getBME280_values(){
window.onload = function() {
fetch('../config.json') // Replace 'deviceID.txt' with 'config.json'
.then(response => response.json()) // Parse response as JSON
.then(data => {
//get device ID
const deviceID = data.deviceID.trim().toUpperCase();
//document.getElementById('pageTitle_plus_ID').innerText = 'token: ' + deviceID;
//get device Name
const deviceName = data.deviceName;
const elements = document.querySelectorAll('.sideBar_sensorName');
elements.forEach((element) => {
element.innerText = deviceName;
});
//NEW way to get config (SQLite)
$.ajax({
url: 'launcher.php?type=get_config_sqlite',
dataType:'json',
//dataType: 'json', // Specify that you expect a JSON response
method: 'GET', // Use GET or POST depending on your needs
success: function(response) {
console.log("Getting SQLite config table:");
console.log(response);
//device name_side bar
const elements = document.querySelectorAll('.sideBar_sensorName');
elements.forEach((element) => {
element.innerText = response.deviceName;
});
},
error: function(xhr, status, error) {
console.error('AJAX request failed:', status, error);
}
});//end AJAX
//getting config_scripts table
$.ajax({
url: 'launcher.php?type=get_config_scripts_sqlite',
dataType:'json',
//dataType: 'json', // Specify that you expect a JSON response
method: 'GET', // Use GET or POST depending on your needs
success: function(response) {
console.log("Getting SQLite config scripts table:");
console.log(response);
const container = document.getElementById('card-container'); // Conteneur des cartes
//creates NPM card
if (response["NPM/get_data_modbus_v3.py"]) {
const cardHTML = `
<div class="col-sm-3">
<div class="card">
<div class="card-header">
Port UART
</div>
<div class="card-body">
<h5 class="card-title">NextPM</h5>
<p class="card-text">Capteur particules fines.</p>
<button class="btn btn-primary" onclick="getNPM_values('ttyAMA5')">Get Data</button>
<br>
<div id="loading_ttyAMA5" class="spinner-border spinner-border-sm" style="display: none;" role="status"></div>
<table class="table table-striped-columns">
<tbody id="data-table-body_ttyAMA5"></tbody>
</table>
</div>
</div>
</div>`;
container.innerHTML += cardHTML; // Add the I2C card if condition is met
}
//creates i2c BME280 card
if (response["BME280/get_data_v2.py"]) {
const i2C_BME_HTML = `
<div class="col-sm-3">
<div class="card">
<div class="card-header">
Port I2C
</div>
<div class="card-body">
<h5 class="card-title">BME280 Temp/Hum sensor</h5>
<p class="card-text">Capteur température et humidité sur le port I2C.</p>
<button class="btn btn-primary mb-1" onclick="getBME280_values()">Get Data</button>
<br>
<div id="loading_BME280" class="spinner-border spinner-border-sm" style="display: none;" role="status"></div>
<table class="table table-striped-columns">
<tbody id="data-table-body_BME280"></tbody>
</table>
</div>
</div>
</div>`;
container.innerHTML += i2C_BME_HTML; // Add the I2C card if condition is met
}
//creates i2c sound card
if (response.i2C_sound) {
const i2C_HTML = `
<div class="col-sm-3">
<div class="card">
<div class="card-header">
Port I2C
</div>
<div class="card-body">
<h5 class="card-title">Decibel Meter</h5>
<p class="card-text">Capteur bruit sur le port I2C.</p>
<button class="btn btn-primary mb-1" onclick="getNoise_values()">Get Data</button>
<br>
<button class="btn btn-success" onclick="startNoise()">Start recording</button>
<button class="btn btn-danger" onclick="stopNoise()">Stop recording</button>
<div id="loading_noise" class="spinner-border spinner-border-sm" style="display: none;" role="status"></div>
<table class="table table-striped-columns">
<tbody id="data-table-body_noise"></tbody>
</table>
</div>
</div>
</div>`;
container.innerHTML += i2C_HTML; // Add the I2C card if condition is met
}
//Si on a des SONDES ENVEA connectée il faut faire un deuxième call dans la table envea_sondes_table
//creates ENVEA cards
if (response["envea/read_value_v2.py"]) {
console.log("Need to display ENVEA sondes");
//getting config_scripts table
$.ajax({
url: 'launcher.php?type=get_envea_sondes_table_sqlite',
dataType:'json',
//dataType: 'json', // Specify that you expect a JSON response
method: 'GET', // Use GET or POST depending on your needs
success: function(sondes) {
console.log("Getting SQLite envea sondes table:");
console.log(sondes);
const ENVEA_sensors = sondes.filter(sonde => sonde.connected); // Filter only connected sondes
ENVEA_sensors.forEach((sensor, index) => {
const port = sensor.port; // Port from the sensor object
const name = sensor.name; // Port from the sensor object
const coefficient = sensor.coefficient;
const cardHTML = `
<div class="col-sm-3">
<div class="card">
<div class="card-header">
Port UART ${port.replace('ttyAMA', '')}
</div>
<div class="card-body">
<h5 class="card-title">Sonde Envea ${name}</h5>
<p class="card-text">Capteur gas.</p>
<button class="btn btn-primary" onclick="getENVEA_values('${port}','${name}')">Get Data</button>
<div id="loading_envea${name}" class="spinner-border spinner-border-sm" style="display: none;" role="status"></div>
<table class="table table-striped-columns">
<tbody id="data-table-body_envea${name}"></tbody>
</table>
</div>
</div>
</div>`;
container.innerHTML += cardHTML; // Ajouter la carte au conteneur
});
},
error: function(xhr, status, error) {
console.error('AJAX request failed:', status, error);
}
});//end AJAX envea Sondes
//get local RTC
$.ajax({
url: 'launcher.php?type=RTC_time',
dataType: 'text', // Specify that you expect a JSON response
method: 'GET', // Use GET or POST depending on your needs
success: function(response) {
console.log("Local RTC: " + response);
const RTC_Element = document.getElementById("RTC_time");
RTC_Element.textContent = response;
},
error: function(xhr, status, error) {
console.error('AJAX request failed:', status, error);
}
});
}//end if
const container = document.getElementById('card-container'); // Conteneur des cartes
//creates NPM cards
const NPM_ports = data.NextPM_ports; // Récupère les ports
NPM_ports.forEach((port, index) => {
const cardHTML = `
<div class="col-sm-3">
<div class="card">
<div class="card-header">
Port UART ${port.replace('ttyAMA', '')}
</div>
<div class="card-body">
<h5 class="card-title">NextPM ${String.fromCharCode(65 + index)}</h5>
<p class="card-text">Capteur particules fines.</p>
<button class="btn btn-primary" onclick="getNPM_values('${port}')">Get Data</button>
<div id="loading_${port}" class="spinner-border spinner-border-sm" style="display: none;" role="status"></div>
<table class="table table-striped-columns">
<tbody id="data-table-body_${port}"></tbody>
</table>
</div>
</div>
</div>`;
container.innerHTML += cardHTML; // Ajouter la carte au conteneur
});
},
error: function(xhr, status, error) {
console.error('AJAX request failed:', status, error);
}
});//end AJAX (config_scripts)
//creates ENVEA cards
const ENVEA_sensors = data.envea_sondes.filter(sonde => sonde.connected); // Filter only connected sondes
//get local RTC
$.ajax({
url: 'launcher.php?type=RTC_time',
dataType: 'text', // Specify that you expect a JSON response
method: 'GET', // Use GET or POST depending on your needs
success: function(response) {
console.log("Local RTC: " + response);
const RTC_Element = document.getElementById("RTC_time");
RTC_Element.textContent = response;
ENVEA_sensors.forEach((sensor, index) => {
const port = sensor.port; // Port from the sensor object
const name = sensor.name; // Port from the sensor object
const coefficient = sensor.coefficient;
const cardHTML = `
<div class="col-sm-3">
<div class="card">
<div class="card-header">
Port UART ${port.replace('ttyAMA', '')}
</div>
<div class="card-body">
<h5 class="card-title">Sonde Envea ${name}</h5>
<p class="card-text">Capteur gas.</p>
<button class="btn btn-primary" onclick="getENVEA_values('${port}','${name}','${coefficient}')">Get Data</button>
<div id="loading_envea${name}" class="spinner-border spinner-border-sm" style="display: none;" role="status"></div>
<table class="table table-striped-columns">
<tbody id="data-table-body_envea${name}"></tbody>
</table>
</div>
</div>
</div>`;
container.innerHTML += cardHTML; // Ajouter la carte au conteneur
});
},
error: function(xhr, status, error) {
console.error('AJAX request failed:', status, error);
}
});
//creates i2c BME280 card
if (data.i2c_BME) {
const i2C_BME_HTML = `
<div class="col-sm-3">
<div class="card">
<div class="card-header">
Port I2C
</div>
<div class="card-body">
<h5 class="card-title">BME280 Temp/Hum sensor</h5>
<p class="card-text">Capteur température et humidité sur le port I2C.</p>
<button class="btn btn-primary mb-1" onclick="getBME280_values()">Get Data</button>
<br>
<div id="loading_BME280" class="spinner-border spinner-border-sm" style="display: none;" role="status"></div>
<table class="table table-striped-columns">
<tbody id="data-table-body_BME280"></tbody>
</table>
</div>
</div>
</div>`;
container.innerHTML += i2C_BME_HTML; // Add the I2C card if condition is met
}
//creates i2c sound card
if (data.i2C_sound) {
const i2C_HTML = `
<div class="col-sm-3">
<div class="card">
<div class="card-header">
Port I2C
</div>
<div class="card-body">
<h5 class="card-title">Decibel Meter</h5>
<p class="card-text">Capteur bruit sur le port I2C.</p>
<button class="btn btn-primary mb-1" onclick="getNoise_values()">Get Data</button>
<br>
<button class="btn btn-success" onclick="startNoise()">Start recording</button>
<button class="btn btn-danger" onclick="stopNoise()">Stop recording</button>
<div id="loading_noise" class="spinner-border spinner-border-sm" style="display: none;" role="status"></div>
<table class="table table-striped-columns">
<tbody id="data-table-body_noise"></tbody>
</table>
</div>
</div>
</div>`;
container.innerHTML += i2C_HTML; // Add the I2C card if condition is met
}
})
.catch(error => console.error('Error loading config.json:', error));
}
} //end windows onload
</script>
</body>

View File

@@ -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
View File

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

View File

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

View File

@@ -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
View 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
View 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

View File

@@ -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

File diff suppressed because it is too large Load Diff

23
config.json.dist → old/config.json.dist Executable file → Normal file
View 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
View File

166
old/master.py Executable file
View 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
View File

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

View File

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

228
services/setup_services.sh Normal file
View File

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

View File

@@ -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
View 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()

View File

@@ -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
View 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
View 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
View 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!")

View File

@@ -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
View File

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

27
windMeter/ads115.py Normal file
View 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
View 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()

View 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")

View 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")