Merge branch 'main' into feature/refactor

This commit is contained in:
fdlamotte
2025-08-06 10:56:24 +02:00
committed by GitHub
6 changed files with 79 additions and 24 deletions

41
.github/python-test.yml vendored Normal file
View File

@@ -0,0 +1,41 @@
# This workflow will install Python dependencies, run tests and lint with a variety of Python versions
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python
name: Python package
on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]
jobs:
build:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python-version: ["3.10", "3.13"]
steps:
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v3
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
python -m pip install flake8 pytest
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
- name: Lint with flake8
run: |
# TODO: Enable this later
# stop the build if there are Python syntax errors or undefined names
# flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
# exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
# flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
- name: Test with pytest
run: |
pytest

View File

@@ -49,7 +49,8 @@ class BLEConnection:
if self.client: if self.client:
logger.debug("Using pre-configured BleakClient.") logger.debug("Using pre-configured BleakClient.")
# If a client is already provided, ensure its disconnect callback is set # If a client is already provided, ensure its disconnect callback is set
self.client._disconnected_callback = self.handle_disconnect assert isinstance(self.client, BleakClient)
self.client.set_disconnected_callback(self.handle_disconnect)
self.address = self.client.address self.address = self.client.address
else: else:

View File

@@ -1,4 +1,5 @@
from enum import Enum from enum import Enum
import inspect
import logging import logging
from typing import Any, Dict, Optional, Callable, List, Union from typing import Any, Dict, Optional, Callable, List, Union
import asyncio import asyncio
@@ -148,6 +149,7 @@ class EventDispatcher:
logger.debug( logger.debug(
f"Dispatching event: {event.type}, {event.payload}, {event.attributes}" 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 ( if (
@@ -165,15 +167,24 @@ class EventDispatcher:
for key, value in subscription.attribute_filters.items() for key, value in subscription.attribute_filters.items()
): ):
continue continue
try:
result = subscription.callback(event.clone()) # Fire the call back asychronously
if asyncio.iscoroutine(result): asyncio.create_task(self._execute_callback(subscription.callback, event.clone()))
await result
except Exception as e:
print(f"Error in event handler: {e}")
self.queue.task_done() self.queue.task_done()
async def _execute_callback(self, callback, event):
"""Execute a callback with proper error handling"""
try:
if asyncio.iscoroutinefunction(callback):
await callback(event)
else:
result = callback(event)
if inspect.iscoroutine(result):
await result
except Exception as e:
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

View File

@@ -132,6 +132,7 @@ class MeshCore:
auto_reconnect: bool = False, auto_reconnect: bool = False,
max_reconnect_attempts: int = 3, max_reconnect_attempts: int = 3,
) -> "MeshCore": ) -> "MeshCore":
""" """
Create and connect a MeshCore instance using BLE connection. Create and connect a MeshCore instance using BLE connection.
@@ -141,7 +142,6 @@ 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( mc = cls(
@@ -152,6 +152,7 @@ class MeshCore:
auto_reconnect=auto_reconnect, auto_reconnect=auto_reconnect,
max_reconnect_attempts=max_reconnect_attempts, max_reconnect_attempts=max_reconnect_attempts,
) )
await mc.connect() await mc.connect()
return mc return mc

View File

@@ -22,6 +22,7 @@ class SerialConnection:
self.inframe = b"" self.inframe = b""
self._disconnect_callback = None self._disconnect_callback = None
self.cx_dly = cx_dly self.cx_dly = cx_dly
self._connected_event = asyncio.Event()
class MCSerialClientProtocol(asyncio.Protocol): class MCSerialClientProtocol(asyncio.Protocol):
def __init__(self, cx): def __init__(self, cx):
@@ -29,20 +30,18 @@ class SerialConnection:
def connection_made(self, transport): def connection_made(self, transport):
self.cx.transport = transport self.cx.transport = transport
logger.debug("port opened") logger.debug('port opened')
if ( if isinstance(transport, serial_asyncio.SerialTransport) and transport.serial:
isinstance(transport, serial_asyncio.SerialTransport) transport.serial.rts = False # You can manipulate Serial object via transport
and transport.serial self.cx._connected_event.set()
):
transport.serial.rts = (
False # You can manipulate Serial object via transport
)
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()
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"))
@@ -56,6 +55,8 @@ class SerialConnection:
""" """
Connects to the device Connects to the device
""" """
self._connected_event.clear()
loop = asyncio.get_running_loop() loop = asyncio.get_running_loop()
await serial_asyncio.create_serial_connection( await serial_asyncio.create_serial_connection(
loop, loop,
@@ -64,7 +65,7 @@ class SerialConnection:
baudrate=self.baudrate, baudrate=self.baudrate,
) )
await asyncio.sleep(self.cx_dly) # wait for cx to establish await self._connected_event.wait()
logger.info("Serial Connection started") logger.info("Serial Connection started")
return self.port return self.port
@@ -109,6 +110,7 @@ class SerialConnection:
if self.transport: if self.transport:
self.transport.close() self.transport.close()
self.transport = None self.transport = None
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):

View File

@@ -8,9 +8,9 @@ from meshcore.ble_cx import (
UART_RX_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.
@@ -34,6 +34,7 @@ class TestBLEConnection(unittest.TestCase):
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.
@@ -65,17 +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()