Merge pull request #6 from fdlamotte/awolden/fix-issues

Improvements and fixes
This commit is contained in:
fdlamotte
2025-05-15 21:32:25 +02:00
committed by GitHub
7 changed files with 164 additions and 19 deletions

View File

@@ -0,0 +1,94 @@
#!/usr/bin/python
import asyncio
import argparse
import logging
from math import log
from meshcore import MeshCore
from meshcore.events import EventType
# Set up logging
logging.basicConfig(level=logging.DEBUG, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)
async def main():
# Parse command line arguments
parser = argparse.ArgumentParser(description='Get status from a repeater via TCP connection')
parser.add_argument('-host', '--hostname', required=True, help='TCP hostname or IP address')
parser.add_argument('-port', '--port', type=int, required=True, help='TCP port number')
parser.add_argument('-r', '--repeater', required=True, help='Repeater name')
parser.add_argument('-pw', '--password', required=True, help='Password for login')
args = parser.parse_args()
# Connect to the device
print(f"Connecting to TCP {args.hostname}:{args.port}...")
mc = await MeshCore.create_tcp(args.hostname, args.port, debug=True)
try:
# Set up a simple event handler to log all events
async def log_event(event):
print(f"EVENT: {event.type.name} - Payload: {event.payload}")
# Subscribe to login events
mc.subscribe(EventType.LOGIN_SUCCESS, log_event)
mc.subscribe(EventType.LOGIN_FAILED, log_event)
mc.subscribe(EventType.STATUS_RESPONSE, log_event)
# Get contacts
await mc.ensure_contacts()
repeater = mc.get_contact_by_name(args.repeater)
if repeater is None:
print(f"Repeater '{args.repeater}' not found in contacts.")
print(f"Available contacts: {mc.contacts}")
return
print(f"Found repeater: {repeater}")
# Send login request
print(f"Sending login request to '{args.repeater}'...")
login_cmd = await mc.commands.send_login(repeater, args.password)
if login_cmd.type == EventType.ERROR:
print(f"Login failed: {login_cmd.payload}")
return
filter = {"pubkey_prefix": repeater["public_key"][0:12]}
login_result = await mc.wait_for_event(EventType.LOGIN_SUCCESS, filter, timeout=10)
print(f"Login result: {login_result}")
send_ver = await mc.commands.send_cmd(repeater, "ver")
if send_ver.type == EventType.ERROR:
print(f"Error sending version command: {send_ver.payload}")
return
await mc.wait_for_event(EventType.MESSAGES_WAITING)
ver_msg = await mc.commands.get_msg()
print(f"Version message: {ver_msg.payload}")
# Send status request
print("Sending status request...")
await mc.commands.send_statusreq(repeater)
# Wait for status response
print("Waiting for status response event...")
status_event = await mc.wait_for_event(EventType.STATUS_RESPONSE, timeout=5.0)
if status_event:
print(f"Status response received: {status_event.payload}")
else:
print("No status response received within timeout")
finally:
# Always disconnect properly
print("Disconnecting...")
await mc.disconnect()
print("Disconnected from device")
if __name__ == "__main__":
try:
asyncio.run(main())
except KeyboardInterrupt:
print("\nOperation cancelled by user")
except Exception as e:
print(f"Error: {e}")

View File

@@ -91,3 +91,9 @@ class BLEConnection:
logger.error("RX characteristic not found") logger.error("RX characteristic not found")
return False return False
await self.client.write_gatt_char(self.rx_char, bytes(data), response=False) await self.client.write_gatt_char(self.rx_char, bytes(data), response=False)
async def disconnect(self):
"""Disconnect from the BLE device."""
if self.client and self.client.is_connected:
await self.client.disconnect()
logger.debug("BLE Connection closed")

View File

@@ -64,6 +64,15 @@ class Event:
# 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):
"""
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
return Event(self.type, copied_payload, self.attributes.copy())
class Subscription: class Subscription:
@@ -79,7 +88,7 @@ class Subscription:
class EventDispatcher: class EventDispatcher:
def __init__(self): def __init__(self):
self.queue = asyncio.Queue() self.queue: asyncio.Queue[Event] = asyncio.Queue()
self.subscriptions: List[Subscription] = [] self.subscriptions: List[Subscription] = []
self.running = False self.running = False
self._task = None self._task = None
@@ -118,7 +127,6 @@ class EventDispatcher:
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():
logger.debug(f"Checking subscription: {subscription.event_type}, {subscription.attribute_filters}")
# 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
@@ -128,7 +136,7 @@ class EventDispatcher:
for key, value in subscription.attribute_filters.items()): for key, value in subscription.attribute_filters.items()):
continue continue
try: try:
result = subscription.callback(event) result = subscription.callback(event.clone())
if asyncio.iscoroutine(result): if asyncio.iscoroutine(result):
await result await result
except Exception as e: except Exception as e:

View File

@@ -5,7 +5,9 @@ from typing import Optional, Dict, Any, Union
from .events import EventDispatcher, EventType from .events import EventDispatcher, EventType
from .reader import MessageReader from .reader import MessageReader
from .commands import CommandHandler from .commands import CommandHandler
from .ble_cx import BLEConnection
from .tcp_cx import TCPConnection
from .serial_cx import SerialConnection
# Setup default logger # Setup default logger
logger = logging.getLogger("meshcore") logger = logging.getLogger("meshcore")
@@ -45,9 +47,7 @@ class MeshCore:
@classmethod @classmethod
async def create_tcp(cls, host: str, port: int, debug: bool = False, default_timeout=None) -> 'MeshCore': async def create_tcp(cls, host: str, port: int, debug: bool = False, default_timeout=None) -> 'MeshCore':
"""Create and connect a MeshCore instance using TCP connection""" """Create and connect a MeshCore instance using TCP connection"""
from .tcp_cx import TCPConnection
connection = TCPConnection(host, port) connection = TCPConnection(host, port)
await connection.connect() await connection.connect()
@@ -58,9 +58,6 @@ class MeshCore:
@classmethod @classmethod
async def create_serial(cls, port: str, baudrate: int = 115200, debug: bool = False, default_timeout=None) -> 'MeshCore': async def create_serial(cls, port: str, baudrate: int = 115200, debug: bool = False, default_timeout=None) -> 'MeshCore':
"""Create and connect a MeshCore instance using serial connection""" """Create and connect a MeshCore instance using serial connection"""
from .serial_cx import SerialConnection
import asyncio
connection = SerialConnection(port, baudrate) connection = SerialConnection(port, baudrate)
await connection.connect() await connection.connect()
await asyncio.sleep(0.1) # Time for transport to establish await asyncio.sleep(0.1) # Time for transport to establish
@@ -75,7 +72,6 @@ class MeshCore:
If address is None, it will scan for and connect to the first available MeshCore device. If address is None, it will scan for and connect to the first available MeshCore device.
""" """
from .ble_cx import BLEConnection
connection = BLEConnection(address) connection = BLEConnection(address)
result = await connection.connect() result = await connection.connect()
@@ -91,7 +87,17 @@ class MeshCore:
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."""
# 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
if hasattr(self, '_auto_fetch_subscription') and self._auto_fetch_subscription:
await self.stop_auto_message_fetching()
# Disconnect the connection object
if self.cx:
await self.cx.disconnect()
def stop(self): def stop(self):
"""Synchronously stop the event dispatcher task""" """Synchronously stop the event dispatcher task"""
@@ -99,7 +105,7 @@ class MeshCore:
self.dispatcher.running = False self.dispatcher.running = False
self.dispatcher._task.cancel() self.dispatcher._task.cancel()
def subscribe(self, event_type: EventType, callback, attribute_filters: Optional[Dict[str, Any]] = None): def subscribe(self, event_type: Union[EventType, None], callback, attribute_filters: Optional[Dict[str, Any]] = None):
""" """
Subscribe to events using EventType enum with optional attribute filtering Subscribe to events using EventType enum with optional attribute filtering

View File

@@ -256,14 +256,31 @@ class MessageReader:
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:
logger.debug("Login success") res = {}
# TODO: Read login attributes if len(data) > 1:
await self.dispatcher.dispatch(Event(EventType.LOGIN_SUCCESS, {})) res["permissions"] = data[1]
res["is_admin"] = (data[1] & 1) == 1 # Check if admin bit is set
if len(data) > 7:
res["pubkey_prefix"] = data[2:8].hex()
attributes = {
"pubkey_prefix": res.get("pubkey_prefix")
}
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:
logger.debug("Login failed") res = {}
# TODO: Read login attributes
await self.dispatcher.dispatch(Event(EventType.LOGIN_FAILED, {})) if len(data) > 7:
res["pubkey_prefix"] = data[2:8].hex()
attributes = {
"pubkey_prefix": res.get("pubkey_prefix")
}
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 = {}

View File

@@ -86,4 +86,11 @@ class SerialConnection:
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):
"""Close the serial connection."""
if self.transport:
self.transport.close()
self.transport = None
logger.debug("Serial Connection closed")

View File

@@ -85,3 +85,10 @@ class TCPConnection:
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):
"""Close the TCP connection."""
if self.transport:
self.transport.close()
self.transport = None
logger.debug("TCP Connection closed")