#App version 2.9 - Added configuration mode for "notconfig" devices import os import sys import subprocess import stat import pwd import grp # Import dependency check/install functions from dependency_utils from dependency_utils import install_package_from_wheel, check_and_install_dependencies # Global configuration mode flag CONFIGURATION_MODE = False # Run dependency check before importing anything else try: check_and_install_dependencies({ 'rdm6300': 'rdm6300-0.1.1-py3-none-any.whl', 'gpiozero': None, 'requests': 'requests-2.32.3-py3-none-any.whl', 'aiohttp': 'aiohttp-3.11.18-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl', 'flask': None, 'urllib3': 'urllib3-2.3.0-py3-none-any.whl', 'certifi': 'certifi-2025.1.31-py3-none-any.whl', 'charset_normalizer': 'charset_normalizer-3.4.1-py3-none-any.whl', 'idna': 'idna-3.10-py3-none-any.whl', 'multidict': 'multidict-6.4.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl', 'aiosignal': 'aiosignal-1.3.2-py2.py3-none-any.whl', 'frozenlist': 'frozenlist-1.6.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl', 'attrs': 'attrs-25.3.0-py3-none-any.whl', 'yarl': 'yarl-1.20.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl', 'aiohappyeyeballs': 'aiohappyeyeballs-2.6.1-py3-none-any.whl', 'propcache': 'propcache-0.3.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl' }, "./Files/reposytory") except Exception as e: print(f"Warning: Dependency check failed: {e}") print("Continuing with existing packages...") def safe_import(module_name, package_name=None): """ Safely import a module with error handling """ try: if package_name: module = __import__(package_name) return getattr(module, module_name) else: return __import__(module_name) except ImportError as e: print(f"Warning: Could not import {module_name}: {e}") return None # Now import required modules with fallbacks rdm6300 = safe_import('rdm6300') if rdm6300 is None: print("ERROR: rdm6300 is required for this application to work!") print("Please ensure rdm6300 is installed from the repository.") sys.exit(1) # Import other required modules import time import logging # Try to import GPIO-related modules try: from gpiozero import OutputDevice print("✓ gpiozero imported successfully") except ImportError as e: print(f"Warning: Could not import gpiozero: {e}") print("LED functionality will be disabled") # Create a dummy OutputDevice class class OutputDevice: def __init__(self, pin): self.pin = pin def on(self): print(f"LED {self.pin} would turn ON") def off(self): print(f"LED {self.pin} would turn OFF") from multiprocessing import Process # Import network-related modules try: import requests print("✓ requests imported successfully") except ImportError as e: print(f"ERROR: requests is required: {e}") sys.exit(1) import threading import urllib.parse from datetime import datetime, timedelta import socket import signal # Import async modules try: import aiohttp import asyncio print("✓ aiohttp and asyncio imported successfully") except ImportError as e: print(f"Warning: Could not import aiohttp: {e}") print("Async functionality may be limited") import asyncio # Import Flask for command server try: from flask import Flask, request, jsonify print("✓ Flask imported successfully") FLASK_AVAILABLE = True except ImportError as e: print(f"Warning: Could not import Flask: {e}") print("Command server functionality will be disabled") FLASK_AVAILABLE = False # Create dummy Flask classes class Flask: def __init__(self, name): pass def route(self, *args, **kwargs): def decorator(f): return f return decorator def run(self, *args, **kwargs): pass def request(): pass def jsonify(data): return data import configparser import json def load_config(): """ Load application configuration from data/config.txt (INI format). Falls back to legacy file (device_info.txt) and hardcoded defaults. """ config_path = "./data/config.txt" defaults = { "chrome_url": "http://filesibiusb05.sibiusb.harting.intra/iweb_v2/index.php/traceability/production", "chrome_local_url": "", "chrome_insecure_origin": "http://filesibiusb05.sibiusb.harting.intra", "card_post_base_url": "https://dataswsibiusb01.sibiusb.harting.intra/RO_Quality_PRD/api/record", "server_log_url": "http://rpi-ansible:80/logs", "update_host": "rpi-ansible", "update_user": "pi", "internet_check_host": "filesibiusb05.sibiusb.harting.intra", "work_place": "notconfig", "device_hostname": "unknown-device", "device_ip": "127.0.0.1", "device_location": "", "device_info_reviewed_at": "1970-01-01T00:00:00", "card_presence": "enable", "last_synced": "1970-01-01T00:00:00", } cfg = dict(defaults) parser = configparser.ConfigParser() if os.path.exists(config_path): try: parser.read(config_path) if parser.has_section("chrome"): if parser.has_option("chrome", "chrome_url"): cfg["chrome_url"] = parser.get("chrome", "chrome_url") if parser.has_option("chrome", "chrome_local_url"): cfg["chrome_local_url"] = parser.get("chrome", "chrome_local_url") if parser.has_option("chrome", "chrome_insecure_origin"): cfg["chrome_insecure_origin"] = parser.get("chrome", "chrome_insecure_origin") if parser.has_section("card_api"): if parser.has_option("card_api", "base_url"): cfg["card_post_base_url"] = parser.get("card_api", "base_url") if parser.has_section("server"): if parser.has_option("server", "log_url"): cfg["server_log_url"] = parser.get("server", "log_url") if parser.has_option("server", "update_host"): cfg["update_host"] = parser.get("server", "update_host") if parser.has_option("server", "update_user"): cfg["update_user"] = parser.get("server", "update_user") if parser.has_option("server", "internet_check_host"): cfg["internet_check_host"] = parser.get("server", "internet_check_host") if parser.has_section("device"): if parser.has_option("device", "work_place"): cfg["work_place"] = parser.get("device", "work_place") elif parser.has_option("device", "name"): cfg["work_place"] = parser.get("device", "name") if parser.has_option("device", "hostname"): cfg["device_hostname"] = parser.get("device", "hostname") if parser.has_option("device", "ip"): cfg["device_ip"] = parser.get("device", "ip") if parser.has_option("device", "info_reviewed_at"): cfg["device_info_reviewed_at"] = parser.get("device", "info_reviewed_at") if parser.has_option("device", "location"): cfg["device_location"] = parser.get("device", "location") if parser.has_option("device", "card_presence"): cfg["card_presence"] = parser.get("device", "card_presence") if parser.has_section("meta"): if parser.has_option("meta", "last_synced"): cfg["last_synced"] = parser.get("meta", "last_synced") print(f"\u2713 Configuration loaded from {config_path}") except Exception as e: print(f"Warning: Could not parse {config_path}: {e}. Using defaults.") else: # Fall back to legacy individual files try: with open("./data/device_info.txt", "r") as f: lines = f.read().strip().split('\n') if len(lines) >= 1 and lines[0].strip(): cfg["device_hostname"] = lines[0].strip() if len(lines) >= 2 and lines[1].strip(): cfg["device_ip"] = lines[1].strip() except Exception: pass print("config.txt not found - using legacy files and defaults") return cfg # --------------------------------------------------------------------------- # Server config sync (runs once at startup) # --------------------------------------------------------------------------- def _get_mac_address(): """Return the MAC address of the primary network interface.""" for iface in ['eth0', 'wlan0', 'eth1']: mac_path = f'/sys/class/net/{iface}/address' try: if os.path.exists(mac_path): with open(mac_path) as f: mac = f.read().strip() if mac and mac != '00:00:00:00:00:00': return mac except Exception: continue # Fallback: derive from UUID node import uuid as _uuid raw = _uuid.getnode() return ':'.join(f'{(raw >> i) & 0xff:02x}' for i in range(40, -1, -8)) def _write_config_from_server(new_cfg): """Overwrite data/config.txt with config received from the server.""" import configparser as _cp p = _cp.ConfigParser() p.add_section("chrome") p.set("chrome", "chrome_url", new_cfg.get("chrome_url", "")) p.set("chrome", "chrome_local_url", new_cfg.get("chrome_local_url", "")) p.set("chrome", "chrome_insecure_origin", new_cfg.get("chrome_insecure_origin", "")) p.add_section("card_api") p.set("card_api", "base_url", new_cfg.get("card_api_base_url", "")) p.add_section("server") p.set("server", "log_url", new_cfg.get("server_log_url", "")) p.set("server", "update_host", new_cfg.get("update_host", "")) p.set("server", "update_user", new_cfg.get("update_user", "")) p.set("server", "internet_check_host", new_cfg.get("internet_check_host", "")) p.add_section("device") # Only overwrite work_place if the server provides a non-empty value. # This prevents a freshly migrated device from losing its idmasa-sourced # work_place on first startup when the server doesn't know the device yet. server_device_name = new_cfg.get("device_name", "").strip() local_work_place = APP_CONFIG.get("work_place", "").strip() effective_work_place = server_device_name if server_device_name else (local_work_place or "notconfig") p.set("device", "work_place", effective_work_place) # Same guard for hostname: prefer server value, fall back to OS hostname, # then whatever was in local config. Never blank it. server_hostname = new_cfg.get("hostname", "").strip() if not server_hostname: try: server_hostname = socket.gethostname() except Exception: server_hostname = APP_CONFIG.get("device_hostname", "").strip() p.set("device", "hostname", server_hostname) p.set("device", "ip", new_cfg.get("device_ip", "")) p.set("device", "location", new_cfg.get("location") or new_cfg.get("device_location", "")) p.set("device", "info_reviewed_at", new_cfg.get("info_reviewed_at") or "1970-01-01T00:00:00") p.set("device", "card_presence", new_cfg.get("card_presence", "enable")) p.add_section("meta") sync_ts = new_cfg.get("config_updated_at") or datetime.now().isoformat() p.set("meta", "last_synced", sync_ts) os.makedirs("./data", exist_ok=True) with open("./data/config.txt", "w") as f: f.write("# WMT Application Configuration\n") f.write(f"# Synced from server: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n\n") p.write(f) print(f"\u2713 config.txt written from server (server ts: {sync_ts})") def sync_config_with_server(): """ Called once at startup. - Derives base URL from server_log_url in APP_CONFIG. - If server config is newer than last_synced → pull and overwrite config.txt, reload APP_CONFIG. - Otherwise → send a device-info update request to the server. Returns True if config was pulled from server. """ global APP_CONFIG try: import requests as _req import urllib.parse as _up parsed = _up.urlparse(APP_CONFIG.get("server_log_url", "http://rpi-ansible:80/logs")) server_base = f"{parsed.scheme}://{parsed.netloc}" mac = _get_mac_address() last_synced_str = APP_CONFIG.get("last_synced", "1970-01-01T00:00:00") try: last_synced = datetime.fromisoformat(last_synced_str) except Exception: last_synced = datetime(1970, 1, 1) local_info_reviewed_str = APP_CONFIG.get("device_info_reviewed_at", "1970-01-01T00:00:00") try: local_info_reviewed = datetime.fromisoformat(local_info_reviewed_str) except Exception: local_info_reviewed = datetime(1970, 1, 1) print(f"Checking server config (MAC={mac}, last_synced={last_synced_str}, info_reviewed_at={local_info_reviewed_str}) ...") # --- Step 1: get server timestamp --- ts_resp = _req.get( f"{server_base}/api/wmt/config/timestamp", params={"mac": mac}, timeout=5 ) ts_resp.raise_for_status() ts_data = ts_resp.json() server_ts_str = ts_data.get("latest_updated_at", "1970-01-01T00:00:00") try: server_ts = datetime.fromisoformat(server_ts_str) except Exception: server_ts = datetime(1970, 1, 1) server_info_reviewed_str = ts_data.get("device_info_reviewed_at") or "1970-01-01T00:00:00" try: server_info_reviewed = datetime.fromisoformat(server_info_reviewed_str) except Exception: server_info_reviewed = datetime(1970, 1, 1) # Pull if global settings are newer OR if admin has reviewed device info more recently needs_pull = server_ts > last_synced or server_info_reviewed > local_info_reviewed if needs_pull: # --- Step 2a: pull config --- print(f"Server config is newer ({server_ts_str}), pulling ...") cfg_resp = _req.get(f"{server_base}/api/wmt/config/{mac}", timeout=5) cfg_resp.raise_for_status() _write_config_from_server(cfg_resp.json()) APP_CONFIG = load_config() print("\u2705 Config synced from server.") return True else: # --- Step 2b: push device info --- print("Local config is current, sending device info to server ...") try: _hostname = socket.gethostname() _ip = socket.gethostbyname(_hostname) except Exception: _hostname = APP_CONFIG.get("device_hostname", "") _ip = APP_CONFIG.get("device_ip", "") _req.post( f"{server_base}/api/wmt/config/update_request", json={ "mac_address": mac, "device_name": APP_CONFIG.get("work_place", ""), "hostname": _hostname, "device_ip": _ip, "client_config_mtime": last_synced_str, "client_info_reviewed_at": local_info_reviewed_str, "card_presence": APP_CONFIG.get("card_presence", "enable"), }, timeout=5, ) print("\u2705 Device info update request sent.") return False except Exception as e: print(f"Config sync skipped (server unreachable or error): {e}") return False # Load global application configuration APP_CONFIG = load_config() # Attempt to sync with server at startup (non-blocking – failures are logged and ignored) sync_config_with_server() # --------------------------------------------------------------------------- # Card presence flag – controls whether RFID functions are started # --------------------------------------------------------------------------- CARD_PRESENCE_ENABLED = APP_CONFIG.get("card_presence", "enable").strip().lower() == "enable" print(f"Card presence: {'ENABLED' if CARD_PRESENCE_ENABLED else 'DISABLED'}") # Early configuration mode detection (before heavy initialization) def early_launch_configuration_mode(): """ Early launch of configuration mode with chromium displaying Screen.html """ try: print("🔧 Configuration mode detected - launching Screen.html in Chromium") # Path to Screen.html relative to the WMT working directory screen_html_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "Files", "Screen.html") screen_html_relative = "./Files/Screen.html" if os.path.exists(screen_html_relative): screen_html_path = os.path.abspath(screen_html_relative) if not os.path.exists(screen_html_path): print(f"❌ Screen.html not found at: {screen_html_path}") return False print(f"📄 Loading Screen.html from: {screen_html_path}") # Terminate any existing Chromium processes chromium_process_name = "chromium" # Local log only in configuration mode logging.info("Refreshing Chromium process (configuration mode)") try: subprocess.run(["pkill", "-f", chromium_process_name], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) time.sleep(5) # Wait for processes to terminate # Launch Chromium with Screen.html url = f"file://{screen_html_path}" subprocess.Popen( ["chromium", "--test-type", "--noerrors", "--kiosk", "--start-fullscreen", url], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, stdin=subprocess.DEVNULL, start_new_session=True ) logging.info("Chromium process restarted successfully (configuration mode)") except Exception as e: logging.error(f"Failed to refresh Chromium process (configuration mode): {e}") return False print("✅ Configuration mode launched successfully") print("ℹ️ Network connectivity checks disabled") print("ℹ️ Server status messages disabled") print("🔧 Device running in configuration mode") print("🔧 To exit configuration mode, change work_place in config.txt") return True except FileNotFoundError: print("❌ Chromium browser not found. Please install chromium-browser:") print(" sudo apt update && sudo apt install chromium-browser") return False except Exception as e: print(f"❌ Error launching configuration mode: {e}") return False # EARLY CONFIGURATION MODE DETECTION print("=" * 60) print("CONFIGURATION MODE DETECTION") print("=" * 60) def _try_autoconfigure_from_prezenta(): """ If work_place is still 'notconfig', look for the idmasa.txt left behind by the migration playbook in Prezenta_Old_Data and use it to self-configure. Updates config.txt and reloads APP_CONFIG. Returns True if a valid work_place was found and written. """ global APP_CONFIG idmasa_candidates = [ "/home/pi/Desktop/Prezenta_Old_Data/data/idmasa.txt", "/home/pi/Desktop/Prezenta_Old_Data/Files/idmasa.txt", "/home/pi/Desktop/Prezenta/data/idmasa.txt", "/home/pi/Desktop/Prezenta/Files/idmasa.txt", ] for path in idmasa_candidates: if os.path.exists(path): try: with open(path, "r") as f: value = f.read().strip() if value and value.lower() != "notconfig": print(f"✅ Auto-configure: found work_place='{value}' in {path}") # Write into config.txt [device] section import configparser as _cp p = _cp.ConfigParser() p.read("./data/config.txt") if not p.has_section("device"): p.add_section("device") p.set("device", "work_place", value) # Also capture real hostname/IP try: _hn = socket.gethostname() _ip = socket.gethostbyname(_hn) except Exception: _hn = APP_CONFIG.get("device_hostname", "") _ip = APP_CONFIG.get("device_ip", "") p.set("device", "hostname", _hn) p.set("device", "ip", _ip) os.makedirs("./data", exist_ok=True) with open("./data/config.txt", "w") as f: f.write("# WMT Application Configuration\n") f.write(f"# Auto-configured from {path}\n\n") p.write(f) APP_CONFIG = load_config() print(f"✅ config.txt updated: work_place={value}, hostname={_hn}, ip={_ip}") return True except Exception as e: print(f"Warning: could not read {path}: {e}") return False def _send_first_registration_log(): """ Send device info to the server immediately after auto-configuration so the server record is created before the periodic sync runs. """ try: import requests as _req import urllib.parse as _up parsed = _up.urlparse(APP_CONFIG.get("server_log_url", "http://rpi-ansible:80/logs")) server_base = f"{parsed.scheme}://{parsed.netloc}" mac = _get_mac_address() try: _hn = socket.gethostname() _ip = socket.gethostbyname(_hn) except Exception: _hn = APP_CONFIG.get("device_hostname", "") _ip = APP_CONFIG.get("device_ip", "") _req.post( f"{server_base}/api/wmt/config/update_request", json={ "mac_address": mac, "device_name": APP_CONFIG.get("work_place", ""), "hostname": _hn, "device_ip": _ip, "card_presence": APP_CONFIG.get("card_presence", "enable"), "client_config_mtime": APP_CONFIG.get("last_synced", "1970-01-01T00:00:00"), }, timeout=5, ) print(f"✅ Registration sent to server (work_place={APP_CONFIG.get('work_place')}, hostname={_hn})") except Exception as e: print(f"Warning: could not send first registration to server: {e}") try: name = APP_CONFIG.get("work_place", "noconfig") print(f"✓ Device name loaded: {name}") # Check if device is in configuration mode if name.lower() == "notconfig": print("🔧 Device configured for setup mode (notconfig)") # --- Try to self-configure from Prezenta_Old_Data before entering config mode --- if _try_autoconfigure_from_prezenta(): name = APP_CONFIG.get("work_place", "notconfig") print(f"✅ Auto-configuration successful — work_place='{name}', skipping config mode") _send_first_registration_log() CONFIGURATION_MODE = False else: # No idmasa.txt found — fall through to manual configuration mode if early_launch_configuration_mode(): print("🚀 Configuration mode active - application will run in setup mode") print("⚠️ Network connectivity checks are DISABLED") print("⚠️ Server status messages are DISABLED") print("🔧 Change config.txt [device] name to exit configuration mode") CONFIGURATION_MODE = True print("🔧 Configuration mode activated - continuing with RFID reader initialization") print("🔧 Network and server monitoring will remain disabled") else: print("❌ Failed to launch configuration mode, continuing with normal operation") CONFIGURATION_MODE = False else: print("✅ Device in normal operation mode") CONFIGURATION_MODE = False except Exception as e: print(f"Error in configuration mode detection: {e}") name = "noconfig" CONFIGURATION_MODE = False print("=" * 60) print("CONTINUING WITH NORMAL INITIALIZATION" if not CONFIGURATION_MODE else "CONFIGURATION MODE ACTIVE") print("=" * 60) # --------------------------------------------------------------------------- # Auto-printer setup # --------------------------------------------------------------------------- def _ensure_printer(device_name): """ Check whether a CUPS printer named *device_name* is installed. If not found, install it automatically using lpadmin. Only called when work_place is set to a real name (not 'notconfig'). """ print(f"🖨 Checking printer for work_place='{device_name}' ...") try: import cups conn = cups.Connection() printers = conn.getPrinters() if device_name in printers: print(f"✓ Printer '{device_name}' already installed — no action needed.") return # Printer not found — install it print(f"🖨 Printer '{device_name}' not found. Starting installation ...") cmd = [ "sudo", "/usr/sbin/lpadmin", "-p", device_name, "-E", "-v", "usb://CITIZEN/CT-S310II?serial=00000000", "-m", "CTS310II.ppd", ] result = subprocess.run(cmd, capture_output=True, text=True, timeout=30) if result.returncode == 0: print(f"✓ Printer '{device_name}' installed successfully.") else: print(f"✗ lpadmin returned code {result.returncode}: {result.stderr.strip()}") except ImportError: print("⚠ python3-cups not available — printer check skipped. " "Install with: sudo apt install python3-cups") except Exception as e: print(f"⚠ Printer setup skipped: {e}") # Run auto-printer setup only when device has a real work_place _auto_printer_name = APP_CONFIG.get("work_place", "notconfig") if not CONFIGURATION_MODE and _auto_printer_name.lower() != "notconfig": _ensure_printer(_auto_printer_name) else: if _auto_printer_name.lower() == "notconfig": print("🖨 Printer setup skipped (work_place=notconfig).") def check_system_requirements(): """ Check and set up system requirements for the application """ print("Checking system requirements...") # 1. Check and install required system packages system_packages = { 'sshpass': 'sshpass_1.09-1_armhf.deb' # Required for auto-update functionality } for package, deb_file in system_packages.items(): try: result = subprocess.run(['which', package], capture_output=True, text=True) if result.returncode == 0: print(f"✓ {package} is installed") else: print(f"Installing {package}...") # Try online installation first try: install_result = subprocess.run(['sudo', 'apt', 'update'], capture_output=True, text=True, timeout=120) install_result = subprocess.run(['sudo', 'apt', 'install', '-y', package], capture_output=True, text=True, timeout=300) if install_result.returncode == 0: print(f"✓ {package} installed successfully (online)") continue else: print(f"Online installation failed, trying offline...") except Exception as online_error: print(f"Online installation failed: {online_error}, trying offline...") # Try offline installation from local .deb file deb_path = f"./Files/system_packages/{deb_file}" if os.path.exists(deb_path): try: print(f"Installing {package} from local package: {deb_path}") offline_result = subprocess.run(['sudo', 'dpkg', '-i', deb_path], capture_output=True, text=True, timeout=120) if offline_result.returncode == 0: print(f"✓ {package} installed successfully (offline)") else: print(f"✗ Offline installation failed: {offline_result.stderr}") # Try to fix dependencies print("Attempting to fix dependencies...") subprocess.run(['sudo', 'apt', '--fix-broken', 'install', '-y'], capture_output=True, text=True, timeout=300) except Exception as offline_error: print(f"✗ Offline installation error: {offline_error}") else: print(f"✗ Local package not found: {deb_path}") print(f" To add offline support, download with: apt download {package}") except Exception as e: print(f"Warning: Could not check/install {package}: {e}") # 2. Check and create required directories required_dirs = ['./data', './Files', './Files/reposytory', './Files/system_packages'] for dir_path in required_dirs: try: os.makedirs(dir_path, exist_ok=True) print(f"✓ Directory ensured: {dir_path}") except Exception as e: print(f"✗ Failed to create directory {dir_path}: {e}") return False # 2. Check required files and create defaults if missing required_files = { './data/log.txt': '', './data/tag.txt': '', './data/device_info.txt': 'unknown-device\n127.0.0.1\n' } for file_path, default_content in required_files.items(): try: if not os.path.exists(file_path): with open(file_path, 'w') as f: f.write(default_content) print(f"✓ Created default file: {file_path}") else: print(f"✓ File exists: {file_path}") except Exception as e: print(f"✗ Failed to create file {file_path}: {e}") # 3. Check file permissions try: for file_path in required_files.keys(): if os.path.exists(file_path): # Ensure read/write permissions for the application os.chmod(file_path, stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP | stat.S_IROTH) print("✓ File permissions set correctly") except Exception as e: print(f"Warning: Could not set file permissions: {e}") return True def check_port_capabilities(): """ Check if the application can bind to port 80 and set up capabilities if needed """ print("Checking port 80 capabilities...") try: # Check if we're running as root if os.geteuid() == 0: print("✓ Running as root - port 80 access available") return True # Check if capabilities are set python_path = sys.executable result = subprocess.run(['getcap', python_path], capture_output=True, text=True) if 'cap_net_bind_service=ep' in result.stdout: print("✓ Port binding capabilities already set") return True # Try to set capabilities print("Setting up port 80 binding capabilities...") setup_script = './setup_port_capability.sh' if os.path.exists(setup_script): result = subprocess.run(['sudo', 'bash', setup_script], capture_output=True, text=True) if result.returncode == 0: print("✓ Port capabilities set successfully") return True else: print(f"✗ Failed to set capabilities: {result.stderr}") else: # Create the setup script if it doesn't exist script_content = f'''#!/bin/bash # Set port binding capability for Python to allow port 80 access echo "Setting port binding capability for Python..." sudo setcap cap_net_bind_service=ep {python_path} echo "Capability set successfully" ''' try: with open(setup_script, 'w') as f: f.write(script_content) os.chmod(setup_script, stat.S_IRWXU | stat.S_IRGRP | stat.S_IROTH) result = subprocess.run(['sudo', 'bash', setup_script], capture_output=True, text=True) if result.returncode == 0: print("✓ Port capabilities set successfully") return True else: print(f"✗ Failed to set capabilities: {result.stderr}") except Exception as e: print(f"✗ Failed to create setup script: {e}") except Exception as e: print(f"Warning: Could not check port capabilities: {e}") print("Warning: Port 80 may not be accessible. App will try to run on default port.") return False def check_hardware_interfaces(): """ Check hardware interfaces (UART/Serial) required for RFID reader """ print("Checking hardware interfaces...") # Check for serial devices serial_devices = ['/dev/ttyS0', '/dev/ttyAMA0', '/dev/ttyUSB0'] available_devices = [] for device in serial_devices: if os.path.exists(device): try: # Check if we can access the device with open(device, 'r'): pass available_devices.append(device) print(f"✓ Serial device available: {device}") except PermissionError: print(f"✗ Permission denied for {device}. Adding user to dialout group...") try: # Add current user to dialout group for serial access username = pwd.getpwuid(os.getuid()).pw_name subprocess.run(['sudo', 'usermod', '-a', '-G', 'dialout', username], capture_output=True, text=True) print(f"✓ User {username} added to dialout group (reboot may be required)") available_devices.append(device) except Exception as e: print(f"✗ Failed to add user to dialout group: {e}") except Exception as e: print(f"Warning: Could not test {device}: {e}") if not available_devices: print("✗ No serial devices found. RFID reader may not work.") # Enable UART if we're on Raspberry Pi try: config_file = '/boot/config.txt' if os.path.exists(config_file): print("Attempting to enable UART in Raspberry Pi config...") result = subprocess.run(['sudo', 'raspi-config', 'nonint', 'do_serial', '0'], capture_output=True, text=True) if result.returncode == 0: print("✓ UART enabled in config (reboot required)") else: print("Warning: Could not enable UART automatically") except Exception as e: print(f"Warning: Could not configure UART: {e}") return False return True def check_network_connectivity(): """ Check network connectivity and DNS resolution """ print("Checking network connectivity...") try: # Test basic connectivity result = subprocess.run(['ping', '-c', '1', '8.8.8.8'], capture_output=True, text=True, timeout=5) if result.returncode == 0: print("✓ Internet connectivity available") # Test DNS resolution try: import socket socket.gethostbyname('google.com') print("✓ DNS resolution working") return True except socket.gaierror: print("✗ DNS resolution failed") return False else: print("✗ No internet connectivity") return False except subprocess.TimeoutExpired: print("✗ Network timeout") return False except Exception as e: print(f"Warning: Could not test network: {e}") return False def initialize_gpio_permissions(): """ Set up GPIO permissions for LED control """ print("Setting up GPIO permissions...") try: # Add user to gpio group if it exists username = pwd.getpwuid(os.getuid()).pw_name # Check if gpio group exists try: grp.getgrnam('gpio') subprocess.run(['sudo', 'usermod', '-a', '-G', 'gpio', username], capture_output=True, text=True) print(f"✓ User {username} added to gpio group") except KeyError: print("Warning: gpio group not found - GPIO access may be limited") # Set up GPIO access via /dev/gpiomem if available gpio_devices = ['/dev/gpiomem', '/dev/mem'] for device in gpio_devices: if os.path.exists(device): print(f"✓ GPIO device available: {device}") break else: print("Warning: No GPIO devices found") except Exception as e: print(f"Warning: Could not set up GPIO permissions: {e}") def perform_system_initialization(): """ Perform complete system initialization for first run """ print("=" * 60) print("SYSTEM INITIALIZATION - Preparing for first run") print("=" * 60) initialization_steps = [ ("System Requirements", check_system_requirements), ("Port Capabilities", check_port_capabilities), ("Hardware Interfaces", check_hardware_interfaces), ("GPIO Permissions", initialize_gpio_permissions), ("Network Connectivity", check_network_connectivity) ] success_count = 0 total_steps = len(initialization_steps) for step_name, step_function in initialization_steps: print(f"\n--- {step_name} ---") try: if step_function(): success_count += 1 print(f"✓ {step_name} completed successfully") else: print(f"⚠ {step_name} completed with warnings") except Exception as e: print(f"✗ {step_name} failed: {e}") print("\n" + "=" * 60) print(f"INITIALIZATION COMPLETE: {success_count}/{total_steps} steps successful") print("=" * 60) if success_count < total_steps: print("Warning: Some initialization steps failed. Application may have limited functionality.") print("Check the messages above for details.") return success_count >= (total_steps - 1) # Allow one failure #configurare variabile def get_device_info(): """ Get hostname and device IP with file-based fallback to avoid socket errors """ config_file = "./data/device_info.txt" hostname = None device_ip = None # Try to get current hostname and IP try: hostname = socket.gethostname() device_ip = socket.gethostbyname(hostname) print(f"Successfully resolved - Hostname: {hostname}, IP: {device_ip}") # Save the working values to file for future fallback try: os.makedirs("./data", exist_ok=True) # Create data directory if it doesn't exist with open(config_file, "w") as f: f.write(f"{hostname}\n{device_ip}\n") print(f"Saved device info to {config_file}") except Exception as e: print(f"Warning: Could not save device info to file: {e}") return hostname, device_ip except socket.gaierror as e: print(f"Socket error occurred: {e}") print("Attempting to load device info from file...") # Try to load from file try: with open(config_file, "r") as f: lines = f.read().strip().split('\n') if len(lines) >= 2: hostname = lines[0].strip() device_ip = lines[1].strip() print(f"Loaded from file - Hostname: {hostname}, IP: {device_ip}") return hostname, device_ip else: print("File exists but doesn't contain valid data") except FileNotFoundError: print(f"No fallback file found at {config_file}") except Exception as e: print(f"Error reading fallback file: {e}") except Exception as e: print(f"Unexpected error getting device info: {e}") # Try to load from file as fallback try: with open(config_file, "r") as f: lines = f.read().strip().split('\n') if len(lines) >= 2: hostname = lines[0].strip() device_ip = lines[1].strip() print(f"Loaded from file after error - Hostname: {hostname}, IP: {device_ip}") return hostname, device_ip except Exception as file_error: print(f"Could not load from file: {file_error}") # Final fallback: use values from APP_CONFIG (config.txt [device] section) try: cfg_hostname = APP_CONFIG.get("device_hostname", "") cfg_ip = APP_CONFIG.get("device_ip", "") if cfg_hostname and cfg_hostname != "unknown-device": hostname = cfg_hostname device_ip = cfg_ip or "127.0.0.1" print(f"Loaded from config.txt - Hostname: {hostname}, IP: {device_ip}") return hostname, device_ip except Exception: pass # Absolute last resort defaults print("All methods failed - Using default values") hostname = hostname or "unknown-device" device_ip = "127.0.0.1" # Try to save these default values for next time try: os.makedirs("./data", exist_ok=True) with open(config_file, "w") as f: f.write(f"{hostname}\n{device_ip}\n") print(f"Saved fallback values to {config_file}") except Exception as e: print(f"Could not save fallback values: {e}") return hostname, device_ip # Perform system initialization (first run setup) if not perform_system_initialization(): print("Warning: System initialization completed with errors.") print("The application will continue but may have limited functionality.") # Get device information with error handling hostname, device_ip = get_device_info() print(f"Final result - Hostname: {hostname}, Device IP: {device_ip}") # Configure logging logging.basicConfig(filename='./data/log.txt', level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') # function to delete old logs def delete_old_logs(): log_dir = './data/' log_file = 'log.txt' log_path = os.path.join(log_dir, log_file) if os.path.exists(log_path): file_mod_time = datetime.fromtimestamp(os.path.getmtime(log_path)) if datetime.now() - file_mod_time > timedelta(days=10): os.remove(log_path) log_info_with_server(f"Deleted old log file: {log_file}") else: log_info_with_server(f"Log file is not older than 10 days: {log_file}") else: log_info_with_server(f"Log file does not exist: {log_file}") # Function to read the work place name from config def read_name_from_file(): try: n_masa = APP_CONFIG.get("work_place", "") if n_masa: return n_masa except Exception: pass return "unknown" def _get_os_version() -> str: """Read OS pretty-name from /etc/os-release (Raspberry Pi OS, etc.).""" try: with open("/etc/os-release") as f: for line in f: if line.startswith("PRETTY_NAME="): return line.split("=", 1)[1].strip().strip('"') except Exception: pass return "" # Function to send logs to a remote server for the Server_monitorizare APP def send_log_to_server(log_message, n_masa, hostname, device_ip): try: log_data = { "hostname": str(hostname), "device_ip": str(device_ip), "nume_masa": str(n_masa), "log_message": str(log_message), # Device metadata – keeps the server record up to date automatically "device_type": "Raspberry Pi", "os_version": _get_os_version(), "location": APP_CONFIG.get("device_location", ""), "mac_address": _get_mac_address(), "card_presence": APP_CONFIG.get("card_presence", "enable"), } server_url = APP_CONFIG.get("server_log_url", "http://rpi-ansible:80/logs") print(log_data) # Debugging: Print log_data to verify its contents response = requests.post(server_url, json=log_data, timeout=5) response.raise_for_status() logging.info("Log successfully sent to server: %s", log_message) except requests.exceptions.RequestException as e: logging.error("Failed to send log to server: %s", e) # Wrapper for logging.info to also send logs to the server Monitorizare APP def log_info_with_server(message): n_masa = read_name_from_file() # Read work place name from config formatted_message = f"{message} (n_masa: {n_masa})" # Format the message logging.info(formatted_message) # Log the formatted message # Only send to server if not in configuration mode try: if not CONFIGURATION_MODE: send_log_to_server(message, n_masa, hostname, device_ip) # Send the original message to the server else: logging.info("Configuration mode: Server logging disabled") except NameError: # CONFIGURATION_MODE not defined yet (during initialization) send_log_to_server(message, n_masa, hostname, device_ip) # Send the original message to the server # Function to execute system commands with proper security def execute_system_command(command): """ Execute system commands with proper logging and security checks """ # Define allowed commands for security allowed_commands = [ "sudo apt update", "sudo apt upgrade -y", "sudo apt update && sudo apt upgrade -y", # Combined update and upgrade "sudo apt autoremove -y", "sudo apt autoclean", "sudo reboot", "sudo shutdown -h now", "df -h", "free -m", "uptime", "systemctl status", "sudo systemctl restart networking", "sudo systemctl restart ssh" ] try: # Check if command is allowed if command not in allowed_commands: log_info_with_server(f"Command '{command}' is not allowed for security reasons") return {"status": "error", "message": f"Command '{command}' is not allowed", "output": ""} log_info_with_server(f"Executing command: {command}") # Execute the command result = subprocess.run( command.split(), capture_output=True, text=True, timeout=300 # 5 minute timeout ) output = result.stdout + result.stderr if result.returncode == 0: log_info_with_server(f"Command '{command}' executed successfully") return {"status": "success", "message": "Command executed successfully", "output": output} else: log_info_with_server(f"Command '{command}' failed with return code {result.returncode}") return {"status": "error", "message": f"Command failed with return code {result.returncode}", "output": output} except subprocess.TimeoutExpired: log_info_with_server(f"Command '{command}' timed out") return {"status": "error", "message": "Command timed out", "output": ""} except Exception as e: log_info_with_server(f"Error executing command '{command}': {str(e)}") return {"status": "error", "message": f"Error: {str(e)}", "output": ""} # Flask app for receiving commands (only if Flask is available) if FLASK_AVAILABLE: command_app = Flask(__name__) @command_app.route('/execute_command', methods=['POST']) def handle_command_execution(): """ Endpoint to receive and execute system commands """ try: data = request.json if not data or 'command' not in data: return jsonify({"error": "Invalid request. 'command' field is required"}), 400 command = data.get('command') # Execute the command result = execute_system_command(command) return jsonify(result), 200 if result['status'] == 'success' else 400 except Exception as e: log_info_with_server(f"Error handling command execution request: {str(e)}") return jsonify({"error": f"Server error: {str(e)}"}), 500 @command_app.route('/status', methods=['GET']) def get_device_status(): """ Endpoint to get device status information """ try: n_masa = read_name_from_file() # Get system information uptime_result = subprocess.run(['uptime'], capture_output=True, text=True) df_result = subprocess.run(['df', '-h', '/'], capture_output=True, text=True) free_result = subprocess.run(['free', '-m'], capture_output=True, text=True) status_info = { "hostname": hostname, "device_ip": device_ip, "nume_masa": n_masa, "timestamp": datetime.now().strftime('%Y-%m-%d %H:%M:%S'), "uptime": uptime_result.stdout.strip() if uptime_result.returncode == 0 else "N/A", "disk_usage": df_result.stdout.strip() if df_result.returncode == 0 else "N/A", "memory_usage": free_result.stdout.strip() if free_result.returncode == 0 else "N/A" } return jsonify(status_info), 200 except Exception as e: log_info_with_server(f"Error getting device status: {str(e)}") return jsonify({"error": f"Error getting status: {str(e)}"}), 500 @command_app.route('/auto_update', methods=['POST']) def auto_update_app(): """ Trigger an immediate WMT client update check against the monitoring server. Delegates to _check_and_apply_update(). """ result = _check_and_apply_update() if result.get('updated'): return jsonify({"status": "success", "message": result['message']}), 200 elif result.get('error'): return jsonify({"error": result['message']}), 500 else: return jsonify({"status": "no_update", "message": result['message']}), 200 @command_app.route('/update_config', methods=['POST']) def update_config_endpoint(): """ Update configuration from Server_Monitorizare_v2. Accepts a JSON body with sections matching config.txt structure. Example body: {"chrome": {"chrome_url": "http://..."}} """ global APP_CONFIG ALLOWED_SECTIONS = { "chrome": ["chrome_url", "chrome_local_url", "chrome_insecure_origin"], "card_api": ["base_url"], "server": ["log_url", "update_host", "update_user", "internet_check_host"], "device": ["name", "hostname", "ip"], } try: data = request.json if not data: return jsonify({"error": "JSON body required"}), 400 config_path = "./data/config.txt" parser = configparser.ConfigParser() if os.path.exists(config_path): parser.read(config_path) updated_keys = [] for section, allowed_keys in ALLOWED_SECTIONS.items(): if section in data and isinstance(data[section], dict): if not parser.has_section(section): parser.add_section(section) for key in allowed_keys: if key in data[section]: parser.set(section, key, str(data[section][key])) updated_keys.append(f"{section}.{key}") os.makedirs("./data", exist_ok=True) with open(config_path, "w") as f: f.write("# WMT Application Configuration\n") f.write(f"# Last updated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n\n") parser.write(f) APP_CONFIG = load_config() log_info_with_server(f"Configuration updated via API: {updated_keys}") return jsonify({"status": "success", "updated_keys": updated_keys}), 200 except Exception as e: log_info_with_server(f"Error updating config: {str(e)}") return jsonify({"error": f"Failed to update configuration: {str(e)}"}), 500 @command_app.route('/reload_config', methods=['POST']) def reload_config_endpoint(): """ Reload configuration from data/config.txt into memory without restarting. """ global APP_CONFIG try: APP_CONFIG = load_config() log_info_with_server("Configuration reloaded via API") return jsonify({ "status": "success", "message": "Configuration reloaded", "chrome_url": APP_CONFIG.get("chrome_url"), "card_post_base_url": APP_CONFIG.get("card_post_base_url"), "server_log_url": APP_CONFIG.get("server_log_url"), "device_name": APP_CONFIG.get("work_place"), }), 200 except Exception as e: return jsonify({"error": f"Failed to reload config: {str(e)}"}), 500 def start_command_server(): """ Start the Flask server with enhanced port handling and fallback """ # Try different ports in order of preference preferred_ports = [ int(os.environ.get('FLASK_PORT', 80)), # Use environment variable or default to 80 80, # Standard HTTP port 5000, # Flask default 8080, # Alternative HTTP port 3000 # Development port ] for port in preferred_ports: try: print(f"Attempting to start command 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 command server...") command_app.run(host='0.0.0.0', port=port, debug=False, use_reloader=False) return # Success, exit function 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: print(f"✗ Failed to start on port {port}: {e}") continue # If we get here, all ports failed log_info_with_server("Error: Could not start command server on any port") print("✗ Could not start command server on any available port") # Start command server in a separate process with enhanced error handling try: print("Initializing command server...") command_server_process = Process(target=start_command_server) command_server_process.daemon = True # Ensure it dies with main process command_server_process.start() # Give the server a moment to start and check if it's running import time time.sleep(2) if command_server_process.is_alive(): port = int(os.environ.get('FLASK_PORT', 80)) print(f"✓ Command server started successfully on port {port}") else: print("Warning: Command server process stopped unexpectedly") except Exception as e: print(f"Warning: Could not start command server: {e}") log_info_with_server(f"Command server startup error: {str(e)}") else: print("Warning: Flask not available - Command server disabled") # Call the function to delete old logs delete_old_logs() def config(): import config # function for posting data to the harting server def post_backup_data(): try: with open("./data/tag.txt", "r") as file: lines = file.readlines() remaining_lines = lines[:] for line in lines: line = line.strip() if line: try: response = requests.post(line, verify=False, timeout=3) # response.raise_for_status() # Raise an error for bad status codes log_info_with_server(f"Data posted successfully:") remaining_lines.remove(line + "\n") except requests.exceptions.Timeout: log_info_with_server("Request timed out.") break except requests.exceptions.RequestException as e: log_info_with_server(f"An error occurred: ") break with open("./data/tag.txt", "w") as file: file.writelines(remaining_lines) #log_info_with_server("Backup data updated.") except FileNotFoundError: log_info_with_server("No backup file found.") # Function to check internet connection def check_internet_connection(): hostname = APP_CONFIG.get("internet_check_host", "filesibiusb05.sibiusb.harting.intra") cmd_block_wifi = 'sudo rfkill block wifi' cmd_unblock_wifi = 'sudo rfkill unblock wifi' log_info_with_server('Internet connection check loaded') delete_old_logs() chromium_process_name = "chromium" while True: try: # Use subprocess to execute the ping command response = subprocess.run( ["ping", "-c", "1", hostname], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL ) if response.returncode == 0: log_info_with_server("Internet is up! Waiting 45 minutes.") post_backup_data() time.sleep(2700) # 45 minutes else: log_info_with_server("Internet is down. Rebooting WiFi.") os.system(cmd_block_wifi) time.sleep(1200) # 20 minutes os.system(cmd_unblock_wifi) # Refresh Chromium process log_info_with_server("Refreshing Chromium process.") try: # Find and terminate Chromium processes subprocess.run(["pkill", "-f", chromium_process_name], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) time.sleep(5) # Wait for processes to terminate # Relaunch Chromium url = APP_CONFIG.get("chrome_url", "http://filesibiusb05.sibiusb.harting.intra/iweb_v2/index.php/traceability/production") chrome_insecure = APP_CONFIG.get("chrome_insecure_origin", "http://filesibiusb05.sibiusb.harting.intra") subprocess.Popen( ["chromium", "--test-type", "--noerrors", "--kiosk", "--start-fullscreen", f"--unsafely-treat-insecure-origin-as-secure={chrome_insecure}", url], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, stdin=subprocess.DEVNULL, start_new_session=True ) log_info_with_server("Chromium process restarted successfully.") except Exception as e: log_info_with_server(f"Failed to refresh Chromium process: {e}") except Exception as e: log_info_with_server(f"An error occurred during internet check: {e}") time.sleep(60) # Retry after 1 minute in case of an error # Start the internet connection check in a separate process (only if not in configuration mode) if not CONFIGURATION_MODE: internet_check_process = Process(target=check_internet_connection) internet_check_process.start() print("✅ Internet connectivity monitoring started") else: print("🔧 Configuration mode: Internet connectivity monitoring DISABLED") # --------------------------------------------------------------------------- # HTTP-based client auto-update helpers # --------------------------------------------------------------------------- def _get_local_version(): """Return local app version as float by reading the first line of this file.""" try: with open(os.path.abspath(__file__), 'r') as f: first_line = f.readline() m = re.search(r'version\s+(\d+\.?\d*)', first_line, re.IGNORECASE) if m: return float(m.group(1)) except Exception: pass return None def _check_and_apply_update(): """ Query the monitoring server for the latest WMT release. If a newer version is available, download the zip, back up app.py, extract into the WMT directory, and schedule a systemd service restart. Returns a dict: {updated, message, error}. """ server_host = APP_CONFIG.get("server_host", "") server_port = APP_CONFIG.get("server_port", "5000") if not server_host: return {"updated": False, "error": False, "message": "server_host not configured – skipping update check"} base_url = f"http://{server_host}:{server_port}" local_version = _get_local_version() try: resp = requests.get(f"{base_url}/api/wmt/client/version", timeout=10) if resp.status_code != 200: return {"updated": False, "error": True, "message": f"Version endpoint returned {resp.status_code}"} meta = resp.json() server_version = float(meta.get("version", 0)) except Exception as e: return {"updated": False, "error": True, "message": f"Could not reach version endpoint: {e}"} if local_version is not None and server_version <= local_version: return {"updated": False, "error": False, "message": f"Already on latest version {local_version}"} log_info_with_server(f"WMT update available: local={local_version} server={server_version} – downloading …") wmt_dir = os.path.dirname(os.path.abspath(__file__)) tmp_zip = "/tmp/wmt_update.zip" app_py = os.path.join(wmt_dir, "app.py") try: # Download the zip dl = requests.get(f"{base_url}/api/wmt/client/download", timeout=60, stream=True) if dl.status_code != 200: return {"updated": False, "error": True, "message": f"Download endpoint returned {dl.status_code}"} with open(tmp_zip, 'wb') as f: for chunk in dl.iter_content(chunk_size=65536): if chunk: f.write(chunk) # Validate the zip import zipfile if not zipfile.is_zipfile(tmp_zip): return {"updated": False, "error": True, "message": "Downloaded file is not a valid zip"} # Backup current app.py bak = f"{app_py}.bak.{local_version or 'old'}" try: import shutil shutil.copy2(app_py, bak) except Exception as e: log_info_with_server(f"Warning: could not back up app.py: {e}") # Extract into WMT directory – skip anything inside data/ to preserve device config with zipfile.ZipFile(tmp_zip, 'r') as zf: for member in zf.infolist(): # Normalise path separators and skip the data folder member_path = member.filename.replace('\\', '/') if member_path.startswith('data/') or member_path == 'data': continue zf.extract(member, wmt_dir) log_info_with_server(f"WMT updated to version {server_version} – scheduling service restart") # Schedule restart via systemd (non-blocking) subprocess.Popen( ["bash", "-c", "sleep 3 && sudo systemctl restart wmt"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, stdin=subprocess.DEVNULL, start_new_session=True ) return {"updated": True, "error": False, "message": f"Updated from {local_version} to {server_version}. Restarting service …"} except Exception as e: log_info_with_server(f"WMT auto-update failed: {e}") return {"updated": False, "error": True, "message": str(e)} finally: try: os.remove(tmp_zip) except OSError: pass # --------------------------------------------------------------------------- # Periodic server config sync (background thread) # --------------------------------------------------------------------------- def _periodic_config_sync(): """ Background thread: re-checks the server for config updates every 5 minutes. When a new config is pulled, Chromium is restarted with the updated URLs. """ sync_interval = 300 # seconds (5 minutes) while True: time.sleep(sync_interval) try: config_changed = sync_config_with_server() if config_changed and not CONFIGURATION_MODE: print("🔄 Config changed – restarting Chromium with updated settings.") try: subprocess.run(["pkill", "-f", "chromium"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) time.sleep(5) _url = APP_CONFIG.get("chrome_url", "") _insecure = APP_CONFIG.get("chrome_insecure_origin", "") subprocess.Popen( ["chromium", "--test-type", "--noerrors", "--kiosk", "--start-fullscreen", f"--unsafely-treat-insecure-origin-as-secure={_insecure}", _url], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, stdin=subprocess.DEVNULL, start_new_session=True ) print("✅ Chromium restarted with new config.") except Exception as e: print(f"Warning: Could not restart Chromium after config update: {e}") except Exception as e: print(f"Periodic config sync error: {e}") # Check for WMT client update every cycle (non-fatal) try: _check_and_apply_update() except Exception as e: print(f"Periodic update check error: {e}") if not CONFIGURATION_MODE: _sync_thread = threading.Thread(target=_periodic_config_sync, daemon=True, name="config-sync") _sync_thread.start() print("✅ Periodic server config sync started (every 5 min)") # Startup version check (non-blocking) _update_thread = threading.Thread(target=_check_and_apply_update, daemon=True, name="startup-update-check") _update_thread.start() else: print("🔧 Configuration mode: Periodic config sync DISABLED") # Launch Chromium with the specified URLs (only if not in configuration mode) if not CONFIGURATION_MODE: url = APP_CONFIG.get("chrome_url", "http://filesibiusb05.sibiusb.harting.intra/iweb_v2/index.php/traceability/production") chrome_insecure = APP_CONFIG.get("chrome_insecure_origin", "http://filesibiusb05.sibiusb.harting.intra") subprocess.Popen(["chromium", "--test-type", "--noerrors", "--kiosk", "--start-fullscreen", f"--unsafely-treat-insecure-origin-as-secure={chrome_insecure}", url], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, stdin=subprocess.DEVNULL, start_new_session=True) print("✅ Main application browser launched") else: print("🔧 Configuration mode: Main application browser launch DISABLED") info = "0" #function to post info def post_info(info): #log_info_with_server("Starting to post data...") info1 = info.strip() # Remove any leading/trailing whitespace, including newlines try: response = requests.post(info1, verify=False, timeout=3) response.raise_for_status() # Raise an error for bad status codes #log_info_with_server("Data posted successfully") except requests.exceptions.Timeout: with open("./data/tag.txt", "a") as file: # Open in append mode file.write(info) log_info_with_server(f"Value was saved to tag.txt") except requests.exceptions.RequestException as e: with open("./data/tag.txt", "a") as file: # Open in append mode file.write(info) log_info_with_server("Value was saved to tag.txt") async def post_info_async(info): try: # Try to use aiohttp if available if 'aiohttp' in globals(): async with aiohttp.ClientSession() as session: try: async with session.post(info, ssl=False, timeout=3) as response: response_text = await response.text() log_info_with_server(f"Data posted successfully") except asyncio.TimeoutError: with open("./data/tag.txt", "a") as file: file.write(info) except Exception as e: with open("./data/tag.txt", "a") as file: file.write(info) else: # Fallback to synchronous requests if aiohttp not available try: response = requests.post(info.strip(), verify=False, timeout=3) response.raise_for_status() log_info_with_server(f"Data posted successfully (fallback)") except requests.exceptions.Timeout: with open("./data/tag.txt", "a") as file: file.write(info) except requests.exceptions.RequestException as e: with open("./data/tag.txt", "a") as file: file.write(info) except Exception as e: # Final fallback - save to file with open("./data/tag.txt", "a") as file: file.write(info) def post_info_thread(info): try: thread = threading.Thread(target=asyncio.run, args=(post_info_async(info),), daemon=True) thread.start() except Exception as e: # Fallback to synchronous posting if async fails print(f"Async posting failed, using sync fallback: {e}") post_info(info) # Initialize LEDs – only when card reader is active class DummyLED: def __init__(self, pin): self.pin = pin def on(self): pass def off(self): pass if CARD_PRESENCE_ENABLED: try: print("Initializing LED controls...") led1 = OutputDevice(23) led2 = OutputDevice(24) print("✓ LED controls initialized successfully") log_info_with_server("LED controls initialized") except Exception as e: print(f"Warning: Could not initialize LED controls: {e}") print("Creating dummy LED objects - visual feedback will be disabled") led1 = DummyLED(23) led2 = DummyLED(24) log_info_with_server("LED controls using dummy mode") else: print("🚫 Card presence disabled – LED controls skipped") led1 = DummyLED(23) led2 = DummyLED(24) # Initialize table name/ID print("Initializing device configuration...") name = APP_CONFIG.get("work_place", "noconfig") logging.info("LED controls initialized") logging.info("Variabila Id Masa A fost initializata ") print(f"✓ Device name set from config: {name}") if not CONFIGURATION_MODE: log_info_with_server(f"Device name initialized: {name}") logging.info(name) #clasa reader class Reader(rdm6300.BaseReader): global info def card_inserted(self, card): if card.value == 12886709: config() return afisare = time.strftime("%Y-%m-%d&%H:%M:%S") _base = APP_CONFIG.get("card_post_base_url", "https://dataswsibiusb01.sibiusb.harting.intra/RO_Quality_PRD/api/record") date = f'{_base}/{name}/{card.value}/1/{afisare}\n' info = date if name == "noconfig": led1.on() time.sleep(5) led1.off() log_info_with_server(f"card inserted {card} but no") else: post_info_thread(info) led1.on() log_info_with_server(f"card inserted {card}") def card_removed(self, card): if card.value == 12886709: log_info_with_server("Removing Config card") return afisare = time.strftime("%Y-%m-%d&%H:%M:%S") _base = APP_CONFIG.get("card_post_base_url", "https://dataswsibiusb01.sibiusb.harting.intra/RO_Quality_PRD/api/record") date = f'{_base}/{name}/{card.value}/0/{afisare}\n' info = date if name == "noconfig": led1.off() log_info_with_server(f"card removed {card}") else: post_info_thread(info) led1.off() log_info_with_server(f"card removed {card}") # Initialize RFID Reader with comprehensive error handling def initialize_rfid_reader(): """ Initialize RFID reader with multiple device attempts and error handling """ print("Initializing RFID reader...") # List of possible serial devices in order of preference serial_devices = ['/dev/ttyS0', '/dev/ttyAMA0', '/dev/ttyUSB0', '/dev/ttyACM0'] for device in serial_devices: try: print(f"Attempting to initialize RFID reader on {device}...") r = Reader(device) r.start() print(f"✓ RFID reader successfully initialized on {device}") log_info_with_server(f"RFID reader started on {device}") return r except FileNotFoundError: print(f"✗ Device {device} not found") continue except PermissionError: print(f"✗ Permission denied for {device}") print(f" Hint: Try adding user to dialout group: sudo usermod -a -G dialout $USER") continue except Exception as e: print(f"✗ Failed to initialize on {device}: {e}") continue # If we get here, all devices failed print("✗ Could not initialize RFID reader on any device") print("Available solutions:") print(" 1. Check hardware connections") print(" 2. Enable UART: sudo raspi-config -> Interface Options -> Serial") print(" 3. Add user to dialout group: sudo usermod -a -G dialout $USER") print(" 4. Reboot the system after making changes") log_info_with_server("ERROR: RFID reader initialization failed") return None # Start RFID reader only if card_presence is enabled if CARD_PRESENCE_ENABLED: try: rfid_reader = initialize_rfid_reader() if rfid_reader is None: print("WARNING: Application starting without RFID functionality") print("Card reading will not work until RFID reader is properly configured") except Exception as e: print(f"Critical error initializing RFID reader: {e}") log_info_with_server(f"Critical RFID error: {str(e)}") print("Application will start but RFID functionality will be disabled") else: rfid_reader = None print("🚫 Card presence disabled – RFID reader NOT started") log_info_with_server("Card presence is disabled – RFID reader skipped, running without card reader")