最近,为了开发和调试 OpenTrace 的 macOS 版本,我从早苗那获得了一台 iMac (24-inch, M1, 2021) 。在用作开发的同时,也顺便拿来替换了办公室的办公电脑。但是由于 iMac 虽然有摄像头却并没有集成 Face ID,每次我解锁的时候都需要手动输入巨长的混合密码。
解决这个问题的方法有很多,比如 Near Lock (看起来已经死了而且 BLE 很不稳定的样子),比如买一个 Apple Watch(主要是不喜欢任何可穿戴科技带给我的异样感),再比如买一块带 Touch ID 的妙控键盘(甚至你还可以多此一举地把它拆出主板和指纹识别,然后放到自己3D打印的盒子里)。
但是我完全不想多花钱,所以脑筋一转,就想到了用前年去剑桥游玩的时候在树莓派官方直营店购买的这一块 Raspberry Pi Zero W,甚至还是 MicroUSB 接口(好像 Zero 2代也还是这个接口)。当时买这个单纯是 Arc 带着我去逛了商店,本着 来都来了不买点什么当纪念品吗 的想法才买的,终于在今天派上了用场。
那么思路其实很简单,就是把这块小板子接到 iMac 上,然后模拟成 HID 设备。再通过同一个 WiFi 局域网暴露一个 HTTP 接口用于远程键入内容。
说干就干,找来了一个 64G TF 卡,刷上 RPi OS Lite,然后参考这篇文章配置它成为 HID 设备,准备工作就完成了。接下来让 Gemini 帮忙写一个脚本(还有对应的 systemd unit file):
import time
from flask import Flask, request
app = Flask(__name__)
# --- HID 映射表 ---
# 格式: '字符': (Modifier, KeyCode)
HID_MAP = {
'\n': (0, 40), '\r': (0, 40), # 回车
' ': (0, 44), # 空格
'\t': (0, 43), # Tab
}
# 填充 a-z (4-29)
for i, c in enumerate("abcdefghijklmnopqrstuvwxyz"):
HID_MAP[c] = (0, 4 + i)
# 填充 A-Z (需 Shift)
for i, c in enumerate("ABCDEFGHIJKLMNOPQRSTUVWXYZ"):
HID_MAP[c] = (2, 4 + i)
# 填充 1-9, 0 (30-39)
for i, c in enumerate("123456789"):
HID_MAP[c] = (0, 30 + i)
HID_MAP['0'] = (0, 39)
# 常见符号映射
SYMBOLS = {
'!': (2, 30), '@': (2, 31), '#': (2, 32), '$': (2, 33), '%': (2, 34),
'^': (2, 35), '&': (2, 36), '*': (2, 37), '(': (2, 38), ')': (2, 39),
'-': (0, 45), '_': (2, 45), '=': (0, 46), '+': (2, 46),
'[': (0, 47), '{': (2, 47), ']': (0, 48), '}': (2, 48),
'\\': (0, 49), '|': (2, 49), ';': (0, 51), ':': (2, 51),
"'": (0, 52), '"': (2, 52), ',': (0, 54), '<': (2, 54),
'.': (0, 55), '>': (2, 55), '/': (0, 56), '?': (2, 56)
}
HID_MAP.update(SYMBOLS)
def write_report(modifier, code):
"""写入 HID 报告"""
try:
with open('/dev/hidg0', 'wb') as fd:
# 按下: [Modifier, Reserved, KeyCode, 0, 0, 0, 0, 0]
fd.write(bytearray([modifier, 0, code, 0, 0, 0, 0, 0]))
# 必须松开: 全 0
fd.write(bytearray([0] * 8))
except Exception as e:
print(f"HID Write Error: {e}")
raise e
def wake_up_host(duration):
print(f"Waking up host for {duration} seconds...")
try:
with open('/dev/hidg0', 'wb') as fd:
fd.write(bytearray([0, 0, 71, 0, 0, 0, 0, 0]))
time.sleep(0.5)
with open('/dev/hidg0', 'wb') as fd:
# 松开
fd.write(bytearray([0] * 8))
# 关键: 松开后额外等待 duration 秒,给屏幕亮起和输入框聚焦的时间
time.sleep(duration)
except Exception as e:
print(f"Wake Up Error: {e}")
def type_string(text, delay):
"""逐字输入"""
for char in text:
if char in HID_MAP:
mod, code = HID_MAP[char]
write_report(mod, code)
time.sleep(delay) # 使用自定义延迟
else:
print(f"Ignored unknown char: {char}")
# 辅助函数: 解析布尔值
def str_to_bool(val):
return str(val).lower() in ['true', '1', 'yes', 'on']
@app.route('/type', methods=['POST'])
def handle_type():
# 1. 获取参数
text = request.form.get('text', '')
if not text:
return "Error: Empty text", 400
# 解析 delay (默认 0.01秒)
try:
delay = float(request.form.get('delay', 0.01))
except:
delay = 0.03
# 解析 enter (默认 True)
need_enter = str_to_bool(request.form.get('enter', 'true'))
# 解析 wake_time (默认 0,即不唤醒)
try:
wake_time = float(request.form.get('wake_time', 0))
except:
wake_time = 0
print(f"Request: text='***', delay={delay}, enter={need_enter}, wake={wake_time}")
try:
# 2. 执行唤醒 (如果有要求)
if wake_time > 0:
wake_up_host(wake_time)
# 3. 输入文本
type_string(text, delay)
# 4. 最后的确认键
if need_enter:
write_report(0, 40) # Enter
return "Typed successfully", 200
except Exception as e:
return f"Error: {str(e)}", 500
if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000)
接下来打开你的 iPhone 设置一个快捷指令,如下:
根据是否在同一个 WiFi 下面可以顺带实现位置判断,开头放一个获取健康数据可以触发 FaceID 认证(解锁状态下无需),最后配合自动化或者 Action Button 进行触发即可。我的配置是直接把它绑定到了 Action Button 上,只要捏一下就可以自动输入密码解锁啦。
最后就是,整套方案的安全性也是可以(应当)再加强的,比如输入接口增加鉴权和请求白名单等等。不过鉴于我办公室是自己放了一个路由器只有自己用,和办公室的大内网是有隔离的,环境相对可控,我就懒了。文章权当抛砖引玉,启发读者动手利用一下闲置的“纪念品”倒也不错!