adding RESET-HAT function & makeup
This commit is contained in:
+98
-56
@@ -12,47 +12,55 @@ import requests
|
||||
from pathlib import Path
|
||||
import configparser
|
||||
|
||||
try:
|
||||
import RPi.GPIO as GPIO
|
||||
# This variable must be GLOBAL, so defined at the top!
|
||||
GPIO_AVAILABLE = True
|
||||
except ImportError:
|
||||
GPIO_AVAILABLE = False
|
||||
print("Warning: RPi.GPIO library not found. Hardware reset disabled.")
|
||||
|
||||
# ==========================================
|
||||
# 0. CARICAMENTO CONFIGURAZIONE UNIFICATA
|
||||
# 0. UNIFIED CONFIGURATION LOADING
|
||||
# ==========================================
|
||||
CONFIG_PATH = Path("/opt/node_config.json")
|
||||
|
||||
def load_config():
|
||||
try:
|
||||
if not CONFIG_PATH.exists():
|
||||
print(f"❌ ERRORE: File {CONFIG_PATH} non trovato!")
|
||||
print(f"❌ ERROR: File {CONFIG_PATH} not found!")
|
||||
sys.exit(1)
|
||||
with open(CONFIG_PATH, 'r') as f:
|
||||
return json.load(f)
|
||||
except Exception as e:
|
||||
print(f"❌ ERRORE CRITICO JSON: {e}")
|
||||
print(f"❌ CRITICAL JSON ERROR: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
# Carichiamo l'unica configurazione necessaria
|
||||
# Load the single necessary configuration
|
||||
cfg = load_config()
|
||||
|
||||
# Identificativi e Topic
|
||||
# Identifiers and Topics
|
||||
CLIENT_ID = cfg.get('client_id', 'iv3jdv').lower()
|
||||
BASE_TOPIC = cfg.get('mqtt', {}).get('base_topic', f"servizi/{CLIENT_ID}")
|
||||
|
||||
TOPIC_CMD = f"{BASE_TOPIC}/cmnd"
|
||||
TOPIC_STAT = f"{BASE_TOPIC}/stat"
|
||||
|
||||
# Variabili di Stato Globali
|
||||
# Global State Variables
|
||||
boot_recovered = False
|
||||
current_status = "ONLINE - Pronto"
|
||||
current_status = "ONLINE - Ready"
|
||||
auto_healing_counter = {}
|
||||
|
||||
# ==========================================
|
||||
# 1. FUNZIONE NOTIFICA TELEGRAM
|
||||
# 1. TELEGRAM NOTIFICATION FUNCTION
|
||||
# ==========================================
|
||||
def send_telegram_message(message):
|
||||
t_cfg = cfg.get('telegram', {})
|
||||
if not t_cfg.get('enabled', False): return
|
||||
|
||||
ora_attuale = int(time.strftime("%H"))
|
||||
if ora_attuale >= 23 or ora_attuale < 7:
|
||||
print(f"🌙 Notte fonda ({ora_attuale}:00): Notifica evitata.")
|
||||
current_hour = int(time.strftime("%H"))
|
||||
if current_hour >= 23 or current_hour < 7:
|
||||
print(f"🌙 Late night ({current_hour}:00): Notification skipped.")
|
||||
return
|
||||
|
||||
token = t_cfg.get('token')
|
||||
@@ -65,58 +73,58 @@ def send_telegram_message(message):
|
||||
payload = {"chat_id": chat_id, "text": f"[{CLIENT_ID.upper()}]\n{clean_msg}"}
|
||||
requests.post(url, json=payload, timeout=10)
|
||||
except Exception as e:
|
||||
print(f"⚠️ Errore invio Telegram: {e}")
|
||||
print(f"⚠️ Telegram send error: {e}")
|
||||
|
||||
# ==========================================
|
||||
# 2. LOGICA CAMBIO PROFILO MULTIPLO
|
||||
# 2. MULTIPLE PROFILE SWITCH LOGIC
|
||||
# ==========================================
|
||||
|
||||
def get_actual_config_from_disk():
|
||||
return "ONLINE - Da memoria"
|
||||
return "ONLINE - From memory"
|
||||
|
||||
def switch_config(config_type):
|
||||
profile = cfg.get('profiles', {}).get(config_type)
|
||||
|
||||
if not profile:
|
||||
return f"ERRORE: Profilo {config_type} non trovato in JSON"
|
||||
return f"ERROR: Profile {config_type} not found in JSON"
|
||||
|
||||
label = profile.get('label', f"Profilo {config_type}")
|
||||
label = profile.get('label', f"Profile {config_type}")
|
||||
services = profile.get('services', [])
|
||||
|
||||
if not services:
|
||||
return f"ERRORE: Nessun servizio configurato per {config_type}"
|
||||
return f"ERROR: No services configured for {config_type}"
|
||||
|
||||
try:
|
||||
# 1. STOP: Ferma prima tutti i demoni coinvolti per liberare i file
|
||||
# 1. STOP: Stop all involved daemons first to release files
|
||||
for s in services:
|
||||
subprocess.run(["sudo", "systemctl", "stop", s['name']], check=False)
|
||||
|
||||
# 2. COPIA: Verifica e copia tutti i file di configurazione
|
||||
# 2. COPY: Verify and copy all configuration files
|
||||
for s in services:
|
||||
if not os.path.exists(s['source']):
|
||||
return f"ERRORE: Manca il file sorgente {s['source']}"
|
||||
return f"ERROR: Missing source file {s['source']}"
|
||||
shutil.copy(s['source'], s['target'])
|
||||
|
||||
# 3. START: Fa ripartire tutti i demoni con i nuovi file
|
||||
# 3. START: Restart all daemons with the new files
|
||||
for s in services:
|
||||
subprocess.run(["sudo", "systemctl", "start", s['name']], check=False)
|
||||
|
||||
send_telegram_message(f"✅ Switch multiplo completato: {label}")
|
||||
send_telegram_message(f"✅ Multiple switch completed: {label}")
|
||||
return f"ONLINE - {label}"
|
||||
|
||||
except Exception as e:
|
||||
return f"ERRORE: {str(e)}"
|
||||
return f"ERROR: {str(e)}"
|
||||
|
||||
def force_online_if_needed(client):
|
||||
global boot_recovered, current_status
|
||||
if not boot_recovered:
|
||||
print("⚠️ Recupero memoria saltato. Imposto stato da disco...")
|
||||
print("⚠️ Memory recovery skipped. Setting status from disk...")
|
||||
current_status = get_actual_config_from_disk()
|
||||
client.publish(TOPIC_STAT, current_status, retain=True)
|
||||
boot_recovered = True
|
||||
|
||||
# ==========================================
|
||||
# 3. TELEMETRIA E AUTO-HEALING
|
||||
# 3. TELEMETRY AND AUTO-HEALING
|
||||
# ==========================================
|
||||
def get_cpu_temperature():
|
||||
temp = 0.0
|
||||
@@ -139,8 +147,8 @@ def get_system_status():
|
||||
"processes": {},
|
||||
"timestamp": time.strftime("%H:%M:%S"),
|
||||
"profiles": {
|
||||
"A": cfg.get('profiles', {}).get('A', {}).get('label', 'PROFILO A'),
|
||||
"B": cfg.get('profiles', {}).get('B', {}).get('label', 'PROFILO B')
|
||||
"A": cfg.get('profiles', {}).get('A', {}).get('label', 'PROFILE A'),
|
||||
"B": cfg.get('profiles', {}).get('B', {}).get('label', 'PROFILE B')
|
||||
}
|
||||
}
|
||||
proc_path = Path(cfg['paths'].get('process_list', ''))
|
||||
@@ -151,7 +159,7 @@ def get_system_status():
|
||||
for name in target_processes:
|
||||
name = name.strip().lower()
|
||||
if name: status["processes"][name] = "online" if name in running_names else "offline"
|
||||
except Exception as e: print(f"Errore processi: {e}")
|
||||
except Exception as e: print(f"Process error: {e}")
|
||||
return status
|
||||
|
||||
def check_auto_healing(client, status):
|
||||
@@ -161,12 +169,12 @@ def check_auto_healing(client, status):
|
||||
attempts = auto_healing_counter.get(proc_name, 0)
|
||||
if attempts < 3:
|
||||
auto_healing_counter[proc_name] = attempts + 1
|
||||
msg = f"🛠 Auto-healing: {proc_name} offline. Riavvio {attempts+1}/3..."
|
||||
msg = f"🛠 Auto-healing: {proc_name} offline. Restarting {attempts+1}/3..."
|
||||
client.publish(f"devices/{CLIENT_ID}/logs", msg)
|
||||
send_telegram_message(msg)
|
||||
subprocess.run(["sudo", "systemctl", "restart", proc_name])
|
||||
elif attempts == 3:
|
||||
msg = f"🚨 CRITICO: {proc_name} fallito!"
|
||||
msg = f"🚨 CRITICAL: {proc_name} failed!"
|
||||
client.publish(f"devices/{CLIENT_ID}/logs", msg)
|
||||
send_telegram_message(msg)
|
||||
auto_healing_counter[proc_name] = 4
|
||||
@@ -176,7 +184,7 @@ def check_auto_healing(client, status):
|
||||
def publish_all(client):
|
||||
status = get_system_status()
|
||||
|
||||
# Lettura della lista file per il menu della Dashboard
|
||||
# Read file list for Dashboard menu
|
||||
file_list_path = Path(cfg['paths'].get('file_list', ''))
|
||||
status["config_files"] = []
|
||||
status["files"] = []
|
||||
@@ -184,9 +192,9 @@ def publish_all(client):
|
||||
if file_list_path.exists():
|
||||
try:
|
||||
files = file_list_path.read_text(encoding="utf-8").splitlines()
|
||||
nomi_estrattti = [Path(f.strip()).stem for f in files if f.strip()]
|
||||
status["config_files"] = nomi_estrattti
|
||||
status["files"] = nomi_estrattti
|
||||
extracted_names = [Path(f.strip()).stem for f in files if f.strip()]
|
||||
status["config_files"] = extracted_names
|
||||
status["files"] = extracted_names
|
||||
except: pass
|
||||
|
||||
client.publish(f"devices/{CLIENT_ID}/services", json.dumps(status), qos=1)
|
||||
@@ -209,7 +217,7 @@ def publish_all_ini_files(client):
|
||||
with open(file_list_path, 'r') as f:
|
||||
files_to_parse = [line.strip() for line in f if line.strip()]
|
||||
except Exception as e:
|
||||
print(f"Errore lettura {file_list_path}: {e}")
|
||||
print(f"Error reading {file_list_path}: {e}")
|
||||
return
|
||||
|
||||
for file_path in files_to_parse:
|
||||
@@ -218,38 +226,38 @@ def publish_all_ini_files(client):
|
||||
try:
|
||||
base_name = os.path.splitext(os.path.basename(file_path))[0]
|
||||
|
||||
# --- INIZIO PARSER MANUALE (Anti-Chiavi Doppie) ---
|
||||
# --- START MANUAL PARSER (Anti-Duplicate Keys) ---
|
||||
ini_data = {}
|
||||
current_section = None
|
||||
|
||||
with open(file_path, 'r', encoding='utf-8', errors='ignore') as f:
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
# Salta righe vuote o commenti
|
||||
# Skip empty lines or comments
|
||||
if not line or line.startswith(('#', ';')):
|
||||
continue
|
||||
# Riconosce le sezioni [Nome Sezione]
|
||||
# Recognize sections [Section Name]
|
||||
if line.startswith('[') and line.endswith(']'):
|
||||
current_section = line[1:-1].strip()
|
||||
ini_data[current_section] = {}
|
||||
# Riconosce le chiavi e i valori
|
||||
# Recognize keys and values
|
||||
elif '=' in line and current_section is not None:
|
||||
k, v = line.split('=', 1)
|
||||
k, v = k.strip(), v.strip()
|
||||
|
||||
# LA MAGIA: Se la chiave esiste già, la unisce con una virgola!
|
||||
# THE MAGIC: If the key already exists, merge it with a comma!
|
||||
if k in ini_data[current_section]:
|
||||
ini_data[current_section][k] = str(ini_data[current_section][k]) + "," + v
|
||||
else:
|
||||
ini_data[current_section][k] = v
|
||||
|
||||
# Pubblicazione sul broker MQTT
|
||||
# Publish on MQTT broker
|
||||
for section, payload in ini_data.items():
|
||||
topic = f"data/{CLIENT_ID}/{base_name}/{section}"
|
||||
client.publish(topic, json.dumps(payload), retain=True)
|
||||
|
||||
except Exception as e:
|
||||
print(f"Errore parsing INI per {file_path}: {e}")
|
||||
print(f"INI parsing error for {file_path}: {e}")
|
||||
|
||||
def write_config_from_json(slug, json_payload):
|
||||
file_list_path = Path(cfg['paths'].get('file_list', ''))
|
||||
@@ -259,20 +267,20 @@ def write_config_from_json(slug, json_payload):
|
||||
for f in files:
|
||||
p = Path(f.strip())
|
||||
if p.stem.lower() == slug.lower():
|
||||
nuovi_dati = json.loads(json_payload)
|
||||
new_data = json.loads(json_payload)
|
||||
shutil.copy(p, str(p) + ".bak")
|
||||
with open(p, 'w', encoding="utf-8") as file: file.write(nuovi_dati.get("raw_text", ""))
|
||||
with open(p, 'w', encoding="utf-8") as file: file.write(new_data.get("raw_text", ""))
|
||||
os.system(f"sudo systemctl restart {slug}")
|
||||
send_telegram_message(f"📝 Config {slug.upper()} aggiornata via Web.")
|
||||
send_telegram_message(f"📝 Config {slug.upper()} updated via Web.")
|
||||
break
|
||||
except Exception as e: print(f"Errore scrittura config: {e}")
|
||||
except Exception as e: print(f"Config write error: {e}")
|
||||
|
||||
# ==========================================
|
||||
# 4. CALLBACK MQTT
|
||||
# 4. MQTT CALLBACKS
|
||||
# ==========================================
|
||||
def on_connect(client, userdata, flags, rc, properties=None):
|
||||
if rc == 0:
|
||||
print(f"✅ Connesso: {CLIENT_ID.upper()}")
|
||||
print(f"✅ Connected: {CLIENT_ID.upper()}")
|
||||
client.subscribe([(TOPIC_CMD, 0), (TOPIC_STAT, 0)])
|
||||
client.subscribe([
|
||||
("devices/control/request", 0),
|
||||
@@ -281,7 +289,7 @@ def on_connect(client, userdata, flags, rc, properties=None):
|
||||
])
|
||||
threading.Timer(5.0, force_online_if_needed, [client]).start()
|
||||
publish_all(client)
|
||||
publish_all_ini_files(client) # Pubblica gli INI appena si connette
|
||||
publish_all_ini_files(client) # Publish INIs as soon as connected
|
||||
|
||||
def on_message(client, userdata, msg):
|
||||
global boot_recovered, current_status, cfg
|
||||
@@ -289,7 +297,7 @@ def on_message(client, userdata, msg):
|
||||
topic = msg.topic
|
||||
|
||||
if topic == TOPIC_STAT and not boot_recovered:
|
||||
if not any(x in payload.upper() for x in ["OFFLINE", "ERRORE", "RIAVVIO"]):
|
||||
if not any(x in payload.upper() for x in ["OFFLINE", "ERROR", "REBOOT"]):
|
||||
current_status = payload
|
||||
boot_recovered = True
|
||||
client.publish(TOPIC_STAT, current_status, retain=True)
|
||||
@@ -302,16 +310,50 @@ def on_message(client, userdata, msg):
|
||||
boot_recovered = True
|
||||
publish_all(client)
|
||||
elif cmd == "REBOOT":
|
||||
client.publish(TOPIC_STAT, f"OFFLINE - Riavvio {CLIENT_ID.upper()}...", retain=False)
|
||||
client.publish(TOPIC_STAT, f"OFFLINE - Rebooting {CLIENT_ID.upper()}...", retain=False)
|
||||
time.sleep(1)
|
||||
subprocess.run(["sudo", "reboot"], check=True)
|
||||
elif cmd == 'RESET_HAT':
|
||||
# Correct GPIO pin for MMDVM board hardware reset
|
||||
RESET_PIN = 21
|
||||
|
||||
if GPIO_AVAILABLE:
|
||||
try:
|
||||
GPIO.setwarnings(False)
|
||||
GPIO.setmode(GPIO.BCM)
|
||||
GPIO.setup(RESET_PIN, GPIO.OUT)
|
||||
|
||||
# 1. Send reset pulse (LOW for 0.5 seconds)
|
||||
GPIO.output(RESET_PIN, GPIO.LOW)
|
||||
time.sleep(0.5)
|
||||
GPIO.output(RESET_PIN, GPIO.HIGH)
|
||||
|
||||
# Release GPIO resources
|
||||
GPIO.cleanup(RESET_PIN)
|
||||
print(f"[{CLIENT_ID}] RESET pulse sent to GPIO {RESET_PIN}")
|
||||
|
||||
# 2. Wait 1.5 seconds to let the microcontroller firmware reboot
|
||||
time.sleep(1.5)
|
||||
|
||||
# 3. Restart MMDVMHost service to realign serial communication
|
||||
print(f"[{CLIENT_ID}] Restarting MMDVMHost...")
|
||||
subprocess.run(["sudo", "systemctl", "restart", "mmdvmhost"], check=False)
|
||||
|
||||
# 4. Send confirmations to dashboard
|
||||
client.publish(f"fleet/{CLIENT_ID}/status", "HAT RESET + MMDVM RESTART OK")
|
||||
client.publish(f"devices/{CLIENT_ID}/logs", "🔌 HAT Reset + MMDVMHost Restarted")
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error during GPIO/MMDVMHost reset: {e}")
|
||||
client.publish(f"fleet/{CLIENT_ID}/status", f"RESET ERROR: {e}")
|
||||
|
||||
elif cmd in ["TG:OFF", "TG:ON"]:
|
||||
nuovo_stato = (cmd == "TG:ON")
|
||||
cfg['telegram']['enabled'] = nuovo_stato
|
||||
new_state = (cmd == "TG:ON")
|
||||
cfg['telegram']['enabled'] = new_state
|
||||
try:
|
||||
with open(CONFIG_PATH, 'w') as f: json.dump(cfg, f, indent=4)
|
||||
client.publish(f"devices/{CLIENT_ID}/logs", f"{'🔔' if nuovo_stato else '🔇'} Notifiche {'ON' if nuovo_stato else 'OFF'}")
|
||||
if nuovo_stato: send_telegram_message("Notifiche riattivate!")
|
||||
client.publish(f"devices/{CLIENT_ID}/logs", f"{'🔔' if new_state else '🔇'} Notifications {'ON' if new_state else 'OFF'}")
|
||||
if new_state: send_telegram_message("Notifications enabled!")
|
||||
except: pass
|
||||
|
||||
elif topic == "devices/control/request" and payload.lower() in ["status", "update"]:
|
||||
@@ -338,7 +380,7 @@ def on_message(client, userdata, msg):
|
||||
def auto_publish_task(client):
|
||||
while True:
|
||||
status = publish_all(client)
|
||||
publish_all_ini_files(client) # <--- ECCO IL LOOP CORRETTO!
|
||||
publish_all_ini_files(client) # <--- HERE IS THE CORRECT LOOP!
|
||||
check_auto_healing(client, status)
|
||||
time.sleep(cfg['settings'].get('update_interval', 30))
|
||||
|
||||
|
||||
Reference in New Issue
Block a user