|
| 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 | + |
| 9 | +from ble_advertising import decode_services, decode_name |
| 10 | + |
| 11 | +from micropython import const |
| 12 | +_IRQ_CENTRAL_CONNECT = const(1 << 0) |
| 13 | +_IRQ_CENTRAL_DISCONNECT = const(1 << 1) |
| 14 | +_IRQ_GATTS_WRITE = const(1 << 2) |
| 15 | +_IRQ_GATTS_READ_REQUEST = const(1 << 3) |
| 16 | +_IRQ_SCAN_RESULT = const(1 << 4) |
| 17 | +_IRQ_SCAN_COMPLETE = const(1 << 5) |
| 18 | +_IRQ_PERIPHERAL_CONNECT = const(1 << 6) |
| 19 | +_IRQ_PERIPHERAL_DISCONNECT = const(1 << 7) |
| 20 | +_IRQ_GATTC_SERVICE_RESULT = const(1 << 8) |
| 21 | +_IRQ_GATTC_CHARACTERISTIC_RESULT = const(1 << 9) |
| 22 | +_IRQ_GATTC_DESCRIPTOR_RESULT = const(1 << 10) |
| 23 | +_IRQ_GATTC_READ_RESULT = const(1 << 11) |
| 24 | +_IRQ_GATTC_WRITE_STATUS = const(1 << 12) |
| 25 | +_IRQ_GATTC_NOTIFY = const(1 << 13) |
| 26 | +_IRQ_GATTC_INDICATE = const(1 << 14) |
| 27 | +_IRQ_ALL = const(0xffff) |
| 28 | + |
| 29 | +# org.bluetooth.service.environmental_sensing |
| 30 | +_ENV_SENSE_UUID = bluetooth.UUID(0x181A) |
| 31 | +# org.bluetooth.characteristic.temperature |
| 32 | +_TEMP_UUID = bluetooth.UUID(0x2A6E) |
| 33 | +_TEMP_CHAR = (_TEMP_UUID, bluetooth.FLAG_READ|bluetooth.FLAG_NOTIFY,) |
| 34 | +_ENV_SENSE_SERVICE = (_ENV_SENSE_UUID, (_TEMP_CHAR,),) |
| 35 | + |
| 36 | +# org.bluetooth.characteristic.gap.appearance.xml |
| 37 | +_ADV_APPEARANCE_GENERIC_THERMOMETER = const(768) |
| 38 | + |
| 39 | +class BLETemperatureCentral: |
| 40 | + def __init__(self, ble): |
| 41 | + self._ble = ble |
| 42 | + self._ble.active(True) |
| 43 | + self._ble.irq(handler=self._irq) |
| 44 | + |
| 45 | + self._reset() |
| 46 | + |
| 47 | + def _reset(self): |
| 48 | + # Cached name and address from a successful scan. |
| 49 | + self._name = None |
| 50 | + self._addr_type = None |
| 51 | + self._addr = None |
| 52 | + |
| 53 | + # Cached value (if we have one) |
| 54 | + self._value = None |
| 55 | + |
| 56 | + # Callbacks for completion of various operations. |
| 57 | + # These reset back to None after being invoked. |
| 58 | + self._scan_callback = None |
| 59 | + self._conn_callback = None |
| 60 | + self._read_callback = None |
| 61 | + |
| 62 | + # Persistent callback for when new data is notified from the device. |
| 63 | + self._notify_callback = None |
| 64 | + |
| 65 | + # Connected device. |
| 66 | + self._conn_handle = None |
| 67 | + self._value_handle = None |
| 68 | + |
| 69 | + def _irq(self, event, data): |
| 70 | + if event == _IRQ_SCAN_RESULT: |
| 71 | + addr_type, addr, connectable, rssi, adv_data = data |
| 72 | + if connectable and _ENV_SENSE_UUID in decode_services(adv_data): |
| 73 | + # Found a potential device, remember it and stop scanning. |
| 74 | + self._addr_type = addr_type |
| 75 | + self._addr = bytes(addr) # Note: The addr buffer is owned by modbluetooth, need to copy it. |
| 76 | + self._name = decode_name(adv_data) or '?' |
| 77 | + self._ble.gap_scan(None) |
| 78 | + |
| 79 | + elif event == _IRQ_SCAN_COMPLETE: |
| 80 | + if self._scan_callback: |
| 81 | + if self._addr: |
| 82 | + # Found a device during the scan (and the scan was explicitly stopped). |
| 83 | + self._scan_callback(self._addr_type, self._addr, self._name) |
| 84 | + self._scan_callback = None |
| 85 | + else: |
| 86 | + # Scan timed out. |
| 87 | + self._scan_callback(None, None, None) |
| 88 | + |
| 89 | + elif event == _IRQ_PERIPHERAL_CONNECT: |
| 90 | + # Connect successful. |
| 91 | + conn_handle, addr_type, addr, = data |
| 92 | + if addr_type == self._addr_type and addr == self._addr: |
| 93 | + self._conn_handle = conn_handle |
| 94 | + self._ble.gattc_discover_services(self._conn_handle) |
| 95 | + |
| 96 | + elif event == _IRQ_PERIPHERAL_DISCONNECT: |
| 97 | + # Disconnect (either initiated by us or the remote end). |
| 98 | + conn_handle, _, _, = data |
| 99 | + if conn_handle == self._conn_handle: |
| 100 | + # If it was initiated by us, it'll already be reset. |
| 101 | + self._reset() |
| 102 | + |
| 103 | + elif event == _IRQ_GATTC_SERVICE_RESULT: |
| 104 | + # Connected device returned a service. |
| 105 | + conn_handle, start_handle, end_handle, uuid = data |
| 106 | + if conn_handle == self._conn_handle and uuid == _ENV_SENSE_UUID: |
| 107 | + self._ble.gattc_discover_characteristics(self._conn_handle, start_handle, end_handle) |
| 108 | + |
| 109 | + elif event == _IRQ_GATTC_CHARACTERISTIC_RESULT: |
| 110 | + # Connected device returned a characteristic. |
| 111 | + conn_handle, def_handle, value_handle, properties, uuid = data |
| 112 | + if conn_handle == self._conn_handle and uuid == _TEMP_UUID: |
| 113 | + self._value_handle = value_handle |
| 114 | + # We've finished connecting and discovering device, fire the connect callback. |
| 115 | + if self._conn_callback: |
| 116 | + self._conn_callback() |
| 117 | + |
| 118 | + elif event == _IRQ_GATTC_READ_RESULT: |
| 119 | + # A read completed successfully. |
| 120 | + conn_handle, value_handle, char_data = data |
| 121 | + if conn_handle == self._conn_handle and value_handle == self._value_handle: |
| 122 | + self._update_value(char_data) |
| 123 | + if self._read_callback: |
| 124 | + self._read_callback(self._value) |
| 125 | + self._read_callback = None |
| 126 | + |
| 127 | + elif event == _IRQ_GATTC_NOTIFY: |
| 128 | + # The ble_temperature.py demo periodically notifies its value. |
<
10000
td data-grid-cell-id="diff-cdcc5beb87edc25b56622a36fd829ba77b27d0ab9df5fa7a5e6054a30f667b0b-empty-129-0" data-selected="false" role="gridcell" style="background-color:var(--diffBlob-additionNum-bgColor, var(--diffBlob-addition-bgColor-num));text-align:center" tabindex="-1" valign="top" class="focusable-grid-cell diff-line-number position-relative left-side">
129 | + conn_handle, value_handle, notify_data = data |
| 130 | + if conn_handle == self._conn_handle and value_handle == self._value_handle: |
| 131 | + self._update_value(notify_data) |
| 132 | + if self._notify_callback: |
| 133 | + self._notify_callback(self._value) |
| 134 | + |
| 135 | + |
| 136 | + # Returns true if we've successfully connected and discovered characteristics. |
| 137 | + def is_connected(self): |
| 138 | + return self._conn_handle is not None and self._value_handle is not None |
| 139 | + |
| 140 | + # Find a device advertising the environmental sensor service. |
| 141 | + def scan(self, callback=None): |
| 142 | + self._addr_type = None |
| 143 | + self._addr = None |
| 144 | + self._scan_callback = callback |
| 145 | + self._ble.gap_scan(2000, 30000, 30000) |
| 146 | + |
| 147 | + # Connect to the specified device (otherwise use cached address from a scan). |
| 148 | + def connect(self, addr_type=None, addr=None, callback=None): |
| 149 | + self._addr_type = addr_type or self._addr_type |
| 150 | + self._addr = addr or self._addr |
| 151 | + self._conn_callback = callback |
| 152 | + if self._addr_type is None or self._addr is None: |
| 153 | + return False |
| 154 | + self._ble.gap_connect(self._addr_type, self._addr) |
| 155 | + return True |
| 156 | + |
| 157 | + # Disconnect from current device. |
| 158 | + def disconnect(self): |
| 159 | + if not self._conn_handle: |
| 160 | + return |
| 161 | + self._ble.gap_disconnect(self._conn_handle) |
| 162 | + self._reset() |
| 163 | + |
| 164 | + # Issues an (asynchronous) read, will invoke callback with data. |
| 165 | + def read(self, callback): |
| 166 | + if not self.is_connected(): |
| 167 | + return |
| 168 | + self._read_callback = callback |
| 169 | + self._ble.gattc_read(self._conn_handle, self._value_handle) |
| 170 | + |
| 171 | + # Sets a callback to be invoked when the device notifies us. |
| 172 | + def on_notify(self, callback): |
| 173 | + self._notify_callback = callback |
| 174 | + |
| 175 | + def _update_value(self, data): |
| 176 | + # Data is sint16 in degrees Celsius with a resolution of 0.01 degrees Celsius. |
| 177 | + self._value = struct.unpack('<h', data)[0] / 100 |
| 178 | + return self._value |
| 179 | + |
| 180 | + def value(self): |
| 181 | + return self._value |
| 182 | + |
| 183 | + |
| 184 | +def demo(): |
| 185 | + ble = bluetooth.BLE() |
| 186 | + central = BLETemperatureCentral(ble) |
| 187 | + |
| 188 | + not_found = False |
| 189 | + |
| 190 | + def on_scan(addr_type, addr, name): |
| 191 | + if addr_type is not None: |
| 192 | + print('Found sensor:', addr_type, addr, name) |
| 193 | + central.connect() |
| 194 | + else: |
| 195 | + nonlocal not_found |
| 196 | + not_found = True |
| 197 | + print('No sensor found.') |
| 198 | + |
| 199 | + central.scan(callback=on_scan) |
| 200 | + |
| 201 | + # Wait for connection... |
| 202 | + while not central.is_connected(): |
| 203 | + time.sleep_ms(100) |
| 204 | + if not_found: |
| 205 | + return |
| 206 | + |
| 207 | + print('Connected') |
| 208 | + |
| 209 | + # Explicitly issue reads, using "print" as the callback. |
| 210 | + while central.is_connected(): |
| 211 | + central.read(callback=print) |
| 212 | + time.sleep_ms(2000) |
| 213 | + |
| 214 | + # Alternative to the above, just show the most recently notified value. |
| 215 | + # while central.is_connected(): |
| 216 | + # print(central.value()) |
| 217 | + # time.sleep_ms(2000) |
| 218 | + |
| 219 | + print('Disconnected') |
| 220 | + |
| 221 | +if __name__ == '__main__': |
| 222 | + demo() |
0 commit comments