Skip to content

Instantly share code, notes, and snippets.

@ktnyt
Created October 19, 2024 10:56
Show Gist options
  • Save ktnyt/33ad0462a0e7cc49176c78530e320291 to your computer and use it in GitHub Desktop.
Save ktnyt/33ad0462a0e7cc49176c78530e320291 to your computer and use it in GitHub Desktop.
import struct
import time
from math import atan2, degrees
import supervisor
import board
import digitalio
import busio
from usb_hid import Device
from hid_service import HIDService
from device_info_service import DeviceInfoService
from adafruit_lsm6ds.lsm6ds3 import LSM6DS3
import adafruit_ble
from adafruit_ble.advertising.standard import ProvideServicesAdvertisement
from adafruit_ble.advertising import Advertisement
supervisor.set_usb_identification(
vid=0x045E,
pid=0x02E0,
)
def vec2deg(x, y):
angle = degrees(atan2(y, x))
if angle < 0:
angle += 360
return angle
def inclination(sensor):
x, y, z = sensor.acceleration
return vec2deg(x, z), vec2deg(y, z)
def clamp(v, lower, upper):
return max(lower, min(v, upper))
# Setup LED to indicate that power is on.
led = digitalio.DigitalInOut(board.LED)
led.direction = digitalio.Direction.OUTPUT
led.value = True
GAMEPAD_REPORT_DESCRIPTOR = bytes([
0x05, 0x01, # Usage Page (Generic Desktop Ctrls)
0x09, 0x05, # Usage (Game Pad)
0xA1, 0x01, # Collection (Application)
0x85, 0x01, # Report ID (1)
0x09, 0x01, # Usage (Pointer)
0xA1, 0x00, # Collection (Physical)
0x09, 0x30, # Usage (X)
0x09, 0x31, # Usage (Y)
0x15, 0x00, # Logical Minimum (0)
0x27, 0xFF, 0xFF, 0x00, 0x00, # Logical Maximum (65534)
0x95, 0x02, # Report Count (2)
0x75, 0x10, # Report Size (16)
0x81, 0x02, # Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position)
0xC0, # End Collection
0x09, 0x01, # Usage (Pointer)
0xA1, 0x00, # Collection (Physical)
0x09, 0x33, # Usage (Rx)
0x09, 0x34, # Usage (Ry)
0x15, 0x00, # Logical Minimum (0)
0x27, 0xFF, 0xFF, 0x00, 0x00, # Logical Maximum (65534)
0x95, 0x02, # Report Count (2)
0x75, 0x10, # Report Size (16)
0x81, 0x02, # Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position)
0xC0, # End Collection
0x05, 0x02, # Usage Page (Generic Desktop Ctrls)
0x09, 0x32, # Usage (Z)
0x15, 0x00, # Logical Minimum (0)
0x26, 0xFF, 0x03, # Logical Maximum (1023)
0x95, 0x01, # Report Count (1)
0x75, 0x0A, # Report Size (10)
0x81, 0x02, # Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position)
0x15, 0x00, # Logical Minimum (0)
0x25, 0x00, # Logical Maximum (0)
0x75, 0x06, # Report Size (6)
0x95, 0x01, # Report Count (1)
0x81, 0x03, # Input (Const,Var,Abs,No Wrap,Linear,Preferred State,No Null Position)
0x05, 0x01, # Usage Page (Generic Desktop Ctrls)
0x09, 0x35, # Usage (Rz)
0x15, 0x00, # Logical Minimum (0)
0x26, 0xFF, 0x03, # Logical Maximum (1023)
0x95, 0x01, # Report Count (1)
0x75, 0x0A, # Report Size (10)
0x81, 0x02, # Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position)
0x15, 0x00, # Logical Minimum (0)
0x25, 0x00, # Logical Maximum (0)
0x75, 0x06, # Report Size (6)
0x95, 0x01, # Report Count (1)
0x81, 0x03, # Input (Const,Var,Abs,No Wrap,Linear,Preferred State,No Null Position)
0x05, 0x01, # Usage Page (Generic Desktop Ctrls)
0x09, 0x39, # Usage (Hat switch)
0x15, 0x01, # Logical Minimum (1)
0x25, 0x08, # Logical Maximum (8)
0x35, 0x00, # Physical Minimum (0)
0x46, 0x3B, 0x01, # Physical Maximum (315)
0x66, 0x14, 0x00, # Unit (System: English Rotation, Length: Centimeter)
0x75, 0x04, # Report Size (4)
0x95, 0x01, # Report Count (1)
0x81, 0x42, # Input (Data,Var,Abs,No Wrap,Linear,Preferred State,Null State)
0x75, 0x04, # Report Size (4)
0x95, 0x01, # Report Count (1)
0x15, 0x00, # Logical Minimum (0)
0x25, 0x00, # Logical Maximum (0)
0x35, 0x00, # Physical Minimum (0)
0x45, 0x00, # Physical Maximum (0)
0x65, 0x00, # Unit (None)
0x81, 0x03, # Input (Const,Var,Abs,No Wrap,Linear,Preferred State,No Null Position)
0x05, 0x09, # Usage Page (Button)
0x19, 0x01, # Usage Minimum (0x01)
0x29, 0x0A, # Usage Maximum (0x0A)
0x15, 0x00, # Logical Minimum (0)
0x25, 0x01, # Logical Maximum (1)
0x75, 0x01, # Report Size (1)
0x95, 0x0A, # Report Count (10)
0x81, 0x02, # Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position)
0x15, 0x00, # Logical Minimum (0)
0x25, 0x00, # Logical Maximum (0)
0x75, 0x06, # Report Size (6)
0x95, 0x01, # Report Count (1)
0x81, 0x03, # Input (Const,Var,Abs,No Wrap,Linear,Preferred State,No Null Position)
0x05, 0x01, # Usage Page (Generic Desktop Ctrls)
0x09, 0x80, # Usage (Sys Control)
0x85, 0x02, # Report ID (2)
0xA1, 0x00, # Collection (Physical)
0x09, 0x85, # Usage (Sys Main Menu)
0x15, 0x00, # Logical Minimum (0)
0x25, 0x01, # Logical Maximum (1)
0x95, 0x01, # Report Count (1)
0x75, 0x01, # Report Size (1)
0x81, 0x02, # Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position)
0x15, 0x00, # Logical Minimum (0)
0x25, 0x00, # Logical Maximum (0)
0x75, 0x07, # Report Size (7)
0x95, 0x01, # Report Count (1)
0x81, 0x03, # Input (Const,Var,Abs,No Wrap,Linear,Preferred State,No Null Position)
0xC0, # End Collection
0x05, 0x0F, # Usage Page (PID Page)
0x09, 0x21, # Usage (0x21)
0x85, 0x03, # Report ID (3)
0xA1, 0x02, # Collection (Logical)
0x09, 0x97, # Usage (0x97)
0x15, 0x00, # Logical Minimum (0)
0x25, 0x01, # Logical Maximum (1)
0x75, 0x04, # Report Size (4)
0x95, 0x01, # Report Count (1)
0x91, 0x02, # Output (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position,Non-volatile)
0x15, 0x00, # Logical Minimum (0)
0x25, 0x00, # Logical Maximum (0)
0x75, 0x04, # Report Size (4)
0x95, 0x01, # Report Count (1)
0x91, 0x03, # Output (Const,Var,Abs,No Wrap,Linear,Preferred State,No Null Position,Non-volatile)
0x09, 0x70, # Usage (0x70)
0x15, 0x00, # Logical Minimum (0)
0x25, 0x64, # Logical Maximum (100)
0x75, 0x08, # Report Size (8)
0x95, 0x04, # Report Count (4)
0x91, 0x02, # Output (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position,Non-volatile)
0x09, 0x50, # Usage (0x50)
0x66, 0x01, 0x10, # Unit (System: SI Linear, Time: Seconds)
0x55, 0x0E, # Unit Exponent (-2)
0x15, 0x00, # Logical Minimum (0)
0x26, 0xFF, 0x00, # Logical Maximum (255)
0x75, 0x08, # Report Size (8)
0x95, 0x01, # Report Count (1)
0x91, 0x02, # Output (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position,Non-volatile)
0x09, 0xA7, # Usage (0xA7)
0x15, 0x00, # Logical Minimum (0)
0x26, 0xFF, 0x00, # Logical Maximum (255)
0x75, 0x08, # Report Size (8)
0x95, 0x01, # Report Count (1)
0x91, 0x02, # Output (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position,Non-volatile)
0x65, 0x00, # Unit (None)
0x55, 0x00, # Unit Exponent (0)
0x09, 0x7C, # Usage (0x7C)
0x15, 0x00, # Logical Minimum (0)
0x26, 0xFF, 0x00, # Logical Maximum (255)
0x75, 0x08, # Report Size (8)
0x95, 0x01, # Report Count (1)
0x91, 0x02, # Output (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position,Non-volatile)
0xC0, # End Collection
0x85, 0x04, # Report ID (4)
0x05, 0x06, # Usage Page (Generic Dev Ctrls)
0x09, 0x20, # Usage (Battery Strength)
0x15, 0x00, # Logical Minimum (0)
0x26, 0xFF, 0x00, # Logical Maximum (255)
0x75, 0x08, # Report Size (8)
0x95, 0x01, # Report Count (1)
0x81, 0x02, # Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position)
0xC0, # End Collection
])
hid = HIDService(hid_descriptor=GAMEPAD_REPORT_DESCRIPTOR)
device_info = DeviceInfoService(
software_revision=adafruit_ble.__version__,
pnp_id=(0x02, 0x045E, 0x02E0, 0x0100),
)
# Setup advertisement as Gamepad (0x03C4)
# https:#www.bluetooth.com/specifications/assigned-numbers/
advertisement = ProvideServicesAdvertisement(device_info, hid)
advertisement.appearance = 0x03C4
scan_response = Advertisement()
scan_response.complete_name = "Xbox Wireless Controller"
ble = adafruit_ble.BLERadio()
ble.name = "Xbox Wireless Controller"
if not ble.connected:
print("advertising")
ble.start_advertising(advertisement, scan_response)
else:
print("already connected")
# Setup LSM6DS3 sensor
dpwr = digitalio.DigitalInOut(board.IMU_PWR)
dpwr.direction = digitalio.Direction.OUTPUT
dpwr.value = 1
time.sleep(1)
i2c = busio.I2C(board.IMU_SCL, board.IMU_SDA)
sensor = LSM6DS3(i2c)
def normalize(v, vmin, vmax):
return (clamp(v, vmin, vmax) - vmin) / (vmax - vmin)
def find_device(devices, usage_page, usage, report_id):
if hasattr(devices, "send_report"):
devices = [devices] # type: ignore
device = None
for dev in devices:
if (
dev.usage_page == usage_page
and dev.usage == usage
and dev._report_id == report_id
and hasattr(dev, "send_report")
):
device = dev
break
if device is None:
raise ValueError("Could not find matching HID device.")
# Wait for USB to be connected only if this is a usb_hid.Device.
if Device and isinstance(device, Device):
if supervisor is None:
# Blinka doesn't have supervisor (see issue Adafruit_Blinka#711), so wait
# one second for USB to become ready
time.sleep(1.0)
elif timeout is None:
# default behavior: wait indefinitely for USB to become ready
while not supervisor.runtime.usb_connected:
time.sleep(1.0)
else:
# wait up to timeout seconds for USB to become ready
for _ in range(timeout):
if supervisor.runtime.usb_connected:
return device
time.sleep(1.0)
raise OSError("Failed to initialize HID device. Is USB connected?")
return device
try:
while True:
while not ble.connected:
led.value = True
time.sleep(0.25)
led.value = False
time.sleep(0.25)
print("connected")
device = find_device(hid.devices, usage_page=0x1, usage=0x05, report_id=0x01)
print(device.__dict__)
report = bytearray(15)
init_xz, init_yz = inclination(sensor)
while ble.connected:
led.value = True
ax, ay, az = sensor.acceleration
xz, yz = inclination(sensor)
xz = int(normalize(xz - init_xz, -30, 30) * 65535)
yz = int(normalize(yz - init_yz, -30, 30) * 65535)
print(xz, yz, "\t", end="\r")
struct.pack_into(
'<HHHHHHBH',
report,
0, # Offset
0x7fff, # LX
0x7fff, # LY
xz,
yz,
0,
0,
0x00, # D-pad
0x0000, # Buttons
)
device.send_report(report)
t_p = time.monotonic_ns()
time.sleep(1/10)
ble.start_advertising(advertisement)
finally:
i2c.unlock()
import binascii
import os
import sys
from adafruit_ble.services import Service
from adafruit_ble.characteristics import StructCharacteristic
from adafruit_ble.characteristics.string import FixedStringCharacteristic
from adafruit_ble.uuid import StandardUUID
class DeviceInfoService(Service):
"""Device information"""
uuid = StandardUUID(0x180A)
model_number = FixedStringCharacteristic(uuid=StandardUUID(0x2A24))
serial_number = FixedStringCharacteristic(uuid=StandardUUID(0x2A25))
firmware_revision = FixedStringCharacteristic(uuid=StandardUUID(0x2A26))
hardware_revision = FixedStringCharacteristic(uuid=StandardUUID(0x2A27))
software_revision = FixedStringCharacteristic(uuid=StandardUUID(0x2A28))
manufacturer = FixedStringCharacteristic(uuid=StandardUUID(0x2A29))
pnp_id = StructCharacteristic('<BHHH', uuid=StandardUUID(0x2A50))
def __init__(
self,
*,
manufacturer = None,
software_revision = None,
model_number = None,
serial_number = None,
firmware_revision = None,
hardware_revision = None,
pnp_id = None,
service = None,
) -> None:
if not service:
if model_number is None:
model_number = sys.platform
if serial_number is None:
try:
import microcontroller # pylint: disable=import-outside-toplevel
serial_number = binascii.hexlify(
microcontroller.cpu.uid # pylint: disable=no-member
).decode("utf-8")
except ImportError:
pass
if firmware_revision is None:
firmware_revision = getattr(os.uname(), "version", None)
super().__init__(
manufacturer=manufacturer,
software_revision=software_revision,
model_number=model_number,
serial_number=serial_number,
firmware_revision=firmware_revision,
hardware_revision=hardware_revision,
pnp_id=pnp_id,
service=service,
)
# SPDX-FileCopyrightText: 2019 Dan Halbert for Adafruit Industries
#
# SPDX-License-Identifier: MIT
"""
:py:mod:`~adafruit_ble.services.standard.hid`
=======================================================
BLE Human Interface Device (HID)
* Author(s): Dan Halbert for Adafruit Industries
"""
from __future__ import annotations
import struct
import _bleio
from micropython import const
from adafruit_ble.characteristics import Attribute, Characteristic
from adafruit_ble.characteristics.int import Uint8Characteristic
from adafruit_ble.uuid import StandardUUID
from adafruit_ble.services import Service
try:
from typing import Dict, Optional
except ImportError:
pass
__version__ = "0.0.0+auto.0"
__repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_BLE.git"
_HID_SERVICE_UUID_NUM = const(0x1812)
_REPORT_UUID_NUM = const(0x2A4D)
_REPORT_MAP_UUID_NUM = const(0x2A4B)
_HID_INFORMATION_UUID_NUM = const(0x2A4A)
_HID_CONTROL_POINT_UUID_NUM = const(0x2A4C)
_REPORT_REF_DESCR_UUID_NUM = const(0x2908)
_REPORT_REF_DESCR_UUID = _bleio.UUID(_REPORT_REF_DESCR_UUID_NUM)
_PROTOCOL_MODE_UUID_NUM = const(0x2A4E)
_APPEARANCE_HID_KEYBOARD = const(961)
_APPEARANCE_HID_MOUSE = const(962)
_APPEARANCE_HID_JOYSTICK = const(963)
_APPEARANCE_HID_GAMEPAD = const(964)
# pylint: disable=line-too-long
DEFAULT_HID_DESCRIPTOR = (
b"\x05\x01" # Usage Page (Generic Desktop Ctrls)
b"\x09\x06" # Usage (Keyboard)
b"\xA1\x01" # Collection (Application)
b"\x85\x01" # Report ID (1)
b"\x05\x07" # Usage Page (Kbrd/Keypad)
b"\x19\xE0" # Usage Minimum (\xE0)
b"\x29\xE7" # Usage Maximum (\xE7)
b"\x15\x00" # Logical Minimum (0)
b"\x25\x01" # Logical Maximum (1)
b"\x75\x01" # Report Size (1)
b"\x95\x08" # Report Count (8)
b"\x81\x02" # Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position)
b"\x81\x01" # Input (Const,Array,Abs,No Wrap,Linear,Preferred State,No Null Position)
b"\x19\x00" # Usage Minimum (\x00)
b"\x29\x89" # Usage Maximum (\x89)
b"\x15\x00" # Logical Minimum (0)
b"\x25\x89" # Logical Maximum (137)
b"\x75\x08" # Report Size (8)
b"\x95\x06" # Report Count (6)
b"\x81\x00" # Input (Data,Array,Abs,No Wrap,Linear,Preferred State,No Null Position)
b"\x05\x08" # Usage Page (LEDs)
b"\x19\x01" # Usage Minimum (Num Lock)
b"\x29\x05" # Usage Maximum (Kana)
b"\x15\x00" # Logical Minimum (0)
b"\x25\x01" # Logical Maximum (1)
b"\x75\x01" # Report Size (1)
b"\x95\x05" # Report Count (5)
b"\x91\x02" # Output (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position,Non-volatile)
b"\x95\x03" # Report Count (3)
b"\x91\x01" # Output (Const,Array,Abs,No Wrap,Linear,Preferred State,No Null Position,Non-volatile)
b"\xC0" # End Collection
b"\x05\x01" # Usage Page (Generic Desktop Ctrls)
b"\x09\x02" # Usage (Mouse)
b"\xA1\x01" # Collection (Application)
b"\x09\x01" # Usage (Pointer)
b"\xA1\x00" # Collection (Physical)
b"\x85\x02" # Report ID (2)
b"\x05\x09" # Usage Page (Button)
b"\x19\x01" # Usage Minimum (\x01)
b"\x29\x05" # Usage Maximum (\x05)
b"\x15\x00" # Logical Minimum (0)
b"\x25\x01" # Logical Maximum (1)
b"\x95\x05" # Report Count (5)
b"\x75\x01" # Report Size (1)
b"\x81\x02" # Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position)
b"\x95\x01" # Report Count (1)
b"\x75\x03" # Report Size (3)
b"\x81\x01" # Input (Const,Array,Abs,No Wrap,Linear,Preferred State,No Null Position)
b"\x05\x01" # Usage Page (Generic Desktop Ctrls)
b"\x09\x30" # Usage (X)
b"\x09\x31" # Usage (Y)
b"\x15\x81" # Logical Minimum (-127)
b"\x25\x7F" # Logical Maximum (127)
b"\x75\x08" # Report Size (8)
b"\x95\x02" # Report Count (2)
b"\x81\x06" # Input (Data,Var,Rel,No Wrap,Linear,Preferred State,No Null Position)
b"\x09\x38" # Usage (Wheel)
b"\x15\x81" # Logical Minimum (-127)
b"\x25\x7F" # Logical Maximum (127)
b"\x75\x08" # Report Size (8)
b"\x95\x01" # Report Count (1)
b"\x81\x06" # Input (Data,Var,Rel,No Wrap,Linear,Preferred State,No Null Position)
b"\xC0" # End Collection
b"\xC0" # End Collection
b"\x05\x0C" # Usage Page (Consumer)
b"\x09\x01" # Usage (Consumer Control)
b"\xA1\x01" # Collection (Application)
b"\x85\x03" # Report ID (3)
b"\x75\x10" # Report Size (16)
b"\x95\x01" # Report Count (1)
b"\x15\x01" # Logical Minimum (1)
b"\x26\x8C\x02" # Logical Maximum (652)
b"\x19\x01" # Usage Minimum (Consumer Control)
b"\x2A\x8C\x02" # Usage Maximum (AC Send)
b"\x81\x00" # Input (Data,Array,Abs,No Wrap,Linear,Preferred State,No Null Position)
b"\xC0" # End Collection
# b'\x05\x01' # Usage Page (Generic Desktop Ctrls)
# b'\x09\x05' # Usage (Game Pad)
# b'\xA1\x01' # Collection (Application)
# b'\x85\x05' # Report ID (5)
# b'\x05\x09' # Usage Page (Button)
# b'\x19\x01' # Usage Minimum (\x01)
# b'\x29\x10' # Usage Maximum (\x10)
# b'\x15\x00' # Logical Minimum (0)
# b'\x25\x01' # Logical Maximum (1)
# b'\x75\x01' # Report Size (1)
# b'\x95\x10' # Report Count (16)
# b'\x81\x02' # Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position)
# b'\x05\x01' # Usage Page (Generic Desktop Ctrls)
# b'\x15\x81' # Logical Minimum (-127)
# b'\x25\x7F' # Logical Maximum (127)
# b'\x09\x30' # Usage (X)
# b'\x09\x31' # Usage (Y)
# b'\x09\x32' # Usage (Z)
# b'\x09\x35' # Usage (Rz)
# b'\x75\x08' # Report Size (8)
# b'\x95\x04' # Report Count (4)
# b'\x81\x02' # Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position)
# b'\xC0' # End Collection
)
"""Default HID descriptor: provides mouse, keyboard, and consumer control devices."""
# pylint: enable=line-too-long
# Boot keyboard and mouse not currently supported.
_BOOT_KEYBOARD_INPUT_REPORT_UUID_NUM = const(0x2A22)
_BOOT_KEYBOARD_OUTPUT_REPORT_UUID_NUM = const(0x2A32)
_BOOT_MOUSE_INPUT_REPORT_UUID_NUM = const(0x2A33)
# Output reports not currently implemented (e.g. LEDs on keyboard)
_REPORT_TYPE_INPUT = const(1)
_REPORT_TYPE_OUTPUT = const(2)
# Boot Protocol mode not currently implemented
_PROTOCOL_MODE_BOOT = b"\x00"
_PROTOCOL_MODE_REPORT = b"\x01"
class ReportIn:
"""A single HID report that transmits HID data into a client."""
uuid = StandardUUID(_REPORT_UUID_NUM)
def __init__(
self,
service: Service,
report_id: int,
usage_page: bytes,
usage: bytes,
*,
max_length: int,
) -> None:
self._characteristic = _bleio.Characteristic.add_to_service(
service.bleio_service,
self.uuid.bleio_uuid,
properties=Characteristic.READ | Characteristic.NOTIFY,
read_perm=Attribute.ENCRYPT_NO_MITM,
write_perm=Attribute.NO_ACCESS,
max_length=max_length,
fixed_length=True,
)
self._report_id = report_id
self.usage_page = usage_page
self.usage = usage
_bleio.Descriptor.add_to_characteristic(
self._characteristic,
_REPORT_REF_DESCR_UUID,
read_perm=Attribute.ENCRYPT_NO_MITM,
write_perm=Attribute.NO_ACCESS,
initial_value=struct.pack("<BB", self._report_id, _REPORT_TYPE_INPUT),
)
def send_report(self, report: Dict) -> None:
"""Send a report to the peers"""
self._characteristic.value = report
class ReportOut:
"""A single HID report that receives HID data from a client."""
# pylint: disable=too-few-public-methods
uuid = StandardUUID(_REPORT_UUID_NUM)
def __init__(
self,
service: Service,
report_id: int,
usage_page: bytes,
usage: bytes,
*,
max_length: int,
) -> None:
self._characteristic = _bleio.Characteristic.add_to_service(
service.bleio_service,
self.uuid.bleio_uuid,
max_length=max_length,
fixed_length=True,
properties=(
Characteristic.READ
| Characteristic.WRITE
| Characteristic.WRITE_NO_RESPONSE
),
read_perm=Attribute.ENCRYPT_NO_MITM,
write_perm=Attribute.ENCRYPT_NO_MITM,
)
self._report_id = report_id
self.usage_page = usage_page
self.usage = usage
_bleio.Descriptor.add_to_characteristic(
self._characteristic,
_REPORT_REF_DESCR_UUID,
read_perm=Attribute.ENCRYPT_NO_MITM,
write_perm=Attribute.NO_ACCESS,
initial_value=struct.pack("<BB", self._report_id, _REPORT_TYPE_OUTPUT),
)
@property
def report(self) -> Dict:
"""The HID OUT report"""
return self._characteristic.value
_ITEM_TYPE_MAIN = const(0)
_ITEM_TYPE_GLOBAL = const(1)
_ITEM_TYPE_LOCAL = const(2)
_MAIN_ITEM_TAG_START_COLLECTION = const(0b1010)
_MAIN_ITEM_TAG_END_COLLECTION = const(0b1100)
_MAIN_ITEM_TAG_INPUT = const(0b1000)
_MAIN_ITEM_TAG_OUTPUT = const(0b1001)
_MAIN_ITEM_TAG_FEATURE = const(0b1011)
class HIDService(Service):
"""
Provide devices for HID over BLE.
:param str hid_descriptor: USB HID descriptor that describes the structure of the reports. Known
as the report map in BLE HID.
Example::
from adafruit_ble.hid_server import HIDServer
hid = HIDServer()
"""
uuid = StandardUUID(0x1812)
boot_keyboard_in = Characteristic(
uuid=StandardUUID(0x2A22),
properties=(Characteristic.READ | Characteristic.NOTIFY),
read_perm=Attribute.ENCRYPT_NO_MITM,
write_perm=Attribute.NO_ACCESS,
max_length=8,
fixed_length=True,
)
boot_keyboard_out = Characteristic(
uuid=StandardUUID(0x2A32),
properties=(
Characteristic.READ
| Characteristic.WRITE
| Characteristic.WRITE_NO_RESPONSE
),
read_perm=Attribute.ENCRYPT_NO_MITM,
write_perm=Attribute.ENCRYPT_NO_MITM,
max_length=1,
fixed_length=True,
)
protocol_mode = Uint8Characteristic(
uuid=StandardUUID(0x2A4E),
properties=(Characteristic.READ | Characteristic.WRITE_NO_RESPONSE),
read_perm=Attribute.OPEN,
write_perm=Attribute.OPEN,
initial_value=1,
max_value=1,
)
"""Protocol mode: boot (0) or report (1)"""
# bcdHID (version), bCountryCode (0 not localized), Flags: RemoteWake, NormallyConnectable
# bcd1.1, country = 0, flag = normal connect
# TODO: Make this a struct.
hid_information = Characteristic(
uuid=StandardUUID(0x2A4A),
properties=Characteristic.READ,
read_perm=Attribute.ENCRYPT_NO_MITM,
write_perm=Attribute.NO_ACCESS,
initial_value=b"\x01\x01\x00\x02",
)
"""Hid information including version, country code and flags."""
report_map = Characteristic(
uuid=StandardUUID(0x2A4B),
properties=Characteristic.READ,
read_perm=Attribute.ENCRYPT_NO_MITM,
write_perm=Attribute.NO_ACCESS,
fixed_length=True,
)
"""This is the USB HID descriptor (not to be confused with a BLE Descriptor). It describes
which report characteristic are what."""
suspended = Uint8Characteristic(
uuid=StandardUUID(0x2A4C),
properties=Characteristic.WRITE_NO_RESPONSE,
read_perm=Attribute.NO_ACCESS,
write_perm=Attribute.ENCRYPT_NO_MITM,
max_value=1,
)
"""Controls whether the device should be suspended (0) or not (1)."""
def __init__(
self,
hid_descriptor: bytes = DEFAULT_HID_DESCRIPTOR,
service: Optional[Service] = None,
) -> None:
super().__init__(report_map=hid_descriptor)
if service:
# TODO: Add support for connecting to a remote hid server.
pass
self._init_devices()
def _init_devices(self) -> None:
# pylint: disable=too-many-branches,too-many-statements,too-many-locals
self.devices = []
hid_descriptor = self.report_map
global_table = [None] * 10
local_table = [None] * 3
collections = []
top_level_collections = []
i = 0
while i < len(hid_descriptor):
b = hid_descriptor[i]
tag = (b & 0xF0) >> 4
_type = (b & 0b1100) >> 2
size = b & 0b11
size = 4 if size == 3 else size
i += 1
data = hid_descriptor[i : i + size]
if _type == _ITEM_TYPE_GLOBAL:
global_table[tag] = data
elif _type == _ITEM_TYPE_MAIN:
if tag == _MAIN_ITEM_TAG_START_COLLECTION:
collections.append(
{
"type": data,
"locals": list(local_table),
"globals": list(global_table),
"mains": [],
}
)
elif tag == _MAIN_ITEM_TAG_END_COLLECTION:
collection = collections.pop()
# This is a top level collection if the collections list is now empty.
if not collections:
top_level_collections.append(collection)
else:
collections[-1]["mains"].append(collection)
elif tag == _MAIN_ITEM_TAG_INPUT:
collections[-1]["mains"].append(
{
"tag": "input",
"locals": list(local_table),
"globals": list(global_table),
}
)
elif tag == _MAIN_ITEM_TAG_OUTPUT:
collections[-1]["mains"].append(
{
"tag": "output",
"locals": list(local_table),
"globals": list(global_table),
}
)
else:
raise RuntimeError("Unsupported main item in HID descriptor")
local_table = [None] * 3
else:
local_table[tag] = data
i += size
def get_report_info(collection: Dict, reports: Dict) -> None:
"""Gets info about hid reports"""
for main in collection["mains"]:
if "type" in main:
get_report_info(main, reports)
else:
report_size, report_id, report_count = [
x[0] for x in main["globals"][7:10]
]
if report_id not in reports:
reports[report_id] = {"input_size": 0, "output_size": 0}
if main["tag"] == "input":
reports[report_id]["input_size"] += report_size * report_count
elif main["tag"] == "output":
reports[report_id]["output_size"] += report_size * report_count
for collection in top_level_collections:
if collection["type"][0] != 1:
raise NotImplementedError(
"Only Application top level collections supported."
)
usage_page = collection["globals"][0][0]
usage = collection["locals"][0][0]
reports = {}
get_report_info(collection, reports)
for report_id, report in reports.items():
output_size = report["output_size"]
if output_size > 0:
self.devices.append(
ReportOut(
self, report_id, usage_page, usage, max_length=output_size // 8
)
)
input_size = reports[report_id]["input_size"]
if input_size > 0:
self.devices.append(
ReportIn(
self, report_id, usage_page, usage, max_length=input_size // 8
)
)
@ThomasAtBBTF
Copy link

What board are you using in the example / video ?
And does the communication take place over BLE or USB?
Would both alternatives be possible?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment