diff --git a/app/device_manager.py b/app/device_manager.py index 2097a9a..a554c0a 100644 --- a/app/device_manager.py +++ b/app/device_manager.py @@ -1547,3 +1547,195 @@ class DeviceManager: except Exception as e: logger.error(f"Trace failed: {e}") return {'error': str(e)} + + def resolve_contact(self, name_or_key: str) -> Optional[Dict]: + """Resolve a contact by name or public key prefix.""" + if not self.is_connected or not self.mc: + return None + contact = self.mc.get_contact_by_name(name_or_key) + if not contact: + contact = self.mc.get_contact_by_key_prefix(name_or_key) + return contact + + # ── Repeater Management ────────────────────────────────────────── + + def repeater_login(self, name_or_key: str, password: str) -> Dict: + """Log into a repeater with given password.""" + if not self.is_connected: + return {'success': False, 'error': 'Device not connected'} + contact = self.resolve_contact(name_or_key) + if not contact: + return {'success': False, 'error': f"Contact not found: {name_or_key}"} + try: + from meshcore.events import EventType + res = self.execute( + self.mc.commands.send_login(contact, password), + timeout=10 + ) + # Wait for LOGIN_SUCCESS or LOGIN_FAILED + timeout = 30 + if res and hasattr(res, 'payload') and 'suggested_timeout' in res.payload: + timeout = res.payload['suggested_timeout'] / 800 + timeout = max(timeout, contact.get('timeout', 0) or 30) + event = self.execute( + self.mc.wait_for_event(EventType.LOGIN_SUCCESS, timeout=timeout), + timeout=timeout + 5 + ) + if event and hasattr(event, 'type') and event.type == EventType.LOGIN_SUCCESS: + return {'success': True, 'message': f'Logged into {contact.get("adv_name", name_or_key)}'} + return {'success': False, 'error': 'Login failed (timeout)'} + except Exception as e: + err = str(e) + if 'LOGIN_FAILED' in err or 'login' in err.lower(): + return {'success': False, 'error': 'Login failed (wrong password?)'} + logger.error(f"Repeater login failed: {e}") + return {'success': False, 'error': err} + + def repeater_logout(self, name_or_key: str) -> Dict: + """Log out of a repeater.""" + if not self.is_connected: + return {'success': False, 'error': 'Device not connected'} + contact = self.resolve_contact(name_or_key) + if not contact: + return {'success': False, 'error': f"Contact not found: {name_or_key}"} + try: + self.execute(self.mc.commands.send_logout(contact), timeout=10) + return {'success': True, 'message': f'Logged out of {contact.get("adv_name", name_or_key)}'} + except Exception as e: + logger.error(f"Repeater logout failed: {e}") + return {'success': False, 'error': str(e)} + + def repeater_cmd(self, name_or_key: str, cmd: str) -> Dict: + """Send a command to a repeater.""" + if not self.is_connected: + return {'success': False, 'error': 'Device not connected'} + contact = self.resolve_contact(name_or_key) + if not contact: + return {'success': False, 'error': f"Contact not found: {name_or_key}"} + try: + res = self.execute(self.mc.commands.send_cmd(contact, cmd), timeout=10) + msg = f'Command sent to {contact.get("adv_name", name_or_key)}: {cmd}' + return {'success': True, 'message': msg} + except Exception as e: + logger.error(f"Repeater cmd failed: {e}") + return {'success': False, 'error': str(e)} + + def repeater_req_status(self, name_or_key: str) -> Dict: + """Request status from a repeater.""" + if not self.is_connected: + return {'success': False, 'error': 'Device not connected'} + contact = self.resolve_contact(name_or_key) + if not contact: + return {'success': False, 'error': f"Contact not found: {name_or_key}"} + try: + timeout = contact.get('timeout', 0) or 30 + event = self.execute( + self.mc.commands.req_status_sync(contact, timeout), + timeout=timeout + 5 + ) + if event and hasattr(event, 'payload'): + return {'success': True, 'data': event.payload} + return {'success': False, 'error': 'No status response (timeout)'} + except Exception as e: + logger.error(f"req_status failed: {e}") + return {'success': False, 'error': str(e)} + + def repeater_req_regions(self, name_or_key: str) -> Dict: + """Request regions from a repeater.""" + if not self.is_connected: + return {'success': False, 'error': 'Device not connected'} + contact = self.resolve_contact(name_or_key) + if not contact: + return {'success': False, 'error': f"Contact not found: {name_or_key}"} + try: + timeout = contact.get('timeout', 0) or 30 + event = self.execute( + self.mc.commands.req_regions_sync(contact, timeout), + timeout=timeout + 5 + ) + if event and hasattr(event, 'payload'): + return {'success': True, 'data': event.payload} + return {'success': False, 'error': 'No regions response (timeout)'} + except Exception as e: + logger.error(f"req_regions failed: {e}") + return {'success': False, 'error': str(e)} + + def repeater_req_owner(self, name_or_key: str) -> Dict: + """Request owner info from a repeater.""" + if not self.is_connected: + return {'success': False, 'error': 'Device not connected'} + contact = self.resolve_contact(name_or_key) + if not contact: + return {'success': False, 'error': f"Contact not found: {name_or_key}"} + try: + timeout = contact.get('timeout', 0) or 30 + event = self.execute( + self.mc.commands.req_owner_sync(contact, timeout), + timeout=timeout + 5 + ) + if event and hasattr(event, 'payload'): + return {'success': True, 'data': event.payload} + return {'success': False, 'error': 'No owner response (timeout)'} + except Exception as e: + logger.error(f"req_owner failed: {e}") + return {'success': False, 'error': str(e)} + + def repeater_req_acl(self, name_or_key: str) -> Dict: + """Request access control list from a repeater.""" + if not self.is_connected: + return {'success': False, 'error': 'Device not connected'} + contact = self.resolve_contact(name_or_key) + if not contact: + return {'success': False, 'error': f"Contact not found: {name_or_key}"} + try: + timeout = contact.get('timeout', 0) or 30 + event = self.execute( + self.mc.commands.req_acl_sync(contact, timeout), + timeout=timeout + 5 + ) + if event and hasattr(event, 'payload'): + return {'success': True, 'data': event.payload} + return {'success': False, 'error': 'No ACL response (timeout)'} + except Exception as e: + logger.error(f"req_acl failed: {e}") + return {'success': False, 'error': str(e)} + + def repeater_req_clock(self, name_or_key: str) -> Dict: + """Request clock/basic info from a repeater.""" + if not self.is_connected: + return {'success': False, 'error': 'Device not connected'} + contact = self.resolve_contact(name_or_key) + if not contact: + return {'success': False, 'error': f"Contact not found: {name_or_key}"} + try: + timeout = contact.get('timeout', 0) or 30 + event = self.execute( + self.mc.commands.req_basic_sync(contact, timeout), + timeout=timeout + 5 + ) + if event and hasattr(event, 'payload'): + return {'success': True, 'data': event.payload} + return {'success': False, 'error': 'No clock response (timeout)'} + except Exception as e: + logger.error(f"req_clock failed: {e}") + return {'success': False, 'error': str(e)} + + def repeater_req_mma(self, name_or_key: str, from_secs: int, to_secs: int) -> Dict: + """Request min/max/avg sensor data from a repeater.""" + if not self.is_connected: + return {'success': False, 'error': 'Device not connected'} + contact = self.resolve_contact(name_or_key) + if not contact: + return {'success': False, 'error': f"Contact not found: {name_or_key}"} + try: + timeout = contact.get('timeout', 0) or 30 + event = self.execute( + self.mc.commands.req_mma_sync(contact, from_secs, to_secs, timeout), + timeout=timeout + 5 + ) + if event and hasattr(event, 'payload'): + return {'success': True, 'data': event.payload} + return {'success': False, 'error': 'No MMA response (timeout)'} + except Exception as e: + logger.error(f"req_mma failed: {e}") + return {'success': False, 'error': str(e)} diff --git a/app/main.py b/app/main.py index d9a2d46..d448593 100644 --- a/app/main.py +++ b/app/main.py @@ -197,6 +197,19 @@ def handle_send_command(data): socketio.start_background_task(execute_and_respond) +def _parse_time_arg(value: str) -> int: + """Parse time argument with optional suffix: s (seconds), m (minutes, default), h (hours).""" + value = value.strip().lower() + if value.endswith('s'): + return int(value[:-1]) + elif value.endswith('h'): + return int(value[:-1]) * 3600 + elif value.endswith('m'): + return int(value[:-1]) * 60 + else: + return int(value) * 60 # default: minutes + + def _execute_console_command(args: list) -> str: """ Execute a console command via DeviceManager. @@ -382,23 +395,143 @@ def _execute_console_command(args: list) -> str: return "Trace unavailable" return result.get('message', result.get('error', 'Unknown')) + # ── Repeater commands ──────────────────────────────────────── + + elif cmd == 'login' and len(args) >= 3: + name = args[1] + password = ' '.join(args[2:]) + result = device_manager.repeater_login(name, password) + if result.get('success'): + return result.get('message', 'OK') + return f"Error: {result.get('error')}" + + elif cmd == 'login': + return "Usage: login " + + elif cmd == 'logout' and len(args) >= 2: + name = ' '.join(args[1:]) + result = device_manager.repeater_logout(name) + if result.get('success'): + return result.get('message', 'OK') + return f"Error: {result.get('error')}" + + elif cmd == 'cmd' and len(args) >= 3: + name = args[1] + remote_cmd = ' '.join(args[2:]) + result = device_manager.repeater_cmd(name, remote_cmd) + if result.get('success'): + return result.get('message', 'OK') + return f"Error: {result.get('error')}" + + elif cmd == 'cmd': + return "Usage: cmd " + + elif cmd == 'req_status' and len(args) >= 2: + name = ' '.join(args[1:]) + result = device_manager.repeater_req_status(name) + if result.get('success'): + data = result['data'] + lines = [f"Status of {name}:"] + for k, v in data.items(): + lines.append(f" {k}: {v}") + return "\n".join(lines) + return f"Error: {result.get('error')}" + + elif cmd == 'req_regions' and len(args) >= 2: + name = ' '.join(args[1:]) + result = device_manager.repeater_req_regions(name) + if result.get('success'): + data = result['data'] + lines = [f"Regions of {name}:"] + for k, v in data.items(): + lines.append(f" {k}: {v}") + return "\n".join(lines) + return f"Error: {result.get('error')}" + + elif cmd == 'req_owner' and len(args) >= 2: + name = ' '.join(args[1:]) + result = device_manager.repeater_req_owner(name) + if result.get('success'): + data = result['data'] + lines = [f"Owner of {name}:"] + for k, v in data.items(): + lines.append(f" {k}: {v}") + return "\n".join(lines) + return f"Error: {result.get('error')}" + + elif cmd == 'req_acl' and len(args) >= 2: + name = ' '.join(args[1:]) + result = device_manager.repeater_req_acl(name) + if result.get('success'): + data = result['data'] + lines = [f"ACL of {name}:"] + if isinstance(data, dict): + for k, v in data.items(): + lines.append(f" {k}: {v}") + else: + lines.append(f" {data}") + return "\n".join(lines) + return f"Error: {result.get('error')}" + + elif cmd == 'req_clock' and len(args) >= 2: + name = ' '.join(args[1:]) + result = device_manager.repeater_req_clock(name) + if result.get('success'): + data = result['data'] + lines = [f"Clock of {name}:"] + for k, v in data.items(): + lines.append(f" {k}: {v}") + return "\n".join(lines) + return f"Error: {result.get('error')}" + + elif cmd == 'req_mma' and len(args) >= 4: + name = args[1] + try: + from_secs = _parse_time_arg(args[2]) + to_secs = _parse_time_arg(args[3]) + except ValueError as e: + return f"Error: {e}" + result = device_manager.repeater_req_mma(name, from_secs, to_secs) + if result.get('success'): + data = result['data'] + lines = [f"MMA of {name} ({args[2]} → {args[3]}):"] + for k, v in data.items(): + lines.append(f" {k}: {v}") + return "\n".join(lines) + return f"Error: {result.get('error')}" + + elif cmd == 'req_mma': + return "Usage: req_mma \n Time format: number with optional suffix s/m/h (default: minutes)" + elif cmd == 'help': return ( - "Available commands:\n" + "Available commands:\n\n" + " General\n" " infos — Device info (firmware, freq, etc.)\n" " status — Connection status, battery, contacts count\n" " stats — Device statistics (uptime, TX/RX, packets)\n" " bat — Battery voltage\n" " contacts — List device contacts with path info\n" " contacts_all — List all known contacts (device + cached)\n" - " channels — List configured channels\n" + " channels — List configured channels\n\n" + " Messaging\n" " chan — Send channel message\n" " msg — Send direct message\n" " advert — Send advertisement\n" " floodadv — Send flood advertisement\n" " telemetry — Request sensor telemetry\n" " neighbors — List neighbors of a node\n" - " trace [tag] — Send trace packet\n" + " trace [tag] — Send trace packet\n\n" + " Repeaters\n" + " login — Log into a repeater\n" + " logout — Log out of a repeater\n" + " cmd — Send command to a repeater\n" + " req_status — Request repeater status\n" + " req_regions — Request repeater regions\n" + " req_owner — Request repeater owner\n" + " req_acl — Request access control list\n" + " req_clock — Request repeater clock\n" + " req_mma — Request min/max/avg sensor data\n\n" " help — Show this help" )