From dac77dfc7d5926edb040bc2495a02945844cb6a9 Mon Sep 17 00:00:00 2001 From: Acelogic Date: Sat, 24 Oct 2026 01:30:17 -0400 Subject: [PATCH 1/3] tools: test the SnapI2CHID keyboard's Fn-key layer Extend the SnapI2CHID harness for -DSNAP_I2C_HID_FNKEYS=1 (drivers/snap-i2c-hid feature/fn-keys, src/hid-keyboard.h): - test-snap-i2c-hid.c, built with the define, also runs test-snap-i2c-hid/fnkeys.c: flag refusals and the three brightness encodings; every report-90 code with a macOS usage and the ones without; arrays, 0x00/0xFF, repeats and other lengths; reports 54 and 55 giving hid_kbd_translate's exact events; the host Fn-lock, including a key held across a flip; yielding; the raw log, which must name no character key; the rate limit; report 90 end to end through the GENI model and transport. Without the define the test is unchanged. - test-snap-i2c-hid-start.cpp: seven fnkeys-* glue scenarios (off without the boot-arg, translate, raw log and its flood, yield, host lock, refused boot-args, transport-refused inputs). - test-snap-i2c-hid-kext.py test_9_fnkeys: the above, 15 mutations of hid-keyboard.c and 9 of the glue that must each fail, and every earlier scenario with the define (alone and with LATENCY). The expected usages are written in the tests from the evidence, not read from the driver's tables. Needs drivers/snap-i2c-hid at feature/fn-keys; the submodule pointer is not moved here. python3 tools/test-snap-i2c-hid-kext.py: 10 tests OK. --- tools/test-snap-i2c-hid-kext.py | 108 ++++++- tools/test-snap-i2c-hid-start.cpp | 235 +++++++++++++++ tools/test-snap-i2c-hid.c | 13 + tools/test-snap-i2c-hid/fnkeys.c | 477 ++++++++++++++++++++++++++++++ 4 files changed, 830 insertions(+), 3 deletions(-) create mode 100644 tools/test-snap-i2c-hid/fnkeys.c 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(); +} From 2e0f049e811279aa00a009b3e88dddb4c32fffe0 Mon Sep 17 00:00:00 2001 From: Acelogic Date: Sat, 24 Oct 2026 01:30:24 -0400 Subject: [PATCH 2/3] tools: pass snap_i2c_fnkeys with cycle --input-fnkeys installed-fresh --input-fnkeys FLAGS (with --input) appends the guest boot-arg snap_i2c_fnkeys=FLAGS, which a SnapI2CHID built with SNAP_I2C_HID_FNKEYS (PROFILE=desktop-fnkeys) reads; other builds ignore it. The sets the kext would refuse are refused here: 0, bits above 0x1f, brightness encoding 3, the host Fn-lock (0x4) without translation (0x1), an encoding alone. Without the option the boot argument addition is unchanged. python3 tools/test-input-fnkeys.py: 4 tests OK; python3 tools/test-zone-log.py: 4 tests OK. --- tools/cycle.py | 6 ++++ tools/cycle_lib/installed_fresh.py | 10 ++++++ tools/test-input-fnkeys.py | 56 ++++++++++++++++++++++++++++++ 3 files changed, 72 insertions(+) create mode 100644 tools/test-input-fnkeys.py diff --git a/tools/cycle.py b/tools/cycle.py index 67286f6..5f125bc 100755 --- a/tools/cycle.py +++ b/tools/cycle.py @@ -295,6 +295,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 abc5c3d..64e09c8 100644 --- a/tools/cycle_lib/installed_fresh.py +++ b/tools/cycle_lib/installed_fresh.py @@ -1164,6 +1164,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() From d576b6d18bd3e53e184e375eca47a01997e54eb2 Mon Sep 17 00:00:00 2001 From: Acelogic Date: Sat, 24 Oct 2026 01:30:24 -0400 Subject: [PATCH 3/3] tools: let build-auxkc-desktop.sh take a SnapI2CHID profile I2C_PROFILE (default desktop) picks the SnapI2CHID profile the desktop AuxKC is built with, as KEXT_PROFILE does for AdrenoX2, and sources.txt records it. I2C_PROFILE=desktop-fnkeys builds the collection for a --input-fnkeys run. Unset, the script runs exactly the earlier commands. bash -n passes. Not run: it needs the J700 kernelcache, the KDK and sudo (build-auxkc-user.py). --- tools/build-auxkc-desktop.sh | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) 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"