Change contract for commands to return full event

This commit is contained in:
Alex Wolden 2025-04-14 11:10:59 -07:00
parent 39ea3cb3f3
commit 6fbf15885d
17 changed files with 231 additions and 104 deletions

126
README.md
View file

@ -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

View file

@ -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 :

View file

@ -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():

View file

@ -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()

View file

@ -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())

View file

@ -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

View file

@ -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

View file

@ -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...")

View file

@ -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()

View file

@ -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())

View file

@ -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())

View file

@ -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())

View file

@ -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])

View file

@ -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

View file

@ -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))

View file

@ -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

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):
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
assert result.type == EventType.ERROR
assert result.payload == error_payload