Merge pull request #5 from fdlamotte/return-full-event

Change contract for commands to return full event
This commit is contained in:
fdlamotte
2025-04-14 21:40:47 +02:00
committed by GitHub
17 changed files with 231 additions and 104 deletions

122
README.md
View File

@@ -21,7 +21,12 @@ async def main():
meshcore = await MeshCore.create_serial("/dev/ttyUSB0") meshcore = await MeshCore.create_serial("/dev/ttyUSB0")
# Get your contacts # 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") print(f"Found {len(contacts)} contacts")
# Send a message to the first contact # Send a message to the first contact
@@ -30,7 +35,12 @@ async def main():
contact = next(iter(contacts.items()))[1] contact = next(iter(contacts.items()))[1]
# Pass the contact object directly to send_msg # 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() await meshcore.disconnect()
@@ -55,6 +65,37 @@ python examples/pubsub_example.py -p /dev/ttyUSB0
## Usage Guide ## 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 ### Connecting to Your Device
Connect via Serial, BLE, or TCP: Connect via Serial, BLE, or TCP:
@@ -76,20 +117,34 @@ Send commands and wait for responses:
```python ```python
# Get device information # Get device information
device_info = await meshcore.commands.send_device_query() result = await meshcore.commands.send_device_query()
print(f"Device model: {device_info['model']}") 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 # Get list of contacts
contacts = await meshcore.commands.get_contacts() result = await meshcore.commands.get_contacts()
for contact_id, contact in contacts.items(): if result.type == EventType.ERROR:
print(f"Contact: {contact['adv_name']} ({contact_id})") 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) # 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 # Setting device parameters
await meshcore.commands.set_name("My Device") result = await meshcore.commands.set_name("My Device")
await meshcore.commands.set_tx_power(20) # Set transmit power 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 ### 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) sent_result = await meshcore.commands.send_msg(dst_key, message)
# Extract the expected acknowledgment code from the message sent event # 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}") print(f"Message sent, waiting for ack with code: {expected_ack}")
# Wait specifically for this acknowledgment # Wait specifically for this acknowledgment
@@ -196,7 +255,10 @@ async def main():
while True: while True:
# Send command (returns battery level) # Send command (returns battery level)
result = await meshcore.commands.get_bat() 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 await asyncio.sleep(60) # Wait 60 seconds between checks
# Start the background task # Start the background task
@@ -257,24 +319,36 @@ Commands that require a destination (`send_msg`, `send_login`, `send_statusreq`,
```python ```python
# Get contacts and send to a specific one # Get contacts and send to a specific one
contacts = await meshcore.commands.get_contacts() result = await meshcore.commands.get_contacts()
for key, contact in contacts.items(): if result.type == EventType.ERROR:
if contact["adv_name"] == "Alice": print(f"Error getting contacts: {result.payload}")
# Option 1: Pass the contact object directly else:
await meshcore.commands.send_msg(contact, "Hello Alice!") 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 # Option 2: Use the public key string
await meshcore.commands.send_msg(contact["public_key"], "Hello again Alice!") 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 # Option 3 (backward compatible): Convert the hex key to bytes
dst_key = bytes.fromhex(contact["public_key"]) dst_key = bytes.fromhex(contact["public_key"])
await meshcore.commands.send_msg(dst_key, "Hello once more Alice!") result = await meshcore.commands.send_msg(dst_key, "Hello once more Alice!")
break if result.type == EventType.ERROR:
print(f"Error sending message: {result.payload}")
break
# You can also directly use a contact found by name # You can also directly use a contact found by name
contact = meshcore.get_contact_by_name("Bob") contact = meshcore.get_contact_by_name("Bob")
if contact: 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 ### Monitoring Channel Messages

View File

@@ -69,8 +69,12 @@ async def main () :
else : else :
if line.startswith("send") : if line.startswith("send") :
line = line[5:] line = line[5:]
ret = await mc.commands.send_msg(contact , line) result = await mc.commands.send_msg(contact, line)
exp_ack = ret["expected_ack"].hex() 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) print(" Sent ... ", end="", flush=True)
res = await mc.wait_for_event(EventType.ACK, attribute_filters={"code": exp_ack}, timeout=5) res = await mc.wait_for_event(EventType.ACK, attribute_filters={"code": exp_ack}, timeout=5)
if res is None : if res is None :

View File

@@ -45,7 +45,11 @@ async def main():
print("Connected to MeshCore device") print("Connected to MeshCore device")
# Get contacts # 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: if contacts:
print(f"\nFound {len(contacts)} contacts:") print(f"\nFound {len(contacts)} contacts:")
for name, contact in contacts.items(): for name, contact in contacts.items():

View File

@@ -17,7 +17,9 @@ async def handle_message(event):
data = event.payload data = event.payload
contact = mc.get_contact_by_key_prefix(data['pubkey_prefix']) 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']}") print(f"{contact['adv_name']}: {data['text']}")
async def main () : async def main () :
@@ -54,7 +56,7 @@ async def main () :
if line.startswith("to ") : if line.startswith("to ") :
dest = line[3:] dest = line[3:]
nc = mc.get_contact_by_name(dest) nc = mc.get_contact_by_name(dest)
if mc is None: if nc is None:
print(f"Contact '{DEST}' not found in contacts.") print(f"Contact '{DEST}' not found in contacts.")
return return
else : else :
@@ -72,8 +74,12 @@ async def main () :
else : else :
if line.startswith("send") : if line.startswith("send") :
line = line[5:] line = line[5:]
ret = await mc.commands.send_msg(contact , line) result = await mc.commands.send_msg(contact, line)
exp_ack = ret["expected_ack"].hex() 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) print(" Sent ... ", end="", flush=True)
res = await mc.wait_for_event(EventType.ACK, attribute_filters={"code": exp_ack}, timeout=5) res = await mc.wait_for_event(EventType.ACK, attribute_filters={"code": exp_ack}, timeout=5)
if res is None : if res is None :
@@ -82,7 +88,7 @@ async def main () :
print ("Ack") print ("Ack")
except KeyboardInterrupt: except KeyboardInterrupt:
meshcore.stop() mc.stop()
print("\nExiting...") print("\nExiting...")
except asyncio.CancelledError: except asyncio.CancelledError:
# Handle task cancellation from KeyboardInterrupt in asyncio.run() # Handle task cancellation from KeyboardInterrupt in asyncio.run()

View File

@@ -5,6 +5,7 @@ import json
from meshcore import MeshCore from meshcore import MeshCore
from meshcore import SerialConnection from meshcore import SerialConnection
from meshcore import EventType
PORT = "/dev/ttyUSB0" PORT = "/dev/ttyUSB0"
BAUDRATE = 115200 BAUDRATE = 115200
@@ -16,6 +17,10 @@ async def main () :
mc = MeshCore(con) mc = MeshCore(con)
await mc.connect() 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()) asyncio.run(main())

View File

@@ -37,13 +37,17 @@ async def main():
# Send the message and get the MSG_SENT event # Send the message and get the MSG_SENT event
print(f"Sending message: '{args.message}'") print(f"Sending message: '{args.message}'")
send_result = await mc.commands.send_msg( result = await mc.commands.send_msg(
contact, contact,
args.message args.message
) )
if result.type == EventType.ERROR:
print(f"⚠️ Failed to send message: {result.payload}")
return
# Extract the expected ACK code # 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}") print(f"Message sent, waiting for ACK with code: {expected_ack}")
# Wait for the specific ACK that matches our message # Wait for the specific ACK that matches our message

View File

@@ -32,7 +32,7 @@ async def main():
print(f"Logging in to repeater '{args.repeater}'...") print(f"Logging in to repeater '{args.repeater}'...")
login_event = await mc.commands.send_login(repeater, args.password) 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") print("Login successful")
# Send status request # Send status request

View File

@@ -34,10 +34,10 @@ async def main():
tag = random.randint(1, 0xFFFFFFFF) tag = random.randint(1, 0xFFFFFFFF)
result = await mc.commands.send_trace(path=args.path, tag=tag) result = await mc.commands.send_trace(path=args.path, tag=tag)
# Check if the result has a success indicator # Check if the result is an error
if result.get("success") == False: if result.type == EventType.ERROR:
print(f"Failed to send trace packet: {result.get('reason', 'unknown error')}") print(f"Failed to send trace packet: {result.payload.get('reason', 'unknown error')}")
elif result: elif result.type == EventType.MSG_SENT:
print(f"Trace packet sent successfully with tag={tag}") print(f"Trace packet sent successfully with tag={tag}")
print("Waiting for trace response matching our tag...") print("Waiting for trace response matching our tag...")

View File

@@ -17,7 +17,9 @@ async def handle_message(event):
data = event.payload data = event.payload
contact = mc.get_contact_by_key_prefix(data['pubkey_prefix']) 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']}") print(f"{contact['adv_name']}: {data['text']}")
async def main () : async def main () :
@@ -54,7 +56,7 @@ async def main () :
if line.startswith("to ") : if line.startswith("to ") :
dest = line[3:] dest = line[3:]
nc = mc.get_contact_by_name(dest) nc = mc.get_contact_by_name(dest)
if mc is None: if nc is None:
print(f"Contact '{DEST}' not found in contacts.") print(f"Contact '{DEST}' not found in contacts.")
return return
else : else :
@@ -72,8 +74,12 @@ async def main () :
else : else :
if line.startswith("send") : if line.startswith("send") :
line = line[5:] line = line[5:]
ret = await mc.commands.send_msg(contact , line) result = await mc.commands.send_msg(contact, line)
exp_ack = ret["expected_ack"].hex() 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) print(" Sent ... ", end="", flush=True)
res = await mc.wait_for_event(EventType.ACK, attribute_filters={"code": exp_ack}, timeout=5) res = await mc.wait_for_event(EventType.ACK, attribute_filters={"code": exp_ack}, timeout=5)
if res is None : if res is None :
@@ -82,7 +88,7 @@ async def main () :
print ("Ack") print ("Ack")
except KeyboardInterrupt: except KeyboardInterrupt:
meshcore.stop() mc.stop()
print("\nExiting...") print("\nExiting...")
except asyncio.CancelledError: except asyncio.CancelledError:
# Handle task cancellation from KeyboardInterrupt in asyncio.run() # Handle task cancellation from KeyboardInterrupt in asyncio.run()

View File

@@ -4,6 +4,7 @@ import asyncio
import json import json
from meshcore import TCPConnection from meshcore import TCPConnection
from meshcore import MeshCore from meshcore import MeshCore
from meshcore import EventType
HOSTNAME = "mchome" HOSTNAME = "mchome"
PORT = 5000 PORT = 5000
@@ -14,5 +15,9 @@ async def main () :
mc = MeshCore(con) mc = MeshCore(con)
await mc.connect() 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()) asyncio.run(main())

View File

@@ -22,9 +22,14 @@ async def main () :
if contact is None: if contact is None:
print(f"Contact '{DEST}' not found in contacts.") print(f"Contact '{DEST}' not found in contacts.")
return return
ret = await mc.commands.send_msg(contact ,MSG) result = await mc.commands.send_msg(contact, MSG)
print (ret) print(result)
exp_ack = ret["expected_ack"].hex()
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)) print(await mc.wait_for_event(EventType.ACK, attribute_filters={"code": exp_ack}, timeout=5))
asyncio.run(main()) asyncio.run(main())

View File

@@ -4,6 +4,7 @@ import asyncio
import json import json
from meshcore import TCPConnection from meshcore import TCPConnection
from meshcore import MeshCore from meshcore import MeshCore
from meshcore import EventType
HOSTNAME = "mchome" HOSTNAME = "mchome"
PORT = 5000 PORT = 5000
@@ -19,9 +20,12 @@ async def main () :
res = True res = True
while res: while res:
result = await mc.commands.get_msg() result = await mc.commands.get_msg()
if result.get("success") == False: if result.type == EventType.NO_MORE_MSGS:
res = False res = False
print("No more messages") 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()) asyncio.run(main())

View File

@@ -1,8 +1,8 @@
import asyncio import asyncio
import logging import logging
from typing import Any, Dict, List, Optional, Union
from .events import EventType
import random import random
from typing import Any, Dict, List, Optional, Union
from .events import Event, EventType
# Define types for destination parameters # Define types for destination parameters
DestinationType = Union[bytes, str, Dict[str, Any]] DestinationType = Union[bytes, str, Dict[str, Any]]
@@ -66,7 +66,7 @@ class CommandHandler:
self.dispatcher = dispatcher self.dispatcher = dispatcher
async def send(self, data: bytes, expected_events: Optional[Union[EventType, List[EventType]]] = None, 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. 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 timeout: Timeout in seconds, or None to use default_timeout
Returns: 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: if not self.dispatcher:
raise RuntimeError("Dispatcher not set, cannot send commands") raise RuntimeError("Dispatcher not set, cannot send commands")
@@ -119,66 +119,68 @@ class CommandHandler:
for future in done: for future in done:
event = await future event = await future
if event: 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: except asyncio.TimeoutError:
logger.debug(f"Command timed out {data}") logger.debug(f"Command timed out {data}")
return {"success": False, "reason": "timeout"} return Event(EventType.ERROR, {"reason": "timeout"})
except Exception as e: except Exception as e:
logger.debug(f"Command error: {e}") logger.debug(f"Command error: {e}")
return {"error": str(e)} return Event(EventType.ERROR, {"error": str(e)})
return {"success": True} # 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") logger.debug("Sending appstart command")
b1 = bytearray(b'\x01\x03 mccli') b1 = bytearray(b'\x01\x03 mccli')
return await self.send(b1, [EventType.SELF_INFO]) 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") logger.debug("Sending device query command")
return await self.send(b"\x16\x03", [EventType.DEVICE_INFO, EventType.ERROR]) 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})") logger.debug(f"Sending advertisement command (flood={flood})")
if flood: if flood:
return await self.send(b"\x07\x01", [EventType.OK, EventType.ERROR]) return await self.send(b"\x07\x01", [EventType.OK, EventType.ERROR])
else: else:
return await self.send(b"\x07", [EventType.OK, EventType.ERROR]) 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}") logger.debug(f"Setting device name to: {name}")
return await self.send(b'\x08' + name.encode("ascii"), [EventType.OK, EventType.ERROR]) 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}") logger.debug(f"Setting coordinates to: lat={lat}, lon={lon}")
return await self.send(b'\x0e'\ return await self.send(b'\x0e'\
+ int(lat*1e6).to_bytes(4, 'little', signed=True)\ + int(lat*1e6).to_bytes(4, 'little', signed=True)\
+ int(lon*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]) + 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") logger.debug("Sending reboot command")
return await self.send(b'\x13reboot') 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") logger.debug("Getting battery information")
return await self.send(b'\x14', [EventType.BATTERY, EventType.ERROR]) 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") logger.debug("Getting device time")
return await self.send(b"\x05", [EventType.CURRENT_TIME, EventType.ERROR]) 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}") logger.debug(f"Setting device time to: {val}")
return await self.send(b"\x06" + int(val).to_bytes(4, 'little'), [EventType.OK, EventType.ERROR]) 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}") logger.debug(f"Setting TX power to: {val}")
return await self.send(b"\x0c" + int(val).to_bytes(4, 'little'), [EventType.OK, EventType.ERROR]) 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}") logger.debug(f"Setting radio params: freq={freq}, bw={bw}, sf={sf}, cr={cr}")
return await self.send(b"\x0b" \ return await self.send(b"\x0b" \
+ int(float(freq)*1000).to_bytes(4, 'little')\ + int(float(freq)*1000).to_bytes(4, 'little')\
@@ -186,7 +188,7 @@ class CommandHandler:
+ int(sf).to_bytes(1, 'little')\ + int(sf).to_bytes(1, 'little')\
+ int(cr).to_bytes(1, 'little'), [EventType.OK, EventType.ERROR]) + 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}") logger.debug(f"Setting tuning params: rx_dly={rx_dly}, af={af}")
return await self.send(b"\x15" \ return await self.send(b"\x15" \
+ int(rx_dly).to_bytes(4, 'little')\ + 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')\
+ int(0).to_bytes(1, 'little'), [EventType.OK, EventType.ERROR]) + 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}") logger.debug(f"Setting device PIN to: {pin}")
return await self.send(b"\x25" \ return await self.send(b"\x25" \
+ int(pin).to_bytes(4, 'little'), [EventType.OK, EventType.ERROR]) + 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") logger.debug("Getting contacts")
return await self.send(b"\x04", [EventType.CONTACTS, EventType.ERROR]) 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) key_bytes = _validate_destination(key, prefix_length=32)
logger.debug(f"Resetting path for contact: {key_bytes.hex()}") logger.debug(f"Resetting path for contact: {key_bytes.hex()}")
data = b"\x0D" + key_bytes data = b"\x0D" + key_bytes
return await self.send(data, [EventType.OK, EventType.ERROR]) 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) key_bytes = _validate_destination(key, prefix_length=32)
logger.debug(f"Sharing contact: {key_bytes.hex()}") logger.debug(f"Sharing contact: {key_bytes.hex()}")
data = b"\x10" + key_bytes data = b"\x10" + key_bytes
return await self.send(data, [EventType.CONTACT_SHARE, EventType.ERROR]) 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: if key:
key_bytes = _validate_destination(key, prefix_length=32) key_bytes = _validate_destination(key, prefix_length=32)
logger.debug(f"Exporting contact: {key_bytes.hex()}") logger.debug(f"Exporting contact: {key_bytes.hex()}")
@@ -225,13 +227,13 @@ class CommandHandler:
data = b"\x11" data = b"\x11"
return await self.send(data, [EventType.OK, EventType.ERROR]) 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) key_bytes = _validate_destination(key, prefix_length=32)
logger.debug(f"Removing contact: {key_bytes.hex()}") logger.debug(f"Removing contact: {key_bytes.hex()}")
data = b"\x0f" + key_bytes data = b"\x0f" + key_bytes
return await self.send(data, [EventType.OK, EventType.ERROR]) 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_hex = path
out_path_len = int(len(path) / 2) out_path_len = int(len(path) / 2)
out_path_hex = out_path_hex + (128-len(out_path_hex)) * "0" 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) + int(contact["adv_lon"]*1e6).to_bytes(4, 'little', signed=True)
return await self.send(data, [EventType.OK, EventType.ERROR]) 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") logger.debug("Requesting pending messages")
return await self.send(b"\x0A", [EventType.CONTACT_MSG_RECV, EventType.CHANNEL_MSG_RECV, EventType.ERROR], timeout) 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) dst_bytes = _validate_destination(dst, prefix_length=32)
logger.debug(f"Sending login request to: {dst_bytes.hex()}") logger.debug(f"Sending login request to: {dst_bytes.hex()}")
data = b"\x1a" + dst_bytes + pwd.encode("ascii") data = b"\x1a" + dst_bytes + pwd.encode("ascii")
return await self.send(data, [EventType.MSG_SENT, EventType.ERROR]) 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) dst_bytes = _validate_destination(dst)
self.login_resp = asyncio.Future() self.login_resp = asyncio.Future()
data = b"\x1d" + dst_bytes data = b"\x1d" + dst_bytes
return await self.send(data, [EventType.MSG_SENT, EventType.ERROR]) 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) dst_bytes = _validate_destination(dst, prefix_length=32)
logger.debug(f"Sending status request to: {dst_bytes.hex()}") logger.debug(f"Sending status request to: {dst_bytes.hex()}")
data = b"\x1b" + dst_bytes data = b"\x1b" + dst_bytes
return await self.send(data, [EventType.MSG_SENT, EventType.ERROR]) 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) dst_bytes = _validate_destination(dst)
logger.debug(f"Sending command to {dst_bytes.hex()}: {cmd}") 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") data = b"\x02\x01\x00" + timestamp.to_bytes(4, 'little') + dst_bytes + cmd.encode("ascii")
return await self.send(data, [EventType.OK, EventType.ERROR]) 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) dst_bytes = _validate_destination(dst)
logger.debug(f"Sending message to {dst_bytes.hex()}: {msg}") 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") 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]) 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}") logger.debug(f"Sending channel message to channel {chan}: {msg}")
# Default to current time if timestamp not provided # 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") data = b"\x03\x00" + chan.to_bytes(1, 'little') + timestamp + msg.encode("ascii")
return await self.send(data, [EventType.MSG_SENT, EventType.ERROR]) 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}") logger.debug(f"Sending CLI command: {cmd}")
data = b"\x32" + cmd.encode('ascii') data = b"\x32" + cmd.encode('ascii')
return await self.send(data, [EventType.CLI_RESPONSE, EventType.ERROR]) return await self.send(data, [EventType.CLI_RESPONSE, EventType.ERROR])
async def send_trace(self, auth_code: int = 0, tag: Optional[int] = None, 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 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 or a bytes/bytearray object with the raw path data
Returns: 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 # Generate random tag if not provided
if tag is None: if tag is None:
@@ -350,11 +352,11 @@ class CommandHandler:
cmd_data.extend(path_bytes) cmd_data.extend(path_bytes)
except ValueError as e: except ValueError as e:
logger.error(f"Invalid path format: {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)): elif isinstance(path, (bytes, bytearray)):
cmd_data.extend(path) cmd_data.extend(path)
else: else:
logger.error(f"Unsupported path type: {type(path)}") 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]) return await self.send(cmd_data, [EventType.MSG_SENT, EventType.ERROR])

View File

@@ -262,7 +262,8 @@ class MeshCore:
result = await self.commands.get_msg() result = await self.commands.get_msg()
# If we got a NO_MORE_MSGS event or an error, stop fetching # 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 break
# Small delay to prevent overwhelming the device # Small delay to prevent overwhelming the device

View File

@@ -22,7 +22,7 @@ class MessageReader:
# Handle command responses # Handle command responses
if packet_type_value == PacketType.OK.value: if packet_type_value == PacketType.OK.value:
result: Dict[str, Any] = {"success": True} result: Dict[str, Any] = {}
if len(data) == 5: if len(data) == 5:
result["value"] = int.from_bytes(data[1:5], byteorder='little') result["value"] = int.from_bytes(data[1:5], byteorder='little')
@@ -31,9 +31,9 @@ class MessageReader:
elif packet_type_value == PacketType.ERROR.value: elif packet_type_value == PacketType.ERROR.value:
if len(data) > 1: if len(data) > 1:
result = {"success": False, "error_code": data[1]} result = {"error_code": data[1]}
else: else:
result = {"success": False} result = {}
# Dispatch event for the ERROR response # Dispatch event for the ERROR response
await self.dispatcher.dispatch(Event(EventType.ERROR, result)) await self.dispatcher.dispatch(Event(EventType.ERROR, result))

View File

@@ -45,7 +45,10 @@ class TCPConnection:
self.host, self.port) self.host, self.port)
logger.info("TCP Connection started") logger.info("TCP Connection started")
return self.host future = asyncio.Future()
future.set_result(self.host)
return future
def set_reader(self, reader) : def set_reader(self, reader) :
self.reader = reader self.reader = reader

View File

@@ -47,22 +47,25 @@ def setup_event_response(mock_dispatcher, event_type, payload, attribute_filters
async def test_send_basic(command_handler, mock_connection): async def test_send_basic(command_handler, mock_connection):
result = await command_handler.send(b"test_data") result = await command_handler.send(b"test_data")
mock_connection.send.assert_called_once_with(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): 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) setup_event_response(mock_dispatcher, EventType.OK, expected_payload)
result = await command_handler.send(b"test_command", [EventType.OK]) result = await command_handler.send(b"test_command", [EventType.OK])
mock_connection.send.assert_called_once_with(b"test_command") 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): async def test_send_timeout(command_handler, mock_connection, mock_dispatcher):
mock_dispatcher.wait_for_event.side_effect = asyncio.TimeoutError mock_dispatcher.wait_for_event.side_effect = asyncio.TimeoutError
result = await command_handler.send(b"test_command", [EventType.OK], timeout=0.1) 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 # Destination validation tests
async def test_validate_destination_bytes(command_handler, mock_connection): 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): 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 # 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): async def simulate_error_event(*args, **kwargs):
# Simulate an ERROR event being returned # 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") mock_connection.send.assert_called_once_with(b"test_command")
# Verify that even though OK was listed first, the ERROR event was returned # Verify that even though OK was listed first, the ERROR event was returned
assert result == error_payload assert result.type == EventType.ERROR
assert result.payload == error_payload