Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Unable to use HID keyboard in boot OS (macOS boot screen) #1136

Open
bmosley opened this issue Aug 22, 2018 · 68 comments
Open

Unable to use HID keyboard in boot OS (macOS boot screen) #1136

bmosley opened this issue Aug 22, 2018 · 68 comments
Assignees
Milestone

Comments

@bmosley
Copy link

bmosley commented Aug 22, 2018

I started putting together a project with my Trinket M0 to use for switching boot volumes on macOS. My goal was to have it select a volume by keystrokes. However, it doesn't seem to be working.Working correctly in macOS once booted

Using the same code for HID keyboard:

  1. plug trinket m0 into usb on mac
  2. reboot system
import time

import board
import digitalio
from adafruit_hid.keyboard import Keyboard
from adafruit_hid.keyboard_layout_us import KeyboardLayoutUS
from adafruit_hid.keycode import Keycode

# The keyboard object!
time.sleep(1)  # Sleep for a bit to avoid a race condition on some systems
keyboard = Keyboard()
keyboard_layout = KeyboardLayoutUS(keyboard)

while True:
    keyboard.press(Keycode.LEFT_ALT)
    time.sleep(40)
    led.value = False
    keyboard.release_all()
@dhalbert
Copy link
Collaborator

The HID keyboard currently presented by CircuitPython is not a boot device keyboard, with its own endpoint(s), which is what the Mac Open Firmware ("BIOS") and regular PC BIOSes expect. It's part of a composite HID device that includes mouse, multimedia keys, and a gamepad. This was to save endpoints for other purposes.

We've had one other person who wants a boot-compatible keyboard and we're thinking about that - we have to count up the available endpoints and what we want for the future (e.g. USB MIDI). Also, in the long run, we are thinking about how to provide user-configurable HID devices.

@bmosley
Copy link
Author

bmosley commented Aug 22, 2018

Thanks. I completely understand and figured that this wasn’t currently supported but thought filing a bug was the best way to ask/suggest implementation of the feature. Is there any roadmap for future HID support? And MIDI sounds quite awesome too :)

@dhalbert
Copy link
Collaborator

No specific timeline with dates, but your request is noted and it's a vote!

@bmosley
Copy link
Author

bmosley commented Aug 22, 2018

wonderful, thank you!

@ATMakersBill
Copy link
Collaborator

This is marked as closed - was it done? If the fix was the same that I used in the XBox stuff which requires a custom build, is there any example of making this work?

@dhalbert dhalbert added this to the Long term milestone Feb 9, 2020
@dhalbert
Copy link
Collaborator

dhalbert commented Feb 9, 2020

The fix requires a new descriptor and its own endpoint, at least. I'm not sure what else. Perhaps it could be added as a compile option for the descriptor generating Python script, and then later as a boot.py. We can reopen this as long term.

@dhalbert dhalbert reopened this Feb 9, 2020
@ATMakersBill
Copy link
Collaborator

I understand this not being in the core CP, but I do think it should remain open until there's some way to do it. I don't mind having to do a custom build (like we do for the XBox Adaptive Controller), but we have found lots of folks who really want to use devices through KVMs, and the composite devices don't work through them.
Here's our current pressing issue: https://www.facebook.com/groups/ATMakers/permalink/815702142174560/

@ATMakersBill
Copy link
Collaborator

BTW, thanks Dan

@dhalbert
Copy link
Collaborator

dhalbert commented Feb 9, 2020

Do you need a bootmode mouse HID capability too?

@ATMakersBill
Copy link
Collaborator

Um... I don't think so? I think the reason this is needed is that the KVM listens in on the keyboard commands and relays them. That lets it watch for trigger keystrokes ('scroll-scroll-enter') to jump between devices. We don't kneed the triggers, but the stupid KVM doesn't know what to do with composite devices. It might be that we just need a non-composite device w/no other profiles. Or only a single device (no serial/mass storage?)

I just know it is a known issue that "complicated" keyboards (even those with media keys) don't work.

@bitboy85
Copy link

bitboy85 commented Mar 5, 2021

Here is another vote for it :)
By now i've tried arduino, vusb, teensy, and pico pi and only teensy seems to have a full featured keyboard that works in BIOS and KVM devices.
@ATMakersBill
The teensy itself is also a composite device, having keyboard, mouse, gamepad, serial and still works fine with a KVM. So my best guess is, that it only depends on a correct implementation of the boot keyboard.
@dhalbert
As far as i know, a boot keyboard needs its own descriptor and endpoint and on top of it, it has two additional functions which are described in the usb docs.

@dhalbert
Copy link
Collaborator

dhalbert commented Mar 5, 2021

We are currently out of endpoints on most boards with 8 endpoints, having added a second CDC. If someone would like to contribute boot keyboard and mouse descriptors and compile flags, we'd be happy to include them. We would not turn them on normally, since they would remove having MIDI or the second CDC available by default.

This is another indicator for user-configurable USB and USB HID descriptors. However, we are pretty much out of space on the smallest builds, and don't have room to add the code for this. It would have to be on something other than the non-Express SAMD21's.

@hathach
Copy link
Member

hathach commented Mar 5, 2021

@dhalbert boot keyboard doesn't need to be on its own interface and/or take extra endpoint. It can be used with existing HID device. Basically the HID device has 2 mode (or protocol): boot mode, and report mode (default).

  • In report mode: it is composite (keyboard + mouse + media key)
  • In boot mode: it only report 8 bytes of keyboard report (no report ID)

Most bios will always start with SET_PROTOCOL = BOOT upon enumeration. We only need to handle the callback, set the mode correctly then skip all other report except keyboard. I will update my hid_composite example in my repo so that you could have a clear example to follow in circuitpython later on. Will tag you on the modification later on, may try to submit PR myself as well. Though for the flash size, it may indeed increase a bit of space.

@dhalbert
Copy link
Collaborator

dhalbert commented Mar 5, 2021

Aha, great, thanks! I will await your example. It would be more complicated to add a boot mouse, but at least having a keyboard will satisfy many needs.

@hathach
Copy link
Member

hathach commented Mar 5, 2021

each interface can only either support boot keyboard or mouse, to support both, we need an extra interface though.

@deshipu
Copy link

deshipu commented Mar 5, 2021

But when in boot mode, you don't need MSC and CDC and MIDI, no?

@dhalbert
Copy link
Collaborator

dhalbert commented Mar 5, 2021

But when in boot mode, you don't need MSC and CDC and MIDI, no?

Good point - it's more a question of having code space on the smallest builds to switch back and forth.

@bitboy85
Copy link

bitboy85 commented Mar 5, 2021

@hathach Are you sure about the not needed extra endpoint? I tried a few times getting this to work with the arduino environment and to have an idea where the issue is i used this tool https://www.thesycon.de/eng/usb_descriptordumper.shtml and a real keyboard always used an extra endpoint.

@dhalbert
during my research i never found someone asking for a boot mouse. Most BIOSes are configured by keyboard and Hotkeys in KVMs are also done with a keyboard

@tannewt
Copy link
Member

tannewt commented Mar 9, 2021

I believe that BOOT mode is missing n-key rollover so we'll want to switch between the two if we can.

@hathach
Copy link
Member

hathach commented Mar 9, 2021

@bitboy85 can you post the descriptor of your real keyboard here along with its hid descriptor for the reference. If it is too long, you could move it to a gist and have a link here.

@bitboy85
Copy link

bitboy85 commented Mar 9, 2021

@hathach
https://gist.github.com/bitboy85/543b34a9505280ead8103a4d4d9cab26
2 different keyboards, no mixed device.

I remember a doc that says, a boot keyboard endpoint requires a packet size of 8 byte.

As additional reference: https://www.usb.org/sites/default/files/hid1_11.pdf
page 50-54: GET_PROTOCOL and SET_PROTOCOL request for boot device
page 62: Keyboard implementation
page 74: Boot keyboard requirements

@hathach
Copy link
Member

hathach commented Mar 9, 2021

thanks for the descriptor, and the reference. I just go through the hid 1.1 briefly again, and didn't see any requirement for boot protocol to has its own interface. The consumer keyboard has the luxury of additional endpoint to implement it in that way. Though we will see how this works out.

@tannewt
Copy link
Member

tannewt commented Mar 9, 2021

@obra may know what we need to be a boot keyboard too.

@akaenner
Copy link

I also need a keyboard that works at boot time. I'm currently working on a software for a DIY split keyboard with each part having a Raspberry Pi Pico in it. All features I planned are now implemented and I will share the code on GitHub in the comming weeks. I just realized, that it does not work at boot time (at least not under macOS). I hope that this issue can be fixed, because otherwise building fully fledged custom keyboards is out of reach by using CircuitPython.

@obra
Copy link

obra commented Mar 12, 2021 via email

@bitboy85
Copy link

bitboy85 commented Apr 8, 2021

As it is possible to config USB devices on a custom build, any hints where to start when trying to implement this?
https://learn.adafruit.com/building-circuitpython/customizing-usb-devices

I've read some docs about the requirement.

@dhalbert
Copy link
Collaborator

The wakeup problem is different than boot keyboard, and I need to work on that separately. If you plug in the RP2040 when the password prompt appears, does it still not work?

My Macs are not set up for this disk encryption, but there may be other boot-time choices (option-shift-whatever, etc.) that I can use to test without having to set one up for encryption. Do you know of some boot-time entry screens similar to the password request?

@akaenner
Copy link

If you plug in the RP2040 when the password prompt appears, does it still not work?

No, this also does not work.

Do you know of some boot-time entry screens similar to the password request?

I don"t know how to reproduce this behaviour without an encrypted HD. But you can encrypt your HD at any time. It just takes some time to finish. See System Preferences > Security & Privacy > File Vault

@dhalbert
Copy link
Collaborator

@akaenner I have a simpler test of this, which is to start the Mac while holding down the "D" key, to get it into diagnostics mode. Then it lists the available wifi networks (wait for the spinning to stop), and an external keyboard's up and down arrow keys can go up and down the list of networks.

The "D" key is for Intel Macs. For M1 ("Apple silicon"), you just keep holding down the power button to get into a startup menu. https://support.apple.com/en-us/HT202731

Anyway, what I see on an Intel MacBook is that a MacroPad set up for boot keyboard and sending up and down arrows does not work, but a vanilla Dell external keyboard does. So this gives me something to work toward.

@DietmarSchwertberger
Copy link

DietmarSchwertberger commented Oct 19, 2021

I just wanted to tell that I'm also looking forward to a better HID support.
I'm on Windows and often after booting I need to un- and re-plug my Raspberry Pi Pico based keyboard first.
(No password or keypress required for booting into Windows desktop.)

Also, usage with the KVM switch is not robust (the KVM is built into my monitor).
Thanks for your work on this issue.

@bitboy85
Copy link

So currently the default firmware has cdc hard coded to interface 0 because the windows driver is also hard coded to interface 0. Is this a requirement by windows or could it also use a hardcoded interface number 1? (having 0 as unused/reserved for boot keyboard).

What about making it the default firmware where the user can choose the interface number?

@dhalbert
Copy link
Collaborator

Fixing CDC to interface is a requirement due to the limitations of third-party drivers in Windows. A third-party cannot write a "class driver", which identifies the device by class and subclass. That is reserved for system-supplied drivers. I can only identify the device by PID/VID, and (optionally) interface number. If I scramble the interface numbers, then it can assign the wrong driver (e.g. MSC for CDC). I have also seen that at least some versions of Windows expected CDC on interface 0 always (incorrectly). In addition, at least some OS's expect the interface numbers to be sequential, so I assign them sequentially. And it would be a big bang to move CDC away from interface 0, and could cause trouble for current Windows users.

Windows driver assignments can get confused if the same device starts presenting different devices on varying interface numbers.

We don't need the Adafruit drivers package on Windows 10, because it has a built-in class driver for CDC. So if we stopped supporting Windows 7 and 8.1, the problem would go away.

@dhalbert
Copy link
Collaborator

Boot keyboard support on interface 0 is now merged into the main branch on CircuitPython, and will be in CircuitPython 7.1.0. Right now the order of the interfaces is fixed, so we are just turning off enough to bring HID to the top as interface 0. Since most keyboard projects are standalone things, and you don't want CIRCUITPY and CDC to be present anyway, especially in the case of a boot keyboard, this didn't seem like a serious limitation.

The boot keyboard suipport works on a few PC BIOS's I tested it on. It does not work on Mac yet, so I am leaving this issue open. I don't understand why, and the errors appear to be below the level of the descriptors: I am seeing unusual USB transmissions, so the issue may be at the USB protocol level.

@DietmarSchwertberger
Copy link

I have just downloaded the automatic build and it seems to fix my problems on Windows. The keyboard can be used right after booting. Thanks a lot for the great work.

@akaenner
Copy link

akaenner commented Nov 26, 2021

I just released my keyboard firmware on github: https://github.com/akaenner/picosplit
And the building instructions for my Raspberry Pi Pico and CircuitPython based keyboard: https://kaenner.de/picosplit

@DietmarSchwertberger
Copy link

@akaenner : On a quick look I did not see exception handlers. See PR #1929. Without, on my Windows PCs the software would not re-connect on re-boot or when the BIOS takes a few seconds before Windows is booting. If you have exception handlers, please point me to the positions, as I would like to update the PR with the actual exception types instead of catching all.

@akaenner
Copy link

@DietmarSchwertberger : I added an exception handler which does a reset. Do you think this is enough? I tested it with a PC running Windows and waking up the computer by pressing a key still does not work. Here is my code: https://github.com/akaenner/picosplit/blob/main/Master/main.py

@deshipu
Copy link

deshipu commented Nov 28, 2021

You really do not want to use a naked "except" like this, ever.

It will catch KeyboardInterrupt, MemoryError and even SystemExit exceptions.

Please use except Exception: if you really must do pokemon programming, but if possible, find out which exceptions you want to catch and only catch those.

@DietmarSchwertberger
Copy link

@akaenner : a reset seems a bit hard and unnecessary.
I have tried with sending my Windows PC to Standby. The exception will only be triggered when the keyboard is trying to send the keypress. Then, Keyboard(usb_hid.devices) will only succeed, once the PC is up and running again. During standby it will constantly fail.
I would guess that the wake-up is done by something outside the normal HID protocol.

@bitboy85
Copy link

bitboy85 commented Dec 5, 2021

I've tried with a Lenovo T410 Notebook. Sadly a keypress from the device completly locks up the BIOS. Even the normal keyboard is not working anymore. This happens at the time where the first keypress is send from the CP-device. Just connecting the CP-device does not trigger this issue.

  • If the device is already connected, BIOS and Grub Bootloader locks up when first keypress is send
  • If the device is plugged-in when BIOS or Grub is already loaded, it locks up without keypress.
    Every following keypress then gives an "error" sound from the bios, no matter if its from the CP-device or from the notebooks own keyboard. In normal mode this sound is played if you press a key which has no effect in the current state. Like pressing "right" when there is no menu to the right.
    A keypress from the CP-device also triggers this sound, so my guess is, the keyboard and the keypress is correctly detected

For whatever reason it worked a single time out of twenty tries but i'm unable to reproduce.

boot.py
from digitalio import DigitalInOut, Direction, Pull
import board
import usb_hid 
import usb_midi
import usb_cdc
import storage

maintenance_mode = False

BTN_A = DigitalInOut(board.GP19)
BTN_B = DigitalInOut(board.GP20)
BTN_A.direction = Direction.INPUT
BTN_A.pull = Pull.UP
BTN_B.direction = Direction.INPUT
BTN_B.pull = Pull.UP


#QuickButtons
if not BTN_A.value:
    if not BTN_B.value:
        maintenance_mode = True

if not maintenance_mode:
    storage.disable_usb_drive()
    usb_cdc.disable()
    usb_midi.disable()
    
usb_hid.enable((usb_hid.Device.KEYBOARD,), boot_device=1 if not maintenance_mode else 0)
lsusb -vd 239a:80f4

Bus 004 Device 009: ID 239a:80f4 Raspberry Pi Pico
Couldn't open device, some information will be missing
Device Descriptor:
bLength 18
bDescriptorType 1
bcdUSB 2.00
bDeviceClass 0
bDeviceSubClass 0
bDeviceProtocol 0
bMaxPacketSize0 64
idVendor 0x239a
idProduct 0x80f4
bcdDevice 1.00
iManufacturer 1
iProduct 2
iSerial 3
bNumConfigurations 1
Configuration Descriptor:
bLength 9
bDescriptorType 2
wTotalLength 0x0029
bNumInterfaces 1
bConfigurationValue 1
iConfiguration 0
bmAttributes 0x80
(Bus Powered)
MaxPower 100mA
Interface Descriptor:
bLength 9
bDescriptorType 4
bInterfaceNumber 0
bAlternateSetting 0
bNumEndpoints 2
bInterfaceClass 3 Human Interface Device
bInterfaceSubClass 1 Boot Interface Subclass
bInterfaceProtocol 1 Keyboard
iInterface 4
HID Device Descriptor:
bLength 9
bDescriptorType 33
bcdHID 1.11
bCountryCode 0 Not supported
bNumDescriptors 1
bDescriptorType 34 Report
wDescriptorLength 67
Report Descriptors:
** UNAVAILABLE **
Endpoint Descriptor:
bLength 7
bDescriptorType 5
bEndpointAddress 0x81 EP 1 IN
bmAttributes 3
Transfer Type Interrupt
Synch Type None
Usage Type Data
wMaxPacketSize 0x0040 1x 64 bytes
bInterval 8
Endpoint Descriptor:
bLength 7
bDescriptorType 5
bEndpointAddress 0x01 EP 1 OUT
bmAttributes 3
Transfer Type Interrupt
Synch Type None
Usage Type Data
wMaxPacketSize 0x0040 1x 64 bytes
bInterval 8

When comparing to http://www.usblyzer.com/reports/usb-properties/usb-keyboard.html
there are some differences.

iInterface is 4 guess it should be 0
wMaxPacketSize is 64 but maybe it should be 8

i tried CP 7.1.0 beta 0 and beta 1

@retrospecced
Copy link

Thank you @dhalbert and everyone for everything, I couldn’t get CircuitPython’s HID to work with my KVM either, due to the needed boot keyboard protocol. Special shout-out to @obra and the boot keyboard descriptors in https://github.com/keyboardio/KeyboardioHID, I used it to modify (slightly) the boot descriptor dhalbert graciously provided for a related issue hathach/tinyusb#1129 (comment), and now it works with my KVM!

CP 7.1.0 rc1 on a Feather Express M4 and using this boot.py

boot.py

import board
import digitalio
import storage
import usb_cdc
import usb_midi
import usb_hid
from time import sleep

switch = digitalio.DigitalInOut(board.D5)
switch.direction = digitalio.Direction.INPUT
switch.pull = digitalio.Pull.UP
sleep(0.1)

BOOT_KEYBOARD_DESCRIPTOR=bytes((
    0x05, 0x01,        # Usage Page (Generic Desktop Ctrls)
    0x09, 0x06,        # Usage (Keyboard)
    0xA1, 0x01,        # Collection (Application)
    0x05, 0x07,        #   Usage Page (Kbrd/Keypad)
    0x19, 0xE0,        #   Usage Minimum (0xE0)
    0x29, 0xE7,        #   Usage Maximum (0xE7)
    0x15, 0x00,        #   Logical Minimum (0)
    0x25, 0x01,        #   Logical Maximum (1)
    0x75, 0x01,        #   Report Size (1)
    0x95, 0x08,        #   Report Count (8)
    0x81, 0x02,        #   Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position)
    0x95, 0x01,        #   Report Count (1)
    0x75, 0x08,        #   Report Size (8)
    0x81, 0x01,        #   Input (Const,Array,Abs,No Wrap,Linear,Preferred State,No Null Position)
    0x95, 0x05,        #   Report Count (3)
    0x75, 0x01,        #   Report Size (1)
    0x05, 0x08,        #   Usage Page (LEDs)
    0x19, 0x01,        #   Usage Minimum (Num Lock)
    0x29, 0x05,        #   Usage Maximum (Kana)
    0x91, 0x02,        #   Output (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position,Non-volatile)
    0x95, 0x01,        #   Report Count (1)
    0x75, 0x03,        #   Report Size (5)
    0x91, 0x01,        #   Output (Const,Array,Abs,No Wrap,Linear,Preferred State,No Null Position,Non-volatile)
    0x95, 0x06,        #   Report Count (6)
    0x75, 0x08,        #   Report Size (8)
    0x15, 0x00,        #   Logical Minimum (0)
    0x26, 0xFF,        #   Logical Maximum (255)
    0x05, 0x07,        #   Usage Page (Kbrd/Keypad)
    0x19, 0x00,        #   Usage Minimum (0x00)
    0x2A, 0xFF,        #   Usage Maximum (0xFF)
    0x81, 0x00,        #   Input (Data,Array,Abs,No Wrap,Linear,Preferred State,No Null Position)
    0xC0,
))

boot_keyboard = usb_hid.Device(
    report_descriptor=BOOT_KEYBOARD_DESCRIPTOR,
    usage=0x06,
    usage_page=0x01,
    report_ids=(0,),
    in_report_lengths=(8,),
    out_report_lengths=(1,),
    )

maintenance_mode = not switch.value
if not maintenance_mode:
    storage.disable_usb_drive()
    usb_cdc.disable()
    usb_midi.disable()
    usb_hid.enable((boot_keyboard,), boot_device=1 )
else:
    usb_hid.enable((usb_hid.Device.KEYBOARD,))

@dhalbert
Copy link
Collaborator

@retrospecced Thanks for reporting this. I compared your descriptor above with the one I referenced, and here are the diffs. Your changes are on the right hand side. See the | in the middle which marks the changed lines.

0x05, 0x01,        // Usage Page (Generic Desktop Ctrls)	0x05, 0x01,        // Usage Page (Generic Desktop Ctrls)
0x09, 0x06,        // Usage (Keyboard)				0x09, 0x06,        // Usage (Keyboard)
0xA1, 0x01,        // Collection (Application)			0xA1, 0x01,        // Collection (Application)
0x05, 0x07,        //   Usage Page (Kbrd/Keypad)		0x05, 0x07,        //   Usage Page (Kbrd/Keypad)
0x19, 0xE0,        //   Usage Minimum (0xE0)			0x19, 0xE0,        //   Usage Minimum (0xE0)
0x29, 0xE7,        //   Usage Maximum (0xE7)			0x29, 0xE7,        //   Usage Maximum (0xE7)
0x15, 0x00,        //   Logical Minimum (0)			0x15, 0x00,        //   Logical Minimum (0)
0x25, 0x01,        //   Logical Maximum (1)			0x25, 0x01,        //   Logical Maximum (1)
0x75, 0x01,        //   Report Size (1)				0x75, 0x01,        //   Report Size (1)
0x95, 0x08,        //   Report Count (8)			0x95, 0x08,        //   Report Count (8)
0x81, 0x02,        //   Input (Data,Var,Abs,No Wrap,Linear,Pr	0x81, 0x02,        //   Input (Data,Var,Abs,No Wrap,Linear,Pr
0x95, 0x01,        //   Report Count (1)			0x95, 0x01,        //   Report Count (1)
0x75, 0x08,        //   Report Size (8)				0x75, 0x08,        //   Report Size (8)
0x81, 0x01,        //   Input (Const,Array,Abs,No Wrap,Linear	0x81, 0x01,        //   Input (Const,Array,Abs,No Wrap,Linear
0x95, 0x03,        //   Report Count (3)		      |	0x95, 0x05,        //   Report Count (3)
0x75, 0x01,        //   Report Size (1)				0x75, 0x01,        //   Report Size (1)
0x05, 0x08,        //   Usage Page (LEDs)			0x05, 0x08,        //   Usage Page (LEDs)
0x19, 0x01,        //   Usage Minimum (Num Lock)		0x19, 0x01,        //   Usage Minimum (Num Lock)
0x29, 0x05,        //   Usage Maximum (Kana)			0x29, 0x05,        //   Usage Maximum (Kana)
0x91, 0x02,        //   Output (Data,Var,Abs,No Wrap,Linear,P	0x91, 0x02,        //   Output (Data,Var,Abs,No Wrap,Linear,P
0x95, 0x01,        //   Report Count (1)			0x95, 0x01,        //   Report Count (1)
0x75, 0x05,        //   Report Size (5)			      |	0x75, 0x03,        //   Report Size (5)
0x91, 0x01,        //   Output (Const,Array,Abs,No Wrap,Linea	0x91, 0x01,        //   Output (Const,Array,Abs,No Wrap,Linea
0x95, 0x06,        //   Report Count (6)			0x95, 0x06,        //   Report Count (6)
0x75, 0x08,        //   Report Size (8)				0x75, 0x08,        //   Report Size (8)
0x15, 0x00,        //   Logical Minimum (0)			0x15, 0x00,        //   Logical Minimum (0)
0x26, 0xFF, 0x00,  //   Logical Maximum (255)		      |	0x26, 0xFF,        //   Logical Maximum (255)
0x05, 0x07,        //   Usage Page (Kbrd/Keypad)		0x05, 0x07,        //   Usage Page (Kbrd/Keypad)
0x19, 0x00,        //   Usage Minimum (0x00)			0x19, 0x00,        //   Usage Minimum (0x00)
0x2A, 0xFF, 0x00,  //   Usage Maximum (0xFF)		      |	0x2A, 0xFF,        //   Usage Maximum (0xFF)
0x81, 0x00,        //   Input (Data,Array,Abs,No Wrap,Linear,	0x81, 0x00,        //   Input (Data,Array,Abs,No Wrap,Linear,
0xC0,              // End Collection				0xC0,              // End Collection

In yours, there are 5 bits of padding and 3 bits of LED's (the comments are incorrect, the 3 and 5 should be reversed. Also, the two-byte 0xFF, 0x00 Maximums are replaced by the one-byte 0xFF. According to http://eleccelerator.com/usbdescreqparser/, yours does not parse properly. Yet it seems to work, so I am not sure why.

0x26 (0b100110) means, for instance, that it is a Logical Maximum, 2 bytes long. The value is signed, so 255 has to be represented as two bytes, otherwise the 0xFF would be interpreted as -1. The lower two bits are the length, so 10 means 2 two bytes. So if that's the descriptor you got from the other repo, it is slightly incorrect.

The actual descriptor does not necessarily matter, because when you set boot_device=1, the supplied HID report descriptor is ignored, and a standard one is used instead. See page 59 in https://www.usb.org/sites/default/files/hid1_12.pdf for this standard descriptor.

Nevertheless, yours works, in your situation. If you change the things I mentioned, does it make any difference?

@retrospecced
Copy link

Thank you for the analysis on the descriptor, and your corrections to mine also work with my KVM, as you probably expected!

When I set boot_device=1, the KVM doesn't work unless I supply your descriptor as a parameter. I don't think it's ignoring it.

#usb_hid.enable((usb_hid.Device.KEYBOARD,), boot_device=1 )
# above line does not work with Belkin SOHO KVM

#usb_hid.enable((reference_keyboard,), boot_device=1 )
# above line works fine with Belkin SOHO KVM
boot.py (updated)

"""CircuitPython Essentials Storage logging boot.py file"""
import board
import digitalio
import storage
import usb_cdc
import usb_midi
import usb_hid
from time import sleep

switch = digitalio.DigitalInOut(board.D5)

switch.direction = digitalio.Direction.INPUT
switch.pull = digitalio.Pull.UP
sleep(0.1)

REFERENCE_BOOT_KEYBOARD_DESCRIPTOR=bytes((
0x05, 0x01,        # Usage Page (Generic Desktop Ctrls)
0x09, 0x06,        # Usage (Keyboard)
0xA1, 0x01,        # Collection (Application)
0x05, 0x07,        #   Usage Page (Kbrd/Keypad)
0x19, 0xE0,        #   Usage Minimum (0xE0)
0x29, 0xE7,        #   Usage Maximum (0xE7)
0x15, 0x00,        #   Logical Minimum (0)
0x25, 0x01,        #   Logical Maximum (1)
0x75, 0x01,        #   Report Size (1)
0x95, 0x08,        #   Report Count (8)
0x81, 0x02,        #   Input (Data,Var,Abs,No Wrap,Linear,Pr
0x95, 0x01,        #   Report Count (1)
0x75, 0x08,        #   Report Size (8)
0x81, 0x01,        #   Input (Const,Array,Abs,No Wrap,Linear
0x95, 0x03,        #   Report Count (3)
0x75, 0x01,        #   Report Size (1)
0x05, 0x08,        #   Usage Page (LEDs)
0x19, 0x01,        #   Usage Minimum (Num Lock)
0x29, 0x05,        #   Usage Maximum (Kana)
0x91, 0x02,        #   Output (Data,Var,Abs,No Wrap,Linear,P
0x95, 0x01,        #   Report Count (1)
0x75, 0x05,        #   Report Size (5)
0x91, 0x01,        #   Output (Const,Array,Abs,No Wrap,Linea
0x95, 0x06,        #   Report Count (6)
0x75, 0x08,        #   Report Size (8)
0x15, 0x00,        #   Logical Minimum (0)
0x26, 0xFF, 0x00,  #   Logical Maximum (255)
0x05, 0x07,        #   Usage Page (Kbrd/Keypad)
0x19, 0x00,        #   Usage Minimum (0x00)
0x2A, 0xFF, 0x00,  #   Usage Maximum (0xFF)
0x81, 0x00,        #   Input (Data,Array,Abs,No Wrap,Linear,
0xC0,              # End Collection
))

reference_keyboard = usb_hid.Device(
    report_descriptor=REFERENCE_BOOT_KEYBOARD_DESCRIPTOR,
    usage=0x06,
    usage_page=0x01,
    report_ids=(0,),
    in_report_lengths=(8,),
    out_report_lengths=(1,),
    )
    
maintenance_mode = not switch.value
if not maintenance_mode:
    storage.disable_usb_drive()
    usb_cdc.disable()
    usb_midi.disable()
    usb_hid.enable((reference_keyboard,), boot_device=1 )
else:
    usb_hid.enable((usb_hid.Device.KEYBOARD,))

#usb_hid.enable((usb_hid.Device.KEYBOARD,), boot_device=1 )
# above line does not work with Belkin SOHO KVM

#usb_hid.enable((reference_keyboard,), boot_device=1 )
# above line works fine with Belkin SOHO KVM

"""output from this boot.py
Adafruit CircuitPython 7.1.0-rc.1 on 2021-12-25; Adafruit Feather M4 Express with samd51j19
Board ID:feather_m4_express
boot.py output:
"""

Again, thanks for your time, let me know if I can test anything else for you, as I am super-thankful of all of your time and effort spent on making CircuitPython work for us (especially for first-time Python programmers like myself!)

@dhalbert
Copy link
Collaborator

When I set boot_device=1, the KVM doesn't work unless I supply your descriptor as a parameter. I don't think it's ignoring it.

You do need a descriptor. How it works is a bit more complicated than one might expect. If you set boot_device=1, then CircuitPython says it is capable of being a boot keyboard. But it doesn't do so unless the host asks for it to be a boot keyboard. If the host doesn't ask, then the supplied descriptor is used. If the host does ask, then the supplied descriptor is sent but ignored, and the host assumes the "standard" one.

@bitboy85
Copy link

bitboy85 commented Jan 2, 2022

Did some further testing and i think this is a weird one. I didn't change the descriptor. I just added usb_hid.get_boot_device() to check if the host requests boot mode -> Yes it does.

So i said it locks up bios or grub boot loader. I found this is only half true.
After sending the first keypress it locks up. Nether from the cp-device nor from the original keyboard an input is accepted.
EXCEPT: pressing and releasing the left CTRL-key does unlock it. The normal keyboard starts working again and even better, the cp-device now acts as a keyboard perfectly fine.

I do not use CTRL key anywhere in my script and i don't have any issues without cp-device so i don't think the error comes from the built-in keyboard.

The behaviour is like a permanently pressed ctrl button. CTRL left button. But only one time after pluggin it in.
Pressing ALT + DEL while in bios reboots the pc.

But i can't find a part in my code where i send ctrl without releasing it.

So its fixable with kbd.release(Keycode.CONTROL) at the beginning of my script. But it doesn't explain why it is pressed in the first place. Even more confusing: https://github.com/adafruit/Adafruit_CircuitPython_HID/blob/main/adafruit_hid/keyboard.py does a release_all in the init part. Maybe something is going wrong there. I'll take a closer look tomorrow.

@retrospecced
Copy link

I'm using a KVM, but what you describe is the same behavior I was seeing before I started using the custom descriptor. https://w3c.github.io/uievents/tools/key-event-viewer.html would report left-control was always pressed before my code ran. Also, the KVM wouldn't forward any shift key presses to the host. So an "A" was always showing as control-a (lowercase), and so on.

paddyoneill added a commit to paddyoneill/feather-rp2040-kvm-macropad that referenced this issue Jan 19, 2022
Even with extra endpoints disabled the macropad was not working with the
KVM switch. A modified boot descriptor is required for the keyboard to
work as a boot device, this was taken from the following GitHub issue:

adafruit/circuitpython#1136 (comment)

An new keyboard object is created that uses the updated boot descriptor.
When booting in maintenance_mode the standard keyboard object is used.
@justinesmithies
Copy link

Thank you for the analysis on the descriptor, and your corrections to mine also work with my KVM, as you probably expected!

When I set boot_device=1, the KVM doesn't work unless I supply your descriptor as a parameter. I don't think it's ignoring it.

#usb_hid.enable((usb_hid.Device.KEYBOARD,), boot_device=1 )
# above line does not work with Belkin SOHO KVM

#usb_hid.enable((reference_keyboard,), boot_device=1 )
# above line works fine with Belkin SOHO KVM

boot.py (updated)

Again, thanks for your time, let me know if I can test anything else for you, as I am super-thankful of all of your time and effort spent on making CircuitPython work for us (especially for first-time Python programmers like myself!)

Just want to thank @retrospecced as using his boot.py resolved my issues whereby I couldn't access my ThinkPads BIOS or enter the grub menu on Arch Linux.
Now I just need to see if it's possible to change my Pico manufacturer info as lsusb shows Adafruit Pico and I'd like to change it to something else.

@bitboy85
Copy link

@retrospecced thx for your effort.
i tried your descriptor (and boot.py) but for some reason the device gets an exclamation mark in the device manager. So i was unable to test with the mentioned startech kvm switch.

@dhalbert
Copy link
Collaborator

@retrospecced's original descriptor has some errors. Did you try the left-hand one in the diff above.

@bitboy85
Copy link

bitboy85 commented Mar 9, 2022

Ah thx. I've thought the provided source does already contain the fix.

Meanwhile i found another descriptor and can confirm working with a startech kvm switch 👍
Other USB functions like storage, midi, and cdc needs to be turned off.
I'm using Circuitpython 7.2.0

BOOT_KEYBOARD_DESCRIPTOR=bytes((
    0x05, 0x01,        # Usage Page (Generic Desktop Ctrls)
    0x09, 0x06,        # Usage (Keyboard)
    0xA1, 0x01,        # Collection (Application)
    0x75, 0x01,        # Report Size (1)
    0x95, 0x08,        # Report Count (8)    
    0x05, 0x07,        # Usage Page (Kbrd/Keypad)
    0x19, 0xE0,        # Usage Minimum (0xE0, 224)
    0x29, 0xE7,        # Usage Maximum (0xE7, 231)
    0x15, 0x00,        # Logical Minimum (0)
    0x25, 0x01,        # Logical Maximum (1)
    0x81, 0x02,        # Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position)
    0x95, 0x01,        # Report Count (1)
    0x75, 0x08,        # Report Size (8)
    0x81, 0x03,        # Input (Const,Array,Abs,No Wrap,Linear,Preferred State,No Null Position)
    0x95, 0x05,        # Report Count (5)
    0x75, 0x01,        # Report Size (1)
    0x05, 0x08,        # Usage Page (LEDs)
    0x19, 0x01,        # Usage Minimum (Num Lock)
    0x29, 0x05,        # Usage Maximum (Kana)
    0x91, 0x02,        # Output (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position,Non-volatile)
    0x95, 0x01,        # Report Count (1)
    0x75, 0x03,        # Report Size (3)
    0x91, 0x03,        # Output (Const,Array,Abs,No Wrap,Linear,Preferred State,No Null Position,Non-volatile)
    0x95, 0x06,        # Report Count (6)
    0x75, 0x08,        # Report Size (8)
    0x15, 0x00,        # Logical Minimum (0)
    0x25, 0x68,        # Logical Maximum (104)
    0x05, 0x07,        # Usage Page (Kbrd/Keypad)
    0x19, 0x00,        # Usage Minimum (0)
    0x29, 0x68,        # Usage Maximum (104)
    0x81, 0x00,        # Input (Data,Array,Abs,No Wrap,Linear,Preferred State,No Null Position)
    0xC0,
))

@RefactorFactory
Copy link

I have a theory of the various keyboard descriptors discussed and the problem where the left Ctrl key seems to be pressed:

If the device sends reports prefixed with a 1-byte report ID of 0x01, the host is not expecting the prefixed report ID byte, and it interprets it as the modifiers field, and a modifiers field of 0x01 corresponds to the left Ctrl key. By calling usb_hid.enable((reference_keyboard,)...) with reference_keyboard having a report ID of 0, that causes CircuitPython to pass a report ID of 0 to TinyUSB which prevents it from prefixing reports with a report ID, solving the issue. The implication is that perhaps the contents of the keyboard descriptor doesn't matter as much as using a report ID of 0. Conclusion: for boot keyboards, always use a report ID of 0 for simple BIOSes. Maybe something to consider for CircuitPython's defaults, so that users don't need to specify a custom keyboard descriptor.

I may have found a way to keep CDC and possibly other functions enabled while also using a boot keyboard:

Keep the other functions enabled, but make the boot keyboard use an endpoint address of EP 1 IN. By doing this with plain Arduino (no CircuitPython), I was able to use a boot keyboard and CDC with my old PC.

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

No branches or pull requests