Adds support for pull-based advert requests and responses.

This commit is contained in:
Michiel Appelman 2026-01-11 21:44:03 +01:00
parent 01e3f21992
commit 0283003b04
4 changed files with 111 additions and 0 deletions

View file

@ -7,6 +7,9 @@ from ..packets import ControlType, PacketType
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 @@ class ControlDataCommandHandler(CommandHandlerBase):
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])

View file

@ -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"

View file

@ -59,3 +59,4 @@ class PacketType(Enum):
BINARY_RESPONSE = 0x8C
PATH_DISCOVERY_RESPONSE = 0x8D
CONTROL_DATA = 0x8E
ADVERT_RESPONSE = 0x8F

View file

@ -729,6 +729,69 @@ class MessageReader:
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('<I', data[offset:offset+4])[0]
offset += 4
res["pubkey"] = data[offset:offset+32].hex()
offset += 32
res["adv_type"] = data[offset]
offset += 1
res["node_name"] = data[offset:offset+32].decode('utf-8', errors='replace').rstrip('\x00')
offset += 32
res["timestamp"] = struct.unpack('<I', data[offset:offset+4])[0]
offset += 4
flags = data[offset]
res["flags"] = flags
offset += 1
# Parse optional fields based on flags
if flags & 0x01: # has latitude
if offset + 4 <= len(data):
lat_i32 = struct.unpack('<i', data[offset:offset+4])[0]
res["latitude"] = lat_i32 / 1e6
offset += 4
if flags & 0x02: # has longitude
if offset + 4 <= len(data):
lon_i32 = struct.unpack('<i', data[offset:offset+4])[0]
res["longitude"] = lon_i32 / 1e6
offset += 4
if flags & 0x04: # has description
if offset + 32 <= len(data):
res["node_desc"] = data[offset:offset+32].decode('utf-8', errors='replace').rstrip('\x00')
offset += 32
attributes = {
"tag": res["tag"],
"pubkey": res["pubkey"],
}
await self.dispatcher.dispatch(Event(EventType.ADVERT_RESPONSE, res, attributes))
elif packet_type_value == PacketType.CONTROL_DATA.value:
logger.debug("Received control data packet")
res={}