---
title: "逆向 AK35i V3 Max 的 Windows 驱动，移植成 macOS 原生应用"
description: "AJAZZ AK35i V3 Max 是一把三模机械键盘，带一块 240×135 的屏幕，官方控制程序只有 Windows 版。这个项目把它的功能移植到 macOS，做成原生应用 AK35i Control Center 和命令行工具 ak35i，覆盖灯效、单键颜色、系统设置、改键、宏、屏幕时钟、屏幕图片、电量，以及驱动原版的实时灯效和音乐律动。"
pubDate: 2026-10-17
tags: ["工程复盘", "开源", "Swift", "macOS", "反向工程", "硬件", "AI Agent"]
---

AJAZZ AK35i V3 Max 是一把三模机械键盘，带一块 240×135 的屏幕，官方控制程序只有 Windows 版。这个项目把它的功能移植到 macOS，做成原生应用 AK35i Control Center 和命令行工具 `ak35i`，覆盖灯效、单键颜色、系统设置、改键、宏、屏幕时钟、屏幕图片、电量，以及驱动原版的实时灯效和音乐律动。

整个过程没有 Windows 电脑，也就没有 USB 抓包。协议全部来自对驱动和键盘固件的静态分析，再在 Mac 上靠键盘的应答和实际效果逐项验证。

<img src="./control-center.png" alt="AK35i Control Center 概览页" loading="lazy" decoding="async">

## 分析对象

驱动取自 AJAZZ 官网驱动索引里与型号同名的条目 `AJAZZ_AK35I MAX V3_Tripe mode RGB with screen_keyboard driver_V1.0.0.zip`，下载后先记 SHA-256。归档里是一个 Inno Setup 6.1.0 安装器，用一次性 Alpine 容器里的 innoextract 只读解包，得到 145 个文件。驱动本身从未运行过。

逆向用到的文件如下。

| 文件 | 作用 |
|---|---|
| `DeviceDriver.exe` | 控制程序，32 位 x86 MFC，所有构包和发包逻辑都在这里 |
| `mui.dll` | 界面库，实时灯效动画和 RGB565 转换在这里 |
| `config.xml` | 设备表，USB 为 `VID_0C45&PID_8009&MI_00`，2.4G 接收器为 `0C45:FDFD` |
| `rgb-keyboard.xml` | 104 键布局（键码、灯号、行列），发包间隔 `cmd_delaytime` 35 ms，亮度和速度上限 5，GIF 帧数上限 141 |
| 语言文件 | 界面字符串 ID，用来把控件和处理函数对上 |
| 两个固件升级程序 | 资源里带 256 KB 的主控固件镜像（ARM Cortex-M，`SN32F290`），只做只读反汇编 |

驱动把配置存在本地 SQLite 里，有 `t_light_data`、`t_key_macro_data`、`t_macro_data` 等十几张表。键盘的改键、宏和屏幕都读不回来，驱动界面上显示的改键和宏完全来自本地数据库。这些建表语句后来用来核对结构体偏移。

## 方法

### 静态分析

入口是 HID 调用。`DeviceDriver.exe` 导入 SetupAPI 和 `DeviceIoControl`，并动态解析 `HidD_SetFeature`、`HidD_GetFeature`。有线下所有设置都经过唯一一处 `HidD_SetFeature` 包装（`0x452010`），上一层是单包发送函数 `0x44faa0` 和实时帧发送函数 `0x44fc90`。从这几个函数的调用方往上追，借语言 ID 和控件 ID 把每个界面功能对到一个构包函数，再按指令读出每个字节的来源。

结构体字段靠数据库定位。例如灯效构包函数 `0x42b400` 之前，`0x4084e0` 执行 `SELECT * FROM t_light_data WHERE profile = ? AND mode = ?` 填结构体，对照建表列序就能确定 `+0x10` 是亮度、`+0x14` 是速度、`+0x24` 是颜色。

分析分了三轮。

1. grok CLI 通过 REA 调 Hopper 6.6 反编译，产出功能拆分初稿。
2. Claude 按指令复核传输封装、灯效、音乐律动和时钟。Hopper 的伪代码会把逐字节写入折叠起来，字段偏移一律以指令为准。
3. 换用 llvm-objdump，Claude 开两个子代理分头追系统配置、改键、宏、屏幕和实时帧，主会话按地址抽查。

初稿的骨架是对的，但有几处照抄就会发错报文，都在复核时改掉了。

- 改键和宏被合成一条命令。改键是 `04 11` 和 `04 27`，宏是 `04 19` 加 `04 15`，格式完全不同。
- `0x418590` 被当成屏幕帧提交，它是改键发送函数。
- `0x4346a0` 被当成用户灯效，它是侧灯模式，本机布局 `sidelight="0"`，走不到。驱动里的“用户灯光”就是单键颜色 `04 23`。
- 音乐律动被写成不发 HID，有线下它每帧都发一个 `04 20`。
- 更早的记录把 `DeviceIoControl(0xB0192)` 写成读报告描述符，它是 `IOCTL_HID_GET_FEATURE`，驱动用它读回写入后的应答。

固件镜像套上 ELF 头后用 llvm-objdump 按 Thumb 反汇编。USB 命令阶段的分发在 `0x5012`，数据阶段在 `0x515C`，无线命令在 `0x67C8`。固件分析有两个用处，一是确认写入命令落在哪块闪存，屏幕上传前必须知道这一点，二是和驱动的 2.4G 分支互相印证。驱动包里的固件是 V1.17，实机后来查出是 V1.30，只靠固件得出的结论都要上实机复核。

### 验证路线

原计划是在 Windows 上用 USBPcap 抓包，逐条对照静态结果。没有 Windows 电脑，改成下面的流程。

1. 按驱动逐字节还原报文，先写成测试。
2. 在 Mac 上发送，按驱动的方式读回应答。
3. 观察效果。灯效和屏幕目测；按键类设置用一次性的只读 IOKit 输入监听，记录键盘实际发出的 HID 用法；能读回的读回核对。
4. 拔插数据线、切换连接档位，确认是否持久化。

试验顺序按可恢复性排。基础灯效最先，写错了能用键盘自带的 `Fn+\|`、`Fn+↑/↓`、`Fn+-/=` 恢复。改键、宏、屏幕读不回来，先找好恢复路径再试。改键写回两层全默认就能恢复，驱动的“重置键盘”发的就是这个；宏的恢复是上传空宏表；屏幕则先分析固件，确认上传不会覆盖出厂开机动画。

几条边界始终不变。只发驱动里还原出的序列，不穷举未知命令。不实现固件升级、DFU 和恢复出厂。每次写入都要在界面上确认，命令行要加 `--apply`。整个项目只破过一次例，见蓝牙一节。

## 有线传输层

### 接口

`0C45:8009` 有两个厂商接口。

| 接口 | 用途页 / 用途 | 报告 | 承载 |
|---|---|---|---|
| 3 | `0xFF13` / `0x01` | Feature 64 字节 | 所有命令和设置数据 |
| 2 | `0xFF68` / `0x61` | Output 4096 字节，Input 64 字节 | 屏幕图像 |

### 报文、会话和应答

驱动的 Feature 缓冲固定 65 字节，第 0 字节是报告 ID 0，后 64 字节是载荷。命令包载荷以 `04 <命令号>` 开头。一次设置是一个会话，通常是下面的形状，每包前等 35 ms。

```text
04 18              开始，要求应答
04 <命令>          前导包，载荷偏移 8 = 后续数据包个数，要求应答
<数据包> × N       每包 64 字节，不读应答
04 02              提交，要求应答，设置在这一步持久化
04 F0              结束，部分功能没有
```

要求应答的包写完后再等 35 ms，用 `IOCTL_HID_GET_FEATURE` 读回。驱动以载荷偏移 3 为 `0x01` 算成功，但调用方从不检查这个结果，失败了照样往下发。

这条规则在 macOS 上先用已经验证过的时钟序列校准，避免和未验证的灯效报文混在一起。读回结果是命令包的应答为命令字原样回显、偏移 3 为 `0x01`；数据包的应答是整包回显，偏移 3 是数据本身。最终判定条件是前两字节等于命令字且偏移 3 为 `0x01`，只用于命令包。

### macOS 实现

不需要内核扩展，用户态 IOKit 就够了。

- 用 `IOHIDManager` 按 VID、PID、主用途页和主用途匹配接口。匹配结果不是恰好一个就停止，不猜。
- `IOHIDDeviceSetReport` 的报告 ID 是单独参数，缓冲里要去掉驱动那个前导 0，只传 64 字节。`IOHIDDeviceGetReport` 读回的 64 字节同样不含报告 ID，偏移和驱动的载荷偏移一致。
- 错误处理比驱动严。`04 18` 没有应答，一个设置字节都不发；之后某包没有应答，按驱动顺序发完收尾包再报错，避免把键盘留在半截会话里。

```swift
// 驱动的 65 字节缓冲去掉报告 ID，剩下 64 字节交给 IOKit
let payload = Array(report.dropFirst())
IOHIDDeviceSetReport(device, kIOHIDReportTypeFeature, 0, payload, payload.count)

// 应答判定，只用于 04 xx 命令包
let acked = response[0] == payload[0] && response[1] == payload[1] && response[3] == 0x01
```

## 各功能的协议

| 功能 | 命令 | 驱动函数 | 数据 | 读回 |
|---|---|---|---|---|
| 屏幕时钟 | `04 28` | `0x423ac0` | 1 包 | 无 |
| 基础灯效 | `04 13` | `0x42b400` | 1 包 | 无 |
| 单键颜色 | `04 23`，读 `04 F5` | `0x44c5f0`、`0x42b1e0` | 576 字节，9 包 | 有 |
| 系统设置 | `04 17` | `0x413ff0` | 1 包 | 无 |
| 改键 | `04 11` 普通层，`04 27` Fn 层 | `0x418590` | 576 字节，9 包 | 无 |
| 宏 | `04 19` 后接 `04 15` | `0x42db70` | N 包 | 无 |
| 屏幕 | `04 72` | `0x422b00` | 4096 字节 Output 报告 | 无 |
| 实时帧 | `04 20` | `0x428110`、`0x432e70` | 512 字节，8 包 | 不保存 |

### 基础灯效 `04 13`

数据帧前 16 字节有效。

| 偏移 | 内容 |
|---|---|
| 0 | 模式号 0~19 |
| 1~3 | R、G、B |
| 8 | 多彩 |
| 9 | 亮度 0~5 |
| 10 | 速度 0~5 |
| 11 | 方向 |
| 14~15 | `AA 55` |

模式表来自驱动首次运行时写库的默认行（`0x42b6b0`），每个模式带一个 `config_func` 位掩码，决定界面显示亮度、速度、方向、颜色中的哪些控件。程序只允许驱动界面能发出的取值组合，比如方向值只有带对应位的模式才会变。

验证时发模式 1（静态恒亮）、颜色 `00 FF 00`、亮度 5、速度 3，三个应答都通过，全键变绿，拔插和切档后保持。之后实测确认亮度 0 为熄灭，速度 0 最慢、5 最快，方向 0 和 1 互为反向。

### 单键颜色 `04 23` 和 `04 F5`

576 字节的块里，每个灯在偏移 4×灯号处占 4 字节 `[灯号, R, G, B]`，没设颜色的槽位全 0，偏移 574~575 为 `AA 55`。写入前驱动先用一次 `04 13` 把灯效切到自定义模式 `0x80`，再走 `04 18`、`04 23`（偏移 8 = 9）、9 个数据包、`04 02`、`04 F0` 的会话。

`04 F5` 读回 9 块 64 字节，格式相同。动画模式下连读三次，结果各不相同，非零区域跟着动画移动，说明它读的是键盘此刻点亮的颜色。只有在模式 `0x80` 下，读回值才等于保存的颜色。读回还会丢精度，写 `FF` 读回 `FC`，每通道约 6 位。

命令行据此实现成先切到 `0x80`，连读两次，不一致就停止，一致再把请求合并进去写入，写完再读一遍，按每通道亮或灭核对。界面上的单键编辑器则按画面所见整块写入，不和键盘里存的合并，避免写进用户看不到的颜色。

### 系统设置 `04 17`

会话是 `04 18`、`04 17`（偏移 2 = 1，偏移 8 = 1）、数据帧、`04 02`，没有 `04 F0`。

| 偏移 | 内容 |
|---|---|
| 0~1 | `00 01`，固定 |
| 2 | 禁用 Alt+Tab |
| 3 | 禁用 Alt+F4 |
| 4 | 禁用 Win 键 |
| 5 | Fn 开关 |
| 6 | 休眠，0 不休眠，1/2/3 为 1、5、30 分钟 |
| 8 | 响应时间档位 1~5 |
| 62~63 | `AA 55` |

键盘没有读回命令，每次写全部六项，不带参数时写驱动首次运行的默认值。

效果用按键监听验证。键盘背面有 Win/Mac 拨动开关，Mac 档下 Win 键和左 Alt 分别发 Option 和 Command，三个禁用开关只在 Win 档下生效。禁用 Alt+F4、Alt+Tab 都有效，写回 0 就恢复。禁用 Win 单独置 1 时 Win 键照常发出，要和偏移 3 同时为 1 才拦截，复现两次。Windows 驱动只勾这一项时发出的字节相同，所以这是固件行为，程序只给提示，不擅自改别的位。休眠、响应时间和 Fn 开关没观察到可判断的效果，仍标为未验证。

判断按键效果要以 HID 记录为准。拿 Spotlight 是否弹出判断 Win 键、拿大写灯判断 CapsLock 改键都出过错，前者受拨动开关和输入法快捷键干扰，后者是因为 macOS 会忽略短于约 0.55 秒的 CapsLock 点按。

### 改键 `04 11` 和 `04 27`

每次发送整层 576 字节，会话同单键颜色。每键 4 字节记录在偏移 4×`key_index`，本机 104 键的 `key_index` 和灯号完全相同。没有记录的键写全 0，即固件默认。

| 类型 | 记录 |
|---|---|
| 默认 | `00 00 00 00` |
| 禁用 | `05 03 00 00` |
| 按键或组合键 | `02 <修饰位> <HID 用法> 00`，Ctrl 1、Shift 2、Alt 4、Win 8 |
| 宏 | `06 <宏序号> <播放方式> <次数>` |
| 鼠标 | `01 01 <按键位> 00`，滚轮 `01 03 01 00` / `01 03 FF 00` |
| 多媒体 | `03 <Consumer 用法> 00 00` |
| 多键 | `07 <k1> <k2> <k3>` |

打开程序、网址、发送文字三类记录是 `05 02 <编号> 00`，键盘只上报编号，动作由驱动在电脑上执行，没有移植。

验证结果是 CapsLock 改成 Esc 后从发 `0x39` 变为 `0x29`，拔插后保持；Fn 层 F1 改成静音后，Fn+F1 从 Consumer `0x183` 变为 `0xE2`。两层写回全默认后都恢复。

### 宏 `04 19` 和 `04 15`

会话是 `04 19`、`04 15`（偏移 8 = 块数 N）、等 30 ms、N 个数据包、等 30 ms、`04 02`，没有 `04 18` 和 `04 F0`。缓冲 3584 字节，布局如下。

- 偏移 0~399 是 100 项索引，每项 4 字节，有内容时为数据块偏移（小端 16 位）加 `00 00`，空宏为 `FF FF FF FF`。
- 数据块从 `0x190` 开始，8 字节块头的前两字节是条目数，后接 4 字节条目。
- 按下 `00 00 <HID> B0`，松开 `00 00 <HID> 30`，鼠标按下 `00 00 <键位> 90`、松开 `10`，延时 `<毫秒低> <毫秒高> 00 50`，不足 10 ms 按 10 ms。
- 设数据结束偏移为 E，E 是 64 的倍数时 N = E/64 + 1，否则 N = E/64 + 2，偏移 N×64−2 写 `AA 55`。

播放方式和次数不在宏表里，在触发它的改键记录里。实测把宏 A 绑到 F12 播放一次、Shift+B 绑到 F11 重复三次，步间隔 20 ms，拔插后仍然有效；上传空宏表并复位改键后恢复原样。

### 屏幕 `04 72`

上传前先看固件。`0x72` 把写地址设到外置闪存 `0x950000`，之后每收到一个 4096 字节块就写一块、地址加 `0x1000`。上电播放的出厂开机动画在 `0x340000`。所以 `04 72` 写的是可上传的 GIF 页，碰不到出厂动画；USB 协议里也没有读外置闪存的命令，GIF 页原内容覆盖后无法恢复。

图像格式如下。

- 256 字节头，偏移 0 是帧数，偏移 1 起是每帧延时，单位 2 ms，其余填 `FF`。
- 每帧 240×135，逐行从上到下，RGB565 小端（取 R 高 5 位、G 高 6 位、B 高 5 位）。
- 整体用 `FF` 补齐到 4096 的倍数，帧数上限 141。同方案的 Aula F108 Pro 有人[传过 214 帧](https://github.com/parsiya/f108-pro)，141 帧以后覆盖了屏幕菜单图形且无法恢复。

发送顺序是 Feature 接口上的 `04 18`、`04 72`（偏移 2 = 槽位 1，偏移 8~9 = 块数），接着在屏幕接口上逐块写 Output 报告，每块键盘回一个 Input 报告 `01 5A 02`，最后在 Feature 接口发 `04 02`。

macOS 上 Output 报告同样用 `IOHIDDeviceSetReport`，类型换成 `kIOHIDReportTypeOutput`；应答用 `IOHIDDeviceRegisterInputReportCallback` 注册回调，写完一块后跑 RunLoop 等回复，超时 1 秒。图片用 ImageIO 解码，等比缩放到 240×135 居中，GIF 保留帧延时。

单帧测试图共 16 块，每块约 0.16 秒，方向和颜色顺序正确，拔插后开机动画照常播放。查看 GIF 页要按 Fn+PrtSc（说明书写法）。按 V1.17 固件的 Fn 表，Fn+R 在满足另外两个状态条件时也会切到这个页面，但在 V1.30 实机上按了没有反应。

### 实时帧 `04 20`

驱动的实时灯效和音乐律动都由电脑逐帧推送，键盘不保存。每帧是 `04 20`（偏移 8 = 8）、512 字节分 8 包、`04 02`，没有 `04 18` 和 `04 F0`。数据按顺序写 `[灯号, R, G, B]`，104 键用 416 字节。停止推送，键盘立即回到原来保存的灯效。

时序照 `0x44fc90`，命令包前后各等 5 ms，读回偏移 3 为 `0xFF` 表示忙，重发一次；数据包间隔 2 ms；`04 20` 没有应答就跳过这一帧。普通设置用的 35 ms 间隔在这里不可用，一帧要发 10 包，帧率会低于 3 帧每秒。按驱动时序实测约 14 帧每秒。

## 2.4G 接收器

驱动在连接模式为 2 时改走另一套短包，固件在 `0x67C8` 处理。这部分由两个子代理分别追驱动和固件，结论独立得出后逐条比对，完全一致。

接收器在 Mac 上报的是 `05AC:024F`，产品名 “2.4G Dongle”，这是苹果铝键盘的 ID，和驱动配置里的 `0C45:FDFD` 不同。它的第 3 个接口用途页 `0xFF60`、用途 `0x61`，输入输出都是 32 字节、不带报告 ID，对应驱动写的 33 字节。选接口时先按用途页和用途匹配，再在代码里核对产品名或 `VID:PID` 和报告长度。只按产品名让 `IOHIDManager` 过滤没有生效，会把同样带 32 字节厂商接口的罗技接收器也匹配进来，第一次试验就因为出现两个候选接口而在发包前停下。

包格式是 `p0` 命令、`p1` 长度、`p2` 序号、`p3~p30` 数据（28 字节）、`p31` 为前面各字节之和。键盘回显 `p0~p2` 表示收到。没有 `04 18` / `04 02` 会话，每条命令立即生效。

| 功能 | 报文 |
|---|---|
| 在线探测 | `02 00`，应答带固件信息块 |
| 电量 | `20 01`，应答字节 3 为百分比 |
| 时钟 | `0C 10 00` + 有线时钟数据帧，整体后移 3 字节 |
| 基础灯效 | `05 10 00` + 有线 `04 13` 数据帧前 14 字节 + `AA 55` |
| 系统设置 | `07 10 00` + 有线 `04 17` 数据帧前 14 字节 + `AA 55` |
| 改键 | 21 包 `10`（普通层）或 `12`（Fn 层），每包 28 字节，内容同有线 576 字节块 |
| 宏 | 若干包 `09`，内容同有线宏表 |
| 单键颜色 | 先 `05 10 00 80 …` 切模式，再 21 包 `14` |
| 音乐律动 | 每帧一包 `0B`，带前景、背景色号和柱高，驱动发 21 列，固件只读前 18 列 |
| 屏幕 | `7F 03 00 <槽位> <包数 24 位>`，之后每包命令字 `0x80 + (i >> 16)`，长度和序号字段放 i 的低两字节 |

设置类数据和有线完全同源，只是切成 28 字节一包，所以有线和 2.4G 共用同一套编码器。

风险集中在固件的写入方式。单包设置命令每条都擦写一个 1 KB 闪存页，不适合跟着滑块连续发。多包流式写入按基址加 28×序号写，到页首就擦，序号没有上限检查。宏表写到第 110 包会进入 Fn 层改键所在的 `0xAC00`，而驱动自己的宏长度上限比这个还宽。程序在发送前拒绝超过 3,024 字节的宏表，这个边界写进了测试。

发送器的规则是先发 `02 00` 探测，键盘不应答就一包不发；之后每包等回显，超时 1 秒，没有回显立即停止，不重发，以免同一数据写两次闪存。屏幕数据是唯一的例外。它写在外置闪存，同一包重发只会把同样的字节写到同一地址，跨扇区的包会先重新擦除，结果一致，所以允许最多重发 3 次。第一次 2.4G 上传在 4,639 包中的第 497 包丢了回显，按规则停止；放开重发后，单帧图片 2,325 包、62 秒传完，重发 0 次。

2.4G 下逐项实测了时钟、电量、灯效、系统设置、改键、宏、单键颜色、音乐柱状图和屏幕。键盘在 2.4G 下没有逐键任意颜色的实时帧，也不能读回颜色，这两项只能用数据线。

电量只有 2.4G 能读。驱动的电量函数在非 2.4G 模式下直接返回；固件的电量来自 I2C 电量计，USB 命令处理代码里没有读取它的地方。版本号也从这里拿，探测应答里的信息块按社区项目 [dantje/ajazz-ak35i-v3-max](https://github.com/dantje/ajazz-ak35i-v3-max) 的格式解析为 `0C45:8009`、V1.30；有线下则直接读 USB 设备描述符里的 bcdDevice，同为 `0x0130`。

## 蓝牙

蓝牙下键盘是 `0C45:FEFE`，HID 报告描述符里没有厂商命令通道，固件在蓝牙模式下也直接丢弃厂商帧，所以设置功能无法经蓝牙使用。程序只用 CoreBluetooth 读标准电池服务（`180F` / `2A19`）。判断键盘是否在蓝牙档，是在 IORegistry 里查 `0C45:FEFE` 的 HID 服务，不打开设备，因此不需要输入监控权限。

GATT 里还有一个厂商服务 `FEE0`，特征 `FEE1` 可读可写。经过确认，往 `FEE1` 写过一次 2.4G 探测包，写入被接受但没有任何回应。事后查明 `FEE0` / `FEE1` 是沁恒 [CH583 系列 BLE OTA 例程](https://github.com/openwch/ch583)的服务和特征，同方案键盘的无线芯片是 CH582F，例程里 `0x81` 是擦除命令。`FEE1` 是蓝牙模块的固件升级通道，现在明确禁止写入。这是整个项目里唯一一次发出驱动中没有的数据，正确的顺序应该是先查资料再试。

## 移植驱动的算法

### 实时灯效

驱动的 8 种实时灯效在 `mui.dll` 的 `CustomLightMode` 里计算。移植方式是逐指令还原更新函数，查找表不手抄。17 张表共 1,730 字节，由脚本从 `mui.dll` 按地址逐字节导出到 `DriverLightTables.swift`，每张表注明原地址。

- 计时以 16 ms 为一拍，速度 s 时每 max(15−s, 1) 拍走一步。
- 每一步按驱动顺序调用全部效果的更新函数，与当前用哪个效果无关，各效果的内部状态都在推进。
- 每键输出颜色为 (颜色 × 亮度) >> 8。

驱动的随机取色每次都先 `srand(0xE8)` 再 `rand()`，MSVC 下恒为 796，实际取色序号只由一个 1~100 循环的计数器决定。序号能取到 106，而漫天飞舞的配色表只有 106 项，越界读到对齐填充和下一张表的首字节，结果是黑色。这些行为都按原样保留，测试用例直接从反汇编推出期望值。

### 音乐律动

驱动的音频管线是 WASAPI 环回采集，256 格环形缓冲，前半加 Hann 窗，实数 FFT，按频率加权，取 v = min(1, 0.5·log10(1.1m) + 0.9m)，再做 17 点平滑，得到 256 个电平。键（行, 列）的采样点在 x = 11·列、y = 10·行 + 7，电平高于 (64 − y)/64 时显示前景色，否则显示背景色。11 种显示模式的配色和整数 HSV 算法也按地址读出。

Mac 上用 Core Audio 进程 tap 采集系统输出，`CATapDescription` 建全局立体声 tap，挂到一个私有聚合设备上读取，要求 macOS 14.2 以上并授予系统录音权限。没有用麦克风，它采到的是房间里的声音。

与驱动的差别有三处。输入是混成单声道的最近 512 帧；静音时柱子会落下，驱动是冻结；FFT 按标准实数布局计算，没有逐指令复刻驱动的浮点舍入。驱动默认振幅下样本被乘以 600，FFT 又不归一化，普通音量就会让全部频段饱和，这一点也照原样保留，靠振幅滑块调低。

2.4G 下改发柱高，同一套引擎算出 18 列高度放进 `0B` 包，由固件按调色板绘制。

## 工程结构

- Swift 6 + SwiftPM，单个可执行目标。同一个二进制从 Finder 启动时打开 SwiftUI 界面，带参数运行时是命令行工具。
- 和设备、媒体打交道的部分用 IOKit HID、CoreBluetooth、Core Audio 和 ImageIO，整个项目没有第三方依赖。
- 协议编码器都是纯函数，输入设置、输出字节。传输层藏在 `FeatureReportDevice`、`WirelessExchanging` 这样的协议后面，等待时间也可注入，测试不需要真机。
- 改协议编码器前先写失败的测试。测试断言的字节来自反汇编，或来自实机验证过的序列，比如把全键变绿的那组灯效报文原样写成了测试。目前共 122 个测试。
- 发布包是 Intel 和 Apple Silicon 通用的 DMG，支持 macOS 13 以上，ad-hoc 签名，没有公证。

## 还没解决的问题

- 禁用 Win 键为什么要同时置 Alt+F4 位才生效。
- Fn+PrtSc 在固件里的出处，以及 Fn+R 在 V1.30 上为什么无效。
- 多帧 GIF 和帧延时没有上过真机。
- 休眠、响应时间、Fn 开关的效果，以及多彩纵横方向 2、3 的含义。
- 换成驱动原版音乐引擎之后的 2.4G 柱状图还没实测。
- 驱动的音乐律动在 Windows 上是否同样饱和，目前只是推断，没有 Windows 机器对照。

---

项目地址 [Ike-li/ak35i-v3-max-macos-control-center](https://github.com/Ike-li/ak35i-v3-max-macos-control-center)。完整的逆向记录在 `docs/STATIC_REVERSING.md`，安全边界在 `docs/SAFETY_MODEL.md`。这是独立的社区项目，与 AJAZZ 没有隶属、授权或合作关系。
