first commit
This commit is contained in:
9
.gitignore
vendored
Executable file
9
.gitignore
vendored
Executable 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
33
BME280/read.py
Executable 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
225
README.md
@@ -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
87
SARA/MQTT/get_config.py
Executable 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
90
SARA/MQTT/login_logout.py
Executable 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
89
SARA/MQTT/publish.py
Executable 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
94
SARA/MQTT/set_config.py
Executable 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
82
SARA/sara.py
Executable 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
70
SARA/sara_connectNetwork.py
Executable 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
65
SARA/sara_eraseMessage.py
Executable 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
62
SARA/sara_readMessage.py
Executable 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
63
SARA/sara_sendMessage.py
Executable 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
69
SARA/sara_setURL.py
Executable 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
71
SARA/sara_writeMessage.py
Executable 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
75
boot_hotspot.sh
Executable 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
38
config.json.dist
Executable 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
28
connexion.sh
Executable 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
9
cron_jobs
Executable 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
90
envea/read_ref.py
Executable 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
80
envea/read_value.py
Executable 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
116
html/admin.html
Executable 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
4085
html/assets/css/bootstrap-grid.css
vendored
Executable file
File diff suppressed because it is too large
Load Diff
1
html/assets/css/bootstrap-grid.css.map
Executable file
1
html/assets/css/bootstrap-grid.css.map
Executable file
File diff suppressed because one or more lines are too long
6
html/assets/css/bootstrap-grid.min.css
vendored
Executable file
6
html/assets/css/bootstrap-grid.min.css
vendored
Executable file
File diff suppressed because one or more lines are too long
1
html/assets/css/bootstrap-grid.min.css.map
Executable file
1
html/assets/css/bootstrap-grid.min.css.map
Executable file
File diff suppressed because one or more lines are too long
4084
html/assets/css/bootstrap-grid.rtl.css
vendored
Executable file
4084
html/assets/css/bootstrap-grid.rtl.css
vendored
Executable file
File diff suppressed because it is too large
Load Diff
1
html/assets/css/bootstrap-grid.rtl.css.map
Executable file
1
html/assets/css/bootstrap-grid.rtl.css.map
Executable file
File diff suppressed because one or more lines are too long
6
html/assets/css/bootstrap-grid.rtl.min.css
vendored
Executable file
6
html/assets/css/bootstrap-grid.rtl.min.css
vendored
Executable file
File diff suppressed because one or more lines are too long
1
html/assets/css/bootstrap-grid.rtl.min.css.map
Executable file
1
html/assets/css/bootstrap-grid.rtl.min.css.map
Executable file
File diff suppressed because one or more lines are too long
597
html/assets/css/bootstrap-reboot.css
vendored
Executable file
597
html/assets/css/bootstrap-reboot.css
vendored
Executable 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 */
|
||||||
1
html/assets/css/bootstrap-reboot.css.map
Executable file
1
html/assets/css/bootstrap-reboot.css.map
Executable file
File diff suppressed because one or more lines are too long
6
html/assets/css/bootstrap-reboot.min.css
vendored
Executable file
6
html/assets/css/bootstrap-reboot.min.css
vendored
Executable file
File diff suppressed because one or more lines are too long
1
html/assets/css/bootstrap-reboot.min.css.map
Executable file
1
html/assets/css/bootstrap-reboot.min.css.map
Executable file
File diff suppressed because one or more lines are too long
594
html/assets/css/bootstrap-reboot.rtl.css
vendored
Executable file
594
html/assets/css/bootstrap-reboot.rtl.css
vendored
Executable 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 */
|
||||||
1
html/assets/css/bootstrap-reboot.rtl.css.map
Executable file
1
html/assets/css/bootstrap-reboot.rtl.css.map
Executable file
File diff suppressed because one or more lines are too long
6
html/assets/css/bootstrap-reboot.rtl.min.css
vendored
Executable file
6
html/assets/css/bootstrap-reboot.rtl.min.css
vendored
Executable file
File diff suppressed because one or more lines are too long
1
html/assets/css/bootstrap-reboot.rtl.min.css.map
Executable file
1
html/assets/css/bootstrap-reboot.rtl.min.css.map
Executable file
File diff suppressed because one or more lines are too long
5402
html/assets/css/bootstrap-utilities.css
vendored
Executable file
5402
html/assets/css/bootstrap-utilities.css
vendored
Executable file
File diff suppressed because it is too large
Load Diff
1
html/assets/css/bootstrap-utilities.css.map
Executable file
1
html/assets/css/bootstrap-utilities.css.map
Executable file
File diff suppressed because one or more lines are too long
6
html/assets/css/bootstrap-utilities.min.css
vendored
Executable file
6
html/assets/css/bootstrap-utilities.min.css
vendored
Executable file
File diff suppressed because one or more lines are too long
1
html/assets/css/bootstrap-utilities.min.css.map
Executable file
1
html/assets/css/bootstrap-utilities.min.css.map
Executable file
File diff suppressed because one or more lines are too long
5393
html/assets/css/bootstrap-utilities.rtl.css
vendored
Executable file
5393
html/assets/css/bootstrap-utilities.rtl.css
vendored
Executable file
File diff suppressed because it is too large
Load Diff
1
html/assets/css/bootstrap-utilities.rtl.css.map
Executable file
1
html/assets/css/bootstrap-utilities.rtl.css.map
Executable file
File diff suppressed because one or more lines are too long
6
html/assets/css/bootstrap-utilities.rtl.min.css
vendored
Executable file
6
html/assets/css/bootstrap-utilities.rtl.min.css
vendored
Executable file
File diff suppressed because one or more lines are too long
1
html/assets/css/bootstrap-utilities.rtl.min.css.map
Executable file
1
html/assets/css/bootstrap-utilities.rtl.min.css.map
Executable file
File diff suppressed because one or more lines are too long
12057
html/assets/css/bootstrap.css
vendored
Executable file
12057
html/assets/css/bootstrap.css
vendored
Executable file
File diff suppressed because it is too large
Load Diff
1
html/assets/css/bootstrap.css.map
Executable file
1
html/assets/css/bootstrap.css.map
Executable file
File diff suppressed because one or more lines are too long
6
html/assets/css/bootstrap.min.css
vendored
Executable file
6
html/assets/css/bootstrap.min.css
vendored
Executable file
File diff suppressed because one or more lines are too long
1
html/assets/css/bootstrap.min.css.map
Executable file
1
html/assets/css/bootstrap.min.css.map
Executable file
File diff suppressed because one or more lines are too long
12030
html/assets/css/bootstrap.rtl.css
vendored
Executable file
12030
html/assets/css/bootstrap.rtl.css
vendored
Executable file
File diff suppressed because it is too large
Load Diff
1
html/assets/css/bootstrap.rtl.css.map
Executable file
1
html/assets/css/bootstrap.rtl.css.map
Executable file
File diff suppressed because one or more lines are too long
6
html/assets/css/bootstrap.rtl.min.css
vendored
Executable file
6
html/assets/css/bootstrap.rtl.min.css
vendored
Executable file
File diff suppressed because one or more lines are too long
1
html/assets/css/bootstrap.rtl.min.css.map
Executable file
1
html/assets/css/bootstrap.rtl.min.css.map
Executable file
File diff suppressed because one or more lines are too long
4
html/assets/icons/arrow-clockwise.svg
Executable file
4
html/assets/icons/arrow-clockwise.svg
Executable 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 |
32
html/assets/icons/bootstrap-icons.svg
Executable file
32
html/assets/icons/bootstrap-icons.svg
Executable 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 |
4
html/assets/icons/bootstrap-icons2.svg
Executable file
4
html/assets/icons/bootstrap-icons2.svg
Executable 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
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
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
6314
html/assets/js/bootstrap.bundle.js
vendored
Executable file
File diff suppressed because it is too large
Load Diff
1
html/assets/js/bootstrap.bundle.js.map
Executable file
1
html/assets/js/bootstrap.bundle.js.map
Executable file
File diff suppressed because one or more lines are too long
7
html/assets/js/bootstrap.bundle.min.js
vendored
Executable file
7
html/assets/js/bootstrap.bundle.min.js
vendored
Executable file
File diff suppressed because one or more lines are too long
1
html/assets/js/bootstrap.bundle.min.js.map
Executable file
1
html/assets/js/bootstrap.bundle.min.js.map
Executable file
File diff suppressed because one or more lines are too long
4447
html/assets/js/bootstrap.esm.js
vendored
Executable file
4447
html/assets/js/bootstrap.esm.js
vendored
Executable file
File diff suppressed because it is too large
Load Diff
1
html/assets/js/bootstrap.esm.js.map
Executable file
1
html/assets/js/bootstrap.esm.js.map
Executable file
File diff suppressed because one or more lines are too long
7
html/assets/js/bootstrap.esm.min.js
vendored
Executable file
7
html/assets/js/bootstrap.esm.min.js
vendored
Executable file
File diff suppressed because one or more lines are too long
1
html/assets/js/bootstrap.esm.min.js.map
Executable file
1
html/assets/js/bootstrap.esm.min.js.map
Executable file
File diff suppressed because one or more lines are too long
4494
html/assets/js/bootstrap.js
vendored
Executable file
4494
html/assets/js/bootstrap.js
vendored
Executable file
File diff suppressed because it is too large
Load Diff
1
html/assets/js/bootstrap.js.map
Executable file
1
html/assets/js/bootstrap.js.map
Executable file
File diff suppressed because one or more lines are too long
7
html/assets/js/bootstrap.min.js
vendored
Executable file
7
html/assets/js/bootstrap.min.js
vendored
Executable file
File diff suppressed because one or more lines are too long
1
html/assets/js/bootstrap.min.js.map
Executable file
1
html/assets/js/bootstrap.min.js.map
Executable file
File diff suppressed because one or more lines are too long
89
html/index.html
Executable file
89
html/index.html
Executable 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
278
html/launcher.php
Executable 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
168
html/logs.html
Executable 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
628
html/saraR4.html
Executable 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
370
html/sensors.html
Executable 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
44
html/sidebar.html
Executable 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
14
html/topbar.html
Executable 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
88
html/wifi.html
Executable 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
1235
index.html
Executable file
File diff suppressed because it is too large
Load Diff
37
install_software.yaml
Executable file
37
install_software.yaml
Executable 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
46
installation.sh
Executable 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
262
launcher.php
Executable 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
405
loop/1_NPM/send_data.py
Executable 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
312
loop/1_NPM/send_data_mqtt.py
Executable 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
6
loop/3_NPM/data.json
Executable 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
101
loop/3_NPM/get_data.py
Executable 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}")
|
||||||
182
loop/3_NPM/get_data_closest_pair.py
Executable file
182
loop/3_NPM/get_data_closest_pair.py
Executable 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
142
loop/3_NPM/send_data.py
Executable 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
69
npm.py
Executable 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
BIN
sound_meter/sound_meter
Executable file
Binary file not shown.
91
sound_meter/sound_meter.c
Executable file
91
sound_meter/sound_meter.c
Executable 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;
|
||||||
|
}
|
||||||
BIN
sound_meter/sound_meter_moving_avg
Executable file
BIN
sound_meter/sound_meter_moving_avg
Executable file
Binary file not shown.
122
sound_meter/sound_meter_moving_avg.c
Executable file
122
sound_meter/sound_meter_moving_avg.c
Executable 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
BIN
sound_meter/sound_meter_nonStop
Executable file
Binary file not shown.
75
sound_meter/sound_meter_nonStop.c
Executable file
75
sound_meter/sound_meter_nonStop.c
Executable 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
23
wifi_list.csv
Executable 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
|
||||||
|
Reference in New Issue
Block a user