Created
October 19, 2024 10:56
-
-
Save ktnyt/33ad0462a0e7cc49176c78530e320291 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | |
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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 | |
) | |
) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
IT'S WORKING
https://gist.github.com/user-attachments/assets/0d9bada6-8107-48bd-a974-c9b25e512edb