iPhone accepts BLE HID keyboard base keys but strips Shift from composite mouse+keyboard device

I’m debugging a custom BLE HID device on iPhone. It is a composite HID mouse + keyboard dongle.

Setup:

  • Hardware: Seeed XIAO nRF52840
  • Firmware: Adafruit Bluefruit Arduino / BLEHidAdafruit
  • BLE HID report map: stock Adafruit composite HID with keyboard, consumer, and mouse reports
  • GAP/advertising appearance: HID_MOUSE
  • iOS adopts the device as an AssistiveTouch pointer
  • Mouse movement and clicks work correctly

Keyboard symptom:

  • Lowercase/unshifted characters type correctly.
  • Shifted characters lose the Shift modifier during text input:
    - A -> a
    - T -> t
    - DoorDash -> doordash
    - ! -> 1
    - @ -> 2
    - # -> 3
    - { -> [
    - } -> ]

Confirmed:

  • The iOS app sends the exact intended string to the dongle.
  • Firmware receives the exact string.
  • Firmware computes and sends the expected HID modifier/keycode:
    • A sends modifier 0x02 + HID_KEY_A
    • ! sends modifier 0x02 + HID_KEY_1
  • A lone isolated "A" still lands as "a", so this does not appear to be a timing or repeated-key issue.
  • Cmd+Space works from the same HID keyboard report path and opens Spotlight.
  • Full Keyboard Access is off.
  • Turning AssistiveTouch off does not fix it.
  • The iPhone never shows "Hardware Keyboard" settings for this device, even when searching Settings.

Question: Is there a documented distinction on iOS between accepting BLE HID keyboard reports for global shortcuts, such as Cmd+Space, and admitting the same device as a full Hardware Keyboard for text composition?

In particular:

  • Does the absence of Hardware Keyboard settings mean iOS has not classified the device as a real external keyboard?
  • Can a composite BLE HID device advertised as HID_MOUSE be accepted for pointer input but have Shift ignored for text input?
  • Does iOS require a different GAP appearance, HID report-map structure, report ordering, or separate keyboard identity for Shift/modifier text composition to work?
  • Is there a recommended way to build a BLE HID device that preserves AssistiveTouch pointer behavior while also being treated as a full external keyboard?

I’m debugging a custom BLE HID device on iPhone. It is a composite HID mouse + keyboard dongle.

So, the first thing I would do here is pair the device with macOS and see what happens there. One of two things will happen in that testing:

  1. (most likely) You see exactly the same failure on macOS-> macOS has much much better tools and access to HID and event systems, making it easier to investigate and debug.

  2. (less likely) It work fine on macOS-> You can now file a bug saying "my keyboard work fine on macOS but fails on iOS", which is a much more "actionable" issue than a HID device simply behaving incorrectly.

Basically, I'd expect any macOS keyboard to work on iOS, so it's easier to focus on macOS first before you worry about iOS. Critically, on the tools side, you can use our HID APIs to see exactly what the machine is receiving and CGEventTap to see exactly how the event system interpreted your HID event(s), both for your accessory and any other accessory you want to test with. At that point, debugging is mostly a matter of comparing behavior and correcting any divergence.

Does iOS require a different GAP appearance, HID report-map structure, report ordering, or separate keyboard identity for Shift/modifier text composition to work?

...

Does iOS require a different GAP appearance, HID report-map structure, report ordering, or separate keyboard identity for Shift/modifier text composition to work?

Our exact requirements for all of this are documented in "Accessory Design Guidelines for Apple Devices", which is linked to off of our Accessories landing page.

Does the absence of Hardware Keyboard settings mean iOS has not classified the device as a real external keyboard?

I'm not certain of the exact criteria but my guess is:

  1. You need to conform to the guidelines above to be listed as "Hardware Keyboard".

  2. The system will still accept events from "other" configuration, most likely to support things like barcode scanners[1].

However, I don't think that alone explains what shift itself is failing. My guess is that this is an issue with the actual event you're sending, but I don't know enough about exactly what's wrong with the event.

Is there a recommended way to build a BLE HID device that preserves AssistiveTouch pointer behavior while also being treated as a full external keyboard?

Our design guidelines actively encourage combination keyboard + trackpad accessories, ro example p.100 (and other places):

"Keyboards should be integrated with Trackpads (page 106) when possible to provide an enhanced user experience."

They don't offer any additional guidance on these, so I suspect it should "just work", assuming the reports are all structured correctly.

[1] Many barcode scanner present themselves to the system as "keyboards" which then work by "typing" out whatever they scan. This is deeply weird, but it makes them compatible with the broadest possible set of use cases with the minimal development effort for users.

__
Kevin Elliott
DTS Engineer, CoreOS/Hardware

Thank you so much for responding, I'm grateful for your time. This guideline PDF is exactly what I was hoping to get.

I'm going to review this and my code. Hopefully it'll be an easy fix!

Thank you so much for responding, I'm grateful for your time. This guideline PDF is exactly what I was hoping to get.

You're very welcome and good luck!

__
Kevin Elliott
DTS Engineer, CoreOS/Hardware

Hey @DTS Engineer, I apologize for the delay. Feature dev got in the way :)

The guidelines PDF and the macOS-first advice were exactly what I needed. Here's where I ended up, plus three things the document doesn't cover.

First, the descriptor audit. I compared our keyboard collection byte-by-byte against the example in 14.2.1 (p.105). Differences: we carry a report ID, our modifiers come before the LED items, there's a reserved constant byte, a 6-slot key array instead of 5, and five LED usages instead of Caps Lock only. To rule those out I built a keyboard-only firmware whose keyboard collection is byte-identical to the composite's, all of those differences included. That build types perfectly on the same iPhone, Shift honored, shifted symbols correct, software keyboard suppressed. So the collection internals are fine; the same bytes work the moment the mouse is gone.

The full differential on one iPhone 16 Plus: Magic Keyboard works. My keyboard-only build works. The composite (same keyboard bytes plus mouse and consumer collections, mouse appearance) types lowercase, Cmd+Space works, but Shift is dropped in text input, there's no Hardware Keyboard settings row, and the on-screen keyboard stays up while it types. A commercial USB mouse+keyboard combo fails identically on this phone, so it's not my firmware.

Then the isolation test. I split the roles into two separate HID services on the same device: a keyboard-only service byte-equivalent to the working build, plus a mouse-only service, advertising as a keyboard. iOS binds both - the cursor works - and Shift is still dropped. So descriptor structure, report IDs, appearance, consumer usages: none of it matters. The one variable that flips the behavior in both directions is whether any mouse role exists on the accessory.

Finally the macOS approach you suggested, and it's clean. Pairing brings up the Keyboard Setup Assistant, so macOS classifies the composite as a keyboard. The raw input report carries modifier 0x02 with keycode 0x04, CGEventTap shows SHIFT=1 on the keyDown, and capitals land fine in TextEdit. Same firmware, same descriptor, same reports iOS strips Shift from. Since it works on macOS and fails on iOS, I've filed a bug per your suggestion: FB24023642, with the descriptor dump, both captures, the capture tool sources, and the two-service firmware diff attached.

The three questions:

  1. Is "no pointer role anywhere on the accessory" effectively a requirement for iOS to admit a device to the hardware-keyboard text path? From the isolation above, any mouse role - even on a fully separate HID service with a keyboard appearance - keeps the keyboard's modifiers off text composition while plain keys and global shortcuts still work. Is that expected behavior, or is the Feedback above a real bug? The guidelines cover keyboards (ch.14) and trackpads (ch.15) but say nothing about how iOS treats the keyboard half of a pointer-carrying accessory.

  2. Chapter 15 gates accessory trackpads to "supported starting in iPadOS 14.5" and never mentions iPhone. You suggested keyboard+trackpad integration - does iPhone (iOS 26) bind a chapter-15-conformant Digitizer/Touch Pad collection at all? And if it does, is the trackpad role exempt from whatever disqualifies mouse-carrying accessories, given 15.1.1 requires holding modifier keys while dragging?

  3. Bottom line for my product: is there any supported way today to ship pointer + keyboard on one radio that preserves text-path modifiers on iPhone? I've now falsified the two obvious shapes (one composite report map, and two separate HID services). If the real answer is "two accessories or a trackpad", knowing that saves me months....or just tons of time

Thanks you so much again :) Everything above is reproducible from the attached captures below. PS I turned them into txt files since i guess they aren't allowed to be uploaded? That way they're safer? hope that makes sense. Lmk if there's a better way to get you these things:

iPhone accepts BLE HID keyboard base keys but strips Shift from composite mouse+keyboard device
 
 
Q