Introduction
When developing a custom HID keyboard device based on an ATmega32U4, everything seemed fine at first. Locally on my PC, typing worked flawlessly. However, when connecting via Windows Remote Desktop (RDP), strange errors appeared: misplaced characters, missing letters, and broken modifier keys like Shift and AltGr.
At first, I thought it was just a "timing" problem. But digging deeper revealed a much more fundamental issue. This article documents what I found, why the Arduino Keyboard library isn't always sufficient, and how to fix HID keyboard emulation correctly.
The Common Misconception
Most tutorials and libraries (like Arduino's Keyboard) send combined HID reports directly: modifiers and characters pressed at the same time.
However, real keyboards work differently, as users do ;-)
Think about how you type on your keyboard:
- You first press Shift.
- Then you press the letter (e.g., "a"). (note it is "a", not "A", you´ve pressed Shift before)
- Only after typing, you release both keys.
In real hardware:
- Modifier keys (e.g., Shift, AltGr) are pressed first, independently.
- After a short delay, the character key is pressed.
- Upon key release, both modifier and character are released properly.
If the HID report mixes these up or sends invalid sequences, RDP (and some strict operating systems) misinterpret the input.
Investigating HID Packets
Using USBPcap and Wireshark, I captured real keyboard traffic. Key findings:
- Modifier keys are transmitted alone before characters.
- Separate USB interrupt packets are sent for each event.
- Even small differences in packet construction cause problems over Remote Desktop.
Example: Typing "A" on a real keyboard generates:
- Press Shift (modifier packet)
- Press a (character packet)
- Release a
- Release Shift
RDP and Windows history:
In the mid-1990s, when RDP was designed, USB keyboards were virtually nonexistent.
Computers used PS/2 or AT keyboards, where the communication model was simple:
one key event at a time, with minimal complexity and no multi-key HID packets.
RDP adopted this straightforward model for maximum compatibility and performance over slow networks.
The RDP client does not forward real USB HID packets to the remote server.
Instead, the RDP stack on the client side detects and interprets keystrokes locally, then converts them into keyboard events and sends them over the network.
In Windows, these events are based on the Virtual Key Codes (VK_XXX constants) and transmitted using classic Windows messages such as WM_KEYDOWN, WM_KEYUP, WM_SYSKEYDOWN, and WM_SYSKEYUP.
If you look closely at this mechanism, you quickly realize:
It was designed in the mid-1990s, in the early days of Windows NT 4.0 Terminal Server and the original RDP protocol.
At that time, multiple simultaneous key presses (such as modifier combinations or 6-key-rollover gaming keyboards) were very uncommon.
Most user interactions were simple typing — individual characters, one at a time.
Performance over low-bandwidth networks (like 56k modems or early ISDN lines) was a critical design factor:
Instead of forwarding raw USB data (likely nonexistent at that time), which would have been heavy and complex, RDP was optimized to send minimal, efficient keystroke events — typically one key per packet.
This legacy is still visible today:
While modern local keyboards use full HID reports (supporting multiple keys and modifiers simultaneously),
RDP environments still expect "classic" single key events — making correct press/release sequencing absolutely crucial, especially when using modifier keys (like Shift, AltGr, Ctrl).
Technical Findings
- Packet structure matters more than timing. Even with perfect delays, wrong HID packets cause errors.
- Correct HID sequence: separate modifier handling, one keypress per packet.
- Minimal delay (~10ms) is sufficient once the packet structure is correct.
I confirmed that even with a very small delay(10), the transmission is reliable — locally and over Remote Desktop.
The key is how the HID packets are structured, not how slow or fast the transmission is.
Solutions Implemented
To fix the problem, I implemented:
- A new
writeSafe()function that internally:- Detects whether Shift or AltGr is needed.
- Presses the modifier key separately.
- Presses the character.
- Releases keys properly.
- New coding patterns:
press()andrelease()for full control.releaseAll()to ensure clean HID states.
- Improved memory efficiency by keeping only command sequences (press/release/write), not full text strings.
Important Notice
Currently, the improved writeSafe() code is not yet publicly available.
If there is interest from the community, I am happy to work on it and provide a detailed improvement suggestion.
Until then, I reccomend to use press() and release() instead of write()**, especially when working with remote systems, and have problems described as above, escpecially when you are using modifier keys.
Here is an example of the difference:
Problematic old style (using only write):
cpp
write("abc-ABC-123-%&[]-089-?-END");
can result in
abc-ABc-123-8%&8]-089-ENd
or can even cause unpredicted Desktop/System command (yes, I´ve had them all, today).
Improved method (using press/release):
cpp
press('a'); releaseAll();
press('b'); releaseAll();
press('c'); releaseAll();
press('-'); releaseAll();
press(Shift); press('A'); releaseAll();
press(Shift); press('B'); releaseAll();
press(Shift); press('C'); releaseAll();
...
press(Shift); press('e'); press('n'); press('d'); releaseAll();
Results
After implementing correct HID packet construction:
- Typing became 100% reliable, locally and over Remote Desktop.
- No more random characters or shortcut activations.
- Clean and scalable HID design, even for future expansions.
Conclusion
The problem wasn't just "bad timing" — it was bad HID packet structure.
By accurately emulating real keyboard behavior — separating modifier and character keys — even low-cost ATmega32U4 devices can behave like professional HID keyboards.
Arduino's Keyboard library could greatly benefit from a smarter writeSafe() method following these findings.
Resources
- USB HID Usage Tables (official)
- Arduino Keyboard Library
- USBPcap project
Remember:
If you're building HID keyboards, think beyond timing:
the real secret is in how you structure your USB packets.
Happy hacking!