diff --git a/src/meshcore/commands/control_data.py b/src/meshcore/commands/control_data.py index 81adb66..a55aeba 100644 --- a/src/meshcore/commands/control_data.py +++ b/src/meshcore/commands/control_data.py @@ -7,6 +7,9 @@ logger = logging.getLogger("meshcore") +# Command codes +CMD_REQUEST_ADVERT = 57 # 0x39 + class ControlDataCommandHandler(CommandHandlerBase): """Helper functions to handle binary requests through binary commands""" @@ -48,3 +51,46 @@ async def send_node_discover_req ( else: res.payload["tag"] = tag return res + + async def request_advert(self, prefix: bytes, path: bytes) -> Event: + """ + Request advertisement from a node via pull-based system. + + Args: + prefix: First byte of target node's public key (PATH_HASH_SIZE = 1) + path: Path to reach the node (1-64 bytes) + + Returns: + Event with type OK on success, ERROR on failure. + The actual response arrives asynchronously as ADVERT_RESPONSE event. + + Raises: + ValueError: If prefix is not 1 byte or path is empty/too long + + Example: + # Get repeater from contacts + contacts = (await mc.commands.get_contacts()).payload + repeater = next(c for c in contacts.values() if c['adv_type'] == 2) + + # Extract prefix and path + prefix = bytes.fromhex(repeater['public_key'])[:1] + path = bytes(repeater.get('out_path', [])) or prefix + + # Send request + result = await mc.commands.request_advert(prefix, path) + if result.type == EventType.ERROR: + print(f"Failed: {result.payload}") + return + + # Wait for response + response = await mc.wait_for_event(EventType.ADVERT_RESPONSE, timeout=30) + if response: + print(f"Node: {response.payload['node_name']}") + """ + if len(prefix) != 1: + raise ValueError("Prefix must be exactly 1 byte (PATH_HASH_SIZE)") + if not path or len(path) > 64: + raise ValueError("Path must be 1-64 bytes") + + cmd = bytes([CMD_REQUEST_ADVERT]) + prefix + bytes([len(path)]) + path + return await self.send(cmd, [EventType.OK, EventType.ERROR]) diff --git a/src/meshcore/events.py b/src/meshcore/events.py index d3b3820..0012939 100644 --- a/src/meshcore/events.py +++ b/src/meshcore/events.py @@ -52,6 +52,7 @@ class EventType(Enum): NEIGHBOURS_RESPONSE = "neighbours_response" SIGN_START = "sign_start" SIGNATURE = "signature" + ADVERT_RESPONSE = "advert_response" # Command response types OK = "command_ok" diff --git a/src/meshcore/packets.py b/src/meshcore/packets.py index 7257c99..6078dec 100644 --- a/src/meshcore/packets.py +++ b/src/meshcore/packets.py @@ -59,3 +59,4 @@ class PacketType(Enum): BINARY_RESPONSE = 0x8C PATH_DISCOVERY_RESPONSE = 0x8D CONTROL_DATA = 0x8E + ADVERT_RESPONSE = 0x8F diff --git a/src/meshcore/reader.py b/src/meshcore/reader.py index c65a8eb..28d591d 100644 --- a/src/meshcore/reader.py +++ b/src/meshcore/reader.py @@ -729,6 +729,69 @@ async def handle_rx(self, data: bytearray): res = {"reason": "private_key_export_disabled"} await self.dispatcher.dispatch(Event(EventType.DISABLED, res)) + elif packet_type_value == PacketType.ADVERT_RESPONSE.value: + logger.debug(f"Received advert response: {data.hex()}") + # PUSH_CODE_ADVERT_RESPONSE (0x8F) format: + # Byte 0: 0x8F (push code) + # Bytes 1-4: tag (uint32) + # Bytes 5-36: pubkey (32 bytes) + # Byte 37: adv_type + # Bytes 38-69: node_name (32 bytes) + # Bytes 70-73: timestamp (uint32) + # Byte 74: flags + # [Optional fields based on flags] + + if len(data) < 75: + logger.error(f"Advert response too short: {len(data)} bytes, need at least 75") + return + + res = {} + offset = 1 # Skip push code + + res["tag"] = struct.unpack('