first commit

This commit is contained in:
PaulVua
2025-01-09 14:09:21 +01:00
parent 531f0ef740
commit 3081e43a1a
96 changed files with 65961 additions and 1 deletions

9
.gitignore vendored Executable file
View File

@@ -0,0 +1,9 @@
logs/app.log
logs/loop.log
deviceID.txt
loop/loop.log
loop/data.json
SARA/baudrate.txt
config.json
.ssh/
sound_meter/moving_avg_minute.txt

33
BME280/read.py Executable file
View File

@@ -0,0 +1,33 @@
# Script to read data from BME280
# Sensor connected to i2c on address 77 (use sudo i2cdetect -y 1 to get the address )
# sudo python3 /var/www/nebuleair_pro_4g/BME280/read.py
import board
import busio
import json
from adafruit_bme280 import basic as adafruit_bme280
# 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")
sensor_data = {
"temp": round(bme280.temperature, 2), # Temperature in °C
"hum": round(bme280.humidity, 2), # Humidity in %
"press": round(bme280.pressure, 2), # Pressure in hPa
}
# Convert to JSON and print
print(json.dumps(sensor_data, indent=4))

225
README.md
View File

@@ -1 +1,224 @@
Test # nebuleair_pro_4g
Based on the Rpi4 or CM4.
# Installation
Installation can be made with Ansible or the classic way.
## Ansible (WORK IN PROGRESS)
Installation with Ansible will use a playbook `install_software.yml`.
## General
See `installation.sh`
```
sudo apt update
sudo apt install git gh apache2 php python3 python3-pip jq autossh i2c-tools python3-smbus -y
sudo pip3 install pyserial requests RPi.GPIO adafruit-circuitpython-bme280 --break-system-packages
sudo mkdir -p /var/www/.ssh
sudo ssh-keygen -t rsa -b 4096 -f /var/www/.ssh/id_rsa -N ""
sudo ssh-copy-id -i /var/www/.ssh/id_rsa.pub -p 50221 airlab_server1@aircarto.fr
sudo gh auth login
git config --global user.email "paulvuarambon@gmail.com"
git config --global user.name "PaulVua"
sudo gh repo clone aircarto/nebuleair_pro_4g /var/www/nebuleair_pro_4g
sudo mkdir /var/www/nebuleair_pro_4g/logs
sudo touch /var/www/nebuleair_pro_4g/logs/app.log /var/www/nebuleair_pro_4g/logs/loop.log
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
sudo crontab /var/www/nebuleair_pro_4g/cron_jobs
```
## Apache
Configuration of Apache to redirect to the html homepage project
```
sudo sed -i 's|DocumentRoot /var/www/html|DocumentRoot /var/www/nebuleair_pro_4g/html|' /etc/apache2/sites-available/000-default.conf
sudo systemctl reload apache2
```
## Sudo athorization
To make things simpler we will allow all users to use "nmcli" as sudo without entering password. For that we need to open the sudoers file with `sudo visudo` and add this to the bottom of the file:
```
ALL ALL=(ALL) NOPASSWD: /usr/bin/nmcli, /usr/sbin/reboot
www-data ALL=(ALL) NOPASSWD: /usr/bin/git pull
www-data ALL=(ALL) NOPASSWD: /usr/bin/ssh
```
## Serial
Need to open all the uart port by modifying `sudo nano /boot/firmware/config.txt`
```
enable_uart=1
dtoverlay=uart0
dtoverlay=uart1
dtoverlay=uart2
dtoverlay=uart3
dtoverlay=uart4
dtoverlay=uart5
```
And reboot !
Then we need to authorize connection over device on `/etc/ttyAMA*`
```
sudo chmod 777 /dev/ttyAMA*
```
## I2C
Decibel meter and BME280 is connected via I2C.
Need to activate by modifying `sudo nano /boot/firmware/config.txt`
```
dtparam=i2c_arm=on
```
And authorize access to `/dev/i2c-1`.
```
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.
### 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).
### 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).
To keep the script running at boot and stay on we create a systemd service
Create the service with `sudo nano /etc/systemd/system/sound_meter.service` and add:
```
[Unit]
Description=Sound Meter Service
After=network.target
[Service]
ExecStart=/var/www/nebuleair_pro_4g/sound_meter/sound_meter_moving_avg
Restart=always
User=airlab
WorkingDirectory=/var/www/nebuleair_pro_4g/sound_meter
[Install]
WantedBy=multi-user.target
```
Then start the service:
```
sudo systemctl daemon-reload
sudo systemctl enable sound_meter.service
sudo systemctl start sound_meter.service
```
## SSH Tunneling
To have a remote access to the RPI we can start a SSH tunneling at boot with the command:
```
ssh -p 50221 -R <device_sshTunelPort>:localhost:22 airlab_server1@aircarto.fr
```
### To make things simpler we need to connect via a ssh key.
```
ssh-keygen -t rsa -b 4096
```
And add the key to the server with `ssh-copy-id -p 50221 airlab_server1@aircarto.fr`
## Crontabs
Attention, authorization for uart are reinitialized after reboot. Need to add the command to `sudo crontab -e `
```
@reboot chmod 777 /dev/ttyAMA* /dev/i2c-1
```
And start the Hotspot check:
```
@reboot /var/www/nebuleair_pro_4g/boot_hotspot.sh >> /var/www/nebuleair_pro_4g/logs/app.log 2>&1
```
And set the base URL for Sara R4 communication:
```
@reboot sleep 30 && /usr/bin/python3 /var/www/nebuleair_pro_4g/SARA/sara_setURL.py ttyAMA2 data.nebuleair.fr >> /var/www/nebuleair_pro_4g/logs/app.log 2>&1
```
### With only 1 NPM
Loop every minutes to get the PM values and send it to the server:
```
* * * * * /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
## Wifi Hotspot (AP)
To connect the device to the internet we need to create a Hotspot using nmcli.
Command to create a AP with SSI: nebuleair_pro and PASS: nebuleaircfg:
```
sudo nmcli device wifi hotspot ifname wlan0 ssid nebuleair_pro password nebuleaircfg
#we also need to set IP addresses
sudo nmcli connection modify Hotspot ipv4.addresses 192.168.4.1/24
sudo nmcli connection modify Hotspot ipv4.method shared
```
This will create a new connection called "Hotspot" using device "wlan0". You can connect to this network
via wifi and access to the self-hosted webpage on 192.168.4.1 (to get the IP `ip addr show wlan0` or `nmcli device show wlan0`).
Only problem is that you cannot perform a wifi scan while wlan0 is in AP mode. So you need to scan the available networks just before creating the Hotspot.
Second issue: hotspot need to be lauched at startup only if it cannot connected to the selected wifi network.
Wifi connection check, wifi scan and activation of the Hotspot need to be lauched at every startup.
This can be doned with script boot_hotspot.sh.
```
@reboot chmod 777 /dev/ttyAMA* /dev/i2c-1
@reboot /var/www/nebuleair_pro_4g/boot_hotspot.sh
```

87
SARA/MQTT/get_config.py Executable file
View File

@@ -0,0 +1,87 @@
'''
Script to see get the SARA R4 MQTT config
ex:
/usr/bin/python3 /var/www/nebuleair_pro_4g/SARA/MQTT/get_config.py ttyAMA2 2
'''
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
timeout = float(parameter[1]) # ex:2
yellow_color = "\033[33m" # ANSI escape code for yellow
red_color = "\033[31m" # ANSI escape code for red
reset_color = "\033[0m" # Reset color to default
green_color = "\033[32m" # Green
#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 {}
def read_complete_response(serial_connection, timeout=2, end_of_response_timeout=2):
response = bytearray()
serial_connection.timeout = timeout
end_time = time.time() + end_of_response_timeout
while True:
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
elif time.time() > end_time:
break
time.sleep(0.1) # Short sleep to prevent busy waiting
# Decode the response and filter out empty lines
decoded_response = response.decode('utf-8')
non_empty_lines = "\n".join(line for line in decoded_response.splitlines() if line.strip())
# Add yellow color to the output
colored_output = f"{yellow_color}{non_empty_lines}\n{reset_color}"
return colored_output
# Define the config file path
config_file = '/var/www/nebuleair_pro_4g/config.json'
# Load the configuration data
config = load_config(config_file)
# Access the shared variables
baudrate = config.get('SaraR4_baudrate', 115200)
ser = serial.Serial(
port=port, #USB0 or ttyS0
baudrate=baudrate, #115200 ou 9600
parity=serial.PARITY_NONE, #PARITY_NONE, PARITY_EVEN or PARITY_ODD
stopbits=serial.STOPBITS_ONE,
bytesize=serial.EIGHTBITS,
timeout = timeout
)
ser.write(b'AT+UMQTT?\r') #General Information
try:
response_SARA_1 = read_complete_response(ser)
print(response_SARA_1)
except serial.SerialException as e:
print(f"Error: {e}")
finally:
if ser.is_open:
ser.close()
#print("Serial closed")

90
SARA/MQTT/login_logout.py Executable file
View File

@@ -0,0 +1,90 @@
'''
Script to see get the SARA R4 MQTT config
ex:
/usr/bin/python3 /var/www/nebuleair_pro_4g/SARA/MQTT/login_logout.py ttyAMA2 1 2
'''
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
login_logout = int(parameter[1]) # ex:1
timeout = float(parameter[2]) # ex:2
yellow_color = "\033[33m" # ANSI escape code for yellow
red_color = "\033[31m" # ANSI escape code for red
reset_color = "\033[0m" # Reset color to default
green_color = "\033[32m" # Green
#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 {}
def read_complete_response(serial_connection, timeout=2, end_of_response_timeout=2):
response = bytearray()
serial_connection.timeout = timeout
end_time = time.time() + end_of_response_timeout
while True:
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
elif time.time() > end_time:
break
time.sleep(0.1) # Short sleep to prevent busy waiting
# Decode the response and filter out empty lines
decoded_response = response.decode('utf-8')
non_empty_lines = "\n".join(line for line in decoded_response.splitlines() if line.strip())
# Add yellow color to the output
colored_output = f"{yellow_color}{non_empty_lines}\n{reset_color}"
return colored_output
# 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
)
command = f'AT+UMQTTC={login_logout}\r'
ser.write((command + '\r').encode('utf-8'))
try:
response_SARA_1 = read_complete_response(ser)
print(response_SARA_1)
except serial.SerialException as e:
print(f"Error: {e}")
finally:
if ser.is_open:
ser.close()
#print("Serial closed")

89
SARA/MQTT/publish.py Executable file
View File

@@ -0,0 +1,89 @@
'''
Script to publish a message via MQTT
/usr/bin/python3 /var/www/nebuleair_pro_4g/SARA/MQTT/publish.py ttyAMA2 Hello 2
'''
import serial
import time
import sys
import json
parameter = sys.argv[1:] # Exclude the script name
port='/dev/'+parameter[0] # ex: ttyAMA2
message = parameter[1] # ex: Hello
timeout = float(parameter[2]) # ex:2
yellow_color = "\033[33m" # ANSI escape code for yellow
red_color = "\033[31m" # ANSI escape code for red
reset_color = "\033[0m" # Reset color to default
green_color = "\033[32m" # Green
def read_complete_response(serial_connection, timeout=2, end_of_response_timeout=2):
response = bytearray()
serial_connection.timeout = timeout
end_time = time.time() + end_of_response_timeout
while True:
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
elif time.time() > end_time:
break
time.sleep(0.1) # Short sleep to prevent busy waiting
# Decode the response and filter out empty lines
decoded_response = response.decode('utf-8')
non_empty_lines = "\n".join(line for line in decoded_response.splitlines() if line.strip())
# Add yellow color to the output
colored_output = f"{yellow_color}{non_empty_lines}\n{reset_color}"
return colored_output
#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
)
command = f'AT+UMQTTC=2,0,0,"notif","{message}"\r'
ser.write((command + '\r').encode('utf-8'))
try:
response_SARA_1 = read_complete_response(ser)
print(response_SARA_1)
except serial.SerialException as e:
print(f"Error: {e}")
finally:
if ser.is_open:
ser.close()
#print("Serial closed")

94
SARA/MQTT/set_config.py Executable file
View File

@@ -0,0 +1,94 @@
'''
Script to connect set the MQTT config
1: local TCP port number
2: server name
3: server IP addr
/usr/bin/python3 /var/www/nebuleair_pro_4g/SARA/MQTT/set_config.py ttyAMA2 2 aircarto.fr 2
'''
import serial
import time
import sys
import json
parameter = sys.argv[1:] # Exclude the script name
port='/dev/'+parameter[0] # ex: ttyAMA2
config_id = parameter[1] # ex: 1
config_value = parameter[2] # ex: aircarto.fr
timeout = float(parameter[3]) # ex:2
yellow_color = "\033[33m" # ANSI escape code for yellow
red_color = "\033[31m" # ANSI escape code for red
reset_color = "\033[0m" # Reset color to default
green_color = "\033[32m" # Green
def read_complete_response(serial_connection, timeout=2, end_of_response_timeout=2):
response = bytearray()
serial_connection.timeout = timeout
end_time = time.time() + end_of_response_timeout
while True:
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
elif time.time() > end_time:
break
time.sleep(0.1) # Short sleep to prevent busy waiting
# Decode the response and filter out empty lines
decoded_response = response.decode('utf-8')
non_empty_lines = "\n".join(line for line in decoded_response.splitlines() if line.strip())
# Add yellow color to the output
colored_output = f"{yellow_color}{non_empty_lines}\n{reset_color}"
return colored_output
#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
)
command = f'AT+UMQTT={config_id},"{config_value}"\r'
ser.write((command + '\r').encode('utf-8'))
try:
response_SARA_1 = read_complete_response(ser)
print(response_SARA_1)
except serial.SerialException as e:
print(f"Error: {e}")
finally:
if ser.is_open:
ser.close()
#print("Serial closed")

82
SARA/sara.py Executable file
View File

@@ -0,0 +1,82 @@
'''
Script to see if the SARA-R410 is running
ex:
python3 /var/www/nebuleair_pro_4g/SARA/sara.py ttyAMA2 AT+CCID? 2
'''
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
command = parameter[1] # ex: AT+CCID?
timeout = float(parameter[2]) # ex:2
#get baudrate
def load_config(config_file):
try:
with open(config_file, 'r') as file:
config_data = json.load(file)
return config_data
except Exception as e:
print(f"Error loading config file: {e}")
return {}
# Define the config file path
config_file = '/var/www/nebuleair_pro_4g/config.json'
# Load the configuration data
config = load_config(config_file)
# Access the shared variables
baudrate = config.get('SaraR4_baudrate', 115200)
ser = serial.Serial(
port=port, #USB0 or ttyS0
baudrate=baudrate, #115200 ou 9600
parity=serial.PARITY_NONE, #PARITY_NONE, PARITY_EVEN or PARITY_ODD
stopbits=serial.STOPBITS_ONE,
bytesize=serial.EIGHTBITS,
timeout = timeout
)
ser.write((command + '\r').encode('utf-8'))
#ser.write(b'ATI\r') #General Information
#ser.write(b'AT+CCID?\r') #SIM card number
#ser.write(b'AT+CPIN?\r') #Check the status of the SIM card
#ser.write(b'AT+CIND?\r') #Indication state (last number is SIM detection: 0 no SIM detection, 1 SIM detected, 2 not available)
#ser.write(b'AT+UGPIOR=?\r') #Reads the current value of the specified GPIO pin
#ser.write(b'AT+UGPIOC?\r') #GPIO select configuration
#ser.write(b'AT+COPS=?\r') #Check the network and cellular technology the modem is currently using
#ser.write(b'AT+COPS=1,2,20801') #connext to orange
#ser.write(b'AT+CFUN=?\r') #Selects/read the level of functionality
#ser.write(b'AT+URAT=?\r') #Radio Access Technology
#ser.write(b'AT+USIMSTAT?')
#ser.write(b'AT+IPR=115200') #Check/Define baud rate
#ser.write(b'AT+CMUX=?')
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("Serial closed")

70
SARA/sara_connectNetwork.py Executable file
View File

@@ -0,0 +1,70 @@
'''
Script to connect SARA-R410 to network SARA-R410
AT+COPS=1,2,20801
mode->1 pour manual
format->2 pour numeric
operator->20801 pour orange
'''
import serial
import time
import sys
import json
parameter = sys.argv[1:] # Exclude the script name
port='/dev/'+parameter[0] # ex: ttyAMA2
networkID = parameter[1] # ex: 20801
timeout = float(parameter[2]) # ex:2
#get baudrate
def load_config(config_file):
try:
with open(config_file, 'r') as file:
config_data = json.load(file)
return config_data
except Exception as e:
print(f"Error loading config file: {e}")
return {}
# Define the config file path
config_file = '/var/www/nebuleair_pro_4g/config.json'
# Load the configuration data
config = load_config(config_file)
# Access the shared variables
baudrate = config.get('SaraR4_baudrate', 115200)
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
)
command = f'AT+COPS=1,2,"{networkID}"\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("Serial closed")

65
SARA/sara_eraseMessage.py Executable file
View File

@@ -0,0 +1,65 @@
'''
Script to erase memory to SARA-R410 memory
'''
import serial
import time
import sys
import json
parameter = sys.argv[1:] # Exclude the script name
port='/dev/'+parameter[0] # ex: ttyAMA2
message = parameter[1] # ex: Hello
#get baudrate
def load_config(config_file):
try:
with open(config_file, 'r') as file:
config_data = json.load(file)
return config_data
except Exception as e:
print(f"Error loading config file: {e}")
return {}
# Define the config file path
config_file = '/var/www/nebuleair_pro_4g/config.json'
# Load the configuration data
config = load_config(config_file)
# Access the shared variables
baudrate = config.get('SaraR4_baudrate', 115200)
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
)
#0. empty file
ser.write(b'AT+UDELFILE="sensordata.json"\r')
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("Serial closed")

62
SARA/sara_readMessage.py Executable file
View File

@@ -0,0 +1,62 @@
'''
Script to read memory to SARA-R410 memory
'''
import serial
import time
import sys
import json
parameter = sys.argv[1:] # Exclude the script name
port='/dev/'+parameter[0] # ex: ttyAMA2
message = parameter[1] # ex: Hello
#get baudrate
def load_config(config_file):
try:
with open(config_file, 'r') as file:
config_data = json.load(file)
return config_data
except Exception as e:
print(f"Error loading config file: {e}")
return {}
# Define the config file path
config_file = '/var/www/nebuleair_pro_4g/config.json'
# Load the configuration data
config = load_config(config_file)
# Access the shared variables
baudrate = config.get('SaraR4_baudrate', 115200)
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
)
ser.write(b'AT+URDFILE="sensordata.json"\r')
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("Serial closed")

63
SARA/sara_sendMessage.py Executable file
View File

@@ -0,0 +1,63 @@
'''
Script to send message to URL with SARA-R410
'''
import serial
import time
import sys
import json
parameter = sys.argv[1:] # Exclude the script name
port='/dev/'+parameter[0] # ex: ttyAMA2
endpoint = parameter[1] # ex: /pro_4G/notif_message.php
#get baudrate
def load_config(config_file):
try:
with open(config_file, 'r') as file:
config_data = json.load(file)
return config_data
except Exception as e:
print(f"Error loading config file: {e}")
return {}
# Define the config file path
config_file = '/var/www/nebuleair_pro_4g/config.json'
# Load the configuration data
config = load_config(config_file)
# Access the shared variables
baudrate = config.get('SaraR4_baudrate', 115200)
ser = serial.Serial(
port=port, #USB0 or ttyS0
baudrate=baudrate, #115200 ou 9600
parity=serial.PARITY_NONE, #PARITY_NONE, PARITY_EVEN or PARITY_ODD
stopbits=serial.STOPBITS_ONE,
bytesize=serial.EIGHTBITS,
timeout = 2
)
command= f'AT+UHTTPC=0,4,"{endpoint}","data.txt","sensordata.json",4\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("Serial closed")

69
SARA/sara_setURL.py Executable file
View File

@@ -0,0 +1,69 @@
'''
Script to set the URL for a HTTP request
'''
import serial
import time
import sys
import json
parameter = sys.argv[1:] # Exclude the script name
#print("Parameters received:")
port='/dev/'+parameter[0] # ex: ttyAMA2
url = parameter[1] # ex: data.mobileair.fr
#get baudrate
def load_config(config_file):
try:
with open(config_file, 'r') as file:
config_data = json.load(file)
return config_data
except Exception as e:
print(f"Error loading config file: {e}")
return {}
# Define the config file path
config_file = '/var/www/nebuleair_pro_4g/config.json'
# Load the configuration data
config = load_config(config_file)
# Access the shared variables
baudrate = config.get('SaraR4_baudrate', 115200)
ser = serial.Serial(
port=port, #USB0 or ttyS0
baudrate=baudrate, #115200 ou 9600
parity=serial.PARITY_NONE, #PARITY_NONE, PARITY_EVEN or PARITY_ODD
stopbits=serial.STOPBITS_ONE,
bytesize=serial.EIGHTBITS,
timeout = 2
)
command = f'AT+UHTTP=0,1,"{url}"\r'
ser.write((command + '\r').encode('utf-8'))
print("****")
print("SET URL (SARA)")
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")

71
SARA/sara_writeMessage.py Executable file
View File

@@ -0,0 +1,71 @@
'''
Script to write message to SARA-R410 memory
'''
import serial
import time
import sys
import json
parameter = sys.argv[1:] # Exclude the script name
port='/dev/'+parameter[0] # ex: ttyAMA2
message = parameter[1] # ex: Hello
#get baudrate
def load_config(config_file):
try:
with open(config_file, 'r') as file:
config_data = json.load(file)
return config_data
except Exception as e:
print(f"Error loading config file: {e}")
return {}
# Define the config file path
config_file = '/var/www/nebuleair_pro_4g/config.json'
# Load the configuration data
config = load_config(config_file)
# Access the shared variables
baudrate = config.get('SaraR4_baudrate', 115200)
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
)
#0. empty file
#ser.write(b'AT+UDELFILE="sensordata.json"\r')
#1. Open sensordata.json (with correct data size)
size_of_string = len(message)
command = f'AT+UDWNFILE="sensordata.json",{size_of_string}\r'
ser.write((command + '\r').encode('utf-8'))
time.sleep(1)
#2. Write to shell
ser.write(message.encode())
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("Serial closed")

75
boot_hotspot.sh Executable file
View File

@@ -0,0 +1,75 @@
#!/bin/bash
# Script to check if wifi is connected and start hotspot if not
# will also retreive unique RPi ID and store it to deviceID.txt
OUTPUT_FILE="/var/www/nebuleair_pro_4g/wifi_list.csv"
JSON_FILE="/var/www/nebuleair_pro_4g/config.json"
echo "-------------------"
echo "-------------------"
echo "NebuleAir pro started at $(date)"
echo "getting SARA R4 serial number"
# Get the last 8 characters of the serial number and write to text file
serial_number=$(cat /proc/cpuinfo | grep Serial | awk '{print substr($3, length($3) - 7)}')
# Define the JSON file path
# Use jq to update the "deviceID" in the JSON file
jq --arg serial_number "$serial_number" '.deviceID = $serial_number' "$JSON_FILE" > temp.json && mv temp.json "$JSON_FILE"
echo "id: $serial_number"
#get the SSH port for tunneling
SSH_TUNNEL_PORT=$(jq -r '.sshTunnel_port' "$JSON_FILE")
#need to wait for the network manager to be ready
sleep 20
# Get the connection state of wlan0
STATE=$(nmcli -g GENERAL.STATE device show wlan0)
# Check if the state is 'disconnected'
if [ "$STATE" == "30 (disconnected)" ]; then
echo "wlan0 is disconnected."
echo "need to perform a wifi scan"
# Perform a wifi scan and save its output to a csv file
# nmcli device wifi list
nmcli -f SSID,SIGNAL,SECURITY device wifi list | awk 'BEGIN { OFS=","; print "SSID,SIGNAL,SECURITY" } NR>1 { print $1,$2,$3 }' > "$OUTPUT_FILE"
# Start the hotspot
echo "Starting hotspot..."
sudo nmcli device wifi hotspot ifname wlan0 ssid nebuleair_pro password nebuleaircfg
# Update JSON to reflect hotspot mode
jq --arg status "hotspot" '.WIFI_status = $status' "$JSON_FILE" > temp.json && mv temp.json "$JSON_FILE"
else
echo "Success: wlan0 is connected!"
CONN_SSID=$(nmcli -g GENERAL.CONNECTION device show wlan0)
echo "Connection: $CONN_SSID"
#update config JSON file
jq --arg status "connected" '.WIFI_status = $status' "$JSON_FILE" > temp.json && mv temp.json "$JSON_FILE"
# Lancer le tunnel SSH
echo "Démarrage du tunnel SSH sur le port $SSH_TUNNEL_PORT..."
# Start the SSH agent if it's not already running
eval "$(ssh-agent -s)"
# Add your SSH private key
ssh-add /home/airlab/.ssh/id_rsa
#connections details
REMOTE_USER="airlab_server1" # Remplacez par votre nom d'utilisateur distant
REMOTE_SERVER="aircarto.fr" # Remplacez par l'adresse de votre serveur
LOCAL_PORT=22 # Port local à rediriger
MONITOR_PORT=0 # Désactive la surveillance de connexion autossh
#autossh -M "$MONITOR_PORT" -f -N -R "$SSH_TUNNEL_PORT:localhost:$LOCAL_PORT" "$REMOTE_USER@$REMOTE_SERVER" -p 50221
# ssh -f -N -R 52221:localhost:22 -p 50221 airlab_server1@aircarto.fr
ssh -i /var/www/.ssh/id_rsa -f -N -R "$SSH_TUNNEL_PORT:localhost:$LOCAL_PORT" -p 50221 -o StrictHostKeyChecking=no "$REMOTE_USER@$REMOTE_SERVER"
#Check if the tunnel was created successfully
if [ $? -eq 0 ]; then
echo "Tunnel started successfully!"
else
echo "Error: Unable to start the tunnel!"
exit 1
fi
fi
echo "-------------------"

38
config.json.dist Executable file
View File

@@ -0,0 +1,38 @@
{
"loop_log": true,
"boot_log": true,
"deviceID": "XXXX",
"SaraR4_baudrate": 115200,
"NextPM_ports": [
"ttyAMA5"
],
"i2C_sound": true,
"i2c_BME": false,
"sshTunnel_port": 59228,
"npm1_status": "connected",
"SARA_R4_general_status": "connected",
"SARA_R4_SIM_status": "connected",
"SARA_R4_network_status": "connected",
"WIFI_status": "connected",
"MQTT_GUI": true,
"envea_sondes": [
{
"connected": true,
"port": "ttyAMA4",
"name": "h2s",
"coefficient" : 4
},
{
"connected": false,
"port": "ttyAMA2",
"name": "no2",
"coefficient" : 1
},
{
"connected": false,
"port": "ttyAMA1",
"name": "o3",
"coefficient" : 1
}
]
}

28
connexion.sh Executable file
View File

@@ -0,0 +1,28 @@
#!/bin/bash
echo "-------"
echo "Start connexion shell script at $(date)"
#disable hotspot
echo "Disable Hotspot:"
sudo nmcli connection down Hotspot
sleep 10
echo "Start connection with:"
echo "SSID: $1"
echo "Password: $2"
sudo nmcli device wifi connect "$1" password "$2"
#check if connection is successfull
if [ $? -eq 0 ]; then
echo "Connection to $1 is successfull"
else
echo "Connection to $1 failed"
echo "Restarting hotspot..."
#enable hotspot
sudo nmcli connection up Hotspot
fi
echo "End connexion shell script"
echo "-------"

9
cron_jobs Executable file
View File

@@ -0,0 +1,9 @@
@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/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

90
envea/read_ref.py Executable file
View File

@@ -0,0 +1,90 @@
import serial
import time
import sys
parameter = sys.argv[1:] # Exclude the script name
#print("Parameters received:")
port='/dev/'+parameter[0]
def read_cairsens(port, baudrate=9600, parity=serial.PARITY_NONE, stopbits=serial.STOPBITS_ONE, databits=serial.EIGHTBITS, timeout=1):
"""
Lit les données de la sonde CAIRSENS via UART.
/usr/bin/python3 /var/www/nebuleair_pro_4g/envea/read_ref.py ttyAMA4
:param port: Le port série utilisé (ex: 'COM1' ou '/dev/ttyAMA0').
:param baudrate: Le débit en bauds (ex: 9600).
:param parity: Le bit de parité (serial.PARITY_NONE, serial.PARITY_EVEN, serial.PARITY_ODD).
:param stopbits: Le nombre de bits de stop (serial.STOPBITS_ONE, serial.STOPBITS_TWO).
:param databits: Le nombre de bits de données (serial.FIVEBITS, serial.SIXBITS, serial.SEVENBITS, serial.EIGHTBITS).
:param timeout: Temps d'attente maximal pour la lecture (en secondes).
:return: Les données reçues sous forme de chaîne de caractères.
"""
try:
# Ouvrir la connexion série
ser = serial.Serial(
port=port,
baudrate=baudrate,
parity=parity,
stopbits=stopbits,
bytesize=databits,
timeout=timeout
)
print(f"Connexion ouverte sur {port} à {baudrate} bauds.")
# Attendre un instant pour stabiliser la connexion
time.sleep(2)
# Envoyer une commande à la sonde (si nécessaire)
# Adapter cette ligne selon la documentation de la sonde
#ser.write(b'\r\n')
ser.write(b'\xFF\x02\x13\x30\x01\x02\x03\x04\x05\x06\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\x1C\xD1\x61\x03')
# Lire les données reçues
#data = ser.read_until(b'\n') # Lire jusqu'à la fin de ligne ou un autre délimiteur
data = ser.readline()
print(f"Données reçues brutes : {data}")
#print(f"Données reçues (utf-8) : {data.decode('utf-8').strip()}")
# Convertir les données en hexadécimal
hex_data = data.hex() # Convertit en chaîne hexadécimale
formatted_hex = ' '.join(hex_data[i:i+2] for i in range(0, len(hex_data), 2)) # Formate avec des espaces
print(f"Données reçues en hexadécimal : {formatted_hex}")
# Extraire les valeurs de l'index 11 à 18
extracted_hex = hex_data[20:36] # Chaque caractère hex est représenté par 2 caractères
print(f"Valeurs hexadécimales extraites (11 à 18) : {extracted_hex}")
# Convertir en ASCII et en valeurs numériques
raw_bytes = bytes.fromhex(extracted_hex)
# ASCII characters
ascii_data = ''.join(chr(b) if 0x20 <= b <= 0x7E else '.' for b in raw_bytes)
print(f"Valeurs converties en ASCII : {ascii_data}")
# Numeric values
numeric_values = [b for b in raw_bytes]
print(f"Valeurs numériques : {numeric_values}")
# Fermer la connexion
ser.close()
print("Connexion fermée.")
return data
except serial.SerialException as e:
print(f"Erreur de connexion série : {e}")
return None
# Exemple d'utilisation
if __name__ == "__main__":
port = port # Remplacez par votre port série (ex: /dev/ttyAMA0 sur Raspberry Pi)
baudrate = 9600 # Débit en bauds (à vérifier dans la documentation)
parity = serial.PARITY_NONE # Parité (NONE, EVEN, ODD)
stopbits = serial.STOPBITS_ONE # Bits de stop (ONE, TWO)
databits = serial.EIGHTBITS # Bits de données (FIVEBITS, SIXBITS, SEVENBITS, EIGHTBITS)
data = read_cairsens(port, baudrate, parity, stopbits, databits)
if data:
print(f"Mesures de la sonde : {data}")

80
envea/read_value.py Executable file
View File

@@ -0,0 +1,80 @@
import serial
import time
import sys
parameter = sys.argv[1:] # Exclude the script name
#print("Parameters received:")
port='/dev/'+parameter[0]
coefficient = 4
def read_cairsens(port, baudrate=9600, parity=serial.PARITY_NONE, stopbits=serial.STOPBITS_ONE, databits=serial.EIGHTBITS, timeout=1):
"""
Lit les données de la sonde CAIRSENS via UART.
/usr/bin/python3 /var/www/nebuleair_pro_4g/envea/read_value.py ttyAMA4
:param port: Le port série utilisé (ex: 'COM1' ou '/dev/ttyAMA0').
:param baudrate: Le débit en bauds (ex: 9600).
:param parity: Le bit de parité (serial.PARITY_NONE, serial.PARITY_EVEN, serial.PARITY_ODD).
:param stopbits: Le nombre de bits de stop (serial.STOPBITS_ONE, serial.STOPBITS_TWO).
:param databits: Le nombre de bits de données (serial.FIVEBITS, serial.SIXBITS, serial.SEVENBITS, serial.EIGHTBITS).
:param timeout: Temps d'attente maximal pour la lecture (en secondes).
:return: Les données reçues sous forme de chaîne de caractères.
"""
try:
# Ouvrir la connexion série
ser = serial.Serial(
port=port,
baudrate=baudrate,
parity=parity,
stopbits=stopbits,
bytesize=databits,
timeout=timeout
)
#print(f"Connexion ouverte sur {port} à {baudrate} bauds.")
# Attendre un instant pour stabiliser la connexion
time.sleep(2)
# Envoyer une commande à la sonde (si nécessaire)
# Adapter cette ligne selon la documentation de la sonde
#ser.write(b'\r\n')
ser.write(b'\xFF\x02\x13\x30\x01\x02\x03\x04\x05\x06\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\x12\xAF\x88\x03')
# Lire les données reçues
#data = ser.read_until(b'\n') # Lire jusqu'à la fin de ligne ou un autre délimiteur
data = ser.readline()
#print(f"Données reçues brutes : {data}")
#print(f"Données reçues (utf-8) : {data.decode('utf-8').strip()}")
# Extraire le 20ème octet
if len(data) >= 20:
byte_20 = data[19] # Les indices commencent à 0, donc 19 pour le 20ème octet
#print(f"20ème octet en hexadécimal : {hex(byte_20)}")
#print(f"20ème octet en nombre : {byte_20}")
else:
print("Données reçues insuffisantes pour extraire le 20ème octet.")
# Fermer la connexion
ser.close()
#print("Connexion fermée.")
return byte_20 * coefficient
except serial.SerialException as e:
print(f"Erreur de connexion série : {e}")
return None
# Exemple d'utilisation
if __name__ == "__main__":
port = port # Remplacez par votre port série (ex: /dev/ttyAMA0 sur Raspberry Pi)
baudrate = 9600 # Débit en bauds (à vérifier dans la documentation)
parity = serial.PARITY_NONE # Parité (NONE, EVEN, ODD)
stopbits = serial.STOPBITS_ONE # Bits de stop (ONE, TWO)
databits = serial.EIGHTBITS # Bits de données (FIVEBITS, SIXBITS, SEVENBITS, EIGHTBITS)
data = read_cairsens(port, baudrate, parity, stopbits, databits)
if data:
print(data)

116
html/admin.html Executable file
View File

@@ -0,0 +1,116 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Responsive Sidebar Template</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">Admin</h1>
<div class="col-lg-6 col-12">
<form>
<div class="form-check form-switch mb-2">
<input class="form-check-input" type="checkbox" role="switch" id="flexSwitchCheckDefault">
<label class="form-check-label" for="flexSwitchCheckDefault">Loop Logs</label>
</div>
<div class="form-check form-switch mb-2">
<input class="form-check-input" type="checkbox" role="switch" id="flexSwitchCheckDefault">
<label class="form-check-label" for="flexSwitchCheckDefault">Boot Logs</label>
</div>
<div class="form-check mb-3">
<input class="form-check-input" type="checkbox" value="" id="flexCheckDefault">
<label class="form-check-label" for="flexCheckDefault">
Sonde temp/hum (BME280)
</label>
</div>
<div class="mb-3">
<label for="exampleInputEmail1" class="form-label">ssh tunnel port</label>
<input type="email" class="form-control" id="exampleInputEmail1" aria-describedby="emailHelp">
</div>
<div class="mb-3">
<label for="exampleInputPassword1" class="form-label">Password</label>
<input type="password" class="form-control" id="exampleInputPassword1">
</div>
<button type="submit" class="btn btn-primary">Submit</button>
</form>
</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));
});
});
</script>
</body>
</html>

4085
html/assets/css/bootstrap-grid.css vendored Executable file

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

6
html/assets/css/bootstrap-grid.min.css vendored Executable file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

4084
html/assets/css/bootstrap-grid.rtl.css vendored Executable file

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

6
html/assets/css/bootstrap-grid.rtl.min.css vendored Executable file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

597
html/assets/css/bootstrap-reboot.css vendored Executable file
View File

@@ -0,0 +1,597 @@
/*!
* Bootstrap Reboot v5.3.3 (https://getbootstrap.com/)
* Copyright 2011-2024 The Bootstrap Authors
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
*/
:root,
[data-bs-theme=light] {
--bs-blue: #0d6efd;
--bs-indigo: #6610f2;
--bs-purple: #6f42c1;
--bs-pink: #d63384;
--bs-red: #dc3545;
--bs-orange: #fd7e14;
--bs-yellow: #ffc107;
--bs-green: #198754;
--bs-teal: #20c997;
--bs-cyan: #0dcaf0;
--bs-black: #000;
--bs-white: #fff;
--bs-gray: #6c757d;
--bs-gray-dark: #343a40;
--bs-gray-100: #f8f9fa;
--bs-gray-200: #e9ecef;
--bs-gray-300: #dee2e6;
--bs-gray-400: #ced4da;
--bs-gray-500: #adb5bd;
--bs-gray-600: #6c757d;
--bs-gray-700: #495057;
--bs-gray-800: #343a40;
--bs-gray-900: #212529;
--bs-primary: #0d6efd;
--bs-secondary: #6c757d;
--bs-success: #198754;
--bs-info: #0dcaf0;
--bs-warning: #ffc107;
--bs-danger: #dc3545;
--bs-light: #f8f9fa;
--bs-dark: #212529;
--bs-primary-rgb: 13, 110, 253;
--bs-secondary-rgb: 108, 117, 125;
--bs-success-rgb: 25, 135, 84;
--bs-info-rgb: 13, 202, 240;
--bs-warning-rgb: 255, 193, 7;
--bs-danger-rgb: 220, 53, 69;
--bs-light-rgb: 248, 249, 250;
--bs-dark-rgb: 33, 37, 41;
--bs-primary-text-emphasis: #052c65;
--bs-secondary-text-emphasis: #2b2f32;
--bs-success-text-emphasis: #0a3622;
--bs-info-text-emphasis: #055160;
--bs-warning-text-emphasis: #664d03;
--bs-danger-text-emphasis: #58151c;
--bs-light-text-emphasis: #495057;
--bs-dark-text-emphasis: #495057;
--bs-primary-bg-subtle: #cfe2ff;
--bs-secondary-bg-subtle: #e2e3e5;
--bs-success-bg-subtle: #d1e7dd;
--bs-info-bg-subtle: #cff4fc;
--bs-warning-bg-subtle: #fff3cd;
--bs-danger-bg-subtle: #f8d7da;
--bs-light-bg-subtle: #fcfcfd;
--bs-dark-bg-subtle: #ced4da;
--bs-primary-border-subtle: #9ec5fe;
--bs-secondary-border-subtle: #c4c8cb;
--bs-success-border-subtle: #a3cfbb;
--bs-info-border-subtle: #9eeaf9;
--bs-warning-border-subtle: #ffe69c;
--bs-danger-border-subtle: #f1aeb5;
--bs-light-border-subtle: #e9ecef;
--bs-dark-border-subtle: #adb5bd;
--bs-white-rgb: 255, 255, 255;
--bs-black-rgb: 0, 0, 0;
--bs-font-sans-serif: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", "Liberation Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
--bs-font-monospace: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
--bs-gradient: linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0));
--bs-body-font-family: var(--bs-font-sans-serif);
--bs-body-font-size: 1rem;
--bs-body-font-weight: 400;
--bs-body-line-height: 1.5;
--bs-body-color: #212529;
--bs-body-color-rgb: 33, 37, 41;
--bs-body-bg: #fff;
--bs-body-bg-rgb: 255, 255, 255;
--bs-emphasis-color: #000;
--bs-emphasis-color-rgb: 0, 0, 0;
--bs-secondary-color: rgba(33, 37, 41, 0.75);
--bs-secondary-color-rgb: 33, 37, 41;
--bs-secondary-bg: #e9ecef;
--bs-secondary-bg-rgb: 233, 236, 239;
--bs-tertiary-color: rgba(33, 37, 41, 0.5);
--bs-tertiary-color-rgb: 33, 37, 41;
--bs-tertiary-bg: #f8f9fa;
--bs-tertiary-bg-rgb: 248, 249, 250;
--bs-heading-color: inherit;
--bs-link-color: #0d6efd;
--bs-link-color-rgb: 13, 110, 253;
--bs-link-decoration: underline;
--bs-link-hover-color: #0a58ca;
--bs-link-hover-color-rgb: 10, 88, 202;
--bs-code-color: #d63384;
--bs-highlight-color: #212529;
--bs-highlight-bg: #fff3cd;
--bs-border-width: 1px;
--bs-border-style: solid;
--bs-border-color: #dee2e6;
--bs-border-color-translucent: rgba(0, 0, 0, 0.175);
--bs-border-radius: 0.375rem;
--bs-border-radius-sm: 0.25rem;
--bs-border-radius-lg: 0.5rem;
--bs-border-radius-xl: 1rem;
--bs-border-radius-xxl: 2rem;
--bs-border-radius-2xl: var(--bs-border-radius-xxl);
--bs-border-radius-pill: 50rem;
--bs-box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
--bs-box-shadow-sm: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
--bs-box-shadow-lg: 0 1rem 3rem rgba(0, 0, 0, 0.175);
--bs-box-shadow-inset: inset 0 1px 2px rgba(0, 0, 0, 0.075);
--bs-focus-ring-width: 0.25rem;
--bs-focus-ring-opacity: 0.25;
--bs-focus-ring-color: rgba(13, 110, 253, 0.25);
--bs-form-valid-color: #198754;
--bs-form-valid-border-color: #198754;
--bs-form-invalid-color: #dc3545;
--bs-form-invalid-border-color: #dc3545;
}
[data-bs-theme=dark] {
color-scheme: dark;
--bs-body-color: #dee2e6;
--bs-body-color-rgb: 222, 226, 230;
--bs-body-bg: #212529;
--bs-body-bg-rgb: 33, 37, 41;
--bs-emphasis-color: #fff;
--bs-emphasis-color-rgb: 255, 255, 255;
--bs-secondary-color: rgba(222, 226, 230, 0.75);
--bs-secondary-color-rgb: 222, 226, 230;
--bs-secondary-bg: #343a40;
--bs-secondary-bg-rgb: 52, 58, 64;
--bs-tertiary-color: rgba(222, 226, 230, 0.5);
--bs-tertiary-color-rgb: 222, 226, 230;
--bs-tertiary-bg: #2b3035;
--bs-tertiary-bg-rgb: 43, 48, 53;
--bs-primary-text-emphasis: #6ea8fe;
--bs-secondary-text-emphasis: #a7acb1;
--bs-success-text-emphasis: #75b798;
--bs-info-text-emphasis: #6edff6;
--bs-warning-text-emphasis: #ffda6a;
--bs-danger-text-emphasis: #ea868f;
--bs-light-text-emphasis: #f8f9fa;
--bs-dark-text-emphasis: #dee2e6;
--bs-primary-bg-subtle: #031633;
--bs-secondary-bg-subtle: #161719;
--bs-success-bg-subtle: #051b11;
--bs-info-bg-subtle: #032830;
--bs-warning-bg-subtle: #332701;
--bs-danger-bg-subtle: #2c0b0e;
--bs-light-bg-subtle: #343a40;
--bs-dark-bg-subtle: #1a1d20;
--bs-primary-border-subtle: #084298;
--bs-secondary-border-subtle: #41464b;
--bs-success-border-subtle: #0f5132;
--bs-info-border-subtle: #087990;
--bs-warning-border-subtle: #997404;
--bs-danger-border-subtle: #842029;
--bs-light-border-subtle: #495057;
--bs-dark-border-subtle: #343a40;
--bs-heading-color: inherit;
--bs-link-color: #6ea8fe;
--bs-link-hover-color: #8bb9fe;
--bs-link-color-rgb: 110, 168, 254;
--bs-link-hover-color-rgb: 139, 185, 254;
--bs-code-color: #e685b5;
--bs-highlight-color: #dee2e6;
--bs-highlight-bg: #664d03;
--bs-border-color: #495057;
--bs-border-color-translucent: rgba(255, 255, 255, 0.15);
--bs-form-valid-color: #75b798;
--bs-form-valid-border-color: #75b798;
--bs-form-invalid-color: #ea868f;
--bs-form-invalid-border-color: #ea868f;
}
*,
*::before,
*::after {
box-sizing: border-box;
}
@media (prefers-reduced-motion: no-preference) {
:root {
scroll-behavior: smooth;
}
}
body {
margin: 0;
font-family: var(--bs-body-font-family);
font-size: var(--bs-body-font-size);
font-weight: var(--bs-body-font-weight);
line-height: var(--bs-body-line-height);
color: var(--bs-body-color);
text-align: var(--bs-body-text-align);
background-color: var(--bs-body-bg);
-webkit-text-size-adjust: 100%;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
}
hr {
margin: 1rem 0;
color: inherit;
border: 0;
border-top: var(--bs-border-width) solid;
opacity: 0.25;
}
h6, h5, h4, h3, h2, h1 {
margin-top: 0;
margin-bottom: 0.5rem;
font-weight: 500;
line-height: 1.2;
color: var(--bs-heading-color);
}
h1 {
font-size: calc(1.375rem + 1.5vw);
}
@media (min-width: 1200px) {
h1 {
font-size: 2.5rem;
}
}
h2 {
font-size: calc(1.325rem + 0.9vw);
}
@media (min-width: 1200px) {
h2 {
font-size: 2rem;
}
}
h3 {
font-size: calc(1.3rem + 0.6vw);
}
@media (min-width: 1200px) {
h3 {
font-size: 1.75rem;
}
}
h4 {
font-size: calc(1.275rem + 0.3vw);
}
@media (min-width: 1200px) {
h4 {
font-size: 1.5rem;
}
}
h5 {
font-size: 1.25rem;
}
h6 {
font-size: 1rem;
}
p {
margin-top: 0;
margin-bottom: 1rem;
}
abbr[title] {
-webkit-text-decoration: underline dotted;
text-decoration: underline dotted;
cursor: help;
-webkit-text-decoration-skip-ink: none;
text-decoration-skip-ink: none;
}
address {
margin-bottom: 1rem;
font-style: normal;
line-height: inherit;
}
ol,
ul {
padding-left: 2rem;
}
ol,
ul,
dl {
margin-top: 0;
margin-bottom: 1rem;
}
ol ol,
ul ul,
ol ul,
ul ol {
margin-bottom: 0;
}
dt {
font-weight: 700;
}
dd {
margin-bottom: 0.5rem;
margin-left: 0;
}
blockquote {
margin: 0 0 1rem;
}
b,
strong {
font-weight: bolder;
}
small {
font-size: 0.875em;
}
mark {
padding: 0.1875em;
color: var(--bs-highlight-color);
background-color: var(--bs-highlight-bg);
}
sub,
sup {
position: relative;
font-size: 0.75em;
line-height: 0;
vertical-align: baseline;
}
sub {
bottom: -0.25em;
}
sup {
top: -0.5em;
}
a {
color: rgba(var(--bs-link-color-rgb), var(--bs-link-opacity, 1));
text-decoration: underline;
}
a:hover {
--bs-link-color-rgb: var(--bs-link-hover-color-rgb);
}
a:not([href]):not([class]), a:not([href]):not([class]):hover {
color: inherit;
text-decoration: none;
}
pre,
code,
kbd,
samp {
font-family: var(--bs-font-monospace);
font-size: 1em;
}
pre {
display: block;
margin-top: 0;
margin-bottom: 1rem;
overflow: auto;
font-size: 0.875em;
}
pre code {
font-size: inherit;
color: inherit;
word-break: normal;
}
code {
font-size: 0.875em;
color: var(--bs-code-color);
word-wrap: break-word;
}
a > code {
color: inherit;
}
kbd {
padding: 0.1875rem 0.375rem;
font-size: 0.875em;
color: var(--bs-body-bg);
background-color: var(--bs-body-color);
border-radius: 0.25rem;
}
kbd kbd {
padding: 0;
font-size: 1em;
}
figure {
margin: 0 0 1rem;
}
img,
svg {
vertical-align: middle;
}
table {
caption-side: bottom;
border-collapse: collapse;
}
caption {
padding-top: 0.5rem;
padding-bottom: 0.5rem;
color: var(--bs-secondary-color);
text-align: left;
}
th {
text-align: inherit;
text-align: -webkit-match-parent;
}
thead,
tbody,
tfoot,
tr,
td,
th {
border-color: inherit;
border-style: solid;
border-width: 0;
}
label {
display: inline-block;
}
button {
border-radius: 0;
}
button:focus:not(:focus-visible) {
outline: 0;
}
input,
button,
select,
optgroup,
textarea {
margin: 0;
font-family: inherit;
font-size: inherit;
line-height: inherit;
}
button,
select {
text-transform: none;
}
[role=button] {
cursor: pointer;
}
select {
word-wrap: normal;
}
select:disabled {
opacity: 1;
}
[list]:not([type=date]):not([type=datetime-local]):not([type=month]):not([type=week]):not([type=time])::-webkit-calendar-picker-indicator {
display: none !important;
}
button,
[type=button],
[type=reset],
[type=submit] {
-webkit-appearance: button;
}
button:not(:disabled),
[type=button]:not(:disabled),
[type=reset]:not(:disabled),
[type=submit]:not(:disabled) {
cursor: pointer;
}
::-moz-focus-inner {
padding: 0;
border-style: none;
}
textarea {
resize: vertical;
}
fieldset {
min-width: 0;
padding: 0;
margin: 0;
border: 0;
}
legend {
float: left;
width: 100%;
padding: 0;
margin-bottom: 0.5rem;
font-size: calc(1.275rem + 0.3vw);
line-height: inherit;
}
@media (min-width: 1200px) {
legend {
font-size: 1.5rem;
}
}
legend + * {
clear: left;
}
::-webkit-datetime-edit-fields-wrapper,
::-webkit-datetime-edit-text,
::-webkit-datetime-edit-minute,
::-webkit-datetime-edit-hour-field,
::-webkit-datetime-edit-day-field,
::-webkit-datetime-edit-month-field,
::-webkit-datetime-edit-year-field {
padding: 0;
}
::-webkit-inner-spin-button {
height: auto;
}
[type=search] {
-webkit-appearance: textfield;
outline-offset: -2px;
}
/* rtl:raw:
[type="tel"],
[type="url"],
[type="email"],
[type="number"] {
direction: ltr;
}
*/
::-webkit-search-decoration {
-webkit-appearance: none;
}
::-webkit-color-swatch-wrapper {
padding: 0;
}
::-webkit-file-upload-button {
font: inherit;
-webkit-appearance: button;
}
::file-selector-button {
font: inherit;
-webkit-appearance: button;
}
output {
display: inline-block;
}
iframe {
border: 0;
}
summary {
display: list-item;
cursor: pointer;
}
progress {
vertical-align: baseline;
}
[hidden] {
display: none !important;
}
/*# sourceMappingURL=bootstrap-reboot.css.map */

File diff suppressed because one or more lines are too long

6
html/assets/css/bootstrap-reboot.min.css vendored Executable file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

594
html/assets/css/bootstrap-reboot.rtl.css vendored Executable file
View File

@@ -0,0 +1,594 @@
/*!
* Bootstrap Reboot v5.3.3 (https://getbootstrap.com/)
* Copyright 2011-2024 The Bootstrap Authors
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
*/
:root,
[data-bs-theme=light] {
--bs-blue: #0d6efd;
--bs-indigo: #6610f2;
--bs-purple: #6f42c1;
--bs-pink: #d63384;
--bs-red: #dc3545;
--bs-orange: #fd7e14;
--bs-yellow: #ffc107;
--bs-green: #198754;
--bs-teal: #20c997;
--bs-cyan: #0dcaf0;
--bs-black: #000;
--bs-white: #fff;
--bs-gray: #6c757d;
--bs-gray-dark: #343a40;
--bs-gray-100: #f8f9fa;
--bs-gray-200: #e9ecef;
--bs-gray-300: #dee2e6;
--bs-gray-400: #ced4da;
--bs-gray-500: #adb5bd;
--bs-gray-600: #6c757d;
--bs-gray-700: #495057;
--bs-gray-800: #343a40;
--bs-gray-900: #212529;
--bs-primary: #0d6efd;
--bs-secondary: #6c757d;
--bs-success: #198754;
--bs-info: #0dcaf0;
--bs-warning: #ffc107;
--bs-danger: #dc3545;
--bs-light: #f8f9fa;
--bs-dark: #212529;
--bs-primary-rgb: 13, 110, 253;
--bs-secondary-rgb: 108, 117, 125;
--bs-success-rgb: 25, 135, 84;
--bs-info-rgb: 13, 202, 240;
--bs-warning-rgb: 255, 193, 7;
--bs-danger-rgb: 220, 53, 69;
--bs-light-rgb: 248, 249, 250;
--bs-dark-rgb: 33, 37, 41;
--bs-primary-text-emphasis: #052c65;
--bs-secondary-text-emphasis: #2b2f32;
--bs-success-text-emphasis: #0a3622;
--bs-info-text-emphasis: #055160;
--bs-warning-text-emphasis: #664d03;
--bs-danger-text-emphasis: #58151c;
--bs-light-text-emphasis: #495057;
--bs-dark-text-emphasis: #495057;
--bs-primary-bg-subtle: #cfe2ff;
--bs-secondary-bg-subtle: #e2e3e5;
--bs-success-bg-subtle: #d1e7dd;
--bs-info-bg-subtle: #cff4fc;
--bs-warning-bg-subtle: #fff3cd;
--bs-danger-bg-subtle: #f8d7da;
--bs-light-bg-subtle: #fcfcfd;
--bs-dark-bg-subtle: #ced4da;
--bs-primary-border-subtle: #9ec5fe;
--bs-secondary-border-subtle: #c4c8cb;
--bs-success-border-subtle: #a3cfbb;
--bs-info-border-subtle: #9eeaf9;
--bs-warning-border-subtle: #ffe69c;
--bs-danger-border-subtle: #f1aeb5;
--bs-light-border-subtle: #e9ecef;
--bs-dark-border-subtle: #adb5bd;
--bs-white-rgb: 255, 255, 255;
--bs-black-rgb: 0, 0, 0;
--bs-font-sans-serif: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", "Liberation Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
--bs-font-monospace: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
--bs-gradient: linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0));
--bs-body-font-family: var(--bs-font-sans-serif);
--bs-body-font-size: 1rem;
--bs-body-font-weight: 400;
--bs-body-line-height: 1.5;
--bs-body-color: #212529;
--bs-body-color-rgb: 33, 37, 41;
--bs-body-bg: #fff;
--bs-body-bg-rgb: 255, 255, 255;
--bs-emphasis-color: #000;
--bs-emphasis-color-rgb: 0, 0, 0;
--bs-secondary-color: rgba(33, 37, 41, 0.75);
--bs-secondary-color-rgb: 33, 37, 41;
--bs-secondary-bg: #e9ecef;
--bs-secondary-bg-rgb: 233, 236, 239;
--bs-tertiary-color: rgba(33, 37, 41, 0.5);
--bs-tertiary-color-rgb: 33, 37, 41;
--bs-tertiary-bg: #f8f9fa;
--bs-tertiary-bg-rgb: 248, 249, 250;
--bs-heading-color: inherit;
--bs-link-color: #0d6efd;
--bs-link-color-rgb: 13, 110, 253;
--bs-link-decoration: underline;
--bs-link-hover-color: #0a58ca;
--bs-link-hover-color-rgb: 10, 88, 202;
--bs-code-color: #d63384;
--bs-highlight-color: #212529;
--bs-highlight-bg: #fff3cd;
--bs-border-width: 1px;
--bs-border-style: solid;
--bs-border-color: #dee2e6;
--bs-border-color-translucent: rgba(0, 0, 0, 0.175);
--bs-border-radius: 0.375rem;
--bs-border-radius-sm: 0.25rem;
--bs-border-radius-lg: 0.5rem;
--bs-border-radius-xl: 1rem;
--bs-border-radius-xxl: 2rem;
--bs-border-radius-2xl: var(--bs-border-radius-xxl);
--bs-border-radius-pill: 50rem;
--bs-box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
--bs-box-shadow-sm: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
--bs-box-shadow-lg: 0 1rem 3rem rgba(0, 0, 0, 0.175);
--bs-box-shadow-inset: inset 0 1px 2px rgba(0, 0, 0, 0.075);
--bs-focus-ring-width: 0.25rem;
--bs-focus-ring-opacity: 0.25;
--bs-focus-ring-color: rgba(13, 110, 253, 0.25);
--bs-form-valid-color: #198754;
--bs-form-valid-border-color: #198754;
--bs-form-invalid-color: #dc3545;
--bs-form-invalid-border-color: #dc3545;
}
[data-bs-theme=dark] {
color-scheme: dark;
--bs-body-color: #dee2e6;
--bs-body-color-rgb: 222, 226, 230;
--bs-body-bg: #212529;
--bs-body-bg-rgb: 33, 37, 41;
--bs-emphasis-color: #fff;
--bs-emphasis-color-rgb: 255, 255, 255;
--bs-secondary-color: rgba(222, 226, 230, 0.75);
--bs-secondary-color-rgb: 222, 226, 230;
--bs-secondary-bg: #343a40;
--bs-secondary-bg-rgb: 52, 58, 64;
--bs-tertiary-color: rgba(222, 226, 230, 0.5);
--bs-tertiary-color-rgb: 222, 226, 230;
--bs-tertiary-bg: #2b3035;
--bs-tertiary-bg-rgb: 43, 48, 53;
--bs-primary-text-emphasis: #6ea8fe;
--bs-secondary-text-emphasis: #a7acb1;
--bs-success-text-emphasis: #75b798;
--bs-info-text-emphasis: #6edff6;
--bs-warning-text-emphasis: #ffda6a;
--bs-danger-text-emphasis: #ea868f;
--bs-light-text-emphasis: #f8f9fa;
--bs-dark-text-emphasis: #dee2e6;
--bs-primary-bg-subtle: #031633;
--bs-secondary-bg-subtle: #161719;
--bs-success-bg-subtle: #051b11;
--bs-info-bg-subtle: #032830;
--bs-warning-bg-subtle: #332701;
--bs-danger-bg-subtle: #2c0b0e;
--bs-light-bg-subtle: #343a40;
--bs-dark-bg-subtle: #1a1d20;
--bs-primary-border-subtle: #084298;
--bs-secondary-border-subtle: #41464b;
--bs-success-border-subtle: #0f5132;
--bs-info-border-subtle: #087990;
--bs-warning-border-subtle: #997404;
--bs-danger-border-subtle: #842029;
--bs-light-border-subtle: #495057;
--bs-dark-border-subtle: #343a40;
--bs-heading-color: inherit;
--bs-link-color: #6ea8fe;
--bs-link-hover-color: #8bb9fe;
--bs-link-color-rgb: 110, 168, 254;
--bs-link-hover-color-rgb: 139, 185, 254;
--bs-code-color: #e685b5;
--bs-highlight-color: #dee2e6;
--bs-highlight-bg: #664d03;
--bs-border-color: #495057;
--bs-border-color-translucent: rgba(255, 255, 255, 0.15);
--bs-form-valid-color: #75b798;
--bs-form-valid-border-color: #75b798;
--bs-form-invalid-color: #ea868f;
--bs-form-invalid-border-color: #ea868f;
}
*,
*::before,
*::after {
box-sizing: border-box;
}
@media (prefers-reduced-motion: no-preference) {
:root {
scroll-behavior: smooth;
}
}
body {
margin: 0;
font-family: var(--bs-body-font-family);
font-size: var(--bs-body-font-size);
font-weight: var(--bs-body-font-weight);
line-height: var(--bs-body-line-height);
color: var(--bs-body-color);
text-align: var(--bs-body-text-align);
background-color: var(--bs-body-bg);
-webkit-text-size-adjust: 100%;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
}
hr {
margin: 1rem 0;
color: inherit;
border: 0;
border-top: var(--bs-border-width) solid;
opacity: 0.25;
}
h6, h5, h4, h3, h2, h1 {
margin-top: 0;
margin-bottom: 0.5rem;
font-weight: 500;
line-height: 1.2;
color: var(--bs-heading-color);
}
h1 {
font-size: calc(1.375rem + 1.5vw);
}
@media (min-width: 1200px) {
h1 {
font-size: 2.5rem;
}
}
h2 {
font-size: calc(1.325rem + 0.9vw);
}
@media (min-width: 1200px) {
h2 {
font-size: 2rem;
}
}
h3 {
font-size: calc(1.3rem + 0.6vw);
}
@media (min-width: 1200px) {
h3 {
font-size: 1.75rem;
}
}
h4 {
font-size: calc(1.275rem + 0.3vw);
}
@media (min-width: 1200px) {
h4 {
font-size: 1.5rem;
}
}
h5 {
font-size: 1.25rem;
}
h6 {
font-size: 1rem;
}
p {
margin-top: 0;
margin-bottom: 1rem;
}
abbr[title] {
-webkit-text-decoration: underline dotted;
text-decoration: underline dotted;
cursor: help;
-webkit-text-decoration-skip-ink: none;
text-decoration-skip-ink: none;
}
address {
margin-bottom: 1rem;
font-style: normal;
line-height: inherit;
}
ol,
ul {
padding-right: 2rem;
}
ol,
ul,
dl {
margin-top: 0;
margin-bottom: 1rem;
}
ol ol,
ul ul,
ol ul,
ul ol {
margin-bottom: 0;
}
dt {
font-weight: 700;
}
dd {
margin-bottom: 0.5rem;
margin-right: 0;
}
blockquote {
margin: 0 0 1rem;
}
b,
strong {
font-weight: bolder;
}
small {
font-size: 0.875em;
}
mark {
padding: 0.1875em;
color: var(--bs-highlight-color);
background-color: var(--bs-highlight-bg);
}
sub,
sup {
position: relative;
font-size: 0.75em;
line-height: 0;
vertical-align: baseline;
}
sub {
bottom: -0.25em;
}
sup {
top: -0.5em;
}
a {
color: rgba(var(--bs-link-color-rgb), var(--bs-link-opacity, 1));
text-decoration: underline;
}
a:hover {
--bs-link-color-rgb: var(--bs-link-hover-color-rgb);
}
a:not([href]):not([class]), a:not([href]):not([class]):hover {
color: inherit;
text-decoration: none;
}
pre,
code,
kbd,
samp {
font-family: var(--bs-font-monospace);
font-size: 1em;
}
pre {
display: block;
margin-top: 0;
margin-bottom: 1rem;
overflow: auto;
font-size: 0.875em;
}
pre code {
font-size: inherit;
color: inherit;
word-break: normal;
}
code {
font-size: 0.875em;
color: var(--bs-code-color);
word-wrap: break-word;
}
a > code {
color: inherit;
}
kbd {
padding: 0.1875rem 0.375rem;
font-size: 0.875em;
color: var(--bs-body-bg);
background-color: var(--bs-body-color);
border-radius: 0.25rem;
}
kbd kbd {
padding: 0;
font-size: 1em;
}
figure {
margin: 0 0 1rem;
}
img,
svg {
vertical-align: middle;
}
table {
caption-side: bottom;
border-collapse: collapse;
}
caption {
padding-top: 0.5rem;
padding-bottom: 0.5rem;
color: var(--bs-secondary-color);
text-align: right;
}
th {
text-align: inherit;
text-align: -webkit-match-parent;
}
thead,
tbody,
tfoot,
tr,
td,
th {
border-color: inherit;
border-style: solid;
border-width: 0;
}
label {
display: inline-block;
}
button {
border-radius: 0;
}
button:focus:not(:focus-visible) {
outline: 0;
}
input,
button,
select,
optgroup,
textarea {
margin: 0;
font-family: inherit;
font-size: inherit;
line-height: inherit;
}
button,
select {
text-transform: none;
}
[role=button] {
cursor: pointer;
}
select {
word-wrap: normal;
}
select:disabled {
opacity: 1;
}
[list]:not([type=date]):not([type=datetime-local]):not([type=month]):not([type=week]):not([type=time])::-webkit-calendar-picker-indicator {
display: none !important;
}
button,
[type=button],
[type=reset],
[type=submit] {
-webkit-appearance: button;
}
button:not(:disabled),
[type=button]:not(:disabled),
[type=reset]:not(:disabled),
[type=submit]:not(:disabled) {
cursor: pointer;
}
::-moz-focus-inner {
padding: 0;
border-style: none;
}
textarea {
resize: vertical;
}
fieldset {
min-width: 0;
padding: 0;
margin: 0;
border: 0;
}
legend {
float: right;
width: 100%;
padding: 0;
margin-bottom: 0.5rem;
font-size: calc(1.275rem + 0.3vw);
line-height: inherit;
}
@media (min-width: 1200px) {
legend {
font-size: 1.5rem;
}
}
legend + * {
clear: right;
}
::-webkit-datetime-edit-fields-wrapper,
::-webkit-datetime-edit-text,
::-webkit-datetime-edit-minute,
::-webkit-datetime-edit-hour-field,
::-webkit-datetime-edit-day-field,
::-webkit-datetime-edit-month-field,
::-webkit-datetime-edit-year-field {
padding: 0;
}
::-webkit-inner-spin-button {
height: auto;
}
[type=search] {
-webkit-appearance: textfield;
outline-offset: -2px;
}
[type="tel"],
[type="url"],
[type="email"],
[type="number"] {
direction: ltr;
}
::-webkit-search-decoration {
-webkit-appearance: none;
}
::-webkit-color-swatch-wrapper {
padding: 0;
}
::-webkit-file-upload-button {
font: inherit;
-webkit-appearance: button;
}
::file-selector-button {
font: inherit;
-webkit-appearance: button;
}
output {
display: inline-block;
}
iframe {
border: 0;
}
summary {
display: list-item;
cursor: pointer;
}
progress {
vertical-align: baseline;
}
[hidden] {
display: none !important;
}
/*# sourceMappingURL=bootstrap-reboot.rtl.css.map */

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

5402
html/assets/css/bootstrap-utilities.css vendored Executable file

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

6
html/assets/css/bootstrap-utilities.min.css vendored Executable file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

5393
html/assets/css/bootstrap-utilities.rtl.css vendored Executable file

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

12057
html/assets/css/bootstrap.css vendored Executable file

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

6
html/assets/css/bootstrap.min.css vendored Executable file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

12030
html/assets/css/bootstrap.rtl.css vendored Executable file

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

6
html/assets/css/bootstrap.rtl.min.css vendored Executable file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-arrow-clockwise" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M8 3a5 5 0 1 0 4.546 2.914.5.5 0 0 1 .908-.417A6 6 0 1 1 8 2z"/>
<path d="M8 4.466V.534a.25.25 0 0 1 .41-.192l2.36 1.966c.12.1.12.284 0 .384L8.41 4.658A.25.25 0 0 1 8 4.466"/>
</svg>

After

Width:  |  Height:  |  Size: 349 B

View File

@@ -0,0 +1,32 @@
<svg xmlns="http://www.w3.org/2000/svg" style="display: none;">
<symbol id="house" viewBox="0 0 16 16">
<path d="M8 3.293l6 6V14.5a1 1 0 0 1-1 1h-3a1 1 0 0 1-1-1v-2.5a1 1 0 0 0-2 0V14.5a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V9.293l6-6zM7.293 1a1 1 0 0 1 1.414 0l7 7a1 1 0 0 1-1.414 1.414L8 2.414 1.707 8.707a1 1 0 1 1-1.414-1.414l7-7z"/>
</symbol>
<symbol id="info-circle" viewBox="0 0 16 16">
<path d="M8 1a7 7 0 1 0 0 14A7 7 0 0 0 8 1zm0 13A6 6 0 1 1 8 2a6 6 0 0 1 0 12z"/>
<path d="M8.93 6.588a.5.5 0 0 0-.427-.255H8.5a.5.5 0 0 0-.5.5v3.99a.5.5 0 0 0 .5.5h.003a.5.5 0 0 0 .491-.41l.008-.09v-3.99a.5.5 0 0 0-.072-.246z"/>
<circle cx="8" cy="4.5" r="1"/>
</symbol>
<symbol id="gear" viewBox="0 0 16 16">
<path d="M8 1.5A2.5 2.5 0 1 1 5.5 4a2.5 2.5 0 0 1 2.5-2.5zM6.5 2a1.5 1.5 0 1 0 3 0 1.5 1.5 0 0 0-3 0z"/>
</symbol>
<symbol id="telephone" viewBox="0 0 16 16">
<path d="M3.654 1.328a.678.678 0 0 1 .493-.289c.295 0 .496.155.593.454l.028.115c.017.075.25 1.098.342 1.448.09.342.03.575-.103.725l-.05.065c-.084.11-.178.22-.276.326-.97.994-1.115 1.174-.614 2.171.475.963 2.042 3.032 3.224 3.564 1.224.554 1.572.456 2.18-.193.396-.414.495-.576.9-.522.305.042 1.122.387 1.447.524.465.197.582.34.541.676-.01.098-.035.268-.055.347-.158.614-.44 1.207-.788 1.504a2.727 2.727 0 0 1-1.336.538c-.27.037-.538.073-.852.073-.883 0-1.68-.29-2.343-.655a15.235 15.235 0 0 1-3.287-2.52A15.133 15.133 0 0 1 1.433 6.47a15.578 15.578 0 0 1-.654-2.343c-.032-.308.001-.582.073-.853a2.725 2.725 0 0 1 .538-1.336C1.39 1.946 1.982 1.664 2.596 1.506a15.57 15.57 0 0 1 .348-.055c.34-.041.48-.141.676-.54.138-.325.483-1.142.522-1.447.054-.406-.108-.504-.523-.9z"/>
</symbol>
<symbol id="thermometer-half" viewBox="0 0 16 16">
<path d="M8 2a2 2 0 1 0-4 0v9.585A1.5 1.5 0 1 0 6.5 15v-3.5h1V15a1.5 1.5 0 1 0 2.5-1.415V2zm-3 .5a1 1 0 1 1 2 0v9h-2V2.5z"/>
</symbol>
<symbol id="router" viewBox="0 0 16 16">
<path d="M7.5 5A1.5 1.5 0 0 1 9 3.5h3A1.5 1.5 0 0 1 13.5 5h-6zm2-2h2a.5.5 0 0 1 0 1h-2a.5.5 0 0 1 0-1zm3 4h-2a.5.5 0 0 0 0 1h2a.5.5 0 0 0 0-1z"/>
</symbol>
<symbol id="wifi" viewBox="0 0 16 16">
<path d="M8 12.5a2.5 2.5 0 1 1-5 0 2.5 2.5 0 0 1 5 0zm1.937-1.43A5.987 5.987 0 0 1 13.5 8a.5.5 0 0 1 1 0 6.987 6.987 0 0 0-4.563 2.57.5.5 0 0 1-.75 0z"/>
</symbol>
<symbol id="journal" viewBox="0 0 16 16">
<path d="M3 0h10a1 1 0 0 1 1 1v14a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V1a1 1 0 0 1 1-1zM2 1v14a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2V1a2 2 0 0 0-2-2H4a2 2 0 0 0-2 2z"/>
</symbol>
<symbol id="person" viewBox="0 0 16 16">
<path d="M10 5a3 3 0 1 1-6 0 3 3 0 0 1 6 0zm4 8c0 1-2 2-5 2s-5-1-5-2 2-3 5-3 5 2 5 3z"/>
</symbol>
</svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-wifi" viewBox="0 0 16 16">
<path d="M15.384 6.115a.485.485 0 0 0-.047-.736A12.44 12.44 0 0 0 8 3C5.259 3 2.723 3.882.663 5.379a.485.485 0 0 0-.048.736.52.52 0 0 0 .668.05A11.45 11.45 0 0 1 8 4c2.507 0 4.827.802 6.716 2.164.205.148.49.13.668-.049"/>
<path d="M13.229 8.271a.482.482 0 0 0-.063-.745A9.46 9.46 0 0 0 8 6c-1.905 0-3.68.56-5.166 1.526a.48.48 0 0 0-.063.745.525.525 0 0 0 .652.065A8.46 8.46 0 0 1 8 7a8.46 8.46 0 0 1 4.576 1.336c.206.132.48.108.653-.065m-2.183 2.183c.226-.226.185-.605-.1-.75A6.5 6.5 0 0 0 8 9c-1.06 0-2.062.254-2.946.704-.285.145-.326.524-.1.75l.015.015c.16.16.407.19.611.09A5.5 5.5 0 0 1 8 10c.868 0 1.69.201 2.42.56.203.1.45.07.61-.091zM9.06 12.44c.196-.196.198-.52-.04-.66A2 2 0 0 0 8 11.5a2 2 0 0 0-1.02.28c-.238.14-.236.464-.04.66l.706.706a.5.5 0 0 0 .707 0l.707-.707z"/>
</svg>

After

Width:  |  Height:  |  Size: 911 B

BIN
html/assets/img/LogoNebuleAir.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

2
html/assets/jquery/jquery-3.7.1.min.js vendored Executable file

File diff suppressed because one or more lines are too long

6314
html/assets/js/bootstrap.bundle.js vendored Executable file

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

7
html/assets/js/bootstrap.bundle.min.js vendored Executable file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

4447
html/assets/js/bootstrap.esm.js vendored Executable file

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

7
html/assets/js/bootstrap.esm.min.js vendored Executable file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

4494
html/assets/js/bootstrap.js vendored Executable file

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

7
html/assets/js/bootstrap.min.js vendored Executable file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

89
html/index.html Executable file
View File

@@ -0,0 +1,89 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Responsive Sidebar Template</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-9 ms-sm-auto col-lg-10 offset-md-3 offset-lg-2 px-md-4">
<h1 class="mt-4">Votre capteur</h1>
<p>This is the main content area. You can add any content you want here.</p>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer nec odio. Praesent libero. Sed cursus ante dapibus diam.</p>
</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));
});
});
</script>
</body>
</html>

278
html/launcher.php Executable file
View File

@@ -0,0 +1,278 @@
<?php
$type=$_GET['type'];
if ($type == "RTC_time") {
$time = shell_exec("date '+%d/%m/%Y %H:%M:%S'");
echo $time;
}
if ($type == "git_pull") {
$command = 'sudo git pull';
$output = shell_exec($command);
echo $output;
}
if ($type == "sshTunnel") {
$ssh_port=$_GET['ssh_port'];
$command = 'sudo ssh -i /var/www/.ssh/id_rsa -f -N -R "'.$ssh_port.':localhost:22" -p 50221 -o StrictHostKeyChecking=no "airlab_server1@aircarto.fr"';
$output = shell_exec($command);
echo $output;
}
if ($type == "reboot") {
$command = 'sudo reboot';
$output = shell_exec($command);
}
if ($type == "npm") {
$port=$_GET['port'];
$command = '/usr/bin/python3 /var/www/nebuleair_pro_4g/npm.py ' . $port;
$output = shell_exec($command);
echo $output;
}
if ($type == "envea") {
$port=$_GET['port'];
$name=$_GET['name'];
$command = '/usr/bin/python3 /var/www/nebuleair_pro_4g/envea/read_value.py ' . $port;
$output = shell_exec($command);
echo $output;
}
if ($type == "noise") {
$command = '/var/www/nebuleair_pro_4g/sound_meter/sound_meter';
$output = shell_exec($command);
echo $output;
}
if ($type == "BME280") {
$command = '/usr/bin/python3 /var/www/nebuleair_pro_4g/BME280/read.py';
$output = shell_exec($command);
echo $output;
}
# SARA R4 COMMANDS
if ($type == "sara") {
$port=$_GET['port'];
$sara_command=$_GET['command'];
$sara_command = escapeshellcmd($sara_command);
$timeout=$_GET['timeout'];
$command = '/usr/bin/python3 /var/www/nebuleair_pro_4g/SARA/sara.py ' . $port . ' ' . $sara_command . ' ' . $timeout;
$output = shell_exec($command);
echo $output;
}
# SARA R4 COMMANDS (MQTT)
if ($type == "sara_getMQTT_config") {
$port=$_GET['port'];
$timeout=$_GET['timeout'];
$command = '/usr/bin/python3 /var/www/nebuleair_pro_4g/SARA/MQTT/get_config.py ' . $port . ' ' . $timeout;
$output = shell_exec($command);
echo $output;
}
# SARA R4 COMMANDS (MQTT)
if ($type == "sara_getMQTT_login_logout") {
$port=$_GET['port'];
$timeout=$_GET['timeout'];
$login_logout=$_GET['login_logout'];
$command = '/usr/bin/python3 /var/www/nebuleair_pro_4g/SARA/MQTT/login_logout.py ' . $port . ' ' . $login_logout . ' ' . $timeout;
$output = shell_exec($command);
echo $output;
}
# SARA R4 COMMANDS (MQTT -> publish)
if ($type == "sara_MQTT_publish") {
$port=$_GET['port'];
$timeout=$_GET['timeout'];
$message=$_GET['message'];
$command = '/usr/bin/python3 /var/www/nebuleair_pro_4g/SARA/MQTT/publish.py ' . $port . ' ' . $message . ' ' . $timeout;
$output = shell_exec($command);
echo $output;
}
#Connect to network
if ($type == "sara_connectNetwork") {
$port=$_GET['port'];
$timeout=$_GET['timeout'];
$networkID=$_GET['networkID'];
$command = '/usr/bin/python3 /var/www/nebuleair_pro_4g/SARA/sara_connectNetwork.py ' . $port . ' ' . $networkID . ' ' . $timeout;
$output = shell_exec($command);
echo $output;
}
#SET THE URL for messaging
if ($type == "sara_setURL") {
$port=$_GET['port'];
$url=$_GET['url'];
$command = '/usr/bin/python3 /var/www/nebuleair_pro_4g/SARA/sara_setURL.py ' . $port . ' ' . $url;
$output = shell_exec($command);
echo $output;
}
#SET APN
if ($type == "sara_APN") {
$port=$_GET['port'];
$timeout=$_GET['timeout'];
$APN_address=$_GET['APN_address'];
$command = '/usr/bin/python3 /var/www/moduleair_pro_4g/SARA/sara_setAPN.py ' . $port . ' ' . $APN_address . ' ' . $timeout;
$output = shell_exec($command);
echo $output;
}
#TO WRITE MESSAGE TO MEMORY
if ($type == "sara_writeMessage") {
$port=$_GET['port'];
$message=$_GET['message'];
$message = escapeshellcmd($message);
$type2=$_GET['type2'];
if ($type2 === "write") {
$command = '/usr/bin/python3 /var/www/nebuleair_pro_4g/SARA/sara_writeMessage.py ' . $port . ' ' . $message;
$output = shell_exec($command);
}
if ($type2 === "read") {
$command = '/usr/bin/python3 /var/www/nebuleair_pro_4g/SARA/sara_readMessage.py ' . $port . ' ' . $message;
$output = shell_exec($command);
}
if ($type2 === "erase") {
$command = '/usr/bin/python3 /var/www/nebuleair_pro_4g/SARA/sara_eraseMessage.py ' . $port . ' ' . $message;
$output = shell_exec($command);
}
echo $output;
}
#Send the typed message to server (for ntfy notification)
if ($type == "sara_sendMessage") {
$port=$_GET['port'];
$endpoint=$_GET['endpoint'];
$endpoint = escapeshellcmd($endpoint);
$command = '/usr/bin/python3 /var/www/nebuleair_pro_4g/SARA/sara_sendMessage.py ' . $port . ' ' . $endpoint;
$output = shell_exec($command);
echo $output;
}
if ($type == "internet") {
//eth0
$command = 'nmcli -g GENERAL.STATE device show eth0';
$eth0_connStatus = shell_exec($command);
$eth0_connStatus = str_replace("\n", "", $eth0_connStatus);
$command = 'nmcli -g IP4.ADDRESS device show eth0';
$eth0_IPAddr = shell_exec($command);
$eth0_IPAddr = str_replace("\n", "", $eth0_IPAddr);
//wlan0
$command = 'nmcli -g GENERAL.STATE device show wlan0';
$wlan0_connStatus = shell_exec($command);
$wlan0_connStatus = str_replace("\n", "", $wlan0_connStatus);
$command = 'nmcli -g IP4.ADDRESS device show wlan0';
$wlan0_IPAddr = shell_exec($command);
$wlan0_IPAddr = str_replace("\n", "", $wlan0_IPAddr);
$data= array(
"ethernet" => array(
"connection" => $eth0_connStatus,
"IP" => $eth0_IPAddr
),
"wifi" => array(
"connection" => $wlan0_connStatus,
"IP" => $wlan0_IPAddr
)
);
$json_data = json_encode($data);
echo $json_data;
}
# IMPORTANT
# c'est ici que la connexion vers le WIFI du client s'effectue.
if ($type == "wifi_connect") {
$SSID=$_GET['SSID'];
$PASS=$_GET['pass'];
echo "will try to connect to </br>";
echo "SSID: " . $SSID;
echo "</br>";
echo "Password: " . $PASS;
echo "</br>";
echo "</br>";
echo "You will be disconnected. If connection is successfull you can find the device on your local network.";
$script_path = __DIR__ . '/connexion.sh';
$log_file = __DIR__ . '/logs/app.log';
shell_exec("$script_path $SSID $PASS >> $log_file 2>&1 &");
#$output = shell_exec('sudo nmcli connection down Hotspot');
#$output2 = shell_exec('sudo nmcli device wifi connect "AirLab" password "123plouf"');
}
if ($type == "wifi_scan") {
// Set the path to your CSV file
$csvFile = '/var/www/nebuleair_pro_4g/wifi_list.csv';
// Initialize an array to hold the JSON data
$jsonData = [];
// Open the CSV file for reading
if (($handle = fopen($csvFile, 'r')) !== false) {
// Get the headers from the first row
$headers = fgetcsv($handle);
// Loop through the rest of the rows
while (($row = fgetcsv($handle)) !== false) {
// Combine headers with row data to create an associative array
$jsonData[] = array_combine($headers, $row);
}
// Close the file handle
fclose($handle);
}
// Set the content type to JSON
header('Content-Type: application/json');
// Convert the array to JSON format and output it
echo json_encode($jsonData, JSON_PRETTY_PRINT);
}
if ($type == "wifi_scan_old") {
$output = shell_exec('nmcli device wifi list ifname wlan0');
// Split the output into lines
$lines = explode("\n", trim($output));
// Initialize an array to hold the results
$wifiNetworks = [];
// Loop through each line and extract the relevant information
foreach ($lines as $index => $line) {
// Skip the header line
if ($index === 0) {
continue;
}
// Split the line by whitespace and filter empty values
$columns = preg_split('/\s+/', $line);
// If the line has enough columns, store the relevant data
if (count($columns) >= 5) {
$wifiNetworks[] = [
'SSID' => $columns[2], // Network name
'BARS' => $columns[8],
'SIGNAL' => $columns[7], // Signal strength
];
}
}
$json_data = json_encode($wifiNetworks);
echo $json_data;
}

168
html/logs.html Executable file
View File

@@ -0,0 +1,168 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Responsive Sidebar Template</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">Le journal</h1>
<p>Le journal des logs permet de savoir si les processus du capteur se déroulent correctement.</p>
<div class="row">
<!-- card 1 -->
<div class="col-lg-6 col-12">
<div class="card" style="height: 80vh;">
<div class="card-header">
Loop logs
</div>
<div class="card-body overflow-auto" id="card_loop_content">
</div>
</div>
</div>
<!-- card 2 -->
<div class="col-lg-6 col-12">
<div class="card" style="height: 80vh;">
<div class="card-header">
Boot logs
</div>
<div class="card-body overflow-auto" id="card_boot_content">
</div>
</div>
</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));
});
const loop_card_content = document.getElementById('card_loop_content');
const boot_card_content = document.getElementById('card_boot_content');
fetch('../logs/loop.log')
.then((response) => {
if (!response.ok) {
throw new Error('Failed to fetch the log file.');
}
return response.text();
})
.then((data) => {
const lines = data.split('\n');
// Format log content
const formattedLog = lines
.map((line) => line.trim()) // Remove extra whitespace
.filter((line) => line) // Remove empty lines
.join('<br>'); // Join formatted lines with line breaks
loop_card_content.innerHTML = `<pre style="white-space: pre-wrap; word-wrap: break-word; margin: 0;">${formattedLog}</pre>`;
loop_card_content.scrollTop = loop_card_content.scrollHeight; // Scroll to the bottom
})
.catch((error) => {
console.error(error);
loop_card_content.textContent = 'Error loading log file.';
});
fetch('../logs/app.log')
.then((response) => {
if (!response.ok) {
throw new Error('Failed to fetch the log file.');
}
return response.text();
})
.then((data) => {
const lines = data.split('\n');
// Format log content
const formattedLog = lines
.map((line) => line.trim()) // Remove extra whitespace
.filter((line) => line) // Remove empty lines
.join('<br>'); // Join formatted lines with line breaks
boot_card_content.innerHTML = `<pre style="white-space: pre-wrap; word-wrap: break-word; margin: 0;">${formattedLog}</pre>`;
boot_card_content.scrollTop = loop_card_content.scrollHeight; // Scroll to the bottom
})
.catch((error) => {
console.error(error);
boot_card_content.textContent = 'Error loading log file.';
});
});
</script>
</body>
</html>

628
html/saraR4.html Executable file
View File

@@ -0,0 +1,628 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Responsive Sidebar Template</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">Modem 4G</h1>
<p>Votre capteur est équipé d'un modem 4G et d'une carte SIM afin d'envoyer les mesures sur internet.</p>
<h3>
Status
<span id="modem-status" class="badge">Loading...</span>
</h3>
<div class="row mb-3">
<div class="col-sm-3">
<div class="card">
<div class="card-body">
<p class="card-text">General information. </p>
<button class="btn btn-primary" onclick="getData_saraR4('ttyAMA2', 'ATI', 2)">Get Data</button>
<div id="loading_ttyAMA2_ATI" class="spinner-border spinner-border-sm" style="display: none;" role="status"></div>
<div id="response_ttyAMA2_ATI"></div>
</div>
</div>
</div>
<div class="col-sm-3">
<div class="card">
<div class="card-body">
<p class="card-text">SIM card information.</p>
<button class="btn btn-primary" onclick="getData_saraR4('ttyAMA2', 'AT+CCID?', 2)">Get Data</button>
<div id="loading_ttyAMA2_AT_CCID_" class="spinner-border spinner-border-sm" style="display: none;" role="status"></div>
<div id="response_ttyAMA2_AT_CCID_"></div>
</div>
</div>
</div>
<div class="col-sm-3">
<div class="card">
<div class="card-body">
<p class="card-text">Actual Network connection</p>
<button class="btn btn-primary" onclick="getData_saraR4('ttyAMA2', 'AT+COPS?', 2)">Get Data</button>
<div id="loading_ttyAMA2_AT_COPS_" class="spinner-border spinner-border-sm" style="display: none;" role="status"></div>
<div id="response_ttyAMA2_AT_COPS_"></div>
</table>
</div>
</div>
</div>
<div class="col-sm-3">
<div class="card">
<div class="card-body">
<p class="card-text">Signal strength </p>
<button class="btn btn-primary" onclick="getData_saraR4('ttyAMA2', 'AT+CSQ', 2)">Get Data</button>
<div id="loading_ttyAMA2_AT_CSQ" class="spinner-border spinner-border-sm" style="display: none;" role="status"></div>
<div id="response_ttyAMA2_AT_CSQ"></div>
</table>
</div>
</div>
</div>
</div>
<h3>Connexion 4G Network</h3>
<div class="row mb-3">
<div class="col-sm-6">
<div class="card text-dark bg-light">
<div class="card-body">
<p class="card-text">Network scan. Attention: 2 min scan.</p>
<button class="btn btn-primary" onclick="getData_saraR4('ttyAMA2', 'AT+COPS=?', 120)">Scan</button>
<div id="loading_ttyAMA2_AT_COPS__" class="spinner-border spinner-border-sm" style="display: none;" role="status"></div>
<div id="response_ttyAMA2_AT_COPS__"></div>
<div id="table-network"></div>
</div>
</div>
</div>
<div class="col-sm-3">
<div class="card text-dark bg-light">
<div class="card-body">
<p class="card-text">Network connexion.</p>
<div class="input-group input-group-sm mb-3">
<span class="input-group-text" id="inputGroup-sizing-sm">Numeric Operator</span>
<input type="text" id="messageInput_network" class="form-control" aria-label="Sizing example input" aria-describedby="inputGroup-sizing-sm">
</div>
<button class="btn btn-primary" onclick="connectNetwork_saraR4('ttyAMA2', document.getElementById('messageInput_network').value, 60)">Connect</button>
<div id="loading_ttyAMA2_AT_COPS_Connect" class="spinner-border spinner-border-sm" style="display: none;" role="status"></div>
<div id="response_ttyAMA2_AT_COPS_Connect"></div>
</div>
</div>
</div>
<div class="col-sm-3">
<div class="card text-dark bg-light">
<div class="card-body">
<p class="card-text">APN</p>
<div class="input-group input-group-sm mb-3">
<span class="input-group-text" id="inputGroup-sizing-sm">Address</span>
<input type="text" id="messageInput_APN" class="form-control" aria-label="Sizing example input" aria-describedby="inputGroup-sizing-sm">
</div>
<button class="btn btn-primary" onclick="connectAPN_saraR4('ttyAMA2', document.getElementById('messageInput_APN').value, 5)">Connect</button>
<button class="btn btn-secondary" onclick="getData_saraR4('ttyAMA2','AT+CGDCONT?', 5)">Get APN</button>
<div id="loading_ttyAMA2_APN" class="spinner-border spinner-border-sm" style="display: none;" role="status"></div>
<div id="response_ttyAMA2_APN"></div>
<div id="loading_ttyAMA2_AT_CGDCONT_" class="spinner-border spinner-border-sm" style="display: none;" role="status"></div>
<div id="response_ttyAMA2_AT_CGDCONT_"></div>
</div>
</div>
</div>
</div>
<h3>MQTT</h3>
<div class="row mb-3">
<!-- Get CONFIG -->
<div class="col-sm-4">
<div class="card">
<div class="card-body">
<p class="card-text">Get config.</p>
<button class="btn btn-primary" onclick="mqtt_getConfig_saraR4('ttyAMA2', 2)">Get Data</button>
<div id="loading_mqtt_getConfig" class="spinner-border spinner-border-sm" style="display: none;" role="status"></div>
<div id="response_mqtt_getConfig"></div>
</div>
</div>
</div>
<!-- MQTT LOGIN -->
<div class="col-sm-4">
<div class="card">
<div class="card-body">
<p class="card-text">Login / logout</p>
<button class="btn btn-success" onclick="mqtt_login_logout('ttyAMA2', 1, 6)">Login </button>
<button class="btn btn-danger" onclick="mqtt_login_logout('ttyAMA2', 0, 6)">Logout </button>
<div id="loading_mqtt_login_logout" class="spinner-border spinner-border-sm" style="display: none;" role="status"></div>
<div id="response_mqtt_login_logout"></div>
</div>
</div>
</div>
<!-- Send MESSAGE -->
<div class="col-sm-4">
<div class="card">
<div class="card-body">
<p class="card-text">Send message (MQTT publish) .</p>
<div class="input-group input-group-sm mb-3">
<span class="input-group-text" id="inputGroup-sizing-sm">Text</span>
<input type="text" id="MQTTmessageInput" class="form-control" aria-label="Sizing example input" aria-describedby="inputGroup-sizing-sm">
</div>
<button class="btn btn-primary" onclick="mqtt_publish('ttyAMA2', document.getElementById('MQTTmessageInput').value, 2)">Send Message</button>
<div id="loading_mqtt_publish" class="spinner-border spinner-border-sm" style="display: none;" role="status"></div>
<div id="response_mqtt_publish"></div>
</div>
</div>
</div>
</div>
<h3>Send message (test)</h3>
<div class="row mb-3">
<!-- SET URL -->
<div class="col-sm-4">
<div class="card">
<div class="card-body">
<p class="card-text">Set url (HTTP).</p>
<button class="btn btn-primary" onclick="setURL_saraR4('ttyAMA2', 'data.nebuleair.fr')">Set URL</button>
<div id="loading_ttyAMA2_setURL" class="spinner-border spinner-border-sm" style="display: none;" role="status"></div>
<div id="response_ttyAMA2_setURL"></div>
</div>
</div>
</div>
<!-- WRITE MESSAGE to memory -->
<div class="col-sm-4">
<div class="card">
<div class="card-body">
<p class="card-text">Write message (local storage).</p>
<div class="input-group input-group-sm mb-3">
<span class="input-group-text" id="inputGroup-sizing-sm">Text</span>
<input type="text" id="messageInput" class="form-control" aria-label="Sizing example input" aria-describedby="inputGroup-sizing-sm">
</div>
<button class="btn btn-primary" onclick="writeMessage_saraR4('ttyAMA2', document.getElementById('messageInput').value , 'write')">Write </button>
<button class="btn btn-warning" onclick="writeMessage_saraR4('ttyAMA2', 'Hello', 'read')">Read </button>
<button class="btn btn-danger" onclick="writeMessage_saraR4('ttyAMA2', 'Hello', 'erase')">Empty </button>
<div id="loading_ttyAMA2_message_write" class="spinner-border spinner-border-sm" style="display: none;" role="status"></div>
<div id="response_ttyAMA2_message_write"></div>
</div>
</div>
</div>
<!-- Send MESSAGE -->
<div class="col-sm-4">
<div class="card">
<div class="card-body">
<p class="card-text">Send message .</p>
<button class="btn btn-primary" onclick="sendMessage_saraR4('ttyAMA2', '/pro_4G/notif_message.php')">Send Message</button>
<div id="loading_ttyAMA2_message_send" class="spinner-border spinner-border-sm" style="display: none;" role="status"></div>
<div id="response_ttyAMA2_message_send"></div>
</div>
</div>
</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));
});
});
function getData_saraR4(port, command, timeout){
console.log("Data from SaraR4");
console.log("Port: " + port );
console.log("Command: " + command );
console.log("Timeout: " + timeout );
const safeCommand = command.replace(/[?+=]/g, "_");
console.log(safeCommand);
$("#loading_"+port+"_"+safeCommand).show();
$.ajax({
url: 'launcher.php?type=sara&port='+port+'&command='+encodeURIComponent(command)+'&timeout='+timeout,
//dataType: 'json', // Specify that you expect a JSON response
method: 'GET', // Use GET or POST depending on your needs
success: function(response) {
//log response
console.log(response);
//hide spinning wheel
$("#loading_"+port+"_"+safeCommand).hide();
// si on fait le scan de network on veut une liste des réseaux
if (command == "AT+COPS=?") {
// Extract data within parentheses
const matches = response.match(/\(.*?\)/g); // Matches all `(...)` sections
const container = document.getElementById('table-network'); // Bootstrap container
// Check if matches exist
if (matches) {
const table = document.createElement('table');
table.className = 'table table-striped'; // Add Bootstrap table styling
const thead = document.createElement('thead');
const tbody = document.createElement('tbody');
// Table header (you can customize this based on your data)
const headerRow = document.createElement('tr');
const header1 = document.createElement('th');
header1.textContent = 'Status';
const header2 = document.createElement('th');
header2.textContent = 'Long oper';
const header3 = document.createElement('th');
header3.textContent = 'Short opeer';
const header4 = document.createElement('th');
header4.textContent = 'Numeric oper';
const header5 = document.createElement('th');
header5.textContent = 'AcT';
headerRow.appendChild(header1);
headerRow.appendChild(header2);
headerRow.appendChild(header3);
headerRow.appendChild(header4);
headerRow.appendChild(header5);
thead.appendChild(headerRow);
table.appendChild(thead);
// Loop through each match and create a row in the table
matches.forEach((item) => {
// Skip empty sections
if (item === "()") return;
const row = document.createElement('tr');
const values = item.slice(1, -1).split(','); // Remove parentheses and split by commas
// Add table cells (td) for each value
values.forEach((value) => {
const cell = document.createElement('td');
cell.textContent = value.trim(); // Remove extra spaces
row.appendChild(cell);
});
tbody.appendChild(row);
});
// Add tbody to table and append the table to the container
table.appendChild(tbody);
container.appendChild(table);
} else {
console.error('No valid data found in response.');
}
} else{
// si c'est une commande AT normale
// Replace newline characters with <br> tags
const formattedResponse = response.replace(/\n/g, "<br>");
$("#response_"+port+"_"+safeCommand).html(formattedResponse);
}
},
error: function(xhr, status, error) {
console.error('AJAX request failed:', status, error);
}
});
}
function connectNetwork_saraR4(port, networkID, timeout){
console.log(" Connect to network (port "+port+" and network id "+networkID+"):");
$("#loading_"+port+"_AT_COPS_Connect").show();
$.ajax({
url: 'launcher.php?type=sara_connectNetwork&port='+port+'&networkID='+encodeURIComponent(networkID)+'&timeout='+timeout,
//dataType: 'json', // Specify that you expect a JSON response
method: 'GET', // Use GET or POST depending on your needs
success: function(response) {
console.log(response);
$("#loading_"+port+"_AT_COPS_Connect").hide();
// Replace newline characters with <br> tags
const formattedResponse = response.replace(/\n/g, "<br>");
$("#response_"+port+"_AT_COPS_Connect").html(formattedResponse);
},
error: function(xhr, status, error) {
console.error('AJAX request failed:', status, error);
}
});
}
function mqtt_getConfig_saraR4(port, timeout){
console.log("GET MQTT config (port "+port+"):");
$("#loading_mqtt_getConfig").show();
$.ajax({
url: 'launcher.php?type=sara_getMQTT_config&port='+port+'&timeout='+timeout,
//dataType: 'json', // Specify that you expect a JSON response
method: 'GET', // Use GET or POST depending on your needs
success: function(response) {
console.log(response);
$("#loading_mqtt_getConfig").hide();
// Replace newline characters with <br> tags
const formattedResponse = response.replace(/\n/g, "<br>");
$("#response_mqtt_getConfig").html(formattedResponse);
},
error: function(xhr, status, error) {
console.error('AJAX request failed:', status, error);
}
});
}
function mqtt_login_logout(port, login_logout, timeout){
console.log("GET MQTT login / logout (port "+port+"):");
$("#loading_mqtt_login_logout").show();
$.ajax({
url: 'launcher.php?type=sara_getMQTT_login_logout&port='+port+'&login_logout='+login_logout+'&timeout='+timeout,
//dataType: 'json', // Specify that you expect a JSON response
method: 'GET', // Use GET or POST depending on your needs
success: function(response) {
console.log(response);
$("#loading_mqtt_login_logout").hide();
const formattedResponse = response.replace(/\n/g, "<br>");
$("#response_mqtt_login_logout").html(formattedResponse);
const regex = /^\+UMQTTC:\s*(\d+),(\d+)/m; // Match "+UMQTTC:", followed by two numbers separated by a comma
const match = response.match(regex);
if (match) {
const firstNumber = match[1]; // The first number after ":"
const secondNumber = match[2]; // The second number after the ","
if (firstNumber == 0) {
console.log("MQTT LOGOUT:");
$("#response_mqtt_login_logout").append("<p>logout</p>");
}
if (firstNumber == 1) {
console.log("MQTT LOGIN:");
$("#response_mqtt_login_logout").append("<p>login</p>");
}
if (secondNumber == 0) {
console.log("ERROR");
$("#response_mqtt_login_logout").append("<p>error</p>");
}
if (secondNumber == 1) {
console.log("SUCCESS");
$("#response_mqtt_login_logout").append("<p>success</p>");
}
} else {
console.log("No matching line found");
}
},
error: function(xhr, status, error) {
console.error('AJAX request failed:', status, error);
}
});
}
function mqtt_publish(port, message, timeout){
console.log(" MQTT publish (port "+port+"):");
$("#loading_mqtt_publish").show();
$.ajax({
url: 'launcher.php?type=sara_MQTT_publish&port='+port+'&timeout='+timeout+'&message='+message,
//dataType: 'json', // Specify that you expect a JSON response
method: 'GET', // Use GET or POST depending on your needs
success: function(response) {
console.log(response);
$("#loading_mqtt_publish").hide();
// Replace newline characters with <br> tags
const formattedResponse = response.replace(/\n/g, "<br>");
$("#response_mqtt_publish").html(formattedResponse);
},
error: function(xhr, status, error) {
console.error('AJAX request failed:', status, error);
}
});
}
function setURL_saraR4(port, url){
console.log("Set URL for HTTP (port "+port+" and URL "+url+"):");
$("#loading_"+port+"_setURL").show();
$.ajax({
url: 'launcher.php?type=sara_setURL&port='+port+'&url='+encodeURIComponent(url),
//dataType: 'json', // Specify that you expect a JSON response
method: 'GET', // Use GET or POST depending on your needs
success: function(response) {
console.log(response);
$("#loading_"+port+"_setURL").hide();
// Replace newline characters with <br> tags
const formattedResponse = response.replace(/\n/g, "<br>");
$("#response_"+port+"_setURL").html(formattedResponse);
},
error: function(xhr, status, error) {
console.error('AJAX request failed:', status, error);
}
});
}
function writeMessage_saraR4(port, message, type){
console.log(type +" message to SARA R4 memory (port "+port+" and message "+message+"):");
$("#loading_"+port+"_message_write").show();
$.ajax({
url: 'launcher.php?type=sara_writeMessage&port='+port+'&message='+encodeURIComponent(message)+'&type2='+type,
//dataType: 'json', // Specify that you expect a JSON response
method: 'GET', // Use GET or POST depending on your needs
success: function(response) {
console.log(response);
$("#loading_"+port+"_message_write").hide();
// Replace newline characters with <br> tags
const formattedResponse = response.replace(/\n/g, "<br>");
$("#response_"+port+"_message_write").html(formattedResponse);
},
error: function(xhr, status, error) {
console.error('AJAX request failed:', status, error);
}
});
}
function sendMessage_saraR4(port, endpoint){
console.log("Send message from SaraR4 (port "+port+" and endpoint "+endpoint+"):");
$("#loading_"+port+"_message_send").show();
$.ajax({
url: 'launcher.php?type=sara_sendMessage&port='+port+'&endpoint='+encodeURIComponent(endpoint),
//dataType: 'json', // Specify that you expect a JSON response
method: 'GET', // Use GET or POST depending on your needs
success: function(response) {
console.log(response);
$("#loading_"+port+"_message_send").hide();
// Replace newline characters with <br> tags
const formattedResponse = response.replace(/\n/g, "<br>");
$("#response_"+port+"_message_send").html(formattedResponse);
},
error: function(xhr, status, error) {
console.error('AJAX request failed:', status, error);
}
});
}
function connectAPN_saraR4(port, APN_address, timeout){
console.log(" Set APN (port "+port+" and adress "+APN_address+"):");
$("#loading_"+port+"_APN").show();
$.ajax({
url: 'launcher.php?type=sara_APN&port='+port+'&APN_address='+encodeURIComponent(APN_address)+'&timeout='+timeout,
//dataType: 'json', // Specify that you expect a JSON response
method: 'GET', // Use GET or POST depending on your needs
success: function(response) {
console.log(response);
$("#loading_"+port+"_APN").hide();
// Replace newline characters with <br> tags
const formattedResponse = response.replace(/\n/g, "<br>");
$("#response_"+port+"_APN").html(formattedResponse);
},
error: function(xhr, status, error) {
console.error('AJAX request failed:', status, error);
}
});
}
window.onload = function() {
fetch('../config.json') // Replace 'deviceID.txt' with 'config.json'
.then(response => response.json()) // Parse response as JSON
.then(data => {
//get device ID
const deviceID = data.deviceID.trim().toUpperCase();
document.getElementById('pageTitle_plus_ID').innerText = 'token: ' + deviceID;
//get 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));
}
</script>
</body>
</html>

370
html/sensors.html Executable file
View File

@@ -0,0 +1,370 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Responsive Sidebar Template</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">Les sondes de mesure</h1>
<p>Votre capteur NebuleAir est équipé de une ou plusieurs sondes qui permettent de mesurer certaines variables environnementales. La mesure
est automatique mais vous pouvez ici vous assurer de leur bon fonctionnement.
</p>
<div class="row mb-3" id="card-container"></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));
});
});
function getNPM_values(port){
console.log("Data from NPM (port "+port+"):");
$("#loading_"+port).show();
$.ajax({
url: 'launcher.php?type=npm&port='+port,
dataType: 'json', // Specify that you expect a JSON response
method: 'GET', // Use GET or POST depending on your needs
success: function(response) {
console.log(response);
const tableBody = document.getElementById("data-table-body_"+port);
tableBody.innerHTML = "";
$("#loading_"+port).hide();
// Create an array of the desired keys
const keysToShow = ["PM1", "PM25", "PM10"];
// Add only the specified elements to the table
keysToShow.forEach(key => {
if (response[key] !== undefined) { // Check if the key exists in the response
const value = response[key];
$("#data-table-body_"+port).append(`
<tr>
<td>${key}</td>
<td>${value} µg/m³</td>
</tr>
`);
}
});
},
error: function(xhr, status, error) {
console.error('AJAX request failed:', status, error);
}
});
}
function getENVEA_values(port, name){
console.log("Data from Envea "+ name+" (port "+port+"):");
$("#loading_envea"+name).show();
$.ajax({
url: 'launcher.php?type=envea&port='+port+'&name='+name,
dataType: 'json', // Specify that you expect a JSON response
method: 'GET', // Use GET or POST depending on your needs
success: function(response) {
console.log(response);
const tableBody = document.getElementById("data-table-body_envea"+name);
tableBody.innerHTML = "";
$("#loading_envea"+name).hide();
// Create an array of the desired keys
// Create an array of the desired keys
const keysToShow = [name];
// Add only the specified elements to the table
keysToShow.forEach(key => {
if (response !== undefined) { // Check if the key exists in the response
const value = response;
$("#data-table-body_envea"+name).append(`
<tr>
<td>${key}</td>
<td>${value} ppb</td>
</tr>
`);
}
});
},
error: function(xhr, status, error) {
console.error('AJAX request failed:', status, error);
}
});
}
function getNoise_values(){
console.log("Data from I2C Noise Sensor:");
$("#loading_noise").show();
$.ajax({
url: 'launcher.php?type=noise',
method: 'GET', // Use GET or POST depending on your needs
success: function(response) {
console.log(response);
const tableBody = document.getElementById("data-table-body_noise");
tableBody.innerHTML = "";
$("#loading_noise").hide();
// Create an array of the desired keys
const keysToShow = ["Noise"];
// 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_noise").append(`
<tr>
<td>${key}</td>
<td>${value} DB</td>
</tr>
`);
}
});
},
error: function(xhr, status, error) {
console.error('AJAX request failed:', status, error);
}
});
}
function getBME280_values(){
console.log("Data from I2C BME280:");
$("#loading_BME280").show();
$.ajax({
url: 'launcher.php?type=BME280',
method: 'GET', // Use GET or POST depending on your needs
success: function(response) {
console.log(response);
const tableBody = document.getElementById("data-table-body_BME280");
tableBody.innerHTML = "";
$("#loading_BME280").hide();
// Parse the JSON response
const data = JSON.parse(response);
const keysToShow = ["temp", "hum", "press"];
// Add only the specified elements to the table
keysToShow.forEach(key => {
if (response !== undefined) { // Check if the key exists in the response
const value = data[key];
const unit = key === "temp" ? "°C"
: key === "hum" ? "%"
: key === "press" ? "hPa"
: ""; // Add appropriate units
$("#data-table-body_BME280").append(`
<tr>
<td>${key.charAt(0).toUpperCase() + key.slice(1)}</td>
<td>${value} ${unit}</td>
</tr>
`);
}
});
},
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 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);
}
});
const container = document.getElementById('card-container'); // Conteneur des cartes
//creates NPM cards
const NPM_ports = data.NextPM_ports; // Récupère les ports
NPM_ports.forEach((port, index) => {
const cardHTML = `
<div class="col-sm-3">
<div class="card">
<div class="card-header">
Port UART ${port.replace('ttyAMA', '')}
</div>
<div class="card-body">
<h5 class="card-title">NextPM ${String.fromCharCode(65 + index)}</h5>
<p class="card-text">Capteur particules fines.</p>
<button class="btn btn-primary" onclick="getNPM_values('${port}')">Get Data</button>
<div id="loading_${port}" class="spinner-border spinner-border-sm" style="display: none;" role="status"></div>
<table class="table table-striped-columns">
<tbody id="data-table-body_${port}"></tbody>
</table>
</div>
</div>
</div>`;
container.innerHTML += cardHTML; // Ajouter la carte au conteneur
});
//creates ENVEA cards
const ENVEA_sensors = data.envea_sondes.filter(sonde => sonde.connected); // Filter only connected sondes
ENVEA_sensors.forEach((sensor, index) => {
const port = sensor.port; // Port from the sensor object
const name = sensor.name; // Port from the sensor object
const 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
});
//creates i2c BME280 card
if (data.i2c_BME) {
const i2C_BME_HTML = `
<div class="col-sm-3">
<div class="card">
<div class="card-header">
Port I2C
</div>
<div class="card-body">
<h5 class="card-title">BME280 Temp/Hum sensor</h5>
<p class="card-text">Capteur température et humidité sur le port I2C.</p>
<button class="btn btn-primary mb-1" onclick="getBME280_values()">Get Data</button>
<br>
<div id="loading_BME280" class="spinner-border spinner-border-sm" style="display: none;" role="status"></div>
<table class="table table-striped-columns">
<tbody id="data-table-body_BME280"></tbody>
</table>
</div>
</div>
</div>`;
container.innerHTML += i2C_BME_HTML; // Add the I2C card if condition is met
}
//creates i2c sound card
if (data.i2C_sound) {
const i2C_HTML = `
<div class="col-sm-3">
<div class="card">
<div class="card-header">
Port I2C
</div>
<div class="card-body">
<h5 class="card-title">Decibel Meter</h5>
<p class="card-text">Capteur bruit sur le port I2C.</p>
<button class="btn btn-primary mb-1" onclick="getNoise_values()">Get Data</button>
<br>
<button class="btn btn-success" onclick="startNoise()">Start recording</button>
<button class="btn btn-danger" onclick="stopNoise()">Stop recording</button>
<div id="loading_noise" class="spinner-border spinner-border-sm" style="display: none;" role="status"></div>
<table class="table table-striped-columns">
<tbody id="data-table-body_noise"></tbody>
</table>
</div>
</div>
</div>`;
container.innerHTML += i2C_HTML; // Add the I2C card if condition is met
}
})
.catch(error => console.error('Error loading config.json:', error));
}
</script>
</body>
</html>

44
html/sidebar.html Executable file
View File

@@ -0,0 +1,44 @@
<!-- Sidebar -->
<nav class="nav flex-column">
<a class="nav-link text-white mt-4" href="index.html">
<svg class="bi me-2" width="16" height="16" fill="currentColor" aria-hidden="true">
<use xlink:href="assets/icons/bootstrap-icons.svg#house"></use>
</svg>
Home
</a>
<a class="nav-link text-white" href="sensors.html">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-thermometer-sun" viewBox="0 0 16 16">
<path d="M5 12.5a1.5 1.5 0 1 1-2-1.415V2.5a.5.5 0 0 1 1 0v8.585A1.5 1.5 0 0 1 5 12.5"/>
<path d="M1 2.5a2.5 2.5 0 0 1 5 0v7.55a3.5 3.5 0 1 1-5 0zM3.5 1A1.5 1.5 0 0 0 2 2.5v7.987l-.167.15a2.5 2.5 0 1 0 3.333 0L5 10.486V2.5A1.5 1.5 0 0 0 3.5 1m5 1a.5.5 0 0 1 .5.5v1a.5.5 0 0 1-1 0v-1a.5.5 0 0 1 .5-.5m4.243 1.757a.5.5 0 0 1 0 .707l-.707.708a.5.5 0 1 1-.708-.708l.708-.707a.5.5 0 0 1 .707 0M8 5.5a.5.5 0 0 1 .5-.5 3 3 0 1 1 0 6 .5.5 0 0 1 0-1 2 2 0 0 0 0-4 .5.5 0 0 1-.5-.5M12.5 8a.5.5 0 0 1 .5-.5h1a.5.5 0 1 1 0 1h-1a.5.5 0 0 1-.5-.5m-1.172 2.828a.5.5 0 0 1 .708 0l.707.708a.5.5 0 0 1-.707.707l-.708-.707a.5.5 0 0 1 0-.708M8.5 12a.5.5 0 0 1 .5.5v1a.5.5 0 0 1-1 0v-1a.5.5 0 0 1 .5-.5"/>
</svg>
Capteurs
</a>
<a class="nav-link text-white" href="saraR4.html">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-reception-4" viewBox="0 0 16 16">
<path d="M0 11.5a.5.5 0 0 1 .5-.5h2a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1-.5-.5zm4-3a.5.5 0 0 1 .5-.5h2a.5.5 0 0 1 .5.5v5a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1-.5-.5zm4-3a.5.5 0 0 1 .5-.5h2a.5.5 0 0 1 .5.5v8a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1-.5-.5zm4-3a.5.5 0 0 1 .5-.5h2a.5.5 0 0 1 .5.5v11a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1-.5-.5z"/>
</svg>
Modem 4G
</a>
<a class="nav-link text-white" href="wifi.html">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-wifi" viewBox="0 0 16 16">
<path d="M15.384 6.115a.485.485 0 0 0-.047-.736A12.44 12.44 0 0 0 8 3C5.259 3 2.723 3.882.663 5.379a.485.485 0 0 0-.048.736.52.52 0 0 0 .668.05A11.45 11.45 0 0 1 8 4c2.507 0 4.827.802 6.716 2.164.205.148.49.13.668-.049"/>
<path d="M13.229 8.271a.482.482 0 0 0-.063-.745A9.46 9.46 0 0 0 8 6c-1.905 0-3.68.56-5.166 1.526a.48.48 0 0 0-.063.745.525.525 0 0 0 .652.065A8.46 8.46 0 0 1 8 7a8.46 8.46 0 0 1 4.576 1.336c.206.132.48.108.653-.065m-2.183 2.183c.226-.226.185-.605-.1-.75A6.5 6.5 0 0 0 8 9c-1.06 0-2.062.254-2.946.704-.285.145-.326.524-.1.75l.015.015c.16.16.407.19.611.09A5.5 5.5 0 0 1 8 10c.868 0 1.69.201 2.42.56.203.1.45.07.61-.091zM9.06 12.44c.196-.196.198-.52-.04-.66A2 2 0 0 0 8 11.5a2 2 0 0 0-1.02.28c-.238.14-.236.464-.04.66l.706.706a.5.5 0 0 0 .707 0l.707-.707z"/>
</svg>
WIFI
</a>
<a class="nav-link text-white" href="logs.html">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-journal-code" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M8.646 5.646a.5.5 0 0 1 .708 0l2 2a.5.5 0 0 1 0 .708l-2 2a.5.5 0 0 1-.708-.708L10.293 8 8.646 6.354a.5.5 0 0 1 0-.708m-1.292 0a.5.5 0 0 0-.708 0l-2 2a.5.5 0 0 0 0 .708l2 2a.5.5 0 0 0 .708-.708L5.707 8l1.647-1.646a.5.5 0 0 0 0-.708"/>
<path d="M3 0h10a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2v-1h1v1a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V2a1 1 0 0 0-1-1H3a1 1 0 0 0-1 1v1H1V2a2 2 0 0 1 2-2"/>
<path d="M1 5v-.5a.5.5 0 0 1 1 0V5h.5a.5.5 0 0 1 0 1h-2a.5.5 0 0 1 0-1zm0 3v-.5a.5.5 0 0 1 1 0V8h.5a.5.5 0 0 1 0 1h-2a.5.5 0 0 1 0-1zm0 3v-.5a.5.5 0 0 1 1 0v.5h.5a.5.5 0 0 1 0 1h-2a.5.5 0 0 1 0-1z"/>
</svg>
Logs
</a>
<a class="nav-link text-white" href="admin.html">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-tools" viewBox="0 0 16 16">
<path d="M1 0 0 1l2.2 3.081a1 1 0 0 0 .815.419h.07a1 1 0 0 1 .708.293l2.675 2.675-2.617 2.654A3.003 3.003 0 0 0 0 13a3 3 0 1 0 5.878-.851l2.654-2.617.968.968-.305.914a1 1 0 0 0 .242 1.023l3.27 3.27a.997.997 0 0 0 1.414 0l1.586-1.586a.997.997 0 0 0 0-1.414l-3.27-3.27a1 1 0 0 0-1.023-.242L10.5 9.5l-.96-.96 2.68-2.643A3.005 3.005 0 0 0 16 3q0-.405-.102-.777l-2.14 2.141L12 4l-.364-1.757L13.777.102a3 3 0 0 0-3.675 3.68L7.462 6.46 4.793 3.793a1 1 0 0 1-.293-.707v-.071a1 1 0 0 0-.419-.814zm9.646 10.646a.5.5 0 0 1 .708 0l2.914 2.915a.5.5 0 0 1-.707.707l-2.915-2.914a.5.5 0 0 1 0-.708M3 11l.471.242.529.026.287.445.445.287.026.529L5 13l-.242.471-.026.529-.445.287-.287.445-.529.026L3 15l-.471-.242L2 14.732l-.287-.445L1.268 14l-.026-.529L1 13l.242-.471.026-.529.445-.287.287-.445.529-.026z"/>
</svg>
Admin
</a>
</nav>

14
html/topbar.html Executable file
View File

@@ -0,0 +1,14 @@
<!-- Topbar -->
<nav class="navbar navbar-dark fixed-top" style="background-color: #8d8d8f;" id="topbar">
<div class="container-fluid">
<a class="navbar-brand" href="#">
<img src="assets/img/LogoNebuleAir.png" alt="Logo" height="30" class="d-inline-block align-text-top">
</a>
<div class="d-flex">
<button class="btn btn-outline-light d-md-none me-2" type="button" data-bs-toggle="offcanvas" data-bs-target="#sidebarOffcanvas" aria-controls="sidebarOffcanvas" aria-label="Toggle Sidebar"></button>
<!-- Texte centré au milieu -->
<span id="pageTitle_plus_ID">Texte au milieu</span>
<button type="button" class="btn btn-outline-light" id="RTC_time">loading...</button>
</div>
</div>
</nav>

88
html/wifi.html Executable file
View File

@@ -0,0 +1,88 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Responsive Sidebar Template</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-9 ms-sm-auto col-lg-10 offset-md-3 offset-lg-2 px-md-4">
<h1 class="mt-4">Connection WIFI</h1>
<p>La connexion WIFI n'est pas obligatoire mais elle vous permet d'effectuer des mises à jour et d'activer le contrôle à distance.</p>
</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));
});
});
</script>
</body>
</html>

1235
index.html Executable file

File diff suppressed because it is too large Load Diff

37
install_software.yaml Executable file
View File

@@ -0,0 +1,37 @@
- name: Installer les logiciels sur le CM4
hosts: nebuleair_pro
become: yes # Ensure tasks run with sudo
tasks:
- name: Mettre à jour les paquets
apt:
update_cache: yes
upgrade: dist
- name: Install necessary packages
apt:
name:
- git
- gh
- apache2
- php
- python3
- python3-pip
- jq
- autossh
- i2c-tools
- python3-smbus
state: present # Ensure the package is installed
update_cache: yes
- name: Install required Python packages
pip:
name:
- pyserial
- requests
- RPi.GPIO
- adafruit-circuitpython-bme280
state: present
executable: pip3 #Specifies which version of pip to use (in this case, pip3).
extra_args: --break-system-packages

46
installation.sh Executable file
View File

@@ -0,0 +1,46 @@
#!/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
# Authenticate GitHub CLI
echo "Authenticating GitHub CLI..."
sudo gh auth login
# Set up Git configuration
echo "Configuring Git..."
git config --global user.email "paulvuarambon@gmail.com"
git config --global user.name "PaulVua"
# Clone the repository
echo "Cloning the NebuleAir Pro 4G repository..."
sudo gh repo clone aircarto/nebuleair_pro_4g /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!"

262
launcher.php Executable file
View File

@@ -0,0 +1,262 @@
<?php
$type=$_GET['type'];
if ($type == "RTC_time") {
$time = shell_exec("date '+%d/%m/%Y %H:%M:%S'");
echo $time;
}
if ($type == "git_pull") {
$command = 'sudo git pull';
$output = shell_exec($command);
echo $output;
}
if ($type == "sshTunnel") {
$ssh_port=$_GET['ssh_port'];
$command = 'sudo ssh -i /var/www/.ssh/id_rsa -f -N -R "'.$ssh_port.':localhost:22" -p 50221 -o StrictHostKeyChecking=no "airlab_server1@aircarto.fr"';
$output = shell_exec($command);
echo $output;
}
if ($type == "reboot") {
$command = 'sudo reboot';
$output = shell_exec($command);
}
if ($type == "npm") {
$port=$_GET['port'];
$command = '/usr/bin/python3 /var/www/nebuleair_pro_4g/npm.py ' . $port;
$output = shell_exec($command);
echo $output;
}
if ($type == "noise") {
$command = '/var/www/nebuleair_pro_4g/sound_meter/sound_meter';
$output = shell_exec($command);
echo $output;
}
if ($type == "BME280") {
$command = '/usr/bin/python3 /var/www/nebuleair_pro_4g/BME280/read.py';
$output = shell_exec($command);
echo $output;
}
# SARA R4 COMMANDS
if ($type == "sara") {
$port=$_GET['port'];
$sara_command=$_GET['command'];
$sara_command = escapeshellcmd($sara_command);
$timeout=$_GET['timeout'];
$command = '/usr/bin/python3 /var/www/nebuleair_pro_4g/SARA/sara.py ' . $port . ' ' . $sara_command . ' ' . $timeout;
$output = shell_exec($command);
echo $output;
}
# SARA R4 COMMANDS (MQTT)
if ($type == "sara_getMQTT_config") {
$port=$_GET['port'];
$timeout=$_GET['timeout'];
$command = '/usr/bin/python3 /var/www/nebuleair_pro_4g/SARA/MQTT/get_config.py ' . $port . ' ' . $timeout;
$output = shell_exec($command);
echo $output;
}
# SARA R4 COMMANDS (MQTT)
if ($type == "sara_getMQTT_login_logout") {
$port=$_GET['port'];
$timeout=$_GET['timeout'];
$login_logout=$_GET['login_logout'];
$command = '/usr/bin/python3 /var/www/nebuleair_pro_4g/SARA/MQTT/login_logout.py ' . $port . ' ' . $login_logout . ' ' . $timeout;
$output = shell_exec($command);
echo $output;
}
# SARA R4 COMMANDS (MQTT -> publish)
if ($type == "sara_MQTT_publish") {
$port=$_GET['port'];
$timeout=$_GET['timeout'];
$message=$_GET['message'];
$command = '/usr/bin/python3 /var/www/nebuleair_pro_4g/SARA/MQTT/publish.py ' . $port . ' ' . $message . ' ' . $timeout;
$output = shell_exec($command);
echo $output;
}
#Connect to network
if ($type == "sara_connectNetwork") {
$port=$_GET['port'];
$timeout=$_GET['timeout'];
$networkID=$_GET['networkID'];
$command = '/usr/bin/python3 /var/www/nebuleair_pro_4g/SARA/sara_connectNetwork.py ' . $port . ' ' . $networkID . ' ' . $timeout;
$output = shell_exec($command);
echo $output;
}
#SET THE URL for messaging
if ($type == "sara_setURL") {
$port=$_GET['port'];
$url=$_GET['url'];
$command = '/usr/bin/python3 /var/www/nebuleair_pro_4g/SARA/sara_setURL.py ' . $port . ' ' . $url;
$output = shell_exec($command);
echo $output;
}
#TO WRITE MESSAGE TO MEMORY
if ($type == "sara_writeMessage") {
$port=$_GET['port'];
$message=$_GET['message'];
$message = escapeshellcmd($message);
$type2=$_GET['type2'];
if ($type2 === "write") {
$command = '/usr/bin/python3 /var/www/nebuleair_pro_4g/SARA/sara_writeMessage.py ' . $port . ' ' . $message;
$output = shell_exec($command);
}
if ($type2 === "read") {
$command = '/usr/bin/python3 /var/www/nebuleair_pro_4g/SARA/sara_readMessage.py ' . $port . ' ' . $message;
$output = shell_exec($command);
}
if ($type2 === "erase") {
$command = '/usr/bin/python3 /var/www/nebuleair_pro_4g/SARA/sara_eraseMessage.py ' . $port . ' ' . $message;
$output = shell_exec($command);
}
echo $output;
}
#Send the typed message to server (for ntfy notification)
if ($type == "sara_sendMessage") {
$port=$_GET['port'];
$endpoint=$_GET['endpoint'];
$endpoint = escapeshellcmd($endpoint);
$command = '/usr/bin/python3 /var/www/nebuleair_pro_4g/SARA/sara_sendMessage.py ' . $port . ' ' . $endpoint;
$output = shell_exec($command);
echo $output;
}
if ($type == "internet") {
//eth0
$command = 'nmcli -g GENERAL.STATE device show eth0';
$eth0_connStatus = shell_exec($command);
$eth0_connStatus = str_replace("\n", "", $eth0_connStatus);
$command = 'nmcli -g IP4.ADDRESS device show eth0';
$eth0_IPAddr = shell_exec($command);
$eth0_IPAddr = str_replace("\n", "", $eth0_IPAddr);
//wlan0
$command = 'nmcli -g GENERAL.STATE device show wlan0';
$wlan0_connStatus = shell_exec($command);
$wlan0_connStatus = str_replace("\n", "", $wlan0_connStatus);
$command = 'nmcli -g IP4.ADDRESS device show wlan0';
$wlan0_IPAddr = shell_exec($command);
$wlan0_IPAddr = str_replace("\n", "", $wlan0_IPAddr);
$data= array(
"ethernet" => array(
"connection" => $eth0_connStatus,
"IP" => $eth0_IPAddr
),
"wifi" => array(
"connection" => $wlan0_connStatus,
"IP" => $wlan0_IPAddr
)
);
$json_data = json_encode($data);
echo $json_data;
}
# IMPORTANT
# c'est ici que la connexion vers le WIFI du client s'effectue.
if ($type == "wifi_connect") {
$SSID=$_GET['SSID'];
$PASS=$_GET['pass'];
echo "will try to connect to </br>";
echo "SSID: " . $SSID;
echo "</br>";
echo "Password: " . $PASS;
echo "</br>";
echo "</br>";
echo "You will be disconnected. If connection is successfull you can find the device on your local network.";
$script_path = __DIR__ . '/connexion.sh';
$log_file = __DIR__ . '/logs/app.log';
shell_exec("$script_path $SSID $PASS >> $log_file 2>&1 &");
#$output = shell_exec('sudo nmcli connection down Hotspot');
#$output2 = shell_exec('sudo nmcli device wifi connect "AirLab" password "123plouf"');
}
if ($type == "wifi_scan") {
// Set the path to your CSV file
$csvFile = '/var/www/nebuleair_pro_4g/wifi_list.csv';
// Initialize an array to hold the JSON data
$jsonData = [];
// Open the CSV file for reading
if (($handle = fopen($csvFile, 'r')) !== false) {
// Get the headers from the first row
$headers = fgetcsv($handle);
// Loop through the rest of the rows
while (($row = fgetcsv($handle)) !== false) {
// Combine headers with row data to create an associative array
$jsonData[] = array_combine($headers, $row);
}
// Close the file handle
fclose($handle);
}
// Set the content type to JSON
header('Content-Type: application/json');
// Convert the array to JSON format and output it
echo json_encode($jsonData, JSON_PRETTY_PRINT);
}
if ($type == "wifi_scan_old") {
$output = shell_exec('nmcli device wifi list ifname wlan0');
// Split the output into lines
$lines = explode("\n", trim($output));
// Initialize an array to hold the results
$wifiNetworks = [];
// Loop through each line and extract the relevant information
foreach ($lines as $index => $line) {
// Skip the header line
if ($index === 0) {
continue;
}
// Split the line by whitespace and filter empty values
$columns = preg_split('/\s+/', $line);
// If the line has enough columns, store the relevant data
if (count($columns) >= 5) {
$wifiNetworks[] = [
'SSID' => $columns[2], // Network name
'BARS' => $columns[8],
'SIGNAL' => $columns[7], // Signal strength
];
}
}
$json_data = json_encode($wifiNetworks);
echo $json_data;
}

405
loop/1_NPM/send_data.py Executable file
View File

@@ -0,0 +1,405 @@
"""
Main loop to gather data from sensor:
* NPM
* Envea
* I2C BME280
* Noise sensor
and send it to AirCarto servers via SARA R4 HTTP post requests
CSV PAYLOAD
ATTENTION : do not change order !
{PM1},{PM25},{PM10},{temp},{hum},{press},{avg_noise},{max_noise},{min_noise},{envea_no2},{envea_h2s},{envea_o3},{4g_signal_quality}
0 -> PM1
1 -> PM25
2 -> PM10
3 -> temp
4 -> hum
5 -> press
6 -> avg_noise
7 -> max_noise
8 -> min_noise
9 -> envea_no2
10 -> envea_h2s
11 -> envea_o3
12 -> 4G signal quality
JSON PAYLOAD
Same as NebuleAir wifi
{"nebuleairid": "82D25549434",
"software_version": "ModuleAirV2-V1-042022",
"sensordatavalues":
[
{"value_type":"NPM_P0","value":"1.54"},
{"value_type":"NPM_P1","value":"1.54"},
{"value_type":"NPM_P2","value":"1.54"},
{"value_type":"NPM_N1","value":"0.02"},
{"value_type":"NPM_N10","value":"0.02"},
{"value_type":"NPM_N25","value":"0.02"},
{"value_type":"MHZ16_CO2","value":"793.00"},
{"value_type":"SGP40_VOC","value":"29915.00"},
{"value_type":"samples","value":"134400"},
{"value_type":"min_micro","value":"137"},
{"value_type":"max_micro","value":"155030"},
{"value_type":"interval","value":"145000"},
{"value_type":"signal","value":"-80"},
{"value_type":"latitude","value":"43.2964"},
{"value_type":"longitude","value":"5.36978"},
{"value_type":"state_npm","value":"State: 00000000"},
{"value_type":"th_npm","value":"28.47 / 37.54"}
]}
"""
import board
import json
import serial
import time
import busio
import re
import RPi.GPIO as GPIO
from adafruit_bme280 import basic as adafruit_bme280
# Record the start time of the script
start_time = time.time()
url="data.nebuleair.fr"
payload = [None] * 20
# Set up GPIO mode (for Blue LED: network status)
GPIO.setwarnings(False)
GPIO.setmode(GPIO.BCM) # Use Broadcom pin numbering
GPIO.setup(23, GPIO.OUT) # Set GPIO23 as an output pin
#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 {}
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"Successfully updated '{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)
# Access the shared variables
baudrate = config.get('SaraR4_baudrate', 115200) #baudrate du sara R4
device_id = config.get('deviceID', '').upper() #device ID en maj
need_to_log = config.get('loop_log', False) #inscription des logs
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
envea_sondes = config.get('envea_sondes', [])
connected_envea_sondes = [sonde for sonde in envea_sondes if sonde.get('connected', 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
)
ser_NPM = serial.Serial(
port='/dev/ttyAMA5',
baudrate=115200,
parity=serial.PARITY_EVEN,
stopbits=serial.STOPBITS_ONE,
bytesize=serial.EIGHTBITS,
timeout = 1
)
ser_envea = serial.Serial(
port='/dev/ttyAMA4',
baudrate=9600,
parity=serial.PARITY_NONE,
stopbits=serial.STOPBITS_ONE,
bytesize=serial.EIGHTBITS,
timeout = 1
)
def read_complete_response(serial_connection, timeout=2, end_of_response_timeout=2):
response = bytearray()
serial_connection.timeout = timeout
end_time = time.time() + end_of_response_timeout
while True:
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
elif time.time() > end_time:
break
time.sleep(0.1) # Short sleep to prevent busy waiting
return response.decode('utf-8')
# Open and read the JSON file
try:
# Send the command to request data (e.g., data for 60 seconds)
ser_NPM.write(b'\x81\x12\x6D')
# Read the response
byte_data = ser_NPM.readline()
#if npm is disconnected byte_data is empty
# Extract the state byte and PM data from the response
state_byte = int.from_bytes(byte_data[2:3], byteorder='big')
state_bits = [int(bit) for bit in bin(state_byte)[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
# Create a dictionary with the parsed data
data = {
'sondeID': device_id,
'PM1': PM1,
'PM25': PM25,
'PM10': PM10
}
message = f"{data['PM1']},{data['PM25']},{data['PM10']}"
payload[0] = data['PM1']
payload[1] = data['PM25']
payload[2] = data['PM10']
# Sonde BME280 connected
if bme_280_config:
#on récupère les infos du BME280 et on les ajoute au payload
i2c = busio.I2C(board.SCL, board.SDA)
bme280 = adafruit_bme280.Adafruit_BME280_I2C(i2c, address=0x76)
bme280.sea_level_pressure = 1013.25 # Update this value for your location
data['temp'] = round(bme280.temperature, 2)
data['hum'] = round(bme280.humidity, 2)
data['press'] = round(bme280.pressure, 2)
message += f",{data['temp']},{data['hum']},{data['press']}"
payload[3] = data['temp']
payload[4] = data['hum']
payload[5] = data['press']
# Sonde Bruit connected
if i2C_sound_config:
#on récupère les infos de sound_metermoving et on les ajoute au message
file_path_data_noise = "/var/www/nebuleair_pro_4g/sound_meter/moving_avg_minute.txt"
# Read the file and extract the numbers
try:
with open(file_path_data_noise, "r") as file:
content = file.read().strip()
avg_noise, max_noise, min_noise = map(int, content.split())
# Append the variables to the JSON and to the message
data['avg_noise'] = avg_noise
data['max_noise'] = max_noise
data['min_noise'] = min_noise
#get BME280 data (SAFE: it returns none if the key do not exist)
message = f"{data.get('PM1', '')},{data.get('PM25', '')},{data.get('PM10', '')},{data.get('temp', '')},{data.get('hum', '')},{data.get('press', '')},{avg_noise},{max_noise},{min_noise}"
payload[6] = data['avg_noise']
payload[7] = data['max_noise']
payload[8] = data['min_noise']
print(message) # Display the message or send it further
except FileNotFoundError:
print(f"Error: File {file_path} not found.")
except ValueError:
print("Error: File content is not valid numbers.")
# Sondes Envea
if connected_envea_sondes:
# Pour chacune des sondes
for device in connected_envea_sondes:
print(f"Connected envea Sonde: {device.get('name', 'Unknown')} on port {device.get('port', 'Unknown')} and coefficient {device.get('coefficient', 'Unknown')} ")
ser_envea.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 = ser_envea.readline()
coefficient = device.get('coefficient', 'Unknown')
if len(data_envea) >= 20:
byte_20 = data_envea[19]
byte_20 = byte_20 * coefficient
payload[10] = byte_20
print(f"Data from envea {byte_20}")
else:
print("Données reçues insuffisantes pour extraire le 20ème octet.")
# Getting the LTE Signal
print("-> Getting signal <-")
ser_sara.write(b'AT+CSQ\r')
response2 = read_complete_response(ser_sara)
print("Response:")
print(response2)
print("<----")
match = re.search(r'\+CSQ:\s*(\d+),', response2)
if match:
signal_quality = match.group(1)
print("Signal Quality:", signal_quality)
payload[12]=signal_quality
time.sleep(1)
#Write Data to saraR4
#1. Open sensordata.json (with correct data size)
csv_string = ','.join(str(value) if value is not None else '' for value in payload)
size_of_string = len(csv_string)
command = f'AT+UDWNFILE="sensordata.json",{size_of_string}\r'
ser_sara.write((command + '\r').encode('utf-8'))
response_SARA_1 = read_complete_response(ser_sara)
#if need_to_log:
#print("Open JSON:")
#print(response_SARA_1)
time.sleep(1)
#2. Write to shell
ser_sara.write(csv_string.encode())
response_SARA_2 = read_complete_response(ser_sara)
if need_to_log:
print("Write to memory:")
print(response_SARA_2)
#3. Send to endpoint (with device ID)
command= f'AT+UHTTPC=0,4,"/pro_4G/data.php?sensor_id={device_id}","server_response.txt","sensordata.json",4\r'
ser_sara.write((command + '\r').encode('utf-8'))
response_SARA_3 = read_complete_response(ser_sara)
if need_to_log:
print("Send data:")
print(response_SARA_3)
# Les types de réponse
# 1.La commande n'a pas fonctionné
# +CME ERROR: No connection to phone
# +CME ERROR: Operation not allowed
# 2.La commande fonctionne: elle renvoie un code
# +UUHTTPCR: <profile_id>,<http_command>,<http_result>
# <http_result>: 1 pour sucess et 0 pour fail
# +UUHTTPCR: 0,4,1 -> OK
# +UUHTTPCR: 0,4,0 -> error
# 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("*****")
print('<span style="color: red;font-weight: bold;">ATTENTION: CME ERROR</span>')
print("error:", lines[-1])
print("*****")
update_json_key(config_file, "SARA_R4_network_status", "disconnected")
# Gestion de l'erreur spécifique
if "No connection to phone" in lines[-1]:
print("No connection to the phone. Retrying or reset may be required.")
# Actions spécifiques pour ce type d'erreur (par exemple, réinitialiser ou tenter de reconnecter)
elif "Operation not allowed" in lines[-1]:
print("Operation not allowed. This may require a different configuration.")
# Actions spécifiques pour ce type d'erreur
# Clignotement LED en cas d'erreur
GPIO.output(23, GPIO.LOW) # Éteindre la LED définitivement
for _ in range(4):
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:
# 2.Si la réponse contient une réponse HTTP valide
# Extract HTTP response code from the last line
# ATTENTION: lines[-1] renvoie l'avant dernière ligne et il peut y avoir un soucis avec le OK
# rechercher plutot
http_response = lines[-1] # "+UUHTTPCR: 0,4,0"
parts = http_response.split(',')
# 2.1 code 0 (HTTP failed)
if len(parts) == 3 and parts[-1] == '0': # The third value indicates success
print("*****")
print('<span style="color: red;font-weight: bold;">ATTENTION: HTTP operation failed</span>')
update_json_key(config_file, "SARA_R4_network_status", "disconnected")
print("*****")
print("resetting the URL (domain name):")
print("Turning off the blue LED...")
for _ in range(4): # Faire clignoter 4 fois
GPIO.output(23, GPIO.HIGH) # Allumer la LED
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.LOW) # Turn off the LED
command = f'AT+UHTTP=0,1,"{url}"\r'
ser_sara.write((command + '\r').encode('utf-8'))
response_SARA_31 = read_complete_response(ser_sara)
if need_to_log:
print(response_SARA_31)
# 2.2 code 1 (HHTP succeded)
else:
# Si la commande HTTP a réussi
print('<span style="color: green; font-weight: bold;">HTTP operation successful.</span>')
update_json_key(config_file, "SARA_R4_network_status", "connected")
print("Turning on the blue LED...")
for _ in range(4): # Faire clignoter 4 fois
GPIO.output(23, GPIO.HIGH) # Allumer la LED
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
ser_sara.write(b'AT+URDFILE="server_response.txt"\r')
response_SARA_4 = read_complete_response(ser_sara)
if need_to_log:
print("Reply from server:")
print(response_SARA_4)
#5. empty json
ser_sara.write(b'AT+UDELFILE="sensordata.json"\r')
response_SARA_5 = read_complete_response(ser_sara)
if need_to_log:
print("Empty JSON:")
print(response_SARA_5)
# Calculate and print the elapsed time
elapsed_time = time.time() - start_time
if need_to_log:
print(f"Elapsed time: {elapsed_time:.2f} seconds")
print("----------------------------------------")
print("----------------------------------------")
except Exception as e:
print(f"Error reading the JSON file: {e}")

312
loop/1_NPM/send_data_mqtt.py Executable file
View File

@@ -0,0 +1,312 @@
"""
Main loop to gather data from sensor:
* NPM
* I2C BME280
* Noise sensor
and send it to AirCarto servers via SARA R4 MQTT requests
"""
import board
import json
import serial
import time
import busio
import RPi.GPIO as GPIO
from adafruit_bme280 import basic as adafruit_bme280
# Record the start time of the script
start_time = time.time()
url="data.nebuleair.fr"
# Set up GPIO mode (for Blue LED: network status)
GPIO.setwarnings(False)
GPIO.setmode(GPIO.BCM) # Use Broadcom pin numbering
GPIO.setup(23, GPIO.OUT) # Set GPIO23 as an output pin
# Add yellow color to the output
yellow_color = "\033[33m" # ANSI escape code for yellow
red_color = "\033[31m" # ANSI escape code for red
reset_color = "\033[0m" # Reset color to default
green_color = "\033[32m" # Green
#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 {}
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"Successfully updated '{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)
# Access the shared variables
baudrate = config.get('SaraR4_baudrate', 115200) #baudrate du sara R4
device_id = config.get('deviceID', '').upper() #device ID en maj
need_to_log = config.get('loop_log', False) #inscription des logs
bme_280_config = config.get('i2c_BME', False) #présence du BME280
i2C_sound_config = config.get('i2C_sound', False) #présence du BME280
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
)
ser_NPM = serial.Serial(
port='/dev/ttyAMA5',
baudrate=115200,
parity=serial.PARITY_EVEN,
stopbits=serial.STOPBITS_ONE,
bytesize=serial.EIGHTBITS,
timeout = 1
)
def read_complete_response(serial_connection, timeout=2, end_of_response_timeout=2):
response = bytearray()
serial_connection.timeout = timeout
end_time = time.time() + end_of_response_timeout
while True:
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
elif time.time() > end_time:
break
time.sleep(0.1) # Short sleep to prevent busy waiting
# Decode the response and filter out empty lines
decoded_response = response.decode('utf-8')
non_empty_lines = "\n".join(line for line in decoded_response.splitlines() if line.strip())
# Add yellow color to the output
colored_output = f"{yellow_color}{non_empty_lines}\n{reset_color}"
return colored_output
# Open and read the JSON file
try:
# Send the command to request data (e.g., data for 60 seconds)
ser_NPM.write(b'\x81\x12\x6D')
# Read the response
byte_data = ser_NPM.readline()
#if npm is disconnected byte_data is empty
# Extract the state byte and PM data from the response
state_byte = int.from_bytes(byte_data[2:3], byteorder='big')
state_bits = [int(bit) for bit in bin(state_byte)[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
# Create a dictionary with the parsed data
data = {
'sondeID': device_id,
'PM1': PM1,
'PM25': PM25,
'PM10': PM10
}
message = f"{data['PM1']},{data['PM25']},{data['PM10']}"
if bme_280_config:
#on récupère les infos du BME280 et on les ajoute au message
i2c = busio.I2C(board.SCL, board.SDA)
bme280 = adafruit_bme280.Adafruit_BME280_I2C(i2c, address=0x76)
bme280.sea_level_pressure = 1013.25 # Update this value for your location
data['temp'] = round(bme280.temperature, 2)
data['hum'] = round(bme280.humidity, 2)
data['press'] = round(bme280.pressure, 2)
message += f",{data['temp']},{data['hum']},{data['press']}"
if i2C_sound_config:
#on récupère les infos de sound_metermoving et on les ajoute au message
file_path_data_noise = "/var/www/nebuleair_pro_4g/sound_meter/moving_avg_minute.txt"
# Read the file and extract the numbers
try:
with open(file_path_data_noise, "r") as file:
content = file.read().strip()
avg_noise, max_noise, min_noise = map(int, content.split())
# Append the variables to the JSON and to the message
data['avg_noise'] = avg_noise
data['max_noise'] = max_noise
data['min_noise'] = min_noise
#get BME280 data (SAFE: it returns none if the key do not exist)
message = f"{data.get('PM1', '')},{data.get('PM25', '')},{data.get('PM10', '')},{data.get('temp', '')},{data.get('hum', '')},{data.get('press', '')},{avg_noise},{max_noise},{min_noise}"
print(message) # Display the message or send it further
except FileNotFoundError:
print(f"Error: File {file_path} not found.")
except ValueError:
print("Error: File content is not valid numbers.")
# Print the content of the JSON file
if need_to_log:
print("Data from sensors:")
print(json.dumps(data, indent=4)) # Pretty print the JSON data
#Write Data to saraR4
#1. MQTT profile configuration
# Note: you need to logout first to change the config
print("")
#print("1.PROFILE CONFIG")
#print(" 1.A. READ CONFIG")
#command = f'AT+UMQTT?\r'
#ser.write((command + '\r').encode('utf-8'))
#response_SARA_1 = read_complete_response(ser)
#if need_to_log:
# print(response_SARA_1)
# La config s'efface à chaque redémarrage!
need_to_update_config = False
if need_to_update_config:
print("1.B. SET CONFIG")
#command = f'AT+UMQTT=1,1883\r' #MQTT local TCP port number
command = f'AT+UMQTT=2,"aircarto.fr"\r' #MQTT server name
#command = f'AT+UMQTT=3,"193.252.54.10"\r' # MQTT server IP address
#command = f'AT+UMQTT=12,1\r' # MQTT clean session
ser.write((command + '\r').encode('utf-8'))
response_SARA_1 = read_complete_response(ser)
if need_to_log:
print(response_SARA_1)
lines = response_SARA_1.strip().splitlines()
for line in lines:
if line.startswith("+UMQTT"):
# Split the line by commas and get the last number
parts = line.split(',')
last_number = parts[-1].strip() # Get the last part and strip any whitespace
if int(last_number) == 1:
print(f"{green_color}MQTT profile configuration SUCCEDED{reset_color}")
else:
print(f"{green_color}ERROR: MQTT profile configuration fail{reset_color}")
#2. MQTT login
need_to_update_login = False
if need_to_update_login:
print("")
print("2.MQTT LOGIN")
#command = f'AT+UMQTTC=1\r' #MQTT login
command = f'AT+UMQTTC=0\r' #MQTT logout
ser.write((command + '\r').encode('utf-8'))
response_SARA_2 = read_complete_response(ser, 8, 8)
if need_to_log:
print(response_SARA_2)
lines = response_SARA_2.strip().splitlines()
for line in lines:
if line.startswith("+UMQTTC"):
parts = line.split(',')
first_number = parts[0].replace("+UMQTTC:", "").strip()
last_number = parts[-1].strip() # Get the last part and strip any whitespace
#print(f"Last number: {last_number}")
if int(first_number) == 0:
print("MQTT logout command ->")
if int(first_number) == 1:
print("MQTT login command ->")
if int(last_number) == 1:
print(f"{green_color}SUCCESS{reset_color}")
else:
print(f"{red_color}FAIL{reset_color}")
if line.startswith("+UUMQTTC"):
parts = line.split(',')
first_number = parts[0].replace("+UUMQTTC:", "").strip()
last_number = parts[-1].strip() # Get the last part and strip any whitespace
if int(first_number) == 1:
print("MQTT login result ->")
if int(last_number) == 0:
print(f"{green_color}connection accepted{reset_color}")
if int(last_number) == 1:
print(f"{green_color}the server does not support the level of the MQTT protocol requested by the Client{reset_color}")
if int(last_number) == 2:
print(f"{green_color} the client identifier is correct UTF-8 but not allowed by the Server{reset_color}")
if int(last_number) == 3:
print(f"{green_color} the network connection has been made but the MQTT service is unavailable{reset_color}")
#3. MQTT publish
print("")
print("3.MQTT PUBLISH")
command = f'AT+UMQTTC=2,0,0,"nebuleair/pro/{device_id}/data","{message}"\r'
ser.write((command + '\r').encode('utf-8'))
response_SARA_3 = read_complete_response(ser)
if need_to_log:
print(response_SARA_3)
lines = response_SARA_3.strip().splitlines()
for line in lines:
if line.startswith("+UMQTTC"):
parts = line.split(',')
first_number = parts[0].replace("+UMQTTC:", "").strip()
last_number = parts[-1].strip() # Get the last part and strip any whitespace
if int(first_number) == 2:
print("MQTT Publish ->")
if int(last_number) == 1:
print(f"{green_color}SUCCESS{reset_color}")
else:
print(f"{red_color}FAIL{reset_color}")
# Calculate and print the elapsed time
elapsed_time = time.time() - start_time
if need_to_log:
print("")
print(f"Elapsed time: {elapsed_time:.2f} seconds")
print("----------------------------------------")
print("----------------------------------------")
except Exception as e:
print(f"Error reading the JSON file: {e}")

6
loop/3_NPM/data.json Executable file
View File

@@ -0,0 +1,6 @@
{
"sondeID": "Average_USB2_USB3",
"PM1": 0.0,
"PM25": 0.0,
"PM10": 0.0
}

101
loop/3_NPM/get_data.py Executable file
View File

@@ -0,0 +1,101 @@
import serial
import json
import time
# Initialize serial ports for the three sensors
ser3 = serial.Serial(
port='/dev/ttyAMA3',
baudrate=115200,
parity=serial.PARITY_EVEN,
stopbits=serial.STOPBITS_ONE,
bytesize=serial.EIGHTBITS,
timeout = 1
)
ser4 = serial.Serial(
port='/dev/ttyAMA4',
baudrate=115200,
parity=serial.PARITY_EVEN,
stopbits=serial.STOPBITS_ONE,
bytesize=serial.EIGHTBITS,
timeout = 1
)
ser5 = serial.Serial(
port='/dev/ttyAMA5',
baudrate=115200,
parity=serial.PARITY_EVEN,
stopbits=serial.STOPBITS_ONE,
bytesize=serial.EIGHTBITS,
timeout = 1
)
# Function to read and parse sensor data
def read_sensor_data(ser, sonde_id):
try:
# Send the command to request data (e.g., data for 60 seconds)
ser.write(b'\x81\x12\x6D')
# Read the response
byte_data = ser.readline()
# Extract the state byte and PM data from the response
state_byte = int.from_bytes(byte_data[2:3], byteorder='big')
state_bits = [int(bit) for bit in bin(state_byte)[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
# Create a dictionary with the parsed data
data = {
'sondeID': sonde_id,
'PM1': PM1,
'PM25': PM25,
'PM10': PM10
}
return data
except Exception as e:
print(f"Error reading from sensor {sonde_id}: {e}")
return None
# Function to create a JSON object with all sensor data
def collect_all_sensor_data():
all_data = {}
# Read data from each sensor and add to the all_data dictionary
sensor_data_3 = read_sensor_data(ser3, 'USB2')
sensor_data_4 = read_sensor_data(ser4, 'USB3')
sensor_data_5 = read_sensor_data(ser5, 'USB4')
# Store the data for each sensor in the dictionary
if sensor_data_3:
all_data['sensor_3'] = sensor_data_3
if sensor_data_4:
all_data['sensor_4'] = sensor_data_4
if sensor_data_5:
all_data['sensor_5'] = sensor_data_5
return all_data
# Main script to run once
if __name__ == "__main__":
try:
# Collect data from all sensors
data = collect_all_sensor_data()
# Convert data to JSON
json_data = json.dumps(data, indent=4)
# Define the output file path
output_file = "/var/www/nebuleair_pro_4g/loop/data.json" # Change this to your desired file path
# Write the JSON data to the file
with open(output_file, 'w') as file:
file.write(json_data)
print(f"Data successfully written to {output_file}")
except Exception as e:
print(f"Error: {e}")

View File

@@ -0,0 +1,182 @@
import serial
import json
import time
import math
# Record the start time of the script
start_time = time.time()
#get 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 {}
# 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
need_to_log = config.get('loop_log', False)
# Initialize serial ports for the three sensors
ser3 = serial.Serial(
port='/dev/ttyAMA3',
baudrate=115200,
parity=serial.PARITY_EVEN,
stopbits=serial.STOPBITS_ONE,
bytesize=serial.EIGHTBITS,
timeout=1
)
ser4 = serial.Serial(
port='/dev/ttyAMA4',
baudrate=115200,
parity=serial.PARITY_EVEN,
stopbits=serial.STOPBITS_ONE,
bytesize=serial.EIGHTBITS,
timeout=1
)
ser5 = serial.Serial(
port='/dev/ttyAMA5',
baudrate=115200,
parity=serial.PARITY_EVEN,
stopbits=serial.STOPBITS_ONE,
bytesize=serial.EIGHTBITS,
timeout=1
)
# Function to read and parse sensor data
def read_sensor_data(ser, sonde_id):
try:
# Send the command to request data (e.g., data for 60 seconds)
ser.write(b'\x81\x12\x6D')
# Read the response
byte_data = ser.readline()
# Extract the state byte and PM data from the response
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
# Create a dictionary with the parsed data
data = {
'sondeID': sonde_id,
'PM1': PM1,
'PM25': PM25,
'PM10': PM10
}
return data
except Exception as e:
print(f"Error reading from sensor {sonde_id}: {e}")
return None
# Function to calculate the Euclidean distance between two sensor readings
def calculate_distance(sensor1, sensor2):
PM1_diff = sensor1['PM1'] - sensor2['PM1']
PM25_diff = sensor1['PM25'] - sensor2['PM25']
PM10_diff = sensor1['PM10'] - sensor2['PM10']
return math.sqrt(PM1_diff**2 + PM25_diff**2 + PM10_diff**2)
# Function to select the closest pair of sensors and average their data
def average_closest_pair(data):
# List of sensor names and their data
sensors = list(data.items())
# Variable to keep track of the smallest distance and corresponding pair
min_distance = float('inf')
closest_pair = None
# Compare each pair of sensors to find the closest one
for i in range(len(sensors)):
for j in range(i + 1, len(sensors)):
sensor1 = sensors[i][1]
sensor2 = sensors[j][1]
# Calculate the distance between the two sensors
distance = calculate_distance(sensor1, sensor2)
# Update the closest pair if a smaller distance is found
if distance < min_distance:
min_distance = distance
closest_pair = (sensor1, sensor2)
# If a closest pair is found, average their values
if closest_pair:
sensor1, sensor2 = closest_pair
averaged_data = {
'sondeID': f"Average_{sensor1['sondeID']}_{sensor2['sondeID']}",
'PM1': round((sensor1['PM1'] + sensor2['PM1']) / 2, 2),
'PM25': round((sensor1['PM25'] + sensor2['PM25']) / 2, 2),
'PM10': round((sensor1['PM10'] + sensor2['PM10']) / 2, 2)
}
return averaged_data
else:
return None
# Function to create a JSON object with all sensor data
def collect_all_sensor_data():
all_data = {}
# Read data from each sensor and add to the all_data dictionary
sensor_data_3 = read_sensor_data(ser3, 'USB2')
sensor_data_4 = read_sensor_data(ser4, 'USB3')
sensor_data_5 = read_sensor_data(ser5, 'USB4')
# Store the data for each sensor in the dictionary
if sensor_data_3:
all_data['sensor_3'] = sensor_data_3
if sensor_data_4:
all_data['sensor_4'] = sensor_data_4
if sensor_data_5:
all_data['sensor_5'] = sensor_data_5
return all_data
# Main script to run once and average data for the closest sensors
if __name__ == "__main__":
try:
# Collect data from all sensors
data = collect_all_sensor_data()
if need_to_log:
print("Getting Data from all sensors:")
print(data)
# Average the closest pair of sensors
averaged_data = average_closest_pair(data)
if need_to_log:
print("Average the closest pair of sensors:")
print(averaged_data)
if averaged_data:
# Convert the averaged data to JSON
json_data = json.dumps(averaged_data, indent=4)
# Define the output file path
output_file = "/var/www/nebuleair_pro_4g/loop/data.json" # Change this to your desired file path
# Write the JSON data to the file
with open(output_file, 'w') as file:
file.write(json_data)
if need_to_log:
print(f"Data successfully written to {output_file}")
else:
print("No closest pair found to average.")
# Calculate and print the elapsed time
elapsed_time = time.time() - start_time
if need_to_log:
print(f"Elapsed time: {elapsed_time:.2f} seconds")
print("-----------------")
except Exception as e:
print(f"Error: {e}")

142
loop/3_NPM/send_data.py Executable file
View File

@@ -0,0 +1,142 @@
import json
import serial
import time
# Record the start time of the script
start_time = time.time()
# Define the path to the JSON file
file_path = "/var/www/nebuleair_pro_4g/loop/data.json" # Replace with your actual file path
url="data.nebuleair.fr"
#get 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 {}
# 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)
device_id = config.get('deviceID', '').upper()
need_to_log = config.get('loop_log', False)
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
)
def read_complete_response(serial_connection, timeout=2, end_of_response_timeout=2):
response = bytearray()
serial_connection.timeout = timeout
end_time = time.time() + end_of_response_timeout
while True:
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
elif time.time() > end_time:
break
time.sleep(0.1) # Short sleep to prevent busy waiting
return response.decode('utf-8')
# Open and read the JSON file
try:
with open(file_path, 'r') as file:
# Load the data from the file
data = json.load(file)
# Print the content of the JSON file
if need_to_log:
print("Data from JSON file:")
print(json.dumps(data, indent=4)) # Pretty print the JSON data
message = f"{data['PM1']},{data['PM25']},{data['PM10']}"
#Write Data to saraR4
#1. Open sensordata.json (with correct data size)
size_of_string = len(message)
command = f'AT+UDWNFILE="sensordata.json",{size_of_string}\r'
ser.write((command + '\r').encode('utf-8'))
response_SARA_1 = read_complete_response(ser)
if need_to_log:
print("Open JSON:")
print(response_SARA_1)
time.sleep(1)
#2. Write to shell
ser.write(message.encode())
response_SARA_2 = read_complete_response(ser)
if need_to_log:
print("Write to memory:")
print(response_SARA_2)
#3. Send to endpoint (with device ID)
command= f'AT+UHTTPC=0,4,"/pro_4G/data.php?sensor_id={device_id}","server_response.txt","sensordata.json",4\r'
ser.write((command + '\r').encode('utf-8'))
response_SARA_3 = read_complete_response(ser)
if need_to_log:
print("Send data:")
print(response_SARA_3)
# Split response into lines
lines = response_SARA_3.strip().splitlines()
# +UUHTTPCR: <profile_id>,<http_command>,<http_result>
# <http_result>: 1 pour sucess et 0 pour fail
# +UUHTTPCR: 0,4,1 -> OK
# +UUHTTPCR: 0,4,0 -> error
# Extract HTTP response code from the last line
http_response = lines[-1] # "+UUHTTPCR: 0,4,0"
parts = http_response.split(',')
# Check HTTP result
if len(parts) == 3 and parts[-1] == '0': # The third value indicates success
print("*****")
print("!ATTENTION!")
print("error: HTTP operation failed.")
print("*****")
print("resetting the URL (domain name):")
command = f'AT+UHTTP=0,1,"{url}"\r'
ser.write((command + '\r').encode('utf-8'))
response_SARA_31 = read_complete_response(ser)
if need_to_log:
print(response_SARA_31)
else:
print("HTTP operation successful.")
#4. Read reply from server
ser.write(b'AT+URDFILE="server_response.txt"\r')
response_SARA_4 = read_complete_response(ser)
if need_to_log:
print("Reply from server:")
print(response_SARA_4)
#5. empty json
ser.write(b'AT+UDELFILE="sensordata.json"\r')
response_SARA_5 = read_complete_response(ser)
if need_to_log:
print("Empty JSON:")
print(response_SARA_5)
# Calculate and print the elapsed time
elapsed_time = time.time() - start_time
if need_to_log:
print(f"Elapsed time: {elapsed_time:.2f} seconds")
print("-----------------")
except Exception as e:
print(f"Error reading the JSON file: {e}")

69
npm.py Executable file
View File

@@ -0,0 +1,69 @@
'''
Script to get NPM values
need parameter: port
/usr/bin/python3 /var/www/nebuleair_pro_4g/npm.py ttyAMA4
'''
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 = 2
)
ser.write(b'\x81\x11\x6E') #data10s
#ser.write(b'\x81\x12\x6D') #data60s
while True:
try:
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
#print(f"State: {Statebits}")
#print(f"PM1: {PM1}")
#print(f"PM25: {PM25}")
#print(f"PM10: {PM10}")
#create JSON
data = {
'capteurID': 'nebuleairpro1',
'sondeID':'USB2',
'PM1': PM1,
'PM25': PM25,
'PM10': PM10,
'sleep' : Statebits[0],
'degradedState' : Statebits[1],
'notReady' : Statebits[2],
'heatError' : Statebits[3],
't_rhError' : Statebits[4],
'fanError' : Statebits[5],
'memoryError' : Statebits[6],
'laserError' : Statebits[7]
}
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()

BIN
sound_meter/sound_meter Executable file

Binary file not shown.

91
sound_meter/sound_meter.c Executable file
View File

@@ -0,0 +1,91 @@
/*
Code pour lire la data du I2C decibel meter
Pour compiler le code: gcc -o sound_meter sound_meter.c
Connexion I2C:
SDA to Pi Pico GPIO 2
SCL to Pi Pico GPIO 3
Device connected on addr 0x48
to start the script:
sudo /var/www/nebuleair_pro_4g/sound_meter/sound_meter
0x00 : VERSION register
0x01 : Device ID (byte 1)
0x02 : Device ID
0x03 : Device ID
0x04 : Device ID (byte 4)
0x05 : SKRATCH register
0x06 : CONTROL register
0x07 : TAVG high register
0x08 : TAVG low register
0x09 : RESET register
0x0A : Decibel register (Latest sound intensity value in decibels, averaged over the last Tavg time period.)
0x0B : Min register (Minimum value of decibel reading captured since power-up or manual reset of MIN/MAX registers.)
0x0C : Max register (Maximum value of decibel reading captured since power-up or manual reset of MIN/MAX registers.)
#Le Tavg est programmé sur 2 octets: le high byte et le low byte.
0x07 #Averaging time (high byte) in milliseconds for calculating sound intensity levels. (default 0x03 -> 3)
0x08 #Averaging time (low byte) in milliseconds for calculating sound intensity levels. (default 0xE8 -> 232)
valeur = (Tavg_high x 256) + Tavg_low
= (3 * 256) + 232
= 1000 ms
= 1 s
*/
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <linux/i2c-dev.h>
#include <sys/ioctl.h>
int main()
{
int file;
char data;
// Open I2C1 for reading the sound meter module registers
if ((file = open("/dev/i2c-1", O_RDWR)) < 0)
{
perror("Failed to open I2C1!");
exit(1);
}
// 0x48 is the decibel meter I2C address
if (ioctl(file, I2C_SLAVE, 0x48) < 0)
{
perror("db Meter module is not connected/recognized at I2C addr = 0x48");
close(file);
exit(1);
}
// Decibel value is stored in register 0x0A
data = 0x0A;
// Write the register address (0x0A) to the device
if (write(file, &data, 1) != 1)
{
perror("Failed to write register 0x0A");
close(file);
exit(1);
}
// Read one byte from the device
if (read(file, &data, 1) != 1)
{
perror("Failed to read register 0x0A");
close(file);
exit(1);
}
// Display the sound level
printf("%d\n", data);
// Close the I2C connection
close(file);
return 0;
}

Binary file not shown.

View File

@@ -0,0 +1,122 @@
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <linux/i2c-dev.h>
#include <sys/ioctl.h>
#define MAX_MEASURES 60 // To store 60 sound level measurements
int main()
{
int file;
char data;
int soundLevels[MAX_MEASURES];
int index = 0;
int sum = 0;
int i;
FILE *fileOutput; // File pointer for writing to file
// Initialize the soundLevels array
for (i = 0; i < MAX_MEASURES; i++) {
soundLevels[i] = 0;
}
// Open I2C1 for reading the sound meter module registers
if ((file = open("/dev/i2c-1", O_RDWR)) < 0)
{
perror("Failed to open I2C1!");
exit(1);
}
// 0x48 is the decibel meter I2C address
if (ioctl(file, I2C_SLAVE, 0x48) < 0)
{
perror("db Meter module is not connected/recognized at I2C addr = 0x48");
close(file);
exit(1);
}
while (1)
{
// Decibel value is stored in register 0x0A
data = 0x0A;
// Write: send 1 byte from data (memory address pointer) to file
if (write(file, &data, 1) != 1)
{
perror("Failed to write register 0x0A");
close(file);
exit(1);
}
// Read the sound level from register 0x0A
if (read(file, &data, 1) != 1)
{
perror("Failed to read register 0x0A");
close(file);
exit(1);
}
// Insert the new reading into the array
sum -= soundLevels[index]; // Subtract the old value to maintain the sum
soundLevels[index] = data; // Store the new value
sum += soundLevels[index]; // Add the new value to the sum
// Move to the next index, wrap around if needed
index = (index + 1) % MAX_MEASURES;
// Calculate the moving average (sum of the last 60 values)
int movingAverage = sum / MAX_MEASURES;
// Find the max and min values in the soundLevels array
int max = soundLevels[0];
int min = soundLevels[0];
for (i = 1; i < MAX_MEASURES; i++)
{
if (soundLevels[i] > max) {
max = soundLevels[i];
}
if (soundLevels[i] < min) {
min = soundLevels[i];
}
}
// Open the file to write the moving average (overwrite)
fileOutput = fopen("/var/www/nebuleair_pro_4g/sound_meter/moving_avg_minute.txt", "w");
if (fileOutput == NULL)
{
perror("Failed to open file for writing");
close(file);
exit(1);
}
// Write the moving average to the file
fprintf(fileOutput, "%d %d %d\n", movingAverage, max, min);
// Close the file after writing
fclose(fileOutput);
// Display the current sound level and the moving average
printf("Current Sound level: %d\n", data);
printf("Current index: %d\n", index);
printf("Sound levels array: ");
for (i = 0; i < MAX_MEASURES; i++)
{
printf("%d ", soundLevels[i]);
}
printf("\nMax Sound level: %d\n", max);
printf("Min Sound level: %d\n", min);
printf("Moving Average (Last 60 seconds): %d\n\n", movingAverage);
// Wait for 1 second before taking the next measurement
sleep(1);
}
close(file);
return 0;
}

BIN
sound_meter/sound_meter_nonStop Executable file

Binary file not shown.

View File

@@ -0,0 +1,75 @@
/*
Code pour lire la data du I2C decibel meter
Pour compiler le code: gcc -o sound_meter sound_meter.c
Connexion I2C:
SDA to Pi Pico GPIO 2
SCL to Pi Pico GPIO 3
Device connected on addr 0x48
0x0A : Decibel register (Latest sound intensity value in decibels, averaged over the last Tavg time period.)
0x0B : Min register (Minimum value of decibel reading captured since power-up or manual reset of MIN/MAX registers.)
0x0C : Max register (Maximum value of decibel reading captured since power-up or manual reset of MIN/MAX registers.)
#Le Tavg est programmé sur 2 octets: le high byte et le low byte.
valeur = (Tavg_high x 256) + Tavg_low
0x07 #Averaging time (high byte) in milliseconds for calculating sound intensity levels. (default 0x03)
0x08 #Averaging time (low byte) in milliseconds for calculating sound intensity levels. (default 0xE8)
*/
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <linux/i2c-dev.h>
#include <sys/ioctl.h>
int main()
{
int file;
char data;
// Open I2C1 for reading the sound meter module registers
if ((file = open("/dev/i2c-1", O_RDWR)) < 0)
{
perror("Failed to open I2C1!");
exit(1);
}
// 0x48 is the decibel meter I2C address
if (ioctl (file, I2C_SLAVE, 0x48) < 0)
{
perror ("db Meter module is not connected/recognized at I2C addr = 0x48");
close (file);
exit (1);
}
while (1)
{
// Decibel value is stored in register 0x0A
data = 0x0A;
//write: send 1 byte from data (memory address pointer) to file
if (write (file, &data, 1) != 1)
{
perror ("Failed to write register 0x0A");
close (file);
exit (1);
}
if (read (file, &data, 1) != 1)
{
perror ("Failed to read register 0x0A");
close (file);
exit (1);
}
printf ("Sound level: %d dB SPL\r\n\r\n", data);
sleep (1);
}
close (file);
return 0;
}

23
wifi_list.csv Executable file
View File

@@ -0,0 +1,23 @@
SSID,SIGNAL,SECURITY
ATMOSUD-EXT,94,WPA2
cPURE,85,WPA2
--,85,WPA2
ATMOSUD-EXT,84,WPA2
ATMOSUD_PRV,72,WPA1
ATMOSUD_PRV,70,WPA1
TP-Link_Extender,67,--
ATMOSUD_PRV,60,WPA1
DIRECT-N9-BRAVIA,59,WPA2
WIFI-ETUDE,57,WPA1
ATMOSUD-EXT,55,WPA1
ATMOSUD_PRV,52,WPA1
DIRECT-78-HP,OfficeJet,200
ATMOSUD-EXT,34,WPA2
ATMOSUD-EXT,32,WPA2
Invites,27,WPA2
Solidarite,Femmes,GParadis
Solidarite,Femmes,GParadis
Invites,19,WPA2
Invites,17,WPA2
--,17,WPA2
--,17,WPA2
1 SSID SIGNAL SECURITY
2 ATMOSUD-EXT 94 WPA2
3 cPURE 85 WPA2
4 -- 85 WPA2
5 ATMOSUD-EXT 84 WPA2
6 ATMOSUD_PRV 72 WPA1
7 ATMOSUD_PRV 70 WPA1
8 TP-Link_Extender 67 --
9 ATMOSUD_PRV 60 WPA1
10 DIRECT-N9-BRAVIA 59 WPA2
11 WIFI-ETUDE 57 WPA1
12 ATMOSUD-EXT 55 WPA1
13 ATMOSUD_PRV 52 WPA1
14 DIRECT-78-HP OfficeJet 200
15 ATMOSUD-EXT 34 WPA2
16 ATMOSUD-EXT 32 WPA2
17 Invites 27 WPA2
18 Solidarite Femmes GParadis
19 Solidarite Femmes GParadis
20 Invites 19 WPA2
21 Invites 17 WPA2
22 -- 17 WPA2
23 -- 17 WPA2