Main class for Ragnar. Manages the primary operations of the application.
| 38 | logger = Logger(name="Ragnar.py", level=logging.DEBUG) |
| 39 | |
| 40 | class Ragnar: |
| 41 | """Main class for Ragnar. Manages the primary operations of the application.""" |
| 42 | def __init__(self, shared_data): |
| 43 | self.shared_data = shared_data |
| 44 | self.commentaire_ia = Commentaireia() |
| 45 | self.orchestrator_thread = None |
| 46 | self.orchestrator = None |
| 47 | self.wifi_manager = WiFiManager(shared_data) |
| 48 | |
| 49 | # Set reference to this instance in shared_data for other modules |
| 50 | self.shared_data.ragnar_instance = self |
| 51 | self.shared_data.headless_mode = False |
| 52 | |
| 53 | # Reference to display instance (will be set when display is started) |
| 54 | self.display = None |
| 55 | |
| 56 | # Web portal lifecycle management (stop during wardriving without WiFi) |
| 57 | self._web_portal_lock = threading.Lock() |
| 58 | self._web_thread_ref = None # Reference to current web server thread |
| 59 | |
| 60 | # PiSugar button listener (for Ragnar/Pwnagotchi swap via hardware button) |
| 61 | self.pisugar_listener = None |
| 62 | try: |
| 63 | from pisugar_button import PiSugarButtonListener |
| 64 | self.pisugar_listener = PiSugarButtonListener(shared_data) |
| 65 | except ImportError: |
| 66 | pass |
| 67 | |
| 68 | def run(self): |
| 69 | """Main loop for Ragnar. Waits for Wi-Fi connection and starts Orchestrator.""" |
| 70 | logger.info("=" * 70) |
| 71 | logger.info("RAGNAR MAIN THREAD STARTING") |
| 72 | logger.info("=" * 70) |
| 73 | |
| 74 | # Start PiSugar button listener (if available) |
| 75 | if self.pisugar_listener: |
| 76 | self.pisugar_listener.start() |
| 77 | |
| 78 | # If wardriving-on-boot is enabled, kick wardriving off BEFORE the WiFi |
| 79 | # manager starts. wifi_manager.start() blocks ~5s minimum (longer with |
| 80 | # no AP available), and wardriving doesn't need WiFi connectivity — |
| 81 | # it scans interfaces directly. Running it in a daemon thread also |
| 82 | # lets us parallelize the GPS spin-up with WiFi startup, so the |
| 83 | # e-paper sees `running == True` within ~1s of boot instead of 20+. |
| 84 | wardriving_boot = self.shared_data.config.get('wardriving_on_boot', False) |
| 85 | if wardriving_boot: |
| 86 | logger.info("Wardriving-on-boot enabled. Starting wardriving early, orchestrator will be suppressed.") |
| 87 | # Wardriving and On-Screen Network Diagnostic mode both hijack the |
| 88 | # e-Paper display and the HAT keys, and the diagnostic layer wins the |
| 89 | # render race (see display.py) — leaving Ragnar stuck on the netdiag |
| 90 | # field-test screen even though wardriving is scanning underneath. |
| 91 | # They are mutually exclusive, so if diagnostic mode was left on, |
| 92 | # turn it off before wardriving takes over the panel. |
| 93 | if self.shared_data.config.get('network_diagnostic_mode', False): |
| 94 | logger.info("Disabling On-Screen Network Diagnostic mode: wardriving-on-boot takes the display.") |
| 95 | self.shared_data.config['network_diagnostic_mode'] = False |
| 96 | try: |
| 97 | self.shared_data.save_config() |