diff --git a/README.md b/README.md index bea2c49..c87cfc6 100644 --- a/README.md +++ b/README.md @@ -26,8 +26,11 @@ async def main(): # Send a message to the first contact if contacts: - contact_key = next(iter(contacts.items()))[1]['public_key'] - await meshcore.commands.send_msg(bytes.fromhex(contact_key), "Hello from Python!") + # Get the first contact + contact = next(iter(contacts.items()))[1] + + # Pass the contact object directly to send_msg + await meshcore.commands.send_msg(contact, "Hello from Python!") await meshcore.disconnect() @@ -247,15 +250,31 @@ This logs detailed information about commands sent and events received. ### Sending Messages to Contacts +Commands that require a destination (`send_msg`, `send_login`, `send_statusreq`, etc.) now accept either: +- A string with the hex representation of a public key +- A contact object with a "public_key" field +- Bytes object (for backward compatibility) + ```python # Get contacts and send to a specific one contacts = await meshcore.commands.get_contacts() for key, contact in contacts.items(): if contact["adv_name"] == "Alice": - # Convert the hex key to bytes + # Option 1: Pass the contact object directly + await meshcore.commands.send_msg(contact, "Hello Alice!") + + # Option 2: Use the public key string + await meshcore.commands.send_msg(contact["public_key"], "Hello again Alice!") + + # Option 3 (backward compatible): Convert the hex key to bytes dst_key = bytes.fromhex(contact["public_key"]) - await meshcore.commands.send_msg(dst_key, "Hello Alice!") + await meshcore.commands.send_msg(dst_key, "Hello once more Alice!") break + +# You can also directly use a contact found by name +contact = meshcore.get_contact_by_name("Bob") +if contact: + await meshcore.commands.send_msg(contact, "Hello Bob!") ``` ### Monitoring Channel Messages diff --git a/examples/serial_msg.py b/examples/serial_msg.py index cb21a10..a5a055d 100755 --- a/examples/serial_msg.py +++ b/examples/serial_msg.py @@ -38,7 +38,7 @@ async def main(): # Send the message and get the MSG_SENT event print(f"Sending message: '{args.message}'") send_result = await mc.commands.send_msg( - bytes.fromhex(contact["public_key"])[0:6], + contact, args.message ) diff --git a/examples/serial_repeater_status.py b/examples/serial_repeater_status.py index 2a77336..134f8e9 100755 --- a/examples/serial_repeater_status.py +++ b/examples/serial_repeater_status.py @@ -16,7 +16,10 @@ async def main () : await mc.commands.get_contacts() repeater = mc.get_contact_by_name(REPEATER) - await mc.commands.send_login(bytes.fromhex(repeater["public_key"]), PASSWORD) + if repeater is None: + print(f"Repeater '{REPEATER}' not found in contacts.") + return + await mc.commands.send_login(repeater, PASSWORD) print("Login sent ... awaiting") diff --git a/examples/tcp_mchome_msg.py b/examples/tcp_mchome_msg.py index ec44d38..cf8762f 100755 --- a/examples/tcp_mchome_msg.py +++ b/examples/tcp_mchome_msg.py @@ -17,6 +17,10 @@ async def main () : await mc.connect() await mc.ensure_contacts() - await mc.commands.send_msg(bytes.fromhex(mc.get_contact_by_name(DEST)["public_key"])[0:6],MSG) + contact = mc.get_contact_by_name(DEST) + if contact is None: + print(f"Contact '{DEST}' not found in contacts.") + return + await mc.commands.send_msg(contact ,MSG) asyncio.run(main()) diff --git a/src/meshcore/commands.py b/src/meshcore/commands.py index 56e0f4b..1532f25 100644 --- a/src/meshcore/commands.py +++ b/src/meshcore/commands.py @@ -1,11 +1,50 @@ import asyncio import logging -from typing import Any, Dict +from typing import Any, Dict, List, Optional, Union from .events import EventType import random + +# Define types for destination parameters +DestinationType = Union[bytes, str, Dict[str, Any]] logger = logging.getLogger("meshcore") +def _validate_destination(dst: DestinationType, prefix_length: int = 6) -> bytes: + """ + Validates and converts a destination to a bytes object. + + Args: + dst: The destination, which can be: + - str: Hex string representation of a public key + - dict: Contact object with a "public_key" field + prefix_length: The length of the prefix to use (default: 6 bytes) + + Returns: + bytes: The destination public key as a bytes object + + Raises: + ValueError: If dst is invalid or doesn't contain required fields + """ + if isinstance(dst, bytes): + # Already bytes, use directly + return dst[:prefix_length] + elif isinstance(dst, str): + # Hex string, convert to bytes + try: + return bytes.fromhex(dst)[:prefix_length] + except ValueError: + raise ValueError(f"Invalid public key hex string: {dst}") + elif isinstance(dst, dict): + # Contact object, extract public_key + if "public_key" not in dst: + raise ValueError("Contact object must have a 'public_key' field") + try: + return bytes.fromhex(dst["public_key"])[:prefix_length] + except ValueError: + raise ValueError(f"Invalid public_key in contact: {dst['public_key']}") + else: + raise ValueError(f"Destination must be a public key string or contact object, got: {type(dst)}") + class CommandHandler: DEFAULT_TIMEOUT = 5.0 @@ -166,41 +205,44 @@ class CommandHandler: logger.debug("Requesting pending messages") return await self.send(b"\x0A", [EventType.CONTACT_MSG_RECV, EventType.CHANNEL_MSG_RECV, EventType.ERROR], timeout) - async def send_login(self, dst, pwd): - logger.debug(f"Sending login request to: {dst.hex() if isinstance(dst, bytes) else dst}") - data = b"\x1a" + dst + pwd.encode("ascii") + async def send_login(self, dst: DestinationType, pwd: str) -> Dict[str, Any]: + dst_bytes = _validate_destination(dst) + logger.debug(f"Sending login request to: {dst_bytes.hex()}") + data = b"\x1a" + dst_bytes + pwd.encode("ascii") return await self.send(data, [EventType.MSG_SENT, EventType.ERROR]) - async def send_logout(self, dst): + async def send_logout(self, dst: DestinationType) -> Dict[str, Any]: + dst_bytes = _validate_destination(dst) self.login_resp = asyncio.Future() - data = b"\x1d" + dst + data = b"\x1d" + dst_bytes return await self.send(data, [EventType.MSG_SENT, EventType.ERROR]) - async def send_statusreq(self, dst): - logger.debug(f"Sending status request to: {dst.hex() if isinstance(dst, bytes) else dst}") - data = b"\x1b" + dst + async def send_statusreq(self, dst: DestinationType) -> Dict[str, Any]: + dst_bytes = _validate_destination(dst) + logger.debug(f"Sending status request to: {dst_bytes.hex()}") + data = b"\x1b" + dst_bytes return await self.send(data, [EventType.MSG_SENT, EventType.ERROR]) - async def send_cmd(self, dst, cmd, timestamp=None): - logger.debug(f"Sending command to {dst.hex() if isinstance(dst, bytes) else dst}: {cmd}") + async def send_cmd(self, dst: DestinationType, cmd: str, timestamp: Optional[int] = None) -> Dict[str, Any]: + dst_bytes = _validate_destination(dst) + logger.debug(f"Sending command to {dst_bytes.hex()}: {cmd}") - # Default to current time if timestamp not provided if timestamp is None: import time - timestamp = int(time.time()).to_bytes(4, 'little') + timestamp = int(time.time()) - data = b"\x02\x01\x00" + timestamp + dst + cmd.encode("ascii") + data = b"\x02\x01\x00" + timestamp.to_bytes(4, 'little') + dst_bytes + cmd.encode("ascii") return await self.send(data, [EventType.OK, EventType.ERROR]) - async def send_msg(self, dst, msg, timestamp=None): - logger.debug(f"Sending message to {dst.hex() if isinstance(dst, bytes) else dst}: {msg}") + async def send_msg(self, dst: DestinationType, msg: str, timestamp: Optional[int] = None) -> Dict[str, Any]: + dst_bytes = _validate_destination(dst) + logger.debug(f"Sending message to {dst_bytes.hex()}: {msg}") - # Default to current time if timestamp not provided if timestamp is None: import time - timestamp = int(time.time()).to_bytes(4, 'little') + timestamp = int(time.time()) - data = b"\x02\x00\x00" + timestamp + dst + msg.encode("ascii") + data = b"\x02\x00\x00" + timestamp.to_bytes(4, 'little') + dst_bytes + msg.encode("ascii") return await self.send(data, [EventType.MSG_SENT, EventType.ERROR]) async def send_chan_msg(self, chan, msg, timestamp=None):