]> Repositories - Adafruit_Blinka-hackapet.git/blob - src/usb_hid.py
correct handling of active devices
[Adafruit_Blinka-hackapet.git] / src / usb_hid.py
1 """
2 `usb_hid` - support for usb hid devices via usb_gadget driver
3 ===========================================================
4 See `CircuitPython:usb_hid` in CircuitPython for more details.
5 For now using report ids in the descriptor
6
7 # regarding usb_gadget see https://www.kernel.org/doc/Documentation/usb/gadget_configfs.txt
8 * Author(s): Björn Bösel
9 """
10
11 from typing import Sequence
12 from pathlib import Path
13 import os
14 import atexit
15
16 for module in ["dwc2", "libcomposite"]:
17     if Path("/proc/modules").read_text().find(module) == -1:
18         raise Exception(
19             "%s module not present in your kernel. did you insmod it?" % module
20         )
21
22 gadget_root = "/sys/kernel/config/usb_gadget/adafruit-blinka"
23 _boot_device = 0
24 _active_devices = []
25
26
27 @property
28 def devices():
29     return _active_devices
30
31
32 def get_boot_device() -> int:
33     return _boot_device
34
35
36 class Device:
37     """HID Device specification: see https://github.com/adafruit/circuitpython/blob/main/shared-bindings/usb_hid/Device.c"""
38
39     def __init__(
40         self,
41         *,
42         descriptor: bytes,
43         usage_page: int,
44         usage: int,
45         report_ids: Sequence[int],
46         in_report_lengths: Sequence[int],
47         out_report_lengths: Sequence[int],
48     ) -> None:
49         self.out_report_lengths = out_report_lengths
50         self.in_report_lengths = in_report_lengths
51         self.report_ids = report_ids
52         self.usage = usage
53         self.usage_page = usage_page
54         self.descriptor = descriptor
55         self._last_received_report = None
56
57     def send_report(self, report: bytearray, report_id: int = None):
58
59         report_id = report_id or self.report_ids[0]
60         device_path = self.gets_device_path(report_id)
61         with open(device_path, "rb+") as fd:
62             fd.write(bytearray(report_id) + report)
63
64     @property
65     def last_received_report(
66         self,
67     ) -> bytes:
68         return self.get_last_received_report()
69
70     def get_last_received_report(self, report_id=None) -> bytes:
71         device_path = self.gets_device_path(report_id or self.report_ids[0])
72         with open(device_path, "rb+") as fd:
73             os.set_blocking(fd.fileno(), False)
74             report = fd.read(self.out_report_lengths[0])
75             if report is not None:
76                 self._last_received_report = report
77         return self._last_received_report
78
79     def gets_device_path(self, report_id):
80         device = (
81             Path(
82                 "%s/functions/hid.usb%s/dev"
83                 % (gadget_root, report_id or self.report_ids[0])
84             )
85             .read_text()
86             .strip()
87             .split(":")[1]
88         )
89         device_path = "/dev/hidg%s" % device
90         return device_path
91
92
93 Device.KEYBOARD = Device(
94     descriptor=bytes(
95         (
96             0x05,
97             0x01,  # usage page (generic desktop ctrls)
98             0x09,
99             0x06,  # usage (keyboard)
100             0xA1,
101             0x01,  # collection (application)
102             0x85,
103             0x01,  # Report ID (1)
104             0x05,
105             0x07,  # usage page (kbrd/keypad)
106             0x19,
107             0xE0,  # usage minimum (0xe0)
108             0x29,
109             0xE7,  # usage maximum (0xe7)
110             0x15,
111             0x00,  # logical minimum (0)
112             0x25,
113             0x01,  # logical maximum (1)
114             0x75,
115             0x01,  # report size (1)
116             0x95,
117             0x08,  # report count (8)
118             0x81,
119             0x02,  # input (data,var,abs,no wrap,linear,preferred state,no null position)
120             0x95,
121             0x01,  # report count (1)
122             0x75,
123             0x08,  # report size (8)
124             0x81,
125             0x01,  # input (const,array,abs,no wrap,linear,preferred state,no null position)
126             0x95,
127             0x03,  # report count (3)
128             0x75,
129             0x01,  # report size (1)
130             0x05,
131             0x08,  # usage page (leds)
132             0x19,
133             0x01,  # usage minimum (num lock)
134             0x29,
135             0x05,  # usage maximum (kana)
136             0x91,
137             0x02,  # output (data,var,abs,no wrap,linear,preferred state,no null position,non-volatile)
138             0x95,
139             0x01,  # report count (1)
140             0x75,
141             0x05,  # report size (5)
142             0x91,
143             0x01,  # output (const,array,abs,no wrap,linear,preferred state,no null position,non-volatile)
144             0x95,
145             0x06,  # report count (6)
146             0x75,
147             0x08,  # report size (8)
148             0x15,
149             0x00,  # logical minimum (0)
150             0x26,
151             0xFF,
152             0x00,  # logical maximum (255)
153             0x05,
154             0x07,  # usage page (kbrd/keypad)
155             0x19,
156             0x00,  # usage minimum (0x00)
157             0x2A,
158             0xFF,
159             0x00,  # usage maximum (0xff)
160             0x81,
161             0x00,  # input (data,array,abs,no wrap,linear,preferred state,no null position)
162             0xC0,  # end collection
163         )
164     ),
165     usage_page=0x1,
166     usage=0x6,
167     report_ids=[0x1],
168     in_report_lengths=[8],
169     out_report_lengths=[1],
170 )
171 Device.MOUSE = Device(
172     descriptor=bytes(
173         (
174             0x05,
175             0x01,  # Usage Page (Generic Desktop Ctrls)
176             0x09,
177             0x02,  # Usage (Mouse)
178             0xA1,
179             0x01,  # Collection (Application)
180             0x85,
181             0x02,  # Report ID (2)
182             0x09,
183             0x01,  # Usage (Pointer)
184             0xA1,
185             0x00,  # Collection (Physical)
186             0x05,
187             0x09,  # Usage Page (Button)
188             0x19,
189             0x01,  # Usage Minimum (0x01)
190             0x29,
191             0x05,  # Usage Maximum (0x05)
192             0x15,
193             0x00,  # Logical Minimum (0)
194             0x25,
195             0x01,  # Logical Maximum (1)
196             0x95,
197             0x05,  # Report Count (5)
198             0x75,
199             0x01,  # Report Size (1)
200             0x81,
201             0x02,  # Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position)
202             0x95,
203             0x01,  # Report Count (1)
204             0x75,
205             0x03,  # Report Size (3)
206             0x81,
207             0x01,  # Input (Const,Array,Abs,No Wrap,Linear,Preferred State,No Null Position)
208             0x05,
209             0x01,  # Usage Page (Generic Desktop Ctrls)
210             0x09,
211             0x30,  # Usage (X)
212             0x09,
213             0x31,  # Usage (Y)
214             0x15,
215             0x81,  # Logical Minimum (-127)
216             0x25,
217             0x7F,  # Logical Maximum (127)
218             0x75,
219             0x08,  # Report Size (8)
220             0x95,
221             0x02,  # Report Count (2)
222             0x81,
223             0x06,  # Input (Data,Var,Rel,No Wrap,Linear,Preferred State,No Null Position)
224             0x09,
225             0x38,  # Usage (Wheel)
226             0x15,
227             0x81,  # Logical Minimum (-127)
228             0x25,
229             0x7F,  # Logical Maximum (127)
230             0x75,
231             0x08,  # Report Size (8)
232             0x95,
233             0x01,  # Report Count (1)
234             0x81,
235             0x06,  # Input (Data,Var,Rel,No Wrap,Linear,Preferred State,No Null Position)
236             0xC0,  # End Collection
237             0xC0,  # End Collection
238         )
239     ),
240     # Omitted for brevity.
241     usage_page=0x1,
242     usage=0x02,
243     report_ids=[2],
244     in_report_lengths=[4],
245     out_report_lengths=[0],
246 )
247
248 Device.CONSUMER_CONTROL = Device(
249     descriptor=bytes(
250         (
251             0x05,
252             0x0C,  # Usage Page (Consumer)
253             0x09,
254             0x01,  # Usage (Consumer Control)
255             0xA1,
256             0x01,  # Collection (Application)
257             0x85,
258             0x03,  # Report ID (3)
259             0x75,
260             0x10,  # Report Size (16)
261             0x95,
262             0x01,  # Report Count (1)
263             0x15,
264             0x01,  # Logical Minimum (1)
265             0x26,
266             0x8C,
267             0x02,  # Logical Maximum (652)
268             0x19,
269             0x01,  # Usage Minimum (Consumer Control)
270             0x2A,
271             0x8C,
272             0x02,  # Usage Maximum (AC Send)
273             0x81,
274             0x00,  # Input (Data,Array,Abs,No Wrap,Linear,Preferred State,No Null Position)
275             0xC0,  # End Collection
276         )
277     ),
278     # Omitted for brevity.
279     usage_page=0x0C,
280     usage=0x01,
281     report_ids=[3],
282     in_report_lengths=[2],
283     out_report_lengths=[0],
284 )
285
286
287 def disable() -> None:
288     try:
289         Path("%s/UDC" % gadget_root).write_text("")
290     except FileNotFoundError:
291         pass
292     for symlink in Path(gadget_root).glob("configs/**/hid.usb*"):
293         symlink.unlink()
294
295     for strings_file in Path(gadget_root).rglob("configs/*/strings/*/*"):
296         if strings_file.is_dir():
297             strings_file.rmdir()
298
299     for strings_file in Path(gadget_root).rglob("configs/*/strings/*"):
300         if strings_file.is_dir():
301             strings_file.rmdir()
302     for config_dir in Path(gadget_root).rglob("configs/*"):
303         if config_dir.is_dir():
304             config_dir.rmdir()
305     for function_dir in Path(gadget_root).rglob("functions/*"):
306         if function_dir.is_dir():
307             function_dir.rmdir()
308     try:
309         Path(gadget_root).rmdir()
310     except:
311         pass
312
313
314 atexit.register(disable)
315
316
317 def enable(devices: Sequence[Device], boot_device: int = 0) -> None:
318     """
319     Specify which USB HID devices that will be available. Can be called in boot.py, before USB is connected.
320     :param devices:
321     :param boot_device:
322     :return:
323     """
324     global _boot_device, _active_devices
325     _boot_device = boot_device
326
327     """ Enable usb_hid
328     shim for https://docs.circuitpython.org/en/latest/shared-bindings/usb_hid/index.html#usb_hid.enable
329
330     """
331     if len(devices) == 0:
332         disable()
333         return
334
335     if boot_device > 0:
336         raise NotImplementedError("not supported yet")
337
338     """
339     1. Creating the gadgets
340     -----------------------
341
342     For each gadget to be created its corresponding directory must be created::
343
344         $ mkdir $CONFIGFS_HOME/usb_gadget/<gadget name>
345
346     e.g.::
347
348         $ mkdir $CONFIGFS_HOME/usb_gadget/g1
349
350         ...
351         ...
352         ...
353
354         $ cd $CONFIGFS_HOME/usb_gadget/g1
355
356     Each gadget needs to have its vendor id <VID> and product id <PID> specified::
357
358         $ echo <VID> > idVendor
359         $ echo <PID> > idProduct
360
361     A gadget also needs its serial number, manufacturer and product strings.
362     In order to have a place to store them, a strings subdirectory must be created
363     for each language, e.g.::
364
365         $ mkdir strings/0x409
366
367     Then the strings can be specified::
368
369         $ echo <serial number> > strings/0x409/serialnumber
370         $ echo <manufacturer> > strings/0x409/manufacturer
371         $ echo <product> > strings/0x409/product
372     """
373     Path("%s/functions" % gadget_root).mkdir(parents=True, exist_ok=True)
374     Path("%s/configs" % gadget_root).mkdir(parents=True, exist_ok=True)
375     Path("%s/bcdDevice" % gadget_root).write_text("%s" % 1)  # Version 1.0.0
376     Path("%s/bcdUSB" % gadget_root).write_text("%s" % 0x0200)  # USB 2.0
377     Path("%s/bDeviceClass" % gadget_root).write_text(
378         "%s" % 0x00
379     )  # multipurpose i guess?
380     Path("%s/bDeviceProtocol" % gadget_root).write_text("%s" % 0x00)
381     Path("%s/bDeviceSubClass" % gadget_root).write_text("%s" % 0x00)
382     Path("%s/bMaxPacketSize0" % gadget_root).write_text("%s" % 0x08)
383     Path("%s/idProduct" % gadget_root).write_text(
384         "%s" % 0x0104
385     )  # Multifunction Composite Gadget
386     Path("%s/idVendor" % gadget_root).write_text("%s" % 0x1D6B)  # Linux Foundation
387     """
388     2. Creating the configurations
389     ------------------------------
390
391     Each gadget will consist of a number of configurations, their corresponding
392     directories must be created:
393
394     $ mkdir configs/<name>.<number>
395
396     where <name> can be any string which is legal in a filesystem and the
397     <number> is the configuration's number, e.g.::
398
399         $ mkdir configs/c.1
400
401         ...
402         ...
403         ...
404
405     Each configuration also needs its strings, so a subdirectory must be created
406     for each language, e.g.::
407
408         $ mkdir configs/c.1/strings/0x409
409
410     Then the configuration string can be specified::
411
412         $ echo <configuration> > configs/c.1/strings/0x409/configuration
413
414     Some attributes can also be set for a configuration, e.g.::
415
416         $ echo 120 > configs/c.1/MaxPower
417         """
418
419     for i, device in enumerate(devices):
420
421         config_root = "%s/configs/device.%s" % (gadget_root, 1)
422         Path("%s/" % config_root).mkdir(parents=True, exist_ok=True)
423         Path("%s/strings/0x409" % config_root).mkdir(parents=True, exist_ok=True)
424         Path("%s/strings/0x409/configuration" % config_root).write_text(
425             "my configuration"
426         )
427         Path("%s/MaxPower" % config_root).write_text("150")
428         Path("%s/bmAttributes" % config_root).write_text("%s" % 0x080)
429         _active_devices.append(device)
430         """
431         3. Creating the functions
432         -------------------------
433
434         The gadget will provide some functions, for each function its corresponding
435         directory must be created::
436
437             $ mkdir functions/<name>.<instance name>
438
439         where <name> corresponds to one of allowed function names and instance name
440         is an arbitrary string allowed in a filesystem, e.g.::
441
442           $ mkdir functions/ncm.usb0 # usb_f_ncm.ko gets loaded with request_module()
443
444           ...
445           ...
446           ...
447
448         Each function provides its specific set of attributes, with either read-only
449         or read-write access. Where applicable they need to be written to as
450         appropriate.
451         Please refer to Documentation/ABI/*/configfs-usb-gadget* for more information.
452
453         """
454
455         # create functions
456         for report_index, report_id in enumerate(device.report_ids):
457             function_root = "%s/functions/hid.usb%s" % (gadget_root, report_id)
458             try:
459                 Path("%s/" % function_root).mkdir(parents=True)
460             except FileExistsError:
461                 continue
462             Path("%s/protocol" % function_root).write_text("%s" % report_id)
463             Path("%s/report_length" % function_root).write_text(
464                 "%s" % device.in_report_lengths[report_index]
465             )
466             Path("%s/subclass" % function_root).write_text("%s" % 1)
467             Path("%s/report_desc" % function_root).write_bytes(device.descriptor)
468             """
469             4. Associating the functions with their configurations
470             ------------------------------------------------------
471
472             At this moment a number of gadgets is created, each of which has a number of
473             configurations specified and a number of functions available. What remains
474             is specifying which function is available in which configuration (the same
475             function can be used in multiple configurations). This is achieved with
476             creating symbolic links::
477
478                 $ ln -s functions/<name>.<instance name> configs/<name>.<number>
479
480             e.g.::
481
482                 $ ln -s functions/ncm.usb0 configs/c.1
483             """
484
485             Path("%s/hid.usb%s" % (config_root, report_id)).symlink_to(function_root)
486     """
487     5. Enabling the gadget
488     ----------------------
489     Such a gadget must be finally enabled so that the USB host can enumerate it.
490
491     In order to enable the gadget it must be bound to a UDC (USB Device
492     Controller)::
493
494         $ echo <udc name> > UDC
495
496     where <udc name> is one of those found in /sys/class/udc/*
497     e.g.::
498
499     $ echo s3c-hsotg > UDC
500
501     """
502     udc = next(Path("/sys/class/udc/").glob("*"))
503     Path("%s/UDC" % gadget_root).write_text("%s" % udc.name)