Skip to content

aioble updates - generic attribute service/service changed #439

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions micropython/bluetooth/aioble/aioble/device.py
Original file line number Diff line number Diff line change
Expand Up @@ -262,7 +262,7 @@ def is_connected(self):
def timeout(self, timeout_ms):
return DeviceTimeout(self, timeout_ms)

async def exchange_mtu(self, mtu=None):
async def exchange_mtu(self, mtu=None, timeout_ms=1000):
if not self.is_connected():
raise ValueError("Not connected")

Expand All @@ -271,7 +271,8 @@ async def exchange_mtu(self, mtu=None):

self._mtu_event = self._mtu_event or asyncio.ThreadSafeFlag()
ble.gattc_exchange_mtu(self._conn_handle)
await self._mtu_event.wait()
with self.timeout(timeout_ms):
await self._mtu_event.wait()
return self.mtu

# Wait for a connection on an L2CAP connection-oriented-channel.
Expand Down
51 changes: 38 additions & 13 deletions micropython/bluetooth/aioble/aioble/security.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from micropython import const, schedule
import uasyncio as asyncio
import binascii
import ustruct
import json

from .core import log_info, log_warn, ble, register_irq_handler
Expand All @@ -26,10 +27,14 @@

_DEFAULT_PATH = "ble_secrets.json"

_secrets = {}
# Maintain list of known keys, newest at the top.
_secrets = []
_modified = False
_path = None

connected_sec = None
gatt_svc = None


# Must call this before stack startup.
def load_secrets(path=None):
Expand All @@ -40,13 +45,14 @@ def load_secrets(path=None):
_path = path or _path or _DEFAULT_PATH

# Reset old secrets.
_secrets = {}
_secrets = []
try:
with open(_path, "r") as f:
entries = json.load(f)
for sec_type, key, value in entries:
for sec_type, key, value, *digest in entries:
digest = digest[0] or None
# Decode bytes from hex.
_secrets[sec_type, binascii.a2b_base64(key)] = binascii.a2b_base64(value)
_secrets.append(((sec_type, binascii.a2b_base64(key)), binascii.a2b_base64(value), digest))
except:
log_warn("No secrets available")

Expand All @@ -65,15 +71,15 @@ def _save_secrets(arg=None):
# Convert bytes to hex strings (otherwise JSON will treat them like
# strings).
json_secrets = [
(sec_type, binascii.b2a_base64(key), binascii.b2a_base64(value))
for (sec_type, key), value in _secrets.items()
(sec_type, binascii.b2a_base64(key), binascii.b2a_base64(value), digest)
for (sec_type, key), value, digest in _secrets
]
json.dump(json_secrets, f)
_modified = False


def _security_irq(event, data):
global _modified
global _modified, connected_sec, gatt_svc

if event == _IRQ_ENCRYPTION_UPDATE:
# Connection has updated (usually due to pairing).
Expand All @@ -88,6 +94,19 @@ def _security_irq(event, data):
if encrypted and connection._pair_event:
connection._pair_event.set()

if bonded and \
None not in (gatt_svc, connected_sec) and \
connected_sec[2] != gatt_svc.hexdigest:
gatt_svc.send_changed(connection)

# Update the hash in the database
_secrets.remove(connected_sec)
updated_sec = connected_sec[:-1] + (gatt_svc.hexdigest,)
_secrets.insert(0, updated_sec)
# Queue up a save (don't synchronously write to flash).
_modified = True
schedule(_save_secrets, None)

elif event == _IRQ_SET_SECRET:
sec_type, key, value = data
key = sec_type, bytes(key)
Expand All @@ -97,13 +116,15 @@ def _security_irq(event, data):

if value is None:
# Delete secret.
if key not in _secrets:
return False
for to_delete in [
entry for entry in _secrets if entry[0] == key
]:
_secrets.remove(to_delete)

del _secrets[key]
else:
# Save secret.
_secrets[key] = value
current_digest = gatt_svc.hexdigest if gatt_svc else None
_secrets.insert(0, (key, value, current_digest))

# Queue up a save (don't synchronously write to flash).
_modified = True
Expand All @@ -119,7 +140,7 @@ def _security_irq(event, data):
if key is None:
# Return the index'th secret of this type.
i = 0
for (t, _key), value in _secrets.items():
for (t, _key), value, digest in _secrets:
if t == sec_type:
if i == index:
return value
Expand All @@ -128,7 +149,11 @@ def _security_irq(event, data):
else:
# Return the secret for this key (or None).
key = sec_type, bytes(key)
return _secrets.get(key, None)

for k, v, d in _secrets:
if k == key:
return v
return None

elif event == _IRQ_PASSKEY_ACTION:
conn_handle, action, passkey = data
Expand Down
23 changes: 16 additions & 7 deletions micropython/bluetooth/aioble/aioble/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,11 +83,15 @@ def read(self):
return ble.gatts_read(self._value_handle)

# Write value to local db.
def write(self, data):
def write(self, data, send_update=False):
if self._value_handle is None:
self._initial = data
else:
ble.gatts_write(self._value_handle, data)
if send_update:
# Send_update arg only added in 1.17, don't pass this arg unless required.
ble.gatts_write(self._value_handle, data, True)
else:
ble.gatts_write(self._value_handle, data)

# Wait for a write on this characteristic.
# Returns the device that did the write.
Expand Down Expand Up @@ -186,10 +190,11 @@ def _indicate_done(conn_handle, value_handle, status):
# Timeout.
return
# See TODO in __init__ to support multiple concurrent indications.
assert connection == characteristic._indicate_connection
characteristic._indicate_status = status
characteristic._indicate_event.set()

if connection == characteristic._indicate_connection:
characteristic._indicate_status = status
characteristic._indicate_event.set()
else:
log_warn("Received indication for unexpected connection")

class BufferedCharacteristic(Characteristic):
def __init__(self, service, uuid, max_len=20, append=False):
Expand Down Expand Up @@ -223,9 +228,13 @@ def __init__(self, characteristic, uuid, read=False, write=False, initial=None):

# Turn the Service/Characteristic/Descriptor classes into a registration tuple
# and then extract their value handles.
def register_services(*services):
def register_services(*services, include_gatt_svc=True):
ensure_active()
_registered_characteristics.clear()
if include_gatt_svc:
from .services.generic_attribute_service import GenericAttributeService
gatt_svc = GenericAttributeService(services)
services = (gatt_svc,) + services
handles = ble.gatts_register_services(tuple(s._tuple() for s in services))
for i in range(len(services)):
service_handles = handles[i]
Expand Down
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
# -*- coding: utf-8 -*-
#
# @file
# @brief: Bluetooth Generic Attribute Service
#
# Copyright (c) 2021, Planet Innovation
# 436 Elgar Road, Box Hill, 3128, VIC, Australia
# Phone: +61 3 9945 7510
#
# The copyright to the computer program(s) herein is the property of
# Planet Innovation, Australia.
# The program(s) may be used and/or copied only with the written permission
# of Planet Innovation or in accordance with the terms and conditions
# stipulated in the agreement/contract under which the program(s) have been
# supplied.
#

import ustruct
import bluetooth
from aioble import Service, Characteristic, security
from aioble.core import ble, log_info
from hashlib import md5
from ubinascii import hexlify
try:
from utyping import *
except:
pass


class GenericAttributeService(Service):
# Generic Attribute service UUID
SERVICE_UUID = bluetooth.UUID(0x1801)

# Service Changed Characteristic
UUID_SERVICE_CHANGED = bluetooth.UUID(0x2A05)
# Database Hash Characteristic (New in BLE 5.1)
UUID_DATABASE_HASH = bluetooth.UUID(0x2B2A)

def __init__(self, services: Tuple[Service]):

super().__init__(self.SERVICE_UUID)

# Database hash is typically a 128bit AES-CMAC value, however
# is generally only monitored for change as an opaque value.
# MD5 is also 128 bit, faster and builtin
hasher = md5()
for service in services:
for char in service.characteristics:
hasher.update(char.uuid)
hasher.update(str(char.flags))
self.digest = hasher.digest()
self.hexdigest = hexlify(self.digest).decode()
log_info("BLE: DB Hash=", self.hexdigest)
security.current_digest = self.hexdigest
security.gatt_svc = self

self.SERVICE_CHANGED = Characteristic(
service=self,
uuid=self.UUID_SERVICE_CHANGED,
read=True,
indicate=True,
initial=''
)

self.DATABASE_HASH = Characteristic(
service=self,
uuid=self.UUID_DATABASE_HASH,
read=True,
initial=self.digest
)

def send_changed(self, connection, start=0, end=0xFFFF):
self.SERVICE_CHANGED.write(ustruct.pack('!HH', start, end))
log_info("Indicate Service Changed")
ble.gatts_indicate(connection._conn_handle, self.SERVICE_CHANGED._value_handle)