forked from auracaster/bumble_mirror
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 368e7eff05 | |||
| 55b813bbf5 | |||
| 14dfc1a501 | |||
| 938282e961 |
@@ -30,7 +30,7 @@ jobs:
|
|||||||
- name: Build package
|
- name: Build package
|
||||||
run: python -m build
|
run: python -m build
|
||||||
- name: Publish package to PyPI
|
- name: Publish package to PyPI
|
||||||
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags')
|
if: github.event_name == 'release' && startsWith(github.ref, 'refs/tags')
|
||||||
uses: pypa/gh-action-pypi-publish@release/v1
|
uses: pypa/gh-action-pypi-publish@release/v1
|
||||||
with:
|
with:
|
||||||
user: __token__
|
user: __token__
|
||||||
|
|||||||
+47
-15
@@ -122,6 +122,8 @@ class ConsoleApp:
|
|||||||
},
|
},
|
||||||
'read': LiveCompleter(self.known_attributes),
|
'read': LiveCompleter(self.known_attributes),
|
||||||
'write': LiveCompleter(self.known_attributes),
|
'write': LiveCompleter(self.known_attributes),
|
||||||
|
'subscribe': LiveCompleter(self.known_attributes),
|
||||||
|
'unsubscribe': LiveCompleter(self.known_attributes),
|
||||||
'quit': None,
|
'quit': None,
|
||||||
'exit': None
|
'exit': None
|
||||||
})
|
})
|
||||||
@@ -331,7 +333,7 @@ class ConsoleApp:
|
|||||||
|
|
||||||
await self.show_attributes(attributes)
|
await self.show_attributes(attributes)
|
||||||
|
|
||||||
def find_attribute(self, param):
|
def find_characteristic(self, param):
|
||||||
parts = param.split('.')
|
parts = param.split('.')
|
||||||
if len(parts) == 2:
|
if len(parts) == 2:
|
||||||
service_uuid = UUID(parts[0]) if parts[0] != '*' else None
|
service_uuid = UUID(parts[0]) if parts[0] != '*' else None
|
||||||
@@ -344,7 +346,10 @@ class ConsoleApp:
|
|||||||
elif len(parts) == 1:
|
elif len(parts) == 1:
|
||||||
if parts[0].startswith('#'):
|
if parts[0].startswith('#'):
|
||||||
attribute_handle = int(f'{parts[0][1:]}', 16)
|
attribute_handle = int(f'{parts[0][1:]}', 16)
|
||||||
return attribute_handle
|
for service in self.connected_peer.services:
|
||||||
|
for characteristic in service.characteristics:
|
||||||
|
if characteristic.handle == attribute_handle:
|
||||||
|
return characteristic
|
||||||
|
|
||||||
async def command(self, command):
|
async def command(self, command):
|
||||||
try:
|
try:
|
||||||
@@ -457,13 +462,13 @@ class ConsoleApp:
|
|||||||
self.show_error('invalid syntax', 'expected read <attribute>')
|
self.show_error('invalid syntax', 'expected read <attribute>')
|
||||||
return
|
return
|
||||||
|
|
||||||
attribute = self.find_attribute(params[0])
|
characteristic = self.find_characteristic(params[0])
|
||||||
if attribute is None:
|
if characteristic is None:
|
||||||
self.show_error('no such characteristic')
|
self.show_error('no such characteristic')
|
||||||
return
|
return
|
||||||
|
|
||||||
value = await self.connected_peer.read_value(attribute)
|
value = await characteristic.read_value()
|
||||||
self.append_to_output(f'VALUE: {value}')
|
self.append_to_output(f'VALUE: 0x{value.hex()}')
|
||||||
|
|
||||||
async def do_write(self, params):
|
async def do_write(self, params):
|
||||||
if not self.connected_peer:
|
if not self.connected_peer:
|
||||||
@@ -482,21 +487,48 @@ class ConsoleApp:
|
|||||||
except ValueError:
|
except ValueError:
|
||||||
value = str.encode(params[1]) # must be a string
|
value = str.encode(params[1]) # must be a string
|
||||||
|
|
||||||
attribute = self.find_attribute(params[0])
|
characteristic = self.find_characteristic(params[0])
|
||||||
if attribute is None:
|
if characteristic is None:
|
||||||
self.show_error('no such characteristic')
|
self.show_error('no such characteristic')
|
||||||
return
|
return
|
||||||
|
|
||||||
# use write with response if supported
|
# use write with response if supported
|
||||||
with_response = (
|
with_response = characteristic.properties & Characteristic.WRITE
|
||||||
(attribute.properties & Characteristic.WRITE)
|
await characteristic.write_value(value, with_response=with_response)
|
||||||
if hasattr(attribute, "properties")
|
|
||||||
else False
|
async def do_subscribe(self, params):
|
||||||
|
if not self.connected_peer:
|
||||||
|
self.show_error('not connected')
|
||||||
|
return
|
||||||
|
|
||||||
|
if len(params) != 1:
|
||||||
|
self.show_error('invalid syntax', 'expected subscribe <attribute>')
|
||||||
|
return
|
||||||
|
|
||||||
|
characteristic = self.find_characteristic(params[0])
|
||||||
|
if characteristic is None:
|
||||||
|
self.show_error('no such characteristic')
|
||||||
|
return
|
||||||
|
|
||||||
|
await characteristic.subscribe(
|
||||||
|
lambda value: self.append_to_output(f"{characteristic} VALUE: 0x{value.hex()}"),
|
||||||
)
|
)
|
||||||
|
|
||||||
await self.connected_peer.write_value(
|
async def do_unsubscribe(self, params):
|
||||||
attribute, value, with_response=with_response
|
if not self.connected_peer:
|
||||||
)
|
self.show_error('not connected')
|
||||||
|
return
|
||||||
|
|
||||||
|
if len(params) != 1:
|
||||||
|
self.show_error('invalid syntax', 'expected subscribe <attribute>')
|
||||||
|
return
|
||||||
|
|
||||||
|
characteristic = self.find_characteristic(params[0])
|
||||||
|
if characteristic is None:
|
||||||
|
self.show_error('no such characteristic')
|
||||||
|
return
|
||||||
|
|
||||||
|
await characteristic.unsubscribe()
|
||||||
|
|
||||||
async def do_exit(self, params):
|
async def do_exit(self, params):
|
||||||
self.ui.exit()
|
self.ui.exit()
|
||||||
|
|||||||
@@ -123,6 +123,9 @@ class Peer:
|
|||||||
async def subscribe(self, characteristic, subscriber=None):
|
async def subscribe(self, characteristic, subscriber=None):
|
||||||
return await self.gatt_client.subscribe(characteristic, subscriber)
|
return await self.gatt_client.subscribe(characteristic, subscriber)
|
||||||
|
|
||||||
|
async def unsubscribe(self, characteristic, subscriber=None):
|
||||||
|
return await self.gatt_client.unsubscribe(characteristic, subscriber)
|
||||||
|
|
||||||
async def read_value(self, attribute):
|
async def read_value(self, attribute):
|
||||||
return await self.gatt_client.read_value(attribute)
|
return await self.gatt_client.read_value(attribute)
|
||||||
|
|
||||||
|
|||||||
+38
-3
@@ -110,6 +110,9 @@ class CharacteristicProxy(AttributeProxy):
|
|||||||
async def subscribe(self, subscriber=None):
|
async def subscribe(self, subscriber=None):
|
||||||
return await self.client.subscribe(self, subscriber)
|
return await self.client.subscribe(self, subscriber)
|
||||||
|
|
||||||
|
async def unsubscribe(self, subscriber=None):
|
||||||
|
return await self.client.unsubscribe(self, subscriber)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f'Characteristic(handle=0x{self.handle:04X}, uuid={self.uuid}, properties={Characteristic.properties_as_string(self.properties)})'
|
return f'Characteristic(handle=0x{self.handle:04X}, uuid={self.uuid}, properties={Characteristic.properties_as_string(self.properties)})'
|
||||||
|
|
||||||
@@ -544,10 +547,36 @@ class Client:
|
|||||||
for subscriber_set in subscriber_sets:
|
for subscriber_set in subscriber_sets:
|
||||||
if subscriber is not None:
|
if subscriber is not None:
|
||||||
subscriber_set.add(subscriber)
|
subscriber_set.add(subscriber)
|
||||||
subscriber_set.add(lambda value: characteristic.emit('update', self.connection, value))
|
# Add the characteristic as a subscriber, which will result in the characteristic
|
||||||
|
# emitting an 'update' event when a notification or indication is received
|
||||||
|
subscriber_set.add(characteristic)
|
||||||
|
|
||||||
await self.write_value(cccd, struct.pack('<H', bits), with_response=True)
|
await self.write_value(cccd, struct.pack('<H', bits), with_response=True)
|
||||||
|
|
||||||
|
async def unsubscribe(self, characteristic, subscriber=None):
|
||||||
|
# If we haven't already discovered the descriptors for this characteristic, do it now
|
||||||
|
if not characteristic.descriptors_discovered:
|
||||||
|
await self.discover_descriptors(characteristic)
|
||||||
|
|
||||||
|
# Look for the CCCD descriptor
|
||||||
|
cccd = characteristic.get_descriptor(GATT_CLIENT_CHARACTERISTIC_CONFIGURATION_DESCRIPTOR)
|
||||||
|
if not cccd:
|
||||||
|
logger.warning('unsubscribing from characteristic with no CCCD descriptor')
|
||||||
|
return
|
||||||
|
|
||||||
|
if subscriber is not None:
|
||||||
|
# Remove matching subscriber from subscriber sets
|
||||||
|
for subscriber_set in (self.notification_subscribers, self.indication_subscribers):
|
||||||
|
subscribers = subscriber_set.get(characteristic.handle, [])
|
||||||
|
if subscriber in subscribers:
|
||||||
|
subscribers.remove(subscriber)
|
||||||
|
else:
|
||||||
|
# Remove all subscribers for this attribute from the sets!
|
||||||
|
self.notification_subscribers.pop(characteristic.handle, None)
|
||||||
|
self.indication_subscribers.pop(characteristic.handle, None)
|
||||||
|
|
||||||
|
await self.write_value(cccd, b'\x00\x00', with_response=True)
|
||||||
|
|
||||||
async def read_value(self, attribute, no_long_read=False):
|
async def read_value(self, attribute, no_long_read=False):
|
||||||
'''
|
'''
|
||||||
See Vol 3, Part G - 4.8.1 Read Characteristic Value
|
See Vol 3, Part G - 4.8.1 Read Characteristic Value
|
||||||
@@ -714,7 +743,10 @@ class Client:
|
|||||||
if not subscribers:
|
if not subscribers:
|
||||||
logger.warning('!!! received notification with no subscriber')
|
logger.warning('!!! received notification with no subscriber')
|
||||||
for subscriber in subscribers:
|
for subscriber in subscribers:
|
||||||
subscriber(notification.attribute_value)
|
if callable(subscriber):
|
||||||
|
subscriber(notification.attribute_value)
|
||||||
|
else:
|
||||||
|
subscriber.emit('update', notification.attribute_value)
|
||||||
|
|
||||||
def on_att_handle_value_indication(self, indication):
|
def on_att_handle_value_indication(self, indication):
|
||||||
# Call all subscribers
|
# Call all subscribers
|
||||||
@@ -722,7 +754,10 @@ class Client:
|
|||||||
if not subscribers:
|
if not subscribers:
|
||||||
logger.warning('!!! received indication with no subscriber')
|
logger.warning('!!! received indication with no subscriber')
|
||||||
for subscriber in subscribers:
|
for subscriber in subscribers:
|
||||||
subscriber(indication.attribute_value)
|
if callable(subscriber):
|
||||||
|
subscriber(indication.attribute_value)
|
||||||
|
else:
|
||||||
|
subscriber.emit('update', indication.attribute_value)
|
||||||
|
|
||||||
# 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())
|
||||||
|
|||||||
+48
-17
@@ -419,10 +419,12 @@ async def test_subscribe_notify():
|
|||||||
assert(len(c) == 1)
|
assert(len(c) == 1)
|
||||||
c3 = c[0]
|
c3 = c[0]
|
||||||
|
|
||||||
|
c1._called = False
|
||||||
c1._last_update = None
|
c1._last_update = None
|
||||||
|
|
||||||
def on_c1_update(connection, value):
|
def on_c1_update(value):
|
||||||
c1._last_update = (connection, value)
|
c1._called = True
|
||||||
|
c1._last_update = value
|
||||||
|
|
||||||
c1.on('update', on_c1_update)
|
c1.on('update', on_c1_update)
|
||||||
await peer.subscribe(c1)
|
await peer.subscribe(c1)
|
||||||
@@ -434,44 +436,73 @@ async def test_subscribe_notify():
|
|||||||
assert(not characteristic1._last_subscription[2])
|
assert(not characteristic1._last_subscription[2])
|
||||||
await server.indicate_subscribers(characteristic1)
|
await server.indicate_subscribers(characteristic1)
|
||||||
await async_barrier()
|
await async_barrier()
|
||||||
assert(c1._last_update is None)
|
assert(not c1._called)
|
||||||
await server.notify_subscribers(characteristic1)
|
await server.notify_subscribers(characteristic1)
|
||||||
await async_barrier()
|
await async_barrier()
|
||||||
assert(c1._last_update is not None)
|
assert(c1._called)
|
||||||
assert(c1._last_update[1] == characteristic1.value)
|
assert(c1._last_update == characteristic1.value)
|
||||||
|
|
||||||
|
c1._called = False
|
||||||
|
await peer.unsubscribe(c1)
|
||||||
|
await server.notify_subscribers(characteristic1)
|
||||||
|
assert(not c1._called)
|
||||||
|
|
||||||
|
c2._called = False
|
||||||
c2._last_update = None
|
c2._last_update = None
|
||||||
|
|
||||||
def on_c2_update(value):
|
def on_c2_update(value):
|
||||||
c2._last_update = (connection, value)
|
c2._called = True
|
||||||
|
c2._last_update = value
|
||||||
|
|
||||||
await peer.subscribe(c2, on_c2_update)
|
await peer.subscribe(c2, on_c2_update)
|
||||||
await async_barrier()
|
await async_barrier()
|
||||||
await server.notify_subscriber(characteristic2._last_subscription[0], characteristic2)
|
await server.notify_subscriber(characteristic2._last_subscription[0], characteristic2)
|
||||||
await async_barrier()
|
await async_barrier()
|
||||||
assert(c2._last_update is None)
|
assert(not c2._called)
|
||||||
await server.indicate_subscriber(characteristic2._last_subscription[0], characteristic2)
|
await server.indicate_subscriber(characteristic2._last_subscription[0], characteristic2)
|
||||||
await async_barrier()
|
await async_barrier()
|
||||||
assert(c2._last_update is not None)
|
assert(c2._called)
|
||||||
assert(c2._last_update[1] == characteristic2.value)
|
assert(c2._last_update == characteristic2.value)
|
||||||
|
|
||||||
c3._last_update = None
|
c2._called = False
|
||||||
|
await peer.unsubscribe(c2, on_c2_update)
|
||||||
|
await server.indicate_subscriber(characteristic2._last_subscription[0], characteristic2)
|
||||||
|
await async_barrier()
|
||||||
|
assert(not c2._called)
|
||||||
|
|
||||||
def on_c3_update(connection, value):
|
def on_c3_update(value):
|
||||||
c3._last_update = (connection, value)
|
c3._called = True
|
||||||
|
c3._last_update = value
|
||||||
|
|
||||||
|
def on_c3_update_2(value):
|
||||||
|
c3._called_2 = True
|
||||||
|
c3._last_update_2 = value
|
||||||
|
|
||||||
c3.on('update', on_c3_update)
|
c3.on('update', on_c3_update)
|
||||||
await peer.subscribe(c3)
|
await peer.subscribe(c3, on_c3_update_2)
|
||||||
await async_barrier()
|
await async_barrier()
|
||||||
await server.notify_subscriber(characteristic3._last_subscription[0], characteristic3)
|
await server.notify_subscriber(characteristic3._last_subscription[0], characteristic3)
|
||||||
await async_barrier()
|
await async_barrier()
|
||||||
assert(c3._last_update is not None)
|
assert(c3._called)
|
||||||
assert(c3._last_update[1] == characteristic3.value)
|
assert(c3._last_update == characteristic3.value)
|
||||||
|
assert(c3._called_2)
|
||||||
|
assert(c3._last_update_2 == characteristic3.value)
|
||||||
characteristic3.value = bytes([1, 2, 3])
|
characteristic3.value = bytes([1, 2, 3])
|
||||||
await server.indicate_subscriber(characteristic3._last_subscription[0], characteristic3)
|
await server.indicate_subscriber(characteristic3._last_subscription[0], characteristic3)
|
||||||
await async_barrier()
|
await async_barrier()
|
||||||
assert(c3._last_update is not None)
|
assert(c3._called)
|
||||||
assert(c3._last_update[1] == characteristic3.value)
|
assert(c3._last_update == characteristic3.value)
|
||||||
|
assert(c3._called_2)
|
||||||
|
assert(c3._last_update_2 == characteristic3.value)
|
||||||
|
|
||||||
|
c3._called = False
|
||||||
|
c3._called_2 = False
|
||||||
|
await peer.unsubscribe(c3)
|
||||||
|
await server.notify_subscriber(characteristic3._last_subscription[0], characteristic3)
|
||||||
|
await server.indicate_subscriber(characteristic3._last_subscription[0], characteristic3)
|
||||||
|
await async_barrier()
|
||||||
|
assert(not c3._called)
|
||||||
|
assert(not c3._called_2)
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
|
|||||||
Reference in New Issue
Block a user