
Configure Macro Keyboard
- 40 installs
- 62 repo stars
- Updated August 3, 2026
- terrylica/cc-skills
Helps with ai & agent building tasks.
About
configure-macro-keyboard is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted development.
- configure-macro-keyboard
- AI & Agent Building
- AI-coding skill
Configure Macro Keyboard by the numbers
- 40 all-time installs (skills.sh)
- Ranked #8,266 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/terrylica/cc-skills --skill configure-macro-keyboardAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 40 |
|---|---|
| repo stars | ★ 62 |
| Last updated | August 3, 2026 |
| Repository | terrylica/cc-skills ↗ |
What it does
Helps with ai & agent building tasks.
Files
Configure a Macro Keyboard on macOS
End-to-end workflow for cheap 3-key USB-C/Bluetooth macro pads (Jieli, Realtek, CH57x, AliExpress-class): identify the device, figure out what each button actually emits, write a Karabiner rule scoped to that device only, and handle USB + Bluetooth in one rule even when the pad's BT firmware emits different keycodes than its USB side.
Just want the turnkey recipe? `references/09-turnkey-walkthrough.md` is a copy-paste-ready 30-minute walkthrough that replicates the "MacroKeyBot" setup used as this plugin's worked example — tap/double-tap on all three buttons: top (Fn for Typeless toggle / Cmd+V paste), middle (Shift+Return / Return), bottom (up_arrow / down_arrow), across USB + Bluetooth. The bottom button uses two different mechanisms (USB: software discrimination; BT: pad-firmware discrimination via separateequal_sign/Option+Zkeycodes) — see `03-patterns.md`. Start there if you know what you want; come back here when you need the reusable workflow or deeper pattern references.
Self-Evolving Skill: If a step breaks on a new pad, fix this file immediately. Every dead-end discovered belongs in references/04-anti-patterns.md.When to Use This Skill
- User mentions "macro pad", "macro keyboard", "3-key pad", "Stream Deck alternative" on macOS
- User wants to remap a cheap HID pad they bought from AliExpress / Amazon
- User wants buttons to emit Fn, Return, media keys, or custom shortcuts
- User hits a wall with BetterTouchTool (BTT can't emit real Fn — see sibling skill
emit-fn-key-on-macos) - User asks about dual USB + Bluetooth configuration for the same pad
- User mentions Jieli, Free3-P, or any pad with "USB Composite Device" as its product string
Prerequisite Check
# 1. Karabiner-Elements installed?
test -d /Applications/Karabiner-Elements.app && echo OK || brew install --cask karabiner-elements
# 2. Input Monitoring + Accessibility granted?
# System Settings → Privacy & Security → Input Monitoring → Karabiner = ON
# System Settings → Privacy & Security → Accessibility → Karabiner = ON
# 3. On macOS Sequoia+: Login Items toggle for privileged daemon
# System Settings → General → Login Items → Allow in the Background → Karabiner-Elements Privileged Daemon = ONIf any of the three is off, the remap will silently fail to grab the device.
Workflow (5 Steps)
Step 1 — Identify the device (USB)
# USB product string, VID, PID, serial, interface layout
ioreg -p IOUSB -l -w 0 | grep -B 2 -A 40 "USB Composite Device" | head -80
# Or system_profiler for a human-readable dump
system_profiler SPUSBDataType | grep -A 15 "USB Composite"Record: idVendor (hex), idProduct (hex), product string, serial, interface count.
Decode VID/PID → decimal for Karabiner (Karabiner's JSON uses decimal):
python3 -c "print(int('0x4c4a', 16), int('0x4155', 16))"
# → 19530 16725See references/01-hardware-identification.md for full decode of a Jieli pad including the HID report descriptor and how to infer the chip family.
Step 2 — Identify the device (Bluetooth, if applicable)
Pair via System Settings → Bluetooth → Connect. Then:
# Pad's BT address + VID/PID + firmware
system_profiler SPBluetoothDataType | grep -A 20 "Free3-P\|<pad-name>"
# Confirm Karabiner sees it as a grabbable device
karabiner_cli --list-connected-devices | jq '.[] | select(.product == "<pad-name>")'Expect different VID/PID than USB. Cheap pads borrow Samsung's 0x04E8 VID for macOS HID compatibility. Your one Karabiner rule must scope to both VID/PIDs via a single device_if with two identifiers.
See references/08-bluetooth-configuration.md for the Jieli/Free3-P live example.
Step 3 — Discover what each button actually emits
Do not assume the stock mapping. Cheap pads ship with arbitrary keycodes (Jieli/Free3-P ships as Ctrl+C/Ctrl+V/Ctrl+X — _not_ cut/copy/paste convention — button order is hardware-random).
Use `ignore: true` diagnostic rule (zero-effect remap that logs raw events). See sibling skill diagnose-hid-keycodes for the full workflow. Quick version:
1. Add a disabled rule with "conditions": [{"type": "device_if", "identifiers": [{...}]}] and "ignore": true on the device 2. Open Karabiner-EventViewer → Main tab 3. Press each button, screenshot the emitted keycode 4. Repeat for BT (in each firmware mode if the pad has multiple)
Step 4 — Write the Karabiner rule
Location: ~/.config/karabiner/karabiner.json → profile 0 → complex_modifications.rules → append a new rule.
Backup first:
cp ~/.config/karabiner/karabiner.json ~/.config/karabiner/karabiner.json.bak.$(date +%Y%m%d-%H%M%S)Rule skeleton (one rule, N manipulators = buttons × transports):
{
"description": "<pad-name>: Top → Fn, Middle → Return, Bottom → Command+Delete",
"manipulators": [
{
"type": "basic",
"from": {
"simultaneous": [{ "key_code": "left_control" }, { "key_code": "c" }],
"simultaneous_options": {
"detect_key_down_uninterruptedly": true,
"key_down_order": "strict_inverse",
"key_up_order": "strict_inverse",
"to_after_key_up": []
},
"modifiers": { "optional": ["any"] }
},
"to": [{ "apple_vendor_top_case_key_code": "keyboard_fn" }],
"conditions": [
{
"type": "device_if",
"identifiers": [
{ "vendor_id": 19530, "product_id": 16725 },
{ "vendor_id": 1256, "product_id": 28705 }
]
}
]
}
]
}_(Repeat the manipulator block for middle, bottom, and the BT-mode variants — 6 manipulators for a pure 3-key pad × 2 transports; +2 manipulators per button per transport for each button that uses Karabiner-side tap-vs-double-tap discrimination (see references/03-patterns.md). The Jieli/Free3-P live example uses tap-vs-double-tap on all three buttons → 12 manipulators total. Note: the bottom button uses Karabiner-side discrimination on USB only (Ctrl+X for both presses); on BT the pad's firmware emits two different keycodes (equal_sign single, Option+Z double), so its 2 BT manipulators are simple immediate translations rather than a detector/handler pair. Total stays at 12 either way: 8 (top + middle, both transports, software discrimination) + 2 (bottom USB, software discrimination) + 2 (bottom BT, firmware-decided keycode translation). JSON does not support // comments, so do not paste comment lines into your config.)_
Five rules to remember:
1. `simultaneous` with `detect_key_down_uninterruptedly: true` — needed when the pad emits modifier + key in one HID report. Default mandatory matcher misses these. 2. `apple_vendor_top_case_key_code: keyboard_fn` is the only way to emit real Fn. key_code: fn does nothing; modifiers: ["fn"] does nothing. 3. `device_if` with MULTIPLE identifiers — put the USB VID/PID and the BT VID/PID both in the same identifiers array. One rule handles both transports. 4. Scope every manipulator to the device. Without device_if, you'll remap your MacBook's built-in keyboard and break Apple's native keys. 5. `modifiers: {"optional": ["any"]}` — lets the firmware's modifier report flow through without blocking the rule.
Full live example (Jieli + Free3-P, 12 manipulators with tap/double-tap on all three buttons; bottom button uses asymmetric mechanisms — software discrimination on USB, firmware-decided-keycode translation on BT): references/raw/karabiner-rule.json.
Step 5 — Verify the grab + test
# Karabiner sees and grabs the device
karabiner_cli --list-connected-devices | jq '.[] | select(.product == "<pad-name>") | {product, is_grabbed}'
# Should return {"product": "...", "is_grabbed": true}
# Live event test
open -a "Karabiner-EventViewer"
# Press buttons → should see your TARGET keycode, not the SOURCEIf is_grabbed: false, re-check Input Monitoring + Accessibility + Login Items (step Prerequisite Check).
If grabbed but buttons pass through unchanged: your simultaneous matcher is probably wrong — the pad emits the combo in one report but you wrote mandatory. Revisit step 3.
If real Fn stops working system-wide after your rule loads: revert immediately. Do not set to_if_held_down with keyboard_fn as the target — this breaks Fn-emission on the whole system (verified failure). See references/04-anti-patterns.md → "Tap-vs-hold Fn emission".
Decision Tree: Which Target Keycode?
| You want button to emit… | Target JSON |
|---|---|
| Return / Enter | {"key_code": "return_or_enter"} |
| Shift+Return (newline without submitting) | {"key_code": "return_or_enter", "modifiers": ["left_shift"]} |
| Fn (for Typeless, dictation) | {"apple_vendor_top_case_key_code": "keyboard_fn"} |
| Command+Delete (delete-to-home) | {"key_code": "delete_or_backspace", "modifiers": ["left_command"]} |
| Option+Delete (delete word) | {"key_code": "delete_or_backspace", "modifiers": ["left_option"]} |
| Media play/pause | {"consumer_key_code": "play_or_pause"} |
| Volume up/down | {"consumer_key_code": "volume_increment"} / volume_decrement |
| Launch an app | {"shell_command": "open -a 'App Name'"} |
| Run a shell command | {"shell_command": "/path/to/script.sh"} |
| Tap = A, double-tap = B (single button) | See pattern in references/03-patterns.md → "Tap vs. double-tap discrimination" (set_variable + to_delayed_action) |
Handling Multiple BT Firmware Modes
Many cheap pads have 2-4 firmware modes that emit different keycodes per mode. The Jieli/Free3-P has 4 modes:
| Mode | Top | Middle | Bottom |
|---|---|---|---|
| 1 | volume_increment | volume_decrement | spacebar (play/pause) |
| 2 | (unexplored) | — | — |
| 3 | (unexplored) | — | — |
| 4 | page_up | page_down | equal_sign |
Pick the mode with the rarest keys (mode 4 for Free3-P — page_up/page_down are rarely used on laptops). Then add manipulators that match those keycodes plainly (no simultaneous needed for single-key firmware modes).
Mode-switch combos are often undocumented. Common attempts: hold all 3 keys ≥ 5s, hold top alone ≥ 5s, press top+bottom simultaneously. Document what works when you find it.
See references/08-bluetooth-configuration.md for the full mode-4 setup.
Deep References (load on demand)
| Topic | File |
|---|---|
| Turnkey walkthrough (start here) | references/09-turnkey-walkthrough.md |
| Device overview (TL;DR tables) | references/overview.md |
| Hardware identification | references/01-hardware-identification.md |
| Live USB config (Jieli) | references/02-usb-wired-configuration.md |
| Reusable patterns | references/03-patterns.md |
| Anti-patterns / dead-ends | references/04-anti-patterns.md |
| BT pairing roadmap (historical) | references/05-bluetooth-roadmap.md |
| BT ecosystem survey | references/06-bluetooth-landscape-survey.md |
| BT toolbox (evaluated tools) | references/07-bluetooth-toolbox.md |
| Live BT config (Jieli mode 4) | references/08-bluetooth-configuration.md |
| Raw dumps | references/raw/ |
Sibling Skills
emit-fn-key-on-macos— focused coverage of why only Karabiner can emit real Fn (BTT / hidutil / QMK on locked firmware all fail)diagnose-hid-keycodes—ignore: true+ EventViewer + Quartz focus-free screencap workflow for figuring out what a mystery button emits
Post-Execution Reflection
After this skill completes, reflect before closing the task:
0. Locate yourself. — Confirm this SKILL.md is the canonical file before any edit. 1. What failed? — Fix the instruction that caused it. 2. What worked better than expected? — Promote to recommended practice. 3. What drifted? — Update vendor IDs, keycodes, or FOSS-tool versions if reality disagrees with the doc. 4. Log it. — Add an evolution-log entry (or 04-anti-patterns.md row) with trigger, fix, evidence.
Do NOT defer. The next invocation inherits whatever you leave behind.
configure-macro-keyboard Skill
End-to-end Karabiner workflow for cheap 3-key USB-C/Bluetooth macro pads. Identifies the device, writes a device-scoped Karabiner rule, handles dual-transport (USB + BT) configurations, and packages reusable patterns (simultaneousmatchers, tap-vs-double-tap pairs,device_if).
Hub: Plugin CLAUDE.md | Sibling skills: emit-fn-key-on-macos · diagnose-hid-keycodes
What This Skill Owns
| File | Role | Edit policy |
|---|---|---|
SKILL.md | User-invocable instructions for configuring a new pad. Loaded into context when the skill fires. | Edit when the 5-step workflow changes shape. Keep terse — references carry the depth. |
references/raw/karabiner-rule.json | SSoT for the live MacroKeyBot rule. Verbatim 12-manipulator export. | Always update in lockstep with ~/.config/karabiner/karabiner.json. The repo file is the source — patch live config from it, not the other way around. |
references/02-usb-wired-configuration.md | Live USB rule walkthrough + Karabiner-side tap/double-tap mechanism + troubleshooting matrix. Notes the BT bottom-button asymmetry but defers details to 08-. | Keep mapping table, abridged JSON, and "How the tap/double-tap pattern works" section in sync with raw/karabiner-rule.json. When changing the bottom button on USB, also check whether the BT path needs a parallel update (and vice versa). |
references/08-bluetooth-configuration.md | Live BT rule walkthrough (mode-4 firmware) + numbered manipulator structure. Documents the bottom-button asymmetry: pad firmware emits two different keycodes for single-vs-double-tap on the bottom button (equal_sign / Option+Z), so manipulators 11 + 12 are immediate translations, NOT a software-discrimination pair. | When the rule changes, update the structure list (1-12), the "Why USB and BT use different mechanisms" explanation, and the "Switching Modes" / "Other BT Modes" sections that reference manipulator numbers. Re-test both transports of any button you change in EventViewer to catch new firmware-side discrimination. |
references/09-turnkey-walkthrough.md | Copy-paste-ready 30-minute MacroKeyBot recipe with VID/PID placeholders. | The full JSON in this file is meant to be lifted as-is. Keep it equivalent to raw/karabiner-rule.json modulo the macrokeybot_* vs jieli_* variable rename. |
references/03-patterns.md | Reusable techniques (live examples are device-specific; the patterns are not). | When a new live example proves a pattern, add it under the pattern's "Live examples" line. Don't fork patterns per device. |
references/04-anti-patterns.md | Dead-ends + historical narrative of the original Ctrl+C → Fn rule. | Append-only. Don't rewrite history when the rule evolves. |
references/overview.md | TL;DR mapping table + device signatures. | First place to update when the live mapping changes. |
references/05-bluetooth-roadmap.md | Historical pre-pairing roadmap. | Don't update — frozen in time. |
references/06-bluetooth-landscape-survey.md | 2026 ecosystem context. | Update only on major ecosystem shifts. |
references/07-bluetooth-toolbox.md | Tier-ranked FOSS BT tools. | Update when a tool is replaced or its install command changes. |
references/raw/{lsusb-verbose,system-profiler,system-profiler-bluetooth,ioreg-hid-device}.txt | Frozen hardware dumps from 2026-04-21. | Re-capture only if the pad firmware or macOS HID stack changes meaningfully. |
references/raw/karabiner-bluetooth-device.json | Karabiner's view of the paired Free3-P. | Re-capture when the BT pairing identity changes. |
Critical Invariants
1. `raw/karabiner-rule.json` and `~/.config/karabiner/karabiner.json` MUST stay in sync. When updating the rule, edit the repo file first, then patch the live config (the live config has 4 sibling rules — never replace the whole file). Backup the live config before patching: cp ~/.config/karabiner/karabiner.json ~/.config/karabiner/karabiner.json.bak.$(date +%Y%m%d-%H%M%S). 2. Manipulator counts are doc-load-bearing. The number "12" appears in 02-, 08-, 09-, overview.md, SKILL.md, plugin CLAUDE.md, plugin README.md, plugin CHANGELOG.md, and this file. When the count changes, grep all docs and update consistently — grep -rEn "[0-9]+[ -]manipulator|[0-9]+ manipulators|twelve manipulator|ten manipulator|eight manipulator" plugins/macro-keyboard/. Also update the comm-based self-check command in plugins/CLAUDE.md if you add new entries to the plugin list. 3. Each Karabiner-side-discriminated button needs its own variable name. Live config uses jieli_top_tap, jieli_middle_tap, and jieli_bottom_tap. Sharing a variable across buttons would let a tap on one arm the double-tap detector on another. Variables are NOT used for the BT bottom button — that path uses pad-firmware-decided keycode translation instead (see invariant #8). 4. `device_if` is non-optional on every manipulator. Dropping it remaps the MacBook's built-in keyboard and breaks Apple's native keys. Both VID/PID identifiers (USB + BT) must be in the identifiers array. 5. `apple_vendor_top_case_key_code: keyboard_fn` is the only path to real Fn. key_code: fn and modifiers: ["fn"] are no-ops. See sibling skill emit-fn-key-on-macos/CLAUDE.md for the why. 6. The top-button tap/double-tap is incompatible with Typeless push-to-talk. Fn fires only after the 200ms detection window expires, so press-and-hold doesn't sustain Fn-down. The live config assumes Typeless is in tap-to-toggle mode. To restore PTT, collapse the top-button pair into a single immediate-Fn manipulator per transport (recipe in 09-turnkey-walkthrough.md's "Adapt for your pad" bullet). 7. The bottom-button tap/double-tap defeats arrow-key auto-repeat on both transports. On USB the single-tap target (up_arrow) fires once after the 200ms detection window; on BT the pad's firmware emits one discrete equal_sign event per gesture. Either way: no auto-repeat stream. Acceptable for cursor-nudge; not for fast-scroll. To restore auto-repeat on USB, collapse the pair into a single immediate-up_arrow manipulator. On BT you can't restore it — the firmware doesn't emit a stream. 8. The bottom button uses ASYMMETRIC mechanisms across transports (verified 2026-05-02 via Karabiner-EventViewer). USB: pad emits Ctrl+X for every press → Karabiner-side software discrimination via set_variable + to_delayed_action (manipulators 9 + 10). BT mode 4: pad firmware does its own double-tap detection → emits equal_sign for single tap, Option+Z for double tap → Karabiner just translates each immediately (manipulators 11 + 12, no variable, no delayed action). Always re-test both transports of every button before assuming software discrimination is needed — see 04-anti-patterns.md → "Assuming the pad emits the same keycode on single-tap and double-tap on every transport". Top + middle buttons do NOT have this asymmetry; they use software discrimination on both transports.
Recent Changes
- 2026-05-02 (afternoon, 2nd revision) — Bottom button targets revised to
up_arrow(single tap) /down_arrow(double tap). During this change, discovered via Karabiner-EventViewer that the pad's BT firmware does its own double-tap discrimination on the bottom button only — emittingequal_signfor single tap andOption+Zfor double tap (top + middle still emitpage_up/page_downon every press). Restructured the BT bottom path: removed the software discrimination (manipulators 11 + 12 no longer usevariable_iforto_delayed_action); replaced with two simple immediate-translation manipulators (equal_sign→up_arrow,Option+Z→down_arrow). USB bottom path kept its software discrimination unchanged (USB still emitsCtrl+Xon every press). Manipulator count stays at 12 but the structural meaning shifted. Live config backup:~/.config/karabiner/karabiner.json.bak.before-bottom-arrows-20260502-141718. New pattern documented in03-patterns.md→ "Translate pad-firmware-decided keycodes". New anti-pattern entry in04-anti-patterns.md→ "Assuming the pad emits the same keycode on single-tap and double-tap on every transport". - 2026-05-02 (morning, 1st revision) — Bottom button gained tap/double-tap pair (single →
down_arrowcursor nudge, double →Cmd+Deleteline-clear). Manipulator count: 10 → 12. New variable:jieli_bottom_tap. No-auto-repeat caveat documented. Live config backup:~/.config/karabiner/karabiner.json.bak.before-bottom-tap-20260502-130639. (Superseded by afternoon revision.) - 2026-04-24 — Top button gained tap/double-tap pair (single → Fn for Typeless toggle, double →
Cmd+Vpaste). Manipulator count: 8 → 10. New variable:jieli_top_tap. PTT-incompatibility caveat documented across all relevant references. Live config backup:~/.config/karabiner/karabiner.json.bak.before-top-tap-20260424-143528. - 2026-04-23 — Middle button gained tap/double-tap pair (single →
Shift+Return, double →Return). Initial introduction of theset_variable+to_delayed_actionpattern. - 2026-04-21 — Bluetooth support added (mode-4 firmware: page_up / page_down / equal_sign). Same rule, dual
device_ifidentifiers.
Common Edits
- Change a button's target keycode: edit
to[0]in the relevant manipulator(s) inraw/karabiner-rule.json, then patch the live config. For software-discriminated tap/double-tap pairs (top + middle on both transports; bottom on USB), the single-tap target is into_delayed_action.to_if_invoked[0]and the double-tap target is in the detector manipulator'sto[0]. For the BT bottom path (firmware-discriminated, manipulators 11 + 12), the single-tap target is in manipulator 11'sto[0]and the double-tap target is in manipulator 12'sto[0]— both are simple, no delayed action. - Tune the double-tap window: change
parameters.basic.to_delayed_action_delay_milliseconds(default 200ms). Top, middle, and bottom-USB pairs tune independently. The BT bottom path is NOT tunable from Karabiner — its window is set by the pad's firmware. - Add a 4th button binding: append a manipulator with the new
fromkeycode. Reuse the samedevice_ifidentifiers. Test both transports in EventViewer to see whether the pad's firmware discriminates single-vs-double-tap on the new button (if it emits a different keycode on double-tap, use the firmware-translation pattern; otherwise use the software-discrimination pattern). - Support a new pad: add its USB VID/PID and BT VID/PID to every manipulator's
device_if.identifiersarray. If the pad emits different keycodes, add transport-specific manipulators (don't try to alias). Always test both transports of every button for firmware-side double-tap discrimination before assuming software discrimination is needed — see04-anti-patterns.md.
Validation
# Rule JSON parses + manipulator count is what you expect
/usr/bin/env python3 -c "import json; d=json.load(open('plugins/macro-keyboard/skills/configure-macro-keyboard/references/raw/karabiner-rule.json')); print(len(d['manipulators']))"
# Live config matches
/usr/bin/env python3 -c "
import json
live = json.load(open('/Users/terryli/.config/karabiner/karabiner.json'))
for r in live['profiles'][0]['complex_modifications']['rules']:
if r['description'].startswith('Jieli/Free3-P'):
print(len(r['manipulators']))
"
# Karabiner reloaded the file
tail -5 /var/log/karabiner/core_service.log
# Look for: "core_configuration is updated."Hardware Identification
Everything discoverable about the device from macOS without opening it up.
USB Device Descriptor
| Field | Value | Interpretation |
|---|---|---|
bcdUSB | 1.10 | USB 1.1 spec (Full Speed only) |
bDeviceClass | 0 (Unknown) | Per-interface class (device is composite) |
idVendor | 0x4c4a (19530) | Jieli Technology Co., Ltd. — Chinese AV SoC manufacturer. Not in the common usb.ids fork, so lsusb shows bare hex |
idProduct | 0x4155 (16725) | Product identifier within Jieli's space |
bcdDevice | 1.00 | Firmware version 1.0 (factory, never updated) |
iManufacturer (string) | Jieli Technology | Generic — indicates the pad vendor did not customize firmware strings |
iProduct (string) | USB Composite Device | Generic default — strongly suggests white-label reference firmware |
iSerialNumber (string) | 433132303730362E | Hex of ASCII C1207062. — likely a factory-assigned unit ID, trailing period is significant |
bNumConfigurations | 1 | Single USB configuration |
MaxPower | 100 mA | Low-power device (typical for a 3-button pad with no LEDs) |
| Negotiated speed | 12 Mbps | Full Speed — confirms the MCU does not implement USB 2.0 High Speed |
USB Interface Structure
The device is a USB Composite Device with 4 interfaces — one HID, one mass storage, and a CDC ACM serial pair:
| Interface | Class | Subclass | Protocol | Purpose |
|---|---|---|---|---|
| 0 | 0x08 (Mass Storage) | 0x06 (SCSI) | 0x50 (BBB) | Virtual disk — dormant on macOS (activates only on Windows for config tool delivery) |
| 1 | 0x03 (HID) | 0x00 (None) | 0x01 (Boot Keyboard) | The actual keyboard interface. Emits standard 8-byte keyboard reports |
| 2 | 0x02 (CDC) | 0x02 (ACM) | 0x01 (AT commands) | Control endpoint for a virtual serial port |
| 3 | 0x0A (CDC Data) | — | — | Data endpoint paired with interface 2 |
On macOS, the CDC interfaces spawn /dev/cu.usbmodem103 and /dev/tty.usbmodem103. The serial port is quiet at 9600 8N1 (no unsolicited output). The protocol for driving it is not publicly documented — likely used by a Windows-only vendor config tool.
HID Report Descriptor
Captured via ioreg -c IOHIDDevice (full hex in `references/ioreg-hid-device.txt`):
05 01 Usage Page (Generic Desktop)
09 06 Usage (Keyboard)
A1 01 Collection (Application)
05 07 Usage Page (Key Codes)
19 E0 Usage Minimum (0xE0 = Left Control)
29 E7 Usage Maximum (0xE7 = Right GUI)
15 00 Logical Minimum (0)
25 01 Logical Maximum (1)
75 01 Report Size (1 bit)
95 08 Report Count (8)
81 02 Input (Data, Variable, Absolute) — 8 bits for modifiers
95 01 Report Count (1)
75 08 Report Size (8)
81 01 Input (Constant) — 1 reserved byte
95 06 Report Count (6)
75 08 Report Size (8)
15 00 Logical Minimum (0)
25 FF Logical Maximum (255)
05 07 Usage Page (Key Codes)
19 00 Usage Minimum (0)
29 FF Usage Maximum (255)
81 00 Input (Data, Array) — 6 bytes of keycodes
C0 End CollectionThis is a bog-standard USB HID boot keyboard report (1 byte modifiers + 1 reserved + 6 keycodes = 8 bytes). It's the same descriptor shipped by nearly every cheap keyboard. There is no vendor-specific HID usage page, no additional consumer control usage, and no configuration endpoint advertised over HID.
Implication: the pad identifies itself as "just a keyboard" to macOS, which is why BTT/Karabiner/macOS all treat its events identically to any other keyboard. Remapping happens because we can filter by VID/PID, not because the pad offers any special protocol.
Firmware Button Wiring (verified 2026-04-21)
Pressing each physical button emits a full Ctrl-modifier + letter combo (both modifier and key in one HID report):
| Physical button | Emitted combo |
|---|---|
| Top | Left Control + C |
| Middle | Left Control + V |
| Bottom | Left Control + X |
Note: this is not the conventional cut/copy/paste order (which would be C→X→V top-to-bottom). The middle button emits V, not X. This was verified experimentally — assumptions about pad layouts should always be verified via Karabiner-EventViewer or similar, never inferred from "standard" conventions.
Chip / SoC Family (Inferred)
Jieli's registered USB VID is 0x4c4a. Chips in their AC69xx family commonly drive composite USB devices with this exact interface mix (HID + Mass Storage + CDC ACM). Candidate SoCs, in order of likelihood based on this device's capabilities:
- AC6966B / AC6925 — mid-range SoCs with USB HID + mass storage + serial over USB. Common in 2020-era cheap macro pads.
- AC6955 — slightly newer variant.
Without physical PCB inspection (or running a Windows vendor tool that reveals the chip version), this is inferred — not confirmed. If PCB inspection becomes relevant, the markings to look for are AC69xxB or JL AC69xx silkscreened on the main chip.
Why This Device is NOT QMK/VIA-Compatible
QMK firmware runs on specific microcontroller families: AVR (ATmega32u4), ARM Cortex-M0/M4 from ST/NXP, and RP2040. Jieli SoCs use a proprietary RISC core (not ARM, not AVR) and are not supported by QMK. VIA and Vial are client-side tools that speak a specific HID command set only present in QMK/Vial firmware, so they cannot detect or configure this pad.
The firmware is also not user-reprogrammable on macOS. Jieli's factory flashing tools are Windows-only; the 64-byte raw HID config channel (if present) is not publicly documented. To change which keycode each button emits, you must remap _after_ the fact at the OS level (as we do with Karabiner).
Device Signature for Rule Scoping
When writing any OS-level rule (Karabiner, hidutil, custom tools) that should apply _only_ to this pad and not to the Apple built-in keyboard or any other connected keyboard, match on:
{
"vendor_id": 19530,
"product_id": 16725
}or in hexadecimal for hidutil:
{ "VendorID": 0x4c4a, "ProductID": 0x4155 }The serial number can be used for additional uniqueness if you happen to own multiple identical pads, but for a single-unit use case the VID/PID pair is sufficient and more portable.
USB-C Wired Configuration (Current)
How the pad is configured right now, when connected via USB-C.
Mapping Summary
| Physical Button | Firmware Emits | Remapped To | What It Does |
|---|---|---|---|
| Top | Ctrl+C | Single-tap → Fn (after ~200ms); Double-tap ≤200ms → Command+V | Single tap toggles Typeless dictation; double tap pastes the system clipboard |
| Middle | Ctrl+V | Single-tap → Shift+Return (after ~200ms); Double-tap ≤200ms → Return | Single tap inserts a newline in chat/compose; double tap commits/sends |
| Bottom | Ctrl+X | Single-tap → up_arrow (after ~200ms, no key-repeat on hold); Double-tap ≤200ms → down_arrow | Single tap moves selection/cursor up by one; double tap moves down by one |
Apple keyboard's built-in Fn key is untouched by this rule. Your coworker using the MacBook Fn for Typeless is unaffected.
Stack
[Macro pad button press]
↓
USB HID boot keyboard report (modifier byte + keycode)
↓
macOS kernel IOKit HID layer
↓
Karabiner DriverKit grabber (seizes the device, routes events)
↓
Complex Modifications rule engine (matches VID/PID + Ctrl+{C|V})
↓
Karabiner Virtual HID Device (emits replacement keycode)
↓
macOS CGEvent stream (Fn appears as kCGEventFlagMaskSecondaryFn)
↓
Typeless's koffi CGEventTap / Cocoa text fieldCurrent Rule
Located in ~/.config/karabiner/karabiner.json → profile 0 → complex_modifications.rules. Exported verbatim in `references/karabiner-rule.json`.
Abridged USB-only view — the full live rule covers USB + Bluetooth in 12 manipulators total. The USB transport (this doc) uses Karabiner-side software detection for all three buttons' tap/double-tap (set_variable + to_delayed_action). The BT transport (see `08-bluetooth-configuration.md`) uses the same software detection for top + middle, but the pad's BT firmware does its own double-tap detection on the bottom button only — single-tap emits equal_sign, double-tap emits Option+Z — so the BT bottom-button manipulators are simple immediate-translation, not delayed-action pairs. See raw/karabiner-rule.json for the verbatim dump.
{
"description": "Jieli macro pad: Ctrl+C -> single-tap Fn / double-tap Cmd+V (top); Ctrl+V -> single-tap Shift+Return / double-tap Return (middle); Ctrl+X -> single-tap up_arrow / double-tap down_arrow (bottom)",
"manipulators": [
{
"type": "basic",
"from": {
"simultaneous": [{ "key_code": "left_control" }, { "key_code": "c" }],
"simultaneous_options": {
"key_down_order": "insensitive",
"key_up_order": "insensitive",
"detect_key_down_uninterruptedly": true
}
},
"to": [
{ "key_code": "v", "modifiers": ["left_command"] },
{ "set_variable": { "name": "jieli_top_tap", "value": 0 } }
],
"conditions": [
{
"type": "device_if",
"identifiers": [{ "vendor_id": 19530, "product_id": 16725 }]
},
{ "type": "variable_if", "name": "jieli_top_tap", "value": 1 }
]
},
{
"type": "basic",
"parameters": { "basic.to_delayed_action_delay_milliseconds": 200 },
"from": {
"simultaneous": [{ "key_code": "left_control" }, { "key_code": "c" }],
"simultaneous_options": {
"key_down_order": "insensitive",
"key_up_order": "insensitive",
"detect_key_down_uninterruptedly": true
}
},
"to": [{ "set_variable": { "name": "jieli_top_tap", "value": 1 } }],
"to_delayed_action": {
"to_if_invoked": [
{ "apple_vendor_top_case_key_code": "keyboard_fn" },
{ "set_variable": { "name": "jieli_top_tap", "value": 0 } }
],
"to_if_canceled": [
{ "set_variable": { "name": "jieli_top_tap", "value": 0 } }
]
},
"conditions": [
{
"type": "device_if",
"identifiers": [{ "vendor_id": 19530, "product_id": 16725 }]
}
]
},
{
"type": "basic",
"from": {
"simultaneous": [{ "key_code": "left_control" }, { "key_code": "v" }],
"simultaneous_options": {
"key_down_order": "insensitive",
"key_up_order": "insensitive",
"detect_key_down_uninterruptedly": true
}
},
"to": [
{ "key_code": "return_or_enter" },
{ "set_variable": { "name": "jieli_middle_tap", "value": 0 } }
],
"conditions": [
{
"type": "device_if",
"identifiers": [{ "vendor_id": 19530, "product_id": 16725 }]
},
{ "type": "variable_if", "name": "jieli_middle_tap", "value": 1 }
]
},
{
"type": "basic",
"parameters": { "basic.to_delayed_action_delay_milliseconds": 200 },
"from": {
"simultaneous": [{ "key_code": "left_control" }, { "key_code": "v" }],
"simultaneous_options": {
"key_down_order": "insensitive",
"key_up_order": "insensitive",
"detect_key_down_uninterruptedly": true
}
},
"to": [{ "set_variable": { "name": "jieli_middle_tap", "value": 1 } }],
"to_delayed_action": {
"to_if_invoked": [
{ "key_code": "return_or_enter", "modifiers": ["left_shift"] },
{ "set_variable": { "name": "jieli_middle_tap", "value": 0 } }
],
"to_if_canceled": [
{ "set_variable": { "name": "jieli_middle_tap", "value": 0 } }
]
},
"conditions": [
{
"type": "device_if",
"identifiers": [{ "vendor_id": 19530, "product_id": 16725 }]
}
]
}
]
}How the tap/double-tap pattern works (all three buttons)
All three buttons use the same two-manipulator pattern, each scoped to its own runtime variable (jieli_top_tap, jieli_middle_tap, jieli_bottom_tap). For the middle button (Ctrl+V):
1. Second-tap detector (listed first — Karabiner evaluates top-down, first match wins): matches only when jieli_middle_tap == 1. Emits Return and resets the variable. 2. First-tap handler: matches when the variable is 0 (unset). Sets the variable to 1 and starts a 200ms delayed action:
to_if_invoked(timer elapsed, no second press arrived) → emitShift+Return+ reset variableto_if_canceled(second press arrived, canceling the delay before it fired) → just reset the variable (the second-tap detector already handled the commit)
The top button (Ctrl+C) substitutes its own targets and variable name: single-tap → Fn (Apple vendor keyboard Fn for Typeless), double-tap → Cmd+V (paste). The bottom button (Ctrl+X) substitutes again: single-tap → up_arrow, double-tap → down_arrow. Otherwise the structure is identical across all three. Use a distinct variable per button — sharing a variable across buttons would let a tap on one button arm the double-tap detector on another.
Design tradeoff: single-tap has ~200ms discrimination latency (unavoidable in any tap/double-tap scheme) but double-tap is instant. The framing varies per button — top/middle keep the "safety" framing (the more common, gentler action is the slow single-tap; the decisive action is the fast double-tap). The bottom button drops that framing — both targets are reversible navigation keys, so neither is "safer" than the other. Pick whichever direction you reach for more often as the single-tap. To invert any pair (fast single-tap, delayed double-tap), swap the two targets in the JSON. For zero-latency alternatives, see 03-patterns.md → "Tap vs. double-tap discrimination" (suggests tap-vs-hold when latency matters).
Top-button caveat — Fn-as-push-to-talk doesn't work in this scheme. Because the Fn keystroke fires only after the 200ms detection window expires, holding the top key emits a single delayed Fn keypress, not a sustained Fn-down state. Push-to-talk (hold to dictate) needs the original "no double-tap, fire Fn immediately" rule — collapse the two top-button manipulators back into a single immediate-Fn manipulator (see git history of raw/karabiner-rule.json before the top-button double-tap addition). This skill assumes Typeless is configured as tap-to-toggle Fn, not push-to-talk.
Bottom-button caveat — arrow keys do not auto-repeat on hold. The single-tap target fires once after the 200ms window expires (or on release, whichever comes first). Holding the bottom key gives you exactly one up_arrow keypress, not the rapid scroll macOS produces when you hold a real arrow key. To navigate a long list, tap repeatedly. If continuous scroll matters more than the double-tap action, collapse the bottom-button pair back into a single immediate-up_arrow (or down_arrow, whichever you prefer) manipulator per transport.
Tuning: basic.to_delayed_action_delay_milliseconds: 200 is the double-tap window. Raise it (250-300) if users miss double-taps, lower it (150) if they accidentally trigger the double-tap target when meaning the single-tap target. All three buttons' windows are independent — tune each separately.
Changing the Mapping
Karabiner auto-reloads on file save — no restart needed. Edits take effect within about 1 second.
Add a binding for an additional button or rebind an existing one
Append a third manipulator inside the rule's manipulators array:
{
"type": "basic",
"from": {
"simultaneous": [{ "key_code": "left_control" }, { "key_code": "x" }],
"simultaneous_options": {
"key_down_order": "insensitive",
"key_up_order": "insensitive",
"detect_key_down_uninterruptedly": true
}
},
"to": [{ "key_code": "YOUR_TARGET" }],
"conditions": [
{
"type": "device_if",
"identifiers": [{ "vendor_id": 19530, "product_id": 16725 }]
}
]
}Useful YOUR_TARGET values:
| Target | Effect |
|---|---|
{"key_code": "escape"} | Escape |
{"key_code": "delete_or_backspace"} | Backspace |
{"key_code": "spacebar"} | Space |
{"key_code": "tab"} | Tab |
{"consumer_key_code": "mute"} | System mute |
{"consumer_key_code": "play_or_pause"} | Media play/pause |
{"shell_command": "open -a 'Some App.app'"} | Launch an app |
{"key_code": "c", "modifiers": ["left_command"]} | Real Cmd+C (copy on Mac) |
Change the top or middle button
Edit the matching manipulator's to array in place. Whatever keycode you put there is what the button emits.
Why Karabiner, Not BTT or hidutil
BTT's `CGEventPost` cannot produce a functional Fn key. Fn requires an HID device declaring the NX_DEVICE_CAPABILITY_INPUTKEYBOARD_FUNCTION capability. App-layer synthetic events don't carry this capability, so Typeless's CGEventTap filter ignores them.
`hidutil` alone cannot emit Fn either, and it can only remap single keycodes — not modifier+key combos. It would solve the "isolate a keycode to this device only" half of the problem, but not the "produce a real Fn" half.
Karabiner-Elements installs a DriverKit Virtual HID Device that declares the Fn capability. When its rule fires, events flow through its virtual keyboard as authentic, OS-trusted keystrokes. This is the only FOSS path on macOS Sequoia that satisfies both requirements.
Persistence Across Reboots
Karabiner registers its privileged daemons via macOS's SMAppService API. They auto-start at login because:
- The DriverKit extension is active and enabled:
org.pqrs.Karabiner-DriverKit-VirtualHIDDevice (1.8.0) - The daemons are registered in System Settings → General → Login Items & Extensions → "Allow in the Background" — all Karabiner entries are toggled on
If the daemons ever fail to come up after a reboot (symptom: pad keys emit unchanged Ctrl+C/V/X), check that toggle. Sometimes a major macOS update resets Login Items approvals.
Troubleshooting
| Symptom | Likely Cause | Check / Fix |
|---|---|---|
| Pad emits Ctrl+C/V/X unmodified | Karabiner daemon not running | pgrep -l karabiner — expect Karabiner-Core-Service, karabiner_console_user_server, karabiner_session_monitor, Karabiner-NotificationWindow. If missing, check Login Items approval |
| Pad is visible in Karabiner's devices list but rule doesn't fire | Karabiner not grabbing the pad | `grep "USB Composite Device" /var/log/karabiner/core_service.log \ |
| Top button triggers something other than Typeless (e.g. emoji picker) | Globe key behavior overriding | defaults read com.apple.HIToolbox AppleFnUsageType — should be 0 (Do Nothing). If not, change in System Settings → Keyboard → "Press 🌐 key to..." |
| Fn fires but Typeless silent | Typeless stopped listening | Restart Typeless.app. If still silent, check Typeless's app-settings.json has "pushToTalk": "Fn" |
| Top button single-tap pastes instead of activating Fn | Variable stuck at 1 | Same fix as the middle button — see "Variable stuck" row below. Apply to jieli_top_tap instead of jieli_middle_tap. |
| Top button's double-tap pastes nothing | Clipboard empty or app blocks ⌘V | Verify with manual ⌘V in the same app. The rule synthesises a real Cmd+V so any app accepting clipboard paste will receive it; sandboxed apps that block synthetic events (1Password CLI, some IDEs) will not. |
| Middle button inserts text instead of newline | Rule targeting wrong keycode | Verify both Ctrl+V manipulators: the second-tap detector's to has return_or_enter; the first-tap handler's to_if_invoked has return_or_enter + modifiers: ["left_shift"]. Neither target should be v. |
| Tap-button fires double-tap target on single press | Variable stuck at 1 | Kickstart Karabiner to clear runtime variables: launchctl kickstart -k gui/$(id -u)/org.pqrs.karabiner.karabiner_console_user_server. Or force a config reload by touching the file. Affects any of jieli_top_tap / jieli_middle_tap / jieli_bottom_tap. |
| Bottom button single-tap moves down instead of up | Variable stuck at 1 | Same fix as above — the variable is jieli_bottom_tap. The double-tap target (down_arrow) is firing on a single tap because the variable was already 1 from a prior incomplete tap cycle. Verify by tapping middle once first; if the variable was stuck across buttons, you'd see middle's double-tap target fire too (it won't, since variables are per-button). |
| Bottom button double-tap moves up twice instead of moving down | Taps too slow (>200ms apart) | Raise basic.to_delayed_action_delay_milliseconds on the bottom-button first-tap handler. Default 200ms; try 250-300. |
| Bottom button arrow keys don't auto-repeat on hold | By design | The single-tap target fires only after the detection window expires — held keys produce one up_arrow, not a stream. To restore continuous scroll, collapse the bottom-button pair back into a single immediate-up_arrow (or down_arrow) manipulator per transport. |
| 200ms single-tap delay feels sluggish | Default detection window | Edit the affected first-tap manipulator's parameters.basic.to_delayed_action_delay_milliseconds to a lower value (e.g. 150). Tradeoff: fewer successful double-tap detections. Top and middle are tuned independently. |
| Double-tap fails — two single-tap targets fire back-to-back | Taps were slower than window | Raise basic.to_delayed_action_delay_milliseconds to 250-300. Tradeoff: more single-tap latency. |
Diagnostic Commands (non-sudo)
# Is Karabiner running?
pgrep -l karabiner
# Is the pad currently grabbed?
grep "USB Composite Device" /var/log/karabiner/core_service.log | tail -2
# What does Karabiner see as the connected devices?
karabiner_cli --list-connected-devices | jq '.[] | select(.device_identifiers.vendor_id == 19530)'
# Current rules in config
jq '.profiles[0].complex_modifications.rules[] | .description' ~/.config/karabiner/karabiner.json
# Full pad-specific rule
jq '.profiles[0].complex_modifications.rules[] | select(.description | startswith("Jieli"))' ~/.config/karabiner/karabiner.jsonReverting
Disable only this rule: Karabiner-Elements → Complex Modifications → toggle off "Jieli macro pad: …". Keeps Karabiner running; other rules (AudioPrioritySetter, Ultra Custom Shortcut, Option-L script) stay active.
Restore pre-change config byte-for-byte. Karabiner doesn't auto-snapshot, so always back up before editing:
# Before any change:
cp ~/.config/karabiner/karabiner.json \
~/.config/karabiner/karabiner.json.bak.$(date +%Y%m%d-%H%M%S)
# To revert:
cp ~/.config/karabiner/karabiner.json.bak.<YOUR_TIMESTAMP> \
~/.config/karabiner/karabiner.jsonKarabiner will auto-reload the restored file within ~1 second (watch the daemon log at /var/log/karabiner/core_service.log to confirm).
Full Karabiner uninstall:
brew uninstall --cask karabiner-elementsThen remove the DriverKit extension in System Settings → General → Login Items & Extensions → Driver Extensions.
Known Limitations
- Push-to-talk minimum latency:
basic.simultaneous_threshold_millisecondsin the Karabiner config is50by default. That's the ceiling on how long Karabiner waits to confirm both Ctrl and the letter are pressed simultaneously. For push-to-talk this is imperceptible; for fast-twitch games it could matter. - Pad firmware not reconfigurable on macOS. Changing which keycode each button emits requires either the Windows-only Jieli vendor tool or reverse-engineering the 64-byte HID config channel — not attempted here.
- No key auto-repeat on any tap/double-tap pair. Holding any of the three buttons produces a single delayed keystroke (the single-tap target), not a stream. Most painful for the bottom button's
up_arrow(real arrow keys auto-repeat for scrolling); least painful for the top button'sFn(you don't need to hold a toggle). To restore auto-repeat for any button, collapse its pair back into a single immediate-target manipulator per transport.
Patterns
Techniques and approaches that worked well during the USB-C wired setup. Re-use these when extending the config, adding the Bluetooth mode, or wiring up any other cheap HID peripheral on macOS.
Pattern: Device-scoped rule via device_if + VID/PID
What: Every Karabiner manipulator for the pad includes a conditions array with {"type": "device_if", "identifiers": [{"vendor_id": 19530, "product_id": 16725}]}.
Why: A rule written without device_if would remap Ctrl+C on _every_ connected keyboard — including your Apple built-in — which would break normal typing immediately. The device filter narrows the rule to exactly one pair of VID/PID.
When to use: Any rule targeting a key combination that's common on other keyboards (Ctrl+{letter}, Cmd+{letter}, Shift+Tab, etc.). Single-device-only behavior is almost always what you want.
Counter-example: If you wanted the Caps Lock → Hyper (or Escape) remap to apply globally (both Apple internal and external keyboards), you wouldn't add device_if. The default scope is "all devices."
Pattern: simultaneous (not mandatory) for modifier + letter combos from macro pads
What: Use from.simultaneous: [{"key_code": "left_control"}, {"key_code": "c"}] with simultaneous_options.detect_key_down_uninterruptedly: true, rather than from: {"key_code": "c", "modifiers": {"mandatory": ["control"]}}.
Why: Cheap HID devices like Jieli macro pads emit the modifier and the key in a single HID report — both change state simultaneously. Karabiner's mandatory form can leak the modifier through to macOS in edge cases, causing the host app to see Ctrl+Fn instead of plain Fn. The simultaneous form with detect_key_down_uninterruptedly: true guarantees both keys are consumed atomically.
When to use: Whenever matching a modifier+key combo from any device that emits both in one HID report. For human-typed combos on real keyboards (where modifier goes down first, then the key), mandatory is fine.
Diagnostic tip: If you write a mandatory rule and the post-remap event has unexpected modifiers still held, switch to simultaneous.
Pattern: apple_vendor_top_case_key_code: keyboard_fn for the Fn target
What: Use {"apple_vendor_top_case_key_code": "keyboard_fn"} in the to array, rather than the shorter alias {"key_code": "fn"}.
Why: Both resolve to the same HID usage page (0x00FF) and usage (0x03) — Apple's vendor-specific Fn encoding. The explicit form bypasses Karabiner's alias lookup table. If a future Karabiner version changes how key_code: fn is resolved or deprecates the alias, the explicit form will still work.
When to use: Whenever emitting Fn. Cost of the explicit form is just a slightly longer JSON key.
Pattern: Use the Karabiner Virtual HID Device to emit Fn
What: Karabiner-Elements installs a DriverKit system extension that registers a virtual HID keyboard declaring NX_DEVICE_CAPABILITY_INPUTKEYBOARD_FUNCTION. When a complex modification emits keyboard_fn, the event flows through this virtual keyboard.
Why: macOS's Fn modifier bit (kCGEventFlagMaskSecondaryFn) is only honored from events originating on devices that declare the Fn capability. App-layer synthesizers (CGEventPost from BTT, AppleScript key-down simulation, PyObjC synthetic events) can't set this bit authentically. Only DriverKit virtual HID devices can.
When to use: Whenever a consumer (like Typeless) reads Fn via CGEventTap flag-change events. Also the right pattern for globe-key press-simulation, dictation-key triggering, and any other "real HID modifier" use case.
Pattern: Isolate a device for inspection via ignore: true
What: To see raw HID events from a device (unremapped), add an entry to profiles[0].devices in karabiner.json:
{
"identifiers": {
"is_keyboard": true,
"vendor_id": 19530,
"product_id": 16725
},
"ignore": true,
"disable_built_in_keyboard_if_exists": false,
"fn_function_keys": [],
"manipulate_caps_lock_led": false,
"simple_modifications": [],
"treat_as_built_in_keyboard": false
}Why: When Karabiner is grabbing a device, Karabiner-EventViewer shows the _output_ of the virtual keyboard (post-modification events). To see what the physical device is actually emitting, Karabiner must release its grab. The ignore: true flag does this cleanly on a per-device basis.
When to use: Any time you need to confirm what keycode a button emits, diagnose why a rule isn't matching, or document a new device. Always revert (remove the entry) after.
Evidence this matters: Without this technique we would have remained stuck on "the middle button emits Ctrl+X" — an assumption that turned out wrong (middle actually emits Ctrl+V). Un-grabbing and reading raw HID events revealed the truth.
Pattern: Capture windows by ID via Quartz, not focus
What: To screenshot a specific window regardless of focus state, use CGWindowListCopyWindowInfo (via pyobjc) to find its Window ID, then screencapture -l <WID> -x file.png.
import Quartz
for w in Quartz.CGWindowListCopyWindowInfo(
Quartz.kCGWindowListOptionAll, Quartz.kCGNullWindowID):
if w.get('kCGWindowOwnerName') == 'Karabiner-EventViewer':
wid = w.get('kCGWindowNumber')
# screencapture -l <wid> -x out.pngWhy: osascript -e 'tell application "X" to activate' can be defeated by macOS focus-stealing prevention, and screencapture -x captures only the foreground. Grabbing a window by ID captures it even when off-screen, minimized, or obscured. No Accessibility permission needed — only Screen Recording.
When to use: Any autonomous UI inspection workflow, especially when working from a terminal that shouldn't steal focus from the window being inspected. Perfect for letting Claude debug UI state without interrupting the user's flow.
Pattern: Edit the config file directly to change device grab state
What: Karabiner re-reads ~/.config/karabiner/karabiner.json automatically when it changes. To toggle per-device grab, ignore flag, or modifier state, edit the JSON and save — no UI interaction needed.
Why: The Karabiner GUI requires focus, clicking, and in some cases Accessibility-permission prompts for its child windows. File edits via jq are scriptable, reversible, and don't need any permissions beyond filesystem access.
When to use: Programmatic configuration, automated tests, scripted setup, CI-style provisioning of a new Mac.
Example: The ignore: true pattern above is a specific instance of this general pattern.
Pattern: Non-sudo audit commands
What: To verify Karabiner's TCC Input Monitoring grant, daemon state, and device grab status, prefer user-scope queries over sudo:
# Daemon state (no sudo needed)
launchctl print gui/$(id -u) | grep -i karabiner
# Proof of Input Monitoring grant (because if not granted, this would return empty or error)
karabiner_cli --list-connected-devices | jq length
# Grab state from log
grep "USB Composite Device" /var/log/karabiner/core_service.log | tail -2Why: sudo triggers Touch ID prompts each time (unless sudo session is cached). For a repeated audit loop, that's dozens of biometric prompts. Working from user-scope commands is both faster and more pleasant.
When to use: Always, unless the data genuinely requires root (which is rare for user-facing tools like Karabiner).
Pattern: Tap vs. double-tap discrimination on one button
What: Let a single macro-pad button emit two different targets depending on whether it's pressed once or twice quickly. Use set_variable + to_delayed_action across two coordinated manipulators sharing the same from trigger.
Why: Cheap macro pads have few buttons, and some target pairs are a natural fit (Return vs. Shift+Return for chat composers; Escape vs. Command+. for dismiss vs. interrupt; Cmd+C vs. Cmd+Shift+C for copy vs. copy-path). Karabiner has no first-class "double-tap" matcher, but the delayed-action + variable pattern yields the same behavior with predictable semantics.
When to use: Any time one button should produce two distinct outputs and you're OK with the single-tap action firing ~200ms late (the double-tap detection window is unavoidable discrimination latency). Not the right pattern when the single-tap target is latency-sensitive — prefer to_if_alone + to_if_held_down (tap vs. hold) instead, which has zero delay on tap.
Structure (two manipulators sharing one from, ordered second-tap-detector first):
{
"description": "<button>: single-tap = X, double-tap = Y",
"manipulators": [
{
"type": "basic",
"from": {
/* your button trigger */
},
"to": [
{ "key_code": "<DOUBLE_TAP_TARGET>" },
{ "set_variable": { "name": "<button>_tap", "value": 0 } }
],
"conditions": [
{
"type": "device_if",
"identifiers": [
/* VID/PID */
]
},
{ "type": "variable_if", "name": "<button>_tap", "value": 1 }
]
},
{
"type": "basic",
"parameters": { "basic.to_delayed_action_delay_milliseconds": 200 },
"from": {
/* same button trigger */
},
"to": [{ "set_variable": { "name": "<button>_tap", "value": 1 } }],
"to_delayed_action": {
"to_if_invoked": [
{ "key_code": "<SINGLE_TAP_TARGET>" },
{ "set_variable": { "name": "<button>_tap", "value": 0 } }
],
"to_if_canceled": [
{ "set_variable": { "name": "<button>_tap", "value": 0 } }
]
},
"conditions": [
{
"type": "device_if",
"identifiers": [
/* VID/PID */
]
}
]
}
]
}Mechanism:
1. First tap arrives with <button>_tap == 0. The detector's variable_if fails. Execution falls through to the handler: variable is set to 1, a 200ms timer starts. 2. If nothing happens within 200ms: to_if_invoked fires → SINGLE_TAP_TARGET is emitted, variable is reset to 0. 3. If a second tap arrives within 200ms: the detector's variable_if now matches (== 1). It fires first (Karabiner evaluates top-down), emitting DOUBLE_TAP_TARGET and resetting the variable. The still-pending delayed action is canceled automatically — to_if_canceled just resets the variable again (idempotent safety).
Why the detector must come first: Karabiner evaluates manipulators in order and takes the first match. If the handler came first, every tap would match the handler first and the detector would never fire. Order matters.
Tuning knob: basic.to_delayed_action_delay_milliseconds. Default 500 is too long for a tap/double-tap gesture — humans double-tap in 100-250ms. Start at 200ms; raise to 250-300 if users miss double-taps, lower to 150 if they accidentally trigger single-tap when meaning double. The same variable can be shared across multiple manipulators (e.g. USB + Bluetooth transports of the same button) because only one path is physically active at a time.
Trade-off framing — choose which side pays the cost:
| Single-tap action | Double-tap action | Framing |
|---|---|---|
Return (send) | Shift+Return | Speed on send, delay on newline — good for high-throughput chat |
Shift+Return | Return (send) | Safety — newline is easy, send is deliberate (accidental sends are suppressed) |
Escape | Command+. | Dismiss is fast, interrupt is deliberate |
Cmd+C | Cmd+Shift+C | Copy is fast, copy-path is deliberate |
Live examples: Jieli/Free3-P top button (Fn / Cmd+V) + middle button (Shift+Return / Return) on both transports, plus bottom button (up_arrow / down_arrow) on USB only. See references/raw/karabiner-rule.json for the full 12-manipulator config. Use a distinct variable name per button (jieli_top_tap, jieli_middle_tap, jieli_bottom_tap) — sharing one variable across buttons would let a tap on one arm the double-tap detector on another.
The Jieli/Free3-P bottom button on BT does NOT use this pattern — see "Pattern: Translate pad-firmware-decided keycodes" below for why. In short, the pad's BT firmware emits equal_sign for a single tap and Option+Z for a double tap, so Karabiner doesn't need to discriminate; it just translates each keycode immediately. Always check what your pad emits for both single and double tap on each transport before assuming software discrimination is needed. The ignore: true diagnostic (`diagnose-hid-keycodes` sibling skill) is the right tool for this.
Anti-pattern warning: don't pick this pattern when the latency-on-single-tap matters _or_ when the single-tap target needs key auto-repeat on hold. Two concrete failure modes:
1. Push-to-talk — tap-vs-double-tap on a PTT button adds 200ms before the mic opens, and holding the button doesn't sustain the modifier. Use tap-vs-hold instead (to_if_alone + to_if_held_down), which discriminates by duration rather than a commit window. The Jieli/Free3-P top button hits this trap if Typeless is configured for push-to-talk: Fn fires only after release, so PTT doesn't work. The live config assumes Typeless is in tap-to-toggle mode. 2. Arrow-key auto-repeat — to_delayed_action's to_if_invoked fires the single-tap target as one discrete event after the timer elapses (or on key-up, whichever comes first). It does not emit a sustained key-down state, so macOS's auto-repeat doesn't kick in. The Jieli/Free3-P bottom button hits this trap with `up_arrow`: holding the bottom key produces one arrow keystroke, not the rapid scroll a real arrow key produces. Acceptable if you only nudge the cursor; painful if you scroll long lists.
To restore the immediate-fire / auto-repeat behavior on any button: collapse its pair into a single immediate-target manipulator per transport (drop parameters, to_delayed_action, and variable_if; set to: [{ ... your target ... }]). See git history of references/raw/karabiner-rule.json for snapshots before each pair was added: pre-2026-04-24 for the immediate-Fn top-button form, pre-2026-05-02 for the bottom-button single-action forms.
Pattern: Translate pad-firmware-decided keycodes (no Karabiner-side discrimination)
What: When the pad's firmware emits two different keycodes for single-tap vs double-tap on the same physical button, you don't need Karabiner-side set_variable + to_delayed_action discrimination. Just write two simple immediate-translation manipulators — one per emitted keycode — and let the firmware do the timing.
Why: This is faster (no software 200ms wait), simpler (no variable, no delayed action, no variable_if ordering), and gives the user better feedback (single tap fires immediately).
Trigger to look for: open Karabiner-EventViewer's Main tab, double-tap the button quickly, and see what comes through. If you see the same keycode twice in a row, the pad isn't doing firmware-level discrimination — use the previous "tap vs. double-tap" pattern. If you see a different keycode (or chord) on the second tap, the firmware is doing it for you.
How:
// Single-tap path — translate the firmware's "single tap" keycode
{
"type": "basic",
"from": { "key_code": "<single-tap firmware code>" },
"to": [{ "key_code": "<your target>" }],
"conditions": [{ "type": "device_if", "identifiers": [/* ... */] }]
},
// Double-tap path — translate the firmware's "double tap" keycode (often a modifier+key chord)
{
"type": "basic",
"from": {
"key_code": "<double-tap key>",
"modifiers": { "mandatory": ["<double-tap modifier>"] }
},
"to": [{ "key_code": "<your other target>" }],
"conditions": [{ "type": "device_if", "identifiers": [/* ... */] }]
}Live example: Jieli/Free3-P bottom button on Bluetooth mode 4 only (verified 2026-05-02 via EventViewer). The pad's BT firmware emits equal_sign for a single tap and Option+Z for a double tap. The rule translates each immediately: equal_sign → up_arrow, Option+Z → down_arrow. No variable, no delayed action.
Not all transports of the same pad behave the same way: the same Free3-P over USB-C emits Ctrl+X for every press regardless of tap rate, so the USB path uses the original software-discrimination pattern. Always test both transports separately.
Anti-pattern warning: don't assume firmware-side discrimination is consistent across buttons. The Free3-P only does it on the bottom button — page_up (top) and page_down (middle) come through on every press, regardless of tap rate. Mixing patterns on the same physical pad is fine; the rule just has both kinds of manipulators.
Pattern: Atomic commits after verifying each change
What: After every rule change, verify it works via EventViewer or direct test before moving to the next. If something breaks later, you can git bisect or revert to a known-good state.
Why: This session went through ~5 rule iterations (Ctrl+C rule → Ctrl+X rule → all-three rule → Ctrl+X=Return+Ctrl+C=Fn rule → Ctrl+V=Return+Ctrl+C=Fn rule). Each one was observable and reversible. If I'd batched all changes into one commit at the end, a regression would have been harder to localize.
When to use: Always for config changes. Especially valuable for keyboard/input configs where "broken" may mean "can't type to debug."
Anti-Patterns
Dead-ends, wrong turns, and things we tried that didn't work. Recorded here so future debugging sessions don't re-walk the same paths.
Anti-pattern: BetterTouchTool CGEventPost for Fn emission
What we tried: Bind a BTT keyboard shortcut trigger on Ctrl+C (the pad's top button) to output the Fn key, so Typeless would see push-to-talk.
Why it failed: BTT uses CGEventPost to synthesize keystrokes. This API cannot set the kCGEventFlagMaskSecondaryFn bit authentically — that bit is only honored by macOS when the originating HID device declares NX_DEVICE_CAPABILITY_INPUTKEYBOARD_FUNCTION. BTT is an app, not an HID device, so its emitted "Fn" events are silently dropped by CGEventTap consumers like Typeless.
The lesson: Any time you need to emit a keystroke that requires modifier-flag provenance (Fn/Globe, and some system-only consumer keys), app-layer tools are insufficient. You need a DriverKit virtual HID device. On macOS today, Karabiner-Elements is the only mature FOSS option.
Alternative considered: Changing Typeless's pushToTalk shortcut to something BTT _can_ emit (F13-F20 or Ctrl+Opt+Cmd+something). Ruled out because the coworker shares the Typeless subscription and uses the real Fn key — we couldn't change the shortcut without breaking their workflow.
Anti-pattern: hidutil alone for Fn emission
What we considered: Remap the pad's Ctrl+C → Fn using hidutil property --matching ... --set '{"UserKeyMapping":...}'.
Why it wouldn't work: hidutil maps single HID usage codes to other single usage codes on a per-device basis. It can't match on a _combination_ (Ctrl+C is two simultaneous keys). It also can't _emit_ Fn as a proper modifier event — it would emit the 0xFF00/0x03 usage but without the capability declaration, macOS would still route it as a regular key press, not a modifier flag change.
The lesson: hidutil is great for single-keycode remaps (Caps Lock → Escape, etc.) but insufficient for modifier+key combos or any remap whose target requires device capability declarations.
Anti-pattern: VIA / Vial / QMK Toolbox on Jieli firmware
What we considered: Install VIA or Vial to reconfigure the pad's firmware directly so each button emits a user-chosen keycode.
Why it wouldn't work: Jieli SoCs use a proprietary RISC core, not AVR or ARM. QMK firmware doesn't support Jieli. VIA and Vial are client-side tools that speak a specific HID command set only present in QMK/Vial firmware — they won't detect or configure a Jieli device.
Secondary issue on macOS 2026: The Homebrew casks for VIA (via), Vial (vial), and QMK Toolbox are Intel-only and Gatekeeper-deprecated. Installing them requires Rosetta 2 (~500 MB) to even run. Combined with the firmware incompatibility, there's zero upside.
The lesson: Before reaching for QMK/VIA/Vial, verify the target device's MCU family. If it's Jieli, Nordic, or another non-QMK SoC, don't waste time installing these tools.
Anti-pattern: {"any": "key_code"} at top level of from
What we tried: A diagnostic "catch-all" Karabiner rule designed to match any keypress from the pad:
{
"type": "basic",
"from": {
"any": "key_code",
"modifiers": {"optional": ["any"]}
},
"to": [{"key_code": "fn"}],
"conditions": [{"type": "device_if", "identifiers": [...]}]
}Why it failed: "any": "key_code" is only valid _inside_ a simultaneous block — not as a top-level from field. Karabiner silently ignores manipulators with invalid from syntax instead of logging an error. The rule was accepted by jq as valid JSON but did nothing.
The lesson: Karabiner's config validation is lenient — don't assume "no error = rule is working." Always verify a new manipulator fires by observing its effect (EventViewer or downstream behavior).
Anti-pattern: Inferring button-to-keycode mapping from a fast sequence test
What we did: User pressed "top mid bottom" in rapid sequence. The event log showed Ctrl+C → Fn (rule-fired) → Ctrl+V. We inferred top=Ctrl+C, middle=Ctrl+X, bottom=Ctrl+V.
Why it was wrong: The inference required an assumption about the press order that was never verified. Later single-button testing revealed the actual wiring was top=Ctrl+C, middle=Ctrl+V, bottom=Ctrl+X — the middle and bottom were swapped relative to our inference.
The cost: One wasted rule iteration (Ctrl+X → Return, which ended up on the wrong physical button), plus user confusion when "middle" didn't do what was expected.
The lesson: For each physical button, isolate a single press and verify its keycode independently. Never trust a multi-press sequence to pin down individual mappings. The ignore: true + EventViewer technique is the right tool for this — use it _before_ writing rules that differentiate between buttons.
Anti-pattern: sudo launchctl bootstrap for SMAppService-registered daemons
What we tried: Manually bootstrap Karabiner's privileged daemons with sudo launchctl bootstrap system /path/to/Karabiner-Core-Service.plist.
Why it failed: Bootstrap failed: 5: Input/output error. Karabiner 15.x uses macOS's SMAppService API, where the daemons are registered at install time but must be enabled by the user via System Settings → General → Login Items & Extensions → "Allow in the Background". launchctl bootstrap is blocked for SMAppService-registered services until the user grants that permission.
The lesson: On macOS Sequoia, any third-party service using SMAppService requires user approval in Login Items. There is no scripted workaround — it's an intentional security policy. Tell the user once, and the toggle persists across reboots.
Anti-pattern: Sudo TCC database queries for authorization audits
What we tried: sudo sqlite3 /Library/Application\ Support/com.apple.TCC/TCC.db "SELECT client, auth_value FROM access WHERE service='kTCCServiceListenEvent'" to verify Input Monitoring was granted to Karabiner.
Why we stopped: Triggered a Touch ID prompt. User flagged this as undesirable.
The alternative that works: Run karabiner_cli --list-connected-devices. If it returns a non-empty device list including the pad, Input Monitoring must be granted — otherwise the call would fail or return only virtual devices. The fact that the tool works _is itself_ the audit evidence.
The lesson: Prefer user-scope, non-privileged evidence of authorization (like observing a privileged feature working) over root-scope direct database queries. Works faster, avoids biometric prompts, and is more portable.
Anti-pattern: Assuming factory button order follows cut/copy/paste convention
What we assumed: A 3-key pad emitting Ctrl+C/X/V would be wired top=C (copy), middle=X (cut), bottom=V (paste) — the conventional toolbar order.
Actual wiring: top=C, middle=V, bottom=X. No standard applies — each pad vendor wires them arbitrarily.
The lesson: Cheap macro pads don't follow any layout convention. Verify each button individually before writing rules. See Pattern: Isolate a device for inspection via ignore: true.
Anti-pattern: Changing Typeless's pushToTalk to F13 without a working emitter
What we considered at one point: Edit ~/Library/Application Support/Typeless/app-settings.json to set "pushToTalk": "F13", then hidutil-remap the pad's Ctrl+C to F13 on the device only. This would avoid the Fn-emission problem entirely.
Why it was blocked: User's coworker shares the Typeless subscription and uses the real Fn key. Changing Typeless's shortcut would break their workflow. This constraint pushed us back to the Karabiner+virtual-Fn path.
The lesson: Always check whether your proposed solution affects other users of the system or account. A "lighter" solution isn't actually lighter if it breaks someone else's workflow.
Anti-pattern: Silent formatting drift from hook processing
What happened: After each Write to a markdown file, a PostToolUse hook reformatted the content (probably prettier or similar). The visible content stays the same but indentation / line wrapping shifts.
Why it's mostly benign: The formatter is idempotent and doesn't change semantics.
Why to stay aware: If you later want to Edit a specific old_string in one of these files, the formatter may have rewrapped lines, so you must Read first to see the post-format state. The claude-code harness warned about this explicitly.
The lesson: After Write-ing a file that gets auto-formatted, Read it before the next Edit targeting specific lines.
Anti-pattern: Over-eager memory file updates before workflow is stable
What happened: Memory file reference_jieli_macropad_karabiner.md was updated three times in this session as the rule evolved. Each update required re-reading, re-editing, and keeping MEMORY.md index in sync. Early updates became stale within minutes.
The lesson: During rapid iteration, wait for the workflow to stabilize before committing to memory files. Better to update memory once at the end than to repeatedly rewrite it during the discovery phase.
Anti-pattern: Assuming the pad emits the same keycode on single-tap and double-tap on every transport
What happened (2026-05-02): Configured the bottom button with the standard Karabiner-side tap-vs-double-tap discrimination pattern (set_variable + to_delayed_action), assuming that — like the top + middle buttons — the pad would emit the same keycode (equal_sign) on every press over Bluetooth. After the rule went live, double-tap appeared to do nothing while single-tap worked. The user's Karabiner-EventViewer dump showed that the pad's BT firmware actually emits `Option+Z` for a double tap instead of repeating equal_sign. The rule had no manipulator matching Option+Z so the double-tap chord passed through to the focused app, while the first-tap delayed action still fired the (wrong) single-tap target ~200ms later.
Why we missed it earlier: the previous rule for the bottom button was a single-action mapping (equal_sign → Cmd+Delete), so double-tap was never tested in EventViewer. We carried the "page_up/page_down behave the same on every press" assumption from the top + middle buttons over to the bottom button without verification. Same pad, same vendor, same BT mode — but a per-button firmware difference.
The fix: drop the software discrimination on the affected transport (BT bottom) and add two simple immediate-translation manipulators — one for each firmware-emitted keycode. Keep software discrimination on the unaffected transport (USB bottom, where the firmware does emit Ctrl+X on every press). See 03-patterns.md → "Pattern: Translate pad-firmware-decided keycodes" for the recipe.
The lesson: before adding the tap-vs-double-tap pattern to a button, double-tap it in EventViewer on each transport you care about and confirm the same keycode comes through twice. If a different keycode (or chord) appears on the second tap, the pad is doing firmware-side discrimination — use the immediate-translation pattern instead. The ignore: true diagnostic (`diagnose-hid-keycodes`) is the right tool: open EventViewer Main tab, double-tap fast, screenshot, repeat per transport. ~10 seconds per check.
Bluetooth Roadmap
Plan for enabling and integrating the pad's Bluetooth mode. Not yet implemented — this document is the upfront thinking so that when we do the work, we don't rediscover basics.
Goal
Make the pad usable wirelessly via Bluetooth, with the same button behavior as the USB-C wired mode (top=Fn, middle=Return, bottom=reserved), so user experience is transport-agnostic.
Preliminary Questions to Answer First
Before touching any code or config, we need to answer these by physical inspection + pairing attempt:
1. Does the pad have a visible mode switch, BT pairing button, or BT indicator LED? Check the enclosure carefully. Some cheap pads require a specific key combo (e.g., holding all 3 keys for 5 seconds) to enter pairing mode. 2. What BT spec does it advertise? Classic BT-HID (which pairs like a standard keyboard), or BLE HID (which needs macOS 10.15+ and is paired differently)? 3. Is the radio always-on, or does it require USB power to be advertising? Some pads use the USB-C port for both power _and_ data, with BT as an alternative radio that only works when powered externally. 4. What's the Bluetooth VID/PID? Often different from the USB VID/PID even for the same physical device. We'll need this to write a Karabiner rule scoped to the BT peripheral.
Expected Pairing Flow
Standard Bluetooth HID peripheral discovery on macOS:
1. Put the pad in pairing mode (per its manual or the mode-switch above). 2. Open System Settings → Bluetooth. 3. Wait for "USB Composite Device" (or another name — BT device name is typically distinct from USB name) to appear. 4. Click Connect.
If the pad is BLE-HID, macOS pairs silently without a prompt. If Classic BT-HID, macOS may show a PIN dialog.
Identifying the BT Peripheral for Karabiner
Once paired, capture the Bluetooth device identifiers:
# Non-sudo CLI (using macOS built-in)
system_profiler SPBluetoothDataType 2>/dev/null | grep -A 20 -i "jieli\|macro"
# Via Karabiner's device list (once pad sends a keystroke)
karabiner_cli --list-connected-devices | jq '.[] | select(.is_bluetooth == true)'Karabiner exposes Bluetooth devices with their own identifiers; the Bluetooth transport reports a different device_id and potentially different vendor_id/product_id than the USB transport.
Karabiner Rule Strategy
Two options:
Option A — Two separate rules (USB and Bluetooth transport each get their own device_if block):
{
"description": "Jieli macro pad USB",
"manipulators": [ ... conditions: device_if with USB VID/PID ... ]
},
{
"description": "Jieli macro pad Bluetooth",
"manipulators": [ ... conditions: device_if with BT VID/PID ... ]
}Clear separation, easy to disable either transport independently. Downside: 2x maintenance if rules change.
Option B — Single rule with two `device_if` identifiers:
"conditions": [{
"type": "device_if",
"identifiers": [
{"vendor_id": 19530, "product_id": 16725},
{"vendor_id": <BT_VID>, "product_id": <BT_PID>}
]
}]One rule matches either transport. Less duplication. Preferred if both transports use identical button behavior (which is the plan).
Recommendation: Option B, with a note in the rule description saying "matches both USB and Bluetooth transports."
Open Risks
- BT-HID Fn capability: Karabiner's Virtual HID Device emits Fn successfully regardless of the source device. That part is already solved — the new transport doesn't affect emission. But _input_ on BT might have different timing/batching characteristics than USB, which could affect the
simultaneousrule's 50ms threshold. Worth testing. - Auto-reconnect after Mac wake-up: Some BT keyboards require re-pressing a key to reconnect after the Mac sleeps. If that's the case here, Karabiner might briefly miss events during the reconnect. Not a blocker — just a UX wart to document.
- Power management: If the pad has a battery, how does it charge? USB-C probably. How does it indicate low battery? Battery service UUID, or a dumb LED, or nothing at all? Affects documentation.
- Interference with other BT keyboards: Macropads with cheap BT radios sometimes interfere with other nearby BT inputs. Worth testing with Magic Keyboard/Magic Trackpad attached.
- Bluetooth TCC implications: On macOS Sequoia, pairing a new HID device sometimes re-prompts for Input Monitoring on every app that uses
CGEventTap. Karabiner should not need re-approval (the TCC grant is per-app, not per-input-device), but Typeless might. Worth a quick check after pairing.
Deliverables for the Bluetooth Phase
When we do this work, the completion criteria:
1. Pad successfully pairs with macOS and survives sleep/wake. 2. BT VID/PID captured and documented in 01-hardware-identification.md (separate section). 3. Karabiner rule updated (Option B) to cover both transports. 4. Button behavior verified in BT mode (top=Fn triggers Typeless, middle=Return inserts newline, bottom=passes through). 5. Behavior across transport switch verified: plug USB → pad works. Unplug USB, BT takes over → pad still works. No config change required. 6. Battery / power management behavior documented (if applicable). 7. This roadmap file replaced by an 06-bluetooth-configuration.md describing the final setup, matching the structure of 02-usb-wired-configuration.md.
Scope Cut
Deliberately out of scope for this phase:
- Multi-host BT switching (some pads support pairing with 2-3 devices and switching between them). Unless the pad has this feature _and_ user wants it, ignore.
- BT firmware update (if the pad exposes DFU). Not attempting firmware mods — pad's factory firmware is fine.
- Custom BT profile (reporting as a different device name). Beyond the needs of remapping.
Bluetooth Macro Keyboard Landscape Survey (2026)
Research companion to `05-bluetooth-roadmap.md`. Summarizes the state of the wireless macro-pad ecosystem so we can enable the Jieli pad's Bluetooth mode with realistic expectations about pairing, battery, latency, reconnect behavior, and available firmware/config options.
Executive Summary
The Bluetooth macro keyboard ecosystem in 2026 spans from 1-key remote pads to 30+ key programmable boards, with two dominant open-source firmware platforms (ZMK for wireless-first, QMK for USB-heritage devices), software-defined alternatives (Elgato Stream Deck, Loupedeck), and a sprawling AliExpress/Amazon "cheap and cheerful" sector dominated by CH57x/Jieli chipsets. For the Jieli 3-key pad in this project, enabling Bluetooth means navigating pairing complexity unique to these generic chips, managing battery behavior that differs sharply between coin cell and Li-Po designs, and empirically testing auto-reconnect on macOS before relying on wireless in mission-critical workflows like push-to-talk during meetings.
---
1. Popular Form Factors and Use Cases
1-3 key remote control pads — Push-to-talk for Zoom/Teams, Discord voice-channel toggles, OBS scene hotkeys, single-action shortcuts. Used for hands-free control when the pad sits on a desk or hangs from a lanyard. Bluetooth priority here is convenience (cable-free) over feature depth. This is the Jieli pad's category.
4-9 key programmable pads — OBS scene switching, Twitch chat commands, Photoshop tool cycling, Discord PTT + mute + raise-hand combos. Sweet spot for content creators and streamers; small enough to fit beside a keyboard or on a mousepad, large enough for multi-tier layer logic (Fn keys unlocking secondary functions). Most popular form factor in 2026 enthusiast builds.
12+ key pads and mini keyboards — Full production workflows (Photoshop palette shortcuts, video editing trim/ripple/slip, Ableton drum patterns), accessibility keyswitches (large mechanical buttons with audible feedback), coding macro sets. Often handwired using QMK or ZMK; USB-C powered for all-day use. Wireless versions rarer due to battery complexity at 12+ keys.
---
2. Notable Brand and Product Categories
Premium Open-Firmware Ecosystem
- Keebio and community handwired builds: QMK-powered, often USB-only; enthusiast DIY culture dominates. Vial-compatible boards let users remap without reflashing.
- Ploopy.co: Open-source trackballs and peripherals powered by QMK; accessibility/ergonomics focus (USB-based).
- ZMK-ready platforms: Nice!Nano controllers (Pro Micro footprint, nRF52840 SoC) used in split keyboards and wireless macro pads. The handwired community around nRF52840 (XIAO nRF52840, SuperMini) is growing rapidly for wireless builds.
Software-Defined Alternatives (Premium)
- Elgato Stream Deck family: 15-key LCD button grid; 200+ plugin integrations; $90–$120 range. Stream Deck Neo offers portability. Not programmable at firmware level — all logic lives in software. USB-only via dock.
- Loupedeck CT / Live / Live S: 17+ buttons + 2–3 rotary encoders; targets photo/video editing and streaming. Loupedeck Marketplace with 200+ profiles. Knob-centric design excels at continuous parameter control (audio faders, filter cutoff). $200–$400+. USB-only.
Hobbyist QMK Macro Pads (5×5 and Smaller)
- Ziddy Makes ZM K9 / K16: 9-key and 16-key hotswap with QMK/Vial preloaded; often wooden base cosmetics. $50–$120.
- TogKey Pad Pocket: 2-key portable with QMK; Bluetooth version available.
- Etsy boutique makers: Custom 9/12/16-key mechanical pads; Vial support increasingly standard.
Cheap AliExpress / Amazon Generic Sector (Jieli/CH57x territory)
- CH57x-based pads: USB 3-key/6-key/12-key "macro keypads" (IDs like
1189:8890,1189:8840,1189:8842). No firmware source provided. Configurable via third-party ch57x-keyboard-tool (open-source Rust). Bluetooth mode usually undocumented. - Jieli-based pads (our category): similar black-box firmware. Different vendor ID (
0x4c4a) but same category — cheap AV SoCs repurposed for HID. Less community tooling than CH57x. - SayoDevice: AliExpress pads with vendor GUI (usually Windows-only). Limited macOS support.
- Amazon house brands: $10–$30 pads; Bluetooth usually available but pairing sequence varies wildly.
DIY Microcontroller Boards
- Adafruit Macropad: 12-key hotswap with RP2040; CircuitPython/Arduino-compatible. USB-only. Educational focus.
- Pimoroni Keybow 2040: 16 RGB buttons, RP2040, CircuitPython. USB-only. Raspberry Pi integration angle.
---
3. Bluetooth-Specific Considerations for End Users
BLE HID vs. Classic Bluetooth HID
BLE (Bluetooth Low Energy) HID is the modern standard. Uses HID-over-GATT; negotiates a "connection interval" (how often the host checks for new keypresses). Interval can be 7.5ms–4000ms; short intervals (7.5–30ms) feel snappy but drain battery faster. BLE pairs ~30× faster than Classic BT, though ongoing latency depends on the negotiated interval. Typical end-to-end latency: 8–30ms.
Classic Bluetooth HID is legacy; rare in post-2020 pads. Lower ongoing power overhead once paired, but macOS handles it poorly and pairing is finickier. Most new devices skip it entirely.
For push-to-talk: BLE with a tight connection interval is responsive enough (typical ~10–20ms latency is imperceptible). macOS doesn't always allow OS-level interval tuning, so device firmware often controls this.
Pairing Flow Variations
QMK/ZMK boards: Hold a dedicated pair button or key combination (e.g., Fn+P for 3 seconds) to enter pairing mode. Device appears in System Settings → Bluetooth. Standard BLE pairing, no codes. First-time setup is seamless; re-pairing after a reset is slower (5–10s vs. 1–2s for reconnection).
Jieli / CH57x pads: Pairing flow is manufacturer-specific and often undocumented. Common patterns:
- Long-press a button (3–5s) to toggle Bluetooth mode on/off.
- Some pads have a dedicated pairing button (paperclip-activated reset hole).
- After power-on in Bluetooth mode, the pad auto-advertises for ~30s; macOS picks it up in Bluetooth settings.
- Gotcha: re-entering pairing mode later often requires the same sequence again, which users forget. Battery timeout is rarely documented.
Elgato Stream Deck, Loupedeck: N/A (USB-only).
Multi-Host Switching
QMK/ZMK with layer keys: Boards like Nice!Nano-based split keyboards support "host switch" layers (Fn+1, Fn+2, Fn+3 to cycle paired hosts). Works well when the pad stays stationary; fragile when it sleeps and wakes to a different host priority.
Jieli pads: Most single-host. Switching hosts usually requires re-pairing or power-cycling.
Elgato, Loupedeck: N/A.
Battery Life Expectations
Coin cell (CR2032, 230+ mAh): Theoretical months of life, real macro-pad life 2–8 weeks. BLE transmission causes 5–20 mA current spikes; effective capacity drops to 50–70% under pulsed loads. Repeated peaks >10 mA cause permanent capacity loss. Only viable for light users (<5 presses/day).
Li-Po pouch (300–500 mAh): 5–14 days with moderate daily use (10–20 presses). Degrades through calendar aging and cycle wear (~300–500 full cycles before notable capacity loss). USB-C charging built-in is standard.
Hot-swap / user-replaceable cells: Some boards accept AA/AAA or swappable coin cells. Adds bulk; useful for fieldwork where charging is impractical.
Charging behavior: Most ZMK boards support "USB charging while active" (pad works while plugged in). QMK varies. Jieli pads usually stop responding during USB charging — firmware limitation.
Latency: BLE vs. USB
- USB: ~1–5ms end-to-end.
- BLE: ~8–30ms depending on connection interval and OS scheduling. macOS typically lands in 10–30ms.
For push-to-talk: 20–30ms is imperceptible (human reaction time is ~150–200ms). The bigger real risk is silent disconnection, not latency itself.
Auto-Reconnect After Sleep/Wake on macOS
Ideal: Pad disconnects gracefully on Mac sleep, reconnects within 1–2s on wake.
macOS Sequoia reality:
- Default config lets Bluetooth stay active during sleep.
- Devices should reconnect within 5s on wake. Some take 10–30s; others never reconnect and require manual re-pairing.
- Workaround: third-party tools like
macos-bluetooth-off-while-sleeptoggle Bluetooth on/off with sleep/wake events for a clean reconnection cycle. - Known issue: unpaired devices may wake the Mac. The "Allow Bluetooth devices to wake this Mac" toggle was removed in Monterey+, frustrating for users who want to disable this.
Interference with Magic Keyboard / Magic Trackpad
BLE shares 2.4 GHz with Wi-Fi, Bluetooth mice, Magic Trackpad. Multiple BLE devices in close proximity (pad + Magic Keyboard + Magic Trackpad + AirPods) can occasionally trigger connection dropouts or latency spikes. Impact is minimal if devices are >30cm apart or if they use staggered advertising channels. If your Magic Trackpad is right next to the pad, watch for occasional missed presses during heavy trackpad use.
---
4. Firmware and Software for Remapping Bluetooth Macro Pads
ZMK (Primary Open-Source BLE Firmware)
- Wireless-first design. Supports nRF52840 / nRF52833 SoCs (Nordic ARM Cortex-M4).
- Popular controllers: Nice!Nano (Pro Micro footprint), XIAO nRF52840, SuperMini nRF52840.
- Features: Excellent battery life via aggressive power management; built-in multi-device support (layer keys or dedicated host-select logic); VIA/Vial support via ZMK Studio (still evolving).
- Config workflow: Edit
.keymapfiles in GitHub → trigger firmware build → download.uf2→ drag onto the controller's mass-storage mount. - Learning curve: Moderate. Requires Devicetree syntax understanding.
QMK (USB-Heritage, Bluetooth Bolted-On)
- Historically USB-focused. Wireless support added via external Bluetooth modules (Bluefruit LE nRF51/nRF52) or by porting to nRF SoCs.
- Weaker battery life than ZMK on wireless; no first-class multi-device switching; layer-based workarounds common.
- VIA requires vendor-uploaded board definitions. Vial is open-source and more friendly; click to change keys, instant save to device.
Vial (Client Configuration Protocol)
- Works with QMK and (partially) ZMK; ZMK Studio is the ZMK-native equivalent.
- UX: Open the Vial app (macOS available at
get.vial.today), connect the board, click a key, select a new function — change saved immediately to the keyboard. No recompile. - Limited to 4 layers (same as VIA). Complex macros harder to express in the GUI.
Vendor-Specific Software
- SayoDevice config tool: Windows-only; macOS support uncertain.
- Stream Deck SDK: closed-source plugin system; extensive integrations (Twitch, OBS, Discord, Spotify); no custom-firmware remapping.
- Loupedeck Workbench: Loupedeck-specific profiling and macro builder.
OS-Level Remapping via Karabiner-Elements (Our Path)
For Bluetooth pads with no firmware access — i.e., generic Jieli/CH57x — Karabiner is the fallback. It's free, open, battle-tested.
- Known Bluetooth-specific limitation: some BLE HID devices report VID/PID as
0, which preventsdevice_if-scoped remapping. We'll need to empirically check whether our Jieli pad's BT transport exposes the same VID/PID as USB (0x4c4a 0x4155), different IDs, or zeros. If zeros, we fall back to adevice_ifmatch onis_bluetooth: true+ keycode pattern, which is less precise but still workable. - Reconnection issues can trigger re-remap failures. Karabiner usually handles re-grab automatically, but worth testing.
---
5. Patterns Users Report About Using BT Macro Pads Well
Placement
- Wrist strap or lanyard: hands-free PTT in meetings. Needs a responsive connection (<50ms).
- Mousepad integration: pad 3M-taped next to a Magic Trackpad — always accessible without reaching. Works for scene-switching during streams.
- Desk drawer / shelf within reach: for less-frequent macros (once an hour). Battery lasts longer when the pad isn't jostled.
Single-Machine Setup
Pair once, leave paired. Much simpler than multi-host. Reliable auto-reconnect once established (barring the edge cases above).
Nomadic Multi-Host
Pair to phone + tablet + Mac; rotate via a host-selection key. Good for creators jumping platforms. Higher battery impact due to more advertising.
Low-Battery Workflow
Critical gotcha: The pad often goes silent without warning when battery drops below ~5%. BLE stack shuts down to preserve remaining capacity. No "low battery" indicator on most pads. Best practice: charge every 1–2 weeks for daily use, or add a launchd/cron reminder to check battery state every Friday.
One Button Per High-Value Action
- Button 1 = PTT (Fn → Typeless)
- Button 2 = Mute (app-specific)
- Button 3 = Raise Hand (Zoom-specific)
Avoid complex multi-tap or hold logic on Bluetooth pads — latency makes them feel sluggish compared to a mechanical keyboard.
---
6. Anti-Patterns and Common Mistakes
- Assuming BT latency is good enough for rhythmic typing: do NOT use Bluetooth for rapid Vim motion macros or fast-twitch combos. The ~20ms latency compounds.
- Buying a "QMK" pad that's actually CH57x/Jieli: AliExpress vendors often falsely label black-box pads as "QMK compatible." Check: does the seller provide source or a flasher? If not, it's OS-level remap or nothing.
- Not testing BT sleep/wake before the important meeting: always do a sleep-30s → wake → press-button → count-seconds test before relying on it live.
- Multi-host pads with confusing switching UX: layer-based host switching (
Fn+1/2/3) is intuitive only after practice; a sleep event may reset layer state, causing unexpected host switches mid-meeting. - Confusing connection state: after a disconnect, the pad may silently stay disconnected. System Settings shows it as "paired" but "disconnected" — users don't notice until a hotkey fails. Add a visible Bluetooth monitor (BitBar widget or
lnavlog tail). - Chaining multi-layer macros without battery backup: if the pad reconnects to a fresh session, layer state resets. Flatten critical actions to single-key triggers on layer 0.
---
7. Concrete Recommendations for This Pad
Our Setup
- Hardware: 3-key Jieli pad, VID
0x4c4aPID0x4155, USB-C + Bluetooth modes - Primary use: Typeless push-to-talk via Fn emulation in Karabiner
- Goal: enable Bluetooth for wireless meetings
Suggested Approach
1. Before enabling Bluetooth — Check the pad's enclosure for mode switch, pairing button, or indicator LED. Photo-document it for the 05-bluetooth-roadmap.md. Note expected battery type (coin cell vs. Li-Po).
2. First pairing on macOS — Enter pairing mode (try holding all 3 keys for 5s, or individual buttons, or a hidden reset). Open System Settings → Bluetooth, look for an advertising device with name "USB Composite Device" or similar. Connect. Test all 3 buttons in TextEdit to confirm they emit the same Ctrl+C / Ctrl+V / Ctrl+X as USB mode.
3. Capture Bluetooth VID/PID — Run system_profiler SPBluetoothDataType | grep -A 20 -i jieli and karabiner_cli --list-connected-devices | jq '.[] | select(.is_bluetooth == true)' to capture the Bluetooth transport's identifiers. Note whether they match USB VID/PID or not.
4. Karabiner rule update — Extend the existing complex modification with a second identifiers entry matching the Bluetooth VID/PID. Prefer a single rule with two identifiers (simpler maintenance) over duplicate rules. See 05-bluetooth-roadmap.md "Option B."
5. Test sleep/wake reconnection rigorously — sleep Mac 30s → wake → press pad → time to response. If >5s or fails, either toggle Bluetooth via a launchd wake listener, or keep USB-C as the primary mode and treat BT as nice-to-have.
6. Battery monitoring workflow — If pad exposes battery in System Settings → Bluetooth, add a Friday launchd reminder to check it. If not, charge weekly by default.
7. Fallback plan — Keep the USB-C cable with you during important meetings. If BT fails to reconnect, plug in within 30 seconds.
8. Latency gate for PTT — Open Zoom/Teams in a mock meeting; press pad PTT repeatedly; confirm Typeless activation feels identical to USB-C mode. If noticeable lag, keep BT for light-duty work and USB for meetings.
9. Documentation discipline — After successful BT enablement, replace 05-bluetooth-roadmap.md with 06-bluetooth-configuration.md (matching the structure of 02-usb-wired-configuration.md). Update 01-hardware-identification.md with a new Bluetooth section noting the BT VID/PID, pairing procedure, and any battery behavior observed.
---
Sources
- ZMK Firmware Documentation
- ZMK Hardware Support
- Bluetooth HID Introduction (novelbits.io)
- HID over GATT Profile Specification (bluetooth.com)
- Nordic Semiconductor BLE Battery Life
- Coin Cell vs LiPo for BLE (hubble.com)
- ch57x-keyboard-tool (GitHub)
- Vial Configurator Manual
- Karabiner-Elements Documentation
- macos-bluetooth-off-while-sleep (GitHub)
- QMK, VIA, and Vial Visual Configurators Overview (maxzsol.com)
Macro Keyboard Module
Dedicated documentation for the 3-key USB-C/Bluetooth macro pad, covering hardware identification, current configuration, patterns and anti-patterns, and the roadmap for Bluetooth enablement.
Current Status
- USB-C wired mode: ✅ working. Remapped via Karabiner so TOP is a tap/double-tap pair (single-tap = Fn for Typeless dictation toggle, double-tap = Cmd+V paste), MIDDLE is a tap/double-tap pair (single-tap = Shift+Return newline, double-tap = Return send), BOTTOM is a tap/double-tap pair (single-tap = up arrow, double-tap = down arrow). See `02-usb-wired-configuration.md`.
- Bluetooth mode: ✅ working on firmware mode 4. Pad emits
page_up/page_downfor top + middle (every press, regardless of tap rate — Karabiner detects single-vs-double-tap in software). Pad's BT firmware also runs its own double-tap detection on the bottom button: single-tap →equal_sign, double-tap →Option+Z. The rule routes accordingly: top/middle use the sameset_variable+to_delayed_actionpattern as USB; bottom uses two simple immediate-translation manipulators. See `08-bluetooth-configuration.md`.
Contents
| File | What's Inside |
|---|---|
| `01-hardware-identification.md` | Manufacturer, VID/PID, serial, USB interface structure, HID descriptor decoded, chip-family inference, why the device identifies as "USB Composite Device" |
| `02-usb-wired-configuration.md` | Current Karabiner rule, exact JSON, behavior table, why Karabiner vs. BTT, step-by-step change instructions, troubleshooting, revert recipes |
| `03-patterns.md` | Re-usable techniques that worked: simultaneous vs mandatory modifiers, device-scoped rules, Quartz window ID capture, ignore:true for un-grabbing, Apple vendor Fn encoding |
| `04-anti-patterns.md` | Dead-ends to avoid: BTT CGEventPost for Fn, hidutil for collision-scoped remaps, VIA/Vial on Jieli firmware, {"any": "key_code"} at top level, assuming button-to-keycode without verification, Touch-ID-triggering audits |
| `05-bluetooth-roadmap.md` | Upcoming work: pairing, mode switch, how to identify the BT HID peripheral, preserving existing Karabiner rule across transport types |
| `06-bluetooth-landscape-survey.md` | 2026 ecosystem survey: form factors, brands (Stream Deck, Loupedeck, ZMK/QMK/Vial boards, AliExpress Jieli/CH57x), BLE vs Classic HID, latency, battery, reconnect patterns, firmware options |
| `07-bluetooth-toolbox.md` | Evaluated + spiked FOSS tools for BT control on this Mac: tier-ranked blueutil / sleepwatcher / Hammerspoon / bleak / LightBlue / PacketLogger, install state, caveats (CoreBluetooth HID lock), pairing-day recipe |
| `08-bluetooth-configuration.md` | Live BT config: Free3-P device signature (Samsung-borrowed VID 0x04E8/0x7021), 4 firmware modes (we use mode 4: page_up/page_down/equal_sign), extended Karabiner rule with USB + BT manipulators, switching between transports |
| `references/` | Verbatim hardware dumps captured 2026-04-21: full lsusb -v output, system_profiler (USB tree + BT metadata), ioreg HID device entry, current Karabiner rule export with USB + BT manipulators |
Quick Reference
Device signatures (use these values when writing any new rule or tool):
| Transport | Vendor ID | Product ID | Name | Notes |
|---|---|---|---|---|
| USB-C | 0x4c4a (19530) | 0x4155 (16725) | USB Composite Device | Jieli Technology; serial C1207062. |
| Bluetooth | 0x04E8 (1256) | 0x7021 (28705) | Free3-P | Samsung-borrowed VID; MAC EC:BD:E4:D3:F7:97; Classic BT HID |
Current button mapping — same user-facing behavior across both transports:
| Physical | USB-C emits | BT mode-4 emits | Effect after Karabiner |
|---|---|---|---|
| Top | Ctrl+C | page_up | Single-tap = Fn (Typeless toggle, ~200ms after release); Double-tap ≤200ms = Cmd+V (paste) |
| Middle | Ctrl+V | page_down | Single-tap = Shift+Return (newline, ~200ms after release); Double-tap ≤200ms = Return (send/commit) |
| Bottom | Ctrl+X (every press) | equal_sign (single) / Option+Z (double) | Single-tap = up_arrow (USB ~200ms; BT immediate); Double-tap = down_arrow (USB immediate; BT immediate). The pad's BT firmware does its own double-tap discrimination on the bottom button — Karabiner just translates each emitted keycode. No key-repeat on hold for either transport. |
The pad's BT firmware has 4 distinct modes; we use mode 4 because its native keys (PageUp, PageDown, =) are rarely used on macOS and map cleanly to Fn/Return via Karabiner.
Config file: ~/.config/karabiner/karabiner.json → profile 0 → complex_modifications.rules → rule named Jieli/Free3-P macro pad: ...
Pre-change backups: Karabiner doesn't auto-snapshot. Always back up before editing:
cp ~/.config/karabiner/karabiner.json \
~/.config/karabiner/karabiner.json.bak.$(date +%Y%m%d-%H%M%S){
"device_id": 4308529721,
"device_identifiers": {
"is_keyboard": true,
"product_id": 28705,
"vendor_id": 1256
},
"location_id": 1691613079,
"product": "Free3-P",
"serial_number": "EC:BD:E4:D3:F7:97",
"transport": "Bluetooth"
}
Free3-P:
Address: EC:BD:E4:D3:F7:97
Vendor ID: 0x04E8
Product ID: 0x7021
Firmware Version: 0.1.11
Minor Type: Keyboard
RSSI: -40
Services: 0x800020 < HID ACL >
Not Connected:
DS19:
Address: 41:42:EB:1F:07:C8
Firmware Version: 0.0.0
Minor Type: Headphones
iPhone TerryLi:
Address: 98:50:2E:A3:61:B5
Manufacturer: Jieli Technology
Location ID: 0x00100000 / 1
Current Available (mA): 500
Current Required (mA): 100
Extra Operating Current (mA): 0