From 6fbf15885df90c4296e689070893dcd44f577f98 Mon Sep 17 00:00:00 2001 From: Alex Wolden Date: Mon, 14 Apr 2025 11:10:59 -0700 Subject: [PATCH] Change contract for commands to return full event --- README.md | 126 +++++++++++++++++++++++------ examples/ble_chat.py | 8 +- examples/pubsub_example.py | 6 +- examples/serial_chat.py | 16 ++-- examples/serial_contacts.py | 7 +- examples/serial_msg.py | 8 +- examples/serial_repeater_status.py | 2 +- examples/serial_trace.py | 8 +- examples/tcp_chat.py | 16 ++-- examples/tcp_mchome_contacts.py | 7 +- examples/tcp_mchome_msg.py | 11 ++- examples/tcp_mchome_readmsgs.py | 8 +- src/meshcore/commands.py | 82 ++++++++++--------- src/meshcore/meshcore.py | 3 +- src/meshcore/reader.py | 6 +- src/meshcore/tcp_cx.py | 5 +- tests/unit/test_commands.py | 16 ++-- 17 files changed, 231 insertions(+), 104 deletions(-) diff --git a/README.md b/README.md index c87cfc6..1d8cf84 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,12 @@ async def main(): meshcore = await MeshCore.create_serial("/dev/ttyUSB0") # Get your contacts - contacts = await meshcore.commands.get_contacts() + result = await meshcore.commands.get_contacts() + if result.type == EventType.ERROR: + print(f"Error getting contacts: {result.payload}") + return + + contacts = result.payload print(f"Found {len(contacts)} contacts") # Send a message to the first contact @@ -30,7 +35,12 @@ async def main(): contact = next(iter(contacts.items()))[1] # Pass the contact object directly to send_msg - await meshcore.commands.send_msg(contact, "Hello from Python!") + result = await meshcore.commands.send_msg(contact, "Hello from Python!") + + if result.type == EventType.ERROR: + print(f"Error sending message: {result.payload}") + else: + print("Message sent successfully!") await meshcore.disconnect() @@ -55,6 +65,37 @@ python examples/pubsub_example.py -p /dev/ttyUSB0 ## Usage Guide +### Command Return Values + +All command methods in MeshCore return an `Event` object that contains both the event type and its payload. This allows for consistent error handling and type checking: + +```python +# Command result structure +result = await meshcore.commands.some_command() + +# Check if the command was successful or resulted in an error +if result.type == EventType.ERROR: + # Handle error case + print(f"Command failed: {result.payload}") +else: + # Handle success case - the event type will be specific to the command + # (e.g., EventType.DEVICE_INFO, EventType.CONTACTS, EventType.MSG_SENT) + print(f"Command succeeded with event type: {result.type}") + # Access the payload data + data = result.payload +``` + +Common error handling pattern: + +```python +result = await meshcore.commands.send_msg(contact, "Hello!") +if result.type == EventType.ERROR: + print(f"Error sending message: {result.payload}") +else: + # For send_msg, a successful result will have type EventType.MSG_SENT + print(f"Message sent with expected ack: {result.payload['expected_ack'].hex()}") +``` + ### Connecting to Your Device Connect via Serial, BLE, or TCP: @@ -76,20 +117,34 @@ Send commands and wait for responses: ```python # Get device information -device_info = await meshcore.commands.send_device_query() -print(f"Device model: {device_info['model']}") +result = await meshcore.commands.send_device_query() +if result.type == EventType.ERROR: + print(f"Error getting device info: {result.payload}") +else: + print(f"Device model: {result.payload['model']}") # Get list of contacts -contacts = await meshcore.commands.get_contacts() -for contact_id, contact in contacts.items(): - print(f"Contact: {contact['adv_name']} ({contact_id})") +result = await meshcore.commands.get_contacts() +if result.type == EventType.ERROR: + print(f"Error getting contacts: {result.payload}") +else: + contacts = result.payload + for contact_id, contact in contacts.items(): + print(f"Contact: {contact['adv_name']} ({contact_id})") # Send a message (destination key in bytes) -await meshcore.commands.send_msg(dst_key, "Hello!") +result = await meshcore.commands.send_msg(dst_key, "Hello!") +if result.type == EventType.ERROR: + print(f"Error sending message: {result.payload}") # Setting device parameters -await meshcore.commands.set_name("My Device") -await meshcore.commands.set_tx_power(20) # Set transmit power +result = await meshcore.commands.set_name("My Device") +if result.type == EventType.ERROR: + print(f"Error setting name: {result.payload}") + +result = await meshcore.commands.set_tx_power(20) # Set transmit power +if result.type == EventType.ERROR: + print(f"Error setting TX power: {result.payload}") ``` ### Finding Contacts @@ -151,7 +206,11 @@ async def send_and_confirm_message(meshcore, dst_key, message): sent_result = await meshcore.commands.send_msg(dst_key, message) # Extract the expected acknowledgment code from the message sent event - expected_ack = sent_result["expected_ack"].hex() + if sent_result.type == EventType.ERROR: + print(f"Error sending message: {sent_result.payload}") + return False + + expected_ack = sent_result.payload["expected_ack"].hex() print(f"Message sent, waiting for ack with code: {expected_ack}") # Wait specifically for this acknowledgment @@ -196,7 +255,10 @@ async def main(): while True: # Send command (returns battery level) result = await meshcore.commands.get_bat() - print(f"Battery check initiated, response: {result}") + if result.type == EventType.ERROR: + print(f"Error checking battery: {result.payload}") + else: + print(f"Battery level: {result.payload.get('level', 'unknown')}%") await asyncio.sleep(60) # Wait 60 seconds between checks # Start the background task @@ -257,24 +319,36 @@ Commands that require a destination (`send_msg`, `send_login`, `send_statusreq`, ```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": - # 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 once more Alice!") - break +result = await meshcore.commands.get_contacts() +if result.type == EventType.ERROR: + print(f"Error getting contacts: {result.payload}") +else: + contacts = result.payload + for key, contact in contacts.items(): + if contact["adv_name"] == "Alice": + # Option 1: Pass the contact object directly + result = await meshcore.commands.send_msg(contact, "Hello Alice!") + if result.type == EventType.ERROR: + print(f"Error sending message: {result.payload}") + + # Option 2: Use the public key string + result = await meshcore.commands.send_msg(contact["public_key"], "Hello again Alice!") + if result.type == EventType.ERROR: + print(f"Error sending message: {result.payload}") + + # Option 3 (backward compatible): Convert the hex key to bytes + dst_key = bytes.fromhex(contact["public_key"]) + result = await meshcore.commands.send_msg(dst_key, "Hello once more Alice!") + if result.type == EventType.ERROR: + print(f"Error sending message: {result.payload}") + 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!") + result = await meshcore.commands.send_msg(contact, "Hello Bob!") + if result.type == EventType.ERROR: + print(f"Error sending message: {result.payload}") ``` ### Monitoring Channel Messages diff --git a/examples/ble_chat.py b/examples/ble_chat.py index 2779d2b..2f02d47 100755 --- a/examples/ble_chat.py +++ b/examples/ble_chat.py @@ -69,8 +69,12 @@ async def main () : else : if line.startswith("send") : line = line[5:] - ret = await mc.commands.send_msg(contact , line) - exp_ack = ret["expected_ack"].hex() + result = await mc.commands.send_msg(contact, line) + if result.type == EventType.ERROR: + print(f"⚠️ Failed to send message: {result.payload}") + continue + + exp_ack = result.payload["expected_ack"].hex() print(" Sent ... ", end="", flush=True) res = await mc.wait_for_event(EventType.ACK, attribute_filters={"code": exp_ack}, timeout=5) if res is None : diff --git a/examples/pubsub_example.py b/examples/pubsub_example.py index e72d175..dde5860 100644 --- a/examples/pubsub_example.py +++ b/examples/pubsub_example.py @@ -45,7 +45,11 @@ async def main(): print("Connected to MeshCore device") # Get contacts - contacts = await meshcore.commands.get_contacts() + result = await meshcore.commands.get_contacts() + if result.type == EventType.ERROR: + print(f"Error fetching contacts: {result.payload}") + return + contacts = result.payload if contacts: print(f"\nFound {len(contacts)} contacts:") for name, contact in contacts.items(): diff --git a/examples/serial_chat.py b/examples/serial_chat.py index a42dde8..d1923d2 100755 --- a/examples/serial_chat.py +++ b/examples/serial_chat.py @@ -17,7 +17,9 @@ async def handle_message(event): data = event.payload contact = mc.get_contact_by_key_prefix(data['pubkey_prefix']) - + if contact is None: + print(f"Unknown contact with pubkey prefix: {data['pubkey_prefix']}") + return print(f"{contact['adv_name']}: {data['text']}") async def main () : @@ -54,7 +56,7 @@ async def main () : if line.startswith("to ") : dest = line[3:] nc = mc.get_contact_by_name(dest) - if mc is None: + if nc is None: print(f"Contact '{DEST}' not found in contacts.") return else : @@ -72,8 +74,12 @@ async def main () : else : if line.startswith("send") : line = line[5:] - ret = await mc.commands.send_msg(contact , line) - exp_ack = ret["expected_ack"].hex() + result = await mc.commands.send_msg(contact, line) + if result.type == EventType.ERROR: + print(f"⚠️ Failed to send message: {result.payload}") + continue + + exp_ack = result.payload["expected_ack"].hex() print(" Sent ... ", end="", flush=True) res = await mc.wait_for_event(EventType.ACK, attribute_filters={"code": exp_ack}, timeout=5) if res is None : @@ -82,7 +88,7 @@ async def main () : print ("Ack") except KeyboardInterrupt: - meshcore.stop() + mc.stop() print("\nExiting...") except asyncio.CancelledError: # Handle task cancellation from KeyboardInterrupt in asyncio.run() diff --git a/examples/serial_contacts.py b/examples/serial_contacts.py index 9b07a02..5035913 100755 --- a/examples/serial_contacts.py +++ b/examples/serial_contacts.py @@ -5,6 +5,7 @@ import json from meshcore import MeshCore from meshcore import SerialConnection +from meshcore import EventType PORT = "/dev/ttyUSB0" BAUDRATE = 115200 @@ -16,6 +17,10 @@ async def main () : mc = MeshCore(con) await mc.connect() - print(json.dumps(await mc.commands.get_contacts(),indent=4)) + result = await mc.commands.get_contacts() + if result.type == EventType.ERROR: + print(f"Error getting contacts: {result.payload}") + else: + print(json.dumps(result.payload, indent=4)) asyncio.run(main()) diff --git a/examples/serial_msg.py b/examples/serial_msg.py index a5a055d..00a3fe1 100755 --- a/examples/serial_msg.py +++ b/examples/serial_msg.py @@ -37,13 +37,17 @@ 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( + result = await mc.commands.send_msg( contact, args.message ) + if result.type == EventType.ERROR: + print(f"⚠️ Failed to send message: {result.payload}") + return + # Extract the expected ACK code - expected_ack = send_result["expected_ack"].hex() + expected_ack = result.payload["expected_ack"].hex() print(f"Message sent, waiting for ACK with code: {expected_ack}") # Wait for the specific ACK that matches our message diff --git a/examples/serial_repeater_status.py b/examples/serial_repeater_status.py index 9e02d50..cdc3267 100755 --- a/examples/serial_repeater_status.py +++ b/examples/serial_repeater_status.py @@ -32,7 +32,7 @@ async def main(): print(f"Logging in to repeater '{args.repeater}'...") login_event = await mc.commands.send_login(repeater, args.password) - if login_event and login_event.get("success") != False: + if login_event.type != EventType.ERROR: print("Login successful") # Send status request diff --git a/examples/serial_trace.py b/examples/serial_trace.py index b169edd..98d8edc 100644 --- a/examples/serial_trace.py +++ b/examples/serial_trace.py @@ -34,10 +34,10 @@ async def main(): tag = random.randint(1, 0xFFFFFFFF) result = await mc.commands.send_trace(path=args.path, tag=tag) - # Check if the result has a success indicator - if result.get("success") == False: - print(f"Failed to send trace packet: {result.get('reason', 'unknown error')}") - elif result: + # Check if the result is an error + if result.type == EventType.ERROR: + print(f"Failed to send trace packet: {result.payload.get('reason', 'unknown error')}") + elif result.type == EventType.MSG_SENT: print(f"Trace packet sent successfully with tag={tag}") print("Waiting for trace response matching our tag...") diff --git a/examples/tcp_chat.py b/examples/tcp_chat.py index ef273e0..fc8d02a 100755 --- a/examples/tcp_chat.py +++ b/examples/tcp_chat.py @@ -17,7 +17,9 @@ async def handle_message(event): data = event.payload contact = mc.get_contact_by_key_prefix(data['pubkey_prefix']) - + if contact is None: + print(f"Unknown contact with pubkey prefix: {data['pubkey_prefix']}") + return print(f"{contact['adv_name']}: {data['text']}") async def main () : @@ -54,7 +56,7 @@ async def main () : if line.startswith("to ") : dest = line[3:] nc = mc.get_contact_by_name(dest) - if mc is None: + if nc is None: print(f"Contact '{DEST}' not found in contacts.") return else : @@ -72,8 +74,12 @@ async def main () : else : if line.startswith("send") : line = line[5:] - ret = await mc.commands.send_msg(contact , line) - exp_ack = ret["expected_ack"].hex() + result = await mc.commands.send_msg(contact, line) + if result.type == EventType.ERROR: + print(f"⚠️ Failed to send message: {result.payload}") + continue + + exp_ack = result.payload["expected_ack"].hex() print(" Sent ... ", end="", flush=True) res = await mc.wait_for_event(EventType.ACK, attribute_filters={"code": exp_ack}, timeout=5) if res is None : @@ -82,7 +88,7 @@ async def main () : print ("Ack") except KeyboardInterrupt: - meshcore.stop() + mc.stop() print("\nExiting...") except asyncio.CancelledError: # Handle task cancellation from KeyboardInterrupt in asyncio.run() diff --git a/examples/tcp_mchome_contacts.py b/examples/tcp_mchome_contacts.py index a4f71f2..ca96306 100755 --- a/examples/tcp_mchome_contacts.py +++ b/examples/tcp_mchome_contacts.py @@ -4,6 +4,7 @@ import asyncio import json from meshcore import TCPConnection from meshcore import MeshCore +from meshcore import EventType HOSTNAME = "mchome" PORT = 5000 @@ -14,5 +15,9 @@ async def main () : mc = MeshCore(con) await mc.connect() - print(json.dumps(await mc.commands.get_contacts(),indent=4)) + result = await mc.commands.get_contacts() + if result.type == EventType.ERROR: + print(f"Error getting contacts: {result.payload}") + else: + print(json.dumps(result.payload, indent=4)) asyncio.run(main()) diff --git a/examples/tcp_mchome_msg.py b/examples/tcp_mchome_msg.py index de2da44..25aeb77 100755 --- a/examples/tcp_mchome_msg.py +++ b/examples/tcp_mchome_msg.py @@ -22,9 +22,14 @@ async def main () : if contact is None: print(f"Contact '{DEST}' not found in contacts.") return - ret = await mc.commands.send_msg(contact ,MSG) - print (ret) - exp_ack = ret["expected_ack"].hex() + result = await mc.commands.send_msg(contact, MSG) + print(result) + + if result.type == EventType.ERROR: + print(f"⚠️ Failed to send message: {result.payload}") + return + + exp_ack = result.payload["expected_ack"].hex() print(await mc.wait_for_event(EventType.ACK, attribute_filters={"code": exp_ack}, timeout=5)) asyncio.run(main()) diff --git a/examples/tcp_mchome_readmsgs.py b/examples/tcp_mchome_readmsgs.py index 59b2cde..79bd92c 100755 --- a/examples/tcp_mchome_readmsgs.py +++ b/examples/tcp_mchome_readmsgs.py @@ -4,6 +4,7 @@ import asyncio import json from meshcore import TCPConnection from meshcore import MeshCore +from meshcore import EventType HOSTNAME = "mchome" PORT = 5000 @@ -19,9 +20,12 @@ async def main () : res = True while res: result = await mc.commands.get_msg() - if result.get("success") == False: + if result.type == EventType.NO_MORE_MSGS: res = False print("No more messages") - print (result) + elif result.type == EventType.ERROR: + res = False + print(f"Error retrieving messages: {result.payload}") + print(result) asyncio.run(main()) diff --git a/src/meshcore/commands.py b/src/meshcore/commands.py index 8e27fea..729533d 100644 --- a/src/meshcore/commands.py +++ b/src/meshcore/commands.py @@ -1,8 +1,8 @@ import asyncio import logging -from typing import Any, Dict, List, Optional, Union -from .events import EventType import random +from typing import Any, Dict, List, Optional, Union +from .events import Event, EventType # Define types for destination parameters DestinationType = Union[bytes, str, Dict[str, Any]] @@ -66,7 +66,7 @@ class CommandHandler: self.dispatcher = dispatcher async def send(self, data: bytes, expected_events: Optional[Union[EventType, List[EventType]]] = None, - timeout: Optional[float] = None) -> Dict[str, Any]: + timeout: Optional[float] = None) -> Event: """ Send a command and wait for expected event responses. @@ -76,7 +76,7 @@ class CommandHandler: timeout: Timeout in seconds, or None to use default_timeout Returns: - Dict[str, Any]: Dictionary containing the response data or status + Event: The full event object that was received in response to the command """ if not self.dispatcher: raise RuntimeError("Dispatcher not set, cannot send commands") @@ -119,66 +119,68 @@ class CommandHandler: for future in done: event = await future if event: - return event.payload + return event - return {"success": False, "reason": "no_event_received"} + # Create an error event when no event is received + return Event(EventType.ERROR, {"reason": "no_event_received"}) except asyncio.TimeoutError: logger.debug(f"Command timed out {data}") - return {"success": False, "reason": "timeout"} + return Event(EventType.ERROR, {"reason": "timeout"}) except Exception as e: logger.debug(f"Command error: {e}") - return {"error": str(e)} - return {"success": True} + return Event(EventType.ERROR, {"error": str(e)}) + # For commands that don't expect events, return a success event + return Event(EventType.OK, {}) - async def send_appstart(self) -> Dict[str, Any]: + async def send_appstart(self) -> Event: logger.debug("Sending appstart command") b1 = bytearray(b'\x01\x03 mccli') return await self.send(b1, [EventType.SELF_INFO]) - async def send_device_query(self) -> Dict[str, Any]: + async def send_device_query(self) -> Event: logger.debug("Sending device query command") return await self.send(b"\x16\x03", [EventType.DEVICE_INFO, EventType.ERROR]) - async def send_advert(self, flood: bool = False) -> Dict[str, Any]: + async def send_advert(self, flood: bool = False) -> Event: logger.debug(f"Sending advertisement command (flood={flood})") if flood: return await self.send(b"\x07\x01", [EventType.OK, EventType.ERROR]) else: return await self.send(b"\x07", [EventType.OK, EventType.ERROR]) - async def set_name(self, name: str) -> Dict[str, Any]: + async def set_name(self, name: str) -> Event: logger.debug(f"Setting device name to: {name}") return await self.send(b'\x08' + name.encode("ascii"), [EventType.OK, EventType.ERROR]) - async def set_coords(self, lat: float, lon: float) -> Dict[str, Any]: + async def set_coords(self, lat: float, lon: float) -> Event: logger.debug(f"Setting coordinates to: lat={lat}, lon={lon}") return await self.send(b'\x0e'\ + int(lat*1e6).to_bytes(4, 'little', signed=True)\ + int(lon*1e6).to_bytes(4, 'little', signed=True)\ + int(0).to_bytes(4, 'little'), [EventType.OK, EventType.ERROR]) - async def reboot(self) -> Dict[str, Any]: + async def reboot(self) -> Event: logger.debug("Sending reboot command") return await self.send(b'\x13reboot') - async def get_bat(self) -> Dict[str, Any]: + async def get_bat(self) -> Event: logger.debug("Getting battery information") return await self.send(b'\x14', [EventType.BATTERY, EventType.ERROR]) - async def get_time(self) -> Dict[str, Any]: + async def get_time(self) -> Event: logger.debug("Getting device time") return await self.send(b"\x05", [EventType.CURRENT_TIME, EventType.ERROR]) - async def set_time(self, val: int) -> Dict[str, Any]: + async def set_time(self, val: int) -> Event: logger.debug(f"Setting device time to: {val}") return await self.send(b"\x06" + int(val).to_bytes(4, 'little'), [EventType.OK, EventType.ERROR]) - async def set_tx_power(self, val: int) -> Dict[str, Any]: + async def set_tx_power(self, val: int) -> Event: logger.debug(f"Setting TX power to: {val}") return await self.send(b"\x0c" + int(val).to_bytes(4, 'little'), [EventType.OK, EventType.ERROR]) - async def set_radio(self, freq: float, bw: float, sf: int, cr: int) -> Dict[str, Any]: + async def set_radio(self, freq: float, bw: float, sf: int, cr: int) -> Event: logger.debug(f"Setting radio params: freq={freq}, bw={bw}, sf={sf}, cr={cr}") return await self.send(b"\x0b" \ + int(float(freq)*1000).to_bytes(4, 'little')\ @@ -186,7 +188,7 @@ class CommandHandler: + int(sf).to_bytes(1, 'little')\ + int(cr).to_bytes(1, 'little'), [EventType.OK, EventType.ERROR]) - async def set_tuning(self, rx_dly: int, af: int) -> Dict[str, Any]: + async def set_tuning(self, rx_dly: int, af: int) -> Event: logger.debug(f"Setting tuning params: rx_dly={rx_dly}, af={af}") return await self.send(b"\x15" \ + int(rx_dly).to_bytes(4, 'little')\ @@ -194,28 +196,28 @@ class CommandHandler: + int(0).to_bytes(1, 'little')\ + int(0).to_bytes(1, 'little'), [EventType.OK, EventType.ERROR]) - async def set_devicepin(self, pin: int) -> Dict[str, Any]: + async def set_devicepin(self, pin: int) -> Event: logger.debug(f"Setting device PIN to: {pin}") return await self.send(b"\x25" \ + int(pin).to_bytes(4, 'little'), [EventType.OK, EventType.ERROR]) - async def get_contacts(self) -> Dict[str, Any]: + async def get_contacts(self) -> Event: logger.debug("Getting contacts") return await self.send(b"\x04", [EventType.CONTACTS, EventType.ERROR]) - async def reset_path(self, key: DestinationType) -> Dict[str, Any]: + async def reset_path(self, key: DestinationType) -> Event: key_bytes = _validate_destination(key, prefix_length=32) logger.debug(f"Resetting path for contact: {key_bytes.hex()}") data = b"\x0D" + key_bytes return await self.send(data, [EventType.OK, EventType.ERROR]) - async def share_contact(self, key: DestinationType) -> Dict[str, Any]: + async def share_contact(self, key: DestinationType) -> Event: key_bytes = _validate_destination(key, prefix_length=32) logger.debug(f"Sharing contact: {key_bytes.hex()}") data = b"\x10" + key_bytes return await self.send(data, [EventType.CONTACT_SHARE, EventType.ERROR]) - async def export_contact(self, key: Optional[DestinationType] = None) -> Dict[str, Any]: + async def export_contact(self, key: Optional[DestinationType] = None) -> Event: if key: key_bytes = _validate_destination(key, prefix_length=32) logger.debug(f"Exporting contact: {key_bytes.hex()}") @@ -225,13 +227,13 @@ class CommandHandler: data = b"\x11" return await self.send(data, [EventType.OK, EventType.ERROR]) - async def remove_contact(self, key: DestinationType) -> Dict[str, Any]: + async def remove_contact(self, key: DestinationType) -> Event: key_bytes = _validate_destination(key, prefix_length=32) logger.debug(f"Removing contact: {key_bytes.hex()}") data = b"\x0f" + key_bytes return await self.send(data, [EventType.OK, EventType.ERROR]) - async def change_contact_path (self, contact, path) -> Dict[str, Any]: + async def change_contact_path (self, contact, path) -> Event: out_path_hex = path out_path_len = int(len(path) / 2) out_path_hex = out_path_hex + (128-len(out_path_hex)) * "0" @@ -249,29 +251,29 @@ class CommandHandler: + int(contact["adv_lon"]*1e6).to_bytes(4, 'little', signed=True) return await self.send(data, [EventType.OK, EventType.ERROR]) - async def get_msg(self, timeout: Optional[float] = 1) -> Dict[str, Any]: + async def get_msg(self, timeout: Optional[float] = 1) -> Event: 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: DestinationType, pwd: str) -> Dict[str, Any]: + async def send_login(self, dst: DestinationType, pwd: str) -> Event: dst_bytes = _validate_destination(dst, prefix_length=32) 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: DestinationType) -> Dict[str, Any]: + async def send_logout(self, dst: DestinationType) -> Event: dst_bytes = _validate_destination(dst) self.login_resp = asyncio.Future() data = b"\x1d" + dst_bytes return await self.send(data, [EventType.MSG_SENT, EventType.ERROR]) - async def send_statusreq(self, dst: DestinationType) -> Dict[str, Any]: + async def send_statusreq(self, dst: DestinationType) -> Event: dst_bytes = _validate_destination(dst, prefix_length=32) 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: DestinationType, cmd: str, timestamp: Optional[int] = None) -> Dict[str, Any]: + async def send_cmd(self, dst: DestinationType, cmd: str, timestamp: Optional[int] = None) -> Event: dst_bytes = _validate_destination(dst) logger.debug(f"Sending command to {dst_bytes.hex()}: {cmd}") @@ -282,7 +284,7 @@ class CommandHandler: 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: DestinationType, msg: str, timestamp: Optional[int] = None) -> Dict[str, Any]: + async def send_msg(self, dst: DestinationType, msg: str, timestamp: Optional[int] = None) -> Event: dst_bytes = _validate_destination(dst) logger.debug(f"Sending message to {dst_bytes.hex()}: {msg}") @@ -293,7 +295,7 @@ class CommandHandler: 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): + async def send_chan_msg(self, chan, msg, timestamp=None) -> Event: logger.debug(f"Sending channel message to channel {chan}: {msg}") # Default to current time if timestamp not provided @@ -304,13 +306,13 @@ class CommandHandler: data = b"\x03\x00" + chan.to_bytes(1, 'little') + timestamp + msg.encode("ascii") return await self.send(data, [EventType.MSG_SENT, EventType.ERROR]) - async def send_cli(self, cmd): + async def send_cli(self, cmd) -> Event: logger.debug(f"Sending CLI command: {cmd}") data = b"\x32" + cmd.encode('ascii') return await self.send(data, [EventType.CLI_RESPONSE, EventType.ERROR]) async def send_trace(self, auth_code: int = 0, tag: Optional[int] = None, - flags: int = 0, path: Optional[Union[str, bytes, bytearray]] = None) -> Dict[str, Any]: + flags: int = 0, path: Optional[Union[str, bytes, bytearray]] = None) -> Event: """ Send a trace packet to test routing through specific repeaters @@ -322,7 +324,7 @@ class CommandHandler: or a bytes/bytearray object with the raw path data Returns: - Dictionary with sent status, tag, and estimated timeout in milliseconds, or False if command failed + Event object with sent status, tag, and estimated timeout in milliseconds """ # Generate random tag if not provided if tag is None: @@ -350,11 +352,11 @@ class CommandHandler: cmd_data.extend(path_bytes) except ValueError as e: logger.error(f"Invalid path format: {e}") - return { "success": False, "reason": "invalid_path_format" } + return Event(EventType.ERROR, {"reason": "invalid_path_format"}) elif isinstance(path, (bytes, bytearray)): cmd_data.extend(path) else: logger.error(f"Unsupported path type: {type(path)}") - return { "success": False, "reason": "unsupported_path_type" } + return Event(EventType.ERROR, {"reason": "unsupported_path_type"}) return await self.send(cmd_data, [EventType.MSG_SENT, EventType.ERROR]) diff --git a/src/meshcore/meshcore.py b/src/meshcore/meshcore.py index 9d065f0..e52959d 100644 --- a/src/meshcore/meshcore.py +++ b/src/meshcore/meshcore.py @@ -262,7 +262,8 @@ class MeshCore: result = await self.commands.get_msg() # If we got a NO_MORE_MSGS event or an error, stop fetching - if not result.get("success") or isinstance(result, dict) and "error" in result: + if result.type == EventType.NO_MORE_MSGS or result.type == EventType.ERROR: + logger.debug("No more messages or error occurred, stopping auto-fetch.") break # Small delay to prevent overwhelming the device diff --git a/src/meshcore/reader.py b/src/meshcore/reader.py index ebcdc03..23289f0 100644 --- a/src/meshcore/reader.py +++ b/src/meshcore/reader.py @@ -22,7 +22,7 @@ class MessageReader: # Handle command responses if packet_type_value == PacketType.OK.value: - result: Dict[str, Any] = {"success": True} + result: Dict[str, Any] = {} if len(data) == 5: result["value"] = int.from_bytes(data[1:5], byteorder='little') @@ -31,9 +31,9 @@ class MessageReader: elif packet_type_value == PacketType.ERROR.value: if len(data) > 1: - result = {"success": False, "error_code": data[1]} + result = {"error_code": data[1]} else: - result = {"success": False} + result = {} # Dispatch event for the ERROR response await self.dispatcher.dispatch(Event(EventType.ERROR, result)) diff --git a/src/meshcore/tcp_cx.py b/src/meshcore/tcp_cx.py index 25c38c8..e90fb2a 100644 --- a/src/meshcore/tcp_cx.py +++ b/src/meshcore/tcp_cx.py @@ -45,7 +45,10 @@ class TCPConnection: self.host, self.port) logger.info("TCP Connection started") - return self.host + future = asyncio.Future() + future.set_result(self.host) + + return future def set_reader(self, reader) : self.reader = reader diff --git a/tests/unit/test_commands.py b/tests/unit/test_commands.py index c37cead..49e2c39 100644 --- a/tests/unit/test_commands.py +++ b/tests/unit/test_commands.py @@ -47,22 +47,25 @@ def setup_event_response(mock_dispatcher, event_type, payload, attribute_filters async def test_send_basic(command_handler, mock_connection): result = await command_handler.send(b"test_data") mock_connection.send.assert_called_once_with(b"test_data") - assert result == {"success": True} + assert result.type == EventType.OK + assert result.payload == {} async def test_send_with_event(command_handler, mock_connection, mock_dispatcher): - expected_payload = {"success": True, "value": 42} + expected_payload = {"value": 42} setup_event_response(mock_dispatcher, EventType.OK, expected_payload) result = await command_handler.send(b"test_command", [EventType.OK]) mock_connection.send.assert_called_once_with(b"test_command") - assert result == expected_payload + assert result.type == EventType.OK + assert result.payload == expected_payload async def test_send_timeout(command_handler, mock_connection, mock_dispatcher): mock_dispatcher.wait_for_event.side_effect = asyncio.TimeoutError result = await command_handler.send(b"test_command", [EventType.OK], timeout=0.1) - assert result == {"success": False, "reason": "timeout"} + assert result.type == EventType.ERROR + assert result.payload == {"reason": "timeout"} # Destination validation tests async def test_validate_destination_bytes(command_handler, mock_connection): @@ -235,7 +238,7 @@ async def test_send_trace(command_handler, mock_connection): async def test_send_with_multiple_expected_events_returns_first_completed(command_handler, mock_connection, mock_dispatcher): # Setup the dispatcher to return an ERROR event - error_payload = {"success": False, "reason": "command_failed"} + error_payload = {"reason": "command_failed"} async def simulate_error_event(*args, **kwargs): # Simulate an ERROR event being returned @@ -251,4 +254,5 @@ async def test_send_with_multiple_expected_events_returns_first_completed(comman mock_connection.send.assert_called_once_with(b"test_command") # Verify that even though OK was listed first, the ERROR event was returned - assert result == error_payload \ No newline at end of file + assert result.type == EventType.ERROR + assert result.payload == error_payload \ No newline at end of file