HOLODATA-COM/SiriRemoteForge
GitHub: HOLODATA-COM/SiriRemoteForge
SiriRemoteForge 将第三代 Apple TV Siri Remote 改造为功能完备的可编程 macOS 控制器,支持热重载配置、应用级配置文件、触控板光标控制及实验性虚拟麦克风。
Stars: 0 | Forks: 0
# 🛰️ SiriRemoteForge
### 十三个按键。随心定义它们的含义。
[](LICENSE)




*你抽屉里的 Apple TV 遥控器是一块 27 毫米的方形玻璃,上面有十三个按键。*
*这个工具能把它变成一个 Mac 控制器,在这里由 **你** 来决定每个按键的含义——并且*
*这些含义会随着你当前正在查看的应用而改变。如果你胆子够大,它甚至可以变成*
*一个麦克风。*
```
// Back button: tap deletes, hold half a second closes the window,
// hold a bit longer quits the app. Each delay is set independently.
"button.menu": { "action": "keystroke", "keys": "delete" },
"button.menu.hold": { "action": "closeWindow", "after": 0.5 },
"button.menu.hold2": { "action": "applescript", "after": 1.2, "script": "…" },
// …but in a browser, "back" means back.
"browser": {
"button.menu": { "action": "keystroke", "keys": "cmd+[" }
}
```
保存文件即可生效。无需重启、无需重新连接,也不用在设置界面中点击操作——
这意味着 agent 或脚本可以通过编辑一个文件来重新配置整个设备。
**你能获得什么**
- 所有输入都可以重新映射:按键、点击环、触控板、滑动、双指轻点。
- 一个按键最多可以代表六种操作——轻点、双击、三击,以及最多三个长按
阶段,每个阶段都有自己的延迟。屏幕上会显示一张卡片,告诉你如果现在松手会触发什么操作;如果按住超过了
最后一个阶段,则会取消操作。
- 支持应用级配置文件,可以通过继承链向下传递,此外还有 **layers**,可以一次性切换
整个遥控器的布局(按住时切换或保持启用状态)。
- 玻璃面板可以驱动光标,支持可调节的加速度、iPod 风格的环形滚动、粘滞拖拽、
摇动寻找指针以及按下点击功能。
- 提供径向应用启动器、带动画的 Space 切换、窗口控制、亮度调节,以及一个原生的
设置应用,里面包含一个绘制且可点击的遥控器界面。
## 功能介绍
- **所有按键均可重新映射。** 按键(Back/Menu、TV、Siri、Play/Pause、Mute、Volume ±、Power)、
点击环(上/下/左/右 + 中心)、单指滑动以及双指轻点——每一个都可以
映射到任意操作。
- **触控板 → 光标。** 遥控器的玻璃触控板可以通过可调节的速度驱动鼠标指针,支持
稳定死区、基于速度的指针加速、按下冻结、轻点点击,
以及 iPod 风格的 **环形滚动**(在外环上画圈滑动手指即可滚动)。
- **应用级配置文件。** 最前方的应用会选择一个 *模式*(例如浏览器模式、终端模式);
绑定配置会沿着 `inherits` 链向下传递,回退到全局默认设置,然后再回退到遥控器的原生行为。
- **Layers。** 一个按键可以切换一个 **layer**(类似于键盘的层):轻点可切换粘滞 layer
的开/关,或者按住以使用临时 layer。Layers **可与应用组合使用**——同一个 layer 可以
在不同的应用中执行不同的操作。屏幕上的 HUD 会确认切换状态。
- **多阶段长按**(`.hold` / `.hold2` / `.hold3`,松手即选择),**多次轻点**
(`.double` / `.triple`),以及 **按住重复**(按住时自动重复发送按键)。
- **附加功能:** 电源键会使所有显示器变暗,*而不是让 Mac 进入睡眠或锁定状态*(macOS 自带的
电源键热键仅对遥控器禁用——你 Mac 的物理电源键不受
影响),并且任何触摸操作都会恢复显示;HUD 会确认遥控器的连接和断开;
摇动光标会触发闪烁的“寻找我的指针”高亮效果;支持带动画的 macOS **Spaces** 切换。
- **两种配置方式:** 手动编辑 `~/.config/siriremote/config.jsonc`(保存时热加载),或者
使用内置的 **Settings** 应用——包含一个 Tuning(调节)选项卡(滑块)和一个 Layout(布局)选项卡(绘制出的遥控器 + 一个内联的、支持点击编辑和分应用层的映射编辑器)。
## 工作原理
```
flowchart LR
R([🛰️ Siri Remote]) -->|BLE / HID| I[① InputIOHIDManager +
MultitouchSupport] I --> G[② Gesture
recognition] G -->|named event| E[③ Engine
config resolution] E -->|Action| X[④ Executor
CGEvent · media ·
AppleScript · shell] X --> M([macOS]) C[(config.jsonc)] -. hot reload .-> E W[⑤ frontmost-app
watcher] -. mode .-> E ``` 发布的这款应用包含两个 Swift 部分,外加一个独立的虚拟麦克风子系统以及 一个早期的 DriverKit 实验: - **`SiriRemoteCore/`** — 一个纯 Swift、无依赖、经过单元测试的引擎(一个 SwiftPM 包)。它 负责管理配置模型(JSONC 解析、`Action` 枚举、分应用模式 + `inherits` 解析、 layers、多阶段长按阈值),配置的 **回写**(将 `Config` 序列化回 JSONC,以便 UI 编辑可以往返转换),以及环形滚动的数学计算。不包含 AppKit,没有 I/O 操作—— 非常容易进行测试(`swift test`)。 - **`app/`** — 使用 `swiftc` 构建的原生 macOS 层(见 `app/build.sh`)。它通过 HID 抢占遥控器(`IOHIDManager`),通过私有的 `MultitouchSupport` 框架读取 触控板,识别手势,监视最前方的应用,并通过 CGEvent / 媒体键 / AppleScript / shell 执行操作。它还承载了 SwiftUI **Settings** 窗口。核心包被直接编译 进这个二进制文件中(没有独立的库)。 - **`mic/`** — 可正常工作的 **虚拟麦克风**:一个 CoreAudio HAL 插件,会发布一个名为 "Siri Remote Mic" 的输入设备,由一个蓝牙语音路由器和一个按需启动的 root daemon 提供支持。详见 [🎙️ 将遥控器变成麦克风](#microphone)。 - **`driverkit/`** — 一个早期的 HIDDriverKit 替代麦克风的概念验证(已被 `mic/` 取代;仅作保留参考)。构建/签名脚本不会安装或激活它。 ## 系统要求 - macOS 13+(Ventura 或更高版本),Apple silicon 或 Intel。 - 一台 **第三代 Siri Remote**(2022 款,USB-C)。首先通过蓝牙进行配对(将其靠近 Mac; 它会显示为键盘/触控板设备)。 - 需要 Xcode 命令行工具(`xcode-select --install`)来使用 `swiftc`。 **无需第三方工具。** 动画化的 Space 切换曾经通过 BetterTouchTool 进行路由;现在 它通过 System Events 进行处理,这需要自动化权限(macOS 仅会询问一次)——参见 `space` 操作。 ## 构建与运行 ``` cd app ./build.sh # compiles the app + SiriRemoteCore into ./HyperVibe ./create_app_bundle.sh # wraps it into HyperVibe.app (icon auto-generated, code-signed) open HyperVibe.app ``` `build.sh` 会生成一个独立的 `./HyperVibe`,你也可以直接运行它(`./HyperVibe --settings` 会在 启动时打开设置窗口)。`create_app_bundle.sh` 会打包出一个可双击运行的 `HyperVibe.app`。 **这是一个菜单栏应用**(没有 Dock 图标):启动后,点击菜单栏上的对讲机图标即可找到 **Settings… / Quit**。如果菜单栏图标被隐藏了(例如在刘海后面),只需**再次双击 `HyperVibe.app`**——这样就能重新打开 Settings 窗口。 ### 权限 macOS 会限制此应用所需的底层访问权限。首次运行时,请在 **System Settings → Privacy & Security** 中授予相应权限: - **Accessibility** — 用于移动光标和发送按键。 - **Input Monitoring** — 用于通过 HID 接收遥控器的按键信号。如果按键没有反应, 这几乎总是原因所在(日志会显示 `IOHIDManagerOpen failed 0xE00002E2`)。 该 bundle 特意**没有**使用加固运行时进行签名——因为在加固运行时下,私有的 MultitouchSupport 触控回调会触发代码签名强制执行机制,导致应用在 你触碰触控板的瞬间就被杀掉。如果存在稳定的本地自签名身份 (`siriRemote Local Signing`),`create_app_bundle.sh` 会优先使用它, 以便权限在重新构建后依然有效;否则它将进行临时签名。 ### 如果绑定了电源键,则需要进行所需的系统设置 **只有在映射了 `button.power` 时才需要。** 否则请跳过此步骤。 macOS 会将遥控器的电源键转换为*系统*电源热键。`loginwindow` 会对该操作做出反应(`PBSleepsMachine`),使 Mac 进入睡眠/锁定状态——**此外还会**运行你绑定的 任何操作,因此对 Power 的绑定会使屏幕变暗,*同时* 将其锁定。请关闭该行为: ``` sudo defaults write /Library/Preferences/com.apple.loginwindow PowerButtonSleepsSystem -bool false ``` 这会立即生效(loginwindow 会在每次按下时重新读取该偏好设置)。想要撤销此操作,请写入 `true`。 **此操作带来的影响:** 你的 Mac 自身的物理电源键在短按时不再使机器进入睡眠。 Touch ID、长按以调出强制关机对话框、合上盖子以及 Apple 菜单中的 睡眠功能都将正常工作。 **在遥控器上找回睡眠功能**——请绑定应用*可以*控制的长按操作: ``` "button.power": { "action": "brightness", "value": 0.0 }, // tap → dim "button.power.hold": { "action": "shell", "command": "pmset sleepnow" } // hold ≥ holdThreshold → sleep ``` 按下电源键还会开启一个 **1 秒的输入保护**:该按键紧挨着玻璃面板,因此 按下时几乎总会触碰到触控板,这在过去会立即撤销刚刚触发的 屏幕变暗效果。在保护期间,触摸和其他按键仍会被读取,但不会触发动作。 ## 🎙️ 将遥控器变成麦克风 第三代 Siri Remote 拥有一个真正出色的近讲麦克风——也就是那个你会拿起来 对着说话的麦克风。macOS 从未将其公开暴露(它不是一个标准的蓝牙音频设备),因此 `mic/` 构建了这样一个设备:一个名为 **"Siri Remote Mic"** 的 CoreAudio 设备,任何应用都可以选择它。 **按住 Siri 按键并说话 → 你的声音将从遥控器的近讲麦克风输入。松开 → 它 会无缝回退到 Mac 的内置麦克风。** 将其与按下即说(push-to-talk)绑定结合使用,这就构成了一个 麦克风始终在你手中的听写装置。 ``` flowchart LR H[Hold Siri + speak] --> B[Remote streams
voice over BLE] B --> P[PacketLogger
HCI capture] P --> RT[srm_router
decode → ring] RT --> S[(shared-memory ring)] S --> HAL[SiriRemoteMic
HAL plug-in] HAL --> A([any app:
“Siri Remote Mic”]) BM[🎤 built-in mic] -. fallback when idle .-> HAL ``` 底层原理:一个按需启动的 **root LaunchDaemon**(`mic/captured/`)仅在有应用 实际使用该设备时运行 Apple 的 PacketLogger;**`srm_router`**(`mic/router/`)将遥控器的 专有 BLE 语音通知解码到无锁的共享内存环中;而一个 **HAL 插件** (`mic/driver/`,一个 [BlackHole](https://github.com/ExistentialAudio/BlackHole) 的加固分支版本) 将该环形缓冲——或者是内置麦克风的回退缓冲——提供给 CoreAudio,并在每次交接时进行交叉淡入淡出。 相关设置位于 [`mic/README.md`](mic/README.md) 以及各组件的 `install.sh` 脚本中。提供了一个打包了所有内容、支持双击运行的安装程序,位于 [`dist/`](dist/README.md) 中。 ## 配置 — `~/.config/siriremote/config.jsonc` JSONC(JSON + `//` 注释)。首次运行时会写入默认配置。**保存时会实时热加载。** 包含三个顶级键:`settings`、`appProfiles`、`modes`。你可以参考 [`examples/config.jsonc`](examples/config.jsonc) 中完整且可用的示例,而维护者本人日常实际使用的 配置——包括按下即说(push-to-talk)、分应用 Music/浏览器/终端模式、layers——位于 [`examples/config.author.jsonc`](examples/config.author.jsonc)。 ``` { "settings": { "defaultMode": "global", "cursorSpeed": 1.4, /* … tuning … */ }, // Frontmost app's bundle id → mode name (plus a "default"). "appProfiles": { "com.google.Chrome": "browser", "dev.warp.Warp-Stable": "terminal", "default": "global" }, "modes": { "global": { "button.tv": { "action": "layer", "to": "L1" }, // TV button = layer L1 "ring.left": { "action": "keystroke", "keys": "left" }, "ring.up.hold": { "action": "shell", "command": "open -a 'Mission Control'" } }, "browser": { // inherits global, overrides some keys "inherits": "global", "button.menu": { "action": "keystroke", "keys": "cmd+[" }, // Back button = history back "L1.ring.left": { "action": "keystroke", "keys": "cmd+opt+left" } // L1 in Chrome = prev tab }, "L1": { "inherits": "global" } // layer marker (see Layers) } } ``` ### 事件键 `ring.up` `ring.down` `ring.left` `ring.right` · `select`(中心点击) · `touch`(表面) · `swipe.up` `swipe.down` `swipe.left` `swipe.right` · `tap.two` · `button.menu`(Back ‹ 按钮) `button.tv` `button.siri` `button.playPause` `button.volumeUp` `button.volumeDown` `button.mute` `button.power`。 可以为任何按键/环形键添加以下后缀: - **`.double` / `.triple`** — 多次轻点变体(`button.siri.double`,`ring.up.triple`)。在 `doubleTapWindow` 时间内对轻点进行计数,并且只有达到的最深计数才会触发操作——三击操作永远不会同时 触发双击或单击。 每个按键的等待时间刚好是其自身绑定所需的时长,不会更长。没有多次轻点 绑定的按键会在按下时立即触发其轻点操作,完全没有任何额外延迟。添加 `.double` 会使双击操作 在第二次按下时立即触发。添加 `.triple` 是唯一会带来 开销的操作:该按键的*双击*现在必须等待一个 `doubleTapWindow`的时间,以查看是否会有第三次 轻点。其他按键不会受到影响,且普通的单击永远不会被两者中的任何一个延迟。 - **`.hold` / `.hold2` / `.hold3`** — 多阶段长按(`ring.up.hold`)。*松手即选择:* 继续按住以达到更深的阶段;当你松手时,达到的最深阶段将被触发。 ### 操作 | `action` | 参数 | 备注 | |---------------|---------------------------------------|-------| | `keystroke` | `keys` 例如 `"cmd+shift+["` | 修饰键 cmd/ctrl/opt/shift(+ `l`/`r` 变体,如 `rcmd`);仅包含修饰键的字符串表示按住的组合键;支持的按键:字母、数字、方向键、esc/enter/space/tab、标点符号 | | `media` | `key` | playpause/next/previous/volup/voldown/mute | | `mouse` | `op` | click/rightclick/move/scroll | | `launch` | `app` 和/或 `url` | 打开应用或 URL | | `shell` | `command` | 通过 `/bin/zsh -c` 运行 — 逃脱机制 | | `applescript` | `script` | 例如控制 Apple Music | | `mode` | `to` | 切换当前活动模式 | | `layer` | `to` | 将此按键变为一个 **layer** 按键(见下文) | | `space` | `to`: `left`/`right` | 切换 macOS Spaces,带动画,通过 System Events 进行(需要自动化权限) | | `fullscreen` | — | 切换最前方窗口的全屏状态,通过 Accessibility API 实现 — 模拟 Ctrl+Cmd+F 不起作用 | | `minimize` | — | 最小化最前方的窗口(Accessibility API) | | `closeWindow` | — | 按下窗口的红色关闭按钮。不是 Cmd+W,因为它在任何支持多标签的应用中都会关闭一个 *标签页* | | `appWheel` | — | 召唤径向启动器 (`settings.appWheel`) | | `repeatKey` | `keys`, `delay?`, `interval?` | 按住时自动重复(遥控器不发送自动重复信号) | | `brightness` | `value` (0…1) | 设置所有显示器的背光;`0` = 最小值(由 Power 调暗屏幕时使用) | ### Layers (layer × app) 将一个键绑定为 `{ "action": "layer", "to": "L1" }`。该键即成为一个 **layer 按键**: - **轻点**它 → 切换 layer `L1` 的*粘滞*开/关状态(保持启用直到再次轻点)。 - **按住**它并按下其他键 → *临时* `L1`(仅在按住时有效)。 **layer 是一种修饰键,而不是第二套键盘。** 按住一个 layer 键永远不会将已绑定的键变为死键: layer 配置未提及的按键将继续执行其未分层时的任何操作,*在当前应用中*。 当按住 layer `L` 时,键 `K` 会按照最具体优先的原则进行解析: | # | 查找 | 含义 | |---|--------|-------| | 1 | 活动应用模式的 `inherits` 链中的 `"L.K"` | 此应用,在此 layer 中 | | 2 | 模式 `L` **自身**绑定中的 `"K"` | 任意应用,在此 layer 中 | | 3 | 活动应用模式的 `inherits` 链中的 `"K"` | 此应用,**不带** layer | 示例:在 `global`(默认模式)中,`L1.ring.left` = `cmd+shift+left`,但在 `browser` 中则为 `cmd+opt+left` (步骤 1);`terminal` 将 `button.menu` 绑定为 `repeatKey delete`,并且没有在 `L1` 中对其进行说明,因此 在终端中按住 `L1` 仍然会执行删除操作(步骤 3)。 一个 layer 会**完整地**接管一个按键。绑定其任意变体 —— `L1.button.playPause`,或者仅仅是它的 `.hold` — 该按键的所有其他变体都将只在 layer 内进行解析;未分层的 `.hold2` 不再 从底层穿透。一个按键就是一个整体,即使它的变体存在于不同的独立键下,另一种选择是为每个变体编写显式的无操作绑定。如果 layer 没有绑定某个按键的任何变体,该按键将完全不受影响地穿透回退。 有两点值得注意的后果: - **不存在针对“任何应用,不带 layer”的第四个步骤。** 步骤 1 和 3 会遍历*应用*模式的 `inherits` 链,这也就是通往 `global` 的途径——仅在 `global` 基础配置中绑定的键在 `terminal` 中处于某个 layer 下时依然会被解析, 因为 `terminal` 继承自 `global`。将此逻辑保留在 `inherits` 中,而不是硬编码一个全局回退,正是让一个模式可以选择退出的原因:一个编写时没有带 `inherits` 的模式是独立的,并且实际上看不到其他任何配置,无论是否分层。反之, 一个忘记写 `"inherits": "global"` 的应用模式将只对其列出的键做出响应。 - **步骤 2 不会遵循模式 `L` 自身的 `inherits`。** Layer 模式被编写为 `"L1": { "inherits": "global" }`,因此如果遵循它,将会使用 `global` 的*基础*绑定进行响应,从而 覆盖掉步骤 3 中针对具体应用的绑定。请将与应用无关的 layer 绑定直接放在 `L1` 模式中;它们 就是步骤 2。保留此标记模式以确保该 layer 存在。 ### 标签和图标 任何绑定都可以带有 `label` 和 `icon`。它们不会改变运行的操作——它们的作用是让长按进度 HUD 能够 显示如果你现在松手会触发的操作名称。 ``` "button.power.hold": { "action": "shell", "command": "pmset sleepnow", "label": "Sleep", "icon": "moon.fill" }, ``` `icon` 是一个 SF Symbol 名称,通常是不必要的:**打开**应用的操作会显示该 应用的真实图标,*而不是*标签(`launch`,以及编写为 `open -a "Some App"` 的 `shell` 命令), 而**针对**某应用的操作(包含 `tell application "X"` 的 `applescript`)会显示该应用的 图标,并位于其标签*旁边*。否则,将从操作类型中自动选择一个符号。 **表现形式的属性会沿着模式链独立向下继承,逐个字段进行,与具体的绑定无关。** 一个键即使在某个模式中被重新绑定时也会保留其身份,因此只需在 `global` 中设置一次 `label`/`icon`,仅覆盖了*操作*的应用模式仍然会显示相同的名称和图标——无需复制,从而避免产生不同步的问题。如果一个模式确实想以不同的方式呈现某个键,只需直接声明, 并且距离更近的模式优先级更高。 ### 长按时序 各个阶段会在达到 `holdThreshold` / `holdThreshold2` / `holdThreshold3` 时触发,但任何绑定都可以使用 **`after`** 设置自己的延迟: ``` "button.menu.hold": { "action": "closeWindow", "after": 0.5 }, "button.menu.hold2": { "action": "applescript", "after": 1.2, "script": "…" }, ``` 全局设置由每个键共享,因此如果没有这个参数,调整一个按键的时序就会改变绑定在同一阶段的 其他所有按键——这曾两次迫使某个绑定被放到一个它不属于的阶段,仅仅是为了 不影响另一个键的时序。**各个阶段是根据其实际延迟排序的,而不是根据后缀排序**,因此 `.hold3` 完全有可能在 `.hold` 之前触发;后缀仅仅是一个名称。 `holdCancelGrace` 是从该键实际绑定的最深阶段开始计算的,因此最深长按设置为 0.5 秒的按键不会在死区中等候几秒钟才被取消。 ### 焦点跟随光标(填满整个显示器的应用) `"focusFollowsCursor": true` 会使光标下方的应用在指针停留在其上方(约 0.15 秒)后成为最前方的应用,因此按键绑定会作用于你正在指向的位置,而不是你最后一次点击的位置——在一个显示器上滚动浏览网页,按下按键,快捷方式就会发送到那个浏览器。 **它只会激活那些窗口已经覆盖该显示器 ≥90% 面积的应用**,而这个限制正是该功能正常运作的体现,而不是缺陷。macOS 没有公开的方法可以在不置顶应用的情况下赋予其键盘焦点,因此不受限制的焦点跟随鼠标功能会在指针每次穿过某个应用时重新排列你的窗口堆栈。一个已经填满整个显示器的应用不会干扰任何东西——将其置顶并不会改变你能看到的任何内容。重叠或半屏窗口将被原样保留。 请注意,条件是*填满显示器*,而不是*处于全屏状态*。一个仍然显示菜单栏的最大化窗口同样安全,这也是大多数人实际使用的方式;字面意义上的全屏测试与作者自己的任何窗口都不匹配。覆盖率是根据该应用在该显示器上所有窗口的并集来计算的,因为某些应用(如 Chrome)会将它们的标签条和内容拆分为单独的窗口,只有合在一起才能覆盖整个显示器。 默认关闭——因为它会改变接收你输入的应用,这不应该是一个出乎意料的变化。 ### 应用轮盘(径向启动器) `"appWheel": ["WeChat", "Google Chrome", "Music", "Warp"]` 列出了应用列表,从顶部开始顺时针排列;留空则禁用。将 `{ "action": "appWheel" }` 绑定到一个长按操作——通常是 layer 键的长按: ``` "button.tv": { "action": "layer", "to": "L1" }, "button.tv.hold": { "action": "appWheel" }, ``` 这是一个普通的长按绑定,因此它会像其他任何绑定一样获得进度卡和取消宽限期,而带有长按阶段的 layer 键仍然可以通过轻点进行切换,并且在按住期间按下其他按键时,它仍然可以作为临时 layer 工作。 该轮盘会**以指针为中心**打开,因此选择操作只是向外轻拨一下,而不是跨越整个 显示器——这在 27 毫米的触控板上非常重要。选择跟随的是光标,而不是手指在触控板上的位置,因此触控板的表现与往常完全一样。**选择**(Select)会启动当前高亮显示的项目;**任何其他按键**都会取消操作;当指针处于中间死区时,没有任何项目会被高亮显示,因此召唤出轮盘并按下选择键不会因为意外而触发操作。 ### 设置(调节) 全部位于 `settings` 以及应用的 **Tuning** 选项卡中:`cursorSpeed`、`cursorDeadzone`、指针加速曲线(`accelMin`/`accelMax`/`accelLowSpeed`/`accelHighSpeed`)、`clickRiseThreshold`、`pressMoveMax`、`holdThreshold`/`holdThreshold2`/`holdThreshold3`、`doubleTapWindow`、`spacesModeWindow`、`findCursorEnabled`、`focusFollowsCursor` 以及 `circularScroll { enabled, minRadius, startThreshold, pixelsPerRadian, scrollEase, invert }`。配置文件是唯一的事实来源——在 Tuning 选项卡中对滑块进行的更改将被回写(带有防抖处理)到 `config.jsonc` 中。 ## Settings 应用 - **Device** — 已配对遥控器的实时状态:**电池电量百分比 (%)**、固件版本、Bluetooth 地址、序列号、供应商/产品 ID,以及 macOS 公开的七个 HID 接口的展开映射图。 电池信息也会显示在窗口顶部的状态胶囊中(`● Connected · 🔋 100%`),并且随着电量下降会变成橙色/红色。电池/固件信息来自系统蓝牙协议栈(`system_profiler`,约 0.15 秒,在主线程之外轮询);接口映射图直接取自 `IOHIDManager`。 - **Tuning** — 用于调整光标手感、加速度、点击、环形滚动和按键时序的分组 滑块,每项调整均实时生效。最下方是 **Startup → Start at login**,它将应用注册到 `SMAppService`(macOS 13+)。注册基于 bundle 进行,因此它跟随 `HyperVibe.app` 并能在原位重建后保留;它也会出现在 **System Settings → General → Login Items** 中,因此即使应用未运行也可以在那里关闭。该开关始终会重新读取真实的注册状态,因此它不会处于 macOS 未接受的位置——如果 macOS 需要批准,页脚会进行提示。支持通过脚本控制:`open HyperVibe.app --args --enable-login-item`(或 `--disable-login-item`)。 - **Layout** — “每个按键的作用”:左侧是一个绘制出的铝制遥控器(点击按键可跳转到其映射;选定的输入保持高亮显示),一个用于选择模式的 **app hub**,一个 **Editing: base / layer** 选择器(layer × app 网格),以及一个带有 Custom / Inherited / System 标签的分组输入→列表。点击任意输入即可打开其对应的停靠编辑器,编辑其 轻点 / 双击 / Hold·· / Hold··· 插槽,更改会直接写入 `config.jsonc`。 ## 仓库布局 ``` SiriRemoteForge/ ├── SiriRemoteCore/ # pure engine (SwiftPM package) — config model, resolution, write-back, tests │ ├── Sources/SiriRemoteCore/ │ └── Tests/SiriRemoteCoreTests/ # `swift test` (config round-trip, resolution, layers, …) ├── app/ # native macOS app (swiftc) │ ├── *.swift # HID, MultitouchSupport, gesture recog, executors, SwiftUI settings │ ├── build.sh # canonical build (compiles the app + ../SiriRemoteCore into one binary) │ ├── create_app_bundle.sh │ ├── tools/make_app_icon.swift │ ├── SiriRemote-Bridging-Header.h / MultitouchSupport.h │ └── HyperVibe.entitlements ├── mic/ # virtual microphone (see the Microphone section) │ ├── driver/ # CoreAudio HAL plug-in ("Siri Remote Mic"), a hardened BlackHole fork │ ├── router/ # srm_router — decode BLE voice notifications → shared-memory ring │ ├── captured/ # on-demand root LaunchDaemon (runs PacketLogger + router) │ └── README.md ├── dist/ # one-double-click installer that bundles all components └── driverkit/ # earlier Siri Remote microphone DEXT proof of concept (superseded by mic/) ├── SiriRemoteMicDriver.xcodeproj ├── Host/ # separate OSSystemExtensionRequest host ├── build-driver.sh # unsigned DEXT build only └── build-host.sh # embeds DEXT; does not launch or activate ``` 应用目标在内部被命名为 `HyperVibe`(历史遗留原因,源于下文提到的 fork);而产品名称是 "siriRemote"。 ## 开发 ``` cd SiriRemoteCore && swift test # unit tests for the engine cd app && ./build.sh # build the app cd driverkit && ./build-host.sh # build-only DEXT + host check ``` 调试日志输出到 `/tmp/hypervibe.log`(HID 事件、设备选择、已执行的操作)。 当前的开发状态和后续注意事项位于 [`HANDOFF.md`](HANDOFF.md) 中。那个**可正常工作的** 麦克风设备是上文[描述过](#microphone)的 `mic/` 蓝牙路由器管线。为了 实现这一目标,必须首先排除带内方法;这些死胡同以及完整的证据日志位于 [`docs/mic-reverse-engineering.md`](docs/mic-reverse-engineering.md)。 死胡同(全部为可选的开发标志,在正常启动中不存在——仅作保留参考): - `--dump-reports` — 清点 IOHID 报告和可读的 Feature 值; - `--activate-mic` — 捕获每个遥控器接口,并发送第三代 `0xAF` 的输入启用字节; - `--dump-gatt
标签:macOS工具, Siri Remote, Swift, 外设控制, 快捷键映射, 硬件工具