forked from auracaster/bumble_mirror
Merge pull request #169 from AlanRosenthal/alan/remote-values
Add `show remote-values`
This commit is contained in:
@@ -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):
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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,
|
||||||
|
)
|
||||||
|
|||||||
@@ -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 =
|
||||||
|
|||||||
Reference in New Issue
Block a user