Compare commits
375 Commits
709a1e5dd5
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a68af89612 | ||
|
|
7045adc7a6 | ||
|
|
c062263b24 | ||
|
|
9f76e3b2de | ||
|
|
0ed18dd5c1 | ||
|
|
cf10d20db5 | ||
|
|
3f7d0c0816 | ||
|
|
47d76be5df | ||
|
|
11585b4783 | ||
|
|
52b86dbc3d | ||
|
|
361c0d1a76 | ||
|
|
bd2e1f1eda | ||
|
|
2b4e9205c1 | ||
|
|
b3c019c27b | ||
|
|
e733cd27e8 | ||
|
|
a9db7750b2 | ||
|
|
c42656e0ae | ||
|
|
eb93ba49bd | ||
|
|
3804a52fda | ||
|
|
ee0577c504 | ||
|
|
72fbbb82a1 | ||
|
|
5b3769769d | ||
|
|
6be18b5bde | ||
|
|
7619caffc4 | ||
|
|
85596c3882 | ||
|
|
6a00ab85d9 | ||
|
|
2ff47dc877 | ||
|
|
d2a3eafaa1 | ||
|
|
6706b22f21 | ||
|
|
ffe13d3639 | ||
|
|
7b324f8ab8 | ||
|
|
ad0f83ce71 | ||
|
|
928c1a1d4e | ||
|
|
24cb96e9a9 | ||
|
|
e2f765de8a | ||
|
|
cb98e38a3e | ||
|
|
4fe79ad112 | ||
|
|
b869ac3e9e | ||
|
|
c09fa3ca72 | ||
|
|
79d9be2c85 | ||
|
|
903dcce2d7 | ||
|
|
425a89de3f | ||
|
|
8f88eae575 | ||
|
|
ffead8597a | ||
|
|
196176667f | ||
|
|
87ddb76e39 | ||
|
|
dbe6c71d33 | ||
|
|
537abb682e | ||
|
|
8d74e3e678 | ||
|
|
5849190220 | ||
|
|
408ab767e1 | ||
|
|
2949c78b56 | ||
|
|
c83f8396aa | ||
|
|
ecd59e537e | ||
|
|
83d854b596 | ||
|
|
a0f8b4b8eb | ||
|
|
8d0507852a | ||
|
|
6e17f39a2c | ||
|
|
5a2b3bb19d | ||
|
|
5bffec10a1 | ||
|
|
e0e8a4cefe | ||
|
|
d5b2e9c6c3 | ||
|
|
7ab06f3413 | ||
|
|
794b86fb9b | ||
|
|
7479344df7 | ||
|
|
98b5b43190 | ||
|
|
1298e79688 | ||
|
|
7a7d1c0c3f | ||
|
|
7c30ccd8f7 | ||
|
|
bc2aec7946 | ||
|
|
6b8d0c18c9 | ||
|
|
b65e9571dc | ||
|
|
e8cef5b593 | ||
|
|
36d4bac0a5 | ||
|
|
a208540093 | ||
|
|
02687f6d74 | ||
|
|
8c55798e34 | ||
|
|
cf502abfef | ||
|
|
e659696044 | ||
|
|
d086a440dd | ||
|
|
86c2d1eb41 | ||
|
|
aa1b90e3d5 | ||
|
|
f1d716d900 | ||
|
|
248732bac9 | ||
|
|
7b0fb0650a | ||
|
|
3e5ee9c77e | ||
|
|
8106af624f | ||
|
|
30bc04b89e | ||
|
|
198836fa13 | ||
|
|
ea2642685c | ||
|
|
f8f5300b9b | ||
|
|
b88d2bc1d9 | ||
|
|
88680f07b0 | ||
|
|
20c6a12251 | ||
|
|
e20bb0b8fc | ||
|
|
50a8cdd938 | ||
|
|
dc1739e033 | ||
|
|
544eebd715 | ||
|
|
6bdaef8c24 | ||
|
|
98cb1ea517 | ||
|
|
4ed185de0c | ||
|
|
3d61ce22d3 | ||
|
|
3a6b529cba | ||
|
|
3c8558ea1d | ||
|
|
49a4623d85 | ||
|
|
d3d72410c1 | ||
|
|
fcfbe4f2d4 | ||
|
|
53e7c77322 | ||
|
|
20ba897cde | ||
|
|
15e43513f4 | ||
|
|
a5717df182 | ||
|
|
8aaed1b93f | ||
|
|
1fa7a2d695 | ||
|
|
80bc16fb26 | ||
|
|
042b2efa93 | ||
|
|
80fcd8bf37 | ||
|
|
b60f044105 | ||
|
|
eeaaeca4a7 | ||
|
|
91a4e7c841 | ||
|
|
8291475e36 | ||
|
|
994bbf7a8d | ||
|
|
e5770b09dc | ||
|
|
79d7f61e4a | ||
|
|
d593449171 | ||
|
|
c571bbd408 | ||
|
|
5742cc7e49 | ||
|
|
4c552e4a31 | ||
|
|
5777b35770 | ||
|
|
13445d574c | ||
|
|
10f84f0c1b | ||
|
|
4f1b140a75 | ||
|
|
a3b2bef5c1 | ||
|
|
f679732591 | ||
|
|
857d590b8f | ||
|
|
141dd68716 | ||
|
|
79a9217307 | ||
|
|
fe604791f0 | ||
|
|
624fb4abbc | ||
|
|
163d60bf34 | ||
|
|
906eaa851d | ||
|
|
954680ef6e | ||
|
|
1f4d38257e | ||
|
|
a38ce79555 | ||
|
|
62ef47aa67 | ||
|
|
ca7533a344 | ||
|
|
403c57bf18 | ||
|
|
129b2de68e | ||
|
|
d2c88e0d18 | ||
|
|
fba5af53cb | ||
|
|
04fbf81798 | ||
|
|
65beead82b | ||
|
|
26ee893a96 | ||
|
|
5cf37c3cee | ||
|
|
3ecc27fd3e | ||
|
|
072fca72cc | ||
|
|
c038084343 | ||
|
|
6069ab04cf | ||
|
|
79f3ede17f | ||
|
|
9de903f2db | ||
|
|
77fcdaa08e | ||
|
|
1fca3091eb | ||
|
|
d0b49bf30c | ||
|
|
4779f426d9 | ||
|
|
9aab95edb6 | ||
|
|
fe61b56b5b | ||
|
|
25c5a7a65a | ||
|
|
4d512685a0 | ||
|
|
44b2e2189d | ||
|
|
74fc3baece | ||
|
|
0539cb67af | ||
|
|
98115ab22b | ||
|
|
2989a7a9ed | ||
|
|
aa458fbac4 | ||
| 707dffd6f8 | |||
| c917131b2d | |||
|
|
057dc7d87b | ||
|
|
fcc30243f5 | ||
|
|
75774cea62 | ||
|
|
3731c2b7cf | ||
|
|
1240ebf6cd | ||
|
|
e27f2430b7 | ||
|
|
ebdc4ae353 | ||
|
|
6cd5191138 | ||
|
|
8d989de425 | ||
|
|
381cf85336 | ||
|
|
caf5488b06 | ||
|
|
5d4f7225b0 | ||
|
|
6d997ff550 | ||
|
|
aa71e359bb | ||
|
|
7bd1d81bf9 | ||
|
|
4bc0dc2acc | ||
|
|
694edfaf27 | ||
|
|
93d77db853 | ||
|
|
122763a4e5 | ||
|
|
c6a8b02c38 | ||
|
|
b93f205fd4 | ||
|
|
8fdd1d6ac5 | ||
|
|
6796aa95bb | ||
|
|
020594e065 | ||
|
|
5a1a4e0d81 | ||
|
|
3cd5b13c25 | ||
|
|
5a0f1c0745 | ||
|
|
2516a3bd1c | ||
|
|
1b8dc54fe0 | ||
|
|
2bd74ca91a | ||
|
|
f40c105abf | ||
|
|
fdef8e2df0 | ||
|
|
386ad6fb03 | ||
|
|
a7c138e93f | ||
|
|
4e4832b128 | ||
|
|
11463b175c | ||
|
|
c06741b11d | ||
|
|
b1352261e7 | ||
|
|
376ff454bf | ||
|
|
932fdf83a2 | ||
|
|
1ca3e2ada2 | ||
|
|
fd1d32a62b | ||
|
|
61b302fe35 | ||
|
|
2aaa229e82 | ||
|
|
fd28069b0c | ||
|
|
b17c996f2f | ||
|
|
8273307cab | ||
|
|
a73eb30d32 | ||
|
|
ba889feee9 | ||
|
|
12c7a0b6af | ||
|
|
08c5ed8841 | ||
|
|
7f5eb7608c | ||
|
|
44f44c3361 | ||
|
|
a8350332ac | ||
|
|
6c6eed1ad6 | ||
|
|
ee71c28d33 | ||
|
|
6d3220665e | ||
|
|
98e5a239f5 | ||
|
|
17f4ce46dd | ||
|
|
338b8a049f | ||
|
|
1e9e80ae55 | ||
|
|
9d280c6e37 | ||
|
|
d4c1178b3d | ||
|
|
f7f6fccd60 | ||
|
|
afceb34c1b | ||
|
|
7a958d5c8e | ||
|
|
8fd76001f2 | ||
|
|
e320a3bc2b | ||
|
|
8a4e184699 | ||
|
|
e61b0a76da | ||
|
|
970a36598c | ||
|
|
e75caff929 | ||
|
|
e82d75a4d6 | ||
|
|
dc27e5f139 | ||
|
|
4bc05091be | ||
|
|
29f9ec445a | ||
|
|
7b398d0d6d | ||
|
|
76336d0073 | ||
|
|
46a8e21e64 | ||
|
|
2129d45ef6 | ||
|
|
6312cd8d72 | ||
|
|
7c17ec82f5 | ||
|
|
b7a6f4c907 | ||
|
|
6b3329b9b8 | ||
|
|
e9b1e0e88e | ||
|
|
2db732ebb3 | ||
|
|
d5302f78ba | ||
|
|
5b7de91d50 | ||
|
|
4d15076d4b | ||
|
|
809742b6d5 | ||
|
|
bca975b0c5 | ||
|
|
dfba956685 | ||
|
|
d07314262e | ||
|
|
dffa639574 | ||
|
|
1fd5a3e75c | ||
|
|
e674b21eaa | ||
|
|
efc94ba5e1 | ||
|
|
26328dec99 | ||
|
|
ec3e81e99e | ||
|
|
1c6af36313 | ||
|
|
f1d6f595ac | ||
|
|
cfc2e0c47f | ||
|
|
1037207df3 | ||
|
|
14044a8856 | ||
|
|
d57a47ef68 | ||
|
|
5e7375cd4e | ||
|
|
c42b16ddb6 | ||
|
|
283a46eb0b | ||
|
|
33b24a9f53 | ||
|
|
10c4348e54 | ||
|
|
072f98ef95 | ||
|
|
7b4ff011ec | ||
|
|
ab2124f50d | ||
|
|
b493d30a41 | ||
|
|
659effb7c4 | ||
|
|
ebb0fd0a2b | ||
|
|
5d121761e7 | ||
|
|
d90fb14c90 | ||
|
|
3f329e0afa | ||
|
|
cd030a9e14 | ||
|
|
709cad6981 | ||
|
|
c59246e320 | ||
|
|
1a15d70aa7 | ||
|
|
bf9ece8589 | ||
|
|
3d507ae659 | ||
|
|
700de9c9f4 | ||
|
|
ec6fbf6bb2 | ||
|
|
8fecde5d56 | ||
|
|
49e93ab3ad | ||
|
|
e0d7614ad8 | ||
|
|
516f6367fa | ||
|
|
0549471669 | ||
|
|
20a0786380 | ||
|
|
92ec2a0bb9 | ||
|
|
c7fb474f66 | ||
|
|
8c5d831878 | ||
|
|
b3f5ee9795 | ||
|
|
4f7a704779 | ||
|
|
b5aeafeb56 | ||
|
|
144d904813 | ||
|
|
e3607143a1 | ||
|
|
7e8bf1294c | ||
|
|
eea1acd701 | ||
|
|
accfd3e371 | ||
|
|
dfbae99ba5 | ||
|
|
4c4c6ce77e | ||
|
|
c4fb7aed72 | ||
|
|
cee6c7f79b | ||
|
|
8fb1882864 | ||
|
|
67fcd78aac | ||
|
|
727fa4cfeb | ||
|
|
6622be14ad | ||
|
|
9774215e7c | ||
|
|
ecd61f765a | ||
|
|
62c729b63b | ||
|
|
e609c38ca0 | ||
|
|
1cb1b05b51 | ||
|
|
7cac769795 | ||
|
|
fb44b57ac1 | ||
|
|
d98eb48535 | ||
|
|
46303b9c19 | ||
|
|
49be391eb3 | ||
|
|
268a0586b8 | ||
|
|
7de382a43d | ||
|
|
c3e2866fab | ||
|
|
a90552148c | ||
|
|
c6073b49b9 | ||
|
|
aaeb20aece | ||
|
|
10cc46a079 | ||
|
|
b8150535e8 | ||
|
|
ef2bd6b895 | ||
|
|
456db5da98 | ||
|
|
c15838fc47 | ||
|
|
d732f3ad2d | ||
|
|
437be5cad9 | ||
|
|
4f59928b1e | ||
|
|
a689a7d2de | ||
|
|
970f62658b | ||
|
|
6e0dc1b257 | ||
|
|
578721a9f2 | ||
|
|
a8ca15505e | ||
|
|
280dcd9be3 | ||
|
|
4a5e0b3577 | ||
|
|
d095e53cd6 | ||
|
|
083d342373 | ||
|
|
c8b9cb46f6 | ||
|
|
4123f977b2 | ||
|
|
833ed458a7 | ||
|
|
155a2bd453 | ||
|
|
660af80ab0 | ||
|
|
8596690f32 | ||
|
|
bd902a0c46 | ||
|
|
545f5f8f3a | ||
|
|
0fb7118abb | ||
|
|
aa3b4d238b | ||
|
|
838d6d7357 | ||
|
|
806576b8b8 | ||
|
|
2650954ecc | ||
|
|
78339fa585 | ||
|
|
3ea7b16b1a |
21
.claude/README.md
Normal file
21
.claude/README.md
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
# Claude Code Settings
|
||||||
|
|
||||||
|
This directory contains configuration for Claude Code.
|
||||||
|
|
||||||
|
## Files
|
||||||
|
|
||||||
|
- **settings.json**: Project-wide default settings (tracked in git)
|
||||||
|
- **settings.local.json**: Local user-specific overrides (not tracked in git)
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
The `settings.json` file contains the default configuration that applies to all developers/devices. If you need to customize settings for your local environment, create a `settings.local.json` file which will override the defaults.
|
||||||
|
|
||||||
|
### Example: Create local overrides
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp .claude/settings.json .claude/settings.local.json
|
||||||
|
# Edit settings.local.json with your preferences
|
||||||
|
```
|
||||||
|
|
||||||
|
Your local changes will not be committed to git.
|
||||||
9
.claude/settings.json
Normal file
9
.claude/settings.json
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"permissions": {
|
||||||
|
"allow": [
|
||||||
|
"Bash(python3:*)"
|
||||||
|
],
|
||||||
|
"deny": []
|
||||||
|
},
|
||||||
|
"enableAllProjectMcpServers": false
|
||||||
|
}
|
||||||
16
.gitignore
vendored
16
.gitignore
vendored
@@ -1,4 +1,4 @@
|
|||||||
logs/app.log
|
logs/*.log
|
||||||
logs/loop.log
|
logs/loop.log
|
||||||
deviceID.txt
|
deviceID.txt
|
||||||
loop/loop.log
|
loop/loop.log
|
||||||
@@ -9,3 +9,17 @@ config.json
|
|||||||
sound_meter/moving_avg_minute.txt
|
sound_meter/moving_avg_minute.txt
|
||||||
wifi_list.csv
|
wifi_list.csv
|
||||||
envea/data/*.txt
|
envea/data/*.txt
|
||||||
|
envea/data/*.json
|
||||||
|
NPM/data/*.txt
|
||||||
|
NPM/data/*.json
|
||||||
|
*.lock
|
||||||
|
sqlite/*.db
|
||||||
|
sqlite/*.sql
|
||||||
|
|
||||||
|
tests/
|
||||||
|
|
||||||
|
# Secrets
|
||||||
|
.env
|
||||||
|
|
||||||
|
# Claude Code local settings
|
||||||
|
.claude/settings.local.json
|
||||||
26
.update-exclude
Normal file
26
.update-exclude
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
# NebuleAir Pro 4G - Fichiers exclus lors de la mise à jour par upload
|
||||||
|
# Ce fichier est versionné dans le repo et voyage avec chaque release.
|
||||||
|
# Quand on ajoute un nouveau capteur avec du cache local, mettre à jour cette liste.
|
||||||
|
|
||||||
|
# Base de données (données capteur, config locale)
|
||||||
|
sqlite/sensors.db
|
||||||
|
sqlite/*.db-journal
|
||||||
|
sqlite/*.db-wal
|
||||||
|
|
||||||
|
# Logs applicatifs
|
||||||
|
logs/
|
||||||
|
|
||||||
|
# Historique git (pour que git pull fonctionne toujours après)
|
||||||
|
.git/
|
||||||
|
|
||||||
|
# Fichiers de configuration locale
|
||||||
|
config.json
|
||||||
|
deviceID.txt
|
||||||
|
wifi_list.csv
|
||||||
|
|
||||||
|
# Données capteurs en cache
|
||||||
|
envea/data/
|
||||||
|
NPM/data/
|
||||||
|
|
||||||
|
# Verrous
|
||||||
|
*.lock
|
||||||
74
BME280/get_data_v2.py
Executable file
74
BME280/get_data_v2.py
Executable file
@@ -0,0 +1,74 @@
|
|||||||
|
'''
|
||||||
|
____ __ __ _____ ____ ___ ___
|
||||||
|
| __ )| \/ | ____|___ \( _ ) / _ \
|
||||||
|
| _ \| |\/| | _| __) / _ \| | | |
|
||||||
|
| |_) | | | | |___ / __/ (_) | |_| |
|
||||||
|
|____/|_| |_|_____|_____\___/ \___/
|
||||||
|
|
||||||
|
Script to read data from BME280
|
||||||
|
Sensor connected to i2c on address 76 (use sudo i2cdetect -y 1 to get the address )
|
||||||
|
-> save data to database (table data_BME280 )
|
||||||
|
sudo python3 /var/www/nebuleair_pro_4g/BME280/get_data_v2.py
|
||||||
|
|
||||||
|
'''
|
||||||
|
|
||||||
|
import board
|
||||||
|
import busio
|
||||||
|
import json
|
||||||
|
import sqlite3
|
||||||
|
from adafruit_bme280 import basic as adafruit_bme280
|
||||||
|
|
||||||
|
# Connect to the SQLite database
|
||||||
|
conn = sqlite3.connect("/var/www/nebuleair_pro_4g/sqlite/sensors.db")
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
# Create I2C bus
|
||||||
|
i2c = busio.I2C(board.SCL, board.SDA)
|
||||||
|
bme280 = adafruit_bme280.Adafruit_BME280_I2C(i2c, address=0x76)
|
||||||
|
|
||||||
|
# Configure settings
|
||||||
|
bme280.sea_level_pressure = 1013.25 # Update this value for your location
|
||||||
|
|
||||||
|
# Read sensor data
|
||||||
|
|
||||||
|
#print(f"Temperature: {bme280.temperature:.2f} °C")
|
||||||
|
#print(f"Humidity: {bme280.humidity:.2f} %")
|
||||||
|
#print(f"Pressure: {bme280.pressure:.2f} hPa")
|
||||||
|
#print(f"Altitude: {bme280.altitude:.2f} m")
|
||||||
|
|
||||||
|
temperature = round(bme280.temperature, 2)
|
||||||
|
humidity = round(bme280.humidity, 2)
|
||||||
|
pressure = round(bme280.pressure, 2)
|
||||||
|
|
||||||
|
sensor_data = {
|
||||||
|
"temp": temperature, # Temperature in °C
|
||||||
|
"hum": humidity, # Humidity in %
|
||||||
|
"press": pressure # Pressure in hPa
|
||||||
|
}
|
||||||
|
|
||||||
|
#GET RTC TIME from SQlite
|
||||||
|
cursor.execute("SELECT * FROM timestamp_table LIMIT 1")
|
||||||
|
row = cursor.fetchone() # Get the first (and only) row
|
||||||
|
rtc_time_str = row[1] # '2025-02-07 12:30:45'
|
||||||
|
|
||||||
|
|
||||||
|
# Convert to JSON and print
|
||||||
|
#print(json.dumps(sensor_data, indent=4))
|
||||||
|
|
||||||
|
|
||||||
|
#save to sqlite database
|
||||||
|
try:
|
||||||
|
cursor.execute('''
|
||||||
|
INSERT INTO data_BME280 (timestamp,temperature, humidity, pressure) VALUES (?,?,?,?)'''
|
||||||
|
, (rtc_time_str,temperature,humidity,pressure))
|
||||||
|
|
||||||
|
# Commit and close the connection
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
#print("Sensor data saved successfully!")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Database error: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
conn.close()
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
# Script to read data from BME280
|
# Script to read data from BME280
|
||||||
# Sensor connected to i2c on address 77 (use sudo i2cdetect -y 1 to get the address )
|
# Sensor connected to i2c on address 76 (use sudo i2cdetect -y 1 to get the address )
|
||||||
# sudo python3 /var/www/nebuleair_pro_4g/BME280/read.py
|
# sudo python3 /var/www/nebuleair_pro_4g/BME280/read.py
|
||||||
|
|
||||||
import board
|
import board
|
||||||
|
|||||||
270
CLAUDE.md
Normal file
270
CLAUDE.md
Normal file
@@ -0,0 +1,270 @@
|
|||||||
|
# CLAUDE.md
|
||||||
|
|
||||||
|
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||||
|
|
||||||
|
## Project Overview
|
||||||
|
|
||||||
|
NebuleAir Pro 4G is an environmental monitoring system running on Raspberry Pi 4/CM4. It collects air quality and environmental data from multiple sensors and transmits it via 4G cellular modem. The system includes a self-hosted web interface for configuration and monitoring.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
### Data Flow
|
||||||
|
1. **Data Collection**: Sensors are polled by individual Python scripts triggered by systemd timers
|
||||||
|
2. **Local Storage**: All sensor data is stored in SQLite database (`sqlite/sensors.db`)
|
||||||
|
3. **Data Transmission**: Main loop script reads aggregated data from SQLite and transmits via SARA R4/R5 4G modem
|
||||||
|
4. **Web Interface**: Apache serves PHP pages that interact with SQLite and execute Python scripts
|
||||||
|
|
||||||
|
### Key Components
|
||||||
|
|
||||||
|
**Sensors & Hardware:**
|
||||||
|
- NextPM (NPM): Particulate matter sensor via Modbus (ttyAMA0)
|
||||||
|
- Envea CAIRSENS: Gas sensors (NO2, H2S, NH3, CO, O3) via serial (ttyAMA2-5)
|
||||||
|
- BME280: Temperature, humidity, pressure via I2C (0x76)
|
||||||
|
- NSRT MK4: Noise sensor via I2C (0x48)
|
||||||
|
- SARA R4/R5: 4G cellular modem (ttyAMA2)
|
||||||
|
- Wind meter: via ADS1115 ADC
|
||||||
|
- MPPT: Solar charger monitoring
|
||||||
|
|
||||||
|
**Software Stack:**
|
||||||
|
- OS: Raspberry Pi OS (Linux)
|
||||||
|
- Web server: Apache2
|
||||||
|
- Database: SQLite3
|
||||||
|
- Languages: Python 3, PHP, Bash
|
||||||
|
- Network: NetworkManager (nmcli)
|
||||||
|
|
||||||
|
### Directory Structure
|
||||||
|
- `NPM/`: NextPM sensor scripts
|
||||||
|
- `envea/`: Envea sensor scripts
|
||||||
|
- `BME280/`: BME280 sensor scripts
|
||||||
|
- `sound_meter/`: Noise sensor code (C program)
|
||||||
|
- `SARA/`: 4G modem communication (AT commands)
|
||||||
|
- `windMeter/`: Wind sensor scripts
|
||||||
|
- `MPPT/`: Solar charger scripts
|
||||||
|
- `RTC/`: DS3231 real-time clock module scripts
|
||||||
|
- `sqlite/`: Database scripts (create, read, write, config)
|
||||||
|
- `loop/`: Main data transmission script
|
||||||
|
- `html/`: Web interface files
|
||||||
|
- `services/`: Systemd service/timer configuration
|
||||||
|
- `logs/`: Application logs
|
||||||
|
|
||||||
|
## Common Development Commands
|
||||||
|
|
||||||
|
### Database Operations
|
||||||
|
```bash
|
||||||
|
# Initialize database
|
||||||
|
/usr/bin/python3 /var/www/nebuleair_pro_4g/sqlite/create_db.py
|
||||||
|
|
||||||
|
# Set configuration
|
||||||
|
/usr/bin/python3 /var/www/nebuleair_pro_4g/sqlite/set_config.py
|
||||||
|
|
||||||
|
# Read data from specific table
|
||||||
|
/usr/bin/python3 /var/www/nebuleair_pro_4g/sqlite/read.py <table_name> <limit>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Systemd Services
|
||||||
|
The system uses systemd timers for scheduling sensor data collection:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# View all NebuleAir timers
|
||||||
|
systemctl list-timers | grep nebuleair
|
||||||
|
|
||||||
|
# Check specific service status
|
||||||
|
systemctl status nebuleair-npm-data.service
|
||||||
|
systemctl status nebuleair-sara-data.service
|
||||||
|
|
||||||
|
# View service logs
|
||||||
|
journalctl -u nebuleair-npm-data.service
|
||||||
|
journalctl -u nebuleair-sara-data.service -f # follow
|
||||||
|
|
||||||
|
# Restart services
|
||||||
|
systemctl restart nebuleair-npm-data.timer
|
||||||
|
systemctl restart nebuleair-sara-data.timer
|
||||||
|
|
||||||
|
# Setup all services
|
||||||
|
sudo /var/www/nebuleair_pro_4g/services/setup_services.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
**Service Schedule:**
|
||||||
|
- `nebuleair-npm-data.timer`: Every 10 seconds (NextPM sensor)
|
||||||
|
- `nebuleair-envea-data.timer`: Every 10 seconds (Envea sensors)
|
||||||
|
- `nebuleair-sara-data.timer`: Every 60 seconds (4G data transmission)
|
||||||
|
- `nebuleair-bme280-data.timer`: Every 120 seconds (BME280 sensor)
|
||||||
|
- `nebuleair-mppt-data.timer`: Every 120 seconds (MPPT charger)
|
||||||
|
- `nebuleair-noise-data.timer`: Every 60 seconds (Noise sensor)
|
||||||
|
- `nebuleair-db-cleanup-data.timer`: Daily (database cleanup)
|
||||||
|
|
||||||
|
### Sensor Testing
|
||||||
|
```bash
|
||||||
|
# Test NextPM sensor
|
||||||
|
/usr/bin/python3 /var/www/nebuleair_pro_4g/NPM/get_data_modbus_v3.py
|
||||||
|
|
||||||
|
# Test Envea sensors (read reference/ID)
|
||||||
|
/usr/bin/python3 /var/www/nebuleair_pro_4g/envea/read_ref_v2.py
|
||||||
|
|
||||||
|
# Test Envea sensor values
|
||||||
|
/usr/bin/python3 /var/www/nebuleair_pro_4g/envea/read_value_v2.py
|
||||||
|
|
||||||
|
# Test BME280
|
||||||
|
/usr/bin/python3 /var/www/nebuleair_pro_4g/BME280/get_data_v2.py
|
||||||
|
|
||||||
|
# Test noise sensor
|
||||||
|
/usr/bin/python3 /var/www/nebuleair_pro_4g/sound_meter/NSRT_mk4_get_data.py
|
||||||
|
|
||||||
|
# Test RTC module
|
||||||
|
/usr/bin/python3 /var/www/nebuleair_pro_4g/RTC/read.py
|
||||||
|
|
||||||
|
# Test MPPT charger
|
||||||
|
/usr/bin/python3 /var/www/nebuleair_pro_4g/MPPT/read.py
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4G Modem (SARA R4/R5)
|
||||||
|
```bash
|
||||||
|
# Send AT command
|
||||||
|
/usr/bin/python3 /var/www/nebuleair_pro_4g/SARA/sara.py ttyAMA2 "AT+CSQ" 5
|
||||||
|
|
||||||
|
# Check network connection
|
||||||
|
/usr/bin/python3 /var/www/nebuleair_pro_4g/SARA/sara_connectNetwork.py ttyAMA2 <networkID> 120
|
||||||
|
|
||||||
|
# Set server URL (HTTP profile 0 for AirCarto)
|
||||||
|
/usr/bin/python3 /var/www/nebuleair_pro_4g/SARA/sara_setURL.py ttyAMA2 data.nebuleair.fr 0
|
||||||
|
|
||||||
|
# Check modem status
|
||||||
|
/usr/bin/python3 /var/www/nebuleair_pro_4g/SARA/check_running.py
|
||||||
|
```
|
||||||
|
|
||||||
|
### Network Configuration
|
||||||
|
```bash
|
||||||
|
# Scan WiFi networks (saved to wifi_list.csv)
|
||||||
|
nmcli device wifi list ifname wlan0
|
||||||
|
|
||||||
|
# Connect to WiFi
|
||||||
|
sudo nmcli device wifi connect "SSID" password "PASSWORD"
|
||||||
|
|
||||||
|
# Create hotspot (done automatically at boot if no connection)
|
||||||
|
sudo nmcli device wifi hotspot ifname wlan0 ssid nebuleair_pro password nebuleaircfg
|
||||||
|
|
||||||
|
# Check connection status
|
||||||
|
nmcli device show wlan0
|
||||||
|
nmcli device show eth0
|
||||||
|
```
|
||||||
|
|
||||||
|
### Permissions & Serial Ports
|
||||||
|
```bash
|
||||||
|
# Grant serial port access (run at boot via cron)
|
||||||
|
chmod 777 /dev/ttyAMA* /dev/i2c-1
|
||||||
|
|
||||||
|
# Check I2C devices
|
||||||
|
sudo i2cdetect -y 1
|
||||||
|
```
|
||||||
|
|
||||||
|
### Logs
|
||||||
|
```bash
|
||||||
|
# View main application log
|
||||||
|
tail -f /var/www/nebuleair_pro_4g/logs/app.log
|
||||||
|
|
||||||
|
# View SARA transmission log
|
||||||
|
tail -f /var/www/nebuleair_pro_4g/logs/sara_service.log
|
||||||
|
|
||||||
|
# View service-specific logs
|
||||||
|
tail -f /var/www/nebuleair_pro_4g/logs/npm_service.log
|
||||||
|
tail -f /var/www/nebuleair_pro_4g/logs/envea_service.log
|
||||||
|
|
||||||
|
# Clear logs (done daily via cron)
|
||||||
|
find /var/www/nebuleair_pro_4g/logs -name "*.log" -type f -exec truncate -s 0 {} \;
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration System
|
||||||
|
|
||||||
|
Configuration is stored in SQLite database (`sqlite/sensors.db`) in the `config_table`:
|
||||||
|
|
||||||
|
**Key Configuration Parameters:**
|
||||||
|
- `deviceID`: Unique device identifier (string)
|
||||||
|
- `modem_config_mode`: When true, disables data transmission loop (bool)
|
||||||
|
- `modem_version`: SARA R4 or R5 (string)
|
||||||
|
- `SaraR4_baudrate`: Modem baudrate, usually 115200 or 9600 (int)
|
||||||
|
- `SARA_R4_neworkID`: Cellular network ID for connection (int)
|
||||||
|
- `send_miotiq`: Enable UDP transmission to Miotiq server (bool)
|
||||||
|
- `send_aircarto`: Enable HTTP transmission to AirCarto server (bool)
|
||||||
|
- `send_uSpot`: Enable HTTPS transmission to uSpot server (bool)
|
||||||
|
- `npm_5channel`: Enable 5-channel particle size distribution (bool)
|
||||||
|
- `envea`: Enable Envea gas sensors (bool)
|
||||||
|
- `windMeter`: Enable wind meter (bool)
|
||||||
|
- `BME280`: Enable BME280 sensor (bool)
|
||||||
|
- `MPPT`: Enable MPPT charger monitoring (bool)
|
||||||
|
- `NOISE`: Enable noise sensor (bool)
|
||||||
|
- `latitude_raw`, `longitude_raw`: GPS coordinates (float)
|
||||||
|
|
||||||
|
Configuration can be updated via web interface (launcher.php) or Python scripts in `sqlite/`.
|
||||||
|
|
||||||
|
## Data Transmission Protocols
|
||||||
|
|
||||||
|
### 1. UDP to Miotiq (Binary Protocol)
|
||||||
|
- 100-byte fixed binary payload via UDP socket
|
||||||
|
- Server: 192.168.0.20:4242
|
||||||
|
- Format: Device ID (8 bytes) + signal quality + sensor data (all packed as binary)
|
||||||
|
|
||||||
|
### 2. HTTP POST to AirCarto
|
||||||
|
- CSV payload sent as file attachment
|
||||||
|
- URL: `data.nebuleair.fr/pro_4G/data.php?sensor_id={id}&datetime={timestamp}`
|
||||||
|
- Uses SARA HTTP client (AT+UHTTPC profile 0)
|
||||||
|
|
||||||
|
### 3. HTTPS POST to uSpot
|
||||||
|
- JSON payload with SSL/TLS
|
||||||
|
- URL: `api-prod.uspot.probesys.net/nebuleair?token=2AFF6dQk68daFZ` (port 443)
|
||||||
|
- Uses SARA HTTPS client with certificate validation (AT+UHTTPC profile 1)
|
||||||
|
|
||||||
|
## Important Implementation Notes
|
||||||
|
|
||||||
|
### SARA R4/R5 Modem Communication
|
||||||
|
- The main transmission script (`loop/SARA_send_data_v2.py`) handles complex error recovery
|
||||||
|
- Error codes from AT+UHTTPER command trigger specific recovery actions:
|
||||||
|
- Error 4: Invalid hostname → Reset HTTP profile URL
|
||||||
|
- Error 11: Server connection error → Hardware modem reboot
|
||||||
|
- Error 22: PDP connection issue → Reset PSD/CSD connection (R5 only)
|
||||||
|
- Error 26/44: Timeout/connection lost → Send WiFi notification
|
||||||
|
- Error 73: SSL error → Re-import certificate and reset HTTPS profile
|
||||||
|
- Modem hardware reboot uses GPIO 16 (transistor control to cut power)
|
||||||
|
- Script waits 2 minutes after system boot before executing
|
||||||
|
|
||||||
|
### Serial Communication
|
||||||
|
- All UART ports must be enabled in `/boot/firmware/config.txt`
|
||||||
|
- Permissions reset after reboot, must be re-granted via cron @reboot
|
||||||
|
- Read functions use custom timeout logic to handle slow modem responses
|
||||||
|
- Special handling for multi-line AT command responses using `wait_for_lines` parameter
|
||||||
|
|
||||||
|
### Time Synchronization
|
||||||
|
- DS3231 RTC module maintains time when no network available
|
||||||
|
- RTC timestamp stored in SQLite (`timestamp_table`)
|
||||||
|
- Script compares server datetime (from HTTP headers) with RTC
|
||||||
|
- Auto-updates RTC if difference > 60 seconds
|
||||||
|
- Handles RTC reset condition (year 2000) and disconnection
|
||||||
|
|
||||||
|
### LED Indicators
|
||||||
|
- GPIO 23 (blue LED): Successful data transmission
|
||||||
|
- GPIO 24 (red LED): Transmission errors
|
||||||
|
- Uses thread locking to prevent GPIO conflicts
|
||||||
|
|
||||||
|
### Web Interface
|
||||||
|
- Apache DocumentRoot set to `/var/www/nebuleair_pro_4g`
|
||||||
|
- `html/launcher.php` provides REST-like API for all operations
|
||||||
|
- Uses shell_exec() to run Python scripts with proper sudo permissions
|
||||||
|
- Configuration updates modify SQLite database, not JSON files
|
||||||
|
|
||||||
|
### Security Considerations
|
||||||
|
- www-data user has sudo access to specific commands (defined in /etc/sudoers)
|
||||||
|
- Terminal command execution in launcher.php has whitelist of allowed commands
|
||||||
|
- No sensitive credentials should be committed (all in database/config files)
|
||||||
|
|
||||||
|
## Boot Sequence
|
||||||
|
|
||||||
|
1. Grant serial/I2C permissions (cron @reboot)
|
||||||
|
2. Check WiFi connection, start hotspot if needed (`boot_hotspot.sh`)
|
||||||
|
3. Start SARA modem initialization (`SARA/reboot/start.py`)
|
||||||
|
4. Systemd timers begin sensor data collection
|
||||||
|
5. SARA transmission loop begins after 2-minute delay
|
||||||
|
|
||||||
|
## Cron Jobs
|
||||||
|
|
||||||
|
Located in `/var/www/nebuleair_pro_4g/cron_jobs`:
|
||||||
|
- @reboot: Permissions setup, hotspot check, SARA initialization
|
||||||
|
- Daily 00:00: Truncate log files
|
||||||
40
GPIO/control.py
Normal file
40
GPIO/control.py
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
'''
|
||||||
|
____ ____ ___ ___
|
||||||
|
/ ___| _ \_ _/ _ \
|
||||||
|
| | _| |_) | | | | |
|
||||||
|
| |_| | __/| | |_| |
|
||||||
|
\____|_| |___\___/
|
||||||
|
|
||||||
|
script to control GPIO output
|
||||||
|
|
||||||
|
GPIO 16 -> SARA 5V
|
||||||
|
GPIO 20 -> SARA PWR ON
|
||||||
|
|
||||||
|
option 1:
|
||||||
|
CLI tool like pinctrl
|
||||||
|
pinctrl set 16 op
|
||||||
|
pinctrl set 16 dh
|
||||||
|
pinctrl set 16 dl
|
||||||
|
|
||||||
|
option 2:
|
||||||
|
python library RPI.GPIO
|
||||||
|
|
||||||
|
|
||||||
|
/usr/bin/python3 /var/www/nebuleair_pro_4g/GPIO/control.py
|
||||||
|
'''
|
||||||
|
|
||||||
|
import RPi.GPIO as GPIO
|
||||||
|
import time
|
||||||
|
|
||||||
|
selected_GPIO = 16
|
||||||
|
|
||||||
|
GPIO.setmode(GPIO.BCM) # Use BCM numbering
|
||||||
|
GPIO.setup(selected_GPIO, GPIO.OUT) # Set GPIO17 as an output
|
||||||
|
|
||||||
|
while True:
|
||||||
|
GPIO.output(selected_GPIO, GPIO.HIGH) # Turn ON
|
||||||
|
time.sleep(1) # Wait 1 sec
|
||||||
|
GPIO.output(selected_GPIO, GPIO.LOW) # Turn OFF
|
||||||
|
time.sleep(1) # Wait 1 sec
|
||||||
|
|
||||||
|
|
||||||
53
MH-Z19/get_data.py
Normal file
53
MH-Z19/get_data.py
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
'''
|
||||||
|
Script to get CO2 values from MH-Z19 sensor
|
||||||
|
need parameter: CO2_port
|
||||||
|
/usr/bin/python3 /var/www/nebuleair_pro_4g/MH-Z19/get_data.py ttyAMA4
|
||||||
|
'''
|
||||||
|
|
||||||
|
import serial
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
|
||||||
|
parameter = sys.argv[1:]
|
||||||
|
port = '/dev/' + parameter[0]
|
||||||
|
|
||||||
|
|
||||||
|
def read_co2():
|
||||||
|
try:
|
||||||
|
ser = serial.Serial(
|
||||||
|
port=port,
|
||||||
|
baudrate=9600,
|
||||||
|
parity=serial.PARITY_NONE,
|
||||||
|
stopbits=serial.STOPBITS_ONE,
|
||||||
|
bytesize=serial.EIGHTBITS,
|
||||||
|
timeout=1
|
||||||
|
)
|
||||||
|
except serial.SerialException as e:
|
||||||
|
print(json.dumps({"error": f"Serial port error: {e}"}))
|
||||||
|
return
|
||||||
|
|
||||||
|
READ_CO2_COMMAND = b'\xFF\x01\x86\x00\x00\x00\x00\x00\x79'
|
||||||
|
|
||||||
|
try:
|
||||||
|
ser.write(READ_CO2_COMMAND)
|
||||||
|
time.sleep(2)
|
||||||
|
response = ser.read(9)
|
||||||
|
|
||||||
|
if len(response) < 9:
|
||||||
|
print(json.dumps({"error": "No data or incomplete data received from sensor"}))
|
||||||
|
return
|
||||||
|
|
||||||
|
if response[0] == 0xFF:
|
||||||
|
co2_concentration = response[2] * 256 + response[3]
|
||||||
|
print(json.dumps({"CO2": co2_concentration}))
|
||||||
|
else:
|
||||||
|
print(json.dumps({"error": "Invalid response from sensor"}))
|
||||||
|
except Exception as e:
|
||||||
|
print(json.dumps({"error": str(e)}))
|
||||||
|
finally:
|
||||||
|
ser.close()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
read_co2()
|
||||||
66
MH-Z19/write_data.py
Normal file
66
MH-Z19/write_data.py
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
'''
|
||||||
|
Script to get CO2 values from MH-Z19 sensor and write to database
|
||||||
|
/usr/bin/python3 /var/www/nebuleair_pro_4g/MH-Z19/write_data.py
|
||||||
|
'''
|
||||||
|
|
||||||
|
import serial
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
import sqlite3
|
||||||
|
|
||||||
|
conn = sqlite3.connect("/var/www/nebuleair_pro_4g/sqlite/sensors.db")
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
mh_z19_port = "/dev/ttyAMA4"
|
||||||
|
|
||||||
|
ser = serial.Serial(
|
||||||
|
port=mh_z19_port,
|
||||||
|
baudrate=9600,
|
||||||
|
parity=serial.PARITY_NONE,
|
||||||
|
stopbits=serial.STOPBITS_ONE,
|
||||||
|
bytesize=serial.EIGHTBITS,
|
||||||
|
timeout=1
|
||||||
|
)
|
||||||
|
|
||||||
|
READ_CO2_COMMAND = b'\xFF\x01\x86\x00\x00\x00\x00\x00\x79'
|
||||||
|
|
||||||
|
|
||||||
|
def read_co2():
|
||||||
|
ser.write(READ_CO2_COMMAND)
|
||||||
|
time.sleep(2)
|
||||||
|
response = ser.read(9)
|
||||||
|
if len(response) < 9:
|
||||||
|
print("Error: No data or incomplete data received from CO2 sensor.")
|
||||||
|
return None
|
||||||
|
if response[0] == 0xFF:
|
||||||
|
co2_concentration = response[2] * 256 + response[3]
|
||||||
|
return co2_concentration
|
||||||
|
else:
|
||||||
|
print("Error reading data from CO2 sensor.")
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
try:
|
||||||
|
co2 = read_co2()
|
||||||
|
if co2 is not None:
|
||||||
|
# Get RTC time from SQLite
|
||||||
|
cursor.execute("SELECT * FROM timestamp_table LIMIT 1")
|
||||||
|
row = cursor.fetchone()
|
||||||
|
rtc_time_str = row[1]
|
||||||
|
# Save to SQLite
|
||||||
|
cursor.execute('INSERT INTO data_MHZ19 (timestamp, CO2) VALUES (?, ?)', (rtc_time_str, co2))
|
||||||
|
conn.commit()
|
||||||
|
print(f"CO2: {co2} ppm (saved at {rtc_time_str})")
|
||||||
|
else:
|
||||||
|
print("Failed to get CO2 data.")
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print("Program terminated.")
|
||||||
|
finally:
|
||||||
|
ser.close()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
282
MPPT/read.py
Normal file
282
MPPT/read.py
Normal file
@@ -0,0 +1,282 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
__ __ ____ ____ _____
|
||||||
|
| \/ | _ \| _ \_ _|
|
||||||
|
| |\/| | |_) | |_) || |
|
||||||
|
| | | | __/| __/ | |
|
||||||
|
|_| |_|_| |_| |_|
|
||||||
|
|
||||||
|
MPPT Chargeur solaire Victron interface UART
|
||||||
|
|
||||||
|
MPPT connections
|
||||||
|
5V / Rx / TX / GND
|
||||||
|
RPI connection
|
||||||
|
-- / GPIO9 / GPIO8 / GND
|
||||||
|
* pas besoin de connecter le 5V (le GND uniquement)
|
||||||
|
|
||||||
|
Fixed version - properly handles continuous data stream
|
||||||
|
"""
|
||||||
|
|
||||||
|
import serial
|
||||||
|
import time
|
||||||
|
import sqlite3
|
||||||
|
import os
|
||||||
|
|
||||||
|
# ===== LOGGING CONFIGURATION =====
|
||||||
|
# Set to True to enable all print statements, False to run silently
|
||||||
|
DEBUG_MODE = False
|
||||||
|
|
||||||
|
# Alternative: Use environment variable (can be set in systemd service)
|
||||||
|
# DEBUG_MODE = os.environ.get('MPPT_DEBUG', 'false').lower() == 'true'
|
||||||
|
|
||||||
|
# Alternative: Check if running under systemd
|
||||||
|
# DEBUG_MODE = os.isatty(1) # True if running in terminal, False if systemd/cron
|
||||||
|
|
||||||
|
# Alternative: Use different log levels
|
||||||
|
# LOG_LEVEL = "ERROR" # Options: "DEBUG", "INFO", "ERROR", "NONE"
|
||||||
|
# =================================
|
||||||
|
|
||||||
|
# Connect to the SQLite database
|
||||||
|
conn = sqlite3.connect("/var/www/nebuleair_pro_4g/sqlite/sensors.db")
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
# Logging function
|
||||||
|
def log(message, level="INFO"):
|
||||||
|
"""Print message only if DEBUG_MODE is True"""
|
||||||
|
if DEBUG_MODE:
|
||||||
|
print(message)
|
||||||
|
# Alternative: could write to a log file instead
|
||||||
|
# with open('/var/log/mppt.log', 'a') as f:
|
||||||
|
# f.write(f"{time.strftime('%Y-%m-%d %H:%M:%S')} [{level}] {message}\n")
|
||||||
|
|
||||||
|
|
||||||
|
def read_vedirect(port='/dev/ttyAMA4', baudrate=19200, timeout=10):
|
||||||
|
"""
|
||||||
|
Read and parse data from Victron MPPT controller
|
||||||
|
Returns parsed data as a dictionary
|
||||||
|
"""
|
||||||
|
required_keys = ['V', 'I', 'VPV', 'PPV', 'CS'] # Essential keys we need
|
||||||
|
|
||||||
|
try:
|
||||||
|
log(f"Opening serial port {port} at {baudrate} baud...")
|
||||||
|
ser = serial.Serial(port, baudrate, timeout=1)
|
||||||
|
|
||||||
|
# Clear any buffered data
|
||||||
|
ser.reset_input_buffer()
|
||||||
|
time.sleep(0.5)
|
||||||
|
|
||||||
|
# Initialize data dictionary
|
||||||
|
data = {}
|
||||||
|
start_time = time.time()
|
||||||
|
lines_read = 0
|
||||||
|
blocks_seen = 0
|
||||||
|
|
||||||
|
while time.time() - start_time < timeout:
|
||||||
|
try:
|
||||||
|
line = ser.readline().decode('utf-8', errors='ignore').strip()
|
||||||
|
|
||||||
|
if not line:
|
||||||
|
continue
|
||||||
|
|
||||||
|
lines_read += 1
|
||||||
|
|
||||||
|
# Check if this line contains tab-separated key-value pair
|
||||||
|
if '\t' in line:
|
||||||
|
parts = line.split('\t', 1)
|
||||||
|
if len(parts) == 2:
|
||||||
|
key, value = parts
|
||||||
|
data[key] = value
|
||||||
|
log(f"{key}: {value}")
|
||||||
|
|
||||||
|
# Check for checksum line (end of block)
|
||||||
|
elif line.startswith('Checksum'):
|
||||||
|
blocks_seen += 1
|
||||||
|
log(f"--- End of block {blocks_seen} ---")
|
||||||
|
|
||||||
|
# Check if we have all required keys
|
||||||
|
missing_keys = [key for key in required_keys if key not in data]
|
||||||
|
|
||||||
|
if not missing_keys:
|
||||||
|
log(f"✓ Complete data block received after {lines_read} lines!")
|
||||||
|
ser.close()
|
||||||
|
return data
|
||||||
|
else:
|
||||||
|
log(f"Block {blocks_seen} incomplete, missing: {', '.join(missing_keys)}")
|
||||||
|
# Don't clear data, maybe we missed the beginning of first block
|
||||||
|
if blocks_seen > 1:
|
||||||
|
# If we've seen multiple blocks and still missing data,
|
||||||
|
# something is wrong
|
||||||
|
log("Multiple incomplete blocks, clearing data...")
|
||||||
|
data = {}
|
||||||
|
|
||||||
|
except UnicodeDecodeError as e:
|
||||||
|
log(f"Decode error: {e}", "ERROR")
|
||||||
|
continue
|
||||||
|
except Exception as e:
|
||||||
|
log(f"Error reading line: {e}", "ERROR")
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Timeout reached
|
||||||
|
log(f"Timeout after {timeout}s, read {lines_read} lines, saw {blocks_seen} blocks")
|
||||||
|
ser.close()
|
||||||
|
|
||||||
|
# If we have some data but not all required keys, return what we have
|
||||||
|
if data and len(data) >= len(required_keys) - 1:
|
||||||
|
log("Returning partial data...")
|
||||||
|
return data
|
||||||
|
|
||||||
|
except serial.SerialException as e:
|
||||||
|
log(f"Serial port error: {e}", "ERROR")
|
||||||
|
except Exception as e:
|
||||||
|
log(f"Unexpected error: {e}", "ERROR")
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def parse_values(data):
|
||||||
|
"""Convert string values to appropriate types"""
|
||||||
|
if not data:
|
||||||
|
return None
|
||||||
|
|
||||||
|
parsed = {}
|
||||||
|
|
||||||
|
# Define conversions for each key
|
||||||
|
conversions = {
|
||||||
|
'PID': str,
|
||||||
|
'FW': int,
|
||||||
|
'SER#': str,
|
||||||
|
'V': lambda x: float(x)/1000, # Convert mV to V
|
||||||
|
'I': lambda x: float(x)/1000, # Convert mA to A
|
||||||
|
'VPV': lambda x: float(x)/1000 if x != '---' else 0, # Convert mV to V
|
||||||
|
'PPV': int,
|
||||||
|
'CS': int,
|
||||||
|
'MPPT': int,
|
||||||
|
'OR': str,
|
||||||
|
'ERR': int,
|
||||||
|
'LOAD': str,
|
||||||
|
'IL': lambda x: float(x)/1000, # Convert mA to A
|
||||||
|
'H19': float, # Total energy absorbed in kWh (already in kWh)
|
||||||
|
'H20': float, # Total energy discharged in kWh
|
||||||
|
'H21': int, # Maximum power today (W)
|
||||||
|
'H22': float, # Energy generated today (kWh)
|
||||||
|
'H23': int, # Maximum power yesterday (W)
|
||||||
|
'HSDS': int # Day sequence number
|
||||||
|
}
|
||||||
|
|
||||||
|
# Convert values according to their type
|
||||||
|
for key, value in data.items():
|
||||||
|
if key in conversions:
|
||||||
|
try:
|
||||||
|
parsed[key] = conversions[key](value)
|
||||||
|
except (ValueError, TypeError) as e:
|
||||||
|
log(f"Conversion error for {key}={value}: {e}", "ERROR")
|
||||||
|
parsed[key] = value # Keep as string if conversion fails
|
||||||
|
else:
|
||||||
|
parsed[key] = value
|
||||||
|
|
||||||
|
return parsed
|
||||||
|
|
||||||
|
|
||||||
|
def get_charger_status(cs_value):
|
||||||
|
"""Convert CS numeric value to human-readable status"""
|
||||||
|
status_map = {
|
||||||
|
0: "Off",
|
||||||
|
2: "Fault",
|
||||||
|
3: "Bulk",
|
||||||
|
4: "Absorption",
|
||||||
|
5: "Float",
|
||||||
|
6: "Storage",
|
||||||
|
7: "Equalize",
|
||||||
|
9: "Inverting",
|
||||||
|
11: "Power supply",
|
||||||
|
245: "Starting-up",
|
||||||
|
247: "Repeated absorption",
|
||||||
|
252: "External control"
|
||||||
|
}
|
||||||
|
return status_map.get(cs_value, f"Unknown ({cs_value})")
|
||||||
|
|
||||||
|
|
||||||
|
def get_mppt_status(mppt_value):
|
||||||
|
"""Convert MPPT value to human-readable status"""
|
||||||
|
mppt_map = {
|
||||||
|
0: "Off",
|
||||||
|
1: "Voltage or current limited",
|
||||||
|
2: "MPP Tracker active"
|
||||||
|
}
|
||||||
|
return mppt_map.get(mppt_value, f"Unknown ({mppt_value})")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
log("=== Victron MPPT Reader ===")
|
||||||
|
log(f"Started at: {time.strftime('%Y-%m-%d %H:%M:%S')}")
|
||||||
|
|
||||||
|
# Read data
|
||||||
|
raw_data = read_vedirect()
|
||||||
|
|
||||||
|
if raw_data:
|
||||||
|
# Parse data
|
||||||
|
parsed_data = parse_values(raw_data)
|
||||||
|
|
||||||
|
if parsed_data:
|
||||||
|
# Display summary
|
||||||
|
log("\n===== MPPT Status Summary =====")
|
||||||
|
log(f"Product: {parsed_data.get('PID', 'Unknown')} (FW: {parsed_data.get('FW', '?')})")
|
||||||
|
log(f"Serial: {parsed_data.get('SER#', 'Unknown')}")
|
||||||
|
log(f"\nBattery: {parsed_data.get('V', 0):.2f}V, {parsed_data.get('I', 0):.2f}A")
|
||||||
|
log(f"Solar Panel: {parsed_data.get('VPV', 0):.2f}V, {parsed_data.get('PPV', 0)}W")
|
||||||
|
log(f"Charger Status: {get_charger_status(parsed_data.get('CS', 0))}")
|
||||||
|
log(f"MPPT Status: {get_mppt_status(parsed_data.get('MPPT', 0))}")
|
||||||
|
log(f"Load Output: {parsed_data.get('LOAD', 'Unknown')}, {parsed_data.get('IL', 0):.2f}A")
|
||||||
|
log(f"\nToday's Energy: {parsed_data.get('H22', 0)}kWh (Max: {parsed_data.get('H21', 0)}W)")
|
||||||
|
log(f"Total Energy: {parsed_data.get('H19', 0)}kWh")
|
||||||
|
|
||||||
|
# Validate critical values
|
||||||
|
battery_voltage = parsed_data.get('V', 0)
|
||||||
|
|
||||||
|
if battery_voltage > 0:
|
||||||
|
# Get timestamp
|
||||||
|
try:
|
||||||
|
cursor.execute("SELECT * FROM timestamp_table LIMIT 1")
|
||||||
|
row = cursor.fetchone()
|
||||||
|
rtc_time_str = row[1] if row else time.strftime('%Y-%m-%d %H:%M:%S')
|
||||||
|
except:
|
||||||
|
rtc_time_str = time.strftime('%Y-%m-%d %H:%M:%S')
|
||||||
|
|
||||||
|
# Extract values for database
|
||||||
|
battery_current = parsed_data.get('I', 0)
|
||||||
|
solar_voltage = parsed_data.get('VPV', 0)
|
||||||
|
solar_power = parsed_data.get('PPV', 0)
|
||||||
|
charger_status = parsed_data.get('CS', 0)
|
||||||
|
|
||||||
|
# Save to database
|
||||||
|
try:
|
||||||
|
cursor.execute('''
|
||||||
|
INSERT INTO data_MPPT (timestamp, battery_voltage, battery_current, solar_voltage, solar_power, charger_status)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?)''',
|
||||||
|
(rtc_time_str, battery_voltage, battery_current, solar_voltage, solar_power, charger_status))
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
log(f"\n✓ Data saved to database at {rtc_time_str}")
|
||||||
|
|
||||||
|
except sqlite3.Error as e:
|
||||||
|
# Always log database errors regardless of DEBUG_MODE
|
||||||
|
if not DEBUG_MODE:
|
||||||
|
print(f"Database error: {e}")
|
||||||
|
else:
|
||||||
|
log(f"\n✗ Database error: {e}", "ERROR")
|
||||||
|
conn.rollback()
|
||||||
|
else:
|
||||||
|
log("\n✗ Invalid data: Battery voltage is zero or missing", "ERROR")
|
||||||
|
else:
|
||||||
|
log("\n✗ Failed to parse data", "ERROR")
|
||||||
|
else:
|
||||||
|
log("\n✗ No valid data received from MPPT controller", "ERROR")
|
||||||
|
log("\nPossible issues:")
|
||||||
|
log("- Check serial connection (TX/RX/GND)")
|
||||||
|
log("- Verify port is /dev/ttyAMA4")
|
||||||
|
log("- Ensure MPPT is powered on")
|
||||||
|
log("- Check baudrate (should be 19200)")
|
||||||
|
|
||||||
|
# Always close the connection
|
||||||
|
conn.close()
|
||||||
|
log("\nDone.")
|
||||||
64
NPM/firmware_version.py
Executable file
64
NPM/firmware_version.py
Executable file
@@ -0,0 +1,64 @@
|
|||||||
|
'''
|
||||||
|
Script to get NPM firmware version
|
||||||
|
need parameter: port
|
||||||
|
/usr/bin/python3 /var/www/nebuleair_pro_4g/NPM/firmware_version.py ttyAMA5
|
||||||
|
'''
|
||||||
|
|
||||||
|
import serial
|
||||||
|
import requests
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
|
||||||
|
parameter = sys.argv[1:] # Exclude the script name
|
||||||
|
#print("Parameters received:")
|
||||||
|
port='/dev/'+parameter[0]
|
||||||
|
|
||||||
|
ser = serial.Serial(
|
||||||
|
port=port,
|
||||||
|
baudrate=115200,
|
||||||
|
parity=serial.PARITY_EVEN,
|
||||||
|
stopbits=serial.STOPBITS_ONE,
|
||||||
|
bytesize=serial.EIGHTBITS,
|
||||||
|
timeout = 1
|
||||||
|
)
|
||||||
|
|
||||||
|
ser.write(b'\x81\x17\x68') #firmware version
|
||||||
|
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
byte_data = ser.readline()
|
||||||
|
formatted = ''.join(f'\\x{byte:02x}' for byte in byte_data)
|
||||||
|
#print(formatted)
|
||||||
|
|
||||||
|
'''
|
||||||
|
la réponse est de type
|
||||||
|
\x81\x17\x00\x10\x46\x12
|
||||||
|
avec
|
||||||
|
\x81 address
|
||||||
|
\x17 command code
|
||||||
|
\x00 state
|
||||||
|
\x10\x46 firmware version
|
||||||
|
\x12 checksum
|
||||||
|
'''
|
||||||
|
# Extract byte 4 and byte 5
|
||||||
|
byte4 = byte_data[3] # 4th byte (index 3)
|
||||||
|
byte5 = byte_data[4] # 5th byte (index 4)
|
||||||
|
firmware_version = int(f"{byte4:02x}{byte5:02x}")
|
||||||
|
|
||||||
|
|
||||||
|
data = {
|
||||||
|
'firmware_version': firmware_version,
|
||||||
|
}
|
||||||
|
json_data = json.dumps(data)
|
||||||
|
print(json_data)
|
||||||
|
break
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print("User interrupt encountered. Exiting...")
|
||||||
|
time.sleep(3)
|
||||||
|
exit()
|
||||||
|
except:
|
||||||
|
# for all other kinds of error, but not specifying which one
|
||||||
|
print("Unknown error...")
|
||||||
|
time.sleep(3)
|
||||||
|
exit()
|
||||||
|
|
||||||
126
NPM/get_data.py
Executable file
126
NPM/get_data.py
Executable file
@@ -0,0 +1,126 @@
|
|||||||
|
'''
|
||||||
|
_ _ ____ __ __
|
||||||
|
| \ | | _ \| \/ |
|
||||||
|
| \| | |_) | |\/| |
|
||||||
|
| |\ | __/| | | |
|
||||||
|
|_| \_|_| |_| |_|
|
||||||
|
|
||||||
|
Script to get NPM values
|
||||||
|
need parameter: port
|
||||||
|
/usr/bin/python3 /var/www/nebuleair_pro_4g/NPM/get_data.py ttyAMA5
|
||||||
|
'''
|
||||||
|
|
||||||
|
import serial
|
||||||
|
import requests
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
|
||||||
|
parameter = sys.argv[1:] # Exclude the script name
|
||||||
|
port='/dev/'+parameter[0]
|
||||||
|
|
||||||
|
ser = serial.Serial(
|
||||||
|
port=port,
|
||||||
|
baudrate=115200,
|
||||||
|
parity=serial.PARITY_EVEN,
|
||||||
|
stopbits=serial.STOPBITS_ONE,
|
||||||
|
bytesize=serial.EIGHTBITS,
|
||||||
|
timeout = 1
|
||||||
|
)
|
||||||
|
|
||||||
|
ser.write(b'\x81\x11\x6E') #data10s
|
||||||
|
#ser.write(b'\x81\x12\x6D') #data60s
|
||||||
|
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
byte_data = ser.readline()
|
||||||
|
|
||||||
|
# Convert raw data to hex string for debugging
|
||||||
|
raw_hex = byte_data.hex() if byte_data else ""
|
||||||
|
|
||||||
|
# Check if we received data
|
||||||
|
if not byte_data or len(byte_data) < 15:
|
||||||
|
data = {
|
||||||
|
'PM1': 0.0,
|
||||||
|
'PM25': 0.0,
|
||||||
|
'PM10': 0.0,
|
||||||
|
'sleep': 0,
|
||||||
|
'degradedState': 0,
|
||||||
|
'notReady': 0,
|
||||||
|
'heatError': 0,
|
||||||
|
't_rhError': 0,
|
||||||
|
'fanError': 0,
|
||||||
|
'memoryError': 0,
|
||||||
|
'laserError': 0,
|
||||||
|
'raw': raw_hex,
|
||||||
|
'message': f"No data received or incomplete frame (length: {len(byte_data)})"
|
||||||
|
}
|
||||||
|
json_data = json.dumps(data)
|
||||||
|
print(json_data)
|
||||||
|
break
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
# Create JSON with raw data and status message
|
||||||
|
data = {
|
||||||
|
'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],
|
||||||
|
'raw': raw_hex,
|
||||||
|
'message': 'OK' if sum(Statebits[1:]) == 0 else 'Sensor error detected'
|
||||||
|
}
|
||||||
|
json_data = json.dumps(data)
|
||||||
|
print(json_data)
|
||||||
|
break
|
||||||
|
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
data = {
|
||||||
|
'PM1': 0.0,
|
||||||
|
'PM25': 0.0,
|
||||||
|
'PM10': 0.0,
|
||||||
|
'sleep': 0,
|
||||||
|
'degradedState': 0,
|
||||||
|
'notReady': 0,
|
||||||
|
'heatError': 0,
|
||||||
|
't_rhError': 0,
|
||||||
|
'fanError': 0,
|
||||||
|
'memoryError': 0,
|
||||||
|
'laserError': 0,
|
||||||
|
'raw': '',
|
||||||
|
'message': 'User interrupt encountered'
|
||||||
|
}
|
||||||
|
print(json.dumps(data))
|
||||||
|
time.sleep(3)
|
||||||
|
exit()
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
data = {
|
||||||
|
'PM1': 0.0,
|
||||||
|
'PM25': 0.0,
|
||||||
|
'PM10': 0.0,
|
||||||
|
'sleep': 0,
|
||||||
|
'degradedState': 0,
|
||||||
|
'notReady': 0,
|
||||||
|
'heatError': 0,
|
||||||
|
't_rhError': 0,
|
||||||
|
'fanError': 0,
|
||||||
|
'memoryError': 0,
|
||||||
|
'laserError': 0,
|
||||||
|
'raw': '',
|
||||||
|
'message': f'Error: {str(e)}'
|
||||||
|
}
|
||||||
|
print(json.dumps(data))
|
||||||
|
time.sleep(3)
|
||||||
|
exit()
|
||||||
137
NPM/get_data_modbus.py
Executable file
137
NPM/get_data_modbus.py
Executable file
@@ -0,0 +1,137 @@
|
|||||||
|
'''
|
||||||
|
_ _ ____ __ __
|
||||||
|
| \ | | _ \| \/ |
|
||||||
|
| \| | |_) | |\/| |
|
||||||
|
| |\ | __/| | | |
|
||||||
|
|_| \_|_| |_| |_|
|
||||||
|
|
||||||
|
Script to get NPM data via Modbus
|
||||||
|
need parameter: port
|
||||||
|
/usr/bin/python3 /var/www/nebuleair_pro_4g/NPM/get_data_modbus.py
|
||||||
|
|
||||||
|
Modbus RTU
|
||||||
|
[Slave Address][Function Code][Starting Address][Quantity of Registers][CRC]
|
||||||
|
|
||||||
|
Pour récupérer les 5 cannaux (a partir du registre 0x80)
|
||||||
|
Donnée actualisée toutes les 10 secondes
|
||||||
|
|
||||||
|
Request
|
||||||
|
\x01\x03\x00\x80\x00\x0A\xE4\x1E
|
||||||
|
|
||||||
|
\x01 Slave Address (slave device address)
|
||||||
|
\x03 Function code (read multiple holding registers)
|
||||||
|
\x00\x80 Starting Address (The request starts reading from holding register address 0x80 or 128)
|
||||||
|
\x00\x0A Quantity of Registers (Requests to read 0x0A or 10 consecutive registers starting from address 128)
|
||||||
|
\xE4\x1E Cyclic Redundancy Check (checksum )
|
||||||
|
|
||||||
|
'''
|
||||||
|
|
||||||
|
import serial
|
||||||
|
import requests
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
import crcmod
|
||||||
|
import sqlite3
|
||||||
|
|
||||||
|
# Connect to the SQLite database
|
||||||
|
conn = sqlite3.connect("/var/www/nebuleair_pro_4g/sqlite/sensors.db")
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
def load_config(config_file):
|
||||||
|
try:
|
||||||
|
with open(config_file, 'r') as file:
|
||||||
|
config_data = json.load(file)
|
||||||
|
return config_data
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error loading config file: {e}")
|
||||||
|
return {}
|
||||||
|
|
||||||
|
# Load the configuration data
|
||||||
|
config_file = '/var/www/nebuleair_pro_4g/config.json'
|
||||||
|
config = load_config(config_file)
|
||||||
|
npm_solo_port = config.get('NPM_solo_port', '') #port du NPM solo
|
||||||
|
|
||||||
|
ser = serial.Serial(
|
||||||
|
port=npm_solo_port,
|
||||||
|
baudrate=115200,
|
||||||
|
parity=serial.PARITY_EVEN,
|
||||||
|
stopbits=serial.STOPBITS_ONE,
|
||||||
|
bytesize=serial.EIGHTBITS,
|
||||||
|
timeout = 0.5
|
||||||
|
)
|
||||||
|
|
||||||
|
# Define Modbus CRC-16 function
|
||||||
|
crc16 = crcmod.predefined.mkPredefinedCrcFun('modbus')
|
||||||
|
|
||||||
|
# Request frame without CRC
|
||||||
|
data = b'\x01\x03\x00\x80\x00\x0A'
|
||||||
|
|
||||||
|
# Calculate CRC
|
||||||
|
crc = crc16(data)
|
||||||
|
crc_low = crc & 0xFF
|
||||||
|
crc_high = (crc >> 8) & 0xFF
|
||||||
|
|
||||||
|
# Append CRC to the frame
|
||||||
|
request = data + bytes([crc_low, crc_high])
|
||||||
|
#print(f"Request frame: {request.hex()}")
|
||||||
|
|
||||||
|
ser.write(request)
|
||||||
|
|
||||||
|
#GET RTC TIME from SQlite
|
||||||
|
cursor.execute("SELECT * FROM timestamp_table LIMIT 1")
|
||||||
|
row = cursor.fetchone() # Get the first (and only) row
|
||||||
|
rtc_time_str = row[1] # '2025-02-07 12:30:45'
|
||||||
|
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
byte_data = ser.readline()
|
||||||
|
formatted = ''.join(f'\\x{byte:02x}' for byte in byte_data)
|
||||||
|
#print(formatted)
|
||||||
|
|
||||||
|
# Extract LSW (first 2 bytes) and MSW (last 2 bytes)
|
||||||
|
lsw_channel1 = int.from_bytes(byte_data[3:5], byteorder='big')
|
||||||
|
msw_chanel1 = int.from_bytes(byte_data[5:7], byteorder='big')
|
||||||
|
raw_value_channel1 = (msw_chanel1 << 16) | lsw_channel1
|
||||||
|
|
||||||
|
lsw_channel2 = int.from_bytes(byte_data[7:9], byteorder='big')
|
||||||
|
msw_chanel2 = int.from_bytes(byte_data[9:11], byteorder='big')
|
||||||
|
raw_value_channel2 = (msw_chanel2 << 16) | lsw_channel2
|
||||||
|
|
||||||
|
lsw_channel3 = int.from_bytes(byte_data[11:13], byteorder='big')
|
||||||
|
msw_chanel3 = int.from_bytes(byte_data[13:15], byteorder='big')
|
||||||
|
raw_value_channel3 = (msw_chanel3 << 16) | lsw_channel3
|
||||||
|
|
||||||
|
lsw_channel4 = int.from_bytes(byte_data[15:17], byteorder='big')
|
||||||
|
msw_chanel4 = int.from_bytes(byte_data[17:19], byteorder='big')
|
||||||
|
raw_value_channel4 = (msw_chanel1 << 16) | lsw_channel4
|
||||||
|
|
||||||
|
lsw_channel5 = int.from_bytes(byte_data[19:21], byteorder='big')
|
||||||
|
msw_chanel5 = int.from_bytes(byte_data[21:23], byteorder='big')
|
||||||
|
raw_value_channel5 = (msw_chanel5 << 16) | lsw_channel5
|
||||||
|
|
||||||
|
print(f"Channel 1 (0.2->0.5): {raw_value_channel1}")
|
||||||
|
print(f"Channel 2 (0.5->1.0): {raw_value_channel2}")
|
||||||
|
print(f"Channel 3 (1.0->2.5): {raw_value_channel3}")
|
||||||
|
print(f"Channel 4 (2.5->5.0): {raw_value_channel4}")
|
||||||
|
print(f"Channel 5 (5.0->10.): {raw_value_channel5}")
|
||||||
|
|
||||||
|
cursor.execute('''
|
||||||
|
INSERT INTO data_NPM_5channels (timestamp,PM_ch1, PM_ch2, PM_ch3, PM_ch4, PM_ch5) VALUES (?,?,?,?,?,?)'''
|
||||||
|
, (rtc_time_str,raw_value_channel1,raw_value_channel2,raw_value_channel3,raw_value_channel4,raw_value_channel5))
|
||||||
|
|
||||||
|
# Commit and close the connection
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
|
||||||
|
break
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print("User interrupt encountered. Exiting...")
|
||||||
|
time.sleep(3)
|
||||||
|
exit()
|
||||||
|
except:
|
||||||
|
# for all other kinds of error, but not specifying which one
|
||||||
|
print("Unknown error...")
|
||||||
|
time.sleep(3)
|
||||||
|
exit()
|
||||||
|
|
||||||
|
conn.close()
|
||||||
188
NPM/get_data_modbus_v2.py
Executable file
188
NPM/get_data_modbus_v2.py
Executable file
@@ -0,0 +1,188 @@
|
|||||||
|
'''
|
||||||
|
_ _ ____ __ __
|
||||||
|
| \ | | _ \| \/ |
|
||||||
|
| \| | |_) | |\/| |
|
||||||
|
| |\ | __/| | | |
|
||||||
|
|_| \_|_| |_| |_|
|
||||||
|
|
||||||
|
Script to get NPM data via Modbus
|
||||||
|
need parameter: port
|
||||||
|
/usr/bin/python3 /var/www/nebuleair_pro_4g/NPM/get_data_modbus_v2.py
|
||||||
|
|
||||||
|
Modbus RTU
|
||||||
|
[Slave Address][Function Code][Starting Address][Quantity of Registers][CRC]
|
||||||
|
|
||||||
|
Pour récupérer
|
||||||
|
les concentrations en PM1, PM10 et PM2.5 (a partir du registre 0x38)
|
||||||
|
les 5 cannaux
|
||||||
|
la température et l'humidité à l'intérieur du capteur
|
||||||
|
Donnée actualisée toutes les 10 secondes
|
||||||
|
|
||||||
|
Request
|
||||||
|
\x01\x03\x00\x38\x00\x55\...\...
|
||||||
|
|
||||||
|
\x01 Slave Address (slave device address)
|
||||||
|
\x03 Function code (read multiple holding registers)
|
||||||
|
\x00\x38 Starting Address (The request starts reading from holding register address x38 or 56)
|
||||||
|
\x00\x55 Quantity of Registers (Requests to read x55 or 85 consecutive registers starting from address 56)
|
||||||
|
\...\... Cyclic Redundancy Check (checksum )
|
||||||
|
|
||||||
|
'''
|
||||||
|
|
||||||
|
import serial
|
||||||
|
import requests
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
import crcmod
|
||||||
|
import sqlite3
|
||||||
|
|
||||||
|
# Connect to the SQLite database
|
||||||
|
conn = sqlite3.connect("/var/www/nebuleair_pro_4g/sqlite/sensors.db")
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
def load_config(config_file):
|
||||||
|
try:
|
||||||
|
with open(config_file, 'r') as file:
|
||||||
|
config_data = json.load(file)
|
||||||
|
return config_data
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error loading config file: {e}")
|
||||||
|
return {}
|
||||||
|
|
||||||
|
# Load the configuration data
|
||||||
|
config_file = '/var/www/nebuleair_pro_4g/config.json'
|
||||||
|
config = load_config(config_file)
|
||||||
|
npm_solo_port = config.get('NPM_solo_port', '') #port du NPM solo
|
||||||
|
|
||||||
|
ser = serial.Serial(
|
||||||
|
port=npm_solo_port,
|
||||||
|
baudrate=115200,
|
||||||
|
parity=serial.PARITY_EVEN,
|
||||||
|
stopbits=serial.STOPBITS_ONE,
|
||||||
|
bytesize=serial.EIGHTBITS,
|
||||||
|
timeout = 2
|
||||||
|
)
|
||||||
|
|
||||||
|
# Define Modbus CRC-16 function
|
||||||
|
crc16 = crcmod.predefined.mkPredefinedCrcFun('modbus')
|
||||||
|
|
||||||
|
# Request frame without CRC
|
||||||
|
data = b'\x01\x03\x00\x38\x00\x55'
|
||||||
|
|
||||||
|
# Calculate CRC
|
||||||
|
crc = crc16(data)
|
||||||
|
crc_low = crc & 0xFF
|
||||||
|
crc_high = (crc >> 8) & 0xFF
|
||||||
|
|
||||||
|
# Append CRC to the frame
|
||||||
|
request = data + bytes([crc_low, crc_high])
|
||||||
|
#print(f"Request frame: {request.hex()}")
|
||||||
|
|
||||||
|
ser.write(request)
|
||||||
|
|
||||||
|
#GET RTC TIME from SQlite
|
||||||
|
cursor.execute("SELECT * FROM timestamp_table LIMIT 1")
|
||||||
|
row = cursor.fetchone() # Get the first (and only) row
|
||||||
|
rtc_time_str = row[1] # '2025-02-07 12:30:45'
|
||||||
|
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
byte_data = ser.readline()
|
||||||
|
formatted = ''.join(f'\\x{byte:02x}' for byte in byte_data)
|
||||||
|
print(formatted)
|
||||||
|
|
||||||
|
# Register base (56 = 0x38)
|
||||||
|
REGISTER_START = 56
|
||||||
|
|
||||||
|
# Function to extract 32-bit values from Modbus response
|
||||||
|
def extract_value(byte_data, register, scale=1, single_register=False, round_to=None):
|
||||||
|
"""Extracts a value from Modbus response.
|
||||||
|
|
||||||
|
- `register`: Modbus register to read.
|
||||||
|
- `scale`: Value is divided by this (e.g., `1000` for PM values).
|
||||||
|
- `single_register`: If `True`, only reads 16 bits (one register).
|
||||||
|
"""
|
||||||
|
offset = (register - REGISTER_START) * 2 + 3 # Calculate byte offset
|
||||||
|
|
||||||
|
if single_register:
|
||||||
|
value = int.from_bytes(byte_data[offset:offset+2], byteorder='big')
|
||||||
|
else:
|
||||||
|
lsw = int.from_bytes(byte_data[offset:offset+2], byteorder='big')
|
||||||
|
msw = int.from_bytes(byte_data[offset+2:offset+4], byteorder='big')
|
||||||
|
value = (msw << 16) | lsw # 32-bit value
|
||||||
|
|
||||||
|
value = value / scale # Apply scaling
|
||||||
|
|
||||||
|
if round_to == 0:
|
||||||
|
return int(value) # Convert to integer to remove .0
|
||||||
|
elif round_to is not None:
|
||||||
|
return round(value, round_to) # Apply normal rounding
|
||||||
|
else:
|
||||||
|
return value # No rounding if round_to is None
|
||||||
|
|
||||||
|
# 10-sec PM Concentration (PM1, PM2.5, PM10)
|
||||||
|
pm1_10s = extract_value(byte_data, 56, 1000, round_to=1)
|
||||||
|
pm25_10s = extract_value(byte_data, 58, 1000, round_to=1)
|
||||||
|
pm10_10s = extract_value(byte_data, 60, 1000, round_to=1)
|
||||||
|
|
||||||
|
print("10 sec concentration:")
|
||||||
|
print(f"PM1: {pm1_10s}")
|
||||||
|
print(f"PM2.5: {pm25_10s}")
|
||||||
|
print(f"PM10: {pm10_10s}")
|
||||||
|
|
||||||
|
# 1-min PM Concentration
|
||||||
|
pm1_1min = extract_value(byte_data, 68, 1000, round_to=1)
|
||||||
|
pm25_1min = extract_value(byte_data, 70, 1000, round_to=1)
|
||||||
|
pm10_1min = extract_value(byte_data, 72, 1000, round_to=1)
|
||||||
|
|
||||||
|
#print("1 min concentration:")
|
||||||
|
#print(f"PM1: {pm1_1min}")
|
||||||
|
#print(f"PM2.5: {pm25_1min}")
|
||||||
|
#print(f"PM10: {pm10_1min}")
|
||||||
|
|
||||||
|
# Extract values for 5 channels
|
||||||
|
channel_1 = extract_value(byte_data, 128, round_to=0) # 0.2 - 0.5μm
|
||||||
|
channel_2 = extract_value(byte_data, 130, round_to=0) # 0.5 - 1.0μm
|
||||||
|
channel_3 = extract_value(byte_data, 132, round_to=0) # 1.0 - 2.5μm
|
||||||
|
channel_4 = extract_value(byte_data, 134, round_to=0) # 2.5 - 5.0μm
|
||||||
|
channel_5 = extract_value(byte_data, 136, round_to=0) # 5.0 - 10.0μm
|
||||||
|
|
||||||
|
print(f"Channel 1 (0.2->0.5): {channel_1}")
|
||||||
|
print(f"Channel 2 (0.5->1.0): {channel_2}")
|
||||||
|
print(f"Channel 3 (1.0->2.5): {channel_3}")
|
||||||
|
print(f"Channel 4 (2.5->5.0): {channel_4}")
|
||||||
|
print(f"Channel 5 (5.0->10.): {channel_5}")
|
||||||
|
|
||||||
|
|
||||||
|
# Retrieve relative humidity from register 106 (0x6A)
|
||||||
|
relative_humidity = extract_value(byte_data, 106, 100, single_register=True)
|
||||||
|
#print(f"Internal Relative Humidity: {relative_humidity} %")
|
||||||
|
# Retrieve temperature from register 106 (0x6A)
|
||||||
|
temperature = extract_value(byte_data, 107, 100, single_register=True)
|
||||||
|
#print(f"Internal temperature: {temperature} °C")
|
||||||
|
|
||||||
|
cursor.execute('''
|
||||||
|
INSERT INTO data_NPM_5channels (timestamp,PM_ch1, PM_ch2, PM_ch3, PM_ch4, PM_ch5) VALUES (?,?,?,?,?,?)'''
|
||||||
|
, (rtc_time_str,channel_1,channel_2,channel_3,channel_4,channel_5))
|
||||||
|
|
||||||
|
cursor.execute('''
|
||||||
|
INSERT INTO data_NPM (timestamp,PM1, PM25, PM10, temp_npm, hum_npm) VALUES (?,?,?,?,?,?)'''
|
||||||
|
, (rtc_time_str,pm1_10s,pm25_10s,pm10_10s,temperature,relative_humidity ))
|
||||||
|
|
||||||
|
|
||||||
|
# Commit and close the connection
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
|
||||||
|
break
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print("User interrupt encountered. Exiting...")
|
||||||
|
time.sleep(3)
|
||||||
|
exit()
|
||||||
|
except:
|
||||||
|
# for all other kinds of error, but not specifying which one
|
||||||
|
print("Unknown error...")
|
||||||
|
time.sleep(3)
|
||||||
|
exit()
|
||||||
|
|
||||||
|
conn.close()
|
||||||
177
NPM/get_data_modbus_v2_1.py
Normal file
177
NPM/get_data_modbus_v2_1.py
Normal file
@@ -0,0 +1,177 @@
|
|||||||
|
'''
|
||||||
|
_ _ ____ __ __
|
||||||
|
| \ | | _ \| \/ |
|
||||||
|
| \| | |_) | |\/| |
|
||||||
|
| |\ | __/| | | |
|
||||||
|
|_| \_|_| |_| |_|
|
||||||
|
|
||||||
|
Script to get NPM data via Modbus
|
||||||
|
|
||||||
|
Improved version with data stream lenght check
|
||||||
|
|
||||||
|
/usr/bin/python3 /var/www/nebuleair_pro_4g/NPM/get_data_modbus_v3.py
|
||||||
|
|
||||||
|
Modbus RTU
|
||||||
|
[Slave Address][Function Code][Starting Address][Quantity of Registers][CRC]
|
||||||
|
|
||||||
|
Pour récupérer
|
||||||
|
les concentrations en PM1, PM10 et PM2.5 (a partir du registre 0x38)
|
||||||
|
les 5 cannaux
|
||||||
|
la température et l'humidité à l'intérieur du capteur
|
||||||
|
Donnée actualisée toutes les 10 secondes
|
||||||
|
|
||||||
|
Request
|
||||||
|
\x01\x03\x00\x38\x00\x55\...\...
|
||||||
|
|
||||||
|
\x01 Slave Address (slave device address)
|
||||||
|
\x03 Function code (read multiple holding registers)
|
||||||
|
\x00\x38 Starting Address (The request starts reading from holding register address x38 or 56)
|
||||||
|
\x00\x55 Quantity of Registers (Requests to read x55 or 85 consecutive registers starting from address 56)
|
||||||
|
\...\... Cyclic Redundancy Check (checksum )
|
||||||
|
|
||||||
|
'''
|
||||||
|
import serial
|
||||||
|
import requests
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
import crcmod
|
||||||
|
import sqlite3
|
||||||
|
import time
|
||||||
|
|
||||||
|
# Connect to the SQLite database
|
||||||
|
conn = sqlite3.connect("/var/www/nebuleair_pro_4g/sqlite/sensors.db")
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
def load_config(config_file):
|
||||||
|
try:
|
||||||
|
with open(config_file, 'r') as file:
|
||||||
|
config_data = json.load(file)
|
||||||
|
return config_data
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error loading config file: {e}")
|
||||||
|
return {}
|
||||||
|
|
||||||
|
# Load the configuration data
|
||||||
|
npm_solo_port = "/dev/ttyAMA5" #port du NPM solo
|
||||||
|
|
||||||
|
#GET RTC TIME from SQlite
|
||||||
|
cursor.execute("SELECT * FROM timestamp_table LIMIT 1")
|
||||||
|
row = cursor.fetchone() # Get the first (and only) row
|
||||||
|
rtc_time_str = row[1] # '2025-02-07 12:30:45'
|
||||||
|
|
||||||
|
# Initialize serial port
|
||||||
|
ser = serial.Serial(
|
||||||
|
port=npm_solo_port,
|
||||||
|
baudrate=115200,
|
||||||
|
parity=serial.PARITY_EVEN,
|
||||||
|
stopbits=serial.STOPBITS_ONE,
|
||||||
|
bytesize=serial.EIGHTBITS,
|
||||||
|
timeout=2
|
||||||
|
)
|
||||||
|
|
||||||
|
# Define Modbus CRC-16 function
|
||||||
|
crc16 = crcmod.predefined.mkPredefinedCrcFun('modbus')
|
||||||
|
|
||||||
|
# Request frame without CRC
|
||||||
|
data = b'\x01\x03\x00\x38\x00\x55'
|
||||||
|
|
||||||
|
# Calculate and append CRC
|
||||||
|
crc = crc16(data)
|
||||||
|
crc_low = crc & 0xFF
|
||||||
|
crc_high = (crc >> 8) & 0xFF
|
||||||
|
request = data + bytes([crc_low, crc_high])
|
||||||
|
|
||||||
|
# Clear serial buffer before sending
|
||||||
|
ser.flushInput()
|
||||||
|
|
||||||
|
# Send request
|
||||||
|
ser.write(request)
|
||||||
|
time.sleep(0.2) # Wait for sensor to respond
|
||||||
|
|
||||||
|
# Read response
|
||||||
|
response_length = 2 + 1 + (2 * 85) + 2 # Address + Function + Data + CRC
|
||||||
|
byte_data = ser.read(response_length)
|
||||||
|
|
||||||
|
# Validate response length
|
||||||
|
if len(byte_data) < response_length:
|
||||||
|
print("[ERROR] Incomplete response received:", byte_data.hex())
|
||||||
|
exit()
|
||||||
|
|
||||||
|
# Verify CRC
|
||||||
|
received_crc = int.from_bytes(byte_data[-2:], byteorder='little')
|
||||||
|
calculated_crc = crc16(byte_data[:-2])
|
||||||
|
|
||||||
|
if received_crc != calculated_crc:
|
||||||
|
print("[ERROR] CRC check failed! Corrupted data received.")
|
||||||
|
exit()
|
||||||
|
|
||||||
|
# Convert response to hex for debugging
|
||||||
|
formatted = ''.join(f'\\x{byte:02x}' for byte in byte_data)
|
||||||
|
#print("Response:", formatted)
|
||||||
|
|
||||||
|
# Extract and print PM values
|
||||||
|
def extract_value(byte_data, register, scale=1, single_register=False, round_to=None):
|
||||||
|
REGISTER_START = 56
|
||||||
|
offset = (register - REGISTER_START) * 2 + 3
|
||||||
|
|
||||||
|
if single_register:
|
||||||
|
value = int.from_bytes(byte_data[offset:offset+2], byteorder='big')
|
||||||
|
else:
|
||||||
|
lsw = int.from_bytes(byte_data[offset:offset+2], byteorder='big')
|
||||||
|
msw = int.from_bytes(byte_data[offset+2:offset+4], byteorder='big')
|
||||||
|
value = (msw << 16) | lsw
|
||||||
|
|
||||||
|
value = value / scale
|
||||||
|
|
||||||
|
if round_to == 0:
|
||||||
|
return int(value)
|
||||||
|
elif round_to is not None:
|
||||||
|
return round(value, round_to)
|
||||||
|
else:
|
||||||
|
return value
|
||||||
|
|
||||||
|
pm1_10s = extract_value(byte_data, 56, 1000, round_to=1)
|
||||||
|
pm25_10s = extract_value(byte_data, 58, 1000, round_to=1)
|
||||||
|
pm10_10s = extract_value(byte_data, 60, 1000, round_to=1)
|
||||||
|
|
||||||
|
#print("10 sec concentration:")
|
||||||
|
#print(f"PM1: {pm1_10s}")
|
||||||
|
#print(f"PM2.5: {pm25_10s}")
|
||||||
|
#print(f"PM10: {pm10_10s}")
|
||||||
|
|
||||||
|
# Extract values for 5 channels
|
||||||
|
channel_1 = extract_value(byte_data, 128, round_to=0) # 0.2 - 0.5μm
|
||||||
|
channel_2 = extract_value(byte_data, 130, round_to=0) # 0.5 - 1.0μm
|
||||||
|
channel_3 = extract_value(byte_data, 132, round_to=0) # 1.0 - 2.5μm
|
||||||
|
channel_4 = extract_value(byte_data, 134, round_to=0) # 2.5 - 5.0μm
|
||||||
|
channel_5 = extract_value(byte_data, 136, round_to=0) # 5.0 - 10.0μm
|
||||||
|
|
||||||
|
#print(f"Channel 1 (0.2->0.5): {channel_1}")
|
||||||
|
#print(f"Channel 2 (0.5->1.0): {channel_2}")
|
||||||
|
#print(f"Channel 3 (1.0->2.5): {channel_3}")
|
||||||
|
#print(f"Channel 4 (2.5->5.0): {channel_4}")
|
||||||
|
#print(f"Channel 5 (5.0->10.): {channel_5}")
|
||||||
|
|
||||||
|
|
||||||
|
# Retrieve relative humidity from register 106 (0x6A)
|
||||||
|
relative_humidity = extract_value(byte_data, 106, 100, single_register=True)
|
||||||
|
# Retrieve temperature from register 106 (0x6A)
|
||||||
|
temperature = extract_value(byte_data, 107, 100, single_register=True)
|
||||||
|
|
||||||
|
#print(f"Internal Relative Humidity: {relative_humidity} %")
|
||||||
|
#print(f"Internal temperature: {temperature} °C")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
cursor.execute('''
|
||||||
|
INSERT INTO data_NPM_5channels (timestamp,PM_ch1, PM_ch2, PM_ch3, PM_ch4, PM_ch5) VALUES (?,?,?,?,?,?)'''
|
||||||
|
, (rtc_time_str,channel_1,channel_2,channel_3,channel_4,channel_5))
|
||||||
|
|
||||||
|
cursor.execute('''
|
||||||
|
INSERT INTO data_NPM (timestamp,PM1, PM25, PM10, temp_npm, hum_npm) VALUES (?,?,?,?,?,?)'''
|
||||||
|
, (rtc_time_str,pm1_10s,pm25_10s,pm10_10s,temperature,relative_humidity ))
|
||||||
|
|
||||||
|
# Commit and close the connection
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
conn.close()
|
||||||
244
NPM/get_data_modbus_v3.py
Executable file
244
NPM/get_data_modbus_v3.py
Executable file
@@ -0,0 +1,244 @@
|
|||||||
|
'''
|
||||||
|
_ _ ____ __ __
|
||||||
|
| \ | | _ \| \/ |
|
||||||
|
| \| | |_) | |\/| |
|
||||||
|
| |\ | __/| | | |
|
||||||
|
|_| \_|_| |_| |_|
|
||||||
|
|
||||||
|
Script to get NPM data via Modbus
|
||||||
|
|
||||||
|
Improved version with data stream lenght check
|
||||||
|
|
||||||
|
/usr/bin/python3 /var/www/nebuleair_pro_4g/NPM/get_data_modbus_v3.py
|
||||||
|
|
||||||
|
Modbus RTU
|
||||||
|
[Slave Address][Function Code][Starting Address][Quantity of Registers][CRC]
|
||||||
|
|
||||||
|
Pour récupérer
|
||||||
|
les concentrations en PM1, PM10 et PM2.5 (a partir du registre 0x38)
|
||||||
|
les 5 cannaux
|
||||||
|
la température et l'humidité à l'intérieur du capteur
|
||||||
|
Donnée actualisée toutes les 10 secondes
|
||||||
|
|
||||||
|
Request
|
||||||
|
\x01\x03\x00\x38\x00\x55\...\...
|
||||||
|
|
||||||
|
\x01 Slave Address (slave device address)
|
||||||
|
\x03 Function code (read multiple holding registers)
|
||||||
|
\x00\x38 Starting Address (The request starts reading from holding register address x38 or 56)
|
||||||
|
\x00\x55 Quantity of Registers (Requests to read x55 or 85 consecutive registers starting from address 56)
|
||||||
|
\...\... Cyclic Redundancy Check (checksum )
|
||||||
|
|
||||||
|
MAJ 2026 --> renvoie des 0 si pas de réponse du NPM
|
||||||
|
|
||||||
|
'''
|
||||||
|
import serial
|
||||||
|
import requests
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
import crcmod
|
||||||
|
import sqlite3
|
||||||
|
import time
|
||||||
|
|
||||||
|
# Dry-run mode: print JSON output without writing to database
|
||||||
|
dry_run = "--dry-run" in sys.argv
|
||||||
|
|
||||||
|
# Connect to the SQLite database
|
||||||
|
conn = sqlite3.connect("/var/www/nebuleair_pro_4g/sqlite/sensors.db")
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
def load_config(config_file):
|
||||||
|
try:
|
||||||
|
with open(config_file, 'r') as file:
|
||||||
|
config_data = json.load(file)
|
||||||
|
return config_data
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error loading config file: {e}")
|
||||||
|
return {}
|
||||||
|
|
||||||
|
# Load the configuration data
|
||||||
|
npm_solo_port = "/dev/ttyAMA5" #port du NPM solo
|
||||||
|
|
||||||
|
#GET RTC TIME from SQlite
|
||||||
|
cursor.execute("SELECT * FROM timestamp_table LIMIT 1")
|
||||||
|
row = cursor.fetchone() # Get the first (and only) row
|
||||||
|
rtc_time_str = row[1] # '2025-02-07 12:30:45'
|
||||||
|
|
||||||
|
# Initialize default error values
|
||||||
|
pm1_10s = 0
|
||||||
|
pm25_10s = 0
|
||||||
|
pm10_10s = 0
|
||||||
|
channel_1 = 0
|
||||||
|
channel_2 = 0
|
||||||
|
channel_3 = 0
|
||||||
|
channel_4 = 0
|
||||||
|
channel_5 = 0
|
||||||
|
relative_humidity = 0
|
||||||
|
temperature = 0
|
||||||
|
npm_status = 0xFF # Default: all errors (will be overwritten if read succeeds)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Initialize serial port
|
||||||
|
ser = serial.Serial(
|
||||||
|
port=npm_solo_port,
|
||||||
|
baudrate=115200,
|
||||||
|
parity=serial.PARITY_EVEN,
|
||||||
|
stopbits=serial.STOPBITS_ONE,
|
||||||
|
bytesize=serial.EIGHTBITS,
|
||||||
|
timeout=2
|
||||||
|
)
|
||||||
|
|
||||||
|
# Define Modbus CRC-16 function
|
||||||
|
crc16 = crcmod.predefined.mkPredefinedCrcFun('modbus')
|
||||||
|
|
||||||
|
# Request frame without CRC
|
||||||
|
data = b'\x01\x03\x00\x38\x00\x55'
|
||||||
|
|
||||||
|
# Calculate and append CRC
|
||||||
|
crc = crc16(data)
|
||||||
|
crc_low = crc & 0xFF
|
||||||
|
crc_high = (crc >> 8) & 0xFF
|
||||||
|
request = data + bytes([crc_low, crc_high])
|
||||||
|
|
||||||
|
# Clear serial buffer before sending
|
||||||
|
ser.flushInput()
|
||||||
|
|
||||||
|
# Send request
|
||||||
|
ser.write(request)
|
||||||
|
time.sleep(0.2) # Wait for sensor to respond
|
||||||
|
|
||||||
|
# Read response
|
||||||
|
response_length = 2 + 1 + (2 * 85) + 2 # Address + Function + Data + CRC
|
||||||
|
byte_data = ser.read(response_length)
|
||||||
|
|
||||||
|
# Validate response length
|
||||||
|
if len(byte_data) < response_length:
|
||||||
|
if not dry_run:
|
||||||
|
print(f"[ERROR] Incomplete response received: {byte_data.hex()}")
|
||||||
|
raise Exception("Incomplete response")
|
||||||
|
|
||||||
|
# Verify CRC
|
||||||
|
received_crc = int.from_bytes(byte_data[-2:], byteorder='little')
|
||||||
|
calculated_crc = crc16(byte_data[:-2])
|
||||||
|
|
||||||
|
if received_crc != calculated_crc:
|
||||||
|
if not dry_run:
|
||||||
|
print("[ERROR] CRC check failed! Corrupted data received.")
|
||||||
|
raise Exception("CRC check failed")
|
||||||
|
|
||||||
|
# Convert response to hex for debugging
|
||||||
|
formatted = ''.join(f'\\x{byte:02x}' for byte in byte_data)
|
||||||
|
#print("Response:", formatted)
|
||||||
|
|
||||||
|
# Extract and print PM values
|
||||||
|
def extract_value(byte_data, register, scale=1, single_register=False, round_to=None):
|
||||||
|
REGISTER_START = 56
|
||||||
|
offset = (register - REGISTER_START) * 2 + 3
|
||||||
|
|
||||||
|
if single_register:
|
||||||
|
value = int.from_bytes(byte_data[offset:offset+2], byteorder='big')
|
||||||
|
else:
|
||||||
|
lsw = int.from_bytes(byte_data[offset:offset+2], byteorder='big')
|
||||||
|
msw = int.from_bytes(byte_data[offset+2:offset+4], byteorder='big')
|
||||||
|
value = (msw << 16) | lsw
|
||||||
|
|
||||||
|
value = value / scale
|
||||||
|
|
||||||
|
if round_to == 0:
|
||||||
|
return int(value)
|
||||||
|
elif round_to is not None:
|
||||||
|
return round(value, round_to)
|
||||||
|
else:
|
||||||
|
return value
|
||||||
|
|
||||||
|
pm1_10s = extract_value(byte_data, 56, 1000, round_to=1)
|
||||||
|
pm25_10s = extract_value(byte_data, 58, 1000, round_to=1)
|
||||||
|
pm10_10s = extract_value(byte_data, 60, 1000, round_to=1)
|
||||||
|
|
||||||
|
#print("10 sec concentration:")
|
||||||
|
#print(f"PM1: {pm1_10s}")
|
||||||
|
#print(f"PM2.5: {pm25_10s}")
|
||||||
|
#print(f"PM10: {pm10_10s}")
|
||||||
|
|
||||||
|
# Extract values for 5 channels
|
||||||
|
channel_1 = extract_value(byte_data, 128, round_to=0) # 0.2 - 0.5μm
|
||||||
|
channel_2 = extract_value(byte_data, 130, round_to=0) # 0.5 - 1.0μm
|
||||||
|
channel_3 = extract_value(byte_data, 132, round_to=0) # 1.0 - 2.5μm
|
||||||
|
channel_4 = extract_value(byte_data, 134, round_to=0) # 2.5 - 5.0μm
|
||||||
|
channel_5 = extract_value(byte_data, 136, round_to=0) # 5.0 - 10.0μm
|
||||||
|
|
||||||
|
#print(f"Channel 1 (0.2->0.5): {channel_1}")
|
||||||
|
#print(f"Channel 2 (0.5->1.0): {channel_2}")
|
||||||
|
#print(f"Channel 3 (1.0->2.5): {channel_3}")
|
||||||
|
#print(f"Channel 4 (2.5->5.0): {channel_4}")
|
||||||
|
#print(f"Channel 5 (5.0->10.): {channel_5}")
|
||||||
|
|
||||||
|
|
||||||
|
# Retrieve relative humidity from register 106 (0x6A)
|
||||||
|
relative_humidity = extract_value(byte_data, 106, 100, single_register=True)
|
||||||
|
# Retrieve temperature from register 106 (0x6A)
|
||||||
|
temperature = extract_value(byte_data, 107, 100, single_register=True)
|
||||||
|
|
||||||
|
#print(f"Internal Relative Humidity: {relative_humidity} %")
|
||||||
|
#print(f"Internal temperature: {temperature} °C")
|
||||||
|
|
||||||
|
# Read NPM status register (register 19 = 0x13, 1 register)
|
||||||
|
# Modbus request: slave=0x01, func=0x03, addr=0x0013, qty=0x0001
|
||||||
|
status_request = b'\x01\x03\x00\x13\x00\x01'
|
||||||
|
status_crc = crc16(status_request)
|
||||||
|
status_request += bytes([status_crc & 0xFF, (status_crc >> 8) & 0xFF])
|
||||||
|
|
||||||
|
ser.flushInput()
|
||||||
|
ser.write(status_request)
|
||||||
|
time.sleep(0.2)
|
||||||
|
|
||||||
|
# Response: addr(1) + func(1) + byte_count(1) + data(2) + crc(2) = 7 bytes
|
||||||
|
status_response = ser.read(7)
|
||||||
|
if len(status_response) == 7:
|
||||||
|
status_recv_crc = int.from_bytes(status_response[-2:], byteorder='little')
|
||||||
|
status_calc_crc = crc16(status_response[:-2])
|
||||||
|
if status_recv_crc == status_calc_crc:
|
||||||
|
npm_status = int.from_bytes(status_response[3:5], byteorder='big') & 0xFF
|
||||||
|
if not dry_run:
|
||||||
|
print(f"NPM status: 0x{npm_status:02X} ({npm_status})")
|
||||||
|
else:
|
||||||
|
if not dry_run:
|
||||||
|
print("[WARNING] NPM status CRC check failed, keeping default")
|
||||||
|
else:
|
||||||
|
if not dry_run:
|
||||||
|
print(f"[WARNING] NPM status incomplete response ({len(status_response)} bytes)")
|
||||||
|
|
||||||
|
ser.close()
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
if not dry_run:
|
||||||
|
print(f"[ERROR] Sensor communication failed: {e}")
|
||||||
|
# Variables already set to -1 at the beginning
|
||||||
|
|
||||||
|
finally:
|
||||||
|
if dry_run:
|
||||||
|
# Print JSON output without writing to database
|
||||||
|
result = {
|
||||||
|
"PM1": pm1_10s,
|
||||||
|
"PM25": pm25_10s,
|
||||||
|
"PM10": pm10_10s,
|
||||||
|
"temperature": temperature,
|
||||||
|
"humidity": relative_humidity,
|
||||||
|
"npm_status": npm_status,
|
||||||
|
"npm_status_hex": f"0x{npm_status:02X}"
|
||||||
|
}
|
||||||
|
print(json.dumps(result))
|
||||||
|
else:
|
||||||
|
# Always save data to database, even if all values are 0
|
||||||
|
cursor.execute('''
|
||||||
|
INSERT INTO data_NPM_5channels (timestamp,PM_ch1, PM_ch2, PM_ch3, PM_ch4, PM_ch5) VALUES (?,?,?,?,?,?)'''
|
||||||
|
, (rtc_time_str, channel_1, channel_2, channel_3, channel_4, channel_5))
|
||||||
|
|
||||||
|
cursor.execute('''
|
||||||
|
INSERT INTO data_NPM (timestamp,PM1, PM25, PM10, temp_npm, hum_npm, npm_status) VALUES (?,?,?,?,?,?,?)'''
|
||||||
|
, (rtc_time_str, pm1_10s, pm25_10s, pm10_10s, temperature, relative_humidity, npm_status))
|
||||||
|
|
||||||
|
# Commit and close the connection
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
conn.close()
|
||||||
52
NPM/get_data_temp_hum.py
Executable file
52
NPM/get_data_temp_hum.py
Executable file
@@ -0,0 +1,52 @@
|
|||||||
|
'''
|
||||||
|
_ _ ____ __ __
|
||||||
|
| \ | | _ \| \/ |
|
||||||
|
| \| | |_) | |\/| |
|
||||||
|
| |\ | __/| | | |
|
||||||
|
|_| \_|_| |_| |_|
|
||||||
|
|
||||||
|
Script to get NPM values: ONLY temp and hum
|
||||||
|
need parameter: port
|
||||||
|
/usr/bin/python3 /var/www/nebuleair_pro_4g/NPM/get_data_temp_hum.py ttyAMA5
|
||||||
|
'''
|
||||||
|
|
||||||
|
import serial
|
||||||
|
import requests
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
|
||||||
|
parameter = sys.argv[1:] # Exclude the script name
|
||||||
|
#print("Parameters received:")
|
||||||
|
port='/dev/'+parameter[0]
|
||||||
|
|
||||||
|
ser = serial.Serial(
|
||||||
|
port=port,
|
||||||
|
baudrate=115200,
|
||||||
|
parity=serial.PARITY_EVEN,
|
||||||
|
stopbits=serial.STOPBITS_ONE,
|
||||||
|
bytesize=serial.EIGHTBITS,
|
||||||
|
timeout = 1
|
||||||
|
)
|
||||||
|
|
||||||
|
ser.write(b'\x81\x14\x6B') # Temp and humidity command
|
||||||
|
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
byte_data_temp_hum = ser.readline()
|
||||||
|
# Decode temperature and humidity values
|
||||||
|
temperature = int.from_bytes(byte_data_temp_hum[3:5], byteorder='big') / 100.0
|
||||||
|
humidity = int.from_bytes(byte_data_temp_hum[5:7], byteorder='big') / 100.0
|
||||||
|
|
||||||
|
print(f"temp: {temperature}")
|
||||||
|
print(f"hum: {humidity}")
|
||||||
|
break
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print("User interrupt encountered. Exiting...")
|
||||||
|
time.sleep(3)
|
||||||
|
exit()
|
||||||
|
except:
|
||||||
|
# for all other kinds of error, but not specifying which one
|
||||||
|
print("Unknown error...")
|
||||||
|
time.sleep(3)
|
||||||
|
exit()
|
||||||
|
|
||||||
104
NPM/get_data_v2.py
Executable file
104
NPM/get_data_v2.py
Executable file
@@ -0,0 +1,104 @@
|
|||||||
|
'''
|
||||||
|
_ _ ____ __ __
|
||||||
|
| \ | | _ \| \/ |
|
||||||
|
| \| | |_) | |\/| |
|
||||||
|
| |\ | __/| | | |
|
||||||
|
|_| \_|_| |_| |_|
|
||||||
|
|
||||||
|
Script to get NPM values (PM1, PM2.5 and PM10)
|
||||||
|
PM and the sensor temp/hum
|
||||||
|
And store them inside sqlite database
|
||||||
|
Uses RTC module for timing (from SQLite db)
|
||||||
|
/usr/bin/python3 /var/www/nebuleair_pro_4g/NPM/get_data_v2.py
|
||||||
|
'''
|
||||||
|
|
||||||
|
import serial
|
||||||
|
import requests
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
import sqlite3
|
||||||
|
import smbus2
|
||||||
|
import time
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
# Connect to the SQLite database
|
||||||
|
conn = sqlite3.connect("/var/www/nebuleair_pro_4g/sqlite/sensors.db")
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
def load_config(config_file):
|
||||||
|
try:
|
||||||
|
with open(config_file, 'r') as file:
|
||||||
|
config_data = json.load(file)
|
||||||
|
return config_data
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error loading config file: {e}")
|
||||||
|
return {}
|
||||||
|
|
||||||
|
# Load the configuration data
|
||||||
|
config_file = '/var/www/nebuleair_pro_4g/config.json'
|
||||||
|
config = load_config(config_file)
|
||||||
|
npm_solo_port = config.get('NPM_solo_port', '') #port du NPM solo
|
||||||
|
|
||||||
|
ser = serial.Serial(
|
||||||
|
port=npm_solo_port,
|
||||||
|
baudrate=115200,
|
||||||
|
parity=serial.PARITY_EVEN,
|
||||||
|
stopbits=serial.STOPBITS_ONE,
|
||||||
|
bytesize=serial.EIGHTBITS,
|
||||||
|
timeout = 0.5
|
||||||
|
)
|
||||||
|
|
||||||
|
# 1️⃣ Request PM Data (PM1, PM2.5, PM10)
|
||||||
|
|
||||||
|
#ser.write(b'\x81\x11\x6E') #data10s
|
||||||
|
ser.write(b'\x81\x12\x6D') #data60s
|
||||||
|
time.sleep(0.5) # Small delay to allow the sensor to process the request
|
||||||
|
|
||||||
|
#print("Start get_data_v2.py script")
|
||||||
|
byte_data = ser.readline()
|
||||||
|
#print(byte_data)
|
||||||
|
stateByte = int.from_bytes(byte_data[2:3], byteorder='big')
|
||||||
|
Statebits = [int(bit) for bit in bin(stateByte)[2:].zfill(8)]
|
||||||
|
PM1 = int.from_bytes(byte_data[9:11], byteorder='big')/10
|
||||||
|
PM25 = int.from_bytes(byte_data[11:13], byteorder='big')/10
|
||||||
|
PM10 = int.from_bytes(byte_data[13:15], byteorder='big')/10
|
||||||
|
|
||||||
|
# 2️⃣ Request Temperature & Humidity
|
||||||
|
ser.write(b'\x81\x14\x6B') # Temp and humidity command
|
||||||
|
time.sleep(0.5) # Small delay to allow the sensor to process the request
|
||||||
|
byte_data_temp_hum = ser.readline()
|
||||||
|
|
||||||
|
# Decode temperature and humidity values
|
||||||
|
temperature = int.from_bytes(byte_data_temp_hum[3:5], byteorder='big') / 100.0
|
||||||
|
humidity = int.from_bytes(byte_data_temp_hum[5:7], byteorder='big') / 100.0
|
||||||
|
|
||||||
|
#print(f"State: {Statebits}")
|
||||||
|
#print(f"PM1: {PM1}")
|
||||||
|
#print(f"PM25: {PM25}")
|
||||||
|
#print(f"PM10: {PM10}")
|
||||||
|
#print(f"temp: {temperature}")
|
||||||
|
#print(f"hum: {humidity}")
|
||||||
|
|
||||||
|
#GET RTC TIME from SQlite
|
||||||
|
cursor.execute("SELECT * FROM timestamp_table LIMIT 1")
|
||||||
|
row = cursor.fetchone() # Get the first (and only) row
|
||||||
|
rtc_time_str = row[1] # '2025-02-07 12:30:45'
|
||||||
|
|
||||||
|
#save to sqlite database
|
||||||
|
try:
|
||||||
|
cursor.execute('''
|
||||||
|
INSERT INTO data_NPM (timestamp,PM1, PM25, PM10, temp_npm, hum_npm) VALUES (?,?,?,?,?,?)'''
|
||||||
|
, (rtc_time_str,PM1,PM25,PM10,temperature,humidity ))
|
||||||
|
|
||||||
|
# Commit and close the connection
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
#print("Sensor data saved successfully!")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Database error: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
172
NPM/old/get_data_modbus_loop.py
Executable file
172
NPM/old/get_data_modbus_loop.py
Executable file
@@ -0,0 +1,172 @@
|
|||||||
|
'''
|
||||||
|
Loop to run every minutes
|
||||||
|
|
||||||
|
* * * * * /usr/bin/python3 /var/www/nebuleair_pro_4g/NPM/get_data_modbus_loop.py ttyAMA5
|
||||||
|
|
||||||
|
saves data (avaerage) to a json file /var/www/nebuleair_pro_4g/NPM/data/data.json
|
||||||
|
saves data (all) to a sqlite database
|
||||||
|
|
||||||
|
first time running the script?
|
||||||
|
sudo mkdir /var/www/nebuleair_pro_4g/NPM/data
|
||||||
|
sudo touch /var/www/nebuleair_pro_4g/NPM/data/data.json
|
||||||
|
'''
|
||||||
|
|
||||||
|
import serial
|
||||||
|
import sys
|
||||||
|
import crcmod
|
||||||
|
import time
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import sqlite3
|
||||||
|
import smbus2 # For RTC DS3231
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
|
||||||
|
# Ensure a port argument is provided
|
||||||
|
if len(sys.argv) < 2:
|
||||||
|
print("Usage: python3 get_data_modbus.py <serial_port>")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
port = '/dev/' + sys.argv[1]
|
||||||
|
|
||||||
|
# Initialize serial communication
|
||||||
|
try:
|
||||||
|
ser = serial.Serial(
|
||||||
|
port=port,
|
||||||
|
baudrate=115200,
|
||||||
|
parity=serial.PARITY_EVEN,
|
||||||
|
stopbits=serial.STOPBITS_ONE,
|
||||||
|
bytesize=serial.EIGHTBITS,
|
||||||
|
timeout=0.5
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error opening serial port {port}: {e}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# Define Modbus CRC-16 function
|
||||||
|
crc16 = crcmod.predefined.mkPredefinedCrcFun('modbus')
|
||||||
|
|
||||||
|
# Request frame without CRC
|
||||||
|
data = b'\x01\x03\x00\x80\x00\x0A'
|
||||||
|
|
||||||
|
# Calculate CRC
|
||||||
|
crc = crc16(data)
|
||||||
|
crc_low = crc & 0xFF
|
||||||
|
crc_high = (crc >> 8) & 0xFF
|
||||||
|
|
||||||
|
# Append CRC to the frame
|
||||||
|
request = data + bytes([crc_low, crc_high])
|
||||||
|
|
||||||
|
# Log request frame
|
||||||
|
print(f"Request frame: {request.hex()}")
|
||||||
|
|
||||||
|
# Initialize SQLite database
|
||||||
|
conn = sqlite3.connect("/var/www/nebuleair_pro_4g/sqlite/sensors.db")
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
# RTC Module (DS3231) Setup
|
||||||
|
RTC_I2C_ADDR = 0x68 # DS3231 I2C Address
|
||||||
|
bus = smbus2.SMBus(1)
|
||||||
|
|
||||||
|
def bcd_to_dec(bcd):
|
||||||
|
return (bcd // 16 * 10) + (bcd % 16)
|
||||||
|
|
||||||
|
def get_rtc_time():
|
||||||
|
"""Reads time from RTC module (DS3231)"""
|
||||||
|
try:
|
||||||
|
data = bus.read_i2c_block_data(RTC_I2C_ADDR, 0x00, 7)
|
||||||
|
seconds = bcd_to_dec(data[0] & 0x7F)
|
||||||
|
minutes = bcd_to_dec(data[1])
|
||||||
|
hours = bcd_to_dec(data[2] & 0x3F)
|
||||||
|
day = bcd_to_dec(data[4])
|
||||||
|
month = bcd_to_dec(data[5])
|
||||||
|
year = bcd_to_dec(data[6]) + 2000
|
||||||
|
return datetime(year, month, day, hours, minutes, seconds).strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"RTC Read Error: {e}")
|
||||||
|
return "N/A"
|
||||||
|
|
||||||
|
# Initialize storage for averaging
|
||||||
|
num_samples = 6
|
||||||
|
channel_sums = [0] * 5 # 5 channels
|
||||||
|
|
||||||
|
#do not start immediately to prevent multiple write/read over NPM serial port
|
||||||
|
time.sleep(3)
|
||||||
|
|
||||||
|
# Loop 6 times to collect data every 10 seconds
|
||||||
|
for i in range(num_samples):
|
||||||
|
print(f"\nIteration {i+1}/{num_samples}")
|
||||||
|
ser.write(request)
|
||||||
|
rtc_timestamp = get_rtc_time()
|
||||||
|
|
||||||
|
try:
|
||||||
|
byte_data = ser.readline()
|
||||||
|
formatted = ''.join(f'\\x{byte:02x}' for byte in byte_data)
|
||||||
|
print(f"Raw Response: {formatted}")
|
||||||
|
|
||||||
|
if len(byte_data) < 23:
|
||||||
|
print("Incomplete response, skipping this sample.")
|
||||||
|
time.sleep(9)
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Extract and process the 5 channels
|
||||||
|
channels = []
|
||||||
|
for j in range(5):
|
||||||
|
lsw = int.from_bytes(byte_data[3 + j*4 : 5 + j*4], byteorder='little')
|
||||||
|
msw = int.from_bytes(byte_data[5 + j*4 : 7 + j*4], byteorder='little')
|
||||||
|
raw_value = (msw << 16) | lsw
|
||||||
|
channels.append(raw_value)
|
||||||
|
|
||||||
|
# Accumulate sum for each channel
|
||||||
|
for j in range(5):
|
||||||
|
channel_sums[j] += channels[j]
|
||||||
|
|
||||||
|
# Print collected values
|
||||||
|
print(f"Timestamp (RTC): {rtc_timestamp}")
|
||||||
|
for j in range(5):
|
||||||
|
print(f"Channel {j+1}: {channels[j]}")
|
||||||
|
|
||||||
|
|
||||||
|
# Save the individual reading to the database
|
||||||
|
cursor.execute('''
|
||||||
|
INSERT INTO data (timestamp, PM_ch1, PM_ch2, PM_ch3, PM_ch4, PM_ch5)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?)
|
||||||
|
''', (rtc_timestamp, channels[0], channels[1], channels[2], channels[3], channels[4]))
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error reading data: {e}")
|
||||||
|
|
||||||
|
# Wait 10 seconds before next reading
|
||||||
|
time.sleep(10)
|
||||||
|
|
||||||
|
# Compute the average values
|
||||||
|
channel_means = [int(s / num_samples) for s in channel_sums]
|
||||||
|
|
||||||
|
# Create JSON structure
|
||||||
|
data_json = {
|
||||||
|
"channel_1": channel_means[0],
|
||||||
|
"channel_2": channel_means[1],
|
||||||
|
"channel_3": channel_means[2],
|
||||||
|
"channel_4": channel_means[3],
|
||||||
|
"channel_5": channel_means[4]
|
||||||
|
}
|
||||||
|
|
||||||
|
# Print final JSON data
|
||||||
|
print("\nFinal JSON Data (Averaged over 6 readings):")
|
||||||
|
print(json.dumps(data_json, indent=4))
|
||||||
|
|
||||||
|
# Define JSON file path
|
||||||
|
output_file = "/var/www/nebuleair_pro_4g/NPM/data/data.json"
|
||||||
|
|
||||||
|
# Write results to a JSON file
|
||||||
|
try:
|
||||||
|
with open(output_file, "w") as f:
|
||||||
|
json.dump(data_json, f, indent=4)
|
||||||
|
print(f"\nAveraged JSON data saved to {output_file}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error writing to file: {e}")
|
||||||
|
|
||||||
|
# Close serial connection
|
||||||
|
ser.close()
|
||||||
126
NPM/old/test_modbus.py
Executable file
126
NPM/old/test_modbus.py
Executable file
@@ -0,0 +1,126 @@
|
|||||||
|
'''
|
||||||
|
_ _ ____ __ __
|
||||||
|
| \ | | _ \| \/ |
|
||||||
|
| \| | |_) | |\/| |
|
||||||
|
| |\ | __/| | | |
|
||||||
|
|_| \_|_| |_| |_|
|
||||||
|
|
||||||
|
Script to get NPM data via Modbus
|
||||||
|
need parameter: port
|
||||||
|
/usr/bin/python3 /var/www/nebuleair_pro_4g/NPM/old/test_modbus.py
|
||||||
|
|
||||||
|
Modbus RTU
|
||||||
|
[Slave Address][Function Code][Starting Address][Quantity of Registers][CRC]
|
||||||
|
|
||||||
|
Pour récupérer
|
||||||
|
les concentrations en PM1, PM10 et PM2.5 (a partir du registre 0x38)
|
||||||
|
les 5 cannaux
|
||||||
|
Donnée actualisée toutes les 10 secondes
|
||||||
|
|
||||||
|
Request
|
||||||
|
\x01\x03\x00\x38\x00\x55\...\...
|
||||||
|
|
||||||
|
\x01 Slave Address (slave device address)
|
||||||
|
\x03 Function code (read multiple holding registers)
|
||||||
|
\x00\x38 Starting Address (The request starts reading from holding register address x38 or 56)
|
||||||
|
\x00\x55 Quantity of Registers (Requests to read x55 or 85 consecutive registers starting from address 56)
|
||||||
|
\...\... Cyclic Redundancy Check (checksum )
|
||||||
|
|
||||||
|
'''
|
||||||
|
|
||||||
|
import serial
|
||||||
|
import requests
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
import crcmod
|
||||||
|
import sqlite3
|
||||||
|
|
||||||
|
# Connect to the SQLite database
|
||||||
|
conn = sqlite3.connect("/var/www/nebuleair_pro_4g/sqlite/sensors.db")
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
def load_config(config_file):
|
||||||
|
try:
|
||||||
|
with open(config_file, 'r') as file:
|
||||||
|
config_data = json.load(file)
|
||||||
|
return config_data
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error loading config file: {e}")
|
||||||
|
return {}
|
||||||
|
|
||||||
|
# Load the configuration data
|
||||||
|
config_file = '/var/www/nebuleair_pro_4g/config.json'
|
||||||
|
config = load_config(config_file)
|
||||||
|
npm_solo_port = config.get('NPM_solo_port', '') #port du NPM solo
|
||||||
|
|
||||||
|
ser = serial.Serial(
|
||||||
|
port=npm_solo_port,
|
||||||
|
baudrate=115200,
|
||||||
|
parity=serial.PARITY_EVEN,
|
||||||
|
stopbits=serial.STOPBITS_ONE,
|
||||||
|
bytesize=serial.EIGHTBITS,
|
||||||
|
timeout = 0.5
|
||||||
|
)
|
||||||
|
|
||||||
|
# Define Modbus CRC-16 function
|
||||||
|
crc16 = crcmod.predefined.mkPredefinedCrcFun('modbus')
|
||||||
|
|
||||||
|
# Request frame without CRC
|
||||||
|
data = b'\x01\x03\x00\x44\x00\x06'
|
||||||
|
|
||||||
|
# Calculate CRC
|
||||||
|
crc = crc16(data)
|
||||||
|
crc_low = crc & 0xFF
|
||||||
|
crc_high = (crc >> 8) & 0xFF
|
||||||
|
|
||||||
|
# Append CRC to the frame
|
||||||
|
request = data + bytes([crc_low, crc_high])
|
||||||
|
#print(f"Request frame: {request.hex()}")
|
||||||
|
|
||||||
|
ser.write(request)
|
||||||
|
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
byte_data = ser.readline()
|
||||||
|
formatted = ''.join(f'\\x{byte:02x}' for byte in byte_data)
|
||||||
|
print(formatted)
|
||||||
|
|
||||||
|
if len(byte_data) < 14:
|
||||||
|
print(f"Error: Received {len(byte_data)} bytes, expected 14!")
|
||||||
|
continue
|
||||||
|
|
||||||
|
#10 secs concentration
|
||||||
|
|
||||||
|
lsw_pm1 = int.from_bytes(byte_data[3:5], byteorder='big')
|
||||||
|
msw_pm1 = int.from_bytes(byte_data[5:7], byteorder='big')
|
||||||
|
raw_value_pm1 = (msw_pm1 << 16) | lsw_pm1
|
||||||
|
raw_value_pm1 = raw_value_pm1 / 1000
|
||||||
|
|
||||||
|
lsw_pm25 = int.from_bytes(byte_data[7:9], byteorder='big')
|
||||||
|
msw_pm25 = int.from_bytes(byte_data[9:11], byteorder='big')
|
||||||
|
raw_value_pm25 = (msw_pm25 << 16) | lsw_pm25
|
||||||
|
raw_value_pm25 = raw_value_pm25 / 1000
|
||||||
|
|
||||||
|
|
||||||
|
lsw_pm10 = int.from_bytes(byte_data[11:13], byteorder='big')
|
||||||
|
msw_pm10 = int.from_bytes(byte_data[13:15], byteorder='big')
|
||||||
|
raw_value_pm10 = (msw_pm10 << 16) | lsw_pm10
|
||||||
|
raw_value_pm10 = raw_value_pm10 / 1000
|
||||||
|
|
||||||
|
print("1 min")
|
||||||
|
print(f"PM1: {raw_value_pm1}")
|
||||||
|
print(f"PM2.5: {raw_value_pm25}")
|
||||||
|
print(f"PM10: {raw_value_pm10}")
|
||||||
|
|
||||||
|
break
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print("User interrupt encountered. Exiting...")
|
||||||
|
time.sleep(3)
|
||||||
|
exit()
|
||||||
|
except:
|
||||||
|
# for all other kinds of error, but not specifying which one
|
||||||
|
print("Unknown error...")
|
||||||
|
time.sleep(3)
|
||||||
|
exit()
|
||||||
|
|
||||||
|
conn.close()
|
||||||
232
README.md
232
README.md
@@ -4,30 +4,46 @@ Based on the Rpi4 or CM4.
|
|||||||
|
|
||||||
# Installation
|
# Installation
|
||||||
|
|
||||||
Installation can be made with Ansible or the classic way.
|
# Express
|
||||||
|
|
||||||
|
You can download the `installation_part1.sh` and run it:
|
||||||
|
```
|
||||||
|
wget http://gitea.aircarto.fr/PaulVua/nebuleair_pro_4g/raw/branch/main/installation_part1.sh
|
||||||
|
chmod +x installation_part1.sh
|
||||||
|
sudo ./installation_part1.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
After reboot you can do the same with part 2.
|
||||||
|
|
||||||
|
```
|
||||||
|
wget http://gitea.aircarto.fr/PaulVua/nebuleair_pro_4g/raw/branch/main/installation_part2.sh
|
||||||
|
chmod +x installation_part2.sh
|
||||||
|
sudo ./installation_part2.sh
|
||||||
|
```
|
||||||
|
|
||||||
## Ansible (WORK IN PROGRESS)
|
|
||||||
Installation with Ansible will use a playbook `install_software.yml`.
|
|
||||||
|
|
||||||
## General
|
## General
|
||||||
|
|
||||||
See `installation.sh`
|
Line by line installation.
|
||||||
|
|
||||||
```
|
```
|
||||||
sudo apt update
|
sudo apt update
|
||||||
sudo apt install git gh apache2 php python3 python3-pip jq autossh i2c-tools python3-smbus -y
|
sudo apt install git gh apache2 sqlite3 php php-sqlite3 python3 python3-pip jq autossh i2c-tools python3-smbus -y
|
||||||
sudo pip3 install pyserial requests RPi.GPIO adafruit-circuitpython-bme280 --break-system-packages
|
sudo pip3 install pyserial requests RPi.GPIO adafruit-circuitpython-bme280 crcmod psutil ntplib pytz gpiozero adafruit-circuitpython-ads1x15 numpy nsrt-mk3-dev --break-system-packages
|
||||||
sudo mkdir -p /var/www/.ssh
|
sudo mkdir -p /var/www/.ssh
|
||||||
sudo ssh-keygen -t rsa -b 4096 -f /var/www/.ssh/id_rsa -N ""
|
sudo ssh-keygen -t rsa -b 4096 -f /var/www/.ssh/id_rsa -N ""
|
||||||
sudo ssh-copy-id -i /var/www/.ssh/id_rsa.pub -p 50221 airlab_server1@aircarto.fr
|
sudo ssh-copy-id -i /var/www/.ssh/id_rsa.pub -p 50221 airlab_server1@aircarto.fr
|
||||||
sudo git clone http://gitea.aircarto.fr/PaulVua/nebuleair_pro_4g.git /var/www/nebuleair_pro_4g
|
sudo git clone http://gitea.aircarto.fr/PaulVua/nebuleair_pro_4g.git /var/www/nebuleair_pro_4g
|
||||||
sudo mkdir /var/www/nebuleair_pro_4g/logs
|
sudo mkdir /var/www/nebuleair_pro_4g/logs
|
||||||
sudo touch /var/www/nebuleair_pro_4g/logs/app.log /var/www/nebuleair_pro_4g/logs/loop.log /var/www/nebuleair_pro_4g/wifi_list.csv
|
sudo touch /var/www/nebuleair_pro_4g/logs/app.log /var/www/nebuleair_pro_4g/logs/loop.log /var/www/nebuleair_pro_4g/wifi_list.csv
|
||||||
sudo cp /var/www/nebuleair_pro_4g/config.json.dist /var/www/nebuleair_pro_4g/config.json
|
/usr/bin/python3 /var/www/nebuleair_pro_4g/sqlite/create_db.py
|
||||||
|
/usr/bin/python3 /var/www/nebuleair_pro_4g/sqlite/set_config.py
|
||||||
sudo chmod -R 777 /var/www/nebuleair_pro_4g/
|
sudo chmod -R 777 /var/www/nebuleair_pro_4g/
|
||||||
git config --global core.fileMode false
|
git config --global core.fileMode false
|
||||||
|
git -C /var/www/nebuleair_pro_4g config core.fileMode false
|
||||||
git config --global --add safe.directory /var/www/nebuleair_pro_4g
|
git config --global --add safe.directory /var/www/nebuleair_pro_4g
|
||||||
sudo crontab /var/www/nebuleair_pro_4g/cron_jobs
|
sudo crontab /var/www/nebuleair_pro_4g/cron_jobs
|
||||||
|
sudo /usr/bin/python3 /var/www/nebuleair_pro_4g/sqlite/create_db.py
|
||||||
```
|
```
|
||||||
## Apache
|
## Apache
|
||||||
Configuration of Apache to redirect to the html homepage project
|
Configuration of Apache to redirect to the html homepage project
|
||||||
@@ -42,7 +58,10 @@ To make things simpler we will allow all users to use "nmcli" as sudo without en
|
|||||||
ALL ALL=(ALL) NOPASSWD: /usr/bin/nmcli, /usr/sbin/reboot
|
ALL ALL=(ALL) NOPASSWD: /usr/bin/nmcli, /usr/sbin/reboot
|
||||||
www-data ALL=(ALL) NOPASSWD: /usr/bin/git pull
|
www-data ALL=(ALL) NOPASSWD: /usr/bin/git pull
|
||||||
www-data ALL=(ALL) NOPASSWD: /usr/bin/ssh
|
www-data ALL=(ALL) NOPASSWD: /usr/bin/ssh
|
||||||
|
www-data ALL=(ALL) NOPASSWD: /usr/bin/python3 *
|
||||||
|
www-data ALL=(ALL) NOPASSWD: /bin/systemctl *
|
||||||
|
www-data ALL=(ALL) NOPASSWD: /usr/bin/pkill
|
||||||
|
www-data ALL=(ALL) NOPASSWD: /var/www/nebuleair_pro_4g/*
|
||||||
```
|
```
|
||||||
## Serial
|
## Serial
|
||||||
|
|
||||||
@@ -66,7 +85,7 @@ sudo chmod 777 /dev/ttyAMA*
|
|||||||
|
|
||||||
## I2C
|
## I2C
|
||||||
|
|
||||||
Decibel meter and BME280 is connected via I2C.
|
Decibel meter, BME280 and the RTC module (DS3231) is connected via I2C.
|
||||||
|
|
||||||
Need to activate by modifying `sudo nano /boot/firmware/config.txt`
|
Need to activate by modifying `sudo nano /boot/firmware/config.txt`
|
||||||
|
|
||||||
@@ -82,14 +101,26 @@ sudo chmod 777 /dev/i2c-1
|
|||||||
|
|
||||||
Attention: sometimes activation with config.txt do not work, you need to activate i2c with `sudo raspi-config` and go to "Interface" -> I2C -> enable.
|
Attention: sometimes activation with config.txt do not work, you need to activate i2c with `sudo raspi-config` and go to "Interface" -> I2C -> enable.
|
||||||
|
|
||||||
|
It is possible to manage raspi-config only with cli: `sudo raspi-config nonint do_i2c 0`
|
||||||
|
|
||||||
|
|
||||||
|
I2C addresses: use `sudo i2cdetect -y 1` to check the connected devices.
|
||||||
|
|
||||||
### BME280
|
### BME280
|
||||||
|
|
||||||
The python script is triggered by the main loop every minutes to get instant temp, hum and press values (no need to have a minute average).
|
The python script is triggered by the main loop every minutes to get instant temp, hum and press values (no need to have a minute average).
|
||||||
|
BME280 address is 0x76.
|
||||||
|
|
||||||
|
### RTC module (DS3231)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
### Noise sensor
|
### Noise sensor
|
||||||
|
|
||||||
As noise varies a lot, we keep the C program running every seconds to create a moving average for the last 60 seconds (we also gather max and min values).
|
As noise varies a lot, we keep the C program running every seconds to create a moving average for the last 60 seconds (we also gather max and min values).
|
||||||
To keep the script running at boot and stay on we create a systemd service
|
To keep the script running at boot and stay on we create a systemd service.
|
||||||
|
|
||||||
|
Nois sensor address is 0x48.
|
||||||
|
|
||||||
Create the service with `sudo nano /etc/systemd/system/sound_meter.service` and add:
|
Create the service with `sudo nano /etc/systemd/system/sound_meter.service` and add:
|
||||||
```
|
```
|
||||||
@@ -149,43 +180,146 @@ And set the base URL for Sara R4 communication:
|
|||||||
@reboot sleep 30 && /usr/bin/python3 /var/www/nebuleair_pro_4g/SARA/sara_setURL.py ttyAMA2 data.nebuleair.fr >> /var/www/nebuleair_pro_4g/logs/app.log 2>&1
|
@reboot sleep 30 && /usr/bin/python3 /var/www/nebuleair_pro_4g/SARA/sara_setURL.py ttyAMA2 data.nebuleair.fr >> /var/www/nebuleair_pro_4g/logs/app.log 2>&1
|
||||||
```
|
```
|
||||||
|
|
||||||
### With only 1 NPM
|
|
||||||
|
|
||||||
Loop every minutes to get the PM values and send it to the server:
|
## UDP Payload Miotiq — Structure 100 bytes
|
||||||
|
|
||||||
|
| Bytes | Taille | Nom | Format | Description |
|
||||||
|
|-------|--------|-----|--------|-------------|
|
||||||
|
| 0-7 | 8 | device_id | ASCII | Identifiant unique du capteur |
|
||||||
|
| 8 | 1 | signal_quality | uint8 | Qualite signal modem (AT+CSQ) |
|
||||||
|
| 9 | 1 | protocol_version | uint8 | Version protocole (0x01) |
|
||||||
|
| 10-11 | 2 | pm1 | uint16 BE | PM1.0 en ug/m3 (x10) |
|
||||||
|
| 12-13 | 2 | pm25 | uint16 BE | PM2.5 en ug/m3 (x10) |
|
||||||
|
| 14-15 | 2 | pm10 | uint16 BE | PM10 en ug/m3 (x10) |
|
||||||
|
| 16-17 | 2 | temperature | int16 BE | Temperature en C (x100, signe) |
|
||||||
|
| 18-19 | 2 | humidity | uint16 BE | Humidite en % (x100) |
|
||||||
|
| 20-21 | 2 | pressure | uint16 BE | Pression en hPa |
|
||||||
|
| 22-23 | 2 | noise_cur_leq | uint16 BE | Bruit LEQ en dB(A) (x10) |
|
||||||
|
| 24-25 | 2 | noise_cur_level | uint16 BE | Bruit instantane en dB(A) (x10) |
|
||||||
|
| 26-27 | 2 | noise_max | uint16 BE | Bruit max en dB(A) (x10) |
|
||||||
|
| 28-29 | 2 | envea_no2 | uint16 BE | NO2 en ppb |
|
||||||
|
| 30-31 | 2 | envea_h2s | uint16 BE | H2S en ppb |
|
||||||
|
| 32-33 | 2 | envea_nh3 | uint16 BE | NH3 en ppb |
|
||||||
|
| 34-35 | 2 | envea_co | uint16 BE | CO en ppb |
|
||||||
|
| 36-37 | 2 | envea_o3 | uint16 BE | O3 en ppb |
|
||||||
|
| 38-39 | 2 | npm_ch1 | uint16 BE | NPM canal 1 (5-channel) |
|
||||||
|
| 40-41 | 2 | npm_ch2 | uint16 BE | NPM canal 2 (5-channel) |
|
||||||
|
| 42-43 | 2 | npm_ch3 | uint16 BE | NPM canal 3 (5-channel) |
|
||||||
|
| 44-45 | 2 | npm_ch4 | uint16 BE | NPM canal 4 (5-channel) |
|
||||||
|
| 46-47 | 2 | npm_ch5 | uint16 BE | NPM canal 5 (5-channel) |
|
||||||
|
| 48-49 | 2 | mppt_temperature | int16 BE | Temperature MPPT en C (x10, signe) |
|
||||||
|
| 50-51 | 2 | mppt_humidity | uint16 BE | Humidite MPPT en % (x10) |
|
||||||
|
| 52-53 | 2 | battery_voltage | uint16 BE | Tension batterie en V (x100) |
|
||||||
|
| 54-55 | 2 | battery_current | int16 BE | Courant batterie en A (x100, signe) |
|
||||||
|
| 56-57 | 2 | solar_voltage | uint16 BE | Tension solaire en V (x100) |
|
||||||
|
| 58-59 | 2 | solar_power | uint16 BE | Puissance solaire en W |
|
||||||
|
| 60-61 | 2 | charger_status | uint16 BE | Status chargeur MPPT |
|
||||||
|
| 62-63 | 2 | wind_speed | uint16 BE | Vitesse vent en m/s (x10) |
|
||||||
|
| 64-65 | 2 | wind_direction | uint16 BE | Direction vent en degres |
|
||||||
|
| 66 | 1 | error_flags | uint8 | Erreurs systeme (voir detail) |
|
||||||
|
| 67 | 1 | npm_status | uint8 | Registre status NextPM |
|
||||||
|
| 68 | 1 | device_status | uint8 | Etat general du boitier |
|
||||||
|
| 69 | 1 | version_major | uint8 | Version firmware major |
|
||||||
|
| 70 | 1 | version_minor | uint8 | Version firmware minor |
|
||||||
|
| 71 | 1 | version_patch | uint8 | Version firmware patch |
|
||||||
|
| 72-99 | 28 | reserved | — | Reserve (initialise a 0xFF) |
|
||||||
|
|
||||||
|
### Consommation data (UDP Miotiq uniquement)
|
||||||
|
|
||||||
|
Taille par paquet : 100 bytes payload + 8 bytes UDP header + 20 bytes IP header = **128 bytes**
|
||||||
|
|
||||||
|
| | Toutes les 60s | Toutes les 10s |
|
||||||
|
|---|---|---|
|
||||||
|
| Paquets/jour | 1 440 | 8 640 |
|
||||||
|
| Par jour | ~180 KB | ~1.08 MB |
|
||||||
|
| Par mois | ~5.3 MB | ~32.4 MB |
|
||||||
|
| Par an | ~63.6 MB | ~388.8 MB |
|
||||||
|
|
||||||
|
> Note : ces chiffres ne comptent que l'UDP vers Miotiq. Les envois HTTP (AirCarto) et HTTPS (uSpot) consomment des donnees supplementaires.
|
||||||
|
|
||||||
|
### Parser Miotiq
|
||||||
|
|
||||||
```
|
```
|
||||||
* * * * * /usr/bin/python3 /var/www/nebuleair_pro_4g/loop/1_NPM/send_data.py >> /var/www/nebuleair_pro_4g/logs/loop.log 2>&1
|
16|device_id|string|||W
|
||||||
|
2|signal_quality|hex2dec|dB||
|
||||||
|
2|version|hex2dec|||W
|
||||||
|
4|ISO_68|hex2dec|ugm3|x/10|
|
||||||
|
4|ISO_39|hex2dec|ugm3|x/10|
|
||||||
|
4|ISO_24|hex2dec|ugm3|x/10|
|
||||||
|
4|ISO_54|hex2dec|degC|x/100|
|
||||||
|
4|ISO_55|hex2dec|%|x/100|
|
||||||
|
4|ISO_53|hex2dec|hPa||
|
||||||
|
4|noise_cur_leq|hex2dec|dB|x/10|
|
||||||
|
4|noise_cur_level|hex2dec|dB|x/10|
|
||||||
|
4|max_noise|hex2dec|dB|x/10|
|
||||||
|
4|ISO_03|hex2dec|ppb||
|
||||||
|
4|ISO_05|hex2dec|ppb||
|
||||||
|
4|ISO_21|hex2dec|ppb||
|
||||||
|
4|ISO_04|hex2dec|ppb||
|
||||||
|
4|ISO_08|hex2dec|ppb||
|
||||||
|
4|npm_ch1|hex2dec|count||
|
||||||
|
4|npm_ch2|hex2dec|count||
|
||||||
|
4|npm_ch3|hex2dec|count||
|
||||||
|
4|npm_ch4|hex2dec|count||
|
||||||
|
4|npm_ch5|hex2dec|count||
|
||||||
|
4|npm_temp|hex2dec|°C|x/10|
|
||||||
|
4|npm_humidity|hex2dec|%|x/10|
|
||||||
|
4|battery_voltage|hex2dec|V|x/100|
|
||||||
|
4|battery_current|hex2dec|A|x/100|
|
||||||
|
4|solar_voltage|hex2dec|V|x/100|
|
||||||
|
4|solar_power|hex2dec|W||
|
||||||
|
4|charger_status|hex2dec|||
|
||||||
|
4|wind_speed|hex2dec|m/s|x/10|
|
||||||
|
4|wind_direction|hex2dec|degrees||
|
||||||
|
2|error_flags|hex2dec|||
|
||||||
|
2|npm_status|hex2dec|||
|
||||||
|
2|device_status|hex2dec|||
|
||||||
|
2|version_major|hex2dec|||
|
||||||
|
2|version_minor|hex2dec|||
|
||||||
|
2|version_patch|hex2dec|||
|
||||||
|
22|reserved|skip|||
|
||||||
```
|
```
|
||||||
|
|
||||||
All in one:
|
### Byte 66 — error_flags
|
||||||
|
|
||||||
```
|
| Bit | Masque | Description |
|
||||||
@reboot chmod 777 /dev/ttyAMA*
|
|-----|--------|-------------|
|
||||||
@reboot /var/www/nebuleair_pro_4g/boot_hotspot.sh >> /var/www/nebuleair_pro_4g/logs/app.log 2>&1
|
| 0 | 0x01 | RTC deconnecte |
|
||||||
@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
|
| 1 | 0x02 | RTC reset (annee 2000) |
|
||||||
* * * * * /usr/bin/python3 /var/www/nebuleair_pro_4g/loop/1_NPM/send_data.py >> /var/www/nebuleair_pro_4g/logs/loop.log 2>&1
|
| 2 | 0x04 | BME280 erreur |
|
||||||
0 0 */2 * * > /var/www/nebuleair_pro_4g/logs/loop.log
|
| 3 | 0x08 | NPM erreur |
|
||||||
```
|
| 4 | 0x10 | Envea erreur |
|
||||||
|
| 5 | 0x20 | Bruit erreur |
|
||||||
|
| 6 | 0x40 | MPPT erreur |
|
||||||
|
| 7 | 0x80 | Vent erreur |
|
||||||
|
|
||||||
### With 3 NPM
|
### Byte 67 — npm_status
|
||||||
Loop every minutes to get the PM values and send it to the server:
|
|
||||||
|
|
||||||
```
|
| Bit | Masque | Description |
|
||||||
* * * * * /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 | 0x01 | Sleep mode |
|
||||||
```
|
| 1 | 0x02 | Degraded mode |
|
||||||
|
| 2 | 0x04 | Not ready |
|
||||||
|
| 3 | 0x08 | Heater error |
|
||||||
|
| 4 | 0x10 | THP sensor error |
|
||||||
|
| 5 | 0x20 | Fan error |
|
||||||
|
| 6 | 0x40 | Memory error |
|
||||||
|
| 7 | 0x80 | Laser error |
|
||||||
|
|
||||||
All in one:
|
### Byte 68 — device_status
|
||||||
|
|
||||||
```
|
| Bit | Masque | Description |
|
||||||
@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
|
| 0 | 0x01 | Modem reboot au cycle precedent |
|
||||||
@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
|
| 1 | 0x02 | WiFi connecte |
|
||||||
* * * * * /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
|
| 2 | 0x04 | Hotspot actif |
|
||||||
* * * * * 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
|
| 3 | 0x08 | Pas de fix GPS |
|
||||||
0 0 */2 * * > /var/www/nebuleair_pro_4g/logs/loop.log
|
| 4 | 0x10 | Batterie faible |
|
||||||
```
|
| 5 | 0x20 | Disque plein |
|
||||||
|
| 6 | 0x40 | Erreur base SQLite |
|
||||||
|
| 7 | 0x80 | Boot recent (uptime < 5 min) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
# Notes
|
# Notes
|
||||||
|
|
||||||
@@ -219,4 +353,28 @@ This can be doned with script boot_hotspot.sh.
|
|||||||
@reboot /var/www/nebuleair_pro_4g/boot_hotspot.sh
|
@reboot /var/www/nebuleair_pro_4g/boot_hotspot.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Claude Code
|
||||||
|
|
||||||
|
Instructions to use claude code on the RPI.
|
||||||
|
|
||||||
|
### Install NPM
|
||||||
|
|
||||||
|
```
|
||||||
|
sudo apt install -y nodejs npm
|
||||||
|
node -v
|
||||||
|
npm -v
|
||||||
|
```
|
||||||
|
|
||||||
|
### Install Claude
|
||||||
|
|
||||||
|
```
|
||||||
|
sudo npm install -g @anthropic-ai/claude-code
|
||||||
|
```
|
||||||
|
|
||||||
|
### Run claude
|
||||||
|
|
||||||
|
```
|
||||||
|
claude
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
77
RTC/read.py
Executable file
77
RTC/read.py
Executable file
@@ -0,0 +1,77 @@
|
|||||||
|
'''
|
||||||
|
____ _____ ____
|
||||||
|
| _ \_ _/ ___|
|
||||||
|
| |_) || || |
|
||||||
|
| _ < | || |___
|
||||||
|
|_| \_\|_| \____|
|
||||||
|
|
||||||
|
Script to read time from RTC module
|
||||||
|
I2C connection
|
||||||
|
Address 0x68
|
||||||
|
/usr/bin/python3 /var/www/nebuleair_pro_4g/RTC/read.py
|
||||||
|
'''
|
||||||
|
import smbus2
|
||||||
|
import time
|
||||||
|
import json
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
# DS3231 I2C address
|
||||||
|
DS3231_ADDR = 0x68
|
||||||
|
|
||||||
|
# Registers for DS3231
|
||||||
|
REG_TIME = 0x00
|
||||||
|
|
||||||
|
def bcd_to_dec(bcd):
|
||||||
|
return (bcd // 16 * 10) + (bcd % 16)
|
||||||
|
|
||||||
|
def read_time(bus):
|
||||||
|
"""Try to read and decode time from the RTC module (DS3231)."""
|
||||||
|
try:
|
||||||
|
data = bus.read_i2c_block_data(DS3231_ADDR, REG_TIME, 7)
|
||||||
|
seconds = bcd_to_dec(data[0] & 0x7F)
|
||||||
|
minutes = bcd_to_dec(data[1])
|
||||||
|
hours = bcd_to_dec(data[2] & 0x3F)
|
||||||
|
day = bcd_to_dec(data[4])
|
||||||
|
month = bcd_to_dec(data[5])
|
||||||
|
year = bcd_to_dec(data[6]) + 2000
|
||||||
|
return datetime(year, month, day, hours, minutes, seconds)
|
||||||
|
except OSError:
|
||||||
|
return None # RTC module not connected
|
||||||
|
|
||||||
|
def main():
|
||||||
|
# Read RTC time
|
||||||
|
bus = smbus2.SMBus(1)
|
||||||
|
# Try to read RTC time
|
||||||
|
rtc_time = read_time(bus)
|
||||||
|
|
||||||
|
# Get current system time
|
||||||
|
system_time = datetime.now() #local
|
||||||
|
utc_time = datetime.utcnow() #UTC
|
||||||
|
|
||||||
|
# If RTC is not connected, set default message
|
||||||
|
# Calculate time difference (in seconds) if RTC is connected
|
||||||
|
if rtc_time:
|
||||||
|
rtc_time_str = rtc_time.strftime('%Y-%m-%d %H:%M:%S')
|
||||||
|
time_difference = int((utc_time - rtc_time).total_seconds()) # Convert to int
|
||||||
|
else:
|
||||||
|
rtc_time_str = "not connected"
|
||||||
|
time_difference = "N/A" # Not applicable
|
||||||
|
|
||||||
|
# Print both times
|
||||||
|
#print(f"RTC module Time: {rtc_time.strftime('%Y-%m-%d %H:%M:%S')}")
|
||||||
|
#print(f"Sys local Time: {system_time.strftime('%Y-%m-%d %H:%M:%S')}")
|
||||||
|
#print(f"Sys UTC Time: {utc_time.strftime('%Y-%m-%d %H:%M:%S')}")
|
||||||
|
|
||||||
|
# Create JSON output
|
||||||
|
time_data = {
|
||||||
|
"rtc_module_time":rtc_time_str,
|
||||||
|
"system_local_time": system_time.strftime('%Y-%m-%d %H:%M:%S'),
|
||||||
|
"system_utc_time": utc_time.strftime('%Y-%m-%d %H:%M:%S'),
|
||||||
|
"time_difference_seconds": time_difference
|
||||||
|
}
|
||||||
|
|
||||||
|
print(json.dumps(time_data, indent=4))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
129
RTC/save_to_db.py
Executable file
129
RTC/save_to_db.py
Executable file
@@ -0,0 +1,129 @@
|
|||||||
|
'''
|
||||||
|
____ _____ ____
|
||||||
|
| _ \_ _/ ___|
|
||||||
|
| |_) || || |
|
||||||
|
| _ < | || |___
|
||||||
|
|_| \_\|_| \____|
|
||||||
|
|
||||||
|
Script to read time from RTC module and save it to DB
|
||||||
|
I2C connection
|
||||||
|
Address 0x68
|
||||||
|
/usr/bin/python3 /var/www/nebuleair_pro_4g/RTC/save_to_db.py
|
||||||
|
|
||||||
|
This need to be run as a system service
|
||||||
|
|
||||||
|
--> sudo nano /etc/systemd/system/rtc_save_to_db.service
|
||||||
|
|
||||||
|
⬇️
|
||||||
|
[Unit]
|
||||||
|
Description=RTC Save to DB Script
|
||||||
|
After=network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
ExecStart=/usr/bin/python3 /var/www/nebuleair_pro_4g/RTC/save_to_db.py
|
||||||
|
Restart=always
|
||||||
|
RestartSec=1
|
||||||
|
User=root
|
||||||
|
WorkingDirectory=/var/www/nebuleair_pro_4g
|
||||||
|
StandardOutput=append:/var/www/nebuleair_pro_4g/logs/rtc_save_to_db.log
|
||||||
|
StandardError=append:/var/www/nebuleair_pro_4g/logs/rtc_save_to_db_errors.log
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
⬆️
|
||||||
|
|
||||||
|
|
||||||
|
sudo systemctl daemon-reload
|
||||||
|
sudo systemctl enable rtc_save_to_db.service
|
||||||
|
|
||||||
|
sudo systemctl start rtc_save_to_db.service
|
||||||
|
|
||||||
|
sudo systemctl status rtc_save_to_db.service
|
||||||
|
|
||||||
|
'''
|
||||||
|
import smbus2
|
||||||
|
import time
|
||||||
|
import json
|
||||||
|
from datetime import datetime
|
||||||
|
import sqlite3
|
||||||
|
|
||||||
|
# DS3231 I2C address
|
||||||
|
DS3231_ADDR = 0x68
|
||||||
|
|
||||||
|
# Registers for DS3231
|
||||||
|
REG_TIME = 0x00
|
||||||
|
|
||||||
|
# Connect to (or create if not existent) the database
|
||||||
|
DB_PATH = "/var/www/nebuleair_pro_4g/sqlite/sensors.db"
|
||||||
|
|
||||||
|
def bcd_to_dec(bcd):
|
||||||
|
return (bcd // 16 * 10) + (bcd % 16)
|
||||||
|
|
||||||
|
def read_time(bus):
|
||||||
|
"""Try to read and decode time from the RTC module (DS3231)."""
|
||||||
|
try:
|
||||||
|
data = bus.read_i2c_block_data(DS3231_ADDR, REG_TIME, 7)
|
||||||
|
seconds = bcd_to_dec(data[0] & 0x7F)
|
||||||
|
minutes = bcd_to_dec(data[1])
|
||||||
|
hours = bcd_to_dec(data[2] & 0x3F)
|
||||||
|
day = bcd_to_dec(data[4])
|
||||||
|
month = bcd_to_dec(data[5])
|
||||||
|
year = bcd_to_dec(data[6]) + 2000
|
||||||
|
return datetime(year, month, day, hours, minutes, seconds)
|
||||||
|
except OSError:
|
||||||
|
return None # RTC module not connected
|
||||||
|
|
||||||
|
def main():
|
||||||
|
# Read RTC time
|
||||||
|
bus = smbus2.SMBus(1)
|
||||||
|
|
||||||
|
while True:
|
||||||
|
# Open a new database connection inside the loop to prevent connection loss
|
||||||
|
conn = sqlite3.connect(DB_PATH)
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
# Try to read RTC time
|
||||||
|
rtc_time = read_time(bus)
|
||||||
|
# Get current system time
|
||||||
|
system_time = datetime.now() #local
|
||||||
|
utc_time = datetime.utcnow() #UTC
|
||||||
|
|
||||||
|
# If RTC is not connected, set default message
|
||||||
|
# Calculate time difference (in seconds) if RTC is connected
|
||||||
|
if rtc_time:
|
||||||
|
rtc_time_str = rtc_time.strftime('%Y-%m-%d %H:%M:%S')
|
||||||
|
time_difference = int((utc_time - rtc_time).total_seconds()) # Convert to int
|
||||||
|
else:
|
||||||
|
rtc_time_str = "not connected"
|
||||||
|
time_difference = "N/A" # Not applicable
|
||||||
|
|
||||||
|
# Print both times
|
||||||
|
#print(f"RTC module Time: {rtc_time.strftime('%Y-%m-%d %H:%M:%S')}")
|
||||||
|
#print(f"Sys local Time: {system_time.strftime('%Y-%m-%d %H:%M:%S')}")
|
||||||
|
#print(f"Sys UTC Time: {utc_time.strftime('%Y-%m-%d %H:%M:%S')}")
|
||||||
|
|
||||||
|
# Create JSON output
|
||||||
|
time_data = {
|
||||||
|
"rtc_module_time":rtc_time_str,
|
||||||
|
"system_local_time": system_time.strftime('%Y-%m-%d %H:%M:%S'),
|
||||||
|
"system_utc_time": utc_time.strftime('%Y-%m-%d %H:%M:%S'),
|
||||||
|
"time_difference_seconds": time_difference
|
||||||
|
}
|
||||||
|
|
||||||
|
#print(json.dumps(time_data, indent=4))
|
||||||
|
|
||||||
|
# Save to database
|
||||||
|
try:
|
||||||
|
cursor.execute("UPDATE timestamp_table SET last_updated = ? WHERE id = 1", (rtc_time_str,))
|
||||||
|
conn.commit()
|
||||||
|
#print("Sensor data saved successfully!")
|
||||||
|
|
||||||
|
except sqlite3.Error as e:
|
||||||
|
print(f"Database error: {e}")
|
||||||
|
|
||||||
|
conn.close() # Close connection to avoid database locking issues
|
||||||
|
time.sleep(1) # Wait for 1 second before reading again
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
183
RTC/set_with_NTP.py
Executable file
183
RTC/set_with_NTP.py
Executable file
@@ -0,0 +1,183 @@
|
|||||||
|
#!/usr/bin/python3
|
||||||
|
"""
|
||||||
|
____ _____ ____
|
||||||
|
| _ \_ _/ ___|
|
||||||
|
| |_) || || |
|
||||||
|
| _ < | || |___
|
||||||
|
|_| \_\|_| \____|
|
||||||
|
|
||||||
|
Script to set the RTC using an NTP server (script used by web UI)
|
||||||
|
RPI needs to be connected to the internet (WIFI).
|
||||||
|
Requires ntplib and pytz:
|
||||||
|
sudo pip3 install ntplib pytz --break-system-packages
|
||||||
|
"""
|
||||||
|
import smbus2
|
||||||
|
import time
|
||||||
|
from datetime import datetime
|
||||||
|
import ntplib
|
||||||
|
import pytz # For timezone handling
|
||||||
|
|
||||||
|
# DS3231 I2C address
|
||||||
|
DS3231_ADDR = 0x68
|
||||||
|
|
||||||
|
# Registers for DS3231
|
||||||
|
REG_TIME = 0x00
|
||||||
|
|
||||||
|
def bcd_to_dec(bcd):
|
||||||
|
"""Convert BCD to decimal."""
|
||||||
|
return (bcd // 16 * 10) + (bcd % 16)
|
||||||
|
|
||||||
|
def dec_to_bcd(dec):
|
||||||
|
"""Convert decimal to BCD."""
|
||||||
|
return (dec // 10 * 16) + (dec % 10)
|
||||||
|
|
||||||
|
def set_time(bus, year, month, day, hour, minute, second):
|
||||||
|
"""Set the RTC time."""
|
||||||
|
# Convert the time to BCD format
|
||||||
|
second_bcd = dec_to_bcd(second)
|
||||||
|
minute_bcd = dec_to_bcd(minute)
|
||||||
|
hour_bcd = dec_to_bcd(hour)
|
||||||
|
day_bcd = dec_to_bcd(day)
|
||||||
|
month_bcd = dec_to_bcd(month)
|
||||||
|
year_bcd = dec_to_bcd(year - 2000) # DS3231 uses year from 2000
|
||||||
|
|
||||||
|
# Write time to DS3231
|
||||||
|
bus.write_i2c_block_data(DS3231_ADDR, REG_TIME, [
|
||||||
|
second_bcd,
|
||||||
|
minute_bcd,
|
||||||
|
hour_bcd,
|
||||||
|
0x01, # Day of the week (1=Monday, etc.)
|
||||||
|
day_bcd,
|
||||||
|
month_bcd,
|
||||||
|
year_bcd
|
||||||
|
])
|
||||||
|
|
||||||
|
def read_time(bus):
|
||||||
|
"""Read the RTC time and validate the values."""
|
||||||
|
try:
|
||||||
|
data = bus.read_i2c_block_data(DS3231_ADDR, REG_TIME, 7)
|
||||||
|
|
||||||
|
# Convert from BCD
|
||||||
|
second = bcd_to_dec(data[0] & 0x7F)
|
||||||
|
minute = bcd_to_dec(data[1])
|
||||||
|
hour = bcd_to_dec(data[2] & 0x3F)
|
||||||
|
day = bcd_to_dec(data[4])
|
||||||
|
month = bcd_to_dec(data[5])
|
||||||
|
year = bcd_to_dec(data[6]) + 2000
|
||||||
|
|
||||||
|
# Print raw values for debugging
|
||||||
|
print(f"Raw RTC values: {data}")
|
||||||
|
print(f"Decoded values: Y:{year} M:{month} D:{day} H:{hour} M:{minute} S:{second}")
|
||||||
|
|
||||||
|
# Validate date values
|
||||||
|
if not (1 <= month <= 12):
|
||||||
|
print(f"Invalid month value: {month}, using default")
|
||||||
|
month = 1
|
||||||
|
|
||||||
|
# Check days in month (simplified)
|
||||||
|
days_in_month = [0, 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
|
||||||
|
if not (1 <= day <= days_in_month[month]):
|
||||||
|
print(f"Invalid day value: {day} for month {month}, using default")
|
||||||
|
day = 1
|
||||||
|
|
||||||
|
# Validate time values
|
||||||
|
if not (0 <= hour <= 23):
|
||||||
|
print(f"Invalid hour value: {hour}, using default")
|
||||||
|
hour = 0
|
||||||
|
|
||||||
|
if not (0 <= minute <= 59):
|
||||||
|
print(f"Invalid minute value: {minute}, using default")
|
||||||
|
minute = 0
|
||||||
|
|
||||||
|
if not (0 <= second <= 59):
|
||||||
|
print(f"Invalid second value: {second}, using default")
|
||||||
|
second = 0
|
||||||
|
|
||||||
|
return (year, month, day, hour, minute, second)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error reading RTC: {e}")
|
||||||
|
# Return a safe default date (2023-01-01 00:00:00)
|
||||||
|
return (2023, 1, 1, 0, 0, 0)
|
||||||
|
|
||||||
|
def get_internet_time():
|
||||||
|
"""Get the current time from an NTP server."""
|
||||||
|
ntp_client = ntplib.NTPClient()
|
||||||
|
# Try multiple NTP servers in case one fails
|
||||||
|
servers = ['pool.ntp.org', 'time.google.com', 'time.windows.com', 'time.apple.com']
|
||||||
|
|
||||||
|
for server in servers:
|
||||||
|
try:
|
||||||
|
print(f"Trying NTP server: {server}")
|
||||||
|
response = ntp_client.request(server, timeout=2)
|
||||||
|
utc_time = datetime.utcfromtimestamp(response.tx_time)
|
||||||
|
print(f"Successfully got time from {server}")
|
||||||
|
return utc_time
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Failed to get time from {server}: {e}")
|
||||||
|
|
||||||
|
# If all servers fail, raise exception
|
||||||
|
raise Exception("All NTP servers failed")
|
||||||
|
|
||||||
|
def main():
|
||||||
|
try:
|
||||||
|
bus = smbus2.SMBus(1)
|
||||||
|
|
||||||
|
# Test if RTC is accessible
|
||||||
|
try:
|
||||||
|
bus.read_byte(DS3231_ADDR)
|
||||||
|
print("RTC module is accessible")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error accessing RTC module: {e}")
|
||||||
|
print("Please check connections and I2C configuration")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Get the current time from the RTC
|
||||||
|
try:
|
||||||
|
year, month, day, hours, minutes, seconds = read_time(bus)
|
||||||
|
# Create datetime object with validation to handle invalid dates
|
||||||
|
rtc_time = datetime(year, month, day, hours, minutes, seconds)
|
||||||
|
print(f"Actual RTC Time : {rtc_time.strftime('%Y-%m-%d %H:%M:%S')}")
|
||||||
|
except ValueError as e:
|
||||||
|
print(f"Invalid date/time read from RTC: {e}")
|
||||||
|
print("Will proceed with setting RTC from internet time")
|
||||||
|
rtc_time = None
|
||||||
|
|
||||||
|
# Get current UTC time from an NTP server
|
||||||
|
try:
|
||||||
|
internet_utc_time = get_internet_time()
|
||||||
|
print(f"Time from Internet (UTC) : {internet_utc_time.strftime('%Y-%m-%d %H:%M:%S')}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error retrieving time from the internet: {e}")
|
||||||
|
if rtc_time is None:
|
||||||
|
print("Cannot proceed without either valid RTC time or internet time")
|
||||||
|
return
|
||||||
|
print("Will keep current RTC time")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Set the RTC to UTC time
|
||||||
|
print("Setting RTC to internet time...")
|
||||||
|
set_time(bus, internet_utc_time.year, internet_utc_time.month, internet_utc_time.day,
|
||||||
|
internet_utc_time.hour, internet_utc_time.minute, internet_utc_time.second)
|
||||||
|
|
||||||
|
# Read and print the new time from RTC
|
||||||
|
print("Reading back new RTC time...")
|
||||||
|
year, month, day, hour, minute, second = read_time(bus)
|
||||||
|
rtc_time_new = datetime(year, month, day, hour, minute, second)
|
||||||
|
print(f"New RTC Time (UTC) : {rtc_time_new.strftime('%Y-%m-%d %H:%M:%S')}")
|
||||||
|
|
||||||
|
# Calculate difference to verify accuracy
|
||||||
|
time_diff = abs((rtc_time_new - internet_utc_time).total_seconds())
|
||||||
|
print(f"Time difference : {time_diff:.2f} seconds")
|
||||||
|
|
||||||
|
if time_diff > 5:
|
||||||
|
print("Warning: RTC time differs significantly from internet time")
|
||||||
|
print("You may need to retry or check RTC module")
|
||||||
|
else:
|
||||||
|
print("RTC successfully synchronized with internet time")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Unexpected error: {e}")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
53
RTC/set_with_browserTime.py
Executable file
53
RTC/set_with_browserTime.py
Executable file
@@ -0,0 +1,53 @@
|
|||||||
|
"""
|
||||||
|
____ _____ ____
|
||||||
|
| _ \_ _/ ___|
|
||||||
|
| |_) || || |
|
||||||
|
| _ < | || |___
|
||||||
|
|_| \_\|_| \____|
|
||||||
|
|
||||||
|
Script to set the RTC using the browser time (script used by the web UI).
|
||||||
|
|
||||||
|
/usr/bin/python3 /var/www/nebuleair_pro_4g/RTC/set_with_browserTime.py '2024-01-30 12:48:39'
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
from datetime import datetime
|
||||||
|
import smbus2
|
||||||
|
|
||||||
|
# DS3231 I2C address
|
||||||
|
DS3231_ADDR = 0x68
|
||||||
|
REG_TIME = 0x00
|
||||||
|
|
||||||
|
def dec_to_bcd(dec):
|
||||||
|
"""Convert decimal to BCD."""
|
||||||
|
return (dec // 10 * 16) + (dec % 10)
|
||||||
|
|
||||||
|
def set_rtc(bus, year, month, day, hour, minute, second):
|
||||||
|
"""Set the RTC time."""
|
||||||
|
second_bcd = dec_to_bcd(second)
|
||||||
|
minute_bcd = dec_to_bcd(minute)
|
||||||
|
hour_bcd = dec_to_bcd(hour)
|
||||||
|
day_bcd = dec_to_bcd(day)
|
||||||
|
month_bcd = dec_to_bcd(month)
|
||||||
|
year_bcd = dec_to_bcd(year - 2000) # RTC stores years since 2000
|
||||||
|
|
||||||
|
bus.write_i2c_block_data(DS3231_ADDR, REG_TIME, [
|
||||||
|
second_bcd, minute_bcd, hour_bcd, 0x01, day_bcd, month_bcd, year_bcd
|
||||||
|
])
|
||||||
|
|
||||||
|
def main():
|
||||||
|
if len(sys.argv) != 2:
|
||||||
|
print("Usage: python3 set_rtc.py 'YYYY-MM-DD HH:MM:SS'")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
rtc_time_str = sys.argv[1]
|
||||||
|
rtc_time = datetime.strptime(rtc_time_str, '%Y-%m-%d %H:%M:%S')
|
||||||
|
|
||||||
|
bus = smbus2.SMBus(1)
|
||||||
|
set_rtc(bus, rtc_time.year, rtc_time.month, rtc_time.day,
|
||||||
|
rtc_time.hour, rtc_time.minute, rtc_time.second)
|
||||||
|
print(f"RTC updated to: {rtc_time}")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
14
SARA/PPP/README.md
Normal file
14
SARA/PPP/README.md
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
# PPP activation
|
||||||
|
|
||||||
|
Une fois la connexion PPP activée on peut retrouver la connexion pp0 avec `ifconfig`.
|
||||||
|
|
||||||
|
### Test avec curl
|
||||||
|
|
||||||
|
On peut forcer l'utilisation du réseau pp0 avec curl:
|
||||||
|
|
||||||
|
`curl --interface ppp0 https://ifconfig.me`
|
||||||
|
|
||||||
|
ou avec ping:
|
||||||
|
|
||||||
|
`ping -I ppp0 google.com`
|
||||||
|
|
||||||
4
SARA/PPP/activate_ppp.sh
Normal file
4
SARA/PPP/activate_ppp.sh
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
|
||||||
|
sudo pppd /dev/ttyAMA2 115200 \
|
||||||
|
connect '/usr/sbin/chat -v -s "" "AT" OK "ATD*99#" CONNECT' \
|
||||||
|
noauth debug dump nodetach nocrtscts
|
||||||
121
SARA/R5/setPDP.py
Normal file
121
SARA/R5/setPDP.py
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
'''
|
||||||
|
____ _ ____ _
|
||||||
|
/ ___| / \ | _ \ / \
|
||||||
|
\___ \ / _ \ | |_) | / _ \
|
||||||
|
___) / ___ \| _ < / ___ \
|
||||||
|
|____/_/ \_\_| \_\/_/ \_\
|
||||||
|
|
||||||
|
Script to set the PDP context for the SARA R5
|
||||||
|
|
||||||
|
|
||||||
|
/usr/bin/python3 /var/www/nebuleair_pro_4g/SARA/R5/setPDP.py
|
||||||
|
|
||||||
|
'''
|
||||||
|
import serial
|
||||||
|
import time
|
||||||
|
import sys
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
|
||||||
|
|
||||||
|
ser_sara = serial.Serial(
|
||||||
|
port='/dev/ttyAMA2',
|
||||||
|
baudrate=115200, #115200 ou 9600
|
||||||
|
parity=serial.PARITY_NONE, #PARITY_NONE, PARITY_EVEN or PARITY_ODD
|
||||||
|
stopbits=serial.STOPBITS_ONE,
|
||||||
|
bytesize=serial.EIGHTBITS,
|
||||||
|
timeout = 2
|
||||||
|
)
|
||||||
|
|
||||||
|
def read_complete_response(serial_connection, timeout=2, end_of_response_timeout=2, wait_for_lines=None, debug=True):
|
||||||
|
'''
|
||||||
|
Fonction très importante !!!
|
||||||
|
Reads the complete response from a serial connection and waits for specific lines.
|
||||||
|
'''
|
||||||
|
if wait_for_lines is None:
|
||||||
|
wait_for_lines = [] # Default to an empty list if not provided
|
||||||
|
|
||||||
|
response = bytearray()
|
||||||
|
serial_connection.timeout = timeout
|
||||||
|
end_time = time.time() + end_of_response_timeout
|
||||||
|
start_time = time.time()
|
||||||
|
|
||||||
|
while True:
|
||||||
|
elapsed_time = time.time() - start_time # Time since function start
|
||||||
|
if serial_connection.in_waiting > 0:
|
||||||
|
data = serial_connection.read(serial_connection.in_waiting)
|
||||||
|
response.extend(data)
|
||||||
|
end_time = time.time() + end_of_response_timeout # Reset timeout on new data
|
||||||
|
|
||||||
|
# Decode and check for any target line
|
||||||
|
decoded_response = response.decode('utf-8', errors='replace')
|
||||||
|
for target_line in wait_for_lines:
|
||||||
|
if target_line in decoded_response:
|
||||||
|
if debug:
|
||||||
|
print(f"[DEBUG] 🔎 Found target line: {target_line} (in {elapsed_time:.2f}s)")
|
||||||
|
return decoded_response # Return response immediately if a target line is found
|
||||||
|
elif time.time() > end_time:
|
||||||
|
if debug:
|
||||||
|
print(f"[DEBUG] Timeout reached. No more data received.")
|
||||||
|
break
|
||||||
|
time.sleep(0.1) # Short sleep to prevent busy waiting
|
||||||
|
|
||||||
|
# Final response and debug output
|
||||||
|
total_elapsed_time = time.time() - start_time
|
||||||
|
if debug:
|
||||||
|
print(f"[DEBUG] ⏱️ elapsed time: {total_elapsed_time:.2f}s. ⏱️")
|
||||||
|
# Check if the elapsed time exceeded 10 seconds
|
||||||
|
if total_elapsed_time > 10 and debug:
|
||||||
|
print(f"[ALERT] 🚨 The operation took too long 🚨")
|
||||||
|
print(f'<span style="color: red;font-weight: bold;">[ALERT] ⚠️{total_elapsed_time:.2f}s⚠️</span>')
|
||||||
|
|
||||||
|
return response.decode('utf-8', errors='replace') # Return the full response if no target line is found
|
||||||
|
|
||||||
|
try:
|
||||||
|
print('Start script')
|
||||||
|
|
||||||
|
# 1. Check connection
|
||||||
|
print('➡️Check SARA R5 connexion')
|
||||||
|
command = f'ATI0\r'
|
||||||
|
ser_sara.write(command.encode('utf-8'))
|
||||||
|
response_SARA_1 = read_complete_response(ser_sara, wait_for_lines=["OK"])
|
||||||
|
print(response_SARA_1, end="")
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
# 2. Activate PDP context 1
|
||||||
|
print('➡️Activate PDP context 1')
|
||||||
|
command = f'AT+CGACT=1,1\r'
|
||||||
|
ser_sara.write(command.encode('utf-8'))
|
||||||
|
response_SARA_2 = read_complete_response(ser_sara, wait_for_lines=["OK"])
|
||||||
|
print(response_SARA_2, end="")
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
# 2. Set the PDP type
|
||||||
|
print('➡️Set the PDP type to IPv4 referring to the outputof the +CGDCONT read command')
|
||||||
|
command = f'AT+UPSD=0,0,0\r'
|
||||||
|
ser_sara.write(command.encode('utf-8'))
|
||||||
|
response_SARA_3 = read_complete_response(ser_sara, wait_for_lines=["OK"])
|
||||||
|
print(response_SARA_3, end="")
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
# 2. Profile #0 is mapped on CID=1.
|
||||||
|
print('➡️Profile #0 is mapped on CID=1.')
|
||||||
|
command = f'AT+UPSD=0,100,1\r'
|
||||||
|
ser_sara.write(command.encode('utf-8'))
|
||||||
|
response_SARA_3 = read_complete_response(ser_sara, wait_for_lines=["OK"])
|
||||||
|
print(response_SARA_3, end="")
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
# 2. Set the PDP type
|
||||||
|
print('➡️Activate the PSD profile #0: the IPv4 address is already assigned by the network.')
|
||||||
|
command = f'AT+UPSDA=0,3\r'
|
||||||
|
ser_sara.write(command.encode('utf-8'))
|
||||||
|
response_SARA_3 = read_complete_response(ser_sara, wait_for_lines=["OK","+UUPSDA"])
|
||||||
|
print(response_SARA_3, end="")
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print("An error occurred:", e)
|
||||||
|
traceback.print_exc() # This prints the full traceback
|
||||||
@@ -25,38 +25,35 @@ url = parameter[1] # ex: data.mobileair.fr
|
|||||||
|
|
||||||
profile_id = 3
|
profile_id = 3
|
||||||
|
|
||||||
#get baudrate
|
baudrate = 115200
|
||||||
def load_config(config_file):
|
send_uSpot = False
|
||||||
try:
|
|
||||||
with open(config_file, 'r') as file:
|
|
||||||
config_data = json.load(file)
|
|
||||||
return config_data
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Error loading config file: {e}")
|
|
||||||
return {}
|
|
||||||
|
|
||||||
# Define the config file path
|
def read_complete_response(serial_connection, timeout=2, end_of_response_timeout=2, wait_for_line=None):
|
||||||
config_file = '/var/www/nebuleair_pro_4g/config.json'
|
|
||||||
# Load the configuration data
|
|
||||||
config = load_config(config_file)
|
|
||||||
# Access the shared variables
|
|
||||||
baudrate = config.get('SaraR4_baudrate', 115200)
|
|
||||||
send_uSpot = config.get('send_uSpot', False)
|
|
||||||
|
|
||||||
def read_complete_response(serial_connection, timeout=2, end_of_response_timeout=2):
|
|
||||||
response = bytearray()
|
response = bytearray()
|
||||||
serial_connection.timeout = timeout
|
serial_connection.timeout = timeout
|
||||||
end_time = time.time() + end_of_response_timeout
|
end_time = time.time() + end_of_response_timeout
|
||||||
|
start_time = time.time()
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
|
elapsed_time = time.time() - start_time # Time since function start
|
||||||
if serial_connection.in_waiting > 0:
|
if serial_connection.in_waiting > 0:
|
||||||
data = serial_connection.read(serial_connection.in_waiting)
|
data = serial_connection.read(serial_connection.in_waiting)
|
||||||
response.extend(data)
|
response.extend(data)
|
||||||
end_time = time.time() + end_of_response_timeout # Reset timeout on new data
|
end_time = time.time() + end_of_response_timeout # Reset timeout on new data
|
||||||
|
|
||||||
|
# Decode and check for the specific line
|
||||||
|
if wait_for_line:
|
||||||
|
decoded_response = response.decode('utf-8', errors='replace')
|
||||||
|
if wait_for_line in decoded_response:
|
||||||
|
print(f"[DEBUG] 🔎Found target line: {wait_for_line}")
|
||||||
|
break
|
||||||
elif time.time() > end_time:
|
elif time.time() > end_time:
|
||||||
|
print(f"[DEBUG] Timeout reached. No more data received.")
|
||||||
break
|
break
|
||||||
time.sleep(0.1) # Short sleep to prevent busy waiting
|
time.sleep(0.1) # Short sleep to prevent busy waiting
|
||||||
|
# Final response and debug output
|
||||||
|
total_elapsed_time = time.time() - start_time
|
||||||
|
print(f"[DEBUG] ⏱️ elapsed time: {total_elapsed_time:.2f}s. ⏱️")
|
||||||
return response.decode('utf-8', errors='replace')
|
return response.decode('utf-8', errors='replace')
|
||||||
|
|
||||||
ser_sara = serial.Serial(
|
ser_sara = serial.Serial(
|
||||||
@@ -97,7 +94,8 @@ try:
|
|||||||
print("Trigger POST REQUEST")
|
print("Trigger POST REQUEST")
|
||||||
command = f'AT+UHTTPC={profile_id},1,"/pro_4G/test.php","http.resp"\r'
|
command = f'AT+UHTTPC={profile_id},1,"/pro_4G/test.php","http.resp"\r'
|
||||||
ser_sara.write((command + '\r').encode('utf-8'))
|
ser_sara.write((command + '\r').encode('utf-8'))
|
||||||
response_SARA_6 = read_complete_response(ser_sara)
|
response_SARA_6 = read_complete_response(ser_sara, timeout=5, end_of_response_timeout=50, wait_for_line="+UUHTTPCR")
|
||||||
|
|
||||||
print(response_SARA_6)
|
print(response_SARA_6)
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
|
|
||||||
|
|||||||
@@ -26,23 +26,8 @@ url = parameter[1] # ex: data.mobileair.fr
|
|||||||
|
|
||||||
profile_id = 3
|
profile_id = 3
|
||||||
|
|
||||||
#get baudrate
|
baudrate = 115200
|
||||||
def load_config(config_file):
|
send_uSpot = False
|
||||||
try:
|
|
||||||
with open(config_file, 'r') as file:
|
|
||||||
config_data = json.load(file)
|
|
||||||
return config_data
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Error loading config file: {e}")
|
|
||||||
return {}
|
|
||||||
|
|
||||||
# Define the config file path
|
|
||||||
config_file = '/var/www/nebuleair_pro_4g/config.json'
|
|
||||||
# Load the configuration data
|
|
||||||
config = load_config(config_file)
|
|
||||||
# Access the shared variables
|
|
||||||
baudrate = config.get('SaraR4_baudrate', 115200)
|
|
||||||
send_uSpot = config.get('send_uSpot', False)
|
|
||||||
|
|
||||||
def read_complete_response(serial_connection, timeout=2, end_of_response_timeout=2):
|
def read_complete_response(serial_connection, timeout=2, end_of_response_timeout=2):
|
||||||
response = bytearray()
|
response = bytearray()
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
'''
|
'''
|
||||||
Script to set the URL for a HTTP request and trigger the POST Request
|
Script to set the URL for a HTTP request and trigger the POST Request
|
||||||
Ex:
|
Ex:
|
||||||
/usr/bin/python3 /var/www/nebuleair_pro_4g/SARA/SSL/full_test_HTTPS_POST.py ttyAMA2 api-prod.uspot.probesys.net
|
/usr/bin/python3 /var/www/nebuleair_pro_4g/SARA/SSL/full_test_HTTPS_POST.py ttyAMA2 api-prod.uspot.probesys.net /nebuleair?token=2AFF6dQk68daFZ
|
||||||
|
/usr/bin/python3 /var/www/nebuleair_pro_4g/SARA/SSL/full_test_HTTPS_POST.py ttyAMA2 webhook.site /0904d7b1-2558-43b9-8b35-df5bc40df967
|
||||||
|
/usr/bin/python3 /var/www/nebuleair_pro_4g/SARA/SSL/full_test_HTTPS_POST.py ttyAMA2 aircarto.fr /tests/test.php
|
||||||
|
/usr/bin/python3 /var/www/nebuleair_pro_4g/SARA/SSL/full_test_HTTPS_POST.py ttyAMA2 ssl.aircarto.fr /test.php
|
||||||
|
|
||||||
|
|
||||||
First profile id:
|
First profile id:
|
||||||
@@ -22,25 +25,11 @@ parameter = sys.argv[1:] # Exclude the script name
|
|||||||
#print("Parameters received:")
|
#print("Parameters received:")
|
||||||
port='/dev/'+parameter[0] # ex: ttyAMA2
|
port='/dev/'+parameter[0] # ex: ttyAMA2
|
||||||
url = parameter[1] # ex: data.mobileair.fr
|
url = parameter[1] # ex: data.mobileair.fr
|
||||||
|
endpoint = parameter[2]
|
||||||
profile_id = 2
|
profile_id = 2
|
||||||
|
|
||||||
#get baudrate
|
baudrate = 115200
|
||||||
def load_config(config_file):
|
send_uSpot = False
|
||||||
try:
|
|
||||||
with open(config_file, 'r') as file:
|
|
||||||
config_data = json.load(file)
|
|
||||||
return config_data
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Error loading config file: {e}")
|
|
||||||
return {}
|
|
||||||
|
|
||||||
# Define the config file path
|
|
||||||
config_file = '/var/www/nebuleair_pro_4g/config.json'
|
|
||||||
# Load the configuration data
|
|
||||||
config = load_config(config_file)
|
|
||||||
# Access the shared variables
|
|
||||||
baudrate = config.get('SaraR4_baudrate', 115200)
|
|
||||||
send_uSpot = config.get('send_uSpot', False)
|
|
||||||
|
|
||||||
def color_text(text, color):
|
def color_text(text, color):
|
||||||
colors = {
|
colors = {
|
||||||
@@ -96,15 +85,15 @@ ser_sara = serial.Serial(
|
|||||||
try:
|
try:
|
||||||
#step 1: import the certificate
|
#step 1: import the certificate
|
||||||
print("****")
|
print("****")
|
||||||
with open("/var/www/nebuleair_pro_4g/SARA/SSL/certificate/e6.der", "rb") as cert_file:
|
certificate_name = "e6"
|
||||||
|
with open("/var/www/nebuleair_pro_4g/SARA/SSL/certificate/e6.pem", "rb") as cert_file:
|
||||||
certificate = cert_file.read()
|
certificate = cert_file.read()
|
||||||
|
|
||||||
size_of_string = len(certificate)
|
size_of_string = len(certificate)
|
||||||
|
|
||||||
print("\033[0;33m Import certificate\033[0m")
|
print("\033[0;33m Import certificate\033[0m")
|
||||||
# AT+USECMNG=0,<type>,<internal_name>,<data_size>
|
# AT+USECMNG=0,<type>,<internal_name>,<data_size>
|
||||||
# type-> 0 -> trusted root CA
|
# type-> 0 -> trusted root CA
|
||||||
command = f'AT+USECMNG=0,0,"e6",{size_of_string}\r'
|
command = f'AT+USECMNG=0,0,"{certificate_name}",{size_of_string}\r'
|
||||||
ser_sara.write((command + '\r').encode('utf-8'))
|
ser_sara.write((command + '\r').encode('utf-8'))
|
||||||
response_SARA_1 = read_complete_response(ser_sara)
|
response_SARA_1 = read_complete_response(ser_sara)
|
||||||
print(response_SARA_1)
|
print(response_SARA_1)
|
||||||
@@ -129,30 +118,34 @@ try:
|
|||||||
# *******************************
|
# *******************************
|
||||||
# SECURITY PROFILE
|
# SECURITY PROFILE
|
||||||
# AT+USECPRF=<profile_id>[,<op_code>[,<param_val>]]
|
# AT+USECPRF=<profile_id>[,<op_code>[,<param_val>]]
|
||||||
|
security_profile_id = 1
|
||||||
|
|
||||||
|
|
||||||
# op_code: 0 -> certificate validation level
|
# op_code: 0 -> certificate validation level
|
||||||
# param_val : 0 -> Level 0 No validation; 1-> Level 1 Root certificate validation
|
# param_val : 0 -> Level 0 No validation; 1-> Level 1 Root certificate validation
|
||||||
security_profile_id = 0
|
|
||||||
print("\033[0;33mSet the security profile (params)\033[0m")
|
print("\033[0;33mSet the security profile (params)\033[0m")
|
||||||
command = f'AT+USECPRF={security_profile_id},0,0\r'
|
certification_level=0
|
||||||
|
command = f'AT+USECPRF={security_profile_id},0,{certification_level}\r'
|
||||||
ser_sara.write((command + '\r').encode('utf-8'))
|
ser_sara.write((command + '\r').encode('utf-8'))
|
||||||
response_SARA_5b = read_complete_response(ser_sara, wait_for_line="OK")
|
response_SARA_5b = read_complete_response(ser_sara, wait_for_line="OK")
|
||||||
print(response_SARA_5b)
|
print(response_SARA_5b)
|
||||||
time.sleep(0.5)
|
time.sleep(0.5)
|
||||||
|
|
||||||
# op_code: 1 -> minimum SSL/TLS version
|
# op_code: 1 -> minimum SSL/TLS version
|
||||||
# param_val : 0 -> any; server can use any version for the connection; 1-> LSv1.0;
|
# param_val : 0 -> any; server can use any version for the connection; 1-> LSv1.0; 2->TLSv1.1; 3->TLSv1.2;
|
||||||
print("\033[0;33mSet the security profile (params)\033[0m")
|
print("\033[0;33mSet the security profile (params)\033[0m")
|
||||||
command = f'AT+USECPRF={security_profile_id},1,0\r'
|
minimum_SSL_version = 0
|
||||||
|
command = f'AT+USECPRF={security_profile_id},1,{minimum_SSL_version}\r'
|
||||||
ser_sara.write((command + '\r').encode('utf-8'))
|
ser_sara.write((command + '\r').encode('utf-8'))
|
||||||
response_SARA_5bb = read_complete_response(ser_sara, wait_for_line="OK")
|
response_SARA_5bb = read_complete_response(ser_sara, wait_for_line="OK")
|
||||||
print(response_SARA_5bb)
|
print(response_SARA_5bb)
|
||||||
time.sleep(0.5)
|
time.sleep(0.5)
|
||||||
|
|
||||||
#op_code: 2 -> cipher suite
|
#op_code: 2 -> legacy cipher suite selection
|
||||||
|
# 0 (factory-programmed value): a list of default cipher suites is proposed at the beginning of handshake process, and a cipher suite will be negotiated among the cipher suites proposed in the list.
|
||||||
print("\033[0;33mSet cipher \033[0m")
|
print("\033[0;33mSet cipher \033[0m")
|
||||||
command = f'AT+USECPRF={security_profile_id},2,0\r'
|
cipher_suite = 0
|
||||||
|
command = f'AT+USECPRF={security_profile_id},2,{cipher_suite}\r'
|
||||||
ser_sara.write((command + '\r').encode('utf-8'))
|
ser_sara.write((command + '\r').encode('utf-8'))
|
||||||
response_SARA_5cc = read_complete_response(ser_sara, wait_for_line="OK")
|
response_SARA_5cc = read_complete_response(ser_sara, wait_for_line="OK")
|
||||||
print(response_SARA_5cc)
|
print(response_SARA_5cc)
|
||||||
@@ -160,12 +153,21 @@ try:
|
|||||||
|
|
||||||
# op_code: 3 -> trusted root certificate internal name
|
# op_code: 3 -> trusted root certificate internal name
|
||||||
print("\033[0;33mSet the security profile (choose cert)\033[0m")
|
print("\033[0;33mSet the security profile (choose cert)\033[0m")
|
||||||
command = f'AT+USECPRF={security_profile_id},3,"e6"\r'
|
command = f'AT+USECPRF={security_profile_id},3,"{certificate_name}"\r'
|
||||||
ser_sara.write((command + '\r').encode('utf-8'))
|
ser_sara.write((command + '\r').encode('utf-8'))
|
||||||
response_SARA_5c = read_complete_response(ser_sara, wait_for_line="OK")
|
response_SARA_5c = read_complete_response(ser_sara, wait_for_line="OK")
|
||||||
print(response_SARA_5c)
|
print(response_SARA_5c)
|
||||||
time.sleep(0.5)
|
time.sleep(0.5)
|
||||||
|
|
||||||
|
# op_code: 10 -> SNI (server name indication)
|
||||||
|
print("\033[0;33mSet the SNI\033[0m")
|
||||||
|
command = f'AT+USECPRF={security_profile_id},10,"{url}"\r'
|
||||||
|
ser_sara.write((command + '\r').encode('utf-8'))
|
||||||
|
response_SARA_5cf = read_complete_response(ser_sara, wait_for_line="OK")
|
||||||
|
print(response_SARA_5cf)
|
||||||
|
time.sleep(0.5)
|
||||||
|
|
||||||
|
|
||||||
# *************************
|
# *************************
|
||||||
# *************************
|
# *************************
|
||||||
|
|
||||||
@@ -256,10 +258,12 @@ try:
|
|||||||
#step 4: trigger the request (http_command=1 for GET and http_command=1 for POST)
|
#step 4: trigger the request (http_command=1 for GET and http_command=1 for POST)
|
||||||
print("****")
|
print("****")
|
||||||
print("\033[0;33mPOST REQUEST\033[0m")
|
print("\033[0;33mPOST REQUEST\033[0m")
|
||||||
|
#parameter (POST)
|
||||||
|
command = f'AT+UHTTPC={profile_id},4,"{endpoint}","https.resp","sensordata_json.json",4\r'
|
||||||
#AIRCARTO
|
#AIRCARTO
|
||||||
#command = f'AT+UHTTPC={profile_id},4,"/tests/test.php","https.resp","sensordata_json.json",4\r'
|
#command = f'AT+UHTTPC={profile_id},4,"/tests/test.php","https.resp","sensordata_json.json",4\r'
|
||||||
#uSPOT
|
#uSPOT
|
||||||
command = f'AT+UHTTPC={profile_id},4,"/nebuleair?token=2AFF6dQk68daFZ","https.resp","sensordata_json.json",4\r'
|
#command = f'AT+UHTTPC={profile_id},4,"/nebuleair?token=2AFF6dQk68daFZ","https.resp","sensordata_json.json",4\r'
|
||||||
#AtmoSud
|
#AtmoSud
|
||||||
#command = f'AT+UHTTPC={profile_id},1,"/","https.resp"\r'
|
#command = f'AT+UHTTPC={profile_id},1,"/","https.resp"\r'
|
||||||
#Webhook
|
#Webhook
|
||||||
@@ -271,7 +275,7 @@ try:
|
|||||||
# Wait for the +UUHTTPCR response
|
# Wait for the +UUHTTPCR response
|
||||||
print("Waiting for +UUHTTPCR response...")
|
print("Waiting for +UUHTTPCR response...")
|
||||||
|
|
||||||
response_SARA_3 = read_complete_response(ser_sara, timeout=5, end_of_response_timeout=30, wait_for_line="+UUHTTPCR")
|
response_SARA_3 = read_complete_response(ser_sara, timeout=5, end_of_response_timeout=50, wait_for_line="+UUHTTPCR")
|
||||||
|
|
||||||
print("\033[0;34m")
|
print("\033[0;34m")
|
||||||
print(response_SARA_3)
|
print(response_SARA_3)
|
||||||
@@ -307,7 +311,7 @@ try:
|
|||||||
print(response_SARA_8)
|
print(response_SARA_8)
|
||||||
|
|
||||||
# Get error code
|
# Get error code
|
||||||
print("\033[0;33mEmpty Memory\033[0m")
|
print("\033[0;33mGet error code\033[0m")
|
||||||
command = f'AT+UHTTPER={profile_id}\r'
|
command = f'AT+UHTTPER={profile_id}\r'
|
||||||
ser_sara.write(command.encode('utf-8'))
|
ser_sara.write(command.encode('utf-8'))
|
||||||
response_SARA_9 = read_complete_response(ser_sara, wait_for_line="OK")
|
response_SARA_9 = read_complete_response(ser_sara, wait_for_line="OK")
|
||||||
@@ -321,8 +325,10 @@ try:
|
|||||||
3 HTTP Protocol error class
|
3 HTTP Protocol error class
|
||||||
10 Wrong HTTP API USAGE
|
10 Wrong HTTP API USAGE
|
||||||
|
|
||||||
error_code
|
error_code (for error_class 3 or 10)
|
||||||
0 No error
|
0 No error
|
||||||
|
11 Server connection error
|
||||||
|
22 PSD or CSD connection not established
|
||||||
73 Secure socket connect error
|
73 Secure socket connect error
|
||||||
'''
|
'''
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ FONCTIONNE SUR data.nebuleair.fr
|
|||||||
FONCTIONNE SUR uSpot
|
FONCTIONNE SUR uSpot
|
||||||
Ex:
|
Ex:
|
||||||
/usr/bin/python3 /var/www/nebuleair_pro_4g/SARA/SSL/full_test_HTTP_POST.py ttyAMA2 api-prod.uspot.probesys.net
|
/usr/bin/python3 /var/www/nebuleair_pro_4g/SARA/SSL/full_test_HTTP_POST.py ttyAMA2 api-prod.uspot.probesys.net
|
||||||
|
/usr/bin/python3 /var/www/nebuleair_pro_4g/SARA/SSL/full_test_HTTP_POST.py ttyAMA2 aircarto.fr /tests/test.php
|
||||||
|
/usr/bin/python3 /var/www/nebuleair_pro_4g/SARA/SSL/full_test_HTTP_POST.py ttyAMA2 ssl.aircarto.fr /test.php
|
||||||
|
|
||||||
To do: need to add profile id as parameter
|
To do: need to add profile id as parameter
|
||||||
|
|
||||||
@@ -25,25 +27,12 @@ parameter = sys.argv[1:] # Exclude the script name
|
|||||||
#print("Parameters received:")
|
#print("Parameters received:")
|
||||||
port='/dev/'+parameter[0] # ex: ttyAMA2
|
port='/dev/'+parameter[0] # ex: ttyAMA2
|
||||||
url = parameter[1] # ex: data.mobileair.fr
|
url = parameter[1] # ex: data.mobileair.fr
|
||||||
profile_id = 1
|
endpoint = parameter[2]
|
||||||
|
|
||||||
#get baudrate
|
profile_id = 3
|
||||||
def load_config(config_file):
|
|
||||||
try:
|
|
||||||
with open(config_file, 'r') as file:
|
|
||||||
config_data = json.load(file)
|
|
||||||
return config_data
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Error loading config file: {e}")
|
|
||||||
return {}
|
|
||||||
|
|
||||||
# Define the config file path
|
baudrate = 115200
|
||||||
config_file = '/var/www/nebuleair_pro_4g/config.json'
|
send_uSpot = False
|
||||||
# Load the configuration data
|
|
||||||
config = load_config(config_file)
|
|
||||||
# Access the shared variables
|
|
||||||
baudrate = config.get('SaraR4_baudrate', 115200)
|
|
||||||
send_uSpot = config.get('send_uSpot', False)
|
|
||||||
|
|
||||||
def read_complete_response(serial_connection, timeout=2, end_of_response_timeout=2):
|
def read_complete_response(serial_connection, timeout=2, end_of_response_timeout=2):
|
||||||
response = bytearray()
|
response = bytearray()
|
||||||
@@ -82,19 +71,11 @@ try:
|
|||||||
print(response_SARA_5)
|
print(response_SARA_5)
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
|
|
||||||
#step 4: set url to SSL (op_code = 6) (http_secure = 1 for HTTPS)(USECMNG_PROFILE = 2)
|
|
||||||
print("****")
|
|
||||||
print("SET SSL")
|
|
||||||
command = f'AT+UHTTP={profile_id},6,0\r'
|
|
||||||
ser_sara.write(command.encode('utf-8'))
|
|
||||||
response_SARA_5 = read_complete_response(ser_sara)
|
|
||||||
print(response_SARA_5)
|
|
||||||
time.sleep(1)
|
|
||||||
|
|
||||||
#step 4: set PORT (op_code = 5)
|
#step 4: set PORT (op_code = 5)
|
||||||
print("****")
|
print("****")
|
||||||
print("SET PORT")
|
print("SET PORT")
|
||||||
command = f'AT+UHTTP={profile_id},5,81\r'
|
command = f'AT+UHTTP={profile_id},5,80\r'
|
||||||
ser_sara.write((command + '\r').encode('utf-8'))
|
ser_sara.write((command + '\r').encode('utf-8'))
|
||||||
response_SARA_55 = read_complete_response(ser_sara)
|
response_SARA_55 = read_complete_response(ser_sara)
|
||||||
print(response_SARA_55)
|
print(response_SARA_55)
|
||||||
@@ -163,10 +144,14 @@ try:
|
|||||||
#step 4: trigger the request (http_command=1 for GET and http_command=1 for POST)
|
#step 4: trigger the request (http_command=1 for GET and http_command=1 for POST)
|
||||||
print("****")
|
print("****")
|
||||||
print("Trigger POST REQUEST")
|
print("Trigger POST REQUEST")
|
||||||
|
command = f'AT+UHTTPC={profile_id},4,"{endpoint}","http.resp","sensordata_json.json",4\r'
|
||||||
|
#AirCarto
|
||||||
#command = f'AT+UHTTPC={profile_id},1,"/tests/test.php","http.resp"\r'
|
#command = f'AT+UHTTPC={profile_id},1,"/tests/test.php","http.resp"\r'
|
||||||
command = f'AT+UHTTPC={profile_id},4,"/nebuleair?token=2AFF6dQk68daFZ","http.resp","sensordata_json.json",4\r'
|
|
||||||
#command = f'AT+UHTTPC={profile_id},4,"/wifi.php","http.resp","sensordata_json.json",4\r'
|
#command = f'AT+UHTTPC={profile_id},4,"/wifi.php","http.resp","sensordata_json.json",4\r'
|
||||||
#command = f'AT+UHTTPC={profile_id},4,"/pro_4G/data.php?sensor_id=52E7573A","http.resp","sensordata_json.json",4\r'
|
#command = f'AT+UHTTPC={profile_id},4,"/pro_4G/data.php?sensor_id=52E7573A","http.resp","sensordata_json.json",4\r'
|
||||||
|
#AtmoSud
|
||||||
|
#command = f'AT+UHTTPC={profile_id},4,"/nebuleair?token=2AFF6dQk68daFZ","http.resp","sensordata_json.json",4\r'
|
||||||
|
|
||||||
|
|
||||||
ser_sara.write(command.encode('utf-8'))
|
ser_sara.write(command.encode('utf-8'))
|
||||||
|
|
||||||
|
|||||||
@@ -21,23 +21,8 @@ port='/dev/'+parameter[0] # ex: ttyAMA2
|
|||||||
url = parameter[1] # ex: data.mobileair.fr
|
url = parameter[1] # ex: data.mobileair.fr
|
||||||
|
|
||||||
|
|
||||||
#get baudrate
|
baudrate = 115200
|
||||||
def load_config(config_file):
|
send_uSpot = False
|
||||||
try:
|
|
||||||
with open(config_file, 'r') as file:
|
|
||||||
config_data = json.load(file)
|
|
||||||
return config_data
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Error loading config file: {e}")
|
|
||||||
return {}
|
|
||||||
|
|
||||||
# Define the config file path
|
|
||||||
config_file = '/var/www/nebuleair_pro_4g/config.json'
|
|
||||||
# Load the configuration data
|
|
||||||
config = load_config(config_file)
|
|
||||||
# Access the shared variables
|
|
||||||
baudrate = config.get('SaraR4_baudrate', 115200)
|
|
||||||
send_uSpot = config.get('send_uSpot', False)
|
|
||||||
|
|
||||||
def read_complete_response(serial_connection, timeout=2, end_of_response_timeout=2):
|
def read_complete_response(serial_connection, timeout=2, end_of_response_timeout=2):
|
||||||
response = bytearray()
|
response = bytearray()
|
||||||
|
|||||||
@@ -23,24 +23,8 @@ parameter = sys.argv[1:] # Exclude the script name
|
|||||||
port='/dev/'+parameter[0] # ex: ttyAMA2
|
port='/dev/'+parameter[0] # ex: ttyAMA2
|
||||||
url = parameter[1] # ex: data.mobileair.fr
|
url = parameter[1] # ex: data.mobileair.fr
|
||||||
|
|
||||||
|
baudrate = 115200
|
||||||
#get baudrate
|
send_uSpot = False
|
||||||
def load_config(config_file):
|
|
||||||
try:
|
|
||||||
with open(config_file, 'r') as file:
|
|
||||||
config_data = json.load(file)
|
|
||||||
return config_data
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Error loading config file: {e}")
|
|
||||||
return {}
|
|
||||||
|
|
||||||
# Define the config file path
|
|
||||||
config_file = '/var/www/nebuleair_pro_4g/config.json'
|
|
||||||
# Load the configuration data
|
|
||||||
config = load_config(config_file)
|
|
||||||
# Access the shared variables
|
|
||||||
baudrate = config.get('SaraR4_baudrate', 115200)
|
|
||||||
send_uSpot = config.get('send_uSpot', False)
|
|
||||||
|
|
||||||
def read_complete_response(serial_connection, timeout=2, end_of_response_timeout=2):
|
def read_complete_response(serial_connection, timeout=2, end_of_response_timeout=2):
|
||||||
response = bytearray()
|
response = bytearray()
|
||||||
|
|||||||
@@ -14,19 +14,7 @@ parameter = sys.argv[1:] # Exclude the script name
|
|||||||
port = '/dev/' + parameter[0] # e.g., ttyAMA2
|
port = '/dev/' + parameter[0] # e.g., ttyAMA2
|
||||||
timeout = float(parameter[1]) # e.g., 2 seconds
|
timeout = float(parameter[1]) # e.g., 2 seconds
|
||||||
|
|
||||||
def load_config(config_file):
|
baudrate = 115200
|
||||||
try:
|
|
||||||
with open(config_file, 'r') as file:
|
|
||||||
config_data = json.load(file)
|
|
||||||
return config_data
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Error loading config file: {e}")
|
|
||||||
return {}
|
|
||||||
|
|
||||||
# Define the config file path
|
|
||||||
config_file = '/var/www/nebuleair_pro_4g/config.json'
|
|
||||||
config = load_config(config_file)
|
|
||||||
baudrate = config.get('SaraR4_baudrate', 115200)
|
|
||||||
|
|
||||||
def read_complete_response(serial_connection, timeout=2, end_of_response_timeout=2):
|
def read_complete_response(serial_connection, timeout=2, end_of_response_timeout=2):
|
||||||
response = bytearray()
|
response = bytearray()
|
||||||
|
|||||||
325
SARA/SSL/test_22.py
Executable file
325
SARA/SSL/test_22.py
Executable file
@@ -0,0 +1,325 @@
|
|||||||
|
'''
|
||||||
|
Script to set the URL for a HTTP request and trigger the POST Request
|
||||||
|
Ex:
|
||||||
|
/usr/bin/python3 /var/www/nebuleair_pro_4g/SARA/SSL/test_22.py ttyAMA2 api-prod.uspot.probesys.net /nebuleair?token=2AFF6dQk68daFZ
|
||||||
|
/usr/bin/python3 /var/www/nebuleair_pro_4g/SARA/SSL/test_22.py ttyAMA2 webhook.site /6bee2237-099a-4ff4-8452-9f4126df7151
|
||||||
|
/usr/bin/python3 /var/www/nebuleair_pro_4g/SARA/SSL/test_22.py ttyAMA2 aircarto.fr /tests/test.php
|
||||||
|
/usr/bin/python3 /var/www/nebuleair_pro_4g/SARA/SSL/test_22.py ttyAMA2 ssl.aircarto.fr /test.php
|
||||||
|
/usr/bin/python3 /var/www/nebuleair_pro_4g/SARA/SSL/test_22.py ttyAMA2 vps.aircarto.fr /test.php
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
First profile id:
|
||||||
|
AT+UHTTP=0,1,"data.nebuleair.fr"
|
||||||
|
Second profile id:
|
||||||
|
AT+UHTTP=1,1,"api-prod.uspot.probesys.net"
|
||||||
|
Third profile id:
|
||||||
|
AT+UHTTP=2,1,"aircarto.fr"
|
||||||
|
|
||||||
|
'''
|
||||||
|
|
||||||
|
import serial
|
||||||
|
import time
|
||||||
|
import sys
|
||||||
|
import json
|
||||||
|
|
||||||
|
parameter = sys.argv[1:] # Exclude the script name
|
||||||
|
#print("Parameters received:")
|
||||||
|
port='/dev/'+parameter[0] # ex: ttyAMA2
|
||||||
|
url = parameter[1] # ex: data.mobileair.fr
|
||||||
|
endpoint = parameter[2]
|
||||||
|
profile_id = 3
|
||||||
|
|
||||||
|
#get baudrate
|
||||||
|
def load_config(config_file):
|
||||||
|
try:
|
||||||
|
with open(config_file, 'r') as file:
|
||||||
|
config_data = json.load(file)
|
||||||
|
return config_data
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error loading config file: {e}")
|
||||||
|
return {}
|
||||||
|
|
||||||
|
# Define the config file path
|
||||||
|
config_file = '/var/www/nebuleair_pro_4g/config.json'
|
||||||
|
# Load the configuration data
|
||||||
|
config = load_config(config_file)
|
||||||
|
# Access the shared variables
|
||||||
|
baudrate = config.get('SaraR4_baudrate', 115200)
|
||||||
|
send_uSpot = config.get('send_uSpot', False)
|
||||||
|
|
||||||
|
def color_text(text, color):
|
||||||
|
colors = {
|
||||||
|
"red": "\033[31m",
|
||||||
|
"green": "\033[32m",
|
||||||
|
"yellow": "\033[33m",
|
||||||
|
"blue": "\033[34m",
|
||||||
|
"magenta": "\033[35m",
|
||||||
|
"cyan": "\033[36m",
|
||||||
|
"white": "\033[37m",
|
||||||
|
}
|
||||||
|
reset = "\033[0m"
|
||||||
|
return f"{colors.get(color, '')}{text}{reset}"
|
||||||
|
|
||||||
|
def read_complete_response(serial_connection, timeout=2, end_of_response_timeout=2, wait_for_line=None):
|
||||||
|
response = bytearray()
|
||||||
|
serial_connection.timeout = timeout
|
||||||
|
end_time = time.time() + end_of_response_timeout
|
||||||
|
start_time = time.time()
|
||||||
|
|
||||||
|
while True:
|
||||||
|
elapsed_time = time.time() - start_time # Time since function start
|
||||||
|
if serial_connection.in_waiting > 0:
|
||||||
|
data = serial_connection.read(serial_connection.in_waiting)
|
||||||
|
response.extend(data)
|
||||||
|
end_time = time.time() + end_of_response_timeout # Reset timeout on new data
|
||||||
|
|
||||||
|
# Decode and check for the specific line
|
||||||
|
if wait_for_line:
|
||||||
|
decoded_response = response.decode('utf-8', errors='replace')
|
||||||
|
if wait_for_line in decoded_response:
|
||||||
|
print(f"[DEBUG] 🔎Found target line: {wait_for_line}")
|
||||||
|
break
|
||||||
|
elif time.time() > end_time:
|
||||||
|
print(f"[DEBUG] Timeout reached. No more data received.")
|
||||||
|
break
|
||||||
|
time.sleep(0.1) # Short sleep to prevent busy waiting
|
||||||
|
# Final response and debug output
|
||||||
|
total_elapsed_time = time.time() - start_time
|
||||||
|
print(f"[DEBUG] ⏱️ elapsed time: {total_elapsed_time:.2f}s. ⏱️")
|
||||||
|
return response.decode('utf-8', errors='replace')
|
||||||
|
|
||||||
|
ser_sara = serial.Serial(
|
||||||
|
port=port, #USB0 or ttyS0
|
||||||
|
baudrate=baudrate, #115200 ou 9600
|
||||||
|
parity=serial.PARITY_NONE, #PARITY_NONE, PARITY_EVEN or PARITY_ODD
|
||||||
|
stopbits=serial.STOPBITS_ONE,
|
||||||
|
bytesize=serial.EIGHTBITS,
|
||||||
|
timeout = 2
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
try:
|
||||||
|
#step 1: import the certificate
|
||||||
|
print("****")
|
||||||
|
with open("/var/www/nebuleair_pro_4g/SARA/SSL/certificate/isrgrootx1.der", "rb") as cert_file:
|
||||||
|
certificate = cert_file.read()
|
||||||
|
|
||||||
|
size_of_string = len(certificate)
|
||||||
|
|
||||||
|
print("\033[0;33m Import certificate\033[0m")
|
||||||
|
# AT+USECMNG=0,<type>,<internal_name>,<data_size>
|
||||||
|
# type-> 0 -> trusted root CA
|
||||||
|
command = f'AT+USECMNG=0,0,"isrgrootx1",{size_of_string}\r'
|
||||||
|
ser_sara.write((command + '\r').encode('utf-8'))
|
||||||
|
response_SARA_1 = read_complete_response(ser_sara)
|
||||||
|
print(response_SARA_1)
|
||||||
|
|
||||||
|
time.sleep(0.5)
|
||||||
|
|
||||||
|
print("\033[0;33mAdd certificate\033[0m")
|
||||||
|
ser_sara.write(certificate)
|
||||||
|
response_SARA_2 = read_complete_response(ser_sara)
|
||||||
|
print(response_SARA_2)
|
||||||
|
|
||||||
|
time.sleep(0.5)
|
||||||
|
|
||||||
|
#check certificate (List all available certificates and private keys)
|
||||||
|
print("\033[0;33mCheck certificate\033[0m")
|
||||||
|
command = f'AT+USECMNG=3\r'
|
||||||
|
ser_sara.write((command + '\r').encode('utf-8'))
|
||||||
|
response_SARA_5b = read_complete_response(ser_sara, wait_for_line="OK")
|
||||||
|
print(response_SARA_5b)
|
||||||
|
time.sleep(0.5)
|
||||||
|
|
||||||
|
# *******************************
|
||||||
|
# SECURITY PROFILE
|
||||||
|
# AT+USECPRF=<profile_id>[,<op_code>[,<param_val>]]
|
||||||
|
security_profile_id = 2
|
||||||
|
|
||||||
|
|
||||||
|
# op_code: 0 -> certificate validation level
|
||||||
|
# param_val : 0 -> Level 0 No validation; 1-> Level 1 Root certificate validation
|
||||||
|
print("\033[0;33mSet the security profile (params)\033[0m")
|
||||||
|
certification_level=1
|
||||||
|
command = f'AT+USECPRF={security_profile_id},0,{certification_level}\r'
|
||||||
|
ser_sara.write((command + '\r').encode('utf-8'))
|
||||||
|
response_SARA_5b = read_complete_response(ser_sara, wait_for_line="OK")
|
||||||
|
print(response_SARA_5b)
|
||||||
|
time.sleep(0.5)
|
||||||
|
|
||||||
|
# op_code: 3 -> trusted root certificate internal name
|
||||||
|
print("\033[0;33mSet the security profile (choose cert)\033[0m")
|
||||||
|
command = f'AT+USECPRF={security_profile_id},3,"isrgrootx1"\r'
|
||||||
|
ser_sara.write((command + '\r').encode('utf-8'))
|
||||||
|
response_SARA_5c = read_complete_response(ser_sara, wait_for_line="OK")
|
||||||
|
print(response_SARA_5c)
|
||||||
|
time.sleep(0.5)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# *************************
|
||||||
|
# *************************
|
||||||
|
|
||||||
|
|
||||||
|
#step 4: set url (op_code = 1)
|
||||||
|
print("\033[0;33mSET URL\033[0m")
|
||||||
|
command = f'AT+UHTTP={profile_id},1,"{url}"\r'
|
||||||
|
ser_sara.write((command + '\r').encode('utf-8'))
|
||||||
|
response_SARA_5 = read_complete_response(ser_sara, wait_for_line="OK")
|
||||||
|
print(response_SARA_5)
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
|
||||||
|
#step 4: set url to SSL (op_code = 6) (http_secure = 1 for HTTPS)(USECMNG_PROFILE = 2)
|
||||||
|
print("\033[0;33mSET SSL\033[0m")
|
||||||
|
http_secure = 1
|
||||||
|
command = f'AT+UHTTP={profile_id},6,{http_secure},{security_profile_id}\r'
|
||||||
|
#command = f'AT+UHTTP={profile_id},6,{http_secure}\r'
|
||||||
|
|
||||||
|
ser_sara.write(command.encode('utf-8'))
|
||||||
|
response_SARA_5 = read_complete_response(ser_sara, wait_for_line="OK")
|
||||||
|
print(response_SARA_5)
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
# Write Data to saraR4
|
||||||
|
payload_json = {
|
||||||
|
"nebuleairid": "C04F8B8D3A08",
|
||||||
|
"software_version": "ModuleAir-V1-012023",
|
||||||
|
"sensordatavalues": [
|
||||||
|
{
|
||||||
|
"value_type": "NPM_P0",
|
||||||
|
"value": "2.3"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value_type": "NPM_P0",
|
||||||
|
"value": "3.30"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value_type": "NPM_P1",
|
||||||
|
"value": "9.05"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value_type": "NPM_P2",
|
||||||
|
"value": "20.60"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value_type": "NPM_N1",
|
||||||
|
"value": "49.00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value_type": "NPM_N10",
|
||||||
|
"value": "49.00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value_type": "NPM_N25",
|
||||||
|
"value": "49.00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value_type": "BME280_temperature",
|
||||||
|
"value": "25.82"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
# 1. Open sensordata_csv.json (with correct data size)
|
||||||
|
payload_string = json.dumps(payload_json) # Convert dict to JSON string
|
||||||
|
size_of_string = len(payload_string)
|
||||||
|
print("\033[0;33mOPEN JSON\033[0m")
|
||||||
|
command = f'AT+UDWNFILE="sensordata_json.json",{size_of_string}\r'
|
||||||
|
ser_sara.write(command.encode('utf-8'))
|
||||||
|
response_SARA_1 = read_complete_response(ser_sara, wait_for_line=">")
|
||||||
|
print(response_SARA_1)
|
||||||
|
time.sleep(0.5)
|
||||||
|
|
||||||
|
#2. Write to shell
|
||||||
|
print("\033[0;33mWrite to Memory\033[0m")
|
||||||
|
ser_sara.write(payload_string.encode())
|
||||||
|
response_SARA_2 = read_complete_response(ser_sara, wait_for_line="OK")
|
||||||
|
print(response_SARA_2)
|
||||||
|
|
||||||
|
#step 4: trigger the request (http_command=1 for GET and http_command=1 for POST)
|
||||||
|
print("****")
|
||||||
|
print("\033[0;33mPOST REQUEST\033[0m")
|
||||||
|
#parameter (POST)
|
||||||
|
command = f'AT+UHTTPC={profile_id},4,"{endpoint}","https.resp","sensordata_json.json",4\r'
|
||||||
|
#AIRCARTO
|
||||||
|
#command = f'AT+UHTTPC={profile_id},4,"/tests/test.php","https.resp","sensordata_json.json",4\r'
|
||||||
|
#uSPOT
|
||||||
|
#command = f'AT+UHTTPC={profile_id},4,"/nebuleair?token=2AFF6dQk68daFZ","https.resp","sensordata_json.json",4\r'
|
||||||
|
#AtmoSud
|
||||||
|
#command = f'AT+UHTTPC={profile_id},1,"/","https.resp"\r'
|
||||||
|
#Webhook
|
||||||
|
#command = f'AT+UHTTPC={profile_id},4,"/6bee2237-099a-4ff4-8452-9f4126df7151","https.resp","sensordata_json.json",4\r'
|
||||||
|
|
||||||
|
|
||||||
|
ser_sara.write(command.encode('utf-8'))
|
||||||
|
|
||||||
|
# Wait for the +UUHTTPCR response
|
||||||
|
print("Waiting for +UUHTTPCR response...")
|
||||||
|
|
||||||
|
response_SARA_3 = read_complete_response(ser_sara, timeout=5, end_of_response_timeout=30, wait_for_line="+UUHTTPCR")
|
||||||
|
|
||||||
|
print("\033[0;34m")
|
||||||
|
print(response_SARA_3)
|
||||||
|
print("\033[0m")
|
||||||
|
|
||||||
|
if "+UUHTTPCR" in response_SARA_3:
|
||||||
|
print("✅ Received +UUHTTPCR response.")
|
||||||
|
lines = response_SARA_3.strip().splitlines()
|
||||||
|
http_response = lines[-1] # "+UUHTTPCR: 0,4,0"
|
||||||
|
parts = http_response.split(',')
|
||||||
|
# code 0 (HTTP failed)
|
||||||
|
if len(parts) == 3 and parts[-1] == '0': # The third value indicates success
|
||||||
|
print("\033[0;31mATTENTION: HTTP operation failed\033[0m")
|
||||||
|
else:
|
||||||
|
print("\033[0;32m HTTP operation successful!!!\033[0m")
|
||||||
|
|
||||||
|
|
||||||
|
#READ REPLY
|
||||||
|
print("****")
|
||||||
|
print("\033[0;33mREPLY SERVER\033[0m")
|
||||||
|
ser_sara.write(b'AT+URDFILE="https.resp"\r')
|
||||||
|
response_SARA_7 = read_complete_response(ser_sara, wait_for_line="OK")
|
||||||
|
print("Reply from server:")
|
||||||
|
|
||||||
|
print("\033[0;32m")
|
||||||
|
print(response_SARA_7)
|
||||||
|
print("\033[0m")
|
||||||
|
|
||||||
|
#5. empty json
|
||||||
|
print("\033[0;33mEmpty Memory\033[0m")
|
||||||
|
ser_sara.write(b'AT+UDELFILE="sensordata_json.json"\r')
|
||||||
|
response_SARA_8 = read_complete_response(ser_sara, wait_for_line="OK")
|
||||||
|
print(response_SARA_8)
|
||||||
|
|
||||||
|
# Get error code
|
||||||
|
print("\033[0;33mEmpty Memory\033[0m")
|
||||||
|
command = f'AT+UHTTPER={profile_id}\r'
|
||||||
|
ser_sara.write(command.encode('utf-8'))
|
||||||
|
response_SARA_9 = read_complete_response(ser_sara, wait_for_line="OK")
|
||||||
|
print(response_SARA_9)
|
||||||
|
|
||||||
|
'''
|
||||||
|
+UHTTPER: profile_id,error_class,error_code
|
||||||
|
|
||||||
|
error_class
|
||||||
|
0 OK, no error
|
||||||
|
3 HTTP Protocol error class
|
||||||
|
10 Wrong HTTP API USAGE
|
||||||
|
|
||||||
|
error_code (for error_class 3)
|
||||||
|
0 No error
|
||||||
|
11 Server connection error
|
||||||
|
73 Secure socket connect error
|
||||||
|
'''
|
||||||
|
|
||||||
|
except serial.SerialException as e:
|
||||||
|
print(f"Error: {e}")
|
||||||
|
|
||||||
|
finally:
|
||||||
|
if ser_sara.is_open:
|
||||||
|
ser_sara.close()
|
||||||
|
print("****")
|
||||||
|
#print("Serial closed")
|
||||||
|
|
||||||
133
SARA/SSL/test_33.py
Executable file
133
SARA/SSL/test_33.py
Executable file
@@ -0,0 +1,133 @@
|
|||||||
|
'''
|
||||||
|
Script to set the URL for a HTTP request and trigger the POST Request
|
||||||
|
Ex:
|
||||||
|
/usr/bin/python3 /var/www/nebuleair_pro_4g/SARA/SSL/test_22.py ttyAMA2 api-prod.uspot.probesys.net /nebuleair?token=2AFF6dQk68daFZ
|
||||||
|
/usr/bin/python3 /var/www/nebuleair_pro_4g/SARA/SSL/test_22.py ttyAMA2 webhook.site /6bee2237-099a-4ff4-8452-9f4126df7151
|
||||||
|
/usr/bin/python3 /var/www/nebuleair_pro_4g/SARA/SSL/test_22.py ttyAMA2 aircarto.fr /tests/test.php
|
||||||
|
/usr/bin/python3 /var/www/nebuleair_pro_4g/SARA/SSL/test_22.py ttyAMA2 ssl.aircarto.fr /test.php
|
||||||
|
/usr/bin/python3 /var/www/nebuleair_pro_4g/SARA/SSL/test_22.py ttyAMA2 vps.aircarto.fr /test.php
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
First profile id:
|
||||||
|
AT+UHTTP=0,1,"data.nebuleair.fr"
|
||||||
|
Second profile id:
|
||||||
|
AT+UHTTP=1,1,"api-prod.uspot.probesys.net"
|
||||||
|
Third profile id:
|
||||||
|
AT+UHTTP=2,1,"aircarto.fr"
|
||||||
|
|
||||||
|
'''
|
||||||
|
|
||||||
|
import serial
|
||||||
|
import time
|
||||||
|
import sys
|
||||||
|
import json
|
||||||
|
|
||||||
|
parameter = sys.argv[1:] # Exclude the script name
|
||||||
|
#print("Parameters received:")
|
||||||
|
port='/dev/'+parameter[0] # ex: ttyAMA2
|
||||||
|
url = parameter[1] # ex: data.mobileair.fr
|
||||||
|
endpoint = parameter[2]
|
||||||
|
profile_id = 3
|
||||||
|
|
||||||
|
#get baudrate
|
||||||
|
def load_config(config_file):
|
||||||
|
try:
|
||||||
|
with open(config_file, 'r') as file:
|
||||||
|
config_data = json.load(file)
|
||||||
|
return config_data
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error loading config file: {e}")
|
||||||
|
return {}
|
||||||
|
|
||||||
|
# Define the config file path
|
||||||
|
config_file = '/var/www/nebuleair_pro_4g/config.json'
|
||||||
|
# Load the configuration data
|
||||||
|
config = load_config(config_file)
|
||||||
|
# Access the shared variables
|
||||||
|
baudrate = config.get('SaraR4_baudrate', 115200)
|
||||||
|
send_uSpot = config.get('send_uSpot', False)
|
||||||
|
|
||||||
|
def color_text(text, color):
|
||||||
|
colors = {
|
||||||
|
"red": "\033[31m",
|
||||||
|
"green": "\033[32m",
|
||||||
|
"yellow": "\033[33m",
|
||||||
|
"blue": "\033[34m",
|
||||||
|
"magenta": "\033[35m",
|
||||||
|
"cyan": "\033[36m",
|
||||||
|
"white": "\033[37m",
|
||||||
|
}
|
||||||
|
reset = "\033[0m"
|
||||||
|
return f"{colors.get(color, '')}{text}{reset}"
|
||||||
|
|
||||||
|
def read_complete_response(serial_connection, timeout=2, end_of_response_timeout=2, wait_for_line=None):
|
||||||
|
response = bytearray()
|
||||||
|
serial_connection.timeout = timeout
|
||||||
|
end_time = time.time() + end_of_response_timeout
|
||||||
|
start_time = time.time()
|
||||||
|
|
||||||
|
while True:
|
||||||
|
elapsed_time = time.time() - start_time # Time since function start
|
||||||
|
if serial_connection.in_waiting > 0:
|
||||||
|
data = serial_connection.read(serial_connection.in_waiting)
|
||||||
|
response.extend(data)
|
||||||
|
end_time = time.time() + end_of_response_timeout # Reset timeout on new data
|
||||||
|
|
||||||
|
# Decode and check for the specific line
|
||||||
|
if wait_for_line:
|
||||||
|
decoded_response = response.decode('utf-8', errors='replace')
|
||||||
|
if wait_for_line in decoded_response:
|
||||||
|
print(f"[DEBUG] 🔎Found target line: {wait_for_line}")
|
||||||
|
break
|
||||||
|
elif time.time() > end_time:
|
||||||
|
print(f"[DEBUG] Timeout reached. No more data received.")
|
||||||
|
break
|
||||||
|
time.sleep(0.1) # Short sleep to prevent busy waiting
|
||||||
|
# Final response and debug output
|
||||||
|
total_elapsed_time = time.time() - start_time
|
||||||
|
print(f"[DEBUG] ⏱️ elapsed time: {total_elapsed_time:.2f}s. ⏱️")
|
||||||
|
return response.decode('utf-8', errors='replace')
|
||||||
|
|
||||||
|
ser_sara = serial.Serial(
|
||||||
|
port=port, #USB0 or ttyS0
|
||||||
|
baudrate=baudrate, #115200 ou 9600
|
||||||
|
parity=serial.PARITY_NONE, #PARITY_NONE, PARITY_EVEN or PARITY_ODD
|
||||||
|
stopbits=serial.STOPBITS_ONE,
|
||||||
|
bytesize=serial.EIGHTBITS,
|
||||||
|
timeout = 2
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
try:
|
||||||
|
|
||||||
|
|
||||||
|
#check certificate (List all available certificates and private keys)
|
||||||
|
print("\033[0;33mCheck certificate\033[0m")
|
||||||
|
command = f'AT+USECMNG=3\r'
|
||||||
|
ser_sara.write((command + '\r').encode('utf-8'))
|
||||||
|
response_SARA_5b = read_complete_response(ser_sara, wait_for_line="OK")
|
||||||
|
print(response_SARA_5b)
|
||||||
|
time.sleep(0.5)
|
||||||
|
|
||||||
|
#security layer
|
||||||
|
|
||||||
|
#1
|
||||||
|
print("\033[0;33mCheck certificate\033[0m")
|
||||||
|
command = f'AT+USECPRF=<profile_id>,<op_code>\r'
|
||||||
|
ser_sara.write((command + '\r').encode('utf-8'))
|
||||||
|
response_SARA_5b = read_complete_response(ser_sara, wait_for_line="OK")
|
||||||
|
print(response_SARA_5b)
|
||||||
|
time.sleep(0.5)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
except serial.SerialException as e:
|
||||||
|
print(f"Error: {e}")
|
||||||
|
|
||||||
|
finally:
|
||||||
|
if ser_sara.is_open:
|
||||||
|
ser_sara.close()
|
||||||
|
print("****")
|
||||||
|
#print("Serial closed")
|
||||||
|
|
||||||
12
SARA/UDP/receiveUDP_downlink.py
Normal file
12
SARA/UDP/receiveUDP_downlink.py
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
'''
|
||||||
|
____ _ ____ _
|
||||||
|
/ ___| / \ | _ \ / \
|
||||||
|
\___ \ / _ \ | |_) | / _ \
|
||||||
|
___) / ___ \| _ < / ___ \
|
||||||
|
|____/_/ \_\_| \_\/_/ \_\
|
||||||
|
|
||||||
|
Script to read UDP message
|
||||||
|
|
||||||
|
/usr/bin/python3 /var/www/nebuleair_pro_4g/SARA/UDP/receiveUDP_downlink.py
|
||||||
|
|
||||||
|
'''
|
||||||
129
SARA/UDP/sendUDP_message.py
Normal file
129
SARA/UDP/sendUDP_message.py
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
'''
|
||||||
|
____ _ ____ _
|
||||||
|
/ ___| / \ | _ \ / \
|
||||||
|
\___ \ / _ \ | |_) | / _ \
|
||||||
|
___) / ___ \| _ < / ___ \
|
||||||
|
|____/_/ \_\_| \_\/_/ \_\
|
||||||
|
|
||||||
|
Script to send UDP message
|
||||||
|
|
||||||
|
/usr/bin/python3 /var/www/nebuleair_pro_4g/SARA/UDP/sendUDP_message.py
|
||||||
|
|
||||||
|
'''
|
||||||
|
import serial
|
||||||
|
import time
|
||||||
|
import sys
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
ser_sara = serial.Serial(
|
||||||
|
port='/dev/ttyAMA2',
|
||||||
|
baudrate=115200, #115200 ou 9600
|
||||||
|
parity=serial.PARITY_NONE, #PARITY_NONE, PARITY_EVEN or PARITY_ODD
|
||||||
|
stopbits=serial.STOPBITS_ONE,
|
||||||
|
bytesize=serial.EIGHTBITS,
|
||||||
|
timeout = 2
|
||||||
|
)
|
||||||
|
|
||||||
|
def read_complete_response(serial_connection, timeout=2, end_of_response_timeout=2, wait_for_lines=None, debug=True):
|
||||||
|
'''
|
||||||
|
Fonction très importante !!!
|
||||||
|
Reads the complete response from a serial connection and waits for specific lines.
|
||||||
|
'''
|
||||||
|
if wait_for_lines is None:
|
||||||
|
wait_for_lines = [] # Default to an empty list if not provided
|
||||||
|
|
||||||
|
response = bytearray()
|
||||||
|
serial_connection.timeout = timeout
|
||||||
|
end_time = time.time() + end_of_response_timeout
|
||||||
|
start_time = time.time()
|
||||||
|
|
||||||
|
while True:
|
||||||
|
elapsed_time = time.time() - start_time # Time since function start
|
||||||
|
if serial_connection.in_waiting > 0:
|
||||||
|
data = serial_connection.read(serial_connection.in_waiting)
|
||||||
|
response.extend(data)
|
||||||
|
end_time = time.time() + end_of_response_timeout # Reset timeout on new data
|
||||||
|
|
||||||
|
# Decode and check for any target line
|
||||||
|
decoded_response = response.decode('utf-8', errors='replace')
|
||||||
|
for target_line in wait_for_lines:
|
||||||
|
if target_line in decoded_response:
|
||||||
|
if debug:
|
||||||
|
print(f"[DEBUG] 🔎 Found target line: {target_line} (in {elapsed_time:.2f}s)")
|
||||||
|
return decoded_response # Return response immediately if a target line is found
|
||||||
|
elif time.time() > end_time:
|
||||||
|
if debug:
|
||||||
|
print(f"[DEBUG] Timeout reached. No more data received.")
|
||||||
|
break
|
||||||
|
time.sleep(0.1) # Short sleep to prevent busy waiting
|
||||||
|
|
||||||
|
# Final response and debug output
|
||||||
|
total_elapsed_time = time.time() - start_time
|
||||||
|
if debug:
|
||||||
|
print(f"[DEBUG] ⏱️ elapsed time: {total_elapsed_time:.2f}s. ⏱️")
|
||||||
|
# Check if the elapsed time exceeded 10 seconds
|
||||||
|
if total_elapsed_time > 10 and debug:
|
||||||
|
print(f"[ALERT] 🚨 The operation took too long 🚨")
|
||||||
|
print(f'<span style="color: red;font-weight: bold;">[ALERT] ⚠️{total_elapsed_time:.2f}s⚠️</span>')
|
||||||
|
|
||||||
|
return response.decode('utf-8', errors='replace') # Return the full response if no target line is found
|
||||||
|
|
||||||
|
try:
|
||||||
|
print('Start script')
|
||||||
|
|
||||||
|
# Increase verbosity
|
||||||
|
command = f'AT+CMEE=2\r'
|
||||||
|
ser_sara.write(command.encode('utf-8'))
|
||||||
|
response_SARA_1 = read_complete_response(ser_sara, wait_for_lines=["OK", "ERROR"])
|
||||||
|
print(response_SARA_1, end="")
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
# 1. Create SOCKET
|
||||||
|
print('➡️Create SOCKET')
|
||||||
|
command = f'AT+USOCR=17\r'
|
||||||
|
ser_sara.write(command.encode('utf-8'))
|
||||||
|
response_SARA_1 = read_complete_response(ser_sara, wait_for_lines=["OK", "ERROR"])
|
||||||
|
print(response_SARA_1, end="")
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
# 2. Retreive Socket ID
|
||||||
|
match = re.search(r'\+USOCR:\s*(\d+)', response_SARA_1)
|
||||||
|
if match:
|
||||||
|
socket_id = match.group(1)
|
||||||
|
print(f"Socket ID: {socket_id}")
|
||||||
|
else:
|
||||||
|
print("Failed to extract socket ID")
|
||||||
|
|
||||||
|
|
||||||
|
#3. Connect to UDP server
|
||||||
|
print("Connect to server:")
|
||||||
|
command = f'AT+USOCO={socket_id},"192.168.0.20",4242\r'
|
||||||
|
ser_sara.write(command.encode('utf-8'))
|
||||||
|
response_SARA_2 = read_complete_response(ser_sara, wait_for_lines=["OK", "+CME ERROR", "ERROR"], debug=False)
|
||||||
|
print(response_SARA_2)
|
||||||
|
|
||||||
|
# 4. Write data and send
|
||||||
|
print("Write data:")
|
||||||
|
command = f'AT+USOWR={socket_id},10\r'
|
||||||
|
ser_sara.write(command.encode('utf-8'))
|
||||||
|
response_SARA_2 = read_complete_response(ser_sara, wait_for_lines=["@","OK", "+CME ERROR", "ERROR"], debug=False)
|
||||||
|
print(response_SARA_2)
|
||||||
|
|
||||||
|
ser_sara.write("1234567890".encode())
|
||||||
|
response_SARA_2 = read_complete_response(ser_sara, wait_for_lines=["@","OK", "+CME ERROR", "ERROR"], debug=False)
|
||||||
|
print(response_SARA_2)
|
||||||
|
|
||||||
|
#Close socket
|
||||||
|
print("Close socket:")
|
||||||
|
command = f'AT+USOCL={socket_id}\r'
|
||||||
|
ser_sara.write(command.encode('utf-8'))
|
||||||
|
response_SARA_2 = read_complete_response(ser_sara, wait_for_lines=["OK", "+CME ERROR", "ERROR"], debug=False)
|
||||||
|
print(response_SARA_2)
|
||||||
|
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print("An error occurred:", e)
|
||||||
|
traceback.print_exc() # This prints the full traceback
|
||||||
109
SARA/cellLocate/get_loc.py
Normal file
109
SARA/cellLocate/get_loc.py
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
'''
|
||||||
|
____ _ ____ _
|
||||||
|
/ ___| / \ | _ \ / \
|
||||||
|
\___ \ / _ \ | |_) | / _ \
|
||||||
|
___) / ___ \| _ < / ___ \
|
||||||
|
|____/_/ \_\_| \_\/_/ \_\
|
||||||
|
|
||||||
|
Script to get Location from GSM
|
||||||
|
|
||||||
|
/usr/bin/python3 /var/www/nebuleair_pro_4g/SARA/cellLocate/get_loc.py ttyAMA2 1
|
||||||
|
|
||||||
|
AT+ULOC=
|
||||||
|
<mode>, ->2 -> single shot position
|
||||||
|
<sensor>, ->2 -> use cellulare CellLocate
|
||||||
|
<response_type>, ->0 -> standard
|
||||||
|
<timeout>, ->2 -> seconds
|
||||||
|
<accuracy> ->1 -> in meters
|
||||||
|
[,<num_hypothesis>]
|
||||||
|
|
||||||
|
exemple: AT+ULOC=2,2,0,2,1
|
||||||
|
'''
|
||||||
|
|
||||||
|
import serial
|
||||||
|
import time
|
||||||
|
import sys
|
||||||
|
import json
|
||||||
|
|
||||||
|
parameter = sys.argv[1:] # Exclude the script name
|
||||||
|
port='/dev/'+parameter[0] # ex: ttyAMA2
|
||||||
|
timeout = float(parameter[1]) # ex:2
|
||||||
|
|
||||||
|
|
||||||
|
#get baudrate
|
||||||
|
def load_config(config_file):
|
||||||
|
try:
|
||||||
|
with open(config_file, 'r') as file:
|
||||||
|
config_data = json.load(file)
|
||||||
|
return config_data
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error loading config file: {e}")
|
||||||
|
return {}
|
||||||
|
|
||||||
|
# Define the config file path
|
||||||
|
config_file = '/var/www/nebuleair_pro_4g/config.json'
|
||||||
|
# Load the configuration data
|
||||||
|
config = load_config(config_file)
|
||||||
|
# Access the shared variables
|
||||||
|
baudrate = config.get('SaraR4_baudrate', 115200)
|
||||||
|
|
||||||
|
ser = serial.Serial(
|
||||||
|
port=port, #USB0 or ttyS0
|
||||||
|
baudrate=baudrate, #115200 ou 9600
|
||||||
|
parity=serial.PARITY_NONE, #PARITY_NONE, PARITY_EVEN or PARITY_ODD
|
||||||
|
stopbits=serial.STOPBITS_ONE,
|
||||||
|
bytesize=serial.EIGHTBITS,
|
||||||
|
timeout = timeout
|
||||||
|
)
|
||||||
|
|
||||||
|
def read_complete_response(serial_connection, timeout=2, end_of_response_timeout=2, wait_for_lines=None, debug=True):
|
||||||
|
'''
|
||||||
|
Fonction très importante !!!
|
||||||
|
Reads the complete response from a serial connection and waits for specific lines.
|
||||||
|
'''
|
||||||
|
if wait_for_lines is None:
|
||||||
|
wait_for_lines = [] # Default to an empty list if not provided
|
||||||
|
|
||||||
|
response = bytearray()
|
||||||
|
serial_connection.timeout = timeout
|
||||||
|
end_time = time.time() + end_of_response_timeout
|
||||||
|
start_time = time.time()
|
||||||
|
|
||||||
|
while True:
|
||||||
|
elapsed_time = time.time() - start_time # Time since function start
|
||||||
|
if serial_connection.in_waiting > 0:
|
||||||
|
data = serial_connection.read(serial_connection.in_waiting)
|
||||||
|
response.extend(data)
|
||||||
|
end_time = time.time() + end_of_response_timeout # Reset timeout on new data
|
||||||
|
|
||||||
|
# Decode and check for any target line
|
||||||
|
decoded_response = response.decode('utf-8', errors='replace')
|
||||||
|
for target_line in wait_for_lines:
|
||||||
|
if target_line in decoded_response:
|
||||||
|
if debug:
|
||||||
|
print(f"[DEBUG] 🔎 Found target line: {target_line} (in {elapsed_time:.2f}s)")
|
||||||
|
return decoded_response # Return response immediately if a target line is found
|
||||||
|
elif time.time() > end_time:
|
||||||
|
if debug:
|
||||||
|
print(f"[DEBUG] Timeout reached. No more data received.")
|
||||||
|
break
|
||||||
|
time.sleep(0.1) # Short sleep to prevent busy waiting
|
||||||
|
|
||||||
|
# Final response and debug output
|
||||||
|
total_elapsed_time = time.time() - start_time
|
||||||
|
if debug:
|
||||||
|
print(f"[DEBUG] ⏱️ elapsed time: {total_elapsed_time:.2f}s. ⏱️")
|
||||||
|
# Check if the elapsed time exceeded 10 seconds
|
||||||
|
if total_elapsed_time > 10 and debug:
|
||||||
|
print(f"[ALERT] 🚨 The operation took too long 🚨")
|
||||||
|
print(f'<span style="color: red;font-weight: bold;">[ALERT] ⚠️{total_elapsed_time:.2f}s⚠️</span>')
|
||||||
|
|
||||||
|
return response.decode('utf-8', errors='replace') # Return the full response if no target line is found
|
||||||
|
|
||||||
|
|
||||||
|
#command = f'ATI\r'
|
||||||
|
command = f'AT+ULOC=2,2,0,2,1\r'
|
||||||
|
ser.write((command + '\r').encode('utf-8'))
|
||||||
|
|
||||||
|
response = read_complete_response(ser, wait_for_lines=["+UULOC"])
|
||||||
|
print(response)
|
||||||
103
SARA/cellLocate/server_conf.py
Normal file
103
SARA/cellLocate/server_conf.py
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
'''
|
||||||
|
____ _ ____ _
|
||||||
|
/ ___| / \ | _ \ / \
|
||||||
|
\___ \ / _ \ | |_) | / _ \
|
||||||
|
___) / ___ \| _ < / ___ \
|
||||||
|
|____/_/ \_\_| \_\/_/ \_\
|
||||||
|
|
||||||
|
Script to Configures the network connection to a Multi GNSS Assistance (MGA) server used also per CellLocate
|
||||||
|
|
||||||
|
/usr/bin/python3 /var/www/nebuleair_pro_4g/SARA/cellLocate/server_conf.py ttyAMA2 1
|
||||||
|
|
||||||
|
AT+UGSRV="cell-live1.services.u-blox.com","cell-live2.services.u-blox.com","XkEKfGqVSbmNE1eZfBZm4Q"
|
||||||
|
|
||||||
|
|
||||||
|
'''
|
||||||
|
|
||||||
|
import serial
|
||||||
|
import time
|
||||||
|
import sys
|
||||||
|
import json
|
||||||
|
|
||||||
|
parameter = sys.argv[1:] # Exclude the script name
|
||||||
|
port='/dev/'+parameter[0] # ex: ttyAMA2
|
||||||
|
timeout = float(parameter[1]) # ex:2
|
||||||
|
|
||||||
|
|
||||||
|
#get baudrate
|
||||||
|
def load_config(config_file):
|
||||||
|
try:
|
||||||
|
with open(config_file, 'r') as file:
|
||||||
|
config_data = json.load(file)
|
||||||
|
return config_data
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error loading config file: {e}")
|
||||||
|
return {}
|
||||||
|
|
||||||
|
# Define the config file path
|
||||||
|
config_file = '/var/www/nebuleair_pro_4g/config.json'
|
||||||
|
# Load the configuration data
|
||||||
|
config = load_config(config_file)
|
||||||
|
# Access the shared variables
|
||||||
|
baudrate = config.get('SaraR4_baudrate', 115200)
|
||||||
|
|
||||||
|
ser = serial.Serial(
|
||||||
|
port=port, #USB0 or ttyS0
|
||||||
|
baudrate=baudrate, #115200 ou 9600
|
||||||
|
parity=serial.PARITY_NONE, #PARITY_NONE, PARITY_EVEN or PARITY_ODD
|
||||||
|
stopbits=serial.STOPBITS_ONE,
|
||||||
|
bytesize=serial.EIGHTBITS,
|
||||||
|
timeout = timeout
|
||||||
|
)
|
||||||
|
|
||||||
|
def read_complete_response(serial_connection, timeout=2, end_of_response_timeout=2, wait_for_lines=None, debug=True):
|
||||||
|
'''
|
||||||
|
Fonction très importante !!!
|
||||||
|
Reads the complete response from a serial connection and waits for specific lines.
|
||||||
|
'''
|
||||||
|
if wait_for_lines is None:
|
||||||
|
wait_for_lines = [] # Default to an empty list if not provided
|
||||||
|
|
||||||
|
response = bytearray()
|
||||||
|
serial_connection.timeout = timeout
|
||||||
|
end_time = time.time() + end_of_response_timeout
|
||||||
|
start_time = time.time()
|
||||||
|
|
||||||
|
while True:
|
||||||
|
elapsed_time = time.time() - start_time # Time since function start
|
||||||
|
if serial_connection.in_waiting > 0:
|
||||||
|
data = serial_connection.read(serial_connection.in_waiting)
|
||||||
|
response.extend(data)
|
||||||
|
end_time = time.time() + end_of_response_timeout # Reset timeout on new data
|
||||||
|
|
||||||
|
# Decode and check for any target line
|
||||||
|
decoded_response = response.decode('utf-8', errors='replace')
|
||||||
|
for target_line in wait_for_lines:
|
||||||
|
if target_line in decoded_response:
|
||||||
|
if debug:
|
||||||
|
print(f"[DEBUG] 🔎 Found target line: {target_line} (in {elapsed_time:.2f}s)")
|
||||||
|
return decoded_response # Return response immediately if a target line is found
|
||||||
|
elif time.time() > end_time:
|
||||||
|
if debug:
|
||||||
|
print(f"[DEBUG] Timeout reached. No more data received.")
|
||||||
|
break
|
||||||
|
time.sleep(0.1) # Short sleep to prevent busy waiting
|
||||||
|
|
||||||
|
# Final response and debug output
|
||||||
|
total_elapsed_time = time.time() - start_time
|
||||||
|
if debug:
|
||||||
|
print(f"[DEBUG] ⏱️ elapsed time: {total_elapsed_time:.2f}s. ⏱️")
|
||||||
|
# Check if the elapsed time exceeded 10 seconds
|
||||||
|
if total_elapsed_time > 10 and debug:
|
||||||
|
print(f"[ALERT] 🚨 The operation took too long 🚨")
|
||||||
|
print(f'<span style="color: red;font-weight: bold;">[ALERT] ⚠️{total_elapsed_time:.2f}s⚠️</span>')
|
||||||
|
|
||||||
|
return response.decode('utf-8', errors='replace') # Return the full response if no target line is found
|
||||||
|
|
||||||
|
|
||||||
|
#command = f'ATI\r'
|
||||||
|
command = f'AT+UGSRV="cell-live1.services.u-blox.com","cell-live2.services.u-blox.com","XkEKfGqVSbmNE1eZfBZm4Q"\r'
|
||||||
|
ser.write((command + '\r').encode('utf-8'))
|
||||||
|
|
||||||
|
response = read_complete_response(ser, wait_for_lines=["+UULOC"])
|
||||||
|
print(response)
|
||||||
27
SARA/check_running.py
Executable file
27
SARA/check_running.py
Executable file
@@ -0,0 +1,27 @@
|
|||||||
|
'''
|
||||||
|
Check if the main loop is running
|
||||||
|
/usr/bin/python3 /var/www/nebuleair_pro_4g/tests/check_running.py
|
||||||
|
'''
|
||||||
|
import psutil
|
||||||
|
import json
|
||||||
|
|
||||||
|
def is_script_running(script_name):
|
||||||
|
"""Check if a given Python script is currently running."""
|
||||||
|
for process in psutil.process_iter(['pid', 'cmdline']):
|
||||||
|
if process.info['cmdline'] and script_name in " ".join(process.info['cmdline']):
|
||||||
|
return True # Script is running
|
||||||
|
return False # Script is not running
|
||||||
|
|
||||||
|
script_to_check = "/var/www/nebuleair_pro_4g/loop/SARA_send_data_v2.py"
|
||||||
|
|
||||||
|
# Determine script status
|
||||||
|
is_running = is_script_running(script_to_check)
|
||||||
|
|
||||||
|
# Create JSON response
|
||||||
|
response = {
|
||||||
|
"message": "The script is still running.❌❌❌" if is_running else "The script is NOT running.✅✅✅",
|
||||||
|
"running": is_running
|
||||||
|
}
|
||||||
|
|
||||||
|
# Print JSON output
|
||||||
|
print(json.dumps(response, indent=4)) # Pretty print for readability
|
||||||
72
SARA/reboot/hardware_reboot.py
Normal file
72
SARA/reboot/hardware_reboot.py
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
'''
|
||||||
|
____ _ ____ _
|
||||||
|
/ ___| / \ | _ \ / \
|
||||||
|
\___ \ / _ \ | |_) | / _ \
|
||||||
|
___) / ___ \| _ < / ___ \
|
||||||
|
|____/_/ \_\_| \_\/_/ \_\
|
||||||
|
|
||||||
|
sudo /usr/bin/python3 /var/www/nebuleair_pro_4g/SARA/reboot/hardware_reboot.py
|
||||||
|
|
||||||
|
Hardware reboot of the SARA R5 modem using GPIO 16 (GND control via transistor).
|
||||||
|
Cuts power for 3 seconds, then verifies modem is responsive with ATI command.
|
||||||
|
Returns JSON result for web interface.
|
||||||
|
'''
|
||||||
|
|
||||||
|
import RPi.GPIO as GPIO
|
||||||
|
import serial
|
||||||
|
import time
|
||||||
|
import json
|
||||||
|
import sqlite3
|
||||||
|
|
||||||
|
SARA_GND_GPIO = 16
|
||||||
|
|
||||||
|
# Load baudrate from config
|
||||||
|
conn = sqlite3.connect("/var/www/nebuleair_pro_4g/sqlite/sensors.db")
|
||||||
|
cursor = conn.cursor()
|
||||||
|
cursor.execute("SELECT value FROM config_table WHERE key='SaraR4_baudrate'")
|
||||||
|
row = cursor.fetchone()
|
||||||
|
baudrate = int(row[0]) if row else 115200
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
result = {
|
||||||
|
"reboot": False,
|
||||||
|
"modem_response": None,
|
||||||
|
"error": None
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Step 1: Cut GND (modem off)
|
||||||
|
GPIO.setmode(GPIO.BCM)
|
||||||
|
GPIO.setup(SARA_GND_GPIO, GPIO.OUT)
|
||||||
|
GPIO.output(SARA_GND_GPIO, GPIO.LOW)
|
||||||
|
time.sleep(3)
|
||||||
|
|
||||||
|
# Step 2: Restore GND (modem on)
|
||||||
|
GPIO.output(SARA_GND_GPIO, GPIO.HIGH)
|
||||||
|
time.sleep(5) # wait for modem boot
|
||||||
|
|
||||||
|
# Step 3: Check modem with ATI
|
||||||
|
ser = serial.Serial('/dev/ttyAMA2', baudrate=baudrate, timeout=3)
|
||||||
|
ser.reset_input_buffer()
|
||||||
|
|
||||||
|
for attempt in range(5):
|
||||||
|
ser.write(b'ATI\r')
|
||||||
|
time.sleep(1)
|
||||||
|
response = ser.read(ser.in_waiting or 1).decode('utf-8', errors='replace')
|
||||||
|
if "OK" in response:
|
||||||
|
result["reboot"] = True
|
||||||
|
result["modem_response"] = response.strip()
|
||||||
|
break
|
||||||
|
time.sleep(2)
|
||||||
|
else:
|
||||||
|
result["error"] = "Modem ne repond pas apres le redemarrage"
|
||||||
|
|
||||||
|
ser.close()
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
result["error"] = str(e)
|
||||||
|
|
||||||
|
finally:
|
||||||
|
GPIO.cleanup(SARA_GND_GPIO)
|
||||||
|
|
||||||
|
print(json.dumps(result))
|
||||||
168
SARA/reboot/start.py
Normal file
168
SARA/reboot/start.py
Normal file
@@ -0,0 +1,168 @@
|
|||||||
|
r'''
|
||||||
|
____ _ ____ _
|
||||||
|
/ ___| / \ | _ \ / \
|
||||||
|
\___ \ / _ \ | |_) | / _ \
|
||||||
|
___) / ___ \| _ < / ___ \
|
||||||
|
|____/_/ \_\_| \_\/_/ \_\
|
||||||
|
|
||||||
|
Script that starts at the boot of the RPI (with cron)
|
||||||
|
|
||||||
|
@reboot sleep 30 && /usr/bin/python3 /var/www/nebuleair_pro_4g/SARA/reboot/start.py >> /var/www/nebuleair_pro_4g/logs/app.log 2>&1
|
||||||
|
|
||||||
|
/usr/bin/python3 /var/www/nebuleair_pro_4g/SARA/reboot/start.py
|
||||||
|
|
||||||
|
Roles:
|
||||||
|
1. Reset modem_config_mode to 0 (boot safety)
|
||||||
|
2. Power on SARA modem via GPIO 16
|
||||||
|
3. Detect modem model (SARA R4 or R5) and save to SQLite
|
||||||
|
|
||||||
|
All other configuration (AirCarto URL, uSpot HTTPS, PDP setup, geolocation)
|
||||||
|
is handled by the main loop script: loop/SARA_send_data_v2.py
|
||||||
|
'''
|
||||||
|
import serial
|
||||||
|
import RPi.GPIO as GPIO
|
||||||
|
import time
|
||||||
|
import re
|
||||||
|
import sqlite3
|
||||||
|
import traceback
|
||||||
|
|
||||||
|
|
||||||
|
#GPIO
|
||||||
|
SARA_power_GPIO = 16
|
||||||
|
|
||||||
|
GPIO.setmode(GPIO.BCM) # Use BCM numbering
|
||||||
|
GPIO.setup(SARA_power_GPIO, GPIO.OUT)
|
||||||
|
|
||||||
|
# database connection
|
||||||
|
conn = sqlite3.connect("/var/www/nebuleair_pro_4g/sqlite/sensors.db")
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
def update_sqlite_config(key, value):
|
||||||
|
"""
|
||||||
|
Updates a specific key in the SQLite config_table with a new value.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
cursor.execute("SELECT type FROM config_table WHERE key = ?", (key,))
|
||||||
|
result = cursor.fetchone()
|
||||||
|
|
||||||
|
if result is None:
|
||||||
|
print(f"Key '{key}' not found in the config_table.")
|
||||||
|
return
|
||||||
|
|
||||||
|
value_type = result[0]
|
||||||
|
|
||||||
|
if value_type == 'bool':
|
||||||
|
if isinstance(value, bool):
|
||||||
|
str_value = '1' if value else '0'
|
||||||
|
else:
|
||||||
|
str_value = '1' if str(value).lower() in ('true', '1', 'yes', 'y') else '0'
|
||||||
|
elif value_type == 'int':
|
||||||
|
str_value = str(int(value))
|
||||||
|
elif value_type == 'float':
|
||||||
|
str_value = str(float(value))
|
||||||
|
else:
|
||||||
|
str_value = str(value)
|
||||||
|
|
||||||
|
cursor.execute("UPDATE config_table SET value = ? WHERE key = ?", (str_value, key))
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
print(f"Updated '{key}' to '{value}' in database.")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error updating the SQLite database: {e}")
|
||||||
|
|
||||||
|
# Load baudrate from config
|
||||||
|
cursor.execute("SELECT value FROM config_table WHERE key = 'SaraR4_baudrate'")
|
||||||
|
row = cursor.fetchone()
|
||||||
|
baudrate = int(row[0]) if row else 115200
|
||||||
|
|
||||||
|
ser_sara = serial.Serial(
|
||||||
|
port='/dev/ttyAMA2',
|
||||||
|
baudrate=baudrate,
|
||||||
|
parity=serial.PARITY_NONE,
|
||||||
|
stopbits=serial.STOPBITS_ONE,
|
||||||
|
bytesize=serial.EIGHTBITS,
|
||||||
|
timeout = 2
|
||||||
|
)
|
||||||
|
|
||||||
|
def read_complete_response(serial_connection, timeout=2, end_of_response_timeout=2, wait_for_lines=None, debug=True):
|
||||||
|
'''
|
||||||
|
Reads the complete response from a serial connection and waits for specific lines.
|
||||||
|
'''
|
||||||
|
if wait_for_lines is None:
|
||||||
|
wait_for_lines = []
|
||||||
|
|
||||||
|
response = bytearray()
|
||||||
|
serial_connection.timeout = timeout
|
||||||
|
end_time = time.time() + end_of_response_timeout
|
||||||
|
start_time = time.time()
|
||||||
|
|
||||||
|
while True:
|
||||||
|
elapsed_time = time.time() - start_time
|
||||||
|
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
|
||||||
|
|
||||||
|
decoded_response = response.decode('utf-8', errors='replace')
|
||||||
|
for target_line in wait_for_lines:
|
||||||
|
if target_line in decoded_response:
|
||||||
|
if debug:
|
||||||
|
print(f"[DEBUG] Found target line: {target_line} (in {elapsed_time:.2f}s)")
|
||||||
|
return decoded_response
|
||||||
|
elif time.time() > end_time:
|
||||||
|
if debug:
|
||||||
|
print(f"[DEBUG] Timeout reached. No more data received.")
|
||||||
|
break
|
||||||
|
time.sleep(0.1)
|
||||||
|
|
||||||
|
total_elapsed_time = time.time() - start_time
|
||||||
|
if debug:
|
||||||
|
print(f"[DEBUG] elapsed time: {total_elapsed_time:.2f}s.")
|
||||||
|
if total_elapsed_time > 10 and debug:
|
||||||
|
print(f"[ALERT] The operation took too long ({total_elapsed_time:.2f}s)")
|
||||||
|
|
||||||
|
return response.decode('utf-8', errors='replace')
|
||||||
|
|
||||||
|
try:
|
||||||
|
print('<h3>Start reboot python script</h3>')
|
||||||
|
|
||||||
|
# 1. Reset modem_config_mode at boot to prevent capteur from staying stuck in config mode
|
||||||
|
cursor.execute("UPDATE config_table SET value = '0' WHERE key = 'modem_config_mode'")
|
||||||
|
conn.commit()
|
||||||
|
print("modem_config_mode reset to 0 (boot safety)")
|
||||||
|
|
||||||
|
# 2. Power on the module (MOSFET via GPIO 16)
|
||||||
|
GPIO.output(SARA_power_GPIO, GPIO.HIGH)
|
||||||
|
time.sleep(5)
|
||||||
|
|
||||||
|
# 3. Detect modem model
|
||||||
|
# SARA R4 response: Manufacturer: u-blox Model: SARA-R410M-02B
|
||||||
|
# SARA R5 response: SARA-R500S-01B-00
|
||||||
|
print("Check SARA Status")
|
||||||
|
command = f'ATI\r'
|
||||||
|
ser_sara.write(command.encode('utf-8'))
|
||||||
|
response_SARA_ATI = read_complete_response(ser_sara, wait_for_lines=["IMEI"])
|
||||||
|
print(response_SARA_ATI)
|
||||||
|
|
||||||
|
model = "Unknown"
|
||||||
|
if "SARA-R410M" in response_SARA_ATI:
|
||||||
|
model = "SARA-R410M"
|
||||||
|
print("Detected SARA R4 modem")
|
||||||
|
elif "SARA-R500" in response_SARA_ATI:
|
||||||
|
model = "SARA-R500"
|
||||||
|
print("Detected SARA R5 modem")
|
||||||
|
else:
|
||||||
|
match = re.search(r"Model:\s*([A-Za-z0-9\-]+)", response_SARA_ATI)
|
||||||
|
if match:
|
||||||
|
model = match.group(1).strip()
|
||||||
|
else:
|
||||||
|
print("Could not identify modem model")
|
||||||
|
|
||||||
|
print(f"Model: {model}")
|
||||||
|
update_sqlite_config("modem_version", model)
|
||||||
|
|
||||||
|
print('<h3>Boot script complete. Modem ready for main loop.</h3>')
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print("An error occurred:", e)
|
||||||
|
traceback.print_exc()
|
||||||
95
SARA/sara.py
95
SARA/sara.py
@@ -1,11 +1,23 @@
|
|||||||
'''
|
r'''
|
||||||
|
____ _ ____ _
|
||||||
|
/ ___| / \ | _ \ / \
|
||||||
|
\___ \ / _ \ | |_) | / _ \
|
||||||
|
___) / ___ \| _ < / ___ \
|
||||||
|
|____/_/ \_\_| \_\/_/ \_\
|
||||||
|
|
||||||
Script to see if the SARA-R410 is running
|
Script to see if the SARA-R410 is running
|
||||||
ex:
|
ex:
|
||||||
|
python3 /var/www/nebuleair_pro_4g/SARA/sara.py ttyAMA2 ATI 2
|
||||||
|
ex 1 (get SIM infos)
|
||||||
python3 /var/www/nebuleair_pro_4g/SARA/sara.py ttyAMA2 AT+CCID? 2
|
python3 /var/www/nebuleair_pro_4g/SARA/sara.py ttyAMA2 AT+CCID? 2
|
||||||
ex 2 (turn on blue light):
|
ex 2 (turn on blue light):
|
||||||
python3 /var/www/nebuleair_pro_4g/SARA/sara.py ttyAMA2 AT+UGPIOC=16,2 2
|
python3 /var/www/nebuleair_pro_4g/SARA/sara.py ttyAMA2 AT+UGPIOC=16,2 2
|
||||||
ex 3 (reconnect network)
|
ex 3 (reconnect network)
|
||||||
python3 /var/www/nebuleair_pro_4g/SARA/sara.py ttyAMA2 AT+COPS=1,2,20801 20
|
python3 /var/www/nebuleair_pro_4g/SARA/sara.py ttyAMA2 AT+COPS=1,2,20801 20
|
||||||
|
ex 4 (get HTTP Profiles)
|
||||||
|
python3 /var/www/nebuleair_pro_4g/SARA/sara.py ttyAMA2 AT+UHTTP? 2
|
||||||
|
ex 5 (get IP addr)
|
||||||
|
python3 /var/www/nebuleair_pro_4g/SARA/sara.py ttyAMA2 AT+CGPADDR=1 2
|
||||||
|
|
||||||
'''
|
'''
|
||||||
|
|
||||||
@@ -20,68 +32,67 @@ port='/dev/'+parameter[0] # ex: ttyAMA2
|
|||||||
command = parameter[1] # ex: AT+CCID?
|
command = parameter[1] # ex: AT+CCID?
|
||||||
timeout = float(parameter[2]) # ex:2
|
timeout = float(parameter[2]) # ex:2
|
||||||
|
|
||||||
#get baudrate
|
|
||||||
def load_config(config_file):
|
|
||||||
try:
|
|
||||||
with open(config_file, 'r') as file:
|
|
||||||
config_data = json.load(file)
|
|
||||||
return config_data
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Error loading config file: {e}")
|
|
||||||
return {}
|
|
||||||
|
|
||||||
# Define the config file path
|
|
||||||
config_file = '/var/www/nebuleair_pro_4g/config.json'
|
|
||||||
# Load the configuration data
|
|
||||||
config = load_config(config_file)
|
|
||||||
# Access the shared variables
|
# Access the shared variables
|
||||||
baudrate = config.get('SaraR4_baudrate', 115200)
|
baudrate = 115200
|
||||||
|
|
||||||
ser = serial.Serial(
|
try:
|
||||||
|
|
||||||
|
ser = serial.Serial(
|
||||||
port=port, #USB0 or ttyS0
|
port=port, #USB0 or ttyS0
|
||||||
baudrate=baudrate, #115200 ou 9600
|
baudrate=baudrate, #115200 ou 9600
|
||||||
parity=serial.PARITY_NONE, #PARITY_NONE, PARITY_EVEN or PARITY_ODD
|
parity=serial.PARITY_NONE, #PARITY_NONE, PARITY_EVEN or PARITY_ODD
|
||||||
stopbits=serial.STOPBITS_ONE,
|
stopbits=serial.STOPBITS_ONE,
|
||||||
bytesize=serial.EIGHTBITS,
|
bytesize=serial.EIGHTBITS,
|
||||||
timeout = timeout
|
timeout = timeout
|
||||||
)
|
)
|
||||||
|
|
||||||
ser.write((command + '\r').encode('utf-8'))
|
# Flush any leftover data from previous commands or modem boot URCs
|
||||||
|
ser.reset_input_buffer()
|
||||||
|
|
||||||
#ser.write(b'ATI\r') #General Information
|
ser.write((command + '\r').encode('utf-8'))
|
||||||
#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'ATI\r') #General Information
|
||||||
#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+CCID?\r') #SIM card number
|
||||||
#ser.write(b'AT+UGPIOR=?\r') #Reads the current value of the specified GPIO pin
|
#ser.write(b'AT+CPIN?\r') #Check the status of the SIM card
|
||||||
#ser.write(b'AT+UGPIOC?\r') #GPIO select configuration
|
#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+COPS=?\r') #Check the network and cellular technology the modem is currently using
|
#ser.write(b'AT+UGPIOR=?\r') #Reads the current value of the specified GPIO pin
|
||||||
#ser.write(b'AT+COPS=1,2,20801') #connext to orange
|
#ser.write(b'AT+UGPIOC?\r') #GPIO select configuration
|
||||||
#ser.write(b'AT+CFUN=?\r') #Selects/read the level of functionality
|
#ser.write(b'AT+COPS=?\r') #Check the network and cellular technology the modem is currently using
|
||||||
#ser.write(b'AT+URAT=?\r') #Radio Access Technology
|
#ser.write(b'AT+COPS=1,2,20801') #connext to orange
|
||||||
#ser.write(b'AT+USIMSTAT?')
|
#ser.write(b'AT+CFUN=?\r') #Selects/read the level of functionality
|
||||||
#ser.write(b'AT+IPR=115200') #Check/Define baud rate
|
#ser.write(b'AT+URAT=?\r') #Radio Access Technology
|
||||||
#ser.write(b'AT+CMUX=?')
|
#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
|
# Read lines until a timeout occurs
|
||||||
response_lines = []
|
response_lines = []
|
||||||
while True:
|
start_time = time.time()
|
||||||
line = ser.readline().decode('utf-8').strip()
|
|
||||||
if not line:
|
while (time.time() - start_time) < timeout:
|
||||||
break # Break the loop if an empty line is encountered
|
line = ser.readline().decode('utf-8', errors='ignore').strip()
|
||||||
|
if line:
|
||||||
response_lines.append(line)
|
response_lines.append(line)
|
||||||
|
|
||||||
|
# Check if we received any data
|
||||||
|
if not response_lines:
|
||||||
|
print(f"ERROR: No response received from {port} after sending command: {command}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
# Print the response
|
# Print the response
|
||||||
for line in response_lines:
|
for line in response_lines:
|
||||||
print(line)
|
print(line)
|
||||||
|
|
||||||
except serial.SerialException as e:
|
except serial.SerialException as e:
|
||||||
print(f"Error: {e}")
|
print(f"ERROR: Serial communication error: {e}")
|
||||||
|
sys.exit(1)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"ERROR: Unexpected error: {e}")
|
||||||
|
sys.exit(1)
|
||||||
finally:
|
finally:
|
||||||
if ser.is_open:
|
# Close the serial port if it's open
|
||||||
|
if 'ser' in locals() and ser.is_open:
|
||||||
ser.close()
|
ser.close()
|
||||||
#print("Serial closed")
|
|
||||||
|
|
||||||
|
|||||||
63
SARA/sara_checkDNS.py
Normal file
63
SARA/sara_checkDNS.py
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
r'''
|
||||||
|
____ _ ____ _
|
||||||
|
/ ___| / \ | _ \ / \
|
||||||
|
\___ \ / _ \ | |_) | / _ \
|
||||||
|
___) / ___ \| _ < / ___ \
|
||||||
|
|____/_/ \_\_| \_\/_/ \_\
|
||||||
|
|
||||||
|
Script to resolve DNS (get IP from domain name) with AT+UDNSRN command
|
||||||
|
Ex:
|
||||||
|
/usr/bin/python3 /var/www/nebuleair_pro_4g/SARA/sara_checkDNS.py ttyAMA2 data.nebuleair.fr
|
||||||
|
To do: need to add profile id as parameter
|
||||||
|
|
||||||
|
'''
|
||||||
|
|
||||||
|
import serial
|
||||||
|
import time
|
||||||
|
import sys
|
||||||
|
import json
|
||||||
|
|
||||||
|
parameter = sys.argv[1:] # Exclude the script name
|
||||||
|
#print("Parameters received:")
|
||||||
|
port='/dev/'+parameter[0] # ex: ttyAMA2
|
||||||
|
url = parameter[1] # ex: data.mobileair.fr
|
||||||
|
|
||||||
|
baudrate = 115200
|
||||||
|
|
||||||
|
ser = serial.Serial(
|
||||||
|
port=port, #USB0 or ttyS0
|
||||||
|
baudrate=baudrate, #115200 ou 9600
|
||||||
|
parity=serial.PARITY_NONE, #PARITY_NONE, PARITY_EVEN or PARITY_ODD
|
||||||
|
stopbits=serial.STOPBITS_ONE,
|
||||||
|
bytesize=serial.EIGHTBITS,
|
||||||
|
timeout = 2
|
||||||
|
)
|
||||||
|
|
||||||
|
command = f'AT+UDNSRN=0,"{url}"\r'
|
||||||
|
ser.write((command + '\r').encode('utf-8'))
|
||||||
|
|
||||||
|
print("****")
|
||||||
|
print("DNS check")
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Read lines until a timeout occurs
|
||||||
|
response_lines = []
|
||||||
|
while True:
|
||||||
|
line = ser.readline().decode('utf-8').strip()
|
||||||
|
if not line:
|
||||||
|
break # Break the loop if an empty line is encountered
|
||||||
|
response_lines.append(line)
|
||||||
|
|
||||||
|
# Print the response
|
||||||
|
for line in response_lines:
|
||||||
|
print(line)
|
||||||
|
|
||||||
|
except serial.SerialException as e:
|
||||||
|
print(f"Error: {e}")
|
||||||
|
|
||||||
|
finally:
|
||||||
|
if ser.is_open:
|
||||||
|
ser.close()
|
||||||
|
print("****")
|
||||||
|
#print("Serial closed")
|
||||||
|
|
||||||
@@ -1,11 +1,18 @@
|
|||||||
'''
|
r'''
|
||||||
|
____ _ ____ _
|
||||||
|
/ ___| / \ | _ \ / \
|
||||||
|
\___ \ / _ \ | |_) | / _ \
|
||||||
|
___) / ___ \| _ < / ___ \
|
||||||
|
|____/_/ \_\_| \_\/_/ \_\
|
||||||
|
|
||||||
Script to connect SARA-R410 to network SARA-R410
|
Script to connect SARA-R410 to network SARA-R410
|
||||||
python3 /var/www/nebuleair_pro_4g/SARA/sara_connectNetwork.py ttyAMA2 20801 10
|
python3 /var/www/nebuleair_pro_4g/SARA/sara_connectNetwork.py ttyAMA2 20801 10
|
||||||
|
|
||||||
AT+COPS=1,2,20801
|
AT+COPS=1,2,20801
|
||||||
mode->1 pour manual
|
mode->1 pour manual
|
||||||
format->2 pour numeric
|
format->2 pour numeric
|
||||||
operator->20801 pour orange
|
operator->20801 pour orange, 20810 pour SFR
|
||||||
|
|
||||||
'''
|
'''
|
||||||
|
|
||||||
import serial
|
import serial
|
||||||
@@ -19,22 +26,54 @@ networkID = parameter[1] # ex: 20801
|
|||||||
timeout = float(parameter[2]) # ex:2
|
timeout = float(parameter[2]) # ex:2
|
||||||
|
|
||||||
|
|
||||||
#get baudrate
|
|
||||||
def load_config(config_file):
|
|
||||||
try:
|
|
||||||
with open(config_file, 'r') as file:
|
|
||||||
config_data = json.load(file)
|
|
||||||
return config_data
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Error loading config file: {e}")
|
|
||||||
return {}
|
|
||||||
|
|
||||||
# Define the config file path
|
|
||||||
config_file = '/var/www/nebuleair_pro_4g/config.json'
|
def read_complete_response(serial_connection, timeout=2, end_of_response_timeout=2, wait_for_lines=None, debug=True):
|
||||||
# Load the configuration data
|
'''
|
||||||
config = load_config(config_file)
|
Fonction très importante !!!
|
||||||
# Access the shared variables
|
Reads the complete response from a serial connection and waits for specific lines.
|
||||||
baudrate = config.get('SaraR4_baudrate', 115200)
|
'''
|
||||||
|
if wait_for_lines is None:
|
||||||
|
wait_for_lines = [] # Default to an empty list if not provided
|
||||||
|
|
||||||
|
response = bytearray()
|
||||||
|
serial_connection.timeout = timeout
|
||||||
|
end_time = time.time() + end_of_response_timeout
|
||||||
|
start_time = time.time()
|
||||||
|
|
||||||
|
while True:
|
||||||
|
elapsed_time = time.time() - start_time # Time since function start
|
||||||
|
if serial_connection.in_waiting > 0:
|
||||||
|
data = serial_connection.read(serial_connection.in_waiting)
|
||||||
|
response.extend(data)
|
||||||
|
end_time = time.time() + end_of_response_timeout # Reset timeout on new data
|
||||||
|
|
||||||
|
# Decode and check for any target line
|
||||||
|
decoded_response = response.decode('utf-8', errors='replace')
|
||||||
|
for target_line in wait_for_lines:
|
||||||
|
if target_line in decoded_response:
|
||||||
|
if debug:
|
||||||
|
print(f"[DEBUG] 🔎 Found target line: {target_line} (in {elapsed_time:.2f}s)")
|
||||||
|
return decoded_response # Return response immediately if a target line is found
|
||||||
|
elif time.time() > end_time:
|
||||||
|
if debug:
|
||||||
|
print(f"[DEBUG] Timeout reached. No more data received.")
|
||||||
|
break
|
||||||
|
time.sleep(0.1) # Short sleep to prevent busy waiting
|
||||||
|
|
||||||
|
# Final response and debug output
|
||||||
|
total_elapsed_time = time.time() - start_time
|
||||||
|
if debug:
|
||||||
|
print(f"[DEBUG] ⏱️ elapsed time: {total_elapsed_time:.2f}s. ⏱️")
|
||||||
|
# Check if the elapsed time exceeded 10 seconds
|
||||||
|
if total_elapsed_time > 10 and debug:
|
||||||
|
print(f"[ALERT] 🚨 The operation took too long 🚨")
|
||||||
|
print(f'<span style="color: red;font-weight: bold;">[ALERT] ⚠️{total_elapsed_time:.2f}s⚠️</span>')
|
||||||
|
|
||||||
|
return response.decode('utf-8', errors='replace') # Return the full response if no target line is found
|
||||||
|
|
||||||
|
|
||||||
|
baudrate = 115200
|
||||||
|
|
||||||
ser = serial.Serial(
|
ser = serial.Serial(
|
||||||
port=port, #USB0 or ttyS0
|
port=port, #USB0 or ttyS0
|
||||||
@@ -50,17 +89,11 @@ ser.write((command + '\r').encode('utf-8'))
|
|||||||
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Read lines until a timeout occurs
|
response = read_complete_response(ser, wait_for_lines=["OK", "ERROR"],timeout=5, end_of_response_timeout=120, debug=True)
|
||||||
response_lines = []
|
|
||||||
while True:
|
|
||||||
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
|
print('<p class="text-danger-emphasis">')
|
||||||
for line in response_lines:
|
print(response)
|
||||||
print(line)
|
print("</p>", end="")
|
||||||
|
|
||||||
except serial.SerialException as e:
|
except serial.SerialException as e:
|
||||||
print(f"Error: {e}")
|
print(f"Error: {e}")
|
||||||
|
|||||||
@@ -11,22 +11,7 @@ parameter = sys.argv[1:] # Exclude the script name
|
|||||||
port='/dev/'+parameter[0] # ex: ttyAMA2
|
port='/dev/'+parameter[0] # ex: ttyAMA2
|
||||||
message = parameter[1] # ex: Hello
|
message = parameter[1] # ex: Hello
|
||||||
|
|
||||||
#get baudrate
|
baudrate = 115200
|
||||||
def load_config(config_file):
|
|
||||||
try:
|
|
||||||
with open(config_file, 'r') as file:
|
|
||||||
config_data = json.load(file)
|
|
||||||
return config_data
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Error loading config file: {e}")
|
|
||||||
return {}
|
|
||||||
|
|
||||||
# Define the config file path
|
|
||||||
config_file = '/var/www/nebuleair_pro_4g/config.json'
|
|
||||||
# Load the configuration data
|
|
||||||
config = load_config(config_file)
|
|
||||||
# Access the shared variables
|
|
||||||
baudrate = config.get('SaraR4_baudrate', 115200)
|
|
||||||
|
|
||||||
ser = serial.Serial(
|
ser = serial.Serial(
|
||||||
port=port, #USB0 or ttyS0
|
port=port, #USB0 or ttyS0
|
||||||
|
|||||||
58
SARA/sara_google_ping.py
Normal file
58
SARA/sara_google_ping.py
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
r'''
|
||||||
|
____ _ ____ _
|
||||||
|
/ ___| / \ | _ \ / \
|
||||||
|
\___ \ / _ \ | |_) | / _ \
|
||||||
|
___) / ___ \| _ < / ___ \
|
||||||
|
|____/_/ \_\_| \_\/_/ \_\
|
||||||
|
|
||||||
|
Script to set the URL for a HTTP request
|
||||||
|
Ex:
|
||||||
|
/usr/bin/python3 /var/www/nebuleair_pro_4g/SARA/sara_google_ping.py
|
||||||
|
|
||||||
|
|
||||||
|
'''
|
||||||
|
|
||||||
|
import serial
|
||||||
|
import time
|
||||||
|
import sys
|
||||||
|
import json
|
||||||
|
|
||||||
|
|
||||||
|
baudrate = 115200
|
||||||
|
|
||||||
|
ser = serial.Serial(
|
||||||
|
port='/dev/ttyAMA2',
|
||||||
|
baudrate=baudrate, #115200 ou 9600
|
||||||
|
parity=serial.PARITY_NONE, #PARITY_NONE, PARITY_EVEN or PARITY_ODD
|
||||||
|
stopbits=serial.STOPBITS_ONE,
|
||||||
|
bytesize=serial.EIGHTBITS,
|
||||||
|
timeout = 2
|
||||||
|
)
|
||||||
|
url="www.google.com"
|
||||||
|
command = f'AT+UPING="{url}"\r'
|
||||||
|
ser.write((command + '\r').encode('utf-8'))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Read lines until a timeout occurs
|
||||||
|
response_lines = []
|
||||||
|
while True:
|
||||||
|
line = ser.readline().decode('utf-8').strip()
|
||||||
|
if not line:
|
||||||
|
break # Break the loop if an empty line is encountered
|
||||||
|
response_lines.append(line)
|
||||||
|
|
||||||
|
# Print the response
|
||||||
|
for line in response_lines:
|
||||||
|
print(line)
|
||||||
|
|
||||||
|
except serial.SerialException as e:
|
||||||
|
print(f"Error: {e}")
|
||||||
|
|
||||||
|
finally:
|
||||||
|
if ser.is_open:
|
||||||
|
ser.close()
|
||||||
|
print("****")
|
||||||
|
#print("Serial closed")
|
||||||
|
|
||||||
165
SARA/sara_ping.py
Normal file
165
SARA/sara_ping.py
Normal file
@@ -0,0 +1,165 @@
|
|||||||
|
r'''
|
||||||
|
____ _ ____ _
|
||||||
|
/ ___| / \ | _ \ / \
|
||||||
|
\___ \ / _ \ | |_) | / _ \
|
||||||
|
___) / ___ \| _ < / ___ \
|
||||||
|
|____/_/ \_\_| \_\/_/ \_\
|
||||||
|
|
||||||
|
Script to do a ping request to data.nebuleair.fr/ping.php
|
||||||
|
python3 /var/www/nebuleair_pro_4g/SARA/sara_ping.py
|
||||||
|
'''
|
||||||
|
|
||||||
|
import serial
|
||||||
|
import time
|
||||||
|
import sys
|
||||||
|
import json
|
||||||
|
|
||||||
|
# SARA R4 UHTTPC profile IDs
|
||||||
|
aircarto_profile_id = 0
|
||||||
|
|
||||||
|
baudrate = 115200
|
||||||
|
|
||||||
|
ser_sara = serial.Serial(
|
||||||
|
port='/dev/ttyAMA2',
|
||||||
|
baudrate=baudrate, #115200 ou 9600
|
||||||
|
parity=serial.PARITY_NONE, #PARITY_NONE, PARITY_EVEN or PARITY_ODD
|
||||||
|
stopbits=serial.STOPBITS_ONE,
|
||||||
|
bytesize=serial.EIGHTBITS,
|
||||||
|
timeout = 2
|
||||||
|
)
|
||||||
|
|
||||||
|
def read_complete_response(serial_connection, timeout=2, end_of_response_timeout=2, wait_for_lines=None, debug=True):
|
||||||
|
'''
|
||||||
|
Fonction très importante !!!
|
||||||
|
Reads the complete response from a serial connection and waits for specific lines.
|
||||||
|
'''
|
||||||
|
if wait_for_lines is None:
|
||||||
|
wait_for_lines = [] # Default to an empty list if not provided
|
||||||
|
|
||||||
|
response = bytearray()
|
||||||
|
serial_connection.timeout = timeout
|
||||||
|
end_time = time.time() + end_of_response_timeout
|
||||||
|
start_time = time.time()
|
||||||
|
|
||||||
|
while True:
|
||||||
|
elapsed_time = time.time() - start_time # Time since function start
|
||||||
|
if serial_connection.in_waiting > 0:
|
||||||
|
data = serial_connection.read(serial_connection.in_waiting)
|
||||||
|
response.extend(data)
|
||||||
|
end_time = time.time() + end_of_response_timeout # Reset timeout on new data
|
||||||
|
|
||||||
|
# Decode and check for any target line
|
||||||
|
decoded_response = response.decode('utf-8', errors='replace')
|
||||||
|
for target_line in wait_for_lines:
|
||||||
|
if target_line in decoded_response:
|
||||||
|
if debug:
|
||||||
|
print(f"[DEBUG] 🔎 Found target line: {target_line} (in {elapsed_time:.2f}s)")
|
||||||
|
return decoded_response # Return response immediately if a target line is found
|
||||||
|
elif time.time() > end_time:
|
||||||
|
if debug:
|
||||||
|
print(f"[DEBUG] Timeout reached. No more data received.")
|
||||||
|
break
|
||||||
|
time.sleep(0.1) # Short sleep to prevent busy waiting
|
||||||
|
|
||||||
|
# Final response and debug output
|
||||||
|
total_elapsed_time = time.time() - start_time
|
||||||
|
if debug:
|
||||||
|
print(f"[DEBUG] ⏱️ elapsed time: {total_elapsed_time:.2f}s. ⏱️")
|
||||||
|
# Check if the elapsed time exceeded 10 seconds
|
||||||
|
if total_elapsed_time > 10 and debug:
|
||||||
|
print(f"[ALERT] 🚨 The operation took too long 🚨")
|
||||||
|
print(f'<span style="color: red;font-weight: bold;">[ALERT] ⚠️{total_elapsed_time:.2f}s⚠️</span>')
|
||||||
|
|
||||||
|
return response.decode('utf-8', errors='replace') # Return the full response if no target line is found
|
||||||
|
|
||||||
|
|
||||||
|
def extract_error_code(response):
|
||||||
|
"""
|
||||||
|
Extract just the error code from AT+UHTTPER response
|
||||||
|
"""
|
||||||
|
for line in response.split('\n'):
|
||||||
|
if '+UHTTPER' in line:
|
||||||
|
try:
|
||||||
|
# Split the line and get the third value (error code)
|
||||||
|
parts = line.split(':')[1].strip().split(',')
|
||||||
|
if len(parts) >= 3:
|
||||||
|
error_code = int(parts[2])
|
||||||
|
return error_code
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Return None if we couldn't find the error code
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
#3. Send to endpoint (with device ID)
|
||||||
|
print("Send data (GET REQUEST):")
|
||||||
|
command= f'AT+UHTTPC={aircarto_profile_id},1,"/ping.php","aircarto_server_response.txt"\r'
|
||||||
|
ser_sara.write(command.encode('utf-8'))
|
||||||
|
|
||||||
|
response_SARA_3 = read_complete_response(ser_sara, timeout=5, end_of_response_timeout=20, wait_for_lines=["+UUHTTPCR", "+CME ERROR"], debug=True)
|
||||||
|
|
||||||
|
print(response_SARA_3)
|
||||||
|
# si on recoit la réponse UHTTPCR
|
||||||
|
if "+UUHTTPCR" in response_SARA_3:
|
||||||
|
print("✅ Received +UUHTTPCR response.")
|
||||||
|
# Split response into lines
|
||||||
|
lines = response_SARA_3.strip().splitlines()
|
||||||
|
# 1.Vérifier si la réponse contient un message d'erreur CME
|
||||||
|
if "+CME ERROR" in lines[-1]:
|
||||||
|
print("error ⛔")
|
||||||
|
else:
|
||||||
|
http_response = lines[-1] # "+UUHTTPCR: 0,4,0"
|
||||||
|
parts = http_response.split(',')
|
||||||
|
# 2.1 code 0 (HTTP failed) ⛔⛔⛔
|
||||||
|
if len(parts) == 3 and parts[-1] == '0': # The third value indicates success
|
||||||
|
print("⛔⛔ATTENTION: HTTP operation failed")
|
||||||
|
#get error code
|
||||||
|
print("Getting error code (11->Server connection error, 73->Secure socket connect error)")
|
||||||
|
command = f'AT+UHTTPER={aircarto_profile_id}\r'
|
||||||
|
ser_sara.write(command.encode('utf-8'))
|
||||||
|
response_SARA_9 = read_complete_response(ser_sara, wait_for_lines=["OK"], debug=False)
|
||||||
|
print('<p class="text-danger-emphasis">')
|
||||||
|
print(response_SARA_9)
|
||||||
|
print("</p>", end="")
|
||||||
|
# Extract just the error code
|
||||||
|
error_code = extract_error_code(response_SARA_9)
|
||||||
|
if error_code is not None:
|
||||||
|
# Display interpretation based on error code
|
||||||
|
if error_code == 0:
|
||||||
|
print('<p class="text-success">No error detected</p>')
|
||||||
|
elif error_code == 4:
|
||||||
|
print('<p class="text-danger">Error 4: Invalid server Hostname</p>')
|
||||||
|
elif error_code == 11:
|
||||||
|
print('<p class="text-danger">Error 11: Server connection error</p>')
|
||||||
|
elif error_code == 22:
|
||||||
|
print('<p class="text-danger">Error 22: PSD or CSD connection not established</p>')
|
||||||
|
elif error_code == 73:
|
||||||
|
print('<p class="text-danger">Error 73: Secure socket connect error</p>')
|
||||||
|
else:
|
||||||
|
print(f'<p class="text-danger">Unknown error code: {error_code}</p>')
|
||||||
|
else:
|
||||||
|
print('<p class="text-danger">Could not extract error code from response</p>')
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# 2.2 code 1 (HHTP succeded)
|
||||||
|
else:
|
||||||
|
# Si la commande HTTP a réussi
|
||||||
|
print("✅✅HTTP operation successful")
|
||||||
|
#4. Read reply from server
|
||||||
|
print("Reply from server:")
|
||||||
|
ser_sara.write(b'AT+URDFILE="aircarto_server_response.txt"\r')
|
||||||
|
response_SARA_4 = read_complete_response(ser_sara, wait_for_lines=["OK"], debug=False)
|
||||||
|
print(response_SARA_4)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
except serial.SerialException as e:
|
||||||
|
print(f"Error: {e}")
|
||||||
|
|
||||||
|
finally:
|
||||||
|
if ser_sara.is_open:
|
||||||
|
ser_sara.close()
|
||||||
|
#print("Serial closed")
|
||||||
|
|
||||||
@@ -11,22 +11,7 @@ parameter = sys.argv[1:] # Exclude the script name
|
|||||||
port='/dev/'+parameter[0] # ex: ttyAMA2
|
port='/dev/'+parameter[0] # ex: ttyAMA2
|
||||||
message = parameter[1] # ex: Hello
|
message = parameter[1] # ex: Hello
|
||||||
|
|
||||||
#get baudrate
|
baudrate = 115200
|
||||||
def load_config(config_file):
|
|
||||||
try:
|
|
||||||
with open(config_file, 'r') as file:
|
|
||||||
config_data = json.load(file)
|
|
||||||
return config_data
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Error loading config file: {e}")
|
|
||||||
return {}
|
|
||||||
|
|
||||||
# Define the config file path
|
|
||||||
config_file = '/var/www/nebuleair_pro_4g/config.json'
|
|
||||||
# Load the configuration data
|
|
||||||
config = load_config(config_file)
|
|
||||||
# Access the shared variables
|
|
||||||
baudrate = config.get('SaraR4_baudrate', 115200)
|
|
||||||
|
|
||||||
ser = serial.Serial(
|
ser = serial.Serial(
|
||||||
port=port, #USB0 or ttyS0
|
port=port, #USB0 or ttyS0
|
||||||
|
|||||||
@@ -10,23 +10,9 @@ import json
|
|||||||
parameter = sys.argv[1:] # Exclude the script name
|
parameter = sys.argv[1:] # Exclude the script name
|
||||||
port='/dev/'+parameter[0] # ex: ttyAMA2
|
port='/dev/'+parameter[0] # ex: ttyAMA2
|
||||||
endpoint = parameter[1] # ex: /pro_4G/notif_message.php
|
endpoint = parameter[1] # ex: /pro_4G/notif_message.php
|
||||||
|
profile_id = parameter[2]
|
||||||
|
|
||||||
#get baudrate
|
baudrate = 115200
|
||||||
def load_config(config_file):
|
|
||||||
try:
|
|
||||||
with open(config_file, 'r') as file:
|
|
||||||
config_data = json.load(file)
|
|
||||||
return config_data
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Error loading config file: {e}")
|
|
||||||
return {}
|
|
||||||
|
|
||||||
# Define the config file path
|
|
||||||
config_file = '/var/www/nebuleair_pro_4g/config.json'
|
|
||||||
# Load the configuration data
|
|
||||||
config = load_config(config_file)
|
|
||||||
# Access the shared variables
|
|
||||||
baudrate = config.get('SaraR4_baudrate', 115200)
|
|
||||||
|
|
||||||
ser = serial.Serial(
|
ser = serial.Serial(
|
||||||
port=port, #USB0 or ttyS0
|
port=port, #USB0 or ttyS0
|
||||||
@@ -37,7 +23,7 @@ ser = serial.Serial(
|
|||||||
timeout = 2
|
timeout = 2
|
||||||
)
|
)
|
||||||
|
|
||||||
command= f'AT+UHTTPC=0,4,"{endpoint}","data.txt","sensordata.json",4\r'
|
command= f'AT+UHTTPC={profile_id},4,"{endpoint}","data.txt","sensordata.json",4\r'
|
||||||
ser.write((command + '\r').encode('utf-8'))
|
ser.write((command + '\r').encode('utf-8'))
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -1,4 +1,10 @@
|
|||||||
'''
|
r'''
|
||||||
|
____ _ ____ _
|
||||||
|
/ ___| / \ | _ \ / \
|
||||||
|
\___ \ / _ \ | |_) | / _ \
|
||||||
|
___) / ___ \| _ < / ___ \
|
||||||
|
|____/_/ \_\_| \_\/_/ \_\
|
||||||
|
|
||||||
Script to connect SARA-R410 to APN
|
Script to connect SARA-R410 to APN
|
||||||
AT+CGDCONT=1,"IP","data.mono"
|
AT+CGDCONT=1,"IP","data.mono"
|
||||||
|
|
||||||
@@ -15,23 +21,7 @@ port='/dev/'+parameter[0] # ex: ttyAMA2
|
|||||||
apn_address = parameter[1] # ex: data.mono
|
apn_address = parameter[1] # ex: data.mono
|
||||||
timeout = float(parameter[2]) # ex:2
|
timeout = float(parameter[2]) # ex:2
|
||||||
|
|
||||||
|
baudrate = 115200
|
||||||
#get baudrate
|
|
||||||
def load_config(config_file):
|
|
||||||
try:
|
|
||||||
with open(config_file, 'r') as file:
|
|
||||||
config_data = json.load(file)
|
|
||||||
return config_data
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Error loading config file: {e}")
|
|
||||||
return {}
|
|
||||||
|
|
||||||
# Define the config file path
|
|
||||||
config_file = '/var/www/nebuleair_pro_4g/config.json'
|
|
||||||
# Load the configuration data
|
|
||||||
config = load_config(config_file)
|
|
||||||
# Access the shared variables
|
|
||||||
baudrate = config.get('SaraR4_baudrate', 115200)
|
|
||||||
|
|
||||||
ser = serial.Serial(
|
ser = serial.Serial(
|
||||||
port=port, #USB0 or ttyS0
|
port=port, #USB0 or ttyS0
|
||||||
@@ -43,6 +33,8 @@ ser = serial.Serial(
|
|||||||
)
|
)
|
||||||
|
|
||||||
command = f'AT+CGDCONT=1,"IP","{apn_address}"\r'
|
command = f'AT+CGDCONT=1,"IP","{apn_address}"\r'
|
||||||
|
#command = f'AT+CGDCONT=1,"IPV4V6","{apn_address}"\r'
|
||||||
|
#command = f'AT+CGDCONT=1,"IP","{apn_address}",0,0\r'
|
||||||
ser.write((command + '\r').encode('utf-8'))
|
ser.write((command + '\r').encode('utf-8'))
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,13 @@
|
|||||||
'''
|
r'''
|
||||||
|
____ _ ____ _
|
||||||
|
/ ___| / \ | _ \ / \
|
||||||
|
\___ \ / _ \ | |_) | / _ \
|
||||||
|
___) / ___ \| _ < / ___ \
|
||||||
|
|____/_/ \_\_| \_\/_/ \_\
|
||||||
|
|
||||||
Script to set the URL for a HTTP request
|
Script to set the URL for a HTTP request
|
||||||
Ex:
|
Ex:
|
||||||
/usr/bin/python3 /var/www/nebuleair_pro_4g/SARA/sara_setURL.py ttyAMA2 data.nebuleair.fr
|
/usr/bin/python3 /var/www/nebuleair_pro_4g/SARA/sara_setURL.py ttyAMA2 data.nebuleair.fr 0
|
||||||
To do: need to add profile id as parameter
|
|
||||||
|
|
||||||
First profile id:
|
First profile id:
|
||||||
AT+UHTTP=0,1,"data.nebuleair.fr"
|
AT+UHTTP=0,1,"data.nebuleair.fr"
|
||||||
@@ -19,24 +24,10 @@ parameter = sys.argv[1:] # Exclude the script name
|
|||||||
#print("Parameters received:")
|
#print("Parameters received:")
|
||||||
port='/dev/'+parameter[0] # ex: ttyAMA2
|
port='/dev/'+parameter[0] # ex: ttyAMA2
|
||||||
url = parameter[1] # ex: data.mobileair.fr
|
url = parameter[1] # ex: data.mobileair.fr
|
||||||
|
profile_id = parameter[2] #ex: 0
|
||||||
|
|
||||||
|
|
||||||
#get baudrate
|
baudrate = 115200
|
||||||
def load_config(config_file):
|
|
||||||
try:
|
|
||||||
with open(config_file, 'r') as file:
|
|
||||||
config_data = json.load(file)
|
|
||||||
return config_data
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Error loading config file: {e}")
|
|
||||||
return {}
|
|
||||||
|
|
||||||
# Define the config file path
|
|
||||||
config_file = '/var/www/nebuleair_pro_4g/config.json'
|
|
||||||
# Load the configuration data
|
|
||||||
config = load_config(config_file)
|
|
||||||
# Access the shared variables
|
|
||||||
baudrate = config.get('SaraR4_baudrate', 115200)
|
|
||||||
|
|
||||||
ser = serial.Serial(
|
ser = serial.Serial(
|
||||||
port=port, #USB0 or ttyS0
|
port=port, #USB0 or ttyS0
|
||||||
@@ -47,7 +38,7 @@ ser = serial.Serial(
|
|||||||
timeout = 2
|
timeout = 2
|
||||||
)
|
)
|
||||||
|
|
||||||
command = f'AT+UHTTP=0,1,"{url}"\r'
|
command = f'AT+UHTTP={profile_id},1,"{url}"\r'
|
||||||
ser.write((command + '\r').encode('utf-8'))
|
ser.write((command + '\r').encode('utf-8'))
|
||||||
|
|
||||||
print("****")
|
print("****")
|
||||||
|
|||||||
@@ -40,22 +40,7 @@ def read_complete_response(serial_connection, timeout=2, end_of_response_timeout
|
|||||||
|
|
||||||
return response.decode('utf-8', errors='replace')
|
return response.decode('utf-8', errors='replace')
|
||||||
|
|
||||||
#get baudrate
|
baudrate = 115200
|
||||||
def load_config(config_file):
|
|
||||||
try:
|
|
||||||
with open(config_file, 'r') as file:
|
|
||||||
config_data = json.load(file)
|
|
||||||
return config_data
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Error loading config file: {e}")
|
|
||||||
return {}
|
|
||||||
|
|
||||||
# Define the config file path
|
|
||||||
config_file = '/var/www/nebuleair_pro_4g/config.json'
|
|
||||||
# Load the configuration data
|
|
||||||
config = load_config(config_file)
|
|
||||||
# Access the shared variables
|
|
||||||
baudrate = config.get('SaraR4_baudrate', 115200)
|
|
||||||
|
|
||||||
ser_sara = serial.Serial(
|
ser_sara = serial.Serial(
|
||||||
port=port, #USB0 or ttyS0
|
port=port, #USB0 or ttyS0
|
||||||
|
|||||||
@@ -12,21 +12,7 @@ port='/dev/'+parameter[0] # ex: ttyAMA2
|
|||||||
message = parameter[1] # ex: Hello
|
message = parameter[1] # ex: Hello
|
||||||
|
|
||||||
#get baudrate
|
#get baudrate
|
||||||
def load_config(config_file):
|
baudrate = 115200
|
||||||
try:
|
|
||||||
with open(config_file, 'r') as file:
|
|
||||||
config_data = json.load(file)
|
|
||||||
return config_data
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Error loading config file: {e}")
|
|
||||||
return {}
|
|
||||||
|
|
||||||
# Define the config file path
|
|
||||||
config_file = '/var/www/nebuleair_pro_4g/config.json'
|
|
||||||
# Load the configuration data
|
|
||||||
config = load_config(config_file)
|
|
||||||
# Access the shared variables
|
|
||||||
baudrate = config.get('SaraR4_baudrate', 115200)
|
|
||||||
|
|
||||||
ser = serial.Serial(
|
ser = serial.Serial(
|
||||||
port=port, #USB0 or ttyS0
|
port=port, #USB0 or ttyS0
|
||||||
|
|||||||
116
boot_hotspot.sh
116
boot_hotspot.sh
@@ -2,26 +2,81 @@
|
|||||||
|
|
||||||
# Script to check if wifi is connected and start hotspot if not
|
# Script to check if wifi is connected and start hotspot if not
|
||||||
# will also retreive unique RPi ID and store it to deviceID.txt
|
# will also retreive unique RPi ID and store it to deviceID.txt
|
||||||
|
# script that starts at boot:
|
||||||
|
# @reboot /var/www/nebuleair_pro_4g/boot_hotspot.sh >> /var/www/nebuleair_pro_4g/logs/app.log 2>&1
|
||||||
|
|
||||||
OUTPUT_FILE="/var/www/nebuleair_pro_4g/wifi_list.csv"
|
OUTPUT_FILE="/var/www/nebuleair_pro_4g/wifi_list.csv"
|
||||||
JSON_FILE="/var/www/nebuleair_pro_4g/config.json"
|
|
||||||
|
|
||||||
echo "-------------------"
|
echo "-------------------"
|
||||||
echo "-------------------"
|
echo "-------------------"
|
||||||
|
|
||||||
echo "NebuleAir pro started at $(date)"
|
echo "NebuleAir pro started at $(date)"
|
||||||
echo "getting SARA R4 serial number"
|
|
||||||
|
|
||||||
|
chmod -R 777 /var/www/nebuleair_pro_4g/
|
||||||
|
|
||||||
|
# Blink GPIO 23 and 24 five times (starts OFF, stays OFF at end)
|
||||||
|
#gpioset -c gpiochip0 -t 1s,1s,1s,1s,1s,1s,1s,1s,1s,1s,0 23=0 24=0
|
||||||
|
|
||||||
|
# Blink GPIO 23 and 24 five times (starts OFF, stays OFF at end)
|
||||||
|
python3 << 'EOF'
|
||||||
|
import RPi.GPIO as GPIO
|
||||||
|
import time
|
||||||
|
|
||||||
|
GPIO.setmode(GPIO.BCM)
|
||||||
|
GPIO.setwarnings(False)
|
||||||
|
GPIO.setup(23, GPIO.OUT)
|
||||||
|
GPIO.setup(24, GPIO.OUT)
|
||||||
|
|
||||||
|
for _ in range(5):
|
||||||
|
GPIO.output(23, GPIO.HIGH)
|
||||||
|
GPIO.output(24, GPIO.HIGH)
|
||||||
|
time.sleep(1)
|
||||||
|
GPIO.output(23, GPIO.LOW)
|
||||||
|
GPIO.output(24, GPIO.LOW)
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
GPIO.cleanup()
|
||||||
|
EOF
|
||||||
|
|
||||||
|
echo "getting RPI serial number"
|
||||||
# Get the last 8 characters of the serial number and write to text file
|
# Get the last 8 characters of the serial number and write to text file
|
||||||
serial_number=$(cat /proc/cpuinfo | grep Serial | awk '{print substr($3, length($3) - 7)}')
|
serial_number=$(cat /proc/cpuinfo | grep Serial | awk '{print substr($3, length($3) - 7)}')
|
||||||
# Define the JSON file path
|
|
||||||
# Use jq to update the "deviceID" in the JSON file
|
# update Sqlite database (only if not already set, i.e., still has default value 'XXXX')
|
||||||
jq --arg serial_number "$serial_number" '.deviceID = $serial_number' "$JSON_FILE" > temp.json && mv temp.json "$JSON_FILE"
|
echo "Updating SQLite database with device ID: $serial_number"
|
||||||
|
sqlite3 /var/www/nebuleair_pro_4g/sqlite/sensors.db "UPDATE config_table SET value='$serial_number' WHERE key='deviceID' AND value='XXXX';"
|
||||||
|
|
||||||
echo "id: $serial_number"
|
echo "id: $serial_number"
|
||||||
#get the SSH port for tunneling
|
|
||||||
SSH_TUNNEL_PORT=$(jq -r '.sshTunnel_port' "$JSON_FILE")
|
# Get deviceID from SQLite config_table (may be different from serial_number if manually configured)
|
||||||
|
DEVICE_ID=$(sqlite3 /var/www/nebuleair_pro_4g/sqlite/sensors.db "SELECT value FROM config_table WHERE key='deviceID'")
|
||||||
|
echo "Device ID from database: $DEVICE_ID"
|
||||||
|
|
||||||
|
# Get deviceName from SQLite config_table for use in hotspot SSID
|
||||||
|
DEVICE_NAME=$(sqlite3 /var/www/nebuleair_pro_4g/sqlite/sensors.db "SELECT value FROM config_table WHERE key='deviceName'")
|
||||||
|
echo "Device Name from database: $DEVICE_NAME"
|
||||||
|
|
||||||
|
# Get SSH tunnel port from SQLite config_table
|
||||||
|
SSH_TUNNEL_PORT=$(sqlite3 /var/www/nebuleair_pro_4g/sqlite/sensors.db "SELECT value FROM config_table WHERE key='sshTunnel_port'")
|
||||||
|
|
||||||
#need to wait for the network manager to be ready
|
#need to wait for the network manager to be ready
|
||||||
sleep 20
|
sleep 20
|
||||||
|
|
||||||
|
# IMPORTANT: Always enable WiFi radio at boot (in case it was disabled by power save)
|
||||||
|
WIFI_RADIO_STATE=$(nmcli radio wifi)
|
||||||
|
echo "WiFi radio state: $WIFI_RADIO_STATE"
|
||||||
|
|
||||||
|
if [ "$WIFI_RADIO_STATE" == "disabled" ]; then
|
||||||
|
echo "WiFi radio is disabled, enabling it..."
|
||||||
|
nmcli radio wifi on
|
||||||
|
# Wait longer for NetworkManager to scan and reconnect to known networks
|
||||||
|
echo "Waiting 15 seconds for WiFi to reconnect to known networks..."
|
||||||
|
sleep 15
|
||||||
|
else
|
||||||
|
echo "WiFi radio is already enabled"
|
||||||
|
fi
|
||||||
|
|
||||||
# Get the connection state of wlan0
|
# Get the connection state of wlan0
|
||||||
STATE=$(nmcli -g GENERAL.STATE device show wlan0)
|
STATE=$(nmcli -g GENERAL.STATE device show wlan0)
|
||||||
|
|
||||||
@@ -32,46 +87,43 @@ if [ "$STATE" == "30 (disconnected)" ]; then
|
|||||||
# Perform a wifi scan and save its output to a csv file
|
# Perform a wifi scan and save its output to a csv file
|
||||||
# nmcli device wifi list
|
# 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"
|
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
|
# Start the hotspot with SSID based on deviceName
|
||||||
echo "Starting hotspot..."
|
echo "Starting hotspot with SSID: $DEVICE_NAME"
|
||||||
sudo nmcli device wifi hotspot ifname wlan0 ssid nebuleair_pro password nebuleaircfg
|
sudo nmcli device wifi hotspot ifname wlan0 ssid "$DEVICE_NAME" password nebuleaircfg
|
||||||
|
|
||||||
# Update JSON to reflect hotspot mode
|
|
||||||
jq --arg status "hotspot" '.WIFI_status = $status' "$JSON_FILE" > temp.json && mv temp.json "$JSON_FILE"
|
|
||||||
|
|
||||||
|
# Update SQLite to reflect hotspot mode
|
||||||
|
sqlite3 /var/www/nebuleair_pro_4g/sqlite/sensors.db "UPDATE config_table SET value='hotspot' WHERE key='WIFI_status'"
|
||||||
|
|
||||||
else
|
else
|
||||||
echo "Success: wlan0 is connected!"
|
echo "🛜Success: wlan0 is connected!🛜"
|
||||||
CONN_SSID=$(nmcli -g GENERAL.CONNECTION device show wlan0)
|
CONN_SSID=$(nmcli -g GENERAL.CONNECTION device show wlan0)
|
||||||
echo "Connection: $CONN_SSID"
|
echo "Connection: $CONN_SSID"
|
||||||
|
|
||||||
#update config JSON file
|
# Update SQLite to reflect hotspot mode
|
||||||
jq --arg status "connected" '.WIFI_status = $status' "$JSON_FILE" > temp.json && mv temp.json "$JSON_FILE"
|
sqlite3 /var/www/nebuleair_pro_4g/sqlite/sensors.db "UPDATE config_table SET value='connected' WHERE key='WIFI_status'"
|
||||||
|
|
||||||
sudo chmod 777 "$JSON_FILE"
|
|
||||||
|
|
||||||
# Lancer le tunnel SSH
|
# Lancer le tunnel SSH
|
||||||
echo "Démarrage du tunnel SSH sur le port $SSH_TUNNEL_PORT..."
|
#echo "Démarrage du tunnel SSH sur le port $SSH_TUNNEL_PORT..."
|
||||||
# Start the SSH agent if it's not already running
|
# Start the SSH agent if it's not already running
|
||||||
eval "$(ssh-agent -s)"
|
#eval "$(ssh-agent -s)"
|
||||||
# Add your SSH private key
|
# Add your SSH private key
|
||||||
ssh-add /home/airlab/.ssh/id_rsa
|
#ssh-add /home/airlab/.ssh/id_rsa
|
||||||
#connections details
|
#connections details
|
||||||
REMOTE_USER="airlab_server1" # Remplacez par votre nom d'utilisateur distant
|
#REMOTE_USER="airlab_server1" # Remplacez par votre nom d'utilisateur distant
|
||||||
REMOTE_SERVER="aircarto.fr" # Remplacez par l'adresse de votre serveur
|
#REMOTE_SERVER="aircarto.fr" # Remplacez par l'adresse de votre serveur
|
||||||
LOCAL_PORT=22 # Port local à rediriger
|
#LOCAL_PORT=22 # Port local à rediriger
|
||||||
MONITOR_PORT=0 # Désactive la surveillance de connexion autossh
|
#MONITOR_PORT=0 # Désactive la surveillance de connexion autossh
|
||||||
|
|
||||||
#autossh -M "$MONITOR_PORT" -f -N -R "$SSH_TUNNEL_PORT:localhost:$LOCAL_PORT" "$REMOTE_USER@$REMOTE_SERVER" -p 50221
|
#autossh -M "$MONITOR_PORT" -f -N -R "$SSH_TUNNEL_PORT:localhost:$LOCAL_PORT" "$REMOTE_USER@$REMOTE_SERVER" -p 50221
|
||||||
# ssh -f -N -R 52221:localhost:22 -p 50221 airlab_server1@aircarto.fr
|
# ssh -f -N -R 52221:localhost:22 -p 50221 airlab_server1@aircarto.fr
|
||||||
ssh -i /var/www/.ssh/id_rsa -f -N -R "$SSH_TUNNEL_PORT:localhost:$LOCAL_PORT" -p 50221 -o StrictHostKeyChecking=no "$REMOTE_USER@$REMOTE_SERVER"
|
#ssh -i /var/www/.ssh/id_rsa -f -N -R "$SSH_TUNNEL_PORT:localhost:$LOCAL_PORT" -p 50221 -o StrictHostKeyChecking=no "$REMOTE_USER@$REMOTE_SERVER"
|
||||||
|
|
||||||
#Check if the tunnel was created successfully
|
#Check if the tunnel was created successfully
|
||||||
if [ $? -eq 0 ]; then
|
#if [ $? -eq 0 ]; then
|
||||||
echo "Tunnel started successfully!"
|
# echo "Tunnel started successfully!"
|
||||||
else
|
#else
|
||||||
echo "Error: Unable to start the tunnel!"
|
# echo "Error: Unable to start the tunnel!"
|
||||||
exit 1
|
# exit 1
|
||||||
fi
|
#fi
|
||||||
fi
|
fi
|
||||||
echo "-------------------"
|
echo "-------------------"
|
||||||
|
|||||||
367
changelog.json
Normal file
367
changelog.json
Normal file
@@ -0,0 +1,367 @@
|
|||||||
|
{
|
||||||
|
"versions": [
|
||||||
|
{
|
||||||
|
"version": "1.6.4",
|
||||||
|
"date": "2026-04-02",
|
||||||
|
"changes": {
|
||||||
|
"features": [
|
||||||
|
"Page modem: boutons Activer/Desactiver LED status connexion PCB (AT+UGPIOC=16,2 / AT+UGPIOC=16,255)"
|
||||||
|
],
|
||||||
|
"improvements": [
|
||||||
|
"Page modem: messages de progression en 3 etapes pendant le reset hardware (coupure, redemarrage, test connexion)",
|
||||||
|
"Page modem: bouton reset hardware desactive pendant l'operation pour eviter les doubles clics"
|
||||||
|
],
|
||||||
|
"fixes": [],
|
||||||
|
"compatibility": []
|
||||||
|
},
|
||||||
|
"notes": "Le reset hardware affiche maintenant les etapes en temps reel (~20s). Deux nouveaux boutons permettent de controler la LED bleue du PCB qui indique l'etat de la connexion reseau du modem."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"version": "1.6.3",
|
||||||
|
"date": "2026-04-01",
|
||||||
|
"changes": {
|
||||||
|
"features": [
|
||||||
|
"Page logs: bouton Auto-refresh pour suivre les logs SARA en temps reel (polling 3s)"
|
||||||
|
],
|
||||||
|
"improvements": [
|
||||||
|
"Service SARA: ajout flag python3 -u (unbuffered) pour ecriture immediate des logs dans le fichier"
|
||||||
|
],
|
||||||
|
"fixes": [],
|
||||||
|
"compatibility": [
|
||||||
|
"Necessite re-execution de setup_services.sh pour activer le mode unbuffered (optionnel, pas d'impact si non fait)"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"notes": "Les logs SARA sont maintenant visibles en temps reel sur la page logs grace au mode unbuffered Python et au rafraichissement automatique. Aucun impact sur les anciennes installations qui ne relancent pas setup_services.sh."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"version": "1.6.2",
|
||||||
|
"date": "2026-03-27",
|
||||||
|
"changes": {
|
||||||
|
"features": [],
|
||||||
|
"improvements": [
|
||||||
|
"Simplification du script de boot SARA (start.py): suppression config AirCarto, uSpot/SSL, PDP et geolocalisation",
|
||||||
|
"La configuration modem est desormais entierement geree par le script principal (SARA_send_data_v2.py)"
|
||||||
|
],
|
||||||
|
"fixes": [],
|
||||||
|
"compatibility": []
|
||||||
|
},
|
||||||
|
"notes": "Le script de boot ne fait plus que 3 choses: reset modem_config_mode, alimentation modem GPIO 16, detection modele R4/R5. Toute la configuration (URLs, certificats, PDP, geolocalisation) est deja geree par le script principal qui tourne chaque minute avec gestion d'erreur et retry."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"version": "1.6.1",
|
||||||
|
"date": "2026-03-19",
|
||||||
|
"changes": {
|
||||||
|
"features": [
|
||||||
|
"Sonometre NSRT MK4: detection deconnexion avec message explicite (page capteurs + self-test)",
|
||||||
|
"Colonne noise_status dans data_NOISE (0x00=OK, 0xFF=deconnecte)",
|
||||||
|
"ERR_NOISE (bit 5, byte 66) dans error_flags UDP quand sonometre deconnecte"
|
||||||
|
],
|
||||||
|
"improvements": [
|
||||||
|
"Script NSRT_mk4_get_data.py ecrit en base meme si capteur deconnecte (valeurs a 0, noise_status=0xFF)",
|
||||||
|
"Script read.py: message d'erreur clair au lieu de l'exception Python brute",
|
||||||
|
"Self-test: affiche 'Capteur deconnecte — verifiez le cablage USB' au lieu de l'erreur technique"
|
||||||
|
],
|
||||||
|
"fixes": [],
|
||||||
|
"compatibility": [
|
||||||
|
"Migration automatique: colonne noise_status ajoutee via set_config.py lors du firmware update"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"notes": "Gestion de la deconnexion du sonometre NSRT MK4 alignee sur le modele NPM: ecriture en base avec status d'erreur, flag ERR_NOISE dans la payload UDP, et messages utilisateur explicites sur l'interface web."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"version": "1.6.0",
|
||||||
|
"date": "2026-03-18",
|
||||||
|
"changes": {
|
||||||
|
"features": [
|
||||||
|
"Payload UDP Miotiq: envoi npm_status (byte 67) — registre status NextPM en temps reel"
|
||||||
|
],
|
||||||
|
"improvements": [
|
||||||
|
"npm_status lu depuis la derniere mesure en base (rowid DESC, pas de moyenne ni de timestamp)"
|
||||||
|
],
|
||||||
|
"fixes": [],
|
||||||
|
"compatibility": [
|
||||||
|
"Necessite mise a jour du parser Miotiq pour decoder le byte 67 (npm_status)"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"notes": "Le capteur envoie maintenant le registre status du NextPM dans chaque trame UDP (byte 67). La valeur est prise de la derniere mesure sans moyenne (un code erreur ne se moyenne pas). Utilise rowid pour eviter toute dependance au RTC."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"version": "1.5.2",
|
||||||
|
"date": "2026-03-18",
|
||||||
|
"changes": {
|
||||||
|
"features": [
|
||||||
|
"Page capteurs: lecture NPM via get_data_modbus_v3.py --dry-run (meme script que le timer)",
|
||||||
|
"Page capteurs: affichage temperature et humidite interne du NPM",
|
||||||
|
"Page capteurs: decodage npm_status avec flags d'erreur individuels"
|
||||||
|
],
|
||||||
|
"improvements": [
|
||||||
|
"NPM get_data_modbus_v3.py: mode --dry-run (print JSON sans ecriture en base)",
|
||||||
|
"Page capteurs: status NPM affiche en vert (OK) ou orange/rouge (erreurs decodees)"
|
||||||
|
],
|
||||||
|
"fixes": [
|
||||||
|
"Page capteurs: suppression unite ug/m3 sur le champ message/status"
|
||||||
|
],
|
||||||
|
"compatibility": []
|
||||||
|
},
|
||||||
|
"notes": "La page capteurs utilise maintenant le meme script Modbus que le timer systemd, en mode dry-run pour eviter les conflits d'ecriture SQLite. Le status NPM est decode bit par bit."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"version": "1.5.1",
|
||||||
|
"date": "2026-03-18",
|
||||||
|
"changes": {
|
||||||
|
"features": [
|
||||||
|
"Payload UDP Miotiq: bytes 69-71 firmware version (major.minor.patch)",
|
||||||
|
"README: documentation complete de la structure des 100 bytes UDP"
|
||||||
|
],
|
||||||
|
"improvements": [],
|
||||||
|
"fixes": [],
|
||||||
|
"compatibility": [
|
||||||
|
"Necessite mise a jour du parser Miotiq pour decoder les bytes 69-71 (firmware version)"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"notes": "Le capteur envoie maintenant sa version firmware dans chaque trame UDP. Cote serveur, bytes 69/70/71 = major/minor/patch. Documentation payload complete ajoutee au README."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"version": "1.5.0",
|
||||||
|
"date": "2026-03-18",
|
||||||
|
"changes": {
|
||||||
|
"features": [
|
||||||
|
"Payload UDP Miotiq: byte 66 error_flags (erreurs systeme RTC/capteurs)",
|
||||||
|
"Payload UDP Miotiq: byte 67 npm_status (registre status NextPM)",
|
||||||
|
"Payload UDP Miotiq: byte 68 device_status (etat general du boitier, specification)",
|
||||||
|
"Methodes SensorPayload: set_error_flags(), set_npm_status(), set_device_status()"
|
||||||
|
],
|
||||||
|
"improvements": [
|
||||||
|
"Initialisation bytes 66-68 a 0x00 au lieu de 0xFF pour eviter faux positifs cote serveur",
|
||||||
|
"Escalade erreur UDP: si PDP reset echoue, notification WiFi + hardware reboot + exit"
|
||||||
|
],
|
||||||
|
"fixes": [],
|
||||||
|
"compatibility": [
|
||||||
|
"Necessite mise a jour du parser Miotiq pour decoder les bytes 66-68 (error_flags, npm_status, device_status)"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"notes": "Ajout de registres d'erreur et d'etat dans la payload UDP (bytes 66-68). Les bytes de status sont initialises a 0x00 (aucune erreur) au lieu de 0xFF. Le flag RTC est implemente, les autres flags seront actives progressivement."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"version": "1.4.6",
|
||||||
|
"date": "2026-03-17",
|
||||||
|
"changes": {
|
||||||
|
"features": [
|
||||||
|
"Page Admin: comparaison RTC vs heure du navigateur (au lieu de system time)",
|
||||||
|
"Page Admin: ajout champ Browser time (UTC) dans l'onglet Clock",
|
||||||
|
"Page Admin: bloquer update firmware en mode hotspot avec message explicatif",
|
||||||
|
"Page Admin: liens Gitea pour mise a jour hors-ligne (releases + main.zip)"
|
||||||
|
],
|
||||||
|
"improvements": [
|
||||||
|
"Page Admin: RTC time mis en evidence (label bold, input large, bordure bleue)",
|
||||||
|
"Page Admin: System time replie dans un details/summary (non utilise par le capteur)",
|
||||||
|
"Page Admin: descriptions ajoutees pour System time, RTC time et Synchroniser le RTC"
|
||||||
|
],
|
||||||
|
"fixes": [
|
||||||
|
"Fix forget_wifi scan: delai 5s + rescan explicite pour remplir wifi_list.csv",
|
||||||
|
"Fix blocage navigateur: revert optimisations fetch qui saturaient la limite 6 connexions/domaine"
|
||||||
|
],
|
||||||
|
"compatibility": []
|
||||||
|
},
|
||||||
|
"notes": "L'onglet Clock compare maintenant le RTC a l'heure du navigateur, plus fiable que le system time Linux (non utilise par le capteur). L'update firmware est bloque en mode hotspot avec un message explicatif. La mise a jour hors-ligne via upload .zip reste disponible."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"version": "1.4.5",
|
||||||
|
"date": "2026-03-17",
|
||||||
|
"changes": {
|
||||||
|
"features": [
|
||||||
|
"Page WiFi: bouton Oublier le reseau pour passer en mode hotspot sans reboot",
|
||||||
|
"Page WiFi: badge Mode Hotspot visible dans la sidebar (lien vers page WiFi)",
|
||||||
|
"Page WiFi: scan des reseaux WiFi en mode hotspot via cache CSV (scan au demarrage)"
|
||||||
|
],
|
||||||
|
"improvements": [
|
||||||
|
"Page WiFi: refonte UI avec cards contextuelles (infos connexion detaillees si connecte, scan si hotspot)",
|
||||||
|
"Page WiFi: affichage SSID, signal, IP, passerelle, hostname, frequence, securite",
|
||||||
|
"Page WiFi: scan WiFi masque quand deja connecte, scan avec colonnes signal et securite",
|
||||||
|
"Page WiFi: migration de config.json vers get_config_sqlite",
|
||||||
|
"Endpoint internet enrichi: SSID, signal, frequence, securite, passerelle, hostname",
|
||||||
|
"Scan WiFi en mode hotspot: lecture du fichier wifi_list.csv avec notice explicative",
|
||||||
|
"forget_wifi.sh: scan WiFi avec rescan explicite et delai avant lancement hotspot"
|
||||||
|
],
|
||||||
|
"fixes": [
|
||||||
|
"Correction VERSION 1.4.3 -> 1.4.4",
|
||||||
|
"Fix IP hotspot: 192.168.43.1 -> 10.42.0.1 (defaut NetworkManager)",
|
||||||
|
"Fix forget_wifi.sh: appel bash explicite + disconnect wlan0 avant delete"
|
||||||
|
],
|
||||||
|
"compatibility": []
|
||||||
|
},
|
||||||
|
"notes": "Le bouton Oublier le reseau supprime la connexion WiFi sauvegardee, scanne les reseaux disponibles, puis demarre le hotspot (pas de reboot necessaire). En mode hotspot, la page WiFi affiche les reseaux scannes au demarrage via un cache CSV. Adresse hotspot: http://10.42.0.1/html/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"version": "1.4.4",
|
||||||
|
"date": "2026-03-16",
|
||||||
|
"changes": {
|
||||||
|
"features": [
|
||||||
|
"Bouton Self Test disponible sur les pages Accueil, Capteurs et Admin (en plus de Modem 4G)",
|
||||||
|
"Test du module RTC DS3231 integre dans le self-test (connexion + synchronisation horloge)"
|
||||||
|
],
|
||||||
|
"improvements": [
|
||||||
|
"Refactoring self-test : code JS et HTML des modals extraits dans des fichiers partages (selftest.js, selftest-modal.html)",
|
||||||
|
"Le modal self-test est charge dynamiquement via fetch, plus besoin de dupliquer le HTML"
|
||||||
|
],
|
||||||
|
"fixes": [],
|
||||||
|
"compatibility": []
|
||||||
|
},
|
||||||
|
"notes": "Le self-test est maintenant accessible depuis toutes les pages principales. Le test RTC verifie la connexion du module et l'ecart avec l'heure systeme."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"version": "1.4.3",
|
||||||
|
"date": "2026-03-16",
|
||||||
|
"changes": {
|
||||||
|
"features": [
|
||||||
|
"Page database: bouton telecharger toute la table (bypass filtre dates)",
|
||||||
|
"Page database: validation obligatoire des dates avant telechargement par periode"
|
||||||
|
],
|
||||||
|
"improvements": [
|
||||||
|
"Payload UDP bruit: bytes 22-23 = noise_cur_leq, 24-25 = noise_cur_level, 26-27 = max_noise (reserve)",
|
||||||
|
"Envoi des deux valeurs bruit (cur_LEQ + DB_A_value) en UDP Miotiq au lieu d'une seule"
|
||||||
|
],
|
||||||
|
"fixes": [],
|
||||||
|
"compatibility": [
|
||||||
|
"Necessite mise a jour du parser Miotiq pour decoder les nouveaux champs noise_cur_leq et noise_cur_level"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"notes": "Mise a jour structure UDP bruit pour alignement avec parser Miotiq et ameliorations page database."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"version": "1.4.2",
|
||||||
|
"date": "2026-03-14",
|
||||||
|
"changes": {
|
||||||
|
"features": [],
|
||||||
|
"improvements": [],
|
||||||
|
"fixes": [
|
||||||
|
"Fix envoi UDP Miotiq: desynchronisation serie causant l'envoi de la commande AT+USOWR comme payload au lieu des donnees capteurs",
|
||||||
|
"Ajout flush buffer serie (reset_input_buffer) avant chaque etape UDP critique",
|
||||||
|
"Verification du prompt @ du modem avant envoi des donnees binaires",
|
||||||
|
"Abort propre de l'envoi UDP si creation socket, connexion ou prompt @ echoue",
|
||||||
|
"Retry creation socket apres reset PDP reussi"
|
||||||
|
],
|
||||||
|
"compatibility": []
|
||||||
|
},
|
||||||
|
"notes": "Corrige un bug ou le modem SARA envoyait la commande AT+USOWR comme donnees UDP, causant des erreurs UNKNOWN_DEVICE sur le parser Miotiq."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"version": "1.4.1",
|
||||||
|
"date": "2026-03-12",
|
||||||
|
"changes": {
|
||||||
|
"features": [],
|
||||||
|
"improvements": [
|
||||||
|
"Migration capteur bruit de l'ancien systeme I2C vers le sonometre NSRT MK4 en USB",
|
||||||
|
"Nouveau script sound_meter/read.py pour lecture a la demande (retour JSON)",
|
||||||
|
"Page capteurs: carte USB avec affichage LEQ et dB(A) au lieu de l'ancien format texte",
|
||||||
|
"Self-test modem: parsing JSON du NSRT MK4 au lieu de texte brut"
|
||||||
|
],
|
||||||
|
"fixes": [
|
||||||
|
"Correction du self-test bruit qui affichait 'Unexpected value' avec le nouveau capteur"
|
||||||
|
],
|
||||||
|
"compatibility": []
|
||||||
|
},
|
||||||
|
"notes": "Mise a jour necessaire si le sonometre NSRT MK4 est connecte en USB. L'ancien capteur I2C n'est plus supporte sur la page capteurs."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"version": "1.4.0",
|
||||||
|
"date": "2026-03-10",
|
||||||
|
"changes": {
|
||||||
|
"features": [
|
||||||
|
"Mise a jour firmware hors-ligne par upload de fichier ZIP via l'interface web admin",
|
||||||
|
"Barre de progression pour suivre l'upload du fichier",
|
||||||
|
"Fichier .update-exclude versionne pour gerer les exclusions rsync de maniere evolutive"
|
||||||
|
],
|
||||||
|
"improvements": [
|
||||||
|
"Vidage du buffer serie avant chaque commande AT dans sara.py (evite les URCs residuelles au demarrage)"
|
||||||
|
],
|
||||||
|
"fixes": [],
|
||||||
|
"compatibility": [
|
||||||
|
"Necessite l'ajout de update_firmware_from_file.sh dans les permissions sudo de www-data",
|
||||||
|
"Necessite Apache mod_rewrite pour html/.htaccess (upload 50MB)"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"notes": "Permet la mise a jour du firmware sans connexion internet : telecharger le .zip depuis Gitea, se connecter au hotspot du capteur, et uploader via admin.html."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"version": "1.3.0",
|
||||||
|
"date": "2026-02-17",
|
||||||
|
"changes": {
|
||||||
|
"features": [
|
||||||
|
"Onglet 'Ecran' pour le controle de l'affichage HDMI (ModuleAir Pro uniquement)",
|
||||||
|
"Demarrage et arret du script d'affichage via l'interface web",
|
||||||
|
"Verification automatique du type d'appareil pour afficher l'onglet"
|
||||||
|
],
|
||||||
|
"improvements": [
|
||||||
|
"Ajout de logs console pour le debougage des commandes web",
|
||||||
|
"Traduction de l'element de menu 'Ecran'"
|
||||||
|
],
|
||||||
|
"fixes": [
|
||||||
|
"Correction des permissions d'execution des scripts python via web (sudo)",
|
||||||
|
"Correction de la visibilite des onglets du menu lateral (doublons ID)"
|
||||||
|
],
|
||||||
|
"compatibility": [
|
||||||
|
"Necessite python3-kivy installe",
|
||||||
|
"Necessite l'ajout de permissions sudo pour www-data (voir documentation)"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"notes": "Ajout de la fonctionnalite de controle d'ecran pour les demonstrations."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"version": "1.2.0",
|
||||||
|
"date": "2026-02-17",
|
||||||
|
"changes": {
|
||||||
|
"features": [
|
||||||
|
"Integration capteur CO2 MH-Z19 (scripts, base de donnees, service systemd, interface web)",
|
||||||
|
"Carte test CO2 sur la page capteurs",
|
||||||
|
"Checkbox activation CO2 sur la page admin",
|
||||||
|
"Consultation et telechargement des mesures CO2 sur la page base de donnees"
|
||||||
|
],
|
||||||
|
"improvements": [],
|
||||||
|
"fixes": [
|
||||||
|
"Logo ModuleAir Pro ne s'affichait pas (script dans innerHTML non execute)"
|
||||||
|
],
|
||||||
|
"compatibility": [
|
||||||
|
"Necessite re-execution de create_db.py, set_config.py et setup_services.sh apres mise a jour"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"notes": "Ajout du support capteur CO2 MH-Z19 pour le ModuleAir Pro. La transmission SARA sera integree dans une version ulterieure."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"version": "1.1.0",
|
||||||
|
"date": "2026-02-16",
|
||||||
|
"changes": {
|
||||||
|
"features": [
|
||||||
|
"Card informations base de donnees (taille, nombre d'entrees, dates min/max par table)",
|
||||||
|
"Telechargement CSV complet par table depuis la page base de donnees",
|
||||||
|
"Bouton version firmware NextPM sur la page capteurs",
|
||||||
|
"Tests capteurs integres dans l'auto-test modem",
|
||||||
|
"Logo dynamique selon le type d'appareil (NebuleAir/ModuleAir)"
|
||||||
|
],
|
||||||
|
"improvements": [
|
||||||
|
"Reordonnancement de l'auto-test : capteurs avant communication"
|
||||||
|
],
|
||||||
|
"fixes": [],
|
||||||
|
"compatibility": []
|
||||||
|
},
|
||||||
|
"notes": "Ameliorations de l'interface web : meilleure visibilite sur l'etat de la base de donnees et des capteurs."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"version": "1.0.0",
|
||||||
|
"date": "2026-02-11",
|
||||||
|
"changes": {
|
||||||
|
"features": [
|
||||||
|
"Support multi-device : NebuleAir Pro / ModuleAir Pro",
|
||||||
|
"Systeme de versioning firmware",
|
||||||
|
"Changelog viewer dans l'interface web"
|
||||||
|
],
|
||||||
|
"improvements": [],
|
||||||
|
"fixes": [],
|
||||||
|
"compatibility": [
|
||||||
|
"Les capteurs existants sont automatiquement configures en 'nebuleair_pro'"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"notes": "Premiere version tracee. Les capteurs anterieurs recevront device_type=nebuleair_pro par defaut lors de la mise a jour."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
41
connexion.sh
41
connexion.sh
@@ -2,26 +2,49 @@
|
|||||||
echo "-------"
|
echo "-------"
|
||||||
echo "Start connexion shell script at $(date)"
|
echo "Start connexion shell script at $(date)"
|
||||||
|
|
||||||
|
# Get deviceName from database for hotspot SSID
|
||||||
|
DEVICE_NAME=$(sqlite3 /var/www/nebuleair_pro_4g/sqlite/sensors.db "SELECT value FROM config_table WHERE key='deviceName'")
|
||||||
|
echo "Device Name: $DEVICE_NAME"
|
||||||
|
|
||||||
#disable hotspot
|
# Find and disable any active hotspot connection
|
||||||
echo "Disable Hotspot:"
|
echo "Disable Hotspot..."
|
||||||
sudo nmcli connection down Hotspot
|
# Get all wireless connections that are currently active (excludes the target WiFi)
|
||||||
sleep 10
|
ACTIVE_HOTSPOT=$(nmcli -t -f NAME,TYPE,DEVICE connection show --active | grep ':802-11-wireless:wlan0' | cut -d: -f1)
|
||||||
|
|
||||||
|
if [ -n "$ACTIVE_HOTSPOT" ]; then
|
||||||
|
echo "Disabling hotspot connection: $ACTIVE_HOTSPOT"
|
||||||
|
sudo nmcli connection down "$ACTIVE_HOTSPOT"
|
||||||
|
else
|
||||||
|
echo "No active hotspot found"
|
||||||
|
fi
|
||||||
|
|
||||||
|
sleep 5
|
||||||
|
|
||||||
echo "Start connection with:"
|
echo "Start connection with:"
|
||||||
echo "SSID: $1"
|
echo "SSID: $1"
|
||||||
echo "Password: $2"
|
echo "Password: [HIDDEN]"
|
||||||
sudo nmcli device wifi connect "$1" password "$2"
|
sudo nmcli device wifi connect "$1" password "$2"
|
||||||
|
|
||||||
#check if connection is successfull
|
# Check if connection is successful
|
||||||
if [ $? -eq 0 ]; then
|
if [ $? -eq 0 ]; then
|
||||||
echo "Connection to $1 is successfull"
|
echo "Connection to $1 is successful"
|
||||||
|
|
||||||
|
# Update SQLite to reflect connected status
|
||||||
|
sqlite3 /var/www/nebuleair_pro_4g/sqlite/sensors.db "UPDATE config_table SET value='connected' WHERE key='WIFI_status'"
|
||||||
|
echo "Updated database: WIFI_status = connected"
|
||||||
else
|
else
|
||||||
echo "Connection to $1 failed"
|
echo "Connection to $1 failed"
|
||||||
echo "Restarting hotspot..."
|
echo "Restarting hotspot..."
|
||||||
#enable hotspot
|
|
||||||
sudo nmcli connection up Hotspot
|
# Recreate hotspot with current deviceName as SSID
|
||||||
|
sudo nmcli device wifi hotspot ifname wlan0 ssid "$DEVICE_NAME" password nebuleaircfg
|
||||||
|
|
||||||
|
# Update SQLite to reflect hotspot mode
|
||||||
|
sqlite3 /var/www/nebuleair_pro_4g/sqlite/sensors.db "UPDATE config_table SET value='hotspot' WHERE key='WIFI_status'"
|
||||||
|
echo "Updated database: WIFI_status = hotspot"
|
||||||
|
echo "Hotspot restarted with SSID: $DEVICE_NAME"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "End connexion shell script"
|
echo "End connexion shell script"
|
||||||
echo "-------"
|
echo "-------"
|
||||||
|
|
||||||
|
|||||||
13
cron_jobs
13
cron_jobs
@@ -1,12 +1,13 @@
|
|||||||
@reboot chmod 777 /dev/ttyAMA* /dev/i2c-1
|
@reboot sleep 10 && chmod 777 /dev/ttyAMA* /dev/i2c-1
|
||||||
|
|
||||||
@reboot /var/www/nebuleair_pro_4g/boot_hotspot.sh >> /var/www/nebuleair_pro_4g/logs/app.log 2>&1
|
@reboot /var/www/nebuleair_pro_4g/boot_hotspot.sh >> /var/www/nebuleair_pro_4g/logs/app.log 2>&1
|
||||||
|
|
||||||
@reboot sleep 30 && /usr/bin/python3 /var/www/nebuleair_pro_4g/SARA/sara_setURL.py ttyAMA2 data.nebuleair.fr >> /var/www/nebuleair_pro_4g/logs/app.log 2>&1
|
@reboot sleep 30 && /usr/bin/python3 /var/www/nebuleair_pro_4g/SARA/reboot/start.py >> /var/www/nebuleair_pro_4g/logs/app.log 2>&1
|
||||||
|
|
||||||
@reboot sleep 45 && /usr/bin/python3 /var/www/nebuleair_pro_4g/SARA/SSL/prepareUspotProfile.py ttyAMA2 api-prod.uspot.probesys.net >> /var/www/nebuleair_pro_4g/logs/app.log 2>&1
|
#0 0 * * * > /var/www/nebuleair_pro_4g/logs/master.log
|
||||||
|
#0 0 * * * > /var/www/nebuleair_pro_4g/logs/master_errors.log
|
||||||
|
#0 0 * * * > /var/www/nebuleair_pro_4g/logs/app.log
|
||||||
|
|
||||||
|
0 0 * * * find /var/www/nebuleair_pro_4g/logs -name "*.log" -type f -exec truncate -s 0 {} \;
|
||||||
|
|
||||||
|
|
||||||
* * * * * /usr/bin/python3 /var/www/nebuleair_pro_4g/loop/1_NPM/send_data.py >> /var/www/nebuleair_pro_4g/logs/loop.log 2>&1
|
|
||||||
|
|
||||||
0 0 */2 * * > /var/www/nebuleair_pro_4g/logs/loop.log
|
|
||||||
|
|||||||
@@ -1,6 +1,16 @@
|
|||||||
"""
|
"""
|
||||||
|
_____ _ ___ _______ _
|
||||||
|
| ____| \ | \ \ / / ____| / \
|
||||||
|
| _| | \| |\ \ / /| _| / _ \
|
||||||
|
| |___| |\ | \ V / | |___ / ___ \
|
||||||
|
|_____|_| \_| \_/ |_____/_/ \_\
|
||||||
|
|
||||||
Main loop to gather data from envea Sensors
|
Main loop to gather data from envea Sensors
|
||||||
/usr/bin/python3 /var/www/nebuleair_pro_4g/envea/read_value_loop.py
|
Need to run every minutes
|
||||||
|
|
||||||
|
* * * * * /usr/bin/python3 /var/www/nebuleair_pro_4g/envea/read_value_loop.py
|
||||||
|
|
||||||
|
Save data to .txt file inside /var/www/nebuleair_pro_4g/envea/data/
|
||||||
"""
|
"""
|
||||||
import json
|
import json
|
||||||
import serial
|
import serial
|
||||||
133
envea/old/read_value_loop_json.py
Normal file
133
envea/old/read_value_loop_json.py
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
"""
|
||||||
|
_____ _ ___ _______ _
|
||||||
|
| ____| \ | \ \ / / ____| / \
|
||||||
|
| _| | \| |\ \ / /| _| / _ \
|
||||||
|
| |___| |\ | \ V / | |___ / ___ \
|
||||||
|
|_____|_| \_| \_/ |_____/_/ \_\
|
||||||
|
|
||||||
|
Main loop to gather data from Envea Sensors
|
||||||
|
|
||||||
|
Runs every minute via cron:
|
||||||
|
|
||||||
|
* * * * * /usr/bin/python3 /var/www/nebuleair_pro_4g/envea/read_value_loop_json.py
|
||||||
|
|
||||||
|
Saves data as JSON inside: /var/www/nebuleair_pro_4g/envea/data/data.json
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import serial
|
||||||
|
import time
|
||||||
|
import traceback
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
# Function to load config data
|
||||||
|
def load_config(config_file):
|
||||||
|
try:
|
||||||
|
with open(config_file, 'r') as file:
|
||||||
|
return json.load(file)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error loading config file: {e}")
|
||||||
|
return {}
|
||||||
|
|
||||||
|
# Function to save data to a JSON file
|
||||||
|
def save_data_to_json(filename, data):
|
||||||
|
try:
|
||||||
|
with open(filename, 'w') as file:
|
||||||
|
json.dump(data, file, indent=4)
|
||||||
|
print(f"Data saved to {filename}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error saving to file {filename}: {e}")
|
||||||
|
|
||||||
|
# Define the config file path
|
||||||
|
config_file = '/var/www/nebuleair_pro_4g/config.json'
|
||||||
|
|
||||||
|
# Load configuration data
|
||||||
|
config = load_config(config_file)
|
||||||
|
|
||||||
|
# Initialize sensors and serial connections
|
||||||
|
envea_sondes = config.get('envea_sondes', [])
|
||||||
|
connected_envea_sondes = [sonde for sonde in envea_sondes if sonde.get('connected', False)]
|
||||||
|
serial_connections = {}
|
||||||
|
|
||||||
|
if connected_envea_sondes:
|
||||||
|
for device in connected_envea_sondes:
|
||||||
|
port = device.get('port', 'Unknown')
|
||||||
|
name = device.get('name', 'Unknown')
|
||||||
|
try:
|
||||||
|
serial_connections[name] = serial.Serial(
|
||||||
|
port=f'/dev/{port}',
|
||||||
|
baudrate=9600,
|
||||||
|
parity=serial.PARITY_NONE,
|
||||||
|
stopbits=serial.STOPBITS_ONE,
|
||||||
|
bytesize=serial.EIGHTBITS,
|
||||||
|
timeout=1
|
||||||
|
)
|
||||||
|
except serial.SerialException as e:
|
||||||
|
print(f"Error opening serial port for {name}: {e}")
|
||||||
|
|
||||||
|
# Function to gather data from sensors
|
||||||
|
def gather_data():
|
||||||
|
global data_h2s, data_no2, data_o3
|
||||||
|
data_h2s = 0
|
||||||
|
data_no2 = 0
|
||||||
|
data_o3 = 0
|
||||||
|
|
||||||
|
try:
|
||||||
|
if connected_envea_sondes:
|
||||||
|
for device in connected_envea_sondes:
|
||||||
|
name = device.get('name', 'Unknown')
|
||||||
|
coefficient = device.get('coefficient', 1)
|
||||||
|
if name in serial_connections:
|
||||||
|
serial_connection = serial_connections[name]
|
||||||
|
try:
|
||||||
|
serial_connection.write(
|
||||||
|
b"\xFF\x02\x13\x30\x01\x02\x03\x04\x05\x06\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\x12\xAF\x88\x03"
|
||||||
|
)
|
||||||
|
data_envea = serial_connection.readline()
|
||||||
|
if len(data_envea) >= 20:
|
||||||
|
byte_20 = data_envea[19] * coefficient
|
||||||
|
if name == "h2s":
|
||||||
|
data_h2s = byte_20
|
||||||
|
elif name == "no2":
|
||||||
|
data_no2 = byte_20
|
||||||
|
elif name == "o3":
|
||||||
|
data_o3 = byte_20
|
||||||
|
except serial.SerialException as e:
|
||||||
|
print(f"Error communicating with {name}: {e}")
|
||||||
|
except Exception as e:
|
||||||
|
print("An error occurred while gathering data:", e)
|
||||||
|
traceback.print_exc()
|
||||||
|
|
||||||
|
# Main loop
|
||||||
|
if __name__ == "__main__":
|
||||||
|
h2s_values = []
|
||||||
|
no2_values = []
|
||||||
|
o3_values = []
|
||||||
|
|
||||||
|
for cycle in range(6): # Run 6 times
|
||||||
|
gather_data()
|
||||||
|
h2s_values.append(data_h2s)
|
||||||
|
no2_values.append(data_no2)
|
||||||
|
o3_values.append(data_o3)
|
||||||
|
|
||||||
|
print(f"Cycle {cycle + 1}:")
|
||||||
|
print(f" H2S: {data_h2s}, NO2: {data_no2}, O3: {data_o3}")
|
||||||
|
time.sleep(9) # Wait 9 seconds
|
||||||
|
|
||||||
|
# Compute the mean values (as integers)
|
||||||
|
mean_h2s = int(sum(h2s_values) / len(h2s_values)) if h2s_values else 0
|
||||||
|
mean_no2 = int(sum(no2_values) / len(no2_values)) if no2_values else 0
|
||||||
|
mean_o3 = int(sum(o3_values) / len(o3_values)) if o3_values else 0
|
||||||
|
|
||||||
|
# Create JSON structure
|
||||||
|
data_json = {
|
||||||
|
"h2s": mean_h2s,
|
||||||
|
"no2": mean_no2,
|
||||||
|
"o3": mean_o3
|
||||||
|
}
|
||||||
|
|
||||||
|
# Define JSON file path
|
||||||
|
output_file = "/var/www/nebuleair_pro_4g/envea/data/data.json"
|
||||||
|
|
||||||
|
# Save to JSON file
|
||||||
|
save_data_to_json(output_file, data_json)
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import serial
|
import serial
|
||||||
import time
|
import time
|
||||||
import sys
|
import sys
|
||||||
|
import re
|
||||||
|
|
||||||
parameter = sys.argv[1:] # Exclude the script name
|
parameter = sys.argv[1:] # Exclude the script name
|
||||||
#print("Parameters received:")
|
#print("Parameters received:")
|
||||||
@@ -61,8 +62,46 @@ def read_cairsens(port, baudrate=9600, parity=serial.PARITY_NONE, stopbits=seria
|
|||||||
|
|
||||||
# ASCII characters
|
# ASCII characters
|
||||||
ascii_data = ''.join(chr(b) if 0x20 <= b <= 0x7E else '.' for b in raw_bytes)
|
ascii_data = ''.join(chr(b) if 0x20 <= b <= 0x7E else '.' for b in raw_bytes)
|
||||||
print(f"Valeurs converties en ASCII : {ascii_data}")
|
sensor_type = "Unknown" # ou None, selon ton besoin
|
||||||
|
sensor_measurement = "Unknown"
|
||||||
|
sensor_range = "Unknown"
|
||||||
|
|
||||||
|
letters = re.findall(r'[A-Za-z]', ascii_data)
|
||||||
|
if len(letters) >= 1:
|
||||||
|
#print(f"First letter found: {letters[0]}")
|
||||||
|
if letters[0] == "C":
|
||||||
|
sensor_type = "Cairclip"
|
||||||
|
if len(letters) >= 2:
|
||||||
|
#print(f"Second letter found: {letters[1]}")
|
||||||
|
if letters[1] == "A":
|
||||||
|
sensor_measurement = "Ammonia(NH3)"
|
||||||
|
if letters[1] == "C":
|
||||||
|
sensor_measurement = "O3 and NO2"
|
||||||
|
if letters[1] == "G":
|
||||||
|
sensor_measurement = "CH4"
|
||||||
|
if letters[1] == "H":
|
||||||
|
sensor_measurement = "H2S"
|
||||||
|
if letters[1] == "N":
|
||||||
|
sensor_measurement = "NO2"
|
||||||
|
if len(letters) >= 3:
|
||||||
|
#print(f"Thrisd letter found: {letters[2]}")
|
||||||
|
if letters[2] == "B":
|
||||||
|
sensor_range = "0-250 ppb"
|
||||||
|
if letters[2] == "M":
|
||||||
|
sensor_range = "0-1ppm"
|
||||||
|
if letters[2] == "V":
|
||||||
|
sensor_range = "0-20 ppm"
|
||||||
|
if letters[2] == "P":
|
||||||
|
sensor_range = "PACKET data block ?"
|
||||||
|
|
||||||
|
if len(letters) < 1:
|
||||||
|
print("No letter found in the ASCII data.")
|
||||||
|
|
||||||
|
print(f"Valeurs converties en ASCII : {sensor_type} {sensor_measurement} {sensor_range}")
|
||||||
|
|
||||||
|
#print(f"Sensor type: {sensor_type}")
|
||||||
|
#print(f"Sensor measurment: {sensor_measurement}")
|
||||||
|
#print(f"Sensor range: {sensor_range}")
|
||||||
# Numeric values
|
# Numeric values
|
||||||
numeric_values = [b for b in raw_bytes]
|
numeric_values = [b for b in raw_bytes]
|
||||||
print(f"Valeurs numériques : {numeric_values}")
|
print(f"Valeurs numériques : {numeric_values}")
|
||||||
|
|||||||
224
envea/read_ref_v2.py
Normal file
224
envea/read_ref_v2.py
Normal file
@@ -0,0 +1,224 @@
|
|||||||
|
"""
|
||||||
|
_____ _ ___ _______ _
|
||||||
|
| ____| \ | \ \ / / ____| / \
|
||||||
|
| _| | \| |\ \ / /| _| / _ \
|
||||||
|
| |___| |\ | \ V / | |___ / ___ \
|
||||||
|
|_____|_| \_| \_/ |_____/_/ \_\
|
||||||
|
|
||||||
|
Gather data from envea Sensors and store them to the SQlite table
|
||||||
|
Use the RTC time for the timestamp
|
||||||
|
|
||||||
|
This script is run by a service nebuleair-envea-data.service
|
||||||
|
|
||||||
|
/usr/bin/python3 /var/www/nebuleair_pro_4g/envea/read_ref_v2.py ttyAMA4
|
||||||
|
|
||||||
|
ATTENTION --> read_ref.py fonctionne mieux
|
||||||
|
"""
|
||||||
|
|
||||||
|
import serial
|
||||||
|
import time
|
||||||
|
import sys
|
||||||
|
|
||||||
|
parameter = sys.argv[1:] # Exclude the script name
|
||||||
|
port='/dev/'+parameter[0]
|
||||||
|
|
||||||
|
# Mapping dictionaries
|
||||||
|
COMPOUND_MAP = {
|
||||||
|
'A': 'Ammonia',
|
||||||
|
'B': 'Benzene',
|
||||||
|
'C': 'Carbon Monoxide',
|
||||||
|
'D': 'Hydrogen Sulfide',
|
||||||
|
'E': 'Ethylene',
|
||||||
|
'F': 'Formaldehyde',
|
||||||
|
'G': 'Gasoline',
|
||||||
|
'H': 'Hydrogen',
|
||||||
|
'I': 'Isobutylene',
|
||||||
|
'J': 'Jet Fuel',
|
||||||
|
'K': 'Kerosene',
|
||||||
|
'L': 'Liquified Petroleum Gas',
|
||||||
|
'M': 'Methane',
|
||||||
|
'N': 'Nitrogen Dioxide',
|
||||||
|
'O': 'Ozone',
|
||||||
|
'P': 'Propane',
|
||||||
|
'Q': 'Quinoline',
|
||||||
|
'R': 'Refrigerant',
|
||||||
|
'S': 'Sulfur Dioxide',
|
||||||
|
'T': 'Toluene',
|
||||||
|
'U': 'Uranium Hexafluoride',
|
||||||
|
'V': 'Vinyl Chloride',
|
||||||
|
'W': 'Water Vapor',
|
||||||
|
'X': 'Xylene',
|
||||||
|
'Y': 'Yttrium',
|
||||||
|
'Z': 'Zinc'
|
||||||
|
}
|
||||||
|
|
||||||
|
RANGE_MAP = {
|
||||||
|
'A': '0-10 ppm',
|
||||||
|
'B': '0-250 ppb',
|
||||||
|
'C': '0-1000 ppm',
|
||||||
|
'D': '0-50 ppm',
|
||||||
|
'E': '0-100 ppm',
|
||||||
|
'F': '0-5 ppm',
|
||||||
|
'G': '0-500 ppm',
|
||||||
|
'H': '0-2000 ppm',
|
||||||
|
'I': '0-200 ppm',
|
||||||
|
'J': '0-300 ppm',
|
||||||
|
'K': '0-400 ppm',
|
||||||
|
'L': '0-600 ppm',
|
||||||
|
'M': '0-800 ppm',
|
||||||
|
'N': '0-20 ppm',
|
||||||
|
'O': '0-1 ppm',
|
||||||
|
'P': '0-5000 ppm',
|
||||||
|
'Q': '0-150 ppm',
|
||||||
|
'R': '0-750 ppm',
|
||||||
|
'S': '0-25 ppm',
|
||||||
|
'T': '0-350 ppm',
|
||||||
|
'U': '0-450 ppm',
|
||||||
|
'V': '0-550 ppm',
|
||||||
|
'W': '0-650 ppm',
|
||||||
|
'X': '0-850 ppm',
|
||||||
|
'Y': '0-950 ppm',
|
||||||
|
'Z': '0-1500 ppm'
|
||||||
|
}
|
||||||
|
|
||||||
|
INTERFACE_MAP = {
|
||||||
|
0x01: 'USB',
|
||||||
|
0x02: 'UART',
|
||||||
|
0x03: 'I2C',
|
||||||
|
0x04: 'SPI'
|
||||||
|
}
|
||||||
|
|
||||||
|
def parse_cairsens_data(hex_data):
|
||||||
|
"""
|
||||||
|
Parse the extracted hex data from CAIRSENS sensor.
|
||||||
|
|
||||||
|
:param hex_data: Hexadecimal string of extracted data (indices 11-28)
|
||||||
|
:return: Dictionary with parsed information
|
||||||
|
"""
|
||||||
|
# Convert hex to bytes for easier processing
|
||||||
|
raw_bytes = bytes.fromhex(hex_data)
|
||||||
|
|
||||||
|
# Initialize result dictionary
|
||||||
|
result = {
|
||||||
|
'device_type': 'Unknown',
|
||||||
|
'compound': 'Unknown',
|
||||||
|
'range': 'Unknown',
|
||||||
|
'interface': 'Unknown',
|
||||||
|
'raw_data': hex_data
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(raw_bytes) >= 4: # Ensure we have at least 4 bytes
|
||||||
|
# First byte: Device type check
|
||||||
|
first_char = chr(raw_bytes[0]) if 0x20 <= raw_bytes[0] <= 0x7E else '?'
|
||||||
|
if first_char == 'C':
|
||||||
|
result['device_type'] = 'CAIRCLIP'
|
||||||
|
else:
|
||||||
|
result['device_type'] = f'Unknown ({first_char})'
|
||||||
|
|
||||||
|
# Second byte: Compound mapping
|
||||||
|
second_char = chr(raw_bytes[1]) if 0x20 <= raw_bytes[1] <= 0x7E else '?'
|
||||||
|
result['compound'] = COMPOUND_MAP.get(second_char, f'Unknown ({second_char})')
|
||||||
|
|
||||||
|
# Third byte: Range mapping
|
||||||
|
third_char = chr(raw_bytes[2]) if 0x20 <= raw_bytes[2] <= 0x7E else '?'
|
||||||
|
result['range'] = RANGE_MAP.get(third_char, f'Unknown ({third_char})')
|
||||||
|
|
||||||
|
# Fourth byte: Interface (raw byte value)
|
||||||
|
interface_byte = raw_bytes[3]
|
||||||
|
result['interface'] = INTERFACE_MAP.get(interface_byte, f'Unknown (0x{interface_byte:02X})')
|
||||||
|
result['interface_raw'] = f'0x{interface_byte:02X}'
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
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_v2.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.readline()
|
||||||
|
print(f"Données reçues brutes : {data}")
|
||||||
|
|
||||||
|
# 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 à 28 (indices 22 à 56 en hex string)
|
||||||
|
extracted_hex = hex_data[22:56] # Each byte is 2 hex chars, so 11*2=22 to 28*2=56
|
||||||
|
print(f"Valeurs hexadécimales extraites (11 à 28) : {extracted_hex}")
|
||||||
|
|
||||||
|
# Parse the extracted data
|
||||||
|
parsed_data = parse_cairsens_data(extracted_hex)
|
||||||
|
|
||||||
|
# Display parsed information
|
||||||
|
print("\n=== CAIRSENS SENSOR INFORMATION ===")
|
||||||
|
print(f"Device Type: {parsed_data['device_type']}")
|
||||||
|
print(f"Compound: {parsed_data['compound']}")
|
||||||
|
print(f"Range: {parsed_data['range']}")
|
||||||
|
print(f"Interface: {parsed_data['interface']} ({parsed_data.get('interface_raw', 'N/A')})")
|
||||||
|
print(f"Raw Data: {parsed_data['raw_data']}")
|
||||||
|
print("=====================================")
|
||||||
|
|
||||||
|
# Convertir en ASCII et en valeurs numériques (pour debug)
|
||||||
|
if extracted_hex:
|
||||||
|
raw_bytes = bytes.fromhex(extracted_hex)
|
||||||
|
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 = [b for b in raw_bytes]
|
||||||
|
print(f"Valeurs numériques : {numeric_values}")
|
||||||
|
|
||||||
|
# Fermer la connexion
|
||||||
|
ser.close()
|
||||||
|
print("Connexion fermée.")
|
||||||
|
|
||||||
|
return parsed_data
|
||||||
|
|
||||||
|
except serial.SerialException as e:
|
||||||
|
print(f"Erreur de connexion série : {e}")
|
||||||
|
return None
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Erreur générale : {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"\nRésultat final : {data}")
|
||||||
@@ -44,9 +44,9 @@ def read_cairsens(port, baudrate=9600, parity=serial.PARITY_NONE, stopbits=seria
|
|||||||
|
|
||||||
|
|
||||||
# Lire les données reçues
|
# 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.read_until(b'\n') # Lire jusqu'à la fin de ligne ou un autre délimiteur
|
||||||
data = ser.readline()
|
data = ser.readline()
|
||||||
#print(f"Données reçues brutes : {data}")
|
print(f"Données reçues brutes : {data}")
|
||||||
#print(f"Données reçues (utf-8) : {data.decode('utf-8').strip()}")
|
#print(f"Données reçues (utf-8) : {data.decode('utf-8').strip()}")
|
||||||
|
|
||||||
# Extraire le 20ème octet
|
# Extraire le 20ème octet
|
||||||
|
|||||||
213
envea/read_value_v2.py
Executable file
213
envea/read_value_v2.py
Executable file
@@ -0,0 +1,213 @@
|
|||||||
|
"""
|
||||||
|
_____ _ ___ _______ _
|
||||||
|
| ____| \ | \ \ / / ____| / \
|
||||||
|
| _| | \| |\ \ / /| _| / _ \
|
||||||
|
| |___| |\ | \ V / | |___ / ___ \
|
||||||
|
|_____|_| \_| \_/ |_____/_/ \_\
|
||||||
|
|
||||||
|
Gather data from envea Sensors and store them to the SQlite table
|
||||||
|
Use the RTC time for the timestamp
|
||||||
|
|
||||||
|
This script is run by a service nebuleair-envea-data.service
|
||||||
|
|
||||||
|
/usr/bin/python3 /var/www/nebuleair_pro_4g/envea/read_value_v2.py -d
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import serial
|
||||||
|
import time
|
||||||
|
import traceback
|
||||||
|
import sqlite3
|
||||||
|
from datetime import datetime
|
||||||
|
import sys
|
||||||
|
|
||||||
|
# Set DEBUG to True to enable debug prints, False to disable
|
||||||
|
DEBUG = False # Change this to False to disable debug output
|
||||||
|
|
||||||
|
# You can also control debug via command line argument
|
||||||
|
if len(sys.argv) > 1 and sys.argv[1] in ['--debug', '-d']:
|
||||||
|
DEBUG = True
|
||||||
|
elif len(sys.argv) > 1 and sys.argv[1] in ['--quiet', '-q']:
|
||||||
|
DEBUG = False
|
||||||
|
|
||||||
|
def debug_print(message):
|
||||||
|
"""Print debug messages only if DEBUG is True"""
|
||||||
|
if DEBUG:
|
||||||
|
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
print(f"[{timestamp}] {message}")
|
||||||
|
|
||||||
|
debug_print("=== ENVEA Sensor Reader Started ===")
|
||||||
|
|
||||||
|
# Connect to the SQLite database
|
||||||
|
try:
|
||||||
|
conn = sqlite3.connect("/var/www/nebuleair_pro_4g/sqlite/sensors.db")
|
||||||
|
cursor = conn.cursor()
|
||||||
|
except Exception as e:
|
||||||
|
debug_print(f"✗ Failed to connect to database: {e}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# GET RTC TIME from SQlite
|
||||||
|
try:
|
||||||
|
cursor.execute("SELECT * FROM timestamp_table LIMIT 1")
|
||||||
|
row = cursor.fetchone() # Get the first (and only) row
|
||||||
|
rtc_time_str = row[1] # '2025-02-07 12:30:45'
|
||||||
|
except Exception as e:
|
||||||
|
debug_print(f"✗ Failed to get RTC time: {e}")
|
||||||
|
rtc_time_str = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
debug_print(f" Using system time instead: {rtc_time_str}")
|
||||||
|
|
||||||
|
# Fetch connected ENVEA sondes from SQLite config table
|
||||||
|
try:
|
||||||
|
cursor.execute("SELECT port, name, coefficient FROM envea_sondes_table WHERE connected = 1")
|
||||||
|
connected_envea_sondes = cursor.fetchall() # List of tuples (port, name, coefficient)
|
||||||
|
debug_print(f"✓ Found {len(connected_envea_sondes)} connected ENVEA sensors")
|
||||||
|
for port, name, coefficient in connected_envea_sondes:
|
||||||
|
debug_print(f" - {name}: port={port}, coefficient={coefficient}")
|
||||||
|
except Exception as e:
|
||||||
|
debug_print(f"✗ Failed to fetch connected sensors: {e}")
|
||||||
|
connected_envea_sondes = []
|
||||||
|
|
||||||
|
serial_connections = {}
|
||||||
|
|
||||||
|
if connected_envea_sondes:
|
||||||
|
debug_print("\n--- Opening Serial Connections ---")
|
||||||
|
for port, name, coefficient in connected_envea_sondes:
|
||||||
|
try:
|
||||||
|
serial_connections[name] = serial.Serial(
|
||||||
|
port=f'/dev/{port}',
|
||||||
|
baudrate=9600,
|
||||||
|
parity=serial.PARITY_NONE,
|
||||||
|
stopbits=serial.STOPBITS_ONE,
|
||||||
|
bytesize=serial.EIGHTBITS,
|
||||||
|
timeout=1
|
||||||
|
)
|
||||||
|
debug_print(f"✓ Opened serial port for {name} on /dev/{port}")
|
||||||
|
except serial.SerialException as e:
|
||||||
|
debug_print(f"✗ Error opening serial port for {name}: {e}")
|
||||||
|
else:
|
||||||
|
debug_print("! No connected ENVEA sensors found in configuration")
|
||||||
|
|
||||||
|
# Initialize sensor data variables
|
||||||
|
global data_h2s, data_no2, data_o3, data_co, data_nh3, data_so2
|
||||||
|
data_h2s = 0
|
||||||
|
data_no2 = 0
|
||||||
|
data_o3 = 0
|
||||||
|
data_co = 0
|
||||||
|
data_nh3 = 0
|
||||||
|
data_so2 = 0
|
||||||
|
|
||||||
|
try:
|
||||||
|
if connected_envea_sondes:
|
||||||
|
debug_print("\n--- Reading Sensor Data ---")
|
||||||
|
for port, name, coefficient in connected_envea_sondes:
|
||||||
|
if name in serial_connections:
|
||||||
|
serial_connection = serial_connections[name]
|
||||||
|
try:
|
||||||
|
debug_print(f"Reading from {name}...")
|
||||||
|
|
||||||
|
calculated_value = None
|
||||||
|
max_retries = 3
|
||||||
|
|
||||||
|
for attempt in range(max_retries):
|
||||||
|
# Flush input buffer to clear any stale data
|
||||||
|
serial_connection.reset_input_buffer()
|
||||||
|
|
||||||
|
# Send command to sensor
|
||||||
|
command = b"\xFF\x02\x13\x30\x01\x02\x03\x04\x05\x06\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\x12\xAF\x88\x03"
|
||||||
|
serial_connection.write(command)
|
||||||
|
if attempt == 0:
|
||||||
|
debug_print(f" → Sent command: {command.hex()}")
|
||||||
|
|
||||||
|
# Wait for sensor response
|
||||||
|
time.sleep(0.8)
|
||||||
|
|
||||||
|
# Read all available data from buffer
|
||||||
|
bytes_available = serial_connection.in_waiting
|
||||||
|
debug_print(f" ← Attempt {attempt + 1}: {bytes_available} bytes available")
|
||||||
|
|
||||||
|
if bytes_available > 0:
|
||||||
|
data_envea = serial_connection.read(bytes_available)
|
||||||
|
else:
|
||||||
|
data_envea = serial_connection.read(32)
|
||||||
|
|
||||||
|
if len(data_envea) > 0:
|
||||||
|
debug_print(f" ← Received {len(data_envea)} bytes: {data_envea.hex()}")
|
||||||
|
|
||||||
|
# Find frame start (0xFF 0x02) in received data
|
||||||
|
frame_start = -1
|
||||||
|
for i in range(len(data_envea) - 1):
|
||||||
|
if data_envea[i] == 0xFF and data_envea[i + 1] == 0x02:
|
||||||
|
frame_start = i
|
||||||
|
break
|
||||||
|
|
||||||
|
if frame_start >= 0:
|
||||||
|
frame_data = data_envea[frame_start:]
|
||||||
|
if len(frame_data) >= 20:
|
||||||
|
byte_20 = frame_data[19]
|
||||||
|
calculated_value = byte_20 * coefficient
|
||||||
|
debug_print(f" → Found valid frame at position {frame_start}")
|
||||||
|
debug_print(f" → Byte 20 = {byte_20} × {coefficient} = {calculated_value}")
|
||||||
|
break # Success, exit retry loop
|
||||||
|
|
||||||
|
debug_print(f" ✗ Attempt {attempt + 1} failed, {'retrying...' if attempt < max_retries - 1 else 'giving up'}")
|
||||||
|
time.sleep(0.2)
|
||||||
|
|
||||||
|
if calculated_value is not None:
|
||||||
|
if name == "h2s":
|
||||||
|
data_h2s = calculated_value
|
||||||
|
elif name == "no2":
|
||||||
|
data_no2 = calculated_value
|
||||||
|
elif name == "o3":
|
||||||
|
data_o3 = calculated_value
|
||||||
|
elif name == "co":
|
||||||
|
data_co = calculated_value
|
||||||
|
elif name == "nh3":
|
||||||
|
data_nh3 = calculated_value
|
||||||
|
elif name == "so2":
|
||||||
|
data_so2 = calculated_value
|
||||||
|
debug_print(f" ✓ {name.upper()} = {calculated_value}")
|
||||||
|
else:
|
||||||
|
debug_print(f" ✗ Failed to read {name} after {max_retries} attempts")
|
||||||
|
|
||||||
|
except serial.SerialException as e:
|
||||||
|
debug_print(f"✗ Error communicating with {name}: {e}")
|
||||||
|
else:
|
||||||
|
debug_print(f"! No serial connection available for {name}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
debug_print(f"\n✗ An error occurred while gathering data: {e}")
|
||||||
|
traceback.print_exc()
|
||||||
|
|
||||||
|
# Display all collected data
|
||||||
|
debug_print(f"\n--- Collected Sensor Data ---")
|
||||||
|
debug_print(f"H2S: {data_h2s} ppb")
|
||||||
|
debug_print(f"NO2: {data_no2} ppb")
|
||||||
|
debug_print(f"O3: {data_o3} ppb")
|
||||||
|
debug_print(f"CO: {data_co} ppb")
|
||||||
|
debug_print(f"NH3: {data_nh3} ppb")
|
||||||
|
debug_print(f"SO2: {data_so2} ppb")
|
||||||
|
|
||||||
|
# Save to sqlite database
|
||||||
|
try:
|
||||||
|
cursor.execute('''
|
||||||
|
INSERT INTO data_envea (timestamp, h2s, no2, o3, co, nh3, so2) VALUES (?,?,?,?,?,?,?)'''
|
||||||
|
, (rtc_time_str, data_h2s, data_no2, data_o3, data_co, data_nh3, data_so2))
|
||||||
|
|
||||||
|
# Commit and close the connection
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
debug_print(f"✗ Database error: {e}")
|
||||||
|
traceback.print_exc()
|
||||||
|
|
||||||
|
# Close serial connections
|
||||||
|
if serial_connections:
|
||||||
|
for name, connection in serial_connections.items():
|
||||||
|
try:
|
||||||
|
connection.close()
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
conn.close()
|
||||||
|
debug_print("\n=== ENVEA Sensor Reader Finished ===\n")
|
||||||
55
forget_wifi.sh
Normal file
55
forget_wifi.sh
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
echo "-------"
|
||||||
|
echo "Start forget WiFi shell script at $(date)"
|
||||||
|
|
||||||
|
# Get deviceName from database for hotspot SSID
|
||||||
|
DEVICE_NAME=$(sqlite3 /var/www/nebuleair_pro_4g/sqlite/sensors.db "SELECT value FROM config_table WHERE key='deviceName'")
|
||||||
|
echo "Device Name: $DEVICE_NAME"
|
||||||
|
|
||||||
|
# Get current active WiFi connection name
|
||||||
|
ACTIVE_WIFI=$(nmcli -t -f NAME,TYPE,DEVICE connection show --active | grep ':802-11-wireless:wlan0' | cut -d: -f1)
|
||||||
|
|
||||||
|
if [ -z "$ACTIVE_WIFI" ]; then
|
||||||
|
echo "No active WiFi connection found on wlan0"
|
||||||
|
echo "End forget WiFi shell script"
|
||||||
|
echo "-------"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Forgetting WiFi connection: $ACTIVE_WIFI"
|
||||||
|
|
||||||
|
# Disconnect wlan0 first to prevent NetworkManager from auto-reconnecting
|
||||||
|
sudo nmcli device disconnect wlan0
|
||||||
|
echo "wlan0 disconnected"
|
||||||
|
|
||||||
|
# Delete (forget) the saved connection
|
||||||
|
sudo nmcli connection delete "$ACTIVE_WIFI"
|
||||||
|
|
||||||
|
if [ $? -eq 0 ]; then
|
||||||
|
echo "Connection '$ACTIVE_WIFI' deleted successfully"
|
||||||
|
else
|
||||||
|
echo "Failed to delete connection '$ACTIVE_WIFI'"
|
||||||
|
fi
|
||||||
|
|
||||||
|
sleep 5
|
||||||
|
|
||||||
|
# Scan WiFi networks BEFORE starting hotspot (scan impossible once hotspot is active)
|
||||||
|
OUTPUT_FILE="/var/www/nebuleair_pro_4g/wifi_list.csv"
|
||||||
|
echo "Scanning WiFi networks (waiting for wlan0 to be ready)..."
|
||||||
|
nmcli device wifi rescan ifname wlan0 2>/dev/null
|
||||||
|
sleep 3
|
||||||
|
nmcli -f SSID,SIGNAL,SECURITY device wifi list ifname wlan0 | awk 'BEGIN { OFS=","; print "SSID,SIGNAL,SECURITY" } NR>1 { print $1,$2,$3 }' > "$OUTPUT_FILE"
|
||||||
|
echo "WiFi scan saved to $OUTPUT_FILE"
|
||||||
|
cat "$OUTPUT_FILE"
|
||||||
|
|
||||||
|
# Start hotspot
|
||||||
|
echo "Starting hotspot with SSID: $DEVICE_NAME"
|
||||||
|
sudo nmcli device wifi hotspot ifname wlan0 ssid "$DEVICE_NAME" password nebuleaircfg
|
||||||
|
|
||||||
|
# Update SQLite to reflect hotspot mode
|
||||||
|
sqlite3 /var/www/nebuleair_pro_4g/sqlite/sensors.db "UPDATE config_table SET value='hotspot' WHERE key='WIFI_status'"
|
||||||
|
echo "Updated database: WIFI_status = hotspot"
|
||||||
|
echo "Hotspot started with SSID: $DEVICE_NAME"
|
||||||
|
|
||||||
|
echo "End forget WiFi shell script"
|
||||||
|
echo "-------"
|
||||||
3
html/.htaccess
Normal file
3
html/.htaccess
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
php_value upload_max_filesize 50M
|
||||||
|
php_value post_max_size 55M
|
||||||
|
php_value max_execution_time 300
|
||||||
1882
html/admin.html
1882
html/admin.html
File diff suppressed because it is too large
Load Diff
66
html/assets/data/operators.json
Normal file
66
html/assets/data/operators.json
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
{
|
||||||
|
"operators": {
|
||||||
|
"20801": { "name": "Orange", "country": "France" },
|
||||||
|
"20802": { "name": "Orange", "country": "France" },
|
||||||
|
"20810": { "name": "SFR", "country": "France" },
|
||||||
|
"20811": { "name": "SFR", "country": "France" },
|
||||||
|
"20813": { "name": "SFR", "country": "France" },
|
||||||
|
"20815": { "name": "Free Mobile", "country": "France" },
|
||||||
|
"20816": { "name": "Free Mobile", "country": "France" },
|
||||||
|
"20820": { "name": "Bouygues Telecom", "country": "France" },
|
||||||
|
"20821": { "name": "Bouygues Telecom", "country": "France" },
|
||||||
|
"20826": { "name": "NRJ Mobile", "country": "France" },
|
||||||
|
"20888": { "name": "Bouygues Telecom", "country": "France" },
|
||||||
|
"22201": { "name": "TIM", "country": "Italy" },
|
||||||
|
"22210": { "name": "Vodafone", "country": "Italy" },
|
||||||
|
"22288": { "name": "WIND", "country": "Italy" },
|
||||||
|
"22299": { "name": "3 Italia", "country": "Italy" },
|
||||||
|
"23410": { "name": "O2", "country": "UK" },
|
||||||
|
"23415": { "name": "Vodafone", "country": "UK" },
|
||||||
|
"23420": { "name": "3", "country": "UK" },
|
||||||
|
"23430": { "name": "EE", "country": "UK" },
|
||||||
|
"23433": { "name": "EE", "country": "UK" },
|
||||||
|
"26201": { "name": "Telekom", "country": "Germany" },
|
||||||
|
"26202": { "name": "Vodafone", "country": "Germany" },
|
||||||
|
"26203": { "name": "O2", "country": "Germany" },
|
||||||
|
"26207": { "name": "O2", "country": "Germany" },
|
||||||
|
"21401": { "name": "Vodafone", "country": "Spain" },
|
||||||
|
"21403": { "name": "Orange", "country": "Spain" },
|
||||||
|
"21404": { "name": "Yoigo", "country": "Spain" },
|
||||||
|
"21407": { "name": "Movistar", "country": "Spain" },
|
||||||
|
"22801": { "name": "Swisscom", "country": "Switzerland" },
|
||||||
|
"22802": { "name": "Sunrise", "country": "Switzerland" },
|
||||||
|
"22803": { "name": "Salt", "country": "Switzerland" },
|
||||||
|
"20601": { "name": "Proximus", "country": "Belgium" },
|
||||||
|
"20610": { "name": "Orange", "country": "Belgium" },
|
||||||
|
"20620": { "name": "Base", "country": "Belgium" },
|
||||||
|
"20404": { "name": "Vodafone", "country": "Netherlands" },
|
||||||
|
"20408": { "name": "KPN", "country": "Netherlands" },
|
||||||
|
"20412": { "name": "T-Mobile", "country": "Netherlands" },
|
||||||
|
"20416": { "name": "T-Mobile", "country": "Netherlands" },
|
||||||
|
"26801": { "name": "Vodafone", "country": "Portugal" },
|
||||||
|
"26803": { "name": "NOS", "country": "Portugal" },
|
||||||
|
"26806": { "name": "MEO", "country": "Portugal" },
|
||||||
|
"29340": { "name": "SI Mobil", "country": "Slovenia" },
|
||||||
|
"29341": { "name": "Mobitel", "country": "Slovenia" }
|
||||||
|
},
|
||||||
|
"modes": {
|
||||||
|
"0": "Automatic",
|
||||||
|
"1": "Manual",
|
||||||
|
"2": "Deregistered",
|
||||||
|
"3": "Format only",
|
||||||
|
"4": "Manual/Automatic"
|
||||||
|
},
|
||||||
|
"accessTechnology": {
|
||||||
|
"0": "GSM",
|
||||||
|
"1": "GSM Compact",
|
||||||
|
"2": "UTRAN (3G)",
|
||||||
|
"3": "GSM/GPRS with EDGE",
|
||||||
|
"4": "UTRAN with HSDPA",
|
||||||
|
"5": "UTRAN with HSUPA",
|
||||||
|
"6": "UTRAN with HSDPA/HSUPA",
|
||||||
|
"7": "LTE (4G)",
|
||||||
|
"8": "EC-GSM-IoT",
|
||||||
|
"9": "LTE Cat-M / NB-IoT"
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
html/assets/img/logoModuleAir.png
Normal file
BIN
html/assets/img/logoModuleAir.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 233 KiB |
20
html/assets/js/chart.js
Executable file
20
html/assets/js/chart.js
Executable file
File diff suppressed because one or more lines are too long
129
html/assets/js/i18n.js
Normal file
129
html/assets/js/i18n.js
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
/**
|
||||||
|
* NebuleAir i18n - Lightweight internationalization system
|
||||||
|
* Works offline with local JSON translation files
|
||||||
|
* Stores language preference in SQLite database
|
||||||
|
*/
|
||||||
|
|
||||||
|
const i18n = {
|
||||||
|
currentLang: 'fr', // Default language
|
||||||
|
translations: {},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize i18n system
|
||||||
|
* Loads language preference from server and applies translations
|
||||||
|
*/
|
||||||
|
async init() {
|
||||||
|
try {
|
||||||
|
// Load language preference from server (SQLite database)
|
||||||
|
const response = await fetch('launcher.php?type=get_language');
|
||||||
|
const data = await response.json();
|
||||||
|
this.currentLang = data.language || 'fr';
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Could not load language preference, using default (fr):', error);
|
||||||
|
this.currentLang = 'fr';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load translations and apply
|
||||||
|
await this.loadTranslations(this.currentLang);
|
||||||
|
this.applyTranslations();
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load translation file for specified language
|
||||||
|
* @param {string} lang - Language code (fr, en)
|
||||||
|
*/
|
||||||
|
async loadTranslations(lang) {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`lang/${lang}.json`);
|
||||||
|
this.translations = await response.json();
|
||||||
|
console.log(`Translations loaded for: ${lang}`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Failed to load translations for ${lang}:`, error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply translations to all elements with data-i18n attribute
|
||||||
|
*/
|
||||||
|
applyTranslations() {
|
||||||
|
document.querySelectorAll('[data-i18n]').forEach(element => {
|
||||||
|
const key = element.getAttribute('data-i18n');
|
||||||
|
const translation = this.get(key);
|
||||||
|
|
||||||
|
if (translation) {
|
||||||
|
// Handle different element types
|
||||||
|
if (element.tagName === 'INPUT' || element.tagName === 'TEXTAREA') {
|
||||||
|
if (element.type === 'button' || element.type === 'submit') {
|
||||||
|
element.value = translation;
|
||||||
|
} else {
|
||||||
|
element.placeholder = translation;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
element.textContent = translation;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.warn(`Translation not found for key: ${key}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update HTML lang attribute
|
||||||
|
document.documentElement.lang = this.currentLang;
|
||||||
|
|
||||||
|
// Update language switcher dropdown
|
||||||
|
const languageSwitcher = document.getElementById('languageSwitcher');
|
||||||
|
if (languageSwitcher) {
|
||||||
|
languageSwitcher.value = this.currentLang;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get translation by key (supports nested keys with dot notation)
|
||||||
|
* @param {string} key - Translation key (e.g., 'sensors.title')
|
||||||
|
* @returns {string} Translated string or key if not found
|
||||||
|
*/
|
||||||
|
get(key) {
|
||||||
|
const keys = key.split('.');
|
||||||
|
let value = this.translations;
|
||||||
|
|
||||||
|
for (const k of keys) {
|
||||||
|
if (value && typeof value === 'object' && k in value) {
|
||||||
|
value = value[k];
|
||||||
|
} else {
|
||||||
|
return key; // Return key if translation not found
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return value;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Change language and reload translations
|
||||||
|
* @param {string} lang - Language code (fr, en)
|
||||||
|
*/
|
||||||
|
async setLanguage(lang) {
|
||||||
|
if (lang === this.currentLang) return;
|
||||||
|
|
||||||
|
this.currentLang = lang;
|
||||||
|
|
||||||
|
// Save to server (SQLite database)
|
||||||
|
try {
|
||||||
|
await fetch(`launcher.php?type=set_language&language=${lang}`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to save language preference:', error);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reload translations and apply
|
||||||
|
await this.loadTranslations(lang);
|
||||||
|
this.applyTranslations();
|
||||||
|
|
||||||
|
// Emit custom event for other scripts to react to language change
|
||||||
|
document.dispatchEvent(new CustomEvent('languageChanged', { detail: { language: lang } }));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Auto-initialize when DOM is ready
|
||||||
|
if (document.readyState === 'loading') {
|
||||||
|
document.addEventListener('DOMContentLoaded', () => i18n.init());
|
||||||
|
} else {
|
||||||
|
i18n.init();
|
||||||
|
}
|
||||||
969
html/assets/js/selftest.js
Normal file
969
html/assets/js/selftest.js
Normal file
@@ -0,0 +1,969 @@
|
|||||||
|
// ============================================
|
||||||
|
// SELF TEST FUNCTIONS (shared across pages)
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
// Cache for operators data
|
||||||
|
let operatorsDataSelfTest = null;
|
||||||
|
|
||||||
|
function loadOperatorsDataSelfTest() {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
if (operatorsDataSelfTest) {
|
||||||
|
resolve(operatorsDataSelfTest);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$.ajax({
|
||||||
|
url: 'assets/data/operators.json',
|
||||||
|
dataType: 'json',
|
||||||
|
method: 'GET',
|
||||||
|
success: function(data) {
|
||||||
|
operatorsDataSelfTest = data;
|
||||||
|
resolve(data);
|
||||||
|
},
|
||||||
|
error: function(xhr, status, error) {
|
||||||
|
console.error('Failed to load operators data:', error);
|
||||||
|
reject(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Global object to store test results for report
|
||||||
|
let selfTestReport = {
|
||||||
|
timestamp: '',
|
||||||
|
deviceId: '',
|
||||||
|
modemVersion: '',
|
||||||
|
results: {},
|
||||||
|
rawResponses: {}
|
||||||
|
};
|
||||||
|
|
||||||
|
function runSelfTest() {
|
||||||
|
console.log("Starting Self Test...");
|
||||||
|
|
||||||
|
// Reset report
|
||||||
|
selfTestReport = {
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
deviceId: document.querySelector('.sideBar_sensorName')?.textContent || 'Unknown',
|
||||||
|
modemVersion: document.getElementById('modem_version')?.textContent || 'Unknown',
|
||||||
|
results: {},
|
||||||
|
rawResponses: {}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Reset UI
|
||||||
|
resetSelfTestUI();
|
||||||
|
|
||||||
|
// Show modal
|
||||||
|
const modal = new bootstrap.Modal(document.getElementById('selfTestModal'));
|
||||||
|
modal.show();
|
||||||
|
|
||||||
|
// Disable buttons during test
|
||||||
|
document.getElementById('selfTestCloseBtn').disabled = true;
|
||||||
|
document.getElementById('selfTestDoneBtn').disabled = true;
|
||||||
|
document.getElementById('selfTestCopyBtn').disabled = true;
|
||||||
|
document.querySelectorAll('.btn_selfTest').forEach(btn => btn.disabled = true);
|
||||||
|
|
||||||
|
// Start test sequence
|
||||||
|
selfTestSequence();
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetSelfTestUI() {
|
||||||
|
// Reset status
|
||||||
|
document.getElementById('selftest_status').innerHTML = `
|
||||||
|
<div class="d-flex align-items-center text-muted">
|
||||||
|
<div class="spinner-border spinner-border-sm me-2" role="status"></div>
|
||||||
|
<span>Preparing test...</span>
|
||||||
|
</div>`;
|
||||||
|
|
||||||
|
// Reset test items
|
||||||
|
document.getElementById('test_wifi_status').className = 'badge bg-secondary';
|
||||||
|
document.getElementById('test_wifi_status').textContent = 'Pending';
|
||||||
|
document.getElementById('test_wifi_detail').textContent = 'Waiting...';
|
||||||
|
|
||||||
|
document.getElementById('test_modem_status').className = 'badge bg-secondary';
|
||||||
|
document.getElementById('test_modem_status').textContent = 'Pending';
|
||||||
|
document.getElementById('test_modem_detail').textContent = 'Waiting...';
|
||||||
|
|
||||||
|
document.getElementById('test_sim_status').className = 'badge bg-secondary';
|
||||||
|
document.getElementById('test_sim_status').textContent = 'Pending';
|
||||||
|
document.getElementById('test_sim_detail').textContent = 'Waiting...';
|
||||||
|
|
||||||
|
document.getElementById('test_signal_status').className = 'badge bg-secondary';
|
||||||
|
document.getElementById('test_signal_status').textContent = 'Pending';
|
||||||
|
document.getElementById('test_signal_detail').textContent = 'Waiting...';
|
||||||
|
|
||||||
|
document.getElementById('test_network_status').className = 'badge bg-secondary';
|
||||||
|
document.getElementById('test_network_status').textContent = 'Pending';
|
||||||
|
document.getElementById('test_network_detail').textContent = 'Waiting...';
|
||||||
|
|
||||||
|
// Reset sensor tests
|
||||||
|
document.getElementById('sensor_tests_container').innerHTML = '';
|
||||||
|
document.getElementById('comm_tests_separator').style.display = 'none';
|
||||||
|
|
||||||
|
// Reset logs
|
||||||
|
document.getElementById('selftest_logs').innerHTML = '';
|
||||||
|
|
||||||
|
// Reset summary
|
||||||
|
document.getElementById('selftest_summary').innerHTML = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function addSelfTestLog(message, isRaw = false) {
|
||||||
|
const logsEl = document.getElementById('selftest_logs');
|
||||||
|
const timestamp = new Date().toLocaleTimeString();
|
||||||
|
|
||||||
|
if (isRaw) {
|
||||||
|
// Raw AT response - format nicely
|
||||||
|
logsEl.textContent += `[${timestamp}] >>> RAW RESPONSE:\n${message}\n<<<\n`;
|
||||||
|
} else {
|
||||||
|
logsEl.textContent += `[${timestamp}] ${message}\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto-scroll to bottom
|
||||||
|
logsEl.parentElement.scrollTop = logsEl.parentElement.scrollHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateTestStatus(testId, status, detail, badge) {
|
||||||
|
document.getElementById(`test_${testId}_status`).className = `badge ${badge}`;
|
||||||
|
document.getElementById(`test_${testId}_status`).textContent = status;
|
||||||
|
document.getElementById(`test_${testId}_detail`).textContent = detail;
|
||||||
|
|
||||||
|
// Store result in report
|
||||||
|
selfTestReport.results[testId] = {
|
||||||
|
status: status,
|
||||||
|
detail: detail
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function setConfigMode(enabled) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
addSelfTestLog(`Setting modem_config_mode to ${enabled}...`);
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
url: `launcher.php?type=update_config_sqlite¶m=modem_config_mode&value=${enabled}`,
|
||||||
|
dataType: 'json',
|
||||||
|
method: 'GET',
|
||||||
|
cache: false,
|
||||||
|
success: function(response) {
|
||||||
|
if (response.success) {
|
||||||
|
addSelfTestLog(`modem_config_mode set to ${enabled}`);
|
||||||
|
// Update checkbox state if it exists on the page
|
||||||
|
const checkbox = document.getElementById('check_modem_configMode');
|
||||||
|
if (checkbox) checkbox.checked = enabled;
|
||||||
|
resolve(true);
|
||||||
|
} else {
|
||||||
|
addSelfTestLog(`Failed to set modem_config_mode: ${response.error || 'Unknown error'}`);
|
||||||
|
reject(new Error(response.error || 'Failed to set config mode'));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: function(xhr, status, error) {
|
||||||
|
addSelfTestLog(`AJAX error setting config mode: ${error}`);
|
||||||
|
reject(new Error(error));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function sendATCommand(command, timeout) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
addSelfTestLog(`Sending AT command: ${command} (timeout: ${timeout}s)`);
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
url: `launcher.php?type=sara&port=ttyAMA2&command=${encodeURIComponent(command)}&timeout=${timeout}`,
|
||||||
|
dataType: 'text',
|
||||||
|
method: 'GET',
|
||||||
|
success: function(response) {
|
||||||
|
// Store raw response in report
|
||||||
|
selfTestReport.rawResponses[command] = response;
|
||||||
|
|
||||||
|
// Log raw response
|
||||||
|
addSelfTestLog(response.trim(), true);
|
||||||
|
|
||||||
|
resolve(response);
|
||||||
|
},
|
||||||
|
error: function(xhr, status, error) {
|
||||||
|
addSelfTestLog(`AT command error: ${error}`);
|
||||||
|
selfTestReport.rawResponses[command] = `ERROR: ${error}`;
|
||||||
|
reject(new Error(error));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function delaySelfTest(ms) {
|
||||||
|
return new Promise(resolve => setTimeout(resolve, ms));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function selfTestSequence() {
|
||||||
|
let testsPassed = 0;
|
||||||
|
let testsFailed = 0;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Collect system info at the start
|
||||||
|
document.getElementById('selftest_status').innerHTML = `
|
||||||
|
<div class="d-flex align-items-center text-primary">
|
||||||
|
<div class="spinner-border spinner-border-sm me-2" role="status"></div>
|
||||||
|
<span>Collecting system information...</span>
|
||||||
|
</div>`;
|
||||||
|
|
||||||
|
// Get system info from config
|
||||||
|
try {
|
||||||
|
const configResponse = await new Promise((resolve, reject) => {
|
||||||
|
$.ajax({
|
||||||
|
url: 'launcher.php?type=get_config_sqlite',
|
||||||
|
dataType: 'json',
|
||||||
|
method: 'GET',
|
||||||
|
success: function(data) { resolve(data); },
|
||||||
|
error: function(xhr, status, error) { reject(new Error(error)); }
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Store in report
|
||||||
|
selfTestReport.deviceId = configResponse.deviceID || 'Unknown';
|
||||||
|
selfTestReport.deviceName = configResponse.deviceName || 'Unknown';
|
||||||
|
selfTestReport.modemVersion = configResponse.modem_version || 'Unknown';
|
||||||
|
selfTestReport.latitude = configResponse.latitude_raw || 'N/A';
|
||||||
|
selfTestReport.longitude = configResponse.longitude_raw || 'N/A';
|
||||||
|
selfTestReport.config = configResponse;
|
||||||
|
|
||||||
|
// Get RTC time
|
||||||
|
try {
|
||||||
|
const rtcTime = await new Promise((resolve, reject) => {
|
||||||
|
$.ajax({
|
||||||
|
url: 'launcher.php?type=RTC_time',
|
||||||
|
dataType: 'text',
|
||||||
|
method: 'GET',
|
||||||
|
success: function(data) { resolve(data.trim()); },
|
||||||
|
error: function(xhr, status, error) { resolve('N/A'); }
|
||||||
|
});
|
||||||
|
});
|
||||||
|
selfTestReport.systemTime = rtcTime;
|
||||||
|
} catch (e) {
|
||||||
|
selfTestReport.systemTime = 'N/A';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log system info
|
||||||
|
addSelfTestLog('════════════════════════════════════════════════════════');
|
||||||
|
addSelfTestLog(' NEBULEAIR PRO 4G - SELF TEST');
|
||||||
|
addSelfTestLog('════════════════════════════════════════════════════════');
|
||||||
|
addSelfTestLog(`Device ID: ${selfTestReport.deviceId}`);
|
||||||
|
addSelfTestLog(`Device Name: ${selfTestReport.deviceName}`);
|
||||||
|
addSelfTestLog(`Modem Version: ${selfTestReport.modemVersion}`);
|
||||||
|
addSelfTestLog(`RTC Time: ${selfTestReport.systemTime}`);
|
||||||
|
addSelfTestLog(`Browser Time: ${new Date().toLocaleString()}`);
|
||||||
|
addSelfTestLog(`GPS: ${selfTestReport.latitude}, ${selfTestReport.longitude}`);
|
||||||
|
addSelfTestLog('────────────────────────────────────────────────────────');
|
||||||
|
addSelfTestLog('');
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
addSelfTestLog(`Warning: Could not get system config: ${error.message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
await delaySelfTest(300);
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════
|
||||||
|
// SENSOR TESTS - Test enabled sensors based on config
|
||||||
|
// ═══════════════════════════════════════════════════════
|
||||||
|
const config = selfTestReport.config || {};
|
||||||
|
const sensorTests = [];
|
||||||
|
|
||||||
|
// NPM is always present
|
||||||
|
sensorTests.push({ id: 'npm', name: 'NextPM (Particles)', type: 'npm', port: 'ttyAMA5' });
|
||||||
|
|
||||||
|
// BME280 if enabled
|
||||||
|
if (config.BME280) {
|
||||||
|
sensorTests.push({ id: 'bme280', name: 'BME280 (Temp/Hum)', type: 'BME280' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Noise if enabled
|
||||||
|
if (config.NOISE) {
|
||||||
|
sensorTests.push({ id: 'noise', name: 'Noise Sensor', type: 'noise' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Envea if enabled
|
||||||
|
if (config.envea) {
|
||||||
|
sensorTests.push({ id: 'envea', name: 'Envea (Gas Sensors)', type: 'envea' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// RTC module is always present (DS3231)
|
||||||
|
sensorTests.push({ id: 'rtc', name: 'RTC Module (DS3231)', type: 'rtc' });
|
||||||
|
|
||||||
|
// Create sensor test UI entries dynamically
|
||||||
|
const sensorContainer = document.getElementById('sensor_tests_container');
|
||||||
|
sensorContainer.innerHTML = '';
|
||||||
|
|
||||||
|
sensorTests.forEach(sensor => {
|
||||||
|
sensorContainer.innerHTML += `
|
||||||
|
<div class="list-group-item d-flex justify-content-between align-items-center" id="test_${sensor.id}">
|
||||||
|
<div>
|
||||||
|
<strong>${sensor.name}</strong>
|
||||||
|
<div class="small text-muted" id="test_${sensor.id}_detail">Waiting...</div>
|
||||||
|
</div>
|
||||||
|
<span id="test_${sensor.id}_status" class="badge bg-secondary">Pending</span>
|
||||||
|
</div>`;
|
||||||
|
});
|
||||||
|
|
||||||
|
addSelfTestLog('');
|
||||||
|
addSelfTestLog('────────────────────────────────────────────────────────');
|
||||||
|
addSelfTestLog('SENSOR TESTS');
|
||||||
|
addSelfTestLog('────────────────────────────────────────────────────────');
|
||||||
|
|
||||||
|
// Run each sensor test
|
||||||
|
for (const sensor of sensorTests) {
|
||||||
|
await delaySelfTest(500);
|
||||||
|
|
||||||
|
document.getElementById('selftest_status').innerHTML = `
|
||||||
|
<div class="d-flex align-items-center text-primary">
|
||||||
|
<div class="spinner-border spinner-border-sm me-2" role="status"></div>
|
||||||
|
<span>Testing ${sensor.name}...</span>
|
||||||
|
</div>`;
|
||||||
|
|
||||||
|
updateTestStatus(sensor.id, 'Testing...', 'Reading sensor data...', 'bg-info');
|
||||||
|
addSelfTestLog(`Testing ${sensor.name}...`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (sensor.type === 'npm') {
|
||||||
|
// NPM sensor test (uses get_data_modbus_v3.py --dry-run)
|
||||||
|
const npmResult = await new Promise((resolve, reject) => {
|
||||||
|
$.ajax({
|
||||||
|
url: 'launcher.php?type=npm',
|
||||||
|
dataType: 'json',
|
||||||
|
method: 'GET',
|
||||||
|
timeout: 15000,
|
||||||
|
success: function(data) { resolve(data); },
|
||||||
|
error: function(xhr, status, error) { reject(new Error(error || status)); }
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
selfTestReport.rawResponses['NPM Sensor'] = JSON.stringify(npmResult, null, 2);
|
||||||
|
addSelfTestLog(`NPM response: PM1=${npmResult.PM1}, PM2.5=${npmResult.PM25}, PM10=${npmResult.PM10}, status=${npmResult.npm_status_hex}`);
|
||||||
|
|
||||||
|
// Decode npm_status flags
|
||||||
|
const status = npmResult.npm_status !== undefined ? npmResult.npm_status : 0;
|
||||||
|
|
||||||
|
if (status === 0xFF) {
|
||||||
|
// 0xFF = no response = disconnected
|
||||||
|
updateTestStatus(sensor.id, 'Failed', 'Capteur déconnecté', 'bg-danger');
|
||||||
|
testsFailed++;
|
||||||
|
} else {
|
||||||
|
const statusFlags = {
|
||||||
|
0x01: "Sleep mode",
|
||||||
|
0x02: "Degraded mode",
|
||||||
|
0x04: "Not ready",
|
||||||
|
0x08: "Heater error",
|
||||||
|
0x10: "THP sensor error",
|
||||||
|
0x20: "Fan error",
|
||||||
|
0x40: "Memory error",
|
||||||
|
0x80: "Laser error"
|
||||||
|
};
|
||||||
|
const activeErrors = [];
|
||||||
|
Object.entries(statusFlags).forEach(([mask, label]) => {
|
||||||
|
if (status & mask) activeErrors.push(label);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (activeErrors.length > 0) {
|
||||||
|
updateTestStatus(sensor.id, 'Warning', `Status ${npmResult.npm_status_hex}: ${activeErrors.join(', ')}`, 'bg-warning');
|
||||||
|
testsFailed++;
|
||||||
|
} else if (npmResult.PM1 !== undefined && npmResult.PM25 !== undefined && npmResult.PM10 !== undefined) {
|
||||||
|
updateTestStatus(sensor.id, 'Passed', `PM1: ${npmResult.PM1} | PM2.5: ${npmResult.PM25} | PM10: ${npmResult.PM10} µg/m³`, 'bg-success');
|
||||||
|
testsPassed++;
|
||||||
|
} else {
|
||||||
|
updateTestStatus(sensor.id, 'Warning', 'Incomplete data received', 'bg-warning');
|
||||||
|
testsFailed++;
|
||||||
|
}
|
||||||
|
} // end else (not 0xFF)
|
||||||
|
|
||||||
|
} else if (sensor.type === 'BME280') {
|
||||||
|
// BME280 sensor test
|
||||||
|
const bme280Result = await new Promise((resolve, reject) => {
|
||||||
|
$.ajax({
|
||||||
|
url: 'launcher.php?type=BME280',
|
||||||
|
dataType: 'text',
|
||||||
|
method: 'GET',
|
||||||
|
timeout: 15000,
|
||||||
|
success: function(data) { resolve(data); },
|
||||||
|
error: function(xhr, status, error) { reject(new Error(error || status)); }
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const bmeData = JSON.parse(bme280Result);
|
||||||
|
selfTestReport.rawResponses['BME280 Sensor'] = JSON.stringify(bmeData, null, 2);
|
||||||
|
addSelfTestLog(`BME280 response: temp=${bmeData.temp}, hum=${bmeData.hum}, press=${bmeData.press}`);
|
||||||
|
|
||||||
|
if (bmeData.temp !== undefined && bmeData.hum !== undefined && bmeData.press !== undefined) {
|
||||||
|
updateTestStatus(sensor.id, 'Passed', `${bmeData.temp}°C | ${bmeData.hum}% | ${bmeData.press} hPa`, 'bg-success');
|
||||||
|
testsPassed++;
|
||||||
|
} else {
|
||||||
|
updateTestStatus(sensor.id, 'Warning', 'Incomplete data received', 'bg-warning');
|
||||||
|
testsFailed++;
|
||||||
|
}
|
||||||
|
|
||||||
|
} else if (sensor.type === 'noise') {
|
||||||
|
// NSRT MK4 noise sensor test (returns JSON)
|
||||||
|
const noiseResult = await new Promise((resolve, reject) => {
|
||||||
|
$.ajax({
|
||||||
|
url: 'launcher.php?type=noise',
|
||||||
|
dataType: 'json',
|
||||||
|
method: 'GET',
|
||||||
|
timeout: 15000,
|
||||||
|
success: function(data) { resolve(data); },
|
||||||
|
error: function(xhr, status, error) { reject(new Error(error || status)); }
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
selfTestReport.rawResponses['Noise Sensor'] = JSON.stringify(noiseResult);
|
||||||
|
addSelfTestLog(`Noise response: ${JSON.stringify(noiseResult)}`);
|
||||||
|
|
||||||
|
if (noiseResult.error) {
|
||||||
|
const noiseMsg = noiseResult.disconnected
|
||||||
|
? 'Capteur déconnecté — vérifiez le câblage USB'
|
||||||
|
: noiseResult.error;
|
||||||
|
updateTestStatus(sensor.id, 'Failed', noiseMsg, 'bg-danger');
|
||||||
|
testsFailed++;
|
||||||
|
} else if (noiseResult.LEQ > 0 && noiseResult.dBA > 0) {
|
||||||
|
updateTestStatus(sensor.id, 'Passed', `LEQ: ${noiseResult.LEQ} dB | dB(A): ${noiseResult.dBA}`, 'bg-success');
|
||||||
|
testsPassed++;
|
||||||
|
} else {
|
||||||
|
updateTestStatus(sensor.id, 'Warning', `Unexpected values: LEQ=${noiseResult.LEQ}, dBA=${noiseResult.dBA}`, 'bg-warning');
|
||||||
|
testsFailed++;
|
||||||
|
}
|
||||||
|
|
||||||
|
} else if (sensor.type === 'envea') {
|
||||||
|
// Envea sensor test - use the debug endpoint for all sensors
|
||||||
|
const enveaResult = await new Promise((resolve, reject) => {
|
||||||
|
$.ajax({
|
||||||
|
url: 'launcher.php?type=envea_debug',
|
||||||
|
dataType: 'text',
|
||||||
|
method: 'GET',
|
||||||
|
timeout: 30000,
|
||||||
|
success: function(data) { resolve(data); },
|
||||||
|
error: function(xhr, status, error) { reject(new Error(error || status)); }
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
selfTestReport.rawResponses['Envea Sensors'] = enveaResult;
|
||||||
|
addSelfTestLog(`Envea response: ${enveaResult.trim().substring(0, 200)}`);
|
||||||
|
|
||||||
|
if (enveaResult.trim() !== '' && !enveaResult.toLowerCase().includes('error')) {
|
||||||
|
updateTestStatus(sensor.id, 'Passed', 'Sensors responding', 'bg-success');
|
||||||
|
testsPassed++;
|
||||||
|
} else if (enveaResult.toLowerCase().includes('error')) {
|
||||||
|
updateTestStatus(sensor.id, 'Failed', 'Sensor error detected', 'bg-danger');
|
||||||
|
testsFailed++;
|
||||||
|
} else {
|
||||||
|
updateTestStatus(sensor.id, 'Failed', 'No data received', 'bg-danger');
|
||||||
|
testsFailed++;
|
||||||
|
}
|
||||||
|
|
||||||
|
} else if (sensor.type === 'rtc') {
|
||||||
|
// RTC DS3231 module test
|
||||||
|
const rtcResult = await new Promise((resolve, reject) => {
|
||||||
|
$.ajax({
|
||||||
|
url: 'launcher.php?type=sys_RTC_module_time',
|
||||||
|
dataType: 'json',
|
||||||
|
method: 'GET',
|
||||||
|
timeout: 10000,
|
||||||
|
success: function(data) { resolve(data); },
|
||||||
|
error: function(xhr, status, error) { reject(new Error(error || status)); }
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
selfTestReport.rawResponses['RTC Module'] = JSON.stringify(rtcResult, null, 2);
|
||||||
|
addSelfTestLog(`RTC response: ${JSON.stringify(rtcResult)}`);
|
||||||
|
|
||||||
|
if (rtcResult.rtc_module_time === 'not connected') {
|
||||||
|
updateTestStatus(sensor.id, 'Failed', 'RTC module not connected', 'bg-danger');
|
||||||
|
testsFailed++;
|
||||||
|
} else if (rtcResult.rtc_module_time) {
|
||||||
|
// Compare RTC with browser time (more reliable than system time)
|
||||||
|
const rtcDate = new Date(rtcResult.rtc_module_time + ' UTC');
|
||||||
|
const browserDate = new Date();
|
||||||
|
const timeDiff = Math.abs(Math.round((browserDate - rtcDate) / 1000));
|
||||||
|
|
||||||
|
if (timeDiff <= 60) {
|
||||||
|
updateTestStatus(sensor.id, 'Passed', `${rtcResult.rtc_module_time} (sync OK vs navigateur, ecart: ${timeDiff}s)`, 'bg-success');
|
||||||
|
testsPassed++;
|
||||||
|
} else {
|
||||||
|
const minutes = Math.floor(timeDiff / 60);
|
||||||
|
const label = minutes > 0 ? `${minutes}min ${timeDiff % 60}s` : `${timeDiff}s`;
|
||||||
|
updateTestStatus(sensor.id, 'Warning', `${rtcResult.rtc_module_time} (desync vs navigateur: ${label})`, 'bg-warning');
|
||||||
|
testsFailed++;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
updateTestStatus(sensor.id, 'Warning', 'Unexpected response', 'bg-warning');
|
||||||
|
testsFailed++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
addSelfTestLog(`${sensor.name} test error: ${error.message}`);
|
||||||
|
updateTestStatus(sensor.id, 'Failed', error.message, 'bg-danger');
|
||||||
|
selfTestReport.rawResponses[`${sensor.name}`] = `ERROR: ${error.message}`;
|
||||||
|
testsFailed++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════
|
||||||
|
// COMMUNICATION TESTS - WiFi, Modem, SIM, Signal, Network
|
||||||
|
// ═══════════════════════════════════════════════════════
|
||||||
|
addSelfTestLog('');
|
||||||
|
addSelfTestLog('────────────────────────────────────────────────────────');
|
||||||
|
addSelfTestLog('COMMUNICATION TESTS');
|
||||||
|
addSelfTestLog('────────────────────────────────────────────────────────');
|
||||||
|
|
||||||
|
document.getElementById('comm_tests_separator').style.display = '';
|
||||||
|
|
||||||
|
// Check WiFi / Network status (informational, no pass/fail)
|
||||||
|
document.getElementById('selftest_status').innerHTML = `
|
||||||
|
<div class="d-flex align-items-center text-primary">
|
||||||
|
<div class="spinner-border spinner-border-sm me-2" role="status"></div>
|
||||||
|
<span>Checking network status...</span>
|
||||||
|
</div>`;
|
||||||
|
|
||||||
|
updateTestStatus('wifi', 'Checking...', 'Getting network info...', 'bg-info');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const wifiResponse = await new Promise((resolve, reject) => {
|
||||||
|
$.ajax({
|
||||||
|
url: 'launcher.php?type=wifi_status',
|
||||||
|
dataType: 'json',
|
||||||
|
method: 'GET',
|
||||||
|
success: function(data) {
|
||||||
|
addSelfTestLog(`WiFi status received`);
|
||||||
|
// Store raw response
|
||||||
|
selfTestReport.rawResponses['WiFi Status'] = JSON.stringify(data, null, 2);
|
||||||
|
resolve(data);
|
||||||
|
},
|
||||||
|
error: function(xhr, status, error) {
|
||||||
|
addSelfTestLog(`WiFi status error: ${error}`);
|
||||||
|
selfTestReport.rawResponses['WiFi Status'] = `ERROR: ${error}`;
|
||||||
|
reject(new Error(error));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Log detailed WiFi info
|
||||||
|
addSelfTestLog(`Mode: ${wifiResponse.mode}, SSID: ${wifiResponse.ssid}, IP: ${wifiResponse.ip}, Hostname: ${wifiResponse.hostname}`);
|
||||||
|
|
||||||
|
if (wifiResponse.connected) {
|
||||||
|
let modeLabel = '';
|
||||||
|
let badgeClass = 'bg-info';
|
||||||
|
|
||||||
|
if (wifiResponse.mode === 'hotspot') {
|
||||||
|
modeLabel = 'Hotspot';
|
||||||
|
badgeClass = 'bg-warning text-dark';
|
||||||
|
} else if (wifiResponse.mode === 'wifi') {
|
||||||
|
modeLabel = 'WiFi';
|
||||||
|
badgeClass = 'bg-info';
|
||||||
|
} else if (wifiResponse.mode === 'ethernet') {
|
||||||
|
modeLabel = 'Ethernet';
|
||||||
|
badgeClass = 'bg-info';
|
||||||
|
}
|
||||||
|
|
||||||
|
const detailText = `${wifiResponse.ssid} | ${wifiResponse.ip} | ${wifiResponse.hostname}.local`;
|
||||||
|
updateTestStatus('wifi', modeLabel, detailText, badgeClass);
|
||||||
|
} else {
|
||||||
|
updateTestStatus('wifi', 'Disconnected', 'No network connection', 'bg-secondary');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
updateTestStatus('wifi', 'Error', error.message, 'bg-secondary');
|
||||||
|
}
|
||||||
|
|
||||||
|
await delaySelfTest(500);
|
||||||
|
|
||||||
|
// Enable config mode
|
||||||
|
document.getElementById('selftest_status').innerHTML = `
|
||||||
|
<div class="d-flex align-items-center text-primary">
|
||||||
|
<div class="spinner-border spinner-border-sm me-2" role="status"></div>
|
||||||
|
<span>Enabling configuration mode...</span>
|
||||||
|
</div>`;
|
||||||
|
|
||||||
|
await setConfigMode(true);
|
||||||
|
|
||||||
|
// Wait for SARA script to release the port (2 seconds should be enough)
|
||||||
|
addSelfTestLog('Waiting for modem port to be available...');
|
||||||
|
await delaySelfTest(2000);
|
||||||
|
|
||||||
|
// Test Modem Connection (ATI)
|
||||||
|
document.getElementById('selftest_status').innerHTML = `
|
||||||
|
<div class="d-flex align-items-center text-primary">
|
||||||
|
<div class="spinner-border spinner-border-sm me-2" role="status"></div>
|
||||||
|
<span>Testing modem connection...</span>
|
||||||
|
</div>`;
|
||||||
|
|
||||||
|
updateTestStatus('modem', 'Testing...', 'Sending ATI command...', 'bg-info');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const modemResponse = await sendATCommand('ATI', 5);
|
||||||
|
|
||||||
|
if (modemResponse.includes('OK') && (modemResponse.toUpperCase().includes('SARA-R5') || modemResponse.toUpperCase().includes('SARA-R4'))) {
|
||||||
|
// Extract model
|
||||||
|
const modelMatch = modemResponse.match(/SARA-R[45]\d*[A-Z]*-\d+[A-Z]*-\d+/i);
|
||||||
|
const model = modelMatch ? modelMatch[0] : 'SARA module';
|
||||||
|
updateTestStatus('modem', 'Passed', `Model: ${model}`, 'bg-success');
|
||||||
|
testsPassed++;
|
||||||
|
} else if (modemResponse.includes('OK')) {
|
||||||
|
updateTestStatus('modem', 'Passed', 'Modem responding', 'bg-success');
|
||||||
|
testsPassed++;
|
||||||
|
} else {
|
||||||
|
updateTestStatus('modem', 'Failed', 'No valid response', 'bg-danger');
|
||||||
|
testsFailed++;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
updateTestStatus('modem', 'Failed', error.message, 'bg-danger');
|
||||||
|
testsFailed++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delay between AT commands
|
||||||
|
await delaySelfTest(1000);
|
||||||
|
|
||||||
|
// Test SIM Card (AT+CCID?)
|
||||||
|
document.getElementById('selftest_status').innerHTML = `
|
||||||
|
<div class="d-flex align-items-center text-primary">
|
||||||
|
<div class="spinner-border spinner-border-sm me-2" role="status"></div>
|
||||||
|
<span>Testing SIM card...</span>
|
||||||
|
</div>`;
|
||||||
|
|
||||||
|
updateTestStatus('sim', 'Testing...', 'Sending AT+CCID? command...', 'bg-info');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const simResponse = await sendATCommand('AT+CCID?', 5);
|
||||||
|
|
||||||
|
const ccidMatch = simResponse.match(/\+CCID:\s*(\d{18,22})/);
|
||||||
|
if (simResponse.includes('OK') && ccidMatch) {
|
||||||
|
const iccid = ccidMatch[1];
|
||||||
|
// Show last 4 digits only for privacy
|
||||||
|
const maskedIccid = '****' + iccid.slice(-4);
|
||||||
|
updateTestStatus('sim', 'Passed', `ICCID: ...${maskedIccid}`, 'bg-success');
|
||||||
|
testsPassed++;
|
||||||
|
} else if (simResponse.includes('ERROR')) {
|
||||||
|
updateTestStatus('sim', 'Failed', 'SIM card not detected', 'bg-danger');
|
||||||
|
testsFailed++;
|
||||||
|
} else {
|
||||||
|
updateTestStatus('sim', 'Warning', 'Unable to read ICCID', 'bg-warning');
|
||||||
|
testsFailed++;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
updateTestStatus('sim', 'Failed', error.message, 'bg-danger');
|
||||||
|
testsFailed++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delay between AT commands
|
||||||
|
await delaySelfTest(1000);
|
||||||
|
|
||||||
|
// Test Signal Strength (AT+CSQ)
|
||||||
|
document.getElementById('selftest_status').innerHTML = `
|
||||||
|
<div class="d-flex align-items-center text-primary">
|
||||||
|
<div class="spinner-border spinner-border-sm me-2" role="status"></div>
|
||||||
|
<span>Testing signal strength...</span>
|
||||||
|
</div>`;
|
||||||
|
|
||||||
|
updateTestStatus('signal', 'Testing...', 'Sending AT+CSQ command...', 'bg-info');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const signalResponse = await sendATCommand('AT+CSQ', 5);
|
||||||
|
|
||||||
|
const csqMatch = signalResponse.match(/\+CSQ:\s*(\d+),(\d+)/);
|
||||||
|
if (signalResponse.includes('OK') && csqMatch) {
|
||||||
|
const signalPower = parseInt(csqMatch[1]);
|
||||||
|
|
||||||
|
if (signalPower === 99) {
|
||||||
|
updateTestStatus('signal', 'Failed', 'No signal detected', 'bg-danger');
|
||||||
|
testsFailed++;
|
||||||
|
} else if (signalPower === 0) {
|
||||||
|
updateTestStatus('signal', 'Warning', 'Very poor signal (0/31)', 'bg-warning');
|
||||||
|
testsFailed++;
|
||||||
|
} else if (signalPower <= 24) {
|
||||||
|
updateTestStatus('signal', 'Passed', `Poor signal (${signalPower}/31)`, 'bg-success');
|
||||||
|
testsPassed++;
|
||||||
|
} else if (signalPower <= 26) {
|
||||||
|
updateTestStatus('signal', 'Passed', `Good signal (${signalPower}/31)`, 'bg-success');
|
||||||
|
testsPassed++;
|
||||||
|
} else if (signalPower <= 28) {
|
||||||
|
updateTestStatus('signal', 'Passed', `Very good signal (${signalPower}/31)`, 'bg-success');
|
||||||
|
testsPassed++;
|
||||||
|
} else {
|
||||||
|
updateTestStatus('signal', 'Passed', `Excellent signal (${signalPower}/31)`, 'bg-success');
|
||||||
|
testsPassed++;
|
||||||
|
}
|
||||||
|
} else if (signalResponse.includes('ERROR')) {
|
||||||
|
updateTestStatus('signal', 'Failed', 'Unable to read signal', 'bg-danger');
|
||||||
|
testsFailed++;
|
||||||
|
} else {
|
||||||
|
updateTestStatus('signal', 'Warning', 'Unexpected response', 'bg-warning');
|
||||||
|
testsFailed++;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
updateTestStatus('signal', 'Failed', error.message, 'bg-danger');
|
||||||
|
testsFailed++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delay between AT commands
|
||||||
|
await delaySelfTest(1000);
|
||||||
|
|
||||||
|
// Test Network Connection (AT+COPS?)
|
||||||
|
document.getElementById('selftest_status').innerHTML = `
|
||||||
|
<div class="d-flex align-items-center text-primary">
|
||||||
|
<div class="spinner-border spinner-border-sm me-2" role="status"></div>
|
||||||
|
<span>Testing network connection...</span>
|
||||||
|
</div>`;
|
||||||
|
|
||||||
|
updateTestStatus('network', 'Testing...', 'Sending AT+COPS? command...', 'bg-info');
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Load operators data for network name lookup
|
||||||
|
let opData = null;
|
||||||
|
try {
|
||||||
|
opData = await loadOperatorsDataSelfTest();
|
||||||
|
} catch (e) {
|
||||||
|
addSelfTestLog('Warning: Could not load operators data');
|
||||||
|
}
|
||||||
|
|
||||||
|
const networkResponse = await sendATCommand('AT+COPS?', 5);
|
||||||
|
|
||||||
|
const copsMatch = networkResponse.match(/\+COPS:\s*(\d+)(?:,(\d+),"?([^",]+)"?,(\d+))?/);
|
||||||
|
if (networkResponse.includes('OK') && copsMatch) {
|
||||||
|
const mode = copsMatch[1];
|
||||||
|
const oper = copsMatch[3];
|
||||||
|
const act = copsMatch[4];
|
||||||
|
|
||||||
|
if (oper) {
|
||||||
|
// Get operator name from lookup table
|
||||||
|
let operatorName = oper;
|
||||||
|
if (opData && opData.operators && opData.operators[oper]) {
|
||||||
|
operatorName = opData.operators[oper].name;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get access technology
|
||||||
|
let actDesc = 'Unknown';
|
||||||
|
if (opData && opData.accessTechnology && opData.accessTechnology[act]) {
|
||||||
|
actDesc = opData.accessTechnology[act];
|
||||||
|
}
|
||||||
|
|
||||||
|
updateTestStatus('network', 'Passed', `${operatorName} (${actDesc})`, 'bg-success');
|
||||||
|
testsPassed++;
|
||||||
|
} else {
|
||||||
|
updateTestStatus('network', 'Warning', 'Not registered to network', 'bg-warning');
|
||||||
|
testsFailed++;
|
||||||
|
}
|
||||||
|
} else if (networkResponse.includes('ERROR')) {
|
||||||
|
updateTestStatus('network', 'Failed', 'Unable to get network info', 'bg-danger');
|
||||||
|
testsFailed++;
|
||||||
|
} else {
|
||||||
|
updateTestStatus('network', 'Warning', 'Unexpected response', 'bg-warning');
|
||||||
|
testsFailed++;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
updateTestStatus('network', 'Failed', error.message, 'bg-danger');
|
||||||
|
testsFailed++;
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
addSelfTestLog(`Test sequence error: ${error.message}`);
|
||||||
|
} finally {
|
||||||
|
// Always disable config mode at the end
|
||||||
|
document.getElementById('selftest_status').innerHTML = `
|
||||||
|
<div class="d-flex align-items-center text-primary">
|
||||||
|
<div class="spinner-border spinner-border-sm me-2" role="status"></div>
|
||||||
|
<span>Disabling configuration mode...</span>
|
||||||
|
</div>`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await delaySelfTest(500);
|
||||||
|
await setConfigMode(false);
|
||||||
|
} catch (error) {
|
||||||
|
addSelfTestLog(`Warning: Failed to disable config mode: ${error.message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show final status
|
||||||
|
const totalTests = testsPassed + testsFailed;
|
||||||
|
let statusClass, statusIcon, statusText;
|
||||||
|
|
||||||
|
if (testsFailed === 0) {
|
||||||
|
statusClass = 'text-success';
|
||||||
|
statusIcon = '✓';
|
||||||
|
statusText = 'All tests passed';
|
||||||
|
} else if (testsPassed === 0) {
|
||||||
|
statusClass = 'text-danger';
|
||||||
|
statusIcon = '✗';
|
||||||
|
statusText = 'All tests failed';
|
||||||
|
} else {
|
||||||
|
statusClass = 'text-warning';
|
||||||
|
statusIcon = '!';
|
||||||
|
statusText = 'Some tests failed';
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('selftest_status').innerHTML = `
|
||||||
|
<div class="d-flex align-items-center ${statusClass}">
|
||||||
|
<span class="fs-4 me-2">${statusIcon}</span>
|
||||||
|
<span><strong>${statusText}</strong></span>
|
||||||
|
</div>`;
|
||||||
|
|
||||||
|
document.getElementById('selftest_summary').innerHTML = `
|
||||||
|
<span class="badge bg-success me-1">${testsPassed} passed</span>
|
||||||
|
<span class="badge bg-danger">${testsFailed} failed</span>`;
|
||||||
|
|
||||||
|
// Store summary in report
|
||||||
|
selfTestReport.summary = {
|
||||||
|
passed: testsPassed,
|
||||||
|
failed: testsFailed,
|
||||||
|
status: statusText
|
||||||
|
};
|
||||||
|
|
||||||
|
// Enable buttons
|
||||||
|
document.getElementById('selfTestCloseBtn').disabled = false;
|
||||||
|
document.getElementById('selfTestDoneBtn').disabled = false;
|
||||||
|
document.getElementById('selfTestCopyBtn').disabled = false;
|
||||||
|
document.querySelectorAll('.btn_selfTest').forEach(btn => btn.disabled = false);
|
||||||
|
|
||||||
|
addSelfTestLog('Self test completed.');
|
||||||
|
addSelfTestLog('Click "Copy Report" to share results with support.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateReport() {
|
||||||
|
// Build formatted report
|
||||||
|
let report = `===============================================================
|
||||||
|
NEBULEAIR PRO 4G - SELF TEST REPORT
|
||||||
|
===============================================================
|
||||||
|
|
||||||
|
DEVICE INFORMATION
|
||||||
|
------------------
|
||||||
|
Device ID: ${selfTestReport.deviceId || 'Unknown'}
|
||||||
|
Device Name: ${selfTestReport.deviceName || 'Unknown'}
|
||||||
|
Modem Version: ${selfTestReport.modemVersion || 'Unknown'}
|
||||||
|
System Time: ${selfTestReport.systemTime || 'Unknown'}
|
||||||
|
Report Date: ${selfTestReport.timestamp}
|
||||||
|
GPS Location: ${selfTestReport.latitude || 'N/A'}, ${selfTestReport.longitude || 'N/A'}
|
||||||
|
|
||||||
|
===============================================================
|
||||||
|
TEST RESULTS
|
||||||
|
===============================================================
|
||||||
|
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Add test results (sensors first, then communication)
|
||||||
|
const testNames = {
|
||||||
|
npm: 'NextPM (Particles)',
|
||||||
|
bme280: 'BME280 (Temp/Hum)',
|
||||||
|
noise: 'Noise Sensor',
|
||||||
|
envea: 'Envea (Gas Sensors)',
|
||||||
|
rtc: 'RTC Module (DS3231)',
|
||||||
|
wifi: 'WiFi/Network',
|
||||||
|
modem: 'Modem Connection',
|
||||||
|
sim: 'SIM Card',
|
||||||
|
signal: 'Signal Strength',
|
||||||
|
network: 'Network Connection'
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const [testId, name] of Object.entries(testNames)) {
|
||||||
|
if (selfTestReport.results[testId]) {
|
||||||
|
const result = selfTestReport.results[testId];
|
||||||
|
const statusIcon = result.status === 'Passed' ? '[OK]' :
|
||||||
|
result.status === 'Failed' ? '[FAIL]' :
|
||||||
|
result.status.includes('Hotspot') || result.status.includes('WiFi') || result.status.includes('Ethernet') ? '[INFO]' : '[WARN]';
|
||||||
|
report += `${statusIcon} ${name}
|
||||||
|
Status: ${result.status}
|
||||||
|
Detail: ${result.detail}
|
||||||
|
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add summary
|
||||||
|
if (selfTestReport.summary) {
|
||||||
|
report += `===============================================================
|
||||||
|
SUMMARY
|
||||||
|
===============================================================
|
||||||
|
|
||||||
|
Passed: ${selfTestReport.summary.passed}
|
||||||
|
Failed: ${selfTestReport.summary.failed}
|
||||||
|
Status: ${selfTestReport.summary.status}
|
||||||
|
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add raw AT responses
|
||||||
|
report += `===============================================================
|
||||||
|
RAW AT RESPONSES
|
||||||
|
===============================================================
|
||||||
|
|
||||||
|
`;
|
||||||
|
|
||||||
|
for (const [command, response] of Object.entries(selfTestReport.rawResponses)) {
|
||||||
|
report += `--- ${command} ---
|
||||||
|
${response}
|
||||||
|
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add full logs
|
||||||
|
report += `===============================================================
|
||||||
|
DETAILED LOGS
|
||||||
|
===============================================================
|
||||||
|
|
||||||
|
${document.getElementById('selftest_logs').textContent}
|
||||||
|
|
||||||
|
===============================================================
|
||||||
|
END OF REPORT - Generated by NebuleAir Pro 4G
|
||||||
|
===============================================================
|
||||||
|
`;
|
||||||
|
|
||||||
|
return report;
|
||||||
|
}
|
||||||
|
|
||||||
|
function openShareReportModal() {
|
||||||
|
// Generate the report
|
||||||
|
const report = generateReport();
|
||||||
|
|
||||||
|
// Put report in textarea
|
||||||
|
document.getElementById('shareReportText').value = report;
|
||||||
|
|
||||||
|
// Open the share modal
|
||||||
|
const shareModal = new bootstrap.Modal(document.getElementById('shareReportModal'));
|
||||||
|
shareModal.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectAllReportText() {
|
||||||
|
const textarea = document.getElementById('shareReportText');
|
||||||
|
textarea.select();
|
||||||
|
textarea.setSelectionRange(0, textarea.value.length); // For mobile devices
|
||||||
|
}
|
||||||
|
|
||||||
|
function downloadReport() {
|
||||||
|
const report = generateReport();
|
||||||
|
|
||||||
|
// Create filename with device ID
|
||||||
|
const deviceId = selfTestReport.deviceId || 'unknown';
|
||||||
|
const date = new Date().toISOString().slice(0, 10);
|
||||||
|
const filename = `logs_nebuleair_${deviceId}_${date}.txt`;
|
||||||
|
|
||||||
|
// Create blob and download
|
||||||
|
const blob = new Blob([report], { type: 'text/plain;charset=utf-8' });
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = url;
|
||||||
|
a.download = filename;
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
|
||||||
|
// Cleanup
|
||||||
|
setTimeout(function() {
|
||||||
|
document.body.removeChild(a);
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load the self-test modal HTML into the page
|
||||||
|
function initSelfTestModal() {
|
||||||
|
fetch('selftest-modal.html')
|
||||||
|
.then(response => response.text())
|
||||||
|
.then(html => {
|
||||||
|
// Insert modal HTML before </body>
|
||||||
|
const container = document.createElement('div');
|
||||||
|
container.id = 'selftest-modal-container';
|
||||||
|
container.innerHTML = html;
|
||||||
|
document.body.appendChild(container);
|
||||||
|
})
|
||||||
|
.catch(error => console.error('Error loading selftest modal:', error));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto-init when DOM is ready
|
||||||
|
document.addEventListener('DOMContentLoaded', initSelfTestModal);
|
||||||
58
html/assets/js/topbar-logo.js
Normal file
58
html/assets/js/topbar-logo.js
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
|
||||||
|
/**
|
||||||
|
* Global configuration handler for UI elements
|
||||||
|
* - Updates Topbar Logo based on device type
|
||||||
|
* - Shows/Hides "Screen" sidebar tab based on device type
|
||||||
|
*/
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
let config = null;
|
||||||
|
|
||||||
|
// Fetch config once
|
||||||
|
fetch('launcher.php?type=get_config_sqlite')
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
config = data;
|
||||||
|
applyConfig(); // Apply immediately if elements are ready
|
||||||
|
})
|
||||||
|
.catch(error => console.error('Error loading config:', error));
|
||||||
|
|
||||||
|
// Observe DOM changes to handle dynamically loaded elements (sidebar, topbar)
|
||||||
|
const observer = new MutationObserver(() => {
|
||||||
|
if (config) applyConfig();
|
||||||
|
});
|
||||||
|
|
||||||
|
observer.observe(document.body, { childList: true, subtree: true });
|
||||||
|
|
||||||
|
function applyConfig() {
|
||||||
|
if (!config) return;
|
||||||
|
|
||||||
|
const isModuleAirPro = (config.device_type === 'moduleair_pro' || config.type === 'moduleair_pro');
|
||||||
|
|
||||||
|
// 1. Topbar Logo Logic
|
||||||
|
const logo = document.getElementById('topbar-logo');
|
||||||
|
if (logo && isModuleAirPro) {
|
||||||
|
// prevent unnecessary re-assignments
|
||||||
|
if (!logo.src.includes('logoModuleAir.png')) {
|
||||||
|
logo.src = 'assets/img/logoModuleAir.png';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Sidebar Screen Tab Logic - Use class since ID might be duplicated (desktop/mobile)
|
||||||
|
const navScreenElements = document.querySelectorAll('.nav-screen-item');
|
||||||
|
if (navScreenElements.length > 0) {
|
||||||
|
navScreenElements.forEach(navScreen => {
|
||||||
|
if (isModuleAirPro) {
|
||||||
|
// Ensure it's visible (bootstrap nav-link usually block or flex)
|
||||||
|
if (navScreen.style.display === 'none') {
|
||||||
|
navScreen.style.display = 'flex';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Hide if not pro
|
||||||
|
if (navScreen.style.display !== 'none') {
|
||||||
|
navScreen.style.display = 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
BIN
html/assets/leaflet/images/layers-2x.png
Executable file
BIN
html/assets/leaflet/images/layers-2x.png
Executable file
Binary file not shown.
|
After Width: | Height: | Size: 1.2 KiB |
BIN
html/assets/leaflet/images/layers.png
Executable file
BIN
html/assets/leaflet/images/layers.png
Executable file
Binary file not shown.
|
After Width: | Height: | Size: 696 B |
BIN
html/assets/leaflet/images/marker-icon-2x.png
Executable file
BIN
html/assets/leaflet/images/marker-icon-2x.png
Executable file
Binary file not shown.
|
After Width: | Height: | Size: 2.4 KiB |
BIN
html/assets/leaflet/images/marker-icon.png
Executable file
BIN
html/assets/leaflet/images/marker-icon.png
Executable file
Binary file not shown.
|
After Width: | Height: | Size: 1.4 KiB |
BIN
html/assets/leaflet/images/marker-shadow.png
Executable file
BIN
html/assets/leaflet/images/marker-shadow.png
Executable file
Binary file not shown.
|
After Width: | Height: | Size: 618 B |
14419
html/assets/leaflet/leaflet-src.esm.js
Executable file
14419
html/assets/leaflet/leaflet-src.esm.js
Executable file
File diff suppressed because it is too large
Load Diff
1
html/assets/leaflet/leaflet-src.esm.js.map
Executable file
1
html/assets/leaflet/leaflet-src.esm.js.map
Executable file
File diff suppressed because one or more lines are too long
14512
html/assets/leaflet/leaflet-src.js
Executable file
14512
html/assets/leaflet/leaflet-src.js
Executable file
File diff suppressed because it is too large
Load Diff
1
html/assets/leaflet/leaflet-src.js.map
Executable file
1
html/assets/leaflet/leaflet-src.js.map
Executable file
File diff suppressed because one or more lines are too long
661
html/assets/leaflet/leaflet.css
Executable file
661
html/assets/leaflet/leaflet.css
Executable file
@@ -0,0 +1,661 @@
|
|||||||
|
/* required styles */
|
||||||
|
|
||||||
|
.leaflet-pane,
|
||||||
|
.leaflet-tile,
|
||||||
|
.leaflet-marker-icon,
|
||||||
|
.leaflet-marker-shadow,
|
||||||
|
.leaflet-tile-container,
|
||||||
|
.leaflet-pane > svg,
|
||||||
|
.leaflet-pane > canvas,
|
||||||
|
.leaflet-zoom-box,
|
||||||
|
.leaflet-image-layer,
|
||||||
|
.leaflet-layer {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
|
.leaflet-container {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.leaflet-tile,
|
||||||
|
.leaflet-marker-icon,
|
||||||
|
.leaflet-marker-shadow {
|
||||||
|
-webkit-user-select: none;
|
||||||
|
-moz-user-select: none;
|
||||||
|
user-select: none;
|
||||||
|
-webkit-user-drag: none;
|
||||||
|
}
|
||||||
|
/* Prevents IE11 from highlighting tiles in blue */
|
||||||
|
.leaflet-tile::selection {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
/* Safari renders non-retina tile on retina better with this, but Chrome is worse */
|
||||||
|
.leaflet-safari .leaflet-tile {
|
||||||
|
image-rendering: -webkit-optimize-contrast;
|
||||||
|
}
|
||||||
|
/* hack that prevents hw layers "stretching" when loading new tiles */
|
||||||
|
.leaflet-safari .leaflet-tile-container {
|
||||||
|
width: 1600px;
|
||||||
|
height: 1600px;
|
||||||
|
-webkit-transform-origin: 0 0;
|
||||||
|
}
|
||||||
|
.leaflet-marker-icon,
|
||||||
|
.leaflet-marker-shadow {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
/* .leaflet-container svg: reset svg max-width decleration shipped in Joomla! (joomla.org) 3.x */
|
||||||
|
/* .leaflet-container img: map is broken in FF if you have max-width: 100% on tiles */
|
||||||
|
.leaflet-container .leaflet-overlay-pane svg {
|
||||||
|
max-width: none !important;
|
||||||
|
max-height: none !important;
|
||||||
|
}
|
||||||
|
.leaflet-container .leaflet-marker-pane img,
|
||||||
|
.leaflet-container .leaflet-shadow-pane img,
|
||||||
|
.leaflet-container .leaflet-tile-pane img,
|
||||||
|
.leaflet-container img.leaflet-image-layer,
|
||||||
|
.leaflet-container .leaflet-tile {
|
||||||
|
max-width: none !important;
|
||||||
|
max-height: none !important;
|
||||||
|
width: auto;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.leaflet-container img.leaflet-tile {
|
||||||
|
/* See: https://bugs.chromium.org/p/chromium/issues/detail?id=600120 */
|
||||||
|
mix-blend-mode: plus-lighter;
|
||||||
|
}
|
||||||
|
|
||||||
|
.leaflet-container.leaflet-touch-zoom {
|
||||||
|
-ms-touch-action: pan-x pan-y;
|
||||||
|
touch-action: pan-x pan-y;
|
||||||
|
}
|
||||||
|
.leaflet-container.leaflet-touch-drag {
|
||||||
|
-ms-touch-action: pinch-zoom;
|
||||||
|
/* Fallback for FF which doesn't support pinch-zoom */
|
||||||
|
touch-action: none;
|
||||||
|
touch-action: pinch-zoom;
|
||||||
|
}
|
||||||
|
.leaflet-container.leaflet-touch-drag.leaflet-touch-zoom {
|
||||||
|
-ms-touch-action: none;
|
||||||
|
touch-action: none;
|
||||||
|
}
|
||||||
|
.leaflet-container {
|
||||||
|
-webkit-tap-highlight-color: transparent;
|
||||||
|
}
|
||||||
|
.leaflet-container a {
|
||||||
|
-webkit-tap-highlight-color: rgba(51, 181, 229, 0.4);
|
||||||
|
}
|
||||||
|
.leaflet-tile {
|
||||||
|
filter: inherit;
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
.leaflet-tile-loaded {
|
||||||
|
visibility: inherit;
|
||||||
|
}
|
||||||
|
.leaflet-zoom-box {
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
-moz-box-sizing: border-box;
|
||||||
|
box-sizing: border-box;
|
||||||
|
z-index: 800;
|
||||||
|
}
|
||||||
|
/* workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=888319 */
|
||||||
|
.leaflet-overlay-pane svg {
|
||||||
|
-moz-user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.leaflet-pane { z-index: 400; }
|
||||||
|
|
||||||
|
.leaflet-tile-pane { z-index: 200; }
|
||||||
|
.leaflet-overlay-pane { z-index: 400; }
|
||||||
|
.leaflet-shadow-pane { z-index: 500; }
|
||||||
|
.leaflet-marker-pane { z-index: 600; }
|
||||||
|
.leaflet-tooltip-pane { z-index: 650; }
|
||||||
|
.leaflet-popup-pane { z-index: 700; }
|
||||||
|
|
||||||
|
.leaflet-map-pane canvas { z-index: 100; }
|
||||||
|
.leaflet-map-pane svg { z-index: 200; }
|
||||||
|
|
||||||
|
.leaflet-vml-shape {
|
||||||
|
width: 1px;
|
||||||
|
height: 1px;
|
||||||
|
}
|
||||||
|
.lvml {
|
||||||
|
behavior: url(#default#VML);
|
||||||
|
display: inline-block;
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* control positioning */
|
||||||
|
|
||||||
|
.leaflet-control {
|
||||||
|
position: relative;
|
||||||
|
z-index: 800;
|
||||||
|
pointer-events: visiblePainted; /* IE 9-10 doesn't have auto */
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
.leaflet-top,
|
||||||
|
.leaflet-bottom {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 1000;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
.leaflet-top {
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
|
.leaflet-right {
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
.leaflet-bottom {
|
||||||
|
bottom: 0;
|
||||||
|
}
|
||||||
|
.leaflet-left {
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
.leaflet-control {
|
||||||
|
float: left;
|
||||||
|
clear: both;
|
||||||
|
}
|
||||||
|
.leaflet-right .leaflet-control {
|
||||||
|
float: right;
|
||||||
|
}
|
||||||
|
.leaflet-top .leaflet-control {
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
.leaflet-bottom .leaflet-control {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
.leaflet-left .leaflet-control {
|
||||||
|
margin-left: 10px;
|
||||||
|
}
|
||||||
|
.leaflet-right .leaflet-control {
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* zoom and fade animations */
|
||||||
|
|
||||||
|
.leaflet-fade-anim .leaflet-popup {
|
||||||
|
opacity: 0;
|
||||||
|
-webkit-transition: opacity 0.2s linear;
|
||||||
|
-moz-transition: opacity 0.2s linear;
|
||||||
|
transition: opacity 0.2s linear;
|
||||||
|
}
|
||||||
|
.leaflet-fade-anim .leaflet-map-pane .leaflet-popup {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
.leaflet-zoom-animated {
|
||||||
|
-webkit-transform-origin: 0 0;
|
||||||
|
-ms-transform-origin: 0 0;
|
||||||
|
transform-origin: 0 0;
|
||||||
|
}
|
||||||
|
svg.leaflet-zoom-animated {
|
||||||
|
will-change: transform;
|
||||||
|
}
|
||||||
|
|
||||||
|
.leaflet-zoom-anim .leaflet-zoom-animated {
|
||||||
|
-webkit-transition: -webkit-transform 0.25s cubic-bezier(0,0,0.25,1);
|
||||||
|
-moz-transition: -moz-transform 0.25s cubic-bezier(0,0,0.25,1);
|
||||||
|
transition: transform 0.25s cubic-bezier(0,0,0.25,1);
|
||||||
|
}
|
||||||
|
.leaflet-zoom-anim .leaflet-tile,
|
||||||
|
.leaflet-pan-anim .leaflet-tile {
|
||||||
|
-webkit-transition: none;
|
||||||
|
-moz-transition: none;
|
||||||
|
transition: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.leaflet-zoom-anim .leaflet-zoom-hide {
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* cursors */
|
||||||
|
|
||||||
|
.leaflet-interactive {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.leaflet-grab {
|
||||||
|
cursor: -webkit-grab;
|
||||||
|
cursor: -moz-grab;
|
||||||
|
cursor: grab;
|
||||||
|
}
|
||||||
|
.leaflet-crosshair,
|
||||||
|
.leaflet-crosshair .leaflet-interactive {
|
||||||
|
cursor: crosshair;
|
||||||
|
}
|
||||||
|
.leaflet-popup-pane,
|
||||||
|
.leaflet-control {
|
||||||
|
cursor: auto;
|
||||||
|
}
|
||||||
|
.leaflet-dragging .leaflet-grab,
|
||||||
|
.leaflet-dragging .leaflet-grab .leaflet-interactive,
|
||||||
|
.leaflet-dragging .leaflet-marker-draggable {
|
||||||
|
cursor: move;
|
||||||
|
cursor: -webkit-grabbing;
|
||||||
|
cursor: -moz-grabbing;
|
||||||
|
cursor: grabbing;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* marker & overlays interactivity */
|
||||||
|
.leaflet-marker-icon,
|
||||||
|
.leaflet-marker-shadow,
|
||||||
|
.leaflet-image-layer,
|
||||||
|
.leaflet-pane > svg path,
|
||||||
|
.leaflet-tile-container {
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.leaflet-marker-icon.leaflet-interactive,
|
||||||
|
.leaflet-image-layer.leaflet-interactive,
|
||||||
|
.leaflet-pane > svg path.leaflet-interactive,
|
||||||
|
svg.leaflet-image-layer.leaflet-interactive path {
|
||||||
|
pointer-events: visiblePainted; /* IE 9-10 doesn't have auto */
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* visual tweaks */
|
||||||
|
|
||||||
|
.leaflet-container {
|
||||||
|
background: #ddd;
|
||||||
|
outline-offset: 1px;
|
||||||
|
}
|
||||||
|
.leaflet-container a {
|
||||||
|
color: #0078A8;
|
||||||
|
}
|
||||||
|
.leaflet-zoom-box {
|
||||||
|
border: 2px dotted #38f;
|
||||||
|
background: rgba(255,255,255,0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* general typography */
|
||||||
|
.leaflet-container {
|
||||||
|
font-family: "Helvetica Neue", Arial, Helvetica, sans-serif;
|
||||||
|
font-size: 12px;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* general toolbar styles */
|
||||||
|
|
||||||
|
.leaflet-bar {
|
||||||
|
box-shadow: 0 1px 5px rgba(0,0,0,0.65);
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
.leaflet-bar a {
|
||||||
|
background-color: #fff;
|
||||||
|
border-bottom: 1px solid #ccc;
|
||||||
|
width: 26px;
|
||||||
|
height: 26px;
|
||||||
|
line-height: 26px;
|
||||||
|
display: block;
|
||||||
|
text-align: center;
|
||||||
|
text-decoration: none;
|
||||||
|
color: black;
|
||||||
|
}
|
||||||
|
.leaflet-bar a,
|
||||||
|
.leaflet-control-layers-toggle {
|
||||||
|
background-position: 50% 50%;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.leaflet-bar a:hover,
|
||||||
|
.leaflet-bar a:focus {
|
||||||
|
background-color: #f4f4f4;
|
||||||
|
}
|
||||||
|
.leaflet-bar a:first-child {
|
||||||
|
border-top-left-radius: 4px;
|
||||||
|
border-top-right-radius: 4px;
|
||||||
|
}
|
||||||
|
.leaflet-bar a:last-child {
|
||||||
|
border-bottom-left-radius: 4px;
|
||||||
|
border-bottom-right-radius: 4px;
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
.leaflet-bar a.leaflet-disabled {
|
||||||
|
cursor: default;
|
||||||
|
background-color: #f4f4f4;
|
||||||
|
color: #bbb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.leaflet-touch .leaflet-bar a {
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
line-height: 30px;
|
||||||
|
}
|
||||||
|
.leaflet-touch .leaflet-bar a:first-child {
|
||||||
|
border-top-left-radius: 2px;
|
||||||
|
border-top-right-radius: 2px;
|
||||||
|
}
|
||||||
|
.leaflet-touch .leaflet-bar a:last-child {
|
||||||
|
border-bottom-left-radius: 2px;
|
||||||
|
border-bottom-right-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* zoom control */
|
||||||
|
|
||||||
|
.leaflet-control-zoom-in,
|
||||||
|
.leaflet-control-zoom-out {
|
||||||
|
font: bold 18px 'Lucida Console', Monaco, monospace;
|
||||||
|
text-indent: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.leaflet-touch .leaflet-control-zoom-in, .leaflet-touch .leaflet-control-zoom-out {
|
||||||
|
font-size: 22px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* layers control */
|
||||||
|
|
||||||
|
.leaflet-control-layers {
|
||||||
|
box-shadow: 0 1px 5px rgba(0,0,0,0.4);
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
.leaflet-control-layers-toggle {
|
||||||
|
background-image: url(images/layers.png);
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
}
|
||||||
|
.leaflet-retina .leaflet-control-layers-toggle {
|
||||||
|
background-image: url(images/layers-2x.png);
|
||||||
|
background-size: 26px 26px;
|
||||||
|
}
|
||||||
|
.leaflet-touch .leaflet-control-layers-toggle {
|
||||||
|
width: 44px;
|
||||||
|
height: 44px;
|
||||||
|
}
|
||||||
|
.leaflet-control-layers .leaflet-control-layers-list,
|
||||||
|
.leaflet-control-layers-expanded .leaflet-control-layers-toggle {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.leaflet-control-layers-expanded .leaflet-control-layers-list {
|
||||||
|
display: block;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.leaflet-control-layers-expanded {
|
||||||
|
padding: 6px 10px 6px 6px;
|
||||||
|
color: #333;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
.leaflet-control-layers-scrollbar {
|
||||||
|
overflow-y: scroll;
|
||||||
|
overflow-x: hidden;
|
||||||
|
padding-right: 5px;
|
||||||
|
}
|
||||||
|
.leaflet-control-layers-selector {
|
||||||
|
margin-top: 2px;
|
||||||
|
position: relative;
|
||||||
|
top: 1px;
|
||||||
|
}
|
||||||
|
.leaflet-control-layers label {
|
||||||
|
display: block;
|
||||||
|
font-size: 13px;
|
||||||
|
font-size: 1.08333em;
|
||||||
|
}
|
||||||
|
.leaflet-control-layers-separator {
|
||||||
|
height: 0;
|
||||||
|
border-top: 1px solid #ddd;
|
||||||
|
margin: 5px -10px 5px -6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Default icon URLs */
|
||||||
|
.leaflet-default-icon-path { /* used only in path-guessing heuristic, see L.Icon.Default */
|
||||||
|
background-image: url(images/marker-icon.png);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* attribution and scale controls */
|
||||||
|
|
||||||
|
.leaflet-container .leaflet-control-attribution {
|
||||||
|
background: #fff;
|
||||||
|
background: rgba(255, 255, 255, 0.8);
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
.leaflet-control-attribution,
|
||||||
|
.leaflet-control-scale-line {
|
||||||
|
padding: 0 5px;
|
||||||
|
color: #333;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
.leaflet-control-attribution a {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
.leaflet-control-attribution a:hover,
|
||||||
|
.leaflet-control-attribution a:focus {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
.leaflet-attribution-flag {
|
||||||
|
display: inline !important;
|
||||||
|
vertical-align: baseline !important;
|
||||||
|
width: 1em;
|
||||||
|
height: 0.6669em;
|
||||||
|
}
|
||||||
|
.leaflet-left .leaflet-control-scale {
|
||||||
|
margin-left: 5px;
|
||||||
|
}
|
||||||
|
.leaflet-bottom .leaflet-control-scale {
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
.leaflet-control-scale-line {
|
||||||
|
border: 2px solid #777;
|
||||||
|
border-top: none;
|
||||||
|
line-height: 1.1;
|
||||||
|
padding: 2px 5px 1px;
|
||||||
|
white-space: nowrap;
|
||||||
|
-moz-box-sizing: border-box;
|
||||||
|
box-sizing: border-box;
|
||||||
|
background: rgba(255, 255, 255, 0.8);
|
||||||
|
text-shadow: 1px 1px #fff;
|
||||||
|
}
|
||||||
|
.leaflet-control-scale-line:not(:first-child) {
|
||||||
|
border-top: 2px solid #777;
|
||||||
|
border-bottom: none;
|
||||||
|
margin-top: -2px;
|
||||||
|
}
|
||||||
|
.leaflet-control-scale-line:not(:first-child):not(:last-child) {
|
||||||
|
border-bottom: 2px solid #777;
|
||||||
|
}
|
||||||
|
|
||||||
|
.leaflet-touch .leaflet-control-attribution,
|
||||||
|
.leaflet-touch .leaflet-control-layers,
|
||||||
|
.leaflet-touch .leaflet-bar {
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
.leaflet-touch .leaflet-control-layers,
|
||||||
|
.leaflet-touch .leaflet-bar {
|
||||||
|
border: 2px solid rgba(0,0,0,0.2);
|
||||||
|
background-clip: padding-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* popup */
|
||||||
|
|
||||||
|
.leaflet-popup {
|
||||||
|
position: absolute;
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
.leaflet-popup-content-wrapper {
|
||||||
|
padding: 1px;
|
||||||
|
text-align: left;
|
||||||
|
border-radius: 12px;
|
||||||
|
}
|
||||||
|
.leaflet-popup-content {
|
||||||
|
margin: 13px 24px 13px 20px;
|
||||||
|
line-height: 1.3;
|
||||||
|
font-size: 13px;
|
||||||
|
font-size: 1.08333em;
|
||||||
|
min-height: 1px;
|
||||||
|
}
|
||||||
|
.leaflet-popup-content p {
|
||||||
|
margin: 17px 0;
|
||||||
|
margin: 1.3em 0;
|
||||||
|
}
|
||||||
|
.leaflet-popup-tip-container {
|
||||||
|
width: 40px;
|
||||||
|
height: 20px;
|
||||||
|
position: absolute;
|
||||||
|
left: 50%;
|
||||||
|
margin-top: -1px;
|
||||||
|
margin-left: -20px;
|
||||||
|
overflow: hidden;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
.leaflet-popup-tip {
|
||||||
|
width: 17px;
|
||||||
|
height: 17px;
|
||||||
|
padding: 1px;
|
||||||
|
|
||||||
|
margin: -10px auto 0;
|
||||||
|
pointer-events: auto;
|
||||||
|
|
||||||
|
-webkit-transform: rotate(45deg);
|
||||||
|
-moz-transform: rotate(45deg);
|
||||||
|
-ms-transform: rotate(45deg);
|
||||||
|
transform: rotate(45deg);
|
||||||
|
}
|
||||||
|
.leaflet-popup-content-wrapper,
|
||||||
|
.leaflet-popup-tip {
|
||||||
|
background: white;
|
||||||
|
color: #333;
|
||||||
|
box-shadow: 0 3px 14px rgba(0,0,0,0.4);
|
||||||
|
}
|
||||||
|
.leaflet-container a.leaflet-popup-close-button {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
border: none;
|
||||||
|
text-align: center;
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
font: 16px/24px Tahoma, Verdana, sans-serif;
|
||||||
|
color: #757575;
|
||||||
|
text-decoration: none;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
.leaflet-container a.leaflet-popup-close-button:hover,
|
||||||
|
.leaflet-container a.leaflet-popup-close-button:focus {
|
||||||
|
color: #585858;
|
||||||
|
}
|
||||||
|
.leaflet-popup-scrolled {
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.leaflet-oldie .leaflet-popup-content-wrapper {
|
||||||
|
-ms-zoom: 1;
|
||||||
|
}
|
||||||
|
.leaflet-oldie .leaflet-popup-tip {
|
||||||
|
width: 24px;
|
||||||
|
margin: 0 auto;
|
||||||
|
|
||||||
|
-ms-filter: "progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678)";
|
||||||
|
filter: progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678);
|
||||||
|
}
|
||||||
|
|
||||||
|
.leaflet-oldie .leaflet-control-zoom,
|
||||||
|
.leaflet-oldie .leaflet-control-layers,
|
||||||
|
.leaflet-oldie .leaflet-popup-content-wrapper,
|
||||||
|
.leaflet-oldie .leaflet-popup-tip {
|
||||||
|
border: 1px solid #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* div icon */
|
||||||
|
|
||||||
|
.leaflet-div-icon {
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* Tooltip */
|
||||||
|
/* Base styles for the element that has a tooltip */
|
||||||
|
.leaflet-tooltip {
|
||||||
|
position: absolute;
|
||||||
|
padding: 6px;
|
||||||
|
background-color: #fff;
|
||||||
|
border: 1px solid #fff;
|
||||||
|
border-radius: 3px;
|
||||||
|
color: #222;
|
||||||
|
white-space: nowrap;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
-moz-user-select: none;
|
||||||
|
-ms-user-select: none;
|
||||||
|
user-select: none;
|
||||||
|
pointer-events: none;
|
||||||
|
box-shadow: 0 1px 3px rgba(0,0,0,0.4);
|
||||||
|
}
|
||||||
|
.leaflet-tooltip.leaflet-interactive {
|
||||||
|
cursor: pointer;
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
.leaflet-tooltip-top:before,
|
||||||
|
.leaflet-tooltip-bottom:before,
|
||||||
|
.leaflet-tooltip-left:before,
|
||||||
|
.leaflet-tooltip-right:before {
|
||||||
|
position: absolute;
|
||||||
|
pointer-events: none;
|
||||||
|
border: 6px solid transparent;
|
||||||
|
background: transparent;
|
||||||
|
content: "";
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Directions */
|
||||||
|
|
||||||
|
.leaflet-tooltip-bottom {
|
||||||
|
margin-top: 6px;
|
||||||
|
}
|
||||||
|
.leaflet-tooltip-top {
|
||||||
|
margin-top: -6px;
|
||||||
|
}
|
||||||
|
.leaflet-tooltip-bottom:before,
|
||||||
|
.leaflet-tooltip-top:before {
|
||||||
|
left: 50%;
|
||||||
|
margin-left: -6px;
|
||||||
|
}
|
||||||
|
.leaflet-tooltip-top:before {
|
||||||
|
bottom: 0;
|
||||||
|
margin-bottom: -12px;
|
||||||
|
border-top-color: #fff;
|
||||||
|
}
|
||||||
|
.leaflet-tooltip-bottom:before {
|
||||||
|
top: 0;
|
||||||
|
margin-top: -12px;
|
||||||
|
margin-left: -6px;
|
||||||
|
border-bottom-color: #fff;
|
||||||
|
}
|
||||||
|
.leaflet-tooltip-left {
|
||||||
|
margin-left: -6px;
|
||||||
|
}
|
||||||
|
.leaflet-tooltip-right {
|
||||||
|
margin-left: 6px;
|
||||||
|
}
|
||||||
|
.leaflet-tooltip-left:before,
|
||||||
|
.leaflet-tooltip-right:before {
|
||||||
|
top: 50%;
|
||||||
|
margin-top: -6px;
|
||||||
|
}
|
||||||
|
.leaflet-tooltip-left:before {
|
||||||
|
right: 0;
|
||||||
|
margin-right: -12px;
|
||||||
|
border-left-color: #fff;
|
||||||
|
}
|
||||||
|
.leaflet-tooltip-right:before {
|
||||||
|
left: 0;
|
||||||
|
margin-left: -12px;
|
||||||
|
border-right-color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Printing */
|
||||||
|
|
||||||
|
@media print {
|
||||||
|
/* Prevent printers from removing background-images of controls. */
|
||||||
|
.leaflet-control {
|
||||||
|
-webkit-print-color-adjust: exact;
|
||||||
|
print-color-adjust: exact;
|
||||||
|
}
|
||||||
|
}
|
||||||
6
html/assets/leaflet/leaflet.js
Executable file
6
html/assets/leaflet/leaflet.js
Executable file
File diff suppressed because one or more lines are too long
1
html/assets/leaflet/leaflet.js.map
Executable file
1
html/assets/leaflet/leaflet.js.map
Executable file
File diff suppressed because one or more lines are too long
701
html/database.html
Executable file
701
html/database.html
Executable file
@@ -0,0 +1,701 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>NebuleAir</title>
|
||||||
|
<link rel="stylesheet" href="assets/css/bootstrap.min.css">
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
#sidebar a.nav-link {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
#sidebar a.nav-link:hover {
|
||||||
|
background-color: rgba(0, 0, 0, 0.5);
|
||||||
|
}
|
||||||
|
#sidebar a.nav-link svg {
|
||||||
|
margin-right: 8px; /* Add spacing between icons and text */
|
||||||
|
}
|
||||||
|
#sidebar {
|
||||||
|
transition: transform 0.3s ease-in-out;
|
||||||
|
}
|
||||||
|
.offcanvas-backdrop {
|
||||||
|
z-index: 1040;
|
||||||
|
}
|
||||||
|
/* Highlight most recent data row with light green background */
|
||||||
|
.table .most-recent-row td {
|
||||||
|
background-color: #d4edda !important;
|
||||||
|
}
|
||||||
|
.table-striped .most-recent-row td {
|
||||||
|
background-color: #d4edda !important;
|
||||||
|
}
|
||||||
|
</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" data-i18n="database.title">Base de données</h1>
|
||||||
|
<p data-i18n="database.description">Le capteur enregistre en local les données de mesures. Vous pouvez ici les consulter et les télécharger.</p>
|
||||||
|
|
||||||
|
<div class="row mb-3">
|
||||||
|
|
||||||
|
<div class="col-lg-4 col-md-6 mb-3">
|
||||||
|
<div class="card text-dark bg-light h-100">
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="card-title" data-i18n="database.viewDatabase">Consulter la base de donnée</h5>
|
||||||
|
<!-- Dropdown to select number of records -->
|
||||||
|
<div class="d-flex align-items-center mb-3">
|
||||||
|
<label for="records_limit" class="form-label me-2" data-i18n="database.numberOfMeasures">Nombre de mesures:</label>
|
||||||
|
<select id="records_limit" class="form-select w-auto">
|
||||||
|
<option value="10" selected data-i18n="database.last10">10 dernières</option>
|
||||||
|
<option value="20" data-i18n="database.last20">20 dernières</option>
|
||||||
|
<option value="30" data-i18n="database.last30">30 dernières</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-primary mb-2" onclick="get_data_sqlite('data_NPM',getSelectedLimit(),false)" data-i18n="database.pmMeasures">Mesures PM</button>
|
||||||
|
<button class="btn btn-primary mb-2" onclick="get_data_sqlite('data_BME280',getSelectedLimit(),false)" data-i18n="database.tempHumMeasures">Mesures Temp/Hum</button>
|
||||||
|
<button class="btn btn-primary mb-2" onclick="get_data_sqlite('data_NPM_5channels',getSelectedLimit(),false)" data-i18n="database.pm5Channels">Mesures PM (5 canaux)</button>
|
||||||
|
<button class="btn btn-primary mb-2" onclick="get_data_sqlite('data_envea',getSelectedLimit(),false)" data-i18n="database.cairsensProbe">Sonde Cairsens</button>
|
||||||
|
<button class="btn btn-primary mb-2" onclick="get_data_sqlite('data_NOISE',getSelectedLimit(),false)" data-i18n="database.noiseProbe">Sonde bruit</button>
|
||||||
|
<button class="btn btn-primary mb-2" onclick="get_data_sqlite('data_WIND',getSelectedLimit(),false)" data-i18n="database.windProbe">Sonde Vent</button>
|
||||||
|
<button class="btn btn-primary mb-2" onclick="get_data_sqlite('data_MPPT',getSelectedLimit(),false)" data-i18n="database.battery">Batterie</button>
|
||||||
|
<button class="btn btn-primary mb-2" onclick="get_data_sqlite('data_MHZ19',getSelectedLimit(),false)">Mesures CO2</button>
|
||||||
|
<button class="btn btn-warning mb-2" onclick="get_data_sqlite('timestamp_table',getSelectedLimit(),false)" data-i18n="database.timestampTable">Timestamp Table</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-lg-4 col-md-6 mb-3">
|
||||||
|
<div class="card text-dark bg-light h-100">
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="card-title" data-i18n="database.downloadData">Télécharger les données</h5>
|
||||||
|
<!-- Date selection for download -->
|
||||||
|
<div class="d-flex align-items-center gap-3 mb-3">
|
||||||
|
<label for="start_date" class="form-label" data-i18n="database.startDate">Date de début:</label>
|
||||||
|
<input type="date" id="start_date" class="form-control w-auto">
|
||||||
|
<label for="end_date" class="form-label" data-i18n="database.endDate">Date de fin:</label>
|
||||||
|
<input type="date" id="end_date" class="form-control w-auto">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button class="btn btn-primary mb-2" onclick="downloadByDate('data_NPM')" data-i18n="database.pmMeasures">Mesures PM</button>
|
||||||
|
<button class="btn btn-primary mb-2" onclick="downloadByDate('data_BME280')" data-i18n="database.tempHumMeasures">Mesures Temp/Hum</button>
|
||||||
|
<button class="btn btn-primary mb-2" onclick="downloadByDate('data_NPM_5channels')" data-i18n="database.pm5Channels">Mesures PM (5 canaux)</button>
|
||||||
|
<button class="btn btn-primary mb-2" onclick="downloadByDate('data_envea')" data-i18n="database.cairsensProbe">Sonde Cairsens</button>
|
||||||
|
<button class="btn btn-primary mb-2" onclick="downloadByDate('data_NOISE')" data-i18n="database.noiseProbe">Sonde Bruit</button>
|
||||||
|
<button class="btn btn-primary mb-2" onclick="downloadByDate('data_mppt')" data-i18n="database.battery">Batterie</button>
|
||||||
|
<button class="btn btn-primary mb-2" onclick="downloadByDate('data_MHZ19')">Mesures CO2</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-lg-4 col-md-6 mb-3">
|
||||||
|
<div class="card text-dark bg-light h-100">
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="card-title" data-i18n="database.downloadAll">Télécharger toute la table</h5>
|
||||||
|
<p class="text-muted small" data-i18n="database.downloadAllDesc">Télécharge l'intégralité des données sans filtre de date.</p>
|
||||||
|
<button class="btn btn-success mb-2" onclick="downloadFullTable('data_NPM')" data-i18n="database.pmMeasures">Mesures PM</button>
|
||||||
|
<button class="btn btn-success mb-2" onclick="downloadFullTable('data_BME280')" data-i18n="database.tempHumMeasures">Mesures Temp/Hum</button>
|
||||||
|
<button class="btn btn-success mb-2" onclick="downloadFullTable('data_NPM_5channels')" data-i18n="database.pm5Channels">Mesures PM (5 canaux)</button>
|
||||||
|
<button class="btn btn-success mb-2" onclick="downloadFullTable('data_envea')" data-i18n="database.cairsensProbe">Sonde Cairsens</button>
|
||||||
|
<button class="btn btn-success mb-2" onclick="downloadFullTable('data_NOISE')" data-i18n="database.noiseProbe">Sonde Bruit</button>
|
||||||
|
<button class="btn btn-success mb-2" onclick="downloadFullTable('data_MPPT')" data-i18n="database.battery">Batterie</button>
|
||||||
|
<button class="btn btn-success mb-2" onclick="downloadFullTable('data_MHZ19')">Mesures CO2</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row mb-3">
|
||||||
|
|
||||||
|
<div class="col-lg-4 col-md-6 mb-3">
|
||||||
|
<div class="card text-dark bg-light h-100">
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="card-title" data-i18n="database.statsTitle">Informations sur la base</h5>
|
||||||
|
<div id="db_stats_content">
|
||||||
|
<div class="text-center py-3">
|
||||||
|
<div class="spinner-border spinner-border-sm" role="status"></div>
|
||||||
|
<span class="ms-2" data-i18n="common.loading">Chargement...</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row mb-3">
|
||||||
|
<div class="col-lg-4 col-md-6 mb-3">
|
||||||
|
<div class="card text-white bg-danger h-100">
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="card-title" data-i18n="database.dangerZone">Zone dangereuse</h5>
|
||||||
|
<p class="card-text" data-i18n="database.dangerWarning">Attention: Cette action est irréversible!</p>
|
||||||
|
<button class="btn btn-dark btn-lg w-100 mb-2" onclick="emptySensorTables()" data-i18n="database.emptyAllTables">Vider toutes les tables de capteurs</button>
|
||||||
|
<small class="d-block mt-2" data-i18n="database.emptyTablesNote">Note: Les tables de configuration et horodatage seront préservées.</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row mt-2">
|
||||||
|
<div id="table_data"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- JAVASCRIPT -->
|
||||||
|
|
||||||
|
<!-- Link Ajax locally -->
|
||||||
|
<script src="assets/jquery/jquery-3.7.1.min.js"></script>
|
||||||
|
<!-- Link Bootstrap JS and Popper.js locally -->
|
||||||
|
<script src="assets/js/bootstrap.bundle.js"></script>
|
||||||
|
<!-- i18n translation system -->
|
||||||
|
<script src="assets/js/i18n.js"></script>
|
||||||
|
<script src="assets/js/topbar-logo.js"></script>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
|
console.log("DOMContentLoaded");
|
||||||
|
|
||||||
|
const elementsToLoad = [
|
||||||
|
{ id: 'topbar', file: 'topbar.html' },
|
||||||
|
{ id: 'sidebar', file: 'sidebar.html' },
|
||||||
|
{ id: 'sidebar_mobile', file: 'sidebar.html' }
|
||||||
|
];
|
||||||
|
|
||||||
|
let loadedCount = 0;
|
||||||
|
const totalElements = elementsToLoad.length;
|
||||||
|
|
||||||
|
function applyTranslationsWhenReady() {
|
||||||
|
if (typeof i18n !== 'undefined' && i18n.translations && Object.keys(i18n.translations).length > 0) {
|
||||||
|
console.log("Applying translations to dynamically loaded content");
|
||||||
|
i18n.applyTranslations();
|
||||||
|
} else {
|
||||||
|
// Retry after a short delay if translations aren't loaded yet
|
||||||
|
setTimeout(applyTranslationsWhenReady, 100);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
elementsToLoad.forEach(({ id, file }) => {
|
||||||
|
fetch(file)
|
||||||
|
.then(response => response.text())
|
||||||
|
.then(data => {
|
||||||
|
const element = document.getElementById(id);
|
||||||
|
if (element) {
|
||||||
|
element.innerHTML = data;
|
||||||
|
}
|
||||||
|
loadedCount++;
|
||||||
|
|
||||||
|
// Re-apply translations after all dynamic content is loaded
|
||||||
|
if (loadedCount === totalElements) {
|
||||||
|
applyTranslationsWhenReady();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => console.error(`Error loading ${file}:`, error));
|
||||||
|
});
|
||||||
|
|
||||||
|
// Also listen for language change events to re-apply translations
|
||||||
|
document.addEventListener('languageChanged', function() {
|
||||||
|
console.log("Language changed, re-applying translations");
|
||||||
|
i18n.applyTranslations();
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
window.onload = function() {
|
||||||
|
|
||||||
|
|
||||||
|
//NEW way to get data from SQLITE
|
||||||
|
$.ajax({
|
||||||
|
url: 'launcher.php?type=get_config_sqlite',
|
||||||
|
dataType:'json',
|
||||||
|
//dataType: 'json', // Specify that you expect a JSON response
|
||||||
|
method: 'GET', // Use GET or POST depending on your needs
|
||||||
|
success: function(response) {
|
||||||
|
console.log("Getting SQLite config table:");
|
||||||
|
console.log(response);
|
||||||
|
|
||||||
|
//get device Name (for the side bar)
|
||||||
|
const deviceName = response.deviceName;
|
||||||
|
const elements = document.querySelectorAll('.sideBar_sensorName');
|
||||||
|
elements.forEach((element) => {
|
||||||
|
element.innerText = deviceName;
|
||||||
|
});
|
||||||
|
|
||||||
|
//device name html page title
|
||||||
|
if (response.deviceName) {
|
||||||
|
document.title = response.deviceName;
|
||||||
|
}
|
||||||
|
|
||||||
|
},
|
||||||
|
error: function(xhr, status, error) {
|
||||||
|
console.error('AJAX request failed:', status, error);
|
||||||
|
}
|
||||||
|
}); //end ajax
|
||||||
|
|
||||||
|
|
||||||
|
// Get database table stats
|
||||||
|
loadDbStats();
|
||||||
|
|
||||||
|
//get local RTC
|
||||||
|
$.ajax({
|
||||||
|
url: 'launcher.php?type=RTC_time',
|
||||||
|
dataType: 'text', // Specify that you expect a JSON response
|
||||||
|
method: 'GET', // Use GET or POST depending on your needs
|
||||||
|
success: function(response) {
|
||||||
|
console.log("Local RTC: " + response);
|
||||||
|
const RTC_Element = document.getElementById("RTC_time");
|
||||||
|
RTC_Element.textContent = response;
|
||||||
|
},
|
||||||
|
error: function(xhr, status, error) {
|
||||||
|
console.error('AJAX request failed:', status, error);
|
||||||
|
}
|
||||||
|
}); //end AJAX
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// TABLE PM
|
||||||
|
function get_data_sqlite(table, limit, download , startDate = "", endDate = "") {
|
||||||
|
console.log(`Getting data for table: ${table}, limit: ${limit}, download: ${download}, start: ${startDate}, end: ${endDate}`);
|
||||||
|
// Construct URL parameters dynamically
|
||||||
|
let url = `launcher.php?type=table_mesure&table=${table}&limit=${limit}&download=${download}`;
|
||||||
|
|
||||||
|
// Add date parameters if downloading
|
||||||
|
if (download) {
|
||||||
|
url += `&start_date=${startDate}&end_date=${endDate}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(url);
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
url: url,
|
||||||
|
dataType: 'text', // Specify that you expect a JSON response
|
||||||
|
method: 'GET', // Use GET or POST depending on your needs
|
||||||
|
success: function(response) {
|
||||||
|
console.log(response);
|
||||||
|
|
||||||
|
// If download is true, generate and trigger CSV download
|
||||||
|
if (download) {
|
||||||
|
downloadCSV(response, table);
|
||||||
|
return; // Exit function after triggering download
|
||||||
|
}
|
||||||
|
|
||||||
|
let rows = response.trim().split("\n");
|
||||||
|
// Generate Bootstrap table
|
||||||
|
|
||||||
|
let tableHTML = `<table class="table table-striped table-bordered">
|
||||||
|
<thead class="table-dark"><tr>`;
|
||||||
|
|
||||||
|
// Define column headers dynamically based on the table type
|
||||||
|
if (table === "data_NPM") {
|
||||||
|
tableHTML += `
|
||||||
|
<th>Timestamp</th>
|
||||||
|
<th>PM1</th>
|
||||||
|
<th>PM2.5</th>
|
||||||
|
<th>PM10</th>
|
||||||
|
<th>Temperature (°C)</th>
|
||||||
|
<th>Humidity (%)</th>
|
||||||
|
<th>Status</th>
|
||||||
|
`;
|
||||||
|
} else if (table === "data_BME280") {
|
||||||
|
tableHTML += `
|
||||||
|
<th>Timestamp</th>
|
||||||
|
<th>Temperature (°C)</th>
|
||||||
|
<th>Humidity (%)</th>
|
||||||
|
<th>Pressure (hPa)</th>
|
||||||
|
`;
|
||||||
|
} else if (table === "data_NPM_5channels") {
|
||||||
|
tableHTML += `
|
||||||
|
<th>Timestamp</th>
|
||||||
|
<th>PM_ch1 (nb/L)</th>
|
||||||
|
<th>PM_ch2 (nb/L)</th>
|
||||||
|
<th>PM_ch3 (nb/L)</th>
|
||||||
|
<th>PM_ch4 (nb/L)</th>
|
||||||
|
<th>PM_ch5 (nb/L)</th>
|
||||||
|
|
||||||
|
`;
|
||||||
|
}else if (table === "data_envea") {
|
||||||
|
tableHTML += `
|
||||||
|
<th>Timestamp</th>
|
||||||
|
<th>NO2</th>
|
||||||
|
<th>H2S</th>
|
||||||
|
<th>NH3</th>
|
||||||
|
<th>CO</th>
|
||||||
|
<th>O3</th>
|
||||||
|
|
||||||
|
`;
|
||||||
|
}else if (table === "timestamp_table") {
|
||||||
|
tableHTML += `
|
||||||
|
<th>Timestamp</th>
|
||||||
|
`;
|
||||||
|
}else if (table === "data_WIND") {
|
||||||
|
tableHTML += `
|
||||||
|
<th>Timestamp</th>
|
||||||
|
<th>speed (km/h)</th>
|
||||||
|
<th>Direction (V)</th>
|
||||||
|
`;
|
||||||
|
}else if (table === "data_MPPT") {
|
||||||
|
tableHTML += `
|
||||||
|
<th>Timestamp</th>
|
||||||
|
<th>Battery Voltage</th>
|
||||||
|
<th>Battery Current</th>
|
||||||
|
<th> solar_voltage</th>
|
||||||
|
<th> solar_power</th>
|
||||||
|
<th> charger_status</th>
|
||||||
|
|
||||||
|
`;
|
||||||
|
}else if (table === "data_NOISE") {
|
||||||
|
tableHTML += `
|
||||||
|
<th>Timestamp</th>
|
||||||
|
<th>Curent LEQ</th>
|
||||||
|
<th>DB_A_value</th>
|
||||||
|
<th>Status</th>
|
||||||
|
|
||||||
|
`;
|
||||||
|
}else if (table === "data_MHZ19") {
|
||||||
|
tableHTML += `
|
||||||
|
<th>Timestamp</th>
|
||||||
|
<th>CO2 (ppm)</th>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
tableHTML += `</tr></thead><tbody>`;
|
||||||
|
|
||||||
|
// Loop through rows and create table rows
|
||||||
|
rows.forEach((row, index) => {
|
||||||
|
let columns = row.replace(/[()]/g, "").split(", "); // Remove parentheses and split
|
||||||
|
// Add special class to first row (most recent data)
|
||||||
|
const rowClass = index === 0 ? ' class="most-recent-row"' : '';
|
||||||
|
tableHTML += `<tr${rowClass}>`;
|
||||||
|
|
||||||
|
if (table === "data_NPM") {
|
||||||
|
const statusVal = parseInt(columns[6]) || 0;
|
||||||
|
const statusBadge = statusVal === 0
|
||||||
|
? '<span class="badge text-bg-success">OK</span>'
|
||||||
|
: `<span class="badge text-bg-warning">0x${statusVal.toString(16).toUpperCase().padStart(2,'0')}</span>`;
|
||||||
|
tableHTML += `
|
||||||
|
<td>${columns[0]}</td>
|
||||||
|
<td>${columns[1]}</td>
|
||||||
|
<td>${columns[2]}</td>
|
||||||
|
<td>${columns[3]}</td>
|
||||||
|
<td>${columns[4]}</td>
|
||||||
|
<td>${columns[5]}</td>
|
||||||
|
<td>${statusBadge}</td>
|
||||||
|
`;
|
||||||
|
} else if (table === "data_BME280") {
|
||||||
|
tableHTML += `
|
||||||
|
<td>${columns[0]}</td>
|
||||||
|
<td>${columns[1]}</td>
|
||||||
|
<td>${columns[2]}</td>
|
||||||
|
<td>${columns[3]}</td>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
else if (table === "data_NPM_5channels") {
|
||||||
|
tableHTML += `
|
||||||
|
<td>${columns[0]}</td>
|
||||||
|
<td>${columns[1]}</td>
|
||||||
|
<td>${columns[2]}</td>
|
||||||
|
<td>${columns[3]}</td>
|
||||||
|
<td>${columns[4]}</td>
|
||||||
|
<td>${columns[5]}</td>
|
||||||
|
|
||||||
|
`;
|
||||||
|
} else if (table === "data_envea") {
|
||||||
|
tableHTML += `
|
||||||
|
<td>${columns[0]}</td>
|
||||||
|
<td>${columns[1]}</td>
|
||||||
|
<td>${columns[2]}</td>
|
||||||
|
<td>${columns[3]}</td>
|
||||||
|
<td>${columns[4]}</td>
|
||||||
|
<td>${columns[5]}</td>
|
||||||
|
|
||||||
|
`;
|
||||||
|
}else if (table === "timestamp_table") {
|
||||||
|
tableHTML += `
|
||||||
|
<td>${columns[1]}</td>
|
||||||
|
`;
|
||||||
|
}else if (table === "data_WIND") {
|
||||||
|
tableHTML += `
|
||||||
|
<td>${columns[0]}</td>
|
||||||
|
<td>${columns[1]}</td>
|
||||||
|
<td>${columns[2]}</td>
|
||||||
|
`;
|
||||||
|
}else if (table === "data_MPPT") {
|
||||||
|
tableHTML += `
|
||||||
|
<td>${columns[0]}</td>
|
||||||
|
<td>${columns[1]}</td>
|
||||||
|
<td>${columns[2]}</td>
|
||||||
|
<td>${columns[3]}</td>
|
||||||
|
<td>${columns[4]}</td>
|
||||||
|
<td>${columns[5]}</td>
|
||||||
|
`;
|
||||||
|
}else if (table === "data_NOISE") {
|
||||||
|
const nStatus = parseInt(columns[3]) || 0;
|
||||||
|
const nStatusLabel = nStatus === 255 ? '❌ Déconnecté' : '✅ OK';
|
||||||
|
tableHTML += `
|
||||||
|
<td>${columns[0]}</td>
|
||||||
|
<td>${columns[1]}</td>
|
||||||
|
<td>${columns[2]}</td>
|
||||||
|
<td>${nStatusLabel}</td>
|
||||||
|
|
||||||
|
`;
|
||||||
|
}else if (table === "data_MHZ19") {
|
||||||
|
tableHTML += `
|
||||||
|
<td>${columns[0]}</td>
|
||||||
|
<td>${columns[1]}</td>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
tableHTML += "</tr>";
|
||||||
|
});
|
||||||
|
|
||||||
|
tableHTML += `</tbody></table>`;
|
||||||
|
|
||||||
|
// Update the #table_data div with the generated table
|
||||||
|
document.getElementById("table_data").innerHTML = tableHTML;
|
||||||
|
},
|
||||||
|
error: function(xhr, status, error) {
|
||||||
|
console.error('AJAX request failed:', status, error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function getSelectedLimit() {
|
||||||
|
return document.getElementById("records_limit").value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getStartDate() {
|
||||||
|
return document.getElementById("start_date").value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getEndDate() {
|
||||||
|
return document.getElementById("end_date").value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function downloadByDate(table) {
|
||||||
|
const startDate = getStartDate();
|
||||||
|
const endDate = getEndDate();
|
||||||
|
if (!startDate || !endDate) {
|
||||||
|
alert("Veuillez sélectionner une date de début et une date de fin.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
get_data_sqlite(table, 10, true, startDate, endDate);
|
||||||
|
}
|
||||||
|
|
||||||
|
function downloadFullTable(table) {
|
||||||
|
window.location.href = 'launcher.php?type=download_full_table&table=' + encodeURIComponent(table);
|
||||||
|
}
|
||||||
|
|
||||||
|
function downloadCSV(response, table) {
|
||||||
|
let rows = response.trim().split("\n");
|
||||||
|
|
||||||
|
let csvContent = "";
|
||||||
|
|
||||||
|
// Add headers based on table type
|
||||||
|
if (table === "data_NPM") {
|
||||||
|
csvContent += "TimestampUTC,PM1,PM2.5,PM10,Temperature_sensor,Humidity_sensor,npm_status\n";
|
||||||
|
} else if (table === "data_BME280") {
|
||||||
|
csvContent += "TimestampUTC,Temperature (°C),Humidity (%),Pressure (hPa)\n";
|
||||||
|
}
|
||||||
|
else if (table === "data_NPM_5channels") {
|
||||||
|
csvContent += "TimestampUTC,PM_ch1,PM_ch2,PM_ch3,PM_ch4,PM_ch5\n";
|
||||||
|
}
|
||||||
|
else if (table === "data_NOISE") {
|
||||||
|
csvContent += "TimestampUTC,Current_LEQ,DB_A_value,noise_status\n";
|
||||||
|
}
|
||||||
|
else if (table === "data_MHZ19") {
|
||||||
|
csvContent += "TimestampUTC,CO2_ppm\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Format rows as CSV
|
||||||
|
rows.forEach(row => {
|
||||||
|
let columns = row.replace(/[()]/g, "").split(", ");
|
||||||
|
csvContent += columns.join(",") + "\n";
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create a downloadable file
|
||||||
|
let blob = new Blob([csvContent], { type: "text/csv" });
|
||||||
|
let url = window.URL.createObjectURL(blob);
|
||||||
|
let a = document.createElement("a");
|
||||||
|
a.href = url;
|
||||||
|
a.download = table + "_data.csv"; // File name
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
document.body.removeChild(a);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Table display names
|
||||||
|
const tableDisplayNames = {
|
||||||
|
'data_NPM': 'PM (NextPM)',
|
||||||
|
'data_NPM_5channels': 'PM 5 canaux',
|
||||||
|
'data_BME280': 'Temp/Hum (BME280)',
|
||||||
|
'data_envea': 'Gaz (Cairsens)',
|
||||||
|
'data_WIND': 'Vent',
|
||||||
|
'data_MPPT': 'Batterie (MPPT)',
|
||||||
|
'data_NOISE': 'Bruit'
|
||||||
|
};
|
||||||
|
|
||||||
|
function loadDbStats() {
|
||||||
|
$.ajax({
|
||||||
|
url: 'launcher.php?type=db_table_stats',
|
||||||
|
dataType: 'json',
|
||||||
|
method: 'GET',
|
||||||
|
success: function(response) {
|
||||||
|
if (!response.success) {
|
||||||
|
document.getElementById('db_stats_content').innerHTML =
|
||||||
|
'<div class="alert alert-danger mb-0">' + (response.error || 'Erreur') + '</div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let html = '<p class="mb-2"><strong data-i18n="database.statsDbSize">Taille totale:</strong> ' + response.size_mb + ' MB</p>';
|
||||||
|
html += '<div class="table-responsive"><table class="table table-sm table-bordered mb-0">';
|
||||||
|
html += '<thead class="table-secondary"><tr>';
|
||||||
|
html += '<th data-i18n="database.statsTable">Table</th>';
|
||||||
|
html += '<th data-i18n="database.statsCount">Entrées</th>';
|
||||||
|
html += '<th data-i18n="database.statsOldest">Plus ancienne</th>';
|
||||||
|
html += '<th data-i18n="database.statsNewest">Plus récente</th>';
|
||||||
|
html += '<th data-i18n="database.statsDownload">CSV</th>';
|
||||||
|
html += '</tr></thead><tbody>';
|
||||||
|
|
||||||
|
response.tables.forEach(function(t) {
|
||||||
|
const displayName = tableDisplayNames[t.name] || t.name;
|
||||||
|
const oldest = t.oldest ? t.oldest.substring(0, 16) : '-';
|
||||||
|
const newest = t.newest ? t.newest.substring(0, 16) : '-';
|
||||||
|
const downloadBtn = t.count > 0
|
||||||
|
? '<a href="launcher.php?type=download_full_table&table=' + t.name + '" class="btn btn-outline-primary btn-sm py-0 px-1" title="Download CSV"><svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" viewBox="0 0 16 16"><path d="M.5 9.9a.5.5 0 0 1 .5.5v2.5a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-2.5a.5.5 0 0 1 1 0v2.5a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2v-2.5a.5.5 0 0 1 .5-.5z"/><path d="M7.646 11.854a.5.5 0 0 0 .708 0l3-3a.5.5 0 0 0-.708-.708L8.5 10.293V1.5a.5.5 0 0 0-1 0v8.793L5.354 8.146a.5.5 0 1 0-.708.708l3 3z"/></svg></a>'
|
||||||
|
: '-';
|
||||||
|
html += '<tr>';
|
||||||
|
html += '<td>' + displayName + '</td>';
|
||||||
|
html += '<td>' + t.count.toLocaleString() + '</td>';
|
||||||
|
html += '<td><small>' + oldest + '</small></td>';
|
||||||
|
html += '<td><small>' + newest + '</small></td>';
|
||||||
|
html += '<td class="text-center">' + downloadBtn + '</td>';
|
||||||
|
html += '</tr>';
|
||||||
|
});
|
||||||
|
|
||||||
|
html += '</tbody></table></div>';
|
||||||
|
html += '<button class="btn btn-outline-secondary btn-sm mt-2" onclick="loadDbStats()" data-i18n="logs.refresh">Refresh</button>';
|
||||||
|
|
||||||
|
document.getElementById('db_stats_content').innerHTML = html;
|
||||||
|
|
||||||
|
// Re-apply translations if i18n is loaded
|
||||||
|
if (typeof i18n !== 'undefined' && i18n.translations && Object.keys(i18n.translations).length > 0) {
|
||||||
|
i18n.applyTranslations();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: function(xhr, status, error) {
|
||||||
|
document.getElementById('db_stats_content').innerHTML =
|
||||||
|
'<div class="alert alert-danger mb-0">Erreur: ' + error + '</div>';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to empty all sensor tables
|
||||||
|
function emptySensorTables() {
|
||||||
|
// Show confirmation dialog
|
||||||
|
const confirmed = confirm(
|
||||||
|
"WARNING: This will permanently delete ALL sensor data from the database!\n\n" +
|
||||||
|
"The following tables will be emptied:\n" +
|
||||||
|
"- data_NPM\n" +
|
||||||
|
"- data_NPM_5channels\n" +
|
||||||
|
"- data_BME280\n" +
|
||||||
|
"- data_envea\n" +
|
||||||
|
"- data_WIND\n" +
|
||||||
|
"- data_MPPT\n" +
|
||||||
|
"- data_NOISE\n\n" +
|
||||||
|
"Configuration and timestamp tables will be preserved.\n\n" +
|
||||||
|
"Are you absolutely sure you want to continue?"
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!confirmed) {
|
||||||
|
console.log("Empty sensor tables operation cancelled by user");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show loading message
|
||||||
|
const tableDataDiv = document.getElementById("table_data");
|
||||||
|
tableDataDiv.innerHTML = '<div class="alert alert-info">Emptying sensor tables... Please wait...</div>';
|
||||||
|
|
||||||
|
// Make AJAX request to empty tables
|
||||||
|
$.ajax({
|
||||||
|
url: 'launcher.php?type=empty_sensor_tables',
|
||||||
|
dataType: 'json',
|
||||||
|
method: 'GET',
|
||||||
|
success: function(response) {
|
||||||
|
console.log("Empty sensor tables response:", response);
|
||||||
|
|
||||||
|
if (response.success) {
|
||||||
|
// Show success message
|
||||||
|
let message = '<div class="alert alert-success">';
|
||||||
|
message += '<h5>Success!</h5>';
|
||||||
|
message += '<p>' + response.message + '</p>';
|
||||||
|
|
||||||
|
if (response.tables_processed && response.tables_processed.length > 0) {
|
||||||
|
message += '<p><strong>Tables emptied:</strong></p><ul>';
|
||||||
|
response.tables_processed.forEach(table => {
|
||||||
|
message += `<li>${table.name}: ${table.deleted} records deleted</li>`;
|
||||||
|
});
|
||||||
|
message += '</ul>';
|
||||||
|
}
|
||||||
|
|
||||||
|
message += '</div>';
|
||||||
|
tableDataDiv.innerHTML = message;
|
||||||
|
} else {
|
||||||
|
// Show error message
|
||||||
|
tableDataDiv.innerHTML = `<div class="alert alert-danger">
|
||||||
|
<h5>Error!</h5>
|
||||||
|
<p>${response.message || response.error || 'Unknown error occurred'}</p>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: function(xhr, status, error) {
|
||||||
|
console.error('AJAX request failed:', status, error);
|
||||||
|
tableDataDiv.innerHTML = `<div class="alert alert-danger">
|
||||||
|
<h5>Error!</h5>
|
||||||
|
<p>Failed to empty sensor tables: ${error}</p>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
307
html/index.html
307
html/index.html
@@ -1,39 +1,50 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>NebuleAir</title>
|
<title>NebuleAir</title>
|
||||||
<link rel="stylesheet" href="assets/css/bootstrap.min.css">
|
<link rel="stylesheet" href="assets/css/bootstrap.min.css">
|
||||||
|
<script src="assets/js/chart.js"></script> <!-- Local Chart.js -->
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
body {
|
body {
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
#sidebar a.nav-link {
|
#sidebar a.nav-link {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
#sidebar a.nav-link:hover {
|
#sidebar a.nav-link:hover {
|
||||||
background-color: rgba(0, 0, 0, 0.5);
|
background-color: rgba(0, 0, 0, 0.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
#sidebar a.nav-link svg {
|
#sidebar a.nav-link svg {
|
||||||
margin-right: 8px; /* Add spacing between icons and text */
|
margin-right: 8px;
|
||||||
|
/* Add spacing between icons and text */
|
||||||
}
|
}
|
||||||
|
|
||||||
#sidebar {
|
#sidebar {
|
||||||
transition: transform 0.3s ease-in-out;
|
transition: transform 0.3s ease-in-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
.offcanvas-backdrop {
|
.offcanvas-backdrop {
|
||||||
z-index: 1040;
|
z-index: 1040;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<!-- Topbar -->
|
<!-- Topbar -->
|
||||||
<span id="topbar"></span>
|
<span id="topbar"></span>
|
||||||
|
|
||||||
<!-- Sidebar Offcanvas for Mobile -->
|
<!-- Sidebar Offcanvas for Mobile -->
|
||||||
<div class="offcanvas offcanvas-start text-white bg-dark" tabindex="-1" id="sidebarOffcanvas" aria-labelledby="sidebarOffcanvasLabel">
|
<div class="offcanvas offcanvas-start text-white bg-dark" tabindex="-1" id="sidebarOffcanvas"
|
||||||
|
aria-labelledby="sidebarOffcanvasLabel">
|
||||||
<div class="offcanvas-header">
|
<div class="offcanvas-header">
|
||||||
<h5 class="offcanvas-title" id="sidebarOffcanvasLabel">NebuleAir</h5>
|
<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>
|
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="offcanvas" aria-label="Close"></button>
|
||||||
@@ -49,39 +60,81 @@
|
|||||||
</aside>
|
</aside>
|
||||||
<!-- Main content -->
|
<!-- Main content -->
|
||||||
<main class="col-md-9 ms-sm-auto col-lg-10 offset-md-3 offset-lg-2 px-md-4">
|
<main class="col-md-9 ms-sm-auto col-lg-10 offset-md-3 offset-lg-2 px-md-4">
|
||||||
<h1 class="mt-4">Votre capteur</h1>
|
<h1 class="mt-4" data-i18n="home.title">Votre capteur</h1>
|
||||||
<p>Bienvenue sur votre interface de configuration de votre capteur.</p>
|
<p data-i18n="home.welcome">Bienvenue sur votre interface de configuration de votre capteur.</p>
|
||||||
|
|
||||||
|
<button class="btn btn-success mb-3 btn_selfTest" onclick="runSelfTest()">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-check2-circle me-1" viewBox="0 0 16 16">
|
||||||
|
<path d="M2.5 8a5.5 5.5 0 1 1 11 0 5.5 5.5 0 0 1-11 0z"/>
|
||||||
|
<path d="M15.354 3.354a.5.5 0 0 0-.708-.708L8 9.293 5.354 6.646a.5.5 0 1 0-.708.708l3 3a.5.5 0 0 0 .708 0l7-7z"/>
|
||||||
|
</svg>
|
||||||
|
Run Self Test
|
||||||
|
</button>
|
||||||
|
|
||||||
<div class="row mb-3">
|
<div class="row mb-3">
|
||||||
|
|
||||||
<div class="col-sm-4">
|
<!-- Card NPM values -->
|
||||||
|
<div class="col-sm-4 mt-2">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
|
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h5 class="card-title">Linux stats</h5>
|
<h5 class="card-title" data-i18n="home.pmMeasures">Mesures PM</h5>
|
||||||
|
<canvas id="sensorPMChart" style="width: 100%; max-width: 600px; height: 200px;"></canvas>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<p class="card-text">Disk usage (total size <span id="disk_size"></span> Gb) </p>
|
<!-- Card Linux Stats -->
|
||||||
|
<div class="col-sm-4 mt-2">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="card-title" data-i18n="home.linuxStats">Statistiques Linux</h5>
|
||||||
|
<p class="card-text"><span data-i18n="home.diskUsage">Utilisation du disque (taille totale</span> <span
|
||||||
|
id="disk_size"></span> Gb) </p>
|
||||||
<div id="disk_space"></div>
|
<div id="disk_space"></div>
|
||||||
|
<p class="card-text"><span data-i18n="home.memoryUsage">Utilisation de la mémoire (taille totale</span>
|
||||||
<p class="card-text">Memory usage (total size <span id="memory_size"></span> Mb) </p>
|
<span id="memory_size"></span> Mb)
|
||||||
|
</p>
|
||||||
<div id="memory_space"></div>
|
<div id="memory_space"></div>
|
||||||
|
<p class="card-text"><span data-i18n="home.databaseSize">Taille de la base de données:</span> <span
|
||||||
|
id="database_size"></span> </p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
<div class="row mb-3">
|
||||||
|
|
||||||
|
<div class="col-sm-4 mt-2">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="card-title">Mesures Temperature</h5>
|
||||||
|
<canvas id="sensorBME_temp" style="width: 100%; max-width: 600px; height: 200px;"></canvas>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
-->
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- JAVASCRIPT -->
|
<!-- JAVASCRIPT -->
|
||||||
|
|
||||||
<!-- Link Ajax locally -->
|
<!-- Link Ajax locally -->
|
||||||
<script src="assets/jquery/jquery-3.7.1.min.js"></script>
|
<script src="assets/jquery/jquery-3.7.1.min.js"></script>
|
||||||
<!-- Link Bootstrap JS and Popper.js locally -->
|
<!-- Link Bootstrap JS and Popper.js locally -->
|
||||||
<script src="assets/js/bootstrap.bundle.js"></script>
|
<script src="assets/js/bootstrap.bundle.js"></script>
|
||||||
|
<!-- i18n translation system -->
|
||||||
|
<script src="assets/js/i18n.js"></script>
|
||||||
|
<script src="assets/js/topbar-logo.js"></script>
|
||||||
|
<script src="assets/js/selftest.js"></script>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
document.addEventListener('DOMContentLoaded', function () {
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
const elementsToLoad = [
|
const elementsToLoad = [
|
||||||
{ id: 'topbar', file: 'topbar.html' },
|
{ id: 'topbar', file: 'topbar.html' },
|
||||||
@@ -96,17 +149,57 @@
|
|||||||
const element = document.getElementById(id);
|
const element = document.getElementById(id);
|
||||||
if (element) {
|
if (element) {
|
||||||
element.innerHTML = data;
|
element.innerHTML = data;
|
||||||
|
// Apply translations after loading dynamic content
|
||||||
|
if (window.i18n && typeof window.i18n.applyTranslations === 'function') {
|
||||||
|
window.i18n.applyTranslations();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(error => console.error(`Error loading ${file}:`, error));
|
.catch(error => console.error(`Error loading ${file}:`, error));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
window.onload = function() {
|
window.onload = function () {
|
||||||
|
|
||||||
|
//NEW way to get data from SQLITE
|
||||||
|
$.ajax({
|
||||||
|
url: 'launcher.php?type=get_config_sqlite',
|
||||||
|
dataType: 'json',
|
||||||
|
//dataType: 'json', // Specify that you expect a JSON response
|
||||||
|
method: 'GET', // Use GET or POST depending on your needs
|
||||||
|
success: function (response) {
|
||||||
|
console.log("Getting SQLite config table:");
|
||||||
|
console.log(response);
|
||||||
|
|
||||||
|
//get device Name (for the side bar)
|
||||||
|
const deviceName = response.deviceName;
|
||||||
|
const elements = document.querySelectorAll('.sideBar_sensorName');
|
||||||
|
elements.forEach((element) => {
|
||||||
|
element.innerText = deviceName;
|
||||||
|
});
|
||||||
|
|
||||||
|
//device name html page title
|
||||||
|
if (response.deviceName) {
|
||||||
|
document.title = response.deviceName;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for device type to show Screen tab
|
||||||
|
// Assuming the key in config is 'device_type' or 'type'
|
||||||
|
if (response.device_type === 'moduleair_pro' || response.type === 'moduleair_pro') {
|
||||||
|
$('.nav-screen-item').show();
|
||||||
|
$('.nav-screen-item').css('display', 'flex'); // Ensure flex display to match others
|
||||||
|
}
|
||||||
|
|
||||||
|
},
|
||||||
|
error: function (xhr, status, error) {
|
||||||
|
console.error('AJAX request failed:', status, error);
|
||||||
|
}
|
||||||
|
}); //end ajax
|
||||||
|
|
||||||
|
/* OLD way of getting config data
|
||||||
fetch('../config.json') // Replace 'deviceID.txt' with 'config.json'
|
fetch('../config.json') // Replace 'deviceID.txt' with 'config.json'
|
||||||
.then(response => response.json()) // Parse response as JSON
|
.then(response => response.json()) // Parse response as JSON
|
||||||
.then(data => {
|
.then(data => {
|
||||||
@@ -124,27 +217,60 @@ window.onload = function() {
|
|||||||
element.innerText = deviceName;
|
element.innerText = deviceName;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
//end fetch config
|
||||||
|
})
|
||||||
|
.catch(error => console.error('Error loading config.json:', error));
|
||||||
|
//end windows on load
|
||||||
|
*/
|
||||||
//get local RTC
|
//get local RTC
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: 'launcher.php?type=RTC_time',
|
url: 'launcher.php?type=RTC_time',
|
||||||
dataType: 'text', // Specify that you expect a JSON response
|
dataType: 'text', // Specify that you expect a JSON response
|
||||||
method: 'GET', // Use GET or POST depending on your needs
|
method: 'GET', // Use GET or POST depending on your needs
|
||||||
success: function(response) {
|
success: function (response) {
|
||||||
console.log("Local RTC: " + response);
|
console.log("Local RTC: " + response);
|
||||||
const RTC_Element = document.getElementById("RTC_time");
|
const RTC_Element = document.getElementById("RTC_time");
|
||||||
RTC_Element.textContent = response;
|
RTC_Element.textContent = response;
|
||||||
},
|
},
|
||||||
error: function(xhr, status, error) {
|
error: function (xhr, status, error) {
|
||||||
console.error('AJAX request failed:', status, error);
|
console.error('AJAX request failed:', status, error);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
//get database size
|
||||||
|
$.ajax({
|
||||||
|
url: 'launcher.php?type=database_size',
|
||||||
|
dataType: 'json', // Specify that you expect a JSON response
|
||||||
|
method: 'GET', // Use GET or POST depending on your needs
|
||||||
|
success: function (response) {
|
||||||
|
console.log(response);
|
||||||
|
|
||||||
|
if (response.size_megabytes !== undefined) {
|
||||||
|
// Extract and format the size in MB
|
||||||
|
const databaseSizeMB = response.size_megabytes + " MB";
|
||||||
|
|
||||||
|
// Update the HTML element with the database size
|
||||||
|
const databaseSizeElement = document.getElementById("database_size");
|
||||||
|
databaseSizeElement.textContent = databaseSizeMB;
|
||||||
|
|
||||||
|
console.log("Database size:", databaseSizeMB);
|
||||||
|
} else if (response.error) {
|
||||||
|
// Handle errors from the PHP response
|
||||||
|
console.error("Error from server:", response.error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: function (xhr, status, error) {
|
||||||
|
console.error('AJAX request failed:', status, error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
//get disk free space
|
//get disk free space
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: 'launcher.php?type=linux_disk',
|
url: 'launcher.php?type=linux_disk',
|
||||||
dataType: 'text', // Specify that you expect a JSON response
|
dataType: 'text', // Specify that you expect a JSON response
|
||||||
method: 'GET', // Use GET or POST depending on your needs
|
method: 'GET', // Use GET or POST depending on your needs
|
||||||
success: function(response) {
|
success: function (response) {
|
||||||
console.log("Linux disk space: " + response);
|
console.log("Linux disk space: " + response);
|
||||||
//1. disk size
|
//1. disk size
|
||||||
const disk_size = document.getElementById("disk_size");
|
const disk_size = document.getElementById("disk_size");
|
||||||
@@ -178,7 +304,7 @@ window.onload = function() {
|
|||||||
diskSpace.appendChild(progressDiv);
|
diskSpace.appendChild(progressDiv);
|
||||||
|
|
||||||
},
|
},
|
||||||
error: function(xhr, status, error) {
|
error: function (xhr, status, error) {
|
||||||
console.error('AJAX request failed:', status, error);
|
console.error('AJAX request failed:', status, error);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -188,7 +314,7 @@ window.onload = function() {
|
|||||||
url: 'launcher.php?type=linux_memory',
|
url: 'launcher.php?type=linux_memory',
|
||||||
dataType: 'text', // Specify that you expect a JSON response
|
dataType: 'text', // Specify that you expect a JSON response
|
||||||
method: 'GET', // Use GET or POST depending on your needs
|
method: 'GET', // Use GET or POST depending on your needs
|
||||||
success: function(response) {
|
success: function (response) {
|
||||||
console.log("Linux memory space: " + response);
|
console.log("Linux memory space: " + response);
|
||||||
//1. memory size
|
//1. memory size
|
||||||
const memory_size = document.getElementById("memory_size");
|
const memory_size = document.getElementById("memory_size");
|
||||||
@@ -210,8 +336,6 @@ window.onload = function() {
|
|||||||
console.log(usedMemory);
|
console.log(usedMemory);
|
||||||
console.log(percentageUsed);
|
console.log(percentageUsed);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Create the outer div with class and attributes
|
// Create the outer div with class and attributes
|
||||||
const progressDiv = document.createElement('div');
|
const progressDiv = document.createElement('div');
|
||||||
progressDiv.className = 'progress mb-3';
|
progressDiv.className = 'progress mb-3';
|
||||||
@@ -234,18 +358,143 @@ window.onload = function() {
|
|||||||
memorySpace.appendChild(progressDiv);
|
memorySpace.appendChild(progressDiv);
|
||||||
|
|
||||||
},
|
},
|
||||||
error: function(xhr, status, error) {
|
error: function (xhr, status, error) {
|
||||||
console.error('AJAX request failed:', status, error);
|
console.error('AJAX request failed:', status, error);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
// GET NPM SQLite values
|
||||||
|
$.ajax({
|
||||||
|
url: 'launcher.php?type=get_npm_sqlite_data',
|
||||||
|
dataType: 'json', // Specify that you expect a JSON response
|
||||||
|
method: 'GET', // Use GET or POST depending on your needs
|
||||||
|
success: function (response) {
|
||||||
|
console.log(response);
|
||||||
|
updatePMChart(response);
|
||||||
|
},
|
||||||
|
error: function (xhr, status, error) {
|
||||||
|
console.error('AJAX request failed:', status, error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
})
|
let chart; // Store the Chart.js instance globally
|
||||||
.catch(error => console.error('Error loading config.json:', error));
|
|
||||||
|
function updatePMChart(data) {
|
||||||
|
const labels = data.map(d => d.timestamp);
|
||||||
|
const PM1 = data.map(d => d.PM1);
|
||||||
|
const PM25 = data.map(d => d.PM25);
|
||||||
|
const PM10 = data.map(d => d.PM10);
|
||||||
|
|
||||||
|
const ctx = document.getElementById('sensorPMChart').getContext('2d');
|
||||||
|
|
||||||
|
if (!chart) {
|
||||||
|
chart = new Chart(ctx, {
|
||||||
|
type: 'line',
|
||||||
|
data: {
|
||||||
|
labels: labels,
|
||||||
|
datasets: [
|
||||||
|
{
|
||||||
|
label: "PM1",
|
||||||
|
data: PM1,
|
||||||
|
borderColor: "rgba(0, 51, 153, 1)",
|
||||||
|
backgroundColor: "rgba(0, 51, 153, 0.2)", // Very light blue background
|
||||||
|
fill: true,
|
||||||
|
tension: 0.4, // Smooth curves
|
||||||
|
pointRadius: 2, // Larger points
|
||||||
|
pointHoverRadius: 6 // Bigger hover points
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "PM2.5",
|
||||||
|
data: PM25,
|
||||||
|
borderColor: "rgba(30, 144, 255, 1)",
|
||||||
|
backgroundColor: "rgba(30, 144, 255, 0.2)", // Very light medium blue background
|
||||||
|
fill: true,
|
||||||
|
tension: 0.4,
|
||||||
|
pointRadius: 2,
|
||||||
|
pointHoverRadius: 6
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "PM10",
|
||||||
|
data: PM10,
|
||||||
|
borderColor: "rgba(135, 206, 250, 1)",
|
||||||
|
backgroundColor: "rgba(135, 206, 250, 0.2)", // Very light blue background
|
||||||
|
fill: true,
|
||||||
|
tension: 0.4,
|
||||||
|
pointRadius: 2,
|
||||||
|
pointHoverRadius: 6
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: true,
|
||||||
|
plugins: {
|
||||||
|
legend: {
|
||||||
|
position: 'top'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
scales: {
|
||||||
|
x: {
|
||||||
|
title: {
|
||||||
|
display: true,
|
||||||
|
text: 'Time (UTC)',
|
||||||
|
font: {
|
||||||
|
size: 16,
|
||||||
|
family: 'Arial, sans-serif'
|
||||||
|
},
|
||||||
|
color: '#4A4A4A'
|
||||||
|
},
|
||||||
|
ticks: {
|
||||||
|
autoSkip: true,
|
||||||
|
maxTicksLimit: 5,
|
||||||
|
color: '#4A4A4A',
|
||||||
|
callback: function (value, index) {
|
||||||
|
// Access the correct label from the `labels` array
|
||||||
|
const label = labels[index]; // Use the original `labels` array
|
||||||
|
if (label && typeof label === 'string' && label.includes(' ')) {
|
||||||
|
return label.split(' ')[1].slice(0, 5); // Extract "HH:MM"
|
||||||
|
}
|
||||||
|
return value; // Fallback for invalid labels
|
||||||
|
}
|
||||||
|
},
|
||||||
|
grid: {
|
||||||
|
display: false // Remove gridlines for a cleaner look
|
||||||
}
|
}
|
||||||
|
|
||||||
</script>
|
|
||||||
|
},
|
||||||
|
y: {
|
||||||
|
title: {
|
||||||
|
display: true,
|
||||||
|
text: 'Values (µg/m³)',
|
||||||
|
font: {
|
||||||
|
size: 16,
|
||||||
|
family: 'Arial, sans-serif'
|
||||||
|
},
|
||||||
|
color: '#4A4A4A'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
chart.data.labels = labels;
|
||||||
|
chart.data.datasets[0].data = PM1;
|
||||||
|
chart.data.datasets[1].data = PM25;
|
||||||
|
chart.data.datasets[2].data = PM10;
|
||||||
|
chart.update();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
247
html/lang/README.md
Normal file
247
html/lang/README.md
Normal file
@@ -0,0 +1,247 @@
|
|||||||
|
# NebuleAir i18n System
|
||||||
|
|
||||||
|
Lightweight internationalization (i18n) system for NebuleAir web interface.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- **Offline-first**: Works completely offline with local JSON translation files
|
||||||
|
- **Database-backed**: Language preference stored in SQLite `config_table`
|
||||||
|
- **Automatic**: Translations apply on page load and when language changes
|
||||||
|
- **Simple API**: Easy-to-use data attributes and JavaScript API
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
### 1. Include i18n.js in your HTML page
|
||||||
|
|
||||||
|
```html
|
||||||
|
<script src="assets/js/i18n.js"></script>
|
||||||
|
```
|
||||||
|
|
||||||
|
The i18n system will automatically initialize when the page loads.
|
||||||
|
|
||||||
|
### 2. Add translation keys to HTML elements
|
||||||
|
|
||||||
|
Use the `data-i18n` attribute to mark elements for translation:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<h1 data-i18n="page.title">Titre en français</h1>
|
||||||
|
<p data-i18n="page.description">Description en français</p>
|
||||||
|
<button data-i18n="common.submit">Soumettre</button>
|
||||||
|
```
|
||||||
|
|
||||||
|
The text content serves as a fallback if translations aren't loaded.
|
||||||
|
|
||||||
|
### 3. Add translations to JSON files
|
||||||
|
|
||||||
|
Edit `lang/fr.json` and `lang/en.json`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"page": {
|
||||||
|
"title": "Mon Titre",
|
||||||
|
"description": "Ma description"
|
||||||
|
},
|
||||||
|
"common": {
|
||||||
|
"submit": "Soumettre"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Translation keys use dot notation for nested objects.
|
||||||
|
|
||||||
|
## Translation Files
|
||||||
|
|
||||||
|
- **`fr.json`**: French translations (default)
|
||||||
|
- **`en.json`**: English translations
|
||||||
|
|
||||||
|
### File Structure Example
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"common": {
|
||||||
|
"save": "Enregistrer",
|
||||||
|
"cancel": "Annuler",
|
||||||
|
"delete": "Supprimer"
|
||||||
|
},
|
||||||
|
"navigation": {
|
||||||
|
"home": "Accueil",
|
||||||
|
"settings": "Paramètres"
|
||||||
|
},
|
||||||
|
"sensors": {
|
||||||
|
"title": "Capteurs",
|
||||||
|
"description": "Liste des capteurs"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## JavaScript API
|
||||||
|
|
||||||
|
### Get Current Language
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const currentLang = i18n.currentLang; // 'fr' or 'en'
|
||||||
|
```
|
||||||
|
|
||||||
|
### Change Language Programmatically
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
await i18n.setLanguage('en'); // Switch to English
|
||||||
|
```
|
||||||
|
|
||||||
|
### Get Translation in JavaScript
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const translation = i18n.get('sensors.title'); // Returns translated string
|
||||||
|
```
|
||||||
|
|
||||||
|
### Manual Translation Application
|
||||||
|
|
||||||
|
If you dynamically create HTML elements, call `applyTranslations()` after adding them to the DOM:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Create new element
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.setAttribute('data-i18n', 'mypage.newElement');
|
||||||
|
div.textContent = 'Fallback text';
|
||||||
|
document.body.appendChild(div);
|
||||||
|
|
||||||
|
// Apply translations
|
||||||
|
i18n.applyTranslations();
|
||||||
|
```
|
||||||
|
|
||||||
|
### Listen for Language Changes
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
document.addEventListener('languageChanged', (event) => {
|
||||||
|
console.log('Language changed to:', event.detail.language);
|
||||||
|
// Reload dynamic content, update charts, etc.
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Special Cases
|
||||||
|
|
||||||
|
### Input Placeholders
|
||||||
|
|
||||||
|
For input fields, the translation applies to the `placeholder` attribute:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<input type="text" data-i18n="form.emailPlaceholder" placeholder="Email...">
|
||||||
|
```
|
||||||
|
|
||||||
|
### Button Values
|
||||||
|
|
||||||
|
For input buttons, the translation applies to the `value` attribute:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<input type="submit" data-i18n="common.submit" value="Submit">
|
||||||
|
```
|
||||||
|
|
||||||
|
### Dynamic Content
|
||||||
|
|
||||||
|
For content created with JavaScript (like sensor cards), add `data-i18n` attributes to your template strings and call `i18n.applyTranslations()` after inserting into the DOM.
|
||||||
|
|
||||||
|
## Example: Migrating an Existing Page
|
||||||
|
|
||||||
|
### Before (French only):
|
||||||
|
|
||||||
|
```html
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Capteurs</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Liste des capteurs</h1>
|
||||||
|
<button onclick="getData()">Obtenir les données</button>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
```
|
||||||
|
|
||||||
|
### After (Multilingual):
|
||||||
|
|
||||||
|
```html
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title data-i18n="sensors.pageTitle">Capteurs</title>
|
||||||
|
<script src="assets/js/i18n.js"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1 data-i18n="sensors.title">Liste des capteurs</h1>
|
||||||
|
<button onclick="getData()" data-i18n="common.getData">Obtenir les données</button>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Add to `lang/fr.json`:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"sensors": {
|
||||||
|
"pageTitle": "Capteurs",
|
||||||
|
"title": "Liste des capteurs"
|
||||||
|
},
|
||||||
|
"common": {
|
||||||
|
"getData": "Obtenir les données"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Add to `lang/en.json`:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"sensors": {
|
||||||
|
"pageTitle": "Sensors",
|
||||||
|
"title": "Sensor List"
|
||||||
|
},
|
||||||
|
"common": {
|
||||||
|
"getData": "Get Data"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Backend Integration
|
||||||
|
|
||||||
|
### Get Language Preference
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const response = await fetch('launcher.php?type=get_language');
|
||||||
|
const data = await response.json();
|
||||||
|
console.log(data.language); // 'fr' or 'en'
|
||||||
|
```
|
||||||
|
|
||||||
|
### Set Language Preference
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const response = await fetch('launcher.php?type=set_language&language=en');
|
||||||
|
const data = await response.json();
|
||||||
|
console.log(data.success); // true
|
||||||
|
```
|
||||||
|
|
||||||
|
Language preference is stored in SQLite `config_table` with key `language`.
|
||||||
|
|
||||||
|
## Completed Pages
|
||||||
|
|
||||||
|
- ✅ **sensors.html** - Fully translated with French/English support
|
||||||
|
|
||||||
|
## TODO: Pages to Migrate
|
||||||
|
|
||||||
|
- ⏳ index.html
|
||||||
|
- ⏳ admin.html
|
||||||
|
- ⏳ wifi.html
|
||||||
|
- ⏳ saraR4.html
|
||||||
|
- ⏳ map.html
|
||||||
|
|
||||||
|
## Tips
|
||||||
|
|
||||||
|
1. **Reuse common translations**: Put frequently used strings (buttons, actions, status messages) in the `common` section
|
||||||
|
2. **Keep keys descriptive**: Use `sensors.bme280.title` instead of `s1` for maintainability
|
||||||
|
3. **Test both languages**: Always verify that both French and English translations display correctly
|
||||||
|
4. **Fallback text**: Always provide fallback text in HTML for graceful degradation
|
||||||
|
|
||||||
|
## Support
|
||||||
|
|
||||||
|
For issues or questions about the i18n system, refer to the implementation in:
|
||||||
|
- `/html/assets/js/i18n.js` - Core translation library
|
||||||
|
- `/html/lang/fr.json` - French translations
|
||||||
|
- `/html/lang/en.json` - English translations
|
||||||
|
- `/html/sensors.html` - Example implementation
|
||||||
113
html/lang/en.json
Normal file
113
html/lang/en.json
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
{
|
||||||
|
"common": {
|
||||||
|
"getData": "Get Data",
|
||||||
|
"loading": "Loading...",
|
||||||
|
"error": "Error",
|
||||||
|
"startRecording": "Start recording",
|
||||||
|
"stopRecording": "Stop recording"
|
||||||
|
},
|
||||||
|
"sensors": {
|
||||||
|
"title": "Measurement Sensors",
|
||||||
|
"description": "Your NebuleAir sensor is equipped with one or more probes that measure environmental variables. Measurements are automatic, but you can verify their operation here.",
|
||||||
|
"npm": {
|
||||||
|
"title": "NextPM",
|
||||||
|
"description": "Particulate matter sensor.",
|
||||||
|
"headerUart": "UART Port"
|
||||||
|
},
|
||||||
|
"bme280": {
|
||||||
|
"title": "BME280 Temp/Humidity Sensor",
|
||||||
|
"description": "Temperature and humidity sensor on I2C port.",
|
||||||
|
"headerI2c": "I2C Port",
|
||||||
|
"temp": "Temperature",
|
||||||
|
"hum": "Humidity",
|
||||||
|
"press": "Pressure"
|
||||||
|
},
|
||||||
|
"noise": {
|
||||||
|
"title": "NSRT MK4",
|
||||||
|
"description": "NSRT MK4 sound level meter on USB port.",
|
||||||
|
"headerUsb": "USB Port"
|
||||||
|
},
|
||||||
|
"envea": {
|
||||||
|
"title": "Envea Probe",
|
||||||
|
"description": "Gas sensor."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"wifi": {
|
||||||
|
"title": "WIFI Connection",
|
||||||
|
"description": "WIFI connection is not mandatory but it allows you to perform updates and enable remote control.",
|
||||||
|
"status": "Status",
|
||||||
|
"connected": "Connected",
|
||||||
|
"hotspot": "Hotspot",
|
||||||
|
"disconnected": "Disconnected",
|
||||||
|
"scan": "Scan",
|
||||||
|
"connect": "Connect",
|
||||||
|
"enterPassword": "Enter password for"
|
||||||
|
},
|
||||||
|
"admin": {
|
||||||
|
"title": "Administration",
|
||||||
|
"parameters": "Parameters (config)",
|
||||||
|
"deviceName": "Device Name",
|
||||||
|
"deviceID": "Device ID",
|
||||||
|
"modemVersion": "Modem Version"
|
||||||
|
},
|
||||||
|
"sidebar": {
|
||||||
|
"home": "Home",
|
||||||
|
"screen": "Screen",
|
||||||
|
"sensors": "Sensors",
|
||||||
|
"database": "Database",
|
||||||
|
"modem4g": "4G Modem",
|
||||||
|
"wifi": "WIFI",
|
||||||
|
"logs": "Logs",
|
||||||
|
"map": "Map",
|
||||||
|
"terminal": "Terminal",
|
||||||
|
"admin": "Admin"
|
||||||
|
},
|
||||||
|
"home": {
|
||||||
|
"title": "Your Sensor",
|
||||||
|
"welcome": "Welcome to your sensor configuration interface.",
|
||||||
|
"pmMeasures": "PM Measurements",
|
||||||
|
"linuxStats": "Linux Statistics",
|
||||||
|
"diskUsage": "Disk usage (total size",
|
||||||
|
"memoryUsage": "Memory usage (total size",
|
||||||
|
"databaseSize": "Database size:"
|
||||||
|
},
|
||||||
|
"database": {
|
||||||
|
"title": "Database",
|
||||||
|
"description": "The sensor records measurement data locally. You can view and download it here.",
|
||||||
|
"viewDatabase": "View Database",
|
||||||
|
"numberOfMeasures": "Number of measurements:",
|
||||||
|
"last10": "Last 10",
|
||||||
|
"last20": "Last 20",
|
||||||
|
"last30": "Last 30",
|
||||||
|
"pmMeasures": "PM Measurements",
|
||||||
|
"tempHumMeasures": "Temp/Hum Measurements",
|
||||||
|
"pm5Channels": "PM Measurements (5 channels)",
|
||||||
|
"cairsensProbe": "Cairsens Probe",
|
||||||
|
"noiseProbe": "Noise Probe",
|
||||||
|
"windProbe": "Wind Probe",
|
||||||
|
"battery": "Battery",
|
||||||
|
"timestampTable": "Timestamp Table",
|
||||||
|
"downloadData": "Download Data",
|
||||||
|
"startDate": "Start date:",
|
||||||
|
"endDate": "End date:",
|
||||||
|
"dangerZone": "Danger Zone",
|
||||||
|
"dangerWarning": "Warning: This action is irreversible!",
|
||||||
|
"emptyAllTables": "Empty all sensor tables",
|
||||||
|
"emptyTablesNote": "Note: Configuration and timestamp tables will be preserved.",
|
||||||
|
"statsTitle": "Database Information",
|
||||||
|
"statsDbSize": "Total size:",
|
||||||
|
"statsTable": "Table",
|
||||||
|
"statsCount": "Entries",
|
||||||
|
"statsOldest": "Oldest",
|
||||||
|
"statsNewest": "Newest",
|
||||||
|
"statsDownload": "CSV"
|
||||||
|
},
|
||||||
|
"logs": {
|
||||||
|
"title": "The Log",
|
||||||
|
"description": "The log allows you to know if the sensor processes are running correctly.",
|
||||||
|
"saraLogs": "Sara logs",
|
||||||
|
"bootLogs": "Boot logs",
|
||||||
|
"refresh": "Refresh",
|
||||||
|
"clear": "Clear"
|
||||||
|
}
|
||||||
|
}
|
||||||
113
html/lang/fr.json
Normal file
113
html/lang/fr.json
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
{
|
||||||
|
"common": {
|
||||||
|
"getData": "Obtenir les données",
|
||||||
|
"loading": "Chargement...",
|
||||||
|
"error": "Erreur",
|
||||||
|
"startRecording": "Démarrer l'enregistrement",
|
||||||
|
"stopRecording": "Arrêter l'enregistrement"
|
||||||
|
},
|
||||||
|
"sensors": {
|
||||||
|
"title": "Les sondes de mesure",
|
||||||
|
"description": "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.",
|
||||||
|
"npm": {
|
||||||
|
"title": "NextPM",
|
||||||
|
"description": "Capteur particules fines.",
|
||||||
|
"headerUart": "Port UART"
|
||||||
|
},
|
||||||
|
"bme280": {
|
||||||
|
"title": "Capteur Temp/Humidité BME280",
|
||||||
|
"description": "Capteur température et humidité sur le port I2C.",
|
||||||
|
"headerI2c": "Port I2C",
|
||||||
|
"temp": "Température",
|
||||||
|
"hum": "Humidité",
|
||||||
|
"press": "Pression"
|
||||||
|
},
|
||||||
|
"noise": {
|
||||||
|
"title": "NSRT MK4",
|
||||||
|
"description": "Sonomètre NSRT MK4 sur port USB.",
|
||||||
|
"headerUsb": "Port USB"
|
||||||
|
},
|
||||||
|
"envea": {
|
||||||
|
"title": "Sonde Envea",
|
||||||
|
"description": "Capteur gaz."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"wifi": {
|
||||||
|
"title": "Connexion WIFI",
|
||||||
|
"description": "La connexion WIFI n'est pas obligatoire mais elle vous permet d'effectuer des mises à jour et d'activer le contrôle à distance.",
|
||||||
|
"status": "Statut",
|
||||||
|
"connected": "Connecté",
|
||||||
|
"hotspot": "Point d'accès",
|
||||||
|
"disconnected": "Déconnecté",
|
||||||
|
"scan": "Scanner",
|
||||||
|
"connect": "Se connecter",
|
||||||
|
"enterPassword": "Entrer le mot de passe pour"
|
||||||
|
},
|
||||||
|
"admin": {
|
||||||
|
"title": "Administration",
|
||||||
|
"parameters": "Paramètres (config)",
|
||||||
|
"deviceName": "Nom de l'appareil",
|
||||||
|
"deviceID": "ID de l'appareil",
|
||||||
|
"modemVersion": "Version du modem"
|
||||||
|
},
|
||||||
|
"sidebar": {
|
||||||
|
"home": "Accueil",
|
||||||
|
"screen": "Écran",
|
||||||
|
"sensors": "Capteurs",
|
||||||
|
"database": "Base de données",
|
||||||
|
"modem4g": "Modem 4G",
|
||||||
|
"wifi": "WIFI",
|
||||||
|
"logs": "Logs",
|
||||||
|
"map": "Carte",
|
||||||
|
"terminal": "Terminal",
|
||||||
|
"admin": "Admin"
|
||||||
|
},
|
||||||
|
"home": {
|
||||||
|
"title": "Votre capteur",
|
||||||
|
"welcome": "Bienvenue sur votre interface de configuration de votre capteur.",
|
||||||
|
"pmMeasures": "Mesures PM",
|
||||||
|
"linuxStats": "Statistiques Linux",
|
||||||
|
"diskUsage": "Utilisation du disque (taille totale",
|
||||||
|
"memoryUsage": "Utilisation de la mémoire (taille totale",
|
||||||
|
"databaseSize": "Taille de la base de données:"
|
||||||
|
},
|
||||||
|
"database": {
|
||||||
|
"title": "Base de données",
|
||||||
|
"description": "Le capteur enregistre en local les données de mesures. Vous pouvez ici les consulter et les télécharger.",
|
||||||
|
"viewDatabase": "Consulter la base de donnée",
|
||||||
|
"numberOfMeasures": "Nombre de mesures:",
|
||||||
|
"last10": "10 dernières",
|
||||||
|
"last20": "20 dernières",
|
||||||
|
"last30": "30 dernières",
|
||||||
|
"pmMeasures": "Mesures PM",
|
||||||
|
"tempHumMeasures": "Mesures Temp/Hum",
|
||||||
|
"pm5Channels": "Mesures PM (5 canaux)",
|
||||||
|
"cairsensProbe": "Sonde Cairsens",
|
||||||
|
"noiseProbe": "Sonde bruit",
|
||||||
|
"windProbe": "Sonde Vent",
|
||||||
|
"battery": "Batterie",
|
||||||
|
"timestampTable": "Timestamp Table",
|
||||||
|
"downloadData": "Télécharger les données",
|
||||||
|
"startDate": "Date de début:",
|
||||||
|
"endDate": "Date de fin:",
|
||||||
|
"dangerZone": "Zone dangereuse",
|
||||||
|
"dangerWarning": "Attention: Cette action est irréversible!",
|
||||||
|
"emptyAllTables": "Vider toutes les tables de capteurs",
|
||||||
|
"emptyTablesNote": "Note: Les tables de configuration et horodatage seront préservées.",
|
||||||
|
"statsTitle": "Informations sur la base",
|
||||||
|
"statsDbSize": "Taille totale:",
|
||||||
|
"statsTable": "Table",
|
||||||
|
"statsCount": "Entrées",
|
||||||
|
"statsOldest": "Plus ancienne",
|
||||||
|
"statsNewest": "Plus récente",
|
||||||
|
"statsDownload": "CSV"
|
||||||
|
},
|
||||||
|
"logs": {
|
||||||
|
"title": "Le journal",
|
||||||
|
"description": "Le journal des logs permet de savoir si les processus du capteur se déroulent correctement.",
|
||||||
|
"saraLogs": "Sara logs",
|
||||||
|
"bootLogs": "Boot logs",
|
||||||
|
"refresh": "Refresh",
|
||||||
|
"clear": "Clear"
|
||||||
|
}
|
||||||
|
}
|
||||||
1808
html/launcher.php
1808
html/launcher.php
File diff suppressed because it is too large
Load Diff
236
html/logs.html
236
html/logs.html
@@ -49,15 +49,21 @@
|
|||||||
</aside>
|
</aside>
|
||||||
<!-- Main content -->
|
<!-- Main content -->
|
||||||
<main class="col-md-10 ms-sm-auto col-lg-11 offset-md-2 offset-lg-1 px-md-4">
|
<main class="col-md-10 ms-sm-auto col-lg-11 offset-md-2 offset-lg-1 px-md-4">
|
||||||
<h1 class="mt-4">Le journal</h1>
|
<h1 class="mt-4" data-i18n="logs.title">Le journal</h1>
|
||||||
<p>Le journal des logs permet de savoir si les processus du capteur se déroulent correctement.</p>
|
<p data-i18n="logs.description">Le journal des logs permet de savoir si les processus du capteur se déroulent correctement.</p>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<!-- card 1 -->
|
<!-- card 1 -->
|
||||||
<div class="col-lg-6 col-12">
|
<div class="col-lg-6 col-12">
|
||||||
<div class="card" style="height: 80vh;">
|
<div class="card" style="height: 80vh;">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
Loop logs <button type="submit" class="btn btn-secondary btn-sm" onclick="clear_loopLogs()">Clear</button>
|
<span data-i18n="logs.saraLogs">Sara logs</span>
|
||||||
|
<button type="submit" class="btn btn-secondary btn-sm" id="refresh-master-log" data-i18n="logs.refresh">Refresh</button>
|
||||||
|
<button type="button" class="btn btn-sm" id="auto-refresh-toggle" onclick="toggleAutoRefresh()">
|
||||||
|
<span id="auto-refresh-icon">▶</span> Auto
|
||||||
|
</button>
|
||||||
|
<button type="submit" class="btn btn-secondary btn-sm" onclick="clear_loopLogs()" data-i18n="logs.clear">Clear</button>
|
||||||
|
|
||||||
|
<span id="script_running"></span>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body overflow-auto" id="card_loop_content">
|
<div class="card-body overflow-auto" id="card_loop_content">
|
||||||
|
|
||||||
@@ -68,7 +74,8 @@
|
|||||||
<div class="col-lg-6 col-12">
|
<div class="col-lg-6 col-12">
|
||||||
<div class="card" style="height: 80vh;">
|
<div class="card" style="height: 80vh;">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
Boot logs
|
<span data-i18n="logs.bootLogs">Boot logs</span>
|
||||||
|
<button type="submit" class="btn btn-secondary btn-sm" id="refresh-boot-log" data-i18n="logs.refresh">Refresh</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body overflow-auto" id="card_boot_content">
|
<div class="card-body overflow-auto" id="card_boot_content">
|
||||||
|
|
||||||
@@ -86,6 +93,9 @@
|
|||||||
<script src="assets/jquery/jquery-3.7.1.min.js"></script>
|
<script src="assets/jquery/jquery-3.7.1.min.js"></script>
|
||||||
<!-- Link Bootstrap JS and Popper.js locally -->
|
<!-- Link Bootstrap JS and Popper.js locally -->
|
||||||
<script src="assets/js/bootstrap.bundle.js"></script>
|
<script src="assets/js/bootstrap.bundle.js"></script>
|
||||||
|
<!-- i18n translation system -->
|
||||||
|
<script src="assets/js/i18n.js"></script>
|
||||||
|
<script src="assets/js/topbar-logo.js"></script>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
document.addEventListener('DOMContentLoaded', function () {
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
@@ -110,77 +120,54 @@
|
|||||||
const loop_card_content = document.getElementById('card_loop_content');
|
const loop_card_content = document.getElementById('card_loop_content');
|
||||||
const boot_card_content = document.getElementById('card_boot_content');
|
const boot_card_content = document.getElementById('card_boot_content');
|
||||||
|
|
||||||
fetch('../logs/loop.log')
|
//Getting Master logs
|
||||||
.then((response) => {
|
console.log("Getting SARA logs");
|
||||||
if (!response.ok) {
|
displayLogFile('../logs/sara_service.log', loop_card_content, true, 1000);
|
||||||
throw new Error('Failed to fetch the log file.');
|
|
||||||
}
|
|
||||||
return response.text();
|
|
||||||
})
|
|
||||||
.then((data) => {
|
|
||||||
const lines = data.split('\n');
|
|
||||||
|
|
||||||
// Format log content
|
console.log("Getting app/boot logs");
|
||||||
const formattedLog = lines
|
displayLogFile('../logs/app.log', boot_card_content, true, 1000);
|
||||||
.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>`;
|
// Setup master log with refresh button
|
||||||
loop_card_content.scrollTop = loop_card_content.scrollHeight; // Scroll to the bottom
|
setupLogRefreshButton('refresh-master-log', '../logs/sara_service.log', 'card_loop_content', 3000);
|
||||||
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
console.error(error);
|
|
||||||
loop_card_content.textContent = 'Error loading log file.';
|
|
||||||
});
|
|
||||||
|
|
||||||
fetch('../logs/app.log')
|
|
||||||
.then((response) => {
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error('Failed to fetch the log file.');
|
|
||||||
}
|
|
||||||
return response.text();
|
|
||||||
})
|
|
||||||
.then((data) => {
|
|
||||||
const lines = data.split('\n');
|
|
||||||
|
|
||||||
// Format log content
|
|
||||||
const formattedLog = lines
|
|
||||||
.map((line) => line.trim()) // Remove extra whitespace
|
|
||||||
.filter((line) => line) // Remove empty lines
|
|
||||||
.join('<br>'); // Join formatted lines with line breaks
|
|
||||||
|
|
||||||
boot_card_content.innerHTML = `<pre style="white-space: pre-wrap; word-wrap: break-word; margin: 0;">${formattedLog}</pre>`;
|
|
||||||
boot_card_content.scrollTop = loop_card_content.scrollHeight; // Scroll to the bottom
|
|
||||||
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
console.error(error);
|
|
||||||
boot_card_content.textContent = 'Error loading log file.';
|
|
||||||
});
|
|
||||||
|
|
||||||
|
// Setup boot log with refresh button
|
||||||
|
setupLogRefreshButton('refresh-boot-log', '../logs/app.log', 'card_boot_content', 300);
|
||||||
|
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
window.onload = function() {
|
window.onload = function() {
|
||||||
fetch('../config.json') // Replace 'deviceID.txt' with 'config.json'
|
getModem_busy_status();
|
||||||
.then(response => response.json()) // Parse response as JSON
|
setInterval(getModem_busy_status, 2000);
|
||||||
.then(data => {
|
|
||||||
console.log("Getting config file (onload)");
|
|
||||||
//get device ID
|
|
||||||
const deviceID = data.deviceID.trim().toUpperCase();
|
|
||||||
// document.getElementById('pageTitle_plus_ID').innerText = 'token: ' + deviceID;
|
|
||||||
//get device Name
|
|
||||||
const deviceName = data.deviceName;
|
|
||||||
|
|
||||||
|
//NEW way to get config (SQLite)
|
||||||
|
$.ajax({
|
||||||
|
url: 'launcher.php?type=get_config_sqlite',
|
||||||
|
dataType:'json',
|
||||||
|
//dataType: 'json', // Specify that you expect a JSON response
|
||||||
|
method: 'GET', // Use GET or POST depending on your needs
|
||||||
|
success: function(response) {
|
||||||
|
console.log("Getting SQLite config table:");
|
||||||
|
console.log(response);
|
||||||
|
|
||||||
|
//device name_side bar
|
||||||
const elements = document.querySelectorAll('.sideBar_sensorName');
|
const elements = document.querySelectorAll('.sideBar_sensorName');
|
||||||
elements.forEach((element) => {
|
elements.forEach((element) => {
|
||||||
element.innerText = deviceName;
|
element.innerText = response.deviceName;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
//device name html page title
|
||||||
|
if (response.deviceName) {
|
||||||
|
document.title = response.deviceName;
|
||||||
|
}
|
||||||
|
|
||||||
|
},
|
||||||
|
error: function(xhr, status, error) {
|
||||||
|
console.error('AJAX request failed:', status, error);
|
||||||
|
}
|
||||||
|
});//end AJAX
|
||||||
|
|
||||||
|
|
||||||
//get local RTC
|
//get local RTC
|
||||||
$.ajax({
|
$.ajax({
|
||||||
@@ -197,10 +184,78 @@ window.onload = function() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
}//end onload
|
||||||
|
|
||||||
|
function displayLogFile(logFilePath, containerElement, scrollToBottom = true, maxLines = 0) {
|
||||||
|
// Show loading indicator
|
||||||
|
containerElement.innerHTML = '<div class="text-center"><i>Loading log file...</i></div>';
|
||||||
|
|
||||||
|
return fetch(logFilePath)
|
||||||
|
.then((response) => {
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Failed to fetch the log file: ${response.status} ${response.statusText}`);
|
||||||
|
}
|
||||||
|
return response.text();
|
||||||
})
|
})
|
||||||
.catch(error => console.error('Error loading config.json:', error));
|
.then((data) => {
|
||||||
|
// Split the log into lines
|
||||||
|
let lines = data.split('\n');
|
||||||
|
|
||||||
|
// Apply max lines limit if specified
|
||||||
|
if (maxLines > 0 && lines.length > maxLines) {
|
||||||
|
lines = lines.slice(-maxLines); // Get only the last N lines
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Format log content
|
||||||
|
const formattedLog = lines
|
||||||
|
.map((line) => line.trim()) // Remove extra whitespace
|
||||||
|
.filter((line) => line) // Remove empty lines
|
||||||
|
.join('<br>'); // Join formatted lines with line breaks
|
||||||
|
|
||||||
|
// Display the formatted log
|
||||||
|
containerElement.innerHTML = `<pre style="white-space: pre-wrap; word-wrap: break-word; margin: 0;">${formattedLog}</pre>`;
|
||||||
|
|
||||||
|
// Scroll to bottom if requested
|
||||||
|
if (scrollToBottom) {
|
||||||
|
containerElement.scrollTop = containerElement.scrollHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
return formattedLog; // Return the formatted log in case the caller needs it
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error(`Error loading log file ${logFilePath}:`, error);
|
||||||
|
containerElement.innerHTML = `<div class="text-danger">Error loading log file: ${error.message}</div>`;
|
||||||
|
throw error; // Re-throw the error for the caller to handle if needed
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set up a refresh button for a log file
|
||||||
|
* @param {string} buttonId - ID of the button element
|
||||||
|
* @param {string} logFilePath - Path to the log file
|
||||||
|
* @param {string} containerId - ID of the container to display the log in
|
||||||
|
* @param {number} maxLines - Maximum number of lines to display (0 for all)
|
||||||
|
*/
|
||||||
|
function setupLogRefreshButton(buttonId, logFilePath, containerId, maxLines = 0) {
|
||||||
|
console.log("Refreshing logs");
|
||||||
|
|
||||||
|
const button = document.getElementById(buttonId);
|
||||||
|
const container = document.getElementById(containerId);
|
||||||
|
|
||||||
|
if (!button || !container) {
|
||||||
|
console.error('Button or container element not found');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initial load
|
||||||
|
displayLogFile(logFilePath, container, true, maxLines);
|
||||||
|
|
||||||
|
// Set up button click handler
|
||||||
|
button.addEventListener('click', () => {
|
||||||
|
displayLogFile(logFilePath, container, true, maxLines);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function clear_loopLogs(){
|
function clear_loopLogs(){
|
||||||
console.log("Clearing loop logs");
|
console.log("Clearing loop logs");
|
||||||
@@ -221,6 +276,63 @@ function clear_loopLogs(){
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Auto-refresh for SARA logs
|
||||||
|
let autoRefreshInterval = null;
|
||||||
|
const AUTO_REFRESH_MS = 3000;
|
||||||
|
|
||||||
|
function toggleAutoRefresh() {
|
||||||
|
const btn = document.getElementById('auto-refresh-toggle');
|
||||||
|
const icon = document.getElementById('auto-refresh-icon');
|
||||||
|
|
||||||
|
if (autoRefreshInterval) {
|
||||||
|
clearInterval(autoRefreshInterval);
|
||||||
|
autoRefreshInterval = null;
|
||||||
|
icon.textContent = '▶';
|
||||||
|
btn.classList.remove('btn-success');
|
||||||
|
btn.classList.add('btn-secondary');
|
||||||
|
} else {
|
||||||
|
// Refresh immediately, then every N seconds
|
||||||
|
displayLogFile('../logs/sara_service.log', document.getElementById('card_loop_content'), true, 1000);
|
||||||
|
autoRefreshInterval = setInterval(() => {
|
||||||
|
displayLogFile('../logs/sara_service.log', document.getElementById('card_loop_content'), true, 1000);
|
||||||
|
}, AUTO_REFRESH_MS);
|
||||||
|
icon.textContent = '⏸';
|
||||||
|
btn.classList.remove('btn-secondary');
|
||||||
|
btn.classList.add('btn-success');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getModem_busy_status() {
|
||||||
|
//console.log("Getting modem busy status");
|
||||||
|
|
||||||
|
const script_is_running = document.getElementById("script_running");
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
url: 'launcher.php?type=getModem_busy',
|
||||||
|
dataType: 'json', // Expecting JSON response
|
||||||
|
method: 'GET',
|
||||||
|
success: function(response) {
|
||||||
|
//console.log(response);
|
||||||
|
|
||||||
|
if (response.running) {
|
||||||
|
// Script is running → Show the Bootstrap spinner
|
||||||
|
script_is_running.innerHTML = `
|
||||||
|
<div class="spinner-border spinner-border-sm text-danger" role="status">
|
||||||
|
<span class="visually-hidden">Modem is busy...</span>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
} else {
|
||||||
|
// Script is NOT running → Show a success message (no spinner)
|
||||||
|
script_is_running.innerHTML = ``;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: function(xhr, status, error) {
|
||||||
|
console.error('AJAX request failed:', status, error);
|
||||||
|
script_is_running.innerHTML = `<span class="text-warning">Error checking status ⚠️</span>`;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
266
html/map.html
Executable file
266
html/map.html
Executable file
@@ -0,0 +1,266 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>NebuleAir</title>
|
||||||
|
<link rel="stylesheet" href="assets/css/bootstrap.min.css">
|
||||||
|
<link rel="stylesheet" href="assets/leaflet/leaflet.css" />
|
||||||
|
<script src="assets/leaflet/leaflet.js"></script>
|
||||||
|
|
||||||
|
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
#sidebar a.nav-link {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
#sidebar a.nav-link:hover {
|
||||||
|
background-color: rgba(0, 0, 0, 0.5);
|
||||||
|
}
|
||||||
|
#sidebar a.nav-link svg {
|
||||||
|
margin-right: 8px; /* Add spacing between icons and text */
|
||||||
|
}
|
||||||
|
#sidebar {
|
||||||
|
transition: transform 0.3s ease-in-out;
|
||||||
|
}
|
||||||
|
.offcanvas-backdrop {
|
||||||
|
z-index: 1040;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<!-- Topbar -->
|
||||||
|
<span id="topbar"></span>
|
||||||
|
|
||||||
|
<!-- Sidebar Offcanvas for Mobile -->
|
||||||
|
<div class="offcanvas offcanvas-start text-white bg-dark" tabindex="-1" id="sidebarOffcanvas" aria-labelledby="sidebarOffcanvasLabel">
|
||||||
|
<div class="offcanvas-header">
|
||||||
|
<h5 class="offcanvas-title" id="sidebarOffcanvasLabel">NebuleAir</h5>
|
||||||
|
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="offcanvas" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
<div class="offcanvas-body" id="sidebar_mobile">
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="container-fluid mt-5">
|
||||||
|
<div class="row">
|
||||||
|
<aside class="col-md-2 col-lg-1 d-none d-md-block vh-100 position-fixed bg-dark text-white" id="sidebar">
|
||||||
|
</aside>
|
||||||
|
<!-- Main content -->
|
||||||
|
<main class="col-md-10 ms-sm-auto col-lg-11 offset-md-2 offset-lg-1 px-md-4">
|
||||||
|
<h1 class="mt-4">Localisation</h1>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
|
||||||
|
|
||||||
|
<div class="col-sm-6 mb-2">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body">
|
||||||
|
<h3 class="mt-1">Zone localisation du capteur</h3>
|
||||||
|
<p class="card-text">Mis à jour automatiquement par le capteur. </p>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-6 mb-3">
|
||||||
|
<label for="device_name" class="form-label">Latitude</label>
|
||||||
|
<input type="text" class="form-control" id="device_latitude_raw" disabled>
|
||||||
|
</div>
|
||||||
|
<div class="col-6 mb-3">
|
||||||
|
<label for="device_name" class="form-label">Longitude</label>
|
||||||
|
<input type="text" class="form-control" id="device_longitude_raw" disabled>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="col-sm-6 mb-2">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body">
|
||||||
|
<h3 class="mt-1">Point précis</h3>
|
||||||
|
<p class="card-text">Mis à jour manuellement (sur aircarto.fr ou sur cette interface si le capteur est connecté au WIFI) </p>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-6 mb-3">
|
||||||
|
<label for="device_name" class="form-label">Latitude</label>
|
||||||
|
<input type="text" class="form-control" id="device_latitude_precision" onchange="update_config('latitude_precision', this.value)">
|
||||||
|
</div>
|
||||||
|
<div class="col-6 mb-3">
|
||||||
|
<label for="device_name" class="form-label">Longitude</label>
|
||||||
|
<input type="text" class="form-control" id="device_longitude_precision" onchange="update_config('longitude_precision', this.value)">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div id="map" style="height: 70vh;"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- JAVASCRIPT -->
|
||||||
|
|
||||||
|
<!-- Link Ajax locally -->
|
||||||
|
<script src="assets/jquery/jquery-3.7.1.min.js"></script>
|
||||||
|
<!-- Link Bootstrap JS and Popper.js locally -->
|
||||||
|
<script src="assets/js/bootstrap.bundle.js"></script>
|
||||||
|
<script src="assets/js/topbar-logo.js"></script>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
|
const elementsToLoad = [
|
||||||
|
{ id: 'topbar', file: 'topbar.html' },
|
||||||
|
{ id: 'sidebar', file: 'sidebar.html' },
|
||||||
|
{ id: 'sidebar_mobile', file: 'sidebar.html' }
|
||||||
|
];
|
||||||
|
|
||||||
|
elementsToLoad.forEach(({ id, file }) => {
|
||||||
|
fetch(file)
|
||||||
|
.then(response => response.text())
|
||||||
|
.then(data => {
|
||||||
|
const element = document.getElementById(id);
|
||||||
|
if (element) {
|
||||||
|
element.innerHTML = data;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => console.error(`Error loading ${file}:`, error));
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
let map;
|
||||||
|
let marker;
|
||||||
|
|
||||||
|
// Function to load and update map
|
||||||
|
function loadConfigAndUpdateMap() {
|
||||||
|
fetch('../config.json')
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
console.log("Getting config file (update)");
|
||||||
|
|
||||||
|
// Get device details
|
||||||
|
const deviceID = data.deviceID.trim().toUpperCase();
|
||||||
|
const deviceName = data.deviceName;
|
||||||
|
const device_latitude_precision = parseFloat(data.latitude_precision);
|
||||||
|
const device_longitude_precision = parseFloat(data.longitude_precision);
|
||||||
|
const device_latitude_raw = parseFloat(data.latitude_raw);
|
||||||
|
const device_longitude_raw = parseFloat(data.longitude_raw);
|
||||||
|
|
||||||
|
console.log("Latitude (precision): " + device_latitude_precision);
|
||||||
|
console.log("Longitude (precision): " + device_longitude_precision);
|
||||||
|
|
||||||
|
// Update input fields
|
||||||
|
document.getElementById("device_latitude_precision").value = device_latitude_precision;
|
||||||
|
document.getElementById("device_longitude_precision").value = device_longitude_precision;
|
||||||
|
document.getElementById("device_latitude_raw").value = device_latitude_raw;
|
||||||
|
document.getElementById("device_longitude_raw").value = device_longitude_raw;
|
||||||
|
|
||||||
|
// If map is not initialized, create it
|
||||||
|
if (!map) {
|
||||||
|
map = L.map('map').setView([device_latitude_precision, device_longitude_precision], 15);
|
||||||
|
|
||||||
|
L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||||
|
maxZoom: 19,
|
||||||
|
attribution: '© <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>'
|
||||||
|
}).addTo(map);
|
||||||
|
|
||||||
|
// Add draggable marker (point precision)
|
||||||
|
marker = L.marker([device_latitude_precision, device_longitude_precision], { draggable: true }).addTo(map);
|
||||||
|
|
||||||
|
//add a circle
|
||||||
|
var circle = L.circle([device_latitude_raw, device_longitude_raw], {
|
||||||
|
color: 'blue',
|
||||||
|
fillColor: '#3399FF',
|
||||||
|
fillOpacity: 0.3,
|
||||||
|
radius: 500
|
||||||
|
}).addTo(map);
|
||||||
|
|
||||||
|
// Event listener when marker is moved
|
||||||
|
marker.on('dragend', function (event) {
|
||||||
|
let newLatLng = marker.getLatLng();
|
||||||
|
console.log("Marker moved to:", newLatLng.lat, newLatLng.lng);
|
||||||
|
|
||||||
|
// Update the input fields with new values
|
||||||
|
document.getElementById("device_latitude_precision").value = newLatLng.lat;
|
||||||
|
document.getElementById("device_longitude_precision").value = newLatLng.lng;
|
||||||
|
|
||||||
|
// Call update function to save new values
|
||||||
|
update_config('latitude_precision', newLatLng.lat);
|
||||||
|
|
||||||
|
setTimeout(() => { update_config('longitude_precision', newLatLng.lng); }, 750);
|
||||||
|
});
|
||||||
|
|
||||||
|
} else {
|
||||||
|
// If the map already exists, update position
|
||||||
|
map.setView([device_latitude, device_longitude], 9);
|
||||||
|
|
||||||
|
// Move marker
|
||||||
|
marker.setLatLng([device_latitude, device_longitude]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update device name in sidebar
|
||||||
|
document.querySelectorAll('.sideBar_sensorName').forEach((element) => {
|
||||||
|
element.innerText = deviceName;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get local RTC time
|
||||||
|
$.ajax({
|
||||||
|
url: 'launcher.php?type=RTC_time',
|
||||||
|
dataType: 'text',
|
||||||
|
method: 'GET',
|
||||||
|
success: function(response) {
|
||||||
|
console.log("Local RTC: " + response);
|
||||||
|
document.getElementById("RTC_time").textContent = response;
|
||||||
|
},
|
||||||
|
error: function(xhr, status, error) {
|
||||||
|
console.error('AJAX request failed:', status, error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch(error => console.error('Error loading config.json:', error));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to update config and refresh the map
|
||||||
|
function update_config(param, value) {
|
||||||
|
console.log("Updating ", param, " : ", value);
|
||||||
|
$.ajax({
|
||||||
|
url: 'launcher.php?type=update_config¶m=' + param + '&value=' + value,
|
||||||
|
dataType: 'text',
|
||||||
|
method: 'GET',
|
||||||
|
cache: false,
|
||||||
|
success: function(response) {
|
||||||
|
console.log(response);
|
||||||
|
},
|
||||||
|
error: function(xhr, status, error) {
|
||||||
|
console.error('AJAX request failed:', status, error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load config and initialize map on page load
|
||||||
|
window.onload = function () {
|
||||||
|
loadConfigAndUpdateMap();
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
1129
html/saraR4.html
1129
html/saraR4.html
File diff suppressed because it is too large
Load Diff
179
html/screen.html
Normal file
179
html/screen.html
Normal file
@@ -0,0 +1,179 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Screen Control</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" data-i18n="screen.title">Contrôle de l'écran</h1>
|
||||||
|
<p data-i18n="screen.description">Gérer l'affichage sur l'écran HDMI.</p>
|
||||||
|
|
||||||
|
<div class="row mt-4">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="card-title">Actions</h5>
|
||||||
|
<p class="card-text">Démarrer ou arrêter l'application d'affichage sur l'écran HDMI.</p>
|
||||||
|
<button id="startBtn" class="btn btn-success m-2" onclick="controlScreen('start')">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor"
|
||||||
|
class="bi bi-play-fill" viewBox="0 0 16 16">
|
||||||
|
<path
|
||||||
|
d="m11.596 8.697-6.363 3.692c-.54.313-1.233-.066-1.233-.697V4.308c0-.63.692-1.01 1.233-.696l6.363 3.692a.802.802 0 0 1 0 1.393z" />
|
||||||
|
</svg>
|
||||||
|
Démarrer
|
||||||
|
</button>
|
||||||
|
<button id="stopBtn" class="btn btn-danger m-2" onclick="controlScreen('stop')">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor"
|
||||||
|
class="bi bi-stop-fill" viewBox="0 0 16 16">
|
||||||
|
<path
|
||||||
|
d="M5 3.5h6A1.5 1.5 0 0 1 12.5 5v6a1.5 1.5 0 0 1-1.5 1.5H5A1.5 1.5 0 0 1 3.5 11V5A1.5 1.5 0 0 1 5 3.5z" />
|
||||||
|
</svg>
|
||||||
|
Arrêter
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="status-message" class="mt-3 col-md-6"></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>
|
||||||
|
<!-- i18n translation system -->
|
||||||
|
<script src="assets/js/i18n.js"></script>
|
||||||
|
<script src="assets/js/topbar-logo.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;
|
||||||
|
// Apply translations after loading dynamic content
|
||||||
|
if (window.i18n && typeof window.i18n.applyTranslations === 'function') {
|
||||||
|
window.i18n.applyTranslations();
|
||||||
|
}
|
||||||
|
// Ensure the screen tab is visible here as well
|
||||||
|
if (id.includes('sidebar')) {
|
||||||
|
setTimeout(() => {
|
||||||
|
const navScreenElements = element.querySelectorAll('.nav-screen-item');
|
||||||
|
navScreenElements.forEach(el => el.style.display = 'flex');
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => console.error(`Error loading ${file}:`, error));
|
||||||
|
});
|
||||||
|
|
||||||
|
// Translation fallback for now if keys are missing
|
||||||
|
setTimeout(() => {
|
||||||
|
if (document.querySelector('[data-i18n="screen.title"]').innerText === "screen.title") {
|
||||||
|
document.querySelector('[data-i18n="screen.title"]').innerText = "Contrôle de l'écran";
|
||||||
|
}
|
||||||
|
if (document.querySelector('[data-i18n="screen.description"]').innerText === "screen.description") {
|
||||||
|
document.querySelector('[data-i18n="screen.description"]').innerText = "Gérer l'affichage sur l'écran HDMI.";
|
||||||
|
}
|
||||||
|
}, 500);
|
||||||
|
});
|
||||||
|
|
||||||
|
function controlScreen(action) {
|
||||||
|
console.log("Sending Screen Control Action:", action);
|
||||||
|
$.ajax({
|
||||||
|
url: 'launcher.php?type=screen_control&action=' + action,
|
||||||
|
dataType: 'text',
|
||||||
|
method: 'GET',
|
||||||
|
success: function (response) {
|
||||||
|
console.log("Server Response:", response);
|
||||||
|
if (action == 'start') {
|
||||||
|
$('#startBtn').removeClass('btn-success').addClass('btn-secondary').prop('disabled', true);
|
||||||
|
$('#stopBtn').removeClass('btn-secondary').addClass('btn-danger').prop('disabled', false);
|
||||||
|
$('#status-message').html('<div class="alert alert-success">L\'écran a été démarré. Réponse: ' + response + '</div>');
|
||||||
|
} else {
|
||||||
|
$('#startBtn').removeClass('btn-secondary').addClass('btn-success').prop('disabled', false);
|
||||||
|
$('#stopBtn').removeClass('btn-danger').addClass('btn-secondary').prop('disabled', true);
|
||||||
|
$('#status-message').html('<div class="alert alert-warning">L\'écran a été arrêté. Réponse: ' + response + '</div>');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: function (xhr, status, error) {
|
||||||
|
console.error("AJAX Error:", status, error);
|
||||||
|
$('#status-message').html('<div class="alert alert-danger">Erreur: ' + error + '</div>');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
134
html/selftest-modal.html
Normal file
134
html/selftest-modal.html
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
<!-- Self Test Modal -->
|
||||||
|
<div class="modal fade" id="selfTestModal" tabindex="-1" aria-labelledby="selfTestModalLabel" aria-hidden="true" data-bs-backdrop="static" data-bs-keyboard="false">
|
||||||
|
<div class="modal-dialog modal-dialog-centered">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title" id="selfTestModalLabel">Modem Self Test</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close" id="selfTestCloseBtn" disabled></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div id="selftest_status" class="mb-3">
|
||||||
|
<div class="d-flex align-items-center text-muted">
|
||||||
|
<div class="spinner-border spinner-border-sm me-2" role="status"></div>
|
||||||
|
<span>Preparing test...</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="list-group" id="selftest_results">
|
||||||
|
<!-- Dynamic sensor test entries will be added here -->
|
||||||
|
<div id="sensor_tests_container"></div>
|
||||||
|
|
||||||
|
<!-- Separator for communication tests -->
|
||||||
|
<div id="comm_tests_separator" class="list-group-item bg-light text-center py-1" style="display:none;">
|
||||||
|
<small class="text-muted fw-bold">COMMUNICATION</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Info: WiFi Status -->
|
||||||
|
<div class="list-group-item d-flex justify-content-between align-items-center" id="test_wifi">
|
||||||
|
<div>
|
||||||
|
<strong>WiFi / Network</strong>
|
||||||
|
<div class="small text-muted" id="test_wifi_detail">Waiting...</div>
|
||||||
|
</div>
|
||||||
|
<span id="test_wifi_status" class="badge bg-secondary">Pending</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Test: Modem Connection -->
|
||||||
|
<div class="list-group-item d-flex justify-content-between align-items-center" id="test_modem">
|
||||||
|
<div>
|
||||||
|
<strong>Modem Connection</strong>
|
||||||
|
<div class="small text-muted" id="test_modem_detail">Waiting...</div>
|
||||||
|
</div>
|
||||||
|
<span id="test_modem_status" class="badge bg-secondary">Pending</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Test: SIM Card -->
|
||||||
|
<div class="list-group-item d-flex justify-content-between align-items-center" id="test_sim">
|
||||||
|
<div>
|
||||||
|
<strong>SIM Card</strong>
|
||||||
|
<div class="small text-muted" id="test_sim_detail">Waiting...</div>
|
||||||
|
</div>
|
||||||
|
<span id="test_sim_status" class="badge bg-secondary">Pending</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Test: Signal Strength -->
|
||||||
|
<div class="list-group-item d-flex justify-content-between align-items-center" id="test_signal">
|
||||||
|
<div>
|
||||||
|
<strong>Signal Strength</strong>
|
||||||
|
<div class="small text-muted" id="test_signal_detail">Waiting...</div>
|
||||||
|
</div>
|
||||||
|
<span id="test_signal_status" class="badge bg-secondary">Pending</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Test: Network Connection -->
|
||||||
|
<div class="list-group-item d-flex justify-content-between align-items-center" id="test_network">
|
||||||
|
<div>
|
||||||
|
<strong>Network Connection</strong>
|
||||||
|
<div class="small text-muted" id="test_network_detail">Waiting...</div>
|
||||||
|
</div>
|
||||||
|
<span id="test_network_status" class="badge bg-secondary">Pending</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Logs section -->
|
||||||
|
<div class="mt-3">
|
||||||
|
<button class="btn btn-sm btn-outline-secondary" type="button" data-bs-toggle="collapse" data-bs-target="#selftest_logs_collapse">
|
||||||
|
Show detailed logs
|
||||||
|
</button>
|
||||||
|
<div class="collapse mt-2" id="selftest_logs_collapse">
|
||||||
|
<div class="card card-body bg-dark text-light" style="max-height: 250px; overflow-y: auto; font-family: monospace; font-size: 0.75rem;">
|
||||||
|
<pre id="selftest_logs" style="margin: 0; white-space: pre-wrap; word-wrap: break-word;"></pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<div id="selftest_summary" class="me-auto"></div>
|
||||||
|
<button type="button" class="btn btn-primary" id="selfTestCopyBtn" onclick="openShareReportModal()" disabled>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-share me-1" viewBox="0 0 16 16">
|
||||||
|
<path d="M13.5 1a1.5 1.5 0 1 0 0 3 1.5 1.5 0 0 0 0-3zM11 2.5a2.5 2.5 0 1 1 .603 1.628l-6.718 3.12a2.499 2.499 0 0 1 0 1.504l6.718 3.12a2.5 2.5 0 1 1-.488.876l-6.718-3.12a2.5 2.5 0 1 1 0-3.256l6.718-3.12A2.5 2.5 0 0 1 11 2.5zm-8.5 4a1.5 1.5 0 1 0 0 3 1.5 1.5 0 0 0 0-3zm11 5.5a1.5 1.5 0 1 0 0 3 1.5 1.5 0 0 0 0-3z"/>
|
||||||
|
</svg>
|
||||||
|
Share Report
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal" id="selfTestDoneBtn" disabled>Close</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Share Report Modal -->
|
||||||
|
<div class="modal fade" id="shareReportModal" tabindex="-1" aria-labelledby="shareReportModalLabel" aria-hidden="true">
|
||||||
|
<div class="modal-dialog modal-lg modal-dialog-centered">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header bg-primary text-white">
|
||||||
|
<h5 class="modal-title" id="shareReportModalLabel">Share Diagnostic Report</h5>
|
||||||
|
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="alert alert-info">
|
||||||
|
<strong>Need help?</strong> You can send this diagnostic report to our support team at
|
||||||
|
<a href="mailto:contact@aircarto.fr?subject=NebuleAir%20Diagnostic%20Report" class="alert-link">contact@aircarto.fr</a>
|
||||||
|
<br><small>Select all the text below (Ctrl+A) and copy it (Ctrl+C), or use the Download button.</small>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<textarea id="shareReportText" class="form-control font-monospace" rows="15" readonly style="font-size: 0.75rem; background-color: #1e1e1e; color: #d4d4d4;"></textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-success" onclick="downloadReport()">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-download me-1" viewBox="0 0 16 16">
|
||||||
|
<path d="M.5 9.9a.5.5 0 0 1 .5.5v2.5a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-2.5a.5.5 0 0 1 1 0v2.5a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2v-2.5a.5.5 0 0 1 .5-.5z"/>
|
||||||
|
<path d="M7.646 11.854a.5.5 0 0 0 .708 0l3-3a.5.5 0 0 0-.708-.708L8.5 10.293V1.5a.5.5 0 0 0-1 0v8.793L5.354 8.146a.5.5 0 1 0-.708.708l3 3z"/>
|
||||||
|
</svg>
|
||||||
|
Download (.txt)
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-outline-primary" onclick="selectAllReportText()">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-cursor-text me-1" viewBox="0 0 16 16">
|
||||||
|
<path d="M5 2a.5.5 0 0 1 .5-.5c.862 0 1.573.287 2.06.566.174.099.321.198.44.286.119-.088.266-.187.44-.286A4.165 4.165 0 0 1 10.5 1.5a.5.5 0 0 1 0 1c-.638 0-1.177.213-1.564.434a3.49 3.49 0 0 0-.436.294V7.5H9a.5.5 0 0 1 0 1h-.5v4.272c.1.08.248.187.436.294.387.221.926.434 1.564.434a.5.5 0 0 1 0 1 4.165 4.165 0 0 1-2.06-.566A4.561 4.561 0 0 1 8 13.65a4.561 4.561 0 0 1-.44.285 4.165 4.165 0 0 1-2.06.566.5.5 0 0 1 0-1c.638 0 1.177-.213 1.564-.434.188-.107.335-.214.436-.294V8.5H7a.5.5 0 0 1 0-1h.5V3.228a3.49 3.49 0 0 0-.436-.294A3.166 3.166 0 0 0 5.5 2.5.5.5 0 0 1 5 2z"/>
|
||||||
|
</svg>
|
||||||
|
Select All
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user