mirror of
https://github.com/MarekWo/mc-webui.git
synced 2026-03-28 17:42:45 +01:00
feat(console): add repeater management commands (Etap 1)
Add 9 new console commands for repeater management: login, logout, cmd, req_status, req_regions, req_owner, req_acl, req_clock, req_mma. Add resolve_contact helper and _parse_time_arg utility. Update help text with categories. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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)}
|
||||
|
||||
139
app/main.py
139
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 <name> <password>"
|
||||
|
||||
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 <name> <command>"
|
||||
|
||||
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 <name> <from_time> <to_time>\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 <idx> <msg> — Send channel message\n"
|
||||
" msg <name> <msg> — Send direct message\n"
|
||||
" advert — Send advertisement\n"
|
||||
" floodadv — Send flood advertisement\n"
|
||||
" telemetry <name> — Request sensor telemetry\n"
|
||||
" neighbors <name> — List neighbors of a node\n"
|
||||
" trace [tag] — Send trace packet\n"
|
||||
" trace [tag] — Send trace packet\n\n"
|
||||
" Repeaters\n"
|
||||
" login <name> <pwd> — Log into a repeater\n"
|
||||
" logout <name> — Log out of a repeater\n"
|
||||
" cmd <name> <cmd> — Send command to a repeater\n"
|
||||
" req_status <name> — Request repeater status\n"
|
||||
" req_regions <name> — Request repeater regions\n"
|
||||
" req_owner <name> — Request repeater owner\n"
|
||||
" req_acl <name> — Request access control list\n"
|
||||
" req_clock <name> — Request repeater clock\n"
|
||||
" req_mma <n> <f> <t> — Request min/max/avg sensor data\n\n"
|
||||
" help — Show this help"
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user