Compare commits
37 Commits
ai_branch_
...
d0b49bf30c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 |
8
.claude/settings.local.json
Normal file
8
.claude/settings.local.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(python3:*)"
|
||||
],
|
||||
"deny": []
|
||||
}
|
||||
}
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -14,4 +14,6 @@ NPM/data/*.txt
|
||||
NPM/data/*.json
|
||||
*.lock
|
||||
sqlite/*.db
|
||||
sqlite/*.sql
|
||||
|
||||
tests/
|
||||
235
MPPT/read.py
235
MPPT/read.py
@@ -1,11 +1,12 @@
|
||||
'''
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
__ __ ____ ____ _____
|
||||
| \/ | _ \| _ \_ _|
|
||||
| |\/| | |_) | |_) || |
|
||||
| | | | __/| __/ | |
|
||||
|_| |_|_| |_| |_|
|
||||
|
||||
Chargeur solaire Victron MPPT interface UART
|
||||
MPPT Chargeur solaire Victron interface UART
|
||||
|
||||
MPPT connections
|
||||
5V / Rx / TX / GND
|
||||
@@ -13,107 +14,125 @@ RPI connection
|
||||
-- / GPIO9 / GPIO8 / GND
|
||||
* pas besoin de connecter le 5V (le GND uniquement)
|
||||
|
||||
typical response from uart:
|
||||
Fixed version - properly handles continuous data stream
|
||||
"""
|
||||
|
||||
PID 0xA075 ->product ID
|
||||
FW 164 ->firmware version
|
||||
SER# HQ2249VJV9W ->serial num
|
||||
|
||||
V 13310 ->Battery voilatage in mV
|
||||
I -130 ->Battery current in mA (negative means its discharging)
|
||||
VPV 10 ->Solar Panel voltage
|
||||
PPV 0 ->Solar Panel power (in W)
|
||||
CS 0 ->Charger status:
|
||||
0=off (no charging),
|
||||
2=Bulk (Max current is being delivered to the battery),
|
||||
3=Absorbtion (battery is nearly full,voltage is held constant.),
|
||||
4=Float (Battery is fully charged, only maintaining charge)
|
||||
MPPT 0 ->MPPT (Maximum Power Point Tracking) state: 0 = Off, 1 = Active, 2 = Not tracking
|
||||
OR 0x00000001
|
||||
ERR 0
|
||||
LOAD ON
|
||||
IL 100
|
||||
H19 18 ->historical data (Total energy absorbed in kWh)
|
||||
H20 0 -> Total energy discharged in kWh
|
||||
H21 0
|
||||
H22 9
|
||||
H23 92
|
||||
HSDS 19
|
||||
Checksum u
|
||||
|
||||
sudo /usr/bin/python3 /var/www/nebuleair_pro_4g/MPPT/read.py
|
||||
|
||||
'''
|
||||
import serial
|
||||
import time
|
||||
import sqlite3
|
||||
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=20, max_attempts=3):
|
||||
|
||||
def read_vedirect(port='/dev/ttyAMA4', baudrate=19200, timeout=10):
|
||||
"""
|
||||
Read and parse data from Victron MPPT controller with retry logic
|
||||
Returns parsed data as a dictionary or None if all attempts fail
|
||||
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
|
||||
|
||||
for attempt in range(max_attempts):
|
||||
try:
|
||||
print(f"Attempt {attempt+1} of {max_attempts}...")
|
||||
log(f"Opening serial port {port} at {baudrate} baud...")
|
||||
ser = serial.Serial(port, baudrate, timeout=1)
|
||||
|
||||
# Initialize data dictionary and tracking variables
|
||||
# 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
|
||||
|
||||
# Check if line contains a key-value pair
|
||||
if '\t' in line:
|
||||
key, value = line.split('\t', 1)
|
||||
data[key] = value
|
||||
print(f"{key}: {value}")
|
||||
else:
|
||||
print(f"Info: {line}")
|
||||
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 a complete data block
|
||||
if 'Checksum' in data:
|
||||
# Check if we have all required keys
|
||||
missing_keys = [key for key in required_keys if key not in data]
|
||||
|
||||
if not missing_keys:
|
||||
log(f"✓ Complete data block received after {lines_read} lines!")
|
||||
ser.close()
|
||||
return data
|
||||
else:
|
||||
print(f"Incomplete data, missing: {', '.join(missing_keys)}")
|
||||
# Clear data and continue reading
|
||||
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 = {}
|
||||
|
||||
# Timeout occurred
|
||||
print(f"Timeout on attempt {attempt+1}: Could not get complete data")
|
||||
ser.close()
|
||||
|
||||
# Add small delay between attempts
|
||||
if attempt < max_attempts - 1:
|
||||
print("Waiting before next attempt...")
|
||||
time.sleep(2)
|
||||
|
||||
except UnicodeDecodeError as e:
|
||||
log(f"Decode error: {e}", "ERROR")
|
||||
continue
|
||||
except Exception as e:
|
||||
print(f"Error on attempt {attempt+1}: {e}")
|
||||
try:
|
||||
ser.close()
|
||||
except:
|
||||
pass
|
||||
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")
|
||||
|
||||
print("All attempts failed")
|
||||
return None
|
||||
|
||||
|
||||
def parse_values(data):
|
||||
"""Convert string values to appropriate types"""
|
||||
if not data:
|
||||
@@ -135,13 +154,13 @@ def parse_values(data):
|
||||
'OR': str,
|
||||
'ERR': int,
|
||||
'LOAD': str,
|
||||
'IL': int,
|
||||
'H19': int, # Total energy absorbed in kWh
|
||||
'H20': int, # Total energy discharged in kWh
|
||||
'H21': int,
|
||||
'H22': int,
|
||||
'H23': int,
|
||||
'HSDS': int
|
||||
'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
|
||||
@@ -149,18 +168,19 @@ def parse_values(data):
|
||||
if key in conversions:
|
||||
try:
|
||||
parsed[key] = conversions[key](value)
|
||||
except (ValueError, TypeError):
|
||||
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",
|
||||
1: "Low power mode",
|
||||
2: "Fault",
|
||||
3: "Bulk",
|
||||
4: "Absorption",
|
||||
@@ -175,8 +195,22 @@ def get_charger_status(cs_value):
|
||||
}
|
||||
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__":
|
||||
# Read data (with retry logic)
|
||||
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:
|
||||
@@ -184,25 +218,37 @@ if __name__ == "__main__":
|
||||
parsed_data = parse_values(raw_data)
|
||||
|
||||
if parsed_data:
|
||||
# Check if we have valid battery voltage
|
||||
if parsed_data.get('V', 0) > 0:
|
||||
print("\n===== MPPT Summary =====")
|
||||
print(f"Battery: {parsed_data.get('V', 0):.2f}V, {parsed_data.get('I', 0):.2f}A")
|
||||
print(f"Solar: {parsed_data.get('VPV', 0):.2f}V, {parsed_data.get('PPV', 0)}W")
|
||||
print(f"Charger status: {get_charger_status(parsed_data.get('CS', 0))}")
|
||||
# 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")
|
||||
|
||||
# Save to SQLite
|
||||
# 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]
|
||||
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
|
||||
battery_voltage = parsed_data.get('V', 0)
|
||||
# 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)
|
||||
@@ -210,16 +256,27 @@ if __name__ == "__main__":
|
||||
(rtc_time_str, battery_voltage, battery_current, solar_voltage, solar_power, charger_status))
|
||||
|
||||
conn.commit()
|
||||
print("MPPT data saved successfully!")
|
||||
log(f"\n✓ Data saved to database at {rtc_time_str}")
|
||||
|
||||
except Exception as e:
|
||||
except sqlite3.Error as e:
|
||||
# Always log database errors regardless of DEBUG_MODE
|
||||
if not DEBUG_MODE:
|
||||
print(f"Database error: {e}")
|
||||
else:
|
||||
print("Invalid data: Battery voltage is zero or missing")
|
||||
log(f"\n✗ Database error: {e}", "ERROR")
|
||||
conn.rollback()
|
||||
else:
|
||||
print("Failed to parse data")
|
||||
log("\n✗ Invalid data: Battery voltage is zero or missing", "ERROR")
|
||||
else:
|
||||
print("No valid data received from MPPT controller")
|
||||
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.")
|
||||
@@ -29,7 +29,7 @@ Line by line installation.
|
||||
```
|
||||
sudo apt update
|
||||
sudo apt install git gh apache2 sqlite3 php php-sqlite3 python3 python3-pip jq autossh i2c-tools python3-smbus -y
|
||||
sudo pip3 install pyserial requests RPi.GPIO adafruit-circuitpython-bme280 crcmod psutil ntplib pytz gpiozero adafruit-circuitpython-ads1x15 numpy --break-system-packages
|
||||
sudo 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 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
|
||||
|
||||
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
|
||||
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
|
||||
0
envea/read_value_loop.py → envea/old/read_value_loop.py
Executable file → Normal file
0
envea/read_value_loop.py → envea/old/read_value_loop.py
Executable file → Normal file
0
envea/read_value_loop_json.py → envea/old/read_value_loop_json.py
Executable file → Normal file
0
envea/read_value_loop_json.py → envea/old/read_value_loop_json.py
Executable file → Normal file
@@ -1,6 +1,7 @@
|
||||
import serial
|
||||
import time
|
||||
import sys
|
||||
import re
|
||||
|
||||
parameter = sys.argv[1:] # Exclude the script name
|
||||
#print("Parameters received:")
|
||||
@@ -61,8 +62,46 @@ def read_cairsens(port, baudrate=9600, parity=serial.PARITY_NONE, stopbits=seria
|
||||
|
||||
# ASCII characters
|
||||
ascii_data = ''.join(chr(b) if 0x20 <= b <= 0x7E else '.' for b in raw_bytes)
|
||||
print(f"Valeurs converties en ASCII : {ascii_data}")
|
||||
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 = [b for b in raw_bytes]
|
||||
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
|
||||
#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()
|
||||
#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()}")
|
||||
|
||||
# Extraire le 20ème octet
|
||||
|
||||
@@ -8,7 +8,9 @@
|
||||
Gather data from envea Sensors and store them to the SQlite table
|
||||
Use the RTC time for the timestamp
|
||||
|
||||
/usr/bin/python3 /var/www/nebuleair_pro_4g/envea/read_value_v2.py
|
||||
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
|
||||
|
||||
"""
|
||||
|
||||
@@ -18,23 +20,58 @@ 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
|
||||
conn = sqlite3.connect("/var/www/nebuleair_pro_4g/sqlite/sensors.db")
|
||||
cursor = conn.cursor()
|
||||
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
|
||||
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'
|
||||
# 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
|
||||
cursor.execute("SELECT port, name, coefficient FROM envea_sondes_table WHERE connected = 1")
|
||||
connected_envea_sondes = cursor.fetchall() # List of tuples (port, name, coefficient)
|
||||
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(
|
||||
@@ -45,58 +82,101 @@ if connected_envea_sondes:
|
||||
bytesize=serial.EIGHTBITS,
|
||||
timeout=1
|
||||
)
|
||||
debug_print(f"✓ Opened serial port for {name} on /dev/{port}")
|
||||
except serial.SerialException as e:
|
||||
print(f"Error opening serial port for {name}: {e}")
|
||||
debug_print(f"✗ Error opening serial port for {name}: {e}")
|
||||
else:
|
||||
debug_print("! No connected ENVEA sensors found in configuration")
|
||||
|
||||
global data_h2s, data_no2, data_o3
|
||||
# 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:
|
||||
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"
|
||||
)
|
||||
debug_print(f"Reading from {name}...")
|
||||
|
||||
# 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)
|
||||
debug_print(f" → Sent command: {command.hex()}")
|
||||
|
||||
# Read response
|
||||
data_envea = serial_connection.readline()
|
||||
debug_print(f" ← Received {len(data_envea)} bytes: {data_envea.hex()}")
|
||||
|
||||
if len(data_envea) >= 20:
|
||||
byte_20 = data_envea[19] * coefficient
|
||||
byte_20 = data_envea[19]
|
||||
raw_value = byte_20
|
||||
calculated_value = byte_20 * coefficient
|
||||
debug_print(f" → Byte 20 value: {raw_value} (0x{raw_value:02X})")
|
||||
debug_print(f" → Calculated value: {raw_value} × {coefficient} = {calculated_value}")
|
||||
|
||||
if name == "h2s":
|
||||
data_h2s = byte_20
|
||||
data_h2s = calculated_value
|
||||
elif name == "no2":
|
||||
data_no2 = byte_20
|
||||
data_no2 = calculated_value
|
||||
elif name == "o3":
|
||||
data_o3 = byte_20
|
||||
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" ✗ Response too short (expected ≥20 bytes)")
|
||||
|
||||
except serial.SerialException as e:
|
||||
print(f"Error communicating with {name}: {e}")
|
||||
debug_print(f"✗ Error communicating with {name}: {e}")
|
||||
else:
|
||||
debug_print(f"! No serial connection available for {name}")
|
||||
|
||||
except Exception as e:
|
||||
print("An error occurred while gathering data:", 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")
|
||||
|
||||
#print(f" H2S: {data_h2s}, NO2: {data_no2}, O3: {data_o3}")
|
||||
|
||||
#save to sqlite database
|
||||
# Save to sqlite database
|
||||
try:
|
||||
cursor.execute('''
|
||||
INSERT INTO data_envea (timestamp,h2s, no2, o3, co, nh3) VALUES (?,?,?,?,?,?)'''
|
||||
, (rtc_time_str,data_h2s,data_no2,data_o3,data_co,data_nh3 ))
|
||||
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()
|
||||
|
||||
#print("Sensor data saved successfully!")
|
||||
|
||||
except Exception as e:
|
||||
print(f"Database error: {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")
|
||||
234
html/admin.html
234
html/admin.html
@@ -91,7 +91,7 @@
|
||||
</div>
|
||||
|
||||
<div class="form-check mb-3">
|
||||
<input class="form-check-input" type="checkbox" value="" id="check_envea" onchange="update_config_sqlite('envea', this.checked)">
|
||||
<input class="form-check-input" type="checkbox" value="" id="check_envea" onchange="update_config_sqlite('envea', this.checked);add_sondeEnveaContainer() ">
|
||||
<label class="form-check-label" for="check_envea">
|
||||
Send Envea sensor data
|
||||
</label>
|
||||
@@ -111,10 +111,32 @@
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-check mb-3">
|
||||
<input class="form-check-input" type="checkbox" value="" id="check_NOISE" onchange="update_config_sqlite('NOISE', this.checked)">
|
||||
<label class="form-check-label" for="check_NOISE">
|
||||
Send Noise data
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-check mb-3">
|
||||
<input class="form-check-input" type="checkbox" value="" id="check_aircarto" onchange="update_config_sqlite('send_aircarto', this.checked)" disabled>
|
||||
<label class="form-check-label" for="check_aircarto">
|
||||
Send to AirCarto (HTTP)
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-check mb-3">
|
||||
<input class="form-check-input" type="checkbox" value="" id="check_uSpot" onchange="update_config_sqlite('send_uSpot', this.checked)" disabled>
|
||||
<label class="form-check-label" for="check_uSpot">
|
||||
Send to uSpot
|
||||
Send to uSpot (HTTPS)
|
||||
</label>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="form-check mb-3">
|
||||
<input class="form-check-input" type="checkbox" value="" id="check_miotiq" onchange="update_config_sqlite('send_miotiq', this.checked)" disabled>
|
||||
<label class="form-check-label" for="check_miotiq">
|
||||
Send to miotiq (UDP)
|
||||
</label>
|
||||
</div>
|
||||
|
||||
@@ -250,6 +272,33 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Envea Detection Modal -->
|
||||
<div class="modal fade" id="enveaDetectionModal" tabindex="-1" aria-labelledby="enveaDetectionModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-xl">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="enveaDetectionModalLabel">Envea Sondes Detection</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body" style="max-height: 70vh; overflow-y: auto;">
|
||||
<div id="detectionProgress" class="text-center" style="display: none;">
|
||||
<div class="spinner-border text-primary" role="status">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
<p class="mt-2">Scanning ports for Envea devices...</p>
|
||||
</div>
|
||||
<div id="detectionResults">
|
||||
<p>Click "Start Detection" to scan for connected Envea devices.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
||||
<button type="button" class="btn btn-primary" id="startDetectionBtn" onclick="startEnveaDetection()">Start Detection</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</main>
|
||||
</div>
|
||||
@@ -331,16 +380,29 @@ window.onload = function() {
|
||||
const checkbox_nmp5channels = document.getElementById("check_NPM_5channels");
|
||||
const checkbox_wind = document.getElementById("check_WindMeter");
|
||||
const checkbox_uSpot = document.getElementById("check_uSpot");
|
||||
const checkbox_aircarto = document.getElementById("check_aircarto");
|
||||
const checkbox_miotiq = document.getElementById("check_miotiq");
|
||||
|
||||
const checkbox_bme = document.getElementById("check_bme280");
|
||||
const checkbox_envea = document.getElementById("check_envea");
|
||||
const checkbox_solar = document.getElementById("check_solarBattery");
|
||||
const checkbox_noise = document.getElementById("check_NOISE");
|
||||
|
||||
checkbox_bme.checked = response["BME280"];
|
||||
checkbox_envea.checked = response["envea"];
|
||||
checkbox_solar.checked = response["MPPT"];
|
||||
checkbox_nmp5channels.checked = response.npm_5channel;
|
||||
checkbox_wind.checked = response["windMeter"];
|
||||
checkbox_noise.checked = response["NOISE"];
|
||||
|
||||
checkbox_uSpot.checked = response["send_uSpot"];
|
||||
checkbox_aircarto.checked = response["send_aircarto"];
|
||||
checkbox_miotiq.checked = response["send_miotiq"];
|
||||
|
||||
// If envea is enabled, show the envea sondes container
|
||||
if (response["envea"]) {
|
||||
add_sondeEnveaContainer();
|
||||
}
|
||||
|
||||
},
|
||||
error: function(xhr, status, error) {
|
||||
@@ -701,7 +763,7 @@ function add_sondeEnveaContainer() {
|
||||
$('#advanced_options').append('<div id="sondes_envea_div" class="input-group mt-4 border p-3 rounded"><legend>Sondes Envea</legend><p>Plouf</p></div>');
|
||||
} else {
|
||||
// Clear existing content if container exists
|
||||
$('#sondes_envea_div').html('<legend>Sondes Envea</legend>');
|
||||
$('#sondes_envea_div').html('<legend>Sondes Envea <button type="button" class="btn btn-sm btn-info ms-2" onclick="detectEnveaSondes()">Detect Devices</button></legend>');
|
||||
$('#envea_table').html('<table class="table table-striped table-bordered">'+
|
||||
'<thead><tr><th scope="col">Software</th><th scope="col">Hardware (PCB)</th></tr></thead>'+
|
||||
'<tbody>' +
|
||||
@@ -726,11 +788,14 @@ function add_sondeEnveaContainer() {
|
||||
onchange="updateSondeStatus(${sonde.id}, this.checked)">
|
||||
</div>
|
||||
<input type="text" class="form-control" placeholder="Name" value="${sonde.name}"
|
||||
id="${sondeId}_name" onchange="updateSondeName(${sonde.id}, this.value)">
|
||||
<input type="text" class="form-control" placeholder="Port" value="${sonde.port}"
|
||||
id="${sondeId}_port" onchange="updateSondePort(${sonde.id}, this.value)">
|
||||
id="${sondeId}_name" readonly style="background-color: #f8f9fa;">
|
||||
<select class="form-control" id="${sondeId}_port" onchange="updateSondePort(${sonde.id}, this.value)">
|
||||
<option value="ttyAMA3" ${sonde.port === 'ttyAMA3' ? 'selected' : ''}>ttyAMA3</option>
|
||||
<option value="ttyAMA4" ${sonde.port === 'ttyAMA4' ? 'selected' : ''}>ttyAMA4</option>
|
||||
<option value="ttyAMA5" ${sonde.port === 'ttyAMA5' ? 'selected' : ''}>ttyAMA5</option>
|
||||
</select>
|
||||
<input type="number" class="form-control" placeholder="Coefficient" value="${sonde.coefficient}"
|
||||
id="${sondeId}_coefficient" onchange="updateSondeCoefficient(${sonde.id}, this.value)">
|
||||
id="${sondeId}_coefficient" onchange="updateSondeCoefficientWithConfirm(${sonde.id}, this.value, this)">
|
||||
</div>
|
||||
`;
|
||||
|
||||
@@ -928,6 +993,23 @@ function updateSondePort(id, port) {
|
||||
});
|
||||
}
|
||||
|
||||
function updateSondeCoefficientWithConfirm(id, coefficient, inputElement) {
|
||||
// Store the previous value in case user cancels
|
||||
const previousValue = inputElement.getAttribute('data-previous-value') || inputElement.defaultValue;
|
||||
|
||||
// Show confirmation dialog
|
||||
const confirmed = confirm(`Are you sure you want to change the coefficient to ${coefficient}?\n\nThis will affect sensor calibration and data accuracy.`);
|
||||
|
||||
if (confirmed) {
|
||||
// Store the new value as previous for next time
|
||||
inputElement.setAttribute('data-previous-value', coefficient);
|
||||
updateSondeCoefficient(id, coefficient);
|
||||
} else {
|
||||
// Revert to previous value
|
||||
inputElement.value = previousValue;
|
||||
}
|
||||
}
|
||||
|
||||
function updateSondeCoefficient(id, coefficient) {
|
||||
console.log(`Updating sonde ${id} coefficient to: ${coefficient}`);
|
||||
const toastLiveExample = document.getElementById('liveToast');
|
||||
@@ -1234,6 +1316,144 @@ function toggleService(serviceName, enable) {
|
||||
});
|
||||
}
|
||||
|
||||
/*
|
||||
_____ ____ _ _ _
|
||||
| ____|_ ____ _____ __ _ | _ \ ___| |_ ___ ___| |_(_) ___ _ __
|
||||
| _| | '_ \ \ / / _ \/ _` | | | | |/ _ \ __/ _ \/ __| __| |/ _ \| '_ \
|
||||
| |___| | | \ V / __/ (_| | | |_| | __/ || __/ (__| |_| | (_) | | | |
|
||||
|_____|_| |_|\_/ \___|\__,_| |____/ \___|\__\___|\___|\__|_|\___/|_| |_|
|
||||
|
||||
*/
|
||||
|
||||
function detectEnveaSondes() {
|
||||
console.log("Opening Envea detection modal");
|
||||
const modal = new bootstrap.Modal(document.getElementById('enveaDetectionModal'));
|
||||
modal.show();
|
||||
|
||||
// Reset modal content
|
||||
document.getElementById('detectionProgress').style.display = 'none';
|
||||
document.getElementById('detectionResults').innerHTML = '<p>Click "Start Detection" to scan for connected Envea devices.</p>';
|
||||
document.getElementById('startDetectionBtn').style.display = 'inline-block';
|
||||
}
|
||||
|
||||
function startEnveaDetection() {
|
||||
console.log("Starting Envea device detection");
|
||||
|
||||
// Show progress spinner
|
||||
document.getElementById('detectionProgress').style.display = 'block';
|
||||
document.getElementById('detectionResults').innerHTML = '';
|
||||
document.getElementById('startDetectionBtn').style.display = 'none';
|
||||
|
||||
// Test the three ports: ttyAMA3, ttyAMA4, ttyAMA5
|
||||
const ports = ['ttyAMA3', 'ttyAMA4', 'ttyAMA5'];
|
||||
let completedTests = 0;
|
||||
let results = [];
|
||||
|
||||
ports.forEach(function(port, index) {
|
||||
$.ajax({
|
||||
url: `launcher.php?type=detect_envea_device&port=${port}`,
|
||||
dataType: 'json',
|
||||
method: 'GET',
|
||||
cache: false,
|
||||
timeout: 10000, // 10 second timeout per port
|
||||
success: function(response) {
|
||||
console.log(`Detection result for ${port}:`, response);
|
||||
|
||||
results[index] = {
|
||||
port: port,
|
||||
success: response.success,
|
||||
data: response.data || '',
|
||||
error: response.error || '',
|
||||
detected: response.detected || false,
|
||||
device_info: response.device_info || ''
|
||||
};
|
||||
|
||||
completedTests++;
|
||||
if (completedTests === ports.length) {
|
||||
displayDetectionResults(results);
|
||||
}
|
||||
},
|
||||
error: function(xhr, status, error) {
|
||||
console.error(`Detection failed for ${port}:`, error);
|
||||
|
||||
results[index] = {
|
||||
port: port,
|
||||
success: false,
|
||||
data: '',
|
||||
error: `Request failed: ${error}`,
|
||||
detected: false,
|
||||
device_info: ''
|
||||
};
|
||||
|
||||
completedTests++;
|
||||
if (completedTests === ports.length) {
|
||||
displayDetectionResults(results);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function displayDetectionResults(results) {
|
||||
console.log("Displaying detection results:", results);
|
||||
|
||||
// Hide progress spinner
|
||||
document.getElementById('detectionProgress').style.display = 'none';
|
||||
|
||||
let htmlContent = '<h6>Detection Results:</h6>';
|
||||
|
||||
// Create cards for each port result
|
||||
results.forEach(function(result, index) {
|
||||
const statusBadge = result.detected ?
|
||||
'<span class="badge bg-success">Device Detected</span>' :
|
||||
result.success ?
|
||||
'<span class="badge bg-warning">No Device</span>' :
|
||||
'<span class="badge bg-danger">Error</span>';
|
||||
|
||||
const deviceInfo = result.device_info || (result.detected ? 'Envea Device' : 'None');
|
||||
const rawData = result.data || 'No data';
|
||||
|
||||
htmlContent += `
|
||||
<div class="card mb-3">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<h6 class="mb-0"><strong>Port ${result.port}</strong></h6>
|
||||
${statusBadge}
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-12 mb-3">
|
||||
<strong>Device Information:</strong>
|
||||
<p class="mb-0">${deviceInfo}</p>
|
||||
</div>
|
||||
${result.error ? `<div class="col-12 mb-3"><div class="alert alert-danger mb-0"><strong>Error:</strong> ${result.error}</div></div>` : ''}
|
||||
<div class="col-12">
|
||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||
<strong>Raw Data Output:</strong>
|
||||
<button class="btn btn-sm btn-outline-secondary" type="button" data-bs-toggle="collapse" data-bs-target="#rawData${index}" aria-expanded="false">
|
||||
Toggle Raw Data
|
||||
</button>
|
||||
</div>
|
||||
<div class="collapse" id="rawData${index}">
|
||||
<pre class="bg-light p-3 rounded" style="white-space: pre-wrap; word-wrap: break-word; max-height: 300px; overflow-y: auto; font-size: 0.85rem;">${rawData}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
|
||||
// Add summary
|
||||
const detectedCount = results.filter(r => r.detected).length;
|
||||
htmlContent += `<div class="alert alert-info mt-3">
|
||||
<i class="bi bi-info-circle"></i> <strong>Summary:</strong> ${detectedCount} device(s) detected out of ${results.length} ports tested.
|
||||
</div>`;
|
||||
|
||||
document.getElementById('detectionResults').innerHTML = htmlContent;
|
||||
document.getElementById('startDetectionBtn').style.display = 'inline-block';
|
||||
document.getElementById('startDetectionBtn').textContent = 'Scan Again';
|
||||
}
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
@@ -71,7 +71,10 @@
|
||||
<button class="btn btn-primary" onclick="get_data_sqlite('data_BME280',getSelectedLimit(),false)">Mesures Temp/Hum</button>
|
||||
<button class="btn btn-primary" onclick="get_data_sqlite('data_NPM_5channels',getSelectedLimit(),false)">Mesures PM (5 canaux)</button>
|
||||
<button class="btn btn-primary" onclick="get_data_sqlite('data_envea',getSelectedLimit(),false)">Sonde Cairsens</button>
|
||||
<button class="btn btn-primary" onclick="get_data_sqlite('data_NOISE',getSelectedLimit(),false)">Sonde bruit</button>
|
||||
|
||||
<button class="btn btn-primary" onclick="get_data_sqlite('data_WIND',getSelectedLimit(),false)">Sonde Vent</button>
|
||||
<button class="btn btn-primary" onclick="get_data_sqlite('data_MPPT',getSelectedLimit(),false)">Batterie</button>
|
||||
|
||||
<button class="btn btn-warning" onclick="get_data_sqlite('timestamp_table',getSelectedLimit(),false)">Timestamp Table</button>
|
||||
|
||||
@@ -96,6 +99,9 @@
|
||||
<button class="btn btn-primary" onclick="get_data_sqlite('data_BME280',10,true, getStartDate(), getEndDate())">Mesures Temp/Hum</button>
|
||||
<button class="btn btn-primary" onclick="get_data_sqlite('data_NPM_5channels',10,true, getStartDate(), getEndDate())">Mesures PM (5 canaux)</button>
|
||||
<button class="btn btn-primary" onclick="get_data_sqlite('data_envea',10,true, getStartDate(), getEndDate())">Sonde Cairsens</button>
|
||||
<button class="btn btn-primary" onclick="get_data_sqlite('data_NOISE',10,true, getStartDate(), getEndDate())">Sonde Bruit</button>
|
||||
|
||||
<button class="btn btn-primary" onclick="get_data_sqlite('data_mppt',10,true, getStartDate(), getEndDate())">Batterie</button>
|
||||
|
||||
</table>
|
||||
</div>
|
||||
@@ -280,8 +286,26 @@ function get_data_sqlite(table, limit, download , startDate = "", endDate = "")
|
||||
<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>
|
||||
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
tableHTML += `</tr></thead><tbody>`;
|
||||
|
||||
// Loop through rows and create table rows
|
||||
@@ -336,6 +360,22 @@ function get_data_sqlite(table, limit, download , startDate = "", endDate = "")
|
||||
<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") {
|
||||
tableHTML += `
|
||||
<td>${columns[0]}</td>
|
||||
<td>${columns[1]}</td>
|
||||
<td>${columns[2]}</td>
|
||||
|
||||
`;
|
||||
}
|
||||
|
||||
tableHTML += "</tr>";
|
||||
|
||||
@@ -1017,6 +1017,10 @@ if ($type == "get_systemd_services") {
|
||||
'description' => 'Tracks solar panel and battery status',
|
||||
'frequency' => 'Every 2 minutes'
|
||||
],
|
||||
'nebuleair-noise-data.timer' => [
|
||||
'description' => 'Get Data from noise sensor',
|
||||
'frequency' => 'Every minute'
|
||||
],
|
||||
'nebuleair-db-cleanup-data.timer' => [
|
||||
'description' => 'Cleans up old data from database',
|
||||
'frequency' => 'Daily'
|
||||
@@ -1198,3 +1202,96 @@ if ($type == "toggle_systemd_service") {
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
_____ ____ _ _ _
|
||||
| ____|_ ____ _____ __ _ | _ \ ___| |_ ___ ___| |_(_) ___ _ __
|
||||
| _| | '_ \ \ / / _ \/ _` | | | | |/ _ \ __/ _ \/ __| __| |/ _ \| '_ \
|
||||
| |___| | | \ V / __/ (_| | | |_| | __/ || __/ (__| |_| | (_) | | | |
|
||||
|_____|_| |_|\_/ \___|\__,_| |____/ \___|\__\___|\___|\__|_|\___/|_| |_|
|
||||
|
||||
*/
|
||||
|
||||
// Detect Envea devices on specified port
|
||||
if ($type == "detect_envea_device") {
|
||||
$port = $_GET['port'] ?? null;
|
||||
|
||||
if (empty($port)) {
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'error' => 'No port specified'
|
||||
]);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Validate port name (security check)
|
||||
$allowedPorts = ['ttyAMA2', 'ttyAMA3', 'ttyAMA4', 'ttyAMA5'];
|
||||
|
||||
if (!in_array($port, $allowedPorts)) {
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'error' => 'Invalid port name'
|
||||
]);
|
||||
exit;
|
||||
}
|
||||
|
||||
try {
|
||||
// Execute the envea detection script
|
||||
$command = "sudo /usr/bin/python3 /var/www/nebuleair_pro_4g/envea/read_ref.py " . escapeshellarg($port) . " 2>&1";
|
||||
$output = shell_exec($command);
|
||||
|
||||
// Check if we got any meaningful output
|
||||
$detected = false;
|
||||
$device_info = '';
|
||||
$raw_data = $output;
|
||||
|
||||
if (!empty($output)) {
|
||||
// Look for indicators that a device is connected
|
||||
if (strpos($output, 'Connexion ouverte') !== false) {
|
||||
// Connection was successful
|
||||
if (strpos($output, 'Données reçues brutes') !== false &&
|
||||
strpos($output, 'b\'\'') === false) {
|
||||
// We received actual data (not empty)
|
||||
$detected = true;
|
||||
$device_info = 'Envea CAIRSENS Device';
|
||||
|
||||
// Try to extract device type from ASCII data if available
|
||||
if (preg_match('/Valeurs converties en ASCII : (.+)/', $output, $matches)) {
|
||||
$ascii_data = trim($matches[1]);
|
||||
if (!empty($ascii_data) && $ascii_data !== '........') {
|
||||
$device_info = "Envea Device: " . $ascii_data;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Connection successful but no data
|
||||
$device_info = 'Port accessible but no Envea device detected';
|
||||
}
|
||||
} else if (strpos($output, 'Erreur de connexion série') !== false) {
|
||||
// Serial connection error
|
||||
$device_info = 'Serial connection error - port may be busy or not available';
|
||||
} else {
|
||||
// Other output
|
||||
$device_info = 'Unexpected response from port';
|
||||
}
|
||||
} else {
|
||||
// No output at all
|
||||
$device_info = 'No response from port';
|
||||
}
|
||||
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'port' => $port,
|
||||
'detected' => $detected,
|
||||
'device_info' => $device_info,
|
||||
'data' => $raw_data,
|
||||
'timestamp' => date('Y-m-d H:i:s')
|
||||
], JSON_PRETTY_PRINT);
|
||||
|
||||
} catch (Exception $e) {
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'error' => 'Script execution failed: ' . $e->getMessage(),
|
||||
'port' => $port
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,11 +23,11 @@ fi
|
||||
|
||||
# Update and install necessary packages
|
||||
info "Updating package list and installing necessary packages..."
|
||||
sudo apt update && sudo apt install -y git gh apache2 sqlite3 php php-sqlite3 python3 python3-pip jq autossh i2c-tools python3-smbus || error "Failed to install required packages."
|
||||
sudo apt update && sudo apt install -y git gh apache2 sqlite3 php php-sqlite3 python3 python3-pip jq autossh i2c-tools python3-smbus python3-rpi.gpio || error "Failed to install required packages."
|
||||
|
||||
# Install Python libraries
|
||||
info "Installing Python libraries..."
|
||||
sudo pip3 install pyserial requests RPi.GPIO adafruit-circuitpython-bme280 crcmod psutil gpiozero ntplib pytz --break-system-packages || error "Failed to install Python libraries."
|
||||
sudo pip3 install pyserial requests adafruit-circuitpython-bme280 crcmod psutil gpiozero ntplib adafruit-circuitpython-ads1x15 nsrt-mk3-dev pytz --break-system-packages || error "Failed to install Python libraries."
|
||||
|
||||
# Clone the repository (check if it exists first)
|
||||
REPO_DIR="/var/www/nebuleair_pro_4g"
|
||||
@@ -99,13 +99,48 @@ fi
|
||||
|
||||
# Add sudo authorization (prevent duplicate entries)
|
||||
info "Setting up sudo authorization..."
|
||||
if ! sudo grep -q "/usr/bin/nmcli" /etc/sudoers; then
|
||||
echo -e "ALL ALL=(ALL) NOPASSWD: /usr/bin/nmcli, /usr/sbin/reboot\nwww-data ALL=(ALL) NOPASSWD: /usr/bin/git pull\nwww-data ALL=(ALL) NOPASSWD: /usr/bin/ssh\nwww-data ALL=(ALL) NOPASSWD: /usr/bin/python3 * www-data ALL=(ALL) NOPASSWD: /bin/systemctl * www-data ALL=(ALL) NOPASSWD: /var/www/nebuleair_pro_4g/*" | sudo tee -a /etc/sudoers > /dev/null
|
||||
SUDOERS_FILE="/etc/sudoers"
|
||||
|
||||
# First, fix any existing syntax errors
|
||||
if sudo visudo -c 2>&1 | grep -q "syntax error"; then
|
||||
warning "Syntax error detected in sudoers file. Attempting to fix..."
|
||||
# Remove the problematic line if it exists
|
||||
sudo sed -i '/www-data ALL=(ALL) NOPASSWD: \/usr\/bin\/python3 \* www-data/d' "$SUDOERS_FILE"
|
||||
fi
|
||||
|
||||
# Add proper sudo rules (each on a separate line)
|
||||
if ! sudo grep -q "/usr/bin/nmcli" "$SUDOERS_FILE"; then
|
||||
# Create a temporary file with the new rules
|
||||
cat <<EOF | sudo tee /tmp/sudoers_additions > /dev/null
|
||||
# NebuleAir Pro 4G sudo rules
|
||||
ALL ALL=(ALL) NOPASSWD: /usr/bin/nmcli, /usr/sbin/reboot
|
||||
www-data ALL=(ALL) NOPASSWD: /usr/bin/git pull
|
||||
www-data ALL=(ALL) NOPASSWD: /usr/bin/ssh
|
||||
www-data ALL=(ALL) NOPASSWD: /usr/bin/python3 *
|
||||
www-data ALL=(ALL) NOPASSWD: /bin/systemctl *
|
||||
www-data ALL=(ALL) NOPASSWD: /var/www/nebuleair_pro_4g/*
|
||||
EOF
|
||||
|
||||
# Validate the temporary file
|
||||
if sudo visudo -c -f /tmp/sudoers_additions; then
|
||||
# Append to sudoers if valid
|
||||
sudo cat /tmp/sudoers_additions >> "$SUDOERS_FILE"
|
||||
success "Sudo authorization added."
|
||||
else
|
||||
error "Failed to add sudo rules - syntax validation failed."
|
||||
fi
|
||||
|
||||
# Clean up
|
||||
sudo rm -f /tmp/sudoers_additions
|
||||
else
|
||||
warning "Sudo authorization already set. Skipping."
|
||||
fi
|
||||
|
||||
# Validate sudoers file after changes
|
||||
if ! sudo visudo -c; then
|
||||
error "Sudoers file has syntax errors! Please fix manually with 'sudo visudo'"
|
||||
fi
|
||||
|
||||
# Open all UART serial ports (avoid duplication)
|
||||
info "Configuring UART serial ports..."
|
||||
if ! grep -q "enable_uart=1" /boot/firmware/config.txt; then
|
||||
@@ -128,6 +163,13 @@ success "I2C ports enabled."
|
||||
info "Creates sqlites databases..."
|
||||
/usr/bin/python3 /var/www/nebuleair_pro_4g/sqlite/create_db.py
|
||||
|
||||
# Final sudoers check
|
||||
if sudo visudo -c; then
|
||||
success "Sudoers file is valid."
|
||||
else
|
||||
error "Sudoers file has errors! System may not function correctly."
|
||||
fi
|
||||
|
||||
# Completion message
|
||||
success "Setup completed successfully!"
|
||||
info "System will reboot in 5 seconds..."
|
||||
|
||||
@@ -46,8 +46,8 @@ info "Activate blue LED"
|
||||
/usr/bin/python3 /var/www/nebuleair_pro_4g/SARA/sara.py ttyAMA2 AT+UGPIOC=16,2 2
|
||||
|
||||
#Connect to network
|
||||
info "Connect SARA R4 to network"
|
||||
python3 /var/www/nebuleair_pro_4g/SARA/sara_connectNetwork.py ttyAMA2 20810 60
|
||||
#info "Connect SARA R4 to network"
|
||||
#python3 /var/www/nebuleair_pro_4g/SARA/sara_connectNetwork.py ttyAMA2 20810 60
|
||||
|
||||
#Need to create the two service
|
||||
# 1. start the scripts to set-up the services
|
||||
|
||||
@@ -28,16 +28,16 @@ CSV PAYLOAD (AirCarto Servers)
|
||||
|
||||
ATTENTION : do not change order !
|
||||
CSV size: 18
|
||||
{PM1},{PM25},{PM10},{temp},{hum},{press},{avg_noise},{max_noise},{min_noise},{envea_no2},{envea_h2s},{envea_o3},{4g_signal_quality}
|
||||
{PM1},{PM25},{PM10},{temp},{hum},{press},{current LEQ},{current level},{FREE},{envea_no2},{envea_h2s},{envea_nh3},{4g_signal_quality}
|
||||
0 -> PM1 (μg/m3)
|
||||
1 -> PM25 (μg/m3)
|
||||
2 -> PM10 (μg/m3)
|
||||
3 -> temp
|
||||
4 -> hum
|
||||
5 -> press
|
||||
6 -> avg_noise
|
||||
7 -> max_noise
|
||||
8 -> min_noise
|
||||
6 -> sound (current LEQ)
|
||||
7 -> sound (current level)
|
||||
8 -> FREE
|
||||
9 -> envea_no2
|
||||
10 -> envea_h2s
|
||||
11 -> envea_nh3
|
||||
@@ -56,6 +56,20 @@ CSV PAYLOAD (AirCarto Servers)
|
||||
24 -> charger_status
|
||||
25 -> Wind speed
|
||||
26 -> Wind direction
|
||||
27 -> envea_CO
|
||||
28 -> envea_O3
|
||||
|
||||
CSV FOR UDP (miotiq)
|
||||
{device_id},{timestamp},{PM1},{PM25},{PM10},{temp},{hum},{press},{avg_noise},{max_noise},{min_noise},{envea_no2},{envea_h2s},{envea_o3},{4g_signal_quality}
|
||||
0 -> device ID
|
||||
1 -> timestamp
|
||||
2 -> PM1
|
||||
3 -> PM2.5
|
||||
4 -> PM10
|
||||
5 -> temp
|
||||
6 -> hum
|
||||
7 -> press
|
||||
|
||||
|
||||
JSON PAYLOAD (Micro-Spot Servers)
|
||||
Same as NebuleAir wifi
|
||||
@@ -106,6 +120,7 @@ import traceback
|
||||
import threading
|
||||
import sys
|
||||
import sqlite3
|
||||
import struct
|
||||
import RPi.GPIO as GPIO
|
||||
from threading import Thread
|
||||
from datetime import datetime
|
||||
@@ -124,6 +139,7 @@ if uptime_seconds < 120:
|
||||
|
||||
#Payload CSV to be sent to data.nebuleair.fr
|
||||
payload_csv = [None] * 30
|
||||
|
||||
#Payload JSON to be sent to uSpot
|
||||
payload_json = {
|
||||
"nebuleairid": "XXX",
|
||||
@@ -211,12 +227,15 @@ device_longitude_raw = config.get('longitude_raw', 0)
|
||||
modem_version=config.get('modem_version', "")
|
||||
Sara_baudrate = config.get('SaraR4_baudrate', 115200)
|
||||
selected_networkID = int(config.get('SARA_R4_neworkID', 0))
|
||||
send_miotiq = config.get('send_miotiq', True)
|
||||
send_aircarto = config.get('send_aircarto', True)
|
||||
send_uSpot = config.get('send_uSpot', False) #envoi sur MicroSpot ()
|
||||
npm_5channel = config.get('npm_5channel', False) #5 canaux du NPM
|
||||
envea_cairsens= config.get('envea', False)
|
||||
wind_meter= config.get('windMeter', False)
|
||||
bme_280_config = config.get('BME280', False)
|
||||
mppt_charger= config.get('MPPT', False)
|
||||
mppt_charger = config.get('MPPT', False)
|
||||
NOISE_sensor = config.get('NOISE', False)
|
||||
|
||||
#update device id in the payload json
|
||||
payload_json["nebuleairid"] = device_id
|
||||
@@ -235,6 +254,115 @@ ser_sara = serial.Serial(
|
||||
timeout = 2
|
||||
)
|
||||
|
||||
class SensorPayload:
|
||||
"""
|
||||
Class to manage a fixed 100-byte sensor payload
|
||||
All positions are predefined, no CSV intermediary
|
||||
"""
|
||||
|
||||
def __init__(self, device_id):
|
||||
# Initialize 100-byte array with 0xFF (no data marker)
|
||||
self.payload = bytearray(100)
|
||||
for i in range(100):
|
||||
self.payload[i] = 0xFF
|
||||
|
||||
# Set device ID (bytes 0-7)
|
||||
device_id_bytes = device_id.encode('ascii')[:8].ljust(8, b'\x00')
|
||||
#device_id_bytes = bytes.fromhex(device_id)[:8].ljust(8, b'\x00')
|
||||
|
||||
self.payload[0:8] = device_id_bytes
|
||||
|
||||
# Set protocol version (byte 9)
|
||||
self.payload[9] = 0x01
|
||||
|
||||
def set_signal_quality(self, value):
|
||||
"""Set 4G signal quality (byte 8)"""
|
||||
if value is not None:
|
||||
self.payload[8] = min(value, 255)
|
||||
|
||||
def set_npm_core(self, pm1, pm25, pm10):
|
||||
"""Set NPM core values (bytes 10-15)"""
|
||||
if pm1 is not None:
|
||||
self.payload[10:12] = struct.pack('>H', int(pm1 * 10))
|
||||
if pm25 is not None:
|
||||
self.payload[12:14] = struct.pack('>H', int(pm25 * 10))
|
||||
if pm10 is not None:
|
||||
self.payload[14:16] = struct.pack('>H', int(pm10 * 10))
|
||||
|
||||
def set_bme280(self, temperature, humidity, pressure):
|
||||
"""Set BME280 values (bytes 16-21)"""
|
||||
if temperature is not None:
|
||||
self.payload[16:18] = struct.pack('>h', int(temperature * 10)) # Signed
|
||||
if humidity is not None:
|
||||
self.payload[18:20] = struct.pack('>H', int(humidity * 10))
|
||||
if pressure is not None:
|
||||
self.payload[20:22] = struct.pack('>H', int(pressure))
|
||||
|
||||
def set_noise(self, avg_noise, max_noise=None, min_noise=None):
|
||||
"""Set noise values (bytes 22-27)"""
|
||||
if avg_noise is not None:
|
||||
self.payload[22:24] = struct.pack('>H', int(avg_noise * 10))
|
||||
if max_noise is not None:
|
||||
self.payload[24:26] = struct.pack('>H', int(max_noise * 10))
|
||||
if min_noise is not None:
|
||||
self.payload[26:28] = struct.pack('>H', int(min_noise * 10))
|
||||
|
||||
def set_envea(self, no2, h2s, nh3, co, o3):
|
||||
"""Set ENVEA gas sensor values (bytes 28-37)"""
|
||||
if no2 is not None:
|
||||
self.payload[28:30] = struct.pack('>H', int(no2))
|
||||
if h2s is not None:
|
||||
self.payload[30:32] = struct.pack('>H', int(h2s))
|
||||
if nh3 is not None:
|
||||
self.payload[32:34] = struct.pack('>H', int(nh3))
|
||||
if co is not None:
|
||||
self.payload[34:36] = struct.pack('>H', int(co))
|
||||
if o3 is not None:
|
||||
self.payload[36:38] = struct.pack('>H', int(o3))
|
||||
|
||||
def set_npm_5channels(self, ch1, ch2, ch3, ch4, ch5):
|
||||
"""Set NPM 5 channel values (bytes 38-47)"""
|
||||
channels = [ch1, ch2, ch3, ch4, ch5]
|
||||
for i, value in enumerate(channels):
|
||||
if value is not None:
|
||||
self.payload[38 + i*2:40 + i*2] = struct.pack('>H', int(value))
|
||||
|
||||
def set_npm_internal(self, temperature, humidity):
|
||||
"""Set NPM internal temp/humidity (bytes 48-51)"""
|
||||
if temperature is not None:
|
||||
self.payload[48:50] = struct.pack('>h', int(temperature * 10)) # Signed
|
||||
if humidity is not None:
|
||||
self.payload[50:52] = struct.pack('>H', int(humidity * 10))
|
||||
|
||||
def set_mppt(self, battery_voltage, battery_current, solar_voltage, solar_power, charger_status):
|
||||
"""Set MPPT charger values (bytes 52-61)"""
|
||||
if battery_voltage is not None:
|
||||
self.payload[52:54] = struct.pack('>H', int(battery_voltage * 10))
|
||||
if battery_current is not None:
|
||||
self.payload[54:56] = struct.pack('>h', int(battery_current * 10)) # Signed
|
||||
if solar_voltage is not None:
|
||||
self.payload[56:58] = struct.pack('>H', int(solar_voltage * 10))
|
||||
if solar_power is not None:
|
||||
self.payload[58:60] = struct.pack('>H', int(solar_power))
|
||||
if charger_status is not None:
|
||||
self.payload[60:62] = struct.pack('>H', int(charger_status))
|
||||
|
||||
def set_wind(self, speed, direction):
|
||||
"""Set wind meter values (bytes 62-65)"""
|
||||
if speed is not None:
|
||||
self.payload[62:64] = struct.pack('>H', int(speed * 10))
|
||||
if direction is not None:
|
||||
self.payload[64:66] = struct.pack('>H', int(direction))
|
||||
|
||||
def get_bytes(self):
|
||||
"""Get the complete 100-byte payload"""
|
||||
return bytes(self.payload)
|
||||
|
||||
def get_base64(self):
|
||||
"""Get base64 encoded payload for transmission"""
|
||||
import base64
|
||||
return base64.b64encode(self.payload).decode('ascii')
|
||||
|
||||
def read_complete_response(serial_connection, timeout=2, end_of_response_timeout=2, wait_for_lines=None, debug=True):
|
||||
'''
|
||||
Fonction très importante !!!
|
||||
@@ -334,7 +462,7 @@ def send_error_notification(device_id, error_type, additional_info=None):
|
||||
try:
|
||||
response = requests.post(alert_url, timeout=3)
|
||||
if response.status_code == 200:
|
||||
print(f"✅ Alert notification sent successfully")
|
||||
#print(f"✅ Alert notification sent successfully")
|
||||
return True
|
||||
else:
|
||||
print(f"⚠️ Alert notification failed: Status code {response.status_code}")
|
||||
@@ -436,6 +564,7 @@ def reset_server_hostname(profile_id):
|
||||
print("⚠️Reseting Server Hostname connection ")
|
||||
http_reset_success = False # Default fallback
|
||||
|
||||
#Pour AirCarto
|
||||
if profile_id == 0:
|
||||
print('<span style="color: orange;font-weight: bold;">🔧 Resetting AirCarto HTTP Profile</span>')
|
||||
command = f'AT+UHTTP={profile_id},1,"data.nebuleair.fr"\r'
|
||||
@@ -446,99 +575,25 @@ def reset_server_hostname(profile_id):
|
||||
http_reset_success = response_SARA_5 is not None and "OK" in response_SARA_5
|
||||
if not http_reset_success:
|
||||
print("⚠️ AirCarto HTTP profile reset failed")
|
||||
#Pour uSpot
|
||||
elif profile_id ==1:
|
||||
pass # TODO: implement handling for profile 1
|
||||
pass #on utilise la fonction reset_server_hostname_https pour uSpot
|
||||
else:
|
||||
print(f"❌ Unsupported profile ID: {profile_id}")
|
||||
http_reset_success = False
|
||||
return http_reset_success
|
||||
|
||||
def modem_complete_reboot_and_reinitialize(modem_version, aircarto_profile_id):
|
||||
def reset_server_hostname_https(profile_id):
|
||||
"""
|
||||
Performs a complete modem restart sequence:
|
||||
1. Reboots the modem using the appropriate command for its version
|
||||
2. Waits for the modem to restart
|
||||
3. Resets the HTTP profile
|
||||
4. For SARA-R5, resets the PDP connection
|
||||
|
||||
Args:
|
||||
modem_version (str): The modem version, e.g., 'SARA-R500' or 'SARA-R410'
|
||||
aircarto_profile_id (int): The HTTP profile ID to reset
|
||||
|
||||
Returns:
|
||||
bool: True if the complete sequence was successful, False otherwise
|
||||
Function that reset server hostname (URL) connection for the SARA R5
|
||||
returns true or false
|
||||
"""
|
||||
print('<span style="color: orange;font-weight: bold;">🔄 Complete SARA reboot and reinitialize sequence 🔄</span>')
|
||||
print("⚠️Reseting Server Hostname HTTS secure connection ")
|
||||
http_reset_success = False # Default fallback
|
||||
|
||||
# Step 1: Reboot the modem - Integrated modem_software_reboot logic
|
||||
print('<span style="color: orange;font-weight: bold;">🔄 Software SARA reboot (CFUN)! 🔄</span>')
|
||||
|
||||
# Use different commands based on modem version
|
||||
if 'R5' in modem_version: # For SARA-R5 series
|
||||
command = 'AT+CFUN=16\r' # Normal restart for R5
|
||||
else: # For SARA-R4 series
|
||||
command = 'AT+CFUN=15\r' # Factory reset for R4
|
||||
|
||||
#ATTENTION : AT+CFUN=16 sometimes causes the modem to reset before replying OK
|
||||
|
||||
ser_sara.write(command.encode('utf-8'))
|
||||
response = read_complete_response(ser_sara, wait_for_lines=["OK", "ERROR"], debug=True)
|
||||
|
||||
print('<p class="text-danger-emphasis">')
|
||||
print(response)
|
||||
print("</p>", end="")
|
||||
|
||||
# Check if reboot command was acknowledged
|
||||
if response is None or ("OK" not in response and "ERROR" in response):
|
||||
print("⚠️ Reboot command may have failed or modem restarted before responding.")
|
||||
# Still continue, as the modem may have rebooted correctly
|
||||
else:
|
||||
print("✅ Modem acknowledged reboot command.")
|
||||
|
||||
# Step 2: Wait for the modem to restart (adjust time as needed)
|
||||
print("Waiting for modem to restart...")
|
||||
time.sleep(7) # 7 seconds should be enough for most modems to restart
|
||||
|
||||
# Step 3: Check if modem is responsive after reboot
|
||||
print("Checking if modem is responsive...")
|
||||
|
||||
for attempt in range(5):
|
||||
ser_sara.write(b'AT\r')
|
||||
response_check = read_complete_response(ser_sara, wait_for_lines=["OK"], debug=True)
|
||||
if response_check and "OK" in response_check:
|
||||
print("✅ Modem is responsive after reboot.")
|
||||
break
|
||||
print(f"⏳ Waiting for modem... attempt {attempt + 1}")
|
||||
time.sleep(2)
|
||||
else:
|
||||
print("❌ Modem not responding after reboot.")
|
||||
return False
|
||||
|
||||
# Step 4: Reset AirCarto HTTP Profile
|
||||
print('<span style="color: orange;font-weight: bold;">🔧 Resetting AirCarto HTTP Profile</span>')
|
||||
#command = f'AT+UHTTP={aircarto_profile_id},1,"data.nebuleair.fr"\r'
|
||||
#ser_sara.write(command.encode('utf-8'))
|
||||
#responseResetHTTP = read_complete_response(ser_sara, timeout=5, end_of_response_timeout=5,wait_for_lines=["OK", "+CME ERROR"], debug=True)
|
||||
#print('<p class="text-danger-emphasis">')
|
||||
#print(responseResetHTTP)
|
||||
#print("</p>", end="")
|
||||
|
||||
print("➡️SET URL")
|
||||
command = f'AT+UHTTP={aircarto_profile_id},1,"data.nebuleair.fr"\r'
|
||||
ser_sara.write((command + '\r').encode('utf-8'))
|
||||
response_SARA_5 = read_complete_response(ser_sara, wait_for_lines=["OK"])
|
||||
print(response_SARA_5)
|
||||
time.sleep(1)
|
||||
|
||||
|
||||
http_reset_success = response_SARA_5 is not None and "OK" in response_SARA_5
|
||||
if not http_reset_success:
|
||||
print("⚠️ AirCarto HTTP profile reset failed")
|
||||
# Continue anyway, don't return False here
|
||||
|
||||
if send_uSpot:
|
||||
print('<span style="color: orange;font-weight: bold;">🔧 Resetting uSpot HTTP Profile</span>')
|
||||
uSpot_profile_id = 1
|
||||
#Pour uSpot
|
||||
if profile_id == 1:
|
||||
print('<span style="color: orange;font-weight: bold;">🔧 Resetting uSpot HTTPs Profile</span>')
|
||||
uSpot_url="api-prod.uspot.probesys.net"
|
||||
security_profile_id = 1
|
||||
|
||||
@@ -613,7 +668,7 @@ def modem_complete_reboot_and_reinitialize(modem_version, aircarto_profile_id):
|
||||
|
||||
#step 4: set url (op_code = 1)
|
||||
print("➡️SET URL")
|
||||
command = f'AT+UHTTP={uSpot_profile_id},1,"{uSpot_url}"\r'
|
||||
command = f'AT+UHTTP={profile_id},1,"{uSpot_url}"\r'
|
||||
ser_sara.write((command + '\r').encode('utf-8'))
|
||||
response_SARA_5 = read_complete_response(ser_sara, wait_for_lines=["OK"])
|
||||
print(response_SARA_5)
|
||||
@@ -622,7 +677,7 @@ def modem_complete_reboot_and_reinitialize(modem_version, aircarto_profile_id):
|
||||
#step 4: set PORT (op_code = 5)
|
||||
print("➡️SET PORT")
|
||||
port = 443
|
||||
command = f'AT+UHTTP={uSpot_profile_id},5,{port}\r'
|
||||
command = f'AT+UHTTP={profile_id},5,{port}\r'
|
||||
ser_sara.write((command + '\r').encode('utf-8'))
|
||||
response_SARA_55 = read_complete_response(ser_sara, wait_for_lines=["OK"])
|
||||
print(response_SARA_55)
|
||||
@@ -631,15 +686,24 @@ def modem_complete_reboot_and_reinitialize(modem_version, aircarto_profile_id):
|
||||
#step 4: set url to SSL (op_code = 6) (http_secure = 1 for HTTPS)(USECMNG_PROFILE = 2)
|
||||
print("➡️SET SSL")
|
||||
http_secure = 1
|
||||
command = f'AT+UHTTP={uSpot_profile_id},6,{http_secure},{security_profile_id}\r'
|
||||
command = f'AT+UHTTP={profile_id},6,{http_secure},{security_profile_id}\r'
|
||||
|
||||
ser_sara.write(command.encode('utf-8'))
|
||||
response_SARA_5fg = read_complete_response(ser_sara, wait_for_lines=["OK"])
|
||||
print(response_SARA_5fg)
|
||||
time.sleep(1)
|
||||
|
||||
# Return overall success
|
||||
return http_reset_success and pdp_reset_success
|
||||
http_reset_success = response_SARA_5 is not None and "OK" in response_SARA_5
|
||||
if not http_reset_success:
|
||||
print("⚠️ AirCarto HTTP profile reset failed")
|
||||
#Pour uSpot
|
||||
elif profile_id ==1:
|
||||
pass #on utilise la fonction reset_server_hostname_https pour uSpot
|
||||
else:
|
||||
print(f"❌ Unsupported profile ID: {profile_id}")
|
||||
http_reset_success = False
|
||||
return http_reset_success
|
||||
|
||||
|
||||
try:
|
||||
'''
|
||||
@@ -651,6 +715,12 @@ try:
|
||||
|
||||
'''
|
||||
print('<h3>START LOOP</h3>')
|
||||
#payload = SensorPayload(device_id)
|
||||
payload = SensorPayload("484AE134")
|
||||
print("deviceID (ASCII):")
|
||||
print(payload.get_bytes()[:8].hex())
|
||||
|
||||
|
||||
#print(f'Modem version: {modem_version}')
|
||||
|
||||
#Local timestamp
|
||||
@@ -698,12 +768,18 @@ try:
|
||||
num_columns = len(data_values[0])
|
||||
averages = [round(sum(col) / len(col),1) for col in zip(*data_values)]
|
||||
|
||||
|
||||
PM1 = averages[0]
|
||||
PM25 = averages[1]
|
||||
PM10 = averages[2]
|
||||
npm_temp = averages[3]
|
||||
npm_hum = averages[4]
|
||||
|
||||
print(f"PM1: {PM1}")
|
||||
print(f"PM2.5: {PM25}")
|
||||
print(f"PM10: {PM10}")
|
||||
|
||||
|
||||
#Add data to payload CSV
|
||||
payload_csv[0] = PM1
|
||||
payload_csv[1] = PM25
|
||||
@@ -711,6 +787,10 @@ try:
|
||||
payload_csv[18] = npm_temp
|
||||
payload_csv[19] = npm_hum
|
||||
|
||||
#add data to payload UDP
|
||||
payload.set_npm_core(PM1, PM25, PM10)
|
||||
payload.set_npm_internal(npm_temp, npm_hum)
|
||||
|
||||
#Add data to payload JSON
|
||||
payload_json["sensordatavalues"].append({"value_type": "NPM_P0", "value": str(PM1)})
|
||||
payload_json["sensordatavalues"].append({"value_type": "NPM_P1", "value": str(PM10)})
|
||||
@@ -751,6 +831,13 @@ try:
|
||||
payload_csv[4] = BME280_humidity
|
||||
payload_csv[5] = BME280_pressure
|
||||
|
||||
#Add data to payload UDP
|
||||
payload.set_bme280(
|
||||
temperature=last_row[1],
|
||||
humidity=last_row[2],
|
||||
pressure=last_row[3]
|
||||
)
|
||||
|
||||
#Add data to payload JSON
|
||||
payload_json["sensordatavalues"].append({"value_type": "BME280_temperature", "value": str(BME280_temperature)})
|
||||
payload_json["sensordatavalues"].append({"value_type": "BME280_humidity", "value": str(BME280_humidity)})
|
||||
@@ -779,6 +866,17 @@ try:
|
||||
payload_csv[9] = averages[0] # envea_no2
|
||||
payload_csv[10] = averages[1] # envea_h2s
|
||||
payload_csv[11] = averages[2] # envea_nh3
|
||||
payload_csv[27] = averages[3] # envea_CO
|
||||
payload_csv[28] = averages[4] # envea_O3
|
||||
|
||||
#Add data to payload UDP
|
||||
payload.set_envea(
|
||||
no2=averages[0],
|
||||
h2s=averages[1],
|
||||
nh3=averages[2],
|
||||
co=averages[3],
|
||||
o3=averages[4]
|
||||
)
|
||||
|
||||
#Add data to payload JSON
|
||||
payload_json["sensordatavalues"].append({"value_type": "CAIRSENS_NO2", "value": str(averages[0])})
|
||||
@@ -799,6 +897,12 @@ try:
|
||||
payload_csv[25] = wind_speed
|
||||
payload_csv[26] = wind_direction
|
||||
|
||||
#Add data to payload UDP
|
||||
payload.set_wind(
|
||||
speed=last_row[1],
|
||||
direction=last_row[2]
|
||||
)
|
||||
|
||||
else:
|
||||
print("No data available in the database.")
|
||||
|
||||
@@ -822,9 +926,43 @@ try:
|
||||
payload_csv[22] = solar_voltage
|
||||
payload_csv[23] = solar_power
|
||||
payload_csv[24] = charger_status
|
||||
|
||||
#Add data to payload UDP
|
||||
payload.set_mppt(
|
||||
battery_voltage=last_row[1],
|
||||
battery_current=last_row[2],
|
||||
solar_voltage=last_row[3],
|
||||
solar_power=last_row[4],
|
||||
charger_status=last_row[5]
|
||||
)
|
||||
else:
|
||||
print("No data available in the database.")
|
||||
|
||||
# NOISE sensor
|
||||
if NOISE_sensor:
|
||||
print("➡️Getting NOISE sensor values")
|
||||
cursor.execute("SELECT * FROM data_NOISE ORDER BY rowid DESC LIMIT 1")
|
||||
last_row = cursor.fetchone()
|
||||
if last_row:
|
||||
print("SQLite DB last available row:", last_row)
|
||||
cur_LEQ = last_row[1]
|
||||
cur_level = last_row[2]
|
||||
|
||||
#Add data to payload CSV
|
||||
<<<<<<< HEAD
|
||||
payload_csv[6] = DB_A_value
|
||||
|
||||
#Add data to payload UDP
|
||||
payload.set_noise(
|
||||
avg_noise=last_row[2], # DB_A_value
|
||||
max_noise=None, # Add if available
|
||||
min_noise=None # Add if available
|
||||
)
|
||||
=======
|
||||
payload_csv[6] = cur_LEQ
|
||||
payload_csv[7] = cur_level
|
||||
>>>>>>> refs/remotes/origin/main
|
||||
|
||||
#print("Verify SARA connection (AT)")
|
||||
|
||||
# Getting the LTE Signal (AT+CSQ)
|
||||
@@ -897,12 +1035,16 @@ try:
|
||||
if match:
|
||||
signal_quality = int(match.group(1))
|
||||
payload_csv[12]=signal_quality
|
||||
payload.set_signal_quality(signal_quality)
|
||||
|
||||
time.sleep(0.1)
|
||||
|
||||
# On vérifie si le signal n'est pas à 99 pour déconnexion
|
||||
# si c'est le cas on essaie de se reconnecter
|
||||
if signal_quality == 99:
|
||||
print('<span style="color: red;font-weight: bold;">⚠️ATTENTION: Signal Quality indicates no signal (99)⚠️</span>')
|
||||
|
||||
#Pas besoin d'essayer de se reconnecter car reconnection automatique
|
||||
print("TRY TO RECONNECT:")
|
||||
command = f'AT+COPS=1,2,{selected_networkID}\r'
|
||||
#command = f'AT+COPS=0\r'
|
||||
@@ -911,16 +1053,106 @@ try:
|
||||
print('<p class="text-danger-emphasis">')
|
||||
print(responseReconnect)
|
||||
print("</p>", end="")
|
||||
|
||||
print('🛑STOP LOOP🛑')
|
||||
print("<hr>")
|
||||
|
||||
#on arrete le script pas besoin de continuer
|
||||
sys.exit()
|
||||
else:
|
||||
print("Signal Quality:", signal_quality)
|
||||
|
||||
|
||||
|
||||
'''
|
||||
____ _____ _ _ ____ _ _ ____ ____
|
||||
/ ___|| ____| \ | | _ \ | | | | _ \| _ \
|
||||
\___ \| _| | \| | | | | | | | | | | | |_) |
|
||||
___) | |___| |\ | |_| | | |_| | |_| | __/
|
||||
|____/|_____|_| \_|____/ \___/|____/|_|
|
||||
|
||||
|
||||
'''
|
||||
|
||||
if send_miotiq:
|
||||
print('<p class="fw-bold">➡️SEND TO MIOTIQ</p>', end="")
|
||||
|
||||
binary_data = payload.get_bytes()
|
||||
|
||||
print(f"Binary payload: {len(binary_data)} bytes")
|
||||
|
||||
|
||||
#create UDP socket (will return socket number) -> 17 is UDP protocol and 6 is TCP protocol
|
||||
# IF ERROR -> need to create the PDP connection
|
||||
print("Create Socket:", end="")
|
||||
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", "+CME ERROR", "ERROR"], debug=False)
|
||||
print('<p class="text-danger-emphasis">')
|
||||
print(response_SARA_1)
|
||||
print("</p>", end="")
|
||||
|
||||
if "+CME ERROR" in response_SARA_1 or "ERROR" in response_SARA_1:
|
||||
print('<span style="color: red;font-weight: bold;">⚠️ATTENTION: need to reset PDP connection⚠️</span>')
|
||||
psd_csd_resets = reset_PSD_CSD_connection()
|
||||
if psd_csd_resets:
|
||||
print("✅PSD CSD connection reset successfully")
|
||||
else:
|
||||
print("⛔There were issues with the modem CSD PSD reinitialize process")
|
||||
|
||||
|
||||
#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")
|
||||
|
||||
#Connect to UDP server (USOCO)
|
||||
print("Connect to server:", end="")
|
||||
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('<p class="text-danger-emphasis">')
|
||||
print(response_SARA_2)
|
||||
print("</p>", end="")
|
||||
|
||||
# Write data and send
|
||||
|
||||
print(f"Write data: {len(binary_data)} bytes")
|
||||
command = f'AT+USOWR={socket_id},{len(binary_data)}\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('<p class="text-danger-emphasis">')
|
||||
print(response_SARA_2)
|
||||
print("</p>", end="")
|
||||
|
||||
# Send the raw payload bytes (already prepared)
|
||||
ser_sara.write(binary_data)
|
||||
response_SARA_2 = read_complete_response(ser_sara, wait_for_lines=["@","OK", "+CME ERROR", "ERROR"], debug=False)
|
||||
print('<p class="text-danger-emphasis">')
|
||||
print(response_SARA_2)
|
||||
print("</p>", end="")
|
||||
|
||||
#Read reply from server (USORD)
|
||||
#print("Read reply:", end="")
|
||||
#command = f'AT+USORD=0,100\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('<p class="text-danger-emphasis">')
|
||||
#print(response_SARA_2)
|
||||
#print("</p>", end="")
|
||||
|
||||
#Close socket
|
||||
print("Close socket:", end="")
|
||||
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('<p class="text-danger-emphasis">')
|
||||
print(response_SARA_2)
|
||||
print("</p>", end="")
|
||||
|
||||
|
||||
|
||||
'''
|
||||
____ _____ _ _ ____ _ ___ ____ ____ _ ____ _____ ___
|
||||
/ ___|| ____| \ | | _ \ / \ |_ _| _ \ / ___| / \ | _ \_ _/ _ \
|
||||
@@ -930,6 +1162,8 @@ try:
|
||||
|
||||
'''
|
||||
|
||||
if send_aircarto:
|
||||
|
||||
print('<p class="fw-bold">➡️SEND TO AIRCARTO SERVERS</p>', end="")
|
||||
# Write Data to saraR4
|
||||
# 1. Open sensordata_csv.json (with correct data size)
|
||||
@@ -1052,17 +1286,24 @@ try:
|
||||
# Display interpretation based on error code
|
||||
if error_code == 0:
|
||||
print('<p class="text-success">No error detected</p>')
|
||||
# INVALID SERVER HOSTNAME
|
||||
elif error_code == 4:
|
||||
print('<p class="text-danger">Error 4: Invalid server Hostname</p>')
|
||||
send_error_notification(device_id, "UHTTPER (error n°4) -> Invalid Server Hostname")
|
||||
print('<p class="text-danger">Error 4: AirCarto - Invalid server Hostname</p>')
|
||||
send_error_notification(device_id, "UHTTPER (error n°4) -> AirCarto Invalid Server Hostname")
|
||||
server_hostname_resets = reset_server_hostname(aircarto_profile_id)
|
||||
if server_hostname_resets:
|
||||
print("✅server hostname reset successfully")
|
||||
else:
|
||||
print("⛔There were issues with the modem server hostname reinitialize process")
|
||||
|
||||
# SERVER CONNECTION ERROR
|
||||
elif error_code == 11:
|
||||
print('<p class="text-danger">Error 11: Server connection error</p>')
|
||||
hardware_reboot_success = modem_hardware_reboot()
|
||||
if hardware_reboot_success:
|
||||
print("✅Modem successfully rebooted and reinitialized")
|
||||
else:
|
||||
print("⛔There were issues with the modem reboot/reinitialize process")
|
||||
# PSD OR CSD ERROR
|
||||
elif error_code == 22:
|
||||
print('<p class="text-danger">⚠️Error 22: PSD or CSD connection not established (SARA-R5 need to reset PDP conection)⚠️</p>')
|
||||
send_error_notification(device_id, "UHTTPER (error n°22) -> PSD or CSD connection not established")
|
||||
@@ -1071,12 +1312,15 @@ try:
|
||||
print("✅PSD CSD connection reset successfully")
|
||||
else:
|
||||
print("⛔There were issues with the modem CSD PSD reinitialize process")
|
||||
# CONNECTION TIMED OUT
|
||||
elif error_code == 26:
|
||||
print('<p class="text-danger">Error 26: Connection timed out</p>')
|
||||
send_error_notification(device_id, "UHTTPER (error n°26) -> Connection timed out")
|
||||
# CONNECTION LOST
|
||||
elif error_code == 44:
|
||||
print('<p class="text-danger">Error 44: Connection lost</p>')
|
||||
send_error_notification(device_id, "UHTTPER (error n°44) -> Connection lost")
|
||||
# SECURE SOCKET ERROR
|
||||
elif error_code == 73:
|
||||
print('<p class="text-danger">Error 73: Secure socket connect error</p>')
|
||||
else:
|
||||
@@ -1085,13 +1329,6 @@ try:
|
||||
print('<p class="text-danger">Could not extract error code from response</p>')
|
||||
|
||||
|
||||
#Software Reboot
|
||||
#software_reboot_success = modem_complete_reboot_and_reinitialize(modem_version, aircarto_profile_id)
|
||||
#if software_reboot_success:
|
||||
# print("✅Modem successfully rebooted and reinitialized")
|
||||
#else:
|
||||
# print("⛔There were issues with the modem reboot/reinitialize process")
|
||||
|
||||
|
||||
# 2.2 code 1 (✅✅HHTP / UUHTTPCR succeded✅✅)
|
||||
else:
|
||||
@@ -1230,12 +1467,12 @@ try:
|
||||
#Send notification (WIFI)
|
||||
send_error_notification(device_id, "SARA CME ERROR")
|
||||
|
||||
#Software Reboot
|
||||
software_reboot_success = modem_complete_reboot_and_reinitialize(modem_version, aircarto_profile_id)
|
||||
if software_reboot_success:
|
||||
print("Modem successfully rebooted and reinitialized")
|
||||
#Hardware Reboot
|
||||
hardware_reboot_success = modem_hardware_reboot()
|
||||
if hardware_reboot_success:
|
||||
print("✅Modem successfully rebooted and reinitialized")
|
||||
else:
|
||||
print("There were issues with the modem reboot/reinitialize process")
|
||||
print("⛔There were issues with the modem reboot/reinitialize process")
|
||||
|
||||
|
||||
#5. empty json
|
||||
@@ -1349,9 +1586,16 @@ try:
|
||||
# Display interpretation based on error code
|
||||
if error_code == 0:
|
||||
print('<p class="text-success">No error detected</p>')
|
||||
# INVALID SERVER HOSTNAME
|
||||
elif error_code == 4:
|
||||
print('<p class="text-danger">Error 4: Invalid server Hostname</p>', end="")
|
||||
print('<p class="text-danger">Error 4: uSpot - Invalid server Hostname</p>', end="")
|
||||
send_error_notification(device_id, "UHTTPER (4) uSpot Invalid server Hostname")
|
||||
server_hostname_resets = reset_server_hostname_https(uSpot_profile_id)
|
||||
if server_hostname_resets:
|
||||
print("✅ uSpot - server hostname reset successfully")
|
||||
else:
|
||||
print("⛔There were issues with the modem server hostname reinitialize process")
|
||||
# SERVER CONNECTION ERROR
|
||||
elif error_code == 11:
|
||||
print('<p class="text-danger">Error 11: Server connection error</p>', end="")
|
||||
elif error_code == 22:
|
||||
@@ -1361,9 +1605,13 @@ try:
|
||||
elif error_code == 44:
|
||||
print('<p class="text-danger">Error 44: Connection lost</p>')
|
||||
elif error_code == 73:
|
||||
print('<p class="text-danger">Error 73: Secure socket connect error</p>', end="")
|
||||
print('<p class="text-danger">Error 73: uSpot - Secure socket connect error</p>', end="")
|
||||
send_error_notification(device_id, "uSpot - Secure socket connect error")
|
||||
#Software Reboot ??
|
||||
server_hostname_resets = reset_server_hostname_https(uSpot_profile_id)
|
||||
if server_hostname_resets:
|
||||
print("✅ uSpot - server hostname reset successfully")
|
||||
else:
|
||||
print("⛔There were issues with the modem server hostname reinitialize process")
|
||||
|
||||
else:
|
||||
print(f'<p class="text-danger">Unknown error code: {error_code}</p>',end="")
|
||||
@@ -1412,6 +1660,13 @@ try:
|
||||
print(f"HTTP response code: {http_response_code}")
|
||||
if http_response_code == 201:
|
||||
print('<span style="font-weight: bold;">✅✅HTTP 201 ressource created.</span>')
|
||||
elif http_response_code == 308:
|
||||
print('<span style="font-weight: bold;"> ⚠️⚠️HTTP 308 Redirect, need to set up HTTPS.</span>')
|
||||
server_hostname_resets = reset_server_hostname_https(uSpot_profile_id)
|
||||
if server_hostname_resets:
|
||||
print("✅server hostname reset successfully")
|
||||
else:
|
||||
print("⛔There were issues with the modem server hostname reinitialize process")
|
||||
|
||||
except Exception as e:
|
||||
# If any error occurs during parsing, keep the default value
|
||||
|
||||
@@ -1,39 +1,277 @@
|
||||
#!/bin/bash
|
||||
# File: /var/www/nebuleair_pro_4g/services/check_services.sh
|
||||
# Purpose: Check status of all NebuleAir services and logs
|
||||
# Install:
|
||||
# sudo chmod +x /var/www/nebuleair_pro_4g/services/check_services.sh
|
||||
# sudo /var/www/nebuleair_pro_4g/services/check_services.sh
|
||||
# Version with fixed color handling for proper table display
|
||||
|
||||
echo "=== NebuleAir Services Status ==="
|
||||
echo ""
|
||||
# Colors for output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
CYAN='\033[0;36m'
|
||||
BOLD='\033[1m'
|
||||
DIM='\033[2m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# Check status of all timers
|
||||
echo "--- TIMER STATUS ---"
|
||||
systemctl list-timers | grep nebuleair
|
||||
echo ""
|
||||
# Service list
|
||||
SERVICES=("npm" "envea" "sara" "bme280" "mppt" "db-cleanup" "noise")
|
||||
|
||||
# Check status of all services
|
||||
echo "--- SERVICE STATUS ---"
|
||||
for service in npm envea sara bme280 mppt db-cleanup; do
|
||||
status=$(systemctl is-active nebuleair-$service-data.service)
|
||||
timer_status=$(systemctl is-active nebuleair-$service-data.timer)
|
||||
|
||||
echo "nebuleair-$service-data: Service=$status, Timer=$timer_status"
|
||||
done
|
||||
echo ""
|
||||
|
||||
# Show recent logs for each service
|
||||
echo "--- RECENT LOGS (last 5 entries per service) ---"
|
||||
for service in npm envea sara bme280 mppt db-cleanup; do
|
||||
echo "[$service service logs]"
|
||||
journalctl -u nebuleair-$service-data.service -n 5 --no-pager
|
||||
# Function to print header
|
||||
print_header() {
|
||||
local text="$1"
|
||||
echo ""
|
||||
echo -e "${BLUE}${BOLD}=== $text ===${NC}"
|
||||
echo -e "${BLUE}$(printf '%.0s=' {1..70})${NC}"
|
||||
}
|
||||
|
||||
# Function to print section
|
||||
print_section() {
|
||||
local text="$1"
|
||||
echo ""
|
||||
echo -e "${CYAN}${BOLD}--- $text ---${NC}"
|
||||
}
|
||||
|
||||
# Function to print a separator line
|
||||
print_separator() {
|
||||
echo "+--------------------------+-----------+-----------+-------------+-------------+-------------------------+"
|
||||
}
|
||||
|
||||
# Clear screen for clean output
|
||||
clear
|
||||
|
||||
# Main header
|
||||
print_header "NebuleAir Services Status Report"
|
||||
echo -e "Generated on: $(date '+%Y-%m-%d %H:%M:%S')"
|
||||
|
||||
# Timer Schedule
|
||||
print_section "Active Timers Schedule"
|
||||
echo ""
|
||||
systemctl list-timers --no-pager | head -n 1
|
||||
systemctl list-timers --no-pager | grep nebuleair || echo "No active nebuleair timers found"
|
||||
|
||||
# Service Status Overview with fixed color handling
|
||||
print_section "Service Status Overview"
|
||||
echo ""
|
||||
print_separator
|
||||
printf "| %-24s | %-9s | %-9s | %-11s | %-11s | %-23s |\n" "Service" "Svc State" "Svc Boot" "Timer State" "Timer Boot" "Health Status"
|
||||
print_separator
|
||||
|
||||
for service in "${SERVICES[@]}"; do
|
||||
# Check the actual service and timer names (with -data suffix)
|
||||
full_service_name="nebuleair-${service}-data"
|
||||
|
||||
# Get raw status values
|
||||
service_status=$(systemctl is-active ${full_service_name}.service 2>/dev/null | tr -d '\n' || echo "not-found")
|
||||
service_enabled=$(systemctl is-enabled ${full_service_name}.service 2>/dev/null | tr -d '\n' || echo "not-found")
|
||||
timer_status=$(systemctl is-active ${full_service_name}.timer 2>/dev/null | tr -d '\n' || echo "not-found")
|
||||
timer_enabled=$(systemctl is-enabled ${full_service_name}.timer 2>/dev/null | tr -d '\n' || echo "not-found")
|
||||
|
||||
# Check if files exist and override if not found
|
||||
if ! systemctl list-unit-files | grep -q "^${full_service_name}.service" &>/dev/null; then
|
||||
service_status="not-found"
|
||||
service_enabled="not-found"
|
||||
fi
|
||||
if ! systemctl list-unit-files | grep -q "^${full_service_name}.timer" &>/dev/null; then
|
||||
timer_status="not-found"
|
||||
timer_enabled="not-found"
|
||||
fi
|
||||
|
||||
# Create display strings without embedded colors for table cells
|
||||
case $service_status in
|
||||
"active") svc_st_display="active"; svc_st_color="${GREEN}" ;;
|
||||
"inactive") svc_st_display="inactive"; svc_st_color="${DIM}" ;;
|
||||
"activating") svc_st_display="starting"; svc_st_color="${YELLOW}" ;;
|
||||
"not-found") svc_st_display="missing"; svc_st_color="${RED}" ;;
|
||||
*) svc_st_display="$service_status"; svc_st_color="${RED}" ;;
|
||||
esac
|
||||
|
||||
case $service_enabled in
|
||||
"enabled"|"static") svc_en_display="enabled"; svc_en_color="${GREEN}" ;;
|
||||
"disabled") svc_en_display="disabled"; svc_en_color="${YELLOW}" ;;
|
||||
"not-found") svc_en_display="missing"; svc_en_color="${RED}" ;;
|
||||
*) svc_en_display="$service_enabled"; svc_en_color="${YELLOW}" ;;
|
||||
esac
|
||||
|
||||
case $timer_status in
|
||||
"active") tim_st_display="active"; tim_st_color="${GREEN}" ;;
|
||||
"inactive") tim_st_display="inactive"; tim_st_color="${RED}" ;;
|
||||
"not-found") tim_st_display="missing"; tim_st_color="${RED}" ;;
|
||||
*) tim_st_display="$timer_status"; tim_st_color="${RED}" ;;
|
||||
esac
|
||||
|
||||
case $timer_enabled in
|
||||
"enabled"|"static") tim_en_display="enabled"; tim_en_color="${GREEN}" ;;
|
||||
"disabled") tim_en_display="disabled"; tim_en_color="${YELLOW}" ;;
|
||||
"not-found") tim_en_display="missing"; tim_en_color="${RED}" ;;
|
||||
*) tim_en_display="$timer_enabled"; tim_en_color="${YELLOW}" ;;
|
||||
esac
|
||||
|
||||
# Determine health status
|
||||
if [[ "$timer_status" == "active" ]]; then
|
||||
if [[ "$timer_enabled" == "enabled" || "$timer_enabled" == "static" ]]; then
|
||||
health_display="✓ OK"
|
||||
health_color="${GREEN}"
|
||||
else
|
||||
health_display="⚠ Boot disabled"
|
||||
health_color="${YELLOW}"
|
||||
fi
|
||||
elif [[ "$timer_status" == "inactive" ]]; then
|
||||
health_display="✗ Timer stopped"
|
||||
health_color="${RED}"
|
||||
else
|
||||
health_display="✗ Timer missing"
|
||||
health_color="${RED}"
|
||||
fi
|
||||
|
||||
# Print row with colors applied outside of printf formatting
|
||||
printf "| %-24s | " "$full_service_name"
|
||||
printf "${svc_st_color}%-9s${NC} | " "$svc_st_display"
|
||||
printf "${svc_en_color}%-9s${NC} | " "$svc_en_display"
|
||||
printf "${tim_st_color}%-11s${NC} | " "$tim_st_display"
|
||||
printf "${tim_en_color}%-11s${NC} | " "$tim_en_display"
|
||||
printf "${health_color}%-23s${NC} |\n" "$health_display"
|
||||
done
|
||||
print_separator
|
||||
|
||||
# Understanding the table
|
||||
echo ""
|
||||
echo -e "${DIM}Note: For timer-based services, it's normal for the service to be 'inactive' and 'disabled'.${NC}"
|
||||
echo -e "${DIM} What matters is that the timer is 'active' and 'enabled'.${NC}"
|
||||
|
||||
# Configuration Issues
|
||||
print_section "Configuration Issues"
|
||||
echo ""
|
||||
issues_found=false
|
||||
|
||||
for service in "${SERVICES[@]}"; do
|
||||
full_service_name="nebuleair-${service}-data"
|
||||
|
||||
timer_status=$(systemctl is-active ${full_service_name}.timer 2>/dev/null | tr -d '\n' || echo "not-found")
|
||||
timer_enabled=$(systemctl is-enabled ${full_service_name}.timer 2>/dev/null | tr -d '\n' || echo "not-found")
|
||||
|
||||
# Check if timer exists
|
||||
if ! systemctl list-unit-files | grep -q "^${full_service_name}.timer" &>/dev/null; then
|
||||
timer_status="not-found"
|
||||
timer_enabled="not-found"
|
||||
fi
|
||||
|
||||
if [[ "$timer_status" != "active" || ("$timer_enabled" != "enabled" && "$timer_enabled" != "static") ]]; then
|
||||
issues_found=true
|
||||
echo -e " ${RED}•${NC} ${BOLD}$full_service_name${NC}"
|
||||
if [[ "$timer_status" == "not-found" ]]; then
|
||||
echo -e " ${RED}→${NC} Timer unit file is missing"
|
||||
elif [[ "$timer_status" != "active" ]]; then
|
||||
echo -e " ${RED}→${NC} Timer is not running (status: $timer_status)"
|
||||
fi
|
||||
if [[ "$timer_enabled" == "not-found" ]]; then
|
||||
echo -e " ${RED}→${NC} Timer unit file is missing"
|
||||
elif [[ "$timer_enabled" != "enabled" && "$timer_enabled" != "static" ]]; then
|
||||
echo -e " ${YELLOW}→${NC} Timer won't start on boot (status: $timer_enabled)"
|
||||
fi
|
||||
echo ""
|
||||
fi
|
||||
done
|
||||
|
||||
echo "=== End of Report ==="
|
||||
if [[ "$issues_found" == "false" ]]; then
|
||||
echo -e " ${GREEN}✓${NC} All timers are properly configured!"
|
||||
fi
|
||||
|
||||
# Recent Executions - Simplified
|
||||
print_section "Last Execution Status"
|
||||
echo ""
|
||||
printf " %-12s %-20s %s\n" "Service" "Last Run" "Status"
|
||||
printf " %-12s %-20s %s\n" "-------" "--------" "------"
|
||||
|
||||
for service in "${SERVICES[@]}"; do
|
||||
full_service_name="nebuleair-${service}-data"
|
||||
|
||||
# Get last execution time and status
|
||||
last_log=$(journalctl -u ${full_service_name}.service -n 3 --no-pager 2>/dev/null | grep -E "(Started|Finished|Failed)" | tail -1)
|
||||
|
||||
if [[ -n "$last_log" ]]; then
|
||||
timestamp=$(echo "$last_log" | awk '{print $1, $2, $3}')
|
||||
if echo "$last_log" | grep -q "Finished"; then
|
||||
status="${GREEN}✓ Success${NC}"
|
||||
elif echo "$last_log" | grep -q "Failed"; then
|
||||
status="${RED}✗ Failed${NC}"
|
||||
elif echo "$last_log" | grep -q "Started"; then
|
||||
status="${YELLOW}⟳ Running${NC}"
|
||||
else
|
||||
status="${DIM}- Unknown${NC}"
|
||||
fi
|
||||
printf " %-12s %-20s %b\n" "$service" "$timestamp" "$status"
|
||||
else
|
||||
printf " %-12s %-20s %b\n" "$service" "-" "${DIM}- No data${NC}"
|
||||
fi
|
||||
done
|
||||
|
||||
# Summary
|
||||
print_section "Summary"
|
||||
echo ""
|
||||
|
||||
working=0
|
||||
needs_attention=0
|
||||
|
||||
for service in "${SERVICES[@]}"; do
|
||||
full_service_name="nebuleair-${service}-data"
|
||||
timer_status=$(systemctl is-active ${full_service_name}.timer 2>/dev/null | tr -d '\n')
|
||||
timer_enabled=$(systemctl is-enabled ${full_service_name}.timer 2>/dev/null | tr -d '\n')
|
||||
|
||||
if [[ "$timer_status" == "active" ]] && [[ "$timer_enabled" == "enabled" || "$timer_enabled" == "static" ]]; then
|
||||
((working++))
|
||||
else
|
||||
((needs_attention++))
|
||||
fi
|
||||
done
|
||||
|
||||
total=${#SERVICES[@]}
|
||||
|
||||
# Visual progress bar
|
||||
echo -n " Overall Health: ["
|
||||
for ((i=1; i<=10; i++)); do
|
||||
if ((i <= working * 10 / total)); then
|
||||
echo -n -e "${GREEN}▰${NC}"
|
||||
else
|
||||
echo -n -e "${RED}▱${NC}"
|
||||
fi
|
||||
done
|
||||
echo -e "] ${working}/${total}"
|
||||
|
||||
echo ""
|
||||
echo -e " ${GREEN}✓${NC} Working properly: ${BOLD}$working${NC} services"
|
||||
echo -e " ${RED}✗${NC} Need attention: ${BOLD}$needs_attention${NC} services"
|
||||
|
||||
# Quick Commands
|
||||
print_section "Quick Commands"
|
||||
echo ""
|
||||
echo -e " ${BOLD}Fix a timer that needs attention:${NC}"
|
||||
echo " $ sudo systemctl enable --now nebuleair-[service]-data.timer"
|
||||
echo ""
|
||||
echo -e " ${BOLD}View live logs:${NC}"
|
||||
echo " $ sudo journalctl -u nebuleair-[service]-data.service -f"
|
||||
echo ""
|
||||
echo -e " ${BOLD}Check timer details:${NC}"
|
||||
echo " $ systemctl status nebuleair-[service]-data.timer"
|
||||
echo ""
|
||||
echo -e " ${BOLD}Run service manually:${NC}"
|
||||
echo " $ sudo systemctl start nebuleair-[service]-data.service"
|
||||
|
||||
# Specific fixes needed
|
||||
if [[ $needs_attention -gt 0 ]]; then
|
||||
echo ""
|
||||
echo -e "${YELLOW}${BOLD}Recommended Actions:${NC}"
|
||||
for service in "${SERVICES[@]}"; do
|
||||
full_service_name="nebuleair-${service}-data"
|
||||
timer_status=$(systemctl is-active ${full_service_name}.timer 2>/dev/null | tr -d '\n')
|
||||
timer_enabled=$(systemctl is-enabled ${full_service_name}.timer 2>/dev/null | tr -d '\n')
|
||||
|
||||
if [[ "$timer_status" != "active" ]] && [[ "$timer_status" != "not-found" ]]; then
|
||||
echo -e " ${RED}→${NC} sudo systemctl start ${full_service_name}.timer"
|
||||
fi
|
||||
if [[ "$timer_enabled" != "enabled" ]] && [[ "$timer_enabled" != "static" ]] && [[ "$timer_enabled" != "not-found" ]]; then
|
||||
echo -e " ${YELLOW}→${NC} sudo systemctl enable ${full_service_name}.timer"
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "For detailed logs use:"
|
||||
echo " sudo journalctl -u nebuleair-[service]-data.service -f"
|
||||
echo "To restart a specific service timer:"
|
||||
echo " sudo systemctl restart nebuleair-[service]-data.timer"
|
||||
@@ -173,6 +173,38 @@ AccuracySec=1s
|
||||
WantedBy=timers.target
|
||||
EOL
|
||||
|
||||
# Create service and timer files for noise Data (every minutes)
|
||||
cat > /etc/systemd/system/nebuleair-noise-data.service << 'EOL'
|
||||
[Unit]
|
||||
Description=NebuleAir noise Data Collection Service
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
ExecStart=/usr/bin/python3 /var/www/nebuleair_pro_4g/sound_meter/NSRT_mk4_get_data.py
|
||||
User=root
|
||||
WorkingDirectory=/var/www/nebuleair_pro_4g
|
||||
StandardOutput=append:/var/www/nebuleair_pro_4g/logs/noise_service.log
|
||||
StandardError=append:/var/www/nebuleair_pro_4g/logs/noise_service_errors.log
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOL
|
||||
|
||||
cat > /etc/systemd/system/nebuleair-noise-data.timer << 'EOL'
|
||||
[Unit]
|
||||
Description=Run NebuleAir MPPT Data Collection every 120 seconds
|
||||
Requires=nebuleair-noise-data.service
|
||||
|
||||
[Timer]
|
||||
OnBootSec=60s
|
||||
OnUnitActiveSec=60s
|
||||
AccuracySec=1s
|
||||
|
||||
[Install]
|
||||
WantedBy=timers.target
|
||||
EOL
|
||||
|
||||
# Create service and timer files for Database Cleanup
|
||||
cat > /etc/systemd/system/nebuleair-db-cleanup-data.service << 'EOL'
|
||||
[Unit]
|
||||
@@ -210,7 +242,7 @@ systemctl daemon-reload
|
||||
|
||||
# Enable and start all timers
|
||||
echo "Enabling and starting all services..."
|
||||
for service in npm envea sara bme280 mppt db-cleanup; do
|
||||
for service in npm envea sara bme280 mppt db-cleanup noise; do
|
||||
systemctl enable nebuleair-$service-data.timer
|
||||
systemctl start nebuleair-$service-data.timer
|
||||
echo "Started nebuleair-$service-data timer"
|
||||
|
||||
55
sound_meter/NSRT_MK4_change_config.py
Normal file
55
sound_meter/NSRT_MK4_change_config.py
Normal file
@@ -0,0 +1,55 @@
|
||||
'''
|
||||
____ ___ _ _ _ _ ____
|
||||
/ ___| / _ \| | | | \ | | _ \
|
||||
\___ \| | | | | | | \| | | | |
|
||||
___) | |_| | |_| | |\ | |_| |
|
||||
|____/ \___/ \___/|_| \_|____/
|
||||
|
||||
python3 /var/www/nebuleair_pro_4g/sound_meter/NSRT_MK4_change_config.py
|
||||
|
||||
1.Intervalle d'enregistrement
|
||||
L'intervalle d'enregistrement définit le temps entre deux points successifs enregistrés.
|
||||
Cela définit également la période d'intégration pour le LEQ, et la période d'observation pour L-min et L-max et Lpeak.
|
||||
L'intervalle d'enregistrement peut être réglé de 125 ms (1/8ème) à 2 H par incréments de 125 ms.
|
||||
|
||||
some parameters can be changed:
|
||||
write_tau(tau: float) -> time constant
|
||||
write_fs(frequency: int) -> sampling freq
|
||||
|
||||
'''
|
||||
|
||||
import nsrt_mk3_dev
|
||||
#from nsrt_mk3_dev import Weighting
|
||||
#from nsrt_mk3_dev.nsrt_mk3_dev import NsrtMk3Dev, Weighting
|
||||
from enum import Enum
|
||||
|
||||
class Weighting(Enum):
|
||||
DB_A = 1
|
||||
DB_C = 2
|
||||
DB_Z = 3
|
||||
|
||||
nsrt = nsrt_mk3_dev.NsrtMk3Dev('/dev/ttyACM0')
|
||||
|
||||
#####################
|
||||
#change time constant
|
||||
nsrt.write_tau(1)
|
||||
#####################
|
||||
|
||||
#####################
|
||||
#change Weighting curve
|
||||
# - Weighting.DB_A (A-weighting - most common for environmental noise)
|
||||
# - Weighting.DB_C (C-weighting - for peak measurements)
|
||||
# - Weighting.DB_Z (Z-weighting - linear/flat response)
|
||||
nsrt.write_weighting(Weighting.DB_A)
|
||||
#####################
|
||||
|
||||
freq_level = nsrt.read_fs() #current sampling frequency
|
||||
time_constant = nsrt.read_tau() #reads the current time constant
|
||||
leq_level = nsrt.read_leq() #current running LEQ and starts the integration of a new LEQ.
|
||||
weighting = nsrt.read_weighting() #weighting curve that is currently selected
|
||||
weighted_level = nsrt.read_level() #current running level in dB.
|
||||
|
||||
print(f'current sampling freq : {freq_level} Hz')
|
||||
print(f'current time constant : {time_constant} s')
|
||||
print(f'current LEQ level: {leq_level:0.2f} dB')
|
||||
print(f'{weighting} value: {weighted_level:0.2f} dBA')
|
||||
72
sound_meter/NSRT_mk4_get_data.py
Normal file
72
sound_meter/NSRT_mk4_get_data.py
Normal file
@@ -0,0 +1,72 @@
|
||||
'''
|
||||
____ ___ _ _ _ _ ____
|
||||
/ ___| / _ \| | | | \ | | _ \
|
||||
\___ \| | | | | | | \| | | | |
|
||||
___) | |_| | |_| | |\ | |_| |
|
||||
|____/ \___/ \___/|_| \_|____/
|
||||
|
||||
python3 /var/www/nebuleair_pro_4g/sound_meter/NSRT_mk4_get_data.py
|
||||
|
||||
Script to get data from the NSRT_MK4 Sound Level Meter
|
||||
|
||||
triggered by a systemd service
|
||||
sudo systemctl status nebuleair-noise-data.service
|
||||
|
||||
Need to install "nsrt_mk3_dev"
|
||||
|
||||
1.Intervalle d'enregistrement
|
||||
L'intervalle d'enregistrement définit le temps entre deux points successifs enregistrés.
|
||||
Cela définit également la période d'intégration pour le LEQ, et la période d'observation pour L-min et L-max et Lpeak.
|
||||
L'intervalle d'enregistrement peut être réglé de 125 ms (1/8ème) à 2 H par incréments de 125 ms.
|
||||
|
||||
some parameters can be changed:
|
||||
write_tau(tau: float) -> time constant
|
||||
write_fs(frequency: int) -> sampling freq
|
||||
|
||||
'''
|
||||
|
||||
import nsrt_mk3_dev
|
||||
import sqlite3
|
||||
|
||||
nsrt = nsrt_mk3_dev.NsrtMk3Dev('/dev/ttyACM0')
|
||||
|
||||
# Connect to the SQLite database
|
||||
conn = sqlite3.connect("/var/www/nebuleair_pro_4g/sqlite/sensors.db")
|
||||
cursor = conn.cursor()
|
||||
|
||||
#GET RTC TIME from SQlite
|
||||
cursor.execute("SELECT * FROM timestamp_table LIMIT 1")
|
||||
row = cursor.fetchone() # Get the first (and only) row
|
||||
rtc_time_str = row[1] # '2025-02-07 12:30:45'
|
||||
|
||||
freq_level = nsrt.read_fs() #current sampling frequency
|
||||
time_constant = nsrt.read_tau() #reads the current time constant
|
||||
leq_level = nsrt.read_leq() #current running LEQ and starts the integration of a new LEQ.
|
||||
weighting = nsrt.read_weighting() #weighting curve that is currently selected ( A ou C)
|
||||
weighted_level = nsrt.read_level() #current running level in dB.
|
||||
|
||||
#print(f'current sampling freq : {freq_level} Hz')
|
||||
#print(f'current time constant : {time_constant} s')
|
||||
#print(f'current LEQ level: {leq_level:0.2f} dB')
|
||||
#print(f'{weighting} value: {weighted_level:0.2f} dBA')
|
||||
# Round values to 2 decimal places before saving
|
||||
leq_level_rounded = round(leq_level, 2)
|
||||
weighted_level_rounded = round(weighted_level, 2)
|
||||
|
||||
#save to db
|
||||
#save to sqlite database
|
||||
try:
|
||||
cursor.execute('''
|
||||
INSERT INTO data_NOISE (timestamp,current_LEQ, DB_A_value) VALUES (?,?,?)'''
|
||||
, (rtc_time_str,leq_level_rounded,weighted_level_rounded))
|
||||
|
||||
# Commit and close the connection
|
||||
conn.commit()
|
||||
|
||||
#print("Sensor data saved successfully!")
|
||||
|
||||
except Exception as e:
|
||||
print(f"Database error: {e}")
|
||||
|
||||
|
||||
conn.close()
|
||||
0
sound_meter/sound_meter → sound_meter/old/sound_meter
Executable file → Normal file
0
sound_meter/sound_meter → sound_meter/old/sound_meter
Executable file → Normal file
0
sound_meter/sound_meter.c → sound_meter/old/sound_meter.c
Executable file → Normal file
0
sound_meter/sound_meter.c → sound_meter/old/sound_meter.c
Executable file → Normal file
0
sound_meter/sound_meter_moving_avg → sound_meter/old/sound_meter_moving_avg
Executable file → Normal file
0
sound_meter/sound_meter_moving_avg → sound_meter/old/sound_meter_moving_avg
Executable file → Normal file
0
sound_meter/sound_meter_moving_avg.c → sound_meter/old/sound_meter_moving_avg.c
Executable file → Normal file
0
sound_meter/sound_meter_moving_avg.c → sound_meter/old/sound_meter_moving_avg.c
Executable file → Normal file
0
sound_meter/sound_meter_nonStop → sound_meter/old/sound_meter_nonStop
Executable file → Normal file
0
sound_meter/sound_meter_nonStop → sound_meter/old/sound_meter_nonStop
Executable file → Normal file
0
sound_meter/sound_meter_nonStop.c → sound_meter/old/sound_meter_nonStop.c
Executable file → Normal file
0
sound_meter/sound_meter_nonStop.c → sound_meter/old/sound_meter_nonStop.c
Executable file → Normal file
@@ -89,7 +89,8 @@ CREATE TABLE IF NOT EXISTS data_envea (
|
||||
h2s REAL,
|
||||
nh3 REAL,
|
||||
co REAL,
|
||||
o3 REAL
|
||||
o3 REAL,
|
||||
so2 REAL
|
||||
)
|
||||
""")
|
||||
|
||||
@@ -126,6 +127,14 @@ CREATE TABLE IF NOT EXISTS data_MPPT (
|
||||
)
|
||||
""")
|
||||
|
||||
# Create a table noise capture (NSRT mk4)
|
||||
cursor.execute("""
|
||||
CREATE TABLE IF NOT EXISTS data_NOISE (
|
||||
timestamp TEXT,
|
||||
current_LEQ REAL,
|
||||
DB_A_value REAL
|
||||
)
|
||||
""")
|
||||
|
||||
# Commit and close the connection
|
||||
conn.commit()
|
||||
|
||||
232
sqlite/delete.py
Normal file
232
sqlite/delete.py
Normal file
@@ -0,0 +1,232 @@
|
||||
'''
|
||||
____ ___ _ _ _
|
||||
/ ___| / _ \| | (_) |_ ___
|
||||
\___ \| | | | | | | __/ _ \
|
||||
___) | |_| | |___| | || __/
|
||||
|____/ \__\_\_____|_|\__\___|
|
||||
|
||||
Script to delete a table from sqlite database
|
||||
/usr/bin/python3 /var/www/nebuleair_pro_4g/sqlite/delete.py table_name [--confirm]
|
||||
|
||||
Available tables are:
|
||||
data_NPM
|
||||
data_NPM_5channels
|
||||
data_BME280
|
||||
data_envea
|
||||
timestamp_table
|
||||
data_MPPT
|
||||
data_WIND
|
||||
modem_status
|
||||
config_table
|
||||
envea_sondes_table
|
||||
|
||||
Examples:
|
||||
# Will ask for confirmation
|
||||
/usr/bin/python3 /var/www/nebuleair_pro_4g/sqlite/delete.py data_NPM
|
||||
|
||||
# Skip confirmation prompt
|
||||
/usr/bin/python3 /var/www/nebuleair_pro_4g/sqlite/delete.py data_NPM --confirm
|
||||
|
||||
# List all tables
|
||||
/usr/bin/python3 /var/www/nebuleair_pro_4g/sqlite/delete.py --list
|
||||
'''
|
||||
|
||||
import sqlite3
|
||||
import sys
|
||||
import os
|
||||
from datetime import datetime
|
||||
|
||||
def list_tables(cursor):
|
||||
"""List all tables in the database"""
|
||||
cursor.execute("SELECT name FROM sqlite_master WHERE type='table' ORDER BY name")
|
||||
tables = cursor.fetchall()
|
||||
|
||||
print("\n📋 Available tables:")
|
||||
print("-" * 40)
|
||||
for table in tables:
|
||||
# Get row count for each table
|
||||
cursor.execute(f"SELECT COUNT(*) FROM {table[0]}")
|
||||
count = cursor.fetchone()[0]
|
||||
print(f" {table[0]} ({count} rows)")
|
||||
print("-" * 40)
|
||||
|
||||
def get_table_info(cursor, table_name):
|
||||
"""Get information about a table"""
|
||||
try:
|
||||
# Check if table exists
|
||||
cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name=?", (table_name,))
|
||||
if not cursor.fetchone():
|
||||
return None
|
||||
|
||||
# Get row count
|
||||
cursor.execute(f"SELECT COUNT(*) FROM {table_name}")
|
||||
row_count = cursor.fetchone()[0]
|
||||
|
||||
# Get table schema
|
||||
cursor.execute(f"PRAGMA table_info({table_name})")
|
||||
columns = cursor.fetchall()
|
||||
|
||||
return {
|
||||
'row_count': row_count,
|
||||
'columns': columns
|
||||
}
|
||||
except sqlite3.Error as e:
|
||||
print(f"Error getting table info: {e}")
|
||||
return None
|
||||
|
||||
def backup_table(cursor, table_name, db_path):
|
||||
"""Create a backup of the table before deletion"""
|
||||
try:
|
||||
backup_dir = os.path.dirname(db_path)
|
||||
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
|
||||
backup_file = os.path.join(backup_dir, f"{table_name}_backup_{timestamp}.sql")
|
||||
|
||||
# Get table schema
|
||||
cursor.execute(f"SELECT sql FROM sqlite_master WHERE type='table' AND name=?", (table_name,))
|
||||
create_sql = cursor.fetchone()
|
||||
|
||||
if create_sql:
|
||||
with open(backup_file, 'w') as f:
|
||||
# Write table creation SQL
|
||||
f.write(f"-- Backup of table {table_name} created on {datetime.now()}\n")
|
||||
f.write(f"{create_sql[0]};\n\n")
|
||||
|
||||
# Write data
|
||||
cursor.execute(f"SELECT * FROM {table_name}")
|
||||
rows = cursor.fetchall()
|
||||
|
||||
if rows:
|
||||
# Get column names
|
||||
cursor.execute(f"PRAGMA table_info({table_name})")
|
||||
columns = [col[1] for col in cursor.fetchall()]
|
||||
|
||||
f.write(f"-- Data for table {table_name}\n")
|
||||
for row in rows:
|
||||
values = []
|
||||
for value in row:
|
||||
if value is None:
|
||||
values.append('NULL')
|
||||
elif isinstance(value, str):
|
||||
escaped_value = value.replace("'", "''")
|
||||
values.append(f"'{escaped_value}'")
|
||||
else:
|
||||
values.append(str(value))
|
||||
|
||||
f.write(f"INSERT INTO {table_name} ({', '.join(columns)}) VALUES ({', '.join(values)});\n")
|
||||
|
||||
print(f"✓ Table backed up to: {backup_file}")
|
||||
return backup_file
|
||||
except Exception as e:
|
||||
print(f"⚠️ Backup failed: {e}")
|
||||
return None
|
||||
|
||||
def delete_table(cursor, table_name, create_backup=True, db_path=None):
|
||||
"""Delete a table from the database"""
|
||||
|
||||
# Get table info first
|
||||
table_info = get_table_info(cursor, table_name)
|
||||
if not table_info:
|
||||
print(f"❌ Table '{table_name}' does not exist!")
|
||||
return False
|
||||
|
||||
print(f"\n📊 Table Information:")
|
||||
print(f" Name: {table_name}")
|
||||
print(f" Rows: {table_info['row_count']}")
|
||||
print(f" Columns: {len(table_info['columns'])}")
|
||||
|
||||
# Create backup if requested
|
||||
backup_file = None
|
||||
if create_backup and db_path:
|
||||
print(f"\n💾 Creating backup...")
|
||||
backup_file = backup_table(cursor, table_name, db_path)
|
||||
|
||||
try:
|
||||
# Delete the table
|
||||
cursor.execute(f"DROP TABLE {table_name}")
|
||||
print(f"\n✅ Table '{table_name}' deleted successfully!")
|
||||
|
||||
if backup_file:
|
||||
print(f" Backup saved: {backup_file}")
|
||||
|
||||
return True
|
||||
|
||||
except sqlite3.Error as e:
|
||||
print(f"❌ Error deleting table: {e}")
|
||||
return False
|
||||
|
||||
def main():
|
||||
if len(sys.argv) < 2:
|
||||
print("Usage: python3 delete_table.py <table_name> [--confirm] [--no-backup]")
|
||||
print(" python3 delete_table.py --list")
|
||||
sys.exit(1)
|
||||
|
||||
db_path = "/var/www/nebuleair_pro_4g/sqlite/sensors.db"
|
||||
|
||||
# Check if database exists
|
||||
if not os.path.exists(db_path):
|
||||
print(f"❌ Database not found: {db_path}")
|
||||
sys.exit(1)
|
||||
|
||||
# Parse arguments
|
||||
args = sys.argv[1:]
|
||||
|
||||
if '--list' in args:
|
||||
# List all tables
|
||||
conn = sqlite3.connect(db_path)
|
||||
cursor = conn.cursor()
|
||||
list_tables(cursor)
|
||||
conn.close()
|
||||
return
|
||||
|
||||
table_name = args[0]
|
||||
skip_confirmation = '--confirm' in args
|
||||
create_backup = '--no-backup' not in args
|
||||
|
||||
try:
|
||||
# Connect to database
|
||||
conn = sqlite3.connect(db_path)
|
||||
cursor = conn.cursor()
|
||||
|
||||
# List available tables first
|
||||
list_tables(cursor)
|
||||
|
||||
# Check if table exists
|
||||
table_info = get_table_info(cursor, table_name)
|
||||
if not table_info:
|
||||
print(f"\n❌ Table '{table_name}' does not exist!")
|
||||
conn.close()
|
||||
sys.exit(1)
|
||||
|
||||
# Confirmation prompt
|
||||
if not skip_confirmation:
|
||||
print(f"\n⚠️ WARNING: You are about to delete table '{table_name}'")
|
||||
print(f" This table contains {table_info['row_count']} rows")
|
||||
if create_backup:
|
||||
print(f" A backup will be created before deletion")
|
||||
else:
|
||||
print(f" NO BACKUP will be created (--no-backup flag used)")
|
||||
|
||||
response = input(f"\nAre you sure you want to delete '{table_name}'? (yes/no): ").lower().strip()
|
||||
|
||||
if response not in ['yes', 'y']:
|
||||
print("❌ Operation cancelled")
|
||||
conn.close()
|
||||
sys.exit(0)
|
||||
|
||||
# Perform deletion
|
||||
success = delete_table(cursor, table_name, create_backup, db_path)
|
||||
|
||||
if success:
|
||||
conn.commit()
|
||||
print(f"\n🎉 Operation completed successfully!")
|
||||
else:
|
||||
print(f"\n❌ Operation failed!")
|
||||
|
||||
conn.close()
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Error: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -9,6 +9,9 @@ Script to flush (delete) data from a sqlite database
|
||||
|
||||
/usr/bin/python3 /var/www/nebuleair_pro_4g/sqlite/flush_old_data.py
|
||||
|
||||
Script that is triggered by a systemd
|
||||
sudo systemctl status nebuleair-db-cleanup-data.service
|
||||
|
||||
Available table are
|
||||
|
||||
data_NPM
|
||||
@@ -16,56 +19,184 @@ data_NPM_5channels
|
||||
data_BME280
|
||||
data_envea
|
||||
timestamp_table
|
||||
data_MPPT
|
||||
data_NOISE
|
||||
data_WIND
|
||||
|
||||
'''
|
||||
|
||||
import sqlite3
|
||||
import datetime
|
||||
import sys
|
||||
|
||||
|
||||
# Connect to the SQLite database
|
||||
conn = sqlite3.connect("/var/www/nebuleair_pro_4g/sqlite/sensors.db")
|
||||
cursor = conn.cursor()
|
||||
def table_exists(cursor, table_name):
|
||||
"""Check if a table exists in the database"""
|
||||
try:
|
||||
cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name=?", (table_name,))
|
||||
return cursor.fetchone() is not None
|
||||
except sqlite3.Error as e:
|
||||
print(f"[ERROR] Failed to check if table '{table_name}' exists: {e}")
|
||||
return False
|
||||
|
||||
#GET RTC TIME from SQlite
|
||||
cursor.execute("SELECT * FROM timestamp_table LIMIT 1")
|
||||
row = cursor.fetchone() # Get the first (and only) row
|
||||
|
||||
if row:
|
||||
def get_table_count(cursor, table_name):
|
||||
"""Get the number of records in a table"""
|
||||
try:
|
||||
cursor.execute(f"SELECT COUNT(*) FROM {table_name}")
|
||||
return cursor.fetchone()[0]
|
||||
except sqlite3.Error as e:
|
||||
print(f"[WARNING] Could not get count for table '{table_name}': {e}")
|
||||
return 0
|
||||
|
||||
|
||||
def delete_old_records(cursor, table_name, cutoff_date_str):
|
||||
"""Delete old records from a specific table"""
|
||||
try:
|
||||
# First check how many records will be deleted
|
||||
cursor.execute(f"SELECT COUNT(*) FROM {table_name} WHERE timestamp < ?", (cutoff_date_str,))
|
||||
records_to_delete = cursor.fetchone()[0]
|
||||
|
||||
if records_to_delete == 0:
|
||||
print(f"[INFO] No old records to delete from '{table_name}'")
|
||||
return True
|
||||
|
||||
# Delete the records
|
||||
cursor.execute(f"DELETE FROM {table_name} WHERE timestamp < ?", (cutoff_date_str,))
|
||||
deleted_count = cursor.rowcount
|
||||
|
||||
print(f"[SUCCESS] Deleted {deleted_count} old records from '{table_name}'")
|
||||
return True
|
||||
|
||||
except sqlite3.Error as e:
|
||||
print(f"[ERROR] Failed to delete records from '{table_name}': {e}")
|
||||
return False
|
||||
|
||||
|
||||
def main():
|
||||
try:
|
||||
# Connect to the SQLite database
|
||||
print("[INFO] Connecting to database...")
|
||||
conn = sqlite3.connect("/var/www/nebuleair_pro_4g/sqlite/sensors.db")
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Check database connection
|
||||
cursor.execute("SELECT sqlite_version()")
|
||||
version = cursor.fetchone()[0]
|
||||
print(f"[INFO] Connected to SQLite version: {version}")
|
||||
|
||||
# GET RTC TIME from SQLite
|
||||
print("[INFO] Getting timestamp from database...")
|
||||
|
||||
# First check if timestamp_table exists
|
||||
if not table_exists(cursor, "timestamp_table"):
|
||||
print("[ERROR] timestamp_table does not exist!")
|
||||
return False
|
||||
|
||||
cursor.execute("SELECT * FROM timestamp_table LIMIT 1")
|
||||
row = cursor.fetchone()
|
||||
|
||||
if not row:
|
||||
print("[ERROR] No timestamp found in timestamp_table.")
|
||||
return False
|
||||
|
||||
rtc_time_str = row[1] # Assuming timestamp is stored as TEXT (YYYY-MM-DD HH:MM:SS)
|
||||
print(f"[INFO] Last recorded timestamp: {rtc_time_str}")
|
||||
|
||||
# Convert last_updated to a datetime object
|
||||
try:
|
||||
last_updated = datetime.datetime.strptime(rtc_time_str, "%Y-%m-%d %H:%M:%S")
|
||||
except ValueError as e:
|
||||
print(f"[ERROR] Invalid timestamp format: {e}")
|
||||
return False
|
||||
|
||||
# Calculate the cutoff date (3 months before last_updated)
|
||||
# Calculate the cutoff date (60 days before last_updated)
|
||||
cutoff_date = last_updated - datetime.timedelta(days=60)
|
||||
cutoff_date_str = cutoff_date.strftime("%Y-%m-%d %H:%M:%S")
|
||||
|
||||
print(f"[INFO] Deleting records older than: {cutoff_date_str}")
|
||||
|
||||
# List of tables to delete old data from
|
||||
tables_to_clean = ["data_NPM", "data_NPM_5channels", "data_BME280", "data_envea","data_WIND", "data_MPPT"]
|
||||
tables_to_clean = [
|
||||
"data_NPM",
|
||||
"data_NPM_5channels",
|
||||
"data_BME280",
|
||||
"data_envea",
|
||||
"data_WIND",
|
||||
"data_MPPT",
|
||||
"data_NOISE"
|
||||
]
|
||||
|
||||
# Check which tables actually exist
|
||||
existing_tables = []
|
||||
missing_tables = []
|
||||
|
||||
# Loop through each table and delete old data
|
||||
for table in tables_to_clean:
|
||||
delete_query = f"DELETE FROM {table} WHERE timestamp < ?"
|
||||
cursor.execute(delete_query, (cutoff_date_str,))
|
||||
print(f"[INFO] Deleted old records from {table}")
|
||||
if table_exists(cursor, table):
|
||||
existing_tables.append(table)
|
||||
record_count = get_table_count(cursor, table)
|
||||
print(f"[INFO] Table '{table}' exists with {record_count} records")
|
||||
else:
|
||||
missing_tables.append(table)
|
||||
print(f"[WARNING] Table '{table}' does not exist - skipping")
|
||||
|
||||
# **Commit changes before running VACUUM**
|
||||
if missing_tables:
|
||||
print(f"[INFO] Missing tables: {', '.join(missing_tables)}")
|
||||
|
||||
if not existing_tables:
|
||||
print("[WARNING] No tables found to clean!")
|
||||
return True
|
||||
|
||||
# Loop through existing tables and delete old data
|
||||
successful_deletions = 0
|
||||
failed_deletions = 0
|
||||
|
||||
for table in existing_tables:
|
||||
if delete_old_records(cursor, table, cutoff_date_str):
|
||||
successful_deletions += 1
|
||||
else:
|
||||
failed_deletions += 1
|
||||
|
||||
# Commit changes before running VACUUM
|
||||
print("[INFO] Committing changes...")
|
||||
conn.commit()
|
||||
print("[INFO] Changes committed successfully!")
|
||||
print("[SUCCESS] Changes committed successfully!")
|
||||
|
||||
# Now it's safe to run VACUUM
|
||||
# Only run VACUUM if at least some deletions were successful
|
||||
if successful_deletions > 0:
|
||||
print("[INFO] Running VACUUM to optimize database space...")
|
||||
try:
|
||||
cursor.execute("VACUUM")
|
||||
print("[SUCCESS] Database optimized successfully!")
|
||||
except sqlite3.Error as e:
|
||||
print(f"[WARNING] VACUUM failed: {e}")
|
||||
|
||||
# Summary
|
||||
print(f"\n[SUMMARY]")
|
||||
print(f"Tables processed successfully: {successful_deletions}")
|
||||
print(f"Tables with errors: {failed_deletions}")
|
||||
print(f"Tables skipped (missing): {len(missing_tables)}")
|
||||
|
||||
if failed_deletions == 0:
|
||||
print("[SUCCESS] Old data flushed successfully!")
|
||||
return True
|
||||
else:
|
||||
print("[WARNING] Some operations failed - check logs above")
|
||||
return False
|
||||
|
||||
else:
|
||||
print("[ERROR] No timestamp found in timestamp_table.")
|
||||
except sqlite3.Error as e:
|
||||
print(f"[ERROR] Database error: {e}")
|
||||
return False
|
||||
except Exception as e:
|
||||
print(f"[ERROR] Unexpected error: {e}")
|
||||
return False
|
||||
finally:
|
||||
# Always close the database connection
|
||||
if 'conn' in locals():
|
||||
conn.close()
|
||||
print("[INFO] Database connection closed")
|
||||
|
||||
|
||||
# Close the database connection
|
||||
conn.close()
|
||||
if __name__ == "__main__":
|
||||
success = main()
|
||||
sys.exit(0 if success else 1)
|
||||
@@ -41,12 +41,15 @@ config_entries = [
|
||||
("SARA_R4_network_status", "connected", "str"),
|
||||
("SARA_R4_neworkID", "20810", "int"),
|
||||
("WIFI_status", "connected", "str"),
|
||||
("send_aircarto", "1", "bool"),
|
||||
("send_uSpot", "0", "bool"),
|
||||
("send_miotiq", "0", "bool"),
|
||||
("npm_5channel", "0", "bool"),
|
||||
("envea", "0", "bool"),
|
||||
("windMeter", "0", "bool"),
|
||||
("BME280", "0", "bool"),
|
||||
("MPPT", "0", "bool"),
|
||||
("NOISE", "0", "bool"),
|
||||
("modem_version", "XXX", "str")
|
||||
]
|
||||
|
||||
@@ -56,18 +59,47 @@ for key, value, value_type in config_entries:
|
||||
(key, value, value_type)
|
||||
)
|
||||
|
||||
# Insert envea sondes
|
||||
# Clean up duplicate envea sondes first (keep only first occurrence of each name)
|
||||
print("Cleaning up duplicate envea sondes...")
|
||||
cursor.execute("""
|
||||
DELETE FROM envea_sondes_table
|
||||
WHERE id NOT IN (
|
||||
SELECT MIN(id)
|
||||
FROM envea_sondes_table
|
||||
GROUP BY name
|
||||
)
|
||||
""")
|
||||
deleted_count = cursor.rowcount
|
||||
if deleted_count > 0:
|
||||
print(f"Deleted {deleted_count} duplicate envea sonde entries")
|
||||
|
||||
# Insert envea sondes (only if they don't already exist)
|
||||
# Attention pour le H2S il y a plusieurs sondes
|
||||
# H2S 1ppm -> coef 4
|
||||
# H2S 20ppm -> coef 1
|
||||
# H2S 200ppm -> coef 10
|
||||
|
||||
envea_sondes = [
|
||||
(False, "ttyAMA4", "h2s", 4),
|
||||
(False, "ttyAMA4", "h2s", 4), #H2S
|
||||
(False, "ttyAMA3", "no2", 1),
|
||||
(False, "ttyAMA3", "nh3", 100),
|
||||
(False, "ttyAMA3", "so2", 4),
|
||||
(False, "ttyAMA2", "o3", 1)
|
||||
]
|
||||
|
||||
for connected, port, name, coefficient in envea_sondes:
|
||||
# Check if sensor with this name already exists
|
||||
cursor.execute("SELECT COUNT(*) FROM envea_sondes_table WHERE name = ?", (name,))
|
||||
exists = cursor.fetchone()[0] > 0
|
||||
|
||||
if not exists:
|
||||
cursor.execute(
|
||||
"INSERT OR IGNORE INTO envea_sondes_table (connected, port, name, coefficient) VALUES (?, ?, ?, ?)",
|
||||
"INSERT INTO envea_sondes_table (connected, port, name, coefficient) VALUES (?, ?, ?, ?)",
|
||||
(1 if connected else 0, port, name, coefficient)
|
||||
)
|
||||
print(f"Added envea sonde: {name}")
|
||||
else:
|
||||
print(f"Envea sonde '{name}' already exists, skipping")
|
||||
|
||||
|
||||
# Commit and close the connection
|
||||
|
||||
33
update_firmware.sh
Normal file → Executable file
33
update_firmware.sh
Normal file → Executable file
@@ -3,6 +3,7 @@
|
||||
# NebuleAir Pro 4G - Comprehensive Update Script
|
||||
# This script performs a complete system update including git pull,
|
||||
# config initialization, and service management
|
||||
# Non-interactive version for WebUI
|
||||
|
||||
echo "======================================"
|
||||
echo "NebuleAir Pro 4G - Firmware Update"
|
||||
@@ -13,6 +14,9 @@ echo ""
|
||||
# Set working directory
|
||||
cd /var/www/nebuleair_pro_4g
|
||||
|
||||
# Ensure this script is executable
|
||||
chmod +x /var/www/nebuleair_pro_4g/update_firmware.sh
|
||||
|
||||
# Function to print status messages
|
||||
print_status() {
|
||||
echo "[$(date '+%H:%M:%S')] $1"
|
||||
@@ -30,14 +34,23 @@ check_status() {
|
||||
|
||||
# Step 1: Git operations
|
||||
print_status "Step 1: Updating firmware from repository..."
|
||||
|
||||
# Disable filemode to prevent permission issues
|
||||
git -C /var/www/nebuleair_pro_4g config core.fileMode false
|
||||
check_status "Git fileMode disabled"
|
||||
|
||||
# Fetch latest changes
|
||||
git fetch origin
|
||||
check_status "Git fetch"
|
||||
|
||||
# Show current branch and any changes
|
||||
# Show current branch
|
||||
print_status "Current branch: $(git branch --show-current)"
|
||||
|
||||
# Check for local changes
|
||||
if [ -n "$(git status --porcelain)" ]; then
|
||||
print_status "Warning: Local changes detected:"
|
||||
git status --short
|
||||
print_status "Warning: Local changes detected, stashing..."
|
||||
git stash push -m "Auto-stash before update $(date)"
|
||||
check_status "Git stash"
|
||||
fi
|
||||
|
||||
# Pull latest changes
|
||||
@@ -59,6 +72,7 @@ sudo chmod 755 /var/www/nebuleair_pro_4g/NPM/*.py
|
||||
sudo chmod 755 /var/www/nebuleair_pro_4g/BME280/*.py
|
||||
sudo chmod 755 /var/www/nebuleair_pro_4g/SARA/*.py
|
||||
sudo chmod 755 /var/www/nebuleair_pro_4g/envea/*.py
|
||||
sudo chmod 755 /var/www/nebuleair_pro_4g/MPPT/*.py 2>/dev/null
|
||||
check_status "File permissions update"
|
||||
|
||||
# Step 4: Restart critical services if they exist
|
||||
@@ -72,16 +86,22 @@ services=(
|
||||
"nebuleair-sara-data.timer"
|
||||
"nebuleair-bme280-data.timer"
|
||||
"nebuleair-mppt-data.timer"
|
||||
"nebuleair-noise-data.timer"
|
||||
)
|
||||
|
||||
for service in "${services[@]}"; do
|
||||
if systemctl list-unit-files | grep -q "$service"; then
|
||||
print_status "Restarting service: $service"
|
||||
# Check if service is enabled before restarting
|
||||
if systemctl is-enabled --quiet "$service" 2>/dev/null; then
|
||||
print_status "Restarting enabled service: $service"
|
||||
sudo systemctl restart "$service"
|
||||
if systemctl is-active --quiet "$service"; then
|
||||
print_status "✓ $service is running"
|
||||
else
|
||||
print_status "⚠ $service may not be active"
|
||||
print_status "⚠ $service failed to start"
|
||||
fi
|
||||
else
|
||||
print_status "ℹ Service $service is disabled, skipping restart"
|
||||
fi
|
||||
else
|
||||
print_status "ℹ Service $service not found (may not be installed)"
|
||||
@@ -113,6 +133,9 @@ print_status "Step 6: Cleaning up..."
|
||||
sudo find /var/www/nebuleair_pro_4g/logs -name "*.log" -size +10M -exec truncate -s 0 {} \;
|
||||
check_status "Log cleanup"
|
||||
|
||||
print_status ""
|
||||
print_status "======================================"
|
||||
print_status "Update completed successfully!"
|
||||
print_status "======================================"
|
||||
|
||||
exit 0
|
||||
Reference in New Issue
Block a user