diff --git a/repeater/data_acquisition/sqlite_handler.py b/repeater/data_acquisition/sqlite_handler.py index 806b707..69f228e 100644 --- a/repeater/data_acquisition/sqlite_handler.py +++ b/repeater/data_acquisition/sqlite_handler.py @@ -557,7 +557,7 @@ class SQLiteHandler: neighbors = conn.execute(""" SELECT pubkey, node_name, is_repeater, route_type, contact_type, - latitude, longitude, first_seen, last_seen, rssi, snr, advert_count + latitude, longitude, first_seen, last_seen, rssi, snr, advert_count, zero_hop FROM adverts a1 WHERE last_seen = ( SELECT MAX(last_seen) @@ -581,6 +581,7 @@ class SQLiteHandler: "rssi": row["rssi"], "snr": row["snr"], "advert_count": row["advert_count"], + "zero_hop": bool(row["zero_hop"]), } return result diff --git a/repeater/handler_helpers/mesh_cli.py b/repeater/handler_helpers/mesh_cli.py index df83655..5bd84d8 100644 --- a/repeater/handler_helpers/mesh_cli.py +++ b/repeater/handler_helpers/mesh_cli.py @@ -16,7 +16,9 @@ class MeshCLI: save_config_callback: Callable, identity_type: str = "repeater", enable_regions: bool = True, - send_advert_callback: Optional[Callable] = None + send_advert_callback: Optional[Callable] = None, + identity = None, + storage_handler = None ): self.config_path = Path(config_path) @@ -25,6 +27,8 @@ class MeshCLI: self.identity_type = identity_type self.enable_regions = enable_regions self.send_advert_callback = send_advert_callback + self.identity = identity + self.storage_handler = storage_handler # Get repeater config shortcut self.repeater_config = config.get('repeater', {}) @@ -245,8 +249,15 @@ class MeshCLI: return f"> {power}" elif param == "public.key": - # TODO: Get from identity - return "Error: Not yet implemented" + if not self.identity: + return "Error: Identity not available" + try: + pubkey = self.identity.get_public_key() + pubkey_hex = pubkey.hex() + return f"> {pubkey_hex}" + except Exception as e: + logger.error(f"Failed to get public key: {e}") + return f"Error: {e}" elif param == "role": role = "room_server" if self.identity_type == "room_server" else "repeater" @@ -498,8 +509,55 @@ class MeshCLI: def _cmd_neighbors(self) -> str: """List neighbors.""" - # TODO: Get neighbors from routing table - return "Error: Not yet implemented" + if not self.storage_handler: + return "Error: Storage not available" + + try: + neighbors = self.storage_handler.get_neighbors() + + if not neighbors: + return "No neighbors discovered yet" + + # Filter to only show repeaters and zero hop nodes + filtered_neighbors = { + pubkey: info for pubkey, info in neighbors.items() + if info.get('is_repeater', False) or info.get('zero_hop', False) + } + + if not filtered_neighbors: + return "No repeaters or zero hop neighbors discovered yet" + + # Format output similar to C++ version + # Format: " heard Xs ago" + import time + current_time = int(time.time()) + + lines = [] + for pubkey, info in filtered_neighbors.items(): + last_seen = info.get('last_seen', 0) + seconds_ago = current_time - last_seen + + # Get short pubkey (first 8 chars) + pubkey_short = pubkey[:8] if len(pubkey) >= 8 else pubkey + node_name = info.get('node_name') or 'Unknown' + + # Format time ago + if seconds_ago < 60: + time_str = f"{seconds_ago}s ago" + elif seconds_ago < 3600: + time_str = f"{seconds_ago // 60}m ago" + elif seconds_ago < 86400: + time_str = f"{seconds_ago // 3600}h ago" + else: + time_str = f"{seconds_ago // 86400}d ago" + + lines.append(f"<{pubkey_short}> {node_name} heard {time_str}") + + return "\n".join(lines) + + except Exception as e: + logger.error(f"Failed to list neighbors: {e}", exc_info=True) + return f"Error: {e}" def _cmd_neighbor_remove(self, command: str) -> str: """Remove a neighbor.""" diff --git a/repeater/handler_helpers/room_server.py b/repeater/handler_helpers/room_server.py index 98064b4..503b524 100644 --- a/repeater/handler_helpers/room_server.py +++ b/repeater/handler_helpers/room_server.py @@ -150,9 +150,11 @@ class RoomServer: save_config_callback, identity_type="room_server", enable_regions=False, # Room servers don't support region commands - send_advert_callback=send_room_advert + send_advert_callback=send_room_advert, + identity=local_identity, + storage_handler=sqlite_handler ) - logger.info(f"Room '{room_name}': Initialized CLI handler") + logger.info(f"Room '{room_name}': Initialized CLI handler with identity and storage") # Enforce hard limit (match C++ MAX_UNSYNCED_POSTS) if max_posts > MAX_UNSYNCED_POSTS: diff --git a/repeater/handler_helpers/text.py b/repeater/handler_helpers/text.py index 4c4c271..82e528b 100644 --- a/repeater/handler_helpers/text.py +++ b/repeater/handler_helpers/text.py @@ -44,18 +44,12 @@ class TextHelper: self.config = config self.save_config_callback = save_config_callback - # Initialize CLI handler if config provided + # Store for later CLI initialization (needs identity and storage) + self.config_path = config_path + self.config = config + + # Initialize CLI handler later when repeater identity is registered self.cli = None - if config_path and config and save_config_callback: - self.cli = MeshCLI( - config_path, - config, - save_config_callback, - identity_type="repeater", - enable_regions=True, - send_advert_callback=send_advert_callback - ) - logger.info("Initialized CLI handler for repeater commands") def register_identity( self, @@ -98,6 +92,20 @@ class TextHelper: if identity_type == "repeater": self.repeater_hash = hash_byte logger.info(f"Set repeater hash for CLI: 0x{hash_byte:02X}") + + # Initialize CLI handler now that we have the repeater identity + if self.config_path and self.config and self.save_config_callback: + self.cli = MeshCLI( + self.config_path, + self.config, + self.save_config_callback, + identity_type="repeater", + enable_regions=True, + send_advert_callback=self.send_advert_callback, + identity=identity, + storage_handler=self.sqlite_handler + ) + logger.info("Initialized CLI handler for repeater commands with identity and storage") # Create RoomServer instance for room_server identities if identity_type == "room_server" and self.sqlite_handler: