Skip to content

Commit a9b4b3b

Browse files
andrewleechclaude
andcommitted
micropython/aioble: Add pairing and bonding multitests.
Adds comprehensive tests for BLE pairing and bonding functionality: - ble_pair.py: Tests encryption without persistent bonding (bond=False) - ble_bond.py: Tests encryption with persistent bonding (bond=True) Both tests verify: - Encrypted characteristic access requiring pairing - Proper connection state tracking (encrypted, authenticated, bonded) - Cross-compatibility with BTstack implementation - Bond storage via aioble.security module Tests use custom EncryptedCharacteristic class to add _FLAG_READ_ENCRYPTED requirement, ensuring pairing is mandatory for characteristic access. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> Signed-off-by: Andrew Leech <andrew.leech@planetinnovation.com.au>
1 parent 5b496e9 commit a9b4b3b

File tree

4 files changed

+340
-0
lines changed

4 files changed

+340
-0
lines changed
Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
# Test BLE GAP pairing with bonding (persistent pairing) using aioble
2+
3+
import sys
4+
5+
# ruff: noqa: E402
6+
sys.path.append("")
7+
8+
from micropython import const
9+
import machine
10+
import time
11+
import os
12+
13+
import asyncio
14+
import aioble
15+
import aioble.security
16+
import bluetooth
17+
18+
TIMEOUT_MS = 5000
19+
20+
SERVICE_UUID = bluetooth.UUID("A5A5A5A5-FFFF-9999-1111-5A5A5A5A5A5A")
21+
CHAR_UUID = bluetooth.UUID("00000000-1111-2222-3333-444444444444")
22+
23+
_FLAG_READ = const(0x0002)
24+
_FLAG_READ_ENCRYPTED = const(0x0200)
25+
26+
27+
# For aioble, we need to directly use the low-level bluetooth API for encrypted characteristics
28+
class EncryptedCharacteristic(aioble.Characteristic):
29+
def __init__(self, service, uuid, **kwargs):
30+
super().__init__(service, uuid, read=True, **kwargs)
31+
# Override flags to add encryption requirement
32+
self.flags |= _FLAG_READ_ENCRYPTED
33+
34+
35+
# Acting in peripheral role.
36+
async def instance0_task():
37+
# Clean up any existing secrets from previous tests
38+
try:
39+
os.remove("ble_secrets.json")
40+
except:
41+
pass
42+
43+
# Load secrets (will be empty initially but enables bond storage)
44+
aioble.security.load_secrets()
45+
46+
service = aioble.Service(SERVICE_UUID)
47+
characteristic = EncryptedCharacteristic(service, CHAR_UUID)
48+
aioble.register_services(service)
49+
50+
multitest.globals(BDADDR=aioble.config("mac"))
51+
multitest.next()
52+
53+
# Write initial characteristic value.
54+
characteristic.write("bonded_data")
55+
56+
# Wait for central to connect to us.
57+
print("advertise")
58+
connection = await aioble.advertise(
59+
20_000, adv_data=b"\x02\x01\x06\x04\xffMPY", timeout_ms=TIMEOUT_MS
60+
)
61+
print("connected")
62+
63+
# Wait for pairing to complete
64+
print("wait_for_bonding")
65+
start_time = time.ticks_ms()
66+
while not connection.encrypted and time.ticks_diff(time.ticks_ms(), start_time) < TIMEOUT_MS:
67+
await asyncio.sleep_ms(100)
68+
69+
# Give additional time for bonding to complete after encryption
70+
await asyncio.sleep_ms(500)
71+
72+
if connection.encrypted:
73+
print(
74+
"bonded encrypted=1 authenticated={} bonded={}".format(
75+
1 if connection.authenticated else 0, 1 if connection.bonded else 0
76+
)
77+
)
78+
else:
79+
print("bonding_timeout")
80+
81+
# Wait for the central to disconnect.
82+
await connection.disconnected(timeout_ms=TIMEOUT_MS)
83+
print("disconnected")
84+
85+
86+
def instance0():
87+
try:
88+
asyncio.run(instance0_task())
89+
finally:
90+
aioble.stop()
91+
92+
93+
# Acting in central role.
94+
async def instance1_task():
95+
multitest.next()
96+
97+
# Clean up any existing secrets from previous tests
98+
try:
99+
os.remove("ble_secrets.json")
100+
except:
101+
pass
102+
103+
# Load secrets (will be empty initially but enables bond storage)
104+
aioble.security.load_secrets()
105+
106+
# Connect to peripheral.
107+
print("connect")
108+
device = aioble.Device(*BDADDR)
109+
connection = await device.connect(timeout_ms=TIMEOUT_MS)
110+
111+
# Discover characteristics (before pairing).
112+
service = await connection.service(SERVICE_UUID)
113+
print("service", service.uuid)
114+
characteristic = await service.characteristic(CHAR_UUID)
115+
print("characteristic", characteristic.uuid)
116+
117+
# Pair with bonding enabled.
118+
print("bond")
119+
await connection.pair(
120+
bond=True, # Enable bonding
121+
le_secure=True,
122+
mitm=False,
123+
timeout_ms=TIMEOUT_MS,
124+
)
125+
126+
# Give additional time for bonding to complete after encryption
127+
await asyncio.sleep_ms(500)
128+
129+
print(
130+
"bonded encrypted={} authenticated={} bonded={}".format(
131+
1 if connection.encrypted else 0,
132+
1 if connection.authenticated else 0,
133+
1 if connection.bonded else 0,
134+
)
135+
)
136+
137+
# Read the peripheral's characteristic, should be encrypted.
138+
print("read_encrypted")
139+
data = await characteristic.read(timeout_ms=TIMEOUT_MS)
140+
print("read", data)
141+
142+
# Check if secrets were saved
143+
try:
144+
os.stat("ble_secrets.json")
145+
print("secrets_exist", "yes")
146+
except:
147+
print("secrets_exist", "no")
148+
149+
# Disconnect from peripheral.
150+
print("disconnect")
151+
await connection.disconnect(timeout_ms=TIMEOUT_MS)
152+
print("disconnected")
153+
154+
155+
def instance1():
156+
try:
157+
asyncio.run(instance1_task())
158+
finally:
159+
aioble.stop()
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
--- instance0 ---
2+
advertise
3+
connected
4+
wait_for_bonding
5+
bonded encrypted=1 authenticated=0 bonded=1
6+
disconnected
7+
--- instance1 ---
8+
connect
9+
service UUID('a5a5a5a5-ffff-9999-1111-5a5a5a5a5a5a')
10+
characteristic UUID('00000000-1111-2222-3333-444444444444')
11+
bond
12+
bonded encrypted=1 authenticated=0 bonded=1
13+
read_encrypted
14+
read b'bonded_data'
15+
secrets_exist yes
16+
disconnect
17+
disconnected
Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
# Test BLE GAP pairing and bonding with aioble
2+
3+
import sys
4+
5+
# ruff: noqa: E402
6+
sys.path.append("")
7+
8+
from micropython import const
9+
import machine
10+
import time
11+
12+
import asyncio
13+
import aioble
14+
import bluetooth
15+
16+
TIMEOUT_MS = 5000
17+
18+
SERVICE_UUID = bluetooth.UUID("A5A5A5A5-FFFF-9999-1111-5A5A5A5A5A5A")
19+
CHAR_UUID = bluetooth.UUID("00000000-1111-2222-3333-444444444444")
20+
21+
_FLAG_READ = const(0x0002)
22+
_FLAG_READ_ENCRYPTED = const(0x0200)
23+
24+
25+
# For aioble, we need to directly use the low-level bluetooth API for encrypted characteristics
26+
class EncryptedCharacteristic(aioble.Characteristic):
27+
def __init__(self, service, uuid, **kwargs):
28+
super().__init__(service, uuid, read=True, **kwargs)
29+
# Override flags to add encryption requirement
30+
self.flags |= _FLAG_READ_ENCRYPTED
31+
32+
33+
# Acting in peripheral role.
34+
async def instance0_task():
35+
# Clear any existing bond state
36+
import os
37+
38+
try:
39+
os.remove("ble_secrets.json")
40+
except:
41+
pass
42+
43+
service = aioble.Service(SERVICE_UUID)
44+
characteristic = EncryptedCharacteristic(service, CHAR_UUID)
45+
aioble.register_services(service)
46+
47+
multitest.globals(BDADDR=aioble.config("mac"))
48+
multitest.next()
49+
50+
# Write initial characteristic value.
51+
characteristic.write("encrypted_data")
52+
53+
# Wait for central to connect to us.
54+
print("advertise")
55+
connection = await aioble.advertise(
56+
20_000, adv_data=b"\x02\x01\x06\x04\xffMPY", timeout_ms=TIMEOUT_MS
57+
)
58+
print("connected")
59+
60+
# Wait for pairing to complete
61+
print("wait_for_pairing")
62+
start_time = time.ticks_ms()
63+
while not connection.encrypted and time.ticks_diff(time.ticks_ms(), start_time) < TIMEOUT_MS:
64+
await asyncio.sleep_ms(100)
65+
66+
# Give a small delay for bonding state to stabilize
67+
await asyncio.sleep_ms(200)
68+
69+
if connection.encrypted:
70+
print(
71+
"paired encrypted=1 authenticated={} bonded={}".format(
72+
1 if connection.authenticated else 0, 1 if connection.bonded else 0
73+
)
74+
)
75+
else:
76+
print("pairing_timeout")
77+
78+
# Wait for the central to disconnect.
79+
await connection.disconnected(timeout_ms=TIMEOUT_MS)
80+
print("disconnected")
81+
82+
83+
def instance0():
84+
try:
85+
asyncio.run(instance0_task())
86+
finally:
87+
aioble.stop()
88+
89+
90+
# Acting in central role.
91+
async def instance1_task():
92+
multitest.next()
93+
94+
# Clear any existing bond state
95+
import os
96+
97+
try:
98+
os.remove("ble_secrets.json")
99+
except:
100+
pass
101+
102+
# Connect to peripheral.
103+
print("connect")
104+
device = aioble.Device(*BDADDR)
105+
connection = await device.connect(timeout_ms=TIMEOUT_MS)
106+
107+
# Discover characteristics (before pairing).
108+
service = await connection.service(SERVICE_UUID)
109+
print("service", service.uuid)
110+
characteristic = await service.characteristic(CHAR_UUID)
111+
print("characteristic", characteristic.uuid)
112+
113+
# Pair with the peripheral.
114+
print("pair")
115+
await connection.pair(
116+
bond=False, # Don't bond for this test
117+
le_secure=True,
118+
mitm=False,
119+
timeout_ms=TIMEOUT_MS,
120+
)
121+
122+
# Give a small delay for bonding state to stabilize
123+
await asyncio.sleep_ms(200)
124+
125+
print(
126+
"paired encrypted={} authenticated={} bonded={}".format(
127+
1 if connection.encrypted else 0,
128+
1 if connection.authenticated else 0,
129+
1 if connection.bonded else 0,
130+
)
131+
)
132+
133+
# Read the peripheral's characteristic, should be encrypted.
134+
print("read_encrypted")
135+
data = await characteristic.read(timeout_ms=TIMEOUT_MS)
136+
print("read", data)
137+
138+
# Disconnect from peripheral.
139+
print("disconnect")
140+
await connection.disconnect(timeout_ms=TIMEOUT_MS)
141+
print("disconnected")
142+
143+
144+
def instance1():
145+
try:
146+
asyncio.run(instance1_task())
147+
finally:
148+
aioble.stop()
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
--- instance0 ---
2+
advertise
3+
connected
4+
wait_for_pairing
5+
paired encrypted=1 authenticated=0 bonded=0
6+
disconnected
7+
--- instance1 ---
8+
connect
9+
service UUID('a5a5a5a5-ffff-9999-1111-5a5a5a5a5a5a')
10+
characteristic UUID('00000000-1111-2222-3333-444444444444')
11+
pair
12+
paired encrypted=1 authenticated=0 bonded=0
13+
read_encrypted
14+
read b'encrypted_data'
15+
disconnect
16+
disconnected

0 commit comments

Comments
 (0)