From 6a5073ab25ef8388b2f9052bcda451056cea8ded Mon Sep 17 00:00:00 2001 From: Taiki Komoda Date: Fri, 7 Nov 2025 04:10:09 +0000 Subject: [PATCH 1/3] Implemented ble hid Device class --- adafruit_ble/services/standard/hid.py | 117 +++++++++++++++++++++----- examples/ble_hid_periph.py | 1 + 2 files changed, 98 insertions(+), 20 deletions(-) diff --git a/adafruit_ble/services/standard/hid.py b/adafruit_ble/services/standard/hid.py index 03f4dcf..c342240 100755 --- a/adafruit_ble/services/standard/hid.py +++ b/adafruit_ble/services/standard/hid.py @@ -26,7 +26,7 @@ from .. import Service try: - from typing import Dict, Optional + from typing import Dict, Optional, Sequence except ImportError: pass @@ -248,6 +248,69 @@ def report(self) -> Dict: return self._characteristic.value +class Device: + """Container that groups multiple ReportIn and ReportOut objects for a given + usage_page/usage. + + Each device may have multiple report IDs. This class keeps mappings from + report_id -> ReportIn/ReportOut and provides convenience methods that use + the first-added report_id when none is specified. + """ + + def __init__(self, usage_page, usage): + self._usage_page = usage_page + self._usage = usage + # Maintain insertion order of report_ids + self._report_id_order = [] # list of report_id in insertion order + self._report_ins = {} # report_id -> ReportIn + self._report_outs = {} # report_id -> ReportOut + + @property + def usage_page(self): + return self._usage_page + + @property + def usage(self): + return self._usage + + def add_report_in(self, report_id: int, report_in: ReportIn) -> None: + """Register a ReportIn instance for a report_id.""" + if report_id not in self._report_id_order: + self._report_id_order.append(report_id) + self._report_ins[report_id] = report_in + + def add_report_out(self, report_id: int, report_out: ReportOut) -> None: + """Register a ReportOut instance for a report_id.""" + if report_id not in self._report_id_order: + self._report_id_order.append(report_id) + self._report_outs[report_id] = report_out + + def _first_report_id(self): + return self._report_id_order[0] if self._report_id_order else None + + def send_report(self, report: Dict, report_id: int | None = None) -> None: + """Send a report via the ReportIn class. + + If report_id is None, uses the first-added report_id for this device. + Raises RuntimeError if no matching ReportIn exists. + """ + if report_id is None: + report_id = self._first_report_id() + if report_id is None or report_id not in self._report_ins: + raise RuntimeError(f"No input report available for report_id {report_id}") + self._report_ins[report_id].send_report(report) + + def get_last_received_report(self, report_id: int | None = None): + """Return the last OUT report received. + If report_id is None, uses the first-added report_id for this device. + """ + if report_id is None: + report_id = self._first_report_id() + if report_id is None or report_id not in self._report_outs: + return None + return self._report_outs[report_id].report + + _ITEM_TYPE_MAIN = const(0) _ITEM_TYPE_GLOBAL = const(1) _ITEM_TYPE_LOCAL = const(2) @@ -268,9 +331,9 @@ class HIDService(Service): Example:: - from adafruit_ble.hid_server import HIDServer + from adafruit_ble.services.standard.hid import HIDService - hid = HIDServer() + hid = HIDService() """ uuid = StandardUUID(0x1812) @@ -429,25 +492,39 @@ def get_report_info(collection: Dict, reports: Dict) -> None: get_report_info(collection, reports) for report_id, report in reports.items(): output_size = report["output_size"] + # Group ReportIn and ReportOut by usage_page and usage into a Devices + input_size = report["input_size"] + + # Find an existing device with same usage_page and usage + device = None + for d in self.devices: + # Device instances expose usage_page and usage properties + if d.usage_page == usage_page and d.usage == usage: + device = d + break + + if device is None: + device = Device(usage_page, usage) + self.devices.append(device) + if output_size > 0: - self.devices.append( - ReportOut( - self, - report_id, - usage_page, - usage, - max_length=output_size // 8, - ) + # Create ReportOut and attach to device + report_out = ReportOut( + self, + report_id, + usage_page, + usage, + max_length=output_size // 8, ) + device.add_report_out(report_id, report_out) - input_size = report["input_size"] if input_size > 0: - self.devices.append( - ReportIn( - self, - report_id, - usage_page, - usage, - max_length=input_size // 8, - ) + # Create ReportIn and attach to device + report_in = ReportIn( + self, + report_id, + usage_page, + usage, + max_length=input_size // 8, ) + device.add_report_in(report_id, report_in) diff --git a/examples/ble_hid_periph.py b/examples/ble_hid_periph.py index 6224a95..6166260 100644 --- a/examples/ble_hid_periph.py +++ b/examples/ble_hid_periph.py @@ -46,5 +46,6 @@ sys.stdout.write(c) kl.write(c) # print("sleeping") + # print(f"{k.led_status}") #read led_status time.sleep(0.1) ble.start_advertising(advertisement) From 266ca15693970bba400175066e64863caebc4d23 Mon Sep 17 00:00:00 2001 From: Taiki Komoda Date: Fri, 14 Nov 2025 01:40:57 +0000 Subject: [PATCH 2/3] Refactor Device class to have separate first report_id --- adafruit_ble/services/standard/hid.py | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/adafruit_ble/services/standard/hid.py b/adafruit_ble/services/standard/hid.py index c342240..560a872 100755 --- a/adafruit_ble/services/standard/hid.py +++ b/adafruit_ble/services/standard/hid.py @@ -285,8 +285,19 @@ def add_report_out(self, report_id: int, report_out: ReportOut) -> None: self._report_id_order.append(report_id) self._report_outs[report_id] = report_out - def _first_report_id(self): - return self._report_id_order[0] if self._report_id_order else None + def _first_report_in_id(self): + """Return the first report_id that has an input (ReportIn).""" + for rid in self._report_id_order: + if rid in self._report_ins: + return rid + return None + + def _first_report_out_id(self): + """Return the first report_id that has an output (ReportOut).""" + for rid in self._report_id_order: + if rid in self._report_outs: + return rid + return None def send_report(self, report: Dict, report_id: int | None = None) -> None: """Send a report via the ReportIn class. @@ -295,7 +306,7 @@ def send_report(self, report: Dict, report_id: int | None = None) -> None: Raises RuntimeError if no matching ReportIn exists. """ if report_id is None: - report_id = self._first_report_id() + report_id = self._first_report_in_id() if report_id is None or report_id not in self._report_ins: raise RuntimeError(f"No input report available for report_id {report_id}") self._report_ins[report_id].send_report(report) @@ -305,7 +316,7 @@ def get_last_received_report(self, report_id: int | None = None): If report_id is None, uses the first-added report_id for this device. """ if report_id is None: - report_id = self._first_report_id() + report_id = self._first_report_out_id() if report_id is None or report_id not in self._report_outs: return None return self._report_outs[report_id].report From 617d297b44fac6eff4f10a858ffb4601427ebdf8 Mon Sep 17 00:00:00 2001 From: Taiki Komoda Date: Mon, 17 Nov 2025 01:09:30 +0000 Subject: [PATCH 3/3] Refactor Device class to set first report IDs at initialization --- adafruit_ble/services/standard/hid.py | 48 ++++++++------------------- 1 file changed, 14 insertions(+), 34 deletions(-) diff --git a/adafruit_ble/services/standard/hid.py b/adafruit_ble/services/standard/hid.py index 560a872..c1bf2be 100755 --- a/adafruit_ble/services/standard/hid.py +++ b/adafruit_ble/services/standard/hid.py @@ -260,8 +260,8 @@ class Device: def __init__(self, usage_page, usage): self._usage_page = usage_page self._usage = usage - # Maintain insertion order of report_ids - self._report_id_order = [] # list of report_id in insertion order + self._first_report_in_id = None + self._first_report_out_id = None self._report_ins = {} # report_id -> ReportIn self._report_outs = {} # report_id -> ReportOut @@ -275,50 +275,30 @@ def usage(self): def add_report_in(self, report_id: int, report_in: ReportIn) -> None: """Register a ReportIn instance for a report_id.""" - if report_id not in self._report_id_order: - self._report_id_order.append(report_id) + if self._first_report_in_id is None: + self._first_report_in_id = report_id self._report_ins[report_id] = report_in def add_report_out(self, report_id: int, report_out: ReportOut) -> None: """Register a ReportOut instance for a report_id.""" - if report_id not in self._report_id_order: - self._report_id_order.append(report_id) + if self._first_report_out_id is None: + self._first_report_out_id = report_id self._report_outs[report_id] = report_out - def _first_report_in_id(self): - """Return the first report_id that has an input (ReportIn).""" - for rid in self._report_id_order: - if rid in self._report_ins: - return rid - return None - - def _first_report_out_id(self): - """Return the first report_id that has an output (ReportOut).""" - for rid in self._report_id_order: - if rid in self._report_outs: - return rid - return None - def send_report(self, report: Dict, report_id: int | None = None) -> None: - """Send a report via the ReportIn class. - - If report_id is None, uses the first-added report_id for this device. - Raises RuntimeError if no matching ReportIn exists. - """ + """Send a report via the ReportIn class.""" + if report_id is None and self._first_report_in_id is not None: + report_id = self._first_report_in_id if report_id is None: - report_id = self._first_report_in_id() - if report_id is None or report_id not in self._report_ins: - raise RuntimeError(f"No input report available for report_id {report_id}") + raise RuntimeError("No input report available") self._report_ins[report_id].send_report(report) def get_last_received_report(self, report_id: int | None = None): - """Return the last OUT report received. - If report_id is None, uses the first-added report_id for this device. - """ + """Return the last OUT report received.""" + if report_id is None and self._first_report_out_id is not None: + report_id = self._first_report_out_id if report_id is None: - report_id = self._first_report_out_id() - if report_id is None or report_id not in self._report_outs: - return None + raise RuntimeError("No output report available") return self._report_outs[report_id].report