feat: Refactor binary commands and apply BLE fixes

Refactored the BinaryCommandHandler to align with the other command handlers, inheriting from CommandHandlerBase. This resolves an AttributeError and simplifies the command structure. Moved binary_commands.py into the commands module. Applied fixes to the BLE connection handler based on feedback, improving reliability on macOS and ensuring the device address is correctly handled.
This commit is contained in:
Ventz Petkov 2025-08-05 15:31:54 -04:00
parent c19fd166f8
commit 36727f4ea3
22 changed files with 1603 additions and 1206 deletions

View file

@ -1,13 +1,13 @@
from enum import Enum
import logging
from math import log
from typing import Any, Dict, Optional, Callable, List, Union
import asyncio
from dataclasses import dataclass, field
logger = logging.getLogger("meshcore")
# Public event types for users to subscribe to
# Public event types for users to subscribe to
class EventType(Enum):
CONTACTS = "contacts"
SELF_INFO = "self_info"
@ -20,7 +20,7 @@ class EventType(Enum):
DEVICE_INFO = "device_info"
MSG_SENT = "message_sent"
NEW_CONTACT = "new_contact"
# Push notifications
ADVERTISEMENT = "advertisement"
PATH_UPDATE = "path_update"
@ -28,7 +28,7 @@ class EventType(Enum):
MESSAGES_WAITING = "messages_waiting"
RAW_DATA = "raw_data"
LOGIN_SUCCESS = "login_success"
LOGIN_FAILED = "login_failed"
LOGIN_FAILED = "login_failed"
STATUS_RESPONSE = "status_response"
LOG_DATA = "log_data"
TRACE_DATA = "trace_data"
@ -38,11 +38,11 @@ class EventType(Enum):
CUSTOM_VARS = "custom_vars"
CHANNEL_INFO = "channel_info"
PATH_RESPONSE = "path_response"
# Command response types
OK = "command_ok"
ERROR = "command_error"
# Connection events
CONNECTED = "connected"
DISCONNECTED = "disconnected"
@ -53,11 +53,17 @@ class Event:
type: EventType
payload: Any
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
Args:
type: The event type
payload: The event payload
@ -67,18 +73,21 @@ class Event:
self.type = type
self.payload = payload
self.attributes = attributes or {}
# Add any keyword arguments to the attributes dictionary
if kwargs:
self.attributes.update(kwargs)
def clone(self):
"""
Create a copy of the event.
Returns:
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())
@ -88,7 +97,7 @@ class Subscription:
self.event_type = event_type
self.callback = callback
self.attribute_filters = attribute_filters or {}
def unsubscribe(self):
self.dispatcher._remove_subscription(self)
@ -99,12 +108,16 @@ class EventDispatcher:
self.subscriptions: List[Subscription] = []
self.running = False
self._task = None
def subscribe(self, event_type: Union[EventType, None], callback: Callable[[Event], Union[None, asyncio.Future]],
attribute_filters: Optional[Dict[str, Any]] = None) -> Subscription:
def subscribe(
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.
Parameters:
-----------
event_type : EventType or None
@ -113,7 +126,7 @@ class EventDispatcher:
Function to call when a matching event is received.
attribute_filters : Dict[str, Any], optional
Dictionary of attribute key-value pairs that must match for the event to trigger the callback.
Returns:
--------
Subscription object that can be used to unsubscribe.
@ -121,26 +134,36 @@ class EventDispatcher:
subscription = Subscription(self, event_type, callback, attribute_filters)
self.subscriptions.append(subscription)
return subscription
def _remove_subscription(self, subscription: Subscription):
if subscription in self.subscriptions:
self.subscriptions.remove(subscription)
async def dispatch(self, event: Event):
await self.queue.put(event)
async def _process_events(self):
while self.running:
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():
# 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
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
if not all(event.attributes.get(key) == value
for key, value in subscription.attribute_filters.items()):
if not all(
event.attributes.get(key) == value
for key, value in subscription.attribute_filters.items()
):
continue
try:
result = subscription.callback(event.clone())
@ -148,14 +171,14 @@ class EventDispatcher:
await result
except Exception as e:
print(f"Error in event handler: {e}")
self.queue.task_done()
async def start(self):
if not self.running:
self.running = True
self._task = asyncio.create_task(self._process_events())
async def stop(self):
if self.running:
self.running = False
@ -167,12 +190,16 @@ class EventDispatcher:
except asyncio.CancelledError:
pass
self._task = None
async def wait_for_event(self, event_type: EventType, attribute_filters: Optional[Dict[str, Any]] = None,
timeout: float | None = None) -> Optional[Event]:
async def wait_for_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.
Parameters:
-----------
event_type : EventType
@ -181,19 +208,19 @@ class EventDispatcher:
Dictionary of attribute key-value pairs that must match for the event to be returned.
timeout : float | None, optional
Maximum time to wait for the event, in seconds.
Returns:
--------
The matched event, or None if timeout occurred before a matching event.
"""
future = asyncio.Future()
def event_handler(event: Event):
if not future.done():
future.set_result(event)
subscription = self.subscribe(event_type, event_handler, attribute_filters)
try:
return await asyncio.wait_for(future, timeout)
except asyncio.TimeoutError: