update
This commit is contained in:
@@ -6,7 +6,7 @@ Version Pro du ModuleAir avec CM4, SaraR4 et ecran Matrix LED p2 64x64.
|
||||
```
|
||||
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 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 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
|
||||
|
||||
29
SARA/sara.py
29
SARA/sara.py
@@ -28,6 +28,7 @@ port='/dev/'+parameter[0] # ex: ttyAMA2
|
||||
command = parameter[1] # ex: AT+CCID?
|
||||
timeout = float(parameter[2]) # ex:2
|
||||
|
||||
try:
|
||||
ser = serial.Serial(
|
||||
port=port, #USB0 or ttyS0
|
||||
baudrate=115200, #115200 ou 9600
|
||||
@@ -54,25 +55,31 @@ ser.write((command + '\r').encode('utf-8'))
|
||||
#ser.write(b'AT+CMUX=?')
|
||||
|
||||
|
||||
|
||||
try:
|
||||
# Read lines until a timeout occurs
|
||||
response_lines = []
|
||||
while True:
|
||||
line = ser.readline().decode('utf-8').strip()
|
||||
if not line:
|
||||
break # Break the loop if an empty line is encountered
|
||||
start_time = time.time()
|
||||
|
||||
while (time.time() - start_time) < timeout:
|
||||
line = ser.readline().decode('utf-8', errors='ignore').strip()
|
||||
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
|
||||
for line in response_lines:
|
||||
print(line)
|
||||
|
||||
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:
|
||||
if ser.is_open:
|
||||
# Close the serial port if it's open
|
||||
if 'ser' in locals() and ser.is_open:
|
||||
ser.close()
|
||||
#print("Serial closed")
|
||||
|
||||
|
||||
@@ -92,11 +92,13 @@ import json
|
||||
import serial
|
||||
import time
|
||||
import busio
|
||||
import requests
|
||||
import re
|
||||
import os
|
||||
import traceback
|
||||
import sys
|
||||
import sqlite3
|
||||
import RPi.GPIO as GPIO
|
||||
from threading import Thread
|
||||
from datetime import datetime
|
||||
|
||||
@@ -130,7 +132,6 @@ conn = sqlite3.connect("/var/www/moduleair_pro_4g/sqlite/sensors.db")
|
||||
cursor = conn.cursor()
|
||||
|
||||
|
||||
|
||||
#get config data from SQLite table
|
||||
def load_config_sqlite():
|
||||
"""
|
||||
@@ -235,6 +236,9 @@ def read_complete_response(serial_connection, timeout=2, end_of_response_timeout
|
||||
'''
|
||||
Fonction très importante !!!
|
||||
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:
|
||||
wait_for_lines = [] # Default to an empty list if not provided
|
||||
@@ -336,79 +340,50 @@ def send_error_notification(device_id, error_type, additional_info=None):
|
||||
|
||||
return False
|
||||
|
||||
def modem_complete_reboot_and_reinitialize(modem_version, aircarto_profile_id):
|
||||
def modem_hardware_reboot():
|
||||
"""
|
||||
Performs a complete modem restart sequence:
|
||||
1. Reboots the modem using the appropriate command for its version
|
||||
2. Waits for the modem to restart
|
||||
3. Resets the HTTP profile
|
||||
4. For SARA-R5, resets the PDP connection
|
||||
Performs a hardware reboot using transistors connected to pin 16 and 20:
|
||||
pin 16 set to SARA GND
|
||||
pin 20 set to SARA ON (not used)
|
||||
|
||||
Args:
|
||||
modem_version (str): The modem version, e.g., 'SARA-R500' or 'SARA-R410'
|
||||
aircarto_profile_id (int): The HTTP profile ID to reset
|
||||
LOW -> cut the current
|
||||
HIGH -> current flow
|
||||
|
||||
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>')
|
||||
print('<span style="color: orange;font-weight: bold;">🔄 Hardware SARA reboot 🔄</span>')
|
||||
|
||||
# Step 1: Reboot the modem - Integrated modem_software_reboot logic
|
||||
print('<span style="color: orange;font-weight: bold;">🔄 Software SARA reboot! 🔄</span>')
|
||||
SARA_power_GPIO = 16
|
||||
SARA_ON_GPIO = 20
|
||||
|
||||
# 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
|
||||
GPIO.setmode(GPIO.BCM) # Use BCM numbering
|
||||
GPIO.setup(SARA_power_GPIO, GPIO.OUT) # Set GPIO17 as an output
|
||||
|
||||
ser_sara.write(command.encode('utf-8'))
|
||||
response = read_complete_response(ser_sara, wait_for_lines=["OK", "ERROR"], debug=True)
|
||||
GPIO.output(SARA_power_GPIO, GPIO.LOW)
|
||||
time.sleep(2)
|
||||
GPIO.output(SARA_power_GPIO, GPIO.HIGH)
|
||||
time.sleep(2)
|
||||
|
||||
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...")
|
||||
|
||||
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 is None or "OK" not in response_check:
|
||||
print("⚠️ Modem not responding after reboot")
|
||||
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
|
||||
|
||||
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
|
||||
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
|
||||
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'
|
||||
@@ -448,8 +423,32 @@ def modem_complete_reboot_and_reinitialize(modem_version, aircarto_profile_id):
|
||||
if not pdp_reset_success:
|
||||
print("⚠️ PDP connection reset had some issues")
|
||||
|
||||
# Return overall success
|
||||
return http_reset_success and pdp_reset_success
|
||||
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:
|
||||
'''
|
||||
@@ -463,11 +462,10 @@ try:
|
||||
print('<h3>START LOOP</h3>')
|
||||
|
||||
#Local timestamp
|
||||
print("➡️Getting local timestamp")
|
||||
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'
|
||||
print(rtc_time_str)
|
||||
print(f"➡️Getting local timestamp: {rtc_time_str}")
|
||||
|
||||
if rtc_time_str == 'not connected':
|
||||
print("⛔ Atttention RTC module not connected⛔")
|
||||
@@ -488,7 +486,7 @@ try:
|
||||
# Convert to InfluxDB RFC3339 format with UTC 'Z' suffix
|
||||
influx_timestamp = dt_object.strftime('%Y-%m-%dT%H:%M:%SZ')
|
||||
rtc_status = "valid"
|
||||
print(influx_timestamp)
|
||||
#print(influx_timestamp)
|
||||
|
||||
#NEXTPM
|
||||
print("➡️Getting NPM values (last 6 measures)")
|
||||
@@ -591,7 +589,7 @@ try:
|
||||
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
|
||||
print("➡️Getting LTE signal")
|
||||
@@ -608,22 +606,47 @@ try:
|
||||
|
||||
#1. No answer at all form SARA
|
||||
if response2 is None or response2 == "":
|
||||
print("No answer from SARA module")
|
||||
print("⚠️ATTENTION: No answer from SARA module")
|
||||
print('🛑STOP LOOP🛑')
|
||||
print("<hr>")
|
||||
|
||||
#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
|
||||
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:
|
||||
print(f"SARA module returned error: {response2}")
|
||||
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('🛑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
|
||||
sys.exit()
|
||||
|
||||
@@ -643,7 +666,7 @@ try:
|
||||
print("TRY TO RECONNECT:")
|
||||
command = f'AT+COPS=1,2,"{selected_networkID}"\r'
|
||||
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(responseReconnect)
|
||||
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
|
||||
# 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)
|
||||
@@ -667,14 +696,14 @@ try:
|
||||
print("Open JSON:")
|
||||
command = f'AT+UDWNFILE="sensordata_csv.json",{size_of_string}\r'
|
||||
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)
|
||||
time.sleep(1)
|
||||
|
||||
#2. Write to shell
|
||||
print("Write data to memory:")
|
||||
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)
|
||||
|
||||
#3. Send to endpoint (with device ID)
|
||||
@@ -750,7 +779,7 @@ try:
|
||||
|
||||
|
||||
# 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'
|
||||
ser_sara.write(command.encode('utf-8'))
|
||||
response_SARA_9 = read_complete_response(ser_sara, wait_for_lines=["OK"], debug=False)
|
||||
@@ -760,16 +789,35 @@ try:
|
||||
|
||||
# Extract just the error code
|
||||
error_code = extract_error_code(response_SARA_9)
|
||||
|
||||
if error_code is not None:
|
||||
# Display interpretation based on error code
|
||||
if error_code == 0:
|
||||
print('<p class="text-success">No error detected</p>')
|
||||
elif error_code == 4:
|
||||
print('<p class="text-danger">Error 4: Invalid server Hostname</p>')
|
||||
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:
|
||||
print('<p class="text-danger">Error 11: Server connection error</p>')
|
||||
elif error_code == 22:
|
||||
print('<p class="text-danger">⚠️Error 22: PSD or CSD connection not established (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:
|
||||
print('<p class="text-danger">Error 73: Secure socket connect error</p>')
|
||||
else:
|
||||
@@ -779,11 +827,11 @@ try:
|
||||
|
||||
|
||||
#Software Reboot
|
||||
software_reboot_success = modem_complete_reboot_and_reinitialize(modem_version, aircarto_profile_id)
|
||||
if software_reboot_success:
|
||||
print("Modem successfully rebooted and reinitialized")
|
||||
else:
|
||||
print("There were issues with the modem reboot/reinitialize process")
|
||||
#software_reboot_success = modem_complete_reboot_and_reinitialize(modem_version, aircarto_profile_id)
|
||||
#if software_reboot_success:
|
||||
# print("Modem successfully rebooted and reinitialized")
|
||||
#else:
|
||||
# print("There were issues with the modem reboot/reinitialize process")
|
||||
|
||||
# 2.2 code 1 (✅✅HHTP / UUHTTPCR succeded✅✅)
|
||||
else:
|
||||
@@ -912,11 +960,11 @@ try:
|
||||
send_error_notification(device_id, "sara_error")
|
||||
|
||||
#Software Reboot
|
||||
software_reboot_success = modem_complete_reboot_and_reinitialize(modem_version, aircarto_profile_id)
|
||||
if software_reboot_success:
|
||||
print("Modem successfully rebooted and reinitialized")
|
||||
else:
|
||||
print("There were issues with the modem reboot/reinitialize process")
|
||||
#software_reboot_success = modem_complete_reboot_and_reinitialize(modem_version, aircarto_profile_id)
|
||||
#if software_reboot_success:
|
||||
# print("Modem successfully rebooted and reinitialized")
|
||||
#else:
|
||||
# print("There were issues with the modem reboot/reinitialize process")
|
||||
|
||||
|
||||
#5. empty json
|
||||
|
||||
BIN
matrix/imageScreen/image_split
Normal file
BIN
matrix/imageScreen/image_split
Normal file
Binary file not shown.
BIN
matrix/imageScreen/image_split_boot
Normal file
BIN
matrix/imageScreen/image_split_boot
Normal file
Binary file not shown.
252
matrix/imageScreen/image_split_boot.cc
Normal file
252
matrix/imageScreen/image_split_boot.cc
Normal 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;
|
||||
}
|
||||
273
matrix/imageScreen/image_split_old.cc
Normal file
273
matrix/imageScreen/image_split_old.cc
Normal 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;
|
||||
}
|
||||
92
services/setup_matrix_services.sh
Normal file
92
services/setup_matrix_services.sh
Normal 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."
|
||||
@@ -1,6 +1,6 @@
|
||||
#!/bin/bash
|
||||
# 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:
|
||||
# sudo chmod +x /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
|
||||
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
|
||||
systemctl daemon-reload
|
||||
|
||||
# Enable and start all timers
|
||||
echo "Enabling and starting all services..."
|
||||
for service in npm envea sara bme280 co2 db-cleanup; do
|
||||
systemctl enable moduleair-$service-data.timer
|
||||
systemctl start moduleair-$service-data.timer
|
||||
echo "Started moduleair-$service-data timer"
|
||||
for service in npm envea sara bme280 co2 db-cleanup matrix-display; do
|
||||
systemctl enable moduleair-$service-data.timer 2>/dev/null || systemctl enable moduleair-$service.timer
|
||||
systemctl start moduleair-$service-data.timer 2>/dev/null || systemctl start moduleair-$service.timer
|
||||
echo "Started moduleair-$service timer"
|
||||
done
|
||||
|
||||
echo "Checking status of all timers..."
|
||||
systemctl list-timers | grep moduleair
|
||||
|
||||
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 "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 "To restart a specific timer:"
|
||||
echo " sudo journalctl -u moduleair-matrix-display.service"
|
||||
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
116
sqlite/set_config_smart.py
Normal 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!")
|
||||
Reference in New Issue
Block a user