diff --git a/tools/build-auxkc-desktop.sh b/tools/build-auxkc-desktop.sh index 11ce806..30640e0 100755 --- a/tools/build-auxkc-desktop.sh +++ b/tools/build-auxkc-desktop.sh @@ -1,7 +1,8 @@ #!/bin/bash # SPDX-License-Identifier: MIT # Build the A16 desktop AuxKC from this checkout: AdrenoX2 (SnapAdreno, PROFILE=desktop unless KEXT_PROFILE says -# otherwise) and SnapI2CHID (PROFILE=desktop), linked the way the deployed desktop collections were: +# otherwise) and SnapI2CHID (PROFILE=desktop unless I2C_PROFILE says otherwise), linked the way the deployed desktop +# collections were: # 1. tools/build-auxkc-user.py kmutil links both kexts against the J700 kernelcache and the KDK # 2. tools/auxkc-autoload.py marks both bundles to autoload and writes packaging.json # 3. build/build-auxkc.py pin admits the collection for the installed desktop guest profile (.json) @@ -12,6 +13,8 @@ # Environment: # KEXT_PROFILE SnapAdreno profile (gpu/adreno-kext/kext/profiles/), default desktop; desktop-nocoherent for a # monitor without --gpu-coherent +# I2C_PROFILE SnapI2CHID profile (drivers/snap-i2c-hid/profiles/), default desktop; desktop-fnkeys for the +# keyboard's Fn-key layer (boot it with tools/cycle.py --input-fnkeys) # REFERENCE optional folder holding AdrenoX2.kext and SnapI2CHID.kext to compare with, section by section # (tools/macho-sections.py): for example a deployed collection's folder # ZERO_AR_DATE exported as 1 unless set: object times in the kexts' debug maps are zeroed, so two builds in the @@ -33,6 +36,7 @@ R=$(cd "$(dirname "$0")/.." && pwd) NAME=${1:?usage: tools/build-auxkc-desktop.sh NAME} case "$NAME" in */*|.*|'') echo "error: NAME is a folder name under build/current-assets" >&2; exit 2;; esac KEXT_PROFILE=${KEXT_PROFILE:-desktop} +I2C_PROFILE=${I2C_PROFILE:-desktop} export ZERO_AR_DATE=${ZERO_AR_DATE:-1} export PYTHONDONTWRITEBYTECODE=1 DEST=$R/build/current-assets/$NAME @@ -44,7 +48,7 @@ rm -rf "$WORK"; mkdir -p "$WORK" OUT=$WORK/adrenox2 PROFILE=$KEXT_PROFILE sh "$R/gpu/adreno-kext/kext/build.sh" > "$WORK/adrenox2.log" 2>&1 || { tail -20 "$WORK/adrenox2.log" >&2; exit 1; } -OUT=$WORK/snapi2chid PROFILE=desktop sh "$R/drivers/snap-i2c-hid/build.sh" > "$WORK/snapi2chid.log" 2>&1 || +OUT=$WORK/snapi2chid PROFILE=$I2C_PROFILE sh "$R/drivers/snap-i2c-hid/build.sh" > "$WORK/snapi2chid.log" 2>&1 || { tail -20 "$WORK/snapi2chid.log" >&2; exit 1; } mkdir -p "$DEST" @@ -57,7 +61,7 @@ cp "$WORK/snapi2chid/DEFINES.txt" "$DEST/SnapI2CHID-DEFINES.txt" printf '%s %s%s\n' "$d" "$(git -C "$R/$d" rev-parse HEAD)" \ "$(git -C "$R/$d" diff --quiet HEAD -- && echo '' || echo ' (uncommitted changes)')" done - echo "AdrenoX2 profile $KEXT_PROFILE, SnapI2CHID profile desktop, ZERO_AR_DATE=$ZERO_AR_DATE" + echo "AdrenoX2 profile $KEXT_PROFILE, SnapI2CHID profile $I2C_PROFILE, ZERO_AR_DATE=$ZERO_AR_DATE" } > "$DEST/sources.txt" cd "$R" diff --git a/tools/cycle.py b/tools/cycle.py index d5e1e6d..491e721 100755 --- a/tools/cycle.py +++ b/tools/cycle.py @@ -306,6 +306,12 @@ def parse(argv=None): p.add_argument('--input-active-us', type=int, default=None, help='with --present-latch: guest boot-arg snap_i2c_active_us, the poll period while touched ' '(default 4000; 0 = the configured 8 ms throughout)') + p.add_argument('--input-fnkeys', type=lambda v: int(v, 0), default=None, metavar='FLAGS', + help='with --input: guest boot-arg snap_i2c_fnkeys=FLAGS for a SnapI2CHID built with ' + 'SNAP_I2C_HID_FNKEYS (drivers/snap-i2c-hid PROFILE=desktop-fnkeys; others ignore it): 0x1 ' + 'the keyboard\'s ASUS hotkeys as macOS usages, 0x2 the rate-limited raw log of what the F-row ' + 'sends, 0x4 the host Fn-lock on Fn+Esc (needs 0x1), 0x8/0x10 the brightness encoding ' + '(hid-keyboard.h); give it to every mode of the attempt') p.add_argument('--hide-amx', action='store_true', help='clear the application AMX-version capability field; preserves kernel AMXIDR ' 'and does not expose SME; give it to every mode of the attempt') diff --git a/tools/cycle_lib/installed_fresh.py b/tools/cycle_lib/installed_fresh.py index c40bc4d..6e1b390 100644 --- a/tools/cycle_lib/installed_fresh.py +++ b/tools/cycle_lib/installed_fresh.py @@ -1194,6 +1194,16 @@ def boot_addition(args): if active_us is not None: need(active_us == 0 or 2000 <= active_us <= 100000, '--input-active-us must be 0 or 2000..100000') extra.append(f'snap_i2c_active_us={active_us}') + # --input-fnkeys: the keyboard's Fn-key layer (drivers/snap-i2c-hid/src/hid-keyboard.h HID_FNKEYS_*), read by a + # SnapI2CHID built with SNAP_I2C_HID_FNKEYS; the same sets the kext refuses are refused here. + fnkeys = getattr(args, 'input_fnkeys', None) + if fnkeys is not None: + need(getattr(args, 'input', False), '--input-fnkeys needs --input (the SnapI2CHID AuxKC)') + need(0 < fnkeys <= 0x1f and not fnkeys & ~0x1f, '--input-fnkeys must be 0x1..0x1f (hid-keyboard.h HID_FNKEYS_*)') + need((fnkeys >> 3) & 3 != 3, '--input-fnkeys: brightness encoding 3 (0x18) does not exist') + need(not fnkeys & 0x4 or fnkeys & 0x1, '--input-fnkeys: the host Fn-lock (0x4) needs translation (0x1)') + need(fnkeys & 0x3, '--input-fnkeys: give translation (0x1) or the raw log (0x2)') + extra.append(f'snap_i2c_fnkeys={fnkeys}') if zone is not None: need(ZONE_LOG_NAME.fullmatch(zone), f'--zone-log {zone!r}: XNU zone names are [A-Za-z0-9_.], at most 64') extra.append(f'zlog1={zone}') diff --git a/tools/test-input-fnkeys.py b/tools/test-input-fnkeys.py new file mode 100644 index 0000000..e2a6a41 --- /dev/null +++ b/tools/test-input-fnkeys.py @@ -0,0 +1,56 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: MIT +"""Host test for cycle.py installed-fresh --input-fnkeys. No A16 access. + +Without the option the boot-argument addition is exactly what earlier attempts qualified; with it, it appends +snap_i2c_fnkeys=FLAGS (decimal, as XNU's boot-arg parser reads any number), and refuses what SnapI2CHID's +hid_fnkeys_init would refuse (drivers/snap-i2c-hid/src/hid-keyboard.h) and a run without the input AuxKC. +""" +import argparse +import importlib.util +import sys +import unittest +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(ROOT / 'tools')) +from cycle_lib import installed_fresh as fresh # noqa: E402 +from cycle_lib.common import Refused # noqa: E402 + +spec = importlib.util.spec_from_file_location('cycle_cli', ROOT / 'tools/cycle.py') +cli = importlib.util.module_from_spec(spec) +spec.loader.exec_module(cli) + +BASE = ' '.join([fresh.TIMESYNC_FLAG, fresh.COMPRESSOR_SW_FLAG]) + + +def ns(**kw): + return argparse.Namespace(**kw) + + +class InputFnKeys(unittest.TestCase): + def test_default_unchanged(self): + self.assertEqual(fresh.boot_addition(ns()), BASE) + self.assertEqual(fresh.boot_addition(ns(input=True, input_fnkeys=None)), BASE) + + def test_flags(self): + for flags in (0x1, 0x2, 0x3, 0x5, 0x7, 0x9, 0x13, 0x17): + self.assertEqual(fresh.boot_addition(ns(input=True, input_fnkeys=flags)), f'{BASE} snap_i2c_fnkeys={flags}') + self.assertEqual(fresh.boot_addition(ns(input=True, console=True, input_fnkeys=3)), + ' '.join([BASE, fresh.CONSOLE_FLAG, 'snap_i2c_fnkeys=3'])) + + def test_refusals(self): + for flags in (0, -1, 0x20, 0x100, 0x19, 0x1f, 0x4, 0x6, 0x8, 0x10): + with self.assertRaises(Refused, msg=hex(flags)): + fresh.boot_addition(ns(input=True, input_fnkeys=flags)) + with self.assertRaises(Refused): + fresh.boot_addition(ns(input_fnkeys=3)) + + def test_cli_reaches_args(self): + a = cli.parse(['installed-fresh', '--prefix', 'x', '--variant', 'fast', '--check', '--input', '--input-fnkeys', + '0x3']) + self.assertEqual(a.input_fnkeys, 3) + + +if __name__ == '__main__': + unittest.main() diff --git a/tools/test-snap-i2c-hid-kext.py b/tools/test-snap-i2c-hid-kext.py index 841afcb..f561183 100755 --- a/tools/test-snap-i2c-hid-kext.py +++ b/tools/test-snap-i2c-hid-kext.py @@ -28,6 +28,11 @@ No hardware. docs/A16-INPUT-INSTALLED-20261017.md. Checks, in order: previous run's leftover controller after a panic and a chainload, v39, and the Precision Touchpad switch behind its feature hook, docs/A16-INPUT-PTP-20261020.md), then mutations of the driver that must fail it. + 8. With -DSNAP_I2C_HID_LATENCY=1: every scenario, the latency ones, and their mutations. + 9. With -DSNAP_I2C_HID_FNKEYS=1 (the keyboard's Fn-key layer, hid-keyboard.h): the transport test + with its Fn-key groups, mutations of hid-keyboard.c it must catch, every glue scenario plus the + Fn-key ones (alone and with SNAP_I2C_HID_LATENCY, the desktop-fnkeys profile's pair), and + mutations of the glue. """ from pathlib import Path import argparse @@ -89,6 +94,50 @@ LATENCY_MUTATIONS = ( ('an uncapped lead', ' if (lead > cap)\n lead = cap; /* T3 */', ' (void)cap;'), ('ignore the boot-arg', '(void)PE_parse_boot_argn("snap_i2c_active_us", &active, sizeof(active));', '(void)0;'), ) +# The keyboard's Fn-key layer, with -DSNAP_I2C_HID_FNKEYS=1 only (test_9_fnkeys). +FNKEYS_FLAG = '-DSNAP_I2C_HID_FNKEYS=1' +FNKEYS_SCENARIOS = ('fnkeys-off', 'fnkeys-translate', 'fnkeys-raw-log', 'fnkeys-yield', 'fnkeys-host-lock', + 'fnkeys-bootarg', 'fnkeys-rejected') +# (description, old, new) in src/hid-keyboard.c: each must fail tools/test-snap-i2c-hid.c's Fn-key groups. +FNKEYS_C_MUTATIONS = ( + ('swap brightness up and down', '*usage = up ? 0x0004u : 0x0005u;', '*usage = up ? 0x0005u : 0x0004u;'), + ('pass report 55\'s brightness through', 'if (page == HID_KBD_PAGE_CONSUMER && frow[i].consumer == usage)', 'if (0)'), + ('map microphone mute to System Speaker Mute', '0x00a9}, /* F10', '0x00a7}, /* F10'), + ('release as the mapping now, not as pressed', + ' *op = h->out_page;\n *ou = h->out_usage;\n memset(h, 0, sizeof(*h));', + ' memset(h, 0, sizeof(*h));\n remap(c->fn, page, usage, op, ou);'), + ('raw-log every key', '#define BAND_FIRST 0x3au', '#define BAND_FIRST 0x04u'), + ('take any modifier for a chord', '#define MODS_GUI 0x88u', '#define MODS_GUI 0xffu'), + ('take 0xFF for a code', 'r[1 + i] && r[1 + i] != 0xffu &&', 'r[1 + i] &&'), + ('flip the lock without HID_FNKEYS_HOST_LOCK', 'if (v == VENDOR_FN_ESC && (fn->flags & HID_FNKEYS_HOST_LOCK)) {', + 'if (v == VENDOR_FN_ESC) {'), + ('flip the lock again while Fn+Esc is held', ' if (contains(fn->vendor, HID_FNKEYS_VENDOR_SLOTS, v))\n continue;', + ' if (contains(fn->vendor, HID_FNKEYS_VENDOR_SLOTS, v) && v != VENDOR_FN_ESC)\n continue;'), + ('accept report 90 at any length', 'if (len != HID_KBD_VENDOR_BYTES) {', 'if (len < 2u) {'), + ('translate without HID_FNKEYS_TRANSLATE', ' if (!translate)\n return;\n if (page == HID_KBD_PAGE_KEYBOARD)', + ' if (page == HID_KBD_PAGE_KEYBOARD)'), + ('dispatch reports 54 and 55 while yielded', + ' if (fn->yielded)\n return result == HID_KBD_EVENTS ? HID_KBD_IGNORED : result;\n', ''), + ('never drop a raw-log line', 'if (!rate->tokens || rate->lines >= HID_FNKEYS_RATE_LIFETIME) {', 'if (0) {'), + ('no lifetime cap', 'if (!rate->tokens || rate->lines >= HID_FNKEYS_RATE_LIFETIME) {', 'if (!rate->tokens) {'), + ('accept unknown flags', 'if (flags & ~HID_FNKEYS_KNOWN)\n return HID_FNKEYS_REFUSED_BITS;', ''), +) +# (description, old, new) in SnapI2CHID.cpp's unit: each must fail one of FNKEYS_SCENARIOS. +FNKEYS_MUTATIONS = ( + ('on without the boot-arg', 'hid_fnkeys_init(&fFn, given ? flags : 0u);', 'hid_fnkeys_init(&fFn, given ? flags : 1u);'), + ('drop report 90 while yielded', + ' (void)hid_fnkeys_translate(&fFn, &fState, report, len, emit, this, note, this);', ' (void)0;'), + ('keep dispatching reports 54 and 55 after yielding', ' fFn.yielded = 1;', ' (void)0;'), + ('release held hotkeys raw when yielding', ' hid_fnkeys_release_keyboard(&fFn, &fState, emit, this);', + ' hid_kbd_release_all(&fState, emit, this);'), + ('stop without releasing report 90', ' hid_fnkeys_release_all(&fFn, &fState, emit, this);', + ' hid_kbd_release_all(&fState, emit, this);'), + ('log past the rate limit', ' if (!hid_fnkeys_rate_allow(&fRate, uptimeUs(now)))\n return false;', + ' (void)hid_fnkeys_rate_allow(&fRate, uptimeUs(now));'), + ('never name refused inputs', ' fKeyboard->noteRejected(fDev->rejected, fDev->rejected_len);', ' (void)0;'), + ('show a refused keyboard report\'s bytes', ' if (input[2] == HID_KBD_REPORT_KEYS) {', ' if (0) {'), + ('name refused inputs without the raw log', ' if (!rawLog() || len < 3u || !mayLog())', ' if (len < 3u || !mayLog())'), +) STANDIN_HEADERS = ('IOKit/IOBufferMemoryDescriptor.h', 'IOKit/IOCommandGate.h', 'IOKit/IOLib.h', 'IOKit/IOService.h', 'IOKit/IOTimerEventSource.h', 'IOKit/IOWorkLoop.h', 'IOKit/hid/IOHIDDevice.h', 'IOKit/hid/IOHIDDeviceKeys.h', 'IOKit/hid/IOHIDKeys.h', @@ -207,15 +256,18 @@ def build_standin(work): return inc -def compile_harness(work, driver_cpp, tag, flags=()): +def compile_harness(work, driver_cpp, tag, flags=(), c_flags=()): + """c_flags: defines the hardware-independent units also take (SNAP_I2C_HID_FNKEYS: hid-keyboard.c); their + objects are cached per define set.""" out = work / f'start-{tag}' src = DRIVER / 'src' objs = [] + suffix = ''.join('-' + f.lstrip('-D').split('=')[0].lower() for f in c_flags) for name in ('geni-i2c', 'hid-rdesc', 'i2c-hid', 'hid-keyboard', 'hid-pointer', 'hid-ptp', 'hid-mt2'): - obj = work / f'{name}.o' + obj = work / f'{name}{suffix}.o' if not obj.exists(): subprocess.run(['cc', '-std=gnu11', '-O1', '-g', '-fsanitize=address,undefined', '-Wall', '-Wextra', - '-Werror', f'-I{src}', '-c', str(src / f'{name}.c'), '-o', str(obj)], check=True) + '-Werror', *c_flags, f'-I{src}', '-c', str(src / f'{name}.c'), '-o', str(obj)], check=True) objs.append(str(obj)) harness = work / f'harness-{tag}.cpp' text = (ROOT / 'tools/test-snap-i2c-hid-start.cpp').read_text() @@ -396,6 +448,56 @@ class Kext(unittest.TestCase): survivors.append(what) self.assertEqual(survivors, [], 'latency mutations the harness did not catch') + def test_9_fnkeys(self): + # The keyboard's Fn-key layer (hid-keyboard.h): the transport test with its Fn-key groups, then every mutation + # of hid-keyboard.c it must catch; the glue with the define passes every scenario (built in and off by + # default, the keyboard is today's) and the Fn-key ones, also with SNAP_I2C_HID_LATENCY (the desktop-fnkeys + # profile's pair); every mutation of the glue is caught. + src = DRIVER / 'src' + units = [src / f'{n}.c' for n in ('geni-i2c', 'hid-rdesc', 'i2c-hid', 'hid-pointer', 'hid-ptp', 'hid-mt2')] + + def transport(keyboard_c, tag): + out = self.work / f'test-snap-i2c-hid-{tag}' + built = run('cc', '-std=gnu11', '-O1', '-g', '-fsanitize=address,undefined', '-Wall', '-Wextra', '-Werror', + FNKEYS_FLAG, f'-I{src}', str(ROOT / 'tools/test-snap-i2c-hid.c'), *map(str, units), + str(keyboard_c), '-o', str(out)) + if built.returncode: + return built + return run(str(out), cwd=ROOT) + + result = transport(src / 'hid-keyboard.c', 'fnkeys') + self.assertEqual(result.returncode, 0, result.stdout[-2000:] + result.stderr[-2000:]) + self.assertRegex(result.stdout, r"\b\d+ checks passed \(34 groups and the Fn-key layer's 8\)") + text = (src / 'hid-keyboard.c').read_text() + survivors = [] + for n, (what, old, new) in enumerate(FNKEYS_C_MUTATIONS): + self.assertEqual(text.count(old), 1, what) + mutant = self.work / f'fnkeys-c-mutant-{n}' / 'hid-keyboard.c' + mutant.parent.mkdir(exist_ok=True) + mutant.write_text(text.replace(old, new)) + if transport(mutant, f'fnkeys-c-mutant-{n}').returncode == 0: + survivors.append(what) + self.assertEqual(survivors, [], 'hid-keyboard.c mutations the transport test did not catch') + + for tag, flags, scenarios in (('fnkeys', (FNKEYS_FLAG,), SCENARIOS + FNKEYS_SCENARIOS), + ('fnkeys-latency', (FNKEYS_FLAG, '-DSNAP_I2C_HID_LATENCY=1'), + SCENARIOS + LATENCY_SCENARIOS + FNKEYS_SCENARIOS)): + harness = compile_harness(self.work, DRIVER / 'src/SnapI2CHID.cpp', tag, flags, (FNKEYS_FLAG,)) + for scenario in scenarios: + result = run(str(harness), scenario) + self.assertEqual(result.returncode, 0, f'{tag} {scenario}: {result.stdout[-1500:]}{result.stderr[-1500:]}') + self.assertIn(f'PASS {scenario}', result.stdout) + survivors = [] + text = read_source(DRIVER / 'src/SnapI2CHID.cpp') + for n, (what, old, new) in enumerate(FNKEYS_MUTATIONS): + self.assertEqual(text.count(old), 1, what) + mutant = self.work / f'fnkeys-mutant-{n}.cpp' + mutant.write_text(text.replace(old, new)) + binary = compile_harness(self.work, mutant, f'fnkeys-mutant-{n}', (FNKEYS_FLAG,), (FNKEYS_FLAG,)) + if all(run(str(binary), s).returncode == 0 for s in FNKEYS_SCENARIOS): + survivors.append(what) + self.assertEqual(survivors, [], 'Fn-key glue mutations the harness did not catch') + def main(): global ARGS, REST diff --git a/tools/test-snap-i2c-hid-start.cpp b/tools/test-snap-i2c-hid-start.cpp index 79b3528..2aea67d 100644 --- a/tools/test-snap-i2c-hid-start.cpp +++ b/tools/test-snap-i2c-hid-start.cpp @@ -367,6 +367,234 @@ static SnapI2CHIDBus *ready_bus(const uint8_t *hid_desc, const uint8_t *rdesc, u return bus; } +#if SNAP_I2C_HID_FNKEYS +// ------------------------------------------------------------- Fn-key layer ---- +// Built with -DSNAP_I2C_HID_FNKEYS=1 (and hid-keyboard.o with it): the layer is off without the boot-arg +// snap_i2c_fnkeys, and the keyboard is then exactly today's; with it, report 90 becomes macOS's usages, the raw log +// names the F-row and hotkeys but never a character key and holds to its rate limit, report 90 stays ours while an +// event driver holds the raw device, a refused boot-arg leaves the layer off and says why, and the bus names inputs +// the transport refused only on the keyboard and only with the raw log. +static SnapI2CHIDBus *fn_bus(bool set, uint32_t flags) +{ + standin.boot_args.clear(); + if (set) + standin.boot_args["snap_i2c_fnkeys"] = flags; + SnapI2CHIDBus *kb = ready_bus(a16_eckb_hid_desc, a16_eckb_report_desc, sizeof(a16_eckb_report_desc)); + kb->publish(); + CHECK(kb->fKeyboard, "the keyboard service runs"); + return kb; +} +static void fn_report(SnapI2CHIDBus *kb, std::vector r) +{ + standin.now_ns += 8000000u; + kb->fKeyboard->handleReport(r.data(), (uint32_t)r.size()); +} +static void fn_vendor(SnapI2CHIDBus *kb, uint8_t code) { fn_report(kb, {0x5a, code, 0, 0, 0, 0}); } +static void fn_keys(SnapI2CHIDBus *kb, uint8_t mods, uint8_t key) { fn_report(kb, {0x36, mods, 0, key, 0, 0, 0, 0, 0}); } +static void fn_consumer(SnapI2CHIDBus *kb, uint16_t u) { fn_report(kb, {0x37, (uint8_t)u, (uint8_t)(u >> 8)}); } +static bool fn_events(size_t from, const std::vector> &want) +{ + bool same = standin.key_events.size() == from + want.size(); + for (size_t i = 0; same && i < want.size(); i++) { + const auto &e = standin.key_events[from + i]; + same = e.page == want[i][0] && e.usage == want[i][1] && e.value == want[i][2]; + } + if (!same) + for (size_t i = from; i < standin.key_events.size(); i++) + printf(" event %zu: 0x%x/0x%x %u\n", i, standin.key_events[i].page, standin.key_events[i].usage, + standin.key_events[i].value); + return same; +} +static size_t occurrences(const char *text) +{ + size_t n = 0; + for (size_t at = standin.log.find(text); at != std::string::npos; at = standin.log.find(text, at + 1)) + n++; + return n; +} +static void fn_done(SnapI2CHIDBus *kb) +{ + kb->stop(kb->fProvider); + kb->release(); +} + +static bool fnkeys_scenario(const std::string &s) +{ + if (s == "fnkeys-off") { + const struct { bool set; uint32_t flags; const char *log; } cases[] = { + {false, 0, "fn keys: off (no snap_i2c_fnkeys boot-arg)"}, {true, 0, "fn keys: off (snap_i2c_fnkeys 0x0)"}, + {true, 0x10, "fn keys: off (snap_i2c_fnkeys 0x10)"}}; + for (const auto &c : cases) { + standin.log.clear(); + standin.key_events.clear(); + SnapI2CHIDBus *kb = fn_bus(c.set, c.flags); + CHECK(kb->fKeyboard->fFn.flags == 0 && logged(c.log), "the layer is off, and says so"); + fn_vendor(kb, 0x10); fn_vendor(kb, 0); + fn_consumer(kb, 0x70); fn_consumer(kb, 0); + fn_keys(kb, 0, 0x3e); fn_keys(kb, 0, 0); + CHECK(fn_events(0, {{12, 0x70, 1}, {12, 0x70, 0}, {7, 0x3e, 1}, {7, 0x3e, 0}}), + "report 90 dispatches nothing, reports 55 and 54 as without the layer"); + CHECK(standin.log.find("fn keys: report") == std::string::npos, "and nothing is raw-logged"); + fn_done(kb); + } + } else if (s == "fnkeys-translate") { + SnapI2CHIDBus *kb = fn_bus(true, 0x1); + CHECK(logged("fn keys: snap_i2c_fnkeys 0x1: translate on, raw log off, host Fn-lock off; brightness down " + "0x00ff/0x0005, up 0x00ff/0x0004; input report 90 6 bytes in the descriptor"), + "the start line names the flags, the brightness usages and the descriptor's report 90"); + fn_vendor(kb, 0x10); fn_vendor(kb, 0); + fn_vendor(kb, 0x20); fn_vendor(kb, 0); + fn_vendor(kb, 0x7c); fn_vendor(kb, 0); + fn_vendor(kb, 0x86); fn_vendor(kb, 0); // MyASUS: no macOS usage + fn_consumer(kb, 0x6f); fn_consumer(kb, 0xe9); fn_consumer(kb, 0); + fn_keys(kb, 0, 0x3e); fn_keys(kb, 0, 0); + CHECK(fn_events(0, {{0xff, 5, 1}, {0xff, 5, 0}, {0xff, 4, 1}, {0xff, 4, 0}, {1, 0xa9, 1}, {1, 0xa9, 0}, + {0xff, 4, 1}, {0xff, 4, 0}, {12, 0xe9, 1}, {12, 0xe9, 0}, {7, 0x3e, 1}, {7, 0x3e, 0}}), + "hotkeys and report 55's brightness as macOS's usages, F5 as F5"); + CHECK(logged("keyboard dispatched 1 key transitions from 1 reports"), "progress is still counted"); + CHECK(standin.log.find("fn keys: report") == std::string::npos && standin.log.find("0x3e") == std::string::npos, + "no raw log without its bit, and no key usage in the log"); + fn_vendor(kb, 0x20); + size_t at = standin.key_events.size(); + kb->stop(kb->fProvider); + CHECK(fn_events(at, {{0xff, 4, 0}}), "stop releases a held hotkey as what it was dispatched as"); + kb->release(); + } else if (s == "fnkeys-raw-log") { + SnapI2CHIDBus *kb = fn_bus(true, 0x3); + fn_vendor(kb, 0x10); fn_vendor(kb, 0); + fn_vendor(kb, 0x6b); fn_vendor(kb, 0); + fn_keys(kb, 0, 0x3e); fn_keys(kb, 0, 0); + fn_consumer(kb, 0xe9); fn_consumer(kb, 0); + fn_keys(kb, 0x0a, 0x72); fn_keys(kb, 0, 0); + fn_keys(kb, 0x08, 0x13); fn_keys(kb, 0, 0); + fn_report(kb, {0x07, 1, 2, 3, 4}); + CHECK(logged("fn keys: report 90 [5a 10 00 00 00 00]: code 0x10 down -> 0x00ff/0x0005") && + logged("fn keys: report 90 [5a 00 00 00 00 00]: code 0x10 up -> 0x00ff/0x0005"), + "report 90's bytes and what each code became"); + CHECK(logged("fn keys: report 90 [5a 6b 00 00 00 00]: code 0x6b down -> nothing"), "an unmapped code is named"); + CHECK(logged("fn keys: report 54 key 0x3e down, modifiers 0x00 -> 0x0007/0x003e"), "an F-key is named"); + CHECK(logged("fn keys: report 55 usage 0x00e9 down -> 0x000c/0x00e9"), "a Consumer usage is named"); + CHECK(logged("fn keys: report 54 key 0x72 down past the logical maximum 0x65, modifiers 0x0a: not dispatched"), + "F23 (the Copilot key's) is named"); + CHECK(logged("fn keys: report 54 chord: modifiers 0x08 and key 0x13 went down in one report"), "a chord is named"); + CHECK(logged("fn keys: report 7, 5 bytes [07 01 02 03 04]: not taken"), "an unknown report is named"); + size_t before = standin.log.size(); + for (uint8_t k = 0x04; k <= 0x39; k++) { + fn_keys(kb, 0x02, k); + fn_keys(kb, 0, 0); + } + CHECK(standin.log.find("fn keys:", before) == std::string::npos, "typing leaves no raw-log line"); + standin.log.clear(); + const uint8_t press[6] = {0x5a, 0x10, 0, 0, 0, 0}, none[6] = {0x5a, 0, 0, 0, 0, 0}; + for (int i = 0; i < 200; i++) { // in one instant + kb->fKeyboard->handleReport(press, 6); + kb->fKeyboard->handleReport(none, 6); + } + CHECK(occurrences("fn keys: report 90") <= HID_FNKEYS_RATE_BURST, "a flood is cut to the burst"); + standin.now_ns += 1000000000ull; + fn_vendor(kb, 0x20); + CHECK(logged("raw-log lines dropped by the rate limit") && logged("code 0x20 down"), + "and the next allowed line says how many were dropped"); + CHECK(fn_events(standin.key_events.size() - 1, {{0xff, 4, 1}}), "dispatching is never rate-limited"); + fn_done(kb); + } else if (s == "fnkeys-yield") { + SnapI2CHIDBus *kb = fn_bus(true, 0x1); + IOService *dk = new IOService; + dk->meta.name = "AppleUserHIDEventService"; + G.nack_commands = true; // the steps' own polls fail at once + fn_consumer(kb, 0x70); // brightness down held when the driver takes over + CHECK(fn_events(0, {{0xff, 5, 1}}), "report 55's brightness, translated"); + CHECK(kb->fHID->handleOpen(dk, 0, nullptr), "an event driver opens the raw device"); + kb->step(); + CHECK(!strcmp(kb->fKeyboard->state(), "yielded") && fn_events(1, {{0xff, 5, 0}}), + "yielding releases it as what it was dispatched as"); + fn_keys(kb, 0, 0x04); fn_keys(kb, 0, 0); + fn_consumer(kb, 0x6f); fn_consumer(kb, 0); + CHECK(fn_events(2, {}), "reports 54 and 55 are the event driver's"); + fn_vendor(kb, 0x20); fn_vendor(kb, 0); + CHECK(fn_events(2, {{0xff, 4, 1}, {0xff, 4, 0}}), "report 90 is still ours"); + kb->fHID->handleClose(dk, 0); + kb->step(); + fn_keys(kb, 0, 0x04); fn_keys(kb, 0, 0); + CHECK(fn_events(4, {{7, 4, 1}, {7, 4, 0}}), "closed, the keyboard dispatches again"); + dk->release(); + fn_done(kb); + } else if (s == "fnkeys-host-lock") { + SnapI2CHIDBus *kb = fn_bus(true, 0x7); + fn_vendor(kb, 0x10); fn_vendor(kb, 0); + fn_vendor(kb, 0x4e); fn_vendor(kb, 0); + CHECK(logged("fn keys: host Fn-lock on: F1-F12 first, hotkeys with Fn"), "Fn+Esc turns the host lock on"); + fn_vendor(kb, 0x10); fn_vendor(kb, 0); + fn_keys(kb, 0, 0x3e); fn_keys(kb, 0, 0); + fn_vendor(kb, 0x4e); fn_vendor(kb, 0); + CHECK(logged("fn keys: host Fn-lock off: hotkeys first, F1-F12 with Fn"), "and off again"); + fn_vendor(kb, 0x10); fn_vendor(kb, 0); + CHECK(fn_events(0, {{0xff, 5, 1}, {0xff, 5, 0}, {7, 0x3e, 1}, {7, 0x3e, 0}, {0xff, 5, 1}, {0xff, 5, 0}, + {0xff, 5, 1}, {0xff, 5, 0}}), + "locked, the brightness key is F5 and Fn+F5 brightness"); + fn_done(kb); + } else if (s == "fnkeys-bootarg") { + const struct { uint32_t flags; const char *log; uint32_t page, usage; } cases[] = { + {0x40, "fn keys: snap_i2c_fnkeys 0x40 refused (unknown bits); off", 0, 0}, + {0x19, "fn keys: snap_i2c_fnkeys 0x19 refused (brightness encoding 3); off", 0, 0}, + {0x04, "fn keys: snap_i2c_fnkeys 0x4 refused (host lock without translate); off", 0, 0}, + {0x09, "brightness down 0xff01/0x0021, up 0xff01/0x0020", 0xff01, 0x21}, + {0x11, "brightness down 0x000c/0x0070, up 0x000c/0x006f", 0x0c, 0x70}, + }; + for (const auto &c : cases) { + standin.log.clear(); + standin.key_events.clear(); + SnapI2CHIDBus *kb = fn_bus(true, c.flags); + CHECK(logged(c.log), "the start line"); + fn_vendor(kb, 0x10); fn_vendor(kb, 0); + CHECK(c.page ? fn_events(0, {{c.page, c.usage, 1}, {c.page, c.usage, 0}}) : fn_events(0, {}), + "a refused boot-arg leaves the layer off; an encoding picks the brightness usages"); + fn_done(kb); + } + } else if (s == "fnkeys-rejected") { + static const uint8_t unknown[] = {0x07, 0x00, 0x40, 0x01, 0x02, 0x03, 0x04}; + static const uint8_t short54[] = {0x0a, 0x00, 0x36, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00}; + SnapI2CHIDBus *kb = fn_bus(true, 0x3); + struct i2c_hid_dev *d = kb->fDev; + memcpy(d->rejected, unknown, sizeof(unknown)); + d->rejected_len = 64; + d->stats.unknown_id++; + kb->noteRejectedInput(); + CHECK(logged("fn keys: transport refused an input: length prefix 7, report 64 [40 01 02 03 04]"), + "an input the transport refused is named by the bus"); + size_t n = standin.log.size(); + kb->noteRejectedInput(); + CHECK(standin.log.size() == n, "once per refusal"); + memset(d->rejected, 0, sizeof(d->rejected)); + memcpy(d->rejected, short54, sizeof(short54)); + d->stats.wrong_size++; + kb->noteRejectedInput(); + CHECK(logged("length prefix 10, report 54 (bytes withheld)") && + standin.log.find("36 00 00 04") == std::string::npos, "a keyboard report by its size only"); + fn_done(kb); + standin.log.clear(); + kb = fn_bus(true, 0x1); // translate only: not named + memcpy(kb->fDev->rejected, unknown, sizeof(unknown)); + kb->fDev->rejected_len = 64; + kb->fDev->stats.unknown_id++; + kb->noteRejectedInput(); + SnapI2CHIDBus *tp = ready_bus(a16_tpad_hid_desc, a16_tpad_report_desc, sizeof(a16_tpad_report_desc)); + tp->publish(); + memcpy(tp->fDev->rejected, unknown, sizeof(unknown)); + tp->fDev->rejected_len = 52; + tp->fDev->stats.unknown_id++; + tp->noteRejectedInput(); // the touchpad's bus has no keyboard + CHECK(standin.log.find("transport refused") == std::string::npos, "without the raw log or a keyboard: nothing"); + tp->stop(tp->fProvider); + tp->release(); + fn_done(kb); + } else { + return false; + } + return true; +} +#endif + int main(int argc, char **argv) { std::string s = argc > 1 ? argv[1] : ""; @@ -1020,6 +1248,13 @@ int main(int argc, char **argv) bus->fTimer->release(); IOFree(bus->fDev, sizeof(*bus->fDev)); bus->fDev = nullptr; +#endif +#if SNAP_I2C_HID_FNKEYS + } else if (s.compare(0, 7, "fnkeys-") == 0) { + if (!fnkeys_scenario(s)) { + printf("unknown scenario %s\n", s.c_str()); + return 2; + } #endif } else { printf("unknown scenario %s\n", s.c_str()); diff --git a/tools/test-snap-i2c-hid.c b/tools/test-snap-i2c-hid.c index 85c545f..db175ae 100644 --- a/tools/test-snap-i2c-hid.c +++ b/tools/test-snap-i2c-hid.c @@ -15,6 +15,10 @@ * drivers/snap-i2c-hid/src/hid-pointer.c drivers/snap-i2c-hid/src/hid-ptp.c \ * -o build/test-snap-i2c-hid && build/test-snap-i2c-hid * + * Built with -DSNAP_I2C_HID_FNKEYS=1 it also runs the keyboard's Fn-key layer + * (test-snap-i2c-hid/fnkeys.c, against hid-keyboard.c built with the same define); + * tools/test-snap-i2c-hid-kext.py runs both builds. + * * The Precision Touchpad groups (test_ptp_*) cover failure modes F1-F22 of * docs/A16-INPUT-PTP-20261020.md, written before src/hid-ptp.c: the real A16 * descriptor, frames encoded by hand from its decoded offsets (not from the @@ -3191,6 +3195,10 @@ static void test_mt_frames(void) CHECK(mt_at(&MF[0].c[0], 2381, 1550) && near(MF[0].c[0].x, 0, 2) && near(MF[0].c[0].y, 5000, 2)); } +#if SNAP_I2C_HID_FNKEYS +#include "test-snap-i2c-hid/fnkeys.c" +#endif + int main(void) { test_start_and_stop(); @@ -3227,6 +3235,11 @@ int main(void) test_ptp_reset_and_reports(); test_mt_features(); test_mt_frames(); +#if SNAP_I2C_HID_FNKEYS + test_fnkeys(); + printf("test-snap-i2c-hid: %u checks passed (34 groups and the Fn-key layer's 8)\n", checks); +#else printf("test-snap-i2c-hid: %u checks passed (34 groups)\n", checks); +#endif return 0; } diff --git a/tools/test-snap-i2c-hid/fnkeys.c b/tools/test-snap-i2c-hid/fnkeys.c new file mode 100644 index 0000000..2cbea14 --- /dev/null +++ b/tools/test-snap-i2c-hid/fnkeys.c @@ -0,0 +1,477 @@ +/* SPDX-License-Identifier: MIT */ +/* The keyboard's Fn-key layer (drivers/snap-i2c-hid/src/hid-keyboard.c, SNAP_I2C_HID_FNKEYS). Part of + * tools/test-snap-i2c-hid.c, included there in order when it is built with -DSNAP_I2C_HID_FNKEYS=1; not compiled on + * its own. The usages each code must become are written out here from hid-keyboard.h's evidence, not read back + * from the implementation's tables, so a wrong table entry fails a check. */ + +static struct hid_fnkeys_note fnotes[160]; +static uint8_t fnote_raw[160][16]; +static unsigned nfnotes; + +static void fn_note(void *ctx, const struct hid_fnkeys_note *n) +{ + (void)ctx; + if (nfnotes < 160) { + fnotes[nfnotes] = *n; + if (n->raw) { + REQUIRE(n->raw_len <= 16); + memcpy(fnote_raw[nfnotes], n->raw, n->raw_len); + fnotes[nfnotes].raw = fnote_raw[nfnotes]; + } + } + nfnotes++; +} + +static int fn_vendor(struct hid_fnkeys *fn, struct hid_kbd_state *st, const uint8_t codes[5]) +{ + uint8_t r[6] = {0x5a, 0, 0, 0, 0, 0}; + memcpy(r + 1, codes, 5); + return hid_fnkeys_translate(fn, st, r, 6, kbd_emit, NULL, fn_note, NULL); +} +static int fn_code(struct hid_fnkeys *fn, struct hid_kbd_state *st, uint8_t a, uint8_t b) +{ + const uint8_t codes[5] = {a, b, 0, 0, 0}; + return fn_vendor(fn, st, codes); +} +static int fn_keys(struct hid_fnkeys *fn, struct hid_kbd_state *st, uint8_t mods, const uint8_t *k, unsigned nk) +{ + uint8_t r[9] = {0x36, mods, 0, 0, 0, 0, 0, 0, 0}; + memcpy(r + 3, k, nk); + return hid_fnkeys_translate(fn, st, r, 9, kbd_emit, NULL, fn_note, NULL); +} +static int fn_key(struct hid_fnkeys *fn, struct hid_kbd_state *st, uint8_t mods, uint8_t key) +{ + return fn_keys(fn, st, mods, &key, key ? 1u : 0u); +} +static int fn_consumer(struct hid_fnkeys *fn, struct hid_kbd_state *st, uint16_t usage) +{ + const uint8_t r[3] = {0x37, (uint8_t)usage, (uint8_t)(usage >> 8)}; + return hid_fnkeys_translate(fn, st, r, 3, kbd_emit, NULL, fn_note, NULL); +} +/* One key down then up, as exactly the two events (page, usage, 1), (page, usage, 0); 0/0: none. */ +static int fn_pair(unsigned from, uint32_t page, uint32_t usage) +{ + if (!page && !usage) + return nkev == from; + return nkev == from + 2 && kev_is(from, page, usage, 1) && kev_is(from + 1, page, usage, 0); +} +static void fn_start(struct hid_fnkeys *fn, struct hid_kbd_state *st, uint32_t flags) +{ + REQUIRE(hid_fnkeys_init(fn, flags) == HID_FNKEYS_OK); + memset(st, 0, sizeof(*st)); + nkev = 0; + nfnotes = 0; +} + +static void test_fnkeys_init(void) +{ + struct hid_fnkeys fn; + uint16_t p, u; + CHECK(hid_fnkeys_init(&fn, 0) == HID_FNKEYS_OK && fn.flags == 0); + CHECK(hid_fnkeys_init(&fn, 0x20) == HID_FNKEYS_REFUSED_BITS && fn.flags == 0); + CHECK(hid_fnkeys_init(&fn, 0x80000001u) == HID_FNKEYS_REFUSED_BITS && fn.flags == 0); + CHECK(hid_fnkeys_init(&fn, 0x01 | 0x18) == HID_FNKEYS_REFUSED_ENCODING && fn.flags == 0); + CHECK(hid_fnkeys_init(&fn, 0x04) == HID_FNKEYS_REFUSED_LOCK && fn.flags == 0); + CHECK(hid_fnkeys_init(&fn, 0x06) == HID_FNKEYS_REFUSED_LOCK && fn.flags == 0); + CHECK(hid_fnkeys_init(&fn, 0x08) == HID_FNKEYS_OK && fn.flags == 0); /* an encoding alone: off */ + CHECK(hid_fnkeys_init(&fn, 0x05) == HID_FNKEYS_OK && fn.flags == 0x05); + CHECK(hid_fnkeys_init(&fn, 0x17) == HID_FNKEYS_OK && fn.flags == 0x17); + CHECK(hid_fnkeys_init(&fn, 0x02) == HID_FNKEYS_OK && fn.flags == 0x02 && !fn.lock && !fn.yielded); + CHECK(strcmp(hid_fnkeys_init_name(HID_FNKEYS_REFUSED_BITS), hid_fnkeys_init_name(HID_FNKEYS_REFUSED_LOCK)) != 0); + /* The three brightness encodings (hid-keyboard.h): top case is the MacBook keyboard's. */ + CHECK(hid_fnkeys_init(&fn, 0x01) == HID_FNKEYS_OK); + hid_fnkeys_brightness(&fn, 0, &p, &u); + CHECK(p == 0x00ff && u == 0x0005); + hid_fnkeys_brightness(&fn, 1, &p, &u); + CHECK(p == 0x00ff && u == 0x0004); + CHECK(hid_fnkeys_init(&fn, 0x09) == HID_FNKEYS_OK); + hid_fnkeys_brightness(&fn, 0, &p, &u); + CHECK(p == 0xff01 && u == 0x0021); + hid_fnkeys_brightness(&fn, 1, &p, &u); + CHECK(p == 0xff01 && u == 0x0020); + CHECK(hid_fnkeys_init(&fn, 0x11) == HID_FNKEYS_OK); + hid_fnkeys_brightness(&fn, 0, &p, &u); + CHECK(p == 0x000c && u == 0x0070); + hid_fnkeys_brightness(&fn, 1, &p, &u); + CHECK(p == 0x000c && u == 0x006f); +} + +/* Report 90's codes: each one pressed and released alone. */ +static void test_fnkeys_vendor(void) +{ + static const struct { uint8_t code; uint16_t page, usage; } mapped[] = { + {0x10, 0x00ff, 0x0005}, {0x20, 0x00ff, 0x0004}, /* brightness down, up */ + {0xc7, 0x00ff, 0x0007}, {0xc4, 0x00ff, 0x0008}, {0xc5, 0x00ff, 0x0009}, /* keyboard light */ + {0x7c, 0x0001, 0x00a9}, /* microphone mute */ + }; + static const uint8_t unmapped[] = {0x6b, 0x82, 0x85, 0x86, 0x4e, 0x7e, 0x5f, 0x8b, 0x9d, 0xcb, 0x38, 0x35, 0x01}; + struct hid_fnkeys fn; + struct hid_kbd_state st; + + fn_start(&fn, &st, HID_FNKEYS_TRANSLATE); + for (size_t i = 0; i < sizeof(mapped) / sizeof(mapped[0]); i++) { + unsigned from = nkev; + CHECK(fn_code(&fn, &st, mapped[i].code, 0) == HID_KBD_EVENTS); + CHECK(fn_code(&fn, &st, 0, 0) == HID_KBD_EVENTS); + CHECK(fn_pair(from, mapped[i].page, mapped[i].usage)); + } + for (size_t i = 0; i < sizeof(unmapped); i++) { + unsigned from = nkev, before = fn.unmapped; + CHECK(fn_code(&fn, &st, unmapped[i], 0) == HID_KBD_EVENTS && fn.unmapped == before + 1); + CHECK(fn_code(&fn, &st, 0, 0) == HID_KBD_EVENTS && nkev == from); + } + CHECK(fn.lock == 0 && fn.lock_toggles == 0); /* 0x4E without HID_FNKEYS_HOST_LOCK: nothing */ + /* 0x00 and 0xFF are no code. */ + { + unsigned before = fn.unmapped; + const uint8_t none[5] = {0xff, 0xff, 0, 0xff, 0}; + nkev = 0; + CHECK(fn_vendor(&fn, &st, none) == HID_KBD_EVENTS && nkev == 0 && fn.unmapped == before); + CHECK(fn.vendor[0] == 0 && fn.vendor[1] == 0); + } + /* A code listed twice is one press; two codes are two; each is released when it leaves the array. */ + nkev = 0; + CHECK(fn_code(&fn, &st, 0x10, 0x10) == HID_KBD_EVENTS && nkev == 1 && kev_is(0, 0xff, 5, 1)); + CHECK(fn_code(&fn, &st, 0x10, 0x20) == HID_KBD_EVENTS && nkev == 2 && kev_is(1, 0xff, 4, 1)); + CHECK(fn_code(&fn, &st, 0x10, 0x20) == HID_KBD_EVENTS && nkev == 2); /* repeated: nothing */ + CHECK(fn_code(&fn, &st, 0x20, 0) == HID_KBD_EVENTS && nkev == 3 && kev_is(2, 0xff, 5, 0)); + /* Another length is refused and changes nothing: 0x20 stays down. */ + { + static const uint8_t five[5] = {0x5a, 0, 0, 0, 0}, seven[7] = {0x5a, 0x10, 0, 0, 0, 0, 0}; + CHECK(hid_fnkeys_translate(&fn, &st, five, 5, kbd_emit, NULL, fn_note, NULL) == HID_KBD_BAD_LENGTH); + CHECK(hid_fnkeys_translate(&fn, &st, seven, 7, kbd_emit, NULL, fn_note, NULL) == HID_KBD_BAD_LENGTH); + CHECK(nkev == 3 && fn.vendor[0] == 0x20); + } + CHECK(fn_code(&fn, &st, 0, 0) == HID_KBD_EVENTS && nkev == 4 && kev_is(3, 0xff, 4, 0)); + /* The other encodings. */ + fn_start(&fn, &st, HID_FNKEYS_TRANSLATE | (HID_FNKEYS_BRIGHTNESS_APPLE_KEYBOARD << HID_FNKEYS_BRIGHTNESS_SHIFT)); + fn_code(&fn, &st, 0x10, 0); + fn_code(&fn, &st, 0x20, 0); + fn_code(&fn, &st, 0, 0); + CHECK(nkev == 4 && kev_is(0, 0xff01, 0x21, 1) && kev_is(1, 0xff01, 0x21, 0) && kev_is(2, 0xff01, 0x20, 1)); + fn_start(&fn, &st, HID_FNKEYS_TRANSLATE | (HID_FNKEYS_BRIGHTNESS_CONSUMER << HID_FNKEYS_BRIGHTNESS_SHIFT)); + fn_code(&fn, &st, 0x10, 0); + fn_code(&fn, &st, 0, 0); + CHECK(fn_pair(0, 0x0c, 0x70)); + /* Without TRANSLATE (the raw log alone) report 90 dispatches nothing, as without the layer. */ + fn_start(&fn, &st, HID_FNKEYS_RAW_LOG); + fn_code(&fn, &st, 0x10, 0); + fn_code(&fn, &st, 0x20, 0x7c); + fn_code(&fn, &st, 0, 0); + CHECK(nkev == 0); +} + +/* Reports 54 and 55 through the layer: brightness translated, everything else exactly hid_kbd_translate's. */ +static void test_fnkeys_consumer_and_keys(void) +{ + struct hid_fnkeys fn; + struct hid_kbd_state st; + static const uint32_t flag_sets[] = {HID_FNKEYS_TRANSLATE, HID_FNKEYS_RAW_LOG, + HID_FNKEYS_TRANSLATE | HID_FNKEYS_HOST_LOCK}; + + fn_start(&fn, &st, HID_FNKEYS_TRANSLATE); + fn_consumer(&fn, &st, 0x70); + fn_consumer(&fn, &st, 0); + CHECK(fn_pair(0, 0xff, 5)); + fn_consumer(&fn, &st, 0x6f); + fn_consumer(&fn, &st, 0xe9); /* brightness up released before volume up */ + fn_consumer(&fn, &st, 0); + CHECK(nkev == 6 && kev_is(2, 0xff, 4, 1) && kev_is(3, 0xff, 4, 0) && kev_is(4, 12, 0xe9, 1) && + kev_is(5, 12, 0xe9, 0)); + nkev = 0; + fn_consumer(&fn, &st, 0xe2); + fn_consumer(&fn, &st, 0xcd); + fn_consumer(&fn, &st, 0); + CHECK(nkev == 4 && kev_is(0, 12, 0xe2, 1) && kev_is(1, 12, 0xe2, 0) && kev_is(2, 12, 0xcd, 1)); + fn_start(&fn, &st, HID_FNKEYS_TRANSLATE | (HID_FNKEYS_BRIGHTNESS_CONSUMER << HID_FNKEYS_BRIGHTNESS_SHIFT)); + fn_consumer(&fn, &st, 0x70); + fn_consumer(&fn, &st, 0); + CHECK(fn_pair(0, 12, 0x70)); + + /* Typing, F-keys and modifiers: the same events, in the same order, as without the layer (lock off). */ + for (size_t f = 0; f < sizeof(flag_sets) / sizeof(flag_sets[0]); f++) { + static const struct { uint8_t mods, keys[3]; } script[] = { + {0x00, {0x04}}, {0x00, {0}}, {0x02, {0x05, 0x06}}, {0x02, {0x06}}, {0x00, {0}}, {0x00, {0x3a}}, + {0x00, {0x3a, 0x45}}, {0x00, {0}}, {0x01, {0x3e}}, {0x05, {0x3e, 0x4f}}, {0x00, {0}}, {0x00, {0x29}}, + {0x00, {0x01, 0x01, 0x01}}, {0x00, {0x66}}, {0x00, {0}}, + }; + uint32_t want[64][3]; + unsigned nwant; + struct hid_kbd_state plain; + memset(&plain, 0, sizeof(plain)); + nkev = 0; + for (size_t i = 0; i < sizeof(script) / sizeof(script[0]); i++) + kbd_report(&plain, script[i].mods, script[i].keys, 3); + nwant = nkev; + REQUIRE(nwant <= 64); + memcpy(want, kev, sizeof(want)); + fn_start(&fn, &st, flag_sets[f]); + for (size_t i = 0; i < sizeof(script) / sizeof(script[0]); i++) + fn_keys(&fn, &st, script[i].mods, script[i].keys, 3); + CHECK(nkev == nwant && !memcmp(kev, want, sizeof(kev[0]) * nwant)); + } +} + +/* HID_FNKEYS_HOST_LOCK: Fn+Esc (0x4E) swaps the F-row's hotkeys and F1-F12. */ +static void test_fnkeys_lock(void) +{ + struct hid_fnkeys fn; + struct hid_kbd_state st; + static const struct { uint8_t code, fkey; } row[] = { + {0xc7, 0x3d}, {0x10, 0x3e}, {0x20, 0x3f}, {0x6b, 0x42}, {0x7c, 0x43}, {0x82, 0x44}, {0x85, 0x44}, {0x86, 0x45}, + }; + static const struct { uint8_t fkey; uint16_t page, usage; } fkeys[] = { + {0x3a, 0x0c, 0xe2}, {0x3b, 0x0c, 0xea}, {0x3c, 0x0c, 0xe9}, {0x3d, 0xff, 0x07}, {0x3e, 0xff, 0x05}, + {0x3f, 0xff, 0x04}, {0x40, 0xff, 0x06}, {0x41, 0, 0}, {0x42, 0, 0}, {0x43, 0x01, 0xa9}, {0x44, 0, 0}, + {0x45, 0, 0}, + }; + + fn_start(&fn, &st, HID_FNKEYS_TRANSLATE | HID_FNKEYS_HOST_LOCK); + fn_code(&fn, &st, 0x10, 0); + fn_code(&fn, &st, 0, 0); + fn_key(&fn, &st, 0, 0x3e); + fn_key(&fn, &st, 0, 0); + CHECK(nkev == 4 && kev_is(0, 0xff, 5, 1) && kev_is(2, 7, 0x3e, 1) && kev_is(3, 7, 0x3e, 0)); /* lock off */ + nkev = 0; + CHECK(fn_code(&fn, &st, 0x4e, 0) == HID_KBD_EVENTS && fn.lock == 1 && fn.lock_toggles == 1 && nkev == 0); + CHECK(fn_code(&fn, &st, 0x4e, 0) == HID_KBD_EVENTS && fn.lock == 1 && fn.lock_toggles == 1); /* held: once */ + CHECK(fn_code(&fn, &st, 0, 0) == HID_KBD_EVENTS && fn.lock == 1 && nkev == 0); /* not again on release */ + for (size_t i = 0; i < sizeof(row) / sizeof(row[0]); i++) { + unsigned from = nkev; + fn_code(&fn, &st, row[i].code, 0); + fn_code(&fn, &st, 0, 0); + CHECK(fn_pair(from, 7, row[i].fkey)); + } + { + unsigned from = nkev; /* off the F-row: still its function */ + fn_code(&fn, &st, 0xc4, 0); + fn_code(&fn, &st, 0, 0); + CHECK(fn_pair(from, 0xff, 0x08)); + from = nkev; + fn_consumer(&fn, &st, 0xe2); + fn_consumer(&fn, &st, 0x70); + fn_consumer(&fn, &st, 0); + CHECK(nkev == from + 4 && kev_is(from, 7, 0x3a, 1) && kev_is(from + 1, 7, 0x3a, 0) && + kev_is(from + 2, 7, 0x3e, 1) && kev_is(from + 3, 7, 0x3e, 0)); + } + for (size_t i = 0; i < sizeof(fkeys) / sizeof(fkeys[0]); i++) { + unsigned from = nkev; + fn_key(&fn, &st, 0, fkeys[i].fkey); + fn_key(&fn, &st, 0, 0); + CHECK(fn_pair(from, fkeys[i].page, fkeys[i].usage)); + } + { + unsigned from = nkev; /* everything else unchanged */ + fn_key(&fn, &st, 0x02, 0x04); + fn_key(&fn, &st, 0, 0); + CHECK(nkev == from + 4 && kev_is(from, 7, 0xe1, 1) && kev_is(from + 1, 7, 4, 1)); + } + /* A key held while the lock flips is released as what it was pressed as. */ + nkev = 0; + CHECK(fn.lock == 1); + fn_code(&fn, &st, 0x10, 0); /* F5 */ + fn_key(&fn, &st, 0, 0x3f); /* brightness up */ + fn_code(&fn, &st, 0x10, 0x4e); /* lock off while both are down */ + CHECK(fn.lock == 0 && fn.lock_toggles == 2); + fn_code(&fn, &st, 0, 0); + fn_key(&fn, &st, 0, 0); + CHECK(nkev == 4 && kev_is(0, 7, 0x3e, 1) && kev_is(1, 0xff, 4, 1) && kev_is(2, 7, 0x3e, 0) && + kev_is(3, 0xff, 4, 0)); + nkev = 0; + fn_code(&fn, &st, 0x10, 0); + fn_code(&fn, &st, 0, 0); + CHECK(fn_pair(0, 0xff, 5)); /* and the lock is off again */ +} + +/* Yielding to an event driver: reports 54 and 55 are its, report 90 stays ours. */ +static void test_fnkeys_yield(void) +{ + struct hid_fnkeys fn; + struct hid_kbd_state st; + fn_start(&fn, &st, HID_FNKEYS_TRANSLATE); + fn_key(&fn, &st, 0x02, 0x04); + fn_consumer(&fn, &st, 0x70); + fn_code(&fn, &st, 0x20, 0); + CHECK(nkev == 4); + nkev = 0; + hid_fnkeys_release_keyboard(&fn, &st, kbd_emit, NULL); + CHECK(nkev == 3 && kev_is(0, 7, 0xe1, 0) && kev_is(1, 7, 4, 0) && kev_is(2, 0xff, 5, 0)); + fn.yielded = 1; + nkev = 0; + CHECK(fn_key(&fn, &st, 0, 0x05) == HID_KBD_IGNORED && fn_consumer(&fn, &st, 0x6f) == HID_KBD_IGNORED && nkev == 0); + CHECK(fn_code(&fn, &st, 0x10, 0) == HID_KBD_EVENTS && nkev == 2 && kev_is(0, 0xff, 4, 0) && kev_is(1, 0xff, 5, 1)); + nkev = 0; + hid_fnkeys_release_all(&fn, &st, kbd_emit, NULL); + CHECK(nkev == 1 && kev_is(0, 0xff, 5, 0) && fn.vendor[0] == 0); + nkev = 0; + hid_fnkeys_release_all(&fn, &st, kbd_emit, NULL); + CHECK(nkev == 0); + fn.yielded = 0; /* resumed from a clean state, as the glue does */ + memset(&st, 0, sizeof(st)); + fn_key(&fn, &st, 0, 0x05); + CHECK(nkev == 1 && kev_is(0, 7, 5, 1)); +} + +/* The raw log: what it names and, above all, what it never names. */ +static void test_fnkeys_notes(void) +{ + struct hid_fnkeys fn; + struct hid_kbd_state st; + + /* Every key that types a character, alone and with Shift, Control, Option: no note. */ + fn_start(&fn, &st, HID_FNKEYS_TRANSLATE | HID_FNKEYS_RAW_LOG); + for (unsigned mods = 0; mods < 4; mods++) + for (unsigned k = 0x04; k <= 0x65; k++) { + if (k >= 0x3a && k <= 0x53) + continue; + fn_key(&fn, &st, (uint8_t)(mods == 0 ? 0 : mods == 1 ? 0x02 : mods == 2 ? 0x01 : 0x04), (uint8_t)k); + fn_key(&fn, &st, 0, 0); + } + { + static const uint8_t many[6] = {0x04, 0x05, 0x06, 0x07, 0x08, 0x09}; + fn_keys(&fn, &st, 0x22, many, 6); /* Shift and a burst in one report: no chord */ + fn_keys(&fn, &st, 0, NULL, 0); + } + CHECK(nfnotes == 0); + /* F1-F12 and the navigation band: one note per transition, with what was dispatched. */ + for (unsigned k = 0x3a; k <= 0x45; k++) { + unsigned from = nfnotes; + fn_key(&fn, &st, 0x01, (uint8_t)k); + fn_key(&fn, &st, 0x00, 0); + CHECK(nfnotes == from + 2 && fnotes[from].kind == HID_FNKEYS_NOTE_KEY && fnotes[from].code == k && + fnotes[from].down == 1 && fnotes[from].mods == 0x01 && fnotes[from].out_page == 7 && + fnotes[from].out_usage == k && !fnotes[from].raw && fnotes[from + 1].down == 0); + } + nfnotes = 0; + fn_key(&fn, &st, 0, 0x4a); + fn_key(&fn, &st, 0, 0); + CHECK(nfnotes == 2 && fnotes[0].kind == HID_FNKEYS_NOTE_KEY && fnotes[0].code == 0x4a); + /* F23 with Shift and GUI (Windows' Copilot key): past the logical maximum, named, not dispatched. */ + nfnotes = 0; + nkev = 0; + fn_key(&fn, &st, 0x0a, 0x72); + fn_key(&fn, &st, 0x00, 0); + CHECK(nfnotes == 2 && fnotes[0].kind == HID_FNKEYS_NOTE_RANGE && fnotes[0].code == 0x72 && fnotes[0].down == 1 && + fnotes[0].mods == 0x0a && fnotes[1].kind == HID_FNKEYS_NOTE_RANGE && fnotes[1].down == 0); + CHECK(nkev == 4 && kev_is(0, 7, 0xe1, 1) && kev_is(1, 7, 0xe3, 1)); /* only the modifiers */ + /* GUI and a key down in one report: a chord. GUI first, then the key: no chord. */ + nfnotes = 0; + fn_key(&fn, &st, 0x08, 0x13); + fn_key(&fn, &st, 0x00, 0); + CHECK(nfnotes == 1 && fnotes[0].kind == HID_FNKEYS_NOTE_CHORD && fnotes[0].code == 0x13 && fnotes[0].mods == 0x08); + nfnotes = 0; + fn_key(&fn, &st, 0x80, 0); + fn_key(&fn, &st, 0x80, 0x16); + fn_key(&fn, &st, 0x00, 0); + CHECK(nfnotes == 0); + /* Report 90: its bytes and what each code became. */ + nfnotes = 0; + fn_code(&fn, &st, 0x10, 0x6b); + fn_code(&fn, &st, 0, 0); + CHECK(nfnotes == 4 && fnotes[0].kind == HID_FNKEYS_NOTE_VENDOR && fnotes[0].code == 0x10 && fnotes[0].down && + fnotes[0].out_page == 0xff && fnotes[0].out_usage == 5 && fnotes[0].raw_len == 6 && + !memcmp(fnotes[0].raw, "\x5a\x10\x6b\x00\x00\x00", 6)); + CHECK(fnotes[1].code == 0x6b && fnotes[1].out_page == 0 && fnotes[1].out_usage == 0); + CHECK(fnotes[2].code == 0x10 && !fnotes[2].down && fnotes[3].code == 0x6b && !fnotes[3].down); + /* Report 55, an unknown report, and report 90 at another length. */ + nfnotes = 0; + fn_consumer(&fn, &st, 0xe9); + fn_consumer(&fn, &st, 0); + { + static const uint8_t unknown[5] = {0x07, 1, 2, 3, 4}, five[5] = {0x5a, 0x10, 0, 0, 0}; + CHECK(hid_fnkeys_translate(&fn, &st, unknown, 5, kbd_emit, NULL, fn_note, NULL) == HID_KBD_IGNORED); + CHECK(hid_fnkeys_translate(&fn, &st, five, 5, kbd_emit, NULL, fn_note, NULL) == HID_KBD_BAD_LENGTH); + } + CHECK(nfnotes == 4 && fnotes[0].kind == HID_FNKEYS_NOTE_CONSUMER && fnotes[0].code == 0xe9 && + fnotes[0].out_page == 12 && fnotes[0].out_usage == 0xe9); + CHECK(fnotes[2].kind == HID_FNKEYS_NOTE_REPORT && fnotes[2].id == 7 && fnotes[2].code == 5 && + fnotes[2].raw_len == 5 && !memcmp(fnotes[2].raw, "\x07\x01\x02\x03\x04", 5)); + CHECK(fnotes[3].kind == HID_FNKEYS_NOTE_REPORT && fnotes[3].id == 0x5a && fnotes[3].code == 5); + /* Yielded: notes from the device's own state, with nothing of ours dispatched; still no characters. */ + fn.yielded = 1; + nfnotes = 0; + nkev = 0; + fn_key(&fn, &st, 0, 0x3e); + fn_key(&fn, &st, 0, 0x04); + fn_key(&fn, &st, 0, 0); + CHECK(nkev == 0 && nfnotes == 2 && fnotes[0].kind == HID_FNKEYS_NOTE_KEY && fnotes[0].code == 0x3e && + fnotes[0].out_page == 0 && fnotes[1].down == 0); + fn.yielded = 0; + /* The host lock's flips. */ + fn_start(&fn, &st, HID_FNKEYS_TRANSLATE | HID_FNKEYS_RAW_LOG | HID_FNKEYS_HOST_LOCK); + fn_code(&fn, &st, 0x4e, 0); + CHECK(nfnotes == 2 && fnotes[0].kind == HID_FNKEYS_NOTE_LOCK && fnotes[0].down == 1 && + fnotes[1].kind == HID_FNKEYS_NOTE_VENDOR && fnotes[1].code == 0x4e); + /* Without HID_FNKEYS_RAW_LOG: no note at all. */ + fn_start(&fn, &st, HID_FNKEYS_TRANSLATE | HID_FNKEYS_HOST_LOCK); + fn_key(&fn, &st, 0x08, 0x3e); + fn_key(&fn, &st, 0x0a, 0x72); + fn_code(&fn, &st, 0x10, 0x4e); + fn_consumer(&fn, &st, 0xe9); + { + static const uint8_t unknown[3] = {0x07, 1, 2}; + hid_fnkeys_translate(&fn, &st, unknown, 3, kbd_emit, NULL, fn_note, NULL); + } + CHECK(nfnotes == 0); +} + +static void test_fnkeys_rate(void) +{ + struct hid_fnkeys_rate rate; + unsigned allowed = 0; + memset(&rate, 0, sizeof(rate)); + for (unsigned i = 0; i < 40; i++) + allowed += (unsigned)hid_fnkeys_rate_allow(&rate, 1000000); + CHECK(allowed == HID_FNKEYS_RATE_BURST && rate.dropped == 40 - HID_FNKEYS_RATE_BURST); + CHECK(!hid_fnkeys_rate_allow(&rate, 1000000 + HID_FNKEYS_RATE_REFILL_US - 1)); + CHECK(hid_fnkeys_rate_allow(&rate, 1000000 + HID_FNKEYS_RATE_REFILL_US)); + CHECK(!hid_fnkeys_rate_allow(&rate, 1000000 + HID_FNKEYS_RATE_REFILL_US + 1)); + CHECK(!hid_fnkeys_rate_allow(&rate, 900000)); /* time going back refills nothing */ + allowed = 0; + for (unsigned i = 0; i < 40; i++) /* a long pause refills only the burst */ + allowed += (unsigned)hid_fnkeys_rate_allow(&rate, 60000000); + CHECK(allowed == HID_FNKEYS_RATE_BURST); + /* The lifetime cap holds whatever the time. */ + { + uint64_t t = 60000000; + while (rate.lines < HID_FNKEYS_RATE_LIFETIME) { + t += HID_FNKEYS_RATE_REFILL_US; + REQUIRE(hid_fnkeys_rate_allow(&rate, t)); + } + CHECK(!hid_fnkeys_rate_allow(&rate, t + 3600000000ull) && rate.lines == HID_FNKEYS_RATE_LIFETIME); + } +} + +/* Report 90 end to end: the A16 keyboard model sends it through the GENI model and the transport, which delivers it + * (its 6 bytes are what the descriptor declares), and the layer turns it into brightness. */ +static void test_fnkeys_transport(void) +{ + static const uint8_t down[] = {0x5a, 0x20, 0, 0, 0, 0}, up[] = {0x5a, 0, 0, 0, 0, 0}; + struct hid_fnkeys fn; + struct hid_kbd_state st; + begin_keyboard(GENI_I2C_IRQ_EVENTS); + run_until(&DEV, I2C_HID_S_READY, 64); + CHECK(i2c_hid_run(&DEV) == I2C_HID_R_OK); + CHECK(hid_rdesc_report_bytes(&DEV.rinfo, HID_REPORT_INPUT, HID_KBD_REPORT_VENDOR) == HID_KBD_VENDOR_BYTES); + dev_push(&KBD, down, sizeof(down)); + dev_push(&KBD, up, sizeof(up)); + poll_ticks(&DEV, 2); + CHECK(ndelivered == 2 && delivered_len[0] == 6 && delivered_len[1] == 6); + fn_start(&fn, &st, HID_FNKEYS_TRANSLATE); + for (uint32_t i = 0; i < ndelivered; i++) + hid_fnkeys_translate(&fn, &st, delivered[i], delivered_len[i], kbd_emit, NULL, NULL, NULL); + CHECK(fn_pair(0, 0xff, 4)); + check_clean(); +} + +static void test_fnkeys(void) +{ + test_fnkeys_init(); + test_fnkeys_vendor(); + test_fnkeys_consumer_and_keys(); + test_fnkeys_lock(); + test_fnkeys_yield(); + test_fnkeys_notes(); + test_fnkeys_rate(); + test_fnkeys_transport(); +}