Skip to content

Commit 2cfb06c

Browse files
author
Alasdair Allan
committed
added bluetooth example code
1 parent 35f9a54 commit 2cfb06c

File tree

3 files changed

+470
-0
lines changed

3 files changed

+470
-0
lines changed

bluetooth/ble_advertising.py

+93
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
# Helpers for generating BLE advertising payloads.
2+
3+
from micropython import const
4+
import struct
5+
import bluetooth
6+
7+
# Advertising payloads are repeated packets of the following form:
8+
# 1 byte data length (N + 1)
9+
# 1 byte type (see constants below)
10+
# N bytes type-specific data
11+
12+
_ADV_TYPE_FLAGS = const(0x01)
13+
_ADV_TYPE_NAME = const(0x09)
14+
_ADV_TYPE_UUID16_COMPLETE = const(0x3)
15+
_ADV_TYPE_UUID32_COMPLETE = const(0x5)
16+
_ADV_TYPE_UUID128_COMPLETE = const(0x7)
17+
_ADV_TYPE_UUID16_MORE = const(0x2)
18+
_ADV_TYPE_UUID32_MORE = const(0x4)
19+
_ADV_TYPE_UUID128_MORE = const(0x6)
20+
_ADV_TYPE_APPEARANCE = const(0x19)
21+
22+
23+
# Generate a payload to be passed to gap_advertise(adv_data=...).
24+
def advertising_payload(limited_disc=False, br_edr=False, name=None, services=None, appearance=0):
25+
payload = bytearray()
26+
27+
def _append(adv_type, value):
28+
nonlocal payload
29+
payload += struct.pack("BB", len(value) + 1, adv_type) + value
30+
31+
_append(
32+
_ADV_TYPE_FLAGS,
33+
struct.pack("B", (0x01 if limited_disc else 0x02) + (0x18 if br_edr else 0x04)),
34+
)
35+
36+
if name:
37+
_append(_ADV_TYPE_NAME, name)
38+
39+
if services:
40+
for uuid in services:
41+
b = bytes(uuid)
42+
if len(b) == 2:
43+
_append(_ADV_TYPE_UUID16_COMPLETE, b)
44+
elif len(b) == 4:
45+
_append(_ADV_TYPE_UUID32_COMPLETE, b)
46+
elif len(b) == 16:
47+
_append(_ADV_TYPE_UUID128_COMPLETE, b)
48+
49+
# See org.bluetooth.characteristic.gap.appearance.xml
50+
if appearance:
51+
_append(_ADV_TYPE_APPEARANCE, struct.pack("<h", appearance))
52+
53+
return payload
54+
55+
56+
def decode_field(payload, adv_type):
57+
i = 0
58+
result = []
59+
while i + 1 < len(payload):
60+
if payload[i + 1] == adv_type:
61+
result.append(payload[i + 2 : i + payload[i] + 1])
62+
i += 1 + payload[i]
63+
return result
64+
65+
66+
def decode_name(payload):
67+
n = decode_field(payload, _ADV_TYPE_NAME)
68+
return str(n[0], "utf-8") if n else ""
69+
70+
71+
def decode_services(payload):
72+
services = []
73+
for u in decode_field(payload, _ADV_TYPE_UUID16_COMPLETE):
74+
services.append(bluetooth.UUID(struct.unpack("<h", u)[0]))
75+
for u in decode_field(payload, _ADV_TYPE_UUID32_COMPLETE):
76+
services.append(bluetooth.UUID(struct.unpack("<d", u)[0]))
77+
for u in decode_field(payload, _ADV_TYPE_UUID128_COMPLETE):
78+
services.append(bluetooth.UUID(u))
79+
return services
80+
81+
82+
def demo():
83+
payload = advertising_payload(
84+
name="micropython",
85+
services=[bluetooth.UUID(0x181A), bluetooth.UUID("6E400001-B5A3-F393-E0A9-E50E24DCCA9E")],
86+
)
87+
print(payload)
88+
print(decode_name(payload))
89+
print(decode_services(payload))
90+
91+
92+
if __name__ == "__main__":
93+
demo()

bluetooth/picow_ble_temp_reader.py

+270
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,270 @@
1+
# This example finds and connects to a BLE temperature sensor (e.g. the one in ble_temperature.py).
2+
3+
import bluetooth
4+
import random
5+
import struct
6+
import time
7+
import micropython
8+
from ble_advertising import decode_services, decode_name
9+
from micropython import const
10+
from machine import Pin
11+
12+
_IRQ_CENTRAL_CONNECT = const(1)
13+
_IRQ_CENTRAL_DISCONNECT = const(2)
14+
_IRQ_GATTS_WRITE = const(3)
15+
_IRQ_GATTS_READ_REQUEST = const(4)
16+
_IRQ_SCAN_RESULT = const(5)
17+
_IRQ_SCAN_DONE = const(6)
18+
_IRQ_PERIPHERAL_CONNECT = const(7)
19+
_IRQ_PERIPHERAL_DISCONNECT = const(8)
20+
_IRQ_GATTC_SERVICE_RESULT = const(9)
21+
_IRQ_GATTC_SERVICE_DONE = const(10)
22+
_IRQ_GATTC_CHARACTERISTIC_RESULT = const(11)
23+
_IRQ_GATTC_CHARACTERISTIC_DONE = const(12)
24+
_IRQ_GATTC_DESCRIPTOR_RESULT = const(13)
25+
_IRQ_GATTC_DESCRIPTOR_DONE = const(14)
26+
_IRQ_GATTC_READ_RESULT = const(15)
27+
_IRQ_GATTC_READ_DONE = const(16)
28+
_IRQ_GATTC_WRITE_DONE = const(17)
29+
_IRQ_GATTC_NOTIFY = const(18)
30+
_IRQ_GATTC_INDICATE = const(19)
31+
32+
_ADV_IND = const(0x00)
33+
_ADV_DIRECT_IND = const(0x01)
34+
_ADV_SCAN_IND = const(0x02)
35+
_ADV_NONCONN_IND = const(0x03)
36+
37+
# org.bluetooth.service.environmental_sensing
38+
_ENV_SENSE_UUID = bluetooth.UUID(0x181A)
39+
# org.bluetooth.characteristic.temperature
40+
_TEMP_UUID = bluetooth.UUID(0x2A6E)
41+
_TEMP_CHAR = (
42+
_TEMP_UUID,
43+
bluetooth.FLAG_READ | bluetooth.FLAG_NOTIFY,
44+
)
45+
_ENV_SENSE_SERVICE = (
46+
_ENV_SENSE_UUID,
47+
(_TEMP_CHAR,),
48+
)
49+
50+
class BLETemperatureCentral:
51+
def __init__(self, ble):
52+
self._ble = ble
53+
self._ble.active(True)
54+
self._ble.irq(self._irq)
55+
self._reset()
56+
self._led = Pin('LED', Pin.OUT)
57+
58+
def _reset(self):
59+
# Cached name and address from a successful scan.
60+
self._name = None
61+
self._addr_type = None
62+
self._addr = None
63+
64+
# Cached value (if we have one)
65+
self._value = None
66+
67+
# Callbacks for completion of various operations.
68+
# These reset back to None after being invoked.
69+
self._scan_callback = None
70+
self._conn_callback = None
71+
self._read_callback = None
72+
73+
# Persistent callback for when new data is notified from the device.
74+
self._notify_callback = None
75+
76+
# Connected device.
77+
self._conn_handle = None
78+
self._start_handle = None
79+
self._end_handle = None
80+
self._value_handle = None
81+
82+
def _irq(self, event, data):
83+
if event == _IRQ_SCAN_RESULT:
84+
addr_type, addr, adv_type, rssi, adv_data = data
85+
if adv_type in (_ADV_IND, _ADV_DIRECT_IND):
86+
type_list = decode_services(adv_data)
87+
if _ENV_SENSE_UUID in type_list:
88+
# Found a potential device, remember it and stop scanning.
89+
self._addr_type = addr_type
90+
self._addr = bytes(addr) # Note: addr buffer is owned by caller so need to copy it.
91+
self._name = decode_name(adv_data) or "?"
92+
self._ble.gap_scan(None)
93+
94+
elif event == _IRQ_SCAN_DONE:
95+
if self._scan_callback:
96+
if self._addr:
97+
# Found a device during the scan (and the scan was explicitly stopped).
98+
self._scan_callback(self._addr_type, self._addr, self._name)
99+
self._scan_callback = None
100+
else:
101+
# Scan timed out.
102+
self._scan_callback(None, None, None)
103+
104+
elif event == _IRQ_PERIPHERAL_CONNECT:
105+
# Connect successful.
106+
conn_handle, addr_type, addr = data
107+
if addr_type == self._addr_type and addr == self._addr:
108+
self._conn_handle = conn_handle
109+
self._ble.gattc_discover_services(self._conn_handle)
110+
111+
elif event == _IRQ_PERIPHERAL_DISCONNECT:
112+
# Disconnect (either initiated by us or the remote end).
113+
conn_handle, _, _ = data
114+
if conn_handle == self._conn_handle:
115+
# If it was initiated by us, it'll already be reset.
116+
self._reset()
117+
118+
elif event == _IRQ_GATTC_SERVICE_RESULT:
119+
# Connected device returned a service.
120+
conn_handle, start_handle, end_handle, uuid = data
121+
if conn_handle == self._conn_handle and uuid == _ENV_SENSE_UUID:
122+
self._start_handle, self._end_handle = start_handle, end_handle
123+
124+
elif event == _IRQ_GATTC_SERVICE_DONE:
125+
# Service query complete.
126+
if self._start_handle and self._end_handle:
127+
self._ble.gattc_discover_characteristics(
128+
self._conn_handle, self._start_handle, self._end_handle
129+
)
130+
else:
131+
print("Failed to find environmental sensing service.")
132+
133+
elif event == _IRQ_GATTC_CHARACTERISTIC_RESULT:
134+
# Connected device returned a characteristic.
135+
conn_handle, def_handle, value_handle, properties, uuid = data
136+
if conn_handle == self._conn_handle and uuid == _TEMP_UUID:
137+
self._value_handle = value_handle
138+
139+
elif event == _IRQ_GATTC_CHARACTERISTIC_DONE:
140+
# Characteristic query complete.
141+
if self._value_handle:
142+
# We've finished connecting and discovering device, fire the connect callback.
143+
if self._conn_callback:
144+
self._conn_callback()
145+
else:
146+
print("Failed to find temperature characteristic.")
147+
148+
elif event == _IRQ_GATTC_READ_RESULT:
149+
# A read completed successfully.
150+
conn_handle, value_handle, char_data = data
151+
if conn_handle == self._conn_handle and value_handle == self._value_handle:
152+
self._update_value(char_data)
153+
if self._read_callback:
154+
self._read_callback(self._value)
155+
self._read_callback = None
156+
157+
elif event == _IRQ_GATTC_READ_DONE:
158+
# Read completed (no-op).
159+
conn_handle, value_handle, status = data
160+
161+
elif event == _IRQ_GATTC_NOTIFY:
162+
# The ble_temperature.py demo periodically notifies its value.
163+
conn_handle, value_handle, notify_data = data
164+
if conn_handle == self._conn_handle and value_handle == self._value_handle:
165+
self._update_value(notify_data)
166+
if self._notify_callback:
167+
self._notify_callback(self._value)
168+
169+
# Returns true if we've successfully connected and discovered characteristics.
170+
def is_connected(self):
171+
return self._conn_handle is not None and self._value_handle is not None
172+
173+
# Find a device advertising the environmental sensor service.
174+
def scan(self, callback=None):
175+
self._addr_type = None
176+
self._addr = None
177+
self._scan_callback = callback
178+
self._ble.gap_scan(2000, 30000, 30000)
179+
180+
# Connect to the specified device (otherwise use cached address from a scan).
181+
def connect(self, addr_type=None, addr=None, callback=None):
182+
self._addr_type = addr_type or self._addr_type
183+
self._addr = addr or self._addr
184+
self._conn_callback = callback
185+
if self._addr_type is None or self._addr is None:
186+
return False
187+
self._ble.gap_connect(self._addr_type, self._addr)
188+
return True
189+
190+
# Disconnect from current device.
191+
def disconnect(self):
192+
if not self._conn_handle:
193+
return
194+
self._ble.gap_disconnect(self._conn_handle)
195+
self._reset()
196+
197+
# Issues an (asynchronous) read, will invoke callback with data.
198+
def read(self, callback):
199+
if not self.is_connected():
200+
return
201+
self._read_callback = callback
202+
try:
203+
self._ble.gattc_read(self._conn_handle, self._value_handle)
204+
except OSError as error:
205+
print(error)
206+
207+
# Sets a callback to be invoked when the device notifies us.
208+
def on_notify(self, callback):
209+
self._notify_callback = callback
210+
211+
def _update_value(self, data):
212+
# Data is sint16 in degrees Celsius with a resolution of 0.01 degrees Celsius.
213+
try:
214+
self._value = struct.unpack("<h", data)[0] / 100
215+
except OSError as error:
216+
print(error)
217+
218+
def value(self):
219+
return self._value
220+
221+
def sleep_ms_flash_led(self, flash_count, delay_ms):
222+
self._led.off()
223+
while(delay_ms > 0):
224+
for i in range(flash_count):
225+
self._led.on()
226+
time.sleep_ms(100)
227+
self._led.off()
228+
time.sleep_ms(100)
229+
delay_ms -= 200
230+
time.sleep_ms(1000)
231+
delay_ms -= 1000
232+
233+
def print_temp(result):
234+
print("read temp: %.2f degc" % result)
235+
236+
def demo(ble, central):
237+
not_found = False
238+
239+
def on_scan(addr_type, addr, name):
240+
if addr_type is not None:
241+
print("Found sensor: %s" % name)
242+
central.connect()
243+
else:
244+
nonlocal not_found
245+
not_found = True
246+
print("No sensor found.")
247+
248+
central.scan(callback=on_scan)
249+
250+
# Wait for connection...
251+
while not central.is_connected():
252+
time.sleep_ms(100)
253+
if not_found:
254+
return
255+
256+
print("Connected")
257+
258+
# Explicitly issue reads
259+
while central.is_connected():
260+
central.read(callback=print_temp)
261+
sleep_ms_flash_led(central, 2, 2000)
262+
263+
print("Disconnected")
264+
265+
if __name__ == "__main__":
266+
ble = bluetooth.BLE()
267+
central = BLETemperatureCentral(ble)
268+
while(True):
269+
demo(ble, central)
270+
sleep_ms_flash_led(central, 1, 10000)

0 commit comments

Comments
 (0)