35 Commits

Author SHA1 Message Date
Your Name
9aab95edb6 update 2025-09-09 09:47:45 +02:00
root
fe61b56b5b update 2025-07-22 16:38:41 +01:00
Your Name
25c5a7a65a update 2025-07-22 15:36:36 +02:00
root
4d512685a0 update 2025-07-22 10:39:13 +02:00
root
44b2e2189d update 2025-07-21 12:22:56 +01:00
root
74fc3baece update 2025-07-21 11:11:09 +01:00
Your Name
0539cb67af update 2025-07-02 08:25:29 +01:00
Your Name
98115ab22b update 2025-07-02 08:01:41 +01:00
Your Name
2989a7a9ed update 2025-06-30 15:10:29 +01:00
Your Name
aa458fbac4 update 2025-06-30 14:59:40 +01:00
707dffd6f8 Actualiser installation_part2.sh 2025-06-24 20:39:37 +00:00
c917131b2d Actualiser installation_part1.sh 2025-06-23 09:36:04 +00:00
root
057dc7d87b update 2025-06-05 16:48:38 +02:00
Your Name
fcc30243f5 update 2025-06-05 15:06:08 +02:00
Your Name
75774cea62 update 2025-06-05 12:50:45 +02:00
Your Name
3731c2b7cf update 2025-06-05 12:42:35 +02:00
Your Name
1240ebf6cd update 2025-06-04 15:54:43 +02:00
root
e27f2430b7 update 2025-05-28 16:00:02 +02:00
root
ebdc4ae353 update 2025-05-28 15:59:39 +02:00
root
6cd5191138 update 2025-05-28 15:40:53 +02:00
Your Name
8d989de425 update 2025-05-27 16:48:48 +02:00
Your Name
381cf85336 update 2025-05-27 16:42:53 +02:00
root
caf5488b06 update 2025-05-27 12:09:34 +02:00
root
5d4f7225b0 update 2025-05-26 14:59:18 +02:00
Your Name
6d997ff550 update_firmware.sh 2025-05-26 09:51:31 +02:00
Your Name
aa71e359bb update 2025-05-26 09:48:55 +02:00
Your Name
7bd1d81bf9 update 2025-05-26 09:34:07 +02:00
Your Name
4bc0dc2acc update 2025-05-26 09:24:47 +02:00
Your Name
694edfaf27 update 2025-05-23 17:52:15 +02:00
Your Name
93d77db853 update 2025-05-23 17:49:03 +02:00
Your Name
122763a4e5 update 2025-05-23 17:43:45 +02:00
Your Name
c6a8b02c38 update 2025-05-23 17:22:37 +02:00
Your Name
b93f205fd4 update 2025-05-23 16:22:32 +02:00
Your Name
8fdd1d6ac5 update 2025-05-23 16:07:00 +02:00
Your Name
6796aa95bb update 2025-05-23 16:03:28 +02:00
35 changed files with 2533 additions and 645 deletions

View File

@@ -0,0 +1,8 @@
{
"permissions": {
"allow": [
"Bash(python3:*)"
],
"deny": []
}
}

2
.gitignore vendored
View File

@@ -14,4 +14,6 @@ NPM/data/*.txt
NPM/data/*.json
*.lock
sqlite/*.db
sqlite/*.sql
tests/

View File

@@ -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}...")
ser = serial.Serial(port, baudrate, timeout=1)
try:
log(f"Opening serial port {port} at {baudrate} baud...")
ser = serial.Serial(port, baudrate, timeout=1)
# Initialize data dictionary and tracking variables
data = {}
start_time = time.time()
# Clear any buffered data
ser.reset_input_buffer()
time.sleep(0.5)
while time.time() - start_time < timeout:
# 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
data = {}
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()
except UnicodeDecodeError as e:
log(f"Decode error: {e}", "ERROR")
continue
except Exception as e:
log(f"Error reading line: {e}", "ERROR")
continue
# Add small delay between attempts
if attempt < max_attempts - 1:
print("Waiting before next attempt...")
time.sleep(2)
# Timeout reached
log(f"Timeout after {timeout}s, read {lines_read} lines, saw {blocks_seen} blocks")
ser.close()
except Exception as e:
print(f"Error on attempt {attempt+1}: {e}")
try:
ser.close()
except:
pass
# 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
cursor.execute("SELECT * FROM timestamp_table LIMIT 1")
row = cursor.fetchone()
rtc_time_str = row[1]
# Validate critical values
battery_voltage = parsed_data.get('V', 0)
# Extract values
battery_voltage = parsed_data.get('V', 0)
if battery_voltage > 0:
# Get timestamp
try:
cursor.execute("SELECT * FROM timestamp_table LIMIT 1")
row = cursor.fetchone()
rtc_time_str = row[1] if row else time.strftime('%Y-%m-%d %H:%M:%S')
except:
rtc_time_str = time.strftime('%Y-%m-%d %H:%M:%S')
# Extract values for database
battery_current = parsed_data.get('I', 0)
solar_voltage = parsed_data.get('VPV', 0)
solar_power = parsed_data.get('PPV', 0)
charger_status = parsed_data.get('CS', 0)
# Save to database
try:
cursor.execute('''
INSERT INTO data_MPPT (timestamp, battery_voltage, battery_current, solar_voltage, solar_power, charger_status)
@@ -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:
print(f"Database error: {e}")
except sqlite3.Error as e:
# Always log database errors regardless of DEBUG_MODE
if not DEBUG_MODE:
print(f"Database error: {e}")
else:
log(f"\n✗ Database error: {e}", "ERROR")
conn.rollback()
else:
print("Invalid data: Battery voltage is zero or missing")
log("\nInvalid data: Battery voltage is zero or missing", "ERROR")
else:
print("Failed to parse data")
log("\nFailed to parse data", "ERROR")
else:
print("No valid data received from MPPT controller")
log("\nNo 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.")

View File

@@ -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
View 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
View 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

View 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
View 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

View File

View File

View 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
View 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}")

View File

@@ -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

View File

@@ -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"\nAn 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")

View File

@@ -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>
@@ -248,7 +270,34 @@
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast" aria-label="Close"></button>
</div>
</div>
</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>
@@ -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>

View File

@@ -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>";

View File

@@ -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
]);
}
}

View File

@@ -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
success "Sudo authorization added."
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..."

View File

@@ -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

File diff suppressed because it is too large Load Diff

View File

@@ -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"

View File

@@ -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"

View 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')

View 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()

View File

View File

View 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
View 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()

View File

@@ -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()
#GET RTC TIME from SQlite
cursor.execute("SELECT * FROM timestamp_table LIMIT 1")
row = cursor.fetchone() # Get the first (and only) row
if row:
rtc_time_str = row[1] # Assuming timestamp is stored as TEXT (YYYY-MM-DD HH:MM:SS)
print(f"[INFO] Last recorded timestamp: {rtc_time_str}")
# Convert last_updated to a datetime object
last_updated = datetime.datetime.strptime(rtc_time_str, "%Y-%m-%d %H:%M:%S")
# Calculate the cutoff date (3 months before last_updated)
cutoff_date = last_updated - datetime.timedelta(days=60)
cutoff_date_str = cutoff_date.strftime("%Y-%m-%d %H:%M:%S")
print(f"[INFO] Deleting records older than: {cutoff_date_str}")
# List of tables to delete old data from
tables_to_clean = ["data_NPM", "data_NPM_5channels", "data_BME280", "data_envea","data_WIND", "data_MPPT"]
# Loop through each table and delete old data
for table in tables_to_clean:
delete_query = f"DELETE FROM {table} WHERE timestamp < ?"
cursor.execute(delete_query, (cutoff_date_str,))
print(f"[INFO] Deleted old records from {table}")
# **Commit changes before running VACUUM**
conn.commit()
print("[INFO] Changes committed successfully!")
# Now it's safe to run VACUUM
print("[INFO] Running VACUUM to optimize database space...")
cursor.execute("VACUUM")
print("[SUCCESS] Old data flushed successfully!")
else:
print("[ERROR] No timestamp found in timestamp_table.")
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
# Close the database connection
conn.close()
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 (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",
"data_NOISE"
]
# Check which tables actually exist
existing_tables = []
missing_tables = []
for table in tables_to_clean:
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")
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("[SUCCESS] Changes committed successfully!")
# 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
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")
if __name__ == "__main__":
success = main()
sys.exit(0 if success else 1)

View File

@@ -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:
cursor.execute(
"INSERT OR IGNORE INTO envea_sondes_table (connected, port, name, coefficient) VALUES (?, ?, ?, ?)",
(1 if connected else 0, port, name, coefficient)
)
# 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 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

39
update_firmware.sh Normal file → Executable file
View 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"
sudo systemctl restart "$service"
if systemctl is-active --quiet "$service"; then
print_status "$service is running"
# 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 failed to start"
fi
else
print_status " $service may not be active"
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