Merge pull request #169 from AlanRosenthal/alan/remote-values

Add `show remote-values`
This commit is contained in:
Alan Rosenthal
2023-04-05 09:00:48 -04:00
committed by GitHub
4 changed files with 97 additions and 4 deletions

View File

@@ -24,6 +24,7 @@ import logging
import os import os
import random import random
import re import re
import humanize
from typing import Optional, Union from typing import Optional, Union
from collections import OrderedDict from collections import OrderedDict
@@ -165,6 +166,7 @@ class ConsoleApp:
'local-services': None, 'local-services': None,
'remote-services': None, 'remote-services': None,
'local-values': None, 'local-values': None,
'remote-values': None,
}, },
'filter': { 'filter': {
'address': None, 'address': None,
@@ -212,6 +214,7 @@ class ConsoleApp:
get_cursor_position=lambda: Point(0, max(0, len(self.log_lines) - 1)) get_cursor_position=lambda: Point(0, max(0, len(self.log_lines) - 1))
) )
self.local_values_text = FormattedTextControl() self.local_values_text = FormattedTextControl()
self.remote_values_text = FormattedTextControl()
self.log_height = Dimension(min=7, weight=4) self.log_height = Dimension(min=7, weight=4)
self.log_max_lines = 100 self.log_max_lines = 100
self.log_lines = [] self.log_lines = []
@@ -234,6 +237,10 @@ class ConsoleApp:
Frame(Window(self.remote_services_text), title='Remote Services'), Frame(Window(self.remote_services_text), title='Remote Services'),
filter=Condition(lambda: self.top_tab == 'remote-services'), filter=Condition(lambda: self.top_tab == 'remote-services'),
), ),
ConditionalContainer(
Frame(Window(self.remote_values_text), title='Remote Values'),
filter=Condition(lambda: self.top_tab == 'remote-values'),
),
ConditionalContainer( ConditionalContainer(
Frame(Window(self.log_text, height=self.log_height), title='Log'), Frame(Window(self.log_text, height=self.log_height), title='Log'),
filter=Condition(lambda: self.top_tab == 'log'), filter=Condition(lambda: self.top_tab == 'log'),
@@ -737,6 +744,7 @@ class ConsoleApp:
'local-services', 'local-services',
'remote-services', 'remote-services',
'local-values', 'local-values',
'remote-values',
}: }:
self.top_tab = params[0] self.top_tab = params[0]
self.ui.invalidate() self.ui.invalidate()
@@ -745,6 +753,10 @@ class ConsoleApp:
await self.do_show_local_values() await self.do_show_local_values()
await asyncio.sleep(1) await asyncio.sleep(1)
while self.top_tab == 'remote-values':
await self.do_show_remote_values()
await asyncio.sleep(1)
async def do_show_local_values(self): async def do_show_local_values(self):
prettytable = PrettyTable() prettytable = PrettyTable()
field_names = ["Service", "Characteristic", "Descriptor"] field_names = ["Service", "Characteristic", "Descriptor"]
@@ -800,6 +812,40 @@ class ConsoleApp:
self.local_values_text.text = prettytable.get_string() self.local_values_text.text = prettytable.get_string()
self.ui.invalidate() self.ui.invalidate()
async def do_show_remote_values(self):
prettytable = PrettyTable(
field_names=[
"Connection",
"Service",
"Characteristic",
"Descriptor",
"Time",
"Value",
]
)
for connection in self.device.connections.values():
for handle, (time, value) in connection.gatt_client.cached_values.items():
row = [connection.handle]
attribute = connection.gatt_client.get_attributes(handle)
if not attribute:
continue
if len(attribute) == 3:
row.extend(
[attribute[0].uuid, attribute[1].uuid, attribute[2].type]
)
elif len(attribute) == 2:
row.extend([attribute[0].uuid, attribute[1].uuid, ""])
elif len(attribute) == 1:
row.extend([attribute[0].uuid, "", ""])
else:
continue
row.extend([humanize.naturaltime(time), value])
prettytable.add_row(row)
self.remote_values_text.text = prettytable.get_string()
self.ui.invalidate()
async def do_get_phy(self, _): async def do_get_phy(self, _):
if not self.connected_peer: if not self.connected_peer:
self.show_error('not connected') self.show_error('not connected')
@@ -899,9 +945,9 @@ class ConsoleApp:
# send data to any subscribers # send data to any subscribers
if isinstance(attribute, Characteristic): if isinstance(attribute, Characteristic):
attribute.write_value(None, value) attribute.write_value(None, value)
if attribute.has_properties([Characteristic.NOTIFY]): if attribute.has_properties(Characteristic.NOTIFY):
await self.device.gatt_server.notify_subscribers(attribute) await self.device.gatt_server.notify_subscribers(attribute)
if attribute.has_properties([Characteristic.INDICATE]): if attribute.has_properties(Characteristic.INDICATE):
await self.device.gatt_server.indicate_subscribers(attribute) await self.device.gatt_server.indicate_subscribers(attribute)
async def do_subscribe(self, params): async def do_subscribe(self, params):

View File

@@ -529,6 +529,7 @@ class Connection(CompositeEventEmitter):
authenticated: bool authenticated: bool
sc: bool sc: bool
link_key_type: int link_key_type: int
gatt_client: gatt_client.Client
@composite_listener @composite_listener
class Listener: class Listener:

View File

@@ -27,7 +27,8 @@ from __future__ import annotations
import asyncio import asyncio
import logging import logging
import struct import struct
from typing import List, Optional, Dict, Any, Callable from datetime import datetime
from typing import List, Optional, Dict, Tuple, Callable, Union, Any
from pyee import EventEmitter from pyee import EventEmitter
@@ -167,7 +168,9 @@ class CharacteristicProxy(AttributeProxy):
async def discover_descriptors(self): async def discover_descriptors(self):
return await self.client.discover_descriptors(self) return await self.client.discover_descriptors(self)
async def subscribe(self, subscriber=None, prefer_notify=True): async def subscribe(
self, subscriber: Optional[Callable] = None, prefer_notify=True
):
if subscriber is not None: if subscriber is not None:
if subscriber in self.subscribers: if subscriber in self.subscribers:
# We already have a proxy subscriber # We already have a proxy subscriber
@@ -221,6 +224,7 @@ class ProfileServiceProxy:
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
class Client: class Client:
services: List[ServiceProxy] services: List[ServiceProxy]
cached_values: Dict[int, Tuple[datetime, bytes]]
def __init__(self, connection): def __init__(self, connection):
self.connection = connection self.connection = connection
@@ -233,6 +237,7 @@ class Client:
) # Notification subscribers, by attribute handle ) # Notification subscribers, by attribute handle
self.indication_subscribers = {} # Indication subscribers, by attribute handle self.indication_subscribers = {} # Indication subscribers, by attribute handle
self.services = [] self.services = []
self.cached_values = {}
def send_gatt_pdu(self, pdu): def send_gatt_pdu(self, pdu):
self.connection.send_l2cap_pdu(ATT_CID, pdu) self.connection.send_l2cap_pdu(ATT_CID, pdu)
@@ -317,6 +322,35 @@ class Client:
if c.uuid == uuid if c.uuid == uuid
] ]
def get_attribute_grouping(
self, attribute_handle: int
) -> Optional[
Union[
ServiceProxy,
Tuple[ServiceProxy, CharacteristicProxy],
Tuple[ServiceProxy, CharacteristicProxy, DescriptorProxy],
]
]:
"""
Get the attribute(s) associated with an attribute handle
"""
for service in self.services:
if service.handle == attribute_handle:
return service
if service.handle <= attribute_handle <= service.end_group_handle:
for characteristic in service.characteristics:
if characteristic.handle == attribute_handle:
return (service, characteristic)
if (
characteristic.handle
<= attribute_handle
<= characteristic.end_group_handle
):
for descriptor in characteristic.descriptors:
if descriptor.handle == attribute_handle:
return (service, characteristic, descriptor)
return None
def on_service_discovered(self, service): def on_service_discovered(self, service):
'''Add a service to the service list if it wasn't already there''' '''Add a service to the service list if it wasn't already there'''
already_known = False already_known = False
@@ -808,6 +842,7 @@ class Client:
offset += len(part) offset += len(part)
self.cache_value(attribute_handle, attribute_value)
# Return the value as bytes # Return the value as bytes
return attribute_value return attribute_value
@@ -942,6 +977,8 @@ class Client:
) )
if not subscribers: if not subscribers:
logger.warning('!!! received notification with no subscriber') logger.warning('!!! received notification with no subscriber')
self.cache_value(notification.attribute_handle, notification.attribute_value)
for subscriber in subscribers: for subscriber in subscribers:
if callable(subscriber): if callable(subscriber):
subscriber(notification.attribute_value) subscriber(notification.attribute_value)
@@ -953,6 +990,8 @@ class Client:
subscribers = self.indication_subscribers.get(indication.attribute_handle, []) subscribers = self.indication_subscribers.get(indication.attribute_handle, [])
if not subscribers: if not subscribers:
logger.warning('!!! received indication with no subscriber') logger.warning('!!! received indication with no subscriber')
self.cache_value(indication.attribute_handle, indication.attribute_value)
for subscriber in subscribers: for subscriber in subscribers:
if callable(subscriber): if callable(subscriber):
subscriber(indication.attribute_value) subscriber(indication.attribute_value)
@@ -961,3 +1000,9 @@ class Client:
# Confirm that we received the indication # Confirm that we received the indication
self.send_confirmation(ATT_Handle_Value_Confirmation()) self.send_confirmation(ATT_Handle_Value_Confirmation())
def cache_value(self, attribute_handle: int, value: bytes):
self.cached_values[attribute_handle] = (
datetime.now(),
value,
)

View File

@@ -44,6 +44,7 @@ install_requires =
pyusb >= 1.2; platform_system!='Emscripten' pyusb >= 1.2; platform_system!='Emscripten'
websockets >= 8.1; platform_system!='Emscripten' websockets >= 8.1; platform_system!='Emscripten'
prettytable >= 3.6.0 prettytable >= 3.6.0
humanize >= 4.6.0
[options.entry_points] [options.entry_points]
console_scripts = console_scripts =