mirror of
https://github.com/ALLFATHER-BV/wadamesh.git
synced 2026-09-25 15:34:39 +00:00
Bluetooth serves either the phone app or a keyboard (Settings > Bluetooth). Pairing lists keyboards in pairing mode and pairs with or without a code; the paired keyboard reconnects by itself. Keys are read the way phones and computers read them: the keyboard's Report Map says which report carries the keys and how, so media keys and touchpads are left alone, and the boot protocol is only the fallback. Key positions are translated with the chosen layout (US, UK, German, French, Belgian), with AltGr and dead keys. While a keyboard is connected: - text goes into the focused field, and touchscreen-only boards keep the on-screen keyboard down (a second tap on a field brings it up anyway) - the arrows, Tab, Enter and Page Up/Down drive the focus highlight - Esc is the back button; a "Back key" setting picks another key for keyboards whose Esc key types a character - tab hotkeys and their hints over the tab bar are switched on - Command tapped alone opens the emoji picker in a chat - the status bar shows a keyboard instead of the Bluetooth glyph The Bluetooth page is now a flex column with the choice on top, and the settings page refit keeps it scrollable when its content grows. Settings: prefs v61 (mode, layout, paired keyboard) and v62 (Back key). Tests: test/test_hid_report_map.cpp, test_touch_prefs_schema.cpp. Not on the P4 boards (Tanmatsu, T-Display P4). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
289 lines
12 KiB
C++
289 lines
12 KiB
C++
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
// Host test for the keyboard report reader used by the Bluetooth keyboard.
|
|
// c++ -std=c++17 -Wall -Wextra -I src test/test_hid_report_map.cpp -o /tmp/hid && /tmp/hid
|
|
|
|
#include <assert.h>
|
|
#include <stdio.h>
|
|
#include <string.h>
|
|
|
|
#include "helpers/input/HidReportMap.h"
|
|
|
|
// USB HID 1.11, appendix E.6: the boot keyboard layout, no report ids.
|
|
static const uint8_t kSpecKeyboard[] = {
|
|
0x05, 0x01, 0x09, 0x06, 0xA1, 0x01,
|
|
0x05, 0x07, 0x19, 0xE0, 0x29, 0xE7, 0x15, 0x00, 0x25, 0x01, 0x75, 0x01, 0x95, 0x08, 0x81, 0x02,
|
|
0x95, 0x01, 0x75, 0x08, 0x81, 0x01,
|
|
0x95, 0x05, 0x75, 0x01, 0x05, 0x08, 0x19, 0x01, 0x29, 0x05, 0x91, 0x02,
|
|
0x95, 0x01, 0x75, 0x03, 0x91, 0x01,
|
|
0x95, 0x06, 0x75, 0x08, 0x15, 0x00, 0x25, 0x65, 0x05, 0x07, 0x19, 0x00, 0x29, 0x65, 0x81, 0x00,
|
|
0xC0,
|
|
};
|
|
|
|
// The usual Bluetooth keyboard: keyboard on id 1, media keys on id 2.
|
|
static const uint8_t kBleKeyboard[] = {
|
|
0x05, 0x01, 0x09, 0x06, 0xA1, 0x01, 0x85, 0x01,
|
|
0x05, 0x07, 0x19, 0xE0, 0x29, 0xE7, 0x15, 0x00, 0x25, 0x01, 0x75, 0x01, 0x95, 0x08, 0x81, 0x02,
|
|
0x95, 0x01, 0x75, 0x08, 0x81, 0x01,
|
|
0x95, 0x05, 0x75, 0x01, 0x05, 0x08, 0x19, 0x01, 0x29, 0x05, 0x91, 0x02,
|
|
0x95, 0x01, 0x75, 0x03, 0x91, 0x01,
|
|
0x95, 0x06, 0x75, 0x08, 0x15, 0x00, 0x25, 0x65, 0x05, 0x07, 0x19, 0x00, 0x29, 0x65, 0x81, 0x00,
|
|
0xC0,
|
|
0x05, 0x0C, 0x09, 0x01, 0xA1, 0x01, 0x85, 0x02,
|
|
0x15, 0x00, 0x25, 0x01, 0x75, 0x01, 0x95, 0x10,
|
|
0x09, 0xB5, 0x09, 0xB6, 0x09, 0xB7, 0x09, 0xCD, 0x09, 0xE2, 0x09, 0xE9, 0x09, 0xEA, 0x0A, 0x23, 0x02,
|
|
0x0A, 0x94, 0x01, 0x0A, 0x92, 0x01, 0x0A, 0x2A, 0x02, 0x0A, 0x21, 0x02, 0x0A, 0x26, 0x02, 0x0A, 0x24, 0x02,
|
|
0x0A, 0x83, 0x01, 0x0A, 0x8A, 0x01,
|
|
0x81, 0x02,
|
|
0xC0,
|
|
};
|
|
|
|
// Full-key rollover: modifiers, LEDs, then one bit per usage 0x00-0xEF (id 6).
|
|
static const uint8_t kNkroKeyboard[] = {
|
|
0x05, 0x01, 0x09, 0x06, 0xA1, 0x01, 0x85, 0x06,
|
|
0x05, 0x07, 0x19, 0xE0, 0x29, 0xE7, 0x15, 0x00, 0x25, 0x01, 0x95, 0x08, 0x75, 0x01, 0x81, 0x02,
|
|
0x05, 0x08, 0x19, 0x01, 0x29, 0x05, 0x95, 0x05, 0x75, 0x01, 0x91, 0x02,
|
|
0x95, 0x01, 0x75, 0x03, 0x91, 0x03,
|
|
0x05, 0x07, 0x19, 0x00, 0x29, 0xEF, 0x15, 0x00, 0x25, 0x01, 0x95, 0xF0, 0x75, 0x01, 0x81, 0x02,
|
|
0xC0,
|
|
};
|
|
|
|
// Keyboard (id 1, no LEDs) next to a touchpad whose report is also 8 bytes (id 3).
|
|
static const uint8_t kKeyboardAndTouchpad[] = {
|
|
0x05, 0x01, 0x09, 0x06, 0xA1, 0x01, 0x85, 0x01,
|
|
0x05, 0x07, 0x19, 0xE0, 0x29, 0xE7, 0x15, 0x00, 0x25, 0x01, 0x75, 0x01, 0x95, 0x08, 0x81, 0x02,
|
|
0x95, 0x01, 0x75, 0x08, 0x81, 0x01,
|
|
0x95, 0x06, 0x75, 0x08, 0x15, 0x00, 0x25, 0x65, 0x05, 0x07, 0x19, 0x00, 0x29, 0x65, 0x81, 0x00,
|
|
0xC0,
|
|
0x05, 0x01, 0x09, 0x02, 0xA1, 0x01, 0x85, 0x03, 0x09, 0x01, 0xA1, 0x00,
|
|
0x05, 0x09, 0x19, 0x01, 0x29, 0x05, 0x15, 0x00, 0x25, 0x01, 0x95, 0x05, 0x75, 0x01, 0x81, 0x02,
|
|
0x95, 0x01, 0x75, 0x03, 0x81, 0x01,
|
|
0x05, 0x01, 0x09, 0x30, 0x09, 0x31, 0x16, 0x01, 0x80, 0x26, 0xFF, 0x7F, 0x75, 0x10, 0x95, 0x02, 0x81, 0x06,
|
|
0x09, 0x38, 0x15, 0x81, 0x25, 0x7F, 0x75, 0x08, 0x95, 0x01, 0x81, 0x06,
|
|
0x05, 0x0C, 0x0A, 0x38, 0x02, 0x95, 0x01, 0x81, 0x06,
|
|
0x95, 0x02, 0x81, 0x01,
|
|
0xC0, 0xC0,
|
|
};
|
|
|
|
// Oddities seen in the wild: modifiers as a list of single usages, and a key
|
|
// array whose "Logical Maximum (255)" was written as the signed byte 0xFF.
|
|
static const uint8_t kQuirkyKeyboard[] = {
|
|
0x05, 0x01, 0x09, 0x06, 0xA1, 0x01,
|
|
0x05, 0x07, 0x09, 0xE0, 0x09, 0xE1, 0x09, 0xE2, 0x09, 0xE3, 0x09, 0xE4, 0x09, 0xE5, 0x09, 0xE6, 0x09, 0xE7,
|
|
0x15, 0x00, 0x25, 0x01, 0x75, 0x01, 0x95, 0x08, 0x81, 0x02,
|
|
0x75, 0x08, 0x95, 0x01, 0x81, 0x03,
|
|
0x15, 0x00, 0x25, 0xFF, 0x19, 0x00, 0x29, 0xFF, 0x75, 0x08, 0x95, 0x05, 0x81, 0x00,
|
|
0xC0,
|
|
};
|
|
|
|
// A real keyboard (Sologic, 2026-09): five key slots and a vendor byte in
|
|
// report 2, media keys as a 16-bit array in report 3.
|
|
static const uint8_t kSologicKeyboard[] = {
|
|
0x05, 0x01, 0x09, 0x06, 0xA1, 0x01, 0x85, 0x02, 0x05, 0x07, 0x19, 0xE0, 0x29, 0xE7, 0x15, 0x00,
|
|
0x25, 0x01, 0x75, 0x01, 0x95, 0x08, 0x81, 0x02, 0x95, 0x01, 0x75, 0x08, 0x81, 0x01, 0x95, 0x05,
|
|
0x75, 0x08, 0x15, 0x00, 0x26, 0xFF, 0x00, 0x05, 0x07, 0x19, 0x00, 0x29, 0xFF, 0x81, 0x00, 0x05,
|
|
0xFF, 0x09, 0x03, 0x75, 0x08, 0x95, 0x01, 0x81, 0x02, 0x95, 0x05, 0x75, 0x01, 0x05, 0x08, 0x19,
|
|
0x01, 0x29, 0x05, 0x91, 0x02, 0x95, 0x01, 0x75, 0x03, 0x91, 0x03, 0xC0, 0x05, 0x0C, 0x09, 0x01,
|
|
0xA1, 0x01, 0x85, 0x03, 0x75, 0x10, 0x95, 0x01, 0x15, 0x00, 0x26, 0x80, 0x03, 0x19, 0x00, 0x2A,
|
|
0x80, 0x03, 0x81, 0x00, 0xC0,
|
|
};
|
|
|
|
// Only media keys: no keyboard report at all.
|
|
static const uint8_t kMediaOnly[] = {
|
|
0x05, 0x0C, 0x09, 0x01, 0xA1, 0x01, 0x85, 0x02,
|
|
0x15, 0x00, 0x25, 0x01, 0x75, 0x01, 0x95, 0x02, 0x09, 0xB5, 0x09, 0xB6, 0x81, 0x02,
|
|
0x95, 0x06, 0x81, 0x01,
|
|
0xC0,
|
|
};
|
|
|
|
static void expectKeys(const uint8_t keys[6], const uint8_t expect[6]) {
|
|
for (int i = 0; i < 6; i++) {
|
|
if (keys[i] != expect[i]) {
|
|
printf("key %d: got 0x%02X, want 0x%02X\n", i, keys[i], expect[i]);
|
|
assert(false);
|
|
}
|
|
}
|
|
}
|
|
|
|
static void testSpecKeyboard() {
|
|
HidReportMapInfo info;
|
|
assert(hidParseReportMap(kSpecKeyboard, sizeof kSpecKeyboard, &info));
|
|
assert(info.kbd_count == 1);
|
|
const HidKeyboardReport& k = info.kbd[0];
|
|
assert(k.report_id == 0);
|
|
assert(k.mod_bit == 0);
|
|
assert(k.keys_bit == 16 && k.keys_count == 6 && k.keys_size == 8);
|
|
assert(k.bitmap_bit < 0);
|
|
assert(k.bits == 64);
|
|
assert(info.have_leds && info.leds.report_id == 0);
|
|
assert(info.leds.num_bit == 0 && info.leds.caps_bit == 1 && info.leds.bits == 8);
|
|
|
|
const uint8_t shift_a[8] = {0x02, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00};
|
|
uint8_t mod = 0xFF, keys[6];
|
|
assert(hidDecodeKeyboard(k, shift_a, sizeof shift_a, &mod, keys));
|
|
assert(mod == 0x02);
|
|
const uint8_t want[6] = {0x04};
|
|
expectKeys(keys, want);
|
|
|
|
const uint8_t rollover[8] = {0x00, 0x00, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01};
|
|
assert(!hidDecodeKeyboard(k, rollover, sizeof rollover, &mod, keys));
|
|
const uint8_t shorter[3] = {0x00, 0x00, 0x04};
|
|
assert(!hidDecodeKeyboard(k, shorter, sizeof shorter, &mod, keys));
|
|
}
|
|
|
|
static void testBleKeyboard() {
|
|
HidReportMapInfo info;
|
|
assert(hidParseReportMap(kBleKeyboard, sizeof kBleKeyboard, &info));
|
|
assert(info.kbd_count == 1); // the media keys are not a keyboard report
|
|
const HidKeyboardReport& k = info.kbd[0];
|
|
assert(k.report_id == 1);
|
|
assert(k.mod_bit == 0 && k.keys_bit == 16 && k.bits == 64);
|
|
assert(info.have_leds && info.leds.report_id == 1 && info.leds.caps_bit == 1);
|
|
|
|
// "y", then "y" and "u" together.
|
|
uint8_t mod = 0, keys[6];
|
|
const uint8_t y[8] = {0x00, 0x00, 0x1C, 0x00, 0x00, 0x00, 0x00, 0x00};
|
|
assert(hidDecodeKeyboard(k, y, sizeof y, &mod, keys));
|
|
const uint8_t want_y[6] = {0x1C};
|
|
expectKeys(keys, want_y);
|
|
const uint8_t yu[8] = {0x00, 0x00, 0x1C, 0x18, 0x00, 0x00, 0x00, 0x00};
|
|
assert(hidDecodeKeyboard(k, yu, sizeof yu, &mod, keys));
|
|
const uint8_t want_yu[6] = {0x1C, 0x18};
|
|
expectKeys(keys, want_yu);
|
|
|
|
uint8_t led[4];
|
|
assert(hidEncodeLeds(info.leds, false, true, led, sizeof led) == 1 && led[0] == 0x02);
|
|
assert(hidEncodeLeds(info.leds, true, true, led, sizeof led) == 1 && led[0] == 0x03);
|
|
assert(hidEncodeLeds(info.leds, false, false, led, sizeof led) == 1 && led[0] == 0x00);
|
|
}
|
|
|
|
static void testSologicKeyboard() {
|
|
assert(sizeof kSologicKeyboard == 101);
|
|
HidReportMapInfo info;
|
|
assert(hidParseReportMap(kSologicKeyboard, sizeof kSologicKeyboard, &info));
|
|
assert(info.kbd_count == 1);
|
|
const HidKeyboardReport& k = info.kbd[0];
|
|
assert(k.report_id == 2);
|
|
assert(k.mod_bit == 0);
|
|
assert(k.keys_bit == 16 && k.keys_count == 5 && k.keys_size == 8);
|
|
assert(k.keys_lmin == 0 && k.keys_lmax == 255 && k.keys_umin == 0);
|
|
assert(k.bitmap_bit < 0);
|
|
assert(k.bits == 64);
|
|
assert(info.have_leds && info.leds.report_id == 2);
|
|
assert(info.leds.num_bit == 0 && info.leds.caps_bit == 1 && info.leds.bits == 8);
|
|
|
|
// "y" and "u" held (as captured), then a report with only the vendor byte set,
|
|
// which is not a key.
|
|
uint8_t mod = 0, keys[6];
|
|
const uint8_t yu[8] = {0x00, 0x00, 0x1C, 0x18, 0x00, 0x00, 0x00, 0x00};
|
|
assert(hidDecodeKeyboard(k, yu, sizeof yu, &mod, keys));
|
|
const uint8_t want_yu[6] = {0x1C, 0x18};
|
|
expectKeys(keys, want_yu);
|
|
const uint8_t vendor[8] = {0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x5A};
|
|
assert(hidDecodeKeyboard(k, vendor, sizeof vendor, &mod, keys));
|
|
assert(mod == 0x02);
|
|
const uint8_t none[6] = {};
|
|
expectKeys(keys, none);
|
|
}
|
|
|
|
static void testNkroKeyboard() {
|
|
HidReportMapInfo info;
|
|
assert(hidParseReportMap(kNkroKeyboard, sizeof kNkroKeyboard, &info));
|
|
assert(info.kbd_count == 1);
|
|
const HidKeyboardReport& k = info.kbd[0];
|
|
assert(k.report_id == 6);
|
|
assert(k.mod_bit == 0);
|
|
assert(k.keys_bit < 0);
|
|
assert(k.bitmap_bit == 8 && k.bitmap_first == 0 && k.bitmap_count == 240);
|
|
assert(k.bits == 248);
|
|
assert(info.have_leds && info.leds.report_id == 6 && info.leds.num_bit == 0 && info.leds.caps_bit == 1);
|
|
|
|
uint8_t report[31] = {};
|
|
report[0] = 0x02; // left shift
|
|
report[(8 + 0x04) / 8] |= 1 << ((8 + 0x04) % 8); // a
|
|
report[(8 + 0x1E) / 8] |= 1 << ((8 + 0x1E) % 8); // 1
|
|
report[(8 + 0xE4) / 8] |= 1 << ((8 + 0xE4) % 8); // right ctrl, as a bitmap bit
|
|
uint8_t mod = 0, keys[6];
|
|
assert(hidDecodeKeyboard(k, report, sizeof report, &mod, keys));
|
|
assert(mod == (0x02 | 0x10));
|
|
const uint8_t want[6] = {0x04, 0x1E};
|
|
expectKeys(keys, want);
|
|
|
|
// Eight keys down: the first six are kept.
|
|
uint8_t many[31] = {};
|
|
for (int u = 0x04; u < 0x0C; u++) many[(8 + u) / 8] |= (uint8_t)(1 << ((8 + u) % 8));
|
|
assert(hidDecodeKeyboard(k, many, sizeof many, &mod, keys));
|
|
const uint8_t want_many[6] = {0x04, 0x05, 0x06, 0x07, 0x08, 0x09};
|
|
expectKeys(keys, want_many);
|
|
|
|
// Cut short (a notification limited to 20 bytes): the bitmap does not fit.
|
|
assert(!hidDecodeKeyboard(k, report, 20, &mod, keys));
|
|
}
|
|
|
|
static void testKeyboardAndTouchpad() {
|
|
HidReportMapInfo info;
|
|
assert(hidParseReportMap(kKeyboardAndTouchpad, sizeof kKeyboardAndTouchpad, &info));
|
|
assert(info.kbd_count == 1);
|
|
assert(info.kbd[0].report_id == 1);
|
|
assert(info.kbd[0].bits == 64);
|
|
assert(!info.have_leds);
|
|
}
|
|
|
|
static void testQuirkyKeyboard() {
|
|
HidReportMapInfo info;
|
|
assert(hidParseReportMap(kQuirkyKeyboard, sizeof kQuirkyKeyboard, &info));
|
|
assert(info.kbd_count == 1);
|
|
const HidKeyboardReport& k = info.kbd[0];
|
|
assert(k.mod_bit == 0);
|
|
assert(k.keys_bit == 16 && k.keys_count == 5);
|
|
assert(k.keys_lmin == 0 && k.keys_lmax == 255);
|
|
assert(k.bits == 56);
|
|
|
|
const uint8_t report[7] = {0x40, 0x00, 0x14, 0xE1, 0x00, 0x00, 0x00}; // AltGr + q, shift as an entry
|
|
uint8_t mod = 0, keys[6];
|
|
assert(hidDecodeKeyboard(k, report, sizeof report, &mod, keys));
|
|
assert(mod == (0x40 | 0x02));
|
|
const uint8_t want[6] = {0x14};
|
|
expectKeys(keys, want);
|
|
}
|
|
|
|
static void testNotKeyboards() {
|
|
HidReportMapInfo info;
|
|
assert(!hidParseReportMap(kMediaOnly, sizeof kMediaOnly, &info));
|
|
assert(info.kbd_count == 0);
|
|
assert(!hidParseReportMap(nullptr, 0, &info));
|
|
|
|
// Every truncation of a valid map parses without reading past its end.
|
|
for (size_t n = 0; n < sizeof kBleKeyboard; n++) {
|
|
uint8_t buf[sizeof kBleKeyboard];
|
|
memcpy(buf, kBleKeyboard, n);
|
|
hidParseReportMap(buf, n, &info);
|
|
assert(info.kbd_count <= HidReportMapInfo::kMaxKeyboards);
|
|
}
|
|
// Garbage never yields a layout that points outside a 64-byte report and
|
|
// then decodes past it.
|
|
uint32_t seed = 12345;
|
|
for (int round = 0; round < 20000; round++) {
|
|
uint8_t buf[48];
|
|
for (uint8_t& b : buf) {
|
|
seed = seed * 1103515245u + 12345u;
|
|
b = (uint8_t)(seed >> 16);
|
|
}
|
|
if (!hidParseReportMap(buf, sizeof buf, &info)) continue;
|
|
uint8_t report[64] = {};
|
|
uint8_t mod, keys[6];
|
|
for (int i = 0; i < info.kbd_count; i++) hidDecodeKeyboard(info.kbd[i], report, sizeof report, &mod, keys);
|
|
}
|
|
}
|
|
|
|
int main() {
|
|
testSpecKeyboard();
|
|
testBleKeyboard();
|
|
testSologicKeyboard();
|
|
testNkroKeyboard();
|
|
testKeyboardAndTouchpad();
|
|
testQuirkyKeyboard();
|
|
testNotKeyboards();
|
|
printf("hid report map: ok\n");
|
|
return 0;
|
|
}
|