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
)
)
@ktnyt
Copy link
Author

ktnyt commented Dec 1, 2024

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