Interesting scanner behaviour that I can't explain

I’m trying to understand some of the behaviour that I’m seeing from Kaleidosocope, and I’m at a loss.

I started out by thinking about a keymap with two keys defined as Key_LeftShift (or any modifier). Looking at the code, it seems like holding both of those keys down, plus a letter key – e.g. lshift+lshift+A – would result in a single erroneous HID report if one of those lshift keys (whichever one gets handled last) was released, because it would call hid::releaseKey(), clearing the bit for lshift in the report, and possibly resulting in this output, despite the fact that a shift key was being held the whole time:


[Note the a]

I tried it out, and noticed that this doesn’t happen. I’m still not sure why, but I thought it might be because of repeat-rate limiting done by the OS. So I tried to slow down the keyboard by adding a single line at the end of Kaleidoscope.loop():


I figured, with a guaranteed one-second gap between reports, the OS would certainly show what was happening. It didn’t. Instead, I saw very surprising things happen when I tapped multiple keys simultaneously. For example, when I very quickly rolled over j, k, and l, I saw this:


I was expecting the first key to be detected would be in one report alone, so the repeated j characters were not surprising, but on the next report after that, I thought I’d get the k and the l simultaneously, so whichever was detected first by the OS (k, most likely, because its bit appears first in the key report) would appear only once, then get masked. Here’s what I expected:


…or maybe just:


…because, by the next loop, the scanner will have had time to do a couple hundred scan cycles. (I’m also trying to figure out how the scanners work). If it worked that way, though, I guess I probably wouldn’t see any output unless I got lucky or held the keys down long enough.

Anyway, I’m just flummoxed. I added some serial debugging code to Keyboard.sendReportUnchecked(), and it shows this happening (the bitfield is the byte from keyReport including j, k, and l; the decimal number is millis()):

40313: 00100000

41317: 01100000

42321: 11100000

43325: 11000000

44328: 10000000

45333: 00000000

[Padded with zeroes for readability]
The first line shows a report with j alone. Then, one second later, there’s a report with both j & k, but not l. This is ~one full second after all three keys were released!

Then, one more second passes, and k is added to the report. Then, almost three seconds and two full reports after the key was released, the j is finally removed from the key report. Then k, then, finally – five seconds after it was released – a report is sent with l removed.

Now, I’m not complaining about any actual behaviour here; it’s utterly ridiculous to have a one-second delay added to the main loop, of course. But I’m trying to understand how this all works, and I’m stumped by this one. What’s going on here?

I’ve done a little more testing now. I added another debug line, to print when a key toggles on, and show its keycode in hex (in handleKeyswitchEvent(), after Layer.lookup()). Sometimes, when I try to tap all three keys simultaneously, I do see two of them change in the same report. Here’s the debug output:

key on: D

39359: 100000

key on: F

key on: E

40374: 11100000

41377: 100000

42382: 0

In this D = j (0x0D), E = k (0x0E), and F = l (0x0F). I only had it record keys toggling on. And in this case, the output is a bit more what I expected:


…but this happens less often than the other pattern. So, I guess the keyscanner buffers several sets of key data, and I only get one of those each time getKeyData() is called?

Now, I’ve even seen it once with all three keys toggling on in the same scan:

key on: F

key on: E

key on: D

701641: 11100000

702645: 100000

703649: 0

…with this output (as expected):