mirror of
https://github.com/meshcore-dev/meshcore_py.git
synced 2026-06-11 11:56:18 +00:00
Merge branch 'main' into feature/refactor
This commit is contained in:
41
.github/python-test.yml
vendored
Normal file
41
.github/python-test.yml
vendored
Normal 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
|
||||||
@@ -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:
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
Reference in New Issue
Block a user