diff --git a/examples/ble_stats.py b/examples/ble_stats.py new file mode 100755 index 0000000..91bff5e --- /dev/null +++ b/examples/ble_stats.py @@ -0,0 +1,72 @@ +#!/usr/bin/python + +import asyncio +import argparse +import json +from meshcore import MeshCore, EventType + +DEFAULT_ADDRESS = "MeshCore-123456789" # Default BLE address or name + +async def main(): + parser = argparse.ArgumentParser(description="Read statistics from MeshCore device via BLE") + parser.add_argument("-a", "--address", default=DEFAULT_ADDRESS, + help="BLE device address or name (default: %(default)s)") + parser.add_argument("-p", "--pin", type=int, default=None, + help="PIN for BLE pairing (optional)") + args = parser.parse_args() + + print(f"Connecting to BLE device: {args.address}") + if args.pin: + print(f"Using PIN pairing: {args.pin}") + mc = await MeshCore.create_ble(args.address, pin=str(args.pin)) + else: + mc = await MeshCore.create_ble(args.address) + + print("Connected successfully!\n") + + try: + # Get core statistics + print("Fetching core statistics...") + result = await mc.commands.get_stats_core() + if result.type == EventType.ERROR: + print(f"❌ Error getting core stats: {result.payload}") + else: + print("📊 Core Statistics:") + print(json.dumps(result.payload, indent=2)) + print() + + # Get radio statistics + print("Fetching radio statistics...") + result = await mc.commands.get_stats_radio() + if result.type == EventType.ERROR: + print(f"❌ Error getting radio stats: {result.payload}") + else: + print("📡 Radio Statistics:") + print(json.dumps(result.payload, indent=2)) + print() + + # Get packet statistics + print("Fetching packet statistics...") + result = await mc.commands.get_stats_packets() + if result.type == EventType.ERROR: + print(f"❌ Error getting packet stats: {result.payload}") + else: + print("📦 Packet Statistics:") + print(json.dumps(result.payload, indent=2)) + print() + + except Exception as e: + print(f"❌ Error: {e}") + finally: + print("Disconnecting...") + await mc.disconnect() + print("Disconnected.") + +if __name__ == "__main__": + try: + asyncio.run(main()) + except KeyboardInterrupt: + print("\nExited cleanly") + except Exception as e: + print(f"Error: {e}") + diff --git a/src/meshcore/commands/device.py b/src/meshcore/commands/device.py index 6d1c657..958d4bc 100644 --- a/src/meshcore/commands/device.py +++ b/src/meshcore/commands/device.py @@ -205,3 +205,18 @@ class DeviceCommands(CommandHandlerBase): async def export_private_key(self) -> Event: logger.debug("Requesting private key export") return await self.send(b"\x17", [EventType.PRIVATE_KEY, EventType.DISABLED, EventType.ERROR]) + + async def get_stats_core(self) -> Event: + logger.debug("Getting core statistics") + # CMD_GET_STATS (56) + STATS_TYPE_CORE (0) + return await self.send(b"\x38\x00", [EventType.STATS_CORE, EventType.ERROR]) + + async def get_stats_radio(self) -> Event: + logger.debug("Getting radio statistics") + # CMD_GET_STATS (56) + STATS_TYPE_RADIO (1) + return await self.send(b"\x38\x01", [EventType.STATS_RADIO, EventType.ERROR]) + + async def get_stats_packets(self) -> Event: + logger.debug("Getting packet statistics") + # CMD_GET_STATS (56) + STATS_TYPE_PACKETS (2) + return await self.send(b"\x38\x02", [EventType.STATS_PACKETS, EventType.ERROR]) diff --git a/src/meshcore/events.py b/src/meshcore/events.py index ac523f7..56a5611 100644 --- a/src/meshcore/events.py +++ b/src/meshcore/events.py @@ -40,6 +40,9 @@ class EventType(Enum): MMA_RESPONSE = "mma_response" ACL_RESPONSE = "acl_response" CUSTOM_VARS = "custom_vars" + STATS_CORE = "stats_core" + STATS_RADIO = "stats_radio" + STATS_PACKETS = "stats_packets" CHANNEL_INFO = "channel_info" PATH_RESPONSE = "path_response" PRIVATE_KEY = "private_key" diff --git a/src/meshcore/packets.py b/src/meshcore/packets.py index 57c1755..af0818b 100644 --- a/src/meshcore/packets.py +++ b/src/meshcore/packets.py @@ -36,6 +36,9 @@ class PacketType(Enum): SIGN_START = 19 SIGNATURE = 20 CUSTOM_VARS = 21 + STATS_CORE = 24 + STATS_RADIO = 25 + STATS_PACKETS = 26 BINARY_REQ = 50 FACTORY_RESET = 51 PATH_DISCOVERY = 52 diff --git a/src/meshcore/reader.py b/src/meshcore/reader.py index 14633d0..84966cc 100644 --- a/src/meshcore/reader.py +++ b/src/meshcore/reader.py @@ -1,5 +1,6 @@ import logging import json +import struct import time import io from typing import Any, Dict @@ -290,6 +291,87 @@ class MessageReader: logger.debug(f"got custom vars : {res}") await self.dispatcher.dispatch(Event(EventType.CUSTOM_VARS, res)) + elif packet_type_value == PacketType.STATS_CORE.value: # RESP_CODE_STATS (24) + logger.debug(f"received stats response: {data.hex()}") + # RESP_CODE_STATS: All stats responses use code 24 with sub-type byte + # Byte 0: response_code (24), Byte 1: stats_type (0=core, 1=radio, 2=packets) + if len(data) < 2: + logger.error(f"Stats response too short: {len(data)} bytes, need at least 2 for header") + await self.dispatcher.dispatch(Event(EventType.ERROR, {"reason": "invalid_frame_length"})) + return + + stats_type = data[1] + + if stats_type == 0: # STATS_TYPE_CORE + # RESP_CODE_STATS + STATS_TYPE_CORE: 11 bytes total + # Format: