diff --git a/esp32-micropython/examples/ble/ble_led-controller.py b/esp32-micropython/examples/ble/ble_led-controller.py new file mode 100644 index 00000000..d559fc41 --- /dev/null +++ b/esp32-micropython/examples/ble/ble_led-controller.py @@ -0,0 +1,43 @@ +from time import sleep +from machine import Pin +from util.button import Button + +led_button = Button(0, release_value=1) + +import util.ble.blesync_client +import util.ble.blesync_uart.client + +built_in_led = Pin(2, Pin.OUT) + + +@blesync_uart.client.UARTService.on_message +def on_message(service, message): + print(message) + + +client = blesync_client.BLEClient(blesync_uart.client.UARTService) + + +def connect(): + while True: + for device in blesync_client.scan(): + if device.adv_name == 'octopus_led': + services = client.connect(addr_type=device.addr_type, addr=device.addr) + return services[blesync_uart.client.UARTService][0] + + +uart_service = connect() + +built_in_led.on() +sleep(1) +built_in_led.off() + + +@led_button.on_press +def on_press_top_button(): + uart_service.send(b'!B516') + + +@led_button.on_release +def on_release_top_button(): + uart_service.send(b'!B507') diff --git a/esp32-micropython/examples/ble/ble_led.py b/esp32-micropython/examples/ble/ble_led.py index 2d6ab036..ee8d1d10 100644 --- a/esp32-micropython/examples/ble/ble_led.py +++ b/esp32-micropython/examples/ble/ble_led.py @@ -1,33 +1,51 @@ -# octopusLAB - main.py - BLE and BlueFruit mobile app. -## uPyShell:~/$ run examples/ble/ble_led.py - -print("---> BLE and BlueFruit mobile app. - led") -print("This is simple Micropython example | ESP32 & octopusLAB") - -from util.shell.terminal import getUid -uID5 = getUid(short=5) - -from time import sleep -from util.led import Led -led = Led(2) - -led.blink() -sleep(3) -led.blink() - -from util.ble import bleuart -import util.ble.bluefruit as bf - -def on_data_received(connection, data): - print(str(data)) - if data == bf.UP: - led.value(1) - if data == bf.DOWN: - led.value(0) - - -devName = 'octopus-led-'+uID5 -print("BLE ESP32 device name: " + devName) - -uart = bleuart.BLEUART(name=devName, on_data_received=on_data_received) -uart.start() \ No newline at end of file +print("---> BLE-BLE led controller") + +from util.shell.terminal import getUid +uID5 = getUid(short=5) + +from time import sleep +from util.pinout import set_pinout +pinout = set_pinout() + +from util.rgb import Rgb +ws = Rgb(pinout.PWM3_PIN) +ws.simpleTest() + + +from util.led import Led +led = Led(2) + +from machine import Pin +import util.ble.blesync_server +import util.ble.blesync_uart.server + + +@blesync_uart.server.UARTService.on_message +def on_message(service, conn_handle, message): + if message == b'!B516': + led.value(1) + elif message == b'!B507': + led.value(0) + + + service.send(conn_handle, message) + + +class Connections: + _connections = [] + + @blesync_server.on_connect + @classmethod + def on_connect(cls, conn_handle, addr_type, addr): + cls._connections.append(conn_handle) + built_in_led.on() + + @blesync_server.on_disconnect + @classmethod + def on_disconnect(cls, conn_handle, addr_type, addr): + cls._connections.remove(conn_handle) + if not cls._connections: + built_in_led.off() + + +blesync_server.Server.start('octopus_led', blesync_uart.server.UARTService) diff --git a/esp32-micropython/examples/ble/ble_led_bluefruit.py b/esp32-micropython/examples/ble/ble_led_bluefruit.py new file mode 100644 index 00000000..2d6ab036 --- /dev/null +++ b/esp32-micropython/examples/ble/ble_led_bluefruit.py @@ -0,0 +1,33 @@ +# octopusLAB - main.py - BLE and BlueFruit mobile app. +## uPyShell:~/$ run examples/ble/ble_led.py + +print("---> BLE and BlueFruit mobile app. - led") +print("This is simple Micropython example | ESP32 & octopusLAB") + +from util.shell.terminal import getUid +uID5 = getUid(short=5) + +from time import sleep +from util.led import Led +led = Led(2) + +led.blink() +sleep(3) +led.blink() + +from util.ble import bleuart +import util.ble.bluefruit as bf + +def on_data_received(connection, data): + print(str(data)) + if data == bf.UP: + led.value(1) + if data == bf.DOWN: + led.value(0) + + +devName = 'octopus-led-'+uID5 +print("BLE ESP32 device name: " + devName) + +uart = bleuart.BLEUART(name=devName, on_data_received=on_data_received) +uart.start() \ No newline at end of file diff --git a/esp32-micropython/lib/blesync.py b/esp32-micropython/lib/blesync.py new file mode 100644 index 00000000..96dc1ccb --- /dev/null +++ b/esp32-micropython/lib/blesync.py @@ -0,0 +1,485 @@ +# The MIT License (MIT) +# Copyright (c) 2019-2022 Jan Cespivo + +__version__ = "1.0.3" + +from collections import deque + +from bluetooth import BLE, UUID +import machine +from micropython import const, schedule +import time + +_IRQ_CENTRAL_CONNECT = const(1) +_IRQ_CENTRAL_DISCONNECT = const(2) +_IRQ_GATTS_WRITE = const(3) +_IRQ_GATTS_READ_REQUEST = const(4) +_IRQ_SCAN_RESULT = const(5) +_IRQ_SCAN_DONE = const(6) +_IRQ_PERIPHERAL_CONNECT = const(7) +_IRQ_PERIPHERAL_DISCONNECT = const(8) +_IRQ_GATTC_SERVICE_RESULT = const(9) +_IRQ_GATTC_SERVICE_DONE = const(10) +_IRQ_GATTC_CHARACTERISTIC_RESULT = const(11) +_IRQ_GATTC_CHARACTERISTIC_DONE = const(12) +_IRQ_GATTC_DESCRIPTOR_RESULT = const(13) +_IRQ_GATTC_DESCRIPTOR_DONE = const(14) +_IRQ_GATTC_READ_RESULT = const(15) +_IRQ_GATTC_READ_DONE = const(16) +_IRQ_GATTC_WRITE_DONE = const(17) +_IRQ_GATTC_NOTIFY = const(18) +_IRQ_GATTC_INDICATE = const(19) +_IRQ_L2CAP_CONNECT = const(23) + +curr_conn_handle = None + + +def _register_callback(irq, callback): + _callbacks[irq].append(callback) + + +def _event(irq, data, key): + _events[irq][key].append(data) + + +def _call_callbacks(irq_data): + irq, data = irq_data + + for callback in _callbacks[irq]: + callback(*data) + + +def _callback(irq, data): + schedule(_call_callbacks, (irq, data)) + + +def _register_event(irq, key, bufferlen=1): + _events[irq][key] = deque(tuple(), bufferlen) + + +# TODO +# def _irq_gatts_read_request(data): +# # A central has issued a read. Note: this is a hard IRQ. +# # Return None to deny the read. +# # Note: This event is not supported on ESP32. +# conn_handle, attr_handle = data +# _event(_IRQ_GATTS_READ_REQUEST, data, conn_handle) + + +_events = { + _IRQ_SCAN_RESULT: {}, + _IRQ_SCAN_DONE: {}, + _IRQ_PERIPHERAL_CONNECT: {}, + _IRQ_PERIPHERAL_DISCONNECT: {}, + _IRQ_GATTC_SERVICE_RESULT: {}, + _IRQ_GATTC_SERVICE_DONE: {}, + _IRQ_GATTC_CHARACTERISTIC_RESULT: {}, + _IRQ_GATTC_CHARACTERISTIC_DONE: {}, + _IRQ_GATTC_DESCRIPTOR_RESULT: {}, + _IRQ_GATTC_DESCRIPTOR_DONE: {}, + _IRQ_GATTC_READ_RESULT: {}, + _IRQ_GATTC_WRITE_DONE: {}, +} + +_callbacks = { + _IRQ_CENTRAL_CONNECT: [], + _IRQ_CENTRAL_DISCONNECT: [], + _IRQ_PERIPHERAL_DISCONNECT: [], + _IRQ_GATTS_WRITE: [], + _IRQ_GATTC_NOTIFY: [], + _IRQ_GATTC_INDICATE: [], + # _IRQ_GATTS_READ_REQUEST: _irq_gatts_read_request, +} + + +def _irq(event, data): + if event == _IRQ_CENTRAL_CONNECT: + print("dis------"*10) + conn_handle, addr_type, addr = data + curr_conn_handle = conn_handle + print(conn_handle,type(conn_handle),curr_conn_handle) + time.sleep(2) + print("dis------"*10) + disconnect_client() + print(">>>>>>>>>>>>>>>"*5) + + if event in ( + _IRQ_CENTRAL_CONNECT, + _IRQ_CENTRAL_DISCONNECT + ): + # A central has connected to this peripheral. + # A central has disconnected from this peripheral. + conn_handle, addr_type, addr = data + data = conn_handle, addr_type, bytes(addr) + _callback(event, data) + elif event == _IRQ_PERIPHERAL_DISCONNECT: + # A central has disconnected from this peripheral or connect timeout + if data == (65535, 255, b'\x00\x00\x00\x00\x00\x00'): + # connect timeout + _event(event, None, None) + else: + conn_handle, addr_type, addr = data + data = conn_handle, addr_type, bytes(addr) + _callback(event, data) + elif event == _IRQ_GATTS_WRITE: + # A central has written to this characteristic or descriptor. + # conn_handle, attr_handle = data + _callback(event, data) + elif event in (_IRQ_GATTC_NOTIFY, _IRQ_GATTC_INDICATE): + # A peripheral has sent a notify request. + # A peripheral has sent an indicate request. + conn_handle, value_handle, notify_data = data + data = conn_handle, value_handle, bytes(notify_data) + _callback(event, data) + elif event == _IRQ_SCAN_RESULT: + # A single scan result. + addr_type, addr, adv_type, rssi, adv_data = data + data = addr_type, bytes(addr), adv_type, rssi, bytes(adv_data) + _event(event, data, None) + elif event == _IRQ_SCAN_DONE: + # A single scan result. + _event(event, None, None) + elif event == _IRQ_PERIPHERAL_CONNECT: + # A successful gap_connect(). + conn_handle, addr_type, addr = data + key = addr_type, bytes(addr) + _event(event, conn_handle, key) + elif event == _IRQ_GATTC_SERVICE_RESULT: + # Called for each service found by gattc_discover_services(). + conn_handle, start_handle, end_handle, uuid = data + data = start_handle, end_handle, UUID(uuid) + _event(event, data, conn_handle) + elif event == _IRQ_GATTC_SERVICE_DONE: + # Called once service discovery is complete. + # Note: Status will be zero on success, implementation-specific value otherwise. + conn_handle, status = data + _event(event, status, conn_handle) + elif event == _IRQ_GATTC_CHARACTERISTIC_RESULT: + # Called for each characteristic found by gattc_discover_services(). + conn_handle, def_handle, value_handle, properties, uuid = data + data = def_handle, value_handle, properties, UUID(uuid) + _event(event, data, conn_handle) + elif event == _IRQ_GATTC_CHARACTERISTIC_DONE: + # Called once service discovery is complete. + # Note: Status will be zero on success, implementation-specific value otherwise. + conn_handle, status = data + _event(event, status, conn_handle) + elif event == _IRQ_GATTC_DESCRIPTOR_RESULT: + # Called for each descriptor found by gattc_discover_descriptors(). + conn_handle, dsc_handle, uuid = data + data = dsc_handle, UUID(uuid) + _event(event, data, conn_handle) + elif event == _IRQ_GATTC_DESCRIPTOR_DONE: + # Called once service discovery is complete. + # Note: Status will be zero on success, implementation-specific value otherwise. + conn_handle, status = data + _event(event, status, conn_handle) + elif event == _IRQ_GATTC_READ_RESULT: + # A gattc_read() has completed. + conn_handle, value_handle, char_data = data + key = conn_handle, value_handle + _event(event, bytes(char_data), key) + elif event == _IRQ_GATTC_READ_DONE: + # A gattc_read() has completed. + # Note: The value_handle will be zero on btstack (but present on NimBLE). + # Note: Status will be zero on success, implementation-specific value otherwise. + # conn_handle, value_handle, status = data + # key = conn_handle, value_handle + # data = status + return # TODO + elif event == _IRQ_GATTC_WRITE_DONE: + # A gattc_write() has completed. + # Note: The value_handle will be zero on btstack (but present on NimBLE). + # Note: Status will be zero on success, implementation-specific value otherwise. + conn_handle, value_handle, status = data + key = conn_handle, value_handle + _event(event, status, key) + elif event == _IRQ_L2CAP_CONNECT: + conn_handle, cid, psm, our_mtu, peer_mtu = data + print("BLE_DIS_"*12) + print("_IRQ_L2CAP_CONNECT",conn_handle, cid) + curr_conn_handle = conn_handle + curr_conn_cid = cid + time.sleep(1) + print("-disconnect_client") + disconnect_client() + + else: + return + +#from machine import Timer + +#tim2.init(period=per, mode=Timer.PERIODIC, callback=lambda t:timerAction()) + + +def disconnect_client(): + #if curr_conn_handle: + BLE.gap_disconnect(0) + #curr_conn_handle = None + +def _wait_for_event(irq, key, event_exception_class=None): + event_queue = _events[irq][key] + + while not event_queue: + machine.idle() + + event_result = event_queue.popleft() + if event_exception_class and event_result: + raise event_exception_class(event_result) + return event_result + + +def _wait_for_disjunct_events(irq_1, key_1, irq_2, key_2): + event_queue_1 = _events[irq_1][key_1] + event_queue_2 = _events[irq_2][key_2] + + while True: + if event_queue_1: + return event_queue_1.popleft(), None + elif event_queue_2: + return None, event_queue_2.popleft() + + machine.idle() + + +_ble = BLE() + +config = _ble.config +gap_advertise = _ble.gap_advertise +gatts_register_services = _ble.gatts_register_services +gatts_read = _ble.gatts_read +gatts_write = _ble.gatts_write +gatts_set_buffer = _ble.gatts_set_buffer +gap_disconnect = _ble.gap_disconnect + + +def _results_until_done( + event_result, event_done, event_done_exception_class, event_key, func, args +): + _register_event(event_result, event_key, bufferlen=100) + _register_event(event_done, event_key) + + func(*args) + + results_queue = _events[event_result][event_key] + done_queue = _events[event_done][event_key] + while True: + while results_queue: + yield results_queue.popleft() + + if done_queue: + done_status = done_queue.popleft() + if done_status: + raise event_done_exception_class(done_status) + return + + machine.idle() + + +def gap_scan(duration_ms, interval_us=None, window_us=None): + """ + if it is interrupted during the iteration, the close() has to be called + see https://github.com/micropython/micropython/issues/6183 + Example: + scan_iter = scan( + duration_ms=duration_ms, + interval_us=interval_us, + window_us=window_us, + ) + + for device in scan_iter: + scan_iter.close() + return device + """ + + assert not (interval_us is None and window_us is not None), \ + "Argument window_us has to be specified if interval_us is specified" + + args = [duration_ms] + if interval_us is not None: + args.append(interval_us) + if window_us is not None: + args.append(window_us) + try: + yield from _results_until_done( + _IRQ_SCAN_RESULT, + _IRQ_SCAN_DONE, + None, + None, + func=_ble.gap_scan, + args=args + ) + except GeneratorExit: + _ble.gap_scan(None) + _wait_for_event(_IRQ_SCAN_DONE, None) + + +def gatts_notify(conn_handle, handle, data=None): + if data is None: + return _ble.gatts_notify(conn_handle, handle) + return _ble.gatts_notify(conn_handle, handle, data) + + +def activate(): + if not _ble.active(): + _ble.active(True) + _ble.irq(_irq) + + +def deactivate(): + if _ble.active(): + _ble.active(False) + + +def is_active(): + return _ble.active() + + +class GapConnectTimeoutError(Exception): + pass + + +def gap_connect(addr_type, addr, timeout_ms=2000): + _register_event(_IRQ_PERIPHERAL_CONNECT, (addr_type, addr)) + _register_event(_IRQ_PERIPHERAL_DISCONNECT, None) + _register_event(_IRQ_L2CAP_CONNECT, (conn_handle, cid, psm, our_mtu, peer_mtu)) + _ble.gap_connect(addr_type, addr, timeout_ms) + conn_handle, _ = _wait_for_disjunct_events( + _IRQ_PERIPHERAL_CONNECT, (addr_type, addr), + _IRQ_PERIPHERAL_DISCONNECT, None + ) + if conn_handle is None: + raise GapConnectTimeoutError + return conn_handle + + +class GattcDiscoverServicesError(Exception): + pass + + +def gattc_discover_services(conn_handle, uuid=None): + args = [conn_handle] + if uuid is not None: + args.append(uuid) + + return list(_results_until_done( + event_result=_IRQ_GATTC_SERVICE_RESULT, + event_done=_IRQ_GATTC_SERVICE_DONE, + event_done_exception_class=GattcDiscoverServicesError, + event_key=conn_handle, + func=_ble.gattc_discover_services, + args=args + )) + + +class GattcDiscoverCharacteristicsError(Exception): + pass + + +def gattc_discover_characteristics( + conn_handle, + start_handle, + end_handle, +): + # TODO uuid argument + return list(_results_until_done( + event_result=_IRQ_GATTC_CHARACTERISTIC_RESULT, + event_done=_IRQ_GATTC_CHARACTERISTIC_DONE, + event_done_exception_class=GattcDiscoverCharacteristicsError, + event_key=conn_handle, + func=_ble.gattc_discover_characteristics, + args=(conn_handle, start_handle, end_handle) + )) + + +class GattcDiscoverDescriptorError(Exception): + pass + + +def gattc_discover_descriptors(conn_handle, start_handle, end_handle): + return list(_results_until_done( + event_result=_IRQ_GATTC_DESCRIPTOR_RESULT, + event_done=_IRQ_GATTC_DESCRIPTOR_DONE, + event_done_exception_class=GattcDiscoverDescriptorError, + event_key=conn_handle, + func=_ble.gattc_discover_descriptors, + args=(conn_handle, start_handle, end_handle) + )) + + +class GattcReadError(Exception): + pass + + +def gattc_read(conn_handle, value_handle): + # conn_handle, value_handle, char_data + _register_event(_IRQ_GATTC_READ_RESULT, (conn_handle, value_handle)) + _ble.gattc_read(conn_handle, value_handle) + return _wait_for_event( + _IRQ_GATTC_READ_RESULT, + (conn_handle, value_handle), + GattcReadError + ) + + +class GattcWriteError(Exception): + pass + + +def gattc_write(conn_handle, value_handle, data, ack=False): + # wait for return status of write if ack is True + # otherwise return None immediately + _register_event(_IRQ_GATTC_WRITE_DONE, (conn_handle, value_handle)) + _ble.gattc_write(conn_handle, value_handle, data, ack) + if ack: + return _wait_for_event( + _IRQ_GATTC_WRITE_DONE, + (conn_handle, value_handle), + GattcWriteError + ) + + +def on_central_connect(callback): + # A central has connected to this peripheral. + # conn_handle, addr_type, addr + return _register_callback(_IRQ_CENTRAL_CONNECT, callback) + + +def on_central_disconnect(callback): + # A central has disconnected from this peripheral. + # conn_handle, addr_type, addr + _register_callback(_IRQ_CENTRAL_DISCONNECT, callback) + return callback + + +def on_peripherial_disconnect(callback): + # Connected peripheral has disconnected. + # conn_handle, addr_type, addr + _register_callback(_IRQ_PERIPHERAL_DISCONNECT, callback) + return callback + + +def on_gatts_write(callback): + # A central has written to this characteristic or descriptor. + # conn_handle, value_handle + _register_callback(_IRQ_GATTS_WRITE, callback) + return callback + + +def on_gattc_notify(callback): + # A peripheral has sent a notify request. + # conn_handle, value_handle, notify_data + _register_callback(_IRQ_GATTC_NOTIFY, callback) + return callback + + +def on_gattc_indicate(callback): + # A peripheral has sent an indicate request. + # conn_handle, value_handle, notify_data + _register_callback(_IRQ_GATTC_INDICATE, callback) + return callback + +# TODO +# def on_gatts_read_request(conn_handle): +# _add_callback(_callback_gatts_read_request, conn_handle, +# gatts_read_request_callback) diff --git a/esp32-micropython/util/ble/blesync.py b/esp32-micropython/util/ble/blesync.py new file mode 100644 index 00000000..c845a25e --- /dev/null +++ b/esp32-micropython/util/ble/blesync.py @@ -0,0 +1,323 @@ +from collections import deque +import time + +from bluetooth import BLE +import machine +from micropython import const, schedule + +_IRQ_CENTRAL_CONNECT = const(1 << 0) +_IRQ_CENTRAL_DISCONNECT = const(1 << 1) +_IRQ_GATTS_WRITE = const(1 << 2) +_IRQ_GATTS_READ_REQUEST = const(1 << 3) +_IRQ_SCAN_RESULT = const(1 << 4) +_IRQ_SCAN_COMPLETE = const(1 << 5) +_IRQ_PERIPHERAL_CONNECT = const(1 << 6) +_IRQ_PERIPHERAL_DISCONNECT = const(1 << 7) +_IRQ_GATTC_SERVICE_RESULT = const(1 << 8) +_IRQ_GATTC_CHARACTERISTIC_RESULT = const(1 << 9) +_IRQ_GATTC_DESCRIPTOR_RESULT = const(1 << 10) +_IRQ_GATTC_READ_RESULT = const(1 << 11) +_IRQ_GATTC_WRITE_STATUS = const(1 << 12) +_IRQ_GATTC_NOTIFY = const(1 << 13) +_IRQ_GATTC_INDICATE = const(1 << 14) + + +def _register_callback(irq, callback): + _callbacks[irq].append(callback) + + +def _event(irq, data, key): + _events[irq][key].append(data) + + +def _call_callbacks(irq_data): + irq, data = irq_data + + for callback in _callbacks[irq]: + callback(*data) + + +def _callback(irq, data): + schedule(_call_callbacks, (irq, data)) + + +def _register_event(irq, key, bufferlen=1): + _events[irq][key] = deque(tuple(), bufferlen) + + +# +# def _irq_gatts_read_request(data): +# # A central has issued a read. Note: this is a hard IRQ. +# # Return None to deny the read. +# # Note: This event is not supported on ESP32. +# conn_handle, attr_handle = data +# _event(_IRQ_GATTS_READ_REQUEST, data, conn_handle) + + +_events = { + _IRQ_SCAN_RESULT: {}, + _IRQ_SCAN_COMPLETE: {}, + _IRQ_PERIPHERAL_CONNECT: {}, + _IRQ_PERIPHERAL_DISCONNECT: {}, + _IRQ_GATTC_SERVICE_RESULT: {}, + _IRQ_GATTC_CHARACTERISTIC_RESULT: {}, + _IRQ_GATTC_DESCRIPTOR_RESULT: {}, + _IRQ_GATTC_READ_RESULT: {}, + _IRQ_GATTC_WRITE_STATUS: {}, +} + +_callbacks = { + _IRQ_CENTRAL_CONNECT: [], + _IRQ_CENTRAL_DISCONNECT: [], + _IRQ_GATTS_WRITE: [], + _IRQ_GATTC_NOTIFY: [], + _IRQ_GATTC_INDICATE: [], + # _IRQ_GATTS_READ_REQUEST: _irq_gatts_read_request, +} + + +def _irq(event, data): + if event in ( + _IRQ_CENTRAL_CONNECT, + _IRQ_CENTRAL_DISCONNECT, + _IRQ_GATTS_WRITE, + _IRQ_GATTC_NOTIFY, + _IRQ_GATTC_INDICATE, + _IRQ_PERIPHERAL_DISCONNECT + ): + _callback(event, data) + return + elif event == _IRQ_GATTC_CHARACTERISTIC_RESULT: + # Called for each characteristic found by gattc_discover_services(). + conn_handle, def_handle, value_handle, properties, uuid = data + for (conn_handle, start_handle, end_handle), event_queue in _events[ + _IRQ_GATTC_CHARACTERISTIC_RESULT + ].items(): + if start_handle <= def_handle <= end_handle: + event_queue.append((def_handle, value_handle, properties, uuid)) + return + elif event == _IRQ_PERIPHERAL_CONNECT: + # A successful gap_connect(). + conn_handle, addr_type, addr = data + key = addr_type, addr + data = conn_handle + elif event == _IRQ_GATTC_SERVICE_RESULT: + # Called for each service found by gattc_discover_services(). + conn_handle, start_handle, end_handle, uuid = data + key = conn_handle + data = start_handle, end_handle, uuid + elif event == _IRQ_GATTC_DESCRIPTOR_RESULT: + # Called for each descriptor found by gattc_discover_descriptors(). + conn_handle, dsc_handle, uuid = data + key = conn_handle + data = dsc_handle, uuid + elif event == _IRQ_GATTC_READ_RESULT: + # A gattc_read() has completed. + conn_handle, value_handle, char_data = data + key = conn_handle, value_handle + data = char_data + elif event == _IRQ_GATTC_WRITE_STATUS: + # A gattc_write() has completed. + conn_handle, value_handle, status = data + key = conn_handle, value_handle + data = status + else: + key = None + _event(event, data, key) + + +class EventTimeoutError(Exception): + pass + + +def _maybe_raise_timeout(timeout_ms, start_time): + if timeout_ms and time.ticks_diff(time.ticks_ms(), start_time) > timeout_ms: + raise EventTimeoutError() + + +def wait_for_event(irq, key, timeout_ms): + start_time = time.ticks_ms() + + event_queue = _events[irq][key] + + while not event_queue: + _maybe_raise_timeout(timeout_ms, start_time) + machine.idle() + + return event_queue.popleft() + + +_ble = BLE() + +gap_advertise = _ble.gap_advertise +gatts_register_services = _ble.gatts_register_services +gatts_read = _ble.gatts_read +gatts_write = _ble.gatts_write +gatts_set_buffer = _ble.gatts_set_buffer +gap_disconnect = _ble.gap_disconnect + + +def gap_scan(duration_ms, interval_us=None, window_us=None, timeout_ms=None): + assert not (interval_us is None and window_us is not None), \ + "Argument window_us has to be specified if interval_us is specified" + + start_time = time.ticks_ms() + + args = [] + if interval_us is not None: + args.append(interval_us) + if window_us is not None: + args.append(window_us) + + _register_event(_IRQ_SCAN_RESULT, None, bufferlen=100) + _register_event(_IRQ_SCAN_COMPLETE, None) + _ble.gap_scan(duration_ms, *args) + + scan_events_queue = _events[_IRQ_SCAN_RESULT][None] + scan_complete_queue = _events[_IRQ_SCAN_COMPLETE][None] + + while True: + while scan_events_queue: + yield scan_events_queue.popleft() + + if scan_complete_queue: + scan_complete_queue.popleft() + return + _maybe_raise_timeout(timeout_ms, start_time) + machine.idle() + + +def gatts_notify(conn_handle, handle, data=None): + if data is None: + return _ble.gatts_notify(conn_handle, handle) + return _ble.gatts_notify(conn_handle, handle, data) + + +def active(change_to=None): + is_active = _ble.active(change_to) + if is_active: + _ble.irq(_irq) + return is_active + + +def gap_connect(addr_type, addr, scan_duration_ms=2000, timeout_ms=None): + _register_event(_IRQ_PERIPHERAL_CONNECT, (addr_type, addr)) + _ble.gap_connect(addr_type, addr, scan_duration_ms) + return wait_for_event(_IRQ_PERIPHERAL_CONNECT, (addr_type, addr), timeout_ms) + + +def gattc_discover_services(conn_handle, timeout_ms=None): + start_time = time.ticks_ms() + _register_event(_IRQ_GATTC_SERVICE_RESULT, conn_handle, bufferlen=100) + _ble.gattc_discover_services(conn_handle) + + event_queue = _events[_IRQ_GATTC_SERVICE_RESULT][conn_handle] + while True: + while event_queue: + start_handle, end_handle, uuid = event_queue.popleft() + yield start_handle, end_handle, uuid + if end_handle == 65535: + return + _maybe_raise_timeout(timeout_ms, start_time) + machine.idle() + + +def gattc_discover_characteristics( + conn_handle, + start_handle, + end_handle, + timeout_ms=None +): + start_time = time.ticks_ms() + _register_event( + _IRQ_GATTC_CHARACTERISTIC_RESULT, + (conn_handle, start_handle, end_handle), + bufferlen=100 + ) + _ble.gattc_discover_characteristics(conn_handle, start_handle, end_handle) + event_queue = _events[_IRQ_GATTC_CHARACTERISTIC_RESULT][ + (conn_handle, start_handle, end_handle) + ] + while True: + while event_queue: + def_handle, value_handle, properties, uuid = event_queue.popleft() + yield def_handle, value_handle, properties, uuid + if value_handle == end_handle: + return + _maybe_raise_timeout(timeout_ms, start_time) + machine.idle() + + +def gattc_discover_descriptors(conn_handle, start_handle, end_handle): + # TODO + # _ble.gattc_discover_descriptors(conn_handle, start_handle, end_handle) + raise NotImplementedError + + +def gattc_read(conn_handle, value_handle, timeout_ms=None): + # conn_handle, value_handle, char_data + _register_event(_IRQ_GATTC_READ_RESULT, (conn_handle, value_handle)) + _ble.gattc_read(conn_handle, value_handle) + return wait_for_event( + _IRQ_GATTC_READ_RESULT, + (conn_handle, value_handle), + timeout_ms + ) + + +def gattc_write(conn_handle, value_handle, data, ack=False, timeout_ms=None): + # wait for return status of write if ack is True + # otherwise return None immediately + _register_event(_IRQ_GATTC_WRITE_STATUS, (conn_handle, value_handle)) + _ble.gattc_write(conn_handle, value_handle, data, ack) + if ack: + return wait_for_event( + _IRQ_GATTC_WRITE_STATUS, + (conn_handle, value_handle), + timeout_ms + ) + + +def on_central_connect(callback): + # A central has connected to this peripheral. + # conn_handle, addr_type, addr + _register_callback(_IRQ_CENTRAL_CONNECT, callback) + return callback + + +def on_central_disconnect(callback): + # A central has disconnected from this peripheral. + # conn_handle, addr_type, addr + _register_callback(_IRQ_CENTRAL_DISCONNECT, callback) + return callback + + +def on_peripherial_disconnect(callback): + _register_callback(_IRQ_PERIPHERAL_DISCONNECT, callback) + return callback + + +def on_gatts_write(callback): + # A central has written to this characteristic or descriptor. + # conn_handle, value_handle + _register_callback(_IRQ_GATTS_WRITE, callback) + return callback + + +def on_gattc_notify(callback): + # A peripheral has sent a notify request. + # conn_handle, value_handle, notify_data + _register_callback(_IRQ_GATTC_NOTIFY, callback) + return callback + + +def on_gattc_indicate(callback): + # A peripheral has sent an indicate request. + # conn_handle, value_handle, notify_data + _register_callback(_IRQ_GATTC_INDICATE, callback) + return callback + +# +# def on_gatts_read_request(conn_handle): +# _add_callback(_callback_gatts_read_request, conn_handle, +# gatts_read_request_callback) diff --git a/esp32-micropython/util/ble/blesync_client.py b/esp32-micropython/util/ble/blesync_client.py new file mode 100644 index 00000000..c41831b8 --- /dev/null +++ b/esp32-micropython/util/ble/blesync_client.py @@ -0,0 +1,260 @@ +from collections import namedtuple + +from micropython import const + +import util.ble.blesync + +_ADV_TYPE_FLAGS = const(0x01) +_ADV_TYPE_NAME = const(0x09) +_ADV_TYPE_UUID16_COMPLETE = const(0x3) +_ADV_TYPE_UUID32_COMPLETE = const(0x5) +_ADV_TYPE_UUID128_COMPLETE = const(0x7) +_ADV_TYPE_UUID16_MORE = const(0x2) +_ADV_TYPE_UUID32_MORE = const(0x4) +_ADV_TYPE_UUID128_MORE = const(0x6) +_ADV_TYPE_APPEARANCE = const(0x19) + + +# 0x00 - ADV_IND - connectable and scannable +# undirected +# advertising +# 0x01 - ADV_DIRECT_IND - connectable +# directed +# advertising +# 0x02 - ADV_SCAN_IND - scannable +# undirected +# advertising +# 0x03 - ADV_NONCONN_IND - non - connectable +# undirected +# advertising +# 0x04 - SCAN_RSP - scan +# response + +def _split_data(payload): + i = 0 + result = [] + data = memoryview(payload) + len_data = len(data) + while i < len_data: + length = data[i] + result.append(data[i + 1:i + 1 + length]) + i += length + 1 + return result + + +def parse_adv_data(payload): + return { + d[0]: d[1:] + for d in _split_data(payload) + } + + +def decode_adv_name(data): + try: + encoded_name = data[_ADV_TYPE_NAME] + except KeyError: + return '' + else: + return str(encoded_name, "utf-8") + + +def decode_adv_type(data): + try: + return data[_ADV_TYPE_FLAGS][0] + except KeyError: + return None + + +# TODO +# def decode_services(payload): +# services = [] +# for u in decode_field(payload, _ADV_TYPE_UUID16_COMPLETE): +# services.append(bluetooth.UUID(struct.unpack(" None: + ''' + Called if client is connected. + ..Note: + Client doesn't need to know the the service or the characteristics are not + discovered yet. So it can be useless to send the data to the client + from this method. + ''' + pass + + def on_disconnect(self, conn_handle, addr_type, addr): + ''' + Called if client is disconnected. + ..Note: + Client doesn't need to know the the service or the characteristics are not + discovered yet. So it can be useless to send the data to the client + from this method. + ''' + pass + + +class BLEServer: + def __init__(self, name, *service_classes, appearance=0): + self._service_classes = service_classes + self._service_by_handle = {} + self._services = [] + self._advertising_payload = advertising_payload( + name=name, + appearance=appearance + ) + + def _get_services_declarations(self): + return [ + (service_class.uuid, service_class.get_characteristics_declarations()) + for service_class in self._service_classes + ] + + def start(self): + blesync.active(True) + services_declarations = self._get_services_declarations() + all_handles = blesync.gatts_register_services(services_declarations) + + for handles, service_class in zip( + all_handles, + self._service_classes + ): + service = service_class(handles) + self._services.append(service) + for handle in handles: + self._service_by_handle[handle] = service + + blesync.on_central_connect(self._on_central_connect) + blesync.on_central_disconnect(self._on_central_disconnect) + blesync.on_gatts_write(self._on_gatts_write) + self._advertise() + + def _advertise(self, interval_us=500000): + blesync.gap_advertise(interval_us, adv_data=self._advertising_payload) + + def _on_gatts_write(self, conn_handle, value_handle): + service = self._service_by_handle[value_handle] + service._on_gatts_write(conn_handle, value_handle) + + def _on_central_connect(self, conn_handle, addr_type, addr): + for service in self._services: + service.on_connect(conn_handle, addr_type, addr) + self._advertise() # TODO optional + + def _on_central_disconnect(self, conn_handle, addr_type, addr): + for service in self._services: + service.on_disconnect(conn_handle, addr_type, addr) + # Start advertising again to allow a new connection. + self._advertise() diff --git a/esp32-micropython/util/ble/blesync_uart/__init__.py b/esp32-micropython/util/ble/blesync_uart/__init__.py new file mode 100644 index 00000000..fd40910d --- /dev/null +++ b/esp32-micropython/util/ble/blesync_uart/__init__.py @@ -0,0 +1,4 @@ + + + + diff --git a/esp32-micropython/util/ble/blesync_uart/client.py b/esp32-micropython/util/ble/blesync_uart/client.py new file mode 100644 index 00000000..994500df --- /dev/null +++ b/esp32-micropython/util/ble/blesync_uart/client.py @@ -0,0 +1,19 @@ +import bluetooth + +import util.ble.blesync_client + + +class UARTService(blesync_client.Service): + uuid = bluetooth.UUID("6E400001-B5A3-F393-E0A9-E50E24DCCA9E") + + _rx = blesync_client.Characteristic( + bluetooth.UUID("6E400002-B5A3-F393-E0A9-E50E24DCCA9E"), + ) + _tx = blesync_client.Characteristic( + bluetooth.UUID("6E400003-B5A3-F393-E0A9-E50E24DCCA9E"), + ) + + on_message = _tx.on_message + + def send(self, message): + self._rx.write(message) diff --git a/esp32-micropython/util/ble/blesync_uart/server.py b/esp32-micropython/util/ble/blesync_uart/server.py new file mode 100644 index 00000000..77230a1f --- /dev/null +++ b/esp32-micropython/util/ble/blesync_uart/server.py @@ -0,0 +1,26 @@ +import bluetooth + +import util.ble.blesync_server + + +class UARTService(blesync_server.Service): + uuid = bluetooth.UUID("6E400001-B5A3-F393-E0A9-E50E24DCCA9E") + + _tx = blesync_server.Characteristic( + bluetooth.UUID("6E400003-B5A3-F393-E0A9-E50E24DCCA9E"), + bluetooth.FLAG_READ | bluetooth.FLAG_NOTIFY, + ) + + _rx = blesync_server.Characteristic( + bluetooth.UUID("6E400002-B5A3-F393-E0A9-E50E24DCCA9E"), + bluetooth.FLAG_WRITE, + buffer_size=100, + append=True + ) + + characteristics = (_tx, _rx) + + on_message = _rx.on_message + + def send(self, conn_handle, message): + self._tx.notify(conn_handle, message) diff --git a/esp32-micropython/util/button.py b/esp32-micropython/util/button.py new file mode 100644 index 00000000..9baeb298 --- /dev/null +++ b/esp32-micropython/util/button.py @@ -0,0 +1,59 @@ +from machine import Pin, Timer +from micropython import schedule + + +class Button: + debounce_time_ms = 10 + + def __init__(self, pin_num: int, release_value=0): + self._on_press_callbacks = [] + self._on_release_callbacks = [] + self.pin = Pin(pin_num, Pin.IN, Pin.PULL_DOWN) # TODO pull + self._debounce_timer = Timer(1) + self._value = release_value + self._release_value = release_value + self._register_irq() + + def _register_irq(self): + self.pin.irq( + trigger=Pin.IRQ_RISING | Pin.IRQ_FALLING, + handler=self._irq, + ) + + def _irq(self, pin): + value = pin.value() + if value == self._value: + return + + pin.irq(trigger=0) + self._value = value + self._debounce_timer.init( + period=Button.debounce_time_ms, + mode=Timer.ONE_SHOT, + callback=self._debounce + ) + + def _debounce(self, _): + if self.pin.value() == self._value: + if self._value == self._release_value: + callback = self._on_release_callback + else: + callback = self._on_press_callback + schedule(callback, None) + self._register_irq() + + def _on_press_callback(self, _): + for callback in self._on_press_callbacks: + callback() + + def _on_release_callback(self, _): + for callback in self._on_release_callbacks: + callback() + + def on_press(self, callback): + self._on_press_callbacks.append(callback) + return callback + + def on_release(self, callback): + self._on_release_callbacks.append(callback) + return callback diff --git a/esp32-micropython/util/pubsub.py b/esp32-micropython/util/pubsub.py new file mode 100644 index 00000000..263a0951 --- /dev/null +++ b/esp32-micropython/util/pubsub.py @@ -0,0 +1,21 @@ +from micropython import schedule + + +class PubSub: + def __init__(self) -> None: + self._subscribers = {} + + def publish(self, name, *args, **kwargs) -> None: + if name not in self._subscribers: + return + + arg = (name, args, kwargs) + + for subscriber in self._subscribers[name]: + schedule(subscriber, arg) + + def subscribe(self, name, subscriber) -> None: + try: + self._subscribers[name].append(subscriber) + except KeyError: + self._subscribers[name] = [subscriber]