mirror of
https://github.com/meshcore-dev/meshcore_py.git
synced 2026-06-11 11:56:18 +00:00
@@ -21,7 +21,7 @@ license-files = ["LICEN[CS]E*"]
|
|||||||
dependencies = [ "bleak", "pyserial-asyncio", "pycayennelpp" ]
|
dependencies = [ "bleak", "pyserial-asyncio", "pycayennelpp" ]
|
||||||
|
|
||||||
[project.optional-dependencies]
|
[project.optional-dependencies]
|
||||||
dev = ["pytest", "pytest-asyncio"]
|
dev = ["pytest", "pytest-asyncio", "black", "ruff"]
|
||||||
|
|
||||||
[project.urls]
|
[project.urls]
|
||||||
Homepage = "https://github.com/fdlamotte/meshcore_py"
|
Homepage = "https://github.com/fdlamotte/meshcore_py"
|
||||||
|
|||||||
@@ -1,11 +1,23 @@
|
|||||||
|
"""A library for communicating with meshcore devices."""
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
from .ble_cx import BLEConnection
|
||||||
|
from .connection_manager import ConnectionManager
|
||||||
|
from .events import EventType
|
||||||
|
from .meshcore import MeshCore
|
||||||
|
from .serial_cx import SerialConnection
|
||||||
|
from .tcp_cx import TCPConnection
|
||||||
|
|
||||||
# Setup default logger
|
# Setup default logger
|
||||||
logging.basicConfig(level=logging.INFO)
|
logging.basicConfig(level=logging.INFO)
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
from meshcore.events import EventType
|
__all__ = [
|
||||||
from meshcore.meshcore import MeshCore, logger
|
"BLEConnection",
|
||||||
from meshcore.connection_manager import ConnectionManager
|
"ConnectionManager",
|
||||||
from meshcore.tcp_cx import TCPConnection
|
"EventType",
|
||||||
from meshcore.ble_cx import BLEConnection
|
"MeshCore",
|
||||||
from meshcore.serial_cx import SerialConnection
|
"SerialConnection",
|
||||||
|
"TCPConnection",
|
||||||
|
"logger",
|
||||||
|
]
|
||||||
|
|||||||
@@ -1,116 +0,0 @@
|
|||||||
import asyncio
|
|
||||||
import logging
|
|
||||||
from enum import Enum
|
|
||||||
import json
|
|
||||||
from .events import Event, EventType
|
|
||||||
from cayennelpp import LppFrame, LppData
|
|
||||||
from cayennelpp.lpp_type import LppType
|
|
||||||
from meshcore.lpp_json_encoder import lpp_json_encoder, my_lpp_types, lpp_format_val
|
|
||||||
|
|
||||||
logger = logging.getLogger("meshcore")
|
|
||||||
|
|
||||||
class BinaryReqType(Enum):
|
|
||||||
TELEMETRY = 0x03
|
|
||||||
MMA = 0x04
|
|
||||||
ACL = 0x05
|
|
||||||
|
|
||||||
def lpp_parse(buf):
|
|
||||||
"""Parse a given byte string and return as a LppFrame object."""
|
|
||||||
i = 0
|
|
||||||
lpp_data_list = []
|
|
||||||
while i < len(buf) and buf[i] != 0:
|
|
||||||
lppdata = LppData.from_bytes(buf[i:])
|
|
||||||
lpp_data_list.append(lppdata)
|
|
||||||
i = i + len(lppdata)
|
|
||||||
|
|
||||||
return json.loads(json.dumps(LppFrame(lpp_data_list), default=lpp_json_encoder))
|
|
||||||
|
|
||||||
def lpp_parse_mma (buf):
|
|
||||||
i = 0
|
|
||||||
res = []
|
|
||||||
while i < len(buf) and buf[i] != 0:
|
|
||||||
chan = buf[i]
|
|
||||||
i = i + 1
|
|
||||||
type = buf[i]
|
|
||||||
lpp_type = LppType.get_lpp_type(type)
|
|
||||||
size = lpp_type.size
|
|
||||||
i = i + 1
|
|
||||||
min = lpp_format_val(lpp_type, lpp_type.decode(buf[i:i+size]))
|
|
||||||
i = i + size
|
|
||||||
max = lpp_format_val(lpp_type, lpp_type.decode(buf[i:i+size]))
|
|
||||||
i = i + size
|
|
||||||
avg = lpp_format_val(lpp_type, lpp_type.decode(buf[i:i+size]))
|
|
||||||
i = i + size
|
|
||||||
res.append({"channel":chan,
|
|
||||||
"type":my_lpp_types[type][0],
|
|
||||||
"min":min,
|
|
||||||
"max":max,
|
|
||||||
"avg":avg,
|
|
||||||
})
|
|
||||||
return res
|
|
||||||
|
|
||||||
def parse_acl (buf):
|
|
||||||
i = 0
|
|
||||||
res = []
|
|
||||||
while i + 7 <= len(buf):
|
|
||||||
key = buf[i:i+6].hex()
|
|
||||||
perm = buf[i+6]
|
|
||||||
if (key != "000000000000"):
|
|
||||||
res.append({"key": key, "perm": perm})
|
|
||||||
i = i + 7
|
|
||||||
return res
|
|
||||||
|
|
||||||
class BinaryCommandHandler :
|
|
||||||
""" Helper functions to handle binary requests through binary commands """
|
|
||||||
def __init__ (self, c):
|
|
||||||
self.commands = c
|
|
||||||
|
|
||||||
@property
|
|
||||||
def dispatcher(self):
|
|
||||||
return self.commands.dispatcher
|
|
||||||
|
|
||||||
async def req_binary (self, contact, request, timeout=0) :
|
|
||||||
res = await self.commands.send_binary_req(contact, request)
|
|
||||||
logger.debug(res)
|
|
||||||
if res.type == EventType.ERROR:
|
|
||||||
logger.error(f"Error while requesting binary data")
|
|
||||||
return None
|
|
||||||
else:
|
|
||||||
exp_tag = res.payload["expected_ack"].hex()
|
|
||||||
timeout = res.payload["suggested_timeout"]/800 if timeout == 0 else timeout
|
|
||||||
res2 = await self.dispatcher.wait_for_event(EventType.BINARY_RESPONSE, attribute_filters={"tag": exp_tag}, timeout=timeout)
|
|
||||||
logger.debug(res2)
|
|
||||||
if res2 is None :
|
|
||||||
return None
|
|
||||||
else:
|
|
||||||
return res2.payload
|
|
||||||
|
|
||||||
async def req_telemetry (self, contact, timeout=0) :
|
|
||||||
code = BinaryReqType.TELEMETRY.value
|
|
||||||
req = code.to_bytes(1, 'little', signed=False)
|
|
||||||
res = await self.req_binary(contact, req, timeout)
|
|
||||||
if (res is None) :
|
|
||||||
return None
|
|
||||||
else:
|
|
||||||
return lpp_parse(bytes.fromhex(res["data"]))
|
|
||||||
|
|
||||||
async def req_mma (self, contact, start, end, timeout=0) :
|
|
||||||
code = BinaryReqType.MMA.value
|
|
||||||
req = code.to_bytes(1, 'little', signed=False)\
|
|
||||||
+ start.to_bytes(4, 'little', signed = False)\
|
|
||||||
+ end.to_bytes(4, 'little', signed=False)\
|
|
||||||
+ b"\0\0"
|
|
||||||
res = await self.req_binary(contact, req, timeout)
|
|
||||||
if (res is None) :
|
|
||||||
return None
|
|
||||||
else:
|
|
||||||
return lpp_parse_mma(bytes.fromhex(res["data"])[4:])
|
|
||||||
|
|
||||||
async def req_acl (self, contact, timeout=0) :
|
|
||||||
code = BinaryReqType.ACL.value
|
|
||||||
req = code.to_bytes(1, 'little', signed=False) + b"\0\0"
|
|
||||||
res = await self.req_binary(contact, req, timeout)
|
|
||||||
if (res is None) :
|
|
||||||
return None
|
|
||||||
else:
|
|
||||||
return parse_acl(bytes.fromhex(res['data']))
|
|
||||||
@@ -1,491 +0,0 @@
|
|||||||
import asyncio
|
|
||||||
import logging
|
|
||||||
import random
|
|
||||||
from typing import Any, Dict, List, Optional, Union
|
|
||||||
from .events import Event, EventType
|
|
||||||
from .binary_commands import BinaryCommandHandler
|
|
||||||
|
|
||||||
# Define types for destination parameters
|
|
||||||
DestinationType = Union[bytes, str, Dict[str, Any]]
|
|
||||||
|
|
||||||
logger = logging.getLogger("meshcore")
|
|
||||||
|
|
||||||
def _validate_destination(dst: DestinationType, prefix_length: int = 6) -> bytes:
|
|
||||||
"""
|
|
||||||
Validates and converts a destination to a bytes object.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
dst: The destination, which can be:
|
|
||||||
- str: Hex string representation of a public key
|
|
||||||
- dict: Contact object with a "public_key" field
|
|
||||||
prefix_length: The length of the prefix to use (default: 6 bytes)
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
bytes: The destination public key as a bytes object
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
ValueError: If dst is invalid or doesn't contain required fields
|
|
||||||
"""
|
|
||||||
if isinstance(dst, bytes):
|
|
||||||
# Already bytes, use directly
|
|
||||||
return dst[:prefix_length]
|
|
||||||
elif isinstance(dst, str):
|
|
||||||
# Hex string, convert to bytes
|
|
||||||
try:
|
|
||||||
return bytes.fromhex(dst)[:prefix_length]
|
|
||||||
except ValueError:
|
|
||||||
raise ValueError(f"Invalid public key hex string: {dst}")
|
|
||||||
elif isinstance(dst, dict):
|
|
||||||
# Contact object, extract public_key
|
|
||||||
if "public_key" not in dst:
|
|
||||||
raise ValueError("Contact object must have a 'public_key' field")
|
|
||||||
try:
|
|
||||||
return bytes.fromhex(dst["public_key"])[:prefix_length]
|
|
||||||
except ValueError:
|
|
||||||
raise ValueError(f"Invalid public_key in contact: {dst['public_key']}")
|
|
||||||
else:
|
|
||||||
raise ValueError(f"Destination must be a public key string or contact object, got: {type(dst)}")
|
|
||||||
|
|
||||||
class CommandHandler:
|
|
||||||
DEFAULT_TIMEOUT = 5.0
|
|
||||||
|
|
||||||
def __init__(self, default_timeout: Optional[float] = None):
|
|
||||||
self._sender_func = None
|
|
||||||
self._reader = None
|
|
||||||
self.dispatcher = None
|
|
||||||
self.binary = BinaryCommandHandler(self)
|
|
||||||
self.default_timeout = default_timeout if default_timeout is not None else self.DEFAULT_TIMEOUT
|
|
||||||
|
|
||||||
def set_connection(self, connection: Any) -> None:
|
|
||||||
async def sender(data: bytes) -> None:
|
|
||||||
await connection.send(data)
|
|
||||||
self._sender_func = sender
|
|
||||||
|
|
||||||
def set_reader(self, reader: Any) -> None:
|
|
||||||
self._reader = reader
|
|
||||||
|
|
||||||
def set_dispatcher(self, dispatcher: Any) -> None:
|
|
||||||
self.dispatcher = dispatcher
|
|
||||||
|
|
||||||
async def send(self, data: bytes, expected_events: Optional[Union[EventType, List[EventType]]] = None,
|
|
||||||
timeout: Optional[float] = None) -> Event:
|
|
||||||
"""
|
|
||||||
Send a command and wait for expected event responses.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
data: The data to send
|
|
||||||
expected_events: EventType or list of EventTypes to wait for
|
|
||||||
timeout: Timeout in seconds, or None to use default_timeout
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
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")
|
|
||||||
|
|
||||||
# Use the provided timeout or fall back to default_timeout
|
|
||||||
timeout = timeout if timeout is not None else self.default_timeout
|
|
||||||
|
|
||||||
if self._sender_func:
|
|
||||||
logger.debug(f"Sending raw data: {data.hex() if isinstance(data, bytes) else data}")
|
|
||||||
await self._sender_func(data)
|
|
||||||
|
|
||||||
if expected_events:
|
|
||||||
try:
|
|
||||||
# Convert single event to list if needed
|
|
||||||
if not isinstance(expected_events, list):
|
|
||||||
expected_events = [expected_events]
|
|
||||||
|
|
||||||
logger.debug(f"Waiting for events {expected_events}, timeout={timeout}")
|
|
||||||
|
|
||||||
# Create futures for all expected events
|
|
||||||
futures = []
|
|
||||||
for event_type in expected_events:
|
|
||||||
future = asyncio.create_task(
|
|
||||||
self.dispatcher.wait_for_event(event_type, {}, timeout)
|
|
||||||
)
|
|
||||||
futures.append(future)
|
|
||||||
|
|
||||||
# Wait for the first event to complete or all to timeout
|
|
||||||
done, pending = await asyncio.wait(
|
|
||||||
futures,
|
|
||||||
timeout=timeout,
|
|
||||||
return_when=asyncio.FIRST_COMPLETED
|
|
||||||
)
|
|
||||||
|
|
||||||
# Cancel all pending futures
|
|
||||||
for future in pending:
|
|
||||||
future.cancel()
|
|
||||||
|
|
||||||
# Check if any future completed successfully
|
|
||||||
for future in done:
|
|
||||||
event = await future
|
|
||||||
if event:
|
|
||||||
return event
|
|
||||||
|
|
||||||
# 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 Event(EventType.ERROR, {"reason": "timeout"})
|
|
||||||
except Exception as e:
|
|
||||||
logger.debug(f"Command error: {e}")
|
|
||||||
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) -> 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) -> 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) -> 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) -> Event:
|
|
||||||
logger.debug(f"Setting device name to: {name}")
|
|
||||||
return await self.send(b'\x08' + name.encode("utf-8"), [EventType.OK, EventType.ERROR])
|
|
||||||
|
|
||||||
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) -> Event:
|
|
||||||
logger.debug("Sending reboot command")
|
|
||||||
return await self.send(b'\x13reboot')
|
|
||||||
|
|
||||||
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) -> Event:
|
|
||||||
logger.debug("Getting device time")
|
|
||||||
return await self.send(b"\x05", [EventType.CURRENT_TIME, EventType.ERROR])
|
|
||||||
|
|
||||||
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) -> 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) -> 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')\
|
|
||||||
+ int(float(bw)*1000).to_bytes(4, 'little')\
|
|
||||||
+ 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) -> 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')\
|
|
||||||
+ int(af).to_bytes(4, 'little')\
|
|
||||||
+ int(0).to_bytes(1, 'little')\
|
|
||||||
+ int(0).to_bytes(1, 'little'), [EventType.OK, EventType.ERROR])
|
|
||||||
|
|
||||||
async def set_other_params(self, manual_add_contacts : bool, telemetry_mode_base : int, telemetry_mode_loc : int, telemetry_mode_env : int, advert_loc_policy : int) :
|
|
||||||
telemetry_mode = (telemetry_mode_base & 0b11) | ((telemetry_mode_loc & 0b11) << 2) | ((telemetry_mode_env & 0b11) << 4)
|
|
||||||
data = b"\x26" + manual_add_contacts.to_bytes(1) + telemetry_mode.to_bytes(1) + advert_loc_policy.to_bytes(1)
|
|
||||||
return await self.send(data, [EventType.OK, EventType.ERROR])
|
|
||||||
|
|
||||||
async def set_telemetry_mode_base(self, telemetry_mode_base : int) :
|
|
||||||
infos = (await self.send_appstart()).payload
|
|
||||||
return await self.set_other_params(
|
|
||||||
infos["manual_add_contacts"],
|
|
||||||
telemetry_mode_base,
|
|
||||||
infos["telemetry_mode_loc"],
|
|
||||||
infos["telemetry_mode_env"],
|
|
||||||
infos["adv_loc_policy"])
|
|
||||||
|
|
||||||
async def set_telemetry_mode_loc(self, telemetry_mode_loc : int) :
|
|
||||||
infos = (await self.send_appstart()).payload
|
|
||||||
return await self.set_other_params(
|
|
||||||
infos["manual_add_contacts"],
|
|
||||||
infos["telemetry_mode_base"],
|
|
||||||
telemetry_mode_loc,
|
|
||||||
infos["telemetry_mode_env"],
|
|
||||||
infos["adv_loc_policy"])
|
|
||||||
|
|
||||||
async def set_telemetry_mode_env(self, telemetry_mode_env : int) :
|
|
||||||
infos = (await self.send_appstart()).payload
|
|
||||||
return await self.set_other_params(
|
|
||||||
infos["manual_add_contacts"],
|
|
||||||
infos["telemetry_mode_base"],
|
|
||||||
infos["telemetry_mode_loc"],
|
|
||||||
telemetry_mode_env,
|
|
||||||
infos["adv_loc_policy"])
|
|
||||||
|
|
||||||
async def set_manual_add_contacts(self, manual_add_contacts:bool) :
|
|
||||||
infos = (await self.send_appstart()).payload
|
|
||||||
return await self.set_other_params(
|
|
||||||
manual_add_contacts,
|
|
||||||
infos["telemetry_mode_base"],
|
|
||||||
infos["telemetry_mode_loc"],
|
|
||||||
infos["telemetry_mode_env"],
|
|
||||||
infos["adv_loc_policy"])
|
|
||||||
|
|
||||||
async def set_advert_loc_policy(self, advert_loc_policy:int) :
|
|
||||||
infos = (await self.send_appstart()).payload
|
|
||||||
return await self.set_other_params(
|
|
||||||
infos["manual_add_contacts"],
|
|
||||||
infos["telemetry_mode_base"],
|
|
||||||
infos["telemetry_mode_loc"],
|
|
||||||
infos["telemetry_mode_env"],
|
|
||||||
advert_loc_policy)
|
|
||||||
|
|
||||||
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, lastmod=0) -> Event:
|
|
||||||
logger.debug("Getting contacts")
|
|
||||||
data=b"\x04"
|
|
||||||
if lastmod > 0:
|
|
||||||
data = data + lastmod.to_bytes(4, 'little')
|
|
||||||
return await self.send(data, [EventType.CONTACTS, EventType.ERROR])
|
|
||||||
|
|
||||||
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) -> 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.OK, EventType.ERROR])
|
|
||||||
|
|
||||||
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()}")
|
|
||||||
data = b"\x11" + key_bytes
|
|
||||||
else:
|
|
||||||
logger.debug("Exporting node")
|
|
||||||
data = b"\x11"
|
|
||||||
return await self.send(data, [EventType.CONTACT_URI, EventType.ERROR])
|
|
||||||
|
|
||||||
async def import_contact(self, card_data) -> Event:
|
|
||||||
data = b"\x12" + card_data
|
|
||||||
return await self.send(data, [EventType.OK, EventType.ERROR])
|
|
||||||
|
|
||||||
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 update_contact (self, contact, path=None, flags=None) -> Event:
|
|
||||||
if path is None :
|
|
||||||
out_path_hex = contact["out_path"]
|
|
||||||
out_path_len = contact["out_path_len"]
|
|
||||||
else :
|
|
||||||
out_path_hex = path
|
|
||||||
out_path_len = int(len(path) / 2)
|
|
||||||
# reflect the change
|
|
||||||
contact["out_path"] = out_path_hex
|
|
||||||
contact["out_path_len"] = out_path_len
|
|
||||||
out_path_hex = out_path_hex + (128-len(out_path_hex)) * "0"
|
|
||||||
|
|
||||||
if flags is None :
|
|
||||||
flags = contact["flags"]
|
|
||||||
else :
|
|
||||||
# reflect the change
|
|
||||||
contact["flags"] = flags
|
|
||||||
|
|
||||||
adv_name_hex = contact["adv_name"].encode().hex()
|
|
||||||
adv_name_hex = adv_name_hex + (64-len(adv_name_hex)) * "0"
|
|
||||||
data = b"\x09" \
|
|
||||||
+ bytes.fromhex(contact["public_key"])\
|
|
||||||
+ contact["type"].to_bytes(1)\
|
|
||||||
+ flags.to_bytes(1)\
|
|
||||||
+ out_path_len.to_bytes(1, 'little', signed=True)\
|
|
||||||
+ bytes.fromhex(out_path_hex)\
|
|
||||||
+ bytes.fromhex(adv_name_hex)\
|
|
||||||
+ contact["last_advert"].to_bytes(4, 'little')\
|
|
||||||
+ int(contact["adv_lat"]*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])
|
|
||||||
|
|
||||||
async def add_contact (self, contact) -> Event:
|
|
||||||
return await self.update_contact(contact)
|
|
||||||
|
|
||||||
async def change_contact_path (self, contact, path) -> Event:
|
|
||||||
return await self.update_contact(contact, path)
|
|
||||||
|
|
||||||
async def change_contact_flags (self, contact, flags) -> Event:
|
|
||||||
return await self.update_contact(contact, flags=flags)
|
|
||||||
|
|
||||||
async def get_msg(self, timeout: Optional[float] = None) -> Event:
|
|
||||||
logger.debug("Requesting pending messages")
|
|
||||||
return await self.send(b"\x0A", [EventType.CONTACT_MSG_RECV, EventType.CHANNEL_MSG_RECV, EventType.ERROR, EventType.NO_MORE_MSGS], timeout)
|
|
||||||
|
|
||||||
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("utf-8")
|
|
||||||
return await self.send(data, [EventType.MSG_SENT, EventType.ERROR])
|
|
||||||
|
|
||||||
async def send_logout(self, dst: DestinationType) -> Event:
|
|
||||||
dst_bytes = _validate_destination(dst, prefix_length=32)
|
|
||||||
self.login_resp = asyncio.Future()
|
|
||||||
data = b"\x1d" + dst_bytes
|
|
||||||
return await self.send(data, [EventType.OK, EventType.ERROR])
|
|
||||||
|
|
||||||
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) -> Event:
|
|
||||||
dst_bytes = _validate_destination(dst)
|
|
||||||
logger.debug(f"Sending command to {dst_bytes.hex()}: {cmd}")
|
|
||||||
|
|
||||||
if timestamp is None:
|
|
||||||
import time
|
|
||||||
timestamp = int(time.time())
|
|
||||||
|
|
||||||
data = b"\x02\x01\x00" + timestamp.to_bytes(4, 'little') + dst_bytes + cmd.encode("utf-8")
|
|
||||||
return await self.send(data, [EventType.MSG_SENT, EventType.ERROR])
|
|
||||||
|
|
||||||
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}")
|
|
||||||
|
|
||||||
if timestamp is None:
|
|
||||||
import time
|
|
||||||
timestamp = int(time.time())
|
|
||||||
|
|
||||||
data = b"\x02\x00\x00" + timestamp.to_bytes(4, 'little') + dst_bytes + msg.encode("utf-8")
|
|
||||||
return await self.send(data, [EventType.MSG_SENT, EventType.ERROR])
|
|
||||||
|
|
||||||
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
|
|
||||||
if timestamp is None:
|
|
||||||
import time
|
|
||||||
timestamp = int(time.time()).to_bytes(4, 'little')
|
|
||||||
|
|
||||||
data = b"\x03\x00" + chan.to_bytes(1, 'little') + timestamp + msg.encode("utf-8")
|
|
||||||
return await self.send(data, [EventType.OK, EventType.ERROR])
|
|
||||||
|
|
||||||
async def send_telemetry_req(self, dst: DestinationType) -> Event :
|
|
||||||
dst_bytes = _validate_destination(dst, prefix_length=32)
|
|
||||||
logger.debug(f"Asking telemetry to {dst_bytes.hex()}")
|
|
||||||
data = b"\x27\x00\x00\x00" + dst_bytes
|
|
||||||
return await self.send(data, [EventType.MSG_SENT, EventType.ERROR])
|
|
||||||
|
|
||||||
async def send_binary_req(self, dst: DestinationType, bin_data) -> Event :
|
|
||||||
dst_bytes = _validate_destination(dst, prefix_length=32)
|
|
||||||
logger.debug(f"Binary request to {dst_bytes.hex()}")
|
|
||||||
data = b"\x32" + dst_bytes + bin_data
|
|
||||||
return await self.send(data, [EventType.MSG_SENT, EventType.ERROR])
|
|
||||||
|
|
||||||
async def send_path_discovery(self, dst: DestinationType) -> Event :
|
|
||||||
dst_bytes = _validate_destination(dst, prefix_length=32)
|
|
||||||
logger.debug(f"Path discovery request for {dst_bytes.hex()}")
|
|
||||||
data = b"\x34\x00" + dst_bytes
|
|
||||||
return await self.send(data, [EventType.MSG_SENT, EventType.ERROR])
|
|
||||||
|
|
||||||
async def get_self_telemetry(self) -> Event :
|
|
||||||
logger.debug(f"Getting self telemetry")
|
|
||||||
data = b"\x27\x00\x00\x00"
|
|
||||||
return await self.send(data, [EventType.TELEMETRY_RESPONSE, EventType.ERROR])
|
|
||||||
|
|
||||||
async def get_custom_vars(self) -> Event:
|
|
||||||
logger.debug(f"Asking for custom vars")
|
|
||||||
data = b"\x28"
|
|
||||||
return await self.send(data, [EventType.CUSTOM_VARS, EventType.ERROR])
|
|
||||||
|
|
||||||
async def set_custom_var(self, key, value) -> Event:
|
|
||||||
logger.debug(f"Setting custom var {key} to {value}")
|
|
||||||
data = b"\x29" + key.encode("utf-8") + b":" + value.encode("utf-8")
|
|
||||||
return await self.send(data, [EventType.OK, EventType.ERROR])
|
|
||||||
|
|
||||||
async def get_channel(self, channel_idx: int) -> Event:
|
|
||||||
logger.debug(f"Getting channel info for channel {channel_idx}")
|
|
||||||
data = b"\x1f" + channel_idx.to_bytes(1, 'little')
|
|
||||||
return await self.send(data, [EventType.CHANNEL_INFO, EventType.ERROR])
|
|
||||||
|
|
||||||
async def set_channel(self, channel_idx: int, channel_name: str, channel_secret: bytes) -> Event:
|
|
||||||
logger.debug(f"Setting channel {channel_idx}: name={channel_name}")
|
|
||||||
|
|
||||||
# Pad channel name to 32 bytes
|
|
||||||
name_bytes = channel_name.encode('utf-8')[:32]
|
|
||||||
name_bytes = name_bytes.ljust(32, b'\x00')
|
|
||||||
|
|
||||||
# Ensure channel secret is exactly 16 bytes
|
|
||||||
if len(channel_secret) != 16:
|
|
||||||
raise ValueError("Channel secret must be exactly 16 bytes")
|
|
||||||
|
|
||||||
data = b"\x20" + channel_idx.to_bytes(1, 'little') + name_bytes + channel_secret
|
|
||||||
return await self.send(data, [EventType.OK, 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) -> Event:
|
|
||||||
"""
|
|
||||||
Send a trace packet to test routing through specific repeaters
|
|
||||||
|
|
||||||
Args:
|
|
||||||
auth_code: 32-bit authentication code (default: 0)
|
|
||||||
tag: 32-bit integer to identify this trace (default: random)
|
|
||||||
flags: 8-bit flags field (default: 0)
|
|
||||||
path: Optional string with comma-separated hex values representing repeater pubkeys (e.g. "23,5f,3a")
|
|
||||||
or a bytes/bytearray object with the raw path data
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Event object with sent status, tag, and estimated timeout in milliseconds
|
|
||||||
"""
|
|
||||||
# Generate random tag if not provided
|
|
||||||
if tag is None:
|
|
||||||
tag = random.randint(1, 0xFFFFFFFF)
|
|
||||||
if auth_code is None:
|
|
||||||
auth_code = random.randint(1, 0xFFFFFFFF)
|
|
||||||
|
|
||||||
logger.debug(f"Sending trace: tag={tag}, auth={auth_code}, flags={flags}, path={path}")
|
|
||||||
|
|
||||||
# Prepare the command packet: CMD(1) + tag(4) + auth_code(4) + flags(1) + [path]
|
|
||||||
cmd_data = bytearray([36]) # CMD_SEND_TRACE_PATH
|
|
||||||
cmd_data.extend(tag.to_bytes(4, 'little'))
|
|
||||||
cmd_data.extend(auth_code.to_bytes(4, 'little'))
|
|
||||||
cmd_data.append(flags)
|
|
||||||
|
|
||||||
# Process path if provided
|
|
||||||
if path:
|
|
||||||
if isinstance(path, str):
|
|
||||||
# Convert comma-separated hex values to bytes
|
|
||||||
try:
|
|
||||||
path_bytes = bytearray()
|
|
||||||
for hex_val in path.split(','):
|
|
||||||
hex_val = hex_val.strip()
|
|
||||||
path_bytes.append(int(hex_val, 16))
|
|
||||||
cmd_data.extend(path_bytes)
|
|
||||||
except ValueError as e:
|
|
||||||
logger.error(f"Invalid path format: {e}")
|
|
||||||
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 Event(EventType.ERROR, {"reason": "unsupported_path_type"})
|
|
||||||
|
|
||||||
return await self.send(cmd_data, [EventType.MSG_SENT, EventType.ERROR])
|
|
||||||
18
src/meshcore/commands/__init__.py
Normal file
18
src/meshcore/commands/__init__.py
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
from typing import Any, Optional
|
||||||
|
|
||||||
|
from ..events import EventDispatcher
|
||||||
|
from ..reader import MessageReader
|
||||||
|
from .base import CommandHandlerBase
|
||||||
|
from .binary import BinaryCommandHandler
|
||||||
|
from .contact import ContactCommands
|
||||||
|
from .device import DeviceCommands
|
||||||
|
from .messaging import MessagingCommands
|
||||||
|
|
||||||
|
|
||||||
|
class CommandHandler(
|
||||||
|
DeviceCommands, ContactCommands, MessagingCommands, BinaryCommandHandler
|
||||||
|
):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = ["CommandHandler"]
|
||||||
146
src/meshcore/commands/base.py
Normal file
146
src/meshcore/commands/base.py
Normal file
@@ -0,0 +1,146 @@
|
|||||||
|
import asyncio
|
||||||
|
import logging
|
||||||
|
import random
|
||||||
|
from typing import Any, Callable, Coroutine, Dict, List, Optional, Union
|
||||||
|
|
||||||
|
from ..events import Event, EventDispatcher, EventType
|
||||||
|
from ..reader import MessageReader
|
||||||
|
|
||||||
|
# Define types for destination parameters
|
||||||
|
DestinationType = Union[bytes, str, Dict[str, Any]]
|
||||||
|
|
||||||
|
logger = logging.getLogger("meshcore")
|
||||||
|
|
||||||
|
|
||||||
|
def _validate_destination(dst: DestinationType, prefix_length: int = 6) -> bytes:
|
||||||
|
"""
|
||||||
|
Validates and converts a destination to a bytes object.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
dst: The destination, which can be:
|
||||||
|
- str: Hex string representation of a public key
|
||||||
|
- dict: Contact object with a "public_key" field
|
||||||
|
prefix_length: The length of the prefix to use (default: 6 bytes)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bytes: The destination public key as a bytes object
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: If dst is invalid or doesn't contain required fields
|
||||||
|
"""
|
||||||
|
if isinstance(dst, bytes):
|
||||||
|
# Already bytes, use directly
|
||||||
|
return dst[:prefix_length]
|
||||||
|
elif isinstance(dst, str):
|
||||||
|
# Hex string, convert to bytes
|
||||||
|
try:
|
||||||
|
return bytes.fromhex(dst)[:prefix_length]
|
||||||
|
except ValueError:
|
||||||
|
raise ValueError(f"Invalid public key hex string: {dst}")
|
||||||
|
elif isinstance(dst, dict):
|
||||||
|
# Contact object, extract public_key
|
||||||
|
if "public_key" not in dst:
|
||||||
|
raise ValueError("Contact object must have a 'public_key' field")
|
||||||
|
try:
|
||||||
|
return bytes.fromhex(dst["public_key"])[:prefix_length]
|
||||||
|
except ValueError:
|
||||||
|
raise ValueError(f"Invalid public_key in contact: {dst['public_key']}")
|
||||||
|
else:
|
||||||
|
raise ValueError(
|
||||||
|
f"Destination must be a public key string or contact object, got: {type(dst)}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class CommandHandlerBase:
|
||||||
|
DEFAULT_TIMEOUT = 5.0
|
||||||
|
|
||||||
|
def __init__(self, default_timeout: Optional[float] = None):
|
||||||
|
self._sender_func: Optional[Callable[[bytes], Coroutine[Any, Any, None]]] = None
|
||||||
|
self._reader: Optional[MessageReader] = None
|
||||||
|
self.dispatcher: Optional[EventDispatcher] = None
|
||||||
|
self.default_timeout = (
|
||||||
|
default_timeout if default_timeout is not None else self.DEFAULT_TIMEOUT
|
||||||
|
)
|
||||||
|
|
||||||
|
def set_connection(self, connection: Any) -> None:
|
||||||
|
async def sender(data: bytes) -> None:
|
||||||
|
await connection.send(data)
|
||||||
|
|
||||||
|
self._sender_func = sender
|
||||||
|
|
||||||
|
def set_reader(self, reader: MessageReader) -> None:
|
||||||
|
self._reader = reader
|
||||||
|
|
||||||
|
def set_dispatcher(self, dispatcher: EventDispatcher) -> None:
|
||||||
|
self.dispatcher = dispatcher
|
||||||
|
|
||||||
|
async def send(
|
||||||
|
self,
|
||||||
|
data: bytes,
|
||||||
|
expected_events: Optional[Union[EventType, List[EventType]]] = None,
|
||||||
|
timeout: Optional[float] = None,
|
||||||
|
) -> Event:
|
||||||
|
"""
|
||||||
|
Send a command and wait for expected event responses.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
data: The data to send
|
||||||
|
expected_events: EventType or list of EventTypes to wait for
|
||||||
|
timeout: Timeout in seconds, or None to use default_timeout
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
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")
|
||||||
|
|
||||||
|
# Use the provided timeout or fall back to default_timeout
|
||||||
|
timeout = timeout if timeout is not None else self.default_timeout
|
||||||
|
|
||||||
|
if self._sender_func:
|
||||||
|
logger.debug(
|
||||||
|
f"Sending raw data: {data.hex() if isinstance(data, bytes) else data}"
|
||||||
|
)
|
||||||
|
await self._sender_func(data)
|
||||||
|
|
||||||
|
if expected_events:
|
||||||
|
try:
|
||||||
|
# Convert single event to list if needed
|
||||||
|
if not isinstance(expected_events, list):
|
||||||
|
expected_events = [expected_events]
|
||||||
|
|
||||||
|
logger.debug(f"Waiting for events {expected_events}, timeout={timeout}")
|
||||||
|
|
||||||
|
# Create futures for all expected events
|
||||||
|
futures = []
|
||||||
|
for event_type in expected_events:
|
||||||
|
future = asyncio.create_task(
|
||||||
|
self.dispatcher.wait_for_event(event_type, {}, timeout)
|
||||||
|
)
|
||||||
|
futures.append(future)
|
||||||
|
|
||||||
|
# Wait for the first event to complete or all to timeout
|
||||||
|
done, pending = await asyncio.wait(
|
||||||
|
futures, timeout=timeout, return_when=asyncio.FIRST_COMPLETED
|
||||||
|
)
|
||||||
|
|
||||||
|
# Cancel all pending futures
|
||||||
|
for future in pending:
|
||||||
|
future.cancel()
|
||||||
|
|
||||||
|
# Check if any future completed successfully
|
||||||
|
for future in done:
|
||||||
|
event = await future
|
||||||
|
if event:
|
||||||
|
return event
|
||||||
|
|
||||||
|
# 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 Event(EventType.ERROR, {"reason": "timeout"})
|
||||||
|
except Exception as e:
|
||||||
|
logger.debug(f"Command error: {e}")
|
||||||
|
return Event(EventType.ERROR, {"error": str(e)})
|
||||||
|
# For commands that don't expect events, return a success event
|
||||||
|
return Event(EventType.OK, {})
|
||||||
126
src/meshcore/commands/binary.py
Normal file
126
src/meshcore/commands/binary.py
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
import logging
|
||||||
|
from enum import Enum
|
||||||
|
import json
|
||||||
|
from .base import CommandHandlerBase
|
||||||
|
from ..events import EventType
|
||||||
|
from cayennelpp import LppFrame, LppData
|
||||||
|
from cayennelpp.lpp_type import LppType
|
||||||
|
from ..lpp_json_encoder import lpp_json_encoder, my_lpp_types, lpp_format_val
|
||||||
|
|
||||||
|
logger = logging.getLogger("meshcore")
|
||||||
|
|
||||||
|
|
||||||
|
class BinaryReqType(Enum):
|
||||||
|
TELEMETRY = 0x03
|
||||||
|
MMA = 0x04
|
||||||
|
ACL = 0x05
|
||||||
|
|
||||||
|
|
||||||
|
def lpp_parse(buf):
|
||||||
|
"""Parse a given byte string and return as a LppFrame object."""
|
||||||
|
i = 0
|
||||||
|
lpp_data_list = []
|
||||||
|
while i < len(buf) and buf[i] != 0:
|
||||||
|
lppdata = LppData.from_bytes(buf[i:])
|
||||||
|
lpp_data_list.append(lppdata)
|
||||||
|
i = i + len(lppdata)
|
||||||
|
|
||||||
|
return json.loads(json.dumps(LppFrame(lpp_data_list), default=lpp_json_encoder))
|
||||||
|
|
||||||
|
|
||||||
|
def lpp_parse_mma(buf):
|
||||||
|
i = 0
|
||||||
|
res = []
|
||||||
|
while i < len(buf) and buf[i] != 0:
|
||||||
|
chan = buf[i]
|
||||||
|
i = i + 1
|
||||||
|
type = buf[i]
|
||||||
|
lpp_type = LppType.get_lpp_type(type)
|
||||||
|
size = lpp_type.size
|
||||||
|
i = i + 1
|
||||||
|
min = lpp_format_val(lpp_type, lpp_type.decode(buf[i : i + size]))
|
||||||
|
i = i + size
|
||||||
|
max = lpp_format_val(lpp_type, lpp_type.decode(buf[i : i + size]))
|
||||||
|
i = i + size
|
||||||
|
avg = lpp_format_val(lpp_type, lpp_type.decode(buf[i : i + size]))
|
||||||
|
i = i + size
|
||||||
|
res.append(
|
||||||
|
{
|
||||||
|
"channel": chan,
|
||||||
|
"type": my_lpp_types[type][0],
|
||||||
|
"min": min,
|
||||||
|
"max": max,
|
||||||
|
"avg": avg,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return res
|
||||||
|
|
||||||
|
|
||||||
|
def parse_acl(buf):
|
||||||
|
i = 0
|
||||||
|
res = []
|
||||||
|
while i + 7 <= len(buf):
|
||||||
|
key = buf[i : i + 6].hex()
|
||||||
|
perm = buf[i + 6]
|
||||||
|
if key != "000000000000":
|
||||||
|
res.append({"key": key, "perm": perm})
|
||||||
|
i = i + 7
|
||||||
|
return res
|
||||||
|
|
||||||
|
|
||||||
|
class BinaryCommandHandler(CommandHandlerBase):
|
||||||
|
"""Helper functions to handle binary requests through binary commands"""
|
||||||
|
|
||||||
|
async def req_binary(self, contact, request, timeout=0):
|
||||||
|
res = await self.send_binary_req(contact, request)
|
||||||
|
logger.debug(res)
|
||||||
|
if res.type == EventType.ERROR:
|
||||||
|
logger.error("Error while requesting binary data")
|
||||||
|
return None
|
||||||
|
else:
|
||||||
|
exp_tag = res.payload["expected_ack"].hex()
|
||||||
|
timeout = (
|
||||||
|
res.payload["suggested_timeout"] / 800 if timeout == 0 else timeout
|
||||||
|
)
|
||||||
|
res2 = await self.dispatcher.wait_for_event(
|
||||||
|
EventType.BINARY_RESPONSE,
|
||||||
|
attribute_filters={"tag": exp_tag},
|
||||||
|
timeout=timeout,
|
||||||
|
)
|
||||||
|
logger.debug(res2)
|
||||||
|
if res2 is None:
|
||||||
|
return None
|
||||||
|
else:
|
||||||
|
return res2.payload
|
||||||
|
|
||||||
|
async def req_telemetry(self, contact, timeout=0):
|
||||||
|
code = BinaryReqType.TELEMETRY.value
|
||||||
|
req = code.to_bytes(1, "little", signed=False)
|
||||||
|
res = await self.req_binary(contact, req, timeout)
|
||||||
|
if res is None:
|
||||||
|
return None
|
||||||
|
else:
|
||||||
|
return lpp_parse(bytes.fromhex(res["data"]))
|
||||||
|
|
||||||
|
async def req_mma(self, contact, start, end, timeout=0):
|
||||||
|
code = BinaryReqType.MMA.value
|
||||||
|
req = (
|
||||||
|
code.to_bytes(1, "little", signed=False)
|
||||||
|
+ start.to_bytes(4, "little", signed=False)
|
||||||
|
+ end.to_bytes(4, "little", signed=False)
|
||||||
|
+ b"\0\0"
|
||||||
|
)
|
||||||
|
res = await self.req_binary(contact, req, timeout)
|
||||||
|
if res is None:
|
||||||
|
return None
|
||||||
|
else:
|
||||||
|
return lpp_parse_mma(bytes.fromhex(res["data"])[4:])
|
||||||
|
|
||||||
|
async def req_acl(self, contact, timeout=0):
|
||||||
|
code = BinaryReqType.ACL.value
|
||||||
|
req = code.to_bytes(1, "little", signed=False) + b"\0\0"
|
||||||
|
res = await self.req_binary(contact, req, timeout)
|
||||||
|
if res is None:
|
||||||
|
return None
|
||||||
|
else:
|
||||||
|
return parse_acl(bytes.fromhex(res["data"]))
|
||||||
91
src/meshcore/commands/contact.py
Normal file
91
src/meshcore/commands/contact.py
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
import logging
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from ..events import Event, EventType
|
||||||
|
from .base import CommandHandlerBase, DestinationType, _validate_destination
|
||||||
|
|
||||||
|
logger = logging.getLogger("meshcore")
|
||||||
|
|
||||||
|
|
||||||
|
class ContactCommands(CommandHandlerBase):
|
||||||
|
async def get_contacts(self, lastmod=0) -> Event:
|
||||||
|
logger.debug("Getting contacts")
|
||||||
|
data = b"\x04"
|
||||||
|
if lastmod > 0:
|
||||||
|
data = data + lastmod.to_bytes(4, "little")
|
||||||
|
return await self.send(data, [EventType.CONTACTS, EventType.ERROR])
|
||||||
|
|
||||||
|
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) -> 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.OK, EventType.ERROR])
|
||||||
|
|
||||||
|
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()}")
|
||||||
|
data = b"\x11" + key_bytes
|
||||||
|
else:
|
||||||
|
logger.debug("Exporting node")
|
||||||
|
data = b"\x11"
|
||||||
|
return await self.send(data, [EventType.CONTACT_URI, EventType.ERROR])
|
||||||
|
|
||||||
|
async def import_contact(self, card_data) -> Event:
|
||||||
|
data = b"\x12" + card_data
|
||||||
|
return await self.send(data, [EventType.OK, EventType.ERROR])
|
||||||
|
|
||||||
|
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 update_contact(self, contact, path=None, flags=None) -> Event:
|
||||||
|
if path is None:
|
||||||
|
out_path_hex = contact["out_path"]
|
||||||
|
out_path_len = contact["out_path_len"]
|
||||||
|
else:
|
||||||
|
out_path_hex = path
|
||||||
|
out_path_len = int(len(path) / 2)
|
||||||
|
# reflect the change
|
||||||
|
contact["out_path"] = out_path_hex
|
||||||
|
contact["out_path_len"] = out_path_len
|
||||||
|
out_path_hex = out_path_hex + (128 - len(out_path_hex)) * "0"
|
||||||
|
|
||||||
|
if flags is None:
|
||||||
|
flags = contact["flags"]
|
||||||
|
else:
|
||||||
|
# reflect the change
|
||||||
|
contact["flags"] = flags
|
||||||
|
|
||||||
|
adv_name_hex = contact["adv_name"].encode().hex()
|
||||||
|
adv_name_hex = adv_name_hex + (64 - len(adv_name_hex)) * "0"
|
||||||
|
data = (
|
||||||
|
b"\x09"
|
||||||
|
+ bytes.fromhex(contact["public_key"])
|
||||||
|
+ contact["type"].to_bytes(1)
|
||||||
|
+ flags.to_bytes(1)
|
||||||
|
+ out_path_len.to_bytes(1, "little", signed=True)
|
||||||
|
+ bytes.fromhex(out_path_hex)
|
||||||
|
+ bytes.fromhex(adv_name_hex)
|
||||||
|
+ contact["last_advert"].to_bytes(4, "little")
|
||||||
|
+ int(contact["adv_lat"] * 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])
|
||||||
|
|
||||||
|
async def add_contact(self, contact) -> Event:
|
||||||
|
return await self.update_contact(contact)
|
||||||
|
|
||||||
|
async def change_contact_path(self, contact, path) -> Event:
|
||||||
|
return await self.update_contact(contact, path)
|
||||||
|
|
||||||
|
async def change_contact_flags(self, contact, flags) -> Event:
|
||||||
|
return await self.update_contact(contact, flags=flags)
|
||||||
200
src/meshcore/commands/device.py
Normal file
200
src/meshcore/commands/device.py
Normal file
@@ -0,0 +1,200 @@
|
|||||||
|
import logging
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from ..events import Event, EventType
|
||||||
|
from .base import CommandHandlerBase, DestinationType, _validate_destination
|
||||||
|
|
||||||
|
logger = logging.getLogger("meshcore")
|
||||||
|
|
||||||
|
|
||||||
|
class DeviceCommands(CommandHandlerBase):
|
||||||
|
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) -> 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) -> 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) -> Event:
|
||||||
|
logger.debug(f"Setting device name to: {name}")
|
||||||
|
return await self.send(
|
||||||
|
b"\x08" + name.encode("utf-8"), [EventType.OK, EventType.ERROR]
|
||||||
|
)
|
||||||
|
|
||||||
|
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) -> Event:
|
||||||
|
logger.debug("Sending reboot command")
|
||||||
|
return await self.send(b"\x13reboot")
|
||||||
|
|
||||||
|
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) -> Event:
|
||||||
|
logger.debug("Getting device time")
|
||||||
|
return await self.send(b"\x05", [EventType.CURRENT_TIME, EventType.ERROR])
|
||||||
|
|
||||||
|
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) -> 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) -> 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")
|
||||||
|
+ int(float(bw) * 1000).to_bytes(4, "little")
|
||||||
|
+ 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) -> 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")
|
||||||
|
+ int(af).to_bytes(4, "little")
|
||||||
|
+ int(0).to_bytes(1, "little")
|
||||||
|
+ int(0).to_bytes(1, "little"),
|
||||||
|
[EventType.OK, EventType.ERROR],
|
||||||
|
)
|
||||||
|
|
||||||
|
async def set_other_params(
|
||||||
|
self,
|
||||||
|
manual_add_contacts: bool,
|
||||||
|
telemetry_mode_base: int,
|
||||||
|
telemetry_mode_loc: int,
|
||||||
|
telemetry_mode_env: int,
|
||||||
|
advert_loc_policy: int,
|
||||||
|
) -> Event:
|
||||||
|
telemetry_mode = (
|
||||||
|
(telemetry_mode_base & 0b11)
|
||||||
|
| ((telemetry_mode_loc & 0b11) << 2)
|
||||||
|
| ((telemetry_mode_env & 0b11) << 4)
|
||||||
|
)
|
||||||
|
data = (
|
||||||
|
b"\x26"
|
||||||
|
+ manual_add_contacts.to_bytes(1)
|
||||||
|
+ telemetry_mode.to_bytes(1)
|
||||||
|
+ advert_loc_policy.to_bytes(1)
|
||||||
|
)
|
||||||
|
return await self.send(data, [EventType.OK, EventType.ERROR])
|
||||||
|
|
||||||
|
async def set_telemetry_mode_base(self, telemetry_mode_base: int) -> Event:
|
||||||
|
infos = (await self.send_appstart()).payload
|
||||||
|
return await self.set_other_params(
|
||||||
|
infos["manual_add_contacts"],
|
||||||
|
telemetry_mode_base,
|
||||||
|
infos["telemetry_mode_loc"],
|
||||||
|
infos["telemetry_mode_env"],
|
||||||
|
infos["adv_loc_policy"],
|
||||||
|
)
|
||||||
|
|
||||||
|
async def set_telemetry_mode_loc(self, telemetry_mode_loc: int) -> Event:
|
||||||
|
infos = (await self.send_appstart()).payload
|
||||||
|
return await self.set_other_params(
|
||||||
|
infos["manual_add_contacts"],
|
||||||
|
infos["telemetry_mode_base"],
|
||||||
|
telemetry_mode_loc,
|
||||||
|
infos["telemetry_mode_env"],
|
||||||
|
infos["adv_loc_policy"],
|
||||||
|
)
|
||||||
|
|
||||||
|
async def set_telemetry_mode_env(self, telemetry_mode_env: int) -> Event:
|
||||||
|
infos = (await self.send_appstart()).payload
|
||||||
|
return await self.set_other_params(
|
||||||
|
infos["manual_add_contacts"],
|
||||||
|
infos["telemetry_mode_base"],
|
||||||
|
infos["telemetry_mode_loc"],
|
||||||
|
telemetry_mode_env,
|
||||||
|
infos["adv_loc_policy"],
|
||||||
|
)
|
||||||
|
|
||||||
|
async def set_manual_add_contacts(self, manual_add_contacts: bool) -> Event:
|
||||||
|
infos = (await self.send_appstart()).payload
|
||||||
|
return await self.set_other_params(
|
||||||
|
manual_add_contacts,
|
||||||
|
infos["telemetry_mode_base"],
|
||||||
|
infos["telemetry_mode_loc"],
|
||||||
|
infos["telemetry_mode_env"],
|
||||||
|
infos["adv_loc_policy"],
|
||||||
|
)
|
||||||
|
|
||||||
|
async def set_advert_loc_policy(self, advert_loc_policy: int) -> Event:
|
||||||
|
infos = (await self.send_appstart()).payload
|
||||||
|
return await self.set_other_params(
|
||||||
|
infos["manual_add_contacts"],
|
||||||
|
infos["telemetry_mode_base"],
|
||||||
|
infos["telemetry_mode_loc"],
|
||||||
|
infos["telemetry_mode_env"],
|
||||||
|
advert_loc_policy,
|
||||||
|
)
|
||||||
|
|
||||||
|
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_self_telemetry(self) -> Event:
|
||||||
|
logger.debug("Getting self telemetry")
|
||||||
|
data = b"\x27\x00\x00\x00"
|
||||||
|
return await self.send(data, [EventType.TELEMETRY_RESPONSE, EventType.ERROR])
|
||||||
|
|
||||||
|
async def get_custom_vars(self) -> Event:
|
||||||
|
logger.debug("Asking for custom vars")
|
||||||
|
data = b"\x28"
|
||||||
|
return await self.send(data, [EventType.CUSTOM_VARS, EventType.ERROR])
|
||||||
|
|
||||||
|
async def set_custom_var(self, key, value) -> Event:
|
||||||
|
logger.debug(f"Setting custom var {key} to {value}")
|
||||||
|
data = b"\x29" + key.encode("utf-8") + b":" + value.encode("utf-8")
|
||||||
|
return await self.send(data, [EventType.OK, EventType.ERROR])
|
||||||
|
|
||||||
|
async def get_channel(self, channel_idx: int) -> Event:
|
||||||
|
logger.debug(f"Getting channel info for channel {channel_idx}")
|
||||||
|
data = b"\x1f" + channel_idx.to_bytes(1, "little")
|
||||||
|
return await self.send(data, [EventType.CHANNEL_INFO, EventType.ERROR])
|
||||||
|
|
||||||
|
async def set_channel(
|
||||||
|
self, channel_idx: int, channel_name: str, channel_secret: bytes
|
||||||
|
) -> Event:
|
||||||
|
logger.debug(f"Setting channel {channel_idx}: name={channel_name}")
|
||||||
|
|
||||||
|
# Pad channel name to 32 bytes
|
||||||
|
name_bytes = channel_name.encode("utf-8")[:32]
|
||||||
|
name_bytes = name_bytes.ljust(32, b"\x00")
|
||||||
|
|
||||||
|
# Ensure channel secret is exactly 16 bytes
|
||||||
|
if len(channel_secret) != 16:
|
||||||
|
raise ValueError("Channel secret must be exactly 16 bytes")
|
||||||
|
|
||||||
|
data = b"\x20" + channel_idx.to_bytes(1, "little") + name_bytes + channel_secret
|
||||||
|
return await self.send(data, [EventType.OK, EventType.ERROR])
|
||||||
167
src/meshcore/commands/messaging.py
Normal file
167
src/meshcore/commands/messaging.py
Normal file
@@ -0,0 +1,167 @@
|
|||||||
|
import logging
|
||||||
|
import random
|
||||||
|
from typing import Optional, Union
|
||||||
|
|
||||||
|
from ..events import Event, EventType
|
||||||
|
from .base import CommandHandlerBase, DestinationType, _validate_destination
|
||||||
|
|
||||||
|
logger = logging.getLogger("meshcore")
|
||||||
|
|
||||||
|
|
||||||
|
class MessagingCommands(CommandHandlerBase):
|
||||||
|
async def get_msg(self, timeout: Optional[float] = None) -> Event:
|
||||||
|
logger.debug("Requesting pending messages")
|
||||||
|
return await self.send(
|
||||||
|
b"\x0a",
|
||||||
|
[
|
||||||
|
EventType.CONTACT_MSG_RECV,
|
||||||
|
EventType.CHANNEL_MSG_RECV,
|
||||||
|
EventType.ERROR,
|
||||||
|
EventType.NO_MORE_MSGS,
|
||||||
|
],
|
||||||
|
timeout,
|
||||||
|
)
|
||||||
|
|
||||||
|
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("utf-8")
|
||||||
|
return await self.send(data, [EventType.MSG_SENT, EventType.ERROR])
|
||||||
|
|
||||||
|
async def send_logout(self, dst: DestinationType) -> Event:
|
||||||
|
dst_bytes = _validate_destination(dst, prefix_length=32)
|
||||||
|
data = b"\x1d" + dst_bytes
|
||||||
|
return await self.send(data, [EventType.OK, EventType.ERROR])
|
||||||
|
|
||||||
|
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
|
||||||
|
) -> Event:
|
||||||
|
dst_bytes = _validate_destination(dst)
|
||||||
|
logger.debug(f"Sending command to {dst_bytes.hex()}: {cmd}")
|
||||||
|
|
||||||
|
if timestamp is None:
|
||||||
|
import time
|
||||||
|
|
||||||
|
timestamp = int(time.time())
|
||||||
|
|
||||||
|
data = (
|
||||||
|
b"\x02\x01\x00"
|
||||||
|
+ timestamp.to_bytes(4, "little")
|
||||||
|
+ dst_bytes
|
||||||
|
+ cmd.encode("utf-8")
|
||||||
|
)
|
||||||
|
return await self.send(data, [EventType.MSG_SENT, EventType.ERROR])
|
||||||
|
|
||||||
|
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}")
|
||||||
|
|
||||||
|
if timestamp is None:
|
||||||
|
import time
|
||||||
|
|
||||||
|
timestamp = int(time.time())
|
||||||
|
|
||||||
|
data = (
|
||||||
|
b"\x02\x00\x00"
|
||||||
|
+ timestamp.to_bytes(4, "little")
|
||||||
|
+ dst_bytes
|
||||||
|
+ msg.encode("utf-8")
|
||||||
|
)
|
||||||
|
return await self.send(data, [EventType.MSG_SENT, EventType.ERROR])
|
||||||
|
|
||||||
|
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
|
||||||
|
if timestamp is None:
|
||||||
|
import time
|
||||||
|
|
||||||
|
timestamp = int(time.time()).to_bytes(4, "little")
|
||||||
|
|
||||||
|
data = (
|
||||||
|
b"\x03\x00" + chan.to_bytes(1, "little") + timestamp + msg.encode("utf-8")
|
||||||
|
)
|
||||||
|
return await self.send(data, [EventType.OK, EventType.ERROR])
|
||||||
|
|
||||||
|
async def send_telemetry_req(self, dst: DestinationType) -> Event:
|
||||||
|
dst_bytes = _validate_destination(dst, prefix_length=32)
|
||||||
|
logger.debug(f"Asking telemetry to {dst_bytes.hex()}")
|
||||||
|
data = b"\x27\x00\x00\x00" + dst_bytes
|
||||||
|
return await self.send(data, [EventType.MSG_SENT, EventType.ERROR])
|
||||||
|
|
||||||
|
async def send_binary_req(self, dst: DestinationType, bin_data) -> Event:
|
||||||
|
dst_bytes = _validate_destination(dst, prefix_length=32)
|
||||||
|
logger.debug(f"Binary request to {dst_bytes.hex()}")
|
||||||
|
data = b"\x32" + dst_bytes + bin_data
|
||||||
|
return await self.send(data, [EventType.MSG_SENT, EventType.ERROR])
|
||||||
|
|
||||||
|
async def send_path_discovery(self, dst: DestinationType) -> Event:
|
||||||
|
dst_bytes = _validate_destination(dst, prefix_length=32)
|
||||||
|
logger.debug(f"Path discovery request for {dst_bytes.hex()}")
|
||||||
|
data = b"\x34\x00" + dst_bytes
|
||||||
|
return await self.send(data, [EventType.MSG_SENT, 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,
|
||||||
|
) -> Event:
|
||||||
|
"""
|
||||||
|
Send a trace packet to test routing through specific repeaters
|
||||||
|
|
||||||
|
Args:
|
||||||
|
auth_code: 32-bit authentication code (default: 0)
|
||||||
|
tag: 32-bit integer to identify this trace (default: random)
|
||||||
|
flags: 8-bit flags field (default: 0)
|
||||||
|
path: Optional string with comma-separated hex values representing repeater pubkeys (e.g. "23,5f,3a")
|
||||||
|
or a bytes/bytearray object with the raw path data
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Event object with sent status, tag, and estimated timeout in milliseconds
|
||||||
|
"""
|
||||||
|
# Generate random tag if not provided
|
||||||
|
if tag is None:
|
||||||
|
tag = random.randint(1, 0xFFFFFFFF)
|
||||||
|
if auth_code is None:
|
||||||
|
auth_code = random.randint(1, 0xFFFFFFFF)
|
||||||
|
|
||||||
|
logger.debug(
|
||||||
|
f"Sending trace: tag={tag}, auth={auth_code}, flags={flags}, path={path}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Prepare the command packet: CMD(1) + tag(4) + auth_code(4) + flags(1) + [path]
|
||||||
|
cmd_data = bytearray([36]) # CMD_SEND_TRACE_PATH
|
||||||
|
cmd_data.extend(tag.to_bytes(4, "little"))
|
||||||
|
cmd_data.extend(auth_code.to_bytes(4, "little"))
|
||||||
|
cmd_data.append(flags)
|
||||||
|
|
||||||
|
# Process path if provided
|
||||||
|
if path:
|
||||||
|
if isinstance(path, str):
|
||||||
|
# Convert comma-separated hex values to bytes
|
||||||
|
try:
|
||||||
|
path_bytes = bytearray()
|
||||||
|
for hex_val in path.split(","):
|
||||||
|
hex_val = hex_val.strip()
|
||||||
|
path_bytes.append(int(hex_val, 16))
|
||||||
|
cmd_data.extend(path_bytes)
|
||||||
|
except ValueError as e:
|
||||||
|
logger.error(f"Invalid path format: {e}")
|
||||||
|
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 Event(EventType.ERROR, {"reason": "unsupported_path_type"})
|
||||||
|
|
||||||
|
return await self.send(cmd_data, [EventType.MSG_SENT, EventType.ERROR])
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
"""
|
"""
|
||||||
Connection manager that orchestrates reconnection logic for any connection type.
|
Connection manager that orchestrates reconnection logic for any connection type.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
from typing import Optional, Any, Callable, Protocol
|
from typing import Optional, Any, Callable, Protocol
|
||||||
@@ -8,21 +9,22 @@ from .events import Event, EventType
|
|||||||
|
|
||||||
logger = logging.getLogger("meshcore")
|
logger = logging.getLogger("meshcore")
|
||||||
|
|
||||||
|
|
||||||
class ConnectionProtocol(Protocol):
|
class ConnectionProtocol(Protocol):
|
||||||
"""Protocol defining the interface that connection classes must implement."""
|
"""Protocol defining the interface that connection classes must implement."""
|
||||||
|
|
||||||
async def connect(self) -> Optional[Any]:
|
async def connect(self) -> Optional[Any]:
|
||||||
"""Connect and return connection info, or None if failed."""
|
"""Connect and return connection info, or None if failed."""
|
||||||
...
|
...
|
||||||
|
|
||||||
async def disconnect(self):
|
async def disconnect(self):
|
||||||
"""Disconnect from the device/server."""
|
"""Disconnect from the device/server."""
|
||||||
...
|
...
|
||||||
|
|
||||||
async def send(self, data):
|
async def send(self, data):
|
||||||
"""Send data through the connection."""
|
"""Send data through the connection."""
|
||||||
...
|
...
|
||||||
|
|
||||||
def set_reader(self, reader):
|
def set_reader(self, reader):
|
||||||
"""Set the message reader."""
|
"""Set the message reader."""
|
||||||
...
|
...
|
||||||
@@ -30,27 +32,32 @@ class ConnectionProtocol(Protocol):
|
|||||||
|
|
||||||
class ConnectionManager:
|
class ConnectionManager:
|
||||||
"""Manages connection lifecycle with auto-reconnect and event emission."""
|
"""Manages connection lifecycle with auto-reconnect and event emission."""
|
||||||
|
|
||||||
def __init__(self, connection: ConnectionProtocol, event_dispatcher=None,
|
def __init__(
|
||||||
auto_reconnect: bool = False, max_reconnect_attempts: int = 3):
|
self,
|
||||||
|
connection: ConnectionProtocol,
|
||||||
|
event_dispatcher=None,
|
||||||
|
auto_reconnect: bool = False,
|
||||||
|
max_reconnect_attempts: int = 3,
|
||||||
|
):
|
||||||
self.connection = connection
|
self.connection = connection
|
||||||
self.event_dispatcher = event_dispatcher
|
self.event_dispatcher = event_dispatcher
|
||||||
self.auto_reconnect = auto_reconnect
|
self.auto_reconnect = auto_reconnect
|
||||||
self.max_reconnect_attempts = max_reconnect_attempts
|
self.max_reconnect_attempts = max_reconnect_attempts
|
||||||
|
|
||||||
self._reconnect_attempts = 0
|
self._reconnect_attempts = 0
|
||||||
self._is_connected = False
|
self._is_connected = False
|
||||||
self._reconnect_task = None
|
self._reconnect_task = None
|
||||||
self._disconnect_callback: Optional[Callable] = None
|
self._disconnect_callback: Optional[Callable] = None
|
||||||
|
|
||||||
def set_disconnect_callback(self, callback: Callable):
|
def set_disconnect_callback(self, callback: Callable):
|
||||||
"""Set a callback to be called when disconnection is detected."""
|
"""Set a callback to be called when disconnection is detected."""
|
||||||
self._disconnect_callback = callback
|
self._disconnect_callback = callback
|
||||||
|
|
||||||
async def connect(self) -> Optional[Any]:
|
async def connect(self) -> Optional[Any]:
|
||||||
"""Connect with event handling and state management."""
|
"""Connect with event handling and state management."""
|
||||||
result = await self.connection.connect()
|
result = await self.connection.connect()
|
||||||
|
|
||||||
if result is not None:
|
if result is not None:
|
||||||
self._is_connected = True
|
self._is_connected = True
|
||||||
self._reconnect_attempts = 0
|
self._reconnect_attempts = 0
|
||||||
@@ -58,9 +65,9 @@ class ConnectionManager:
|
|||||||
logger.debug(f"Connected successfully: {result}")
|
logger.debug(f"Connected successfully: {result}")
|
||||||
else:
|
else:
|
||||||
logger.debug("Connection failed")
|
logger.debug("Connection failed")
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
async def disconnect(self):
|
async def disconnect(self):
|
||||||
"""Disconnect with proper cleanup."""
|
"""Disconnect with proper cleanup."""
|
||||||
if self._reconnect_task:
|
if self._reconnect_task:
|
||||||
@@ -70,80 +77,93 @@ class ConnectionManager:
|
|||||||
except asyncio.CancelledError:
|
except asyncio.CancelledError:
|
||||||
pass
|
pass
|
||||||
self._reconnect_task = None
|
self._reconnect_task = None
|
||||||
|
|
||||||
if self._is_connected:
|
if self._is_connected:
|
||||||
await self.connection.disconnect()
|
await self.connection.disconnect()
|
||||||
self._is_connected = False
|
self._is_connected = False
|
||||||
await self._emit_event(EventType.DISCONNECTED, {"reason": "manual_disconnect"})
|
await self._emit_event(
|
||||||
|
EventType.DISCONNECTED, {"reason": "manual_disconnect"}
|
||||||
|
)
|
||||||
|
|
||||||
async def handle_disconnect(self, reason: str = "unknown"):
|
async def handle_disconnect(self, reason: str = "unknown"):
|
||||||
"""Handle unexpected disconnections with optional auto-reconnect."""
|
"""Handle unexpected disconnections with optional auto-reconnect."""
|
||||||
if not self._is_connected:
|
if not self._is_connected:
|
||||||
return
|
return
|
||||||
|
|
||||||
self._is_connected = False
|
self._is_connected = False
|
||||||
logger.debug(f"Connection lost: {reason}")
|
logger.debug(f"Connection lost: {reason}")
|
||||||
|
|
||||||
if self.auto_reconnect and self._reconnect_attempts < self.max_reconnect_attempts:
|
if (
|
||||||
|
self.auto_reconnect
|
||||||
|
and self._reconnect_attempts < self.max_reconnect_attempts
|
||||||
|
):
|
||||||
self._reconnect_task = asyncio.create_task(self._attempt_reconnect())
|
self._reconnect_task = asyncio.create_task(self._attempt_reconnect())
|
||||||
else:
|
else:
|
||||||
await self._emit_event(EventType.DISCONNECTED, {
|
await self._emit_event(
|
||||||
"reason": reason,
|
EventType.DISCONNECTED,
|
||||||
"reconnect_failed": self._reconnect_attempts >= self.max_reconnect_attempts
|
{
|
||||||
})
|
"reason": reason,
|
||||||
|
"reconnect_failed": self._reconnect_attempts
|
||||||
|
>= self.max_reconnect_attempts,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
async def _attempt_reconnect(self):
|
async def _attempt_reconnect(self):
|
||||||
"""Attempt to reconnect with flat delay."""
|
"""Attempt to reconnect with flat delay."""
|
||||||
logger.debug(f"Attempting reconnection ({self._reconnect_attempts + 1}/{self.max_reconnect_attempts})")
|
logger.debug(
|
||||||
|
f"Attempting reconnection ({self._reconnect_attempts + 1}/{self.max_reconnect_attempts})"
|
||||||
|
)
|
||||||
self._reconnect_attempts += 1
|
self._reconnect_attempts += 1
|
||||||
|
|
||||||
# Flat 1 second delay for all attempts
|
# Flat 1 second delay for all attempts
|
||||||
await asyncio.sleep(1)
|
await asyncio.sleep(1)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
result = await self.connection.connect()
|
result = await self.connection.connect()
|
||||||
if result is not None:
|
if result is not None:
|
||||||
self._is_connected = True
|
self._is_connected = True
|
||||||
self._reconnect_attempts = 0
|
self._reconnect_attempts = 0
|
||||||
await self._emit_event(EventType.CONNECTED, {
|
await self._emit_event(
|
||||||
"connection_info": result,
|
EventType.CONNECTED,
|
||||||
"reconnected": True
|
{"connection_info": result, "reconnected": True},
|
||||||
})
|
)
|
||||||
logger.debug(f"Reconnected successfully")
|
logger.debug("Reconnected successfully")
|
||||||
else:
|
else:
|
||||||
# Reconnection failed, try again if we haven't exceeded max attempts
|
# Reconnection failed, try again if we haven't exceeded max attempts
|
||||||
if self._reconnect_attempts < self.max_reconnect_attempts:
|
if self._reconnect_attempts < self.max_reconnect_attempts:
|
||||||
self._reconnect_task = asyncio.create_task(self._attempt_reconnect())
|
self._reconnect_task = asyncio.create_task(
|
||||||
|
self._attempt_reconnect()
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
await self._emit_event(EventType.DISCONNECTED, {
|
await self._emit_event(
|
||||||
"reason": "reconnect_failed",
|
EventType.DISCONNECTED,
|
||||||
"max_attempts_exceeded": True
|
{"reason": "reconnect_failed", "max_attempts_exceeded": True},
|
||||||
})
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.debug(f"Reconnection attempt failed: {e}")
|
logger.debug(f"Reconnection attempt failed: {e}")
|
||||||
if self._reconnect_attempts < self.max_reconnect_attempts:
|
if self._reconnect_attempts < self.max_reconnect_attempts:
|
||||||
self._reconnect_task = asyncio.create_task(self._attempt_reconnect())
|
self._reconnect_task = asyncio.create_task(self._attempt_reconnect())
|
||||||
else:
|
else:
|
||||||
await self._emit_event(EventType.DISCONNECTED, {
|
await self._emit_event(
|
||||||
"reason": f"reconnect_error: {e}",
|
EventType.DISCONNECTED,
|
||||||
"max_attempts_exceeded": True
|
{"reason": f"reconnect_error: {e}", "max_attempts_exceeded": True},
|
||||||
})
|
)
|
||||||
|
|
||||||
async def _emit_event(self, event_type: EventType, payload: dict):
|
async def _emit_event(self, event_type: EventType, payload: dict):
|
||||||
"""Emit connection events if dispatcher is available."""
|
"""Emit connection events if dispatcher is available."""
|
||||||
if self.event_dispatcher:
|
if self.event_dispatcher:
|
||||||
event = Event(event_type, payload)
|
event = Event(event_type, payload)
|
||||||
await self.event_dispatcher.dispatch(event)
|
await self.event_dispatcher.dispatch(event)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_connected(self) -> bool:
|
def is_connected(self) -> bool:
|
||||||
"""Check if the connection is active."""
|
"""Check if the connection is active."""
|
||||||
return self._is_connected
|
return self._is_connected
|
||||||
|
|
||||||
async def send(self, data):
|
async def send(self, data):
|
||||||
"""Send data through the managed connection."""
|
"""Send data through the managed connection."""
|
||||||
return await self.connection.send(data)
|
return await self.connection.send(data)
|
||||||
|
|
||||||
def set_reader(self, reader):
|
def set_reader(self, reader):
|
||||||
"""Set the message reader on the underlying connection."""
|
"""Set the message reader on the underlying connection."""
|
||||||
self.connection.set_reader(reader)
|
self.connection.set_reader(reader)
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
from enum import Enum
|
from enum import Enum
|
||||||
import inspect
|
import inspect
|
||||||
import logging
|
import logging
|
||||||
from math import log
|
|
||||||
from typing import Any, Dict, Optional, Callable, List, Union
|
from typing import Any, Dict, Optional, Callable, List, Union
|
||||||
import asyncio
|
import asyncio
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
|
|
||||||
logger = logging.getLogger("meshcore")
|
logger = logging.getLogger("meshcore")
|
||||||
|
|
||||||
# Public event types for users to subscribe to
|
|
||||||
|
# Public event types for users to subscribe to
|
||||||
class EventType(Enum):
|
class EventType(Enum):
|
||||||
CONTACTS = "contacts"
|
CONTACTS = "contacts"
|
||||||
SELF_INFO = "self_info"
|
SELF_INFO = "self_info"
|
||||||
@@ -21,7 +21,7 @@ class EventType(Enum):
|
|||||||
DEVICE_INFO = "device_info"
|
DEVICE_INFO = "device_info"
|
||||||
MSG_SENT = "message_sent"
|
MSG_SENT = "message_sent"
|
||||||
NEW_CONTACT = "new_contact"
|
NEW_CONTACT = "new_contact"
|
||||||
|
|
||||||
# Push notifications
|
# Push notifications
|
||||||
ADVERTISEMENT = "advertisement"
|
ADVERTISEMENT = "advertisement"
|
||||||
PATH_UPDATE = "path_update"
|
PATH_UPDATE = "path_update"
|
||||||
@@ -29,7 +29,7 @@ class EventType(Enum):
|
|||||||
MESSAGES_WAITING = "messages_waiting"
|
MESSAGES_WAITING = "messages_waiting"
|
||||||
RAW_DATA = "raw_data"
|
RAW_DATA = "raw_data"
|
||||||
LOGIN_SUCCESS = "login_success"
|
LOGIN_SUCCESS = "login_success"
|
||||||
LOGIN_FAILED = "login_failed"
|
LOGIN_FAILED = "login_failed"
|
||||||
STATUS_RESPONSE = "status_response"
|
STATUS_RESPONSE = "status_response"
|
||||||
LOG_DATA = "log_data"
|
LOG_DATA = "log_data"
|
||||||
TRACE_DATA = "trace_data"
|
TRACE_DATA = "trace_data"
|
||||||
@@ -39,11 +39,11 @@ class EventType(Enum):
|
|||||||
CUSTOM_VARS = "custom_vars"
|
CUSTOM_VARS = "custom_vars"
|
||||||
CHANNEL_INFO = "channel_info"
|
CHANNEL_INFO = "channel_info"
|
||||||
PATH_RESPONSE = "path_response"
|
PATH_RESPONSE = "path_response"
|
||||||
|
|
||||||
# Command response types
|
# Command response types
|
||||||
OK = "command_ok"
|
OK = "command_ok"
|
||||||
ERROR = "command_error"
|
ERROR = "command_error"
|
||||||
|
|
||||||
# Connection events
|
# Connection events
|
||||||
CONNECTED = "connected"
|
CONNECTED = "connected"
|
||||||
DISCONNECTED = "disconnected"
|
DISCONNECTED = "disconnected"
|
||||||
@@ -54,11 +54,17 @@ class Event:
|
|||||||
type: EventType
|
type: EventType
|
||||||
payload: Any
|
payload: Any
|
||||||
attributes: Dict[str, Any] = field(default_factory=dict)
|
attributes: Dict[str, Any] = field(default_factory=dict)
|
||||||
|
|
||||||
def __init__(self, type: EventType, payload: Any, attributes: Optional[Dict[str, Any]] = None, **kwargs):
|
def __init__(
|
||||||
|
self,
|
||||||
|
type: EventType,
|
||||||
|
payload: Any,
|
||||||
|
attributes: Optional[Dict[str, Any]] = None,
|
||||||
|
**kwargs,
|
||||||
|
):
|
||||||
"""
|
"""
|
||||||
Initialize an Event
|
Initialize an Event
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
type: The event type
|
type: The event type
|
||||||
payload: The event payload
|
payload: The event payload
|
||||||
@@ -68,18 +74,21 @@ class Event:
|
|||||||
self.type = type
|
self.type = type
|
||||||
self.payload = payload
|
self.payload = payload
|
||||||
self.attributes = attributes or {}
|
self.attributes = attributes or {}
|
||||||
|
|
||||||
# Add any keyword arguments to the attributes dictionary
|
# Add any keyword arguments to the attributes dictionary
|
||||||
if kwargs:
|
if kwargs:
|
||||||
self.attributes.update(kwargs)
|
self.attributes.update(kwargs)
|
||||||
|
|
||||||
def clone(self):
|
def clone(self):
|
||||||
"""
|
"""
|
||||||
Create a copy of the event.
|
Create a copy of the event.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
A new Event object with the same type, payload, and attributes.
|
A new Event object with the same type, payload, and attributes.
|
||||||
"""
|
"""
|
||||||
copied_payload = self.payload.copy() if isinstance(self.payload, dict) else self.payload
|
copied_payload = (
|
||||||
|
self.payload.copy() if isinstance(self.payload, dict) else self.payload
|
||||||
|
)
|
||||||
return Event(self.type, copied_payload, self.attributes.copy())
|
return Event(self.type, copied_payload, self.attributes.copy())
|
||||||
|
|
||||||
|
|
||||||
@@ -89,7 +98,7 @@ class Subscription:
|
|||||||
self.event_type = event_type
|
self.event_type = event_type
|
||||||
self.callback = callback
|
self.callback = callback
|
||||||
self.attribute_filters = attribute_filters or {}
|
self.attribute_filters = attribute_filters or {}
|
||||||
|
|
||||||
def unsubscribe(self):
|
def unsubscribe(self):
|
||||||
self.dispatcher._remove_subscription(self)
|
self.dispatcher._remove_subscription(self)
|
||||||
|
|
||||||
@@ -100,12 +109,16 @@ class EventDispatcher:
|
|||||||
self.subscriptions: List[Subscription] = []
|
self.subscriptions: List[Subscription] = []
|
||||||
self.running = False
|
self.running = False
|
||||||
self._task = None
|
self._task = None
|
||||||
|
|
||||||
def subscribe(self, event_type: Union[EventType, None], callback: Callable[[Event], Union[None, asyncio.Future]],
|
def subscribe(
|
||||||
attribute_filters: Optional[Dict[str, Any]] = None) -> Subscription:
|
self,
|
||||||
|
event_type: Union[EventType, None],
|
||||||
|
callback: Callable[[Event], Union[None, asyncio.Future]],
|
||||||
|
attribute_filters: Optional[Dict[str, Any]] = None,
|
||||||
|
) -> Subscription:
|
||||||
"""
|
"""
|
||||||
Subscribe to events with optional attribute filtering.
|
Subscribe to events with optional attribute filtering.
|
||||||
|
|
||||||
Parameters:
|
Parameters:
|
||||||
-----------
|
-----------
|
||||||
event_type : EventType or None
|
event_type : EventType or None
|
||||||
@@ -114,7 +127,7 @@ class EventDispatcher:
|
|||||||
Function to call when a matching event is received.
|
Function to call when a matching event is received.
|
||||||
attribute_filters : Dict[str, Any], optional
|
attribute_filters : Dict[str, Any], optional
|
||||||
Dictionary of attribute key-value pairs that must match for the event to trigger the callback.
|
Dictionary of attribute key-value pairs that must match for the event to trigger the callback.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
--------
|
--------
|
||||||
Subscription object that can be used to unsubscribe.
|
Subscription object that can be used to unsubscribe.
|
||||||
@@ -122,27 +135,37 @@ class EventDispatcher:
|
|||||||
subscription = Subscription(self, event_type, callback, attribute_filters)
|
subscription = Subscription(self, event_type, callback, attribute_filters)
|
||||||
self.subscriptions.append(subscription)
|
self.subscriptions.append(subscription)
|
||||||
return subscription
|
return subscription
|
||||||
|
|
||||||
def _remove_subscription(self, subscription: Subscription):
|
def _remove_subscription(self, subscription: Subscription):
|
||||||
if subscription in self.subscriptions:
|
if subscription in self.subscriptions:
|
||||||
self.subscriptions.remove(subscription)
|
self.subscriptions.remove(subscription)
|
||||||
|
|
||||||
async def dispatch(self, event: Event):
|
async def dispatch(self, event: Event):
|
||||||
await self.queue.put(event)
|
await self.queue.put(event)
|
||||||
|
|
||||||
async def _process_events(self):
|
async def _process_events(self):
|
||||||
while self.running:
|
while self.running:
|
||||||
event = await self.queue.get()
|
event = await self.queue.get()
|
||||||
logger.debug(f"Dispatching event: {event.type}, {event.payload}, {event.attributes}")
|
logger.debug(
|
||||||
|
f"Dispatching event: {event.type}, {event.payload}, {event.attributes}"
|
||||||
|
)
|
||||||
|
|
||||||
for subscription in self.subscriptions.copy():
|
for subscription in self.subscriptions.copy():
|
||||||
# Check if event type matches
|
# Check if event type matches
|
||||||
if subscription.event_type is None or subscription.event_type == event.type:
|
if (
|
||||||
|
subscription.event_type is None
|
||||||
|
or subscription.event_type == event.type
|
||||||
|
):
|
||||||
# Check if all attribute filters match
|
# Check if all attribute filters match
|
||||||
if subscription.attribute_filters and subscription.attribute_filters != {}:
|
if (
|
||||||
|
subscription.attribute_filters
|
||||||
|
and subscription.attribute_filters != {}
|
||||||
|
):
|
||||||
# Skip if any filter doesn't match the corresponding event attribute
|
# Skip if any filter doesn't match the corresponding event attribute
|
||||||
if not all(event.attributes.get(key) == value
|
if not all(
|
||||||
for key, value in subscription.attribute_filters.items()):
|
event.attributes.get(key) == value
|
||||||
|
for key, value in subscription.attribute_filters.items()
|
||||||
|
):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Fire the call back asychronously
|
# Fire the call back asychronously
|
||||||
@@ -161,12 +184,12 @@ class EventDispatcher:
|
|||||||
await result
|
await result
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error in event handler for {event.type}: {e}", exc_info=True)
|
logger.error(f"Error in event handler for {event.type}: {e}", exc_info=True)
|
||||||
|
|
||||||
async def start(self):
|
async def start(self):
|
||||||
if not self.running:
|
if not self.running:
|
||||||
self.running = True
|
self.running = True
|
||||||
self._task = asyncio.create_task(self._process_events())
|
self._task = asyncio.create_task(self._process_events())
|
||||||
|
|
||||||
async def stop(self):
|
async def stop(self):
|
||||||
if self.running:
|
if self.running:
|
||||||
self.running = False
|
self.running = False
|
||||||
@@ -178,12 +201,16 @@ class EventDispatcher:
|
|||||||
except asyncio.CancelledError:
|
except asyncio.CancelledError:
|
||||||
pass
|
pass
|
||||||
self._task = None
|
self._task = None
|
||||||
|
|
||||||
async def wait_for_event(self, event_type: EventType, attribute_filters: Optional[Dict[str, Any]] = None,
|
async def wait_for_event(
|
||||||
timeout: float | None = None) -> Optional[Event]:
|
self,
|
||||||
|
event_type: EventType,
|
||||||
|
attribute_filters: Optional[Dict[str, Any]] = None,
|
||||||
|
timeout: float | None = None,
|
||||||
|
) -> Optional[Event]:
|
||||||
"""
|
"""
|
||||||
Wait for an event of the specified type that matches all attribute filters.
|
Wait for an event of the specified type that matches all attribute filters.
|
||||||
|
|
||||||
Parameters:
|
Parameters:
|
||||||
-----------
|
-----------
|
||||||
event_type : EventType
|
event_type : EventType
|
||||||
@@ -192,19 +219,19 @@ class EventDispatcher:
|
|||||||
Dictionary of attribute key-value pairs that must match for the event to be returned.
|
Dictionary of attribute key-value pairs that must match for the event to be returned.
|
||||||
timeout : float | None, optional
|
timeout : float | None, optional
|
||||||
Maximum time to wait for the event, in seconds.
|
Maximum time to wait for the event, in seconds.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
--------
|
--------
|
||||||
The matched event, or None if timeout occurred before a matching event.
|
The matched event, or None if timeout occurred before a matching event.
|
||||||
"""
|
"""
|
||||||
future = asyncio.Future()
|
future = asyncio.Future()
|
||||||
|
|
||||||
def event_handler(event: Event):
|
def event_handler(event: Event):
|
||||||
if not future.done():
|
if not future.done():
|
||||||
future.set_result(event)
|
future.set_result(event)
|
||||||
|
|
||||||
subscription = self.subscribe(event_type, event_handler, attribute_filters)
|
subscription = self.subscribe(event_type, event_handler, attribute_filters)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
return await asyncio.wait_for(future, timeout)
|
return await asyncio.wait_for(future, timeout)
|
||||||
except asyncio.TimeoutError:
|
except asyncio.TimeoutError:
|
||||||
|
|||||||
@@ -2,60 +2,63 @@ from cayennelpp import LppFrame, LppData
|
|||||||
from cayennelpp.lpp_type import LppType
|
from cayennelpp.lpp_type import LppType
|
||||||
|
|
||||||
# Format : type name "how to display value"
|
# Format : type name "how to display value"
|
||||||
# display : None: (use lib default), []: only one value to display, ["field1", "field2" ...]: meaning of each field
|
# display : None: (use lib default), []: only one value to display, ["field1", "field2" ...]: meaning of each field
|
||||||
my_lpp_types = {
|
my_lpp_types = {
|
||||||
0: ('digital input', []),
|
0: ("digital input", []),
|
||||||
1: ('digital output', []),
|
1: ("digital output", []),
|
||||||
2: ('analog input', []),
|
2: ("analog input", []),
|
||||||
3: ('analog output', []),
|
3: ("analog output", []),
|
||||||
100: ('generic sensor', []),
|
100: ("generic sensor", []),
|
||||||
101: ('illuminance', []),
|
101: ("illuminance", []),
|
||||||
102: ('presence', []),
|
102: ("presence", []),
|
||||||
103: ('temperature', []),
|
103: ("temperature", []),
|
||||||
104: ('humidity', []),
|
104: ("humidity", []),
|
||||||
113: ('accelerometer', ["acc_x", "acc_y", "acc_z"]),
|
113: ("accelerometer", ["acc_x", "acc_y", "acc_z"]),
|
||||||
115: ('barometer', []),
|
115: ("barometer", []),
|
||||||
116: ('voltage', []),
|
116: ("voltage", []),
|
||||||
117: ('current', []),
|
117: ("current", []),
|
||||||
118: ('frequency', []),
|
118: ("frequency", []),
|
||||||
120: ('percentage', []),
|
120: ("percentage", []),
|
||||||
121: ('altitude', []),
|
121: ("altitude", []),
|
||||||
122: ('load', []),
|
122: ("load", []),
|
||||||
125: ('concentration', []),
|
125: ("concentration", []),
|
||||||
128: ('power', []),
|
128: ("power", []),
|
||||||
130: ('distance', []),
|
130: ("distance", []),
|
||||||
131: ('energy', []),
|
131: ("energy", []),
|
||||||
132: ('direction', None),
|
132: ("direction", None),
|
||||||
133: ('time', []),
|
133: ("time", []),
|
||||||
134: ('gyrometer', None),
|
134: ("gyrometer", None),
|
||||||
135: ('colour', ["red", "green", "blue"]),
|
135: ("colour", ["red", "green", "blue"]),
|
||||||
136: ('gps', ["latitude", "longitude", "altitude"]),
|
136: ("gps", ["latitude", "longitude", "altitude"]),
|
||||||
142: ('switch', []),
|
142: ("switch", []),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def lpp_format_val(type, val):
|
def lpp_format_val(type, val):
|
||||||
if my_lpp_types[type.type][1] is None :
|
if my_lpp_types[type.type][1] is None:
|
||||||
return val
|
return val
|
||||||
|
|
||||||
if len(my_lpp_types[type.type][1]) == 0 :
|
if len(my_lpp_types[type.type][1]) == 0:
|
||||||
return val[0]
|
return val[0]
|
||||||
|
|
||||||
val_dict = {}
|
val_dict = {}
|
||||||
i = 0
|
i = 0
|
||||||
for t in my_lpp_types[type.type][1] :
|
for t in my_lpp_types[type.type][1]:
|
||||||
val_dict[t] = val[i]
|
val_dict[t] = val[i]
|
||||||
i = i + 1
|
i = i + 1
|
||||||
return val_dict
|
return val_dict
|
||||||
|
|
||||||
def lpp_json_encoder (obj, types = my_lpp_types) :
|
|
||||||
|
def lpp_json_encoder(obj, types=my_lpp_types):
|
||||||
"""Encode LppType, LppData, and LppFrame to JSON."""
|
"""Encode LppType, LppData, and LppFrame to JSON."""
|
||||||
if isinstance(obj, LppFrame):
|
if isinstance(obj, LppFrame):
|
||||||
return obj.data
|
return obj.data
|
||||||
if isinstance(obj, LppType):
|
if isinstance(obj, LppType):
|
||||||
return my_lpp_types[obj.type][0]
|
return my_lpp_types[obj.type][0]
|
||||||
if isinstance(obj, LppData):
|
if isinstance(obj, LppData):
|
||||||
return {"channel" : obj.channel,
|
return {
|
||||||
"type" : obj.type,
|
"channel": obj.channel,
|
||||||
"value" : lpp_format_val(obj.type, obj.value)
|
"type": obj.type,
|
||||||
|
"value": lpp_format_val(obj.type, obj.value),
|
||||||
}
|
}
|
||||||
raise TypeError(repr(obj) + " is not JSON serialized")
|
raise TypeError(repr(obj) + " is not JSON serialized")
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
from typing import Optional, Dict, Any, Union
|
from typing import Any, Callable, Coroutine, Dict, Optional, Union
|
||||||
|
|
||||||
from .events import EventDispatcher, EventType
|
from .events import Event, EventDispatcher, EventType, Subscription
|
||||||
from .reader import MessageReader
|
from .reader import MessageReader
|
||||||
from .commands import CommandHandler
|
from .commands import CommandHandler
|
||||||
from .connection_manager import ConnectionManager
|
from .connection_manager import ConnectionManager
|
||||||
@@ -13,21 +13,31 @@ from .serial_cx import SerialConnection
|
|||||||
# Setup default logger
|
# Setup default logger
|
||||||
logger = logging.getLogger("meshcore")
|
logger = logging.getLogger("meshcore")
|
||||||
|
|
||||||
|
|
||||||
class MeshCore:
|
class MeshCore:
|
||||||
"""
|
"""
|
||||||
Interface to a MeshCore device
|
Interface to a MeshCore device
|
||||||
"""
|
"""
|
||||||
def __init__(self, cx, debug=False, only_error=False, default_timeout=None, auto_reconnect=False, max_reconnect_attempts=3):
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
cx: Union[BLEConnection, TCPConnection, SerialConnection],
|
||||||
|
debug: bool = False,
|
||||||
|
only_error: bool = False,
|
||||||
|
default_timeout: Optional[float] = None,
|
||||||
|
auto_reconnect: bool = False,
|
||||||
|
max_reconnect_attempts: int = 3,
|
||||||
|
):
|
||||||
# Wrap connection with ConnectionManager
|
# Wrap connection with ConnectionManager
|
||||||
self.dispatcher = EventDispatcher()
|
self.dispatcher = EventDispatcher()
|
||||||
self.connection_manager = ConnectionManager(
|
self.connection_manager = ConnectionManager(
|
||||||
cx, self.dispatcher, auto_reconnect, max_reconnect_attempts
|
cx, self.dispatcher, auto_reconnect, max_reconnect_attempts
|
||||||
)
|
)
|
||||||
self.cx = self.connection_manager # For backward compatibility
|
self.cx = self.connection_manager # For backward compatibility
|
||||||
|
|
||||||
self._reader = MessageReader(self.dispatcher)
|
self._reader = MessageReader(self.dispatcher)
|
||||||
self.commands = CommandHandler(default_timeout=default_timeout)
|
self.commands = CommandHandler(default_timeout=default_timeout)
|
||||||
|
|
||||||
# Set up logger
|
# Set up logger
|
||||||
if debug:
|
if debug:
|
||||||
logger.setLevel(logging.DEBUG)
|
logger.setLevel(logging.DEBUG)
|
||||||
@@ -35,14 +45,14 @@ class MeshCore:
|
|||||||
logger.setLevel(logging.ERROR)
|
logger.setLevel(logging.ERROR)
|
||||||
else:
|
else:
|
||||||
logger.setLevel(logging.INFO)
|
logger.setLevel(logging.INFO)
|
||||||
|
|
||||||
# Set up connections
|
# Set up connections
|
||||||
self.commands.set_connection(self.connection_manager)
|
self.commands.set_connection(self.connection_manager)
|
||||||
|
|
||||||
# Set the dispatcher in the command handler
|
# Set the dispatcher in the command handler
|
||||||
self.commands.set_dispatcher(self.dispatcher)
|
self.commands.set_dispatcher(self.dispatcher)
|
||||||
self.commands.set_reader(self._reader)
|
self.commands.set_reader(self._reader)
|
||||||
|
|
||||||
# Initialize state (private)
|
# Initialize state (private)
|
||||||
self._contacts = {}
|
self._contacts = {}
|
||||||
self._contacts_dirty = True
|
self._contacts_dirty = True
|
||||||
@@ -51,40 +61,78 @@ class MeshCore:
|
|||||||
self._time = 0
|
self._time = 0
|
||||||
self._lastmod = 0
|
self._lastmod = 0
|
||||||
self._auto_update_contacts = False
|
self._auto_update_contacts = False
|
||||||
|
|
||||||
# Set up event subscriptions to track data
|
# Set up event subscriptions to track data
|
||||||
self._setup_data_tracking()
|
self._setup_data_tracking()
|
||||||
|
|
||||||
self.connection_manager.set_reader(self._reader)
|
self.connection_manager.set_reader(self._reader)
|
||||||
|
|
||||||
# Set up disconnect callback
|
# Set up disconnect callback
|
||||||
cx.set_disconnect_callback(self.connection_manager.handle_disconnect)
|
cx.set_disconnect_callback(self.connection_manager.handle_disconnect)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
async def create_tcp(cls, host: str, port: int, debug: bool = False, only_error:bool = False, default_timeout=None,
|
async def create_tcp(
|
||||||
auto_reconnect: bool = False, max_reconnect_attempts: int = 3) -> 'MeshCore':
|
cls,
|
||||||
"""Create and connect a MeshCore instance using TCP connection"""
|
host: str,
|
||||||
|
port: int,
|
||||||
|
debug: bool = False,
|
||||||
|
only_error: bool = False,
|
||||||
|
default_timeout=None,
|
||||||
|
auto_reconnect: bool = False,
|
||||||
|
max_reconnect_attempts: int = 3,
|
||||||
|
) -> "MeshCore":
|
||||||
|
"""Create and connect a MeshCore instance using TCP connection"""
|
||||||
connection = TCPConnection(host, port)
|
connection = TCPConnection(host, port)
|
||||||
|
|
||||||
mc = cls(connection, debug=debug, only_error=only_error, default_timeout=default_timeout,
|
mc = cls(
|
||||||
auto_reconnect=auto_reconnect, max_reconnect_attempts=max_reconnect_attempts)
|
connection,
|
||||||
|
debug=debug,
|
||||||
|
only_error=only_error,
|
||||||
|
default_timeout=default_timeout,
|
||||||
|
auto_reconnect=auto_reconnect,
|
||||||
|
max_reconnect_attempts=max_reconnect_attempts,
|
||||||
|
)
|
||||||
await mc.connect()
|
await mc.connect()
|
||||||
return mc
|
return mc
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
async def create_serial(cls, port: str, baudrate: int = 115200, debug: bool = False, only_error:bool=False, default_timeout=None,
|
async def create_serial(
|
||||||
auto_reconnect: bool = False, max_reconnect_attempts: int = 3, cx_dly:float = 0.1) -> 'MeshCore':
|
cls,
|
||||||
|
port: str,
|
||||||
|
baudrate: int = 115200,
|
||||||
|
debug: bool = False,
|
||||||
|
only_error: bool = False,
|
||||||
|
default_timeout=None,
|
||||||
|
auto_reconnect: bool = False,
|
||||||
|
max_reconnect_attempts: int = 3,
|
||||||
|
cx_dly: float = 0.1,
|
||||||
|
) -> "MeshCore":
|
||||||
"""Create and connect a MeshCore instance using serial connection"""
|
"""Create and connect a MeshCore instance using serial connection"""
|
||||||
connection = SerialConnection(port, baudrate, cx_dly=cx_dly)
|
connection = SerialConnection(port, baudrate, cx_dly=cx_dly)
|
||||||
|
|
||||||
mc = cls(connection, debug=debug, only_error=only_error, default_timeout=default_timeout,
|
mc = cls(
|
||||||
auto_reconnect=auto_reconnect, max_reconnect_attempts=max_reconnect_attempts)
|
connection,
|
||||||
|
debug=debug,
|
||||||
|
only_error=only_error,
|
||||||
|
default_timeout=default_timeout,
|
||||||
|
auto_reconnect=auto_reconnect,
|
||||||
|
max_reconnect_attempts=max_reconnect_attempts,
|
||||||
|
)
|
||||||
await mc.connect()
|
await mc.connect()
|
||||||
return mc
|
return mc
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
async def create_ble(cls, address: Optional[str] = None, client=None, debug: bool = False, only_error:bool=False, default_timeout=None,
|
async def create_ble(
|
||||||
auto_reconnect: bool = False, max_reconnect_attempts: int = 3) -> 'MeshCore':
|
cls,
|
||||||
|
address: Optional[str] = None,
|
||||||
|
client=None,
|
||||||
|
debug: bool = False,
|
||||||
|
only_error: bool = False,
|
||||||
|
default_timeout=None,
|
||||||
|
auto_reconnect: bool = False,
|
||||||
|
max_reconnect_attempts: int = 3,
|
||||||
|
) -> "MeshCore":
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Create and connect a MeshCore instance using BLE connection.
|
Create and connect a MeshCore instance using BLE connection.
|
||||||
|
|
||||||
@@ -94,87 +142,103 @@ class MeshCore:
|
|||||||
If provided, 'address' is ignored for connection
|
If provided, 'address' is ignored for connection
|
||||||
but can be used for identification.
|
but can be used for identification.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
connection = BLEConnection(address=address, client=client)
|
connection = BLEConnection(address=address, client=client)
|
||||||
|
|
||||||
mc = cls(connection, debug=debug, only_error=only_error, default_timeout=default_timeout,
|
mc = cls(
|
||||||
auto_reconnect=auto_reconnect, max_reconnect_attempts=max_reconnect_attempts)
|
connection,
|
||||||
|
debug=debug,
|
||||||
|
only_error=only_error,
|
||||||
|
default_timeout=default_timeout,
|
||||||
|
auto_reconnect=auto_reconnect,
|
||||||
|
max_reconnect_attempts=max_reconnect_attempts,
|
||||||
|
)
|
||||||
|
|
||||||
await mc.connect()
|
await mc.connect()
|
||||||
return mc
|
return mc
|
||||||
|
|
||||||
async def connect(self):
|
async def connect(self):
|
||||||
await self.dispatcher.start()
|
await self.dispatcher.start()
|
||||||
result = await self.connection_manager.connect()
|
result = await self.connection_manager.connect()
|
||||||
if result is None:
|
if result is None:
|
||||||
raise ConnectionError("Failed to connect to device")
|
raise ConnectionError("Failed to connect to device")
|
||||||
return await self.commands.send_appstart()
|
return await self.commands.send_appstart()
|
||||||
|
|
||||||
async def disconnect(self):
|
async def disconnect(self):
|
||||||
"""Disconnect from the device and clean up resources."""
|
"""Disconnect from the device and clean up resources."""
|
||||||
# First stop the dispatcher to prevent any new events
|
# First stop the dispatcher to prevent any new events
|
||||||
await self.dispatcher.stop()
|
await self.dispatcher.stop()
|
||||||
|
|
||||||
# Stop auto message fetching if it's running
|
# Stop auto message fetching if it's running
|
||||||
if hasattr(self, '_auto_fetch_subscription') and self._auto_fetch_subscription:
|
if hasattr(self, "_auto_fetch_subscription") and self._auto_fetch_subscription:
|
||||||
await self.stop_auto_message_fetching()
|
await self.stop_auto_message_fetching()
|
||||||
|
|
||||||
# Disconnect the connection object
|
# Disconnect the connection object
|
||||||
await self.connection_manager.disconnect()
|
await self.connection_manager.disconnect()
|
||||||
|
|
||||||
def stop(self):
|
def stop(self):
|
||||||
"""Synchronously stop the event dispatcher task"""
|
"""Synchronously stop the event dispatcher task"""
|
||||||
if self.dispatcher._task and not self.dispatcher._task.done():
|
if self.dispatcher._task and not self.dispatcher._task.done():
|
||||||
self.dispatcher.running = False
|
self.dispatcher.running = False
|
||||||
self.dispatcher._task.cancel()
|
self.dispatcher._task.cancel()
|
||||||
|
|
||||||
def subscribe(self, event_type: Union[EventType, None], callback, attribute_filters: Optional[Dict[str, Any]] = None):
|
def subscribe(
|
||||||
|
self,
|
||||||
|
event_type: Union[EventType, None],
|
||||||
|
callback: Callable[[Event], Coroutine[Any, Any, None]],
|
||||||
|
attribute_filters: Optional[Dict[str, Any]] = None,
|
||||||
|
) -> Subscription:
|
||||||
"""
|
"""
|
||||||
Subscribe to events using EventType enum with optional attribute filtering
|
Subscribe to events using EventType enum with optional attribute filtering
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
event_type: Type of event to subscribe to, from EventType enum
|
event_type: Type of event to subscribe to, from EventType enum
|
||||||
callback: Async function to call when event occurs
|
callback: Async function to call when event occurs
|
||||||
attribute_filters: Dictionary of attribute key-value pairs that must match for the event to trigger the callback
|
attribute_filters: Dictionary of attribute key-value pairs that must match for the event to trigger the callback
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Subscription object that can be used to unsubscribe
|
Subscription object that can be used to unsubscribe
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
# Subscribe to ACK events where the 'code' attribute has a specific value
|
# Subscribe to ACK events where the 'code' attribute has a specific value
|
||||||
mc.subscribe(
|
mc.subscribe(
|
||||||
EventType.ACK,
|
EventType.ACK,
|
||||||
my_callback_function,
|
my_callback_function,
|
||||||
attribute_filters={'code': 'SUCCESS'}
|
attribute_filters={'code': 'SUCCESS'}
|
||||||
)
|
)
|
||||||
"""
|
"""
|
||||||
return self.dispatcher.subscribe(event_type, callback, attribute_filters)
|
return self.dispatcher.subscribe(event_type, callback, attribute_filters)
|
||||||
|
|
||||||
def unsubscribe(self, subscription):
|
def unsubscribe(self, subscription: Subscription) -> None:
|
||||||
"""
|
"""
|
||||||
Unsubscribe from events using a subscription object
|
Unsubscribe from events using a subscription object
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
subscription: Subscription object returned from subscribe()
|
subscription: Subscription object returned from subscribe()
|
||||||
"""
|
"""
|
||||||
if subscription:
|
if subscription:
|
||||||
subscription.unsubscribe()
|
subscription.unsubscribe()
|
||||||
|
|
||||||
async def wait_for_event(self, event_type: EventType, attribute_filters: Optional[Dict[str, Any]] = None, timeout=None):
|
async def wait_for_event(
|
||||||
|
self,
|
||||||
|
event_type: EventType,
|
||||||
|
attribute_filters: Optional[Dict[str, Any]] = None,
|
||||||
|
timeout: Optional[float] = None,
|
||||||
|
) -> Optional[Event]:
|
||||||
"""
|
"""
|
||||||
Wait for an event using EventType enum with optional attribute filtering
|
Wait for an event using EventType enum with optional attribute filtering
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
event_type: Type of event to wait for, from EventType enum
|
event_type: Type of event to wait for, from EventType enum
|
||||||
attribute_filters: Dictionary of attribute key-value pairs to match against the event
|
attribute_filters: Dictionary of attribute key-value pairs to match against the event
|
||||||
timeout: Maximum time to wait in seconds, or None to use default_timeout
|
timeout: Maximum time to wait in seconds, or None to use default_timeout
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Event object or None if timeout
|
Event object or None if timeout
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
# Wait for an ACK event where the 'code' attribute has a specific value
|
# Wait for an ACK event where the 'code' attribute has a specific value
|
||||||
await mc.wait_for_event(
|
await mc.wait_for_event(
|
||||||
EventType.ACK,
|
EventType.ACK,
|
||||||
attribute_filters={'code': 'SUCCESS'},
|
attribute_filters={'code': 'SUCCESS'},
|
||||||
timeout=30.0
|
timeout=30.0
|
||||||
)
|
)
|
||||||
@@ -182,22 +246,25 @@ class MeshCore:
|
|||||||
# Use the provided timeout or fall back to default_timeout
|
# Use the provided timeout or fall back to default_timeout
|
||||||
if timeout is None:
|
if timeout is None:
|
||||||
timeout = self.default_timeout
|
timeout = self.default_timeout
|
||||||
|
|
||||||
return await self.dispatcher.wait_for_event(event_type, attribute_filters, timeout)
|
return await self.dispatcher.wait_for_event(
|
||||||
|
event_type, attribute_filters, timeout
|
||||||
|
)
|
||||||
|
|
||||||
def _setup_data_tracking(self):
|
def _setup_data_tracking(self):
|
||||||
"""Set up event subscriptions to track data internally"""
|
"""Set up event subscriptions to track data internally"""
|
||||||
|
|
||||||
async def _update_contacts(event):
|
async def _update_contacts(event):
|
||||||
#self._contacts.update(event.payload)
|
# self._contacts.update(event.payload)
|
||||||
for c in event.payload.values():
|
for c in event.payload.values():
|
||||||
if c["public_key"] in self._contacts:
|
if c["public_key"] in self._contacts:
|
||||||
self._contacts[c["public_key"]].update(c)
|
self._contacts[c["public_key"]].update(c)
|
||||||
else:
|
else:
|
||||||
self._contacts[c["public_key"]]=c
|
self._contacts[c["public_key"]] = c
|
||||||
if "lastmod" in event.attributes :
|
if "lastmod" in event.attributes:
|
||||||
self._lastmod = event.attributes['lastmod']
|
self._lastmod = event.attributes["lastmod"]
|
||||||
self._contacts_dirty = False
|
self._contacts_dirty = False
|
||||||
|
|
||||||
async def _add_pending_contact(event):
|
async def _add_pending_contact(event):
|
||||||
c = event.payload
|
c = event.payload
|
||||||
self._pending_contacts[c["public_key"]] = c
|
self._pending_contacts[c["public_key"]] = c
|
||||||
@@ -206,13 +273,13 @@ class MeshCore:
|
|||||||
self._contacts_dirty = True
|
self._contacts_dirty = True
|
||||||
if self._auto_update_contacts:
|
if self._auto_update_contacts:
|
||||||
await self.ensure_contacts(follow=True)
|
await self.ensure_contacts(follow=True)
|
||||||
|
|
||||||
async def _update_self_info(event):
|
async def _update_self_info(event):
|
||||||
self._self_info = event.payload
|
self._self_info = event.payload
|
||||||
|
|
||||||
async def _update_time(event):
|
async def _update_time(event):
|
||||||
self._time = event.payload.get("time", 0)
|
self._time = event.payload.get("time", 0)
|
||||||
|
|
||||||
# Subscribe to events to update internal state
|
# Subscribe to events to update internal state
|
||||||
self.subscribe(EventType.CONTACTS, _update_contacts)
|
self.subscribe(EventType.CONTACTS, _update_contacts)
|
||||||
self.subscribe(EventType.NEW_CONTACT, _add_pending_contact)
|
self.subscribe(EventType.NEW_CONTACT, _add_pending_contact)
|
||||||
@@ -220,166 +287,177 @@ class MeshCore:
|
|||||||
self.subscribe(EventType.CURRENT_TIME, _update_time)
|
self.subscribe(EventType.CURRENT_TIME, _update_time)
|
||||||
self.subscribe(EventType.ADVERTISEMENT, _contact_change)
|
self.subscribe(EventType.ADVERTISEMENT, _contact_change)
|
||||||
self.subscribe(EventType.PATH_UPDATE, _contact_change)
|
self.subscribe(EventType.PATH_UPDATE, _contact_change)
|
||||||
|
|
||||||
# Getter methods for state
|
# Getter methods for state
|
||||||
@property
|
@property
|
||||||
def contacts(self):
|
def contacts(self) -> Dict[str, Any]:
|
||||||
"""Get the current contacts"""
|
"""Get the current contacts"""
|
||||||
return self._contacts
|
return self._contacts
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def contacts_dirty(self):
|
def contacts_dirty(self) -> bool:
|
||||||
"""Get wether contact list is in sync"""
|
"""Get wether contact list is in sync"""
|
||||||
return self._contacts_dirty
|
return self._contacts_dirty
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def auto_update_contacts(self):
|
def auto_update_contacts(self) -> bool:
|
||||||
"""Get wether contact list is in sync"""
|
"""Get wether contact list is in sync"""
|
||||||
return self._auto_update_contacts
|
return self._auto_update_contacts
|
||||||
|
|
||||||
@auto_update_contacts.setter
|
@auto_update_contacts.setter
|
||||||
def auto_update_contacts(self, value):
|
def auto_update_contacts(self, value: bool) -> None:
|
||||||
self._auto_update_contacts = value
|
self._auto_update_contacts = value
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def self_info(self):
|
def self_info(self) -> Dict[str, Any]:
|
||||||
"""Get device self info"""
|
"""Get device self info"""
|
||||||
return self._self_info
|
return self._self_info
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def time(self):
|
def time(self) -> int:
|
||||||
"""Get the current device time"""
|
"""Get the current device time"""
|
||||||
return self._time
|
return self._time
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_connected(self):
|
def is_connected(self) -> bool:
|
||||||
"""Check if the connection is active"""
|
"""Check if the connection is active"""
|
||||||
return self.connection_manager.is_connected
|
return self.connection_manager.is_connected
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def default_timeout(self):
|
def default_timeout(self) -> float:
|
||||||
"""Get the default timeout for commands"""
|
"""Get the default timeout for commands"""
|
||||||
return self.commands.default_timeout
|
return self.commands.default_timeout
|
||||||
|
|
||||||
@default_timeout.setter
|
@default_timeout.setter
|
||||||
def default_timeout(self, value):
|
def default_timeout(self, value: float) -> None:
|
||||||
"""Set the default timeout for commands"""
|
"""Set the default timeout for commands"""
|
||||||
self.commands.default_timeout = value
|
self.commands.default_timeout = value
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def pending_contacts(self):
|
def pending_contacts(self) -> Dict[str, Any]:
|
||||||
"""Get pending contacts"""
|
"""Get pending contacts"""
|
||||||
return self._pending_contacts
|
return self._pending_contacts
|
||||||
|
|
||||||
def pop_pending_contact(self, key):
|
def pop_pending_contact(self, key: str) -> Optional[Dict[str, Any]]:
|
||||||
return self._pending_contacts.pop(key, None)
|
return self._pending_contacts.pop(key, None)
|
||||||
|
|
||||||
def flush_pending_contacts(self): # would be interesting to have a time param
|
def flush_pending_contacts(self) -> None: # would be interesting to have a time param
|
||||||
self._pending_contacts = {}
|
self._pending_contacts = {}
|
||||||
|
|
||||||
def get_contact_by_name(self, name) -> Optional[Dict[str, Any]]:
|
def get_contact_by_name(self, name: str) -> Optional[Dict[str, Any]]:
|
||||||
"""
|
"""
|
||||||
Find a contact by its name (adv_name field)
|
Find a contact by its name (adv_name field)
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
name: The name to search for
|
name: The name to search for
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Contact dictionary or None if not found
|
Contact dictionary or None if not found
|
||||||
"""
|
"""
|
||||||
if not self._contacts:
|
if not self._contacts:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
for _, contact in self._contacts.items():
|
for _, contact in self._contacts.items():
|
||||||
if contact.get("adv_name", "").lower() == name.lower():
|
if contact.get("adv_name", "").lower() == name.lower():
|
||||||
return contact
|
return contact
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def get_contact_by_key_prefix(self, prefix) -> Optional[Dict[str, Any]]:
|
def get_contact_by_key_prefix(self, prefix: str) -> Optional[Dict[str, Any]]:
|
||||||
"""
|
"""
|
||||||
Find a contact by its public key prefix
|
Find a contact by its public key prefix
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
prefix: The public key prefix to search for (can be a partial prefix)
|
prefix: The public key prefix to search for (can be a partial prefix)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Contact dictionary or None if not found
|
Contact dictionary or None if not found
|
||||||
"""
|
"""
|
||||||
if not self._contacts or not prefix:
|
if not self._contacts or not prefix:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# Convert the prefix to lowercase for case-insensitive matching
|
# Convert the prefix to lowercase for case-insensitive matching
|
||||||
prefix = prefix.lower()
|
prefix = prefix.lower()
|
||||||
|
|
||||||
for contact_id, contact in self._contacts.items():
|
for contact_id, contact in self._contacts.items():
|
||||||
public_key = contact.get("public_key", "").lower()
|
public_key = contact.get("public_key", "").lower()
|
||||||
if public_key.startswith(prefix):
|
if public_key.startswith(prefix):
|
||||||
return contact
|
return contact
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
async def start_auto_message_fetching(self):
|
async def start_auto_message_fetching(self) -> Subscription:
|
||||||
"""
|
"""
|
||||||
Start automatically fetching messages when messages_waiting events are received.
|
Start automatically fetching messages when messages_waiting events are received.
|
||||||
This will continuously check for new messages when the device indicates
|
This will continuously check for new messages when the device indicates
|
||||||
messages are waiting.
|
messages are waiting.
|
||||||
"""
|
"""
|
||||||
self._auto_fetch_task = None
|
self._auto_fetch_task = None
|
||||||
self._auto_fetch_running = True
|
self._auto_fetch_running = True
|
||||||
|
|
||||||
async def _handle_messages_waiting(event):
|
async def _handle_messages_waiting(event):
|
||||||
# Only start a new fetch task if one isn't already running
|
# Only start a new fetch task if one isn't already running
|
||||||
if not self._auto_fetch_task or self._auto_fetch_task.done():
|
if not self._auto_fetch_task or self._auto_fetch_task.done():
|
||||||
self._auto_fetch_task = asyncio.create_task(_fetch_messages_loop())
|
self._auto_fetch_task = asyncio.create_task(_fetch_messages_loop())
|
||||||
|
|
||||||
async def _fetch_messages_loop():
|
async def _fetch_messages_loop():
|
||||||
while self._auto_fetch_running:
|
while self._auto_fetch_running:
|
||||||
try:
|
try:
|
||||||
# Request the next message
|
# Request the next message
|
||||||
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 result.type == EventType.NO_MORE_MSGS or result.type == EventType.ERROR:
|
if (
|
||||||
logger.debug("No more messages or error occurred, stopping auto-fetch.")
|
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
|
||||||
await asyncio.sleep(0.1)
|
await asyncio.sleep(0.1)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error fetching messages: {e}")
|
logger.error(f"Error fetching messages: {e}")
|
||||||
break
|
break
|
||||||
|
|
||||||
# Subscribe to MESSAGES_WAITING events
|
# Subscribe to MESSAGES_WAITING events
|
||||||
self._auto_fetch_subscription = self.subscribe(EventType.MESSAGES_WAITING, _handle_messages_waiting)
|
self._auto_fetch_subscription = self.subscribe(
|
||||||
|
EventType.MESSAGES_WAITING, _handle_messages_waiting
|
||||||
|
)
|
||||||
|
|
||||||
# Check for any pending messages immediately
|
# Check for any pending messages immediately
|
||||||
await self.commands.get_msg()
|
await self.commands.get_msg()
|
||||||
|
|
||||||
return self._auto_fetch_subscription
|
return self._auto_fetch_subscription
|
||||||
|
|
||||||
async def stop_auto_message_fetching(self):
|
async def stop_auto_message_fetching(self):
|
||||||
"""
|
"""
|
||||||
Stop automatically fetching messages when messages_waiting events are received.
|
Stop automatically fetching messages when messages_waiting events are received.
|
||||||
"""
|
"""
|
||||||
if hasattr(self, '_auto_fetch_subscription') and self._auto_fetch_subscription:
|
if hasattr(self, "_auto_fetch_subscription") and self._auto_fetch_subscription:
|
||||||
self.unsubscribe(self._auto_fetch_subscription)
|
self.unsubscribe(self._auto_fetch_subscription)
|
||||||
self._auto_fetch_subscription = None
|
self._auto_fetch_subscription = None
|
||||||
|
|
||||||
if hasattr(self, '_auto_fetch_running'):
|
if hasattr(self, "_auto_fetch_running"):
|
||||||
self._auto_fetch_running = False
|
self._auto_fetch_running = False
|
||||||
|
|
||||||
if hasattr(self, '_auto_fetch_task') and self._auto_fetch_task and not self._auto_fetch_task.done():
|
if (
|
||||||
|
hasattr(self, "_auto_fetch_task")
|
||||||
|
and self._auto_fetch_task
|
||||||
|
and not self._auto_fetch_task.done()
|
||||||
|
):
|
||||||
self._auto_fetch_task.cancel()
|
self._auto_fetch_task.cancel()
|
||||||
try:
|
try:
|
||||||
await self._auto_fetch_task # type: ignore
|
await self._auto_fetch_task # type: ignore
|
||||||
except asyncio.CancelledError:
|
except asyncio.CancelledError:
|
||||||
pass
|
pass
|
||||||
self._auto_fetch_task = None
|
self._auto_fetch_task = None
|
||||||
|
|
||||||
async def ensure_contacts(self, follow=False):
|
async def ensure_contacts(self, follow: bool = False) -> bool:
|
||||||
"""Ensure contacts are fetched"""
|
"""Ensure contacts are fetched"""
|
||||||
if not self._contacts or (follow and self._contacts_dirty) :
|
if not self._contacts or (follow and self._contacts_dirty):
|
||||||
await self.commands.get_contacts(lastmod = self._lastmod)
|
await self.commands.get_contacts(lastmod=self._lastmod)
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
from enum import Enum
|
from enum import Enum
|
||||||
|
|
||||||
|
|
||||||
# Packet prefixes for the protocol
|
# Packet prefixes for the protocol
|
||||||
class PacketType(Enum):
|
class PacketType(Enum):
|
||||||
OK = 0
|
OK = 0
|
||||||
@@ -26,7 +27,7 @@ class PacketType(Enum):
|
|||||||
CUSTOM_VARS = 21
|
CUSTOM_VARS = 21
|
||||||
BINARY_REQ = 50
|
BINARY_REQ = 50
|
||||||
FACTORY_RESET = 51
|
FACTORY_RESET = 51
|
||||||
|
|
||||||
# Push notifications
|
# Push notifications
|
||||||
ADVERTISEMENT = 0x80
|
ADVERTISEMENT = 0x80
|
||||||
PATH_UPDATE = 0x81
|
PATH_UPDATE = 0x81
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
import sys
|
|
||||||
import logging
|
import logging
|
||||||
import asyncio
|
|
||||||
import json
|
import json
|
||||||
from typing import Any, Optional, Dict
|
from typing import Any, Dict
|
||||||
from .events import Event, EventType, EventDispatcher
|
from .events import Event, EventType, EventDispatcher
|
||||||
from .packets import PacketType
|
from .packets import PacketType
|
||||||
from cayennelpp import LppFrame, LppData
|
from cayennelpp import LppFrame, LppData
|
||||||
@@ -18,35 +16,37 @@ class MessageReader:
|
|||||||
# before events are dispatched
|
# before events are dispatched
|
||||||
self.contacts = {} # Temporary storage during contact list building
|
self.contacts = {} # Temporary storage during contact list building
|
||||||
self.contact_nb = 0 # Used for contact processing
|
self.contact_nb = 0 # Used for contact processing
|
||||||
|
|
||||||
async def handle_rx(self, data: bytearray):
|
async def handle_rx(self, data: bytearray):
|
||||||
packet_type_value = data[0]
|
packet_type_value = data[0]
|
||||||
logger.debug(f"Received data: {data.hex()}")
|
logger.debug(f"Received data: {data.hex()}")
|
||||||
|
|
||||||
# Handle command responses
|
# Handle command responses
|
||||||
if packet_type_value == PacketType.OK.value:
|
if packet_type_value == PacketType.OK.value:
|
||||||
result: Dict[str, Any] = {}
|
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")
|
||||||
|
|
||||||
# Dispatch event for the OK response
|
# Dispatch event for the OK response
|
||||||
await self.dispatcher.dispatch(Event(EventType.OK, result))
|
await self.dispatcher.dispatch(Event(EventType.OK, result))
|
||||||
|
|
||||||
elif packet_type_value == PacketType.ERROR.value:
|
elif packet_type_value == PacketType.ERROR.value:
|
||||||
if len(data) > 1:
|
if len(data) > 1:
|
||||||
result = {"error_code": data[1]}
|
result = {"error_code": data[1]}
|
||||||
else:
|
else:
|
||||||
result = {}
|
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))
|
||||||
|
|
||||||
elif packet_type_value == PacketType.CONTACT_START.value:
|
elif packet_type_value == PacketType.CONTACT_START.value:
|
||||||
self.contact_nb = int.from_bytes(data[1:5], byteorder='little')
|
self.contact_nb = int.from_bytes(data[1:5], byteorder="little")
|
||||||
self.contacts = {}
|
self.contacts = {}
|
||||||
|
|
||||||
elif packet_type_value == PacketType.CONTACT.value or\
|
elif (
|
||||||
packet_type_value == PacketType.PUSH_CODE_NEW_ADVERT.value:
|
packet_type_value == PacketType.CONTACT.value
|
||||||
|
or packet_type_value == PacketType.PUSH_CODE_NEW_ADVERT.value
|
||||||
|
):
|
||||||
c = {}
|
c = {}
|
||||||
c["public_key"] = data[1:33].hex()
|
c["public_key"] = data[1:33].hex()
|
||||||
c["type"] = data[33]
|
c["type"] = data[33]
|
||||||
@@ -55,195 +55,215 @@ class MessageReader:
|
|||||||
plen = int.from_bytes(data[35:36], signed=True)
|
plen = int.from_bytes(data[35:36], signed=True)
|
||||||
if plen == -1:
|
if plen == -1:
|
||||||
plen = 0
|
plen = 0
|
||||||
c["out_path"] = data[36:36+plen].hex()
|
c["out_path"] = data[36 : 36 + plen].hex()
|
||||||
c["adv_name"] = data[100:132].decode('utf-8', 'ignore').replace("\0","")
|
c["adv_name"] = data[100:132].decode("utf-8", "ignore").replace("\0", "")
|
||||||
c["last_advert"] = int.from_bytes(data[132:136], byteorder='little')
|
c["last_advert"] = int.from_bytes(data[132:136], byteorder="little")
|
||||||
c["adv_lat"] = int.from_bytes(data[136:140], byteorder='little',signed=True)/1e6
|
c["adv_lat"] = (
|
||||||
c["adv_lon"] = int.from_bytes(data[140:144], byteorder='little',signed=True)/1e6
|
int.from_bytes(data[136:140], byteorder="little", signed=True) / 1e6
|
||||||
c["lastmod"] = int.from_bytes(data[144:148], byteorder='little')
|
)
|
||||||
|
c["adv_lon"] = (
|
||||||
|
int.from_bytes(data[140:144], byteorder="little", signed=True) / 1e6
|
||||||
|
)
|
||||||
|
c["lastmod"] = int.from_bytes(data[144:148], byteorder="little")
|
||||||
|
|
||||||
if packet_type_value == PacketType.PUSH_CODE_NEW_ADVERT.value :
|
if packet_type_value == PacketType.PUSH_CODE_NEW_ADVERT.value:
|
||||||
await self.dispatcher.dispatch(Event(EventType.NEW_CONTACT, c))
|
await self.dispatcher.dispatch(Event(EventType.NEW_CONTACT, c))
|
||||||
else:
|
else:
|
||||||
self.contacts[c["public_key"]] = c
|
self.contacts[c["public_key"]] = c
|
||||||
|
|
||||||
elif packet_type_value == PacketType.CONTACT_END.value:
|
elif packet_type_value == PacketType.CONTACT_END.value:
|
||||||
lastmod = int.from_bytes(data[1:5], byteorder='little')
|
lastmod = int.from_bytes(data[1:5], byteorder="little")
|
||||||
attributes = {
|
attributes = {
|
||||||
"lastmod": lastmod,
|
"lastmod": lastmod,
|
||||||
}
|
}
|
||||||
await self.dispatcher.dispatch(Event(EventType.CONTACTS, self.contacts, attributes))
|
await self.dispatcher.dispatch(
|
||||||
|
Event(EventType.CONTACTS, self.contacts, attributes)
|
||||||
|
)
|
||||||
|
|
||||||
elif packet_type_value == PacketType.SELF_INFO.value:
|
elif packet_type_value == PacketType.SELF_INFO.value:
|
||||||
self_info = {}
|
self_info = {}
|
||||||
self_info["adv_type"] = data[1]
|
self_info["adv_type"] = data[1]
|
||||||
self_info["tx_power"] = data[2]
|
self_info["tx_power"] = data[2]
|
||||||
self_info["max_tx_power"] = data[3]
|
self_info["max_tx_power"] = data[3]
|
||||||
self_info["public_key"] = data[4:36].hex()
|
self_info["public_key"] = data[4:36].hex()
|
||||||
self_info["adv_lat"] = int.from_bytes(data[36:40], byteorder='little', signed=True)/1e6
|
self_info["adv_lat"] = (
|
||||||
self_info["adv_lon"] = int.from_bytes(data[40:44], byteorder='little', signed=True)/1e6
|
int.from_bytes(data[36:40], byteorder="little", signed=True) / 1e6
|
||||||
|
)
|
||||||
|
self_info["adv_lon"] = (
|
||||||
|
int.from_bytes(data[40:44], byteorder="little", signed=True) / 1e6
|
||||||
|
)
|
||||||
self_info["adv_loc_policy"] = data[45]
|
self_info["adv_loc_policy"] = data[45]
|
||||||
self_info["telemetry_mode_env"] = (data[46] >> 4) & 0b11
|
self_info["telemetry_mode_env"] = (data[46] >> 4) & 0b11
|
||||||
self_info["telemetry_mode_loc"] = (data[46] >> 2) & 0b11
|
self_info["telemetry_mode_loc"] = (data[46] >> 2) & 0b11
|
||||||
self_info["telemetry_mode_base"] = (data[46]) & 0b11
|
self_info["telemetry_mode_base"] = (data[46]) & 0b11
|
||||||
self_info["manual_add_contacts"] = data[47] > 0
|
self_info["manual_add_contacts"] = data[47] > 0
|
||||||
self_info["radio_freq"] = int.from_bytes(data[48:52], byteorder='little') / 1000
|
self_info["radio_freq"] = (
|
||||||
self_info["radio_bw"] = int.from_bytes(data[52:56], byteorder='little') / 1000
|
int.from_bytes(data[48:52], byteorder="little") / 1000
|
||||||
|
)
|
||||||
|
self_info["radio_bw"] = (
|
||||||
|
int.from_bytes(data[52:56], byteorder="little") / 1000
|
||||||
|
)
|
||||||
self_info["radio_sf"] = data[56]
|
self_info["radio_sf"] = data[56]
|
||||||
self_info["radio_cr"] = data[57]
|
self_info["radio_cr"] = data[57]
|
||||||
self_info["name"] = data[58:].decode('utf-8', 'ignore')
|
self_info["name"] = data[58:].decode("utf-8", "ignore")
|
||||||
await self.dispatcher.dispatch(Event(EventType.SELF_INFO, self_info))
|
await self.dispatcher.dispatch(Event(EventType.SELF_INFO, self_info))
|
||||||
|
|
||||||
elif packet_type_value == PacketType.MSG_SENT.value:
|
elif packet_type_value == PacketType.MSG_SENT.value:
|
||||||
res = {}
|
res = {}
|
||||||
res["type"] = data[1]
|
res["type"] = data[1]
|
||||||
res["expected_ack"] = bytes(data[2:6])
|
res["expected_ack"] = bytes(data[2:6])
|
||||||
res["suggested_timeout"] = int.from_bytes(data[6:10], byteorder='little')
|
res["suggested_timeout"] = int.from_bytes(data[6:10], byteorder="little")
|
||||||
|
|
||||||
attributes = {
|
attributes = {
|
||||||
"type": res["type"],
|
"type": res["type"],
|
||||||
"expected_ack": res["expected_ack"].hex()
|
"expected_ack": res["expected_ack"].hex(),
|
||||||
}
|
}
|
||||||
|
|
||||||
await self.dispatcher.dispatch(Event(EventType.MSG_SENT, res, attributes))
|
await self.dispatcher.dispatch(Event(EventType.MSG_SENT, res, attributes))
|
||||||
|
|
||||||
elif packet_type_value == PacketType.CONTACT_MSG_RECV.value:
|
elif packet_type_value == PacketType.CONTACT_MSG_RECV.value:
|
||||||
res = {}
|
res = {}
|
||||||
res["type"] = "PRIV"
|
res["type"] = "PRIV"
|
||||||
res["pubkey_prefix"] = data[1:7].hex()
|
res["pubkey_prefix"] = data[1:7].hex()
|
||||||
res["path_len"] = data[7]
|
res["path_len"] = data[7]
|
||||||
res["txt_type"] = data[8]
|
res["txt_type"] = data[8]
|
||||||
res["sender_timestamp"] = int.from_bytes(data[9:13], byteorder='little')
|
res["sender_timestamp"] = int.from_bytes(data[9:13], byteorder="little")
|
||||||
if data[8] == 2:
|
if data[8] == 2:
|
||||||
res["signature"] = data[13:17].hex()
|
res["signature"] = data[13:17].hex()
|
||||||
res["text"] = data[17:].decode('utf-8', 'ignore')
|
res["text"] = data[17:].decode("utf-8", "ignore")
|
||||||
else:
|
else:
|
||||||
res["text"] = data[13:].decode('utf-8', 'ignore')
|
res["text"] = data[13:].decode("utf-8", "ignore")
|
||||||
|
|
||||||
attributes = {
|
attributes = {
|
||||||
"pubkey_prefix": res["pubkey_prefix"],
|
"pubkey_prefix": res["pubkey_prefix"],
|
||||||
"txt_type": res["txt_type"]
|
"txt_type": res["txt_type"],
|
||||||
}
|
}
|
||||||
|
|
||||||
evt_type = EventType.CONTACT_MSG_RECV
|
evt_type = EventType.CONTACT_MSG_RECV
|
||||||
|
|
||||||
await self.dispatcher.dispatch(Event(evt_type, res, attributes))
|
await self.dispatcher.dispatch(Event(evt_type, res, attributes))
|
||||||
|
|
||||||
elif packet_type_value == 16: # A reply to CMD_SYNC_NEXT_MESSAGE (ver >= 3)
|
elif packet_type_value == 16: # A reply to CMD_SYNC_NEXT_MESSAGE (ver >= 3)
|
||||||
res = {}
|
res = {}
|
||||||
res["type"] = "PRIV"
|
res["type"] = "PRIV"
|
||||||
res["SNR"] = int.from_bytes(data[1:2], byteorder='little', signed=True) / 4
|
res["SNR"] = int.from_bytes(data[1:2], byteorder="little", signed=True) / 4
|
||||||
res["pubkey_prefix"] = data[4:10].hex()
|
res["pubkey_prefix"] = data[4:10].hex()
|
||||||
res["path_len"] = data[10]
|
res["path_len"] = data[10]
|
||||||
res["txt_type"] = data[11]
|
res["txt_type"] = data[11]
|
||||||
res["sender_timestamp"] = int.from_bytes(data[12:16], byteorder='little')
|
res["sender_timestamp"] = int.from_bytes(data[12:16], byteorder="little")
|
||||||
if data[11] == 2:
|
if data[11] == 2:
|
||||||
res["signature"] = data[16:20].hex()
|
res["signature"] = data[16:20].hex()
|
||||||
res["text"] = data[20:].decode('utf-8', 'ignore')
|
res["text"] = data[20:].decode("utf-8", "ignore")
|
||||||
else:
|
else:
|
||||||
res["text"] = data[16:].decode('utf-8', 'ignore')
|
res["text"] = data[16:].decode("utf-8", "ignore")
|
||||||
|
|
||||||
attributes = {
|
attributes = {
|
||||||
"pubkey_prefix": res["pubkey_prefix"],
|
"pubkey_prefix": res["pubkey_prefix"],
|
||||||
"txt_type": res["txt_type"]
|
"txt_type": res["txt_type"],
|
||||||
}
|
}
|
||||||
|
|
||||||
await self.dispatcher.dispatch(Event(EventType.CONTACT_MSG_RECV, res, attributes))
|
await self.dispatcher.dispatch(
|
||||||
|
Event(EventType.CONTACT_MSG_RECV, res, attributes)
|
||||||
|
)
|
||||||
|
|
||||||
elif packet_type_value == PacketType.CHANNEL_MSG_RECV.value:
|
elif packet_type_value == PacketType.CHANNEL_MSG_RECV.value:
|
||||||
res = {}
|
res = {}
|
||||||
res["type"] = "CHAN"
|
res["type"] = "CHAN"
|
||||||
res["channel_idx"] = data[1]
|
res["channel_idx"] = data[1]
|
||||||
res["path_len"] = data[2]
|
res["path_len"] = data[2]
|
||||||
res["txt_type"] = data[3]
|
res["txt_type"] = data[3]
|
||||||
res["sender_timestamp"] = int.from_bytes(data[4:8], byteorder='little')
|
res["sender_timestamp"] = int.from_bytes(data[4:8], byteorder="little")
|
||||||
res["text"] = data[8:].decode('utf-8', 'ignore')
|
res["text"] = data[8:].decode("utf-8", "ignore")
|
||||||
|
|
||||||
attributes = {
|
attributes = {
|
||||||
"channel_idx": res["channel_idx"],
|
"channel_idx": res["channel_idx"],
|
||||||
"txt_type": res["txt_type"]
|
"txt_type": res["txt_type"],
|
||||||
}
|
}
|
||||||
|
|
||||||
await self.dispatcher.dispatch(Event(EventType.CHANNEL_MSG_RECV, res, attributes))
|
await self.dispatcher.dispatch(
|
||||||
|
Event(EventType.CHANNEL_MSG_RECV, res, attributes)
|
||||||
|
)
|
||||||
|
|
||||||
elif packet_type_value == 17: # A reply to CMD_SYNC_NEXT_MESSAGE (ver >= 3)
|
elif packet_type_value == 17: # A reply to CMD_SYNC_NEXT_MESSAGE (ver >= 3)
|
||||||
res = {}
|
res = {}
|
||||||
res["type"] = "CHAN"
|
res["type"] = "CHAN"
|
||||||
res["SNR"] = int.from_bytes(data[1:2], byteorder='little', signed=True) / 4
|
res["SNR"] = int.from_bytes(data[1:2], byteorder="little", signed=True) / 4
|
||||||
res["channel_idx"] = data[4]
|
res["channel_idx"] = data[4]
|
||||||
res["path_len"] = data[5]
|
res["path_len"] = data[5]
|
||||||
res["txt_type"] = data[6]
|
res["txt_type"] = data[6]
|
||||||
res["sender_timestamp"] = int.from_bytes(data[7:11], byteorder='little')
|
res["sender_timestamp"] = int.from_bytes(data[7:11], byteorder="little")
|
||||||
res["text"] = data[11:].decode('utf-8', 'ignore')
|
res["text"] = data[11:].decode("utf-8", "ignore")
|
||||||
|
|
||||||
attributes = {
|
attributes = {
|
||||||
"channel_idx": res["channel_idx"],
|
"channel_idx": res["channel_idx"],
|
||||||
"txt_type": res["txt_type"]
|
"txt_type": res["txt_type"],
|
||||||
}
|
}
|
||||||
|
|
||||||
await self.dispatcher.dispatch(Event(EventType.CHANNEL_MSG_RECV, res, attributes))
|
await self.dispatcher.dispatch(
|
||||||
|
Event(EventType.CHANNEL_MSG_RECV, res, attributes)
|
||||||
|
)
|
||||||
|
|
||||||
elif packet_type_value == PacketType.CURRENT_TIME.value:
|
elif packet_type_value == PacketType.CURRENT_TIME.value:
|
||||||
time_value = int.from_bytes(data[1:5], byteorder='little')
|
time_value = int.from_bytes(data[1:5], byteorder="little")
|
||||||
result = {"time": time_value}
|
result = {"time": time_value}
|
||||||
await self.dispatcher.dispatch(Event(EventType.CURRENT_TIME, result))
|
await self.dispatcher.dispatch(Event(EventType.CURRENT_TIME, result))
|
||||||
|
|
||||||
elif packet_type_value == PacketType.NO_MORE_MSGS.value:
|
elif packet_type_value == PacketType.NO_MORE_MSGS.value:
|
||||||
result = {"messages_available": False}
|
result = {"messages_available": False}
|
||||||
await self.dispatcher.dispatch(Event(EventType.NO_MORE_MSGS, result))
|
await self.dispatcher.dispatch(Event(EventType.NO_MORE_MSGS, result))
|
||||||
|
|
||||||
elif packet_type_value == PacketType.CONTACT_URI.value:
|
elif packet_type_value == PacketType.CONTACT_URI.value:
|
||||||
contact_uri = "meshcore://" + data[1:].hex()
|
contact_uri = "meshcore://" + data[1:].hex()
|
||||||
result = {"uri": contact_uri}
|
result = {"uri": contact_uri}
|
||||||
await self.dispatcher.dispatch(Event(EventType.CONTACT_URI, result))
|
await self.dispatcher.dispatch(Event(EventType.CONTACT_URI, result))
|
||||||
|
|
||||||
elif packet_type_value == PacketType.BATTERY.value:
|
elif packet_type_value == PacketType.BATTERY.value:
|
||||||
battery_level = int.from_bytes(data[1:3], byteorder='little')
|
battery_level = int.from_bytes(data[1:3], byteorder="little")
|
||||||
result = {"level": battery_level}
|
result = {"level": battery_level}
|
||||||
if len(data) > 3 : # has storage info as well
|
if len(data) > 3: # has storage info as well
|
||||||
result["used_kb"] = int.from_bytes(data[3:7], byteorder='little')
|
result["used_kb"] = int.from_bytes(data[3:7], byteorder="little")
|
||||||
result["total_kb"] = int.from_bytes(data[7:11], byteorder='little')
|
result["total_kb"] = int.from_bytes(data[7:11], byteorder="little")
|
||||||
await self.dispatcher.dispatch(Event(EventType.BATTERY, result))
|
await self.dispatcher.dispatch(Event(EventType.BATTERY, result))
|
||||||
|
|
||||||
elif packet_type_value == PacketType.DEVICE_INFO.value:
|
elif packet_type_value == PacketType.DEVICE_INFO.value:
|
||||||
res = {}
|
res = {}
|
||||||
res["fw ver"] = data[1]
|
res["fw ver"] = data[1]
|
||||||
if data[1] >= 3:
|
if data[1] >= 3:
|
||||||
res["max_contacts"] = data[2] * 2
|
res["max_contacts"] = data[2] * 2
|
||||||
res["max_channels"] = data[3]
|
res["max_channels"] = data[3]
|
||||||
res["ble_pin"] = int.from_bytes(data[4:8], byteorder='little')
|
res["ble_pin"] = int.from_bytes(data[4:8], byteorder="little")
|
||||||
res["fw_build"] = data[8:20].decode('utf-8', 'ignore').replace("\0","")
|
res["fw_build"] = data[8:20].decode("utf-8", "ignore").replace("\0", "")
|
||||||
res["model"] = data[20:60].decode('utf-8', 'ignore').replace("\0","")
|
res["model"] = data[20:60].decode("utf-8", "ignore").replace("\0", "")
|
||||||
res["ver"] = data[60:80].decode('utf-8', 'ignore').replace("\0","")
|
res["ver"] = data[60:80].decode("utf-8", "ignore").replace("\0", "")
|
||||||
await self.dispatcher.dispatch(Event(EventType.DEVICE_INFO, res))
|
await self.dispatcher.dispatch(Event(EventType.DEVICE_INFO, res))
|
||||||
|
|
||||||
elif packet_type_value == PacketType.CUSTOM_VARS.value:
|
elif packet_type_value == PacketType.CUSTOM_VARS.value:
|
||||||
logger.debug(f"received custom vars response: {data.hex()}")
|
logger.debug(f"received custom vars response: {data.hex()}")
|
||||||
res = {}
|
res = {}
|
||||||
rawdata = data[1:].decode('utf-8', 'ignore')
|
rawdata = data[1:].decode("utf-8", "ignore")
|
||||||
if not rawdata == "" :
|
if not rawdata == "":
|
||||||
pairs = rawdata.split(",")
|
pairs = rawdata.split(",")
|
||||||
for p in pairs :
|
for p in pairs:
|
||||||
psplit = p.split(":")
|
psplit = p.split(":")
|
||||||
res[psplit[0]] = psplit[1]
|
res[psplit[0]] = psplit[1]
|
||||||
logger.debug(f"got custom vars : {res}")
|
logger.debug(f"got custom vars : {res}")
|
||||||
await self.dispatcher.dispatch(Event(EventType.CUSTOM_VARS, res))
|
await self.dispatcher.dispatch(Event(EventType.CUSTOM_VARS, res))
|
||||||
|
|
||||||
elif packet_type_value == PacketType.CHANNEL_INFO.value:
|
elif packet_type_value == PacketType.CHANNEL_INFO.value:
|
||||||
logger.debug(f"received channel info response: {data.hex()}")
|
logger.debug(f"received channel info response: {data.hex()}")
|
||||||
res = {}
|
res = {}
|
||||||
res["channel_idx"] = data[1]
|
res["channel_idx"] = data[1]
|
||||||
|
|
||||||
# Channel name is null-terminated, so find the first null byte
|
# Channel name is null-terminated, so find the first null byte
|
||||||
name_bytes = data[2:34]
|
name_bytes = data[2:34]
|
||||||
null_pos = name_bytes.find(0)
|
null_pos = name_bytes.find(0)
|
||||||
if null_pos >= 0:
|
if null_pos >= 0:
|
||||||
res["channel_name"] = name_bytes[:null_pos].decode('utf-8', 'ignore')
|
res["channel_name"] = name_bytes[:null_pos].decode("utf-8", "ignore")
|
||||||
else:
|
else:
|
||||||
res["channel_name"] = name_bytes.decode('utf-8', 'ignore')
|
res["channel_name"] = name_bytes.decode("utf-8", "ignore")
|
||||||
|
|
||||||
res["channel_secret"] = data[34:50]
|
res["channel_secret"] = data[34:50]
|
||||||
await self.dispatcher.dispatch(Event(EventType.CHANNEL_INFO, res, res))
|
await self.dispatcher.dispatch(Event(EventType.CHANNEL_INFO, res, res))
|
||||||
|
|
||||||
# Push notifications
|
# Push notifications
|
||||||
elif packet_type_value == PacketType.ADVERTISEMENT.value:
|
elif packet_type_value == PacketType.ADVERTISEMENT.value:
|
||||||
@@ -251,30 +271,28 @@ class MessageReader:
|
|||||||
res = {}
|
res = {}
|
||||||
res["public_key"] = data[1:33].hex()
|
res["public_key"] = data[1:33].hex()
|
||||||
await self.dispatcher.dispatch(Event(EventType.ADVERTISEMENT, res, res))
|
await self.dispatcher.dispatch(Event(EventType.ADVERTISEMENT, res, res))
|
||||||
|
|
||||||
elif packet_type_value == PacketType.PATH_UPDATE.value:
|
elif packet_type_value == PacketType.PATH_UPDATE.value:
|
||||||
logger.debug("Code path update")
|
logger.debug("Code path update")
|
||||||
res = {}
|
res = {}
|
||||||
res["public_key"] = data[1:33].hex()
|
res["public_key"] = data[1:33].hex()
|
||||||
await self.dispatcher.dispatch(Event(EventType.PATH_UPDATE, res, res))
|
await self.dispatcher.dispatch(Event(EventType.PATH_UPDATE, res, res))
|
||||||
|
|
||||||
elif packet_type_value == PacketType.ACK.value:
|
elif packet_type_value == PacketType.ACK.value:
|
||||||
logger.debug("Received ACK")
|
logger.debug("Received ACK")
|
||||||
ack_data = {}
|
ack_data = {}
|
||||||
|
|
||||||
if len(data) >= 5:
|
if len(data) >= 5:
|
||||||
ack_data["code"] = bytes(data[1:5]).hex()
|
ack_data["code"] = bytes(data[1:5]).hex()
|
||||||
|
|
||||||
attributes = {
|
attributes = {"code": ack_data.get("code", "")}
|
||||||
"code": ack_data.get("code", "")
|
|
||||||
}
|
|
||||||
|
|
||||||
await self.dispatcher.dispatch(Event(EventType.ACK, ack_data, attributes))
|
await self.dispatcher.dispatch(Event(EventType.ACK, ack_data, attributes))
|
||||||
|
|
||||||
elif packet_type_value == PacketType.MESSAGES_WAITING.value:
|
elif packet_type_value == PacketType.MESSAGES_WAITING.value:
|
||||||
logger.debug("Msgs are waiting")
|
logger.debug("Msgs are waiting")
|
||||||
await self.dispatcher.dispatch(Event(EventType.MESSAGES_WAITING, {}))
|
await self.dispatcher.dispatch(Event(EventType.MESSAGES_WAITING, {}))
|
||||||
|
|
||||||
elif packet_type_value == PacketType.RAW_DATA.value:
|
elif packet_type_value == PacketType.RAW_DATA.value:
|
||||||
res = {}
|
res = {}
|
||||||
res["SNR"] = data[1] / 4
|
res["SNR"] = data[1] / 4
|
||||||
@@ -283,145 +301,161 @@ class MessageReader:
|
|||||||
logger.debug("Received raw data")
|
logger.debug("Received raw data")
|
||||||
print(res)
|
print(res)
|
||||||
await self.dispatcher.dispatch(Event(EventType.RAW_DATA, res))
|
await self.dispatcher.dispatch(Event(EventType.RAW_DATA, res))
|
||||||
|
|
||||||
elif packet_type_value == PacketType.LOGIN_SUCCESS.value:
|
elif packet_type_value == PacketType.LOGIN_SUCCESS.value:
|
||||||
res = {}
|
res = {}
|
||||||
if len(data) > 1:
|
if len(data) > 1:
|
||||||
res["permissions"] = data[1]
|
res["permissions"] = data[1]
|
||||||
res["is_admin"] = (data[1] & 1) == 1 # Check if admin bit is set
|
res["is_admin"] = (data[1] & 1) == 1 # Check if admin bit is set
|
||||||
|
|
||||||
if len(data) > 7:
|
if len(data) > 7:
|
||||||
res["pubkey_prefix"] = data[2:8].hex()
|
res["pubkey_prefix"] = data[2:8].hex()
|
||||||
|
|
||||||
attributes = {
|
attributes = {"pubkey_prefix": res.get("pubkey_prefix")}
|
||||||
"pubkey_prefix": res.get("pubkey_prefix")
|
|
||||||
}
|
await self.dispatcher.dispatch(
|
||||||
|
Event(EventType.LOGIN_SUCCESS, res, attributes)
|
||||||
await self.dispatcher.dispatch(Event(EventType.LOGIN_SUCCESS, res, attributes))
|
)
|
||||||
|
|
||||||
elif packet_type_value == PacketType.LOGIN_FAILED.value:
|
elif packet_type_value == PacketType.LOGIN_FAILED.value:
|
||||||
res = {}
|
res = {}
|
||||||
|
|
||||||
if len(data) > 7:
|
if len(data) > 7:
|
||||||
res["pubkey_prefix"] = data[2:8].hex()
|
res["pubkey_prefix"] = data[2:8].hex()
|
||||||
|
|
||||||
attributes = {
|
attributes = {"pubkey_prefix": res.get("pubkey_prefix")}
|
||||||
"pubkey_prefix": res.get("pubkey_prefix")
|
|
||||||
}
|
await self.dispatcher.dispatch(
|
||||||
|
Event(EventType.LOGIN_FAILED, res, attributes)
|
||||||
await self.dispatcher.dispatch(Event(EventType.LOGIN_FAILED, res, attributes))
|
)
|
||||||
|
|
||||||
elif packet_type_value == PacketType.STATUS_RESPONSE.value:
|
elif packet_type_value == PacketType.STATUS_RESPONSE.value:
|
||||||
res = {}
|
res = {}
|
||||||
res["pubkey_pre"] = data[2:8].hex()
|
res["pubkey_pre"] = data[2:8].hex()
|
||||||
res["bat"] = int.from_bytes(data[8:10], byteorder='little')
|
res["bat"] = int.from_bytes(data[8:10], byteorder="little")
|
||||||
res["tx_queue_len"] = int.from_bytes(data[10:12], byteorder='little')
|
res["tx_queue_len"] = int.from_bytes(data[10:12], byteorder="little")
|
||||||
res["noise_floor"] = int.from_bytes(data[12:14], byteorder='little', signed=True)
|
res["noise_floor"] = int.from_bytes(
|
||||||
res["last_rssi"] = int.from_bytes(data[14:16], byteorder='little', signed=True)
|
data[12:14], byteorder="little", signed=True
|
||||||
res["nb_recv"] = int.from_bytes(data[16:20], byteorder='little', signed=False)
|
)
|
||||||
res["nb_sent"] = int.from_bytes(data[20:24], byteorder='little', signed=False)
|
res["last_rssi"] = int.from_bytes(
|
||||||
res["airtime"] = int.from_bytes(data[24:28], byteorder='little')
|
data[14:16], byteorder="little", signed=True
|
||||||
res["uptime"] = int.from_bytes(data[28:32], byteorder='little')
|
)
|
||||||
res["sent_flood"] = int.from_bytes(data[32:36], byteorder='little')
|
res["nb_recv"] = int.from_bytes(
|
||||||
res["sent_direct"] = int.from_bytes(data[36:40], byteorder='little')
|
data[16:20], byteorder="little", signed=False
|
||||||
res["recv_flood"] = int.from_bytes(data[40:44], byteorder='little')
|
)
|
||||||
res["recv_direct"] = int.from_bytes(data[44:48], byteorder='little')
|
res["nb_sent"] = int.from_bytes(
|
||||||
res["full_evts"] = int.from_bytes(data[48:50], byteorder='little')
|
data[20:24], byteorder="little", signed=False
|
||||||
res["last_snr"] = int.from_bytes(data[50:52], byteorder='little', signed=True) / 4
|
)
|
||||||
res["direct_dups"] = int.from_bytes(data[52:54], byteorder='little')
|
res["airtime"] = int.from_bytes(data[24:28], byteorder="little")
|
||||||
res["flood_dups"] = int.from_bytes(data[54:56], byteorder='little')
|
res["uptime"] = int.from_bytes(data[28:32], byteorder="little")
|
||||||
res["rx_airtime"] = int.from_bytes(data[56:60], byteorder='little')
|
res["sent_flood"] = int.from_bytes(data[32:36], byteorder="little")
|
||||||
|
res["sent_direct"] = int.from_bytes(data[36:40], byteorder="little")
|
||||||
|
res["recv_flood"] = int.from_bytes(data[40:44], byteorder="little")
|
||||||
|
res["recv_direct"] = int.from_bytes(data[44:48], byteorder="little")
|
||||||
|
res["full_evts"] = int.from_bytes(data[48:50], byteorder="little")
|
||||||
|
res["last_snr"] = (
|
||||||
|
int.from_bytes(data[50:52], byteorder="little", signed=True) / 4
|
||||||
|
)
|
||||||
|
res["direct_dups"] = int.from_bytes(data[52:54], byteorder="little")
|
||||||
|
res["flood_dups"] = int.from_bytes(data[54:56], byteorder="little")
|
||||||
|
res["rx_airtime"] = int.from_bytes(data[56:60], byteorder="little")
|
||||||
|
|
||||||
data_hex = data[8:].hex()
|
data_hex = data[8:].hex()
|
||||||
logger.debug(f"Status response: {data_hex}")
|
logger.debug(f"Status response: {data_hex}")
|
||||||
|
|
||||||
attributes = {
|
attributes = {
|
||||||
"pubkey_prefix": res["pubkey_pre"],
|
"pubkey_prefix": res["pubkey_pre"],
|
||||||
}
|
}
|
||||||
await self.dispatcher.dispatch(Event(EventType.STATUS_RESPONSE, res, attributes))
|
await self.dispatcher.dispatch(
|
||||||
|
Event(EventType.STATUS_RESPONSE, res, attributes)
|
||||||
|
)
|
||||||
|
|
||||||
elif packet_type_value == PacketType.LOG_DATA.value:
|
elif packet_type_value == PacketType.LOG_DATA.value:
|
||||||
logger.debug(f"Received RF log data: {data.hex()}")
|
logger.debug(f"Received RF log data: {data.hex()}")
|
||||||
|
|
||||||
# Parse as raw RX data
|
# Parse as raw RX data
|
||||||
log_data: Dict[str, Any] = {
|
log_data: Dict[str, Any] = {"raw_hex": data[1:].hex()}
|
||||||
"raw_hex": data[1:].hex()
|
|
||||||
}
|
|
||||||
|
|
||||||
# First byte is SNR (signed byte, multiplied by 4)
|
# First byte is SNR (signed byte, multiplied by 4)
|
||||||
if len(data) > 1:
|
if len(data) > 1:
|
||||||
snr_byte = data[1]
|
snr_byte = data[1]
|
||||||
# Convert to signed value
|
# Convert to signed value
|
||||||
snr = (snr_byte if snr_byte < 128 else snr_byte - 256) / 4.0
|
snr = (snr_byte if snr_byte < 128 else snr_byte - 256) / 4.0
|
||||||
log_data["snr"] = snr
|
log_data["snr"] = snr
|
||||||
|
|
||||||
# Second byte is RSSI (signed byte)
|
# Second byte is RSSI (signed byte)
|
||||||
if len(data) > 2:
|
if len(data) > 2:
|
||||||
rssi_byte = data[2]
|
rssi_byte = data[2]
|
||||||
# Convert to signed value
|
# Convert to signed value
|
||||||
rssi = rssi_byte if rssi_byte < 128 else rssi_byte - 256
|
rssi = rssi_byte if rssi_byte < 128 else rssi_byte - 256
|
||||||
log_data["rssi"] = rssi
|
log_data["rssi"] = rssi
|
||||||
|
|
||||||
# Remaining bytes are the raw data payload
|
# Remaining bytes are the raw data payload
|
||||||
if len(data) > 3:
|
if len(data) > 3:
|
||||||
log_data["payload"] = data[3:].hex()
|
log_data["payload"] = data[3:].hex()
|
||||||
log_data["payload_length"] = len(data) - 3
|
log_data["payload_length"] = len(data) - 3
|
||||||
|
|
||||||
attributes = {
|
attributes = {
|
||||||
"pubkey_prefix": log_data["raw_hex"],
|
"pubkey_prefix": log_data["raw_hex"],
|
||||||
}
|
}
|
||||||
|
|
||||||
# Dispatch as RF log data
|
# Dispatch as RF log data
|
||||||
await self.dispatcher.dispatch(Event(EventType.RX_LOG_DATA, log_data, attributes))
|
await self.dispatcher.dispatch(
|
||||||
|
Event(EventType.RX_LOG_DATA, log_data, attributes)
|
||||||
|
)
|
||||||
|
|
||||||
elif packet_type_value == PacketType.TRACE_DATA.value:
|
elif packet_type_value == PacketType.TRACE_DATA.value:
|
||||||
logger.debug(f"Received trace data: {data.hex()}")
|
logger.debug(f"Received trace data: {data.hex()}")
|
||||||
res = {}
|
res = {}
|
||||||
|
|
||||||
# According to the source, format is:
|
# According to the source, format is:
|
||||||
# 0x89, reserved(0), path_len, flags, tag(4), auth(4), path_hashes[], path_snrs[], final_snr
|
# 0x89, reserved(0), path_len, flags, tag(4), auth(4), path_hashes[], path_snrs[], final_snr
|
||||||
|
|
||||||
reserved = data[1]
|
|
||||||
path_len = data[2]
|
path_len = data[2]
|
||||||
flags = data[3]
|
flags = data[3]
|
||||||
tag = int.from_bytes(data[4:8], byteorder='little')
|
tag = int.from_bytes(data[4:8], byteorder="little")
|
||||||
auth_code = int.from_bytes(data[8:12], byteorder='little')
|
auth_code = int.from_bytes(data[8:12], byteorder="little")
|
||||||
|
|
||||||
# Initialize result
|
# Initialize result
|
||||||
res["tag"] = tag
|
res["tag"] = tag
|
||||||
res["auth"] = auth_code
|
res["auth"] = auth_code
|
||||||
res["flags"] = flags
|
res["flags"] = flags
|
||||||
res["path_len"] = path_len
|
res["path_len"] = path_len
|
||||||
|
|
||||||
# Process path as array of objects with hash and SNR
|
# Process path as array of objects with hash and SNR
|
||||||
path_nodes = []
|
path_nodes = []
|
||||||
|
|
||||||
if path_len > 0 and len(data) >= 12 + path_len*2 + 1:
|
if path_len > 0 and len(data) >= 12 + path_len * 2 + 1:
|
||||||
# Extract path with hash and SNR pairs
|
# Extract path with hash and SNR pairs
|
||||||
for i in range(path_len):
|
for i in range(path_len):
|
||||||
node = {
|
node = {
|
||||||
"hash": f"{data[12+i]:02x}",
|
"hash": f"{data[12+i]:02x}",
|
||||||
# SNR is stored as a signed byte representing SNR * 4
|
# SNR is stored as a signed byte representing SNR * 4
|
||||||
"snr": (data[12+path_len+i] if data[12+path_len+i] < 128 else data[12+path_len+i] - 256) / 4.0
|
"snr": (
|
||||||
|
data[12 + path_len + i]
|
||||||
|
if data[12 + path_len + i] < 128
|
||||||
|
else data[12 + path_len + i] - 256
|
||||||
|
)
|
||||||
|
/ 4.0,
|
||||||
}
|
}
|
||||||
path_nodes.append(node)
|
path_nodes.append(node)
|
||||||
|
|
||||||
# Add the final node (our device) with its SNR
|
# Add the final node (our device) with its SNR
|
||||||
final_snr_byte = data[12+path_len*2]
|
final_snr_byte = data[12 + path_len * 2]
|
||||||
final_snr = (final_snr_byte if final_snr_byte < 128 else final_snr_byte - 256) / 4.0
|
final_snr = (
|
||||||
path_nodes.append({
|
final_snr_byte if final_snr_byte < 128 else final_snr_byte - 256
|
||||||
"snr": final_snr
|
) / 4.0
|
||||||
})
|
path_nodes.append({"snr": final_snr})
|
||||||
|
|
||||||
res["path"] = path_nodes
|
res["path"] = path_nodes
|
||||||
|
|
||||||
logger.debug(f"Parsed trace data: {res}")
|
logger.debug(f"Parsed trace data: {res}")
|
||||||
|
|
||||||
attributes = {
|
attributes = {
|
||||||
"tag": res["tag"],
|
"tag": res["tag"],
|
||||||
"auth_code": res["auth"],
|
"auth_code": res["auth"],
|
||||||
}
|
}
|
||||||
|
|
||||||
await self.dispatcher.dispatch(Event(EventType.TRACE_DATA, res, attributes))
|
await self.dispatcher.dispatch(Event(EventType.TRACE_DATA, res, attributes))
|
||||||
|
|
||||||
elif packet_type_value == PacketType.TELEMETRY_RESPONSE.value:
|
elif packet_type_value == PacketType.TELEMETRY_RESPONSE.value:
|
||||||
@@ -439,15 +473,19 @@ class MessageReader:
|
|||||||
lpp_data_list.append(lppdata)
|
lpp_data_list.append(lppdata)
|
||||||
i = i + len(lppdata)
|
i = i + len(lppdata)
|
||||||
|
|
||||||
lpp = json.loads(json.dumps(LppFrame(lpp_data_list), default=lpp_json_encoder))
|
lpp = json.loads(
|
||||||
|
json.dumps(LppFrame(lpp_data_list), default=lpp_json_encoder)
|
||||||
|
)
|
||||||
|
|
||||||
res["lpp"] = lpp
|
res["lpp"] = lpp
|
||||||
|
|
||||||
attributes = {
|
attributes = {
|
||||||
"raw" : buf.hex(),
|
"raw": buf.hex(),
|
||||||
}
|
}
|
||||||
|
|
||||||
await self.dispatcher.dispatch(Event(EventType.TELEMETRY_RESPONSE, res, attributes))
|
await self.dispatcher.dispatch(
|
||||||
|
Event(EventType.TELEMETRY_RESPONSE, res, attributes)
|
||||||
|
)
|
||||||
|
|
||||||
elif packet_type_value == PacketType.BINARY_RESPONSE.value:
|
elif packet_type_value == PacketType.BINARY_RESPONSE.value:
|
||||||
logger.debug(f"Received binary data: {data.hex()}")
|
logger.debug(f"Received binary data: {data.hex()}")
|
||||||
@@ -456,11 +494,11 @@ class MessageReader:
|
|||||||
res["tag"] = data[2:6].hex()
|
res["tag"] = data[2:6].hex()
|
||||||
res["data"] = data[6:].hex()
|
res["data"] = data[6:].hex()
|
||||||
|
|
||||||
attributes = {
|
attributes = {"tag": res["tag"]}
|
||||||
"tag" : res["tag"]
|
|
||||||
}
|
|
||||||
|
|
||||||
await self.dispatcher.dispatch(Event(EventType.BINARY_RESPONSE, res, attributes))
|
await self.dispatcher.dispatch(
|
||||||
|
Event(EventType.BINARY_RESPONSE, res, attributes)
|
||||||
|
)
|
||||||
|
|
||||||
elif packet_type_value == PacketType.PATH_DISCOVERY_RESPONSE.value:
|
elif packet_type_value == PacketType.PATH_DISCOVERY_RESPONSE.value:
|
||||||
logger.debug(f"Received path discovery response: {data.hex()}")
|
logger.debug(f"Received path discovery response: {data.hex()}")
|
||||||
@@ -468,18 +506,17 @@ class MessageReader:
|
|||||||
res["pubkey_pre"] = data[2:8].hex()
|
res["pubkey_pre"] = data[2:8].hex()
|
||||||
opl = data[8]
|
opl = data[8]
|
||||||
res["out_path_len"] = opl
|
res["out_path_len"] = opl
|
||||||
res["out_path"] = data[9:9+opl].hex()
|
res["out_path"] = data[9 : 9 + opl].hex()
|
||||||
ipl = data[9+opl]
|
ipl = data[9 + opl]
|
||||||
res["in_path_len"] = ipl
|
res["in_path_len"] = ipl
|
||||||
res["in_path"] = data[10+opl:10+opl+ipl].hex()
|
res["in_path"] = data[10 + opl : 10 + opl + ipl].hex()
|
||||||
|
|
||||||
attributes = {
|
attributes = {"pubkey_pre": res["pubkey_pre"]}
|
||||||
"pubkey_pre" : res["pubkey_pre"]
|
|
||||||
}
|
|
||||||
|
|
||||||
await self.dispatcher.dispatch(Event(EventType.PATH_RESPONSE, res, attributes))
|
await self.dispatcher.dispatch(
|
||||||
|
Event(EventType.PATH_RESPONSE, res, attributes)
|
||||||
|
)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
logger.debug(f"Unhandled data received {data}")
|
logger.debug(f"Unhandled data received {data}")
|
||||||
logger.debug(f"Unhandled packet type: {packet_type_value}")
|
logger.debug(f"Unhandled packet type: {packet_type_value}")
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
"""
|
|
||||||
mccli.py : CLI interface to MeschCore BLE companion app
|
|
||||||
"""
|
"""
|
||||||
|
mccli.py : CLI interface to MeschCore BLE companion app
|
||||||
|
"""
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
import serial_asyncio
|
import serial_asyncio
|
||||||
@@ -8,6 +9,7 @@ import serial_asyncio
|
|||||||
# Get logger
|
# Get logger
|
||||||
logger = logging.getLogger("meshcore")
|
logger = logging.getLogger("meshcore")
|
||||||
|
|
||||||
|
|
||||||
class SerialConnection:
|
class SerialConnection:
|
||||||
def __init__(self, port, baudrate, cx_dly=0.2):
|
def __init__(self, port, baudrate, cx_dly=0.2):
|
||||||
self.port = port
|
self.port = port
|
||||||
@@ -32,21 +34,22 @@ class SerialConnection:
|
|||||||
if isinstance(transport, serial_asyncio.SerialTransport) and transport.serial:
|
if isinstance(transport, serial_asyncio.SerialTransport) and transport.serial:
|
||||||
transport.serial.rts = False # You can manipulate Serial object via transport
|
transport.serial.rts = False # You can manipulate Serial object via transport
|
||||||
self.cx._connected_event.set()
|
self.cx._connected_event.set()
|
||||||
|
|
||||||
def data_received(self, data):
|
def data_received(self, data):
|
||||||
self.cx.handle_rx(data)
|
self.cx.handle_rx(data)
|
||||||
|
|
||||||
def connection_lost(self, exc):
|
def connection_lost(self, exc):
|
||||||
logger.debug('Serial port closed')
|
logger.debug('Serial port closed')
|
||||||
self.cx._connected_event.clear()
|
self.cx._connected_event.clear()
|
||||||
|
|
||||||
if self.cx._disconnect_callback:
|
if self.cx._disconnect_callback:
|
||||||
asyncio.create_task(self.cx._disconnect_callback("serial_disconnect"))
|
asyncio.create_task(self.cx._disconnect_callback("serial_disconnect"))
|
||||||
|
|
||||||
def pause_writing(self):
|
def pause_writing(self):
|
||||||
logger.debug('pause writing')
|
logger.debug("pause writing")
|
||||||
|
|
||||||
def resume_writing(self):
|
def resume_writing(self):
|
||||||
logger.debug('resume writing')
|
logger.debug("resume writing")
|
||||||
|
|
||||||
async def connect(self):
|
async def connect(self):
|
||||||
"""
|
"""
|
||||||
@@ -56,39 +59,42 @@ class SerialConnection:
|
|||||||
|
|
||||||
loop = asyncio.get_running_loop()
|
loop = asyncio.get_running_loop()
|
||||||
await serial_asyncio.create_serial_connection(
|
await serial_asyncio.create_serial_connection(
|
||||||
loop, lambda: self.MCSerialClientProtocol(self),
|
loop,
|
||||||
self.port, baudrate=self.baudrate)
|
lambda: self.MCSerialClientProtocol(self),
|
||||||
|
self.port,
|
||||||
|
baudrate=self.baudrate,
|
||||||
|
)
|
||||||
|
|
||||||
await self._connected_event.wait()
|
await self._connected_event.wait()
|
||||||
logger.info("Serial Connection started")
|
logger.info("Serial Connection started")
|
||||||
return self.port
|
return self.port
|
||||||
|
|
||||||
def set_reader(self, reader) :
|
def set_reader(self, reader):
|
||||||
self.reader = reader
|
self.reader = reader
|
||||||
|
|
||||||
def handle_rx(self, data: bytearray):
|
def handle_rx(self, data: bytearray):
|
||||||
headerlen = len(self.header)
|
headerlen = len(self.header)
|
||||||
framelen = len(self.inframe)
|
framelen = len(self.inframe)
|
||||||
if not self.frame_started : # wait start of frame
|
if not self.frame_started: # wait start of frame
|
||||||
if len(data) >= 3 - headerlen:
|
if len(data) >= 3 - headerlen:
|
||||||
self.header = self.header + data[:3-headerlen]
|
self.header = self.header + data[: 3 - headerlen]
|
||||||
self.frame_started = True
|
self.frame_started = True
|
||||||
self.frame_size = int.from_bytes(self.header[1:], byteorder='little')
|
self.frame_size = int.from_bytes(self.header[1:], byteorder="little")
|
||||||
self.handle_rx(data[3-headerlen:])
|
self.handle_rx(data[3 - headerlen :])
|
||||||
else:
|
else:
|
||||||
self.header = self.header + data
|
self.header = self.header + data
|
||||||
else:
|
else:
|
||||||
if framelen + len(data) < self.frame_size:
|
if framelen + len(data) < self.frame_size:
|
||||||
self.inframe = self.inframe + data
|
self.inframe = self.inframe + data
|
||||||
else:
|
else:
|
||||||
self.inframe = self.inframe + data[:self.frame_size-framelen]
|
self.inframe = self.inframe + data[: self.frame_size - framelen]
|
||||||
if not self.reader is None:
|
if self.reader is not None:
|
||||||
asyncio.create_task(self.reader.handle_rx(self.inframe))
|
asyncio.create_task(self.reader.handle_rx(self.inframe))
|
||||||
self.frame_started = False
|
self.frame_started = False
|
||||||
self.header = b""
|
self.header = b""
|
||||||
self.inframe = b""
|
self.inframe = b""
|
||||||
if framelen + len(data) > self.frame_size:
|
if framelen + len(data) > self.frame_size:
|
||||||
self.handle_rx(data[self.frame_size-framelen:])
|
self.handle_rx(data[self.frame_size - framelen :])
|
||||||
|
|
||||||
async def send(self, data):
|
async def send(self, data):
|
||||||
if not self.transport:
|
if not self.transport:
|
||||||
@@ -98,7 +104,7 @@ class SerialConnection:
|
|||||||
pkt = b"\x3c" + size.to_bytes(2, byteorder="little") + data
|
pkt = b"\x3c" + size.to_bytes(2, byteorder="little") + data
|
||||||
logger.debug(f"sending pkt : {pkt}")
|
logger.debug(f"sending pkt : {pkt}")
|
||||||
self.transport.write(pkt)
|
self.transport.write(pkt)
|
||||||
|
|
||||||
async def disconnect(self):
|
async def disconnect(self):
|
||||||
"""Close the serial connection."""
|
"""Close the serial connection."""
|
||||||
if self.transport:
|
if self.transport:
|
||||||
@@ -106,7 +112,7 @@ class SerialConnection:
|
|||||||
self.transport = None
|
self.transport = None
|
||||||
self._connected_event.clear()
|
self._connected_event.clear()
|
||||||
logger.debug("Serial Connection closed")
|
logger.debug("Serial Connection closed")
|
||||||
|
|
||||||
def set_disconnect_callback(self, callback):
|
def set_disconnect_callback(self, callback):
|
||||||
"""Set callback to handle disconnections."""
|
"""Set callback to handle disconnections."""
|
||||||
self._disconnect_callback = callback
|
self._disconnect_callback = callback
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
"""
|
|
||||||
mccli.py : CLI interface to MeschCore BLE companion app
|
|
||||||
"""
|
"""
|
||||||
|
mccli.py : CLI interface to MeschCore BLE companion app
|
||||||
|
"""
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
@@ -10,6 +11,7 @@ logger = logging.getLogger("meshcore")
|
|||||||
# TCP disconnect detection threshold
|
# TCP disconnect detection threshold
|
||||||
TCP_DISCONNECT_THRESHOLD = 5
|
TCP_DISCONNECT_THRESHOLD = 5
|
||||||
|
|
||||||
|
|
||||||
class TCPConnection:
|
class TCPConnection:
|
||||||
def __init__(self, host, port):
|
def __init__(self, host, port):
|
||||||
self.host = host
|
self.host = host
|
||||||
@@ -32,18 +34,18 @@ class TCPConnection:
|
|||||||
# Reset counters on new connection
|
# Reset counters on new connection
|
||||||
self.cx._send_count = 0
|
self.cx._send_count = 0
|
||||||
self.cx._receive_count = 0
|
self.cx._receive_count = 0
|
||||||
logger.debug('connection established')
|
logger.debug("connection established")
|
||||||
|
|
||||||
def data_received(self, data):
|
def data_received(self, data):
|
||||||
logger.debug('data received')
|
logger.debug("data received")
|
||||||
self.cx._receive_count += 1
|
self.cx._receive_count += 1
|
||||||
self.cx.handle_rx(data)
|
self.cx.handle_rx(data)
|
||||||
|
|
||||||
def error_received(self, exc):
|
def error_received(self, exc):
|
||||||
logger.error(f'Error received: {exc}')
|
logger.error(f"Error received: {exc}")
|
||||||
|
|
||||||
def connection_lost(self, exc):
|
def connection_lost(self, exc):
|
||||||
logger.debug('TCP server closed the connection')
|
logger.debug("TCP server closed the connection")
|
||||||
if self.cx._disconnect_callback:
|
if self.cx._disconnect_callback:
|
||||||
asyncio.create_task(self.cx._disconnect_callback("tcp_disconnect"))
|
asyncio.create_task(self.cx._disconnect_callback("tcp_disconnect"))
|
||||||
|
|
||||||
@@ -53,41 +55,41 @@ class TCPConnection:
|
|||||||
"""
|
"""
|
||||||
loop = asyncio.get_running_loop()
|
loop = asyncio.get_running_loop()
|
||||||
await loop.create_connection(
|
await loop.create_connection(
|
||||||
lambda: self.MCClientProtocol(self),
|
lambda: self.MCClientProtocol(self), self.host, self.port
|
||||||
self.host, self.port)
|
)
|
||||||
|
|
||||||
logger.info("TCP Connection started")
|
logger.info("TCP Connection started")
|
||||||
future = asyncio.Future()
|
future = asyncio.Future()
|
||||||
future.set_result(self.host)
|
future.set_result(self.host)
|
||||||
|
|
||||||
return future
|
return future
|
||||||
|
|
||||||
def set_reader(self, reader) :
|
def set_reader(self, reader):
|
||||||
self.reader = reader
|
self.reader = reader
|
||||||
|
|
||||||
def handle_rx(self, data: bytearray):
|
def handle_rx(self, data: bytearray):
|
||||||
headerlen = len(self.header)
|
headerlen = len(self.header)
|
||||||
framelen = len(self.inframe)
|
framelen = len(self.inframe)
|
||||||
if not self.frame_started : # wait start of frame
|
if not self.frame_started: # wait start of frame
|
||||||
if len(data) >= 3 - headerlen:
|
if len(data) >= 3 - headerlen:
|
||||||
self.header = self.header + data[:3-headerlen]
|
self.header = self.header + data[: 3 - headerlen]
|
||||||
self.frame_started = True
|
self.frame_started = True
|
||||||
self.frame_size = int.from_bytes(self.header[1:], byteorder='little')
|
self.frame_size = int.from_bytes(self.header[1:], byteorder="little")
|
||||||
self.handle_rx(data[3-headerlen:])
|
self.handle_rx(data[3 - headerlen :])
|
||||||
else:
|
else:
|
||||||
self.header = self.header + data
|
self.header = self.header + data
|
||||||
else:
|
else:
|
||||||
if framelen + len(data) < self.frame_size:
|
if framelen + len(data) < self.frame_size:
|
||||||
self.inframe = self.inframe + data
|
self.inframe = self.inframe + data
|
||||||
else:
|
else:
|
||||||
self.inframe = self.inframe + data[:self.frame_size-framelen]
|
self.inframe = self.inframe + data[: self.frame_size - framelen]
|
||||||
if not self.reader is None:
|
if self.reader is not None:
|
||||||
asyncio.create_task(self.reader.handle_rx(self.inframe))
|
asyncio.create_task(self.reader.handle_rx(self.inframe))
|
||||||
self.frame_started = False
|
self.frame_started = False
|
||||||
self.header = b""
|
self.header = b""
|
||||||
self.inframe = b""
|
self.inframe = b""
|
||||||
if framelen + len(data) > self.frame_size:
|
if framelen + len(data) > self.frame_size:
|
||||||
self.handle_rx(data[self.frame_size-framelen:])
|
self.handle_rx(data[self.frame_size - framelen :])
|
||||||
|
|
||||||
async def send(self, data):
|
async def send(self, data):
|
||||||
if not self.transport:
|
if not self.transport:
|
||||||
@@ -95,28 +97,30 @@ class TCPConnection:
|
|||||||
if self._disconnect_callback:
|
if self._disconnect_callback:
|
||||||
await self._disconnect_callback("tcp_transport_lost")
|
await self._disconnect_callback("tcp_transport_lost")
|
||||||
return
|
return
|
||||||
|
|
||||||
self._send_count += 1
|
self._send_count += 1
|
||||||
|
|
||||||
# Check if we've sent packets without any responses
|
# Check if we've sent packets without any responses
|
||||||
if self._send_count - self._receive_count >= TCP_DISCONNECT_THRESHOLD:
|
if self._send_count - self._receive_count >= TCP_DISCONNECT_THRESHOLD:
|
||||||
logger.debug(f"TCP disconnect detected: sent {self._send_count}, received {self._receive_count}")
|
logger.debug(
|
||||||
|
f"TCP disconnect detected: sent {self._send_count}, received {self._receive_count}"
|
||||||
|
)
|
||||||
if self._disconnect_callback:
|
if self._disconnect_callback:
|
||||||
await self._disconnect_callback("tcp_no_response")
|
await self._disconnect_callback("tcp_no_response")
|
||||||
return
|
return
|
||||||
|
|
||||||
size = len(data)
|
size = len(data)
|
||||||
pkt = b"\x3c" + size.to_bytes(2, byteorder="little") + data
|
pkt = b"\x3c" + size.to_bytes(2, byteorder="little") + data
|
||||||
logger.debug(f"sending pkt : {pkt}")
|
logger.debug(f"sending pkt : {pkt}")
|
||||||
self.transport.write(pkt)
|
self.transport.write(pkt)
|
||||||
|
|
||||||
async def disconnect(self):
|
async def disconnect(self):
|
||||||
"""Close the TCP connection."""
|
"""Close the TCP connection."""
|
||||||
if self.transport:
|
if self.transport:
|
||||||
self.transport.close()
|
self.transport.close()
|
||||||
self.transport = None
|
self.transport = None
|
||||||
logger.debug("TCP Connection closed")
|
logger.debug("TCP Connection closed")
|
||||||
|
|
||||||
def set_disconnect_callback(self, callback):
|
def set_disconnect_callback(self, callback):
|
||||||
"""Set callback to handle disconnections."""
|
"""Set callback to handle disconnections."""
|
||||||
self._disconnect_callback = callback
|
self._disconnect_callback = callback
|
||||||
|
|||||||
@@ -2,10 +2,15 @@ import asyncio
|
|||||||
import unittest
|
import unittest
|
||||||
from unittest.mock import AsyncMock, MagicMock, patch
|
from unittest.mock import AsyncMock, MagicMock, patch
|
||||||
|
|
||||||
from meshcore.ble_cx import BLEConnection, UART_SERVICE_UUID, UART_TX_CHAR_UUID, UART_RX_CHAR_UUID
|
from meshcore.ble_cx import (
|
||||||
|
BLEConnection,
|
||||||
|
UART_TX_CHAR_UUID,
|
||||||
|
UART_RX_CHAR_UUID,
|
||||||
|
)
|
||||||
|
|
||||||
class TestBLEConnection(unittest.TestCase):
|
class TestBLEConnection(unittest.TestCase):
|
||||||
@patch('meshcore.ble_cx.BleakClient')
|
@patch("meshcore.ble_cx.BleakClient")
|
||||||
|
|
||||||
def test_ble_connection_and_disconnection(self, mock_bleak_client):
|
def test_ble_connection_and_disconnection(self, mock_bleak_client):
|
||||||
"""
|
"""
|
||||||
Tests the BLEConnection class for connecting and disconnecting from a BLE device.
|
Tests the BLEConnection class for connecting and disconnecting from a BLE device.
|
||||||
@@ -13,7 +18,7 @@ class TestBLEConnection(unittest.TestCase):
|
|||||||
# Arrange
|
# Arrange
|
||||||
mock_client_instance = self._get_mock_bleak_client()
|
mock_client_instance = self._get_mock_bleak_client()
|
||||||
mock_bleak_client.return_value = mock_client_instance
|
mock_bleak_client.return_value = mock_client_instance
|
||||||
|
|
||||||
address = "00:11:22:33:44:55"
|
address = "00:11:22:33:44:55"
|
||||||
ble_conn = BLEConnection(address=address)
|
ble_conn = BLEConnection(address=address)
|
||||||
|
|
||||||
@@ -23,10 +28,13 @@ class TestBLEConnection(unittest.TestCase):
|
|||||||
|
|
||||||
# Assert
|
# Assert
|
||||||
mock_client_instance.connect.assert_called_once()
|
mock_client_instance.connect.assert_called_once()
|
||||||
mock_client_instance.start_notify.assert_called_once_with(UART_TX_CHAR_UUID, ble_conn.handle_rx)
|
mock_client_instance.start_notify.assert_called_once_with(
|
||||||
|
UART_TX_CHAR_UUID, ble_conn.handle_rx
|
||||||
|
)
|
||||||
mock_client_instance.disconnect.assert_called_once()
|
mock_client_instance.disconnect.assert_called_once()
|
||||||
|
|
||||||
@patch('meshcore.ble_cx.BleakClient')
|
@patch("meshcore.ble_cx.BleakClient")
|
||||||
|
|
||||||
def test_send_data(self, mock_bleak_client):
|
def test_send_data(self, mock_bleak_client):
|
||||||
"""
|
"""
|
||||||
Tests the send method of the BLEConnection class.
|
Tests the send method of the BLEConnection class.
|
||||||
@@ -34,7 +42,7 @@ class TestBLEConnection(unittest.TestCase):
|
|||||||
# Arrange
|
# Arrange
|
||||||
mock_client_instance = self._get_mock_bleak_client()
|
mock_client_instance = self._get_mock_bleak_client()
|
||||||
mock_bleak_client.return_value = mock_client_instance
|
mock_bleak_client.return_value = mock_client_instance
|
||||||
|
|
||||||
address = "00:11:22:33:44:55"
|
address = "00:11:22:33:44:55"
|
||||||
ble_conn = BLEConnection(address=address)
|
ble_conn = BLEConnection(address=address)
|
||||||
asyncio.run(ble_conn.connect())
|
asyncio.run(ble_conn.connect())
|
||||||
@@ -44,8 +52,9 @@ class TestBLEConnection(unittest.TestCase):
|
|||||||
asyncio.run(ble_conn.send(data_to_send))
|
asyncio.run(ble_conn.send(data_to_send))
|
||||||
|
|
||||||
# Assert
|
# Assert
|
||||||
assert(isinstance(ble_conn.rx_char, MagicMock))
|
ble_conn.rx_char.write_gatt_char.assert_called_once_with(
|
||||||
ble_conn.rx_char.write_gatt_char.assert_called_once_with(ble_conn.rx_char, data_to_send, response=True)
|
ble_conn.rx_char, data_to_send, response=False
|
||||||
|
)
|
||||||
|
|
||||||
def _get_mock_bleak_client(self):
|
def _get_mock_bleak_client(self):
|
||||||
"""
|
"""
|
||||||
@@ -57,16 +66,15 @@ class TestBLEConnection(unittest.TestCase):
|
|||||||
mock_client.start_notify = AsyncMock()
|
mock_client.start_notify = AsyncMock()
|
||||||
mock_client.write_gatt_char = AsyncMock()
|
mock_client.write_gatt_char = AsyncMock()
|
||||||
mock_client.is_connected = True
|
mock_client.is_connected = True
|
||||||
|
|
||||||
mock_service = MagicMock()
|
mock_service = MagicMock()
|
||||||
mock_char = MagicMock()
|
mock_char = MagicMock()
|
||||||
mock_char.uuid = UART_RX_CHAR_UUID
|
mock_char.uuid = UART_RX_CHAR_UUID
|
||||||
mock_char.write_gatt_char = mock_client.write_gatt_char
|
mock_char.write_gatt_char = mock_client.write_gatt_char
|
||||||
|
|
||||||
mock_service.get_characteristic.return_value = mock_char
|
mock_service.get_characteristic.return_value = mock_char
|
||||||
mock_client.services.get_service.return_value = mock_service
|
mock_client.services.get_service.return_value = mock_service
|
||||||
|
|
||||||
return mock_client
|
return mock_client
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
import pytest
|
import pytest
|
||||||
import asyncio
|
import asyncio
|
||||||
from unittest.mock import MagicMock, patch, AsyncMock
|
from unittest.mock import MagicMock, AsyncMock
|
||||||
from meshcore.commands import CommandHandler
|
from meshcore.commands import CommandHandler
|
||||||
from meshcore.events import EventType, Event
|
from meshcore.events import EventType, Event
|
||||||
|
|
||||||
pytestmark = pytest.mark.asyncio
|
pytestmark = pytest.mark.asyncio
|
||||||
|
|
||||||
|
|
||||||
# Fixtures
|
# Fixtures
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def mock_connection():
|
def mock_connection():
|
||||||
@@ -13,6 +14,7 @@ def mock_connection():
|
|||||||
connection.send = AsyncMock()
|
connection.send = AsyncMock()
|
||||||
return connection
|
return connection
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def mock_dispatcher():
|
def mock_dispatcher():
|
||||||
dispatcher = MagicMock()
|
dispatcher = MagicMock()
|
||||||
@@ -20,29 +22,36 @@ def mock_dispatcher():
|
|||||||
dispatcher.dispatch = AsyncMock()
|
dispatcher.dispatch = AsyncMock()
|
||||||
return dispatcher
|
return dispatcher
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def command_handler(mock_connection, mock_dispatcher):
|
def command_handler(mock_connection, mock_dispatcher):
|
||||||
handler = CommandHandler()
|
handler = CommandHandler()
|
||||||
|
|
||||||
async def sender(data):
|
async def sender(data):
|
||||||
await mock_connection.send(data)
|
await mock_connection.send(data)
|
||||||
|
|
||||||
handler._sender_func = sender
|
handler._sender_func = sender
|
||||||
|
|
||||||
handler.dispatcher = mock_dispatcher
|
handler.dispatcher = mock_dispatcher
|
||||||
return handler
|
return handler
|
||||||
|
|
||||||
|
|
||||||
# Test helper
|
# Test helper
|
||||||
def setup_event_response(mock_dispatcher, event_type, payload, attribute_filters=None):
|
def setup_event_response(mock_dispatcher, event_type, payload, attribute_filters=None):
|
||||||
async def wait_response(requested_type, filters=None, timeout=None):
|
async def wait_response(requested_type, filters=None, timeout=None):
|
||||||
if requested_type == event_type:
|
if requested_type == event_type:
|
||||||
if filters and attribute_filters:
|
if filters and attribute_filters:
|
||||||
if not all(attribute_filters.get(key) == value for key, value in filters.items()):
|
if not all(
|
||||||
|
attribute_filters.get(key) == value
|
||||||
|
for key, value in filters.items()
|
||||||
|
):
|
||||||
return None
|
return None
|
||||||
return Event(event_type, payload)
|
return Event(event_type, payload)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
mock_dispatcher.wait_for_event.side_effect = wait_response
|
mock_dispatcher.wait_for_event.side_effect = wait_response
|
||||||
|
|
||||||
|
|
||||||
# Basic tests
|
# Basic tests
|
||||||
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")
|
||||||
@@ -50,141 +59,163 @@ async def test_send_basic(command_handler, mock_connection):
|
|||||||
assert result.type == EventType.OK
|
assert result.type == EventType.OK
|
||||||
assert result.payload == {}
|
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 = {"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.type == EventType.OK
|
assert result.type == EventType.OK
|
||||||
assert result.payload == expected_payload
|
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.type == EventType.ERROR
|
assert result.type == EventType.ERROR
|
||||||
assert result.payload == {"reason": "timeout"}
|
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):
|
||||||
dst = b"123456789012" # 12 bytes
|
dst = b"123456789012" # 12 bytes
|
||||||
await command_handler.send_msg(dst, "test message")
|
await command_handler.send_msg(dst, "test message")
|
||||||
|
|
||||||
assert mock_connection.send.call_args[0][0].startswith(b"\x02\x00\x00")
|
assert mock_connection.send.call_args[0][0].startswith(b"\x02\x00\x00")
|
||||||
assert b"123456" in mock_connection.send.call_args[0][0]
|
assert b"123456" in mock_connection.send.call_args[0][0]
|
||||||
|
|
||||||
|
|
||||||
async def test_validate_destination_hex_string(command_handler, mock_connection):
|
async def test_validate_destination_hex_string(command_handler, mock_connection):
|
||||||
dst = "0123456789abcdef"
|
dst = "0123456789abcdef"
|
||||||
await command_handler.send_msg(dst, "test message")
|
await command_handler.send_msg(dst, "test message")
|
||||||
|
|
||||||
assert mock_connection.send.call_args[0][0].startswith(b"\x02\x00\x00")
|
assert mock_connection.send.call_args[0][0].startswith(b"\x02\x00\x00")
|
||||||
assert b"\x01\x23\x45\x67\x89\xab" in mock_connection.send.call_args[0][0]
|
assert b"\x01\x23\x45\x67\x89\xab" in mock_connection.send.call_args[0][0]
|
||||||
|
|
||||||
|
|
||||||
async def test_validate_destination_contact_object(command_handler, mock_connection):
|
async def test_validate_destination_contact_object(command_handler, mock_connection):
|
||||||
dst = {"public_key": "0123456789abcdef", "adv_name": "Test Contact"}
|
dst = {"public_key": "0123456789abcdef", "adv_name": "Test Contact"}
|
||||||
await command_handler.send_msg(dst, "test message")
|
await command_handler.send_msg(dst, "test message")
|
||||||
|
|
||||||
assert mock_connection.send.call_args[0][0].startswith(b"\x02\x00\x00")
|
assert mock_connection.send.call_args[0][0].startswith(b"\x02\x00\x00")
|
||||||
assert b"\x01\x23\x45\x67\x89\xab" in mock_connection.send.call_args[0][0]
|
assert b"\x01\x23\x45\x67\x89\xab" in mock_connection.send.call_args[0][0]
|
||||||
|
|
||||||
|
|
||||||
# Command tests
|
# Command tests
|
||||||
async def test_send_login(command_handler, mock_connection):
|
async def test_send_login(command_handler, mock_connection):
|
||||||
await command_handler.send_login("0123456789abcdef", "password")
|
await command_handler.send_login("0123456789abcdef", "password")
|
||||||
|
|
||||||
assert mock_connection.send.call_args[0][0].startswith(b"\x1a")
|
assert mock_connection.send.call_args[0][0].startswith(b"\x1a")
|
||||||
assert b"\x01\x23\x45\x67\x89\xab" in mock_connection.send.call_args[0][0]
|
assert b"\x01\x23\x45\x67\x89\xab" in mock_connection.send.call_args[0][0]
|
||||||
assert b"password" in mock_connection.send.call_args[0][0]
|
assert b"password" in mock_connection.send.call_args[0][0]
|
||||||
|
|
||||||
|
|
||||||
async def test_send_msg(command_handler, mock_connection):
|
async def test_send_msg(command_handler, mock_connection):
|
||||||
await command_handler.send_msg("0123456789abcdef", "hello")
|
await command_handler.send_msg("0123456789abcdef", "hello")
|
||||||
|
|
||||||
assert mock_connection.send.call_args[0][0].startswith(b"\x02\x00\x00")
|
assert mock_connection.send.call_args[0][0].startswith(b"\x02\x00\x00")
|
||||||
assert b"\x01\x23\x45\x67\x89\xab" in mock_connection.send.call_args[0][0]
|
assert b"\x01\x23\x45\x67\x89\xab" in mock_connection.send.call_args[0][0]
|
||||||
assert b"hello" in mock_connection.send.call_args[0][0]
|
assert b"hello" in mock_connection.send.call_args[0][0]
|
||||||
|
|
||||||
|
|
||||||
async def test_send_cmd(command_handler, mock_connection):
|
async def test_send_cmd(command_handler, mock_connection):
|
||||||
await command_handler.send_cmd("0123456789abcdef", "test_cmd")
|
await command_handler.send_cmd("0123456789abcdef", "test_cmd")
|
||||||
|
|
||||||
assert mock_connection.send.call_args[0][0].startswith(b"\x02\x01\x00")
|
assert mock_connection.send.call_args[0][0].startswith(b"\x02\x01\x00")
|
||||||
assert b"\x01\x23\x45\x67\x89\xab" in mock_connection.send.call_args[0][0]
|
assert b"\x01\x23\x45\x67\x89\xab" in mock_connection.send.call_args[0][0]
|
||||||
assert b"test_cmd" in mock_connection.send.call_args[0][0]
|
assert b"test_cmd" in mock_connection.send.call_args[0][0]
|
||||||
|
|
||||||
|
|
||||||
# Device settings tests
|
# Device settings tests
|
||||||
async def test_set_name(command_handler, mock_connection):
|
async def test_set_name(command_handler, mock_connection):
|
||||||
await command_handler.set_name("Test Device")
|
await command_handler.set_name("Test Device")
|
||||||
|
|
||||||
assert mock_connection.send.call_args[0][0].startswith(b"\x08")
|
assert mock_connection.send.call_args[0][0].startswith(b"\x08")
|
||||||
assert b"Test Device" in mock_connection.send.call_args[0][0]
|
assert b"Test Device" in mock_connection.send.call_args[0][0]
|
||||||
|
|
||||||
|
|
||||||
async def test_set_coords(command_handler, mock_connection):
|
async def test_set_coords(command_handler, mock_connection):
|
||||||
await command_handler.set_coords(37.7749, -122.4194)
|
await command_handler.set_coords(37.7749, -122.4194)
|
||||||
|
|
||||||
assert mock_connection.send.call_args[0][0].startswith(b"\x0e")
|
assert mock_connection.send.call_args[0][0].startswith(b"\x0e")
|
||||||
# Could add more detailed assertions for the byte encoding
|
# Could add more detailed assertions for the byte encoding
|
||||||
|
|
||||||
|
|
||||||
async def test_send_appstart(command_handler, mock_connection):
|
async def test_send_appstart(command_handler, mock_connection):
|
||||||
await command_handler.send_appstart()
|
await command_handler.send_appstart()
|
||||||
assert mock_connection.send.call_args[0][0].startswith(b"\x01\x03")
|
assert mock_connection.send.call_args[0][0].startswith(b"\x01\x03")
|
||||||
assert b"mccli" in mock_connection.send.call_args[0][0]
|
assert b"mccli" in mock_connection.send.call_args[0][0]
|
||||||
|
|
||||||
|
|
||||||
async def test_send_device_query(command_handler, mock_connection):
|
async def test_send_device_query(command_handler, mock_connection):
|
||||||
await command_handler.send_device_query()
|
await command_handler.send_device_query()
|
||||||
assert mock_connection.send.call_args[0][0].startswith(b"\x16\x03")
|
assert mock_connection.send.call_args[0][0].startswith(b"\x16\x03")
|
||||||
|
|
||||||
|
|
||||||
async def test_send_advert(command_handler, mock_connection):
|
async def test_send_advert(command_handler, mock_connection):
|
||||||
# Test without flood
|
# Test without flood
|
||||||
await command_handler.send_advert(flood=False)
|
await command_handler.send_advert(flood=False)
|
||||||
assert mock_connection.send.call_args[0][0] == b"\x07"
|
assert mock_connection.send.call_args[0][0] == b"\x07"
|
||||||
|
|
||||||
# Test with flood
|
# Test with flood
|
||||||
mock_connection.reset_mock()
|
mock_connection.reset_mock()
|
||||||
await command_handler.send_advert(flood=True)
|
await command_handler.send_advert(flood=True)
|
||||||
assert mock_connection.send.call_args[0][0] == b"\x07\x01"
|
assert mock_connection.send.call_args[0][0] == b"\x07\x01"
|
||||||
|
|
||||||
|
|
||||||
async def test_reboot(command_handler, mock_connection):
|
async def test_reboot(command_handler, mock_connection):
|
||||||
await command_handler.reboot()
|
await command_handler.reboot()
|
||||||
assert mock_connection.send.call_args[0][0].startswith(b"\x13reboot")
|
assert mock_connection.send.call_args[0][0].startswith(b"\x13reboot")
|
||||||
|
|
||||||
|
|
||||||
async def test_get_bat(command_handler, mock_connection):
|
async def test_get_bat(command_handler, mock_connection):
|
||||||
await command_handler.get_bat()
|
await command_handler.get_bat()
|
||||||
assert mock_connection.send.call_args[0][0].startswith(b"\x14")
|
assert mock_connection.send.call_args[0][0].startswith(b"\x14")
|
||||||
|
|
||||||
|
|
||||||
async def test_get_time(command_handler, mock_connection):
|
async def test_get_time(command_handler, mock_connection):
|
||||||
await command_handler.get_time()
|
await command_handler.get_time()
|
||||||
assert mock_connection.send.call_args[0][0].startswith(b"\x05")
|
assert mock_connection.send.call_args[0][0].startswith(b"\x05")
|
||||||
|
|
||||||
|
|
||||||
async def test_set_time(command_handler, mock_connection):
|
async def test_set_time(command_handler, mock_connection):
|
||||||
timestamp = 1620000000 # Example timestamp
|
timestamp = 1620000000 # Example timestamp
|
||||||
await command_handler.set_time(timestamp)
|
await command_handler.set_time(timestamp)
|
||||||
assert mock_connection.send.call_args[0][0].startswith(b"\x06")
|
assert mock_connection.send.call_args[0][0].startswith(b"\x06")
|
||||||
|
|
||||||
|
|
||||||
async def test_set_tx_power(command_handler, mock_connection):
|
async def test_set_tx_power(command_handler, mock_connection):
|
||||||
await command_handler.set_tx_power(20)
|
await command_handler.set_tx_power(20)
|
||||||
assert mock_connection.send.call_args[0][0].startswith(b"\x0c")
|
assert mock_connection.send.call_args[0][0].startswith(b"\x0c")
|
||||||
|
|
||||||
|
|
||||||
async def test_get_contacts(command_handler, mock_connection):
|
async def test_get_contacts(command_handler, mock_connection):
|
||||||
await command_handler.get_contacts()
|
await command_handler.get_contacts()
|
||||||
assert mock_connection.send.call_args[0][0].startswith(b"\x04")
|
assert mock_connection.send.call_args[0][0].startswith(b"\x04")
|
||||||
|
|
||||||
|
|
||||||
async def test_reset_path(command_handler, mock_connection):
|
async def test_reset_path(command_handler, mock_connection):
|
||||||
dst = "0123456789abcdef"
|
dst = "0123456789abcdef"
|
||||||
await command_handler.reset_path(dst)
|
await command_handler.reset_path(dst)
|
||||||
assert mock_connection.send.call_args[0][0].startswith(b"\x0D")
|
assert mock_connection.send.call_args[0][0].startswith(b"\x0d")
|
||||||
assert b"\x01\x23\x45\x67\x89\xab" in mock_connection.send.call_args[0][0]
|
assert b"\x01\x23\x45\x67\x89\xab" in mock_connection.send.call_args[0][0]
|
||||||
|
|
||||||
|
|
||||||
async def test_share_contact(command_handler, mock_connection):
|
async def test_share_contact(command_handler, mock_connection):
|
||||||
dst = "0123456789abcdef"
|
dst = "0123456789abcdef"
|
||||||
await command_handler.share_contact(dst)
|
await command_handler.share_contact(dst)
|
||||||
assert mock_connection.send.call_args[0][0].startswith(b"\x10")
|
assert mock_connection.send.call_args[0][0].startswith(b"\x10")
|
||||||
assert b"\x01\x23\x45\x67\x89\xab" in mock_connection.send.call_args[0][0]
|
assert b"\x01\x23\x45\x67\x89\xab" in mock_connection.send.call_args[0][0]
|
||||||
|
|
||||||
|
|
||||||
async def test_export_contact(command_handler, mock_connection):
|
async def test_export_contact(command_handler, mock_connection):
|
||||||
# Test exporting all contacts
|
# Test exporting all contacts
|
||||||
await command_handler.export_contact()
|
await command_handler.export_contact()
|
||||||
assert mock_connection.send.call_args[0][0] == b"\x11"
|
assert mock_connection.send.call_args[0][0] == b"\x11"
|
||||||
|
|
||||||
# Test exporting specific contact
|
# Test exporting specific contact
|
||||||
mock_connection.reset_mock()
|
mock_connection.reset_mock()
|
||||||
dst = "0123456789abcdef"
|
dst = "0123456789abcdef"
|
||||||
@@ -192,20 +223,23 @@ async def test_export_contact(command_handler, mock_connection):
|
|||||||
assert mock_connection.send.call_args[0][0].startswith(b"\x11")
|
assert mock_connection.send.call_args[0][0].startswith(b"\x11")
|
||||||
assert b"\x01\x23\x45\x67\x89\xab" in mock_connection.send.call_args[0][0]
|
assert b"\x01\x23\x45\x67\x89\xab" in mock_connection.send.call_args[0][0]
|
||||||
|
|
||||||
|
|
||||||
async def test_remove_contact(command_handler, mock_connection):
|
async def test_remove_contact(command_handler, mock_connection):
|
||||||
dst = "0123456789abcdef"
|
dst = "0123456789abcdef"
|
||||||
await command_handler.remove_contact(dst)
|
await command_handler.remove_contact(dst)
|
||||||
assert mock_connection.send.call_args[0][0].startswith(b"\x0f")
|
assert mock_connection.send.call_args[0][0].startswith(b"\x0f")
|
||||||
assert b"\x01\x23\x45\x67\x89\xab" in mock_connection.send.call_args[0][0]
|
assert b"\x01\x23\x45\x67\x89\xab" in mock_connection.send.call_args[0][0]
|
||||||
|
|
||||||
|
|
||||||
async def test_get_msg(command_handler, mock_connection):
|
async def test_get_msg(command_handler, mock_connection):
|
||||||
await command_handler.get_msg()
|
await command_handler.get_msg()
|
||||||
assert mock_connection.send.call_args[0][0].startswith(b"\x0A")
|
assert mock_connection.send.call_args[0][0].startswith(b"\x0a")
|
||||||
|
|
||||||
# Test with custom timeout
|
# Test with custom timeout
|
||||||
mock_connection.reset_mock()
|
mock_connection.reset_mock()
|
||||||
await command_handler.get_msg(timeout=5.0)
|
await command_handler.get_msg(timeout=5.0)
|
||||||
assert mock_connection.send.call_args[0][0].startswith(b"\x0A")
|
assert mock_connection.send.call_args[0][0].startswith(b"\x0a")
|
||||||
|
|
||||||
|
|
||||||
async def test_send_logout(command_handler, mock_connection):
|
async def test_send_logout(command_handler, mock_connection):
|
||||||
dst = "0123456789abcdef"
|
dst = "0123456789abcdef"
|
||||||
@@ -213,65 +247,74 @@ async def test_send_logout(command_handler, mock_connection):
|
|||||||
assert mock_connection.send.call_args[0][0].startswith(b"\x1d")
|
assert mock_connection.send.call_args[0][0].startswith(b"\x1d")
|
||||||
assert b"\x01\x23\x45\x67\x89\xab" in mock_connection.send.call_args[0][0]
|
assert b"\x01\x23\x45\x67\x89\xab" in mock_connection.send.call_args[0][0]
|
||||||
|
|
||||||
|
|
||||||
async def test_send_statusreq(command_handler, mock_connection):
|
async def test_send_statusreq(command_handler, mock_connection):
|
||||||
dst = "0123456789abcdef"
|
dst = "0123456789abcdef"
|
||||||
await command_handler.send_statusreq(dst)
|
await command_handler.send_statusreq(dst)
|
||||||
assert mock_connection.send.call_args[0][0].startswith(b"\x1b")
|
assert mock_connection.send.call_args[0][0].startswith(b"\x1b")
|
||||||
assert b"\x01\x23\x45\x67\x89\xab" in mock_connection.send.call_args[0][0]
|
assert b"\x01\x23\x45\x67\x89\xab" in mock_connection.send.call_args[0][0]
|
||||||
|
|
||||||
|
|
||||||
async def test_send_trace(command_handler, mock_connection):
|
async def test_send_trace(command_handler, mock_connection):
|
||||||
# Test with minimal parameters
|
# Test with minimal parameters
|
||||||
await command_handler.send_trace()
|
await command_handler.send_trace()
|
||||||
first_call = mock_connection.send.call_args[0][0]
|
first_call = mock_connection.send.call_args[0][0]
|
||||||
assert first_call.startswith(b"\x24") # 36 in decimal = 0x24 in hex
|
assert first_call.startswith(b"\x24") # 36 in decimal = 0x24 in hex
|
||||||
|
|
||||||
# Test with all parameters
|
# Test with all parameters
|
||||||
mock_connection.reset_mock()
|
mock_connection.reset_mock()
|
||||||
await command_handler.send_trace(
|
await command_handler.send_trace(
|
||||||
auth_code=12345,
|
auth_code=12345, tag=67890, flags=1, path="01,23,45"
|
||||||
tag=67890,
|
|
||||||
flags=1,
|
|
||||||
path="01,23,45"
|
|
||||||
)
|
)
|
||||||
second_call = mock_connection.send.call_args[0][0]
|
second_call = mock_connection.send.call_args[0][0]
|
||||||
assert second_call.startswith(b"\x24")
|
assert second_call.startswith(b"\x24")
|
||||||
|
|
||||||
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 = {"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
|
||||||
return Event(EventType.ERROR, error_payload)
|
return Event(EventType.ERROR, error_payload)
|
||||||
|
|
||||||
# Patch the wait_for_event method to return our simulated event
|
# Patch the wait_for_event method to return our simulated event
|
||||||
mock_dispatcher.wait_for_event.side_effect = simulate_error_event
|
mock_dispatcher.wait_for_event.side_effect = simulate_error_event
|
||||||
|
|
||||||
# Call send with both OK and ERROR in the expected_events list, with OK first
|
# Call send with both OK and ERROR in the expected_events list, with OK first
|
||||||
result = await command_handler.send(b"test_command", [EventType.OK, EventType.ERROR])
|
result = await command_handler.send(
|
||||||
|
b"test_command", [EventType.OK, EventType.ERROR]
|
||||||
|
)
|
||||||
|
|
||||||
# Verify the command was sent
|
# Verify the command was sent
|
||||||
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.type == EventType.ERROR
|
assert result.type == EventType.ERROR
|
||||||
assert result.payload == error_payload
|
assert result.payload == error_payload
|
||||||
|
|
||||||
|
|
||||||
# Channel command tests
|
# Channel command tests
|
||||||
async def test_get_channel(command_handler, mock_connection):
|
async def test_get_channel(command_handler, mock_connection):
|
||||||
await command_handler.get_channel(3)
|
await command_handler.get_channel(3)
|
||||||
assert mock_connection.send.call_args[0][0] == b"\x1f\x03"
|
assert mock_connection.send.call_args[0][0] == b"\x1f\x03"
|
||||||
|
|
||||||
|
|
||||||
async def test_set_channel(command_handler, mock_connection):
|
async def test_set_channel(command_handler, mock_connection):
|
||||||
channel_secret = bytes(range(16)) # 16 bytes: 0x00, 0x01, ..., 0x0f
|
channel_secret = bytes(range(16)) # 16 bytes: 0x00, 0x01, ..., 0x0f
|
||||||
await command_handler.set_channel(5, "MyChannel", channel_secret)
|
await command_handler.set_channel(5, "MyChannel", channel_secret)
|
||||||
|
|
||||||
expected_data = b"\x20\x05" # CMD_SET_CHANNEL + channel_idx=5
|
expected_data = b"\x20\x05" # CMD_SET_CHANNEL + channel_idx=5
|
||||||
expected_data += b"MyChannel" + b"\x00" * (32 - len("MyChannel")) # 32-byte padded name
|
expected_data += b"MyChannel" + b"\x00" * (
|
||||||
|
32 - len("MyChannel")
|
||||||
|
) # 32-byte padded name
|
||||||
expected_data += channel_secret # 16-byte secret
|
expected_data += channel_secret # 16-byte secret
|
||||||
|
|
||||||
assert mock_connection.send.call_args[0][0] == expected_data
|
assert mock_connection.send.call_args[0][0] == expected_data
|
||||||
|
|
||||||
|
|
||||||
async def test_set_channel_invalid_secret_length(command_handler):
|
async def test_set_channel_invalid_secret_length(command_handler):
|
||||||
with pytest.raises(ValueError, match="Channel secret must be exactly 16 bytes"):
|
with pytest.raises(ValueError, match="Channel secret must be exactly 16 bytes"):
|
||||||
await command_handler.set_channel(1, "Test", b"tooshort")
|
await command_handler.set_channel(1, "Test", b"tooshort")
|
||||||
|
|||||||
@@ -1,127 +1,129 @@
|
|||||||
import pytest
|
import pytest
|
||||||
import asyncio
|
import asyncio
|
||||||
from unittest.mock import MagicMock, AsyncMock
|
from unittest.mock import MagicMock
|
||||||
from meshcore.events import EventDispatcher, EventType, Event
|
from meshcore.events import EventDispatcher, EventType, Event
|
||||||
|
|
||||||
pytestmark = pytest.mark.asyncio
|
pytestmark = pytest.mark.asyncio
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def dispatcher():
|
def dispatcher():
|
||||||
return EventDispatcher()
|
return EventDispatcher()
|
||||||
|
|
||||||
|
|
||||||
async def test_subscribe_with_attribute_filter(dispatcher):
|
async def test_subscribe_with_attribute_filter(dispatcher):
|
||||||
callback = MagicMock()
|
callback = MagicMock()
|
||||||
|
|
||||||
# Subscribe with attribute filters
|
# Subscribe with attribute filters
|
||||||
subscription = dispatcher.subscribe(
|
dispatcher.subscribe(
|
||||||
EventType.MSG_SENT,
|
EventType.MSG_SENT,
|
||||||
callback,
|
callback,
|
||||||
attribute_filters={"type": 1, "expected_ack": "1234"}
|
attribute_filters={"type": 1, "expected_ack": "1234"},
|
||||||
)
|
)
|
||||||
|
|
||||||
# Start the dispatcher
|
# Start the dispatcher
|
||||||
await dispatcher.start()
|
await dispatcher.start()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Dispatch event that should NOT match (wrong type)
|
# Dispatch event that should NOT match (wrong type)
|
||||||
await dispatcher.dispatch(Event(
|
await dispatcher.dispatch(
|
||||||
EventType.MSG_SENT,
|
Event(
|
||||||
{"some": "data"},
|
EventType.MSG_SENT,
|
||||||
{"type": 2, "expected_ack": "1234"}
|
{"some": "data"},
|
||||||
))
|
{"type": 2, "expected_ack": "1234"},
|
||||||
|
)
|
||||||
|
)
|
||||||
await asyncio.sleep(0.1) # Allow processing
|
await asyncio.sleep(0.1) # Allow processing
|
||||||
|
|
||||||
# Callback should NOT have been called
|
# Callback should NOT have been called
|
||||||
assert callback.call_count == 0
|
assert callback.call_count == 0
|
||||||
|
|
||||||
# Dispatch event that should match all filters
|
# Dispatch event that should match all filters
|
||||||
await dispatcher.dispatch(Event(
|
await dispatcher.dispatch(
|
||||||
EventType.MSG_SENT,
|
Event(
|
||||||
{"some": "data"},
|
EventType.MSG_SENT,
|
||||||
{"type": 1, "expected_ack": "1234"}
|
{"some": "data"},
|
||||||
))
|
{"type": 1, "expected_ack": "1234"},
|
||||||
|
)
|
||||||
|
)
|
||||||
await asyncio.sleep(0.1) # Allow processing
|
await asyncio.sleep(0.1) # Allow processing
|
||||||
|
|
||||||
# Callback should have been called once
|
# Callback should have been called once
|
||||||
assert callback.call_count == 1
|
assert callback.call_count == 1
|
||||||
|
|
||||||
finally:
|
finally:
|
||||||
await dispatcher.stop()
|
await dispatcher.stop()
|
||||||
|
|
||||||
|
|
||||||
async def test_wait_for_event_with_attribute_filter(dispatcher):
|
async def test_wait_for_event_with_attribute_filter(dispatcher):
|
||||||
await dispatcher.start()
|
await dispatcher.start()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
future_event = asyncio.create_task(
|
future_event = asyncio.create_task(
|
||||||
dispatcher.wait_for_event(
|
dispatcher.wait_for_event(
|
||||||
EventType.ACK,
|
EventType.ACK, attribute_filters={"code": "1234"}, timeout=3.0
|
||||||
attribute_filters={"code": "1234"},
|
|
||||||
timeout=3.0
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
await asyncio.sleep(0.1)
|
await asyncio.sleep(0.1)
|
||||||
|
|
||||||
await dispatcher.dispatch(Event(
|
await dispatcher.dispatch(
|
||||||
EventType.ACK,
|
Event(EventType.ACK, {"some": "data"}, {"code": "5678"})
|
||||||
{"some": "data"},
|
)
|
||||||
{"code": "5678"}
|
|
||||||
))
|
|
||||||
|
|
||||||
await asyncio.sleep(0.1)
|
await asyncio.sleep(0.1)
|
||||||
|
|
||||||
await dispatcher.dispatch(Event(
|
await dispatcher.dispatch(
|
||||||
EventType.ACK,
|
Event(EventType.ACK, {"ack": "data"}, {"code": "1234"})
|
||||||
{"ack": "data"},
|
)
|
||||||
{"code": "1234"}
|
|
||||||
))
|
|
||||||
|
|
||||||
result = await asyncio.wait_for(future_event, 3.0)
|
result = await asyncio.wait_for(future_event, 3.0)
|
||||||
|
|
||||||
assert result is not None
|
assert result is not None
|
||||||
assert result.type == EventType.ACK
|
assert result.type == EventType.ACK
|
||||||
assert result.attributes["code"] == "1234"
|
assert result.attributes["code"] == "1234"
|
||||||
assert result.payload == {"ack": "data"}
|
assert result.payload == {"ack": "data"}
|
||||||
|
|
||||||
finally:
|
finally:
|
||||||
await dispatcher.stop()
|
await dispatcher.stop()
|
||||||
|
|
||||||
|
|
||||||
async def test_wait_for_event_timeout_with_filter(dispatcher):
|
async def test_wait_for_event_timeout_with_filter(dispatcher):
|
||||||
await dispatcher.start()
|
await dispatcher.start()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Wait for an event that won't arrive
|
# Wait for an event that won't arrive
|
||||||
result = await dispatcher.wait_for_event(
|
result = await dispatcher.wait_for_event(
|
||||||
EventType.ACK,
|
EventType.ACK, attribute_filters={"code": "1234"}, timeout=0.1
|
||||||
attribute_filters={"code": "1234"},
|
|
||||||
timeout=0.1
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Should get None due to timeout
|
# Should get None due to timeout
|
||||||
assert result is None
|
assert result is None
|
||||||
|
|
||||||
finally:
|
finally:
|
||||||
await dispatcher.stop()
|
await dispatcher.stop()
|
||||||
|
|
||||||
|
|
||||||
async def test_event_init_with_kwargs():
|
async def test_event_init_with_kwargs():
|
||||||
# Test creating an event with keyword attributes
|
# Test creating an event with keyword attributes
|
||||||
event = Event(EventType.ACK, {"data": "value"}, code="1234", status="ok")
|
event = Event(EventType.ACK, {"data": "value"}, code="1234", status="ok")
|
||||||
|
|
||||||
assert event.type == EventType.ACK
|
assert event.type == EventType.ACK
|
||||||
assert event.payload == {"data": "value"}
|
assert event.payload == {"data": "value"}
|
||||||
assert event.attributes == {"code": "1234", "status": "ok"}
|
assert event.attributes == {"code": "1234", "status": "ok"}
|
||||||
|
|
||||||
|
|
||||||
async def test_channel_info_event():
|
async def test_channel_info_event():
|
||||||
# Test CHANNEL_INFO event type
|
# Test CHANNEL_INFO event type
|
||||||
channel_payload = {
|
channel_payload = {
|
||||||
"channel_idx": 3,
|
"channel_idx": 3,
|
||||||
"channel_name": "TestChannel",
|
"channel_name": "TestChannel",
|
||||||
"channel_secret": b"\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f\x10"
|
"channel_secret": b"\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f\x10",
|
||||||
}
|
}
|
||||||
|
|
||||||
event = Event(EventType.CHANNEL_INFO, channel_payload)
|
event = Event(EventType.CHANNEL_INFO, channel_payload)
|
||||||
|
|
||||||
assert event.type == EventType.CHANNEL_INFO
|
assert event.type == EventType.CHANNEL_INFO
|
||||||
assert event.payload["channel_idx"] == 3
|
assert event.payload["channel_idx"] == 3
|
||||||
assert event.payload["channel_name"] == "TestChannel"
|
assert event.payload["channel_name"] == "TestChannel"
|
||||||
assert len(event.payload["channel_secret"]) == 16
|
assert len(event.payload["channel_secret"]) == 16
|
||||||
|
|||||||
Reference in New Issue
Block a user