Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions src/meshcore/commands/control_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"""

Expand Down Expand Up @@ -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])
1 change: 1 addition & 0 deletions src/meshcore/events.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
1 change: 1 addition & 0 deletions src/meshcore/packets.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,3 +59,4 @@ class PacketType(Enum):
BINARY_RESPONSE = 0x8C
PATH_DISCOVERY_RESPONSE = 0x8D
CONTROL_DATA = 0x8E
ADVERT_RESPONSE = 0x8F
63 changes: 63 additions & 0 deletions src/meshcore/reader.py
Original file line number Diff line number Diff line change
Expand Up @@ -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('<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={}
Expand Down