Skip to content
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

WIP: examples/bluetooth: Add BLE HID mouse and keyboard examples. #6559

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
125 changes: 125 additions & 0 deletions examples/bluetooth/ble_hid_keyboard.py
@@ -0,0 +1,125 @@
# Implements a BLE HID keyboard

from micropython import const
import struct
import bluetooth


def ble_irq(event, data):
global conn_handle
if event == 1:
print("connect")
conn_handle = data[0]
else:
print("event:", event, data)


ble = bluetooth.BLE()
ble.active(1)
ble.irq(ble_irq)

UUID = bluetooth.UUID

F_READ = bluetooth.FLAG_READ
F_WRITE = bluetooth.FLAG_WRITE
F_READ_WRITE = bluetooth.FLAG_READ | bluetooth.FLAG_WRITE
F_READ_NOTIFY = bluetooth.FLAG_READ | bluetooth.FLAG_NOTIFY

ATT_F_READ = 0x01
ATT_F_WRITE = 0x02

hid_service = (
UUID(0x1812), # Human Interface Device
(
(UUID(0x2A4A), F_READ), # HID information
(UUID(0x2A4B), F_READ), # HID report map
(UUID(0x2A4C), F_WRITE), # HID control point
(UUID(0x2A4D), F_READ_NOTIFY, ((UUID(0x2908), ATT_F_READ),)), # HID report / reference
(UUID(0x2A4D), F_READ_WRITE, ((UUID(0x2908), ATT_F_READ),)), # HID report / reference
(UUID(0x2A4E), F_READ_WRITE), # HID protocol mode
),
)

# fmt: off
HID_REPORT_MAP = bytes([
0x05, 0x01, # Usage Page (Generic Desktop)
0x09, 0x06, # Usage (Keyboard)
0xA1, 0x01, # Collection (Application)
0x85, 0x01, # Report ID (1)
0x75, 0x01, # Report Size (1)
0x95, 0x08, # Report Count (8)
0x05, 0x07, # Usage Page (Key Codes)
0x19, 0xE0, # Usage Minimum (224)
0x29, 0xE7, # Usage Maximum (231)
0x15, 0x00, # Logical Minimum (0)
0x25, 0x01, # Logical Maximum (1)
0x81, 0x02, # Input (Data, Variable, Absolute); Modifier byte
0x95, 0x01, # Report Count (1)
0x75, 0x08, # Report Size (8)
0x81, 0x01, # Input (Constant); Reserved byte
0x95, 0x05, # Report Count (5)
0x75, 0x01, # Report Size (1)
0x05, 0x08, # Usage Page (LEDs)
0x19, 0x01, # Usage Minimum (1)
0x29, 0x05, # Usage Maximum (5)
0x91, 0x02, # Output (Data, Variable, Absolute); LED report
0x95, 0x01, # Report Count (1)
0x75, 0x03, # Report Size (3)
0x91, 0x01, # Output (Constant); LED report padding
0x95, 0x06, # Report Count (6)
0x75, 0x08, # Report Size (8)
0x15, 0x00, # Logical Minimum (0)
0x25, 0x65, # Logical Maximum (101)
0x05, 0x07, # Usage Page (Key Codes)
0x19, 0x00, # Usage Minimum (0)
0x29, 0x65, # Usage Maximum (101)
0x81, 0x00, # Input (Data, Array); Key array (6 bytes)
0xC0, # End Collection
])
# fmt: on

# register services
ble.config(gap_name="MP-keyboard")
handles = ble.gatts_register_services((hid_service,))
print(handles)
h_info, h_hid, _, h_rep, h_d1, _, h_d2, h_proto = handles[0]

# set initial data
ble.gatts_write(h_info, b"\x01\x01\x00\x02") # HID info: ver=1.1, country=0, flags=normal
ble.gatts_write(h_hid, HID_REPORT_MAP) # HID report map
ble.gatts_write(h_d1, struct.pack("<BB", 1, 1)) # report: id=1, type=input
ble.gatts_write(h_d2, struct.pack("<BB", 1, 2)) # report: id=1, type=output
ble.gatts_write(h_proto, b"\x01") # protocol mode: report

# advertise
adv = (
b"\x02\x01\x06"
b"\x03\x03\x12\x18" # complete list of 16-bit service UUIDs: 0x1812
b"\x03\x19\xc1\x03" # appearance: keyboard
b"\x0c\x09MP-keyboard" # complete local name
)
conn_handle = None
ble.gap_advertise(100_000, adv)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

import ble_advertising
_ADV_APPEARANCE_KEYBOARD = const(961)
adv = ble_advertising.advertising_payload(services=[UUID(0x1812)], appearance=_ADV_APPEARANCE_KEYBOARD, name="MP-keyboard")


# once connected use the following to send reports


def send_char(char):
if char == " ":
mod = 0
code = 0x2C
elif ord("a") <= ord(char) <= ord("z"):
mod = 0
code = 0x04 + ord(char) - ord("a")
elif ord("A") <= ord(char) <= ord("Z"):
mod = 2
code = 0x04 + ord(char) - ord("A")
else:
assert 0
ble.gatts_notify(conn_handle, h_rep, struct.pack("8B", mod, 0, code, 0, 0, 0, 0, 0))
ble.gatts_notify(conn_handle, h_rep, b"\x00\x00\x00\x00\x00\x00\x00\x00")


def send_str(st):
for c in st:
send_char(c)
110 changes: 110 additions & 0 deletions examples/bluetooth/ble_hid_mouse.py
@@ -0,0 +1,110 @@
# Implements a BLE HID mouse

from micropython import const
import struct
import bluetooth


def ble_irq(event, data):
global conn_handle
if event == 1:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_IRQ_CENTRAL_CONNECT = const(1)
_IRQ_CENTRAL_DISCONNECT = const(2)
if event == _IRQ_CENTRAL_CONNECT:
    print("connect")
    conn_handle, _, _ = data
elif event == _IRQ_CENTRAL_DISCONNECT:
    conn_handle = None

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another suggestion would be to start again advertising upon disconnect.

def advertise():
    global conn_handle
    conn_handle = None
    adv = (
        b"\x02\x01\x06"
        b"\x03\x03\x12\x18"  # complete list of 16-bit service UUIDs: 0x1812
        b"\x03\x19\xc2\x03"  # appearance: mouse
        b"\x09\x09MP-mouse"  # complete local name
    )
    ble.gap_advertise(100_000, adv)

Create global variable conn_handle and call advertise() here (in disconnect) in addition to calling it after registering the service. Thanks for this example, it was exactly what I needed to get myself started!

print("connect")
conn_handle = data[0]
else:
print("event:", event, data)


ble = bluetooth.BLE()
ble.active(1)
ble.irq(ble_irq)

UUID = bluetooth.UUID

F_READ = bluetooth.FLAG_READ
F_WRITE = bluetooth.FLAG_WRITE
F_READ_WRITE = bluetooth.FLAG_READ | bluetooth.FLAG_WRITE
F_READ_NOTIFY = bluetooth.FLAG_READ | bluetooth.FLAG_NOTIFY

ATT_F_READ = 0x01
ATT_F_WRITE = 0x02

hid_service = (
UUID(0x1812), # Human Interface Device
(
(UUID(0x2A4A), F_READ), # HID information
(UUID(0x2A4B), F_READ), # HID report map
(UUID(0x2A4C), F_WRITE), # HID control point
(UUID(0x2A4D), F_READ_NOTIFY, ((UUID(0x2908), ATT_F_READ),)), # HID report / reference
(UUID(0x2A4E), F_READ_WRITE), # HID protocol mode
),
)

# fmt: off
HID_REPORT_MAP = bytes([
0x05, 0x01, # Usage Page (Generic Desktop)
0x09, 0x02, # Usage (Mouse)
0xA1, 0x01, # Collection (Application)
0x09, 0x01, # Usage (Pointer)
0xA1, 0x00, # Collection (Physical)
0x85, 0x01, # Report ID (1)
0x95, 0x03, # Report Count (3)
0x75, 0x01, # Report Size (1)
0x05, 0x09, # Usage Page (Buttons)
0x19, 0x01, # Usage Minimum (1)
0x29, 0x03, # Usage Maximum (3)
0x15, 0x00, # Logical Minimum (0)
0x25, 0x01, # Logical Maximum (1)
0x81, 0x02, # Input(Data, Variable, Absolute); 3 button bits
0x95, 0x01, # Report Count(1)
0x75, 0x05, # Report Size(5)
0x81, 0x01, # Input(Constant); 5 bit padding
0x75, 0x08, # Report Size (8)
0x95, 0x02, # Report Count (3)
0x05, 0x01, # Usage Page (Generic Desktop)
0x09, 0x30, # Usage (X)
0x09, 0x31, # Usage (Y)
0x09, 0x38, # Usage (Wheel)
0x15, 0x81, # Logical Minimum (-127)
0x25, 0x7F, # Logical Maximum (127)
0x81, 0x06, # Input(Data, Variable, Relative); 3 position bytes (X,Y,Wheel)
0xC0, # End Collection
0xC0, # End Collection
])
# fmt: on

# register services
ble.config(gap_name="MP-mouse")
handles = ble.gatts_register_services((hid_service,))
print(handles)
h_info, h_hid, _, h_rep, h_d1, h_proto = handles[0]

# set initial data
ble.gatts_write(h_info, b"\x01\x01\x00\x02") # HID info: ver=1.1, country=0, flags=normal
ble.gatts_write(h_hid, HID_REPORT_MAP) # HID report map
ble.gatts_write(h_d1, struct.pack("<BB", 1, 1)) # report: id=1, type=input
ble.gatts_write(h_proto, b"\x01") # protocol mode: report

# advertise
adv = (
b"\x02\x01\x06"
b"\x03\x03\x12\x18" # complete list of 16-bit service UUIDs: 0x1812
b"\x03\x19\xc2\x03" # appearance: mouse
b"\x09\x09MP-mouse" # complete local name
)
conn_handle = None
ble.gap_advertise(100_000, adv)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

import ble_advertising
_ADV_APPEARANCE_MOUSE = const(962)
adv = ble_advertising.advertising_payload(services=[UUID(0x1812)], appearance=_ADV_APPEARANCE_MOUSE, name="MP-mouse")

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I kind of wanted this example to be raw and just use the bluetooth module as-is, without any helpers. As a way to show exactly what's needed to make a BLE HID, to show there's no complexity hiding anywhere. IMO that's valuable for learning about BLE (and HID). That's also why I added mouse and keyboard as separate but very similar files.

So maybe we can have a "raw" example (perhaps just mouse, it's simpler) and then a more "friendly" version which has both keyboard and mouse support (selectable via some option).

And then eventually also a proper (async) HID library, but that's for the future.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, sounds good!


# once connected use the following to send reports


def send_mouse(button_mask, x, y, wheel):
ble.gatts_notify(conn_handle, h_rep, struct.pack("4B", button_mask, x, y, wheel))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if conn_handle is not None



def send_click(button):
send_mouse(1 << button, 0, 0, 0)
send_mouse(0, 0, 0, 0)


def send_motion(x, y):
send_mouse(0, x, y, 0)