Compare commits
177 Commits
709a1e5dd5
...
ai_branch_
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
020594e065 | ||
|
|
5a1a4e0d81 | ||
|
|
3cd5b13c25 | ||
|
|
5a0f1c0745 | ||
|
|
2516a3bd1c | ||
|
|
1b8dc54fe0 | ||
|
|
2bd74ca91a | ||
|
|
f40c105abf | ||
|
|
fdef8e2df0 | ||
|
|
386ad6fb03 | ||
|
|
a7c138e93f | ||
|
|
4e4832b128 | ||
|
|
11463b175c | ||
|
|
c06741b11d | ||
|
|
b1352261e7 | ||
|
|
376ff454bf | ||
|
|
932fdf83a2 | ||
|
|
1ca3e2ada2 | ||
|
|
fd1d32a62b | ||
|
|
61b302fe35 | ||
|
|
2aaa229e82 | ||
|
|
fd28069b0c | ||
|
|
b17c996f2f | ||
|
|
8273307cab | ||
|
|
a73eb30d32 | ||
|
|
ba889feee9 | ||
|
|
12c7a0b6af | ||
|
|
08c5ed8841 | ||
|
|
7f5eb7608c | ||
|
|
44f44c3361 | ||
|
|
a8350332ac | ||
|
|
6c6eed1ad6 | ||
|
|
ee71c28d33 | ||
|
|
6d3220665e | ||
|
|
98e5a239f5 | ||
|
|
17f4ce46dd | ||
|
|
338b8a049f | ||
|
|
1e9e80ae55 | ||
|
|
9d280c6e37 | ||
|
|
d4c1178b3d | ||
|
|
f7f6fccd60 | ||
|
|
afceb34c1b | ||
|
|
7a958d5c8e | ||
|
|
8fd76001f2 | ||
|
|
e320a3bc2b | ||
|
|
8a4e184699 | ||
|
|
e61b0a76da | ||
|
|
970a36598c | ||
|
|
e75caff929 | ||
|
|
e82d75a4d6 | ||
|
|
dc27e5f139 | ||
|
|
4bc05091be | ||
|
|
29f9ec445a | ||
|
|
7b398d0d6d | ||
|
|
76336d0073 | ||
|
|
46a8e21e64 | ||
|
|
2129d45ef6 | ||
|
|
6312cd8d72 | ||
|
|
7c17ec82f5 | ||
|
|
b7a6f4c907 | ||
|
|
6b3329b9b8 | ||
|
|
e9b1e0e88e | ||
|
|
2db732ebb3 | ||
|
|
d5302f78ba | ||
|
|
5b7de91d50 | ||
|
|
4d15076d4b | ||
|
|
809742b6d5 | ||
|
|
bca975b0c5 | ||
|
|
dfba956685 | ||
|
|
d07314262e | ||
|
|
dffa639574 | ||
|
|
1fd5a3e75c | ||
|
|
e674b21eaa | ||
|
|
efc94ba5e1 | ||
|
|
26328dec99 | ||
|
|
ec3e81e99e | ||
|
|
1c6af36313 | ||
|
|
f1d6f595ac | ||
|
|
cfc2e0c47f | ||
|
|
1037207df3 | ||
|
|
14044a8856 | ||
|
|
d57a47ef68 | ||
|
|
5e7375cd4e | ||
|
|
c42b16ddb6 | ||
|
|
283a46eb0b | ||
|
|
33b24a9f53 | ||
|
|
10c4348e54 | ||
|
|
072f98ef95 | ||
|
|
7b4ff011ec | ||
|
|
ab2124f50d | ||
|
|
b493d30a41 | ||
|
|
659effb7c4 | ||
|
|
ebb0fd0a2b | ||
|
|
5d121761e7 | ||
|
|
d90fb14c90 | ||
|
|
3f329e0afa | ||
|
|
cd030a9e14 | ||
|
|
709cad6981 | ||
|
|
c59246e320 | ||
|
|
1a15d70aa7 | ||
|
|
bf9ece8589 | ||
|
|
3d507ae659 | ||
|
|
700de9c9f4 | ||
|
|
ec6fbf6bb2 | ||
|
|
8fecde5d56 | ||
|
|
49e93ab3ad | ||
|
|
e0d7614ad8 | ||
|
|
516f6367fa | ||
|
|
0549471669 | ||
|
|
20a0786380 | ||
|
|
92ec2a0bb9 | ||
|
|
c7fb474f66 | ||
|
|
8c5d831878 | ||
|
|
b3f5ee9795 | ||
|
|
4f7a704779 | ||
|
|
b5aeafeb56 | ||
|
|
144d904813 | ||
|
|
e3607143a1 | ||
|
|
7e8bf1294c | ||
|
|
eea1acd701 | ||
|
|
accfd3e371 | ||
|
|
dfbae99ba5 | ||
|
|
4c4c6ce77e | ||
|
|
c4fb7aed72 | ||
|
|
cee6c7f79b | ||
|
|
8fb1882864 | ||
|
|
67fcd78aac | ||
|
|
727fa4cfeb | ||
|
|
6622be14ad | ||
|
|
9774215e7c | ||
|
|
ecd61f765a | ||
|
|
62c729b63b | ||
|
|
e609c38ca0 | ||
|
|
1cb1b05b51 | ||
|
|
7cac769795 | ||
|
|
fb44b57ac1 | ||
|
|
d98eb48535 | ||
|
|
46303b9c19 | ||
|
|
49be391eb3 | ||
|
|
268a0586b8 | ||
|
|
7de382a43d | ||
|
|
c3e2866fab | ||
|
|
a90552148c | ||
|
|
c6073b49b9 | ||
|
|
aaeb20aece | ||
|
|
10cc46a079 | ||
|
|
b8150535e8 | ||
|
|
ef2bd6b895 | ||
|
|
456db5da98 | ||
|
|
c15838fc47 | ||
|
|
d732f3ad2d | ||
|
|
437be5cad9 | ||
|
|
4f59928b1e | ||
|
|
a689a7d2de | ||
|
|
970f62658b | ||
|
|
6e0dc1b257 | ||
|
|
578721a9f2 | ||
|
|
a8ca15505e | ||
|
|
280dcd9be3 | ||
|
|
4a5e0b3577 | ||
|
|
d095e53cd6 | ||
|
|
083d342373 | ||
|
|
c8b9cb46f6 | ||
|
|
4123f977b2 | ||
|
|
833ed458a7 | ||
|
|
155a2bd453 | ||
|
|
660af80ab0 | ||
|
|
8596690f32 | ||
|
|
bd902a0c46 | ||
|
|
545f5f8f3a | ||
|
|
0fb7118abb | ||
|
|
aa3b4d238b | ||
|
|
838d6d7357 | ||
|
|
806576b8b8 | ||
|
|
2650954ecc | ||
|
|
78339fa585 | ||
|
|
3ea7b16b1a |
10
.gitignore
vendored
10
.gitignore
vendored
@@ -1,4 +1,4 @@
|
|||||||
logs/app.log
|
logs/*.log
|
||||||
logs/loop.log
|
logs/loop.log
|
||||||
deviceID.txt
|
deviceID.txt
|
||||||
loop/loop.log
|
loop/loop.log
|
||||||
@@ -8,4 +8,10 @@ config.json
|
|||||||
.ssh/
|
.ssh/
|
||||||
sound_meter/moving_avg_minute.txt
|
sound_meter/moving_avg_minute.txt
|
||||||
wifi_list.csv
|
wifi_list.csv
|
||||||
envea/data/*.txt
|
envea/data/*.txt
|
||||||
|
envea/data/*.json
|
||||||
|
NPM/data/*.txt
|
||||||
|
NPM/data/*.json
|
||||||
|
*.lock
|
||||||
|
sqlite/*.db
|
||||||
|
tests/
|
||||||
74
BME280/get_data_v2.py
Executable file
74
BME280/get_data_v2.py
Executable file
@@ -0,0 +1,74 @@
|
|||||||
|
'''
|
||||||
|
____ __ __ _____ ____ ___ ___
|
||||||
|
| __ )| \/ | ____|___ \( _ ) / _ \
|
||||||
|
| _ \| |\/| | _| __) / _ \| | | |
|
||||||
|
| |_) | | | | |___ / __/ (_) | |_| |
|
||||||
|
|____/|_| |_|_____|_____\___/ \___/
|
||||||
|
|
||||||
|
Script to read data from BME280
|
||||||
|
Sensor connected to i2c on address 76 (use sudo i2cdetect -y 1 to get the address )
|
||||||
|
-> save data to database (table data_BME280 )
|
||||||
|
sudo python3 /var/www/nebuleair_pro_4g/BME280/get_data_v2.py
|
||||||
|
|
||||||
|
'''
|
||||||
|
|
||||||
|
import board
|
||||||
|
import busio
|
||||||
|
import json
|
||||||
|
import sqlite3
|
||||||
|
from adafruit_bme280 import basic as adafruit_bme280
|
||||||
|
|
||||||
|
# Connect to the SQLite database
|
||||||
|
conn = sqlite3.connect("/var/www/nebuleair_pro_4g/sqlite/sensors.db")
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
# Create I2C bus
|
||||||
|
i2c = busio.I2C(board.SCL, board.SDA)
|
||||||
|
bme280 = adafruit_bme280.Adafruit_BME280_I2C(i2c, address=0x76)
|
||||||
|
|
||||||
|
# Configure settings
|
||||||
|
bme280.sea_level_pressure = 1013.25 # Update this value for your location
|
||||||
|
|
||||||
|
# Read sensor data
|
||||||
|
|
||||||
|
#print(f"Temperature: {bme280.temperature:.2f} °C")
|
||||||
|
#print(f"Humidity: {bme280.humidity:.2f} %")
|
||||||
|
#print(f"Pressure: {bme280.pressure:.2f} hPa")
|
||||||
|
#print(f"Altitude: {bme280.altitude:.2f} m")
|
||||||
|
|
||||||
|
temperature = round(bme280.temperature, 2)
|
||||||
|
humidity = round(bme280.humidity, 2)
|
||||||
|
pressure = round(bme280.pressure, 2)
|
||||||
|
|
||||||
|
sensor_data = {
|
||||||
|
"temp": temperature, # Temperature in °C
|
||||||
|
"hum": humidity, # Humidity in %
|
||||||
|
"press": pressure # Pressure in hPa
|
||||||
|
}
|
||||||
|
|
||||||
|
#GET RTC TIME from SQlite
|
||||||
|
cursor.execute("SELECT * FROM timestamp_table LIMIT 1")
|
||||||
|
row = cursor.fetchone() # Get the first (and only) row
|
||||||
|
rtc_time_str = row[1] # '2025-02-07 12:30:45'
|
||||||
|
|
||||||
|
|
||||||
|
# Convert to JSON and print
|
||||||
|
#print(json.dumps(sensor_data, indent=4))
|
||||||
|
|
||||||
|
|
||||||
|
#save to sqlite database
|
||||||
|
try:
|
||||||
|
cursor.execute('''
|
||||||
|
INSERT INTO data_BME280 (timestamp,temperature, humidity, pressure) VALUES (?,?,?,?)'''
|
||||||
|
, (rtc_time_str,temperature,humidity,pressure))
|
||||||
|
|
||||||
|
# Commit and close the connection
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
#print("Sensor data saved successfully!")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Database error: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
conn.close()
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
# Script to read data from BME280
|
# Script to read data from BME280
|
||||||
# Sensor connected to i2c on address 77 (use sudo i2cdetect -y 1 to get the address )
|
# Sensor connected to i2c on address 76 (use sudo i2cdetect -y 1 to get the address )
|
||||||
# sudo python3 /var/www/nebuleair_pro_4g/BME280/read.py
|
# sudo python3 /var/www/nebuleair_pro_4g/BME280/read.py
|
||||||
|
|
||||||
import board
|
import board
|
||||||
|
|||||||
40
GPIO/control.py
Normal file
40
GPIO/control.py
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
'''
|
||||||
|
____ ____ ___ ___
|
||||||
|
/ ___| _ \_ _/ _ \
|
||||||
|
| | _| |_) | | | | |
|
||||||
|
| |_| | __/| | |_| |
|
||||||
|
\____|_| |___\___/
|
||||||
|
|
||||||
|
script to control GPIO output
|
||||||
|
|
||||||
|
GPIO 16 -> SARA 5V
|
||||||
|
GPIO 20 -> SARA PWR ON
|
||||||
|
|
||||||
|
option 1:
|
||||||
|
CLI tool like pinctrl
|
||||||
|
pinctrl set 16 op
|
||||||
|
pinctrl set 16 dh
|
||||||
|
pinctrl set 16 dl
|
||||||
|
|
||||||
|
option 2:
|
||||||
|
python library RPI.GPIO
|
||||||
|
|
||||||
|
|
||||||
|
/usr/bin/python3 /var/www/nebuleair_pro_4g/GPIO/control.py
|
||||||
|
'''
|
||||||
|
|
||||||
|
import RPi.GPIO as GPIO
|
||||||
|
import time
|
||||||
|
|
||||||
|
selected_GPIO = 16
|
||||||
|
|
||||||
|
GPIO.setmode(GPIO.BCM) # Use BCM numbering
|
||||||
|
GPIO.setup(selected_GPIO, GPIO.OUT) # Set GPIO17 as an output
|
||||||
|
|
||||||
|
while True:
|
||||||
|
GPIO.output(selected_GPIO, GPIO.HIGH) # Turn ON
|
||||||
|
time.sleep(1) # Wait 1 sec
|
||||||
|
GPIO.output(selected_GPIO, GPIO.LOW) # Turn OFF
|
||||||
|
time.sleep(1) # Wait 1 sec
|
||||||
|
|
||||||
|
|
||||||
225
MPPT/read.py
Normal file
225
MPPT/read.py
Normal file
@@ -0,0 +1,225 @@
|
|||||||
|
'''
|
||||||
|
__ __ ____ ____ _____
|
||||||
|
| \/ | _ \| _ \_ _|
|
||||||
|
| |\/| | |_) | |_) || |
|
||||||
|
| | | | __/| __/ | |
|
||||||
|
|_| |_|_| |_| |_|
|
||||||
|
|
||||||
|
Chargeur solaire Victron MPPT interface UART
|
||||||
|
|
||||||
|
MPPT connections
|
||||||
|
5V / Rx / TX / GND
|
||||||
|
RPI connection
|
||||||
|
-- / GPIO9 / GPIO8 / GND
|
||||||
|
* pas besoin de connecter le 5V (le GND uniquement)
|
||||||
|
|
||||||
|
typical response from uart:
|
||||||
|
|
||||||
|
PID 0xA075 ->product ID
|
||||||
|
FW 164 ->firmware version
|
||||||
|
SER# HQ2249VJV9W ->serial num
|
||||||
|
|
||||||
|
V 13310 ->Battery voilatage in mV
|
||||||
|
I -130 ->Battery current in mA (negative means its discharging)
|
||||||
|
VPV 10 ->Solar Panel voltage
|
||||||
|
PPV 0 ->Solar Panel power (in W)
|
||||||
|
CS 0 ->Charger status:
|
||||||
|
0=off (no charging),
|
||||||
|
2=Bulk (Max current is being delivered to the battery),
|
||||||
|
3=Absorbtion (battery is nearly full,voltage is held constant.),
|
||||||
|
4=Float (Battery is fully charged, only maintaining charge)
|
||||||
|
MPPT 0 ->MPPT (Maximum Power Point Tracking) state: 0 = Off, 1 = Active, 2 = Not tracking
|
||||||
|
OR 0x00000001
|
||||||
|
ERR 0
|
||||||
|
LOAD ON
|
||||||
|
IL 100
|
||||||
|
H19 18 ->historical data (Total energy absorbed in kWh)
|
||||||
|
H20 0 -> Total energy discharged in kWh
|
||||||
|
H21 0
|
||||||
|
H22 9
|
||||||
|
H23 92
|
||||||
|
HSDS 19
|
||||||
|
Checksum u
|
||||||
|
|
||||||
|
sudo /usr/bin/python3 /var/www/nebuleair_pro_4g/MPPT/read.py
|
||||||
|
|
||||||
|
'''
|
||||||
|
import serial
|
||||||
|
import time
|
||||||
|
import sqlite3
|
||||||
|
|
||||||
|
# Connect to the SQLite database
|
||||||
|
conn = sqlite3.connect("/var/www/nebuleair_pro_4g/sqlite/sensors.db")
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
|
||||||
|
def read_vedirect(port='/dev/ttyAMA4', baudrate=19200, timeout=20, max_attempts=3):
|
||||||
|
"""
|
||||||
|
Read and parse data from Victron MPPT controller with retry logic
|
||||||
|
Returns parsed data as a dictionary or None if all attempts fail
|
||||||
|
"""
|
||||||
|
required_keys = ['V', 'I', 'VPV', 'PPV', 'CS'] # Essential keys we need
|
||||||
|
|
||||||
|
for attempt in range(max_attempts):
|
||||||
|
try:
|
||||||
|
print(f"Attempt {attempt+1} of {max_attempts}...")
|
||||||
|
ser = serial.Serial(port, baudrate, timeout=1)
|
||||||
|
|
||||||
|
# Initialize data dictionary and tracking variables
|
||||||
|
data = {}
|
||||||
|
start_time = time.time()
|
||||||
|
|
||||||
|
while time.time() - start_time < timeout:
|
||||||
|
line = ser.readline().decode('utf-8', errors='ignore').strip()
|
||||||
|
|
||||||
|
if not line:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Check if line contains a key-value pair
|
||||||
|
if '\t' in line:
|
||||||
|
key, value = line.split('\t', 1)
|
||||||
|
data[key] = value
|
||||||
|
print(f"{key}: {value}")
|
||||||
|
else:
|
||||||
|
print(f"Info: {line}")
|
||||||
|
|
||||||
|
# Check if we have a complete data block
|
||||||
|
if 'Checksum' in data:
|
||||||
|
# Check if we have all required keys
|
||||||
|
missing_keys = [key for key in required_keys if key not in data]
|
||||||
|
if not missing_keys:
|
||||||
|
ser.close()
|
||||||
|
return data
|
||||||
|
else:
|
||||||
|
print(f"Incomplete data, missing: {', '.join(missing_keys)}")
|
||||||
|
# Clear data and continue reading
|
||||||
|
data = {}
|
||||||
|
|
||||||
|
# Timeout occurred
|
||||||
|
print(f"Timeout on attempt {attempt+1}: Could not get complete data")
|
||||||
|
ser.close()
|
||||||
|
|
||||||
|
# Add small delay between attempts
|
||||||
|
if attempt < max_attempts - 1:
|
||||||
|
print("Waiting before next attempt...")
|
||||||
|
time.sleep(2)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error on attempt {attempt+1}: {e}")
|
||||||
|
try:
|
||||||
|
ser.close()
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
print("All attempts failed")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def parse_values(data):
|
||||||
|
"""Convert string values to appropriate types"""
|
||||||
|
if not data:
|
||||||
|
return None
|
||||||
|
|
||||||
|
parsed = {}
|
||||||
|
|
||||||
|
# Define conversions for each key
|
||||||
|
conversions = {
|
||||||
|
'PID': str,
|
||||||
|
'FW': int,
|
||||||
|
'SER#': str,
|
||||||
|
'V': lambda x: float(x)/1000, # Convert mV to V
|
||||||
|
'I': lambda x: float(x)/1000, # Convert mA to A
|
||||||
|
'VPV': lambda x: float(x)/1000 if x != '---' else 0, # Convert mV to V
|
||||||
|
'PPV': int,
|
||||||
|
'CS': int,
|
||||||
|
'MPPT': int,
|
||||||
|
'OR': str,
|
||||||
|
'ERR': int,
|
||||||
|
'LOAD': str,
|
||||||
|
'IL': int,
|
||||||
|
'H19': int, # Total energy absorbed in kWh
|
||||||
|
'H20': int, # Total energy discharged in kWh
|
||||||
|
'H21': int,
|
||||||
|
'H22': int,
|
||||||
|
'H23': int,
|
||||||
|
'HSDS': int
|
||||||
|
}
|
||||||
|
|
||||||
|
# Convert values according to their type
|
||||||
|
for key, value in data.items():
|
||||||
|
if key in conversions:
|
||||||
|
try:
|
||||||
|
parsed[key] = conversions[key](value)
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
parsed[key] = value # Keep as string if conversion fails
|
||||||
|
else:
|
||||||
|
parsed[key] = value
|
||||||
|
|
||||||
|
return parsed
|
||||||
|
|
||||||
|
def get_charger_status(cs_value):
|
||||||
|
"""Convert CS numeric value to human-readable status"""
|
||||||
|
status_map = {
|
||||||
|
0: "Off",
|
||||||
|
1: "Low power mode",
|
||||||
|
2: "Fault",
|
||||||
|
3: "Bulk",
|
||||||
|
4: "Absorption",
|
||||||
|
5: "Float",
|
||||||
|
6: "Storage",
|
||||||
|
7: "Equalize",
|
||||||
|
9: "Inverting",
|
||||||
|
11: "Power supply",
|
||||||
|
245: "Starting-up",
|
||||||
|
247: "Repeated absorption",
|
||||||
|
252: "External control"
|
||||||
|
}
|
||||||
|
return status_map.get(cs_value, f"Unknown ({cs_value})")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
# Read data (with retry logic)
|
||||||
|
raw_data = read_vedirect()
|
||||||
|
|
||||||
|
if raw_data:
|
||||||
|
# Parse data
|
||||||
|
parsed_data = parse_values(raw_data)
|
||||||
|
|
||||||
|
if parsed_data:
|
||||||
|
# Check if we have valid battery voltage
|
||||||
|
if parsed_data.get('V', 0) > 0:
|
||||||
|
print("\n===== MPPT Summary =====")
|
||||||
|
print(f"Battery: {parsed_data.get('V', 0):.2f}V, {parsed_data.get('I', 0):.2f}A")
|
||||||
|
print(f"Solar: {parsed_data.get('VPV', 0):.2f}V, {parsed_data.get('PPV', 0)}W")
|
||||||
|
print(f"Charger status: {get_charger_status(parsed_data.get('CS', 0))}")
|
||||||
|
|
||||||
|
# Save to SQLite
|
||||||
|
cursor.execute("SELECT * FROM timestamp_table LIMIT 1")
|
||||||
|
row = cursor.fetchone()
|
||||||
|
rtc_time_str = row[1]
|
||||||
|
|
||||||
|
# Extract values
|
||||||
|
battery_voltage = parsed_data.get('V', 0)
|
||||||
|
battery_current = parsed_data.get('I', 0)
|
||||||
|
solar_voltage = parsed_data.get('VPV', 0)
|
||||||
|
solar_power = parsed_data.get('PPV', 0)
|
||||||
|
charger_status = parsed_data.get('CS', 0)
|
||||||
|
|
||||||
|
try:
|
||||||
|
cursor.execute('''
|
||||||
|
INSERT INTO data_MPPT (timestamp, battery_voltage, battery_current, solar_voltage, solar_power, charger_status)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?)''',
|
||||||
|
(rtc_time_str, battery_voltage, battery_current, solar_voltage, solar_power, charger_status))
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
print("MPPT data saved successfully!")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Database error: {e}")
|
||||||
|
else:
|
||||||
|
print("Invalid data: Battery voltage is zero or missing")
|
||||||
|
else:
|
||||||
|
print("Failed to parse data")
|
||||||
|
else:
|
||||||
|
print("No valid data received from MPPT controller")
|
||||||
|
|
||||||
|
# Always close the connection
|
||||||
|
conn.close()
|
||||||
64
NPM/firmware_version.py
Executable file
64
NPM/firmware_version.py
Executable file
@@ -0,0 +1,64 @@
|
|||||||
|
'''
|
||||||
|
Script to get NPM firmware version
|
||||||
|
need parameter: port
|
||||||
|
/usr/bin/python3 /var/www/nebuleair_pro_4g/NPM/firmware_version.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\x17\x68') #firmware version
|
||||||
|
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
byte_data = ser.readline()
|
||||||
|
formatted = ''.join(f'\\x{byte:02x}' for byte in byte_data)
|
||||||
|
#print(formatted)
|
||||||
|
|
||||||
|
'''
|
||||||
|
la réponse est de type
|
||||||
|
\x81\x17\x00\x10\x46\x12
|
||||||
|
avec
|
||||||
|
\x81 address
|
||||||
|
\x17 command code
|
||||||
|
\x00 state
|
||||||
|
\x10\x46 firmware version
|
||||||
|
\x12 checksum
|
||||||
|
'''
|
||||||
|
# Extract byte 4 and byte 5
|
||||||
|
byte4 = byte_data[3] # 4th byte (index 3)
|
||||||
|
byte5 = byte_data[4] # 5th byte (index 4)
|
||||||
|
firmware_version = int(f"{byte4:02x}{byte5:02x}")
|
||||||
|
|
||||||
|
|
||||||
|
data = {
|
||||||
|
'firmware_version': firmware_version,
|
||||||
|
}
|
||||||
|
json_data = json.dumps(data)
|
||||||
|
print(json_data)
|
||||||
|
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()
|
||||||
|
|
||||||
@@ -1,7 +1,13 @@
|
|||||||
'''
|
'''
|
||||||
|
_ _ ____ __ __
|
||||||
|
| \ | | _ \| \/ |
|
||||||
|
| \| | |_) | |\/| |
|
||||||
|
| |\ | __/| | | |
|
||||||
|
|_| \_|_| |_| |_|
|
||||||
|
|
||||||
Script to get NPM values
|
Script to get NPM values
|
||||||
need parameter: port
|
need parameter: port
|
||||||
/usr/bin/python3 /var/www/nebuleair_pro_4g/npm.py ttyAMA4
|
/usr/bin/python3 /var/www/nebuleair_pro_4g/NPM/get_data.py ttyAMA5
|
||||||
'''
|
'''
|
||||||
|
|
||||||
import serial
|
import serial
|
||||||
@@ -19,7 +25,7 @@ ser = serial.Serial(
|
|||||||
parity=serial.PARITY_EVEN,
|
parity=serial.PARITY_EVEN,
|
||||||
stopbits=serial.STOPBITS_ONE,
|
stopbits=serial.STOPBITS_ONE,
|
||||||
bytesize=serial.EIGHTBITS,
|
bytesize=serial.EIGHTBITS,
|
||||||
timeout = 2
|
timeout = 1
|
||||||
)
|
)
|
||||||
|
|
||||||
ser.write(b'\x81\x11\x6E') #data10s
|
ser.write(b'\x81\x11\x6E') #data10s
|
||||||
137
NPM/get_data_modbus.py
Executable file
137
NPM/get_data_modbus.py
Executable file
@@ -0,0 +1,137 @@
|
|||||||
|
'''
|
||||||
|
_ _ ____ __ __
|
||||||
|
| \ | | _ \| \/ |
|
||||||
|
| \| | |_) | |\/| |
|
||||||
|
| |\ | __/| | | |
|
||||||
|
|_| \_|_| |_| |_|
|
||||||
|
|
||||||
|
Script to get NPM data via Modbus
|
||||||
|
need parameter: port
|
||||||
|
/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
|
||||||
|
|
||||||
|
\x01 Slave Address (slave device address)
|
||||||
|
\x03 Function code (read multiple holding registers)
|
||||||
|
\x00\x80 Starting Address (The request starts reading from holding register address 0x80 or 128)
|
||||||
|
\x00\x0A Quantity of Registers (Requests to read 0x0A or 10 consecutive registers starting from address 128)
|
||||||
|
\xE4\x1E 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\x80\x00\x0A'
|
||||||
|
|
||||||
|
# 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)
|
||||||
|
|
||||||
|
# Extract LSW (first 2 bytes) and MSW (last 2 bytes)
|
||||||
|
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='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='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='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='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}")
|
||||||
|
print(f"Channel 2 (0.5->1.0): {raw_value_channel2}")
|
||||||
|
print(f"Channel 3 (1.0->2.5): {raw_value_channel3}")
|
||||||
|
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
|
||||||
|
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()
|
||||||
188
NPM/get_data_modbus_v2.py
Executable file
188
NPM/get_data_modbus_v2.py
Executable file
@@ -0,0 +1,188 @@
|
|||||||
|
'''
|
||||||
|
_ _ ____ __ __
|
||||||
|
| \ | | _ \| \/ |
|
||||||
|
| \| | |_) | |\/| |
|
||||||
|
| |\ | __/| | | |
|
||||||
|
|_| \_|_| |_| |_|
|
||||||
|
|
||||||
|
Script to get NPM data via Modbus
|
||||||
|
need parameter: port
|
||||||
|
/usr/bin/python3 /var/www/nebuleair_pro_4g/NPM/get_data_modbus_v2.py
|
||||||
|
|
||||||
|
Modbus RTU
|
||||||
|
[Slave Address][Function Code][Starting Address][Quantity of Registers][CRC]
|
||||||
|
|
||||||
|
Pour récupérer
|
||||||
|
les concentrations en PM1, PM10 et PM2.5 (a partir du registre 0x38)
|
||||||
|
les 5 cannaux
|
||||||
|
la température et l'humidité à l'intérieur du capteur
|
||||||
|
Donnée actualisée toutes les 10 secondes
|
||||||
|
|
||||||
|
Request
|
||||||
|
\x01\x03\x00\x38\x00\x55\...\...
|
||||||
|
|
||||||
|
\x01 Slave Address (slave device address)
|
||||||
|
\x03 Function code (read multiple holding registers)
|
||||||
|
\x00\x38 Starting Address (The request starts reading from holding register address x38 or 56)
|
||||||
|
\x00\x55 Quantity of Registers (Requests to read x55 or 85 consecutive registers starting from address 56)
|
||||||
|
\...\... Cyclic Redundancy Check (checksum )
|
||||||
|
|
||||||
|
'''
|
||||||
|
|
||||||
|
import serial
|
||||||
|
import requests
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
import crcmod
|
||||||
|
import sqlite3
|
||||||
|
|
||||||
|
# Connect to the SQLite database
|
||||||
|
conn = sqlite3.connect("/var/www/nebuleair_pro_4g/sqlite/sensors.db")
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
def load_config(config_file):
|
||||||
|
try:
|
||||||
|
with open(config_file, 'r') as file:
|
||||||
|
config_data = json.load(file)
|
||||||
|
return config_data
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error loading config file: {e}")
|
||||||
|
return {}
|
||||||
|
|
||||||
|
# Load the configuration data
|
||||||
|
config_file = '/var/www/nebuleair_pro_4g/config.json'
|
||||||
|
config = load_config(config_file)
|
||||||
|
npm_solo_port = config.get('NPM_solo_port', '') #port du NPM solo
|
||||||
|
|
||||||
|
ser = serial.Serial(
|
||||||
|
port=npm_solo_port,
|
||||||
|
baudrate=115200,
|
||||||
|
parity=serial.PARITY_EVEN,
|
||||||
|
stopbits=serial.STOPBITS_ONE,
|
||||||
|
bytesize=serial.EIGHTBITS,
|
||||||
|
timeout = 2
|
||||||
|
)
|
||||||
|
|
||||||
|
# Define Modbus CRC-16 function
|
||||||
|
crc16 = crcmod.predefined.mkPredefinedCrcFun('modbus')
|
||||||
|
|
||||||
|
# Request frame without CRC
|
||||||
|
data = b'\x01\x03\x00\x38\x00\x55'
|
||||||
|
|
||||||
|
# Calculate CRC
|
||||||
|
crc = crc16(data)
|
||||||
|
crc_low = crc & 0xFF
|
||||||
|
crc_high = (crc >> 8) & 0xFF
|
||||||
|
|
||||||
|
# Append CRC to the frame
|
||||||
|
request = data + bytes([crc_low, crc_high])
|
||||||
|
#print(f"Request frame: {request.hex()}")
|
||||||
|
|
||||||
|
ser.write(request)
|
||||||
|
|
||||||
|
#GET RTC TIME from SQlite
|
||||||
|
cursor.execute("SELECT * FROM timestamp_table LIMIT 1")
|
||||||
|
row = cursor.fetchone() # Get the first (and only) row
|
||||||
|
rtc_time_str = row[1] # '2025-02-07 12:30:45'
|
||||||
|
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
byte_data = ser.readline()
|
||||||
|
formatted = ''.join(f'\\x{byte:02x}' for byte in byte_data)
|
||||||
|
print(formatted)
|
||||||
|
|
||||||
|
# Register base (56 = 0x38)
|
||||||
|
REGISTER_START = 56
|
||||||
|
|
||||||
|
# Function to extract 32-bit values from Modbus response
|
||||||
|
def extract_value(byte_data, register, scale=1, single_register=False, round_to=None):
|
||||||
|
"""Extracts a value from Modbus response.
|
||||||
|
|
||||||
|
- `register`: Modbus register to read.
|
||||||
|
- `scale`: Value is divided by this (e.g., `1000` for PM values).
|
||||||
|
- `single_register`: If `True`, only reads 16 bits (one register).
|
||||||
|
"""
|
||||||
|
offset = (register - REGISTER_START) * 2 + 3 # Calculate byte offset
|
||||||
|
|
||||||
|
if single_register:
|
||||||
|
value = int.from_bytes(byte_data[offset:offset+2], byteorder='big')
|
||||||
|
else:
|
||||||
|
lsw = int.from_bytes(byte_data[offset:offset+2], byteorder='big')
|
||||||
|
msw = int.from_bytes(byte_data[offset+2:offset+4], byteorder='big')
|
||||||
|
value = (msw << 16) | lsw # 32-bit value
|
||||||
|
|
||||||
|
value = value / scale # Apply scaling
|
||||||
|
|
||||||
|
if round_to == 0:
|
||||||
|
return int(value) # Convert to integer to remove .0
|
||||||
|
elif round_to is not None:
|
||||||
|
return round(value, round_to) # Apply normal rounding
|
||||||
|
else:
|
||||||
|
return value # No rounding if round_to is None
|
||||||
|
|
||||||
|
# 10-sec PM Concentration (PM1, PM2.5, PM10)
|
||||||
|
pm1_10s = extract_value(byte_data, 56, 1000, round_to=1)
|
||||||
|
pm25_10s = extract_value(byte_data, 58, 1000, round_to=1)
|
||||||
|
pm10_10s = extract_value(byte_data, 60, 1000, round_to=1)
|
||||||
|
|
||||||
|
print("10 sec concentration:")
|
||||||
|
print(f"PM1: {pm1_10s}")
|
||||||
|
print(f"PM2.5: {pm25_10s}")
|
||||||
|
print(f"PM10: {pm10_10s}")
|
||||||
|
|
||||||
|
# 1-min PM Concentration
|
||||||
|
pm1_1min = extract_value(byte_data, 68, 1000, round_to=1)
|
||||||
|
pm25_1min = extract_value(byte_data, 70, 1000, round_to=1)
|
||||||
|
pm10_1min = extract_value(byte_data, 72, 1000, round_to=1)
|
||||||
|
|
||||||
|
#print("1 min concentration:")
|
||||||
|
#print(f"PM1: {pm1_1min}")
|
||||||
|
#print(f"PM2.5: {pm25_1min}")
|
||||||
|
#print(f"PM10: {pm10_1min}")
|
||||||
|
|
||||||
|
# Extract values for 5 channels
|
||||||
|
channel_1 = extract_value(byte_data, 128, round_to=0) # 0.2 - 0.5μm
|
||||||
|
channel_2 = extract_value(byte_data, 130, round_to=0) # 0.5 - 1.0μm
|
||||||
|
channel_3 = extract_value(byte_data, 132, round_to=0) # 1.0 - 2.5μm
|
||||||
|
channel_4 = extract_value(byte_data, 134, round_to=0) # 2.5 - 5.0μm
|
||||||
|
channel_5 = extract_value(byte_data, 136, round_to=0) # 5.0 - 10.0μm
|
||||||
|
|
||||||
|
print(f"Channel 1 (0.2->0.5): {channel_1}")
|
||||||
|
print(f"Channel 2 (0.5->1.0): {channel_2}")
|
||||||
|
print(f"Channel 3 (1.0->2.5): {channel_3}")
|
||||||
|
print(f"Channel 4 (2.5->5.0): {channel_4}")
|
||||||
|
print(f"Channel 5 (5.0->10.): {channel_5}")
|
||||||
|
|
||||||
|
|
||||||
|
# Retrieve relative humidity from register 106 (0x6A)
|
||||||
|
relative_humidity = extract_value(byte_data, 106, 100, single_register=True)
|
||||||
|
#print(f"Internal Relative Humidity: {relative_humidity} %")
|
||||||
|
# Retrieve temperature from register 106 (0x6A)
|
||||||
|
temperature = extract_value(byte_data, 107, 100, single_register=True)
|
||||||
|
#print(f"Internal temperature: {temperature} °C")
|
||||||
|
|
||||||
|
cursor.execute('''
|
||||||
|
INSERT INTO data_NPM_5channels (timestamp,PM_ch1, PM_ch2, PM_ch3, PM_ch4, PM_ch5) VALUES (?,?,?,?,?,?)'''
|
||||||
|
, (rtc_time_str,channel_1,channel_2,channel_3,channel_4,channel_5))
|
||||||
|
|
||||||
|
cursor.execute('''
|
||||||
|
INSERT INTO data_NPM (timestamp,PM1, PM25, PM10, temp_npm, hum_npm) VALUES (?,?,?,?,?,?)'''
|
||||||
|
, (rtc_time_str,pm1_10s,pm25_10s,pm10_10s,temperature,relative_humidity ))
|
||||||
|
|
||||||
|
|
||||||
|
# Commit and close the connection
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
|
||||||
|
break
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print("User interrupt encountered. Exiting...")
|
||||||
|
time.sleep(3)
|
||||||
|
exit()
|
||||||
|
except:
|
||||||
|
# for all other kinds of error, but not specifying which one
|
||||||
|
print("Unknown error...")
|
||||||
|
time.sleep(3)
|
||||||
|
exit()
|
||||||
|
|
||||||
|
conn.close()
|
||||||
177
NPM/get_data_modbus_v3.py
Executable file
177
NPM/get_data_modbus_v3.py
Executable file
@@ -0,0 +1,177 @@
|
|||||||
|
'''
|
||||||
|
_ _ ____ __ __
|
||||||
|
| \ | | _ \| \/ |
|
||||||
|
| \| | |_) | |\/| |
|
||||||
|
| |\ | __/| | | |
|
||||||
|
|_| \_|_| |_| |_|
|
||||||
|
|
||||||
|
Script to get NPM data via Modbus
|
||||||
|
|
||||||
|
Improved version with data stream lenght check
|
||||||
|
|
||||||
|
/usr/bin/python3 /var/www/nebuleair_pro_4g/NPM/get_data_modbus_v3.py
|
||||||
|
|
||||||
|
Modbus RTU
|
||||||
|
[Slave Address][Function Code][Starting Address][Quantity of Registers][CRC]
|
||||||
|
|
||||||
|
Pour récupérer
|
||||||
|
les concentrations en PM1, PM10 et PM2.5 (a partir du registre 0x38)
|
||||||
|
les 5 cannaux
|
||||||
|
la température et l'humidité à l'intérieur du capteur
|
||||||
|
Donnée actualisée toutes les 10 secondes
|
||||||
|
|
||||||
|
Request
|
||||||
|
\x01\x03\x00\x38\x00\x55\...\...
|
||||||
|
|
||||||
|
\x01 Slave Address (slave device address)
|
||||||
|
\x03 Function code (read multiple holding registers)
|
||||||
|
\x00\x38 Starting Address (The request starts reading from holding register address x38 or 56)
|
||||||
|
\x00\x55 Quantity of Registers (Requests to read x55 or 85 consecutive registers starting from address 56)
|
||||||
|
\...\... Cyclic Redundancy Check (checksum )
|
||||||
|
|
||||||
|
'''
|
||||||
|
import serial
|
||||||
|
import requests
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
import crcmod
|
||||||
|
import sqlite3
|
||||||
|
import time
|
||||||
|
|
||||||
|
# Connect to the SQLite database
|
||||||
|
conn = sqlite3.connect("/var/www/nebuleair_pro_4g/sqlite/sensors.db")
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
def load_config(config_file):
|
||||||
|
try:
|
||||||
|
with open(config_file, 'r') as file:
|
||||||
|
config_data = json.load(file)
|
||||||
|
return config_data
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error loading config file: {e}")
|
||||||
|
return {}
|
||||||
|
|
||||||
|
# Load the configuration data
|
||||||
|
npm_solo_port = "/dev/ttyAMA5" #port du NPM solo
|
||||||
|
|
||||||
|
#GET RTC TIME from SQlite
|
||||||
|
cursor.execute("SELECT * FROM timestamp_table LIMIT 1")
|
||||||
|
row = cursor.fetchone() # Get the first (and only) row
|
||||||
|
rtc_time_str = row[1] # '2025-02-07 12:30:45'
|
||||||
|
|
||||||
|
# Initialize serial port
|
||||||
|
ser = serial.Serial(
|
||||||
|
port=npm_solo_port,
|
||||||
|
baudrate=115200,
|
||||||
|
parity=serial.PARITY_EVEN,
|
||||||
|
stopbits=serial.STOPBITS_ONE,
|
||||||
|
bytesize=serial.EIGHTBITS,
|
||||||
|
timeout=2
|
||||||
|
)
|
||||||
|
|
||||||
|
# Define Modbus CRC-16 function
|
||||||
|
crc16 = crcmod.predefined.mkPredefinedCrcFun('modbus')
|
||||||
|
|
||||||
|
# Request frame without CRC
|
||||||
|
data = b'\x01\x03\x00\x38\x00\x55'
|
||||||
|
|
||||||
|
# Calculate and append CRC
|
||||||
|
crc = crc16(data)
|
||||||
|
crc_low = crc & 0xFF
|
||||||
|
crc_high = (crc >> 8) & 0xFF
|
||||||
|
request = data + bytes([crc_low, crc_high])
|
||||||
|
|
||||||
|
# Clear serial buffer before sending
|
||||||
|
ser.flushInput()
|
||||||
|
|
||||||
|
# Send request
|
||||||
|
ser.write(request)
|
||||||
|
time.sleep(0.2) # Wait for sensor to respond
|
||||||
|
|
||||||
|
# Read response
|
||||||
|
response_length = 2 + 1 + (2 * 85) + 2 # Address + Function + Data + CRC
|
||||||
|
byte_data = ser.read(response_length)
|
||||||
|
|
||||||
|
# Validate response length
|
||||||
|
if len(byte_data) < response_length:
|
||||||
|
print("[ERROR] Incomplete response received:", byte_data.hex())
|
||||||
|
exit()
|
||||||
|
|
||||||
|
# Verify CRC
|
||||||
|
received_crc = int.from_bytes(byte_data[-2:], byteorder='little')
|
||||||
|
calculated_crc = crc16(byte_data[:-2])
|
||||||
|
|
||||||
|
if received_crc != calculated_crc:
|
||||||
|
print("[ERROR] CRC check failed! Corrupted data received.")
|
||||||
|
exit()
|
||||||
|
|
||||||
|
# Convert response to hex for debugging
|
||||||
|
formatted = ''.join(f'\\x{byte:02x}' for byte in byte_data)
|
||||||
|
#print("Response:", formatted)
|
||||||
|
|
||||||
|
# Extract and print PM values
|
||||||
|
def extract_value(byte_data, register, scale=1, single_register=False, round_to=None):
|
||||||
|
REGISTER_START = 56
|
||||||
|
offset = (register - REGISTER_START) * 2 + 3
|
||||||
|
|
||||||
|
if single_register:
|
||||||
|
value = int.from_bytes(byte_data[offset:offset+2], byteorder='big')
|
||||||
|
else:
|
||||||
|
lsw = int.from_bytes(byte_data[offset:offset+2], byteorder='big')
|
||||||
|
msw = int.from_bytes(byte_data[offset+2:offset+4], byteorder='big')
|
||||||
|
value = (msw << 16) | lsw
|
||||||
|
|
||||||
|
value = value / scale
|
||||||
|
|
||||||
|
if round_to == 0:
|
||||||
|
return int(value)
|
||||||
|
elif round_to is not None:
|
||||||
|
return round(value, round_to)
|
||||||
|
else:
|
||||||
|
return value
|
||||||
|
|
||||||
|
pm1_10s = extract_value(byte_data, 56, 1000, round_to=1)
|
||||||
|
pm25_10s = extract_value(byte_data, 58, 1000, round_to=1)
|
||||||
|
pm10_10s = extract_value(byte_data, 60, 1000, round_to=1)
|
||||||
|
|
||||||
|
#print("10 sec concentration:")
|
||||||
|
#print(f"PM1: {pm1_10s}")
|
||||||
|
#print(f"PM2.5: {pm25_10s}")
|
||||||
|
#print(f"PM10: {pm10_10s}")
|
||||||
|
|
||||||
|
# Extract values for 5 channels
|
||||||
|
channel_1 = extract_value(byte_data, 128, round_to=0) # 0.2 - 0.5μm
|
||||||
|
channel_2 = extract_value(byte_data, 130, round_to=0) # 0.5 - 1.0μm
|
||||||
|
channel_3 = extract_value(byte_data, 132, round_to=0) # 1.0 - 2.5μm
|
||||||
|
channel_4 = extract_value(byte_data, 134, round_to=0) # 2.5 - 5.0μm
|
||||||
|
channel_5 = extract_value(byte_data, 136, round_to=0) # 5.0 - 10.0μm
|
||||||
|
|
||||||
|
#print(f"Channel 1 (0.2->0.5): {channel_1}")
|
||||||
|
#print(f"Channel 2 (0.5->1.0): {channel_2}")
|
||||||
|
#print(f"Channel 3 (1.0->2.5): {channel_3}")
|
||||||
|
#print(f"Channel 4 (2.5->5.0): {channel_4}")
|
||||||
|
#print(f"Channel 5 (5.0->10.): {channel_5}")
|
||||||
|
|
||||||
|
|
||||||
|
# Retrieve relative humidity from register 106 (0x6A)
|
||||||
|
relative_humidity = extract_value(byte_data, 106, 100, single_register=True)
|
||||||
|
# Retrieve temperature from register 106 (0x6A)
|
||||||
|
temperature = extract_value(byte_data, 107, 100, single_register=True)
|
||||||
|
|
||||||
|
#print(f"Internal Relative Humidity: {relative_humidity} %")
|
||||||
|
#print(f"Internal temperature: {temperature} °C")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
cursor.execute('''
|
||||||
|
INSERT INTO data_NPM_5channels (timestamp,PM_ch1, PM_ch2, PM_ch3, PM_ch4, PM_ch5) VALUES (?,?,?,?,?,?)'''
|
||||||
|
, (rtc_time_str,channel_1,channel_2,channel_3,channel_4,channel_5))
|
||||||
|
|
||||||
|
cursor.execute('''
|
||||||
|
INSERT INTO data_NPM (timestamp,PM1, PM25, PM10, temp_npm, hum_npm) VALUES (?,?,?,?,?,?)'''
|
||||||
|
, (rtc_time_str,pm1_10s,pm25_10s,pm10_10s,temperature,relative_humidity ))
|
||||||
|
|
||||||
|
# Commit and close the connection
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
conn.close()
|
||||||
52
NPM/get_data_temp_hum.py
Executable file
52
NPM/get_data_temp_hum.py
Executable file
@@ -0,0 +1,52 @@
|
|||||||
|
'''
|
||||||
|
_ _ ____ __ __
|
||||||
|
| \ | | _ \| \/ |
|
||||||
|
| \| | |_) | |\/| |
|
||||||
|
| |\ | __/| | | |
|
||||||
|
|_| \_|_| |_| |_|
|
||||||
|
|
||||||
|
Script to get NPM values: ONLY temp and hum
|
||||||
|
need parameter: port
|
||||||
|
/usr/bin/python3 /var/www/nebuleair_pro_4g/NPM/get_data_temp_hum.py ttyAMA5
|
||||||
|
'''
|
||||||
|
|
||||||
|
import serial
|
||||||
|
import requests
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
|
||||||
|
parameter = sys.argv[1:] # Exclude the script name
|
||||||
|
#print("Parameters received:")
|
||||||
|
port='/dev/'+parameter[0]
|
||||||
|
|
||||||
|
ser = serial.Serial(
|
||||||
|
port=port,
|
||||||
|
baudrate=115200,
|
||||||
|
parity=serial.PARITY_EVEN,
|
||||||
|
stopbits=serial.STOPBITS_ONE,
|
||||||
|
bytesize=serial.EIGHTBITS,
|
||||||
|
timeout = 1
|
||||||
|
)
|
||||||
|
|
||||||
|
ser.write(b'\x81\x14\x6B') # Temp and humidity command
|
||||||
|
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
byte_data_temp_hum = ser.readline()
|
||||||
|
# Decode temperature and humidity values
|
||||||
|
temperature = int.from_bytes(byte_data_temp_hum[3:5], byteorder='big') / 100.0
|
||||||
|
humidity = int.from_bytes(byte_data_temp_hum[5:7], byteorder='big') / 100.0
|
||||||
|
|
||||||
|
print(f"temp: {temperature}")
|
||||||
|
print(f"hum: {humidity}")
|
||||||
|
break
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print("User interrupt encountered. Exiting...")
|
||||||
|
time.sleep(3)
|
||||||
|
exit()
|
||||||
|
except:
|
||||||
|
# for all other kinds of error, but not specifying which one
|
||||||
|
print("Unknown error...")
|
||||||
|
time.sleep(3)
|
||||||
|
exit()
|
||||||
|
|
||||||
104
NPM/get_data_v2.py
Executable file
104
NPM/get_data_v2.py
Executable file
@@ -0,0 +1,104 @@
|
|||||||
|
'''
|
||||||
|
_ _ ____ __ __
|
||||||
|
| \ | | _ \| \/ |
|
||||||
|
| \| | |_) | |\/| |
|
||||||
|
| |\ | __/| | | |
|
||||||
|
|_| \_|_| |_| |_|
|
||||||
|
|
||||||
|
Script to get NPM values (PM1, PM2.5 and PM10)
|
||||||
|
PM and the sensor temp/hum
|
||||||
|
And store them inside sqlite database
|
||||||
|
Uses RTC module for timing (from SQLite db)
|
||||||
|
/usr/bin/python3 /var/www/nebuleair_pro_4g/NPM/get_data_v2.py
|
||||||
|
'''
|
||||||
|
|
||||||
|
import serial
|
||||||
|
import requests
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
import sqlite3
|
||||||
|
import smbus2
|
||||||
|
import time
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
# Connect to the SQLite database
|
||||||
|
conn = sqlite3.connect("/var/www/nebuleair_pro_4g/sqlite/sensors.db")
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
def load_config(config_file):
|
||||||
|
try:
|
||||||
|
with open(config_file, 'r') as file:
|
||||||
|
config_data = json.load(file)
|
||||||
|
return config_data
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error loading config file: {e}")
|
||||||
|
return {}
|
||||||
|
|
||||||
|
# Load the configuration data
|
||||||
|
config_file = '/var/www/nebuleair_pro_4g/config.json'
|
||||||
|
config = load_config(config_file)
|
||||||
|
npm_solo_port = config.get('NPM_solo_port', '') #port du NPM solo
|
||||||
|
|
||||||
|
ser = serial.Serial(
|
||||||
|
port=npm_solo_port,
|
||||||
|
baudrate=115200,
|
||||||
|
parity=serial.PARITY_EVEN,
|
||||||
|
stopbits=serial.STOPBITS_ONE,
|
||||||
|
bytesize=serial.EIGHTBITS,
|
||||||
|
timeout = 0.5
|
||||||
|
)
|
||||||
|
|
||||||
|
# 1️⃣ Request PM Data (PM1, PM2.5, PM10)
|
||||||
|
|
||||||
|
#ser.write(b'\x81\x11\x6E') #data10s
|
||||||
|
ser.write(b'\x81\x12\x6D') #data60s
|
||||||
|
time.sleep(0.5) # Small delay to allow the sensor to process the request
|
||||||
|
|
||||||
|
#print("Start get_data_v2.py script")
|
||||||
|
byte_data = ser.readline()
|
||||||
|
#print(byte_data)
|
||||||
|
stateByte = int.from_bytes(byte_data[2:3], byteorder='big')
|
||||||
|
Statebits = [int(bit) for bit in bin(stateByte)[2:].zfill(8)]
|
||||||
|
PM1 = int.from_bytes(byte_data[9:11], byteorder='big')/10
|
||||||
|
PM25 = int.from_bytes(byte_data[11:13], byteorder='big')/10
|
||||||
|
PM10 = int.from_bytes(byte_data[13:15], byteorder='big')/10
|
||||||
|
|
||||||
|
# 2️⃣ Request Temperature & Humidity
|
||||||
|
ser.write(b'\x81\x14\x6B') # Temp and humidity command
|
||||||
|
time.sleep(0.5) # Small delay to allow the sensor to process the request
|
||||||
|
byte_data_temp_hum = ser.readline()
|
||||||
|
|
||||||
|
# Decode temperature and humidity values
|
||||||
|
temperature = int.from_bytes(byte_data_temp_hum[3:5], byteorder='big') / 100.0
|
||||||
|
humidity = int.from_bytes(byte_data_temp_hum[5:7], byteorder='big') / 100.0
|
||||||
|
|
||||||
|
#print(f"State: {Statebits}")
|
||||||
|
#print(f"PM1: {PM1}")
|
||||||
|
#print(f"PM25: {PM25}")
|
||||||
|
#print(f"PM10: {PM10}")
|
||||||
|
#print(f"temp: {temperature}")
|
||||||
|
#print(f"hum: {humidity}")
|
||||||
|
|
||||||
|
#GET RTC TIME from SQlite
|
||||||
|
cursor.execute("SELECT * FROM timestamp_table LIMIT 1")
|
||||||
|
row = cursor.fetchone() # Get the first (and only) row
|
||||||
|
rtc_time_str = row[1] # '2025-02-07 12:30:45'
|
||||||
|
|
||||||
|
#save to sqlite database
|
||||||
|
try:
|
||||||
|
cursor.execute('''
|
||||||
|
INSERT INTO data_NPM (timestamp,PM1, PM25, PM10, temp_npm, hum_npm) VALUES (?,?,?,?,?,?)'''
|
||||||
|
, (rtc_time_str,PM1,PM25,PM10,temperature,humidity ))
|
||||||
|
|
||||||
|
# Commit and close the connection
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
#print("Sensor data saved successfully!")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Database error: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
172
NPM/old/get_data_modbus_loop.py
Executable file
172
NPM/old/get_data_modbus_loop.py
Executable file
@@ -0,0 +1,172 @@
|
|||||||
|
'''
|
||||||
|
Loop to run every minutes
|
||||||
|
|
||||||
|
* * * * * /usr/bin/python3 /var/www/nebuleair_pro_4g/NPM/get_data_modbus_loop.py ttyAMA5
|
||||||
|
|
||||||
|
saves data (avaerage) to a json file /var/www/nebuleair_pro_4g/NPM/data/data.json
|
||||||
|
saves data (all) to a sqlite database
|
||||||
|
|
||||||
|
first time running the script?
|
||||||
|
sudo mkdir /var/www/nebuleair_pro_4g/NPM/data
|
||||||
|
sudo touch /var/www/nebuleair_pro_4g/NPM/data/data.json
|
||||||
|
'''
|
||||||
|
|
||||||
|
import serial
|
||||||
|
import sys
|
||||||
|
import crcmod
|
||||||
|
import time
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import sqlite3
|
||||||
|
import smbus2 # For RTC DS3231
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
|
||||||
|
# Ensure a port argument is provided
|
||||||
|
if len(sys.argv) < 2:
|
||||||
|
print("Usage: python3 get_data_modbus.py <serial_port>")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
port = '/dev/' + sys.argv[1]
|
||||||
|
|
||||||
|
# Initialize serial communication
|
||||||
|
try:
|
||||||
|
ser = serial.Serial(
|
||||||
|
port=port,
|
||||||
|
baudrate=115200,
|
||||||
|
parity=serial.PARITY_EVEN,
|
||||||
|
stopbits=serial.STOPBITS_ONE,
|
||||||
|
bytesize=serial.EIGHTBITS,
|
||||||
|
timeout=0.5
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error opening serial port {port}: {e}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# Define Modbus CRC-16 function
|
||||||
|
crc16 = crcmod.predefined.mkPredefinedCrcFun('modbus')
|
||||||
|
|
||||||
|
# Request frame without CRC
|
||||||
|
data = b'\x01\x03\x00\x80\x00\x0A'
|
||||||
|
|
||||||
|
# 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])
|
||||||
|
|
||||||
|
# Log request frame
|
||||||
|
print(f"Request frame: {request.hex()}")
|
||||||
|
|
||||||
|
# Initialize SQLite database
|
||||||
|
conn = sqlite3.connect("/var/www/nebuleair_pro_4g/sqlite/sensors.db")
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
# RTC Module (DS3231) Setup
|
||||||
|
RTC_I2C_ADDR = 0x68 # DS3231 I2C Address
|
||||||
|
bus = smbus2.SMBus(1)
|
||||||
|
|
||||||
|
def bcd_to_dec(bcd):
|
||||||
|
return (bcd // 16 * 10) + (bcd % 16)
|
||||||
|
|
||||||
|
def get_rtc_time():
|
||||||
|
"""Reads time from RTC module (DS3231)"""
|
||||||
|
try:
|
||||||
|
data = bus.read_i2c_block_data(RTC_I2C_ADDR, 0x00, 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).strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"RTC Read Error: {e}")
|
||||||
|
return "N/A"
|
||||||
|
|
||||||
|
# Initialize storage for averaging
|
||||||
|
num_samples = 6
|
||||||
|
channel_sums = [0] * 5 # 5 channels
|
||||||
|
|
||||||
|
#do not start immediately to prevent multiple write/read over NPM serial port
|
||||||
|
time.sleep(3)
|
||||||
|
|
||||||
|
# Loop 6 times to collect data every 10 seconds
|
||||||
|
for i in range(num_samples):
|
||||||
|
print(f"\nIteration {i+1}/{num_samples}")
|
||||||
|
ser.write(request)
|
||||||
|
rtc_timestamp = get_rtc_time()
|
||||||
|
|
||||||
|
try:
|
||||||
|
byte_data = ser.readline()
|
||||||
|
formatted = ''.join(f'\\x{byte:02x}' for byte in byte_data)
|
||||||
|
print(f"Raw Response: {formatted}")
|
||||||
|
|
||||||
|
if len(byte_data) < 23:
|
||||||
|
print("Incomplete response, skipping this sample.")
|
||||||
|
time.sleep(9)
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Extract and process the 5 channels
|
||||||
|
channels = []
|
||||||
|
for j in range(5):
|
||||||
|
lsw = int.from_bytes(byte_data[3 + j*4 : 5 + j*4], byteorder='little')
|
||||||
|
msw = int.from_bytes(byte_data[5 + j*4 : 7 + j*4], byteorder='little')
|
||||||
|
raw_value = (msw << 16) | lsw
|
||||||
|
channels.append(raw_value)
|
||||||
|
|
||||||
|
# Accumulate sum for each channel
|
||||||
|
for j in range(5):
|
||||||
|
channel_sums[j] += channels[j]
|
||||||
|
|
||||||
|
# Print collected values
|
||||||
|
print(f"Timestamp (RTC): {rtc_timestamp}")
|
||||||
|
for j in range(5):
|
||||||
|
print(f"Channel {j+1}: {channels[j]}")
|
||||||
|
|
||||||
|
|
||||||
|
# Save the individual reading to the database
|
||||||
|
cursor.execute('''
|
||||||
|
INSERT INTO data (timestamp, PM_ch1, PM_ch2, PM_ch3, PM_ch4, PM_ch5)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?)
|
||||||
|
''', (rtc_timestamp, channels[0], channels[1], channels[2], channels[3], channels[4]))
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error reading data: {e}")
|
||||||
|
|
||||||
|
# Wait 10 seconds before next reading
|
||||||
|
time.sleep(10)
|
||||||
|
|
||||||
|
# Compute the average values
|
||||||
|
channel_means = [int(s / num_samples) for s in channel_sums]
|
||||||
|
|
||||||
|
# Create JSON structure
|
||||||
|
data_json = {
|
||||||
|
"channel_1": channel_means[0],
|
||||||
|
"channel_2": channel_means[1],
|
||||||
|
"channel_3": channel_means[2],
|
||||||
|
"channel_4": channel_means[3],
|
||||||
|
"channel_5": channel_means[4]
|
||||||
|
}
|
||||||
|
|
||||||
|
# Print final JSON data
|
||||||
|
print("\nFinal JSON Data (Averaged over 6 readings):")
|
||||||
|
print(json.dumps(data_json, indent=4))
|
||||||
|
|
||||||
|
# Define JSON file path
|
||||||
|
output_file = "/var/www/nebuleair_pro_4g/NPM/data/data.json"
|
||||||
|
|
||||||
|
# Write results to a JSON file
|
||||||
|
try:
|
||||||
|
with open(output_file, "w") as f:
|
||||||
|
json.dump(data_json, f, indent=4)
|
||||||
|
print(f"\nAveraged JSON data saved to {output_file}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error writing to file: {e}")
|
||||||
|
|
||||||
|
# Close serial connection
|
||||||
|
ser.close()
|
||||||
126
NPM/old/test_modbus.py
Executable file
126
NPM/old/test_modbus.py
Executable file
@@ -0,0 +1,126 @@
|
|||||||
|
'''
|
||||||
|
_ _ ____ __ __
|
||||||
|
| \ | | _ \| \/ |
|
||||||
|
| \| | |_) | |\/| |
|
||||||
|
| |\ | __/| | | |
|
||||||
|
|_| \_|_| |_| |_|
|
||||||
|
|
||||||
|
Script to get NPM data via Modbus
|
||||||
|
need parameter: port
|
||||||
|
/usr/bin/python3 /var/www/nebuleair_pro_4g/NPM/old/test_modbus.py
|
||||||
|
|
||||||
|
Modbus RTU
|
||||||
|
[Slave Address][Function Code][Starting Address][Quantity of Registers][CRC]
|
||||||
|
|
||||||
|
Pour récupérer
|
||||||
|
les concentrations en PM1, PM10 et PM2.5 (a partir du registre 0x38)
|
||||||
|
les 5 cannaux
|
||||||
|
Donnée actualisée toutes les 10 secondes
|
||||||
|
|
||||||
|
Request
|
||||||
|
\x01\x03\x00\x38\x00\x55\...\...
|
||||||
|
|
||||||
|
\x01 Slave Address (slave device address)
|
||||||
|
\x03 Function code (read multiple holding registers)
|
||||||
|
\x00\x38 Starting Address (The request starts reading from holding register address x38 or 56)
|
||||||
|
\x00\x55 Quantity of Registers (Requests to read x55 or 85 consecutive registers starting from address 56)
|
||||||
|
\...\... Cyclic Redundancy Check (checksum )
|
||||||
|
|
||||||
|
'''
|
||||||
|
|
||||||
|
import serial
|
||||||
|
import requests
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
import crcmod
|
||||||
|
import sqlite3
|
||||||
|
|
||||||
|
# Connect to the SQLite database
|
||||||
|
conn = sqlite3.connect("/var/www/nebuleair_pro_4g/sqlite/sensors.db")
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
def load_config(config_file):
|
||||||
|
try:
|
||||||
|
with open(config_file, 'r') as file:
|
||||||
|
config_data = json.load(file)
|
||||||
|
return config_data
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error loading config file: {e}")
|
||||||
|
return {}
|
||||||
|
|
||||||
|
# Load the configuration data
|
||||||
|
config_file = '/var/www/nebuleair_pro_4g/config.json'
|
||||||
|
config = load_config(config_file)
|
||||||
|
npm_solo_port = config.get('NPM_solo_port', '') #port du NPM solo
|
||||||
|
|
||||||
|
ser = serial.Serial(
|
||||||
|
port=npm_solo_port,
|
||||||
|
baudrate=115200,
|
||||||
|
parity=serial.PARITY_EVEN,
|
||||||
|
stopbits=serial.STOPBITS_ONE,
|
||||||
|
bytesize=serial.EIGHTBITS,
|
||||||
|
timeout = 0.5
|
||||||
|
)
|
||||||
|
|
||||||
|
# Define Modbus CRC-16 function
|
||||||
|
crc16 = crcmod.predefined.mkPredefinedCrcFun('modbus')
|
||||||
|
|
||||||
|
# Request frame without CRC
|
||||||
|
data = b'\x01\x03\x00\x44\x00\x06'
|
||||||
|
|
||||||
|
# Calculate CRC
|
||||||
|
crc = crc16(data)
|
||||||
|
crc_low = crc & 0xFF
|
||||||
|
crc_high = (crc >> 8) & 0xFF
|
||||||
|
|
||||||
|
# Append CRC to the frame
|
||||||
|
request = data + bytes([crc_low, crc_high])
|
||||||
|
#print(f"Request frame: {request.hex()}")
|
||||||
|
|
||||||
|
ser.write(request)
|
||||||
|
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
byte_data = ser.readline()
|
||||||
|
formatted = ''.join(f'\\x{byte:02x}' for byte in byte_data)
|
||||||
|
print(formatted)
|
||||||
|
|
||||||
|
if len(byte_data) < 14:
|
||||||
|
print(f"Error: Received {len(byte_data)} bytes, expected 14!")
|
||||||
|
continue
|
||||||
|
|
||||||
|
#10 secs concentration
|
||||||
|
|
||||||
|
lsw_pm1 = int.from_bytes(byte_data[3:5], byteorder='big')
|
||||||
|
msw_pm1 = int.from_bytes(byte_data[5:7], byteorder='big')
|
||||||
|
raw_value_pm1 = (msw_pm1 << 16) | lsw_pm1
|
||||||
|
raw_value_pm1 = raw_value_pm1 / 1000
|
||||||
|
|
||||||
|
lsw_pm25 = int.from_bytes(byte_data[7:9], byteorder='big')
|
||||||
|
msw_pm25 = int.from_bytes(byte_data[9:11], byteorder='big')
|
||||||
|
raw_value_pm25 = (msw_pm25 << 16) | lsw_pm25
|
||||||
|
raw_value_pm25 = raw_value_pm25 / 1000
|
||||||
|
|
||||||
|
|
||||||
|
lsw_pm10 = int.from_bytes(byte_data[11:13], byteorder='big')
|
||||||
|
msw_pm10 = int.from_bytes(byte_data[13:15], byteorder='big')
|
||||||
|
raw_value_pm10 = (msw_pm10 << 16) | lsw_pm10
|
||||||
|
raw_value_pm10 = raw_value_pm10 / 1000
|
||||||
|
|
||||||
|
print("1 min")
|
||||||
|
print(f"PM1: {raw_value_pm1}")
|
||||||
|
print(f"PM2.5: {raw_value_pm25}")
|
||||||
|
print(f"PM10: {raw_value_pm10}")
|
||||||
|
|
||||||
|
break
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print("User interrupt encountered. Exiting...")
|
||||||
|
time.sleep(3)
|
||||||
|
exit()
|
||||||
|
except:
|
||||||
|
# for all other kinds of error, but not specifying which one
|
||||||
|
print("Unknown error...")
|
||||||
|
time.sleep(3)
|
||||||
|
exit()
|
||||||
|
|
||||||
|
conn.close()
|
||||||
87
README.md
87
README.md
@@ -4,30 +4,46 @@ Based on the Rpi4 or CM4.
|
|||||||
|
|
||||||
# Installation
|
# 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
|
## General
|
||||||
|
|
||||||
See `installation.sh`
|
Line by line installation.
|
||||||
|
|
||||||
```
|
```
|
||||||
sudo apt update
|
sudo apt update
|
||||||
sudo apt install git gh apache2 php python3 python3-pip jq autossh i2c-tools python3-smbus -y
|
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 --break-system-packages
|
sudo pip3 install pyserial requests RPi.GPIO adafruit-circuitpython-bme280 crcmod psutil ntplib pytz gpiozero adafruit-circuitpython-ads1x15 numpy --break-system-packages
|
||||||
sudo mkdir -p /var/www/.ssh
|
sudo mkdir -p /var/www/.ssh
|
||||||
sudo ssh-keygen -t rsa -b 4096 -f /var/www/.ssh/id_rsa -N ""
|
sudo ssh-keygen -t rsa -b 4096 -f /var/www/.ssh/id_rsa -N ""
|
||||||
sudo ssh-copy-id -i /var/www/.ssh/id_rsa.pub -p 50221 airlab_server1@aircarto.fr
|
sudo ssh-copy-id -i /var/www/.ssh/id_rsa.pub -p 50221 airlab_server1@aircarto.fr
|
||||||
sudo git clone http://gitea.aircarto.fr/PaulVua/nebuleair_pro_4g.git /var/www/nebuleair_pro_4g
|
sudo git clone http://gitea.aircarto.fr/PaulVua/nebuleair_pro_4g.git /var/www/nebuleair_pro_4g
|
||||||
sudo mkdir /var/www/nebuleair_pro_4g/logs
|
sudo mkdir /var/www/nebuleair_pro_4g/logs
|
||||||
sudo touch /var/www/nebuleair_pro_4g/logs/app.log /var/www/nebuleair_pro_4g/logs/loop.log /var/www/nebuleair_pro_4g/wifi_list.csv
|
sudo touch /var/www/nebuleair_pro_4g/logs/app.log /var/www/nebuleair_pro_4g/logs/loop.log /var/www/nebuleair_pro_4g/wifi_list.csv
|
||||||
sudo cp /var/www/nebuleair_pro_4g/config.json.dist /var/www/nebuleair_pro_4g/config.json
|
/usr/bin/python3 /var/www/nebuleair_pro_4g/sqlite/create_db.py
|
||||||
|
/usr/bin/python3 /var/www/nebuleair_pro_4g/sqlite/set_config.py
|
||||||
sudo chmod -R 777 /var/www/nebuleair_pro_4g/
|
sudo chmod -R 777 /var/www/nebuleair_pro_4g/
|
||||||
git config --global core.fileMode false
|
git config --global core.fileMode false
|
||||||
|
git -C /var/www/nebuleair_pro_4g config core.fileMode false
|
||||||
git config --global --add safe.directory /var/www/nebuleair_pro_4g
|
git config --global --add safe.directory /var/www/nebuleair_pro_4g
|
||||||
sudo crontab /var/www/nebuleair_pro_4g/cron_jobs
|
sudo crontab /var/www/nebuleair_pro_4g/cron_jobs
|
||||||
|
sudo /usr/bin/python3 /var/www/nebuleair_pro_4g/sqlite/create_db.py
|
||||||
```
|
```
|
||||||
## Apache
|
## Apache
|
||||||
Configuration of Apache to redirect to the html homepage project
|
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
|
ALL ALL=(ALL) NOPASSWD: /usr/bin/nmcli, /usr/sbin/reboot
|
||||||
www-data ALL=(ALL) NOPASSWD: /usr/bin/git pull
|
www-data ALL=(ALL) NOPASSWD: /usr/bin/git pull
|
||||||
www-data ALL=(ALL) NOPASSWD: /usr/bin/ssh
|
www-data ALL=(ALL) NOPASSWD: /usr/bin/ssh
|
||||||
|
www-data ALL=(ALL) NOPASSWD: /usr/bin/python3 *
|
||||||
|
www-data ALL=(ALL) NOPASSWD: /bin/systemctl *
|
||||||
|
www-data ALL=(ALL) NOPASSWD: /var/www/nebuleair_pro_4g/*
|
||||||
```
|
```
|
||||||
## Serial
|
## Serial
|
||||||
|
|
||||||
@@ -66,7 +84,7 @@ sudo chmod 777 /dev/ttyAMA*
|
|||||||
|
|
||||||
## I2C
|
## I2C
|
||||||
|
|
||||||
Decibel meter and BME280 is connected via I2C.
|
Decibel meter, BME280 and the RTC module (DS3231) is connected via I2C.
|
||||||
|
|
||||||
Need to activate by modifying `sudo nano /boot/firmware/config.txt`
|
Need to activate by modifying `sudo nano /boot/firmware/config.txt`
|
||||||
|
|
||||||
@@ -82,14 +100,26 @@ sudo chmod 777 /dev/i2c-1
|
|||||||
|
|
||||||
Attention: sometimes activation with config.txt do not work, you need to activate i2c with `sudo raspi-config` and go to "Interface" -> I2C -> enable.
|
Attention: sometimes activation with config.txt do not work, you need to activate i2c with `sudo raspi-config` and go to "Interface" -> I2C -> enable.
|
||||||
|
|
||||||
|
It is possible to manage raspi-config only with cli: `sudo raspi-config nonint do_i2c 0`
|
||||||
|
|
||||||
|
|
||||||
|
I2C addresses: use `sudo i2cdetect -y 1` to check the connected devices.
|
||||||
|
|
||||||
### BME280
|
### BME280
|
||||||
|
|
||||||
The python script is triggered by the main loop every minutes to get instant temp, hum and press values (no need to have a minute average).
|
The python script is triggered by the main loop every minutes to get instant temp, hum and press values (no need to have a minute average).
|
||||||
|
BME280 address is 0x76.
|
||||||
|
|
||||||
|
### RTC module (DS3231)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
### Noise sensor
|
### Noise sensor
|
||||||
|
|
||||||
As noise varies a lot, we keep the C program running every seconds to create a moving average for the last 60 seconds (we also gather max and min values).
|
As noise varies a lot, we keep the C program running every seconds to create a moving average for the last 60 seconds (we also gather max and min values).
|
||||||
To keep the script running at boot and stay on we create a systemd service
|
To keep the script running at boot and stay on we create a systemd service.
|
||||||
|
|
||||||
|
Nois sensor address is 0x48.
|
||||||
|
|
||||||
Create the service with `sudo nano /etc/systemd/system/sound_meter.service` and add:
|
Create the service with `sudo nano /etc/systemd/system/sound_meter.service` and add:
|
||||||
```
|
```
|
||||||
@@ -149,43 +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
|
@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:
|
|
||||||
|
|
||||||
```
|
|
||||||
* * * * * /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
|
|
||||||
* * * * * /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
|
# Notes
|
||||||
|
|
||||||
|
|||||||
77
RTC/read.py
Executable file
77
RTC/read.py
Executable file
@@ -0,0 +1,77 @@
|
|||||||
|
'''
|
||||||
|
____ _____ ____
|
||||||
|
| _ \_ _/ ___|
|
||||||
|
| |_) || || |
|
||||||
|
| _ < | || |___
|
||||||
|
|_| \_\|_| \____|
|
||||||
|
|
||||||
|
Script to read time from RTC module
|
||||||
|
I2C connection
|
||||||
|
Address 0x68
|
||||||
|
/usr/bin/python3 /var/www/nebuleair_pro_4g/RTC/read.py
|
||||||
|
'''
|
||||||
|
import smbus2
|
||||||
|
import time
|
||||||
|
import json
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
# DS3231 I2C address
|
||||||
|
DS3231_ADDR = 0x68
|
||||||
|
|
||||||
|
# Registers for DS3231
|
||||||
|
REG_TIME = 0x00
|
||||||
|
|
||||||
|
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)
|
||||||
|
# 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))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
129
RTC/save_to_db.py
Executable file
129
RTC/save_to_db.py
Executable file
@@ -0,0 +1,129 @@
|
|||||||
|
'''
|
||||||
|
____ _____ ____
|
||||||
|
| _ \_ _/ ___|
|
||||||
|
| |_) || || |
|
||||||
|
| _ < | || |___
|
||||||
|
|_| \_\|_| \____|
|
||||||
|
|
||||||
|
Script to read time from RTC module and save it to DB
|
||||||
|
I2C connection
|
||||||
|
Address 0x68
|
||||||
|
/usr/bin/python3 /var/www/nebuleair_pro_4g/RTC/save_to_db.py
|
||||||
|
|
||||||
|
This need to be run as a system service
|
||||||
|
|
||||||
|
--> sudo nano /etc/systemd/system/rtc_save_to_db.service
|
||||||
|
|
||||||
|
⬇️
|
||||||
|
[Unit]
|
||||||
|
Description=RTC Save to DB Script
|
||||||
|
After=network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
ExecStart=/usr/bin/python3 /var/www/nebuleair_pro_4g/RTC/save_to_db.py
|
||||||
|
Restart=always
|
||||||
|
RestartSec=1
|
||||||
|
User=root
|
||||||
|
WorkingDirectory=/var/www/nebuleair_pro_4g
|
||||||
|
StandardOutput=append:/var/www/nebuleair_pro_4g/logs/rtc_save_to_db.log
|
||||||
|
StandardError=append:/var/www/nebuleair_pro_4g/logs/rtc_save_to_db_errors.log
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
⬆️
|
||||||
|
|
||||||
|
|
||||||
|
sudo systemctl daemon-reload
|
||||||
|
sudo systemctl enable rtc_save_to_db.service
|
||||||
|
|
||||||
|
sudo systemctl start rtc_save_to_db.service
|
||||||
|
|
||||||
|
sudo systemctl status rtc_save_to_db.service
|
||||||
|
|
||||||
|
'''
|
||||||
|
import smbus2
|
||||||
|
import time
|
||||||
|
import json
|
||||||
|
from datetime import datetime
|
||||||
|
import sqlite3
|
||||||
|
|
||||||
|
# DS3231 I2C address
|
||||||
|
DS3231_ADDR = 0x68
|
||||||
|
|
||||||
|
# Registers for DS3231
|
||||||
|
REG_TIME = 0x00
|
||||||
|
|
||||||
|
# Connect to (or create if not existent) the database
|
||||||
|
DB_PATH = "/var/www/nebuleair_pro_4g/sqlite/sensors.db"
|
||||||
|
|
||||||
|
def bcd_to_dec(bcd):
|
||||||
|
return (bcd // 16 * 10) + (bcd % 16)
|
||||||
|
|
||||||
|
def read_time(bus):
|
||||||
|
"""Try to read and decode time from the RTC module (DS3231)."""
|
||||||
|
try:
|
||||||
|
data = bus.read_i2c_block_data(DS3231_ADDR, REG_TIME, 7)
|
||||||
|
seconds = bcd_to_dec(data[0] & 0x7F)
|
||||||
|
minutes = bcd_to_dec(data[1])
|
||||||
|
hours = bcd_to_dec(data[2] & 0x3F)
|
||||||
|
day = bcd_to_dec(data[4])
|
||||||
|
month = bcd_to_dec(data[5])
|
||||||
|
year = bcd_to_dec(data[6]) + 2000
|
||||||
|
return datetime(year, month, day, hours, minutes, seconds)
|
||||||
|
except OSError:
|
||||||
|
return None # RTC module not connected
|
||||||
|
|
||||||
|
def main():
|
||||||
|
# Read RTC time
|
||||||
|
bus = smbus2.SMBus(1)
|
||||||
|
|
||||||
|
while True:
|
||||||
|
# Open a new database connection inside the loop to prevent connection loss
|
||||||
|
conn = sqlite3.connect(DB_PATH)
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
# Try to read RTC time
|
||||||
|
rtc_time = read_time(bus)
|
||||||
|
# Get current system time
|
||||||
|
system_time = datetime.now() #local
|
||||||
|
utc_time = datetime.utcnow() #UTC
|
||||||
|
|
||||||
|
# If RTC is not connected, set default message
|
||||||
|
# Calculate time difference (in seconds) if RTC is connected
|
||||||
|
if rtc_time:
|
||||||
|
rtc_time_str = rtc_time.strftime('%Y-%m-%d %H:%M:%S')
|
||||||
|
time_difference = int((utc_time - rtc_time).total_seconds()) # Convert to int
|
||||||
|
else:
|
||||||
|
rtc_time_str = "not connected"
|
||||||
|
time_difference = "N/A" # Not applicable
|
||||||
|
|
||||||
|
# Print both times
|
||||||
|
#print(f"RTC module Time: {rtc_time.strftime('%Y-%m-%d %H:%M:%S')}")
|
||||||
|
#print(f"Sys local Time: {system_time.strftime('%Y-%m-%d %H:%M:%S')}")
|
||||||
|
#print(f"Sys UTC Time: {utc_time.strftime('%Y-%m-%d %H:%M:%S')}")
|
||||||
|
|
||||||
|
# Create JSON output
|
||||||
|
time_data = {
|
||||||
|
"rtc_module_time":rtc_time_str,
|
||||||
|
"system_local_time": system_time.strftime('%Y-%m-%d %H:%M:%S'),
|
||||||
|
"system_utc_time": utc_time.strftime('%Y-%m-%d %H:%M:%S'),
|
||||||
|
"time_difference_seconds": time_difference
|
||||||
|
}
|
||||||
|
|
||||||
|
#print(json.dumps(time_data, indent=4))
|
||||||
|
|
||||||
|
# Save to database
|
||||||
|
try:
|
||||||
|
cursor.execute("UPDATE timestamp_table SET last_updated = ? WHERE id = 1", (rtc_time_str,))
|
||||||
|
conn.commit()
|
||||||
|
#print("Sensor data saved successfully!")
|
||||||
|
|
||||||
|
except sqlite3.Error as e:
|
||||||
|
print(f"Database error: {e}")
|
||||||
|
|
||||||
|
conn.close() # Close connection to avoid database locking issues
|
||||||
|
time.sleep(1) # Wait for 1 second before reading again
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
183
RTC/set_with_NTP.py
Executable file
183
RTC/set_with_NTP.py
Executable file
@@ -0,0 +1,183 @@
|
|||||||
|
#!/usr/bin/python3
|
||||||
|
"""
|
||||||
|
____ _____ ____
|
||||||
|
| _ \_ _/ ___|
|
||||||
|
| |_) || || |
|
||||||
|
| _ < | || |___
|
||||||
|
|_| \_\|_| \____|
|
||||||
|
|
||||||
|
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
|
||||||
|
"""
|
||||||
|
import smbus2
|
||||||
|
import time
|
||||||
|
from datetime import datetime
|
||||||
|
import ntplib
|
||||||
|
import pytz # For timezone handling
|
||||||
|
|
||||||
|
# DS3231 I2C address
|
||||||
|
DS3231_ADDR = 0x68
|
||||||
|
|
||||||
|
# Registers for DS3231
|
||||||
|
REG_TIME = 0x00
|
||||||
|
|
||||||
|
def bcd_to_dec(bcd):
|
||||||
|
"""Convert BCD to decimal."""
|
||||||
|
return (bcd // 16 * 10) + (bcd % 16)
|
||||||
|
|
||||||
|
def dec_to_bcd(dec):
|
||||||
|
"""Convert decimal to BCD."""
|
||||||
|
return (dec // 10 * 16) + (dec % 10)
|
||||||
|
|
||||||
|
def set_time(bus, year, month, day, hour, minute, second):
|
||||||
|
"""Set the RTC time."""
|
||||||
|
# Convert the time to BCD format
|
||||||
|
second_bcd = dec_to_bcd(second)
|
||||||
|
minute_bcd = dec_to_bcd(minute)
|
||||||
|
hour_bcd = dec_to_bcd(hour)
|
||||||
|
day_bcd = dec_to_bcd(day)
|
||||||
|
month_bcd = dec_to_bcd(month)
|
||||||
|
year_bcd = dec_to_bcd(year - 2000) # DS3231 uses year from 2000
|
||||||
|
|
||||||
|
# Write time to DS3231
|
||||||
|
bus.write_i2c_block_data(DS3231_ADDR, REG_TIME, [
|
||||||
|
second_bcd,
|
||||||
|
minute_bcd,
|
||||||
|
hour_bcd,
|
||||||
|
0x01, # Day of the week (1=Monday, etc.)
|
||||||
|
day_bcd,
|
||||||
|
month_bcd,
|
||||||
|
year_bcd
|
||||||
|
])
|
||||||
|
|
||||||
|
def read_time(bus):
|
||||||
|
"""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()
|
||||||
|
# Try multiple NTP servers in case one fails
|
||||||
|
servers = ['pool.ntp.org', 'time.google.com', 'time.windows.com', 'time.apple.com']
|
||||||
|
|
||||||
|
for server in servers:
|
||||||
|
try:
|
||||||
|
print(f"Trying NTP server: {server}")
|
||||||
|
response = ntp_client.request(server, timeout=2)
|
||||||
|
utc_time = datetime.utcfromtimestamp(response.tx_time)
|
||||||
|
print(f"Successfully got time from {server}")
|
||||||
|
return utc_time
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Failed to get time from {server}: {e}")
|
||||||
|
|
||||||
|
# If all servers fail, raise exception
|
||||||
|
raise Exception("All NTP servers failed")
|
||||||
|
|
||||||
|
def main():
|
||||||
|
try:
|
||||||
|
bus = smbus2.SMBus(1)
|
||||||
|
|
||||||
|
# Test if RTC is accessible
|
||||||
|
try:
|
||||||
|
bus.read_byte(DS3231_ADDR)
|
||||||
|
print("RTC module is accessible")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error accessing RTC module: {e}")
|
||||||
|
print("Please check connections and I2C configuration")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Get the current time from the RTC
|
||||||
|
try:
|
||||||
|
year, month, day, hours, minutes, seconds = read_time(bus)
|
||||||
|
# Create datetime object with validation to handle invalid dates
|
||||||
|
rtc_time = datetime(year, month, day, hours, minutes, seconds)
|
||||||
|
print(f"Actual RTC Time : {rtc_time.strftime('%Y-%m-%d %H:%M:%S')}")
|
||||||
|
except ValueError as e:
|
||||||
|
print(f"Invalid date/time read from RTC: {e}")
|
||||||
|
print("Will proceed with setting RTC from internet time")
|
||||||
|
rtc_time = None
|
||||||
|
|
||||||
|
# Get current UTC time from an NTP server
|
||||||
|
try:
|
||||||
|
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"Unexpected error: {e}")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
53
RTC/set_with_browserTime.py
Executable file
53
RTC/set_with_browserTime.py
Executable file
@@ -0,0 +1,53 @@
|
|||||||
|
"""
|
||||||
|
____ _____ ____
|
||||||
|
| _ \_ _/ ___|
|
||||||
|
| |_) || || |
|
||||||
|
| _ < | || |___
|
||||||
|
|_| \_\|_| \____|
|
||||||
|
|
||||||
|
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'
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
from datetime import datetime
|
||||||
|
import smbus2
|
||||||
|
|
||||||
|
# DS3231 I2C address
|
||||||
|
DS3231_ADDR = 0x68
|
||||||
|
REG_TIME = 0x00
|
||||||
|
|
||||||
|
def dec_to_bcd(dec):
|
||||||
|
"""Convert decimal to BCD."""
|
||||||
|
return (dec // 10 * 16) + (dec % 10)
|
||||||
|
|
||||||
|
def set_rtc(bus, year, month, day, hour, minute, second):
|
||||||
|
"""Set the RTC time."""
|
||||||
|
second_bcd = dec_to_bcd(second)
|
||||||
|
minute_bcd = dec_to_bcd(minute)
|
||||||
|
hour_bcd = dec_to_bcd(hour)
|
||||||
|
day_bcd = dec_to_bcd(day)
|
||||||
|
month_bcd = dec_to_bcd(month)
|
||||||
|
year_bcd = dec_to_bcd(year - 2000) # RTC stores years since 2000
|
||||||
|
|
||||||
|
bus.write_i2c_block_data(DS3231_ADDR, REG_TIME, [
|
||||||
|
second_bcd, minute_bcd, hour_bcd, 0x01, day_bcd, month_bcd, year_bcd
|
||||||
|
])
|
||||||
|
|
||||||
|
def main():
|
||||||
|
if len(sys.argv) != 2:
|
||||||
|
print("Usage: python3 set_rtc.py 'YYYY-MM-DD HH:MM:SS'")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
rtc_time_str = sys.argv[1]
|
||||||
|
rtc_time = datetime.strptime(rtc_time_str, '%Y-%m-%d %H:%M:%S')
|
||||||
|
|
||||||
|
bus = smbus2.SMBus(1)
|
||||||
|
set_rtc(bus, rtc_time.year, rtc_time.month, rtc_time.day,
|
||||||
|
rtc_time.hour, rtc_time.minute, rtc_time.second)
|
||||||
|
print(f"RTC updated to: {rtc_time}")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
166
SARA/R5/setPDP.py
Normal file
166
SARA/R5/setPDP.py
Normal file
@@ -0,0 +1,166 @@
|
|||||||
|
'''
|
||||||
|
____ _ ____ _
|
||||||
|
/ ___| / \ | _ \ / \
|
||||||
|
\___ \ / _ \ | |_) | / _ \
|
||||||
|
___) / ___ \| _ < / ___ \
|
||||||
|
|____/_/ \_\_| \_\/_/ \_\
|
||||||
|
|
||||||
|
Script to set the PDP context for the SARA R5
|
||||||
|
|
||||||
|
|
||||||
|
/usr/bin/python3 /var/www/nebuleair_pro_4g/SARA/R5/setPDP.py
|
||||||
|
|
||||||
|
'''
|
||||||
|
import serial
|
||||||
|
import time
|
||||||
|
import sys
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
|
||||||
|
#get data from config
|
||||||
|
def load_config(config_file):
|
||||||
|
try:
|
||||||
|
with open(config_file, 'r') as file:
|
||||||
|
config_data = json.load(file)
|
||||||
|
return config_data
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error loading config file: {e}")
|
||||||
|
return {}
|
||||||
|
|
||||||
|
#Fonction pour mettre à jour le JSON de configuration
|
||||||
|
def update_json_key(file_path, key, value):
|
||||||
|
"""
|
||||||
|
Updates a specific key in a JSON file with a new value.
|
||||||
|
|
||||||
|
:param file_path: Path to the JSON file.
|
||||||
|
:param key: The key to update in the JSON file.
|
||||||
|
:param value: The new value to assign to the key.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Load the existing data
|
||||||
|
with open(file_path, "r") as file:
|
||||||
|
data = json.load(file)
|
||||||
|
|
||||||
|
# Check if the key exists in the JSON file
|
||||||
|
if key in data:
|
||||||
|
data[key] = value # Update the key with the new value
|
||||||
|
else:
|
||||||
|
print(f"Key '{key}' not found in the JSON file.")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Write the updated data back to the file
|
||||||
|
with open(file_path, "w") as file:
|
||||||
|
json.dump(data, file, indent=2) # Use indent for pretty printing
|
||||||
|
|
||||||
|
print(f"💾 updating '{key}' to '{value}'.")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error updating the JSON file: {e}")
|
||||||
|
|
||||||
|
# Define the config file path
|
||||||
|
config_file = '/var/www/nebuleair_pro_4g/config.json'
|
||||||
|
# Load the configuration data
|
||||||
|
config = load_config(config_file)
|
||||||
|
baudrate = config.get('SaraR4_baudrate', 115200) #baudrate du sara R4
|
||||||
|
device_id = config.get('deviceID', '').upper() #device ID en maj
|
||||||
|
|
||||||
|
ser_sara = serial.Serial(
|
||||||
|
port='/dev/ttyAMA2',
|
||||||
|
baudrate=baudrate, #115200 ou 9600
|
||||||
|
parity=serial.PARITY_NONE, #PARITY_NONE, PARITY_EVEN or PARITY_ODD
|
||||||
|
stopbits=serial.STOPBITS_ONE,
|
||||||
|
bytesize=serial.EIGHTBITS,
|
||||||
|
timeout = 2
|
||||||
|
)
|
||||||
|
|
||||||
|
def read_complete_response(serial_connection, timeout=2, end_of_response_timeout=2, wait_for_lines=None, debug=True):
|
||||||
|
'''
|
||||||
|
Fonction très importante !!!
|
||||||
|
Reads the complete response from a serial connection and waits for specific lines.
|
||||||
|
'''
|
||||||
|
if wait_for_lines is None:
|
||||||
|
wait_for_lines = [] # Default to an empty list if not provided
|
||||||
|
|
||||||
|
response = bytearray()
|
||||||
|
serial_connection.timeout = timeout
|
||||||
|
end_time = time.time() + end_of_response_timeout
|
||||||
|
start_time = time.time()
|
||||||
|
|
||||||
|
while True:
|
||||||
|
elapsed_time = time.time() - start_time # Time since function start
|
||||||
|
if serial_connection.in_waiting > 0:
|
||||||
|
data = serial_connection.read(serial_connection.in_waiting)
|
||||||
|
response.extend(data)
|
||||||
|
end_time = time.time() + end_of_response_timeout # Reset timeout on new data
|
||||||
|
|
||||||
|
# Decode and check for any target line
|
||||||
|
decoded_response = response.decode('utf-8', errors='replace')
|
||||||
|
for target_line in wait_for_lines:
|
||||||
|
if target_line in decoded_response:
|
||||||
|
if debug:
|
||||||
|
print(f"[DEBUG] 🔎 Found target line: {target_line} (in {elapsed_time:.2f}s)")
|
||||||
|
return decoded_response # Return response immediately if a target line is found
|
||||||
|
elif time.time() > end_time:
|
||||||
|
if debug:
|
||||||
|
print(f"[DEBUG] Timeout reached. No more data received.")
|
||||||
|
break
|
||||||
|
time.sleep(0.1) # Short sleep to prevent busy waiting
|
||||||
|
|
||||||
|
# Final response and debug output
|
||||||
|
total_elapsed_time = time.time() - start_time
|
||||||
|
if debug:
|
||||||
|
print(f"[DEBUG] ⏱️ elapsed time: {total_elapsed_time:.2f}s. ⏱️")
|
||||||
|
# Check if the elapsed time exceeded 10 seconds
|
||||||
|
if total_elapsed_time > 10 and debug:
|
||||||
|
print(f"[ALERT] 🚨 The operation took too long 🚨")
|
||||||
|
print(f'<span style="color: red;font-weight: bold;">[ALERT] ⚠️{total_elapsed_time:.2f}s⚠️</span>')
|
||||||
|
|
||||||
|
return response.decode('utf-8', errors='replace') # Return the full response if no target line is found
|
||||||
|
|
||||||
|
try:
|
||||||
|
print('Start script')
|
||||||
|
|
||||||
|
# 1. Check connection
|
||||||
|
print('➡️Check SARA R5 connexion')
|
||||||
|
command = f'ATI0\r'
|
||||||
|
ser_sara.write(command.encode('utf-8'))
|
||||||
|
response_SARA_1 = read_complete_response(ser_sara, wait_for_lines=["OK"])
|
||||||
|
print(response_SARA_1, end="")
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
# 2. Activate PDP context 1
|
||||||
|
print('➡️Activate PDP context 1')
|
||||||
|
command = f'AT+CGACT=1,1\r'
|
||||||
|
ser_sara.write(command.encode('utf-8'))
|
||||||
|
response_SARA_2 = read_complete_response(ser_sara, wait_for_lines=["OK"])
|
||||||
|
print(response_SARA_2, end="")
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
# 2. Set the PDP type
|
||||||
|
print('➡️Set the PDP type to IPv4 referring to the outputof the +CGDCONT read command')
|
||||||
|
command = f'AT+UPSD=0,0,0\r'
|
||||||
|
ser_sara.write(command.encode('utf-8'))
|
||||||
|
response_SARA_3 = read_complete_response(ser_sara, wait_for_lines=["OK"])
|
||||||
|
print(response_SARA_3, end="")
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
# 2. Profile #0 is mapped on CID=1.
|
||||||
|
print('➡️Profile #0 is mapped on CID=1.')
|
||||||
|
command = f'AT+UPSD=0,100,1\r'
|
||||||
|
ser_sara.write(command.encode('utf-8'))
|
||||||
|
response_SARA_3 = read_complete_response(ser_sara, wait_for_lines=["OK"])
|
||||||
|
print(response_SARA_3, end="")
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
# 2. Set the PDP type
|
||||||
|
print('➡️Activate the PSD profile #0: the IPv4 address is already assigned by the network.')
|
||||||
|
command = f'AT+UPSDA=0,3\r'
|
||||||
|
ser_sara.write(command.encode('utf-8'))
|
||||||
|
response_SARA_3 = read_complete_response(ser_sara, wait_for_lines=["OK","+UUPSDA"])
|
||||||
|
print(response_SARA_3, end="")
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print("An error occurred:", e)
|
||||||
|
traceback.print_exc() # This prints the full traceback
|
||||||
@@ -25,38 +25,35 @@ url = parameter[1] # ex: data.mobileair.fr
|
|||||||
|
|
||||||
profile_id = 3
|
profile_id = 3
|
||||||
|
|
||||||
#get baudrate
|
baudrate = 115200
|
||||||
def load_config(config_file):
|
send_uSpot = False
|
||||||
try:
|
|
||||||
with open(config_file, 'r') as file:
|
|
||||||
config_data = json.load(file)
|
|
||||||
return config_data
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Error loading config file: {e}")
|
|
||||||
return {}
|
|
||||||
|
|
||||||
# Define the config file path
|
def read_complete_response(serial_connection, timeout=2, end_of_response_timeout=2, wait_for_line=None):
|
||||||
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):
|
|
||||||
response = bytearray()
|
response = bytearray()
|
||||||
serial_connection.timeout = timeout
|
serial_connection.timeout = timeout
|
||||||
end_time = time.time() + end_of_response_timeout
|
end_time = time.time() + end_of_response_timeout
|
||||||
|
start_time = time.time()
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
|
elapsed_time = time.time() - start_time # Time since function start
|
||||||
if serial_connection.in_waiting > 0:
|
if serial_connection.in_waiting > 0:
|
||||||
data = serial_connection.read(serial_connection.in_waiting)
|
data = serial_connection.read(serial_connection.in_waiting)
|
||||||
response.extend(data)
|
response.extend(data)
|
||||||
end_time = time.time() + end_of_response_timeout # Reset timeout on new 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:
|
elif time.time() > end_time:
|
||||||
|
print(f"[DEBUG] Timeout reached. No more data received.")
|
||||||
break
|
break
|
||||||
time.sleep(0.1) # Short sleep to prevent busy waiting
|
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')
|
return response.decode('utf-8', errors='replace')
|
||||||
|
|
||||||
ser_sara = serial.Serial(
|
ser_sara = serial.Serial(
|
||||||
@@ -97,7 +94,8 @@ try:
|
|||||||
print("Trigger POST REQUEST")
|
print("Trigger POST REQUEST")
|
||||||
command = f'AT+UHTTPC={profile_id},1,"/pro_4G/test.php","http.resp"\r'
|
command = f'AT+UHTTPC={profile_id},1,"/pro_4G/test.php","http.resp"\r'
|
||||||
ser_sara.write((command + '\r').encode('utf-8'))
|
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)
|
print(response_SARA_6)
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
|
|
||||||
|
|||||||
@@ -26,23 +26,8 @@ url = parameter[1] # ex: data.mobileair.fr
|
|||||||
|
|
||||||
profile_id = 3
|
profile_id = 3
|
||||||
|
|
||||||
#get baudrate
|
baudrate = 115200
|
||||||
def load_config(config_file):
|
send_uSpot = False
|
||||||
try:
|
|
||||||
with open(config_file, 'r') as file:
|
|
||||||
config_data = json.load(file)
|
|
||||||
return config_data
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Error loading config file: {e}")
|
|
||||||
return {}
|
|
||||||
|
|
||||||
# Define the config file path
|
|
||||||
config_file = '/var/www/nebuleair_pro_4g/config.json'
|
|
||||||
# Load the configuration data
|
|
||||||
config = load_config(config_file)
|
|
||||||
# Access the shared variables
|
|
||||||
baudrate = config.get('SaraR4_baudrate', 115200)
|
|
||||||
send_uSpot = config.get('send_uSpot', False)
|
|
||||||
|
|
||||||
def read_complete_response(serial_connection, timeout=2, end_of_response_timeout=2):
|
def read_complete_response(serial_connection, timeout=2, end_of_response_timeout=2):
|
||||||
response = bytearray()
|
response = bytearray()
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
'''
|
'''
|
||||||
Script to set the URL for a HTTP request and trigger the POST Request
|
Script to set the URL for a HTTP request and trigger the POST Request
|
||||||
Ex:
|
Ex:
|
||||||
/usr/bin/python3 /var/www/nebuleair_pro_4g/SARA/SSL/full_test_HTTPS_POST.py ttyAMA2 api-prod.uspot.probesys.net
|
/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 /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
|
||||||
|
|
||||||
|
|
||||||
First profile id:
|
First profile id:
|
||||||
@@ -22,25 +25,11 @@ parameter = sys.argv[1:] # Exclude the script name
|
|||||||
#print("Parameters received:")
|
#print("Parameters received:")
|
||||||
port='/dev/'+parameter[0] # ex: ttyAMA2
|
port='/dev/'+parameter[0] # ex: ttyAMA2
|
||||||
url = parameter[1] # ex: data.mobileair.fr
|
url = parameter[1] # ex: data.mobileair.fr
|
||||||
|
endpoint = parameter[2]
|
||||||
profile_id = 2
|
profile_id = 2
|
||||||
|
|
||||||
#get baudrate
|
baudrate = 115200
|
||||||
def load_config(config_file):
|
send_uSpot = False
|
||||||
try:
|
|
||||||
with open(config_file, 'r') as file:
|
|
||||||
config_data = json.load(file)
|
|
||||||
return config_data
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Error loading config file: {e}")
|
|
||||||
return {}
|
|
||||||
|
|
||||||
# Define the config file path
|
|
||||||
config_file = '/var/www/nebuleair_pro_4g/config.json'
|
|
||||||
# Load the configuration data
|
|
||||||
config = load_config(config_file)
|
|
||||||
# Access the shared variables
|
|
||||||
baudrate = config.get('SaraR4_baudrate', 115200)
|
|
||||||
send_uSpot = config.get('send_uSpot', False)
|
|
||||||
|
|
||||||
def color_text(text, color):
|
def color_text(text, color):
|
||||||
colors = {
|
colors = {
|
||||||
@@ -96,15 +85,15 @@ ser_sara = serial.Serial(
|
|||||||
try:
|
try:
|
||||||
#step 1: import the certificate
|
#step 1: import the certificate
|
||||||
print("****")
|
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()
|
certificate = cert_file.read()
|
||||||
|
|
||||||
size_of_string = len(certificate)
|
size_of_string = len(certificate)
|
||||||
|
|
||||||
print("\033[0;33m Import certificate\033[0m")
|
print("\033[0;33m Import certificate\033[0m")
|
||||||
# AT+USECMNG=0,<type>,<internal_name>,<data_size>
|
# AT+USECMNG=0,<type>,<internal_name>,<data_size>
|
||||||
# type-> 0 -> trusted root CA
|
# 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'))
|
ser_sara.write((command + '\r').encode('utf-8'))
|
||||||
response_SARA_1 = read_complete_response(ser_sara)
|
response_SARA_1 = read_complete_response(ser_sara)
|
||||||
print(response_SARA_1)
|
print(response_SARA_1)
|
||||||
@@ -129,30 +118,34 @@ try:
|
|||||||
# *******************************
|
# *******************************
|
||||||
# SECURITY PROFILE
|
# SECURITY PROFILE
|
||||||
# AT+USECPRF=<profile_id>[,<op_code>[,<param_val>]]
|
# AT+USECPRF=<profile_id>[,<op_code>[,<param_val>]]
|
||||||
|
security_profile_id = 1
|
||||||
|
|
||||||
|
|
||||||
# op_code: 0 -> certificate validation level
|
# op_code: 0 -> certificate validation level
|
||||||
# param_val : 0 -> Level 0 No validation; 1-> Level 1 Root certificate validation
|
# param_val : 0 -> Level 0 No validation; 1-> Level 1 Root certificate validation
|
||||||
security_profile_id = 0
|
|
||||||
print("\033[0;33mSet the security profile (params)\033[0m")
|
print("\033[0;33mSet the security profile (params)\033[0m")
|
||||||
command = f'AT+USECPRF={security_profile_id},0,0\r'
|
certification_level=0
|
||||||
|
command = f'AT+USECPRF={security_profile_id},0,{certification_level}\r'
|
||||||
ser_sara.write((command + '\r').encode('utf-8'))
|
ser_sara.write((command + '\r').encode('utf-8'))
|
||||||
response_SARA_5b = read_complete_response(ser_sara, wait_for_line="OK")
|
response_SARA_5b = read_complete_response(ser_sara, wait_for_line="OK")
|
||||||
print(response_SARA_5b)
|
print(response_SARA_5b)
|
||||||
time.sleep(0.5)
|
time.sleep(0.5)
|
||||||
|
|
||||||
# op_code: 1 -> minimum SSL/TLS version
|
# op_code: 1 -> minimum SSL/TLS version
|
||||||
# param_val : 0 -> any; server can use any version for the connection; 1-> LSv1.0;
|
# 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")
|
print("\033[0;33mSet the security profile (params)\033[0m")
|
||||||
command = f'AT+USECPRF={security_profile_id},1,0\r'
|
minimum_SSL_version = 0
|
||||||
|
command = f'AT+USECPRF={security_profile_id},1,{minimum_SSL_version}\r'
|
||||||
ser_sara.write((command + '\r').encode('utf-8'))
|
ser_sara.write((command + '\r').encode('utf-8'))
|
||||||
response_SARA_5bb = read_complete_response(ser_sara, wait_for_line="OK")
|
response_SARA_5bb = read_complete_response(ser_sara, wait_for_line="OK")
|
||||||
print(response_SARA_5bb)
|
print(response_SARA_5bb)
|
||||||
time.sleep(0.5)
|
time.sleep(0.5)
|
||||||
|
|
||||||
#op_code: 2 -> cipher suite
|
#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")
|
print("\033[0;33mSet cipher \033[0m")
|
||||||
command = f'AT+USECPRF={security_profile_id},2,0\r'
|
cipher_suite = 0
|
||||||
|
command = f'AT+USECPRF={security_profile_id},2,{cipher_suite}\r'
|
||||||
ser_sara.write((command + '\r').encode('utf-8'))
|
ser_sara.write((command + '\r').encode('utf-8'))
|
||||||
response_SARA_5cc = read_complete_response(ser_sara, wait_for_line="OK")
|
response_SARA_5cc = read_complete_response(ser_sara, wait_for_line="OK")
|
||||||
print(response_SARA_5cc)
|
print(response_SARA_5cc)
|
||||||
@@ -160,12 +153,21 @@ try:
|
|||||||
|
|
||||||
# op_code: 3 -> trusted root certificate internal name
|
# op_code: 3 -> trusted root certificate internal name
|
||||||
print("\033[0;33mSet the security profile (choose cert)\033[0m")
|
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'))
|
ser_sara.write((command + '\r').encode('utf-8'))
|
||||||
response_SARA_5c = read_complete_response(ser_sara, wait_for_line="OK")
|
response_SARA_5c = read_complete_response(ser_sara, wait_for_line="OK")
|
||||||
print(response_SARA_5c)
|
print(response_SARA_5c)
|
||||||
time.sleep(0.5)
|
time.sleep(0.5)
|
||||||
|
|
||||||
|
# op_code: 10 -> SNI (server name indication)
|
||||||
|
print("\033[0;33mSet the SNI\033[0m")
|
||||||
|
command = f'AT+USECPRF={security_profile_id},10,"{url}"\r'
|
||||||
|
ser_sara.write((command + '\r').encode('utf-8'))
|
||||||
|
response_SARA_5cf = read_complete_response(ser_sara, wait_for_line="OK")
|
||||||
|
print(response_SARA_5cf)
|
||||||
|
time.sleep(0.5)
|
||||||
|
|
||||||
|
|
||||||
# *************************
|
# *************************
|
||||||
# *************************
|
# *************************
|
||||||
|
|
||||||
@@ -256,10 +258,12 @@ try:
|
|||||||
#step 4: trigger the request (http_command=1 for GET and http_command=1 for POST)
|
#step 4: trigger the request (http_command=1 for GET and http_command=1 for POST)
|
||||||
print("****")
|
print("****")
|
||||||
print("\033[0;33mPOST REQUEST\033[0m")
|
print("\033[0;33mPOST REQUEST\033[0m")
|
||||||
|
#parameter (POST)
|
||||||
|
command = f'AT+UHTTPC={profile_id},4,"{endpoint}","https.resp","sensordata_json.json",4\r'
|
||||||
#AIRCARTO
|
#AIRCARTO
|
||||||
#command = f'AT+UHTTPC={profile_id},4,"/tests/test.php","https.resp","sensordata_json.json",4\r'
|
#command = f'AT+UHTTPC={profile_id},4,"/tests/test.php","https.resp","sensordata_json.json",4\r'
|
||||||
#uSPOT
|
#uSPOT
|
||||||
command = f'AT+UHTTPC={profile_id},4,"/nebuleair?token=2AFF6dQk68daFZ","https.resp","sensordata_json.json",4\r'
|
#command = f'AT+UHTTPC={profile_id},4,"/nebuleair?token=2AFF6dQk68daFZ","https.resp","sensordata_json.json",4\r'
|
||||||
#AtmoSud
|
#AtmoSud
|
||||||
#command = f'AT+UHTTPC={profile_id},1,"/","https.resp"\r'
|
#command = f'AT+UHTTPC={profile_id},1,"/","https.resp"\r'
|
||||||
#Webhook
|
#Webhook
|
||||||
@@ -271,7 +275,7 @@ try:
|
|||||||
# Wait for the +UUHTTPCR response
|
# Wait for the +UUHTTPCR response
|
||||||
print("Waiting for +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("\033[0;34m")
|
||||||
print(response_SARA_3)
|
print(response_SARA_3)
|
||||||
@@ -307,7 +311,7 @@ try:
|
|||||||
print(response_SARA_8)
|
print(response_SARA_8)
|
||||||
|
|
||||||
# Get error code
|
# 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'
|
command = f'AT+UHTTPER={profile_id}\r'
|
||||||
ser_sara.write(command.encode('utf-8'))
|
ser_sara.write(command.encode('utf-8'))
|
||||||
response_SARA_9 = read_complete_response(ser_sara, wait_for_line="OK")
|
response_SARA_9 = read_complete_response(ser_sara, wait_for_line="OK")
|
||||||
@@ -321,8 +325,10 @@ try:
|
|||||||
3 HTTP Protocol error class
|
3 HTTP Protocol error class
|
||||||
10 Wrong HTTP API USAGE
|
10 Wrong HTTP API USAGE
|
||||||
|
|
||||||
error_code
|
error_code (for error_class 3 or 10)
|
||||||
0 No error
|
0 No error
|
||||||
|
11 Server connection error
|
||||||
|
22 PSD or CSD connection not established
|
||||||
73 Secure socket connect error
|
73 Secure socket connect error
|
||||||
'''
|
'''
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ FONCTIONNE SUR data.nebuleair.fr
|
|||||||
FONCTIONNE SUR uSpot
|
FONCTIONNE SUR uSpot
|
||||||
Ex:
|
Ex:
|
||||||
/usr/bin/python3 /var/www/nebuleair_pro_4g/SARA/SSL/full_test_HTTP_POST.py ttyAMA2 api-prod.uspot.probesys.net
|
/usr/bin/python3 /var/www/nebuleair_pro_4g/SARA/SSL/full_test_HTTP_POST.py ttyAMA2 api-prod.uspot.probesys.net
|
||||||
|
/usr/bin/python3 /var/www/nebuleair_pro_4g/SARA/SSL/full_test_HTTP_POST.py ttyAMA2 aircarto.fr /tests/test.php
|
||||||
|
/usr/bin/python3 /var/www/nebuleair_pro_4g/SARA/SSL/full_test_HTTP_POST.py ttyAMA2 ssl.aircarto.fr /test.php
|
||||||
|
|
||||||
To do: need to add profile id as parameter
|
To do: need to add profile id as parameter
|
||||||
|
|
||||||
@@ -25,25 +27,12 @@ parameter = sys.argv[1:] # Exclude the script name
|
|||||||
#print("Parameters received:")
|
#print("Parameters received:")
|
||||||
port='/dev/'+parameter[0] # ex: ttyAMA2
|
port='/dev/'+parameter[0] # ex: ttyAMA2
|
||||||
url = parameter[1] # ex: data.mobileair.fr
|
url = parameter[1] # ex: data.mobileair.fr
|
||||||
profile_id = 1
|
endpoint = parameter[2]
|
||||||
|
|
||||||
#get baudrate
|
profile_id = 3
|
||||||
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
|
baudrate = 115200
|
||||||
config_file = '/var/www/nebuleair_pro_4g/config.json'
|
send_uSpot = False
|
||||||
# Load the configuration data
|
|
||||||
config = load_config(config_file)
|
|
||||||
# Access the shared variables
|
|
||||||
baudrate = config.get('SaraR4_baudrate', 115200)
|
|
||||||
send_uSpot = config.get('send_uSpot', False)
|
|
||||||
|
|
||||||
def read_complete_response(serial_connection, timeout=2, end_of_response_timeout=2):
|
def read_complete_response(serial_connection, timeout=2, end_of_response_timeout=2):
|
||||||
response = bytearray()
|
response = bytearray()
|
||||||
@@ -82,19 +71,11 @@ try:
|
|||||||
print(response_SARA_5)
|
print(response_SARA_5)
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
|
|
||||||
#step 4: set url to SSL (op_code = 6) (http_secure = 1 for HTTPS)(USECMNG_PROFILE = 2)
|
|
||||||
print("****")
|
|
||||||
print("SET SSL")
|
|
||||||
command = f'AT+UHTTP={profile_id},6,0\r'
|
|
||||||
ser_sara.write(command.encode('utf-8'))
|
|
||||||
response_SARA_5 = read_complete_response(ser_sara)
|
|
||||||
print(response_SARA_5)
|
|
||||||
time.sleep(1)
|
|
||||||
|
|
||||||
#step 4: set PORT (op_code = 5)
|
#step 4: set PORT (op_code = 5)
|
||||||
print("****")
|
print("****")
|
||||||
print("SET PORT")
|
print("SET PORT")
|
||||||
command = f'AT+UHTTP={profile_id},5,81\r'
|
command = f'AT+UHTTP={profile_id},5,80\r'
|
||||||
ser_sara.write((command + '\r').encode('utf-8'))
|
ser_sara.write((command + '\r').encode('utf-8'))
|
||||||
response_SARA_55 = read_complete_response(ser_sara)
|
response_SARA_55 = read_complete_response(ser_sara)
|
||||||
print(response_SARA_55)
|
print(response_SARA_55)
|
||||||
@@ -163,10 +144,14 @@ try:
|
|||||||
#step 4: trigger the request (http_command=1 for GET and http_command=1 for POST)
|
#step 4: trigger the request (http_command=1 for GET and http_command=1 for POST)
|
||||||
print("****")
|
print("****")
|
||||||
print("Trigger POST REQUEST")
|
print("Trigger POST REQUEST")
|
||||||
|
command = f'AT+UHTTPC={profile_id},4,"{endpoint}","http.resp","sensordata_json.json",4\r'
|
||||||
|
#AirCarto
|
||||||
#command = f'AT+UHTTPC={profile_id},1,"/tests/test.php","http.resp"\r'
|
#command = f'AT+UHTTPC={profile_id},1,"/tests/test.php","http.resp"\r'
|
||||||
command = f'AT+UHTTPC={profile_id},4,"/nebuleair?token=2AFF6dQk68daFZ","http.resp","sensordata_json.json",4\r'
|
|
||||||
#command = f'AT+UHTTPC={profile_id},4,"/wifi.php","http.resp","sensordata_json.json",4\r'
|
#command = f'AT+UHTTPC={profile_id},4,"/wifi.php","http.resp","sensordata_json.json",4\r'
|
||||||
#command = f'AT+UHTTPC={profile_id},4,"/pro_4G/data.php?sensor_id=52E7573A","http.resp","sensordata_json.json",4\r'
|
#command = f'AT+UHTTPC={profile_id},4,"/pro_4G/data.php?sensor_id=52E7573A","http.resp","sensordata_json.json",4\r'
|
||||||
|
#AtmoSud
|
||||||
|
#command = f'AT+UHTTPC={profile_id},4,"/nebuleair?token=2AFF6dQk68daFZ","http.resp","sensordata_json.json",4\r'
|
||||||
|
|
||||||
|
|
||||||
ser_sara.write(command.encode('utf-8'))
|
ser_sara.write(command.encode('utf-8'))
|
||||||
|
|
||||||
|
|||||||
@@ -21,23 +21,8 @@ port='/dev/'+parameter[0] # ex: ttyAMA2
|
|||||||
url = parameter[1] # ex: data.mobileair.fr
|
url = parameter[1] # ex: data.mobileair.fr
|
||||||
|
|
||||||
|
|
||||||
#get baudrate
|
baudrate = 115200
|
||||||
def load_config(config_file):
|
send_uSpot = False
|
||||||
try:
|
|
||||||
with open(config_file, 'r') as file:
|
|
||||||
config_data = json.load(file)
|
|
||||||
return config_data
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Error loading config file: {e}")
|
|
||||||
return {}
|
|
||||||
|
|
||||||
# Define the config file path
|
|
||||||
config_file = '/var/www/nebuleair_pro_4g/config.json'
|
|
||||||
# Load the configuration data
|
|
||||||
config = load_config(config_file)
|
|
||||||
# Access the shared variables
|
|
||||||
baudrate = config.get('SaraR4_baudrate', 115200)
|
|
||||||
send_uSpot = config.get('send_uSpot', False)
|
|
||||||
|
|
||||||
def read_complete_response(serial_connection, timeout=2, end_of_response_timeout=2):
|
def read_complete_response(serial_connection, timeout=2, end_of_response_timeout=2):
|
||||||
response = bytearray()
|
response = bytearray()
|
||||||
|
|||||||
@@ -23,24 +23,8 @@ parameter = sys.argv[1:] # Exclude the script name
|
|||||||
port='/dev/'+parameter[0] # ex: ttyAMA2
|
port='/dev/'+parameter[0] # ex: ttyAMA2
|
||||||
url = parameter[1] # ex: data.mobileair.fr
|
url = parameter[1] # ex: data.mobileair.fr
|
||||||
|
|
||||||
|
baudrate = 115200
|
||||||
#get baudrate
|
send_uSpot = False
|
||||||
def load_config(config_file):
|
|
||||||
try:
|
|
||||||
with open(config_file, 'r') as file:
|
|
||||||
config_data = json.load(file)
|
|
||||||
return config_data
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Error loading config file: {e}")
|
|
||||||
return {}
|
|
||||||
|
|
||||||
# Define the config file path
|
|
||||||
config_file = '/var/www/nebuleair_pro_4g/config.json'
|
|
||||||
# Load the configuration data
|
|
||||||
config = load_config(config_file)
|
|
||||||
# Access the shared variables
|
|
||||||
baudrate = config.get('SaraR4_baudrate', 115200)
|
|
||||||
send_uSpot = config.get('send_uSpot', False)
|
|
||||||
|
|
||||||
def read_complete_response(serial_connection, timeout=2, end_of_response_timeout=2):
|
def read_complete_response(serial_connection, timeout=2, end_of_response_timeout=2):
|
||||||
response = bytearray()
|
response = bytearray()
|
||||||
|
|||||||
@@ -14,19 +14,7 @@ parameter = sys.argv[1:] # Exclude the script name
|
|||||||
port = '/dev/' + parameter[0] # e.g., ttyAMA2
|
port = '/dev/' + parameter[0] # e.g., ttyAMA2
|
||||||
timeout = float(parameter[1]) # e.g., 2 seconds
|
timeout = float(parameter[1]) # e.g., 2 seconds
|
||||||
|
|
||||||
def load_config(config_file):
|
baudrate = 115200
|
||||||
try:
|
|
||||||
with open(config_file, 'r') as file:
|
|
||||||
config_data = json.load(file)
|
|
||||||
return config_data
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Error loading config file: {e}")
|
|
||||||
return {}
|
|
||||||
|
|
||||||
# Define the config file path
|
|
||||||
config_file = '/var/www/nebuleair_pro_4g/config.json'
|
|
||||||
config = load_config(config_file)
|
|
||||||
baudrate = config.get('SaraR4_baudrate', 115200)
|
|
||||||
|
|
||||||
def read_complete_response(serial_connection, timeout=2, end_of_response_timeout=2):
|
def read_complete_response(serial_connection, timeout=2, end_of_response_timeout=2):
|
||||||
response = bytearray()
|
response = bytearray()
|
||||||
|
|||||||
325
SARA/SSL/test_22.py
Executable file
325
SARA/SSL/test_22.py
Executable file
@@ -0,0 +1,325 @@
|
|||||||
|
'''
|
||||||
|
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/test_22.py ttyAMA2 api-prod.uspot.probesys.net /nebuleair?token=2AFF6dQk68daFZ
|
||||||
|
/usr/bin/python3 /var/www/nebuleair_pro_4g/SARA/SSL/test_22.py ttyAMA2 webhook.site /6bee2237-099a-4ff4-8452-9f4126df7151
|
||||||
|
/usr/bin/python3 /var/www/nebuleair_pro_4g/SARA/SSL/test_22.py ttyAMA2 aircarto.fr /tests/test.php
|
||||||
|
/usr/bin/python3 /var/www/nebuleair_pro_4g/SARA/SSL/test_22.py ttyAMA2 ssl.aircarto.fr /test.php
|
||||||
|
/usr/bin/python3 /var/www/nebuleair_pro_4g/SARA/SSL/test_22.py ttyAMA2 vps.aircarto.fr /test.php
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
First profile id:
|
||||||
|
AT+UHTTP=0,1,"data.nebuleair.fr"
|
||||||
|
Second profile id:
|
||||||
|
AT+UHTTP=1,1,"api-prod.uspot.probesys.net"
|
||||||
|
Third profile id:
|
||||||
|
AT+UHTTP=2,1,"aircarto.fr"
|
||||||
|
|
||||||
|
'''
|
||||||
|
|
||||||
|
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
|
||||||
|
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)
|
||||||
|
|
||||||
|
def color_text(text, color):
|
||||||
|
colors = {
|
||||||
|
"red": "\033[31m",
|
||||||
|
"green": "\033[32m",
|
||||||
|
"yellow": "\033[33m",
|
||||||
|
"blue": "\033[34m",
|
||||||
|
"magenta": "\033[35m",
|
||||||
|
"cyan": "\033[36m",
|
||||||
|
"white": "\033[37m",
|
||||||
|
}
|
||||||
|
reset = "\033[0m"
|
||||||
|
return f"{colors.get(color, '')}{text}{reset}"
|
||||||
|
|
||||||
|
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(
|
||||||
|
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
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
try:
|
||||||
|
#step 1: import the certificate
|
||||||
|
print("****")
|
||||||
|
with open("/var/www/nebuleair_pro_4g/SARA/SSL/certificate/isrgrootx1.der", "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,"isrgrootx1",{size_of_string}\r'
|
||||||
|
ser_sara.write((command + '\r').encode('utf-8'))
|
||||||
|
response_SARA_1 = read_complete_response(ser_sara)
|
||||||
|
print(response_SARA_1)
|
||||||
|
|
||||||
|
time.sleep(0.5)
|
||||||
|
|
||||||
|
print("\033[0;33mAdd certificate\033[0m")
|
||||||
|
ser_sara.write(certificate)
|
||||||
|
response_SARA_2 = read_complete_response(ser_sara)
|
||||||
|
print(response_SARA_2)
|
||||||
|
|
||||||
|
time.sleep(0.5)
|
||||||
|
|
||||||
|
#check certificate (List all available certificates and private keys)
|
||||||
|
print("\033[0;33mCheck certificate\033[0m")
|
||||||
|
command = f'AT+USECMNG=3\r'
|
||||||
|
ser_sara.write((command + '\r').encode('utf-8'))
|
||||||
|
response_SARA_5b = read_complete_response(ser_sara, wait_for_line="OK")
|
||||||
|
print(response_SARA_5b)
|
||||||
|
time.sleep(0.5)
|
||||||
|
|
||||||
|
# *******************************
|
||||||
|
# SECURITY PROFILE
|
||||||
|
# AT+USECPRF=<profile_id>[,<op_code>[,<param_val>]]
|
||||||
|
security_profile_id = 2
|
||||||
|
|
||||||
|
|
||||||
|
# op_code: 0 -> certificate validation level
|
||||||
|
# param_val : 0 -> Level 0 No validation; 1-> Level 1 Root certificate validation
|
||||||
|
print("\033[0;33mSet the security profile (params)\033[0m")
|
||||||
|
certification_level=1
|
||||||
|
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_line="OK")
|
||||||
|
print(response_SARA_5b)
|
||||||
|
time.sleep(0.5)
|
||||||
|
|
||||||
|
# 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,"isrgrootx1"\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)
|
||||||
|
time.sleep(0.5)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# *************************
|
||||||
|
# *************************
|
||||||
|
|
||||||
|
|
||||||
|
#step 4: set url (op_code = 1)
|
||||||
|
print("\033[0;33mSET URL\033[0m")
|
||||||
|
command = f'AT+UHTTP={profile_id},1,"{url}"\r'
|
||||||
|
ser_sara.write((command + '\r').encode('utf-8'))
|
||||||
|
response_SARA_5 = read_complete_response(ser_sara, wait_for_line="OK")
|
||||||
|
print(response_SARA_5)
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
|
||||||
|
#step 4: set url to SSL (op_code = 6) (http_secure = 1 for HTTPS)(USECMNG_PROFILE = 2)
|
||||||
|
print("\033[0;33mSET SSL\033[0m")
|
||||||
|
http_secure = 1
|
||||||
|
command = f'AT+UHTTP={profile_id},6,{http_secure},{security_profile_id}\r'
|
||||||
|
#command = f'AT+UHTTP={profile_id},6,{http_secure}\r'
|
||||||
|
|
||||||
|
ser_sara.write(command.encode('utf-8'))
|
||||||
|
response_SARA_5 = read_complete_response(ser_sara, wait_for_line="OK")
|
||||||
|
print(response_SARA_5)
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
# Write Data to saraR4
|
||||||
|
payload_json = {
|
||||||
|
"nebuleairid": "C04F8B8D3A08",
|
||||||
|
"software_version": "ModuleAir-V1-012023",
|
||||||
|
"sensordatavalues": [
|
||||||
|
{
|
||||||
|
"value_type": "NPM_P0",
|
||||||
|
"value": "2.3"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value_type": "NPM_P0",
|
||||||
|
"value": "3.30"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value_type": "NPM_P1",
|
||||||
|
"value": "9.05"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value_type": "NPM_P2",
|
||||||
|
"value": "20.60"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value_type": "NPM_N1",
|
||||||
|
"value": "49.00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value_type": "NPM_N10",
|
||||||
|
"value": "49.00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value_type": "NPM_N25",
|
||||||
|
"value": "49.00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value_type": "BME280_temperature",
|
||||||
|
"value": "25.82"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
# 1. Open sensordata_csv.json (with correct data size)
|
||||||
|
payload_string = json.dumps(payload_json) # Convert dict to JSON string
|
||||||
|
size_of_string = len(payload_string)
|
||||||
|
print("\033[0;33mOPEN JSON\033[0m")
|
||||||
|
command = f'AT+UDWNFILE="sensordata_json.json",{size_of_string}\r'
|
||||||
|
ser_sara.write(command.encode('utf-8'))
|
||||||
|
response_SARA_1 = read_complete_response(ser_sara, wait_for_line=">")
|
||||||
|
print(response_SARA_1)
|
||||||
|
time.sleep(0.5)
|
||||||
|
|
||||||
|
#2. Write to shell
|
||||||
|
print("\033[0;33mWrite to Memory\033[0m")
|
||||||
|
ser_sara.write(payload_string.encode())
|
||||||
|
response_SARA_2 = read_complete_response(ser_sara, wait_for_line="OK")
|
||||||
|
print(response_SARA_2)
|
||||||
|
|
||||||
|
#step 4: trigger the request (http_command=1 for GET and http_command=1 for POST)
|
||||||
|
print("****")
|
||||||
|
print("\033[0;33mPOST REQUEST\033[0m")
|
||||||
|
#parameter (POST)
|
||||||
|
command = f'AT+UHTTPC={profile_id},4,"{endpoint}","https.resp","sensordata_json.json",4\r'
|
||||||
|
#AIRCARTO
|
||||||
|
#command = f'AT+UHTTPC={profile_id},4,"/tests/test.php","https.resp","sensordata_json.json",4\r'
|
||||||
|
#uSPOT
|
||||||
|
#command = f'AT+UHTTPC={profile_id},4,"/nebuleair?token=2AFF6dQk68daFZ","https.resp","sensordata_json.json",4\r'
|
||||||
|
#AtmoSud
|
||||||
|
#command = f'AT+UHTTPC={profile_id},1,"/","https.resp"\r'
|
||||||
|
#Webhook
|
||||||
|
#command = f'AT+UHTTPC={profile_id},4,"/6bee2237-099a-4ff4-8452-9f4126df7151","https.resp","sensordata_json.json",4\r'
|
||||||
|
|
||||||
|
|
||||||
|
ser_sara.write(command.encode('utf-8'))
|
||||||
|
|
||||||
|
# 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")
|
||||||
|
|
||||||
|
print("\033[0;34m")
|
||||||
|
print(response_SARA_3)
|
||||||
|
print("\033[0m")
|
||||||
|
|
||||||
|
if "+UUHTTPCR" in response_SARA_3:
|
||||||
|
print("✅ Received +UUHTTPCR response.")
|
||||||
|
lines = response_SARA_3.strip().splitlines()
|
||||||
|
http_response = lines[-1] # "+UUHTTPCR: 0,4,0"
|
||||||
|
parts = http_response.split(',')
|
||||||
|
# code 0 (HTTP failed)
|
||||||
|
if len(parts) == 3 and parts[-1] == '0': # The third value indicates success
|
||||||
|
print("\033[0;31mATTENTION: HTTP operation failed\033[0m")
|
||||||
|
else:
|
||||||
|
print("\033[0;32m HTTP operation successful!!!\033[0m")
|
||||||
|
|
||||||
|
|
||||||
|
#READ REPLY
|
||||||
|
print("****")
|
||||||
|
print("\033[0;33mREPLY SERVER\033[0m")
|
||||||
|
ser_sara.write(b'AT+URDFILE="https.resp"\r')
|
||||||
|
response_SARA_7 = read_complete_response(ser_sara, wait_for_line="OK")
|
||||||
|
print("Reply from server:")
|
||||||
|
|
||||||
|
print("\033[0;32m")
|
||||||
|
print(response_SARA_7)
|
||||||
|
print("\033[0m")
|
||||||
|
|
||||||
|
#5. empty json
|
||||||
|
print("\033[0;33mEmpty Memory\033[0m")
|
||||||
|
ser_sara.write(b'AT+UDELFILE="sensordata_json.json"\r')
|
||||||
|
response_SARA_8 = read_complete_response(ser_sara, wait_for_line="OK")
|
||||||
|
print(response_SARA_8)
|
||||||
|
|
||||||
|
# Get error code
|
||||||
|
print("\033[0;33mEmpty Memory\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")
|
||||||
|
print(response_SARA_9)
|
||||||
|
|
||||||
|
'''
|
||||||
|
+UHTTPER: profile_id,error_class,error_code
|
||||||
|
|
||||||
|
error_class
|
||||||
|
0 OK, no error
|
||||||
|
3 HTTP Protocol error class
|
||||||
|
10 Wrong HTTP API USAGE
|
||||||
|
|
||||||
|
error_code (for error_class 3)
|
||||||
|
0 No error
|
||||||
|
11 Server connection error
|
||||||
|
73 Secure socket connect error
|
||||||
|
'''
|
||||||
|
|
||||||
|
except serial.SerialException as e:
|
||||||
|
print(f"Error: {e}")
|
||||||
|
|
||||||
|
finally:
|
||||||
|
if ser_sara.is_open:
|
||||||
|
ser_sara.close()
|
||||||
|
print("****")
|
||||||
|
#print("Serial closed")
|
||||||
|
|
||||||
133
SARA/SSL/test_33.py
Executable file
133
SARA/SSL/test_33.py
Executable file
@@ -0,0 +1,133 @@
|
|||||||
|
'''
|
||||||
|
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/test_22.py ttyAMA2 api-prod.uspot.probesys.net /nebuleair?token=2AFF6dQk68daFZ
|
||||||
|
/usr/bin/python3 /var/www/nebuleair_pro_4g/SARA/SSL/test_22.py ttyAMA2 webhook.site /6bee2237-099a-4ff4-8452-9f4126df7151
|
||||||
|
/usr/bin/python3 /var/www/nebuleair_pro_4g/SARA/SSL/test_22.py ttyAMA2 aircarto.fr /tests/test.php
|
||||||
|
/usr/bin/python3 /var/www/nebuleair_pro_4g/SARA/SSL/test_22.py ttyAMA2 ssl.aircarto.fr /test.php
|
||||||
|
/usr/bin/python3 /var/www/nebuleair_pro_4g/SARA/SSL/test_22.py ttyAMA2 vps.aircarto.fr /test.php
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
First profile id:
|
||||||
|
AT+UHTTP=0,1,"data.nebuleair.fr"
|
||||||
|
Second profile id:
|
||||||
|
AT+UHTTP=1,1,"api-prod.uspot.probesys.net"
|
||||||
|
Third profile id:
|
||||||
|
AT+UHTTP=2,1,"aircarto.fr"
|
||||||
|
|
||||||
|
'''
|
||||||
|
|
||||||
|
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
|
||||||
|
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)
|
||||||
|
|
||||||
|
def color_text(text, color):
|
||||||
|
colors = {
|
||||||
|
"red": "\033[31m",
|
||||||
|
"green": "\033[32m",
|
||||||
|
"yellow": "\033[33m",
|
||||||
|
"blue": "\033[34m",
|
||||||
|
"magenta": "\033[35m",
|
||||||
|
"cyan": "\033[36m",
|
||||||
|
"white": "\033[37m",
|
||||||
|
}
|
||||||
|
reset = "\033[0m"
|
||||||
|
return f"{colors.get(color, '')}{text}{reset}"
|
||||||
|
|
||||||
|
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(
|
||||||
|
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
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
try:
|
||||||
|
|
||||||
|
|
||||||
|
#check certificate (List all available certificates and private keys)
|
||||||
|
print("\033[0;33mCheck certificate\033[0m")
|
||||||
|
command = f'AT+USECMNG=3\r'
|
||||||
|
ser_sara.write((command + '\r').encode('utf-8'))
|
||||||
|
response_SARA_5b = read_complete_response(ser_sara, wait_for_line="OK")
|
||||||
|
print(response_SARA_5b)
|
||||||
|
time.sleep(0.5)
|
||||||
|
|
||||||
|
#security layer
|
||||||
|
|
||||||
|
#1
|
||||||
|
print("\033[0;33mCheck certificate\033[0m")
|
||||||
|
command = f'AT+USECPRF=<profile_id>,<op_code>\r'
|
||||||
|
ser_sara.write((command + '\r').encode('utf-8'))
|
||||||
|
response_SARA_5b = read_complete_response(ser_sara, wait_for_line="OK")
|
||||||
|
print(response_SARA_5b)
|
||||||
|
time.sleep(0.5)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
except serial.SerialException as e:
|
||||||
|
print(f"Error: {e}")
|
||||||
|
|
||||||
|
finally:
|
||||||
|
if ser_sara.is_open:
|
||||||
|
ser_sara.close()
|
||||||
|
print("****")
|
||||||
|
#print("Serial closed")
|
||||||
|
|
||||||
109
SARA/cellLocate/get_loc.py
Normal file
109
SARA/cellLocate/get_loc.py
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
'''
|
||||||
|
____ _ ____ _
|
||||||
|
/ ___| / \ | _ \ / \
|
||||||
|
\___ \ / _ \ | |_) | / _ \
|
||||||
|
___) / ___ \| _ < / ___ \
|
||||||
|
|____/_/ \_\_| \_\/_/ \_\
|
||||||
|
|
||||||
|
Script to get Location from GSM
|
||||||
|
|
||||||
|
/usr/bin/python3 /var/www/nebuleair_pro_4g/SARA/cellLocate/get_loc.py ttyAMA2 1
|
||||||
|
|
||||||
|
AT+ULOC=
|
||||||
|
<mode>, ->2 -> single shot position
|
||||||
|
<sensor>, ->2 -> use cellulare CellLocate
|
||||||
|
<response_type>, ->0 -> standard
|
||||||
|
<timeout>, ->2 -> seconds
|
||||||
|
<accuracy> ->1 -> in meters
|
||||||
|
[,<num_hypothesis>]
|
||||||
|
|
||||||
|
exemple: AT+ULOC=2,2,0,2,1
|
||||||
|
'''
|
||||||
|
|
||||||
|
import serial
|
||||||
|
import time
|
||||||
|
import sys
|
||||||
|
import json
|
||||||
|
|
||||||
|
parameter = sys.argv[1:] # Exclude the script name
|
||||||
|
port='/dev/'+parameter[0] # ex: ttyAMA2
|
||||||
|
timeout = float(parameter[1]) # ex:2
|
||||||
|
|
||||||
|
|
||||||
|
#get baudrate
|
||||||
|
def load_config(config_file):
|
||||||
|
try:
|
||||||
|
with open(config_file, 'r') as file:
|
||||||
|
config_data = json.load(file)
|
||||||
|
return config_data
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error loading config file: {e}")
|
||||||
|
return {}
|
||||||
|
|
||||||
|
# Define the config file path
|
||||||
|
config_file = '/var/www/nebuleair_pro_4g/config.json'
|
||||||
|
# Load the configuration data
|
||||||
|
config = load_config(config_file)
|
||||||
|
# Access the shared variables
|
||||||
|
baudrate = config.get('SaraR4_baudrate', 115200)
|
||||||
|
|
||||||
|
ser = serial.Serial(
|
||||||
|
port=port, #USB0 or ttyS0
|
||||||
|
baudrate=baudrate, #115200 ou 9600
|
||||||
|
parity=serial.PARITY_NONE, #PARITY_NONE, PARITY_EVEN or PARITY_ODD
|
||||||
|
stopbits=serial.STOPBITS_ONE,
|
||||||
|
bytesize=serial.EIGHTBITS,
|
||||||
|
timeout = timeout
|
||||||
|
)
|
||||||
|
|
||||||
|
def read_complete_response(serial_connection, timeout=2, end_of_response_timeout=2, wait_for_lines=None, debug=True):
|
||||||
|
'''
|
||||||
|
Fonction très importante !!!
|
||||||
|
Reads the complete response from a serial connection and waits for specific lines.
|
||||||
|
'''
|
||||||
|
if wait_for_lines is None:
|
||||||
|
wait_for_lines = [] # Default to an empty list if not provided
|
||||||
|
|
||||||
|
response = bytearray()
|
||||||
|
serial_connection.timeout = timeout
|
||||||
|
end_time = time.time() + end_of_response_timeout
|
||||||
|
start_time = time.time()
|
||||||
|
|
||||||
|
while True:
|
||||||
|
elapsed_time = time.time() - start_time # Time since function start
|
||||||
|
if serial_connection.in_waiting > 0:
|
||||||
|
data = serial_connection.read(serial_connection.in_waiting)
|
||||||
|
response.extend(data)
|
||||||
|
end_time = time.time() + end_of_response_timeout # Reset timeout on new data
|
||||||
|
|
||||||
|
# Decode and check for any target line
|
||||||
|
decoded_response = response.decode('utf-8', errors='replace')
|
||||||
|
for target_line in wait_for_lines:
|
||||||
|
if target_line in decoded_response:
|
||||||
|
if debug:
|
||||||
|
print(f"[DEBUG] 🔎 Found target line: {target_line} (in {elapsed_time:.2f}s)")
|
||||||
|
return decoded_response # Return response immediately if a target line is found
|
||||||
|
elif time.time() > end_time:
|
||||||
|
if debug:
|
||||||
|
print(f"[DEBUG] Timeout reached. No more data received.")
|
||||||
|
break
|
||||||
|
time.sleep(0.1) # Short sleep to prevent busy waiting
|
||||||
|
|
||||||
|
# Final response and debug output
|
||||||
|
total_elapsed_time = time.time() - start_time
|
||||||
|
if debug:
|
||||||
|
print(f"[DEBUG] ⏱️ elapsed time: {total_elapsed_time:.2f}s. ⏱️")
|
||||||
|
# Check if the elapsed time exceeded 10 seconds
|
||||||
|
if total_elapsed_time > 10 and debug:
|
||||||
|
print(f"[ALERT] 🚨 The operation took too long 🚨")
|
||||||
|
print(f'<span style="color: red;font-weight: bold;">[ALERT] ⚠️{total_elapsed_time:.2f}s⚠️</span>')
|
||||||
|
|
||||||
|
return response.decode('utf-8', errors='replace') # Return the full response if no target line is found
|
||||||
|
|
||||||
|
|
||||||
|
#command = f'ATI\r'
|
||||||
|
command = f'AT+ULOC=2,2,0,2,1\r'
|
||||||
|
ser.write((command + '\r').encode('utf-8'))
|
||||||
|
|
||||||
|
response = read_complete_response(ser, wait_for_lines=["+UULOC"])
|
||||||
|
print(response)
|
||||||
103
SARA/cellLocate/server_conf.py
Normal file
103
SARA/cellLocate/server_conf.py
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
'''
|
||||||
|
____ _ ____ _
|
||||||
|
/ ___| / \ | _ \ / \
|
||||||
|
\___ \ / _ \ | |_) | / _ \
|
||||||
|
___) / ___ \| _ < / ___ \
|
||||||
|
|____/_/ \_\_| \_\/_/ \_\
|
||||||
|
|
||||||
|
Script to Configures the network connection to a Multi GNSS Assistance (MGA) server used also per CellLocate
|
||||||
|
|
||||||
|
/usr/bin/python3 /var/www/nebuleair_pro_4g/SARA/cellLocate/server_conf.py ttyAMA2 1
|
||||||
|
|
||||||
|
AT+UGSRV="cell-live1.services.u-blox.com","cell-live2.services.u-blox.com","XkEKfGqVSbmNE1eZfBZm4Q"
|
||||||
|
|
||||||
|
|
||||||
|
'''
|
||||||
|
|
||||||
|
import serial
|
||||||
|
import time
|
||||||
|
import sys
|
||||||
|
import json
|
||||||
|
|
||||||
|
parameter = sys.argv[1:] # Exclude the script name
|
||||||
|
port='/dev/'+parameter[0] # ex: ttyAMA2
|
||||||
|
timeout = float(parameter[1]) # ex:2
|
||||||
|
|
||||||
|
|
||||||
|
#get baudrate
|
||||||
|
def load_config(config_file):
|
||||||
|
try:
|
||||||
|
with open(config_file, 'r') as file:
|
||||||
|
config_data = json.load(file)
|
||||||
|
return config_data
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error loading config file: {e}")
|
||||||
|
return {}
|
||||||
|
|
||||||
|
# Define the config file path
|
||||||
|
config_file = '/var/www/nebuleair_pro_4g/config.json'
|
||||||
|
# Load the configuration data
|
||||||
|
config = load_config(config_file)
|
||||||
|
# Access the shared variables
|
||||||
|
baudrate = config.get('SaraR4_baudrate', 115200)
|
||||||
|
|
||||||
|
ser = serial.Serial(
|
||||||
|
port=port, #USB0 or ttyS0
|
||||||
|
baudrate=baudrate, #115200 ou 9600
|
||||||
|
parity=serial.PARITY_NONE, #PARITY_NONE, PARITY_EVEN or PARITY_ODD
|
||||||
|
stopbits=serial.STOPBITS_ONE,
|
||||||
|
bytesize=serial.EIGHTBITS,
|
||||||
|
timeout = timeout
|
||||||
|
)
|
||||||
|
|
||||||
|
def read_complete_response(serial_connection, timeout=2, end_of_response_timeout=2, wait_for_lines=None, debug=True):
|
||||||
|
'''
|
||||||
|
Fonction très importante !!!
|
||||||
|
Reads the complete response from a serial connection and waits for specific lines.
|
||||||
|
'''
|
||||||
|
if wait_for_lines is None:
|
||||||
|
wait_for_lines = [] # Default to an empty list if not provided
|
||||||
|
|
||||||
|
response = bytearray()
|
||||||
|
serial_connection.timeout = timeout
|
||||||
|
end_time = time.time() + end_of_response_timeout
|
||||||
|
start_time = time.time()
|
||||||
|
|
||||||
|
while True:
|
||||||
|
elapsed_time = time.time() - start_time # Time since function start
|
||||||
|
if serial_connection.in_waiting > 0:
|
||||||
|
data = serial_connection.read(serial_connection.in_waiting)
|
||||||
|
response.extend(data)
|
||||||
|
end_time = time.time() + end_of_response_timeout # Reset timeout on new data
|
||||||
|
|
||||||
|
# Decode and check for any target line
|
||||||
|
decoded_response = response.decode('utf-8', errors='replace')
|
||||||
|
for target_line in wait_for_lines:
|
||||||
|
if target_line in decoded_response:
|
||||||
|
if debug:
|
||||||
|
print(f"[DEBUG] 🔎 Found target line: {target_line} (in {elapsed_time:.2f}s)")
|
||||||
|
return decoded_response # Return response immediately if a target line is found
|
||||||
|
elif time.time() > end_time:
|
||||||
|
if debug:
|
||||||
|
print(f"[DEBUG] Timeout reached. No more data received.")
|
||||||
|
break
|
||||||
|
time.sleep(0.1) # Short sleep to prevent busy waiting
|
||||||
|
|
||||||
|
# Final response and debug output
|
||||||
|
total_elapsed_time = time.time() - start_time
|
||||||
|
if debug:
|
||||||
|
print(f"[DEBUG] ⏱️ elapsed time: {total_elapsed_time:.2f}s. ⏱️")
|
||||||
|
# Check if the elapsed time exceeded 10 seconds
|
||||||
|
if total_elapsed_time > 10 and debug:
|
||||||
|
print(f"[ALERT] 🚨 The operation took too long 🚨")
|
||||||
|
print(f'<span style="color: red;font-weight: bold;">[ALERT] ⚠️{total_elapsed_time:.2f}s⚠️</span>')
|
||||||
|
|
||||||
|
return response.decode('utf-8', errors='replace') # Return the full response if no target line is found
|
||||||
|
|
||||||
|
|
||||||
|
#command = f'ATI\r'
|
||||||
|
command = f'AT+UGSRV="cell-live1.services.u-blox.com","cell-live2.services.u-blox.com","XkEKfGqVSbmNE1eZfBZm4Q"\r'
|
||||||
|
ser.write((command + '\r').encode('utf-8'))
|
||||||
|
|
||||||
|
response = read_complete_response(ser, wait_for_lines=["+UULOC"])
|
||||||
|
print(response)
|
||||||
27
SARA/check_running.py
Executable file
27
SARA/check_running.py
Executable file
@@ -0,0 +1,27 @@
|
|||||||
|
'''
|
||||||
|
Check if the main loop is running
|
||||||
|
/usr/bin/python3 /var/www/nebuleair_pro_4g/tests/check_running.py
|
||||||
|
'''
|
||||||
|
import psutil
|
||||||
|
import json
|
||||||
|
|
||||||
|
def is_script_running(script_name):
|
||||||
|
"""Check if a given Python script is currently running."""
|
||||||
|
for process in psutil.process_iter(['pid', 'cmdline']):
|
||||||
|
if process.info['cmdline'] and script_name in " ".join(process.info['cmdline']):
|
||||||
|
return True # Script is running
|
||||||
|
return False # Script is not running
|
||||||
|
|
||||||
|
script_to_check = "/var/www/nebuleair_pro_4g/loop/SARA_send_data_v2.py"
|
||||||
|
|
||||||
|
# Determine script status
|
||||||
|
is_running = is_script_running(script_to_check)
|
||||||
|
|
||||||
|
# Create JSON response
|
||||||
|
response = {
|
||||||
|
"message": "The script is still running.❌❌❌" if is_running else "The script is NOT running.✅✅✅",
|
||||||
|
"running": is_running
|
||||||
|
}
|
||||||
|
|
||||||
|
# Print JSON output
|
||||||
|
print(json.dumps(response, indent=4)) # Pretty print for readability
|
||||||
398
SARA/reboot/start.py
Normal file
398
SARA/reboot/start.py
Normal file
@@ -0,0 +1,398 @@
|
|||||||
|
'''
|
||||||
|
____ _ ____ _
|
||||||
|
/ ___| / \ | _ \ / \
|
||||||
|
\___ \ / _ \ | |_) | / _ \
|
||||||
|
___) / ___ \| _ < / ___ \
|
||||||
|
|____/_/ \_\_| \_\/_/ \_\
|
||||||
|
|
||||||
|
Script that starts at the boot of the RPI (with cron)
|
||||||
|
|
||||||
|
@reboot sleep 30 && /usr/bin/python3 /var/www/nebuleair_pro_4g/SARA/reboot/start.py >> /var/www/nebuleair_pro_4g/logs/app.log 2>&1
|
||||||
|
|
||||||
|
/usr/bin/python3 /var/www/nebuleair_pro_4g/SARA/reboot/start.py
|
||||||
|
|
||||||
|
'''
|
||||||
|
import serial
|
||||||
|
import RPi.GPIO as GPIO
|
||||||
|
import time
|
||||||
|
import sys
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
import sqlite3
|
||||||
|
|
||||||
|
#GPIO
|
||||||
|
SARA_power_GPIO = 16
|
||||||
|
SARA_ON_GPIO = 20
|
||||||
|
|
||||||
|
GPIO.setmode(GPIO.BCM) # Use BCM numbering
|
||||||
|
GPIO.setup(SARA_power_GPIO, GPIO.OUT) # Set GPIO17 as an output
|
||||||
|
|
||||||
|
# database connection
|
||||||
|
conn = sqlite3.connect("/var/www/nebuleair_pro_4g/sqlite/sensors.db")
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
#get config data from SQLite table
|
||||||
|
def load_config_sqlite():
|
||||||
|
"""
|
||||||
|
Load configuration data from SQLite config table
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: Configuration data with proper type conversion
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
|
||||||
|
# Query the config table
|
||||||
|
cursor.execute("SELECT key, value, type FROM config_table")
|
||||||
|
rows = cursor.fetchall()
|
||||||
|
|
||||||
|
# Create config dictionary
|
||||||
|
config_data = {}
|
||||||
|
for key, value, type_name in rows:
|
||||||
|
# Convert value based on its type
|
||||||
|
if type_name == 'bool':
|
||||||
|
config_data[key] = value == '1' or value == 'true'
|
||||||
|
elif type_name == 'int':
|
||||||
|
config_data[key] = int(value)
|
||||||
|
elif type_name == 'float':
|
||||||
|
config_data[key] = float(value)
|
||||||
|
else:
|
||||||
|
config_data[key] = value
|
||||||
|
|
||||||
|
return config_data
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error loading config from SQLite: {e}")
|
||||||
|
return {}
|
||||||
|
|
||||||
|
def update_sqlite_config(key, value):
|
||||||
|
"""
|
||||||
|
Updates a specific key in the SQLite config_table with a new value.
|
||||||
|
|
||||||
|
:param key: The key to update in the config_table.
|
||||||
|
:param value: The new value to assign to the key.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
|
||||||
|
# Check if the key exists and get its type
|
||||||
|
cursor.execute("SELECT type FROM config_table WHERE key = ?", (key,))
|
||||||
|
result = cursor.fetchone()
|
||||||
|
|
||||||
|
if result is None:
|
||||||
|
print(f"Key '{key}' not found in the config_table.")
|
||||||
|
conn.close()
|
||||||
|
return
|
||||||
|
|
||||||
|
# Get the type of the value from the database
|
||||||
|
value_type = result[0]
|
||||||
|
|
||||||
|
# Convert the value to the appropriate string representation based on its type
|
||||||
|
if value_type == 'bool':
|
||||||
|
# Convert Python boolean or string 'true'/'false' to '1'/'0'
|
||||||
|
if isinstance(value, bool):
|
||||||
|
str_value = '1' if value else '0'
|
||||||
|
else:
|
||||||
|
str_value = '1' if str(value).lower() in ('true', '1', 'yes', 'y') else '0'
|
||||||
|
elif value_type == 'int':
|
||||||
|
str_value = str(int(value))
|
||||||
|
elif value_type == 'float':
|
||||||
|
str_value = str(float(value))
|
||||||
|
else:
|
||||||
|
str_value = str(value)
|
||||||
|
|
||||||
|
# Update the value in the database
|
||||||
|
cursor.execute("UPDATE config_table SET value = ? WHERE key = ?", (str_value, key))
|
||||||
|
|
||||||
|
# Commit the changes and close the connection
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
print(f"💾 Updated '{key}' to '{value}' in database.")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error updating the SQLite database: {e}")
|
||||||
|
|
||||||
|
#Load config
|
||||||
|
config = load_config_sqlite()
|
||||||
|
#config
|
||||||
|
baudrate = config.get('SaraR4_baudrate', 115200) #baudrate du sara R4
|
||||||
|
device_id = config.get('deviceID', '').upper() #device ID en maj
|
||||||
|
|
||||||
|
sara_r5_DPD_setup = False
|
||||||
|
|
||||||
|
ser_sara = serial.Serial(
|
||||||
|
port='/dev/ttyAMA2',
|
||||||
|
baudrate=baudrate, #115200 ou 9600
|
||||||
|
parity=serial.PARITY_NONE, #PARITY_NONE, PARITY_EVEN or PARITY_ODD
|
||||||
|
stopbits=serial.STOPBITS_ONE,
|
||||||
|
bytesize=serial.EIGHTBITS,
|
||||||
|
timeout = 2
|
||||||
|
)
|
||||||
|
|
||||||
|
def read_complete_response(serial_connection, timeout=2, end_of_response_timeout=2, wait_for_lines=None, debug=True):
|
||||||
|
'''
|
||||||
|
Fonction très importante !!!
|
||||||
|
Reads the complete response from a serial connection and waits for specific lines.
|
||||||
|
'''
|
||||||
|
if wait_for_lines is None:
|
||||||
|
wait_for_lines = [] # Default to an empty list if not provided
|
||||||
|
|
||||||
|
response = bytearray()
|
||||||
|
serial_connection.timeout = timeout
|
||||||
|
end_time = time.time() + end_of_response_timeout
|
||||||
|
start_time = time.time()
|
||||||
|
|
||||||
|
while True:
|
||||||
|
elapsed_time = time.time() - start_time # Time since function start
|
||||||
|
if serial_connection.in_waiting > 0:
|
||||||
|
data = serial_connection.read(serial_connection.in_waiting)
|
||||||
|
response.extend(data)
|
||||||
|
end_time = time.time() + end_of_response_timeout # Reset timeout on new data
|
||||||
|
|
||||||
|
# Decode and check for any target line
|
||||||
|
decoded_response = response.decode('utf-8', errors='replace')
|
||||||
|
for target_line in wait_for_lines:
|
||||||
|
if target_line in decoded_response:
|
||||||
|
if debug:
|
||||||
|
print(f"[DEBUG] 🔎 Found target line: {target_line} (in {elapsed_time:.2f}s)")
|
||||||
|
return decoded_response # Return response immediately if a target line is found
|
||||||
|
elif time.time() > end_time:
|
||||||
|
if debug:
|
||||||
|
print(f"[DEBUG] Timeout reached. No more data received.")
|
||||||
|
break
|
||||||
|
time.sleep(0.1) # Short sleep to prevent busy waiting
|
||||||
|
|
||||||
|
# Final response and debug output
|
||||||
|
total_elapsed_time = time.time() - start_time
|
||||||
|
if debug:
|
||||||
|
print(f"[DEBUG] ⏱️ elapsed time: {total_elapsed_time:.2f}s. ⏱️")
|
||||||
|
# Check if the elapsed time exceeded 10 seconds
|
||||||
|
if total_elapsed_time > 10 and debug:
|
||||||
|
print(f"[ALERT] 🚨 The operation took too long 🚨")
|
||||||
|
print(f'<span style="color: red;font-weight: bold;">[ALERT] ⚠️{total_elapsed_time:.2f}s⚠️</span>')
|
||||||
|
|
||||||
|
return response.decode('utf-8', errors='replace') # Return the full response if no target line is found
|
||||||
|
|
||||||
|
try:
|
||||||
|
print('<h3>Start reboot python script</h3>')
|
||||||
|
|
||||||
|
#First we need to power on the module (if connected to mosfet via gpio16)
|
||||||
|
GPIO.output(SARA_power_GPIO, GPIO.HIGH)
|
||||||
|
time.sleep(5)
|
||||||
|
|
||||||
|
#check modem status
|
||||||
|
#Attention:
|
||||||
|
# SARA R4 response: Manufacturer: u-blox Model: SARA-R410M-02B
|
||||||
|
# SArA R5 response: SARA-R500S-01B-00
|
||||||
|
print("⚙️Check SARA Status")
|
||||||
|
command = f'ATI\r'
|
||||||
|
ser_sara.write(command.encode('utf-8'))
|
||||||
|
response_SARA_ATI = read_complete_response(ser_sara, wait_for_lines=["IMEI"])
|
||||||
|
print(response_SARA_ATI)
|
||||||
|
|
||||||
|
# Check for SARA model with more robust regex
|
||||||
|
model = "Unknown"
|
||||||
|
if "SARA-R410M" in response_SARA_ATI:
|
||||||
|
model = "SARA-R410M"
|
||||||
|
print("📱 Detected SARA R4 modem")
|
||||||
|
elif "SARA-R500" in response_SARA_ATI:
|
||||||
|
model = "SARA-R500"
|
||||||
|
print("📱 Detected SARA R5 modem")
|
||||||
|
sara_r5_DPD_setup = True
|
||||||
|
else:
|
||||||
|
# Fallback to regex match if direct string match fails
|
||||||
|
match = re.search(r"Model:\s*([A-Za-z0-9\-]+)", response_SARA_ATI)
|
||||||
|
if match:
|
||||||
|
model = match.group(1).strip()
|
||||||
|
else:
|
||||||
|
model = "Unknown"
|
||||||
|
print("⚠️ Could not identify modem model")
|
||||||
|
|
||||||
|
print(f"🔍 Model: {model}")
|
||||||
|
update_sqlite_config("modem_version", model)
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
'''
|
||||||
|
AIRCARTO
|
||||||
|
'''
|
||||||
|
# 1. Set AIRCARTO URL (profile id = 0)
|
||||||
|
print('➡️Set aircarto URL')
|
||||||
|
aircarto_profile_id = 0
|
||||||
|
aircarto_url="data.nebuleair.fr"
|
||||||
|
command = f'AT+UHTTP={aircarto_profile_id},1,"{aircarto_url}"\r'
|
||||||
|
ser_sara.write(command.encode('utf-8'))
|
||||||
|
response_SARA_1 = read_complete_response(ser_sara, wait_for_lines=["OK"])
|
||||||
|
print(response_SARA_1)
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
'''
|
||||||
|
uSpot
|
||||||
|
'''
|
||||||
|
print("➡️➡️Set uSpot URL with SSL")
|
||||||
|
|
||||||
|
security_profile_id = 1
|
||||||
|
uSpot_profile_id = 1
|
||||||
|
uSpot_url="api-prod.uspot.probesys.net"
|
||||||
|
|
||||||
|
|
||||||
|
#step 1: import the certificate
|
||||||
|
print("➡️ import certificate")
|
||||||
|
certificate_name = "e6"
|
||||||
|
with open("/var/www/nebuleair_pro_4g/SARA/SSL/certificate/e6.pem", "rb") as cert_file:
|
||||||
|
certificate = cert_file.read()
|
||||||
|
size_of_string = len(certificate)
|
||||||
|
|
||||||
|
# AT+USECMNG=0,<type>,<internal_name>,<data_size>
|
||||||
|
# type-> 0 -> trusted root CA
|
||||||
|
command = f'AT+USECMNG=0,0,"{certificate_name}",{size_of_string}\r'
|
||||||
|
ser_sara.write((command + '\r').encode('utf-8'))
|
||||||
|
response_SARA_1 = read_complete_response(ser_sara)
|
||||||
|
print(response_SARA_1)
|
||||||
|
|
||||||
|
time.sleep(0.5)
|
||||||
|
|
||||||
|
print("➡️ add certificate")
|
||||||
|
ser_sara.write(certificate)
|
||||||
|
response_SARA_2 = read_complete_response(ser_sara)
|
||||||
|
print(response_SARA_2)
|
||||||
|
|
||||||
|
time.sleep(0.5)
|
||||||
|
|
||||||
|
# op_code: 0 -> certificate validation level
|
||||||
|
# param_val : 0 -> Level 0 No validation; 1-> Level 1 Root certificate validation
|
||||||
|
print("➡️Set the security profile (params)")
|
||||||
|
certification_level=0
|
||||||
|
command = f'AT+USECPRF={security_profile_id},0,{certification_level}\r'
|
||||||
|
ser_sara.write((command + '\r').encode('utf-8'))
|
||||||
|
response_SARA_5b = read_complete_response(ser_sara, wait_for_lines=["OK"])
|
||||||
|
print(response_SARA_5b)
|
||||||
|
time.sleep(0.5)
|
||||||
|
|
||||||
|
# op_code: 1 -> minimum SSL/TLS version
|
||||||
|
# param_val : 0 -> any; server can use any version for the connection; 1-> LSv1.0; 2->TLSv1.1; 3->TLSv1.2;
|
||||||
|
print("➡️Set the security profile (params)")
|
||||||
|
minimum_SSL_version = 0
|
||||||
|
command = f'AT+USECPRF={security_profile_id},1,{minimum_SSL_version}\r'
|
||||||
|
ser_sara.write((command + '\r').encode('utf-8'))
|
||||||
|
response_SARA_5bb = read_complete_response(ser_sara, wait_for_lines=["OK"])
|
||||||
|
print(response_SARA_5bb)
|
||||||
|
time.sleep(0.5)
|
||||||
|
|
||||||
|
#op_code: 2 -> legacy cipher suite selection
|
||||||
|
# 0 (factory-programmed value): a list of default cipher suites is proposed at the beginning of handshake process, and a cipher suite will be negotiated among the cipher suites proposed in the list.
|
||||||
|
print("➡️Set cipher")
|
||||||
|
cipher_suite = 0
|
||||||
|
command = f'AT+USECPRF={security_profile_id},2,{cipher_suite}\r'
|
||||||
|
ser_sara.write((command + '\r').encode('utf-8'))
|
||||||
|
response_SARA_5cc = read_complete_response(ser_sara, wait_for_lines=["OK"])
|
||||||
|
print(response_SARA_5cc)
|
||||||
|
time.sleep(0.5)
|
||||||
|
|
||||||
|
# op_code: 3 -> trusted root certificate internal name
|
||||||
|
print("➡️Set the security profile (choose cert)")
|
||||||
|
command = f'AT+USECPRF={security_profile_id},3,"{certificate_name}"\r'
|
||||||
|
ser_sara.write((command + '\r').encode('utf-8'))
|
||||||
|
response_SARA_5c = read_complete_response(ser_sara, wait_for_lines=["OK"])
|
||||||
|
print(response_SARA_5c)
|
||||||
|
time.sleep(0.5)
|
||||||
|
|
||||||
|
# op_code: 10 -> SNI (server name indication)
|
||||||
|
print("➡️Set the SNI")
|
||||||
|
command = f'AT+USECPRF={security_profile_id},10,"{uSpot_url}"\r'
|
||||||
|
ser_sara.write((command + '\r').encode('utf-8'))
|
||||||
|
response_SARA_5cf = read_complete_response(ser_sara, wait_for_lines=["OK"])
|
||||||
|
print(response_SARA_5cf)
|
||||||
|
time.sleep(0.5)
|
||||||
|
|
||||||
|
#step 4: set url (op_code = 1)
|
||||||
|
print("➡️SET URL")
|
||||||
|
command = f'AT+UHTTP={uSpot_profile_id},1,"{uSpot_url}"\r'
|
||||||
|
ser_sara.write((command + '\r').encode('utf-8'))
|
||||||
|
response_SARA_5 = read_complete_response(ser_sara, wait_for_lines=["OK"])
|
||||||
|
print(response_SARA_5)
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
#step 4: set PORT (op_code = 5)
|
||||||
|
print("➡️SET PORT")
|
||||||
|
port = 443
|
||||||
|
command = f'AT+UHTTP={uSpot_profile_id},5,{port}\r'
|
||||||
|
ser_sara.write((command + '\r').encode('utf-8'))
|
||||||
|
response_SARA_55 = read_complete_response(ser_sara, wait_for_lines=["OK"])
|
||||||
|
print(response_SARA_55)
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
#step 4: set url to SSL (op_code = 6) (http_secure = 1 for HTTPS)(USECMNG_PROFILE = 2)
|
||||||
|
print("➡️SET SSL")
|
||||||
|
http_secure = 1
|
||||||
|
command = f'AT+UHTTP={uSpot_profile_id},6,{http_secure},{security_profile_id}\r'
|
||||||
|
|
||||||
|
ser_sara.write(command.encode('utf-8'))
|
||||||
|
response_SARA_5fg = read_complete_response(ser_sara, wait_for_lines=["OK"])
|
||||||
|
print(response_SARA_5fg)
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
|
||||||
|
'''
|
||||||
|
SARA R5
|
||||||
|
'''
|
||||||
|
|
||||||
|
if sara_r5_DPD_setup:
|
||||||
|
print("➡️➡️SARA R5 PDP SETUP")
|
||||||
|
# 2. Activate PDP context 1
|
||||||
|
print('➡️Activate PDP context 1')
|
||||||
|
command = f'AT+CGACT=1,1\r'
|
||||||
|
ser_sara.write(command.encode('utf-8'))
|
||||||
|
response_SARA_2 = read_complete_response(ser_sara, wait_for_lines=["OK"])
|
||||||
|
print(response_SARA_2, end="")
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
# 2. Set the PDP type
|
||||||
|
print('➡️Set the PDP type to IPv4 referring to the outputof the +CGDCONT read command')
|
||||||
|
command = f'AT+UPSD=0,0,0\r'
|
||||||
|
ser_sara.write(command.encode('utf-8'))
|
||||||
|
response_SARA_3 = read_complete_response(ser_sara, wait_for_lines=["OK"])
|
||||||
|
print(response_SARA_3, end="")
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
# 2. Profile #0 is mapped on CID=1.
|
||||||
|
print('➡️Profile #0 is mapped on CID=1.')
|
||||||
|
command = f'AT+UPSD=0,100,1\r'
|
||||||
|
ser_sara.write(command.encode('utf-8'))
|
||||||
|
response_SARA_3 = read_complete_response(ser_sara, wait_for_lines=["OK"])
|
||||||
|
print(response_SARA_3, end="")
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
# 2. Set the PDP type
|
||||||
|
print('➡️Activate the PSD profile #0: the IPv4 address is already assigned by the network.')
|
||||||
|
command = f'AT+UPSDA=0,3\r'
|
||||||
|
ser_sara.write(command.encode('utf-8'))
|
||||||
|
response_SARA_3 = read_complete_response(ser_sara, wait_for_lines=["OK","+UUPSDA"])
|
||||||
|
print(response_SARA_3, end="")
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
|
||||||
|
#3. Get localisation (CellLocate)
|
||||||
|
mode = 2 #single shot position
|
||||||
|
sensor = 2 #use cellular CellLocate® location information
|
||||||
|
response_type = 0
|
||||||
|
timeout_s = 2
|
||||||
|
accuracy_m = 1
|
||||||
|
command = f'AT+ULOC={mode},{sensor},{response_type},{timeout_s},{accuracy_m}\r'
|
||||||
|
ser_sara.write((command + '\r').encode('utf-8'))
|
||||||
|
response_SARA_3 = read_complete_response(ser_sara, wait_for_lines=["+UULOC"])
|
||||||
|
print(response_SARA_3)
|
||||||
|
|
||||||
|
match = re.search(r"\+UULOC: \d{2}/\d{2}/\d{4},\d{2}:\d{2}:\d{2}\.\d{3},([-+]?\d+\.\d+),([-+]?\d+\.\d+)", response_SARA_3)
|
||||||
|
if match:
|
||||||
|
latitude = match.group(1)
|
||||||
|
longitude = match.group(2)
|
||||||
|
print(f"📍 Latitude: {latitude}, Longitude: {longitude}")
|
||||||
|
else:
|
||||||
|
print("❌ Failed to extract coordinates.")
|
||||||
|
|
||||||
|
#update sqlite table
|
||||||
|
update_sqlite_config("latitude_raw", float(latitude))
|
||||||
|
update_sqlite_config("longitude_raw", float(longitude))
|
||||||
|
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print("An error occurred:", e)
|
||||||
|
traceback.print_exc() # This prints the full traceback
|
||||||
112
SARA/sara.py
112
SARA/sara.py
@@ -1,11 +1,23 @@
|
|||||||
'''
|
'''
|
||||||
|
____ _ ____ _
|
||||||
|
/ ___| / \ | _ \ / \
|
||||||
|
\___ \ / _ \ | |_) | / _ \
|
||||||
|
___) / ___ \| _ < / ___ \
|
||||||
|
|____/_/ \_\_| \_\/_/ \_\
|
||||||
|
|
||||||
Script to see if the SARA-R410 is running
|
Script to see if the SARA-R410 is running
|
||||||
ex:
|
ex:
|
||||||
|
python3 /var/www/nebuleair_pro_4g/SARA/sara.py ttyAMA2 ATI 2
|
||||||
|
ex 1 (get SIM infos)
|
||||||
python3 /var/www/nebuleair_pro_4g/SARA/sara.py ttyAMA2 AT+CCID? 2
|
python3 /var/www/nebuleair_pro_4g/SARA/sara.py ttyAMA2 AT+CCID? 2
|
||||||
ex 2 (turn on blue light):
|
ex 2 (turn on blue light):
|
||||||
python3 /var/www/nebuleair_pro_4g/SARA/sara.py ttyAMA2 AT+UGPIOC=16,2 2
|
python3 /var/www/nebuleair_pro_4g/SARA/sara.py ttyAMA2 AT+UGPIOC=16,2 2
|
||||||
ex 3 (reconnect network)
|
ex 3 (reconnect network)
|
||||||
python3 /var/www/nebuleair_pro_4g/SARA/sara.py ttyAMA2 AT+COPS=1,2,20801 20
|
python3 /var/www/nebuleair_pro_4g/SARA/sara.py ttyAMA2 AT+COPS=1,2,20801 20
|
||||||
|
ex 4 (get HTTP Profiles)
|
||||||
|
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?
|
command = parameter[1] # ex: AT+CCID?
|
||||||
timeout = float(parameter[2]) # ex:2
|
timeout = float(parameter[2]) # ex:2
|
||||||
|
|
||||||
#get baudrate
|
|
||||||
def load_config(config_file):
|
|
||||||
try:
|
|
||||||
with open(config_file, 'r') as file:
|
|
||||||
config_data = json.load(file)
|
|
||||||
return config_data
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Error loading config file: {e}")
|
|
||||||
return {}
|
|
||||||
|
|
||||||
# Define the config file path
|
|
||||||
config_file = '/var/www/nebuleair_pro_4g/config.json'
|
|
||||||
# Load the configuration data
|
|
||||||
config = load_config(config_file)
|
|
||||||
# Access the shared variables
|
# Access the shared variables
|
||||||
baudrate = config.get('SaraR4_baudrate', 115200)
|
baudrate = 115200
|
||||||
|
|
||||||
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=?')
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
try:
|
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
|
# Read lines until a timeout occurs
|
||||||
response_lines = []
|
response_lines = []
|
||||||
while True:
|
start_time = time.time()
|
||||||
line = ser.readline().decode('utf-8').strip()
|
|
||||||
if not line:
|
while (time.time() - start_time) < timeout:
|
||||||
break # Break the loop if an empty line is encountered
|
line = ser.readline().decode('utf-8', errors='ignore').strip()
|
||||||
response_lines.append(line)
|
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
|
# Print the response
|
||||||
for line in response_lines:
|
for line in response_lines:
|
||||||
print(line)
|
print(line)
|
||||||
|
|
||||||
except serial.SerialException as e:
|
except serial.SerialException as e:
|
||||||
print(f"Error: {e}")
|
print(f"ERROR: Serial communication error: {e}")
|
||||||
|
sys.exit(1)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"ERROR: Unexpected error: {e}")
|
||||||
|
sys.exit(1)
|
||||||
finally:
|
finally:
|
||||||
if ser.is_open:
|
# Close the serial port if it's open
|
||||||
|
if 'ser' in locals() and ser.is_open:
|
||||||
ser.close()
|
ser.close()
|
||||||
#print("Serial closed")
|
|
||||||
|
|
||||||
|
|||||||
63
SARA/sara_checkDNS.py
Normal file
63
SARA/sara_checkDNS.py
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
'''
|
||||||
|
____ _ ____ _
|
||||||
|
/ ___| / \ | _ \ / \
|
||||||
|
\___ \ / _ \ | |_) | / _ \
|
||||||
|
___) / ___ \| _ < / ___ \
|
||||||
|
|____/_/ \_\_| \_\/_/ \_\
|
||||||
|
|
||||||
|
Script to resolve DNS (get IP from domain name) with AT+UDNSRN command
|
||||||
|
Ex:
|
||||||
|
/usr/bin/python3 /var/www/nebuleair_pro_4g/SARA/sara_checkDNS.py ttyAMA2 data.nebuleair.fr
|
||||||
|
To do: need to add profile id as parameter
|
||||||
|
|
||||||
|
'''
|
||||||
|
|
||||||
|
import serial
|
||||||
|
import time
|
||||||
|
import sys
|
||||||
|
import json
|
||||||
|
|
||||||
|
parameter = sys.argv[1:] # Exclude the script name
|
||||||
|
#print("Parameters received:")
|
||||||
|
port='/dev/'+parameter[0] # ex: ttyAMA2
|
||||||
|
url = parameter[1] # ex: data.mobileair.fr
|
||||||
|
|
||||||
|
baudrate = 115200
|
||||||
|
|
||||||
|
ser = serial.Serial(
|
||||||
|
port=port, #USB0 or ttyS0
|
||||||
|
baudrate=baudrate, #115200 ou 9600
|
||||||
|
parity=serial.PARITY_NONE, #PARITY_NONE, PARITY_EVEN or PARITY_ODD
|
||||||
|
stopbits=serial.STOPBITS_ONE,
|
||||||
|
bytesize=serial.EIGHTBITS,
|
||||||
|
timeout = 2
|
||||||
|
)
|
||||||
|
|
||||||
|
command = f'AT+UDNSRN=0,"{url}"\r'
|
||||||
|
ser.write((command + '\r').encode('utf-8'))
|
||||||
|
|
||||||
|
print("****")
|
||||||
|
print("DNS check")
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Read lines until a timeout occurs
|
||||||
|
response_lines = []
|
||||||
|
while True:
|
||||||
|
line = ser.readline().decode('utf-8').strip()
|
||||||
|
if not line:
|
||||||
|
break # Break the loop if an empty line is encountered
|
||||||
|
response_lines.append(line)
|
||||||
|
|
||||||
|
# Print the response
|
||||||
|
for line in response_lines:
|
||||||
|
print(line)
|
||||||
|
|
||||||
|
except serial.SerialException as e:
|
||||||
|
print(f"Error: {e}")
|
||||||
|
|
||||||
|
finally:
|
||||||
|
if ser.is_open:
|
||||||
|
ser.close()
|
||||||
|
print("****")
|
||||||
|
#print("Serial closed")
|
||||||
|
|
||||||
@@ -1,11 +1,18 @@
|
|||||||
'''
|
'''
|
||||||
|
____ _ ____ _
|
||||||
|
/ ___| / \ | _ \ / \
|
||||||
|
\___ \ / _ \ | |_) | / _ \
|
||||||
|
___) / ___ \| _ < / ___ \
|
||||||
|
|____/_/ \_\_| \_\/_/ \_\
|
||||||
|
|
||||||
Script to connect SARA-R410 to network SARA-R410
|
Script to connect SARA-R410 to network SARA-R410
|
||||||
python3 /var/www/nebuleair_pro_4g/SARA/sara_connectNetwork.py ttyAMA2 20801 10
|
python3 /var/www/nebuleair_pro_4g/SARA/sara_connectNetwork.py ttyAMA2 20801 10
|
||||||
|
|
||||||
AT+COPS=1,2,20801
|
AT+COPS=1,2,20801
|
||||||
mode->1 pour manual
|
mode->1 pour manual
|
||||||
format->2 pour numeric
|
format->2 pour numeric
|
||||||
operator->20801 pour orange
|
operator->20801 pour orange, 20810 pour SFR
|
||||||
|
|
||||||
'''
|
'''
|
||||||
|
|
||||||
import serial
|
import serial
|
||||||
@@ -19,22 +26,54 @@ networkID = parameter[1] # ex: 20801
|
|||||||
timeout = float(parameter[2]) # ex:2
|
timeout = float(parameter[2]) # ex:2
|
||||||
|
|
||||||
|
|
||||||
#get baudrate
|
|
||||||
def load_config(config_file):
|
|
||||||
try:
|
|
||||||
with open(config_file, 'r') as file:
|
|
||||||
config_data = json.load(file)
|
|
||||||
return config_data
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Error loading config file: {e}")
|
|
||||||
return {}
|
|
||||||
|
|
||||||
# Define the config file path
|
|
||||||
config_file = '/var/www/nebuleair_pro_4g/config.json'
|
def read_complete_response(serial_connection, timeout=2, end_of_response_timeout=2, wait_for_lines=None, debug=True):
|
||||||
# Load the configuration data
|
'''
|
||||||
config = load_config(config_file)
|
Fonction très importante !!!
|
||||||
# Access the shared variables
|
Reads the complete response from a serial connection and waits for specific lines.
|
||||||
baudrate = config.get('SaraR4_baudrate', 115200)
|
'''
|
||||||
|
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(
|
ser = serial.Serial(
|
||||||
port=port, #USB0 or ttyS0
|
port=port, #USB0 or ttyS0
|
||||||
@@ -50,17 +89,11 @@ ser.write((command + '\r').encode('utf-8'))
|
|||||||
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Read lines until a timeout occurs
|
response = read_complete_response(ser, wait_for_lines=["OK", "ERROR"],timeout=5, end_of_response_timeout=120, debug=True)
|
||||||
response_lines = []
|
|
||||||
while True:
|
print('<p class="text-danger-emphasis">')
|
||||||
line = ser.readline().decode('utf-8').strip()
|
print(response)
|
||||||
if not line:
|
print("</p>", end="")
|
||||||
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:
|
except serial.SerialException as e:
|
||||||
print(f"Error: {e}")
|
print(f"Error: {e}")
|
||||||
|
|||||||
@@ -11,22 +11,7 @@ parameter = sys.argv[1:] # Exclude the script name
|
|||||||
port='/dev/'+parameter[0] # ex: ttyAMA2
|
port='/dev/'+parameter[0] # ex: ttyAMA2
|
||||||
message = parameter[1] # ex: Hello
|
message = parameter[1] # ex: Hello
|
||||||
|
|
||||||
#get baudrate
|
baudrate = 115200
|
||||||
def load_config(config_file):
|
|
||||||
try:
|
|
||||||
with open(config_file, 'r') as file:
|
|
||||||
config_data = json.load(file)
|
|
||||||
return config_data
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Error loading config file: {e}")
|
|
||||||
return {}
|
|
||||||
|
|
||||||
# Define the config file path
|
|
||||||
config_file = '/var/www/nebuleair_pro_4g/config.json'
|
|
||||||
# Load the configuration data
|
|
||||||
config = load_config(config_file)
|
|
||||||
# Access the shared variables
|
|
||||||
baudrate = config.get('SaraR4_baudrate', 115200)
|
|
||||||
|
|
||||||
ser = serial.Serial(
|
ser = serial.Serial(
|
||||||
port=port, #USB0 or ttyS0
|
port=port, #USB0 or ttyS0
|
||||||
|
|||||||
58
SARA/sara_google_ping.py
Normal file
58
SARA/sara_google_ping.py
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
'''
|
||||||
|
____ _ ____ _
|
||||||
|
/ ___| / \ | _ \ / \
|
||||||
|
\___ \ / _ \ | |_) | / _ \
|
||||||
|
___) / ___ \| _ < / ___ \
|
||||||
|
|____/_/ \_\_| \_\/_/ \_\
|
||||||
|
|
||||||
|
Script to set the URL for a HTTP request
|
||||||
|
Ex:
|
||||||
|
/usr/bin/python3 /var/www/nebuleair_pro_4g/SARA/sara_google_ping.py
|
||||||
|
|
||||||
|
|
||||||
|
'''
|
||||||
|
|
||||||
|
import serial
|
||||||
|
import time
|
||||||
|
import sys
|
||||||
|
import json
|
||||||
|
|
||||||
|
|
||||||
|
baudrate = 115200
|
||||||
|
|
||||||
|
ser = serial.Serial(
|
||||||
|
port='/dev/ttyAMA2',
|
||||||
|
baudrate=baudrate, #115200 ou 9600
|
||||||
|
parity=serial.PARITY_NONE, #PARITY_NONE, PARITY_EVEN or PARITY_ODD
|
||||||
|
stopbits=serial.STOPBITS_ONE,
|
||||||
|
bytesize=serial.EIGHTBITS,
|
||||||
|
timeout = 2
|
||||||
|
)
|
||||||
|
url="www.google.com"
|
||||||
|
command = f'AT+UPING="{url}"\r'
|
||||||
|
ser.write((command + '\r').encode('utf-8'))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Read lines until a timeout occurs
|
||||||
|
response_lines = []
|
||||||
|
while True:
|
||||||
|
line = ser.readline().decode('utf-8').strip()
|
||||||
|
if not line:
|
||||||
|
break # Break the loop if an empty line is encountered
|
||||||
|
response_lines.append(line)
|
||||||
|
|
||||||
|
# Print the response
|
||||||
|
for line in response_lines:
|
||||||
|
print(line)
|
||||||
|
|
||||||
|
except serial.SerialException as e:
|
||||||
|
print(f"Error: {e}")
|
||||||
|
|
||||||
|
finally:
|
||||||
|
if ser.is_open:
|
||||||
|
ser.close()
|
||||||
|
print("****")
|
||||||
|
#print("Serial closed")
|
||||||
|
|
||||||
165
SARA/sara_ping.py
Normal file
165
SARA/sara_ping.py
Normal file
@@ -0,0 +1,165 @@
|
|||||||
|
'''
|
||||||
|
____ _ ____ _
|
||||||
|
/ ___| / \ | _ \ / \
|
||||||
|
\___ \ / _ \ | |_) | / _ \
|
||||||
|
___) / ___ \| _ < / ___ \
|
||||||
|
|____/_/ \_\_| \_\/_/ \_\
|
||||||
|
|
||||||
|
Script to do a ping request to data.nebuleair.fr/ping.php
|
||||||
|
python3 /var/www/nebuleair_pro_4g/SARA/sara_ping.py
|
||||||
|
'''
|
||||||
|
|
||||||
|
import serial
|
||||||
|
import time
|
||||||
|
import sys
|
||||||
|
import json
|
||||||
|
|
||||||
|
# SARA R4 UHTTPC profile IDs
|
||||||
|
aircarto_profile_id = 0
|
||||||
|
|
||||||
|
baudrate = 115200
|
||||||
|
|
||||||
|
ser_sara = serial.Serial(
|
||||||
|
port='/dev/ttyAMA2',
|
||||||
|
baudrate=baudrate, #115200 ou 9600
|
||||||
|
parity=serial.PARITY_NONE, #PARITY_NONE, PARITY_EVEN or PARITY_ODD
|
||||||
|
stopbits=serial.STOPBITS_ONE,
|
||||||
|
bytesize=serial.EIGHTBITS,
|
||||||
|
timeout = 2
|
||||||
|
)
|
||||||
|
|
||||||
|
def read_complete_response(serial_connection, timeout=2, end_of_response_timeout=2, wait_for_lines=None, debug=True):
|
||||||
|
'''
|
||||||
|
Fonction très importante !!!
|
||||||
|
Reads the complete response from a serial connection and waits for specific lines.
|
||||||
|
'''
|
||||||
|
if wait_for_lines is None:
|
||||||
|
wait_for_lines = [] # Default to an empty list if not provided
|
||||||
|
|
||||||
|
response = bytearray()
|
||||||
|
serial_connection.timeout = timeout
|
||||||
|
end_time = time.time() + end_of_response_timeout
|
||||||
|
start_time = time.time()
|
||||||
|
|
||||||
|
while True:
|
||||||
|
elapsed_time = time.time() - start_time # Time since function start
|
||||||
|
if serial_connection.in_waiting > 0:
|
||||||
|
data = serial_connection.read(serial_connection.in_waiting)
|
||||||
|
response.extend(data)
|
||||||
|
end_time = time.time() + end_of_response_timeout # Reset timeout on new data
|
||||||
|
|
||||||
|
# Decode and check for any target line
|
||||||
|
decoded_response = response.decode('utf-8', errors='replace')
|
||||||
|
for target_line in wait_for_lines:
|
||||||
|
if target_line in decoded_response:
|
||||||
|
if debug:
|
||||||
|
print(f"[DEBUG] 🔎 Found target line: {target_line} (in {elapsed_time:.2f}s)")
|
||||||
|
return decoded_response # Return response immediately if a target line is found
|
||||||
|
elif time.time() > end_time:
|
||||||
|
if debug:
|
||||||
|
print(f"[DEBUG] Timeout reached. No more data received.")
|
||||||
|
break
|
||||||
|
time.sleep(0.1) # Short sleep to prevent busy waiting
|
||||||
|
|
||||||
|
# Final response and debug output
|
||||||
|
total_elapsed_time = time.time() - start_time
|
||||||
|
if debug:
|
||||||
|
print(f"[DEBUG] ⏱️ elapsed time: {total_elapsed_time:.2f}s. ⏱️")
|
||||||
|
# Check if the elapsed time exceeded 10 seconds
|
||||||
|
if total_elapsed_time > 10 and debug:
|
||||||
|
print(f"[ALERT] 🚨 The operation took too long 🚨")
|
||||||
|
print(f'<span style="color: red;font-weight: bold;">[ALERT] ⚠️{total_elapsed_time:.2f}s⚠️</span>')
|
||||||
|
|
||||||
|
return response.decode('utf-8', errors='replace') # Return the full response if no target line is found
|
||||||
|
|
||||||
|
|
||||||
|
def extract_error_code(response):
|
||||||
|
"""
|
||||||
|
Extract just the error code from AT+UHTTPER response
|
||||||
|
"""
|
||||||
|
for line in response.split('\n'):
|
||||||
|
if '+UHTTPER' in line:
|
||||||
|
try:
|
||||||
|
# Split the line and get the third value (error code)
|
||||||
|
parts = line.split(':')[1].strip().split(',')
|
||||||
|
if len(parts) >= 3:
|
||||||
|
error_code = int(parts[2])
|
||||||
|
return error_code
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Return None if we couldn't find the error code
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
#3. Send to endpoint (with device ID)
|
||||||
|
print("Send data (GET REQUEST):")
|
||||||
|
command= f'AT+UHTTPC={aircarto_profile_id},1,"/ping.php","aircarto_server_response.txt"\r'
|
||||||
|
ser_sara.write(command.encode('utf-8'))
|
||||||
|
|
||||||
|
response_SARA_3 = read_complete_response(ser_sara, timeout=5, end_of_response_timeout=120, wait_for_lines=["+UUHTTPCR", "+CME ERROR"], debug=True)
|
||||||
|
|
||||||
|
print(response_SARA_3)
|
||||||
|
# si on recoit la réponse UHTTPCR
|
||||||
|
if "+UUHTTPCR" in response_SARA_3:
|
||||||
|
print("✅ Received +UUHTTPCR response.")
|
||||||
|
# Split response into lines
|
||||||
|
lines = response_SARA_3.strip().splitlines()
|
||||||
|
# 1.Vérifier si la réponse contient un message d'erreur CME
|
||||||
|
if "+CME ERROR" in lines[-1]:
|
||||||
|
print("error ⛔")
|
||||||
|
else:
|
||||||
|
http_response = lines[-1] # "+UUHTTPCR: 0,4,0"
|
||||||
|
parts = http_response.split(',')
|
||||||
|
# 2.1 code 0 (HTTP failed) ⛔⛔⛔
|
||||||
|
if len(parts) == 3 and parts[-1] == '0': # The third value indicates success
|
||||||
|
print("⛔⛔ATTENTION: HTTP operation failed")
|
||||||
|
#get error code
|
||||||
|
print("Getting error code (11->Server connection error, 73->Secure socket connect error)")
|
||||||
|
command = f'AT+UHTTPER={aircarto_profile_id}\r'
|
||||||
|
ser_sara.write(command.encode('utf-8'))
|
||||||
|
response_SARA_9 = read_complete_response(ser_sara, wait_for_lines=["OK"], debug=False)
|
||||||
|
print('<p class="text-danger-emphasis">')
|
||||||
|
print(response_SARA_9)
|
||||||
|
print("</p>", end="")
|
||||||
|
# Extract just the error code
|
||||||
|
error_code = extract_error_code(response_SARA_9)
|
||||||
|
if error_code is not None:
|
||||||
|
# Display interpretation based on error code
|
||||||
|
if error_code == 0:
|
||||||
|
print('<p class="text-success">No error detected</p>')
|
||||||
|
elif error_code == 4:
|
||||||
|
print('<p class="text-danger">Error 4: Invalid server Hostname</p>')
|
||||||
|
elif error_code == 11:
|
||||||
|
print('<p class="text-danger">Error 11: Server connection error</p>')
|
||||||
|
elif error_code == 22:
|
||||||
|
print('<p class="text-danger">Error 22: PSD or CSD connection not established</p>')
|
||||||
|
elif error_code == 73:
|
||||||
|
print('<p class="text-danger">Error 73: Secure socket connect error</p>')
|
||||||
|
else:
|
||||||
|
print(f'<p class="text-danger">Unknown error code: {error_code}</p>')
|
||||||
|
else:
|
||||||
|
print('<p class="text-danger">Could not extract error code from response</p>')
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# 2.2 code 1 (HHTP succeded)
|
||||||
|
else:
|
||||||
|
# Si la commande HTTP a réussi
|
||||||
|
print("✅✅HTTP operation successful")
|
||||||
|
#4. Read reply from server
|
||||||
|
print("Reply from server:")
|
||||||
|
ser_sara.write(b'AT+URDFILE="aircarto_server_response.txt"\r')
|
||||||
|
response_SARA_4 = read_complete_response(ser_sara, wait_for_lines=["OK"], debug=False)
|
||||||
|
print(response_SARA_4)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
except serial.SerialException as e:
|
||||||
|
print(f"Error: {e}")
|
||||||
|
|
||||||
|
finally:
|
||||||
|
if ser_sara.is_open:
|
||||||
|
ser_sara.close()
|
||||||
|
#print("Serial closed")
|
||||||
|
|
||||||
@@ -11,22 +11,7 @@ parameter = sys.argv[1:] # Exclude the script name
|
|||||||
port='/dev/'+parameter[0] # ex: ttyAMA2
|
port='/dev/'+parameter[0] # ex: ttyAMA2
|
||||||
message = parameter[1] # ex: Hello
|
message = parameter[1] # ex: Hello
|
||||||
|
|
||||||
#get baudrate
|
baudrate = 115200
|
||||||
def load_config(config_file):
|
|
||||||
try:
|
|
||||||
with open(config_file, 'r') as file:
|
|
||||||
config_data = json.load(file)
|
|
||||||
return config_data
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Error loading config file: {e}")
|
|
||||||
return {}
|
|
||||||
|
|
||||||
# Define the config file path
|
|
||||||
config_file = '/var/www/nebuleair_pro_4g/config.json'
|
|
||||||
# Load the configuration data
|
|
||||||
config = load_config(config_file)
|
|
||||||
# Access the shared variables
|
|
||||||
baudrate = config.get('SaraR4_baudrate', 115200)
|
|
||||||
|
|
||||||
ser = serial.Serial(
|
ser = serial.Serial(
|
||||||
port=port, #USB0 or ttyS0
|
port=port, #USB0 or ttyS0
|
||||||
|
|||||||
@@ -10,23 +10,9 @@ import json
|
|||||||
parameter = sys.argv[1:] # Exclude the script name
|
parameter = sys.argv[1:] # Exclude the script name
|
||||||
port='/dev/'+parameter[0] # ex: ttyAMA2
|
port='/dev/'+parameter[0] # ex: ttyAMA2
|
||||||
endpoint = parameter[1] # ex: /pro_4G/notif_message.php
|
endpoint = parameter[1] # ex: /pro_4G/notif_message.php
|
||||||
|
profile_id = parameter[2]
|
||||||
|
|
||||||
#get baudrate
|
baudrate = 115200
|
||||||
def load_config(config_file):
|
|
||||||
try:
|
|
||||||
with open(config_file, 'r') as file:
|
|
||||||
config_data = json.load(file)
|
|
||||||
return config_data
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Error loading config file: {e}")
|
|
||||||
return {}
|
|
||||||
|
|
||||||
# Define the config file path
|
|
||||||
config_file = '/var/www/nebuleair_pro_4g/config.json'
|
|
||||||
# Load the configuration data
|
|
||||||
config = load_config(config_file)
|
|
||||||
# Access the shared variables
|
|
||||||
baudrate = config.get('SaraR4_baudrate', 115200)
|
|
||||||
|
|
||||||
ser = serial.Serial(
|
ser = serial.Serial(
|
||||||
port=port, #USB0 or ttyS0
|
port=port, #USB0 or ttyS0
|
||||||
@@ -37,7 +23,7 @@ ser = serial.Serial(
|
|||||||
timeout = 2
|
timeout = 2
|
||||||
)
|
)
|
||||||
|
|
||||||
command= f'AT+UHTTPC=0,4,"{endpoint}","data.txt","sensordata.json",4\r'
|
command= f'AT+UHTTPC={profile_id},4,"{endpoint}","data.txt","sensordata.json",4\r'
|
||||||
ser.write((command + '\r').encode('utf-8'))
|
ser.write((command + '\r').encode('utf-8'))
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -1,4 +1,10 @@
|
|||||||
'''
|
'''
|
||||||
|
____ _ ____ _
|
||||||
|
/ ___| / \ | _ \ / \
|
||||||
|
\___ \ / _ \ | |_) | / _ \
|
||||||
|
___) / ___ \| _ < / ___ \
|
||||||
|
|____/_/ \_\_| \_\/_/ \_\
|
||||||
|
|
||||||
Script to connect SARA-R410 to APN
|
Script to connect SARA-R410 to APN
|
||||||
AT+CGDCONT=1,"IP","data.mono"
|
AT+CGDCONT=1,"IP","data.mono"
|
||||||
|
|
||||||
@@ -15,23 +21,7 @@ port='/dev/'+parameter[0] # ex: ttyAMA2
|
|||||||
apn_address = parameter[1] # ex: data.mono
|
apn_address = parameter[1] # ex: data.mono
|
||||||
timeout = float(parameter[2]) # ex:2
|
timeout = float(parameter[2]) # ex:2
|
||||||
|
|
||||||
|
baudrate = 115200
|
||||||
#get baudrate
|
|
||||||
def load_config(config_file):
|
|
||||||
try:
|
|
||||||
with open(config_file, 'r') as file:
|
|
||||||
config_data = json.load(file)
|
|
||||||
return config_data
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Error loading config file: {e}")
|
|
||||||
return {}
|
|
||||||
|
|
||||||
# Define the config file path
|
|
||||||
config_file = '/var/www/nebuleair_pro_4g/config.json'
|
|
||||||
# Load the configuration data
|
|
||||||
config = load_config(config_file)
|
|
||||||
# Access the shared variables
|
|
||||||
baudrate = config.get('SaraR4_baudrate', 115200)
|
|
||||||
|
|
||||||
ser = serial.Serial(
|
ser = serial.Serial(
|
||||||
port=port, #USB0 or ttyS0
|
port=port, #USB0 or ttyS0
|
||||||
@@ -43,6 +33,8 @@ ser = serial.Serial(
|
|||||||
)
|
)
|
||||||
|
|
||||||
command = f'AT+CGDCONT=1,"IP","{apn_address}"\r'
|
command = f'AT+CGDCONT=1,"IP","{apn_address}"\r'
|
||||||
|
#command = f'AT+CGDCONT=1,"IPV4V6","{apn_address}"\r'
|
||||||
|
#command = f'AT+CGDCONT=1,"IP","{apn_address}",0,0\r'
|
||||||
ser.write((command + '\r').encode('utf-8'))
|
ser.write((command + '\r').encode('utf-8'))
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,13 @@
|
|||||||
'''
|
'''
|
||||||
|
____ _ ____ _
|
||||||
|
/ ___| / \ | _ \ / \
|
||||||
|
\___ \ / _ \ | |_) | / _ \
|
||||||
|
___) / ___ \| _ < / ___ \
|
||||||
|
|____/_/ \_\_| \_\/_/ \_\
|
||||||
|
|
||||||
Script to set the URL for a HTTP request
|
Script to set the URL for a HTTP request
|
||||||
Ex:
|
Ex:
|
||||||
/usr/bin/python3 /var/www/nebuleair_pro_4g/SARA/sara_setURL.py ttyAMA2 data.nebuleair.fr
|
/usr/bin/python3 /var/www/nebuleair_pro_4g/SARA/sara_setURL.py ttyAMA2 data.nebuleair.fr 0
|
||||||
To do: need to add profile id as parameter
|
|
||||||
|
|
||||||
First profile id:
|
First profile id:
|
||||||
AT+UHTTP=0,1,"data.nebuleair.fr"
|
AT+UHTTP=0,1,"data.nebuleair.fr"
|
||||||
@@ -19,24 +24,10 @@ parameter = sys.argv[1:] # Exclude the script name
|
|||||||
#print("Parameters received:")
|
#print("Parameters received:")
|
||||||
port='/dev/'+parameter[0] # ex: ttyAMA2
|
port='/dev/'+parameter[0] # ex: ttyAMA2
|
||||||
url = parameter[1] # ex: data.mobileair.fr
|
url = parameter[1] # ex: data.mobileair.fr
|
||||||
|
profile_id = parameter[2] #ex: 0
|
||||||
|
|
||||||
|
|
||||||
#get baudrate
|
baudrate = 115200
|
||||||
def load_config(config_file):
|
|
||||||
try:
|
|
||||||
with open(config_file, 'r') as file:
|
|
||||||
config_data = json.load(file)
|
|
||||||
return config_data
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Error loading config file: {e}")
|
|
||||||
return {}
|
|
||||||
|
|
||||||
# Define the config file path
|
|
||||||
config_file = '/var/www/nebuleair_pro_4g/config.json'
|
|
||||||
# Load the configuration data
|
|
||||||
config = load_config(config_file)
|
|
||||||
# Access the shared variables
|
|
||||||
baudrate = config.get('SaraR4_baudrate', 115200)
|
|
||||||
|
|
||||||
ser = serial.Serial(
|
ser = serial.Serial(
|
||||||
port=port, #USB0 or ttyS0
|
port=port, #USB0 or ttyS0
|
||||||
@@ -47,7 +38,7 @@ ser = serial.Serial(
|
|||||||
timeout = 2
|
timeout = 2
|
||||||
)
|
)
|
||||||
|
|
||||||
command = f'AT+UHTTP=0,1,"{url}"\r'
|
command = f'AT+UHTTP={profile_id},1,"{url}"\r'
|
||||||
ser.write((command + '\r').encode('utf-8'))
|
ser.write((command + '\r').encode('utf-8'))
|
||||||
|
|
||||||
print("****")
|
print("****")
|
||||||
|
|||||||
@@ -40,22 +40,7 @@ def read_complete_response(serial_connection, timeout=2, end_of_response_timeout
|
|||||||
|
|
||||||
return response.decode('utf-8', errors='replace')
|
return response.decode('utf-8', errors='replace')
|
||||||
|
|
||||||
#get baudrate
|
baudrate = 115200
|
||||||
def load_config(config_file):
|
|
||||||
try:
|
|
||||||
with open(config_file, 'r') as file:
|
|
||||||
config_data = json.load(file)
|
|
||||||
return config_data
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Error loading config file: {e}")
|
|
||||||
return {}
|
|
||||||
|
|
||||||
# Define the config file path
|
|
||||||
config_file = '/var/www/nebuleair_pro_4g/config.json'
|
|
||||||
# Load the configuration data
|
|
||||||
config = load_config(config_file)
|
|
||||||
# Access the shared variables
|
|
||||||
baudrate = config.get('SaraR4_baudrate', 115200)
|
|
||||||
|
|
||||||
ser_sara = serial.Serial(
|
ser_sara = serial.Serial(
|
||||||
port=port, #USB0 or ttyS0
|
port=port, #USB0 or ttyS0
|
||||||
|
|||||||
@@ -12,21 +12,7 @@ port='/dev/'+parameter[0] # ex: ttyAMA2
|
|||||||
message = parameter[1] # ex: Hello
|
message = parameter[1] # ex: Hello
|
||||||
|
|
||||||
#get baudrate
|
#get baudrate
|
||||||
def load_config(config_file):
|
baudrate = 115200
|
||||||
try:
|
|
||||||
with open(config_file, 'r') as file:
|
|
||||||
config_data = json.load(file)
|
|
||||||
return config_data
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Error loading config file: {e}")
|
|
||||||
return {}
|
|
||||||
|
|
||||||
# Define the config file path
|
|
||||||
config_file = '/var/www/nebuleair_pro_4g/config.json'
|
|
||||||
# Load the configuration data
|
|
||||||
config = load_config(config_file)
|
|
||||||
# Access the shared variables
|
|
||||||
baudrate = config.get('SaraR4_baudrate', 115200)
|
|
||||||
|
|
||||||
ser = serial.Serial(
|
ser = serial.Serial(
|
||||||
port=port, #USB0 or ttyS0
|
port=port, #USB0 or ttyS0
|
||||||
|
|||||||
@@ -2,23 +2,45 @@
|
|||||||
|
|
||||||
# Script to check if wifi is connected and start hotspot if not
|
# Script to check if wifi is connected and start hotspot if not
|
||||||
# will also retreive unique RPi ID and store it to deviceID.txt
|
# will also retreive unique RPi ID and store it to deviceID.txt
|
||||||
|
# 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"
|
OUTPUT_FILE="/var/www/nebuleair_pro_4g/wifi_list.csv"
|
||||||
JSON_FILE="/var/www/nebuleair_pro_4g/config.json"
|
|
||||||
|
|
||||||
echo "-------------------"
|
echo "-------------------"
|
||||||
echo "-------------------"
|
echo "-------------------"
|
||||||
|
|
||||||
echo "NebuleAir pro started at $(date)"
|
echo "NebuleAir pro started at $(date)"
|
||||||
echo "getting SARA R4 serial number"
|
|
||||||
|
|
||||||
|
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
|
||||||
|
gpioset gpiochip0 23=1 24=1
|
||||||
|
#echo "LEDs ON"
|
||||||
|
sleep 1
|
||||||
|
|
||||||
|
# Turn GPIO 23 and 24 OFF
|
||||||
|
gpioset gpiochip0 23=0 24=0
|
||||||
|
#echo "LEDs OFF"
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "getting RPI serial number"
|
||||||
# Get the last 8 characters of the serial number and write to text file
|
# 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)}')
|
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
|
# update Sqlite database
|
||||||
jq --arg serial_number "$serial_number" '.deviceID = $serial_number' "$JSON_FILE" > temp.json && mv temp.json "$JSON_FILE"
|
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"
|
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
|
#need to wait for the network manager to be ready
|
||||||
sleep 20
|
sleep 20
|
||||||
@@ -36,42 +58,39 @@ if [ "$STATE" == "30 (disconnected)" ]; then
|
|||||||
echo "Starting hotspot..."
|
echo "Starting hotspot..."
|
||||||
sudo nmcli device wifi hotspot ifname wlan0 ssid nebuleair_pro password nebuleaircfg
|
sudo nmcli device wifi hotspot ifname wlan0 ssid nebuleair_pro password nebuleaircfg
|
||||||
|
|
||||||
# Update JSON to reflect hotspot mode
|
# Update SQLite to reflect hotspot mode
|
||||||
jq --arg status "hotspot" '.WIFI_status = $status' "$JSON_FILE" > temp.json && mv temp.json "$JSON_FILE"
|
sqlite3 /var/www/nebuleair_pro_4g/sqlite/sensors.db "UPDATE config_table SET value='hotspot' WHERE key='WIFI_status'"
|
||||||
|
|
||||||
|
|
||||||
else
|
else
|
||||||
echo "Success: wlan0 is connected!"
|
echo "🛜Success: wlan0 is connected!🛜"
|
||||||
CONN_SSID=$(nmcli -g GENERAL.CONNECTION device show wlan0)
|
CONN_SSID=$(nmcli -g GENERAL.CONNECTION device show wlan0)
|
||||||
echo "Connection: $CONN_SSID"
|
echo "Connection: $CONN_SSID"
|
||||||
|
|
||||||
#update config JSON file
|
# Update SQLite to reflect hotspot mode
|
||||||
jq --arg status "connected" '.WIFI_status = $status' "$JSON_FILE" > temp.json && mv temp.json "$JSON_FILE"
|
sqlite3 /var/www/nebuleair_pro_4g/sqlite/sensors.db "UPDATE config_table SET value='connected' WHERE key='WIFI_status'"
|
||||||
|
|
||||||
sudo chmod 777 "$JSON_FILE"
|
|
||||||
|
|
||||||
# Lancer le tunnel SSH
|
# Lancer le tunnel SSH
|
||||||
echo "Démarrage du tunnel SSH sur le port $SSH_TUNNEL_PORT..."
|
#echo "Démarrage du tunnel SSH sur le port $SSH_TUNNEL_PORT..."
|
||||||
# Start the SSH agent if it's not already running
|
# Start the SSH agent if it's not already running
|
||||||
eval "$(ssh-agent -s)"
|
#eval "$(ssh-agent -s)"
|
||||||
# Add your SSH private key
|
# Add your SSH private key
|
||||||
ssh-add /home/airlab/.ssh/id_rsa
|
#ssh-add /home/airlab/.ssh/id_rsa
|
||||||
#connections details
|
#connections details
|
||||||
REMOTE_USER="airlab_server1" # Remplacez par votre nom d'utilisateur distant
|
#REMOTE_USER="airlab_server1" # Remplacez par votre nom d'utilisateur distant
|
||||||
REMOTE_SERVER="aircarto.fr" # Remplacez par l'adresse de votre serveur
|
#REMOTE_SERVER="aircarto.fr" # Remplacez par l'adresse de votre serveur
|
||||||
LOCAL_PORT=22 # Port local à rediriger
|
#LOCAL_PORT=22 # Port local à rediriger
|
||||||
MONITOR_PORT=0 # Désactive la surveillance de connexion autossh
|
#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
|
#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 -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
|
#Check if the tunnel was created successfully
|
||||||
if [ $? -eq 0 ]; then
|
#if [ $? -eq 0 ]; then
|
||||||
echo "Tunnel started successfully!"
|
# echo "Tunnel started successfully!"
|
||||||
else
|
#else
|
||||||
echo "Error: Unable to start the tunnel!"
|
# echo "Error: Unable to start the tunnel!"
|
||||||
exit 1
|
# exit 1
|
||||||
fi
|
#fi
|
||||||
fi
|
fi
|
||||||
echo "-------------------"
|
echo "-------------------"
|
||||||
|
|||||||
13
cron_jobs
13
cron_jobs
@@ -1,12 +1,13 @@
|
|||||||
@reboot chmod 777 /dev/ttyAMA* /dev/i2c-1
|
@reboot sleep 10 && 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 /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
|
@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
|
||||||
|
|
||||||
|
0 0 * * * find /var/www/nebuleair_pro_4g/logs -name "*.log" -type f -exec truncate -s 0 {} \;
|
||||||
|
|
||||||
|
|
||||||
* * * * * /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
|
|
||||||
|
|||||||
12
envea/read_value_loop.py
Normal file → Executable file
12
envea/read_value_loop.py
Normal file → Executable file
@@ -1,6 +1,16 @@
|
|||||||
"""
|
"""
|
||||||
|
_____ _ ___ _______ _
|
||||||
|
| ____| \ | \ \ / / ____| / \
|
||||||
|
| _| | \| |\ \ / /| _| / _ \
|
||||||
|
| |___| |\ | \ V / | |___ / ___ \
|
||||||
|
|_____|_| \_| \_/ |_____/_/ \_\
|
||||||
|
|
||||||
Main loop to gather data from envea Sensors
|
Main loop to gather data from envea Sensors
|
||||||
/usr/bin/python3 /var/www/nebuleair_pro_4g/envea/read_value_loop.py
|
Need to run every minutes
|
||||||
|
|
||||||
|
* * * * * /usr/bin/python3 /var/www/nebuleair_pro_4g/envea/read_value_loop.py
|
||||||
|
|
||||||
|
Save data to .txt file inside /var/www/nebuleair_pro_4g/envea/data/
|
||||||
"""
|
"""
|
||||||
import json
|
import json
|
||||||
import serial
|
import serial
|
||||||
|
|||||||
133
envea/read_value_loop_json.py
Executable file
133
envea/read_value_loop_json.py
Executable file
@@ -0,0 +1,133 @@
|
|||||||
|
"""
|
||||||
|
_____ _ ___ _______ _
|
||||||
|
| ____| \ | \ \ / / ____| / \
|
||||||
|
| _| | \| |\ \ / /| _| / _ \
|
||||||
|
| |___| |\ | \ V / | |___ / ___ \
|
||||||
|
|_____|_| \_| \_/ |_____/_/ \_\
|
||||||
|
|
||||||
|
Main loop to gather data from Envea Sensors
|
||||||
|
|
||||||
|
Runs every minute via cron:
|
||||||
|
|
||||||
|
* * * * * /usr/bin/python3 /var/www/nebuleair_pro_4g/envea/read_value_loop_json.py
|
||||||
|
|
||||||
|
Saves data as JSON inside: /var/www/nebuleair_pro_4g/envea/data/data.json
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import serial
|
||||||
|
import time
|
||||||
|
import traceback
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
# Function to load config data
|
||||||
|
def load_config(config_file):
|
||||||
|
try:
|
||||||
|
with open(config_file, 'r') as file:
|
||||||
|
return json.load(file)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error loading config file: {e}")
|
||||||
|
return {}
|
||||||
|
|
||||||
|
# Function to save data to a JSON file
|
||||||
|
def save_data_to_json(filename, data):
|
||||||
|
try:
|
||||||
|
with open(filename, 'w') as file:
|
||||||
|
json.dump(data, file, indent=4)
|
||||||
|
print(f"Data saved to {filename}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error saving to file {filename}: {e}")
|
||||||
|
|
||||||
|
# Define the config file path
|
||||||
|
config_file = '/var/www/nebuleair_pro_4g/config.json'
|
||||||
|
|
||||||
|
# Load configuration data
|
||||||
|
config = load_config(config_file)
|
||||||
|
|
||||||
|
# Initialize sensors and serial connections
|
||||||
|
envea_sondes = config.get('envea_sondes', [])
|
||||||
|
connected_envea_sondes = [sonde for sonde in envea_sondes if sonde.get('connected', False)]
|
||||||
|
serial_connections = {}
|
||||||
|
|
||||||
|
if connected_envea_sondes:
|
||||||
|
for device in connected_envea_sondes:
|
||||||
|
port = device.get('port', 'Unknown')
|
||||||
|
name = device.get('name', 'Unknown')
|
||||||
|
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}")
|
||||||
|
|
||||||
|
# Function to gather data from sensors
|
||||||
|
def gather_data():
|
||||||
|
global data_h2s, data_no2, data_o3
|
||||||
|
data_h2s = 0
|
||||||
|
data_no2 = 0
|
||||||
|
data_o3 = 0
|
||||||
|
|
||||||
|
try:
|
||||||
|
if connected_envea_sondes:
|
||||||
|
for device in connected_envea_sondes:
|
||||||
|
name = device.get('name', 'Unknown')
|
||||||
|
coefficient = device.get('coefficient', 1)
|
||||||
|
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()
|
||||||
|
|
||||||
|
# Main loop
|
||||||
|
if __name__ == "__main__":
|
||||||
|
h2s_values = []
|
||||||
|
no2_values = []
|
||||||
|
o3_values = []
|
||||||
|
|
||||||
|
for cycle in range(6): # Run 6 times
|
||||||
|
gather_data()
|
||||||
|
h2s_values.append(data_h2s)
|
||||||
|
no2_values.append(data_no2)
|
||||||
|
o3_values.append(data_o3)
|
||||||
|
|
||||||
|
print(f"Cycle {cycle + 1}:")
|
||||||
|
print(f" H2S: {data_h2s}, NO2: {data_no2}, O3: {data_o3}")
|
||||||
|
time.sleep(9) # Wait 9 seconds
|
||||||
|
|
||||||
|
# Compute the mean values (as integers)
|
||||||
|
mean_h2s = int(sum(h2s_values) / len(h2s_values)) if h2s_values else 0
|
||||||
|
mean_no2 = int(sum(no2_values) / len(no2_values)) if no2_values else 0
|
||||||
|
mean_o3 = int(sum(o3_values) / len(o3_values)) if o3_values else 0
|
||||||
|
|
||||||
|
# Create JSON structure
|
||||||
|
data_json = {
|
||||||
|
"h2s": mean_h2s,
|
||||||
|
"no2": mean_no2,
|
||||||
|
"o3": mean_o3
|
||||||
|
}
|
||||||
|
|
||||||
|
# Define JSON file path
|
||||||
|
output_file = "/var/www/nebuleair_pro_4g/envea/data/data.json"
|
||||||
|
|
||||||
|
# Save to JSON file
|
||||||
|
save_data_to_json(output_file, data_json)
|
||||||
102
envea/read_value_v2.py
Executable file
102
envea/read_value_v2.py
Executable file
@@ -0,0 +1,102 @@
|
|||||||
|
"""
|
||||||
|
_____ _ ___ _______ _
|
||||||
|
| ____| \ | \ \ / / ____| / \
|
||||||
|
| _| | \| |\ \ / /| _| / _ \
|
||||||
|
| |___| |\ | \ V / | |___ / ___ \
|
||||||
|
|_____|_| \_| \_/ |_____/_/ \_\
|
||||||
|
|
||||||
|
Gather data from envea Sensors and store them to the SQlite table
|
||||||
|
Use the RTC time for the timestamp
|
||||||
|
|
||||||
|
/usr/bin/python3 /var/www/nebuleair_pro_4g/envea/read_value_v2.py
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import serial
|
||||||
|
import time
|
||||||
|
import traceback
|
||||||
|
import sqlite3
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
# Connect to the SQLite database
|
||||||
|
conn = sqlite3.connect("/var/www/nebuleair_pro_4g/sqlite/sensors.db")
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
#GET RTC TIME from SQlite
|
||||||
|
cursor.execute("SELECT * FROM timestamp_table LIMIT 1")
|
||||||
|
row = cursor.fetchone() # Get the first (and only) row
|
||||||
|
rtc_time_str = row[1] # '2025-02-07 12:30:45'
|
||||||
|
|
||||||
|
# Fetch connected ENVEA sondes from SQLite config table
|
||||||
|
cursor.execute("SELECT port, name, coefficient FROM envea_sondes_table WHERE connected = 1")
|
||||||
|
connected_envea_sondes = cursor.fetchall() # List of tuples (port, name, coefficient)
|
||||||
|
|
||||||
|
serial_connections = {}
|
||||||
|
|
||||||
|
if connected_envea_sondes:
|
||||||
|
for port, name, coefficient in connected_envea_sondes:
|
||||||
|
try:
|
||||||
|
serial_connections[name] = serial.Serial(
|
||||||
|
port=f'/dev/{port}',
|
||||||
|
baudrate=9600,
|
||||||
|
parity=serial.PARITY_NONE,
|
||||||
|
stopbits=serial.STOPBITS_ONE,
|
||||||
|
bytesize=serial.EIGHTBITS,
|
||||||
|
timeout=1
|
||||||
|
)
|
||||||
|
except serial.SerialException as e:
|
||||||
|
print(f"Error opening serial port for {name}: {e}")
|
||||||
|
|
||||||
|
global data_h2s, data_no2, data_o3
|
||||||
|
data_h2s = 0
|
||||||
|
data_no2 = 0
|
||||||
|
data_o3 = 0
|
||||||
|
data_co = 0
|
||||||
|
data_nh3 = 0
|
||||||
|
|
||||||
|
try:
|
||||||
|
if connected_envea_sondes:
|
||||||
|
for port, name, coefficient in connected_envea_sondes:
|
||||||
|
if name in serial_connections:
|
||||||
|
serial_connection = serial_connections[name]
|
||||||
|
try:
|
||||||
|
serial_connection.write(
|
||||||
|
b"\xFF\x02\x13\x30\x01\x02\x03\x04\x05\x06\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\x12\xAF\x88\x03"
|
||||||
|
)
|
||||||
|
data_envea = serial_connection.readline()
|
||||||
|
if len(data_envea) >= 20:
|
||||||
|
byte_20 = data_envea[19] * coefficient
|
||||||
|
if name == "h2s":
|
||||||
|
data_h2s = byte_20
|
||||||
|
elif name == "no2":
|
||||||
|
data_no2 = byte_20
|
||||||
|
elif name == "o3":
|
||||||
|
data_o3 = byte_20
|
||||||
|
except serial.SerialException as e:
|
||||||
|
print(f"Error communicating with {name}: {e}")
|
||||||
|
except Exception as e:
|
||||||
|
print("An error occurred while gathering data:", e)
|
||||||
|
traceback.print_exc()
|
||||||
|
|
||||||
|
|
||||||
|
#print(f" H2S: {data_h2s}, NO2: {data_no2}, O3: {data_o3}")
|
||||||
|
|
||||||
|
#save to sqlite database
|
||||||
|
try:
|
||||||
|
cursor.execute('''
|
||||||
|
INSERT INTO data_envea (timestamp,h2s, no2, o3, co, nh3) VALUES (?,?,?,?,?,?)'''
|
||||||
|
, (rtc_time_str,data_h2s,data_no2,data_o3,data_co,data_nh3 ))
|
||||||
|
|
||||||
|
# Commit and close the connection
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
#print("Sensor data saved successfully!")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Database error: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
1199
html/admin.html
1199
html/admin.html
File diff suppressed because it is too large
Load Diff
20
html/assets/js/chart.js
Executable file
20
html/assets/js/chart.js
Executable file
File diff suppressed because one or more lines are too long
BIN
html/assets/leaflet/images/layers-2x.png
Executable file
BIN
html/assets/leaflet/images/layers-2x.png
Executable file
Binary file not shown.
|
After Width: | Height: | Size: 1.2 KiB |
BIN
html/assets/leaflet/images/layers.png
Executable file
BIN
html/assets/leaflet/images/layers.png
Executable file
Binary file not shown.
|
After Width: | Height: | Size: 696 B |
BIN
html/assets/leaflet/images/marker-icon-2x.png
Executable file
BIN
html/assets/leaflet/images/marker-icon-2x.png
Executable file
Binary file not shown.
|
After Width: | Height: | Size: 2.4 KiB |
BIN
html/assets/leaflet/images/marker-icon.png
Executable file
BIN
html/assets/leaflet/images/marker-icon.png
Executable file
Binary file not shown.
|
After Width: | Height: | Size: 1.4 KiB |
BIN
html/assets/leaflet/images/marker-shadow.png
Executable file
BIN
html/assets/leaflet/images/marker-shadow.png
Executable file
Binary file not shown.
|
After Width: | Height: | Size: 618 B |
14419
html/assets/leaflet/leaflet-src.esm.js
Executable file
14419
html/assets/leaflet/leaflet-src.esm.js
Executable file
File diff suppressed because it is too large
Load Diff
1
html/assets/leaflet/leaflet-src.esm.js.map
Executable file
1
html/assets/leaflet/leaflet-src.esm.js.map
Executable file
File diff suppressed because one or more lines are too long
14512
html/assets/leaflet/leaflet-src.js
Executable file
14512
html/assets/leaflet/leaflet-src.js
Executable file
File diff suppressed because it is too large
Load Diff
1
html/assets/leaflet/leaflet-src.js.map
Executable file
1
html/assets/leaflet/leaflet-src.js.map
Executable file
File diff suppressed because one or more lines are too long
661
html/assets/leaflet/leaflet.css
Executable file
661
html/assets/leaflet/leaflet.css
Executable file
@@ -0,0 +1,661 @@
|
|||||||
|
/* required styles */
|
||||||
|
|
||||||
|
.leaflet-pane,
|
||||||
|
.leaflet-tile,
|
||||||
|
.leaflet-marker-icon,
|
||||||
|
.leaflet-marker-shadow,
|
||||||
|
.leaflet-tile-container,
|
||||||
|
.leaflet-pane > svg,
|
||||||
|
.leaflet-pane > canvas,
|
||||||
|
.leaflet-zoom-box,
|
||||||
|
.leaflet-image-layer,
|
||||||
|
.leaflet-layer {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
|
.leaflet-container {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.leaflet-tile,
|
||||||
|
.leaflet-marker-icon,
|
||||||
|
.leaflet-marker-shadow {
|
||||||
|
-webkit-user-select: none;
|
||||||
|
-moz-user-select: none;
|
||||||
|
user-select: none;
|
||||||
|
-webkit-user-drag: none;
|
||||||
|
}
|
||||||
|
/* Prevents IE11 from highlighting tiles in blue */
|
||||||
|
.leaflet-tile::selection {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
/* Safari renders non-retina tile on retina better with this, but Chrome is worse */
|
||||||
|
.leaflet-safari .leaflet-tile {
|
||||||
|
image-rendering: -webkit-optimize-contrast;
|
||||||
|
}
|
||||||
|
/* hack that prevents hw layers "stretching" when loading new tiles */
|
||||||
|
.leaflet-safari .leaflet-tile-container {
|
||||||
|
width: 1600px;
|
||||||
|
height: 1600px;
|
||||||
|
-webkit-transform-origin: 0 0;
|
||||||
|
}
|
||||||
|
.leaflet-marker-icon,
|
||||||
|
.leaflet-marker-shadow {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
/* .leaflet-container svg: reset svg max-width decleration shipped in Joomla! (joomla.org) 3.x */
|
||||||
|
/* .leaflet-container img: map is broken in FF if you have max-width: 100% on tiles */
|
||||||
|
.leaflet-container .leaflet-overlay-pane svg {
|
||||||
|
max-width: none !important;
|
||||||
|
max-height: none !important;
|
||||||
|
}
|
||||||
|
.leaflet-container .leaflet-marker-pane img,
|
||||||
|
.leaflet-container .leaflet-shadow-pane img,
|
||||||
|
.leaflet-container .leaflet-tile-pane img,
|
||||||
|
.leaflet-container img.leaflet-image-layer,
|
||||||
|
.leaflet-container .leaflet-tile {
|
||||||
|
max-width: none !important;
|
||||||
|
max-height: none !important;
|
||||||
|
width: auto;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.leaflet-container img.leaflet-tile {
|
||||||
|
/* See: https://bugs.chromium.org/p/chromium/issues/detail?id=600120 */
|
||||||
|
mix-blend-mode: plus-lighter;
|
||||||
|
}
|
||||||
|
|
||||||
|
.leaflet-container.leaflet-touch-zoom {
|
||||||
|
-ms-touch-action: pan-x pan-y;
|
||||||
|
touch-action: pan-x pan-y;
|
||||||
|
}
|
||||||
|
.leaflet-container.leaflet-touch-drag {
|
||||||
|
-ms-touch-action: pinch-zoom;
|
||||||
|
/* Fallback for FF which doesn't support pinch-zoom */
|
||||||
|
touch-action: none;
|
||||||
|
touch-action: pinch-zoom;
|
||||||
|
}
|
||||||
|
.leaflet-container.leaflet-touch-drag.leaflet-touch-zoom {
|
||||||
|
-ms-touch-action: none;
|
||||||
|
touch-action: none;
|
||||||
|
}
|
||||||
|
.leaflet-container {
|
||||||
|
-webkit-tap-highlight-color: transparent;
|
||||||
|
}
|
||||||
|
.leaflet-container a {
|
||||||
|
-webkit-tap-highlight-color: rgba(51, 181, 229, 0.4);
|
||||||
|
}
|
||||||
|
.leaflet-tile {
|
||||||
|
filter: inherit;
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
.leaflet-tile-loaded {
|
||||||
|
visibility: inherit;
|
||||||
|
}
|
||||||
|
.leaflet-zoom-box {
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
-moz-box-sizing: border-box;
|
||||||
|
box-sizing: border-box;
|
||||||
|
z-index: 800;
|
||||||
|
}
|
||||||
|
/* workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=888319 */
|
||||||
|
.leaflet-overlay-pane svg {
|
||||||
|
-moz-user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.leaflet-pane { z-index: 400; }
|
||||||
|
|
||||||
|
.leaflet-tile-pane { z-index: 200; }
|
||||||
|
.leaflet-overlay-pane { z-index: 400; }
|
||||||
|
.leaflet-shadow-pane { z-index: 500; }
|
||||||
|
.leaflet-marker-pane { z-index: 600; }
|
||||||
|
.leaflet-tooltip-pane { z-index: 650; }
|
||||||
|
.leaflet-popup-pane { z-index: 700; }
|
||||||
|
|
||||||
|
.leaflet-map-pane canvas { z-index: 100; }
|
||||||
|
.leaflet-map-pane svg { z-index: 200; }
|
||||||
|
|
||||||
|
.leaflet-vml-shape {
|
||||||
|
width: 1px;
|
||||||
|
height: 1px;
|
||||||
|
}
|
||||||
|
.lvml {
|
||||||
|
behavior: url(#default#VML);
|
||||||
|
display: inline-block;
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* control positioning */
|
||||||
|
|
||||||
|
.leaflet-control {
|
||||||
|
position: relative;
|
||||||
|
z-index: 800;
|
||||||
|
pointer-events: visiblePainted; /* IE 9-10 doesn't have auto */
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
.leaflet-top,
|
||||||
|
.leaflet-bottom {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 1000;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
.leaflet-top {
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
|
.leaflet-right {
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
.leaflet-bottom {
|
||||||
|
bottom: 0;
|
||||||
|
}
|
||||||
|
.leaflet-left {
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
.leaflet-control {
|
||||||
|
float: left;
|
||||||
|
clear: both;
|
||||||
|
}
|
||||||
|
.leaflet-right .leaflet-control {
|
||||||
|
float: right;
|
||||||
|
}
|
||||||
|
.leaflet-top .leaflet-control {
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
.leaflet-bottom .leaflet-control {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
.leaflet-left .leaflet-control {
|
||||||
|
margin-left: 10px;
|
||||||
|
}
|
||||||
|
.leaflet-right .leaflet-control {
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* zoom and fade animations */
|
||||||
|
|
||||||
|
.leaflet-fade-anim .leaflet-popup {
|
||||||
|
opacity: 0;
|
||||||
|
-webkit-transition: opacity 0.2s linear;
|
||||||
|
-moz-transition: opacity 0.2s linear;
|
||||||
|
transition: opacity 0.2s linear;
|
||||||
|
}
|
||||||
|
.leaflet-fade-anim .leaflet-map-pane .leaflet-popup {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
.leaflet-zoom-animated {
|
||||||
|
-webkit-transform-origin: 0 0;
|
||||||
|
-ms-transform-origin: 0 0;
|
||||||
|
transform-origin: 0 0;
|
||||||
|
}
|
||||||
|
svg.leaflet-zoom-animated {
|
||||||
|
will-change: transform;
|
||||||
|
}
|
||||||
|
|
||||||
|
.leaflet-zoom-anim .leaflet-zoom-animated {
|
||||||
|
-webkit-transition: -webkit-transform 0.25s cubic-bezier(0,0,0.25,1);
|
||||||
|
-moz-transition: -moz-transform 0.25s cubic-bezier(0,0,0.25,1);
|
||||||
|
transition: transform 0.25s cubic-bezier(0,0,0.25,1);
|
||||||
|
}
|
||||||
|
.leaflet-zoom-anim .leaflet-tile,
|
||||||
|
.leaflet-pan-anim .leaflet-tile {
|
||||||
|
-webkit-transition: none;
|
||||||
|
-moz-transition: none;
|
||||||
|
transition: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.leaflet-zoom-anim .leaflet-zoom-hide {
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* cursors */
|
||||||
|
|
||||||
|
.leaflet-interactive {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.leaflet-grab {
|
||||||
|
cursor: -webkit-grab;
|
||||||
|
cursor: -moz-grab;
|
||||||
|
cursor: grab;
|
||||||
|
}
|
||||||
|
.leaflet-crosshair,
|
||||||
|
.leaflet-crosshair .leaflet-interactive {
|
||||||
|
cursor: crosshair;
|
||||||
|
}
|
||||||
|
.leaflet-popup-pane,
|
||||||
|
.leaflet-control {
|
||||||
|
cursor: auto;
|
||||||
|
}
|
||||||
|
.leaflet-dragging .leaflet-grab,
|
||||||
|
.leaflet-dragging .leaflet-grab .leaflet-interactive,
|
||||||
|
.leaflet-dragging .leaflet-marker-draggable {
|
||||||
|
cursor: move;
|
||||||
|
cursor: -webkit-grabbing;
|
||||||
|
cursor: -moz-grabbing;
|
||||||
|
cursor: grabbing;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* marker & overlays interactivity */
|
||||||
|
.leaflet-marker-icon,
|
||||||
|
.leaflet-marker-shadow,
|
||||||
|
.leaflet-image-layer,
|
||||||
|
.leaflet-pane > svg path,
|
||||||
|
.leaflet-tile-container {
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.leaflet-marker-icon.leaflet-interactive,
|
||||||
|
.leaflet-image-layer.leaflet-interactive,
|
||||||
|
.leaflet-pane > svg path.leaflet-interactive,
|
||||||
|
svg.leaflet-image-layer.leaflet-interactive path {
|
||||||
|
pointer-events: visiblePainted; /* IE 9-10 doesn't have auto */
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* visual tweaks */
|
||||||
|
|
||||||
|
.leaflet-container {
|
||||||
|
background: #ddd;
|
||||||
|
outline-offset: 1px;
|
||||||
|
}
|
||||||
|
.leaflet-container a {
|
||||||
|
color: #0078A8;
|
||||||
|
}
|
||||||
|
.leaflet-zoom-box {
|
||||||
|
border: 2px dotted #38f;
|
||||||
|
background: rgba(255,255,255,0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* general typography */
|
||||||
|
.leaflet-container {
|
||||||
|
font-family: "Helvetica Neue", Arial, Helvetica, sans-serif;
|
||||||
|
font-size: 12px;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* general toolbar styles */
|
||||||
|
|
||||||
|
.leaflet-bar {
|
||||||
|
box-shadow: 0 1px 5px rgba(0,0,0,0.65);
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
.leaflet-bar a {
|
||||||
|
background-color: #fff;
|
||||||
|
border-bottom: 1px solid #ccc;
|
||||||
|
width: 26px;
|
||||||
|
height: 26px;
|
||||||
|
line-height: 26px;
|
||||||
|
display: block;
|
||||||
|
text-align: center;
|
||||||
|
text-decoration: none;
|
||||||
|
color: black;
|
||||||
|
}
|
||||||
|
.leaflet-bar a,
|
||||||
|
.leaflet-control-layers-toggle {
|
||||||
|
background-position: 50% 50%;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.leaflet-bar a:hover,
|
||||||
|
.leaflet-bar a:focus {
|
||||||
|
background-color: #f4f4f4;
|
||||||
|
}
|
||||||
|
.leaflet-bar a:first-child {
|
||||||
|
border-top-left-radius: 4px;
|
||||||
|
border-top-right-radius: 4px;
|
||||||
|
}
|
||||||
|
.leaflet-bar a:last-child {
|
||||||
|
border-bottom-left-radius: 4px;
|
||||||
|
border-bottom-right-radius: 4px;
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
.leaflet-bar a.leaflet-disabled {
|
||||||
|
cursor: default;
|
||||||
|
background-color: #f4f4f4;
|
||||||
|
color: #bbb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.leaflet-touch .leaflet-bar a {
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
line-height: 30px;
|
||||||
|
}
|
||||||
|
.leaflet-touch .leaflet-bar a:first-child {
|
||||||
|
border-top-left-radius: 2px;
|
||||||
|
border-top-right-radius: 2px;
|
||||||
|
}
|
||||||
|
.leaflet-touch .leaflet-bar a:last-child {
|
||||||
|
border-bottom-left-radius: 2px;
|
||||||
|
border-bottom-right-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* zoom control */
|
||||||
|
|
||||||
|
.leaflet-control-zoom-in,
|
||||||
|
.leaflet-control-zoom-out {
|
||||||
|
font: bold 18px 'Lucida Console', Monaco, monospace;
|
||||||
|
text-indent: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.leaflet-touch .leaflet-control-zoom-in, .leaflet-touch .leaflet-control-zoom-out {
|
||||||
|
font-size: 22px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* layers control */
|
||||||
|
|
||||||
|
.leaflet-control-layers {
|
||||||
|
box-shadow: 0 1px 5px rgba(0,0,0,0.4);
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
.leaflet-control-layers-toggle {
|
||||||
|
background-image: url(images/layers.png);
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
}
|
||||||
|
.leaflet-retina .leaflet-control-layers-toggle {
|
||||||
|
background-image: url(images/layers-2x.png);
|
||||||
|
background-size: 26px 26px;
|
||||||
|
}
|
||||||
|
.leaflet-touch .leaflet-control-layers-toggle {
|
||||||
|
width: 44px;
|
||||||
|
height: 44px;
|
||||||
|
}
|
||||||
|
.leaflet-control-layers .leaflet-control-layers-list,
|
||||||
|
.leaflet-control-layers-expanded .leaflet-control-layers-toggle {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.leaflet-control-layers-expanded .leaflet-control-layers-list {
|
||||||
|
display: block;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.leaflet-control-layers-expanded {
|
||||||
|
padding: 6px 10px 6px 6px;
|
||||||
|
color: #333;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
.leaflet-control-layers-scrollbar {
|
||||||
|
overflow-y: scroll;
|
||||||
|
overflow-x: hidden;
|
||||||
|
padding-right: 5px;
|
||||||
|
}
|
||||||
|
.leaflet-control-layers-selector {
|
||||||
|
margin-top: 2px;
|
||||||
|
position: relative;
|
||||||
|
top: 1px;
|
||||||
|
}
|
||||||
|
.leaflet-control-layers label {
|
||||||
|
display: block;
|
||||||
|
font-size: 13px;
|
||||||
|
font-size: 1.08333em;
|
||||||
|
}
|
||||||
|
.leaflet-control-layers-separator {
|
||||||
|
height: 0;
|
||||||
|
border-top: 1px solid #ddd;
|
||||||
|
margin: 5px -10px 5px -6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Default icon URLs */
|
||||||
|
.leaflet-default-icon-path { /* used only in path-guessing heuristic, see L.Icon.Default */
|
||||||
|
background-image: url(images/marker-icon.png);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* attribution and scale controls */
|
||||||
|
|
||||||
|
.leaflet-container .leaflet-control-attribution {
|
||||||
|
background: #fff;
|
||||||
|
background: rgba(255, 255, 255, 0.8);
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
.leaflet-control-attribution,
|
||||||
|
.leaflet-control-scale-line {
|
||||||
|
padding: 0 5px;
|
||||||
|
color: #333;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
.leaflet-control-attribution a {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
.leaflet-control-attribution a:hover,
|
||||||
|
.leaflet-control-attribution a:focus {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
.leaflet-attribution-flag {
|
||||||
|
display: inline !important;
|
||||||
|
vertical-align: baseline !important;
|
||||||
|
width: 1em;
|
||||||
|
height: 0.6669em;
|
||||||
|
}
|
||||||
|
.leaflet-left .leaflet-control-scale {
|
||||||
|
margin-left: 5px;
|
||||||
|
}
|
||||||
|
.leaflet-bottom .leaflet-control-scale {
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
.leaflet-control-scale-line {
|
||||||
|
border: 2px solid #777;
|
||||||
|
border-top: none;
|
||||||
|
line-height: 1.1;
|
||||||
|
padding: 2px 5px 1px;
|
||||||
|
white-space: nowrap;
|
||||||
|
-moz-box-sizing: border-box;
|
||||||
|
box-sizing: border-box;
|
||||||
|
background: rgba(255, 255, 255, 0.8);
|
||||||
|
text-shadow: 1px 1px #fff;
|
||||||
|
}
|
||||||
|
.leaflet-control-scale-line:not(:first-child) {
|
||||||
|
border-top: 2px solid #777;
|
||||||
|
border-bottom: none;
|
||||||
|
margin-top: -2px;
|
||||||
|
}
|
||||||
|
.leaflet-control-scale-line:not(:first-child):not(:last-child) {
|
||||||
|
border-bottom: 2px solid #777;
|
||||||
|
}
|
||||||
|
|
||||||
|
.leaflet-touch .leaflet-control-attribution,
|
||||||
|
.leaflet-touch .leaflet-control-layers,
|
||||||
|
.leaflet-touch .leaflet-bar {
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
.leaflet-touch .leaflet-control-layers,
|
||||||
|
.leaflet-touch .leaflet-bar {
|
||||||
|
border: 2px solid rgba(0,0,0,0.2);
|
||||||
|
background-clip: padding-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* popup */
|
||||||
|
|
||||||
|
.leaflet-popup {
|
||||||
|
position: absolute;
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
.leaflet-popup-content-wrapper {
|
||||||
|
padding: 1px;
|
||||||
|
text-align: left;
|
||||||
|
border-radius: 12px;
|
||||||
|
}
|
||||||
|
.leaflet-popup-content {
|
||||||
|
margin: 13px 24px 13px 20px;
|
||||||
|
line-height: 1.3;
|
||||||
|
font-size: 13px;
|
||||||
|
font-size: 1.08333em;
|
||||||
|
min-height: 1px;
|
||||||
|
}
|
||||||
|
.leaflet-popup-content p {
|
||||||
|
margin: 17px 0;
|
||||||
|
margin: 1.3em 0;
|
||||||
|
}
|
||||||
|
.leaflet-popup-tip-container {
|
||||||
|
width: 40px;
|
||||||
|
height: 20px;
|
||||||
|
position: absolute;
|
||||||
|
left: 50%;
|
||||||
|
margin-top: -1px;
|
||||||
|
margin-left: -20px;
|
||||||
|
overflow: hidden;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
.leaflet-popup-tip {
|
||||||
|
width: 17px;
|
||||||
|
height: 17px;
|
||||||
|
padding: 1px;
|
||||||
|
|
||||||
|
margin: -10px auto 0;
|
||||||
|
pointer-events: auto;
|
||||||
|
|
||||||
|
-webkit-transform: rotate(45deg);
|
||||||
|
-moz-transform: rotate(45deg);
|
||||||
|
-ms-transform: rotate(45deg);
|
||||||
|
transform: rotate(45deg);
|
||||||
|
}
|
||||||
|
.leaflet-popup-content-wrapper,
|
||||||
|
.leaflet-popup-tip {
|
||||||
|
background: white;
|
||||||
|
color: #333;
|
||||||
|
box-shadow: 0 3px 14px rgba(0,0,0,0.4);
|
||||||
|
}
|
||||||
|
.leaflet-container a.leaflet-popup-close-button {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
border: none;
|
||||||
|
text-align: center;
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
font: 16px/24px Tahoma, Verdana, sans-serif;
|
||||||
|
color: #757575;
|
||||||
|
text-decoration: none;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
.leaflet-container a.leaflet-popup-close-button:hover,
|
||||||
|
.leaflet-container a.leaflet-popup-close-button:focus {
|
||||||
|
color: #585858;
|
||||||
|
}
|
||||||
|
.leaflet-popup-scrolled {
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.leaflet-oldie .leaflet-popup-content-wrapper {
|
||||||
|
-ms-zoom: 1;
|
||||||
|
}
|
||||||
|
.leaflet-oldie .leaflet-popup-tip {
|
||||||
|
width: 24px;
|
||||||
|
margin: 0 auto;
|
||||||
|
|
||||||
|
-ms-filter: "progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678)";
|
||||||
|
filter: progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678);
|
||||||
|
}
|
||||||
|
|
||||||
|
.leaflet-oldie .leaflet-control-zoom,
|
||||||
|
.leaflet-oldie .leaflet-control-layers,
|
||||||
|
.leaflet-oldie .leaflet-popup-content-wrapper,
|
||||||
|
.leaflet-oldie .leaflet-popup-tip {
|
||||||
|
border: 1px solid #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* div icon */
|
||||||
|
|
||||||
|
.leaflet-div-icon {
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* Tooltip */
|
||||||
|
/* Base styles for the element that has a tooltip */
|
||||||
|
.leaflet-tooltip {
|
||||||
|
position: absolute;
|
||||||
|
padding: 6px;
|
||||||
|
background-color: #fff;
|
||||||
|
border: 1px solid #fff;
|
||||||
|
border-radius: 3px;
|
||||||
|
color: #222;
|
||||||
|
white-space: nowrap;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
-moz-user-select: none;
|
||||||
|
-ms-user-select: none;
|
||||||
|
user-select: none;
|
||||||
|
pointer-events: none;
|
||||||
|
box-shadow: 0 1px 3px rgba(0,0,0,0.4);
|
||||||
|
}
|
||||||
|
.leaflet-tooltip.leaflet-interactive {
|
||||||
|
cursor: pointer;
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
.leaflet-tooltip-top:before,
|
||||||
|
.leaflet-tooltip-bottom:before,
|
||||||
|
.leaflet-tooltip-left:before,
|
||||||
|
.leaflet-tooltip-right:before {
|
||||||
|
position: absolute;
|
||||||
|
pointer-events: none;
|
||||||
|
border: 6px solid transparent;
|
||||||
|
background: transparent;
|
||||||
|
content: "";
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Directions */
|
||||||
|
|
||||||
|
.leaflet-tooltip-bottom {
|
||||||
|
margin-top: 6px;
|
||||||
|
}
|
||||||
|
.leaflet-tooltip-top {
|
||||||
|
margin-top: -6px;
|
||||||
|
}
|
||||||
|
.leaflet-tooltip-bottom:before,
|
||||||
|
.leaflet-tooltip-top:before {
|
||||||
|
left: 50%;
|
||||||
|
margin-left: -6px;
|
||||||
|
}
|
||||||
|
.leaflet-tooltip-top:before {
|
||||||
|
bottom: 0;
|
||||||
|
margin-bottom: -12px;
|
||||||
|
border-top-color: #fff;
|
||||||
|
}
|
||||||
|
.leaflet-tooltip-bottom:before {
|
||||||
|
top: 0;
|
||||||
|
margin-top: -12px;
|
||||||
|
margin-left: -6px;
|
||||||
|
border-bottom-color: #fff;
|
||||||
|
}
|
||||||
|
.leaflet-tooltip-left {
|
||||||
|
margin-left: -6px;
|
||||||
|
}
|
||||||
|
.leaflet-tooltip-right {
|
||||||
|
margin-left: 6px;
|
||||||
|
}
|
||||||
|
.leaflet-tooltip-left:before,
|
||||||
|
.leaflet-tooltip-right:before {
|
||||||
|
top: 50%;
|
||||||
|
margin-top: -6px;
|
||||||
|
}
|
||||||
|
.leaflet-tooltip-left:before {
|
||||||
|
right: 0;
|
||||||
|
margin-right: -12px;
|
||||||
|
border-left-color: #fff;
|
||||||
|
}
|
||||||
|
.leaflet-tooltip-right:before {
|
||||||
|
left: 0;
|
||||||
|
margin-left: -12px;
|
||||||
|
border-right-color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Printing */
|
||||||
|
|
||||||
|
@media print {
|
||||||
|
/* Prevent printers from removing background-images of controls. */
|
||||||
|
.leaflet-control {
|
||||||
|
-webkit-print-color-adjust: exact;
|
||||||
|
print-color-adjust: exact;
|
||||||
|
}
|
||||||
|
}
|
||||||
6
html/assets/leaflet/leaflet.js
Executable file
6
html/assets/leaflet/leaflet.js
Executable file
File diff suppressed because one or more lines are too long
1
html/assets/leaflet/leaflet.js.map
Executable file
1
html/assets/leaflet/leaflet.js.map
Executable file
File diff suppressed because one or more lines are too long
406
html/database.html
Executable file
406
html/database.html
Executable file
@@ -0,0 +1,406 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>NebuleAir</title>
|
||||||
|
<link rel="stylesheet" href="assets/css/bootstrap.min.css">
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
#sidebar a.nav-link {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
#sidebar a.nav-link:hover {
|
||||||
|
background-color: rgba(0, 0, 0, 0.5);
|
||||||
|
}
|
||||||
|
#sidebar a.nav-link svg {
|
||||||
|
margin-right: 8px; /* Add spacing between icons and text */
|
||||||
|
}
|
||||||
|
#sidebar {
|
||||||
|
transition: transform 0.3s ease-in-out;
|
||||||
|
}
|
||||||
|
.offcanvas-backdrop {
|
||||||
|
z-index: 1040;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<!-- Topbar -->
|
||||||
|
<span id="topbar"></span>
|
||||||
|
|
||||||
|
<!-- Sidebar Offcanvas for Mobile -->
|
||||||
|
<div class="offcanvas offcanvas-start text-white bg-dark" tabindex="-1" id="sidebarOffcanvas" aria-labelledby="sidebarOffcanvasLabel">
|
||||||
|
<div class="offcanvas-header">
|
||||||
|
<h5 class="offcanvas-title" id="sidebarOffcanvasLabel">NebuleAir</h5>
|
||||||
|
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="offcanvas" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
<div class="offcanvas-body" id="sidebar_mobile">
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="container-fluid mt-5">
|
||||||
|
<div class="row">
|
||||||
|
<aside class="col-md-2 col-lg-1 d-none d-md-block vh-100 position-fixed bg-dark text-white" id="sidebar">
|
||||||
|
</aside>
|
||||||
|
<!-- Main content -->
|
||||||
|
<main class="col-md-10 ms-sm-auto col-lg-11 offset-md-2 offset-lg-1 px-md-4">
|
||||||
|
<h1 class="mt-4">Base de données</h1>
|
||||||
|
<p>Le capteur enregistre en local les données de mesures. Vous pouvez ici les consulter et les télécharger.</p>
|
||||||
|
|
||||||
|
<div class="row mb-3">
|
||||||
|
|
||||||
|
<div class="col-sm-5">
|
||||||
|
<div class="card text-dark bg-light">
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="card-title">Consulter la base de donnée</h5>
|
||||||
|
<!-- Dropdown to select number of records -->
|
||||||
|
<div class="d-flex align-items-center mb-3">
|
||||||
|
<label for="records_limit" class="form-label me-2">Nombre de mesures:</label>
|
||||||
|
<select id="records_limit" class="form-select w-auto">
|
||||||
|
<option value="10" selected>10 dernières</option>
|
||||||
|
<option value="20">20 dernières</option>
|
||||||
|
<option value="30">30 dernières</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-primary" onclick="get_data_sqlite('data_NPM',getSelectedLimit(),false)">Mesures PM</button>
|
||||||
|
<button class="btn btn-primary" onclick="get_data_sqlite('data_BME280',getSelectedLimit(),false)">Mesures Temp/Hum</button>
|
||||||
|
<button class="btn btn-primary" onclick="get_data_sqlite('data_NPM_5channels',getSelectedLimit(),false)">Mesures PM (5 canaux)</button>
|
||||||
|
<button class="btn btn-primary" onclick="get_data_sqlite('data_envea',getSelectedLimit(),false)">Sonde Cairsens</button>
|
||||||
|
<button class="btn btn-primary" onclick="get_data_sqlite('data_WIND',getSelectedLimit(),false)">Sonde Vent</button>
|
||||||
|
|
||||||
|
<button class="btn btn-warning" onclick="get_data_sqlite('timestamp_table',getSelectedLimit(),false)">Timestamp Table</button>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-sm-5">
|
||||||
|
<div class="card text-dark bg-light">
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="card-title">Télécharger les données</h5>
|
||||||
|
<!-- Date selection for download -->
|
||||||
|
<div class="d-flex align-items-center gap-3 mb-3">
|
||||||
|
<label for="start_date" class="form-label">Date de début:</label>
|
||||||
|
<input type="date" id="start_date" class="form-control w-auto">
|
||||||
|
<label for="end_date" class="form-label">Date de fin:</label>
|
||||||
|
<input type="date" id="end_date" class="form-control w-auto">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<button class="btn btn-primary" onclick="get_data_sqlite('data_NPM',10,true, getStartDate(), getEndDate())">Mesures PM</button>
|
||||||
|
<button class="btn btn-primary" onclick="get_data_sqlite('data_BME280',10,true, getStartDate(), getEndDate())">Mesures Temp/Hum</button>
|
||||||
|
<button class="btn btn-primary" onclick="get_data_sqlite('data_NPM_5channels',10,true, getStartDate(), getEndDate())">Mesures PM (5 canaux)</button>
|
||||||
|
<button class="btn btn-primary" onclick="get_data_sqlite('data_envea',10,true, getStartDate(), getEndDate())">Sonde Cairsens</button>
|
||||||
|
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
|
||||||
|
<div class="row mt-2">
|
||||||
|
<div id="table_data"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- JAVASCRIPT -->
|
||||||
|
|
||||||
|
<!-- Link Ajax locally -->
|
||||||
|
<script src="assets/jquery/jquery-3.7.1.min.js"></script>
|
||||||
|
<!-- Link Bootstrap JS and Popper.js locally -->
|
||||||
|
<script src="assets/js/bootstrap.bundle.js"></script>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
|
console.log("DOMContentLoaded");
|
||||||
|
|
||||||
|
const elementsToLoad = [
|
||||||
|
{ id: 'topbar', file: 'topbar.html' },
|
||||||
|
{ id: 'sidebar', file: 'sidebar.html' },
|
||||||
|
{ id: 'sidebar_mobile', file: 'sidebar.html' }
|
||||||
|
];
|
||||||
|
|
||||||
|
elementsToLoad.forEach(({ id, file }) => {
|
||||||
|
fetch(file)
|
||||||
|
.then(response => response.text())
|
||||||
|
.then(data => {
|
||||||
|
const element = document.getElementById(id);
|
||||||
|
if (element) {
|
||||||
|
element.innerHTML = data;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => console.error(`Error loading ${file}:`, error));
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
window.onload = function() {
|
||||||
|
|
||||||
|
|
||||||
|
//NEW way to get data from SQLITE
|
||||||
|
$.ajax({
|
||||||
|
url: 'launcher.php?type=get_config_sqlite',
|
||||||
|
dataType:'json',
|
||||||
|
//dataType: 'json', // Specify that you expect a JSON response
|
||||||
|
method: 'GET', // Use GET or POST depending on your needs
|
||||||
|
success: function(response) {
|
||||||
|
console.log("Getting SQLite config table:");
|
||||||
|
console.log(response);
|
||||||
|
|
||||||
|
//get device Name (for the side bar)
|
||||||
|
const deviceName = response.deviceName;
|
||||||
|
const elements = document.querySelectorAll('.sideBar_sensorName');
|
||||||
|
elements.forEach((element) => {
|
||||||
|
element.innerText = deviceName;
|
||||||
|
});
|
||||||
|
|
||||||
|
//device name html page title
|
||||||
|
if (response.deviceName) {
|
||||||
|
document.title = response.deviceName;
|
||||||
|
}
|
||||||
|
|
||||||
|
},
|
||||||
|
error: function(xhr, status, error) {
|
||||||
|
console.error('AJAX request failed:', status, error);
|
||||||
|
}
|
||||||
|
}); //end ajax
|
||||||
|
|
||||||
|
|
||||||
|
//get local RTC
|
||||||
|
$.ajax({
|
||||||
|
url: 'launcher.php?type=RTC_time',
|
||||||
|
dataType: 'text', // Specify that you expect a JSON response
|
||||||
|
method: 'GET', // Use GET or POST depending on your needs
|
||||||
|
success: function(response) {
|
||||||
|
console.log("Local RTC: " + response);
|
||||||
|
const RTC_Element = document.getElementById("RTC_time");
|
||||||
|
RTC_Element.textContent = response;
|
||||||
|
},
|
||||||
|
error: function(xhr, status, error) {
|
||||||
|
console.error('AJAX request failed:', status, error);
|
||||||
|
}
|
||||||
|
}); //end AJAX
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// TABLE PM
|
||||||
|
function get_data_sqlite(table, limit, download , startDate = "", endDate = "") {
|
||||||
|
console.log(`Getting data for table: ${table}, limit: ${limit}, download: ${download}, start: ${startDate}, end: ${endDate}`);
|
||||||
|
// Construct URL parameters dynamically
|
||||||
|
let url = `launcher.php?type=table_mesure&table=${table}&limit=${limit}&download=${download}`;
|
||||||
|
|
||||||
|
// Add date parameters if downloading
|
||||||
|
if (download) {
|
||||||
|
url += `&start_date=${startDate}&end_date=${endDate}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(url);
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
url: url,
|
||||||
|
dataType: 'text', // Specify that you expect a JSON response
|
||||||
|
method: 'GET', // Use GET or POST depending on your needs
|
||||||
|
success: function(response) {
|
||||||
|
console.log(response);
|
||||||
|
|
||||||
|
// If download is true, generate and trigger CSV download
|
||||||
|
if (download) {
|
||||||
|
downloadCSV(response, table);
|
||||||
|
return; // Exit function after triggering download
|
||||||
|
}
|
||||||
|
|
||||||
|
let rows = response.trim().split("\n");
|
||||||
|
// Generate Bootstrap table
|
||||||
|
|
||||||
|
let tableHTML = `<table class="table table-striped table-bordered">
|
||||||
|
<thead class="table-dark"><tr>`;
|
||||||
|
|
||||||
|
// Define column headers dynamically based on the table type
|
||||||
|
if (table === "data_NPM") {
|
||||||
|
tableHTML += `
|
||||||
|
<th>Timestamp</th>
|
||||||
|
<th>PM1</th>
|
||||||
|
<th>PM2.5</th>
|
||||||
|
<th>PM10</th>
|
||||||
|
<th>Temperature (°C)</th>
|
||||||
|
<th>Humidity (%)</th>
|
||||||
|
`;
|
||||||
|
} else if (table === "data_BME280") {
|
||||||
|
tableHTML += `
|
||||||
|
<th>Timestamp</th>
|
||||||
|
<th>Temperature (°C)</th>
|
||||||
|
<th>Humidity (%)</th>
|
||||||
|
<th>Pressure (hPa)</th>
|
||||||
|
`;
|
||||||
|
} else if (table === "data_NPM_5channels") {
|
||||||
|
tableHTML += `
|
||||||
|
<th>Timestamp</th>
|
||||||
|
<th>PM_ch1 (nb/L)</th>
|
||||||
|
<th>PM_ch2 (nb/L)</th>
|
||||||
|
<th>PM_ch3 (nb/L)</th>
|
||||||
|
<th>PM_ch4 (nb/L)</th>
|
||||||
|
<th>PM_ch5 (nb/L)</th>
|
||||||
|
|
||||||
|
`;
|
||||||
|
}else if (table === "data_envea") {
|
||||||
|
tableHTML += `
|
||||||
|
<th>Timestamp</th>
|
||||||
|
<th>NO2</th>
|
||||||
|
<th>H2S</th>
|
||||||
|
<th>NH3</th>
|
||||||
|
<th>CO</th>
|
||||||
|
<th>O3</th>
|
||||||
|
|
||||||
|
`;
|
||||||
|
}else if (table === "timestamp_table") {
|
||||||
|
tableHTML += `
|
||||||
|
<th>Timestamp</th>
|
||||||
|
`;
|
||||||
|
}else if (table === "data_WIND") {
|
||||||
|
tableHTML += `
|
||||||
|
<th>Timestamp</th>
|
||||||
|
<th>speed (km/h)</th>
|
||||||
|
<th>Direction (V)</th>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
tableHTML += `</tr></thead><tbody>`;
|
||||||
|
|
||||||
|
// Loop through rows and create table rows
|
||||||
|
rows.forEach(row => {
|
||||||
|
let columns = row.replace(/[()]/g, "").split(", "); // Remove parentheses and split
|
||||||
|
tableHTML += "<tr>";
|
||||||
|
|
||||||
|
if (table === "data_NPM") {
|
||||||
|
tableHTML += `
|
||||||
|
<td>${columns[0]}</td>
|
||||||
|
<td>${columns[1]}</td>
|
||||||
|
<td>${columns[2]}</td>
|
||||||
|
<td>${columns[3]}</td>
|
||||||
|
<td>${columns[4]}</td>
|
||||||
|
<td>${columns[5]}</td>
|
||||||
|
`;
|
||||||
|
} else if (table === "data_BME280") {
|
||||||
|
tableHTML += `
|
||||||
|
<td>${columns[0]}</td>
|
||||||
|
<td>${columns[1]}</td>
|
||||||
|
<td>${columns[2]}</td>
|
||||||
|
<td>${columns[3]}</td>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
else if (table === "data_NPM_5channels") {
|
||||||
|
tableHTML += `
|
||||||
|
<td>${columns[0]}</td>
|
||||||
|
<td>${columns[1]}</td>
|
||||||
|
<td>${columns[2]}</td>
|
||||||
|
<td>${columns[3]}</td>
|
||||||
|
<td>${columns[4]}</td>
|
||||||
|
<td>${columns[5]}</td>
|
||||||
|
|
||||||
|
`;
|
||||||
|
} else if (table === "data_envea") {
|
||||||
|
tableHTML += `
|
||||||
|
<td>${columns[0]}</td>
|
||||||
|
<td>${columns[1]}</td>
|
||||||
|
<td>${columns[2]}</td>
|
||||||
|
<td>${columns[3]}</td>
|
||||||
|
<td>${columns[4]}</td>
|
||||||
|
<td>${columns[5]}</td>
|
||||||
|
|
||||||
|
`;
|
||||||
|
}else if (table === "timestamp_table") {
|
||||||
|
tableHTML += `
|
||||||
|
<td>${columns[1]}</td>
|
||||||
|
`;
|
||||||
|
}else if (table === "data_WIND") {
|
||||||
|
tableHTML += `
|
||||||
|
<td>${columns[0]}</td>
|
||||||
|
<td>${columns[1]}</td>
|
||||||
|
<td>${columns[2]}</td>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
tableHTML += "</tr>";
|
||||||
|
});
|
||||||
|
|
||||||
|
tableHTML += `</tbody></table>`;
|
||||||
|
|
||||||
|
// Update the #table_data div with the generated table
|
||||||
|
document.getElementById("table_data").innerHTML = tableHTML;
|
||||||
|
},
|
||||||
|
error: function(xhr, status, error) {
|
||||||
|
console.error('AJAX request failed:', status, error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function getSelectedLimit() {
|
||||||
|
return document.getElementById("records_limit").value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getStartDate() {
|
||||||
|
return document.getElementById("start_date").value || "2025-01-01"; // Default to a safe date
|
||||||
|
}
|
||||||
|
|
||||||
|
function getEndDate() {
|
||||||
|
return document.getElementById("end_date").value || "2025-12-31"; // Default to a safe date
|
||||||
|
}
|
||||||
|
|
||||||
|
function downloadCSV(response, table) {
|
||||||
|
let rows = response.trim().split("\n");
|
||||||
|
|
||||||
|
let csvContent = "";
|
||||||
|
|
||||||
|
// Add headers based on table type
|
||||||
|
if (table === "data_NPM") {
|
||||||
|
csvContent += "TimestampUTC,PM1,PM2.5,PM10,Temperature_sensor,Humidity_sensor\n";
|
||||||
|
} else if (table === "data_BME280") {
|
||||||
|
csvContent += "TimestampUTC,Temperature (°C),Humidity (%),Pressure (hPa)\n";
|
||||||
|
}
|
||||||
|
else if (table === "data_NPM_5channels") {
|
||||||
|
csvContent += "TimestampUTC,PM_ch1,PM_ch2,PM_ch3,PM_ch4,PM_ch5\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Format rows as CSV
|
||||||
|
rows.forEach(row => {
|
||||||
|
let columns = row.replace(/[()]/g, "").split(", ");
|
||||||
|
csvContent += columns.join(",") + "\n";
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create a downloadable file
|
||||||
|
let blob = new Blob([csvContent], { type: "text/csv" });
|
||||||
|
let url = window.URL.createObjectURL(blob);
|
||||||
|
let a = document.createElement("a");
|
||||||
|
a.href = url;
|
||||||
|
a.download = table + "_data.csv"; // File name
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
document.body.removeChild(a);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
234
html/index.html
234
html/index.html
@@ -5,6 +5,8 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>NebuleAir</title>
|
<title>NebuleAir</title>
|
||||||
<link rel="stylesheet" href="assets/css/bootstrap.min.css">
|
<link rel="stylesheet" href="assets/css/bootstrap.min.css">
|
||||||
|
<script src="assets/js/chart.js"></script> <!-- Local Chart.js -->
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
body {
|
body {
|
||||||
overflow-x: hidden;
|
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">
|
<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>
|
<h1 class="mt-4">Votre capteur</h1>
|
||||||
<p>Bienvenue sur votre interface de configuration de votre capteur.</p>
|
<p>Bienvenue sur votre interface de configuration de votre capteur.</p>
|
||||||
|
|
||||||
<div class="row mb-3">
|
<div class="row mb-3">
|
||||||
|
|
||||||
<div class="col-sm-4">
|
<!-- Card NPM values -->
|
||||||
<div class="card">
|
<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">
|
<div class="card-body">
|
||||||
<h5 class="card-title">Linux stats</h5>
|
<h5 class="card-title">Linux stats</h5>
|
||||||
|
|
||||||
<p class="card-text">Disk usage (total size <span id="disk_size"></span> Gb) </p>
|
<p class="card-text">Disk usage (total size <span id="disk_size"></span> Gb) </p>
|
||||||
<div id="disk_space"></div>
|
<div id="disk_space"></div>
|
||||||
|
|
||||||
<p class="card-text">Memory usage (total size <span id="memory_size"></span> Mb) </p>
|
<p class="card-text">Memory usage (total size <span id="memory_size"></span> Mb) </p>
|
||||||
<div id="memory_space"></div>
|
<div id="memory_space"></div>
|
||||||
|
<p class="card-text"> Database size: <span id="database_size"></span> </p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</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>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
-->
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -107,6 +135,35 @@
|
|||||||
|
|
||||||
window.onload = function() {
|
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'
|
fetch('../config.json') // Replace 'deviceID.txt' with 'config.json'
|
||||||
.then(response => response.json()) // Parse response as JSON
|
.then(response => response.json()) // Parse response as JSON
|
||||||
.then(data => {
|
.then(data => {
|
||||||
@@ -123,7 +180,12 @@ window.onload = function() {
|
|||||||
elements.forEach((element) => {
|
elements.forEach((element) => {
|
||||||
element.innerText = deviceName;
|
element.innerText = deviceName;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
//end fetch config
|
||||||
|
})
|
||||||
|
.catch(error => console.error('Error loading config.json:', error));
|
||||||
|
//end windows on load
|
||||||
|
*/
|
||||||
//get local RTC
|
//get local RTC
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: 'launcher.php?type=RTC_time',
|
url: 'launcher.php?type=RTC_time',
|
||||||
@@ -138,6 +200,34 @@ window.onload = function() {
|
|||||||
console.error('AJAX request failed:', status, error);
|
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
|
//get disk free space
|
||||||
$.ajax({
|
$.ajax({
|
||||||
@@ -210,8 +300,6 @@ window.onload = function() {
|
|||||||
console.log(usedMemory);
|
console.log(usedMemory);
|
||||||
console.log(percentageUsed);
|
console.log(percentageUsed);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Create the outer div with class and attributes
|
// Create the outer div with class and attributes
|
||||||
const progressDiv = document.createElement('div');
|
const progressDiv = document.createElement('div');
|
||||||
progressDiv.className = 'progress mb-3';
|
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>
|
</script>
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
242
html/logs.html
242
html/logs.html
@@ -56,8 +56,11 @@
|
|||||||
<div class="col-lg-6 col-12">
|
<div class="col-lg-6 col-12">
|
||||||
<div class="card" style="height: 80vh;">
|
<div class="card" style="height: 80vh;">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
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>
|
||||||
<div class="card-body overflow-auto" id="card_loop_content">
|
<div class="card-body overflow-auto" id="card_loop_content">
|
||||||
|
|
||||||
@@ -69,6 +72,7 @@
|
|||||||
<div class="card" style="height: 80vh;">
|
<div class="card" style="height: 80vh;">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
Boot logs
|
Boot logs
|
||||||
|
<button type="submit" class="btn btn-secondary btn-sm" id="refresh-boot-log">Refresh</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body overflow-auto" id="card_boot_content">
|
<div class="card-body overflow-auto" id="card_boot_content">
|
||||||
|
|
||||||
@@ -110,97 +114,142 @@
|
|||||||
const loop_card_content = document.getElementById('card_loop_content');
|
const loop_card_content = document.getElementById('card_loop_content');
|
||||||
const boot_card_content = document.getElementById('card_boot_content');
|
const boot_card_content = document.getElementById('card_boot_content');
|
||||||
|
|
||||||
fetch('../logs/loop.log')
|
//Getting Master logs
|
||||||
.then((response) => {
|
console.log("Getting SARA logs");
|
||||||
if (!response.ok) {
|
displayLogFile('../logs/sara_service.log', loop_card_content, true, 1000);
|
||||||
throw new Error('Failed to fetch the log file.');
|
|
||||||
}
|
console.log("Getting app/boot logs");
|
||||||
return response.text();
|
displayLogFile('../logs/app.log', boot_card_content, true, 1000);
|
||||||
})
|
|
||||||
.then((data) => {
|
|
||||||
const lines = data.split('\n');
|
|
||||||
|
|
||||||
// Format log content
|
// Setup master log with refresh button
|
||||||
const formattedLog = lines
|
setupLogRefreshButton('refresh-master-log', '../logs/sara_service.log', 'card_loop_content', 3000);
|
||||||
.map((line) => line.trim()) // Remove extra whitespace
|
|
||||||
.filter((line) => line) // Remove empty lines
|
// Setup boot log with refresh button
|
||||||
.join('<br>'); // Join formatted lines with line breaks
|
setupLogRefreshButton('refresh-boot-log', '../logs/app.log', 'card_boot_content', 300);
|
||||||
|
|
||||||
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.';
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
window.onload = function() {
|
window.onload = function() {
|
||||||
fetch('../config.json') // Replace 'deviceID.txt' with 'config.json'
|
getModem_busy_status();
|
||||||
.then(response => response.json()) // Parse response as JSON
|
setInterval(getModem_busy_status, 2000);
|
||||||
.then(data => {
|
|
||||||
console.log("Getting config file (onload)");
|
//NEW way to get config (SQLite)
|
||||||
//get device ID
|
$.ajax({
|
||||||
const deviceID = data.deviceID.trim().toUpperCase();
|
url: 'launcher.php?type=get_config_sqlite',
|
||||||
// document.getElementById('pageTitle_plus_ID').innerText = 'token: ' + deviceID;
|
dataType:'json',
|
||||||
//get device Name
|
//dataType: 'json', // Specify that you expect a JSON response
|
||||||
const deviceName = data.deviceName;
|
method: 'GET', // Use GET or POST depending on your needs
|
||||||
|
success: function(response) {
|
||||||
const elements = document.querySelectorAll('.sideBar_sensorName');
|
console.log("Getting SQLite config table:");
|
||||||
elements.forEach((element) => {
|
console.log(response);
|
||||||
element.innerText = deviceName;
|
|
||||||
});
|
//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
|
//get local RTC
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: 'launcher.php?type=RTC_time',
|
url: 'launcher.php?type=RTC_time',
|
||||||
dataType: 'text', // Specify that you expect a JSON response
|
dataType: 'text', // Specify that you expect a JSON response
|
||||||
method: 'GET', // Use GET or POST depending on your needs
|
method: 'GET', // Use GET or POST depending on your needs
|
||||||
success: function(response) {
|
success: function(response) {
|
||||||
console.log("Local RTC: " + response);
|
console.log("Local RTC: " + response);
|
||||||
const RTC_Element = document.getElementById("RTC_time");
|
const RTC_Element = document.getElementById("RTC_time");
|
||||||
RTC_Element.textContent = response;
|
RTC_Element.textContent = response;
|
||||||
},
|
},
|
||||||
error: function(xhr, status, error) {
|
error: function(xhr, status, error) {
|
||||||
console.error('AJAX request failed:', 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(){
|
function clear_loopLogs(){
|
||||||
console.log("Clearing loop logs");
|
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>
|
</script>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
265
html/map.html
Executable file
265
html/map.html
Executable file
@@ -0,0 +1,265 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>NebuleAir</title>
|
||||||
|
<link rel="stylesheet" href="assets/css/bootstrap.min.css">
|
||||||
|
<link rel="stylesheet" href="assets/leaflet/leaflet.css" />
|
||||||
|
<script src="assets/leaflet/leaflet.js"></script>
|
||||||
|
|
||||||
|
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
#sidebar a.nav-link {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
#sidebar a.nav-link:hover {
|
||||||
|
background-color: rgba(0, 0, 0, 0.5);
|
||||||
|
}
|
||||||
|
#sidebar a.nav-link svg {
|
||||||
|
margin-right: 8px; /* Add spacing between icons and text */
|
||||||
|
}
|
||||||
|
#sidebar {
|
||||||
|
transition: transform 0.3s ease-in-out;
|
||||||
|
}
|
||||||
|
.offcanvas-backdrop {
|
||||||
|
z-index: 1040;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<!-- Topbar -->
|
||||||
|
<span id="topbar"></span>
|
||||||
|
|
||||||
|
<!-- Sidebar Offcanvas for Mobile -->
|
||||||
|
<div class="offcanvas offcanvas-start text-white bg-dark" tabindex="-1" id="sidebarOffcanvas" aria-labelledby="sidebarOffcanvasLabel">
|
||||||
|
<div class="offcanvas-header">
|
||||||
|
<h5 class="offcanvas-title" id="sidebarOffcanvasLabel">NebuleAir</h5>
|
||||||
|
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="offcanvas" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
<div class="offcanvas-body" id="sidebar_mobile">
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="container-fluid mt-5">
|
||||||
|
<div class="row">
|
||||||
|
<aside class="col-md-2 col-lg-1 d-none d-md-block vh-100 position-fixed bg-dark text-white" id="sidebar">
|
||||||
|
</aside>
|
||||||
|
<!-- Main content -->
|
||||||
|
<main class="col-md-10 ms-sm-auto col-lg-11 offset-md-2 offset-lg-1 px-md-4">
|
||||||
|
<h1 class="mt-4">Localisation</h1>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
|
||||||
|
|
||||||
|
<div class="col-sm-6 mb-2">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body">
|
||||||
|
<h3 class="mt-1">Zone localisation du capteur</h3>
|
||||||
|
<p class="card-text">Mis à jour automatiquement par le capteur. </p>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-6 mb-3">
|
||||||
|
<label for="device_name" class="form-label">Latitude</label>
|
||||||
|
<input type="text" class="form-control" id="device_latitude_raw" disabled>
|
||||||
|
</div>
|
||||||
|
<div class="col-6 mb-3">
|
||||||
|
<label for="device_name" class="form-label">Longitude</label>
|
||||||
|
<input type="text" class="form-control" id="device_longitude_raw" disabled>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="col-sm-6 mb-2">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body">
|
||||||
|
<h3 class="mt-1">Point précis</h3>
|
||||||
|
<p class="card-text">Mis à jour manuellement (sur aircarto.fr ou sur cette interface si le capteur est connecté au WIFI) </p>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-6 mb-3">
|
||||||
|
<label for="device_name" class="form-label">Latitude</label>
|
||||||
|
<input type="text" class="form-control" id="device_latitude_precision" onchange="update_config('latitude_precision', this.value)">
|
||||||
|
</div>
|
||||||
|
<div class="col-6 mb-3">
|
||||||
|
<label for="device_name" class="form-label">Longitude</label>
|
||||||
|
<input type="text" class="form-control" id="device_longitude_precision" onchange="update_config('longitude_precision', this.value)">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div id="map" style="height: 70vh;"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- JAVASCRIPT -->
|
||||||
|
|
||||||
|
<!-- Link Ajax locally -->
|
||||||
|
<script src="assets/jquery/jquery-3.7.1.min.js"></script>
|
||||||
|
<!-- Link Bootstrap JS and Popper.js locally -->
|
||||||
|
<script src="assets/js/bootstrap.bundle.js"></script>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
|
const elementsToLoad = [
|
||||||
|
{ id: 'topbar', file: 'topbar.html' },
|
||||||
|
{ id: 'sidebar', file: 'sidebar.html' },
|
||||||
|
{ id: 'sidebar_mobile', file: 'sidebar.html' }
|
||||||
|
];
|
||||||
|
|
||||||
|
elementsToLoad.forEach(({ id, file }) => {
|
||||||
|
fetch(file)
|
||||||
|
.then(response => response.text())
|
||||||
|
.then(data => {
|
||||||
|
const element = document.getElementById(id);
|
||||||
|
if (element) {
|
||||||
|
element.innerHTML = data;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => console.error(`Error loading ${file}:`, error));
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
let map;
|
||||||
|
let marker;
|
||||||
|
|
||||||
|
// Function to load and update map
|
||||||
|
function loadConfigAndUpdateMap() {
|
||||||
|
fetch('../config.json')
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
console.log("Getting config file (update)");
|
||||||
|
|
||||||
|
// Get device details
|
||||||
|
const deviceID = data.deviceID.trim().toUpperCase();
|
||||||
|
const deviceName = data.deviceName;
|
||||||
|
const device_latitude_precision = parseFloat(data.latitude_precision);
|
||||||
|
const device_longitude_precision = parseFloat(data.longitude_precision);
|
||||||
|
const device_latitude_raw = parseFloat(data.latitude_raw);
|
||||||
|
const device_longitude_raw = parseFloat(data.longitude_raw);
|
||||||
|
|
||||||
|
console.log("Latitude (precision): " + device_latitude_precision);
|
||||||
|
console.log("Longitude (precision): " + device_longitude_precision);
|
||||||
|
|
||||||
|
// Update input fields
|
||||||
|
document.getElementById("device_latitude_precision").value = device_latitude_precision;
|
||||||
|
document.getElementById("device_longitude_precision").value = device_longitude_precision;
|
||||||
|
document.getElementById("device_latitude_raw").value = device_latitude_raw;
|
||||||
|
document.getElementById("device_longitude_raw").value = device_longitude_raw;
|
||||||
|
|
||||||
|
// If map is not initialized, create it
|
||||||
|
if (!map) {
|
||||||
|
map = L.map('map').setView([device_latitude_precision, device_longitude_precision], 15);
|
||||||
|
|
||||||
|
L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||||
|
maxZoom: 19,
|
||||||
|
attribution: '© <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>'
|
||||||
|
}).addTo(map);
|
||||||
|
|
||||||
|
// Add draggable marker (point precision)
|
||||||
|
marker = L.marker([device_latitude_precision, device_longitude_precision], { draggable: true }).addTo(map);
|
||||||
|
|
||||||
|
//add a circle
|
||||||
|
var circle = L.circle([device_latitude_raw, device_longitude_raw], {
|
||||||
|
color: 'blue',
|
||||||
|
fillColor: '#3399FF',
|
||||||
|
fillOpacity: 0.3,
|
||||||
|
radius: 500
|
||||||
|
}).addTo(map);
|
||||||
|
|
||||||
|
// Event listener when marker is moved
|
||||||
|
marker.on('dragend', function (event) {
|
||||||
|
let newLatLng = marker.getLatLng();
|
||||||
|
console.log("Marker moved to:", newLatLng.lat, newLatLng.lng);
|
||||||
|
|
||||||
|
// Update the input fields with new values
|
||||||
|
document.getElementById("device_latitude_precision").value = newLatLng.lat;
|
||||||
|
document.getElementById("device_longitude_precision").value = newLatLng.lng;
|
||||||
|
|
||||||
|
// Call update function to save new values
|
||||||
|
update_config('latitude_precision', newLatLng.lat);
|
||||||
|
|
||||||
|
setTimeout(() => { update_config('longitude_precision', newLatLng.lng); }, 750);
|
||||||
|
});
|
||||||
|
|
||||||
|
} else {
|
||||||
|
// If the map already exists, update position
|
||||||
|
map.setView([device_latitude, device_longitude], 9);
|
||||||
|
|
||||||
|
// Move marker
|
||||||
|
marker.setLatLng([device_latitude, device_longitude]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update device name in sidebar
|
||||||
|
document.querySelectorAll('.sideBar_sensorName').forEach((element) => {
|
||||||
|
element.innerText = deviceName;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get local RTC time
|
||||||
|
$.ajax({
|
||||||
|
url: 'launcher.php?type=RTC_time',
|
||||||
|
dataType: 'text',
|
||||||
|
method: 'GET',
|
||||||
|
success: function(response) {
|
||||||
|
console.log("Local RTC: " + response);
|
||||||
|
document.getElementById("RTC_time").textContent = response;
|
||||||
|
},
|
||||||
|
error: function(xhr, status, error) {
|
||||||
|
console.error('AJAX request failed:', status, error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch(error => console.error('Error loading config.json:', error));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to update config and refresh the map
|
||||||
|
function update_config(param, value) {
|
||||||
|
console.log("Updating ", param, " : ", value);
|
||||||
|
$.ajax({
|
||||||
|
url: 'launcher.php?type=update_config¶m=' + param + '&value=' + value,
|
||||||
|
dataType: 'text',
|
||||||
|
method: 'GET',
|
||||||
|
cache: false,
|
||||||
|
success: function(response) {
|
||||||
|
console.log(response);
|
||||||
|
},
|
||||||
|
error: function(xhr, status, error) {
|
||||||
|
console.error('AJAX request failed:', status, error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load config and initialize map on page load
|
||||||
|
window.onload = function () {
|
||||||
|
loadConfigAndUpdateMap();
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
777
html/saraR4.html
777
html/saraR4.html
@@ -50,12 +50,21 @@
|
|||||||
<!-- Main content -->
|
<!-- Main content -->
|
||||||
<main class="col-md-10 ms-sm-auto col-lg-11 offset-md-2 offset-lg-1 px-md-4">
|
<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>
|
<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>
|
<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>
|
<h3>
|
||||||
Status
|
Status
|
||||||
<span id="modem-status" class="badge">Loading...</span>
|
<span id="modem-status" class="badge">Loading...</span>
|
||||||
</h3>
|
</h3>
|
||||||
|
-->
|
||||||
<div class="row mb-3">
|
<div class="row mb-3">
|
||||||
|
|
||||||
<div class="col-sm-3">
|
<div class="col-sm-3">
|
||||||
@@ -63,7 +72,7 @@
|
|||||||
|
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<p class="card-text">General information. </p>
|
<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="loading_ttyAMA2_ATI" class="spinner-border spinner-border-sm" style="display: none;" role="status"></div>
|
||||||
<div id="response_ttyAMA2_ATI"></div>
|
<div id="response_ttyAMA2_ATI"></div>
|
||||||
|
|
||||||
@@ -71,19 +80,19 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-sm-3">
|
<div class="col-sm-2">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
|
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<p class="card-text">SIM card information.</p>
|
<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="loading_ttyAMA2_AT_CCID_" class="spinner-border spinner-border-sm" style="display: none;" role="status"></div>
|
||||||
<div id="response_ttyAMA2_AT_CCID_"></div>
|
<div id="response_ttyAMA2_AT_CCID_"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-sm-3">
|
<div class="col-sm-2">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
|
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
@@ -97,17 +106,29 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-sm-3">
|
<div class="col-sm-2">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<p class="card-text">Signal strength </p>
|
<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="loading_ttyAMA2_AT_CSQ" class="spinner-border spinner-border-sm" style="display: none;" role="status"></div>
|
||||||
<div id="response_ttyAMA2_AT_CSQ"></div>
|
<div id="response_ttyAMA2_AT_CSQ"></div>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
|
|
||||||
@@ -172,10 +193,9 @@
|
|||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
<!--
|
||||||
<h3>MQTT</h3>
|
<h3>MQTT</h3>
|
||||||
<div class="row mb-3">
|
<div class="row mb-3">
|
||||||
<!-- Get CONFIG -->
|
|
||||||
<div class="col-sm-4">
|
<div class="col-sm-4">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
@@ -187,7 +207,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- MQTT LOGIN -->
|
|
||||||
|
|
||||||
<div class="col-sm-4">
|
<div class="col-sm-4">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
@@ -202,7 +221,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Send MESSAGE -->
|
|
||||||
|
|
||||||
<div class="col-sm-4">
|
<div class="col-sm-4">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
@@ -219,7 +237,25 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
<h3>Send message (test)</h3>
|
||||||
<div class="row mb-3">
|
<div class="row mb-3">
|
||||||
<!-- SET URL -->
|
<!-- SET URL -->
|
||||||
@@ -269,7 +305,20 @@
|
|||||||
|
|
||||||
|
|
||||||
</div>
|
</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>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -282,26 +331,118 @@
|
|||||||
<script src="assets/js/bootstrap.bundle.js"></script>
|
<script src="assets/js/bootstrap.bundle.js"></script>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
document.addEventListener('DOMContentLoaded', function () {
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
const elementsToLoad = [
|
const elementsToLoad = [
|
||||||
{ id: 'topbar', file: 'topbar.html' },
|
{ id: 'topbar', file: 'topbar.html' },
|
||||||
{ id: 'sidebar', file: 'sidebar.html' },
|
{ id: 'sidebar', file: 'sidebar.html' },
|
||||||
{ id: 'sidebar_mobile', file: 'sidebar.html' }
|
{ id: 'sidebar_mobile', file: 'sidebar.html' }
|
||||||
];
|
];
|
||||||
|
|
||||||
elementsToLoad.forEach(({ id, file }) => {
|
elementsToLoad.forEach(({ id, file }) => {
|
||||||
fetch(file)
|
fetch(file)
|
||||||
.then(response => response.text())
|
.then(response => response.text())
|
||||||
.then(data => {
|
.then(data => {
|
||||||
const element = document.getElementById(id);
|
const element = document.getElementById(id);
|
||||||
if (element) {
|
if (element) {
|
||||||
element.innerHTML = data;
|
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){
|
function getData_saraR4(port, command, timeout){
|
||||||
console.log("Data from SaraR4");
|
console.log("Data from SaraR4");
|
||||||
console.log("Port: " + port );
|
console.log("Port: " + port );
|
||||||
@@ -315,6 +456,7 @@ function getData_saraR4(port, command, timeout){
|
|||||||
|
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: 'launcher.php?type=sara&port='+port+'&command='+encodeURIComponent(command)+'&timeout='+timeout,
|
url: 'launcher.php?type=sara&port='+port+'&command='+encodeURIComponent(command)+'&timeout='+timeout,
|
||||||
|
dataType:'text',
|
||||||
//dataType: 'json', // Specify that you expect a JSON response
|
//dataType: 'json', // Specify that you expect a JSON response
|
||||||
method: 'GET', // Use GET or POST depending on your needs
|
method: 'GET', // Use GET or POST depending on your needs
|
||||||
success: function(response) {
|
success: function(response) {
|
||||||
@@ -382,8 +524,10 @@ function getData_saraR4(port, command, timeout){
|
|||||||
} else{
|
} else{
|
||||||
// si c'est une commande AT normale
|
// si c'est une commande AT normale
|
||||||
// Replace newline characters with <br> tags
|
// 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);
|
$("#response_"+port+"_"+safeCommand).html(formattedResponse);
|
||||||
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
error: function(xhr, status, error) {
|
error: function(xhr, status, error) {
|
||||||
@@ -392,254 +536,335 @@ function getData_saraR4(port, command, timeout){
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function connectNetwork_saraR4(port, networkID, timeout){
|
function connectNetwork_saraR4(port, networkID, timeout){
|
||||||
console.log(" Connect to network (port "+port+" and network id "+networkID+"):");
|
console.log(" Connect to network (port "+port+" and network id "+networkID+"):");
|
||||||
$("#loading_"+port+"_AT_COPS_Connect").show();
|
$("#loading_"+port+"_AT_COPS_Connect").show();
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: 'launcher.php?type=sara_connectNetwork&port='+port+'&networkID='+encodeURIComponent(networkID)+'&timeout='+timeout,
|
url: 'launcher.php?type=sara_connectNetwork&port='+port+'&networkID='+encodeURIComponent(networkID)+'&timeout='+timeout,
|
||||||
//dataType: 'json', // Specify that you expect a JSON response
|
dataType:'text',
|
||||||
method: 'GET', // Use GET or POST depending on your needs
|
//dataType: 'json', // Specify that you expect a JSON response
|
||||||
success: function(response) {
|
method: 'GET', // Use GET or POST depending on your needs
|
||||||
console.log(response);
|
success: function(response) {
|
||||||
$("#loading_"+port+"_AT_COPS_Connect").hide();
|
console.log(response);
|
||||||
// Replace newline characters with <br> tags
|
$("#loading_"+port+"_AT_COPS_Connect").hide();
|
||||||
const formattedResponse = response.replace(/\n/g, "<br>");
|
// Replace newline characters with <br> tags
|
||||||
$("#response_"+port+"_AT_COPS_Connect").html(formattedResponse);
|
const formattedResponse = response.replace(/\n/g, "<br>");
|
||||||
|
$("#response_"+port+"_AT_COPS_Connect").html(formattedResponse);
|
||||||
|
|
||||||
},
|
},
|
||||||
error: function(xhr, status, error) {
|
error: function(xhr, status, error) {
|
||||||
console.error('AJAX request failed:', 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));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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¶m='+param+'&value='+checked,
|
||||||
|
dataType: 'json', // Specify that you expect a JSON response
|
||||||
|
method: 'GET', // Use GET or POST depending on your needs
|
||||||
|
cache: false, // Prevent AJAX from caching
|
||||||
|
success: function(response) {
|
||||||
|
|
||||||
|
console.log("AJAX success:");
|
||||||
|
console.log(response);
|
||||||
|
|
||||||
|
// Format the response nicely
|
||||||
|
let formattedMessage = '';
|
||||||
|
|
||||||
|
if (response.success) {
|
||||||
|
// Success message
|
||||||
|
toastLiveExample.classList.remove('text-bg-danger');
|
||||||
|
toastLiveExample.classList.add('text-bg-success');
|
||||||
|
|
||||||
|
formattedMessage = `
|
||||||
|
<strong>Success!</strong><br>
|
||||||
|
Parameter: ${response.param || param}<br>
|
||||||
|
Value: ${response.value || checked}<br>
|
||||||
|
${response.message || ''}
|
||||||
|
`;
|
||||||
|
} else {
|
||||||
|
// Error message
|
||||||
|
toastLiveExample.classList.remove('text-bg-success');
|
||||||
|
toastLiveExample.classList.add('text-bg-danger');
|
||||||
|
|
||||||
|
formattedMessage = `
|
||||||
|
<strong>Error!</strong><br>
|
||||||
|
${response.error || 'Unknown error'}<br>
|
||||||
|
Parameter: ${response.param || param}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the toast body with formatted content
|
||||||
|
toastBody.innerHTML = formattedMessage;
|
||||||
|
// Show the toast
|
||||||
|
const toastBootstrap = bootstrap.Toast.getOrCreateInstance(toastLiveExample)
|
||||||
|
toastBootstrap.show()
|
||||||
|
|
||||||
|
|
||||||
|
},
|
||||||
|
error: function(xhr, status, error) {
|
||||||
|
console.error('AJAX request failed:', status, error);
|
||||||
|
// Update toast with error message
|
||||||
|
toastBody.textContent = 'Error: ' + error;
|
||||||
|
|
||||||
|
// Set toast to danger color
|
||||||
|
toastLiveExample.classList.remove('text-bg-success');
|
||||||
|
toastLiveExample.classList.add('text-bg-danger');
|
||||||
|
|
||||||
|
// Show the toast for errors too
|
||||||
|
const toastBootstrap = bootstrap.Toast.getOrCreateInstance(toastLiveExample);
|
||||||
|
toastBootstrap.show();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -102,6 +102,16 @@ function getNPM_values(port){
|
|||||||
$("#loading_"+port).hide();
|
$("#loading_"+port).hide();
|
||||||
// Create an array of the desired keys
|
// Create an array of the desired keys
|
||||||
const keysToShow = ["PM1", "PM25", "PM10"];
|
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
|
// Add only the specified elements to the table
|
||||||
keysToShow.forEach(key => {
|
keysToShow.forEach(key => {
|
||||||
if (response[key] !== undefined) { // Check if the key exists in the response
|
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({
|
// Check for errors and add them to the table
|
||||||
url: 'launcher.php?type=envea&port='+port+'&name='+name,
|
Object.keys(errorMessages).forEach(errorKey => {
|
||||||
dataType: 'json', // Specify that you expect a JSON response
|
if (response[errorKey] === 1) {
|
||||||
method: 'GET', // Use GET or POST depending on your needs
|
$("#data-table-body_" + port).append(`
|
||||||
success: function(response) {
|
<tr class="error-row">
|
||||||
console.log(response);
|
<td><b>${errorKey}</b></td>
|
||||||
const tableBody = document.getElementById("data-table-body_envea"+name);
|
<td style="color: red;">⚠ ${errorMessages[errorKey]}</td>
|
||||||
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>
|
|
||||||
</tr>
|
</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(){
|
function getNoise_values(){
|
||||||
console.log("Data from I2C Noise Sensor:");
|
console.log("Data from I2C Noise Sensor:");
|
||||||
@@ -163,6 +195,7 @@ function getNoise_values(){
|
|||||||
|
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: 'launcher.php?type=noise',
|
url: 'launcher.php?type=noise',
|
||||||
|
dataType: 'text',
|
||||||
method: 'GET', // Use GET or POST depending on your needs
|
method: 'GET', // Use GET or POST depending on your needs
|
||||||
success: function(response) {
|
success: function(response) {
|
||||||
console.log(response);
|
console.log(response);
|
||||||
@@ -197,6 +230,8 @@ function getBME280_values(){
|
|||||||
|
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: 'launcher.php?type=BME280',
|
url: 'launcher.php?type=BME280',
|
||||||
|
dataType: 'text',
|
||||||
|
|
||||||
method: 'GET', // Use GET or POST depending on your needs
|
method: 'GET', // Use GET or POST depending on your needs
|
||||||
success: function(response) {
|
success: function(response) {
|
||||||
console.log(response);
|
console.log(response);
|
||||||
@@ -236,143 +271,190 @@ function getBME280_values(){
|
|||||||
|
|
||||||
|
|
||||||
window.onload = function() {
|
window.onload = function() {
|
||||||
fetch('../config.json') // Replace 'deviceID.txt' with 'config.json'
|
|
||||||
.then(response => response.json()) // Parse response as JSON
|
//NEW way to get config (SQLite)
|
||||||
.then(data => {
|
$.ajax({
|
||||||
//get device ID
|
url: 'launcher.php?type=get_config_sqlite',
|
||||||
const deviceID = data.deviceID.trim().toUpperCase();
|
dataType:'json',
|
||||||
//document.getElementById('pageTitle_plus_ID').innerText = 'token: ' + deviceID;
|
//dataType: 'json', // Specify that you expect a JSON response
|
||||||
//get device Name
|
method: 'GET', // Use GET or POST depending on your needs
|
||||||
const deviceName = data.deviceName;
|
success: function(response) {
|
||||||
|
console.log("Getting SQLite config table:");
|
||||||
const elements = document.querySelectorAll('.sideBar_sensorName');
|
console.log(response);
|
||||||
elements.forEach((element) => {
|
|
||||||
element.innerText = deviceName;
|
|
||||||
});
|
//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;
|
|
||||||
|
|
||||||
},
|
}//end if
|
||||||
error: function(xhr, status, error) {
|
|
||||||
console.error('AJAX request failed:', status, error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const container = document.getElementById('card-container'); // Conteneur des cartes
|
},
|
||||||
|
error: function(xhr, status, error) {
|
||||||
//creates NPM cards
|
console.error('AJAX request failed:', status, error);
|
||||||
const NPM_ports = data.NextPM_ports; // Récupère les ports
|
}
|
||||||
NPM_ports.forEach((port, index) => {
|
});//end AJAX (config_scripts)
|
||||||
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
|
|
||||||
});
|
|
||||||
|
|
||||||
//creates ENVEA cards
|
//get local RTC
|
||||||
const ENVEA_sensors = data.envea_sondes.filter(sonde => sonde.connected); // Filter only connected sondes
|
$.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
|
error: function(xhr, status, error) {
|
||||||
const name = sensor.name; // Port from the sensor object
|
console.error('AJAX request failed:', status, error);
|
||||||
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
|
|
||||||
});
|
|
||||||
|
|
||||||
//creates i2c BME280 card
|
} //end windows onload
|
||||||
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));
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -13,6 +13,13 @@
|
|||||||
</svg>
|
</svg>
|
||||||
Capteurs
|
Capteurs
|
||||||
</a>
|
</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">
|
<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">
|
<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"/>
|
<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>
|
</svg>
|
||||||
Logs
|
Logs
|
||||||
</a>
|
</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">
|
<a class="nav-link text-white" href="admin.html">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-tools" viewBox="0 0 16 16">
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-tools" viewBox="0 0 16 16">
|
||||||
<path d="M1 0 0 1l2.2 3.081a1 1 0 0 0 .815.419h.07a1 1 0 0 1 .708.293l2.675 2.675-2.617 2.654A3.003 3.003 0 0 0 0 13a3 3 0 1 0 5.878-.851l2.654-2.617.968.968-.305.914a1 1 0 0 0 .242 1.023l3.27 3.27a.997.997 0 0 0 1.414 0l1.586-1.586a.997.997 0 0 0 0-1.414l-3.27-3.27a1 1 0 0 0-1.023-.242L10.5 9.5l-.96-.96 2.68-2.643A3.005 3.005 0 0 0 16 3q0-.405-.102-.777l-2.14 2.141L12 4l-.364-1.757L13.777.102a3 3 0 0 0-3.675 3.68L7.462 6.46 4.793 3.793a1 1 0 0 1-.293-.707v-.071a1 1 0 0 0-.419-.814zm9.646 10.646a.5.5 0 0 1 .708 0l2.914 2.915a.5.5 0 0 1-.707.707l-2.915-2.914a.5.5 0 0 1 0-.708M3 11l.471.242.529.026.287.445.445.287.026.529L5 13l-.242.471-.026.529-.445.287-.287.445-.529.026L3 15l-.471-.242L2 14.732l-.287-.445L1.268 14l-.026-.529L1 13l.242-.471.026-.529.445-.287.287-.445.529-.026z"/>
|
<path d="M1 0 0 1l2.2 3.081a1 1 0 0 0 .815.419h.07a1 1 0 0 1 .708.293l2.675 2.675-2.617 2.654A3.003 3.003 0 0 0 0 13a3 3 0 1 0 5.878-.851l2.654-2.617.968.968-.305.914a1 1 0 0 0 .242 1.023l3.27 3.27a.997.997 0 0 0 1.414 0l1.586-1.586a.997.997 0 0 0 0-1.414l-3.27-3.27a1 1 0 0 0-1.023-.242L10.5 9.5l-.96-.96 2.68-2.643A3.005 3.005 0 0 0 16 3q0-.405-.102-.777l-2.14 2.141L12 4l-.364-1.757L13.777.102a3 3 0 0 0-3.675 3.68L7.462 6.46 4.793 3.793a1 1 0 0 1-.293-.707v-.071a1 1 0 0 0-.419-.814zm9.646 10.646a.5.5 0 0 1 .708 0l2.914 2.915a.5.5 0 0 1-.707.707l-2.915-2.914a.5.5 0 0 1 0-.708M3 11l.471.242.529.026.287.445.445.287.026.529L5 13l-.242.471-.026.529-.445.287-.287.445-.529.026L3 15l-.471-.242L2 14.732l-.287-.445L1.268 14l-.026-.529L1 13l.242-.471.026-.529.445-.287.287-.445.529-.026z"/>
|
||||||
|
|||||||
413
html/terminal.html
Normal file
413
html/terminal.html
Normal file
@@ -0,0 +1,413 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>NebuleAir - Terminal</title>
|
||||||
|
<link rel="stylesheet" href="assets/css/bootstrap.min.css">
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
#sidebar a.nav-link {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
#sidebar a.nav-link:hover {
|
||||||
|
background-color: rgba(0, 0, 0, 0.5);
|
||||||
|
}
|
||||||
|
#sidebar a.nav-link svg {
|
||||||
|
margin-right: 8px; /* Add spacing between icons and text */
|
||||||
|
}
|
||||||
|
#sidebar {
|
||||||
|
transition: transform 0.3s ease-in-out;
|
||||||
|
}
|
||||||
|
.offcanvas-backdrop {
|
||||||
|
z-index: 1040;
|
||||||
|
}
|
||||||
|
#terminal {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 400px;
|
||||||
|
max-height: 400px;
|
||||||
|
overflow-y: auto;
|
||||||
|
background-color: #000;
|
||||||
|
color: #00ff00;
|
||||||
|
font-family: monospace;
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 5px;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-wrap: break-word;
|
||||||
|
}
|
||||||
|
#cmdLine {
|
||||||
|
width: 100%;
|
||||||
|
background-color: #000;
|
||||||
|
color: #00ff00;
|
||||||
|
font-family: monospace;
|
||||||
|
padding: 10px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 0 0 5px 5px;
|
||||||
|
margin-top: -1px;
|
||||||
|
}
|
||||||
|
#cmdLine:focus {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
.command-container {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.password-popup {
|
||||||
|
display: none;
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background-color: rgba(0, 0, 0, 0.5);
|
||||||
|
z-index: 2000;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.password-container {
|
||||||
|
background-color: white;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 5px;
|
||||||
|
box-shadow: 0 0 10px rgba(0, 0, 0, 0.3);
|
||||||
|
width: 300px;
|
||||||
|
}
|
||||||
|
.limited-commands {
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 5px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
.limited-commands code {
|
||||||
|
white-space: nowrap;
|
||||||
|
margin-right: 10px;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<!-- Topbar -->
|
||||||
|
<span id="topbar"></span>
|
||||||
|
|
||||||
|
<!-- Sidebar Offcanvas for Mobile -->
|
||||||
|
<div class="offcanvas offcanvas-start text-white bg-dark" tabindex="-1" id="sidebarOffcanvas" aria-labelledby="sidebarOffcanvasLabel">
|
||||||
|
<div class="offcanvas-header">
|
||||||
|
<h5 class="offcanvas-title" id="sidebarOffcanvasLabel">NebuleAir</h5>
|
||||||
|
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="offcanvas" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
<div class="offcanvas-body" id="sidebar_mobile">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="container-fluid mt-5">
|
||||||
|
<div class="row">
|
||||||
|
<!-- Side bar -->
|
||||||
|
<aside class="col-md-2 col-lg-1 d-none d-md-block vh-100 position-fixed bg-dark text-white" id="sidebar">
|
||||||
|
</aside>
|
||||||
|
<!-- Main content -->
|
||||||
|
<main class="col-md-10 ms-sm-auto col-lg-11 offset-md-2 offset-lg-1 px-md-4">
|
||||||
|
<h1 class="mt-4">Terminal Console</h1>
|
||||||
|
|
||||||
|
<div class="row mb-3">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="alert alert-warning">
|
||||||
|
<strong>Warning:</strong> This terminal provides direct access to system commands.
|
||||||
|
Use with caution as improper commands may affect system functionality.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="limited-commands">
|
||||||
|
<h5>Quick Commands:</h5>
|
||||||
|
<div>
|
||||||
|
<code onclick="insertCommand('ls -la')">ls -la</code>
|
||||||
|
<code onclick="insertCommand('df -h')">df -h</code>
|
||||||
|
<code onclick="insertCommand('free -h')">free -h</code>
|
||||||
|
<code onclick="insertCommand('uptime')">uptime</code>
|
||||||
|
<code onclick="insertCommand('systemctl status master_nebuleair.service')">service status</code>
|
||||||
|
<code onclick="insertCommand('cat /var/www/nebuleair_pro_4g/config.json')">view config</code>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header d-flex justify-content-between align-items-center">
|
||||||
|
<h5 class="mb-0">Command Console</h5>
|
||||||
|
<div>
|
||||||
|
<button id="accessBtn" class="btn btn-primary me-2">Access Terminal</button>
|
||||||
|
<button id="clearBtn" class="btn btn-secondary" disabled>Clear</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-body p-0">
|
||||||
|
<div class="command-container" id="commandContainer">
|
||||||
|
<div id="terminal">Welcome to NebuleAir Terminal Console
|
||||||
|
Type your commands below. Type 'help' for a list of commands.
|
||||||
|
</div>
|
||||||
|
<input type="text" id="cmdLine" placeholder="Enter command..." disabled>
|
||||||
|
</div>
|
||||||
|
<div id="errorMsg" class="alert alert-danger m-3" style="display:none;"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Password Modal -->
|
||||||
|
<div class="password-popup" id="passwordModal">
|
||||||
|
<div class="password-container">
|
||||||
|
<h5>Authentication Required</h5>
|
||||||
|
<p>Please enter the admin password to access the terminal:</p>
|
||||||
|
<div class="mb-3">
|
||||||
|
<input type="password" class="form-control" id="adminPassword" placeholder="Password">
|
||||||
|
</div>
|
||||||
|
<div class="mb-3 d-flex justify-content-between">
|
||||||
|
<button class="btn btn-secondary" id="cancelPasswordBtn">Cancel</button>
|
||||||
|
<button class="btn btn-primary" id="submitPasswordBtn">Submit</button>
|
||||||
|
</div>
|
||||||
|
<div id="passwordError" class="text-danger mt-2" style="display:none;"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- JAVASCRIPT -->
|
||||||
|
<!-- Link Ajax locally -->
|
||||||
|
<script src="assets/jquery/jquery-3.7.1.min.js"></script>
|
||||||
|
<!-- Link Bootstrap JS and Popper.js locally -->
|
||||||
|
<script src="assets/js/bootstrap.bundle.js"></script>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
|
const elementsToLoad = [
|
||||||
|
{ id: 'topbar', file: 'topbar.html' },
|
||||||
|
{ id: 'sidebar', file: 'sidebar.html' },
|
||||||
|
{ id: 'sidebar_mobile', file: 'sidebar.html' }
|
||||||
|
];
|
||||||
|
|
||||||
|
elementsToLoad.forEach(({ id, file }) => {
|
||||||
|
fetch(file)
|
||||||
|
.then(response => response.text())
|
||||||
|
.then(data => {
|
||||||
|
const element = document.getElementById(id);
|
||||||
|
if (element) {
|
||||||
|
element.innerHTML = data;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => console.error(`Error loading ${file}:`, error));
|
||||||
|
});
|
||||||
|
|
||||||
|
// Initialize elements
|
||||||
|
initializeElements();
|
||||||
|
});
|
||||||
|
|
||||||
|
window.onload = function() {
|
||||||
|
|
||||||
|
//NEW way to get config (SQLite)
|
||||||
|
$.ajax({
|
||||||
|
url: 'launcher.php?type=get_config_sqlite',
|
||||||
|
dataType:'json',
|
||||||
|
//dataType: 'json', // Specify that you expect a JSON response
|
||||||
|
method: 'GET', // Use GET or POST depending on your needs
|
||||||
|
success: function(response) {
|
||||||
|
console.log("Getting SQLite config table:");
|
||||||
|
console.log(response);
|
||||||
|
|
||||||
|
|
||||||
|
//device name_side bar
|
||||||
|
const elements = document.querySelectorAll('.sideBar_sensorName');
|
||||||
|
elements.forEach((element) => {
|
||||||
|
element.innerText = response.deviceName;
|
||||||
|
});
|
||||||
|
|
||||||
|
},
|
||||||
|
error: function(xhr, status, error) {
|
||||||
|
console.error('AJAX request failed:', status, error);
|
||||||
|
}
|
||||||
|
});//end AJAX
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add admin password (should be changed to something more secure)
|
||||||
|
const ADMIN_PASSWORD = "123plouf";
|
||||||
|
|
||||||
|
// Global variables
|
||||||
|
let terminal;
|
||||||
|
let cmdLine;
|
||||||
|
let commandContainer;
|
||||||
|
let accessBtn;
|
||||||
|
let clearBtn;
|
||||||
|
let passwordModal;
|
||||||
|
let adminPassword;
|
||||||
|
let submitPasswordBtn;
|
||||||
|
let cancelPasswordBtn;
|
||||||
|
let passwordError;
|
||||||
|
let errorMsg;
|
||||||
|
let commandHistory = [];
|
||||||
|
let historyIndex = -1;
|
||||||
|
|
||||||
|
// Initialize DOM references after document is loaded
|
||||||
|
function initializeElements() {
|
||||||
|
terminal = document.getElementById('terminal');
|
||||||
|
cmdLine = document.getElementById('cmdLine');
|
||||||
|
commandContainer = document.getElementById('commandContainer');
|
||||||
|
accessBtn = document.getElementById('accessBtn');
|
||||||
|
clearBtn = document.getElementById('clearBtn');
|
||||||
|
passwordModal = document.getElementById('passwordModal');
|
||||||
|
adminPassword = document.getElementById('adminPassword');
|
||||||
|
submitPasswordBtn = document.getElementById('submitPasswordBtn');
|
||||||
|
cancelPasswordBtn = document.getElementById('cancelPasswordBtn');
|
||||||
|
passwordError = document.getElementById('passwordError');
|
||||||
|
errorMsg = document.getElementById('errorMsg');
|
||||||
|
|
||||||
|
// Set up event listeners
|
||||||
|
accessBtn.addEventListener('click', function() {
|
||||||
|
passwordModal.style.display = 'flex';
|
||||||
|
adminPassword.value = ''; // Clear password field
|
||||||
|
passwordError.style.display = 'none';
|
||||||
|
adminPassword.focus();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Password submit button
|
||||||
|
submitPasswordBtn.addEventListener('click', function() {
|
||||||
|
if (adminPassword.value === ADMIN_PASSWORD) {
|
||||||
|
passwordModal.style.display = 'none';
|
||||||
|
enableTerminal();
|
||||||
|
} else {
|
||||||
|
passwordError.textContent = 'Invalid password';
|
||||||
|
passwordError.style.display = 'block';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Enter key for password
|
||||||
|
adminPassword.addEventListener('keypress', function(e) {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
submitPasswordBtn.click();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Cancel password button
|
||||||
|
cancelPasswordBtn.addEventListener('click', function() {
|
||||||
|
passwordModal.style.display = 'none';
|
||||||
|
});
|
||||||
|
|
||||||
|
// Clear button
|
||||||
|
clearBtn.addEventListener('click', function() {
|
||||||
|
terminal.innerHTML = 'Terminal cleared.\n';
|
||||||
|
});
|
||||||
|
|
||||||
|
// Command line input events
|
||||||
|
cmdLine.addEventListener('keypress', function(e) {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
const command = cmdLine.value.trim();
|
||||||
|
if (command) {
|
||||||
|
executeCommand(command);
|
||||||
|
commandHistory.push(command);
|
||||||
|
historyIndex = commandHistory.length;
|
||||||
|
cmdLine.value = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Command history navigation with arrow keys
|
||||||
|
cmdLine.addEventListener('keydown', function(e) {
|
||||||
|
if (e.key === 'ArrowUp') {
|
||||||
|
if (historyIndex > 0) {
|
||||||
|
historyIndex--;
|
||||||
|
cmdLine.value = commandHistory[historyIndex];
|
||||||
|
}
|
||||||
|
e.preventDefault();
|
||||||
|
} else if (e.key === 'ArrowDown') {
|
||||||
|
if (historyIndex < commandHistory.length - 1) {
|
||||||
|
historyIndex++;
|
||||||
|
cmdLine.value = commandHistory[historyIndex];
|
||||||
|
} else {
|
||||||
|
historyIndex = commandHistory.length;
|
||||||
|
cmdLine.value = '';
|
||||||
|
}
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enable terminal access
|
||||||
|
function enableTerminal() {
|
||||||
|
commandContainer.style.display = 'block';
|
||||||
|
cmdLine.disabled = false;
|
||||||
|
clearBtn.disabled = false;
|
||||||
|
accessBtn.textContent = 'Authenticated';
|
||||||
|
accessBtn.classList.remove('btn-primary');
|
||||||
|
accessBtn.classList.add('btn-success');
|
||||||
|
accessBtn.disabled = true;
|
||||||
|
cmdLine.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insert a predefined command
|
||||||
|
function insertCommand(cmd) {
|
||||||
|
// Only allow insertion if terminal is enabled
|
||||||
|
if (cmdLine.disabled === false) {
|
||||||
|
cmdLine.value = cmd;
|
||||||
|
cmdLine.focus();
|
||||||
|
} else {
|
||||||
|
// Alert user that they need to authenticate first
|
||||||
|
alert('Please access the terminal first by clicking "Access Terminal" and entering the password.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute a command
|
||||||
|
function executeCommand(command) {
|
||||||
|
// Add command to terminal with user prefix
|
||||||
|
terminal.innerHTML += `<span style="color: cyan;">user@nebuleair</span>:<span style="color: yellow;">~</span>$ ${command}\n`;
|
||||||
|
terminal.scrollTop = terminal.scrollHeight;
|
||||||
|
|
||||||
|
// Handle special commands
|
||||||
|
if (command === 'clear') {
|
||||||
|
terminal.innerHTML = 'Terminal cleared.\n';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Filter dangerous commands
|
||||||
|
const dangerousCommands = [
|
||||||
|
'rm -rf /', 'rm -rf /*', 'rm -rf ~', 'rm -rf ~/*',
|
||||||
|
'mkfs', 'dd if=/dev/zero', 'dd if=/dev/random',
|
||||||
|
'>>', '>', '|', ';', '&&', '||',
|
||||||
|
'wget', 'curl', 'ssh', 'scp', 'nc',
|
||||||
|
'chmod -R', 'chown -R'
|
||||||
|
];
|
||||||
|
|
||||||
|
// Check for dangerous commands or command chaining
|
||||||
|
const hasDangerousCommand = dangerousCommands.some(cmd => command.includes(cmd));
|
||||||
|
if (hasDangerousCommand || command.includes('&') || command.includes(';') || command.includes('|')) {
|
||||||
|
terminal.innerHTML += '<span style="color: red;">Error: This command is not allowed for security reasons.</span>\n';
|
||||||
|
terminal.scrollTop = terminal.scrollHeight;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute the command via AJAX
|
||||||
|
$.ajax({
|
||||||
|
url: 'launcher.php?type=execute_command',
|
||||||
|
method: 'POST',
|
||||||
|
dataType:'json',
|
||||||
|
data: {
|
||||||
|
type: 'execute_command',
|
||||||
|
command: command
|
||||||
|
},
|
||||||
|
success: function(response) {
|
||||||
|
console.log(response);
|
||||||
|
|
||||||
|
if (response.success) {
|
||||||
|
// Add command output to terminal
|
||||||
|
terminal.innerHTML += `<span style="color: #00ff00;">${response.output}</span>\n`;
|
||||||
|
} else {
|
||||||
|
terminal.innerHTML += `<span style="color: red;">Error: ${response.message}</span>\n`;
|
||||||
|
}
|
||||||
|
terminal.scrollTop = terminal.scrollHeight;
|
||||||
|
},
|
||||||
|
error: function(xhr, status, error) {
|
||||||
|
terminal.innerHTML += `<span style="color: red;">Error executing command: ${error}</span>\n`;
|
||||||
|
terminal.scrollTop = terminal.scrollHeight;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -302,6 +302,11 @@ function get_internet(){
|
|||||||
element.innerText = deviceName;
|
element.innerText = deviceName;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
//device name html page title
|
||||||
|
if (response.deviceName) {
|
||||||
|
document.title = response.deviceName;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
//get wifi connection status
|
//get wifi connection status
|
||||||
const WIFI_statusElement = document.getElementById("wifi-status");
|
const WIFI_statusElement = document.getElementById("wifi-status");
|
||||||
|
|||||||
@@ -1,38 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
# Exit immediately if a command exits with a non-zero status
|
|
||||||
set -e
|
|
||||||
|
|
||||||
# Update and install necessary packages
|
|
||||||
echo "Updating package list and installing necessary packages..."
|
|
||||||
sudo apt update
|
|
||||||
sudo apt install -y git gh apache2 php python3 python3-pip jq autossh i2c-tools python3-smbus
|
|
||||||
|
|
||||||
# Install Python libraries
|
|
||||||
echo "Installing Python libraries..."
|
|
||||||
sudo pip3 install pyserial requests RPi.GPIO adafruit-circuitpython-bme280 --break-system-packages
|
|
||||||
|
|
||||||
# Set up SSH for /var/www
|
|
||||||
echo "Setting up SSH keys..."
|
|
||||||
sudo mkdir -p /var/www/.ssh
|
|
||||||
sudo ssh-keygen -t rsa -b 4096 -f /var/www/.ssh/id_rsa -N ""
|
|
||||||
sudo ssh-copy-id -i /var/www/.ssh/id_rsa.pub -p 50221 airlab_server1@aircarto.fr
|
|
||||||
|
|
||||||
|
|
||||||
# Clone the repository
|
|
||||||
echo "Cloning the NebuleAir Pro 4G repository..."
|
|
||||||
sudo git clone http://gitea.aircarto.fr/PaulVua/nebuleair_pro_4g.git /var/www/nebuleair_pro_4g
|
|
||||||
|
|
||||||
# Set up repository files and permissions
|
|
||||||
echo "Setting up repository files and permissions..."
|
|
||||||
sudo mkdir -p /var/www/nebuleair_pro_4g/logs
|
|
||||||
sudo touch /var/www/nebuleair_pro_4g/logs/app.log /var/www/nebuleair_pro_4g/logs/loop.log
|
|
||||||
sudo cp /var/www/nebuleair_pro_4g/config.json.dist /var/www/nebuleair_pro_4g/config.json
|
|
||||||
sudo chmod -R 777 /var/www/nebuleair_pro_4g/
|
|
||||||
git config core.fileMode false
|
|
||||||
|
|
||||||
# Set up cron jobs
|
|
||||||
echo "Setting up cron jobs..."
|
|
||||||
sudo crontab /var/www/nebuleair_pro_4g/cron_jobs
|
|
||||||
|
|
||||||
echo "Setup completed successfully!"
|
|
||||||
135
installation_part1.sh
Normal file
135
installation_part1.sh
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Exit on error, unset variable usage, and error in a pipeline
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Define color variables
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
BLUE='\033[0;34m'
|
||||||
|
NC='\033[0m' # No color
|
||||||
|
|
||||||
|
# Function to print messages in color
|
||||||
|
info() { echo -e "${BLUE}[INFO]${NC} $1"; }
|
||||||
|
success() { echo -e "${GREEN}[SUCCESS]${NC} $1"; }
|
||||||
|
warning() { echo -e "${YELLOW}[WARNING]${NC} $1"; }
|
||||||
|
error() { echo -e "${RED}[ERROR]${NC} $1"; exit 1; }
|
||||||
|
|
||||||
|
# Check for root privileges
|
||||||
|
if [[ "$EUID" -ne 0 ]]; then
|
||||||
|
error "This script must be run as root. Use 'sudo ./installation.sh'"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Update and install necessary packages
|
||||||
|
info "Updating package list and installing necessary packages..."
|
||||||
|
sudo apt update && sudo apt install -y git gh apache2 sqlite3 php php-sqlite3 python3 python3-pip jq autossh i2c-tools python3-smbus || error "Failed to install required packages."
|
||||||
|
|
||||||
|
# Install Python libraries
|
||||||
|
info "Installing Python libraries..."
|
||||||
|
sudo pip3 install pyserial requests RPi.GPIO adafruit-circuitpython-bme280 crcmod psutil gpiozero ntplib pytz --break-system-packages || error "Failed to install Python libraries."
|
||||||
|
|
||||||
|
# Clone the repository (check if it exists first)
|
||||||
|
REPO_DIR="/var/www/nebuleair_pro_4g"
|
||||||
|
if [[ -d "$REPO_DIR" ]]; then
|
||||||
|
warning "Repository already exists. Will update instead of clone."
|
||||||
|
# Save current directory
|
||||||
|
local current_dir=$(pwd)
|
||||||
|
# Navigate to repository directory
|
||||||
|
cd "$REPO_DIR"
|
||||||
|
# Stash any local changes
|
||||||
|
sudo git stash || warning "Failed to stash local changes"
|
||||||
|
# Pull latest changes
|
||||||
|
sudo git pull || error "Failed to pull latest changes"
|
||||||
|
# Return to original directory
|
||||||
|
cd "$current_dir"
|
||||||
|
success "Repository updated successfully!"
|
||||||
|
else
|
||||||
|
info "Cloning the NebuleAir Pro 4G repository..."
|
||||||
|
sudo git clone http://gitea.aircarto.fr/PaulVua/nebuleair_pro_4g.git "$REPO_DIR" || error "Failed to clone repository."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Set up repository files and permissions
|
||||||
|
info "Setting up repository files and permissions..."
|
||||||
|
sudo mkdir -p "$REPO_DIR/logs"
|
||||||
|
sudo touch "$REPO_DIR/logs/app.log" "$REPO_DIR/logs/master.log" "$REPO_DIR/logs/master_errors.log" "$REPO_DIR/wifi_list.csv"
|
||||||
|
sudo chmod -R 755 "$REPO_DIR/"
|
||||||
|
sudo chown -R www-data:www-data "$REPO_DIR/"
|
||||||
|
sudo git config --global core.fileMode false
|
||||||
|
#sudo git -C /var/www/nebuleair_pro_4g config core.fileMode false
|
||||||
|
sudo git config --global --add safe.directory "$REPO_DIR"
|
||||||
|
|
||||||
|
# Set up cron jobs (ensure file exists first)
|
||||||
|
info "Setting up cron jobs..."
|
||||||
|
if [[ -f "$REPO_DIR/cron_jobs" ]]; then
|
||||||
|
sudo crontab "$REPO_DIR/cron_jobs"
|
||||||
|
success "Cron jobs set up successfully."
|
||||||
|
else
|
||||||
|
warning "Cron jobs file not found. Skipping."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Create databases
|
||||||
|
info "Creating databases..."
|
||||||
|
if [[ -f "$REPO_DIR/sqlite/create_db.py" ]]; then
|
||||||
|
sudo /usr/bin/python3 "$REPO_DIR/sqlite/create_db.py" || error "Failed to create databases."
|
||||||
|
success "Databases created successfully."
|
||||||
|
else
|
||||||
|
warning "Database creation script not found."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Set config
|
||||||
|
info "Set config..."
|
||||||
|
if [[ -f "$REPO_DIR/sqlite/set_config.py" ]]; then
|
||||||
|
sudo /usr/bin/python3 "$REPO_DIR/sqlite/set_config.py" || error "Failed to set config."
|
||||||
|
success "Databases created successfully."
|
||||||
|
else
|
||||||
|
warning "Database creation script not found."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Configure Apache
|
||||||
|
info "Configuring Apache..."
|
||||||
|
APACHE_CONF="/etc/apache2/sites-available/000-default.conf"
|
||||||
|
if grep -q "DocumentRoot /var/www/nebuleair_pro_4g" "$APACHE_CONF"; then
|
||||||
|
warning "Apache configuration already set. Skipping."
|
||||||
|
else
|
||||||
|
sudo sed -i 's|DocumentRoot /var/www/html|DocumentRoot /var/www/nebuleair_pro_4g|' "$APACHE_CONF"
|
||||||
|
sudo systemctl reload apache2
|
||||||
|
success "Apache configuration updated and reloaded."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Add sudo authorization (prevent duplicate entries)
|
||||||
|
info "Setting up sudo authorization..."
|
||||||
|
if ! sudo grep -q "/usr/bin/nmcli" /etc/sudoers; then
|
||||||
|
echo -e "ALL ALL=(ALL) NOPASSWD: /usr/bin/nmcli, /usr/sbin/reboot\nwww-data ALL=(ALL) NOPASSWD: /usr/bin/git pull\nwww-data ALL=(ALL) NOPASSWD: /usr/bin/ssh\nwww-data ALL=(ALL) NOPASSWD: /usr/bin/python3 * www-data ALL=(ALL) NOPASSWD: /bin/systemctl * www-data ALL=(ALL) NOPASSWD: /var/www/nebuleair_pro_4g/*" | sudo tee -a /etc/sudoers > /dev/null
|
||||||
|
success "Sudo authorization added."
|
||||||
|
else
|
||||||
|
warning "Sudo authorization already set. Skipping."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Open all UART serial ports (avoid duplication)
|
||||||
|
info "Configuring UART serial ports..."
|
||||||
|
if ! grep -q "enable_uart=1" /boot/firmware/config.txt; then
|
||||||
|
echo -e "\nenable_uart=1\ndtoverlay=uart0\ndtoverlay=uart1\ndtoverlay=uart2\ndtoverlay=uart3\ndtoverlay=uart4\ndtoverlay=uart5" | sudo tee -a /boot/firmware/config.txt > /dev/null
|
||||||
|
success "UART configuration added."
|
||||||
|
else
|
||||||
|
warning "UART configuration already set. Skipping."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Ensure correct permissions for serial devices
|
||||||
|
info "Setting permissions for serial devices..."
|
||||||
|
sudo chmod 666 /dev/ttyAMA* || warning "Failed to set permissions for /dev/ttyAMA*"
|
||||||
|
|
||||||
|
# Enable I2C ports
|
||||||
|
info "Enabling I2C ports..."
|
||||||
|
sudo raspi-config nonint do_i2c 0
|
||||||
|
success "I2C ports enabled."
|
||||||
|
|
||||||
|
#creates databases
|
||||||
|
info "Creates sqlites databases..."
|
||||||
|
/usr/bin/python3 /var/www/nebuleair_pro_4g/sqlite/create_db.py
|
||||||
|
|
||||||
|
# Completion message
|
||||||
|
success "Setup completed successfully!"
|
||||||
|
info "System will reboot in 5 seconds..."
|
||||||
|
sleep 5
|
||||||
|
sudo reboot
|
||||||
102
installation_part2.sh
Normal file
102
installation_part2.sh
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Script to set up the App after rebooting
|
||||||
|
|
||||||
|
# Exit on error, unset variable usage, and error in a pipeline
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Define color variables
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
BLUE='\033[0;34m'
|
||||||
|
NC='\033[0m' # No color
|
||||||
|
|
||||||
|
# Function to print messages in color
|
||||||
|
info() { echo -e "${BLUE}[INFO]${NC} $1"; }
|
||||||
|
success() { echo -e "${GREEN}[SUCCESS]${NC} $1"; }
|
||||||
|
warning() { echo -e "${YELLOW}[WARNING]${NC} $1"; }
|
||||||
|
error() { echo -e "${RED}[ERROR]${NC} $1"; exit 1; }
|
||||||
|
|
||||||
|
# Check for root privileges
|
||||||
|
if [[ "$EUID" -ne 0 ]]; then
|
||||||
|
error "This script must be run as root. Use 'sudo ./installation.sh'"
|
||||||
|
fi
|
||||||
|
REPO_DIR="/var/www/nebuleair_pro_4g"
|
||||||
|
#set up the RTC
|
||||||
|
info "Set up the RTC"
|
||||||
|
/usr/bin/python3 /var/www/nebuleair_pro_4g/RTC/set_with_NTP.py
|
||||||
|
|
||||||
|
#Wake up SARA
|
||||||
|
info "Wake Up SARA"
|
||||||
|
pinctrl set 16 op
|
||||||
|
pinctrl set 16 dh
|
||||||
|
sleep 5
|
||||||
|
|
||||||
|
#Check SARA connection
|
||||||
|
info "Check SARA connection"
|
||||||
|
/usr/bin/python3 /var/www/nebuleair_pro_4g/SARA/sara.py ttyAMA2 ATI 2
|
||||||
|
|
||||||
|
#set up SARA R4 APN
|
||||||
|
info "Set up Monogoto APN"
|
||||||
|
/usr/bin/python3 /var/www/nebuleair_pro_4g/SARA/sara_setAPN.py ttyAMA2 data.mono 2
|
||||||
|
|
||||||
|
#activate blue network led on the SARA R4
|
||||||
|
info "Activate blue LED"
|
||||||
|
/usr/bin/python3 /var/www/nebuleair_pro_4g/SARA/sara.py ttyAMA2 AT+UGPIOC=16,2 2
|
||||||
|
|
||||||
|
#Connect to network
|
||||||
|
info "Connect SARA R4 to network"
|
||||||
|
python3 /var/www/nebuleair_pro_4g/SARA/sara_connectNetwork.py ttyAMA2 20810 60
|
||||||
|
|
||||||
|
#Need to create the two service
|
||||||
|
# 1. start the scripts to set-up the services
|
||||||
|
# 2. rtc_save_to_db
|
||||||
|
|
||||||
|
#1. set-up the services (SARA, NPM, BME280, etc)
|
||||||
|
info "Setting up systemd services..."
|
||||||
|
|
||||||
|
if [[ -f "$REPO_DIR/services/setup_services.sh" ]]; then
|
||||||
|
sudo chmod +x "$REPO_DIR/services/setup_services.sh"
|
||||||
|
sudo "$REPO_DIR/services/setup_services.sh" || warning "Failed to set up systemd services"
|
||||||
|
success "Systemd services set up successfully."
|
||||||
|
else
|
||||||
|
warning "Systemd services setup script not found."
|
||||||
|
fi
|
||||||
|
|
||||||
|
#2. Add rtc_save_to_db.service
|
||||||
|
SERVICE_FILE_2="/etc/systemd/system/rtc_save_to_db.service"
|
||||||
|
info "Setting up systemd service for rtc_save_to_db..."
|
||||||
|
|
||||||
|
# Create the systemd service file (overwrite if necessary)
|
||||||
|
sudo bash -c "cat > $SERVICE_FILE_2" <<EOF
|
||||||
|
[Unit]
|
||||||
|
Description=RTC Save to DB Script
|
||||||
|
After=network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
ExecStart=/usr/bin/python3 /var/www/nebuleair_pro_4g/RTC/save_to_db.py
|
||||||
|
Restart=always
|
||||||
|
RestartSec=1
|
||||||
|
User=root
|
||||||
|
WorkingDirectory=/var/www/nebuleair_pro_4g
|
||||||
|
StandardOutput=append:/var/www/nebuleair_pro_4g/logs/rtc_save_to_db.log
|
||||||
|
StandardError=append:/var/www/nebuleair_pro_4g/logs/rtc_save_to_db_errors.log
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
EOF
|
||||||
|
|
||||||
|
success "Systemd service file created: $SERVICE_FILE_2"
|
||||||
|
|
||||||
|
# Reload systemd to recognize the new service
|
||||||
|
info "Reloading systemd daemon..."
|
||||||
|
sudo systemctl daemon-reload
|
||||||
|
|
||||||
|
# Enable the service to start on boot
|
||||||
|
info "Enabling the service to start on boot..."
|
||||||
|
sudo systemctl enable rtc_save_to_db.service
|
||||||
|
|
||||||
|
# Start the service immediately
|
||||||
|
info "Starting the service..."
|
||||||
|
sudo systemctl start rtc_save_to_db.service
|
||||||
@@ -12,10 +12,11 @@ CSV PAYLOAD (AirCarto Servers)
|
|||||||
/pro_4G/data.php?sensor_id={device_id}
|
/pro_4G/data.php?sensor_id={device_id}
|
||||||
|
|
||||||
ATTENTION : do not change order !
|
ATTENTION : do not change order !
|
||||||
|
CSV size: 18
|
||||||
{PM1},{PM25},{PM10},{temp},{hum},{press},{avg_noise},{max_noise},{min_noise},{envea_no2},{envea_h2s},{envea_o3},{4g_signal_quality}
|
{PM1},{PM25},{PM10},{temp},{hum},{press},{avg_noise},{max_noise},{min_noise},{envea_no2},{envea_h2s},{envea_o3},{4g_signal_quality}
|
||||||
0 -> PM1
|
0 -> PM1 (μg/m3)
|
||||||
1 -> PM25
|
1 -> PM25 (μg/m3)
|
||||||
2 -> PM10
|
2 -> PM10 (μg/m3)
|
||||||
3 -> temp
|
3 -> temp
|
||||||
4 -> hum
|
4 -> hum
|
||||||
5 -> press
|
5 -> press
|
||||||
@@ -25,7 +26,12 @@ CSV PAYLOAD (AirCarto Servers)
|
|||||||
9 -> envea_no2
|
9 -> envea_no2
|
||||||
10 -> envea_h2s
|
10 -> envea_h2s
|
||||||
11 -> envea_o3
|
11 -> envea_o3
|
||||||
12 -> 4G signal quality
|
12 -> 4G signal quality,
|
||||||
|
13 -> PM 0.2μm to 0.5μm quantity (Nb/L)
|
||||||
|
14 -> PM 0.5μm to 1.0μm quantity (Nb/L)
|
||||||
|
15 -> PM 1.0μm to 2.5μm quantity (Nb/L)
|
||||||
|
16 -> PM 2.5μm to 5.0μm quantity (Nb/L)
|
||||||
|
17 -> PM 5.0μm to 10μm quantity (Nb/L)
|
||||||
|
|
||||||
JSON PAYLOAD (Micro-Spot Servers)
|
JSON PAYLOAD (Micro-Spot Servers)
|
||||||
Same as NebuleAir wifi
|
Same as NebuleAir wifi
|
||||||
@@ -54,9 +60,12 @@ JSON PAYLOAD (Micro-Spot Servers)
|
|||||||
{"value_type":"latitude","value":"43.2964"},
|
{"value_type":"latitude","value":"43.2964"},
|
||||||
{"value_type":"longitude","value":"5.36978"},
|
{"value_type":"longitude","value":"5.36978"},
|
||||||
{"value_type":"state_npm","value":"State: 00000000"},
|
{"value_type":"state_npm","value":"State: 00000000"},
|
||||||
{"value_type":"th_npm","value":"28.47 / 37.54"},
|
{"value_type":"BME280_temperature","value":"28.47"},
|
||||||
|
{"value_type":"BME280_humidity","value":"28.47"},
|
||||||
|
{"value_type":"BME280_pressure","value":"28.47"},
|
||||||
{"value_type":"CAIRSENS_NO2","value":"54"},
|
{"value_type":"CAIRSENS_NO2","value":"54"},
|
||||||
{"value_type":"CAIRSENS_H2S","value":"54"},
|
{"value_type":"CAIRSENS_H2S","value":"54"},
|
||||||
|
{"value_type":"CAIRSENS_O3","value":"54"}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
@@ -68,9 +77,9 @@ import busio
|
|||||||
import re
|
import re
|
||||||
import os
|
import os
|
||||||
import traceback
|
import traceback
|
||||||
|
import sys
|
||||||
|
|
||||||
import RPi.GPIO as GPIO
|
import RPi.GPIO as GPIO
|
||||||
|
from threading import Thread
|
||||||
from adafruit_bme280 import basic as adafruit_bme280
|
from adafruit_bme280 import basic as adafruit_bme280
|
||||||
|
|
||||||
# Record the start time of the script
|
# Record the start time of the script
|
||||||
@@ -82,10 +91,9 @@ with open('/proc/uptime', 'r') as f:
|
|||||||
|
|
||||||
# Skip execution if uptime is less than 2 minutes (120 seconds)
|
# Skip execution if uptime is less than 2 minutes (120 seconds)
|
||||||
if uptime_seconds < 120:
|
if uptime_seconds < 120:
|
||||||
print("System just booted, skipping execution.")
|
print(f"System just booted ({uptime_seconds:.2f} seconds uptime), skipping execution.")
|
||||||
exit()
|
sys.exit()
|
||||||
|
|
||||||
url_nebuleair="data.nebuleair.fr"
|
|
||||||
payload_csv = [None] * 20
|
payload_csv = [None] * 20
|
||||||
payload_json = {
|
payload_json = {
|
||||||
"nebuleairid": "82D25549434",
|
"nebuleairid": "82D25549434",
|
||||||
@@ -93,11 +101,34 @@ payload_json = {
|
|||||||
"sensordatavalues": [] # Empty list to start with
|
"sensordatavalues": [] # Empty list to start with
|
||||||
}
|
}
|
||||||
|
|
||||||
|
aircarto_profile_id = 0
|
||||||
|
uSpot_profile_id = 1
|
||||||
|
|
||||||
# Set up GPIO mode (for Blue LED: network status)
|
def blink_led(pin, blink_count, delay=1):
|
||||||
GPIO.setwarnings(False)
|
"""
|
||||||
GPIO.setmode(GPIO.BCM) # Use Broadcom pin numbering
|
Blink an LED on a specified GPIO pin.
|
||||||
GPIO.setup(23, GPIO.OUT) # Set GPIO23 as an output pin
|
|
||||||
|
Args:
|
||||||
|
pin (int): GPIO pin number (BCM mode) to which the LED is connected.
|
||||||
|
blink_count (int): Number of times the LED should blink.
|
||||||
|
delay (float): Time in seconds for the LED to stay ON or OFF (default is 1 second).
|
||||||
|
"""
|
||||||
|
# GPIO setup
|
||||||
|
GPIO.setwarnings(False)
|
||||||
|
GPIO.setmode(GPIO.BCM) # Use BCM numbering
|
||||||
|
GPIO.setup(pin, GPIO.OUT) # Set the specified pin as an output
|
||||||
|
|
||||||
|
try:
|
||||||
|
for _ in range(blink_count):
|
||||||
|
GPIO.output(pin, GPIO.HIGH) # Turn the LED on
|
||||||
|
#print(f"LED on GPIO {pin} is ON")
|
||||||
|
time.sleep(delay) # Wait for the specified delay
|
||||||
|
GPIO.output(pin, GPIO.LOW) # Turn the LED off
|
||||||
|
#print(f"LED on GPIO {pin} is OFF")
|
||||||
|
time.sleep(delay) # Wait for the specified delay
|
||||||
|
finally:
|
||||||
|
GPIO.cleanup(pin) # Clean up the specific pin to reset its state
|
||||||
|
print(f"GPIO {pin} cleaned up")
|
||||||
|
|
||||||
#get data from config
|
#get data from config
|
||||||
def load_config(config_file):
|
def load_config(config_file):
|
||||||
@@ -143,6 +174,7 @@ config_file = '/var/www/nebuleair_pro_4g/config.json'
|
|||||||
|
|
||||||
# Load the configuration data
|
# Load the configuration data
|
||||||
config = load_config(config_file)
|
config = load_config(config_file)
|
||||||
|
loop_activation = config.get('loop_activation', True) #activation de la loop principale
|
||||||
baudrate = config.get('SaraR4_baudrate', 115200) #baudrate du sara R4
|
baudrate = config.get('SaraR4_baudrate', 115200) #baudrate du sara R4
|
||||||
device_id = config.get('deviceID', '').upper() #device ID en maj
|
device_id = config.get('deviceID', '').upper() #device ID en maj
|
||||||
need_to_log = config.get('loop_log', False) #inscription des logs
|
need_to_log = config.get('loop_log', False) #inscription des logs
|
||||||
@@ -150,6 +182,7 @@ bme_280_config = config.get('i2c_BME', False) #présence du BME280
|
|||||||
i2C_sound_config = config.get('i2C_sound', False) #présence du capteur son
|
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_aircarto = config.get('send_aircarto', True) #envoi sur AirCarto (data.nebuleair.fr)
|
||||||
send_uSpot = config.get('send_uSpot', False) #envoi sur MicroSpot ()
|
send_uSpot = config.get('send_uSpot', False) #envoi sur MicroSpot ()
|
||||||
|
npm_5channel = config.get('NextPM_5channels', False) #5 canaux du NPM
|
||||||
|
|
||||||
envea_sondes = config.get('envea_sondes', [])
|
envea_sondes = config.get('envea_sondes', [])
|
||||||
connected_envea_sondes = [sonde for sonde in envea_sondes if sonde.get('connected', False)]
|
connected_envea_sondes = [sonde for sonde in envea_sondes if sonde.get('connected', False)]
|
||||||
@@ -158,6 +191,10 @@ selected_networkID = config.get('SARA_R4_neworkID', '')
|
|||||||
#update device id in the payload json
|
#update device id in the payload json
|
||||||
payload_json["nebuleairid"] = device_id
|
payload_json["nebuleairid"] = device_id
|
||||||
|
|
||||||
|
if loop_activation == False:
|
||||||
|
print("Loop activation is False , skipping execution.")
|
||||||
|
sys.exit()
|
||||||
|
|
||||||
ser_sara = serial.Serial(
|
ser_sara = serial.Serial(
|
||||||
port='/dev/ttyAMA2',
|
port='/dev/ttyAMA2',
|
||||||
baudrate=baudrate, #115200 ou 9600
|
baudrate=baudrate, #115200 ou 9600
|
||||||
@@ -197,7 +234,7 @@ if connected_envea_sondes:
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
def read_complete_response(serial_connection, timeout=2, end_of_response_timeout=2, wait_for_line=None):
|
def read_complete_response(serial_connection, timeout=2, end_of_response_timeout=2, wait_for_line=None, debug=True):
|
||||||
response = bytearray()
|
response = bytearray()
|
||||||
serial_connection.timeout = timeout
|
serial_connection.timeout = timeout
|
||||||
end_time = time.time() + end_of_response_timeout
|
end_time = time.time() + end_of_response_timeout
|
||||||
@@ -214,15 +251,20 @@ def read_complete_response(serial_connection, timeout=2, end_of_response_timeout
|
|||||||
if wait_for_line:
|
if wait_for_line:
|
||||||
decoded_response = response.decode('utf-8', errors='replace')
|
decoded_response = response.decode('utf-8', errors='replace')
|
||||||
if wait_for_line in decoded_response:
|
if wait_for_line in decoded_response:
|
||||||
print(f"[DEBUG] 🔎Found target line: {wait_for_line}")
|
if debug: print(f"[DEBUG] 🔎Found target line: {wait_for_line}")
|
||||||
break
|
break
|
||||||
elif time.time() > end_time:
|
elif time.time() > end_time:
|
||||||
print(f"[DEBUG] Timeout reached. No more data received.")
|
if debug: print(f"[DEBUG] Timeout reached. No more data received.")
|
||||||
break
|
break
|
||||||
time.sleep(0.1) # Short sleep to prevent busy waiting
|
time.sleep(0.1) # Short sleep to prevent busy waiting
|
||||||
# Final response and debug output
|
# Final response and debug output
|
||||||
total_elapsed_time = time.time() - start_time
|
total_elapsed_time = time.time() - start_time
|
||||||
print(f"[DEBUG] ⏱️ elapsed time: {total_elapsed_time:.2f}s. ⏱️")
|
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 response.decode('utf-8', errors='replace')
|
||||||
|
|
||||||
# Open and read the JSON file
|
# Open and read the JSON file
|
||||||
@@ -265,7 +307,50 @@ try:
|
|||||||
payload_csv[3] = round(bme280.temperature, 2)
|
payload_csv[3] = round(bme280.temperature, 2)
|
||||||
payload_csv[4] = round(bme280.humidity, 2)
|
payload_csv[4] = round(bme280.humidity, 2)
|
||||||
payload_csv[5] = round(bme280.pressure, 2)
|
payload_csv[5] = round(bme280.pressure, 2)
|
||||||
|
|
||||||
|
payload_json["sensordatavalues"].append({"value_type": "BME280_temperature", "value": f"{round(bme280.temperature, 2)}"})
|
||||||
|
payload_json["sensordatavalues"].append({"value_type": "BME280_humidity", "value": f"{round(bme280.humidity, 2)}"})
|
||||||
|
payload_json["sensordatavalues"].append({"value_type": "BME280_pressure", "value": f"{round(bme280.pressure, 2)}"})
|
||||||
|
|
||||||
|
# NPM sur 5 cannaux
|
||||||
|
if npm_5channel:
|
||||||
|
print("Getting NPM 5 Channels")
|
||||||
|
# Define the path to the JSON file
|
||||||
|
json_file_path_npm = "/var/www/nebuleair_pro_4g/NPM/data/data.json"
|
||||||
|
# Read the JSON file
|
||||||
|
try:
|
||||||
|
with open(json_file_path_npm, "r") as file:
|
||||||
|
data = json.load(file) # Load JSON into a dictionary
|
||||||
|
|
||||||
|
# Extract values
|
||||||
|
channel_1 = data.get("channel_1", 0)
|
||||||
|
channel_2 = data.get("channel_2", 0)
|
||||||
|
channel_3 = data.get("channel_3", 0)
|
||||||
|
channel_4 = data.get("channel_4", 0)
|
||||||
|
channel_5 = data.get("channel_5", 0)
|
||||||
|
|
||||||
|
# Print extracted values
|
||||||
|
print(f"Channel 1: {channel_1}")
|
||||||
|
print(f"Channel 2: {channel_2}")
|
||||||
|
print(f"Channel 3: {channel_3}")
|
||||||
|
print(f"Channel 4: {channel_4}")
|
||||||
|
print(f"Channel 5: {channel_5}")
|
||||||
|
|
||||||
|
#add to CSV
|
||||||
|
payload_csv[13] = channel_1
|
||||||
|
payload_csv[14] = channel_2
|
||||||
|
payload_csv[15] = channel_3
|
||||||
|
payload_csv[16] = channel_4
|
||||||
|
payload_csv[17] = channel_5
|
||||||
|
|
||||||
|
|
||||||
|
except FileNotFoundError:
|
||||||
|
print(f"Error: JSON file not found at {json_file_path_npm}")
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
print("Error: JSON file is not formatted correctly")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Unexpected error: {e}")
|
||||||
|
|
||||||
# Sonde Bruit connected
|
# Sonde Bruit connected
|
||||||
if i2C_sound_config:
|
if i2C_sound_config:
|
||||||
#on récupère les infos de sound_metermoving et on les ajoute au message
|
#on récupère les infos de sound_metermoving et on les ajoute au message
|
||||||
@@ -282,52 +367,48 @@ try:
|
|||||||
payload_csv[8] = min_noise
|
payload_csv[8] = min_noise
|
||||||
|
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
print(f"Error: File {file_path} not found.")
|
print(f"Error: File {file_path_data_noise} not found.")
|
||||||
except ValueError:
|
except ValueError:
|
||||||
print("Error: File content is not valid numbers.")
|
print("Error: File content is not valid numbers.")
|
||||||
|
|
||||||
# Sondes Envea
|
# Sondes Envea
|
||||||
if connected_envea_sondes:
|
if connected_envea_sondes:
|
||||||
# Pour chacune des sondes
|
print("Getting Envea values")
|
||||||
for device in connected_envea_sondes:
|
# Define the path to the JSON file
|
||||||
port = device.get('port', 'Unknown')
|
json_file_path_envea = "/var/www/nebuleair_pro_4g/envea/data/data.json"
|
||||||
name = device.get('name', 'Unknown')
|
# Read the JSON file
|
||||||
coefficient = device.get('coefficient', 'Unknown')
|
try:
|
||||||
|
with open(json_file_path_envea, "r") as file:
|
||||||
|
data = json.load(file) # Load JSON into a dictionary
|
||||||
|
|
||||||
print(f"Connected envea Sonde: {name} on port {port} and coefficient {coefficient} ")
|
# Extract values
|
||||||
|
h2s = data.get("h2s", 0)
|
||||||
if name in serial_connections:
|
no2 = data.get("no2", 0)
|
||||||
serial_connection = serial_connections[name]
|
o3 = data.get("o3", 0)
|
||||||
|
|
||||||
try:
|
# Print extracted values
|
||||||
# Write data to the device
|
print(f"h2s : {h2s}")
|
||||||
serial_connection.write(
|
print(f"no2 : {no2}")
|
||||||
b"\xFF\x02\x13\x30\x01\x02\x03\x04\x05\x06\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\x12\xAF\x88\x03"
|
print(f"o3: {o3}")
|
||||||
)
|
|
||||||
|
|
||||||
# Read data from the device
|
|
||||||
data_envea = serial_connection.readline()
|
|
||||||
if len(data_envea) >= 20:
|
|
||||||
byte_20 = data_envea[19]
|
|
||||||
byte_20 = byte_20 * coefficient
|
|
||||||
|
|
||||||
# Update payload CSV based on device type
|
|
||||||
if name == "h2s":
|
|
||||||
payload_csv[10] = byte_20
|
|
||||||
if name == "no2":
|
|
||||||
payload_csv[9] = byte_20
|
|
||||||
if name == "o3":
|
|
||||||
payload_csv[11] = byte_20
|
|
||||||
|
|
||||||
print(f"Data from envea {name}: {byte_20}")
|
|
||||||
else:
|
|
||||||
print(f"Données reçues insuffisantes pour {name} pour extraire le 20ème octet.")
|
|
||||||
|
|
||||||
except serial.SerialException as e:
|
|
||||||
print(f"Error communicating with {name}: {e}")
|
|
||||||
else:
|
|
||||||
print(f"No serial connection for {name}")
|
|
||||||
|
|
||||||
|
#add to CSV
|
||||||
|
payload_csv[10] = h2s
|
||||||
|
payload_csv[9] = no2
|
||||||
|
payload_csv[11] = o3
|
||||||
|
|
||||||
|
#add to JSON
|
||||||
|
payload_json["sensordatavalues"].append({"value_type": "CAIRSENS_H2S", "value": str(h2s)})
|
||||||
|
payload_json["sensordatavalues"].append({"value_type": "CAIRSENS_NO2", "value": str(no2)})
|
||||||
|
payload_json["sensordatavalues"].append({"value_type": "CAIRSENS_O3", "value": str(o3)})
|
||||||
|
|
||||||
|
|
||||||
|
except FileNotFoundError:
|
||||||
|
print(f"Error: JSON file not found at {json_file_path_envea}")
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
print("Error: JSON file is not formatted correctly")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Unexpected error: {e}")
|
||||||
|
|
||||||
# Getting the LTE Signal
|
# Getting the LTE Signal
|
||||||
print("-> Getting LTE signal <-")
|
print("-> Getting LTE signal <-")
|
||||||
ser_sara.write(b'AT+CSQ\r')
|
ser_sara.write(b'AT+CSQ\r')
|
||||||
@@ -337,11 +418,31 @@ try:
|
|||||||
print("</p>")
|
print("</p>")
|
||||||
match = re.search(r'\+CSQ:\s*(\d+),', response2)
|
match = re.search(r'\+CSQ:\s*(\d+),', response2)
|
||||||
if match:
|
if match:
|
||||||
signal_quality = match.group(1)
|
signal_quality = int(match.group(1))
|
||||||
print("Signal Quality:", signal_quality)
|
|
||||||
payload_csv[12]=signal_quality
|
payload_csv[12]=signal_quality
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
|
|
||||||
|
# On vérifie si le signal n'est pas à 99 pour déconnexion
|
||||||
|
# si c'est le cas on essaie de se reconnecter
|
||||||
|
if signal_quality == 99:
|
||||||
|
print('<span style="color: red;font-weight: bold;">⚠️ATTENTION: Signal Quality indicates no signal (99)⚠️</span>')
|
||||||
|
print("TRY TO RECONNECT:")
|
||||||
|
command = f'AT+COPS=1,2,"{selected_networkID}"\r'
|
||||||
|
ser_sara.write(command.encode('utf-8'))
|
||||||
|
responseReconnect = read_complete_response(ser_sara, timeout=20, end_of_response_timeout=20)
|
||||||
|
print('<p class="text-danger-emphasis">')
|
||||||
|
print(responseReconnect)
|
||||||
|
print("</p>")
|
||||||
|
|
||||||
|
print('🛑STOP LOOP🛑')
|
||||||
|
print("<hr>")
|
||||||
|
|
||||||
|
#on arrete le script pas besoin de continuer
|
||||||
|
sys.exit()
|
||||||
|
else:
|
||||||
|
print("Signal Quality:", signal_quality)
|
||||||
|
|
||||||
|
|
||||||
#print(payload_json)
|
#print(payload_json)
|
||||||
|
|
||||||
'''
|
'''
|
||||||
@@ -355,36 +456,28 @@ try:
|
|||||||
print("Open JSON:")
|
print("Open JSON:")
|
||||||
command = f'AT+UDWNFILE="sensordata_csv.json",{size_of_string}\r'
|
command = f'AT+UDWNFILE="sensordata_csv.json",{size_of_string}\r'
|
||||||
ser_sara.write(command.encode('utf-8'))
|
ser_sara.write(command.encode('utf-8'))
|
||||||
response_SARA_1 = read_complete_response(ser_sara, wait_for_line=">")
|
response_SARA_1 = read_complete_response(ser_sara, wait_for_line=">", debug=False)
|
||||||
print(response_SARA_1)
|
print(response_SARA_1)
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
|
|
||||||
#2. Write to shell
|
#2. Write to shell
|
||||||
print("Write data to memory:")
|
print("Write data to memory:")
|
||||||
ser_sara.write(csv_string.encode())
|
ser_sara.write(csv_string.encode())
|
||||||
response_SARA_2 = read_complete_response(ser_sara, wait_for_line="OK")
|
response_SARA_2 = read_complete_response(ser_sara, wait_for_line="OK", debug=False)
|
||||||
print(response_SARA_2)
|
print(response_SARA_2)
|
||||||
|
|
||||||
#3. Send to endpoint (with device ID)
|
#3. Send to endpoint (with device ID)
|
||||||
print("Send data (POST REQUEST):")
|
print("Send data (POST REQUEST):")
|
||||||
command= f'AT+UHTTPC=0,4,"/pro_4G/data.php?sensor_id={device_id}","server_response.txt","sensordata_csv.json",4\r'
|
command= f'AT+UHTTPC={aircarto_profile_id},4,"/pro_4G/data.php?sensor_id={device_id}","server_response.txt","sensordata_csv.json",4\r'
|
||||||
ser_sara.write(command.encode('utf-8'))
|
ser_sara.write(command.encode('utf-8'))
|
||||||
|
|
||||||
response_SARA_3 = read_complete_response(ser_sara, timeout=5, end_of_response_timeout=45, wait_for_line="+UUHTTPCR")
|
response_SARA_3 = read_complete_response(ser_sara, timeout=5, end_of_response_timeout=120, wait_for_line="+UUHTTPCR")
|
||||||
|
|
||||||
print('<p class="text-danger-emphasis">')
|
print('<p class="text-danger-emphasis">')
|
||||||
print(response_SARA_3)
|
print(response_SARA_3)
|
||||||
print("</p>")
|
print("</p>")
|
||||||
|
|
||||||
# Wait for the +UUHTTPCR response
|
# si on recoit la réponse UHTTPCR
|
||||||
#print("Waiting for +UUHTTPCR response...")
|
|
||||||
#response_received = False
|
|
||||||
#while not response_received:
|
|
||||||
# response_SARA_3 = read_complete_response(ser_sara, timeout=5)
|
|
||||||
# print(response_SARA_3.strip())
|
|
||||||
# if "+UUHTTPCR" in response_SARA_3:
|
|
||||||
# response_received = True
|
|
||||||
|
|
||||||
if "+UUHTTPCR" in response_SARA_3:
|
if "+UUHTTPCR" in response_SARA_3:
|
||||||
print("✅ Received +UUHTTPCR response.")
|
print("✅ Received +UUHTTPCR response.")
|
||||||
|
|
||||||
@@ -431,14 +524,9 @@ try:
|
|||||||
print("Operation not allowed. This may require a different configuration.")
|
print("Operation not allowed. This may require a different configuration.")
|
||||||
# Actions spécifiques pour ce type d'erreur
|
# Actions spécifiques pour ce type d'erreur
|
||||||
|
|
||||||
# Clignotement LED en cas d'erreur
|
# Clignotement LED rouge en cas d'erreur
|
||||||
GPIO.output(23, GPIO.LOW) # Éteindre la LED définitivement
|
led_thread = Thread(target=blink_led, args=(24, 5, 0.5))
|
||||||
for _ in range(4):
|
led_thread.start()
|
||||||
GPIO.output(23, GPIO.HIGH) # Allumer la LED
|
|
||||||
time.sleep(0.1)
|
|
||||||
GPIO.output(23, GPIO.LOW) # Éteindre la LED
|
|
||||||
time.sleep(0.1)
|
|
||||||
GPIO.output(23, GPIO.LOW) # Turn off the LED
|
|
||||||
|
|
||||||
else:
|
else:
|
||||||
# 2.Si la réponse contient une réponse HTTP valide
|
# 2.Si la réponse contient une réponse HTTP valide
|
||||||
@@ -454,46 +542,68 @@ try:
|
|||||||
print('<span style="color: red;font-weight: bold;">ATTENTION: HTTP operation failed</span>')
|
print('<span style="color: red;font-weight: bold;">ATTENTION: HTTP operation failed</span>')
|
||||||
update_json_key(config_file, "SARA_R4_network_status", "disconnected")
|
update_json_key(config_file, "SARA_R4_network_status", "disconnected")
|
||||||
print("*****")
|
print("*****")
|
||||||
print("resetting the URL (domain name):")
|
print("Blink red LED")
|
||||||
print("Turning off the blue LED...")
|
# Run LED blinking in a separate thread
|
||||||
for _ in range(4): # Faire clignoter 4 fois
|
led_thread = Thread(target=blink_led, args=(24, 5, 0.5))
|
||||||
GPIO.output(23, GPIO.HIGH) # Allumer la LED
|
led_thread.start()
|
||||||
time.sleep(0.1) # Attendre 100 ms
|
|
||||||
GPIO.output(23, GPIO.LOW) # Éteindre la LED
|
# Get error code
|
||||||
time.sleep(0.1) # Attendre 100 ms
|
print("Getting error code (11->Server connection error, 73->Secure socket connect error)")
|
||||||
GPIO.output(23, GPIO.LOW) # Turn off the LED
|
command = f'AT+UHTTPER={aircarto_profile_id}\r'
|
||||||
command = f'AT+UHTTP=0,1,"{url_nebuleair}"\r'
|
ser_sara.write(command.encode('utf-8'))
|
||||||
ser_sara.write(command.encode('utf-8'))
|
response_SARA_9 = read_complete_response(ser_sara, wait_for_line="OK", debug=False)
|
||||||
response_SARA_31 = read_complete_response(ser_sara)
|
print('<p class="text-danger-emphasis">')
|
||||||
if need_to_log:
|
print(response_SARA_9)
|
||||||
print(response_SARA_31)
|
print("</p>")
|
||||||
|
|
||||||
|
'''
|
||||||
|
+UHTTPER: profile_id,error_class,error_code
|
||||||
|
|
||||||
|
error_class
|
||||||
|
0 OK, no error
|
||||||
|
3 HTTP Protocol error class
|
||||||
|
10 Wrong HTTP API USAGE
|
||||||
|
|
||||||
|
error_code (for error_class 3)
|
||||||
|
0 No error
|
||||||
|
11 Server connection error
|
||||||
|
73 Secure socket connect error
|
||||||
|
'''
|
||||||
|
|
||||||
|
#Pas forcément un moyen de résoudre le soucis
|
||||||
|
#print("resetting the URL (domain name):")
|
||||||
|
#command = f'AT+UHTTP={aircarto_profile_id},1,"{url_nebuleair}"\r'
|
||||||
|
#ser_sara.write(command.encode('utf-8'))
|
||||||
|
#response_SARA_31 = read_complete_response(ser_sara)
|
||||||
|
#print(response_SARA_31)
|
||||||
|
|
||||||
# 2.2 code 1 (HHTP succeded)
|
# 2.2 code 1 (HHTP succeded)
|
||||||
else:
|
else:
|
||||||
# Si la commande HTTP a réussi
|
# Si la commande HTTP a réussi
|
||||||
print('<span class="badge text-bg-success">HTTP operation successful.</span>')
|
print('<span class="badge text-bg-success">HTTP operation successful.</span>')
|
||||||
update_json_key(config_file, "SARA_R4_network_status", "connected")
|
update_json_key(config_file, "SARA_R4_network_status", "connected")
|
||||||
print("Turning on the blue LED...")
|
print("Blink blue LED")
|
||||||
for _ in range(4): # Faire clignoter 4 fois
|
led_thread = Thread(target=blink_led, args=(23, 5, 0.5))
|
||||||
GPIO.output(23, GPIO.HIGH) # Allumer la LED
|
led_thread.start()
|
||||||
time.sleep(0.1) # Attendre 100 ms
|
|
||||||
GPIO.output(23, GPIO.LOW) # Éteindre la LED
|
|
||||||
time.sleep(0.1) # Attendre 100 ms
|
|
||||||
GPIO.output(23, GPIO.HIGH) # Turn on the LED
|
|
||||||
#4. Read reply from server
|
#4. Read reply from server
|
||||||
print("Reply from server:")
|
print("Reply from server:")
|
||||||
ser_sara.write(b'AT+URDFILE="server_response.txt"\r')
|
ser_sara.write(b'AT+URDFILE="server_response.txt"\r')
|
||||||
response_SARA_4 = read_complete_response(ser_sara, wait_for_line="OK")
|
response_SARA_4 = read_complete_response(ser_sara, wait_for_line="OK", debug=False)
|
||||||
print('<p class="text-success">')
|
print('<p class="text-success">')
|
||||||
print(response_SARA_4)
|
print(response_SARA_4)
|
||||||
print('</p>')
|
print('</p>')
|
||||||
else:
|
else:
|
||||||
print('<span style="color: red;font-weight: bold;">No UUHTTPCR response</span>')
|
print('<span style="color: red;font-weight: bold;">No UUHTTPCR response</span>')
|
||||||
|
print("Blink red LED")
|
||||||
|
# Run LED blinking in a separate thread
|
||||||
|
led_thread = Thread(target=blink_led, args=(24, 5, 0.5))
|
||||||
|
led_thread.start()
|
||||||
|
|
||||||
|
|
||||||
#5. empty json
|
#5. empty json
|
||||||
print("Empty SARA memory:")
|
print("Empty SARA memory:")
|
||||||
ser_sara.write(b'AT+UDELFILE="sensordata_csv.json"\r')
|
ser_sara.write(b'AT+UDELFILE="sensordata_csv.json"\r')
|
||||||
response_SARA_5 = read_complete_response(ser_sara, wait_for_line="OK")
|
response_SARA_5 = read_complete_response(ser_sara, wait_for_line="OK", debug=False)
|
||||||
print(response_SARA_5)
|
print(response_SARA_5)
|
||||||
|
|
||||||
'''
|
'''
|
||||||
@@ -504,32 +614,31 @@ try:
|
|||||||
print(">>>>>>>>")
|
print(">>>>>>>>")
|
||||||
print(">>>>>>>>")
|
print(">>>>>>>>")
|
||||||
print("SEND TO MICRO SPOT (HTTP):")
|
print("SEND TO MICRO SPOT (HTTP):")
|
||||||
profile_id = 1
|
|
||||||
|
|
||||||
#step 4: set url (op_code = 1)
|
#step 4: set url (op_code = 1)
|
||||||
print("****")
|
print("****")
|
||||||
print("SET URL")
|
print("SET URL")
|
||||||
command = f'AT+UHTTP={profile_id},1,"api-prod.uspot.probesys.net"\r'
|
command = f'AT+UHTTP={uSpot_profile_id},1,"api-prod.uspot.probesys.net"\r'
|
||||||
ser_sara.write((command + '\r').encode('utf-8'))
|
ser_sara.write((command + '\r').encode('utf-8'))
|
||||||
response_SARA_5 = read_complete_response(ser_sara, wait_for_line="OK")
|
response_SARA_5 = read_complete_response(ser_sara, wait_for_line="OK", debug=False)
|
||||||
print(response_SARA_5)
|
print(response_SARA_5)
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
|
|
||||||
#step 4: set url to SSL (op_code = 6) (http_secure = 1 for HTTPS)(USECMNG_PROFILE = 2)
|
#step 4: set url to SSL (op_code = 6) (http_secure = 1 for HTTPS)(USECMNG_PROFILE = 2)
|
||||||
print("****")
|
print("****")
|
||||||
print("SET SSL")
|
print("SET SSL")
|
||||||
command = f'AT+UHTTP={profile_id},6,0\r'
|
command = f'AT+UHTTP={uSpot_profile_id},6,0\r'
|
||||||
ser_sara.write(command.encode('utf-8'))
|
ser_sara.write(command.encode('utf-8'))
|
||||||
response_SARA_5 = read_complete_response(ser_sara, wait_for_line="OK")
|
response_SARA_5 = read_complete_response(ser_sara, wait_for_line="OK", debug=False)
|
||||||
print(response_SARA_5)
|
print(response_SARA_5)
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
|
|
||||||
#step 4: set PORT (op_code = 5)
|
#step 4: set PORT (op_code = 5)
|
||||||
print("****")
|
print("****")
|
||||||
print("SET PORT")
|
print("SET PORT")
|
||||||
command = f'AT+UHTTP={profile_id},5,81\r'
|
command = f'AT+UHTTP={uSpot_profile_id},5,81\r'
|
||||||
ser_sara.write((command + '\r').encode('utf-8'))
|
ser_sara.write((command + '\r').encode('utf-8'))
|
||||||
response_SARA_55 = read_complete_response(ser_sara, wait_for_line="OK")
|
response_SARA_55 = read_complete_response(ser_sara, wait_for_line="OK", debug=False)
|
||||||
print(response_SARA_55)
|
print(response_SARA_55)
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
|
|
||||||
@@ -543,20 +652,20 @@ try:
|
|||||||
size_of_string = len(payload_string)
|
size_of_string = len(payload_string)
|
||||||
command = f'AT+UDWNFILE="sensordata_json.json",{size_of_string}\r'
|
command = f'AT+UDWNFILE="sensordata_json.json",{size_of_string}\r'
|
||||||
ser_sara.write((command + '\r').encode('utf-8'))
|
ser_sara.write((command + '\r').encode('utf-8'))
|
||||||
response_SARA_1 = read_complete_response(ser_sara, wait_for_line=">")
|
response_SARA_1 = read_complete_response(ser_sara, wait_for_line=">", debug=False)
|
||||||
print(response_SARA_1)
|
print(response_SARA_1)
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
|
|
||||||
#2. Write to shell
|
#2. Write to shell
|
||||||
print("Write to memory:")
|
print("Write to memory:")
|
||||||
ser_sara.write(payload_string.encode())
|
ser_sara.write(payload_string.encode())
|
||||||
response_SARA_2 = read_complete_response(ser_sara, wait_for_line="OK")
|
response_SARA_2 = read_complete_response(ser_sara, wait_for_line="OK", debug=False)
|
||||||
print(response_SARA_2)
|
print(response_SARA_2)
|
||||||
|
|
||||||
#step 4: trigger the request (http_command=1 for GET and http_command=1 for POST)
|
#step 4: trigger the request (http_command=1 for GET and http_command=1 for POST)
|
||||||
print("****")
|
print("****")
|
||||||
print("Trigger POST REQUEST")
|
print("Trigger POST REQUEST")
|
||||||
command = f'AT+UHTTPC={profile_id},4,"/nebuleair?token=2AFF6dQk68daFZ","http.resp","sensordata_json.json",4\r'
|
command = f'AT+UHTTPC={uSpot_profile_id},4,"/nebuleair?token=2AFF6dQk68daFZ","http.resp","sensordata_json.json",4\r'
|
||||||
ser_sara.write(command.encode('utf-8'))
|
ser_sara.write(command.encode('utf-8'))
|
||||||
|
|
||||||
|
|
||||||
@@ -569,7 +678,7 @@ try:
|
|||||||
print("****")
|
print("****")
|
||||||
print("Read reply from server")
|
print("Read reply from server")
|
||||||
ser_sara.write(b'AT+URDFILE="http.resp"\r')
|
ser_sara.write(b'AT+URDFILE="http.resp"\r')
|
||||||
response_SARA_7 = read_complete_response(ser_sara, wait_for_line="OK")
|
response_SARA_7 = read_complete_response(ser_sara, wait_for_line="OK", debug=False)
|
||||||
print('<p class="text-success">')
|
print('<p class="text-success">')
|
||||||
print(response_SARA_7)
|
print(response_SARA_7)
|
||||||
print('</p>')
|
print('</p>')
|
||||||
@@ -578,17 +687,15 @@ try:
|
|||||||
#5. empty json
|
#5. empty json
|
||||||
print("Empty SARA memory:")
|
print("Empty SARA memory:")
|
||||||
ser_sara.write(b'AT+UDELFILE="sensordata_json.json"\r')
|
ser_sara.write(b'AT+UDELFILE="sensordata_json.json"\r')
|
||||||
response_SARA_8 = read_complete_response(ser_sara, wait_for_line="OK")
|
response_SARA_8 = read_complete_response(ser_sara, wait_for_line="OK", debug=False)
|
||||||
print(response_SARA_8)
|
print(response_SARA_8)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# Calculate and print the elapsed time
|
# Calculate and print the elapsed time
|
||||||
elapsed_time = time.time() - start_time_script
|
elapsed_time = time.time() - start_time_script
|
||||||
if need_to_log:
|
print(f"Elapsed time: {elapsed_time:.2f} seconds")
|
||||||
print(f"Elapsed time: {elapsed_time:.2f} seconds")
|
print("<hr>")
|
||||||
print("<hr>")
|
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print("An error occurred:", e)
|
print("An error occurred:", e)
|
||||||
|
|||||||
1442
loop/SARA_send_data_v2.py
Executable file
1442
loop/SARA_send_data_v2.py
Executable file
File diff suppressed because it is too large
Load Diff
23
config.json.dist → old/config.json.dist
Executable file → Normal file
23
config.json.dist → old/config.json.dist
Executable file → Normal file
@@ -1,24 +1,39 @@
|
|||||||
{
|
{
|
||||||
"loop_log": true,
|
"modem_config_mode": false,
|
||||||
"boot_log": true,
|
"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",
|
"deviceID": "XXXX",
|
||||||
|
"npm_5channel": false,
|
||||||
|
"latitude_raw": 0,
|
||||||
|
"longitude_raw":0,
|
||||||
|
"latitude_precision": 0,
|
||||||
|
"longitude_precision": 0,
|
||||||
"deviceName": "NebuleAir-proXXX",
|
"deviceName": "NebuleAir-proXXX",
|
||||||
"SaraR4_baudrate": 115200,
|
"SaraR4_baudrate": 115200,
|
||||||
|
"NPM_solo_port": "/dev/ttyAMA5",
|
||||||
"NextPM_ports": [
|
"NextPM_ports": [
|
||||||
"ttyAMA5"
|
"ttyAMA5"
|
||||||
],
|
],
|
||||||
"i2C_sound": false,
|
"i2C_sound": false,
|
||||||
"i2c_BME": false,
|
"i2c_RTC": false,
|
||||||
|
"local_storage": false,
|
||||||
"sshTunnel_port": 59228,
|
"sshTunnel_port": 59228,
|
||||||
"npm1_status": "connected",
|
"npm1_status": "connected",
|
||||||
"SARA_R4_general_status": "connected",
|
"SARA_R4_general_status": "connected",
|
||||||
"SARA_R4_SIM_status": "connected",
|
"SARA_R4_SIM_status": "connected",
|
||||||
"SARA_R4_network_status": "connected",
|
"SARA_R4_network_status": "connected",
|
||||||
"SARA_R4_neworkID": 0,
|
"SARA_R4_neworkID": 20810,
|
||||||
"WIFI_status": "connected",
|
"WIFI_status": "connected",
|
||||||
"MQTT_GUI": false,
|
"MQTT_GUI": false,
|
||||||
"send_aircarto": true,
|
"send_aircarto": true,
|
||||||
"send_uSpot": false,
|
"send_uSpot": false,
|
||||||
|
"modem_version": "XXX",
|
||||||
"envea_sondes": [
|
"envea_sondes": [
|
||||||
{
|
{
|
||||||
"connected": false,
|
"connected": false,
|
||||||
0
install_software.yaml → old/install_software.yaml
Executable file → Normal file
0
install_software.yaml → old/install_software.yaml
Executable file → Normal file
@@ -1,6 +1,11 @@
|
|||||||
<?php
|
<?php
|
||||||
$type=$_GET['type'];
|
$type=$_GET['type'];
|
||||||
|
|
||||||
|
if ($type == "update_config") {
|
||||||
|
|
||||||
|
echo "updating....";
|
||||||
|
}
|
||||||
|
|
||||||
if ($type == "RTC_time") {
|
if ($type == "RTC_time") {
|
||||||
$time = shell_exec("date '+%d/%m/%Y %H:%M:%S'");
|
$time = shell_exec("date '+%d/%m/%Y %H:%M:%S'");
|
||||||
echo $time;
|
echo $time;
|
||||||
166
old/master.py
Executable file
166
old/master.py
Executable file
@@ -0,0 +1,166 @@
|
|||||||
|
'''
|
||||||
|
__ __ _
|
||||||
|
| \/ | __ _ ___| |_ ___ _ __
|
||||||
|
| |\/| |/ _` / __| __/ _ \ '__|
|
||||||
|
| | | | (_| \__ \ || __/ |
|
||||||
|
|_| |_|\__,_|___/\__\___|_|
|
||||||
|
|
||||||
|
Master Python script that will trigger other scripts at every chosen time pace
|
||||||
|
This script is triggered as a systemd service used as an alternative to cronjobs
|
||||||
|
|
||||||
|
Attention:
|
||||||
|
to do -> prevent SARA R4 Script to run again if it's taking more than 60 secs to finish (using a lock file ??)
|
||||||
|
|
||||||
|
First time: need to create the service file
|
||||||
|
|
||||||
|
--> sudo nano /etc/systemd/system/master_nebuleair.service
|
||||||
|
|
||||||
|
⬇️
|
||||||
|
[Unit]
|
||||||
|
Description=Master manager for the Python loop scripts
|
||||||
|
After=network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
ExecStart=/usr/bin/python3 /var/www/nebuleair_pro_4g/master.py
|
||||||
|
Restart=always
|
||||||
|
User=root
|
||||||
|
WorkingDirectory=/var/www/nebuleair_pro_4g
|
||||||
|
StandardOutput=append:/var/www/nebuleair_pro_4g/logs/master.log
|
||||||
|
StandardError=append:/var/www/nebuleair_pro_4g/logs/master_errors.log
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
⬆️
|
||||||
|
|
||||||
|
Reload systemd (first time after creating the service):
|
||||||
|
sudo systemctl daemon-reload
|
||||||
|
|
||||||
|
Enable (once), start (once and after stopping) and restart (after modification)systemd:
|
||||||
|
sudo systemctl enable master_nebuleair.service
|
||||||
|
sudo systemctl start master_nebuleair.service
|
||||||
|
sudo systemctl restart master_nebuleair.service
|
||||||
|
|
||||||
|
Check the service status:
|
||||||
|
sudo systemctl status master_nebuleair.service
|
||||||
|
|
||||||
|
|
||||||
|
Specific scripts can be disabled with config.json
|
||||||
|
Exemple: stop gathering data from NPM
|
||||||
|
Exemple: stop sending data with SARA R4
|
||||||
|
|
||||||
|
'''
|
||||||
|
import time
|
||||||
|
import threading
|
||||||
|
import subprocess
|
||||||
|
import os
|
||||||
|
import sqlite3
|
||||||
|
|
||||||
|
|
||||||
|
# Base directory where scripts are stored
|
||||||
|
SCRIPT_DIR = "/var/www/nebuleair_pro_4g/"
|
||||||
|
DB_PATH = "/var/www/nebuleair_pro_4g/sqlite/sensors.db"
|
||||||
|
|
||||||
|
# Lock file path for SARA script
|
||||||
|
SARA_LOCK_FILE = "/var/www/nebuleair_pro_4g/sara_script.lock"
|
||||||
|
|
||||||
|
|
||||||
|
def is_script_enabled(script_name):
|
||||||
|
"""Check if a script is enabled in the database."""
|
||||||
|
try:
|
||||||
|
conn = sqlite3.connect(DB_PATH)
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
cursor.execute(
|
||||||
|
"SELECT enabled FROM config_scripts_table WHERE script_path = ?",
|
||||||
|
(script_name,)
|
||||||
|
)
|
||||||
|
|
||||||
|
result = cursor.fetchone()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
if result is None:
|
||||||
|
return True # Default to enabled if not found in database
|
||||||
|
|
||||||
|
return bool(result[0])
|
||||||
|
except Exception:
|
||||||
|
# If any database error occurs, default to enabled
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def create_lock_file():
|
||||||
|
"""Create a lock file for the SARA script."""
|
||||||
|
with open(SARA_LOCK_FILE, 'w') as f:
|
||||||
|
f.write(str(int(time.time())))
|
||||||
|
|
||||||
|
|
||||||
|
def remove_lock_file():
|
||||||
|
"""Remove the SARA script lock file."""
|
||||||
|
if os.path.exists(SARA_LOCK_FILE):
|
||||||
|
os.remove(SARA_LOCK_FILE)
|
||||||
|
|
||||||
|
|
||||||
|
def is_script_locked():
|
||||||
|
"""Check if the SARA script is currently locked."""
|
||||||
|
if not os.path.exists(SARA_LOCK_FILE):
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Check if lock is older than 60 seconds (stale)
|
||||||
|
with open(SARA_LOCK_FILE, 'r') as f:
|
||||||
|
try:
|
||||||
|
lock_time = int(f.read().strip())
|
||||||
|
if time.time() - lock_time > 60:
|
||||||
|
# Lock is stale, remove it
|
||||||
|
remove_lock_file()
|
||||||
|
return False
|
||||||
|
except ValueError:
|
||||||
|
# Invalid lock file content
|
||||||
|
remove_lock_file()
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def run_script(script_name, interval, delay=0):
|
||||||
|
"""Run a script in a synchronized loop with an optional start delay."""
|
||||||
|
script_path = os.path.join(SCRIPT_DIR, script_name) # Build full path
|
||||||
|
next_run = time.monotonic() + delay # Apply the initial delay
|
||||||
|
|
||||||
|
while True:
|
||||||
|
if is_script_enabled(script_name):
|
||||||
|
# Special handling for SARA script to prevent concurrent runs
|
||||||
|
if script_name == "loop/SARA_send_data_v2.py":
|
||||||
|
if not is_script_locked():
|
||||||
|
create_lock_file()
|
||||||
|
try:
|
||||||
|
subprocess.run(["python3", script_path], timeout=200)
|
||||||
|
finally:
|
||||||
|
remove_lock_file()
|
||||||
|
else:
|
||||||
|
# Run other scripts normally
|
||||||
|
subprocess.run(["python3", script_path])
|
||||||
|
|
||||||
|
# Wait until the next exact interval
|
||||||
|
next_run += interval
|
||||||
|
sleep_time = max(0, next_run - time.monotonic()) # Prevent negative sleep times
|
||||||
|
time.sleep(sleep_time)
|
||||||
|
|
||||||
|
|
||||||
|
# Define scripts and their execution intervals (seconds)
|
||||||
|
SCRIPTS = [
|
||||||
|
# Format: (script_name, interval_in_seconds, start_delay_in_seconds)
|
||||||
|
("NPM/get_data_modbus_v3.py", 10, 0), # Get NPM data (modbus 5 channels) every 10s
|
||||||
|
("envea/read_value_v2.py", 10, 0), # Get Envea data every 10s
|
||||||
|
("loop/SARA_send_data_v2.py", 60, 1), # Send data every 60 seconds, with 1s delay
|
||||||
|
("BME280/get_data_v2.py", 120, 0), # Get BME280 data every 120 seconds
|
||||||
|
("MPPT/read.py", 120, 0), # Get MPPT data every 120 seconds
|
||||||
|
("sqlite/flush_old_data.py", 86400, 0) # Flush old data inside db every day
|
||||||
|
]
|
||||||
|
|
||||||
|
# Start threads for scripts
|
||||||
|
for script_name, interval, delay in SCRIPTS:
|
||||||
|
thread = threading.Thread(target=run_script, args=(script_name, interval, delay), daemon=True)
|
||||||
|
thread.start()
|
||||||
|
|
||||||
|
# Keep the main script running
|
||||||
|
while True:
|
||||||
|
time.sleep(1)
|
||||||
18
services/README.md
Normal file
18
services/README.md
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
# NebuleAir Pro Services
|
||||||
|
|
||||||
|
Les scripts importants tournent à l'aide d'un service et d'un timer associé.
|
||||||
|
|
||||||
|
Pour les installer:
|
||||||
|
|
||||||
|
sudo chmod +x /var/www/nebuleair_pro_4g/services/setup_services.sh
|
||||||
|
sudo /var/www/nebuleair_pro_4g/services/setup_services.sh
|
||||||
|
|
||||||
|
Supprimer l'ancien master:
|
||||||
|
sudo systemctl stop master_nebuleair.service
|
||||||
|
sudo systemctl disable master_nebuleair.service
|
||||||
|
|
||||||
|
# Check les services
|
||||||
|
|
||||||
|
SARA:
|
||||||
|
sudo systemctl status nebuleair-sara-data.service
|
||||||
|
|
||||||
39
services/check_services.sh
Normal file
39
services/check_services.sh
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# File: /var/www/nebuleair_pro_4g/services/check_services.sh
|
||||||
|
# Purpose: Check status of all NebuleAir services and logs
|
||||||
|
# Install:
|
||||||
|
# sudo chmod +x /var/www/nebuleair_pro_4g/services/check_services.sh
|
||||||
|
# sudo /var/www/nebuleair_pro_4g/services/check_services.sh
|
||||||
|
|
||||||
|
echo "=== NebuleAir Services Status ==="
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Check status of all timers
|
||||||
|
echo "--- TIMER STATUS ---"
|
||||||
|
systemctl list-timers | grep nebuleair
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Check status of all services
|
||||||
|
echo "--- SERVICE STATUS ---"
|
||||||
|
for service in npm envea sara bme280 mppt db-cleanup; do
|
||||||
|
status=$(systemctl is-active nebuleair-$service-data.service)
|
||||||
|
timer_status=$(systemctl is-active nebuleair-$service-data.timer)
|
||||||
|
|
||||||
|
echo "nebuleair-$service-data: Service=$status, Timer=$timer_status"
|
||||||
|
done
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Show recent logs for each service
|
||||||
|
echo "--- RECENT LOGS (last 5 entries per service) ---"
|
||||||
|
for service in npm envea sara bme280 mppt db-cleanup; do
|
||||||
|
echo "[$service service logs]"
|
||||||
|
journalctl -u nebuleair-$service-data.service -n 5 --no-pager
|
||||||
|
echo ""
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "=== End of Report ==="
|
||||||
|
echo ""
|
||||||
|
echo "For detailed logs use:"
|
||||||
|
echo " sudo journalctl -u nebuleair-[service]-data.service -f"
|
||||||
|
echo "To restart a specific service timer:"
|
||||||
|
echo " sudo systemctl restart nebuleair-[service]-data.timer"
|
||||||
228
services/setup_services.sh
Normal file
228
services/setup_services.sh
Normal file
@@ -0,0 +1,228 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# File: /var/www/nebuleair_pro_4g/services/setup_services.sh
|
||||||
|
# Purpose: Set up all systemd services for NebuleAir data collection
|
||||||
|
# to install:
|
||||||
|
# sudo chmod +x /var/www/nebuleair_pro_4g/services/setup_services.sh
|
||||||
|
# sudo /var/www/nebuleair_pro_4g/services/setup_services.sh
|
||||||
|
|
||||||
|
echo "Setting up NebuleAir systemd services and timers..."
|
||||||
|
|
||||||
|
# Create directory for logs if it doesn't exist
|
||||||
|
mkdir -p /var/www/nebuleair_pro_4g/logs
|
||||||
|
|
||||||
|
# Create service and timer files for NPM Data
|
||||||
|
cat > /etc/systemd/system/nebuleair-npm-data.service << 'EOL'
|
||||||
|
[Unit]
|
||||||
|
Description=NebuleAir NPM Data Collection Service
|
||||||
|
After=network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=oneshot
|
||||||
|
ExecStart=/usr/bin/python3 /var/www/nebuleair_pro_4g/NPM/get_data_modbus_v3.py
|
||||||
|
User=root
|
||||||
|
WorkingDirectory=/var/www/nebuleair_pro_4g
|
||||||
|
StandardOutput=append:/var/www/nebuleair_pro_4g/logs/npm_service.log
|
||||||
|
StandardError=append:/var/www/nebuleair_pro_4g/logs/npm_service_errors.log
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
EOL
|
||||||
|
|
||||||
|
cat > /etc/systemd/system/nebuleair-npm-data.timer << 'EOL'
|
||||||
|
[Unit]
|
||||||
|
Description=Run NebuleAir NPM Data Collection every 10 seconds
|
||||||
|
Requires=nebuleair-npm-data.service
|
||||||
|
|
||||||
|
[Timer]
|
||||||
|
OnBootSec=10s
|
||||||
|
OnUnitActiveSec=10s
|
||||||
|
AccuracySec=1s
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=timers.target
|
||||||
|
EOL
|
||||||
|
|
||||||
|
# Create service and timer files for Envea Data
|
||||||
|
cat > /etc/systemd/system/nebuleair-envea-data.service << 'EOL'
|
||||||
|
[Unit]
|
||||||
|
Description=NebuleAir Envea Data Collection Service
|
||||||
|
After=network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=oneshot
|
||||||
|
ExecStart=/usr/bin/python3 /var/www/nebuleair_pro_4g/envea/read_value_v2.py
|
||||||
|
User=root
|
||||||
|
WorkingDirectory=/var/www/nebuleair_pro_4g
|
||||||
|
StandardOutput=append:/var/www/nebuleair_pro_4g/logs/envea_service.log
|
||||||
|
StandardError=append:/var/www/nebuleair_pro_4g/logs/envea_service_errors.log
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
EOL
|
||||||
|
|
||||||
|
cat > /etc/systemd/system/nebuleair-envea-data.timer << 'EOL'
|
||||||
|
[Unit]
|
||||||
|
Description=Run NebuleAir Envea Data Collection every 10 seconds
|
||||||
|
Requires=nebuleair-envea-data.service
|
||||||
|
|
||||||
|
[Timer]
|
||||||
|
OnBootSec=10s
|
||||||
|
OnUnitActiveSec=10s
|
||||||
|
AccuracySec=1s
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=timers.target
|
||||||
|
EOL
|
||||||
|
|
||||||
|
# Create service and timer files for SARA Data (No Lock File Needed)
|
||||||
|
cat > /etc/systemd/system/nebuleair-sara-data.service << 'EOL'
|
||||||
|
[Unit]
|
||||||
|
Description=NebuleAir SARA Data Transmission Service
|
||||||
|
After=network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=oneshot
|
||||||
|
ExecStart=/usr/bin/python3 /var/www/nebuleair_pro_4g/loop/SARA_send_data_v2.py
|
||||||
|
User=root
|
||||||
|
WorkingDirectory=/var/www/nebuleair_pro_4g
|
||||||
|
StandardOutput=append:/var/www/nebuleair_pro_4g/logs/sara_service.log
|
||||||
|
StandardError=append:/var/www/nebuleair_pro_4g/logs/sara_service_errors.log
|
||||||
|
RuntimeMaxSec=200s
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
EOL
|
||||||
|
|
||||||
|
cat > /etc/systemd/system/nebuleair-sara-data.timer << 'EOL'
|
||||||
|
[Unit]
|
||||||
|
Description=Run NebuleAir SARA Data Transmission every 60 seconds
|
||||||
|
Requires=nebuleair-sara-data.service
|
||||||
|
|
||||||
|
[Timer]
|
||||||
|
OnBootSec=60s
|
||||||
|
OnUnitActiveSec=60s
|
||||||
|
AccuracySec=1s
|
||||||
|
# This is the key setting that prevents overlap
|
||||||
|
Persistent=true
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=timers.target
|
||||||
|
EOL
|
||||||
|
|
||||||
|
# Create service and timer files for BME280 Data
|
||||||
|
cat > /etc/systemd/system/nebuleair-bme280-data.service << 'EOL'
|
||||||
|
[Unit]
|
||||||
|
Description=NebuleAir BME280 Data Collection Service
|
||||||
|
After=network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=oneshot
|
||||||
|
ExecStart=/usr/bin/python3 /var/www/nebuleair_pro_4g/BME280/get_data_v2.py
|
||||||
|
User=root
|
||||||
|
WorkingDirectory=/var/www/nebuleair_pro_4g
|
||||||
|
StandardOutput=append:/var/www/nebuleair_pro_4g/logs/bme280_service.log
|
||||||
|
StandardError=append:/var/www/nebuleair_pro_4g/logs/bme280_service_errors.log
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
EOL
|
||||||
|
|
||||||
|
cat > /etc/systemd/system/nebuleair-bme280-data.timer << 'EOL'
|
||||||
|
[Unit]
|
||||||
|
Description=Run NebuleAir BME280 Data Collection every 120 seconds
|
||||||
|
Requires=nebuleair-bme280-data.service
|
||||||
|
|
||||||
|
[Timer]
|
||||||
|
OnBootSec=120s
|
||||||
|
OnUnitActiveSec=120s
|
||||||
|
AccuracySec=1s
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=timers.target
|
||||||
|
EOL
|
||||||
|
|
||||||
|
# Create service and timer files for MPPT Data
|
||||||
|
cat > /etc/systemd/system/nebuleair-mppt-data.service << 'EOL'
|
||||||
|
[Unit]
|
||||||
|
Description=NebuleAir MPPT Data Collection Service
|
||||||
|
After=network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=oneshot
|
||||||
|
ExecStart=/usr/bin/python3 /var/www/nebuleair_pro_4g/MPPT/read.py
|
||||||
|
User=root
|
||||||
|
WorkingDirectory=/var/www/nebuleair_pro_4g
|
||||||
|
StandardOutput=append:/var/www/nebuleair_pro_4g/logs/mppt_service.log
|
||||||
|
StandardError=append:/var/www/nebuleair_pro_4g/logs/mppt_service_errors.log
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
EOL
|
||||||
|
|
||||||
|
cat > /etc/systemd/system/nebuleair-mppt-data.timer << 'EOL'
|
||||||
|
[Unit]
|
||||||
|
Description=Run NebuleAir MPPT Data Collection every 120 seconds
|
||||||
|
Requires=nebuleair-mppt-data.service
|
||||||
|
|
||||||
|
[Timer]
|
||||||
|
OnBootSec=120s
|
||||||
|
OnUnitActiveSec=120s
|
||||||
|
AccuracySec=1s
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=timers.target
|
||||||
|
EOL
|
||||||
|
|
||||||
|
# Create service and timer files for Database Cleanup
|
||||||
|
cat > /etc/systemd/system/nebuleair-db-cleanup-data.service << 'EOL'
|
||||||
|
[Unit]
|
||||||
|
Description=NebuleAir Database Cleanup Service
|
||||||
|
After=network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=oneshot
|
||||||
|
ExecStart=/usr/bin/python3 /var/www/nebuleair_pro_4g/sqlite/flush_old_data.py
|
||||||
|
User=root
|
||||||
|
WorkingDirectory=/var/www/nebuleair_pro_4g
|
||||||
|
StandardOutput=append:/var/www/nebuleair_pro_4g/logs/db_cleanup_service.log
|
||||||
|
StandardError=append:/var/www/nebuleair_pro_4g/logs/db_cleanup_service_errors.log
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
EOL
|
||||||
|
|
||||||
|
cat > /etc/systemd/system/nebuleair-db-cleanup-data.timer << 'EOL'
|
||||||
|
[Unit]
|
||||||
|
Description=Run NebuleAir Database Cleanup daily
|
||||||
|
Requires=nebuleair-db-cleanup-data.service
|
||||||
|
|
||||||
|
[Timer]
|
||||||
|
OnBootSec=1h
|
||||||
|
OnUnitActiveSec=24h
|
||||||
|
AccuracySec=1h
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=timers.target
|
||||||
|
EOL
|
||||||
|
|
||||||
|
# Reload systemd to recognize new services
|
||||||
|
systemctl daemon-reload
|
||||||
|
|
||||||
|
# Enable and start all timers
|
||||||
|
echo "Enabling and starting all services..."
|
||||||
|
for service in npm envea sara bme280 mppt db-cleanup; do
|
||||||
|
systemctl enable nebuleair-$service-data.timer
|
||||||
|
systemctl start nebuleair-$service-data.timer
|
||||||
|
echo "Started nebuleair-$service-data timer"
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "Checking status of all timers..."
|
||||||
|
systemctl list-timers | grep nebuleair
|
||||||
|
|
||||||
|
echo "Setup complete. All NebuleAir services are now running."
|
||||||
|
echo "To check the status of a specific service:"
|
||||||
|
echo " sudo systemctl status nebuleair-npm-data.service"
|
||||||
|
echo "To view logs for a specific service:"
|
||||||
|
echo " sudo journalctl -u nebuleair-npm-data.service"
|
||||||
|
echo "To restart a specific timer:"
|
||||||
|
echo " sudo systemctl restart nebuleair-npm-data.timer"
|
||||||
134
sqlite/create_db.py
Executable file
134
sqlite/create_db.py
Executable file
@@ -0,0 +1,134 @@
|
|||||||
|
'''
|
||||||
|
____ ___ _ _ _
|
||||||
|
/ ___| / _ \| | (_) |_ ___
|
||||||
|
\___ \| | | | | | | __/ _ \
|
||||||
|
___) | |_| | |___| | || __/
|
||||||
|
|____/ \__\_\_____|_|\__\___|
|
||||||
|
|
||||||
|
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 if not existent) the database
|
||||||
|
conn = sqlite3.connect("/var/www/nebuleair_pro_4g/sqlite/sensors.db")
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
#create a config table
|
||||||
|
cursor.execute("""
|
||||||
|
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,
|
||||||
|
PM1 REAL,
|
||||||
|
PM25 REAL,
|
||||||
|
PM10 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,
|
||||||
|
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,
|
||||||
|
PM_ch4 INTEGER,
|
||||||
|
PM_ch5 INTEGER
|
||||||
|
)
|
||||||
|
""")
|
||||||
|
|
||||||
|
# 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()
|
||||||
|
|
||||||
|
print("Database and table created successfully!")
|
||||||
71
sqlite/flush_old_data.py
Executable file
71
sqlite/flush_old_data.py
Executable file
@@ -0,0 +1,71 @@
|
|||||||
|
'''
|
||||||
|
____ ___ _ _ _
|
||||||
|
/ ___| / _ \| | (_) |_ ___
|
||||||
|
\___ \| | | | | | | __/ _ \
|
||||||
|
___) | |_| | |___| | || __/
|
||||||
|
|____/ \__\_\_____|_|\__\___|
|
||||||
|
|
||||||
|
Script to flush (delete) data from a sqlite database
|
||||||
|
|
||||||
|
/usr/bin/python3 /var/www/nebuleair_pro_4g/sqlite/flush_old_data.py
|
||||||
|
|
||||||
|
Available table are
|
||||||
|
|
||||||
|
data_NPM
|
||||||
|
data_NPM_5channels
|
||||||
|
data_BME280
|
||||||
|
data_envea
|
||||||
|
timestamp_table
|
||||||
|
|
||||||
|
'''
|
||||||
|
|
||||||
|
import sqlite3
|
||||||
|
import datetime
|
||||||
|
|
||||||
|
|
||||||
|
# Connect to the SQLite database
|
||||||
|
conn = sqlite3.connect("/var/www/nebuleair_pro_4g/sqlite/sensors.db")
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
#GET RTC TIME from SQlite
|
||||||
|
cursor.execute("SELECT * FROM timestamp_table LIMIT 1")
|
||||||
|
row = cursor.fetchone() # Get the first (and only) row
|
||||||
|
|
||||||
|
if row:
|
||||||
|
rtc_time_str = row[1] # Assuming timestamp is stored as TEXT (YYYY-MM-DD HH:MM:SS)
|
||||||
|
print(f"[INFO] Last recorded timestamp: {rtc_time_str}")
|
||||||
|
|
||||||
|
# Convert last_updated to a datetime object
|
||||||
|
last_updated = datetime.datetime.strptime(rtc_time_str, "%Y-%m-%d %H:%M:%S")
|
||||||
|
|
||||||
|
# Calculate the cutoff date (3 months before last_updated)
|
||||||
|
cutoff_date = last_updated - datetime.timedelta(days=60)
|
||||||
|
cutoff_date_str = cutoff_date.strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
|
||||||
|
print(f"[INFO] Deleting records older than: {cutoff_date_str}")
|
||||||
|
|
||||||
|
# List of tables to delete old data from
|
||||||
|
tables_to_clean = ["data_NPM", "data_NPM_5channels", "data_BME280", "data_envea","data_WIND", "data_MPPT"]
|
||||||
|
|
||||||
|
# Loop through each table and delete old data
|
||||||
|
for table in tables_to_clean:
|
||||||
|
delete_query = f"DELETE FROM {table} WHERE timestamp < ?"
|
||||||
|
cursor.execute(delete_query, (cutoff_date_str,))
|
||||||
|
print(f"[INFO] Deleted old records from {table}")
|
||||||
|
|
||||||
|
# **Commit changes before running VACUUM**
|
||||||
|
conn.commit()
|
||||||
|
print("[INFO] Changes committed successfully!")
|
||||||
|
|
||||||
|
# Now it's safe to run VACUUM
|
||||||
|
print("[INFO] Running VACUUM to optimize database space...")
|
||||||
|
cursor.execute("VACUUM")
|
||||||
|
|
||||||
|
print("[SUCCESS] Old data flushed successfully!")
|
||||||
|
|
||||||
|
else:
|
||||||
|
print("[ERROR] No timestamp found in timestamp_table.")
|
||||||
|
|
||||||
|
|
||||||
|
# Close the database connection
|
||||||
|
conn.close()
|
||||||
53
sqlite/read.py
Executable file
53
sqlite/read.py
Executable file
@@ -0,0 +1,53 @@
|
|||||||
|
'''
|
||||||
|
____ ___ _ _ _
|
||||||
|
/ ___| / _ \| | (_) |_ ___
|
||||||
|
\___ \| | | | | | | __/ _ \
|
||||||
|
___) | |_| | |___| | || __/
|
||||||
|
|____/ \__\_\_____|_|\__\___|
|
||||||
|
|
||||||
|
Script to read data from a sqlite database
|
||||||
|
/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_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:
|
||||||
|
print(row)
|
||||||
|
|
||||||
|
# Close the database connection
|
||||||
|
conn.close()
|
||||||
43
sqlite/read_config.py
Normal file
43
sqlite/read_config.py
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
'''
|
||||||
|
____ ___ _ _ _
|
||||||
|
/ ___| / _ \| | (_) |_ ___
|
||||||
|
\___ \| | | | | | | __/ _ \
|
||||||
|
___) | |_| | |___| | || __/
|
||||||
|
|____/ \__\_\_____|_|\__\___|
|
||||||
|
|
||||||
|
Script to read data from a sqlite database
|
||||||
|
/usr/bin/python3 /var/www/nebuleair_pro_4g/sqlite/read_config.py config_table
|
||||||
|
|
||||||
|
Available table are
|
||||||
|
config_table
|
||||||
|
config_scripts_table
|
||||||
|
envea_sondes_table
|
||||||
|
|
||||||
|
'''
|
||||||
|
|
||||||
|
import sqlite3
|
||||||
|
import sys
|
||||||
|
parameter = sys.argv[1:] # Exclude the script name
|
||||||
|
#print("Parameters received:")
|
||||||
|
table_name=parameter[0]
|
||||||
|
|
||||||
|
|
||||||
|
# Connect to the SQLite database
|
||||||
|
conn = sqlite3.connect("/var/www/nebuleair_pro_4g/sqlite/sensors.db")
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
# Retrieve the data
|
||||||
|
query = f"SELECT * FROM {table_name}"
|
||||||
|
cursor.execute(query)
|
||||||
|
|
||||||
|
|
||||||
|
rows = cursor.fetchall()
|
||||||
|
rows.reverse() # Reverse the order in Python (to get ascending order)
|
||||||
|
|
||||||
|
|
||||||
|
# Display the results
|
||||||
|
for row in rows:
|
||||||
|
print(row)
|
||||||
|
|
||||||
|
# Close the database connection
|
||||||
|
conn.close()
|
||||||
59
sqlite/read_select_date.py
Executable file
59
sqlite/read_select_date.py
Executable file
@@ -0,0 +1,59 @@
|
|||||||
|
'''
|
||||||
|
____ ___ _ _ _
|
||||||
|
/ ___| / _ \| | (_) |_ ___
|
||||||
|
\___ \| | | | | | | __/ _ \
|
||||||
|
___) | |_| | |___| | || __/
|
||||||
|
|____/ \__\_\_____|_|\__\___|
|
||||||
|
|
||||||
|
Script to read data from a sqlite database using start date and end date
|
||||||
|
|
||||||
|
/usr/bin/python3 /var/www/nebuleair_pro_4g/sqlite/read_select_date.py data_NPM 2025-02-09 2025-02-11
|
||||||
|
|
||||||
|
Available table are
|
||||||
|
data_NPM
|
||||||
|
data_NPM_5channels
|
||||||
|
data_BME280
|
||||||
|
data_envea
|
||||||
|
timestamp_table
|
||||||
|
|
||||||
|
'''
|
||||||
|
|
||||||
|
import sqlite3
|
||||||
|
import sys
|
||||||
|
|
||||||
|
parameter = sys.argv[1:] # Exclude the script name
|
||||||
|
#print("Parameters received:")
|
||||||
|
table_name=parameter[0]
|
||||||
|
start_date=parameter[1]
|
||||||
|
end_date=parameter[2]
|
||||||
|
|
||||||
|
# Convert to full timestamp range
|
||||||
|
start_timestamp = f"{start_date} 00:00:00"
|
||||||
|
end_timestamp = f"{end_date} 23:59:59"
|
||||||
|
|
||||||
|
# Connect to the SQLite database
|
||||||
|
conn = sqlite3.connect("/var/www/nebuleair_pro_4g/sqlite/sensors.db")
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
# Retrieve the last 10 sensor readings
|
||||||
|
#cursor.execute("SELECT * FROM data_NPM ORDER BY timestamp DESC LIMIT 10")
|
||||||
|
#cursor.execute("SELECT * FROM data_BME280 ORDER BY timestamp DESC LIMIT 10")
|
||||||
|
#cursor.execute("SELECT * FROM timestamp_table")
|
||||||
|
if table_name == "timestamp_table":
|
||||||
|
cursor.execute("SELECT * FROM timestamp_table")
|
||||||
|
|
||||||
|
else:
|
||||||
|
query = f"SELECT * FROM {table_name} WHERE timestamp BETWEEN ? AND ? ORDER BY timestamp ASC"
|
||||||
|
cursor.execute(query, (start_timestamp, end_timestamp))
|
||||||
|
|
||||||
|
|
||||||
|
rows = cursor.fetchall()
|
||||||
|
rows.reverse() # Reverse the order in Python (to get ascending order)
|
||||||
|
|
||||||
|
|
||||||
|
# Display the results
|
||||||
|
for row in rows:
|
||||||
|
print(row)
|
||||||
|
|
||||||
|
# Close the database connection
|
||||||
|
conn.close()
|
||||||
77
sqlite/set_config.py
Normal file
77
sqlite/set_config.py
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
'''
|
||||||
|
____ ___ _ _ _
|
||||||
|
/ ___| / _ \| | (_) |_ ___
|
||||||
|
\___ \| | | | | | | __/ _ \
|
||||||
|
___) | |_| | |___| | || __/
|
||||||
|
|____/ \__\_\_____|_|\__\___|
|
||||||
|
|
||||||
|
Script to set the config
|
||||||
|
/usr/bin/python3 /var/www/nebuleair_pro_4g/sqlite/set_config.py
|
||||||
|
|
||||||
|
in case of readonly error:
|
||||||
|
sudo chmod 777 /var/www/nebuleair_pro_4g/sqlite/sensors.db
|
||||||
|
'''
|
||||||
|
|
||||||
|
import sqlite3
|
||||||
|
|
||||||
|
# Connect to (or create if not existent) the database
|
||||||
|
conn = sqlite3.connect("/var/www/nebuleair_pro_4g/sqlite/sensors.db")
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
print(f"Connected to database")
|
||||||
|
|
||||||
|
# Note: Using INSERT OR IGNORE to add only new configurations without overwriting existing ones
|
||||||
|
print("Adding new configurations (existing ones will be preserved)")
|
||||||
|
|
||||||
|
|
||||||
|
# Insert general configurations
|
||||||
|
config_entries = [
|
||||||
|
("modem_config_mode", "0", "bool"),
|
||||||
|
("deviceID", "XXXX", "str"),
|
||||||
|
("latitude_raw", "0", "int"),
|
||||||
|
("longitude_raw", "0", "int"),
|
||||||
|
("latitude_precision", "0", "int"),
|
||||||
|
("longitude_precision", "0", "int"),
|
||||||
|
("deviceName", "NebuleAir-proXXX", "str"),
|
||||||
|
("SaraR4_baudrate", "115200", "int"),
|
||||||
|
("NPM_solo_port", "/dev/ttyAMA5", "str"),
|
||||||
|
("sshTunnel_port", "59228", "int"),
|
||||||
|
("SARA_R4_general_status", "connected", "str"),
|
||||||
|
("SARA_R4_SIM_status", "connected", "str"),
|
||||||
|
("SARA_R4_network_status", "connected", "str"),
|
||||||
|
("SARA_R4_neworkID", "20810", "int"),
|
||||||
|
("WIFI_status", "connected", "str"),
|
||||||
|
("send_uSpot", "0", "bool"),
|
||||||
|
("npm_5channel", "0", "bool"),
|
||||||
|
("envea", "0", "bool"),
|
||||||
|
("windMeter", "0", "bool"),
|
||||||
|
("BME280", "0", "bool"),
|
||||||
|
("MPPT", "0", "bool"),
|
||||||
|
("modem_version", "XXX", "str")
|
||||||
|
]
|
||||||
|
|
||||||
|
for key, value, value_type in config_entries:
|
||||||
|
cursor.execute(
|
||||||
|
"INSERT OR IGNORE INTO config_table (key, value, type) VALUES (?, ?, ?)",
|
||||||
|
(key, value, value_type)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Insert envea sondes
|
||||||
|
envea_sondes = [
|
||||||
|
(False, "ttyAMA4", "h2s", 4),
|
||||||
|
(False, "ttyAMA3", "no2", 1),
|
||||||
|
(False, "ttyAMA2", "o3", 1)
|
||||||
|
]
|
||||||
|
|
||||||
|
for connected, port, name, coefficient in envea_sondes:
|
||||||
|
cursor.execute(
|
||||||
|
"INSERT OR IGNORE INTO envea_sondes_table (connected, port, name, coefficient) VALUES (?, ?, ?, ?)",
|
||||||
|
(1 if connected else 0, port, name, coefficient)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# Commit and close the connection
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
print("Database updated successfully!")
|
||||||
33
sqlite/write.py
Executable file
33
sqlite/write.py
Executable file
@@ -0,0 +1,33 @@
|
|||||||
|
'''
|
||||||
|
____ ___ _ _ _
|
||||||
|
/ ___| / _ \| | (_) |_ ___
|
||||||
|
\___ \| | | | | | | __/ _ \
|
||||||
|
___) | |_| | |___| | || __/
|
||||||
|
|____/ \__\_\_____|_|\__\___|
|
||||||
|
|
||||||
|
Script to write data to a sqlite database
|
||||||
|
/usr/bin/python3 /var/www/nebuleair_pro_4g/sqlite/write.py
|
||||||
|
|
||||||
|
'''
|
||||||
|
|
||||||
|
import sqlite3
|
||||||
|
|
||||||
|
# Connect to the SQLite database
|
||||||
|
conn = sqlite3.connect("/var/www/nebuleair_pro_4g/sqlite/sensors.db")
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
# Insert a sample temperature reading
|
||||||
|
timestamp = "2025-10-11"
|
||||||
|
PM1 = 25.3
|
||||||
|
PM25 = 18.3
|
||||||
|
PM10 = 9.3
|
||||||
|
|
||||||
|
cursor.execute('''
|
||||||
|
INSERT INTO data (timestamp, PM1, PM25, PM10) VALUES (?,?,?,?,?)'''
|
||||||
|
, (timestamp,PM1,PM25,PM10))
|
||||||
|
|
||||||
|
# Commit and close the connection
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
print("Sensor data saved successfully!")
|
||||||
118
update_firmware.sh
Normal file
118
update_firmware.sh
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# NebuleAir Pro 4G - Comprehensive Update Script
|
||||||
|
# This script performs a complete system update including git pull,
|
||||||
|
# config initialization, and service management
|
||||||
|
|
||||||
|
echo "======================================"
|
||||||
|
echo "NebuleAir Pro 4G - Firmware Update"
|
||||||
|
echo "======================================"
|
||||||
|
echo "Started at: $(date)"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Set working directory
|
||||||
|
cd /var/www/nebuleair_pro_4g
|
||||||
|
|
||||||
|
# Function to print status messages
|
||||||
|
print_status() {
|
||||||
|
echo "[$(date '+%H:%M:%S')] $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Function to check command success
|
||||||
|
check_status() {
|
||||||
|
if [ $? -eq 0 ]; then
|
||||||
|
print_status "✓ $1 completed successfully"
|
||||||
|
else
|
||||||
|
print_status "✗ $1 failed"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Step 1: Git operations
|
||||||
|
print_status "Step 1: Updating firmware from repository..."
|
||||||
|
git fetch origin
|
||||||
|
check_status "Git fetch"
|
||||||
|
|
||||||
|
# Show current branch and any changes
|
||||||
|
print_status "Current branch: $(git branch --show-current)"
|
||||||
|
if [ -n "$(git status --porcelain)" ]; then
|
||||||
|
print_status "Warning: Local changes detected:"
|
||||||
|
git status --short
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Pull latest changes
|
||||||
|
git pull origin $(git branch --show-current)
|
||||||
|
check_status "Git pull"
|
||||||
|
|
||||||
|
# Step 2: Update database configuration
|
||||||
|
print_status ""
|
||||||
|
print_status "Step 2: Updating database configuration..."
|
||||||
|
/usr/bin/python3 /var/www/nebuleair_pro_4g/sqlite/set_config.py
|
||||||
|
check_status "Database configuration update"
|
||||||
|
|
||||||
|
# Step 3: Check and fix file permissions
|
||||||
|
print_status ""
|
||||||
|
print_status "Step 3: Checking file permissions..."
|
||||||
|
sudo chmod +x /var/www/nebuleair_pro_4g/update_firmware.sh
|
||||||
|
sudo chmod 755 /var/www/nebuleair_pro_4g/sqlite/*.py
|
||||||
|
sudo chmod 755 /var/www/nebuleair_pro_4g/NPM/*.py
|
||||||
|
sudo chmod 755 /var/www/nebuleair_pro_4g/BME280/*.py
|
||||||
|
sudo chmod 755 /var/www/nebuleair_pro_4g/SARA/*.py
|
||||||
|
sudo chmod 755 /var/www/nebuleair_pro_4g/envea/*.py
|
||||||
|
check_status "File permissions update"
|
||||||
|
|
||||||
|
# Step 4: Restart critical services if they exist
|
||||||
|
print_status ""
|
||||||
|
print_status "Step 4: Managing system services..."
|
||||||
|
|
||||||
|
# List of services to check and restart
|
||||||
|
services=(
|
||||||
|
"nebuleair-npm-data.timer"
|
||||||
|
"nebuleair-envea-data.timer"
|
||||||
|
"nebuleair-sara-data.timer"
|
||||||
|
"nebuleair-bme280-data.timer"
|
||||||
|
"nebuleair-mppt-data.timer"
|
||||||
|
)
|
||||||
|
|
||||||
|
for service in "${services[@]}"; do
|
||||||
|
if systemctl list-unit-files | grep -q "$service"; then
|
||||||
|
print_status "Restarting service: $service"
|
||||||
|
sudo systemctl restart "$service"
|
||||||
|
if systemctl is-active --quiet "$service"; then
|
||||||
|
print_status "✓ $service is running"
|
||||||
|
else
|
||||||
|
print_status "⚠ $service may not be active"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
print_status "ℹ Service $service not found (may not be installed)"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# Step 5: System health check
|
||||||
|
print_status ""
|
||||||
|
print_status "Step 5: System health check..."
|
||||||
|
|
||||||
|
# Check disk space
|
||||||
|
disk_usage=$(df / | awk 'NR==2 {print $5}' | sed 's/%//')
|
||||||
|
if [ "$disk_usage" -gt 90 ]; then
|
||||||
|
print_status "⚠ Warning: Disk usage is high ($disk_usage%)"
|
||||||
|
else
|
||||||
|
print_status "✓ Disk usage is acceptable ($disk_usage%)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check if database is accessible
|
||||||
|
if [ -f "/var/www/nebuleair_pro_4g/sqlite/sensors.db" ]; then
|
||||||
|
print_status "✓ Database file exists"
|
||||||
|
else
|
||||||
|
print_status "⚠ Warning: Database file not found"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Step 6: Final cleanup
|
||||||
|
print_status ""
|
||||||
|
print_status "Step 6: Cleaning up..."
|
||||||
|
sudo find /var/www/nebuleair_pro_4g/logs -name "*.log" -size +10M -exec truncate -s 0 {} \;
|
||||||
|
check_status "Log cleanup"
|
||||||
|
|
||||||
|
print_status "Update completed successfully!"
|
||||||
|
|
||||||
|
exit 0
|
||||||
27
windMeter/ads115.py
Normal file
27
windMeter/ads115.py
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
'''
|
||||||
|
Script to test the abs115 an analog-to-digital converter
|
||||||
|
sudo /usr/bin/python3 /var/www/nebuleair_pro_4g/windMeter/ads115.py
|
||||||
|
|
||||||
|
'''
|
||||||
|
import time
|
||||||
|
import board
|
||||||
|
import busio
|
||||||
|
import adafruit_ads1x15.ads1115 as ADS
|
||||||
|
from adafruit_ads1x15.analog_in import AnalogIn
|
||||||
|
|
||||||
|
i2c = busio.I2C(board.SCL, board.SDA)
|
||||||
|
ads = ADS.ADS1115(i2c)
|
||||||
|
channel = AnalogIn(ads, ADS.P0)
|
||||||
|
|
||||||
|
print("Testing ADS1115 readings...")
|
||||||
|
readings = []
|
||||||
|
|
||||||
|
for i in range(5):
|
||||||
|
voltage = channel.voltage
|
||||||
|
readings.append(voltage)
|
||||||
|
print(f"Voltage: {voltage:.6f}V")
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
# Calculate and display the mean
|
||||||
|
mean_voltage = sum(readings) / len(readings)
|
||||||
|
print(f"\nMean voltage: {mean_voltage:.6f}V")
|
||||||
140
windMeter/read.py
Normal file
140
windMeter/read.py
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
'''
|
||||||
|
__ _____ _ _ ____
|
||||||
|
\ \ / /_ _| \ | | _ \
|
||||||
|
\ \ /\ / / | || \| | | | |
|
||||||
|
\ V V / | || |\ | |_| |
|
||||||
|
\_/\_/ |___|_| \_|____/
|
||||||
|
|
||||||
|
Script to read wind speed from a Davis Anémomètre-girouette Vantage Pro (6410)
|
||||||
|
https://www.shapemaker.io/blog/wind-speed-measurements-with-anemometer-and-a-raspberry-pi
|
||||||
|
|
||||||
|
Connexion:
|
||||||
|
black (wind speed ) -> gpio21
|
||||||
|
green (wind direction) -> ADS1115 (module I2C)
|
||||||
|
Yellow -> 5v
|
||||||
|
RED -> GND
|
||||||
|
|
||||||
|
Attention: The Raspberry Pi doesn't have analog inputs, so we need an analog-to-digital converter (ADC) to read the wind direction.
|
||||||
|
|
||||||
|
sudo /usr/bin/python3 /var/www/nebuleair_pro_4g/windMeter/read.py
|
||||||
|
|
||||||
|
this need to run as a service
|
||||||
|
|
||||||
|
--> sudo nano /etc/systemd/system/windMeter.service
|
||||||
|
|
||||||
|
⬇️
|
||||||
|
[Unit]
|
||||||
|
Description=Master manager for the Python wind meter scripts
|
||||||
|
After=network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
ExecStart=/usr/bin/python3 /var/www/nebuleair_pro_4g/windMeter/read.py
|
||||||
|
Restart=always
|
||||||
|
User=root
|
||||||
|
WorkingDirectory=/var/www/nebuleair_pro_4g
|
||||||
|
StandardOutput=append:/var/www/nebuleair_pro_4g/logs/wind.log
|
||||||
|
StandardError=append:/var/www/nebuleair_pro_4g/logs/wind_errors.log
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
⬆️
|
||||||
|
|
||||||
|
Reload systemd (first time after creating the service):
|
||||||
|
sudo systemctl daemon-reload
|
||||||
|
|
||||||
|
Enable (once), start (once and after stopping) and restart (after modification)systemd:
|
||||||
|
sudo systemctl enable windMeter.service
|
||||||
|
sudo systemctl start windMeter.service
|
||||||
|
sudo systemctl restart windMeter.service
|
||||||
|
|
||||||
|
Check the service status:
|
||||||
|
sudo systemctl status windMeter.service
|
||||||
|
|
||||||
|
'''
|
||||||
|
#!/usr/bin/python3
|
||||||
|
import time
|
||||||
|
import sqlite3
|
||||||
|
import board
|
||||||
|
import busio
|
||||||
|
import numpy as np
|
||||||
|
import threading
|
||||||
|
import adafruit_ads1x15.ads1115 as ADS
|
||||||
|
from adafruit_ads1x15.analog_in import AnalogIn
|
||||||
|
from gpiozero import Button
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
# Constants
|
||||||
|
DB_PATH = "/var/www/nebuleair_pro_4g/sqlite/sensors.db"
|
||||||
|
|
||||||
|
# Initialize I2C & ADS1115
|
||||||
|
i2c = busio.I2C(board.SCL, board.SDA)
|
||||||
|
ads = ADS.ADS1115(i2c)
|
||||||
|
channel = AnalogIn(ads, ADS.P0) # Connect to A0 on the ADS1115
|
||||||
|
|
||||||
|
# Wind speed sensor setup
|
||||||
|
wind_speed_sensor = Button(21)
|
||||||
|
wind_count = 0
|
||||||
|
wind_lock = threading.Lock()
|
||||||
|
|
||||||
|
def spin():
|
||||||
|
global wind_count
|
||||||
|
with wind_lock:
|
||||||
|
wind_count += 1
|
||||||
|
|
||||||
|
def reset_wind():
|
||||||
|
global wind_count
|
||||||
|
with wind_lock:
|
||||||
|
wind_count = 0
|
||||||
|
|
||||||
|
wind_speed_sensor.when_activated = spin # More reliable
|
||||||
|
|
||||||
|
def calc_speed(spins, interval):
|
||||||
|
return spins * (2.25 / interval) * 1.60934 # Convert MPH to km/h
|
||||||
|
|
||||||
|
def get_wind_direction():
|
||||||
|
voltage = channel.voltage
|
||||||
|
return voltage
|
||||||
|
|
||||||
|
def save_to_database(wind_speed, wind_direction, spin_count):
|
||||||
|
"""Save wind data to SQLite database."""
|
||||||
|
try:
|
||||||
|
conn = sqlite3.connect(DB_PATH)
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
cursor.execute("SELECT * FROM timestamp_table LIMIT 1")
|
||||||
|
row = cursor.fetchone()
|
||||||
|
rtc_time_str = row[1] if row else datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
|
||||||
|
cursor.execute('''
|
||||||
|
INSERT INTO data_wind (timestamp, wind_speed, wind_direction)
|
||||||
|
VALUES (?, ?, ?)
|
||||||
|
''', (rtc_time_str, round(wind_speed, 2), round(wind_direction, 2)))
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
print(f"Saved: {rtc_time_str}, {wind_speed:.2f} km/h, {wind_direction:.2f}V, Spins: {spin_count}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Database error: {e}")
|
||||||
|
|
||||||
|
def main():
|
||||||
|
print("Wind monitoring started...")
|
||||||
|
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
reset_wind()
|
||||||
|
print("Measuring for 60 seconds...")
|
||||||
|
time.sleep(60)
|
||||||
|
|
||||||
|
wind_speed_kmh = calc_speed(wind_count, 60)
|
||||||
|
wind_direction = get_wind_direction()
|
||||||
|
|
||||||
|
save_to_database(wind_speed_kmh, wind_direction, wind_count)
|
||||||
|
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print("\nMonitoring stopped.")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error: {e}")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
84
windMeter/read_wind_direction.py
Normal file
84
windMeter/read_wind_direction.py
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
'''
|
||||||
|
__ _____ _ _ ____
|
||||||
|
\ \ / /_ _| \ | | _ \
|
||||||
|
\ \ /\ / / | || \| | | | |
|
||||||
|
\ V V / | || |\ | |_| |
|
||||||
|
\_/\_/ |___|_| \_|____/
|
||||||
|
|
||||||
|
|
||||||
|
Script to read wind speed from a Davis Anémomètre-girouette Vantage Pro (6410)
|
||||||
|
https://www.shapemaker.io/blog/wind-speed-measurements-with-anemometer-and-a-raspberry-pi
|
||||||
|
|
||||||
|
Connexion:
|
||||||
|
black (wind speed ) -> gpio21
|
||||||
|
green (wind direction) -> ADS1115 (module I2C)
|
||||||
|
Yellow -> 5v
|
||||||
|
RED -> GND
|
||||||
|
|
||||||
|
Attention: The Raspberry Pi doesn't have analog inputs, so we need an analog-to-digital converter (ADC) to read the wind direction.
|
||||||
|
|
||||||
|
sudo /usr/bin/python3 /var/www/nebuleair_pro_4g/windMeter/read_wind_direction.py
|
||||||
|
|
||||||
|
'''
|
||||||
|
import time
|
||||||
|
import board
|
||||||
|
import busio
|
||||||
|
import adafruit_ads1x15.ads1115 as ADS
|
||||||
|
from adafruit_ads1x15.analog_in import AnalogIn
|
||||||
|
|
||||||
|
# Create the I2C bus and ADC object
|
||||||
|
i2c = busio.I2C(board.SCL, board.SDA)
|
||||||
|
ads = ADS.ADS1115(i2c)
|
||||||
|
|
||||||
|
# Connect to the channel with your Davis wind vane
|
||||||
|
wind_dir_sensor = AnalogIn(ads, ADS.P0)
|
||||||
|
|
||||||
|
# Check the current voltage range
|
||||||
|
min_voltage = 9999
|
||||||
|
max_voltage = -9999
|
||||||
|
|
||||||
|
def get_wind_direction():
|
||||||
|
"""Get wind direction angle from Davis Vantage Pro2 wind vane"""
|
||||||
|
global min_voltage, max_voltage
|
||||||
|
|
||||||
|
# Read voltage from ADS1115
|
||||||
|
voltage = wind_dir_sensor.voltage
|
||||||
|
|
||||||
|
# Update min/max for calibration
|
||||||
|
if voltage < min_voltage:
|
||||||
|
min_voltage = voltage
|
||||||
|
if voltage > max_voltage:
|
||||||
|
max_voltage = voltage
|
||||||
|
|
||||||
|
# We'll use a safer mapping approach
|
||||||
|
# Assuming the Davis sensor is linear from 0° to 360°
|
||||||
|
estimated_max = 3.859 # Initial estimate, will refine
|
||||||
|
|
||||||
|
# Calculate angle with bounds checking
|
||||||
|
angle = (voltage / estimated_max) * 360.0
|
||||||
|
|
||||||
|
# Ensure angle is in 0-360 range
|
||||||
|
angle = angle % 360
|
||||||
|
|
||||||
|
return voltage, angle
|
||||||
|
|
||||||
|
# Main loop
|
||||||
|
try:
|
||||||
|
print("Reading wind direction. Press Ctrl+C to exit.")
|
||||||
|
print("Voltage, Angle, Min Voltage, Max Voltage")
|
||||||
|
while True:
|
||||||
|
voltage, angle = get_wind_direction()
|
||||||
|
print(f"{voltage:.3f}V, {angle:.1f}°, {min_voltage:.3f}V, {max_voltage:.3f}V")
|
||||||
|
time.sleep(1)
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print("\nProgram stopped")
|
||||||
|
print(f"Observed voltage range: {min_voltage:.3f}V to {max_voltage:.3f}V")
|
||||||
|
|
||||||
|
# Suggest calibration if we have enough data
|
||||||
|
if max_voltage > min_voltage:
|
||||||
|
print("\nSuggested calibration for your setup:")
|
||||||
|
print(f"max_voltage = {max_voltage:.3f}")
|
||||||
|
print(f"def get_wind_direction():")
|
||||||
|
print(f" voltage = wind_dir_sensor.voltage")
|
||||||
|
print(f" angle = (voltage / {max_voltage:.3f}) * 360.0")
|
||||||
|
print(f" return angle % 360")
|
||||||
67
windMeter/read_wind_speed.py
Normal file
67
windMeter/read_wind_speed.py
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
'''
|
||||||
|
__ _____ _ _ ____
|
||||||
|
\ \ / /_ _| \ | | _ \
|
||||||
|
\ \ /\ / / | || \| | | | |
|
||||||
|
\ V V / | || |\ | |_| |
|
||||||
|
\_/\_/ |___|_| \_|____/
|
||||||
|
|
||||||
|
|
||||||
|
Script to read wind speed from a Davis Anémomètre-girouette Vantage Pro (6410)
|
||||||
|
https://www.shapemaker.io/blog/wind-speed-measurements-with-anemometer-and-a-raspberry-pi
|
||||||
|
|
||||||
|
Connexion:
|
||||||
|
black (wind speed ) -> gpio21
|
||||||
|
green (wind direction) -> ADS1115 (module I2C)
|
||||||
|
Yellow -> 5v
|
||||||
|
RED -> GND
|
||||||
|
|
||||||
|
Attention: The Raspberry Pi doesn't have analog inputs, so we need an analog-to-digital converter (ADC) to read the wind direction.
|
||||||
|
|
||||||
|
sudo /usr/bin/python3 /var/www/nebuleair_pro_4g/windMeter/read_wind_speed.py
|
||||||
|
|
||||||
|
'''
|
||||||
|
|
||||||
|
|
||||||
|
import time
|
||||||
|
from gpiozero import Button
|
||||||
|
from signal import pause
|
||||||
|
|
||||||
|
# Setup wind speed sensor on GPIO pin 21 (instead of 5)
|
||||||
|
wind_speed_sensor = Button(21)
|
||||||
|
wind_count = 0
|
||||||
|
|
||||||
|
def spin():
|
||||||
|
global wind_count
|
||||||
|
wind_count = wind_count + 1
|
||||||
|
|
||||||
|
def calc_speed(spins, interval):
|
||||||
|
# Davis anemometer formula: V = P*(2.25/T) in MPH
|
||||||
|
# P = pulses per sample period, T = sample period in seconds
|
||||||
|
wind_speed_mph = spins * (2.25 / interval)
|
||||||
|
return wind_speed_mph
|
||||||
|
|
||||||
|
def reset_wind():
|
||||||
|
global wind_count
|
||||||
|
wind_count = 0
|
||||||
|
|
||||||
|
# Register the event handler for the sensor
|
||||||
|
wind_speed_sensor.when_pressed = spin
|
||||||
|
|
||||||
|
try:
|
||||||
|
print("Wind speed measurement started. Press Ctrl+C to exit.")
|
||||||
|
|
||||||
|
while True:
|
||||||
|
# Reset the counter
|
||||||
|
reset_wind()
|
||||||
|
|
||||||
|
# Wait for 3 seconds and count rotations
|
||||||
|
print("Measuring for 3 seconds...")
|
||||||
|
time.sleep(3)
|
||||||
|
|
||||||
|
# Calculate and display wind speed
|
||||||
|
wind_speed = calc_speed(wind_count, 3)
|
||||||
|
print(f"Wind count: {wind_count} spins")
|
||||||
|
print(f"Wind speed: {wind_speed:.2f} mph ({wind_speed * 1.60934:.2f} km/h)")
|
||||||
|
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print("\nMeasurement stopped by user")
|
||||||
Reference in New Issue
Block a user