diff --git a/examples/tcp_login_status.py b/examples/tcp_login_status.py new file mode 100644 index 0000000..698de64 --- /dev/null +++ b/examples/tcp_login_status.py @@ -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}") \ No newline at end of file diff --git a/src/meshcore/ble_cx.py b/src/meshcore/ble_cx.py index fbdca89..72e363f 100644 --- a/src/meshcore/ble_cx.py +++ b/src/meshcore/ble_cx.py @@ -91,3 +91,9 @@ class BLEConnection: logger.error("RX characteristic not found") return 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") diff --git a/src/meshcore/events.py b/src/meshcore/events.py index 94da1e3..e263899 100644 --- a/src/meshcore/events.py +++ b/src/meshcore/events.py @@ -64,6 +64,15 @@ class Event: # 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 + return Event(self.type, copied_payload, self.attributes.copy()) class Subscription: @@ -79,7 +88,7 @@ class Subscription: class EventDispatcher: def __init__(self): - self.queue = asyncio.Queue() + self.queue: asyncio.Queue[Event] = asyncio.Queue() self.subscriptions: List[Subscription] = [] self.running = False self._task = None @@ -118,7 +127,6 @@ class EventDispatcher: event = await self.queue.get() logger.debug(f"Dispatching event: {event.type}, {event.payload}, {event.attributes}") for subscription in self.subscriptions.copy(): - logger.debug(f"Checking subscription: {subscription.event_type}, {subscription.attribute_filters}") # Check if event type matches if subscription.event_type is None or subscription.event_type == event.type: # Check if all attribute filters match @@ -128,7 +136,7 @@ class EventDispatcher: for key, value in subscription.attribute_filters.items()): continue try: - result = subscription.callback(event) + result = subscription.callback(event.clone()) if asyncio.iscoroutine(result): await result except Exception as e: diff --git a/src/meshcore/meshcore.py b/src/meshcore/meshcore.py index e52959d..ad55b34 100644 --- a/src/meshcore/meshcore.py +++ b/src/meshcore/meshcore.py @@ -5,7 +5,9 @@ from typing import Optional, Dict, Any, Union from .events import EventDispatcher, EventType from .reader import MessageReader from .commands import CommandHandler - +from .ble_cx import BLEConnection +from .tcp_cx import TCPConnection +from .serial_cx import SerialConnection # Setup default logger logger = logging.getLogger("meshcore") @@ -45,9 +47,7 @@ class MeshCore: @classmethod 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""" - from .tcp_cx import TCPConnection - + """Create and connect a MeshCore instance using TCP connection""" connection = TCPConnection(host, port) await connection.connect() @@ -58,9 +58,6 @@ class MeshCore: @classmethod 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""" - from .serial_cx import SerialConnection - import asyncio - connection = SerialConnection(port, baudrate) await connection.connect() 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. """ - from .ble_cx import BLEConnection connection = BLEConnection(address) result = await connection.connect() @@ -91,7 +87,17 @@ class MeshCore: return await self.commands.send_appstart() 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() + + # 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): """Synchronously stop the event dispatcher task""" @@ -99,7 +105,7 @@ class MeshCore: self.dispatcher.running = False 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 diff --git a/src/meshcore/reader.py b/src/meshcore/reader.py index 8f63fc6..1cc8499 100644 --- a/src/meshcore/reader.py +++ b/src/meshcore/reader.py @@ -256,14 +256,31 @@ class MessageReader: await self.dispatcher.dispatch(Event(EventType.RAW_DATA, res)) elif packet_type_value == PacketType.LOGIN_SUCCESS.value: - logger.debug("Login success") - # TODO: Read login attributes - await self.dispatcher.dispatch(Event(EventType.LOGIN_SUCCESS, {})) + res = {} + if len(data) > 1: + 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: - logger.debug("Login failed") - # TODO: Read login attributes - await self.dispatcher.dispatch(Event(EventType.LOGIN_FAILED, {})) + res = {} + + 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: res = {} diff --git a/src/meshcore/serial_cx.py b/src/meshcore/serial_cx.py index b7b07c6..61931b5 100644 --- a/src/meshcore/serial_cx.py +++ b/src/meshcore/serial_cx.py @@ -86,4 +86,11 @@ class SerialConnection: size = len(data) pkt = b"\x3c" + size.to_bytes(2, byteorder="little") + data logger.debug(f"sending pkt : {pkt}") - self.transport.write(pkt) \ No newline at end of file + 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") \ No newline at end of file diff --git a/src/meshcore/tcp_cx.py b/src/meshcore/tcp_cx.py index e90fb2a..8d0aff9 100644 --- a/src/meshcore/tcp_cx.py +++ b/src/meshcore/tcp_cx.py @@ -85,3 +85,10 @@ class TCPConnection: pkt = b"\x3c" + size.to_bytes(2, byteorder="little") + data logger.debug(f"sending pkt : {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")