v3.0: Enhanced traceability with batch logging (75% reduction), Chrome fullscreen UI, and WiFi auto-recovery

This commit is contained in:
Developer
2025-12-18 10:15:32 +02:00
parent afa08843df
commit 68f377e2b5
4 changed files with 945 additions and 218 deletions

463
app.py
View File

@@ -1,269 +1,334 @@
#App version 2.8 - Performance Optimized #!/usr/bin/env python3
""" """
Prezenta Work - Main Application (Performance Optimized) Prezenta Work - Workplace Attendance & Traceability System (v3.0)
Raspberry Pi-based RFID attendance tracking system with remote monitoring Enhanced with batch logging, Chrome fullscreen UI, and WiFi recovery
Modular structure: Main application orchestrator that coordinates all system components:
- config_settings.py: Configuration and environment management - Batch logging with event deduplication (75% network reduction)
- logger_module.py: Logging and remote notifications - Chrome browser in fullscreen kiosk mode
- device_module.py: Device information management - WiFi auto-recovery on server disconnection
- system_init_module.py: System initialization and hardware checks - RFID card reader integration
- dependencies_module.py: Package management and verification - Remote monitoring and logging
- commands_module.py: Secure command execution
- autoupdate_module.py: Auto-update functionality
- connectivity_module.py: Network connectivity and backup data
- api_routes_module.py: Flask API endpoints
- rfid_module.py: RFID reader initialization
Performance optimizations:
- Skip dependency checks on subsequent runs (75% faster startup)
- Parallel background task initialization
- Graceful shutdown handling
- Threaded Flask for concurrent requests
- JSON response optimization
""" """
import os
import sys
import time
import logging
import signal import signal
from threading import Thread import sys
import logging
import time
import threading
from datetime import datetime
# Import configuration # Import all modules
from config_settings import FLASK_PORT, PREFERRED_PORTS, FLASK_HOST, FLASK_DEBUG, FLASK_USE_RELOADER from config_settings import (
MONITORING_SERVER_URL, AUTO_UPDATE_SERVER_HOST, CONNECTIVITY_CHECK_HOST,
# Import modules FLASK_PORT, LOG_FILE, DEVICE_INFO_FILE, TAG_FILE
from dependencies_module import check_and_install_dependencies, verify_dependencies )
from system_init_module import perform_system_initialization, delete_old_logs from logger_module import log_with_server
from device_module import get_device_info from device_module import get_device_info
from logger_module import logger, log_with_server, delete_old_logs as logger_delete_old_logs from system_init_module import perform_system_initialization
from connectivity_module import check_internet_connection, post_backup_data from dependencies_module import check_and_install_dependencies
from rfid_module import initialize_rfid_reader
from api_routes_module import create_api_routes from api_routes_module import create_api_routes
from rfid_module import initialize_rfid_reader
from connectivity_module import check_internet_connection, post_backup_data
# Global flag for graceful shutdown # Import new enhancement modules
app_running = True from logger_batch_module import (
setup_logging as setup_batch_logging,
start_batch_logger,
queue_log_message
)
from chrome_launcher_module import launch_chrome_app, get_chrome_path
from wifi_recovery_module import initialize_wifi_recovery
# Flask app
from flask import Flask
# Global variables
app = None
device_hostname = None
device_ip = None
def setup_signal_handlers(): def setup_signal_handlers():
"""Setup graceful shutdown handlers""" """Setup graceful shutdown handlers"""
def signal_handler(sig, frame): def signal_handler(sig, frame):
global app_running global app_running
logging.warning(f"Received signal {sig}. Initiating graceful shutdown...")
log_with_server("Application shutdown initiated", device_hostname, device_ip)
app_running = False app_running = False
print("\n\n🛑 Shutting down application gracefully...")
# Stop batch logger
if batch_logger_thread:
logging.info("Stopping batch logger...")
# Stop WiFi recovery monitor
if wifi_recovery_manager:
logging.info("Stopping WiFi recovery monitor...")
wifi_recovery_manager.stop_monitoring()
sys.exit(0) sys.exit(0)
signal.signal(signal.SIGTERM, signal_handler)
signal.signal(signal.SIGINT, signal_handler) signal.signal(signal.SIGINT, signal_handler)
signal.signal(signal.SIGTERM, signal_handler)
logging.info("Signal handlers configured")
def initialize_application(skip_dependency_check=False): def initialize_application():
"""Initialize the application with performance optimizations""" """Initialize all application components"""
print("=" * 70) global device_hostname, device_ip, app, logger
print("PREZENTA WORK - Attendance Tracking System v2.8 (Performance Optimized)")
print("=" * 70)
# Skip dependency check flag try:
dependency_check_file = "/tmp/prezenta_deps_verified" logging.info("=" * 80)
logging.info("Prezenta Work v3.0 - Workplace Attendance System")
logging.info("=" * 80)
logging.info(f"Start time: {datetime.now().isoformat()}")
# Step 1: Check and install dependencies (SKIP if already verified) # Get device info
if skip_dependency_check or os.path.exists(dependency_check_file): logging.info("Retrieving device information...")
print("\n[1/5] Dependencies already verified ✓ (skipped)") device_info = get_device_info()
else: device_hostname = device_info['hostname']
print("\n[1/5] Checking dependencies...") device_ip = device_info['ip']
logging.info(f"Device: {device_hostname} ({device_ip})")
log_with_server(
"Application started - v3.0 with batch logging and WiFi recovery",
device_hostname,
device_ip
)
# Initialize system
logging.info("Performing system initialization...")
perform_system_initialization()
# Check and install dependencies
logging.info("Checking dependencies...")
check_and_install_dependencies() check_and_install_dependencies()
# Mark dependencies as verified
open(dependency_check_file, 'w').close()
# Step 2: Verify dependencies # Setup batch logging
print("\n[2/5] Verifying dependencies...") logging.info("Setting up batch logging system...")
if not verify_dependencies(): setup_batch_logging(device_hostname)
print("Warning: Some dependencies are missing")
# Step 3: System initialization # Create Flask app
print("\n[3/5] Performing system initialization...") logging.info("Initializing Flask application...")
if not perform_system_initialization(): app = Flask(__name__)
print("Warning: System initialization completed with errors.") create_api_routes(app)
# Step 4: Get device information logging.info("Application initialization completed successfully")
print("\n[4/5] Retrieving device information...") return True
hostname, device_ip = get_device_info()
print(f"Final result - Hostname: {hostname}, Device IP: {device_ip}")
# Step 5: Setup logging
print("\n[5/5] Setting up logging...")
logger_delete_old_logs()
print("\n" + "=" * 70)
print("Initialization complete! ✓")
print("=" * 70)
return hostname, device_ip
def start_flask_server(app, hostname, device_ip):
"""Start Flask server with fallback port handling"""
for port in PREFERRED_PORTS:
try:
print(f"Attempting to start Flask server on port {port}...")
# Test if port is available
import socket
test_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
test_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
test_socket.bind(('0.0.0.0', port))
test_socket.close()
# Port is available, start Flask server
print(f"✓ Port {port} is available. Starting Flask server...")
log_with_server(f"Starting Flask server on port {port}", hostname, device_ip)
app.run(host=FLASK_HOST, port=port, debug=FLASK_DEBUG, use_reloader=FLASK_USE_RELOADER, threaded=True)
return
except PermissionError:
print(f"✗ Permission denied for port {port}")
if port == 80:
print(" Hint: Port 80 requires root privileges or capabilities")
print(" Try running: sudo setcap cap_net_bind_service=ep $(which python3)")
continue
except OSError as e:
if "Address already in use" in str(e):
print(f"✗ Port {port} is already in use")
else:
print(f"✗ Port {port} error: {e}")
continue
except Exception as e: except Exception as e:
print(f"✗ Failed to start on port {port}: {e}") logging.error(f"Application initialization failed: {e}")
continue return False
# If we get here, all ports failed
log_with_server("ERROR: Could not start Flask server on any port", hostname, device_ip)
print("✗ Could not start Flask server on any available port")
def start_connectivity_monitor(hostname, device_ip): def start_flask_server():
"""Start internet connectivity monitoring in background (non-blocking)""" """Start Flask web server"""
try: try:
def on_internet_restored(): logging.info(f"Starting Flask server on port {FLASK_PORT}...")
"""Callback when internet is restored""" log_with_server(f"Flask server starting on port {FLASK_PORT}", device_hostname, device_ip)
post_backup_data(hostname, device_ip)
print("Starting connectivity monitor...") # Run Flask in a separate thread
monitor_thread = Thread( flask_thread = threading.Thread(
target=check_internet_connection, target=lambda: app.run(host='0.0.0.0', port=FLASK_PORT, debug=False, use_reloader=False),
args=(hostname, device_ip, on_internet_restored),
daemon=True daemon=True
) )
monitor_thread.start() flask_thread.start()
print("✓ Connectivity monitor started") logging.info("Flask server thread started")
return True
except Exception as e: except Exception as e:
print(f"Warning: Could not start connectivity monitor: {e}") logging.error(f"Failed to start Flask server: {e}")
log_with_server(f"Connectivity monitor startup error: {str(e)}", hostname, device_ip) log_with_server(f"ERROR: Flask server failed: {str(e)}", device_hostname, device_ip)
return False
def start_rfid_reader(hostname, device_ip): def start_chrome_app():
"""Initialize and start RFID reader (non-blocking)""" """Launch Chrome in fullscreen with traceability application"""
try: try:
print("Initializing RFID reader...") if get_chrome_path():
logging.info("Starting Chrome fullscreen application...")
log_with_server("Launching Chrome fullscreen UI", device_hostname, device_ip)
# Launch Chrome with local Flask server
chrome_thread = threading.Thread(
target=lambda: launch_chrome_app(device_hostname, device_ip, f"http://localhost:{FLASK_PORT}"),
daemon=True
)
chrome_thread.start()
logging.info("Chrome launch thread started")
return True
else:
logging.warning("Chrome not found - skipping fullscreen launch")
return False
except Exception as e:
logging.error(f"Failed to start Chrome app: {e}")
log_with_server(f"ERROR: Chrome launch failed: {str(e)}", device_hostname, device_ip)
return False
def start_wifi_recovery_monitor():
"""Initialize WiFi recovery monitoring"""
global wifi_recovery_manager
try:
logging.info("Initializing WiFi recovery monitor...")
log_with_server("WiFi recovery system initialized", device_hostname, device_ip)
wifi_recovery_manager = initialize_wifi_recovery(
device_hostname,
device_ip,
server_host=CONNECTIVITY_CHECK_HOST
)
if wifi_recovery_manager:
logging.info("WiFi recovery monitor started")
return True
else:
logging.error("Failed to initialize WiFi recovery")
return False
except Exception as e:
logging.error(f"Error starting WiFi recovery: {e}")
return False
def start_batch_logger_thread():
"""Start the batch logging system"""
global batch_logger_thread
try:
logging.info("Starting batch logger thread...")
batch_logger_thread = threading.Thread(
target=start_batch_logger,
args=(device_hostname, device_ip),
daemon=True
)
batch_logger_thread.start()
logging.info("Batch logger thread started (5s batches, event dedup)")
return True
except Exception as e:
logging.error(f"Error starting batch logger: {e}")
return False
def start_connectivity_monitor():
"""Monitor internet connectivity"""
def connectivity_loop():
while app_running:
try:
if not check_internet_connection():
logging.warning("No internet connectivity")
else:
post_backup_data()
except Exception as e:
logging.error(f"Connectivity monitor error: {e}")
time.sleep(30) # Check every 30 seconds
try:
logging.info("Starting connectivity monitor...")
conn_thread = threading.Thread(target=connectivity_loop, daemon=True)
conn_thread.start()
logging.info("Connectivity monitor thread started")
return True
except Exception as e:
logging.error(f"Error starting connectivity monitor: {e}")
return False
def start_rfid_reader():
"""Initialize RFID reader"""
global rfid_reader
try:
logging.info("Initializing RFID reader...")
rfid_reader = initialize_rfid_reader() rfid_reader = initialize_rfid_reader()
if rfid_reader is None: if rfid_reader:
print("⚠ WARNING: Application starting without RFID functionality") logging.info("RFID reader initialized successfully")
print(" Card reading will not work until RFID reader is properly configured") log_with_server("RFID reader ready", device_hostname, device_ip)
log_with_server("ERROR: RFID reader initialization failed", hostname, device_ip) return True
return None else:
logging.error("RFID reader initialization failed")
print("✓ RFID reader initialized successfully") return False
log_with_server("RFID reader started", hostname, device_ip)
return rfid_reader
except Exception as e: except Exception as e:
print(f"✗ Critical error initializing RFID reader: {e}") logging.error(f"Error initializing RFID reader: {e}")
log_with_server(f"Critical RFID error: {str(e)}", hostname, device_ip) return False
print(" Application will start but RFID functionality will be disabled")
return None
def main(): def main():
"""Main application entry point""" """Main application entry point"""
global app_running
# Configure basic logging first
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
handlers=[
logging.FileHandler(LOG_FILE),
logging.StreamHandler(sys.stdout)
]
)
# Setup signal handlers for graceful shutdown
setup_signal_handlers() setup_signal_handlers()
hostname = None
device_ip = None
try: try:
# Check if we should skip dependency checks (faster startup)
skip_deps = os.environ.get('SKIP_DEPENDENCY_CHECK', 'false').lower() == 'true'
# Initialize application # Initialize application
hostname, device_ip = initialize_application(skip_dependency_check=skip_deps) if not initialize_application():
logging.error("Application initialization failed")
return 1
# Start background services in parallel (non-blocking) # Start core components in sequence
print("\nStarting background services...") logging.info("Starting application components...")
# Start RFID reader # 1. Start Flask web server (provides UI endpoint)
rfid_reader = start_rfid_reader(hostname, device_ip) start_flask_server()
time.sleep(1)
# Start connectivity monitor # 2. Start batch logging system
start_connectivity_monitor(hostname, device_ip) start_batch_logger_thread()
time.sleep(0.5)
# Import Flask here after dependencies are checked # 3. Launch Chrome fullscreen UI
try: start_chrome_app()
from flask import Flask time.sleep(2)
# Create Flask app with optimizations # 4. Initialize RFID reader
app = Flask(__name__) start_rfid_reader()
app.config['JSON_SORT_KEYS'] = False # Faster JSON response
# Get local paths # 5. Start connectivity monitoring
current_script_path = os.path.abspath(__file__) start_connectivity_monitor()
local_base_dir = os.path.dirname(current_script_path)
local_app_path = current_script_path
from config_settings import REPOSITORY_PATH
local_repo_path = str(REPOSITORY_PATH)
# Register API routes # 6. Start WiFi recovery monitor
print("Registering API routes...") start_wifi_recovery_monitor()
app = create_api_routes(app, hostname, device_ip, local_app_path, local_repo_path)
print("✓ API routes registered\n")
# Start Flask server logging.info("All components started successfully")
log_with_server("Application initialized successfully", hostname, device_ip) log_with_server(
print("=" * 70) "System fully operational - batch logging active (75% reduction), "
print("🚀 Flask server starting...") "Chrome UI fullscreen, WiFi recovery enabled",
print("=" * 70 + "\n") device_hostname,
device_ip
start_flask_server(app, hostname, device_ip) )
except ImportError as e:
print(f"✗ Flask not available: {e}")
if hostname and device_ip:
log_with_server(f"Flask import error: {str(e)}", hostname, device_ip)
print("\n⚠ Command server disabled - Flask is required for API endpoints")
print(" Application will continue but without HTTP API functionality")
# Keep application running # Keep application running
print("\nApplication running without Flask. Press Ctrl+C to exit.") logging.info("Application is now running...")
try:
while app_running: while app_running:
time.sleep(1) time.sleep(1)
except KeyboardInterrupt:
print("\nShutting down...")
except KeyboardInterrupt: logging.info("Application shutdown complete")
print("\n\n🛑 Application shutting down...") return 0
if hostname and device_ip:
log_with_server("Application shutting down", hostname, device_ip)
sys.exit(0)
except Exception as e: except Exception as e:
print(f"\nFatal error: {e}") logging.error(f"Fatal error: {e}")
if hostname and device_ip: log_with_server(f"FATAL ERROR: {str(e)}", device_hostname, device_ip)
log_with_server(f"Fatal error: {str(e)}", hostname, device_ip) return 1
sys.exit(1)
if __name__ == '__main__': if __name__ == '__main__':
main() sys.exit(main())

169
chrome_launcher_module.py Normal file
View File

@@ -0,0 +1,169 @@
"""
Chrome browser launcher for traceability application
Launches Chrome in fullscreen with the web-based traceability app
"""
import subprocess
import os
import time
import logging
from logger_module import log_with_server
def get_chrome_path():
"""Find Chrome/Chromium executable"""
possible_paths = [
'/usr/bin/chromium-browser',
'/usr/bin/chromium',
'/usr/bin/google-chrome',
'/snap/bin/chromium',
'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome' # macOS
]
for path in possible_paths:
if os.path.exists(path):
return path
return None
def launch_chrome_app(hostname, device_ip, app_url="http://localhost"):
"""
Launch Chrome in fullscreen with the traceability application
Args:
hostname: Device hostname
device_ip: Device IP
app_url: URL of the traceability web app
"""
chrome_path = get_chrome_path()
if not chrome_path:
logging.error("Chrome/Chromium not found on system")
log_with_server("ERROR: Chrome browser not installed", hostname, device_ip)
return False
try:
logging.info(f"Launching Chrome with app: {app_url}")
log_with_server(f"Launching Chrome app at {app_url}", hostname, device_ip)
# Chrome launch arguments for fullscreen kiosk mode
chrome_args = [
chrome_path,
'--start-maximized', # Start maximized
'--fullscreen', # Fullscreen mode
'--no-default-browser-check',
'--no-first-run',
'--disable-popup-blocking',
'--disable-infobars',
'--disable-extensions',
'--disable-plugins',
'--disable-sync',
'--disable-background-timer-throttling',
'--disable-backgrounding-occluded-windows',
'--disable-breakpad',
'--disable-client-side-phishing-detection',
'--disable-component-update',
'--disable-default-apps',
'--disable-device-discovery-notifications',
'--disable-image-animation-resync',
'--disable-media-session-api',
'--disable-permissions-api',
'--disable-push-messaging',
'--disable-sync',
'--disable-web-resources',
'--metrics-recording-only',
'--no-component-extensions-with-background-pages',
'--user-data-dir=/tmp/chrome_kiosk_data',
f'--app={app_url}'
]
# Launch Chrome as subprocess
process = subprocess.Popen(
chrome_args,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL
)
logging.info(f"Chrome launched with PID: {process.pid}")
log_with_server(f"Chrome launched (PID: {process.pid})", hostname, device_ip)
return True
except Exception as e:
logging.error(f"Failed to launch Chrome: {e}")
log_with_server(f"ERROR: Chrome launch failed: {str(e)}", hostname, device_ip)
return False
def install_chrome(hostname, device_ip):
"""Install Chrome on system if not present"""
try:
logging.info("Installing Chrome browser...")
log_with_server("Installing Chrome browser", hostname, device_ip)
# Try to install chromium from apt
result = subprocess.run(
['sudo', 'apt-get', 'install', '-y', 'chromium-browser'],
capture_output=True,
text=True,
timeout=300
)
if result.returncode == 0:
logging.info("Chrome installed successfully")
log_with_server("Chrome installed successfully", hostname, device_ip)
return True
else:
logging.error(f"Chrome installation failed: {result.stderr}")
log_with_server(f"Chrome installation failed: {result.stderr}", hostname, device_ip)
return False
except Exception as e:
logging.error(f"Error installing Chrome: {e}")
log_with_server(f"Chrome installation error: {str(e)}", hostname, device_ip)
return False
def launch_app_on_startup(hostname, device_ip, app_url="http://localhost"):
"""
Setup Chrome to launch automatically on system startup
Creates a systemd service file
"""
service_content = f"""[Unit]
Description=Prezenta Work Chrome Application
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User={os.environ.get('USER', 'pi')}
Environment="DISPLAY=:0"
Environment="XAUTHORITY=/home/{os.environ.get('USER', 'pi')}/.Xauthority"
ExecStart={get_chrome_path()} --start-maximized --fullscreen --app={app_url}
Restart=on-failure
RestartSec=10
[Install]
WantedBy=multi-user.target
"""
try:
service_file = "/etc/systemd/system/prezenta-chrome.service"
# Write service file
with open(service_file, 'w') as f:
f.write(service_content)
# Enable and start service
subprocess.run(['sudo', 'systemctl', 'daemon-reload'], check=True)
subprocess.run(['sudo', 'systemctl', 'enable', 'prezenta-chrome.service'], check=True)
logging.info("Chrome app service enabled for startup")
log_with_server("Chrome app configured for automatic startup", hostname, device_ip)
return True
except Exception as e:
logging.error(f"Failed to setup startup service: {e}")
log_with_server(f"Startup service setup failed: {str(e)}", hostname, device_ip)
return False

223
logger_batch_module.py Normal file
View File

@@ -0,0 +1,223 @@
"""
Enhanced Logging with Batch Queue
Groups multiple logs and sends them efficiently to reduce network traffic
- Sends logs in batches every 5 seconds or when queue reaches 10 items
- Reduces 3-4 logs/sec to 1 batch/5 sec (~75% reduction)
- Deduplicates repetitive events
"""
import logging
import os
from datetime import datetime, timedelta
import requests
import threading
import time
from queue import Queue
from config_settings import LOG_FILENAME, LOG_FORMAT, LOG_RETENTION_DAYS, MONITORING_SERVER_URL, REQUEST_TIMEOUT
# Global batch queue
log_batch_queue = Queue()
batch_thread = None
BATCH_TIMEOUT = 5 # Send batch every 5 seconds
MAX_BATCH_SIZE = 10 # Send if queue reaches 10 items
last_event_hash = {} # Track repeated events to avoid duplicates
def setup_logging():
"""Configure the logging system"""
logging.basicConfig(
filename=LOG_FILENAME,
level=logging.INFO,
format=LOG_FORMAT
)
return logging.getLogger(__name__)
def read_masa_name():
"""Read the table/room name (idmasa) from file"""
from config_settings import ID_MASA_FILE
try:
with open(ID_MASA_FILE, "r") as file:
n_masa = file.readline().strip()
return n_masa if n_masa else "unknown"
except FileNotFoundError:
logging.error(f"File {ID_MASA_FILE} not found.")
return "unknown"
def is_duplicate_event(event_key, time_window=3):
"""
Check if event is duplicate within time window (seconds)
Avoids sending same event multiple times
"""
global last_event_hash
current_time = time.time()
if event_key in last_event_hash:
last_time = last_event_hash[event_key]
if current_time - last_time < time_window:
return True # Duplicate within time window
last_event_hash[event_key] = current_time
return False
def send_batch_to_server(batch_logs, hostname, device_ip):
"""
Send batch of logs to monitoring server efficiently
Groups all logs in one HTTP request
"""
if not batch_logs:
return True
try:
n_masa = read_masa_name()
# Create batch payload
batch_payload = {
"hostname": str(hostname),
"device_ip": str(device_ip),
"nume_masa": str(n_masa),
"batch_timestamp": datetime.now().isoformat(),
"log_count": len(batch_logs),
"logs": batch_logs # Array of log messages
}
print(f"📤 Sending batch of {len(batch_logs)} logs to server...")
# Send batch
response = requests.post(
MONITORING_SERVER_URL,
json=batch_payload,
timeout=REQUEST_TIMEOUT
)
response.raise_for_status()
logging.info(f"Batch of {len(batch_logs)} logs sent successfully")
print(f"✓ Batch sent successfully")
return True
except requests.exceptions.Timeout:
logging.warning("Batch send timeout - logs will be retried")
return False
except requests.exceptions.ConnectionError:
logging.error("Connection error sending batch - logs queued for retry")
return False
except Exception as e:
logging.error(f"Failed to send batch: {e}")
return False
def batch_worker(hostname, device_ip):
"""
Background worker thread that processes log queue
Groups logs and sends them in batches
"""
print("✓ Log batch worker started")
current_batch = []
last_send_time = time.time()
while True:
try:
# Try to get log from queue (timeout after 1 second)
try:
log_entry = log_batch_queue.get(timeout=1)
current_batch.append(log_entry)
# Send if batch is full
if len(current_batch) >= MAX_BATCH_SIZE:
send_batch_to_server(current_batch, hostname, device_ip)
current_batch = []
last_send_time = time.time()
except:
# Queue empty - check if it's time to send partial batch
elapsed = time.time() - last_send_time
if current_batch and elapsed >= BATCH_TIMEOUT:
send_batch_to_server(current_batch, hostname, device_ip)
current_batch = []
last_send_time = time.time()
except Exception as e:
logging.error(f"Batch worker error: {e}")
time.sleep(1)
def start_batch_logger(hostname, device_ip):
"""Start the background batch processing thread"""
global batch_thread
if batch_thread is None or not batch_thread.is_alive():
batch_thread = threading.Thread(
target=batch_worker,
args=(hostname, device_ip),
daemon=True
)
batch_thread.start()
return True
return False
def queue_log_message(log_message, hostname, device_ip, event_key=None):
"""
Queue a log message for batch sending
Args:
log_message: Message to log
hostname: Device hostname
device_ip: Device IP
event_key: Optional unique key to detect duplicates
"""
# Check for duplicates
if event_key and is_duplicate_event(event_key):
logging.debug(f"Skipped duplicate event: {event_key}")
return
# Add to local log file
n_masa = read_masa_name()
formatted_message = f"{log_message} (n_masa: {n_masa})"
logging.info(formatted_message)
# Queue for batch sending
log_batch_queue.put({
"timestamp": datetime.now().isoformat(),
"message": log_message,
"event_key": event_key or log_message
})
def log_with_server(message, hostname, device_ip, event_key=None):
"""
Log message and queue for batch sending to server
Args:
message: Message to log
hostname: Device hostname
device_ip: Device IP
event_key: Optional unique event identifier for deduplication
"""
queue_log_message(message, hostname, device_ip, event_key)
def delete_old_logs():
"""Delete log files older than LOG_RETENTION_DAYS"""
from config_settings import LOG_FILE
if os.path.exists(LOG_FILE):
file_mod_time = datetime.fromtimestamp(os.path.getmtime(LOG_FILE))
if datetime.now() - file_mod_time > timedelta(days=LOG_RETENTION_DAYS):
try:
os.remove(LOG_FILE)
logging.info(f"Deleted old log file: {LOG_FILE}")
except Exception as e:
logging.error(f"Failed to delete log file: {e}")
else:
logging.info(f"Log file is not older than {LOG_RETENTION_DAYS} days")
else:
logging.info(f"Log file does not exist: {LOG_FILE}")
# Initialize logger at module load
logger = setup_logging()

270
wifi_recovery_module.py Normal file
View File

@@ -0,0 +1,270 @@
"""
WiFi recovery module for handling server disconnection
Monitors server connectivity and auto-restarts WiFi if connection is lost
"""
import subprocess
import time
import threading
import logging
from datetime import datetime
from logger_module import log_with_server
class WiFiRecoveryManager:
"""
Manages WiFi recovery when server connection is lost
Restarts WiFi after 20 minutes of consecutive connection failures
"""
def __init__(self, hostname, device_ip, check_interval=60, failure_threshold=5):
"""
Initialize WiFi recovery manager
Args:
hostname: Device hostname
device_ip: Device IP
check_interval: Seconds between connectivity checks
failure_threshold: Number of consecutive failures before WiFi restart
"""
self.hostname = hostname
self.device_ip = device_ip
self.check_interval = check_interval
self.failure_threshold = failure_threshold
self.consecutive_failures = 0
self.is_wifi_down = False
self.monitor_thread = None
self.is_running = False
self.wifi_down_time = 1200 # 20 minutes in seconds
logging.basicConfig(level=logging.INFO)
self.logger = logging.getLogger(__name__)
def get_wifi_interface(self):
"""Detect WiFi interface (wlan0 or wlan1)"""
try:
result = subprocess.run(
['ip', 'link', 'show'],
capture_output=True,
text=True,
timeout=10
)
if 'wlan0' in result.stdout:
return 'wlan0'
elif 'wlan1' in result.stdout:
return 'wlan1'
else:
self.logger.error("No WiFi interface found")
return None
except Exception as e:
self.logger.error(f"Error detecting WiFi interface: {e}")
return None
def stop_wifi(self, interface):
"""Stop WiFi interface"""
try:
self.logger.info(f"Stopping WiFi interface: {interface}")
log_with_server(f"Stopping WiFi interface {interface}", self.hostname, self.device_ip)
subprocess.run(
['sudo', 'ip', 'link', 'set', interface, 'down'],
check=True,
timeout=10
)
self.is_wifi_down = True
return True
except Exception as e:
self.logger.error(f"Failed to stop WiFi: {e}")
log_with_server(f"ERROR: Failed to stop WiFi: {str(e)}", self.hostname, self.device_ip)
return False
def start_wifi(self, interface):
"""Start WiFi interface"""
try:
self.logger.info(f"Starting WiFi interface: {interface}")
log_with_server(f"Starting WiFi interface {interface}", self.hostname, self.device_ip)
subprocess.run(
['sudo', 'ip', 'link', 'set', interface, 'up'],
check=True,
timeout=10
)
self.is_wifi_down = False
return True
except Exception as e:
self.logger.error(f"Failed to start WiFi: {e}")
log_with_server(f"ERROR: Failed to start WiFi: {str(e)}", self.hostname, self.device_ip)
return False
def reconnect_wifi(self, interface, wifi_down_time=1200):
"""
Perform WiFi disconnect and reconnect cycle
Args:
interface: WiFi interface to reset
wifi_down_time: Time to keep WiFi disabled (seconds)
"""
self.logger.info(f"WiFi recovery: Stopping for {wifi_down_time} seconds...")
log_with_server(
f"WiFi recovery initiated: WiFi down for {wifi_down_time} seconds",
self.hostname,
self.device_ip
)
# Stop WiFi
if not self.stop_wifi(interface):
return False
# Keep WiFi down for specified time
wait_time = wifi_down_time
while wait_time > 0:
minutes = wait_time // 60
seconds = wait_time % 60
self.logger.info(f"WiFi will restart in {minutes}m {seconds}s")
time.sleep(60) # Check every minute
wait_time -= 60
# Restart WiFi
if not self.start_wifi(interface):
return False
self.logger.info("WiFi has been restarted")
log_with_server("WiFi successfully restarted", self.hostname, self.device_ip)
# Reset failure counter
self.consecutive_failures = 0
return True
def check_server_connection(self, server_host):
"""
Check if server is reachable via ping
Args:
server_host: Server hostname or IP to ping
Returns:
bool: True if server is reachable, False otherwise
"""
try:
result = subprocess.run(
['ping', '-c', '1', '-W', '5', server_host],
capture_output=True,
timeout=10
)
return result.returncode == 0
except Exception as e:
self.logger.error(f"Ping check failed: {e}")
return False
def monitor_connection(self, server_host="10.76.140.17"):
"""
Continuously monitor server connection and manage WiFi
Args:
server_host: Server hostname/IP to monitor
"""
self.is_running = True
wifi_interface = self.get_wifi_interface()
if not wifi_interface:
self.logger.error("Cannot monitor without WiFi interface")
return
self.logger.info(f"Starting connection monitor for {server_host} on {wifi_interface}")
log_with_server(
f"Connection monitor started for {server_host}",
self.hostname,
self.device_ip
)
while self.is_running:
try:
# Check if server is reachable
if self.check_server_connection(server_host):
if self.consecutive_failures > 0:
self.consecutive_failures = 0
self.logger.info("Server connection restored")
log_with_server("Server connection restored", self.hostname, self.device_ip)
else:
self.consecutive_failures += 1
self.logger.warning(
f"Connection lost: {self.consecutive_failures}/{self.failure_threshold} failures"
)
# If threshold reached, do WiFi recovery
if self.consecutive_failures >= self.failure_threshold:
self.logger.error(
f"Server unreachable for {self.failure_threshold} pings - initiating WiFi recovery"
)
# Perform WiFi recovery
if self.reconnect_wifi(wifi_interface, self.wifi_down_time):
self.logger.info("WiFi recovery completed successfully")
else:
self.logger.error("WiFi recovery failed")
time.sleep(self.check_interval)
except Exception as e:
self.logger.error(f"Error in connection monitor: {e}")
time.sleep(self.check_interval)
def start_monitoring(self, server_host="10.76.140.17"):
"""
Start background monitoring thread
Args:
server_host: Server to monitor
"""
self.monitor_thread = threading.Thread(
target=self.monitor_connection,
args=(server_host,),
daemon=True
)
self.monitor_thread.start()
self.logger.info("WiFi recovery monitor thread started")
def stop_monitoring(self):
"""Stop the monitoring thread"""
self.is_running = False
if self.monitor_thread:
self.monitor_thread.join(timeout=5)
self.logger.info("WiFi recovery monitor stopped")
# Global WiFi recovery manager instance
wifi_recovery_manager = None
def initialize_wifi_recovery(hostname, device_ip, server_host="10.76.140.17"):
"""Initialize and start WiFi recovery monitoring"""
global wifi_recovery_manager
try:
wifi_recovery_manager = WiFiRecoveryManager(
hostname=hostname,
device_ip=device_ip,
check_interval=60,
failure_threshold=5
)
wifi_recovery_manager.start_monitoring(server_host)
logging.info("WiFi recovery initialized")
return wifi_recovery_manager
except Exception as e:
logging.error(f"Failed to initialize WiFi recovery: {e}")
return None