diff --git a/repeater/handler_helpers/mesh_cli.py b/repeater/handler_helpers/mesh_cli.py index 78f1c0d..90569c8 100644 --- a/repeater/handler_helpers/mesh_cli.py +++ b/repeater/handler_helpers/mesh_cli.py @@ -63,8 +63,12 @@ class MeshCLI: def _route_command(self, command: str) -> str: + # Help + if command == "help" or command.startswith("help "): + return self._cmd_help(command) + # System commands - if command == "reboot": + elif command == "reboot": return self._cmd_reboot() elif command == "advert": return self._cmd_advert() @@ -131,6 +135,105 @@ class MeshCLI: else: return "Unknown command" + # ==================== Help Command ==================== + + def _cmd_help(self, command: str) -> str: + """Show available commands or detailed help for a specific command.""" + parts = command.split(None, 1) + if len(parts) == 2: + return self._help_detail(parts[1]) + + lines = [ + "=== pyMC CLI Commands ===", + "", + "System:", + " reboot Restart the repeater service", + " advert Send self advertisement", + " clock Show current UTC time", + " clock sync Sync clock (no-op, uses system time)", + " ver Show version info", + " password Change admin password", + " clear stats Clear statistics", + "", + "Get:", + " get name Node name", + " get radio Radio params (freq,bw,sf,cr)", + " get freq Frequency (MHz)", + " get tx TX power", + " get af Airtime factor", + " get repeat Repeat mode (on/off)", + " get lat / get lon GPS coordinates", + " get role Identity role", + " get guest.password Guest password", + " get allow.read.only Read-only access setting", + " get advert.interval Advert interval (minutes)", + " get flood.advert.interval Flood advert interval (hours)", + " get flood.max Max flood hops", + " get rxdelay RX delay base", + " get txdelay TX delay factor", + " get direct.txdelay Direct TX delay factor", + " get multi.acks Multi-ack count", + " get int.thresh Interference threshold", + " get agc.reset.interval AGC reset interval", + "", + "Set: (use 'help set' for details)", + " set ", + "", + "Other:", + " neighbors List neighbors", + " neighbor.remove Remove neighbor by pubkey", + " tempradio ", + " setperm Set ACL permissions", + " log start|stop|erase Logging control", + ] + if self.enable_regions: + lines.append(" region ... Region commands") + lines += ["", "Type 'help ' for details on a specific command."] + return "\n".join(lines) + + def _help_detail(self, topic: str) -> str: + """Return detailed help for a specific command topic.""" + topic = topic.strip() + details = { + "set": ( + "Set commands \u2014 set :\n" + " set name Set node name\n" + " set radio Set radio (restart required)\n" + " set freq Set frequency (restart required)\n" + " set tx Set TX power\n" + " set af Airtime factor\n" + " set repeat on|off Enable/disable repeating\n" + " set lat Latitude\n" + " set lon Longitude\n" + " set guest.password Guest password\n" + " set allow.read.only on|off Read-only access\n" + " set advert.interval 60-240 minutes\n" + " set flood.advert.interval
3-48 hours\n" + " set flood.max Max flood hops (max 64)\n" + " set rxdelay RX delay base (>=0)\n" + " set txdelay TX delay factor (>=0)\n" + " set direct.txdelay Direct TX delay (>=0)\n" + " set multi.acks Multi-ack count\n" + " set int.thresh Interference threshold\n" + " set agc.reset.interval AGC reset (rounded to x4)" + ), + "get": "Get commands \u2014 type 'help' to see all 'get' parameters.", + "reboot": "Restart the repeater service via systemd.", + "advert": "Trigger a self-advertisement flood packet.", + "clock": "'clock' shows UTC time. 'clock sync' is a no-op (system time used).", + "ver": "Show repeater version and identity type.", + "password": "password \u2014 Change the admin password.", + "tempradio": ( + "tempradio \n" + " Apply temporary radio parameters that revert after timeout.\n" + " freq: 300-2500 MHz, bw: 7-500 kHz, sf: 5-12, cr: 5-8" + ), + "neighbors": "List known neighbor nodes from the routing table.", + "setperm": "setperm \u2014 Set ACL permissions for a node.", + "log": "log start|stop|erase \u2014 Control logging.", + } + return details.get(topic, f"No detailed help for '{topic}'. Type 'help' for command list.") + # ==================== System Commands ==================== def _cmd_reboot(self) -> str: @@ -159,7 +262,18 @@ class MeshCLI: await asyncio.sleep(1.5) await self.send_advert_callback() - asyncio.create_task(delayed_advert()) + # Schedule on the running event loop (may be called from a non-async thread) + try: + loop = asyncio.get_running_loop() + loop.create_task(delayed_advert()) + except RuntimeError: + # Called from a sync context (e.g. CherryPy HTTP thread) + loop = asyncio.get_event_loop_policy().get_event_loop() + if loop.is_running(): + loop.call_soon_threadsafe(asyncio.ensure_future, delayed_advert()) + else: + return "Error: Event loop not running" + logger.info("Advert scheduled for sending (1.5s delay)") return "OK - Advert sent" except Exception as e: