diff --git a/pyproject.toml b/pyproject.toml index cc8e641..1719f2e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "meshcore" -version = "2.2.5" +version = "2.2.6" authors = [ { name="Florent de Lamotte", email="florent@frizoncorrea.fr" }, { name="Alex Wolden", email="awolden@gmail.com" }, diff --git a/src/meshcore/commands/base.py b/src/meshcore/commands/base.py index db1436c..9a7ca6a 100644 --- a/src/meshcore/commands/base.py +++ b/src/meshcore/commands/base.py @@ -3,7 +3,7 @@ import logging import random from typing import Any, Callable, Coroutine, Dict, List, Optional, Union -from meshcore.packets import BinaryReqType +from meshcore.packets import BinaryReqType, AnonReqType from ..events import Event, EventDispatcher, EventType from ..reader import MessageReader @@ -187,3 +187,24 @@ class CommandHandlerBase: self._reader.register_binary_request(pubkey_prefix.hex(), exp_tag, request_type, actual_timeout, context=context) return result + + async def send_anon_req(self, dst: DestinationType, request_type: AnonReqType, data: Optional[bytes] = None, context={}, timeout=None, min_timeout=0) -> Event: + dst_bytes = _validate_destination(dst, prefix_length=32) + pubkey_prefix = _validate_destination(dst, prefix_length=6) + logger.debug(f"Anon Binary request to {dst_bytes.hex()}") + data = b"\x39" + dst_bytes + request_type.value.to_bytes(1, "little", signed=False) + (data if data else b"") + + result = await self.send(data, [EventType.MSG_SENT, EventType.ERROR]) + + # Register the request with the reader if we have both reader and request_type + if (result.type == EventType.MSG_SENT and + self._reader is not None and + request_type is not None): + + exp_tag = result.payload["expected_ack"].hex() + # Use provided timeout or fallback to suggested timeout (with 5s default) + actual_timeout = timeout if timeout is not None and timeout > 0 else result.payload.get("suggested_timeout", 4000) / 800.0 + actual_timeout = min_timeout if actual_timeout < min_timeout else actual_timeout + self._reader.register_binary_request(pubkey_prefix.hex(), exp_tag, request_type, actual_timeout, context=context) + + return result diff --git a/src/meshcore/commands/binary.py b/src/meshcore/commands/binary.py index c47e5f0..ffe5e60 100644 --- a/src/meshcore/commands/binary.py +++ b/src/meshcore/commands/binary.py @@ -1,10 +1,12 @@ import asyncio import logging import random +import io from .base import CommandHandlerBase from ..events import EventType from ..packets import BinaryReqType +from ..packets import AnonReqType logger = logging.getLogger("meshcore") @@ -242,3 +244,109 @@ class BinaryCommandHandler(CommandHandlerBase): res["neighbours"] += next_res["neighbours"] return res + + async def req_regions_async(self, contact, timeout=0, min_timeout=0): + req = b"\0" # The return path, I currently do nothing with, so direct only + return await self.send_anon_req( + contact, + AnonReqType.REGIONS, + data=req, + timeout=timeout + ) + + async def req_regions_sync(self, contact, timeout=0, min_timeout=0): + res = await self.req_regions_async(contact, timeout, min_timeout) + + if res.type == EventType.ERROR: + return None + + timeout = res.payload["suggested_timeout"] / 800 if timeout == 0 else timeout + timeout = timeout if timeout > min_timeout else min_timeout + + if self.dispatcher is None: + return None + + region_event = await self.dispatcher.wait_for_event( + EventType.BINARY_RESPONSE, + attribute_filters={"tag": res.payload["expected_ack"].hex()}, + timeout=timeout, + ) + + if region_event is None: + return None + + pkt = bytes().fromhex(region_event.payload["data"]) + pbuf = io.BytesIO(pkt) + tag_again = pbuf.read(4) + return pbuf.read().decode("utf-8", "ignore").strip("\x00") + + async def req_owner_async(self, contact, timeout=0, min_timeout=0): + req = b"\0" + return await self.send_anon_req( + contact, + AnonReqType.OWNER, + data=req, + timeout=timeout + ) + + async def req_owner_sync(self, contact, timeout=0, min_timeout=0): + + res = await self.req_owner_async(contact, timeout, min_timeout) + + if res.type == EventType.ERROR: + return None + + timeout = res.payload["suggested_timeout"] / 800 if timeout == 0 else timeout + timeout = timeout if timeout > min_timeout else min_timeout + + if self.dispatcher is None: + return None + + owner_event = await self.dispatcher.wait_for_event( + EventType.BINARY_RESPONSE, + attribute_filters={"tag": res.payload["expected_ack"].hex()}, + timeout=timeout, + ) + + if owner_event is None: + return None + + pkt = bytes().fromhex(owner_event.payload["data"]) + pbuf = io.BytesIO(pkt) + tag_again = pbuf.read(4) + strings = pbuf.read().decode("utf-8", "ignore").split("\n", 1) + + return dict(name=strings[0], owner=strings[1].strip("\x00")) + + async def req_basic_async(self, contact, timeout=0, min_timeout=0): + req = b"\0" + return await self.send_anon_req( + contact, + AnonReqType.BASIC, + data=req, + timeout=timeout + ) + + async def req_basic_sync(self, contact, timeout=0, min_timeout=0): + + res = await self.req_basic_async(contact, timeout, min_timeout) + + if res.type == EventType.ERROR: + return None + + timeout = res.payload["suggested_timeout"] / 800 if timeout == 0 else timeout + timeout = timeout if timeout > min_timeout else min_timeout + + if self.dispatcher is None: + return None + + basic_event = await self.dispatcher.wait_for_event( + EventType.BINARY_RESPONSE, + attribute_filters={"tag": res.payload["expected_ack"].hex()}, + timeout=timeout, + ) + + if basic_event is None: + return None + + return basic_event.payload diff --git a/src/meshcore/packets.py b/src/meshcore/packets.py index 6078dec..8eb707f 100644 --- a/src/meshcore/packets.py +++ b/src/meshcore/packets.py @@ -1,5 +1,10 @@ from enum import Enum +class AnonReqType(Enum): + REGIONS = 0x01 + OWNER = 0x02 + BASIC = 0x03 # just remote clock + class BinaryReqType(Enum): STATUS = 0x01 KEEP_ALIVE = 0x02 diff --git a/src/meshcore/reader.py b/src/meshcore/reader.py index f53851c..1b11d22 100644 --- a/src/meshcore/reader.py +++ b/src/meshcore/reader.py @@ -597,10 +597,10 @@ class MessageReader: ) elif packet_type_value == PacketType.BINARY_RESPONSE.value: - logger.debug(f"Received binary data: {data.hex()}") dbuf.read(1) tag = dbuf.read(4).hex() response_data = dbuf.read() + logger.debug(f"Received binary data: {data.hex()}, tag {tag}, data {response_data.hex()}") # Always dispatch generic BINARY_RESPONSE binary_res = {"tag": tag, "data": response_data.hex()}