Merge branch 'main' into feature/refactor

This commit is contained in:
fdlamotte 2025-08-06 10:56:24 +02:00 committed by GitHub
commit 4ce3a6fd9a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
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:
logger.debug("Using pre-configured BleakClient.")
# 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
else:

View file

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

View file

@ -132,6 +132,7 @@ class MeshCore:
auto_reconnect: bool = False,
max_reconnect_attempts: int = 3,
) -> "MeshCore":
"""
Create and connect a MeshCore instance using BLE connection.
@ -141,7 +142,6 @@ class MeshCore:
If provided, 'address' is ignored for connection
but can be used for identification.
"""
connection = BLEConnection(address=address, client=client)
mc = cls(
@ -152,6 +152,7 @@ class MeshCore:
auto_reconnect=auto_reconnect,
max_reconnect_attempts=max_reconnect_attempts,
)
await mc.connect()
return mc

View file

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

View file

@ -8,9 +8,9 @@ from meshcore.ble_cx import (
UART_RX_CHAR_UUID,
)
class TestBLEConnection(unittest.TestCase):
@patch("meshcore.ble_cx.BleakClient")
def test_ble_connection_and_disconnection(self, mock_bleak_client):
"""
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()
@patch("meshcore.ble_cx.BleakClient")
def test_send_data(self, mock_bleak_client):
"""
Tests the send method of the BLEConnection class.
@ -65,17 +66,15 @@ class TestBLEConnection(unittest.TestCase):
mock_client.start_notify = AsyncMock()
mock_client.write_gatt_char = AsyncMock()
mock_client.is_connected = True
mock_service = MagicMock()
mock_char = MagicMock()
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_client.services.get_service.return_value = mock_service
return mock_client
if __name__ == '__main__':
if __name__ == "__main__":
unittest.main()