repo: merge the Fn-key layer tools (fn-keys)

This commit is contained in:
Acelogic committed 2026-10-24 03:46:39 -04:00
commit c993f8151f
8 files changed
+909 -6

No files matched your search

+7 -3
View File
@@ -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 (<kc>.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"
+6
View File
@@ -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')
+10
View File
@@ -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}')
+56
View File
@@ -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()
+105 -3
View File
@@ -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
+235
View File
@@ -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<uint8_t> 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<std::array<uint32_t, 3>> &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());
+13
View File
@@ -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;
}
+477
View File
@@ -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();
}