This commit is contained in:
Your Name
2025-05-22 14:44:17 +02:00
parent ae62c472da
commit 8d1fd7ba63
10 changed files with 1024 additions and 184 deletions

View File

@@ -6,7 +6,7 @@ Version Pro du ModuleAir avec CM4, SaraR4 et ecran Matrix LED p2 64x64.
``` ```
sudo apt update sudo apt update
sudo apt install git gh apache2 sqlite3 php php-sqlite3 libsqlite3-dev python3 python3-pip jq g++ autossh i2c-tools python3-smbus -y sudo apt install git gh apache2 sqlite3 php php-sqlite3 libsqlite3-dev python3 python3-pip jq g++ autossh i2c-tools python3-smbus -y
sudo pip3 install pyserial requests sensirion-shdlc-sfa3x RPi.GPIO adafruit-circuitpython-bme280 crcmod psutil --break-system-packages sudo pip3 install pyserial requests sensirion-shdlc-sfa3x RPi.GPIO gpiozero adafruit-circuitpython-bme280 crcmod psutil --break-system-packages
sudo git clone http://gitea.aircarto.fr/PaulVua/moduleair_pro_4g.git /var/www/moduleair_pro_4g sudo git clone http://gitea.aircarto.fr/PaulVua/moduleair_pro_4g.git /var/www/moduleair_pro_4g
sudo mkdir /var/www/moduleair_pro_4g/logs sudo mkdir /var/www/moduleair_pro_4g/logs
sudo touch /var/www/moduleair_pro_4g/logs/app.log /var/www/moduleair_pro_4g/logs/loop.log /var/www/moduleair_pro_4g/matrix/input_NPM.txt /var/www/moduleair_pro_4g/matrix/input_MHZ16.txt /var/www/moduleair_pro_4g/wifi_list.csv sudo touch /var/www/moduleair_pro_4g/logs/app.log /var/www/moduleair_pro_4g/logs/loop.log /var/www/moduleair_pro_4g/matrix/input_NPM.txt /var/www/moduleair_pro_4g/matrix/input_MHZ16.txt /var/www/moduleair_pro_4g/wifi_list.csv

View File

@@ -28,51 +28,58 @@ port='/dev/'+parameter[0] # ex: ttyAMA2
command = parameter[1] # ex: AT+CCID? command = parameter[1] # ex: AT+CCID?
timeout = float(parameter[2]) # ex:2 timeout = float(parameter[2]) # ex:2
ser = serial.Serial(
port=port, #USB0 or ttyS0
baudrate=115200, #115200 ou 9600
parity=serial.PARITY_NONE, #PARITY_NONE, PARITY_EVEN or PARITY_ODD
stopbits=serial.STOPBITS_ONE,
bytesize=serial.EIGHTBITS,
timeout = timeout
)
ser.write((command + '\r').encode('utf-8'))
#ser.write(b'ATI\r') #General Information
#ser.write(b'AT+CCID?\r') #SIM card number
#ser.write(b'AT+CPIN?\r') #Check the status of the SIM card
#ser.write(b'AT+CIND?\r') #Indication state (last number is SIM detection: 0 no SIM detection, 1 SIM detected, 2 not available)
#ser.write(b'AT+UGPIOR=?\r') #Reads the current value of the specified GPIO pin
#ser.write(b'AT+UGPIOC?\r') #GPIO select configuration
#ser.write(b'AT+COPS=?\r') #Check the network and cellular technology the modem is currently using
#ser.write(b'AT+COPS=1,2,20801') #connext to orange
#ser.write(b'AT+CFUN=?\r') #Selects/read the level of functionality
#ser.write(b'AT+URAT=?\r') #Radio Access Technology
#ser.write(b'AT+USIMSTAT?')
#ser.write(b'AT+IPR=115200') #Check/Define baud rate
#ser.write(b'AT+CMUX=?')
try: try:
ser = serial.Serial(
port=port, #USB0 or ttyS0
baudrate=115200, #115200 ou 9600
parity=serial.PARITY_NONE, #PARITY_NONE, PARITY_EVEN or PARITY_ODD
stopbits=serial.STOPBITS_ONE,
bytesize=serial.EIGHTBITS,
timeout = timeout
)
ser.write((command + '\r').encode('utf-8'))
#ser.write(b'ATI\r') #General Information
#ser.write(b'AT+CCID?\r') #SIM card number
#ser.write(b'AT+CPIN?\r') #Check the status of the SIM card
#ser.write(b'AT+CIND?\r') #Indication state (last number is SIM detection: 0 no SIM detection, 1 SIM detected, 2 not available)
#ser.write(b'AT+UGPIOR=?\r') #Reads the current value of the specified GPIO pin
#ser.write(b'AT+UGPIOC?\r') #GPIO select configuration
#ser.write(b'AT+COPS=?\r') #Check the network and cellular technology the modem is currently using
#ser.write(b'AT+COPS=1,2,20801') #connext to orange
#ser.write(b'AT+CFUN=?\r') #Selects/read the level of functionality
#ser.write(b'AT+URAT=?\r') #Radio Access Technology
#ser.write(b'AT+USIMSTAT?')
#ser.write(b'AT+IPR=115200') #Check/Define baud rate
#ser.write(b'AT+CMUX=?')
# Read lines until a timeout occurs # Read lines until a timeout occurs
response_lines = [] response_lines = []
while True: start_time = time.time()
line = ser.readline().decode('utf-8').strip()
if not line: while (time.time() - start_time) < timeout:
break # Break the loop if an empty line is encountered line = ser.readline().decode('utf-8', errors='ignore').strip()
response_lines.append(line) if line:
response_lines.append(line)
# Check if we received any data
if not response_lines:
print(f"ERROR: No response received from {port} after sending command: {command}")
sys.exit(1)
# Print the response # Print the response
for line in response_lines: for line in response_lines:
print(line) print(line)
except serial.SerialException as e: except serial.SerialException as e:
print(f"Error: {e}") print(f"ERROR: Serial communication error: {e}")
sys.exit(1)
except Exception as e:
print(f"ERROR: Unexpected error: {e}")
sys.exit(1)
finally: finally:
if ser.is_open: # Close the serial port if it's open
ser.close() if 'ser' in locals() and ser.is_open:
#print("Serial closed") ser.close()

View File

@@ -92,11 +92,13 @@ import json
import serial import serial
import time import time
import busio import busio
import requests
import re import re
import os import os
import traceback import traceback
import sys import sys
import sqlite3 import sqlite3
import RPi.GPIO as GPIO
from threading import Thread from threading import Thread
from datetime import datetime from datetime import datetime
@@ -130,7 +132,6 @@ conn = sqlite3.connect("/var/www/moduleair_pro_4g/sqlite/sensors.db")
cursor = conn.cursor() cursor = conn.cursor()
#get config data from SQLite table #get config data from SQLite table
def load_config_sqlite(): def load_config_sqlite():
""" """
@@ -235,6 +236,9 @@ def read_complete_response(serial_connection, timeout=2, end_of_response_timeout
''' '''
Fonction très importante !!! Fonction très importante !!!
Reads the complete response from a serial connection and waits for specific lines. Reads the complete response from a serial connection and waits for specific lines.
timeout -> temps d'attente de la réponse de la première ligne (assez rapide car le SARA répond direct avec la commande recue)
end_of_response_timeout -> le temps d'inactivité entre deux lignes imprimées (plus long dans certain cas: le SARA mouline avant de finir vraiment)
wait_for_lines -> si on rencontre la string la fonction s'arrete
''' '''
if wait_for_lines is None: if wait_for_lines is None:
wait_for_lines = [] # Default to an empty list if not provided wait_for_lines = [] # Default to an empty list if not provided
@@ -336,121 +340,116 @@ def send_error_notification(device_id, error_type, additional_info=None):
return False return False
def modem_complete_reboot_and_reinitialize(modem_version, aircarto_profile_id): def modem_hardware_reboot():
""" """
Performs a complete modem restart sequence: Performs a hardware reboot using transistors connected to pin 16 and 20:
1. Reboots the modem using the appropriate command for its version pin 16 set to SARA GND
2. Waits for the modem to restart pin 20 set to SARA ON (not used)
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
"""
print('<span style="color: orange;font-weight: bold;">🔄 Complete SARA reboot and reinitialize sequence 🔄</span>')
# Step 1: Reboot the modem - Integrated modem_software_reboot logic
print('<span style="color: orange;font-weight: bold;">🔄 Software SARA reboot! 🔄</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
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
reboot_success = response is not None and "OK" in response
if not reboot_success:
print("⚠️ Modem reboot command failed")
return False
# Step 2: Wait for the modem to restart (adjust time as needed)
print("Waiting for modem to restart...")
time.sleep(15) # 15 seconds should be enough for most modems to restart
# Step 3: Check if modem is responsive after reboot
print("Checking if modem is responsive...")
ser_sara.write(b'AT\r')
response_check = read_complete_response(ser_sara, wait_for_lines=["OK"], debug=True)
if response_check is None or "OK" not in response_check:
print("⚠️ Modem not responding after reboot")
return False
print("✅ Modem restarted successfully")
# Step 4: Reset the HTTP Profile
print('<span style="color: orange;font-weight: bold;">🔧 Resetting the 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="")
http_reset_success = responseResetHTTP is not None and "OK" in responseResetHTTP
if not http_reset_success:
print("⚠️ HTTP profile reset failed")
# Continue anyway, don't return False here
# Step 5: For SARA-R5, reset the PDP connection
pdp_reset_success = True
if modem_version == "SARA-R500":
print("⚠️ Need to reset PDP connection for SARA-R500")
# Activate PDP context 1
print('➡️ Activate PDP context 1')
command = f'AT+CGACT=1,1\r'
ser_sara.write(command.encode('utf-8'))
response_pdp1 = read_complete_response(ser_sara, wait_for_lines=["OK"])
print(response_pdp1, end="")
pdp_reset_success = pdp_reset_success and (response_pdp1 is not None and "OK" in response_pdp1)
time.sleep(1)
# Set the PDP type
print('➡️ Set the PDP type to IPv4 referring to the output of the +CGDCONT read command')
command = f'AT+UPSD=0,0,0\r'
ser_sara.write(command.encode('utf-8'))
response_pdp2 = read_complete_response(ser_sara, wait_for_lines=["OK"])
print(response_pdp2, end="")
pdp_reset_success = pdp_reset_success and (response_pdp2 is not None and "OK" in response_pdp2)
time.sleep(1)
# Profile #0 is mapped on CID=1
print('➡️ Profile #0 is mapped on CID=1.')
command = f'AT+UPSD=0,100,1\r'
ser_sara.write(command.encode('utf-8'))
response_pdp3 = read_complete_response(ser_sara, wait_for_lines=["OK"])
print(response_pdp3, end="")
pdp_reset_success = pdp_reset_success and (response_pdp3 is not None and "OK" in response_pdp3)
time.sleep(1)
# Activate the PSD profile
print('➡️ Activate the PSD profile #0: the IPv4 address is already assigned by the network.')
command = f'AT+UPSDA=0,3\r'
ser_sara.write(command.encode('utf-8'))
response_pdp4 = read_complete_response(ser_sara, wait_for_lines=["OK", "+UUPSDA"])
print(response_pdp4, end="")
pdp_reset_success = pdp_reset_success and (response_pdp4 is not None and ("OK" in response_pdp4 or "+UUPSDA" in response_pdp4))
time.sleep(1)
if not pdp_reset_success:
print("⚠️ PDP connection reset had some issues")
# Return overall success
return http_reset_success and pdp_reset_success
LOW -> cut the current
HIGH -> current flow
"""
print('<span style="color: orange;font-weight: bold;">🔄 Hardware SARA reboot 🔄</span>')
SARA_power_GPIO = 16
SARA_ON_GPIO = 20
GPIO.setmode(GPIO.BCM) # Use BCM numbering
GPIO.setup(SARA_power_GPIO, GPIO.OUT) # Set GPIO17 as an output
GPIO.output(SARA_power_GPIO, GPIO.LOW)
time.sleep(2)
GPIO.output(SARA_power_GPIO, GPIO.HIGH)
time.sleep(2)
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.")
return True
print(f"⏳ Waiting for modem... attempt {attempt + 1}")
time.sleep(2)
else:
print("❌ Modem not responding after reboot.")
return False
def reset_PSD_CSD_connection():
"""
Function that reset the PSD CSD connection for the SARA R5
returns true or false
"""
print("Reseting PDP connection ")
pdp_reset_success = True
# Activate PDP context 1
print('➡️ Activate PDP context 1')
command = f'AT+CGACT=1,1\r'
ser_sara.write(command.encode('utf-8'))
response_pdp1 = read_complete_response(ser_sara, wait_for_lines=["OK"])
print(response_pdp1, end="")
pdp_reset_success = pdp_reset_success and (response_pdp1 is not None and "OK" in response_pdp1)
time.sleep(1)
# Set the PDP type
print('➡️ Set the PDP type to IPv4 referring to the output of the +CGDCONT read command')
command = f'AT+UPSD=0,0,0\r'
ser_sara.write(command.encode('utf-8'))
response_pdp2 = read_complete_response(ser_sara, wait_for_lines=["OK"])
print(response_pdp2, end="")
pdp_reset_success = pdp_reset_success and (response_pdp2 is not None and "OK" in response_pdp2)
time.sleep(1)
# Profile #0 is mapped on CID=1
print('➡️ Profile #0 is mapped on CID=1.')
command = f'AT+UPSD=0,100,1\r'
ser_sara.write(command.encode('utf-8'))
response_pdp3 = read_complete_response(ser_sara, wait_for_lines=["OK"])
print(response_pdp3, end="")
pdp_reset_success = pdp_reset_success and (response_pdp3 is not None and "OK" in response_pdp3)
time.sleep(1)
# Activate the PSD profile
print('➡️ Activate the PSD profile #0: the IPv4 address is already assigned by the network.')
command = f'AT+UPSDA=0,3\r'
ser_sara.write(command.encode('utf-8'))
response_pdp4 = read_complete_response(ser_sara, wait_for_lines=["OK", "+UUPSDA"])
print(response_pdp4, end="")
pdp_reset_success = pdp_reset_success and (response_pdp4 is not None and ("OK" in response_pdp4 or "+UUPSDA" in response_pdp4))
time.sleep(1)
if not pdp_reset_success:
print("⚠️ PDP connection reset had some issues")
return pdp_reset_success
def reset_server_hostname(profile_id):
"""
Function that reset server hostname (URL) connection for the SARA R5
returns true or false
"""
print("Reseting Server Hostname connection ")
http_reset_success = False # Default fallback
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'
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")
elif profile_id ==1:
pass # TODO: implement handling for profile 1
else:
print(f"❌ Unsupported profile ID: {profile_id}")
http_reset_success = False
return http_reset_success
try: try:
''' '''
_ ___ ___ ____ _ ___ ___ ____
@@ -463,11 +462,10 @@ try:
print('<h3>START LOOP</h3>') print('<h3>START LOOP</h3>')
#Local timestamp #Local timestamp
print("Getting local timestamp")
cursor.execute("SELECT * FROM timestamp_table LIMIT 1") cursor.execute("SELECT * FROM timestamp_table LIMIT 1")
row = cursor.fetchone() # Get the first (and only) row row = cursor.fetchone() # Get the first (and only) row
rtc_time_str = row[1] # '2025-02-07 12:30:45' rtc_time_str = row[1] # '2025-02-07 12:30:45'
print(rtc_time_str) print(f"Getting local timestamp: {rtc_time_str}")
if rtc_time_str == 'not connected': if rtc_time_str == 'not connected':
print("⛔ Atttention RTC module not connected⛔") print("⛔ Atttention RTC module not connected⛔")
@@ -488,7 +486,7 @@ try:
# Convert to InfluxDB RFC3339 format with UTC 'Z' suffix # Convert to InfluxDB RFC3339 format with UTC 'Z' suffix
influx_timestamp = dt_object.strftime('%Y-%m-%dT%H:%M:%SZ') influx_timestamp = dt_object.strftime('%Y-%m-%dT%H:%M:%SZ')
rtc_status = "valid" rtc_status = "valid"
print(influx_timestamp) #print(influx_timestamp)
#NEXTPM #NEXTPM
print("Getting NPM values (last 6 measures)") print("Getting NPM values (last 6 measures)")
@@ -591,7 +589,7 @@ try:
payload_json["sensordatavalues"].append({"value_type": "CAIRSENS_NH3", "value": str(averages[2])}) payload_json["sensordatavalues"].append({"value_type": "CAIRSENS_NH3", "value": str(averages[2])})
print("Verify SARA R4 connection") #print("Verify SARA R4 connection")
# Getting the LTE Signal # Getting the LTE Signal
print("Getting LTE signal") print("Getting LTE signal")
@@ -608,22 +606,47 @@ try:
#1. No answer at all form SARA #1. No answer at all form SARA
if response2 is None or response2 == "": if response2 is None or response2 == "":
print("No answer from SARA module") print("ATTENTION: No answer from SARA module")
print('🛑STOP LOOP🛑') print('🛑STOP LOOP🛑')
print("<hr>") print("<hr>")
#Send notification (WIFI) #Send notification (WIFI)
send_error_notification(device_id, "serial_error") send_error_notification(device_id, "SERIAL ISSUE ->no answer from sara")
#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")
#end loop #end loop
sys.exit() sys.exit()
#2. si on a une erreur #2. si on a une reponse du SARA mais c'est une erreur
elif "+CME ERROR" in response2: elif "+CME ERROR" in response2:
print(f"SARA module returned error: {response2}") print(f"SARA module returned error: {response2}")
print("The CSQ command is not supported by this module or in its current state") print("The CSQ command is not supported by this module or in its current state")
print("ATTENTION: SARA is connected over serial but CSQ command not supported") print("ATTENTION: SARA is connected over serial but CSQ command not supported")
print('🛑STOP LOOP🛑') print('🛑STOP LOOP🛑')
print("<hr>")
#end loop
sys.exit()
#3. On peut avoir une erreur de type "Socket:bind: Treck error 222 : Invalid argument"
elif "Socket:bind: Treck error" in response2:
print(f"SARA module returned error: {response2}")
print("ATTENTION: low-level error from the Treck TCP/IP stack")
print('🛑STOP LOOP🛑')
print("<hr>")
#Send notification (WIFI)
send_error_notification(device_id, "SERIAL ISSUE -> Treck TCP/IP stack error")
#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")
#end loop #end loop
sys.exit() sys.exit()
@@ -643,7 +666,7 @@ try:
print("TRY TO RECONNECT:") print("TRY TO RECONNECT:")
command = f'AT+COPS=1,2,"{selected_networkID}"\r' command = f'AT+COPS=1,2,"{selected_networkID}"\r'
ser_sara.write(command.encode('utf-8')) ser_sara.write(command.encode('utf-8'))
responseReconnect = read_complete_response(ser_sara, timeout=20, end_of_response_timeout=20) responseReconnect = read_complete_response(ser_sara, timeout=20, end_of_response_timeout=20, wait_for_lines=["OK", "+CME ERROR", "ERROR"], debug=True)
print('<p class="text-danger-emphasis">') print('<p class="text-danger-emphasis">')
print(responseReconnect) print(responseReconnect)
print("</p>", end="") print("</p>", end="")
@@ -658,8 +681,14 @@ try:
''' '''
SEND TO AIRCARTO ____ _____ _ _ ____ _ ___ ____ ____ _ ____ _____ ___
/ ___|| ____| \ | | _ \ / \ |_ _| _ \ / ___| / \ | _ \_ _/ _ \
\___ \| _| | \| | | | | / _ \ | || |_) | | / _ \ | |_) || || | | |
___) | |___| |\ | |_| | / ___ \ | || _ <| |___ / ___ \| _ < | || |_| |
|____/|_____|_| \_|____/ /_/ \_\___|_| \_\\____/_/ \_\_| \_\|_| \___/
''' '''
print('<p class="fw-bold">➡SEND TO AIRCARTO SERVERS</p>', end="")
# Write Data to saraR4 # Write Data to saraR4
# 1. Open sensordata_csv.json (with correct data size) # 1. Open sensordata_csv.json (with correct data size)
csv_string = ','.join(str(value) if value is not None else '' for value in payload_csv) csv_string = ','.join(str(value) if value is not None else '' for value in payload_csv)
@@ -667,14 +696,14 @@ try:
print("Open JSON:") print("Open JSON:")
command = f'AT+UDWNFILE="sensordata_csv.json",{size_of_string}\r' command = f'AT+UDWNFILE="sensordata_csv.json",{size_of_string}\r'
ser_sara.write(command.encode('utf-8')) ser_sara.write(command.encode('utf-8'))
response_SARA_1 = read_complete_response(ser_sara, wait_for_lines=[">"], debug=True) response_SARA_1 = read_complete_response(ser_sara, wait_for_lines=[">"], debug=False)
print(response_SARA_1) print(response_SARA_1)
time.sleep(1) time.sleep(1)
#2. Write to shell #2. Write to shell
print("Write data to memory:") print("Write data to memory:")
ser_sara.write(csv_string.encode()) ser_sara.write(csv_string.encode())
response_SARA_2 = read_complete_response(ser_sara, wait_for_lines=["OK"], debug=True) response_SARA_2 = read_complete_response(ser_sara, wait_for_lines=["OK"], debug=False)
print(response_SARA_2) print(response_SARA_2)
#3. Send to endpoint (with device ID) #3. Send to endpoint (with device ID)
@@ -750,7 +779,7 @@ try:
# Get error code # Get error code
print("Getting error code (11->Server connection error, 73->Secure socket connect error)") print("Getting error code")
command = f'AT+UHTTPER={aircarto_profile_id}\r' command = f'AT+UHTTPER={aircarto_profile_id}\r'
ser_sara.write(command.encode('utf-8')) ser_sara.write(command.encode('utf-8'))
response_SARA_9 = read_complete_response(ser_sara, wait_for_lines=["OK"], debug=False) response_SARA_9 = read_complete_response(ser_sara, wait_for_lines=["OK"], debug=False)
@@ -760,16 +789,35 @@ try:
# Extract just the error code # Extract just the error code
error_code = extract_error_code(response_SARA_9) error_code = extract_error_code(response_SARA_9)
if error_code is not None: if error_code is not None:
# Display interpretation based on error code # Display interpretation based on error code
if error_code == 0: if error_code == 0:
print('<p class="text-success">No error detected</p>') print('<p class="text-success">No error detected</p>')
elif error_code == 4: elif error_code == 4:
print('<p class="text-danger">Error 4: Invalid server Hostname</p>') print('<p class="text-danger">Error 4: Invalid server Hostname</p>')
send_error_notification(device_id, "UHTTPER (error n°4) -> 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")
elif error_code == 11: elif error_code == 11:
print('<p class="text-danger">Error 11: Server connection error</p>') print('<p class="text-danger">Error 11: Server connection error</p>')
elif error_code == 22: 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>') 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")
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")
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")
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")
elif error_code == 73: elif error_code == 73:
print('<p class="text-danger">Error 73: Secure socket connect error</p>') print('<p class="text-danger">Error 73: Secure socket connect error</p>')
else: else:
@@ -779,11 +827,11 @@ try:
#Software Reboot #Software Reboot
software_reboot_success = modem_complete_reboot_and_reinitialize(modem_version, aircarto_profile_id) #software_reboot_success = modem_complete_reboot_and_reinitialize(modem_version, aircarto_profile_id)
if software_reboot_success: #if software_reboot_success:
print("Modem successfully rebooted and reinitialized") # print("Modem successfully rebooted and reinitialized")
else: #else:
print("There were issues with the modem reboot/reinitialize process") # print("There were issues with the modem reboot/reinitialize process")
# 2.2 code 1 (✅✅HHTP / UUHTTPCR succeded✅✅) # 2.2 code 1 (✅✅HHTP / UUHTTPCR succeded✅✅)
else: else:
@@ -912,11 +960,11 @@ try:
send_error_notification(device_id, "sara_error") send_error_notification(device_id, "sara_error")
#Software Reboot #Software Reboot
software_reboot_success = modem_complete_reboot_and_reinitialize(modem_version, aircarto_profile_id) #software_reboot_success = modem_complete_reboot_and_reinitialize(modem_version, aircarto_profile_id)
if software_reboot_success: #if software_reboot_success:
print("Modem successfully rebooted and reinitialized") # print("Modem successfully rebooted and reinitialized")
else: #else:
print("There were issues with the modem reboot/reinitialize process") # print("There were issues with the modem reboot/reinitialize process")
#5. empty json #5. empty json

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,252 @@
// -*- mode: c++; c-basic-offset: 2; indent-tabs-mode: nil; -*-
//
// Example how to display an image with split reveal animation - BOOT VERSION
// Runs once at boot and exits
//
// This requires an external dependency, so install these first before you
// can call `make image-example`
// sudo apt-get update
// sudo apt-get install libgraphicsmagick++-dev libwebp-dev -y
/*
pour compiler:
g++ -I/var/www/moduleair_pro_4g/matrix/include -L/var/www/moduleair_pro_4g/matrix/lib /var/www/moduleair_pro_4g/matrix/imageScreen/image_split_boot.cc -o /var/www/moduleair_pro_4g/matrix/imageScreen/image_split_boot -lrgbmatrix `GraphicsMagick++-config --cppflags --libs`
pour lancer:
sudo /var/www/moduleair_pro_4g/matrix/imageScreen/image_split_boot /var/www/moduleair_pro_4g/matrix/imageScreen/ModuleAir128x64.png
*/
#include "led-matrix.h"
#include <math.h>
#include <signal.h>
#include <stdio.h>
#include <unistd.h>
#include <chrono>
#include <thread>
#include <exception>
#include <Magick++.h>
#include <magick/image.h>
using rgb_matrix::Canvas;
using rgb_matrix::RGBMatrix;
using rgb_matrix::FrameCanvas;
// Make sure we can exit gracefully when Ctrl-C is pressed.
volatile bool interrupt_received = false;
static void InterruptHandler(int signo) {
interrupt_received = true;
}
using ImageVector = std::vector<Magick::Image>;
// Given the filename, load the image and scale to the size of the matrix.
static ImageVector LoadImageAndScaleImage(const char *filename,
int target_width,
int target_height) {
ImageVector result;
ImageVector frames;
try {
readImages(&frames, filename);
} catch (std::exception &e) {
if (e.what())
fprintf(stderr, "%s\n", e.what());
return result;
}
if (frames.empty()) {
fprintf(stderr, "No image found.");
return result;
}
// Animated images have partial frames that need to be put together
if (frames.size() > 1) {
Magick::coalesceImages(&result, frames.begin(), frames.end());
} else {
result.push_back(frames[0]); // just a single still image.
}
for (Magick::Image &image : result) {
image.scale(Magick::Geometry(target_width, target_height));
}
return result;
}
// Copy image with split reveal animation effect
void CopyImageToCanvasWithSplitReveal(const Magick::Image &image, Canvas *canvas, float progress) {
const int matrix_height = canvas->height();
const int matrix_width = canvas->width();
const int half_height = matrix_height / 2;
// Calculate how many rows of each half to show based on progress (0.0 to 1.0)
int top_rows_to_show = (int)(half_height * progress);
int bottom_rows_to_show = (int)(half_height * progress);
// Clear the canvas first
canvas->Clear();
// Draw top half (sliding down from above)
for (int row = 0; row < top_rows_to_show && row < half_height; ++row) {
// Calculate source row in the original image
int src_row = row;
if (src_row < (int)image.rows()) {
for (size_t x = 0; x < image.columns() && x < matrix_width; ++x) {
const Magick::Color &c = image.pixelColor(x, src_row);
if (c.alphaQuantum() < 256) {
canvas->SetPixel(x, row,
ScaleQuantumToChar(c.redQuantum()),
ScaleQuantumToChar(c.greenQuantum()),
ScaleQuantumToChar(c.blueQuantum()));
}
}
}
}
// Draw bottom half (sliding up from below)
for (int row = 0; row < bottom_rows_to_show && row < half_height; ++row) {
// Calculate destination row (starting from bottom)
int dest_row = matrix_height - 1 - row;
// Calculate source row in the original image (from bottom half)
int src_row = (int)image.rows() - 1 - row;
if (src_row >= half_height && src_row < (int)image.rows() && dest_row >= half_height) {
for (size_t x = 0; x < image.columns() && x < matrix_width; ++x) {
const Magick::Color &c = image.pixelColor(x, src_row);
if (c.alphaQuantum() < 256) {
canvas->SetPixel(x, dest_row,
ScaleQuantumToChar(c.redQuantum()),
ScaleQuantumToChar(c.greenQuantum()),
ScaleQuantumToChar(c.blueQuantum()));
}
}
}
}
}
// Show image with split reveal animation - BOOT VERSION (runs once and exits)
void ShowBootSplitRevealAnimation(const Magick::Image &image, RGBMatrix *matrix) {
const int animation_duration_ms = 3000; // 3 seconds animation
const int display_duration_ms = 5000; // 5 seconds display
const int fps = 60; // 60 frames per second for smooth animation
const int frame_delay_ms = 1000 / fps;
const int total_frames = animation_duration_ms / frame_delay_ms;
FrameCanvas *offscreen_canvas = matrix->CreateFrameCanvas();
printf("Starting boot split reveal animation...\n");
auto start_time = std::chrono::steady_clock::now();
// Animation phase
for (int frame = 0; frame <= total_frames && !interrupt_received; ++frame) {
// Calculate progress (0.0 to 1.0)
float progress = (float)frame / total_frames;
// Apply easing function for smoother animation (ease-out)
progress = 1.0f - (1.0f - progress) * (1.0f - progress);
// Render frame with current progress
CopyImageToCanvasWithSplitReveal(image, offscreen_canvas, progress);
offscreen_canvas = matrix->SwapOnVSync(offscreen_canvas);
// Maintain consistent frame rate
std::this_thread::sleep_for(std::chrono::milliseconds(frame_delay_ms));
}
auto animation_end = std::chrono::steady_clock::now();
auto animation_duration = std::chrono::duration_cast<std::chrono::milliseconds>(animation_end - start_time);
printf("Boot animation completed in %ld ms\n", animation_duration.count());
// Display final complete image
printf("Displaying boot image for %d seconds...\n", display_duration_ms / 1000);
auto display_start = std::chrono::steady_clock::now();
while (!interrupt_received) {
auto current_time = std::chrono::steady_clock::now();
auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(current_time - display_start);
if (elapsed.count() >= display_duration_ms) {
break;
}
// Keep refreshing the display
offscreen_canvas = matrix->SwapOnVSync(offscreen_canvas);
std::this_thread::sleep_for(std::chrono::milliseconds(50));
}
auto total_end = std::chrono::steady_clock::now();
auto total_duration = std::chrono::duration_cast<std::chrono::milliseconds>(total_end - start_time);
printf("Boot display completed. Total time: %ld ms (%ds animation + %ds display)\n",
total_duration.count(), animation_duration_ms / 1000, display_duration_ms / 1000);
}
int main(int argc, char *argv[]) {
Magick::InitializeMagick(*argv);
// Check if a filename was provided
if (argc != 2) {
fprintf(stderr, "Usage: %s <image-filename>\n", argv[0]);
return 1;
}
const char *filename = argv[1];
// Initialize with hardcoded defaults for your specific hardware
RGBMatrix::Options defaults;
defaults.hardware_mapping = "moduleair_pinout";
defaults.rows = 64;
defaults.cols = 128;
defaults.chain_length = 1;
defaults.parallel = 1;
defaults.row_address_type = 3;
defaults.show_refresh_rate = false; // Disable refresh rate display for boot
defaults.brightness = 100;
defaults.pwm_bits = 1;
defaults.panel_type = "FM6126A";
defaults.disable_hardware_pulsing = false;
// Set up runtime options
rgb_matrix::RuntimeOptions runtime_opt;
runtime_opt.gpio_slowdown = 4; // Adjust if needed for your Pi model
runtime_opt.daemon = 0;
runtime_opt.drop_privileges = 0; // Set to 1 if not running as root
// Handle Ctrl+C to exit gracefully
signal(SIGTERM, InterruptHandler);
signal(SIGINT, InterruptHandler);
// Create the matrix with our defaults
RGBMatrix *matrix = RGBMatrix::CreateFromOptions(defaults, runtime_opt);
if (matrix == NULL) {
fprintf(stderr, "Failed to create matrix\n");
return 1;
}
// Load and process the image
ImageVector images = LoadImageAndScaleImage(filename,
matrix->width(),
matrix->height());
// Display the image with split reveal animation (once only)
switch (images.size()) {
case 0: // failed to load image
fprintf(stderr, "Failed to load image\n");
break;
case 1: // single image - show with split reveal animation
ShowBootSplitRevealAnimation(images[0], matrix);
break;
default: // animated image - just show first frame with split reveal
printf("Animated image detected, using first frame for boot animation\n");
ShowBootSplitRevealAnimation(images[0], matrix);
break;
}
// Clean up and exit
printf("Boot animation finished. Cleaning up and exiting...\n");
matrix->Clear();
delete matrix;
return 0;
}

View File

@@ -0,0 +1,273 @@
// -*- mode: c++; c-basic-offset: 2; indent-tabs-mode: nil; -*-
//
// Example how to display an image with split reveal animation
//
// This requires an external dependency, so install these first before you
// can call `make image-example`
// sudo apt-get update
// sudo apt-get install libgraphicsmagick++-dev libwebp-dev -y
/*
pour compiler:
g++ -I/var/www/moduleair_pro_4g/matrix/include -L/var/www/moduleair_pro_4g/matrix/lib /var/www/moduleair_pro_4g/matrix/imageScreen/image_split.cc -o /var/www/moduleair_pro_4g/matrix/imageScreen/image_split -lrgbmatrix `GraphicsMagick++-config --cppflags --libs`
pour lancer:
sudo /var/www/moduleair_pro_4g/matrix/imageScreen/image_split /var/www/moduleair_pro_4g/matrix/imageScreen/ModuleAir128x64.png
*/
#include "led-matrix.h"
#include <math.h>
#include <signal.h>
#include <stdio.h>
#include <unistd.h>
#include <chrono>
#include <thread>
#include <exception>
#include <Magick++.h>
#include <magick/image.h>
using rgb_matrix::Canvas;
using rgb_matrix::RGBMatrix;
using rgb_matrix::FrameCanvas;
// Make sure we can exit gracefully when Ctrl-C is pressed.
volatile bool interrupt_received = false;
static void InterruptHandler(int signo) {
interrupt_received = true;
}
using ImageVector = std::vector<Magick::Image>;
// Given the filename, load the image and scale to the size of the matrix.
static ImageVector LoadImageAndScaleImage(const char *filename,
int target_width,
int target_height) {
ImageVector result;
ImageVector frames;
try {
readImages(&frames, filename);
} catch (std::exception &e) {
if (e.what())
fprintf(stderr, "%s\n", e.what());
return result;
}
if (frames.empty()) {
fprintf(stderr, "No image found.");
return result;
}
// Animated images have partial frames that need to be put together
if (frames.size() > 1) {
Magick::coalesceImages(&result, frames.begin(), frames.end());
} else {
result.push_back(frames[0]); // just a single still image.
}
for (Magick::Image &image : result) {
image.scale(Magick::Geometry(target_width, target_height));
}
return result;
}
// Copy image with split reveal animation effect
void CopyImageToCanvasWithSplitReveal(const Magick::Image &image, Canvas *canvas, float progress) {
const int matrix_height = canvas->height();
const int matrix_width = canvas->width();
const int half_height = matrix_height / 2;
// Calculate how many rows of each half to show based on progress (0.0 to 1.0)
int top_rows_to_show = (int)(half_height * progress);
int bottom_rows_to_show = (int)(half_height * progress);
// Clear the canvas first
canvas->Clear();
// Draw top half (sliding down from above)
for (int row = 0; row < top_rows_to_show && row < half_height; ++row) {
// Calculate source row in the original image
int src_row = row;
if (src_row < (int)image.rows()) {
for (size_t x = 0; x < image.columns() && x < matrix_width; ++x) {
const Magick::Color &c = image.pixelColor(x, src_row);
if (c.alphaQuantum() < 256) {
canvas->SetPixel(x, row,
ScaleQuantumToChar(c.redQuantum()),
ScaleQuantumToChar(c.greenQuantum()),
ScaleQuantumToChar(c.blueQuantum()));
}
}
}
}
// Draw bottom half (sliding up from below)
for (int row = 0; row < bottom_rows_to_show && row < half_height; ++row) {
// Calculate destination row (starting from bottom)
int dest_row = matrix_height - 1 - row;
// Calculate source row in the original image (from bottom half)
int src_row = (int)image.rows() - 1 - row;
if (src_row >= half_height && src_row < (int)image.rows() && dest_row >= half_height) {
for (size_t x = 0; x < image.columns() && x < matrix_width; ++x) {
const Magick::Color &c = image.pixelColor(x, src_row);
if (c.alphaQuantum() < 256) {
canvas->SetPixel(x, dest_row,
ScaleQuantumToChar(c.redQuantum()),
ScaleQuantumToChar(c.greenQuantum()),
ScaleQuantumToChar(c.blueQuantum()));
}
}
}
}
}
// Show image with split reveal animation
void ShowSplitRevealAnimation(const Magick::Image &image, RGBMatrix *matrix) {
const int animation_duration_ms = 3000; // 3 seconds total
const int fps = 60; // 60 frames per second for smooth animation
const int frame_delay_ms = 1000 / fps;
const int total_frames = animation_duration_ms / frame_delay_ms;
FrameCanvas *offscreen_canvas = matrix->CreateFrameCanvas();
printf("Starting split reveal animation (3 seconds)...\n");
auto start_time = std::chrono::steady_clock::now();
for (int frame = 0; frame <= total_frames && !interrupt_received; ++frame) {
// Calculate progress (0.0 to 1.0)
float progress = (float)frame / total_frames;
// Apply easing function for smoother animation (ease-out)
progress = 1.0f - (1.0f - progress) * (1.0f - progress);
// Render frame with current progress
CopyImageToCanvasWithSplitReveal(image, offscreen_canvas, progress);
offscreen_canvas = matrix->SwapOnVSync(offscreen_canvas);
// Maintain consistent frame rate
std::this_thread::sleep_for(std::chrono::milliseconds(frame_delay_ms));
// Debug output every 30 frames (approximately every 0.5 seconds)
if (frame % 30 == 0) {
printf("Animation progress: %.1f%%\n", progress * 100);
}
}
auto end_time = std::chrono::steady_clock::now();
auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end_time - start_time);
printf("Animation completed in %ld ms\n", duration.count());
// Show final complete image for 3 seconds
printf("Displaying full image for 3 seconds...\n");
auto display_start = std::chrono::steady_clock::now();
const int display_duration_ms = 3000; // 3 seconds
while (!interrupt_received) {
auto current_time = std::chrono::steady_clock::now();
auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(current_time - display_start);
if (elapsed.count() >= display_duration_ms) {
break;
}
// Keep refreshing the display
offscreen_canvas = matrix->SwapOnVSync(offscreen_canvas);
std::this_thread::sleep_for(std::chrono::milliseconds(50));
}
printf("Display completed. Total time: 6 seconds (3s animation + 3s display)\n");
}
// Regular copy function for animated images
void CopyImageToCanvas(const Magick::Image &image, Canvas *canvas) {
const int offset_x = 0, offset_y = 0;
for (size_t y = 0; y < image.rows(); ++y) {
for (size_t x = 0; x < image.columns(); ++x) {
const Magick::Color &c = image.pixelColor(x, y);
if (c.alphaQuantum() < 256) {
canvas->SetPixel(x + offset_x, y + offset_y,
ScaleQuantumToChar(c.redQuantum()),
ScaleQuantumToChar(c.greenQuantum()),
ScaleQuantumToChar(c.blueQuantum()));
}
}
}
}
int usage(const char *progname) {
fprintf(stderr, "Usage: %s <image-filename>\n", progname);
return 1;
}
int main(int argc, char *argv[]) {
Magick::InitializeMagick(*argv);
// Check if a filename was provided
if (argc != 2) {
fprintf(stderr, "Usage: %s <image-filename>\n", argv[0]);
return 1;
}
const char *filename = argv[1];
// Initialize with hardcoded defaults for your specific hardware
RGBMatrix::Options defaults;
defaults.hardware_mapping = "moduleair_pinout";
defaults.rows = 64;
defaults.cols = 128;
defaults.chain_length = 1;
defaults.parallel = 1;
defaults.row_address_type = 3;
defaults.show_refresh_rate = true;
defaults.brightness = 100;
defaults.pwm_bits = 1;
defaults.panel_type = "FM6126A";
defaults.disable_hardware_pulsing = false;
// Set up runtime options
rgb_matrix::RuntimeOptions runtime_opt;
runtime_opt.gpio_slowdown = 4; // Adjust if needed for your Pi model
runtime_opt.daemon = 0;
runtime_opt.drop_privileges = 0; // Set to 1 if not running as root
// Handle Ctrl+C to exit gracefully
signal(SIGTERM, InterruptHandler);
signal(SIGINT, InterruptHandler);
// Create the matrix with our defaults
RGBMatrix *matrix = RGBMatrix::CreateFromOptions(defaults, runtime_opt);
if (matrix == NULL) {
fprintf(stderr, "Failed to create matrix\n");
return 1;
}
// Load and process the image
ImageVector images = LoadImageAndScaleImage(filename,
matrix->width(),
matrix->height());
// Display the image with split reveal animation
switch (images.size()) {
case 0: // failed to load image
fprintf(stderr, "Failed to load image\n");
break;
case 1: // single image - show with split reveal animation
ShowSplitRevealAnimation(images[0], matrix);
break;
default: // animated image - just show first frame with split reveal
printf("Animated image detected, using first frame for split reveal animation\n");
ShowSplitRevealAnimation(images[0], matrix);
break;
}
// Clean up and exit
printf("Animation finished. Cleaning up and exiting...\n");
matrix->Clear();
delete matrix;
return 0;
}

View File

@@ -0,0 +1,92 @@
#!/bin/bash
# File: /var/www/moduleair_pro_4g/services/setup_matrix_services.sh
# Purpose: Set up LED matrix boot display service (runs once at boot)
# to install:
# sudo chmod +x /var/www/moduleair_pro_4g/services/setup_matrix_services.sh
# sudo /var/www/moduleair_pro_4g/services/setup_matrix_services.sh
echo "Setting up moduleair boot display service..."
# Create directory for logs if it doesn't exist
mkdir -p /var/www/moduleair_pro_4g/logs
# Create service file for LED Matrix Boot Display (one-shot at boot)
cat > /etc/systemd/system/moduleair-matrix-boot.service << 'EOL'
[Unit]
Description=moduleair LED Matrix Boot Display Service (runs once)
After=network.target
After=multi-user.target
[Service]
Type=oneshot
ExecStart=/var/www/moduleair_pro_4g/matrix/imageScreen/image_split_boot /var/www/moduleair_pro_4g/matrix/imageScreen/ModuleAir128x64.png
User=root
WorkingDirectory=/var/www/moduleair_pro_4g/matrix/imageScreen/
StandardOutput=append:/var/www/moduleair_pro_4g/logs/matrix_boot_service.log
StandardError=append:/var/www/moduleair_pro_4g/logs/matrix_boot_service_errors.log
TimeoutStartSec=30s
RemainAfterExit=yes
[Install]
WantedBy=multi-user.target
EOL
# Compile the boot version of the program
echo "Compiling boot version of image_split..."
cd /var/www/moduleair_pro_4g/matrix/imageScreen/
# Make sure we have the boot version source file
if [ ! -f "image_split_boot.cc" ]; then
echo "Creating image_split_boot.cc from provided code..."
# The code will be created separately as image_split_boot.cc
echo "Please save the boot version code as image_split_boot.cc first"
exit 1
fi
# Compile the boot version
g++ -I/var/www/moduleair_pro_4g/matrix/include -L/var/www/moduleair_pro_4g/matrix/lib \
/var/www/moduleair_pro_4g/matrix/imageScreen/image_split_boot.cc \
-o /var/www/moduleair_pro_4g/matrix/imageScreen/image_split_boot \
-lrgbmatrix `GraphicsMagick++-config --cppflags --libs`
# Check if compilation was successful
if [ ! -f "image_split_boot" ]; then
echo "Compilation failed! Please check dependencies and try again."
exit 1
fi
# Make sure the matrix executable has proper permissions
echo "Setting permissions for LED matrix boot executable..."
chmod +x /var/www/moduleair_pro_4g/matrix/imageScreen/image_split_boot
# Reload systemd to recognize new service
systemctl daemon-reload
# Enable the boot service
echo "Enabling boot display service..."
systemctl enable moduleair-matrix-boot.service
echo "Setup complete!"
echo ""
echo "LED Matrix Boot Display will:"
echo " - Run once at boot after network and multi-user target"
echo " - Show 3-second split reveal animation"
echo " - Display image for 5 seconds"
echo " - Exit automatically (total ~8 seconds)"
echo ""
echo "To test the service manually:"
echo " sudo systemctl start moduleair-matrix-boot.service"
echo ""
echo "To check the status:"
echo " sudo systemctl status moduleair-matrix-boot.service"
echo ""
echo "To view logs:"
echo " sudo journalctl -u moduleair-matrix-boot.service"
echo " tail -f /var/www/moduleair_pro_4g/logs/matrix_boot_service.log"
echo ""
echo "To disable boot display:"
echo " sudo systemctl disable moduleair-matrix-boot.service"
echo ""
echo "Note: The old repeating matrix display timer has been disabled."
echo "If you want to re-enable it, run the original setup_services.sh script."

View File

@@ -1,6 +1,6 @@
#!/bin/bash #!/bin/bash
# File: /var/www/moduleair_pro_4g/services/setup_services.sh # File: /var/www/moduleair_pro_4g/services/setup_services.sh
# Purpose: Set up all systemd services for moduleair data collection # Purpose: Set up all systemd services for moduleair data collection and LED matrix display
# to install: # to install:
# sudo chmod +x /var/www/moduleair_pro_4g/services/setup_services.sh # sudo chmod +x /var/www/moduleair_pro_4g/services/setup_services.sh
# sudo /var/www/moduleair_pro_4g/services/setup_services.sh # sudo /var/www/moduleair_pro_4g/services/setup_services.sh
@@ -205,24 +205,76 @@ AccuracySec=1h
WantedBy=timers.target WantedBy=timers.target
EOL EOL
# Create service and timer files for LED Matrix Display
cat > /etc/systemd/system/moduleair-matrix-display.service << 'EOL'
[Unit]
Description=moduleair LED Matrix Display Service
After=network.target
[Service]
Type=oneshot
ExecStart=/var/www/moduleair_pro_4g/matrix/imageScreen/image_split /var/www/moduleair_pro_4g/matrix/imageScreen/ModuleAir128x64.png
User=root
WorkingDirectory=/var/www/moduleair_pro_4g/matrix/imageScreen/
StandardOutput=append:/var/www/moduleair_pro_4g/logs/matrix_service.log
StandardError=append:/var/www/moduleair_pro_4g/logs/matrix_service_errors.log
[Install]
WantedBy=multi-user.target
EOL
cat > /etc/systemd/system/moduleair-matrix-display.timer << 'EOL'
[Unit]
Description=Run moduleair LED Matrix Display every 10 seconds
Requires=moduleair-matrix-display.service
[Timer]
OnBootSec=30s
OnUnitActiveSec=10s
AccuracySec=1s
[Install]
WantedBy=timers.target
EOL
# Make sure the matrix executable has proper permissions
echo "Setting permissions for LED matrix executable..."
chmod +x /var/www/moduleair_pro_4g/matrix/imageScreen/image_split
# Reload systemd to recognize new services # Reload systemd to recognize new services
systemctl daemon-reload systemctl daemon-reload
# Enable and start all timers # Enable and start all timers
echo "Enabling and starting all services..." echo "Enabling and starting all services..."
for service in npm envea sara bme280 co2 db-cleanup; do for service in npm envea sara bme280 co2 db-cleanup matrix-display; do
systemctl enable moduleair-$service-data.timer systemctl enable moduleair-$service-data.timer 2>/dev/null || systemctl enable moduleair-$service.timer
systemctl start moduleair-$service-data.timer systemctl start moduleair-$service-data.timer 2>/dev/null || systemctl start moduleair-$service.timer
echo "Started moduleair-$service-data timer" echo "Started moduleair-$service timer"
done done
echo "Checking status of all timers..." echo "Checking status of all timers..."
systemctl list-timers | grep moduleair systemctl list-timers | grep moduleair
echo "Setup complete. All moduleair services are now running." echo "Setup complete. All moduleair services are now running."
echo "To check the status of a specific service:" echo ""
echo "LED Matrix Display will:"
echo " - Start 30 seconds after boot"
echo " - Run every 10 seconds (6s animation + 4s gap)"
echo " - Show split reveal animation + 3s display"
echo ""
echo "To check the status of services:"
echo " sudo systemctl status moduleair-npm-data.service" echo " sudo systemctl status moduleair-npm-data.service"
echo "To view logs for a specific service:" echo " sudo systemctl status moduleair-matrix-display.service"
echo ""
echo "To view logs for services:"
echo " sudo journalctl -u moduleair-npm-data.service" echo " sudo journalctl -u moduleair-npm-data.service"
echo "To restart a specific timer:" echo " sudo journalctl -u moduleair-matrix-display.service"
echo " sudo systemctl restart moduleair-npm-data.timer" echo " tail -f /var/www/moduleair_pro_4g/logs/matrix_service.log"
echo ""
echo "To restart timers:"
echo " sudo systemctl restart moduleair-npm-data.timer"
echo " sudo systemctl restart moduleair-matrix-display.timer"
echo ""
echo "To disable matrix display:"
echo " sudo systemctl stop moduleair-matrix-display.timer"
echo " sudo systemctl disable moduleair-matrix-display.timer"

116
sqlite/set_config_smart.py Normal file
View File

@@ -0,0 +1,116 @@
'''
____ ___ _ _ _
/ ___| / _ \| | (_) |_ ___
\___ \| | | | | | | __/ _ \
___) | |_| | |___| | || __/
|____/ \__\_\_____|_|\__\___|
Script to set the config (smart version - doesn't overwrite existing data)
/usr/bin/python3 /var/www/moduleair_pro_4g/sqlite/set_config_smart.py
in case of readonly error:
sudo chmod 777 /var/www/moduleair_pro_4g/sqlite/sensors.db
'''
import sqlite3
# Connect to (or create if not existent) the database
conn = sqlite3.connect("/var/www/moduleair_pro_4g/sqlite/sensors.db")
cursor = conn.cursor()
print(f"Connected to database")
def safe_insert_config(key, value, value_type):
"""Insert config only if it doesn't exist"""
cursor.execute("SELECT COUNT(*) FROM config_table WHERE key = ?", (key,))
if cursor.fetchone()[0] == 0:
cursor.execute(
"INSERT INTO config_table (key, value, type) VALUES (?, ?, ?)",
(key, value, value_type)
)
print(f"Added config: {key} = {value}")
else:
print(f"Config already exists: {key}")
def safe_insert_envea_sonde(connected, port, name, coefficient):
"""Insert envea sonde only if it doesn't exist (check by port and name)"""
cursor.execute("SELECT COUNT(*) FROM envea_sondes_table WHERE port = ? AND name = ?", (port, name))
if cursor.fetchone()[0] == 0:
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} on {port}")
else:
print(f"Envea sonde already exists: {name} on {port}")
# Check if tables are empty (first run)
cursor.execute("SELECT COUNT(*) FROM config_table")
config_count = cursor.fetchone()[0]
cursor.execute("SELECT COUNT(*) FROM config_scripts_table")
scripts_count = cursor.fetchone()[0]
cursor.execute("SELECT COUNT(*) FROM envea_sondes_table")
sondes_count = cursor.fetchone()[0]
# If this is the first run (all tables empty), clear and do fresh install
if config_count == 0 and scripts_count == 0 and sondes_count == 0:
print("First run detected - setting up initial configuration...")
# Clear existing data (if any)
cursor.execute("DELETE FROM config_table")
cursor.execute("DELETE FROM config_scripts_table")
cursor.execute("DELETE FROM envea_sondes_table")
print("Tables cleared for fresh setup")
else:
print("Existing configuration detected - will only add missing entries...")
# Insert general configurations
config_entries = [
("modem_config_mode", "0", "bool"),
("deviceID", "XXXX", "str"),
("npm_5channel", "0", "bool"),
("latitude_raw", "0", "int"),
("longitude_raw", "0", "int"),
("latitude_precision", "0", "int"),
("longitude_precision", "0", "int"),
("deviceName", "ModuleAir-proXXX", "str"),
("SaraR4_baudrate", "115200", "int"),
("NPM_solo_port", "/dev/ttyAMA5", "str"),
("sshTunnel_port", "59228", "int"),
("SARA_R4_general_status", "connected", "str"),
("SARA_R4_SIM_status", "connected", "str"),
("SARA_R4_network_status", "connected", "str"),
("SARA_R4_neworkID", "20810", "int"),
("WIFI_status", "connected", "str"),
("send_uSpot", "0", "bool"),
("windMeter", "0", "bool"),
("modem_version", "XXX", "str"),
# Add new config entries here
("matrix_display", "enabled", "str"),
("matrix_display_type", "split_reveal", "str"),
("matrix_brightness", "100", "int")
]
print("\nProcessing general configurations...")
for key, value, value_type in config_entries:
safe_insert_config(key, value, value_type)
# Insert envea sondes
envea_sondes = [
(False, "ttyAMA4", "h2s", 4),
(False, "ttyAMA3", "no2", 1),
(False, "ttyAMA2", "o3", 1),
# Add new sondes here if needed
]
print("\nProcessing envea sondes...")
for connected, port, name, coefficient in envea_sondes:
safe_insert_envea_sonde(connected, port, name, coefficient)
# Commit and close the connection
conn.commit()
conn.close()
print("\nDatabase updated successfully!")