adafruit / circuitpython

CircuitPython - a Python implementation for teaching coding with microcontrollers
https://circuitpython.org
Other
4.11k stars 1.22k forks source link

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

Open bmosley opened 6 years ago

bmosley commented 6 years ago

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()
DietmarSchwertberger commented 3 years ago

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 commented 2 years ago

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 commented 2 years ago

@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 commented 2 years ago

@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 commented 2 years ago

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 commented 2 years ago

@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 commented 2 years ago

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.

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

boot.py ```python 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 commented 2 years ago

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 https://github.com/hathach/tinyusb/issues/1129#issuecomment-937756019, and now it works with my KVM!

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

boot.py

```Python 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 commented 2 years ago

@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 commented 2 years ago

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)

```Python """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 commented 2 years ago

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 commented 2 years ago

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 commented 2 years ago

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.

justinesmithies commented 2 years ago

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 commented 2 years ago

@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 commented 2 years ago

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

bitboy85 commented 2 years ago

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 commented 2 years ago

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.