repo: merge the Fn-key layer tools (fn-keys)
This commit is contained in:
commit
c993f8151f
8 files changed
+909
-6
No files matched your search
@@ -1,7 +1,8 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
# SPDX-License-Identifier: MIT
|
# SPDX-License-Identifier: MIT
|
||||||
# Build the A16 desktop AuxKC from this checkout: AdrenoX2 (SnapAdreno, PROFILE=desktop unless KEXT_PROFILE says
|
# 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
|
# 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
|
# 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)
|
# 3. build/build-auxkc.py pin admits the collection for the installed desktop guest profile (<kc>.json)
|
||||||
@@ -12,6 +13,8 @@
|
|||||||
# Environment:
|
# Environment:
|
||||||
# KEXT_PROFILE SnapAdreno profile (gpu/adreno-kext/kext/profiles/), default desktop; desktop-nocoherent for a
|
# KEXT_PROFILE SnapAdreno profile (gpu/adreno-kext/kext/profiles/), default desktop; desktop-nocoherent for a
|
||||||
# monitor without --gpu-coherent
|
# 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
|
# 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
|
# (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
|
# 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}
|
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
|
case "$NAME" in */*|.*|'') echo "error: NAME is a folder name under build/current-assets" >&2; exit 2;; esac
|
||||||
KEXT_PROFILE=${KEXT_PROFILE:-desktop}
|
KEXT_PROFILE=${KEXT_PROFILE:-desktop}
|
||||||
|
I2C_PROFILE=${I2C_PROFILE:-desktop}
|
||||||
export ZERO_AR_DATE=${ZERO_AR_DATE:-1}
|
export ZERO_AR_DATE=${ZERO_AR_DATE:-1}
|
||||||
export PYTHONDONTWRITEBYTECODE=1
|
export PYTHONDONTWRITEBYTECODE=1
|
||||||
DEST=$R/build/current-assets/$NAME
|
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 ||
|
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; }
|
{ 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; }
|
{ tail -20 "$WORK/snapi2chid.log" >&2; exit 1; }
|
||||||
|
|
||||||
mkdir -p "$DEST"
|
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)" \
|
printf '%s %s%s\n' "$d" "$(git -C "$R/$d" rev-parse HEAD)" \
|
||||||
"$(git -C "$R/$d" diff --quiet HEAD -- && echo '' || echo ' (uncommitted changes)')"
|
"$(git -C "$R/$d" diff --quiet HEAD -- && echo '' || echo ' (uncommitted changes)')"
|
||||||
done
|
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"
|
} > "$DEST/sources.txt"
|
||||||
|
|
||||||
cd "$R"
|
cd "$R"
|
||||||
|
|||||||
@@ -306,6 +306,12 @@ def parse(argv=None):
|
|||||||
p.add_argument('--input-active-us', type=int, default=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 '
|
help='with --present-latch: guest boot-arg snap_i2c_active_us, the poll period while touched '
|
||||||
'(default 4000; 0 = the configured 8 ms throughout)')
|
'(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',
|
p.add_argument('--hide-amx', action='store_true',
|
||||||
help='clear the application AMX-version capability field; preserves kernel AMXIDR '
|
help='clear the application AMX-version capability field; preserves kernel AMXIDR '
|
||||||
'and does not expose SME; give it to every mode of the attempt')
|
'and does not expose SME; give it to every mode of the attempt')
|
||||||
|
|||||||
@@ -1194,6 +1194,16 @@ def boot_addition(args):
|
|||||||
if active_us is not None:
|
if active_us is not None:
|
||||||
need(active_us == 0 or 2000 <= active_us <= 100000, '--input-active-us must be 0 or 2000..100000')
|
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}')
|
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:
|
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')
|
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}')
|
extra.append(f'zlog1={zone}')
|
||||||
|
|||||||
@@ -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()
|
||||||
@@ -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
|
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
|
Touchpad switch behind its feature hook, docs/A16-INPUT-PTP-20261020.md), then mutations of the
|
||||||
driver that must fail it.
|
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
|
from pathlib import Path
|
||||||
import argparse
|
import argparse
|
||||||
@@ -89,6 +94,50 @@ LATENCY_MUTATIONS = (
|
|||||||
('an uncapped lead', ' if (lead > cap)\n lead = cap; /* T3 */', ' (void)cap;'),
|
('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;'),
|
('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',
|
STANDIN_HEADERS = ('IOKit/IOBufferMemoryDescriptor.h', 'IOKit/IOCommandGate.h', 'IOKit/IOLib.h',
|
||||||
'IOKit/IOService.h', 'IOKit/IOTimerEventSource.h', 'IOKit/IOWorkLoop.h',
|
'IOKit/IOService.h', 'IOKit/IOTimerEventSource.h', 'IOKit/IOWorkLoop.h',
|
||||||
'IOKit/hid/IOHIDDevice.h', 'IOKit/hid/IOHIDDeviceKeys.h', 'IOKit/hid/IOHIDKeys.h',
|
'IOKit/hid/IOHIDDevice.h', 'IOKit/hid/IOHIDDeviceKeys.h', 'IOKit/hid/IOHIDKeys.h',
|
||||||
@@ -207,15 +256,18 @@ def build_standin(work):
|
|||||||
return inc
|
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}'
|
out = work / f'start-{tag}'
|
||||||
src = DRIVER / 'src'
|
src = DRIVER / 'src'
|
||||||
objs = []
|
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'):
|
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():
|
if not obj.exists():
|
||||||
subprocess.run(['cc', '-std=gnu11', '-O1', '-g', '-fsanitize=address,undefined', '-Wall', '-Wextra',
|
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))
|
objs.append(str(obj))
|
||||||
harness = work / f'harness-{tag}.cpp'
|
harness = work / f'harness-{tag}.cpp'
|
||||||
text = (ROOT / 'tools/test-snap-i2c-hid-start.cpp').read_text()
|
text = (ROOT / 'tools/test-snap-i2c-hid-start.cpp').read_text()
|
||||||
@@ -396,6 +448,56 @@ class Kext(unittest.TestCase):
|
|||||||
survivors.append(what)
|
survivors.append(what)
|
||||||
self.assertEqual(survivors, [], 'latency mutations the harness did not catch')
|
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():
|
def main():
|
||||||
global ARGS, REST
|
global ARGS, REST
|
||||||
|
|||||||
@@ -367,6 +367,234 @@ static SnapI2CHIDBus *ready_bus(const uint8_t *hid_desc, const uint8_t *rdesc, u
|
|||||||
return bus;
|
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)
|
int main(int argc, char **argv)
|
||||||
{
|
{
|
||||||
std::string s = argc > 1 ? argv[1] : "";
|
std::string s = argc > 1 ? argv[1] : "";
|
||||||
@@ -1020,6 +1248,13 @@ int main(int argc, char **argv)
|
|||||||
bus->fTimer->release();
|
bus->fTimer->release();
|
||||||
IOFree(bus->fDev, sizeof(*bus->fDev));
|
IOFree(bus->fDev, sizeof(*bus->fDev));
|
||||||
bus->fDev = nullptr;
|
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
|
#endif
|
||||||
} else {
|
} else {
|
||||||
printf("unknown scenario %s\n", s.c_str());
|
printf("unknown scenario %s\n", s.c_str());
|
||||||
|
|||||||
@@ -15,6 +15,10 @@
|
|||||||
* drivers/snap-i2c-hid/src/hid-pointer.c drivers/snap-i2c-hid/src/hid-ptp.c \
|
* 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
|
* -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
|
* 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
|
* 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
|
* 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));
|
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)
|
int main(void)
|
||||||
{
|
{
|
||||||
test_start_and_stop();
|
test_start_and_stop();
|
||||||
@@ -3227,6 +3235,11 @@ int main(void)
|
|||||||
test_ptp_reset_and_reports();
|
test_ptp_reset_and_reports();
|
||||||
test_mt_features();
|
test_mt_features();
|
||||||
test_mt_frames();
|
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);
|
printf("test-snap-i2c-hid: %u checks passed (34 groups)\n", checks);
|
||||||
|
#endif
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
Reference in new issue
Block a user