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 random
import re
import humanize
from typing import Optional, Union
from collections import OrderedDict
@@ -165,6 +166,7 @@ class ConsoleApp:
'local-services': None,
'remote-services': None,
'local-values': None,
'remote-values': None,
},
'filter': {
'address': None,
@@ -212,6 +214,7 @@ class ConsoleApp:
get_cursor_position=lambda: Point(0, max(0, len(self.log_lines) - 1))
)
self.local_values_text = FormattedTextControl()
self.remote_values_text = FormattedTextControl()
self.log_height = Dimension(min=7, weight=4)
self.log_max_lines = 100
self.log_lines = []
@@ -234,6 +237,10 @@ class ConsoleApp:
Frame(Window(self.remote_services_text), title='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(
Frame(Window(self.log_text, height=self.log_height), title='Log'),
filter=Condition(lambda: self.top_tab == 'log'),
@@ -737,6 +744,7 @@ class ConsoleApp:
'local-services',
'remote-services',
'local-values',
'remote-values',
}:
self.top_tab = params[0]
self.ui.invalidate()
@@ -745,6 +753,10 @@ class ConsoleApp:
await self.do_show_local_values()
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):
prettytable = PrettyTable()
field_names = ["Service", "Characteristic", "Descriptor"]
@@ -800,6 +812,40 @@ class ConsoleApp:
self.local_values_text.text = prettytable.get_string()
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, _):
if not self.connected_peer:
self.show_error('not connected')
@@ -899,9 +945,9 @@ class ConsoleApp:
# send data to any subscribers
if isinstance(attribute, Characteristic):
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)
if attribute.has_properties([Characteristic.INDICATE]):
if attribute.has_properties(Characteristic.INDICATE):
await self.device.gatt_server.indicate_subscribers(attribute)
async def do_subscribe(self, params):

View File

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

View File

@@ -27,7 +27,8 @@ from __future__ import annotations
import asyncio
import logging
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
@@ -167,7 +168,9 @@ class CharacteristicProxy(AttributeProxy):
async def 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 in self.subscribers:
# We already have a proxy subscriber
@@ -221,6 +224,7 @@ class ProfileServiceProxy:
# -----------------------------------------------------------------------------
class Client:
services: List[ServiceProxy]
cached_values: Dict[int, Tuple[datetime, bytes]]
def __init__(self, connection):
self.connection = connection
@@ -233,6 +237,7 @@ class Client:
) # Notification subscribers, by attribute handle
self.indication_subscribers = {} # Indication subscribers, by attribute handle
self.services = []
self.cached_values = {}
def send_gatt_pdu(self, pdu):
self.connection.send_l2cap_pdu(ATT_CID, pdu)
@@ -317,6 +322,35 @@ class Client:
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):
'''Add a service to the service list if it wasn't already there'''
already_known = False
@@ -808,6 +842,7 @@ class Client:
offset += len(part)
self.cache_value(attribute_handle, attribute_value)
# Return the value as bytes
return attribute_value
@@ -942,6 +977,8 @@ class Client:
)
if not subscribers:
logger.warning('!!! received notification with no subscriber')
self.cache_value(notification.attribute_handle, notification.attribute_value)
for subscriber in subscribers:
if callable(subscriber):
subscriber(notification.attribute_value)
@@ -953,6 +990,8 @@ class Client:
subscribers = self.indication_subscribers.get(indication.attribute_handle, [])
if not subscribers:
logger.warning('!!! received indication with no subscriber')
self.cache_value(indication.attribute_handle, indication.attribute_value)
for subscriber in subscribers:
if callable(subscriber):
subscriber(indication.attribute_value)
@@ -961,3 +1000,9 @@ class Client:
# Confirm that we received the indication
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'
websockets >= 8.1; platform_system!='Emscripten'
prettytable >= 3.6.0
humanize >= 4.6.0
[options.entry_points]
console_scripts =