bt-keyboard-switcher

我现在为了方便移动工作,又重新使用我十六年前购买的 MacBook Air 11" Late 2010 ,原因无他,最近几年消费电子产品价格暴涨加上自己失业,没一分钱都要考虑花费是否值得。

另外,我也在思考如何充分发挥我购买到的众多电子产品的价值,通过软件结合巧妙的使用方式来完成更多更强的任务。

但是十六年前的 MacBook Air 11" Late 2010 再怎么设计精良制造完美,毕竟无法承受现代复杂的WEB页面和视频多媒体,所以我决定扬长避短,结合我已经拥有的 iPhone 12 miniiPad mini 5 :

  • MacBook Air 11" Late 2010 安装 Alpine Linux (未来自己编译 Gentoo Linux ),以轻量级终端操作为主,主攻开发任务

  • iPhone 12 miniiPad mini 5 负责现代化的WEB访问和大量的桌面应用,因为大多数常用软件都有iOS版本,完全可以享受到最新的应用

  • 通过Linux模拟蓝牙键盘和鼠标来实现对iOS设备的输入,充分发挥键盘的输入优势同时能够使用最新的软件

Linux主机转换成蓝牙键盘鼠标hdiclient 由于以来BlueZ 4时代的过时API,导致在现代Linux上已经无法编译。那么在Alpine Linux (OpenRC + Wayland/Sway) 环境下,目前最完美的替代方案是使用基于 Python 3 + BlueZ 5 (DBus) + evdev 的开源项目: Github: bt-keyboard-switcher :

  • 绕过 Wayland 安全限制:在 Sway (Wayland) 下,普通的截屏、按键监听软件(如 X11 下的 xdotool)会因为安全协议被彻底禁用。而这个方案使用 Python 的 evdev 库,直接读取内核级的 /dev/input/event* 原始设备节点,完全不依赖图形显示服务(无论在 TTY 还是 Sway 下都能完美运行)

  • 原生支持 iOS/iPadOS:该项目专门针对 iOS 的蓝牙 HID 协议进行了适配,不仅支持键盘,还支持鼠标/触控板的同步模拟

  • 支持多设备热切换:可以通过快捷键(如 Ctrl + F1 切换到 iPad,Ctrl + F2 切换到 iPhone,Ctrl + F12 切回本地)

安装

  • 安装必要的系统组件、Python 环境及蓝牙工具

安装
sudo apk add bluez bluez-deprecated python3 py3-dbus py3-evdev py3-udev git
  • 加载 uhid 内核模块(蓝牙 HID 模拟必须)

加载uhid模块
sudo modprobe uhid
  • 为了确保每次开机自动加载,将其写入配置

配置自动加载
echo "uhid" | sudo tee -a /etc/modules-load.d/uhid.conf

配置

BlueZ 默认的 input 插件会尝试作为一个“输入接收端”(去连接外部键盘),这会与“模拟发送端”发生冲突,所以必须在启东市禁用 input

  • (放弃这步)修改 /etc/bluetooth/main.conf :

/etc/bluetooth/main.conf
[General]
# 禁用默认输入插件
Disable = input
# 修改蓝牙设备类别,使其伪装成键盘鼠标复合设备(Class = 0x002540)
Class = 0x002540

警告

现在修改 /etc/bluetooth/main.conf 配置不生效

注意,这里一定要想办法 disable 掉 input ,并且要使得 bluetoothctl 中执行 show 的时候,看到 Class = 0x002540 而不是默认 0x00010c (Computer类型会导致iOS扫描自称为Computer类型的设备时自动忽略)

由于我尝试编辑配置文件没有生效,所以按照gemini提示,改成命令行运行:

在命令行disable掉input,hostname
sudo rc-service bluetooth stop
sudo /usr/lib/bluetooth/bluetoothd -n -d --noplugin=input,hostname

为什么要 --noplugininputhostname 呢?

原因是,如果不禁止 input ,那么在执行下文的 keyboardswitcher.py 会出现报错显示设备已经注册:

如果不禁止 input 就会报错
/home/admin/docs/github/nutki/bt-keyboard-switcher/keyboardswitcher.py:189: SyntaxWarning: invalid escape sequence '\ '
os.system("hciconfig hci0 name Pi\ Keyboard/Mouse")
Traceback (most recent call last):
File "/home/admin/docs/github/nutki/bt-keyboard-switcher/keyboardswitcher.py", line 439, in <module>
bt = BluetoothDeviceManager()
^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/admin/docs/github/nutki/bt-keyboard-switcher/keyboardswitcher.py", line 198, in init
self.manager.RegisterProfile("/org/bluez/hci0", "00001124-0000-1000-8000-00805f9b34fb", opts)
File "/usr/lib/python3.12/site-packages/dbus/proxies.py", line 72, in call
return self._proxy_method(*args, **keywords)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/lib/python3.12/site-packages/dbus/proxies.py", line 141, in call
return self._connection.call_blocking(self._named_service,
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/lib/python3.12/site-packages/dbus/connection.py", line 696, in call_blocking
reply_message = self.send_message_with_reply_and_block(
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
dbus.exceptions.DBusException: org.bluez.Error.NotPermitted: UUID already registered

如果只禁止 input 但没有禁止 hostname 那么iPad或iPhone在执行谰言配对扫描时就因为bluetootd默认读取Linux主机名而申明自己是 0x0000010c ,也就是Computer类型,这会导致iPad/iPhone静默忽略掉申明计算机类型的蓝牙设备配对,也就是完全看不到Linux模拟蓝牙键盘设备。这里可以通过 bluetoothctl 命令中的 show 指令看到如下状态:

当没有禁止hostname时候 bluetoothctl show 看到的是 0x0000010c 类型
[bluetoothctl]> show
Controller 44:2A:60:F4:54:F5 (public)
	Manufacturer: 0x000f (15)
	Version: 0x04 (4)
	Name: BlueZ 5.82
	Alias: Pi Keyboard/Mouse
	Class: 0x0000010c (268)
	Powered: yes
	PowerState: on
	Discoverable: no
	DiscoverableTimeout: 0x000000b4 (180)
	Pairable: yes
	UUID: Human Interface Device... (00001124-0000-1000-8000-00805f9b34fb)
	UUID: PnP Information           (00001200-0000-1000-8000-00805f9b34fb)
	UUID: A/V Remote Control Target (0000110c-0000-1000-8000-00805f9b34fb)
	UUID: A/V Remote Control        (0000110e-0000-1000-8000-00805f9b34fb)
	Modalias: usb:v1D6Bp0246d0552
	Discovering: no
  • 下载 bt-keyboard-switcher :

下载
cd ~
git clone https://github.com/nutki/bt-keyboard-switcher.git
cd bt-keyboard-switcher
  • 获取本地输入设备路径:

运行以下命令,找出物理键盘和触控板的 /dev/input/eventX 编号(我这里是 MacBook Air 11" Late 2010 )

获取本地输入设备路径
cat /proc/bus/input/devices

不过,这个寻找event方法有一个缺陷就是每次启动系统内核加载驱动的顺序可能发生变化。今天键盘是 event1,明天重启后可能就变成了 event2,这会导致你的配置文件失效。

所以改进为使用 Linux udev 服务自动创建的持久化设备软链接

执行:

检查 /dev/input/by-id/
ls -l /dev/input/by-id/

输出显示中:

  • 含有 -event-kbd 结尾的文件就是键盘

  • 含有 -event-mouse-mouse 结尾的文件就是触控板/鼠标

检查 /dev/input/by-id/
total 0
lrwxrwxrwx    1 root     root             9 Jul  2 21:59 usb-Apple_Inc._Apple_Internal_Keyboard___Trackpad-event-kbd -> ../event1
lrwxrwxrwx    1 root     root             9 Jul  2 21:59 usb-Apple_Inc._Apple_Internal_Keyboard___Trackpad-if02-event-mouse -> ../event9
lrwxrwxrwx    1 root     root             9 Jul  2 21:59 usb-Apple_Inc._Apple_Internal_Keyboard___Trackpad-if02-mouse -> ../mouse1

在我的实践中,上述 event1event9 代表了当前的键盘和touchpad

  • 配置 bt-key-board-switcher 目录下的 config.ini ,我的配置如下

配置 config.ini 填写输入设备的id
[sources]
# 1. 填入你通过 ls -l /dev/input/by-id/ 查看到的键盘路径
keyboard = /dev/input/by-id/usb-Apple_Inc._Apple_Internal_Keyboard___Trackpad-event-kbd

# 2. 填入你查看到的触控板路径
mouse = /dev/input/by-id/usb-Apple_Inc._Apple_Internal_Keyboard___Trackpad-if02-event-mouse
  • 运行 keyboardswitcher.py :

运行 keyboardswitcher.py
sudo python3 keyboardswitcher.py
  • 执行 hciconfig 强行向蓝牙天线写入参数:

写入配置
# 强行将物理芯片的 Class 修改为“键盘/鼠标复合外设
sudo hciconfig hci0 class 0x002540

# 强行开启物理芯片的广播与发现模式(piscan 代表 Page Scan + Inquiry Scan,即处于可发现、可连接状态)
sudo hciconfig hci0 piscan

修改完成后,执行检查:

检查配置
hciconfig hci0 class

此时应该看到 明确显示为 Class: 0x002540

检查配置显示 Class: 0x002540
hci0:	Type: Primary  Bus: USB
	BD Address: 44:2A:60:F4:54:F5  ACL MTU: 1021:8  SCO MTU: 64:1
	Class: 0x002540
	Service Classes: Unspecified
	Device Class: Peripheral, Keyboard
  • 输入以下命令使得自己的电脑蓝牙设备处于可发现状态:

运行 bluetoothctl
$ bluetoothctl 
hci0 new_settings: powered bondable ssp br/edr 
Agent registered
[CHG] Controller 44:2A:60:F4:54:F5 Pairable: yes
[bluetoothctl]> power on
Changing power on succeeded
[bluetoothctl]> discoverable on
hci0 new_settings: powered connectable bondable ssp br/edr 
[CHG] Controller 44:2A:60:F4:54:F5 Connectable: yes
Changing discoverable on succeeded
hci0 new_settings: powered connectable discoverable bondable ssp br/edr 
[CHG] Controller 44:2A:60:F4:54:F5 Discoverable: yes
[bluetoothctl]> pairable on
Changing pairable on succeeded
  • 现在在iPad/iPhone上扫描配对设备,就会看到有一个 Pi Keyboard/Mouse 可以配对,请在iOS设备端选择配对

  • 此时在 bluetoothctl 交互终端中有提示iOS设备配对信息,请确认输入 yes :

配置设置
[NEW] Device 34:A8:EB:18:06:A0 Huatai’s iPad mini 5
Request confirmation
[agent] Confirm passkey 785539 (yes/no): yes
[CHG] Device 34:A8:EB:18:06:A0 Bonded: yes
[CHG] Device 34:A8:EB:18:06:A0 UUIDs: ...
...
[CHG] Device 34:A8:EB:18:06:A0 ServicesResolved: yes
[CHG] Device 34:A8:EB:18:06:A0 Paired: yes
Authorize service
[agent] Authorize service 0000110e-0000-... (yes/no): yes
...

完成配对

现在就可以开始在iPad上输入 ,使用 ctrl+F12 切换回本机,输入 ctrl+F1 切换到iPad输入,非常流畅。

配置和脚本

上述通过命令行完成了蓝牙配对和连接,显然每次都命令行切换蓝牙类型并连接是非常麻烦的。所以执行以下配方法:

  • 永久配置 OpenRC 蓝牙后台,确保在 Alpine 系统后台启动蓝牙时,默认就彻底禁用 input 和 hostname 插件: 编辑 /etc/conf.d/bluetooth

/etc/conf.d/bluetooth 关闭input,hostname插件
# 确保 OpenRC 启动蓝牙时永远不加载这两个插件
BLUETOOTHD_OPTS="--noplugin=input,hostname"
  • 重启蓝牙服务

重启bluetooth
sudo rc-service bluetooth restart

此时检查 ps aux | grep bluetoothd 应该看到运行参数带上了 --noplugin=input,hostname

警告

我在实践中遇到了配置 /etc/conf.d/bluetooth 无效的问题,所以最终实际上改用脚本方式来修改bluetoothd的运行参数,见下文。

  • 配置 share-hid.sh 脚本

(如果能够调整bluetoothd参数) share-hid.sh 脚本一键配置蓝牙
#!/bin/sh
# =====================================================================
# MacBook Air 2010 -> iPad/iPhone 蓝牙键盘鼠标共享一键启动脚本
# =====================================================================

# 1. 确保脚本是以 root (sudo) 权限运行
if [ "$(id -u)" -ne 0 ]; then
  echo "[-] 错误: 此脚本需要 root 权限,请使用 'sudo $0' 运行。"
  exit 1
fi

# 2. 确保后台蓝牙服务正在运行 (由于我们在 conf.d 配置了参数,这里可以直接启动)
echo "[*] 正在确保 OpenRC 蓝牙服务处于运行状态..."
rc-service bluetooth start >/dev/null 2>&1
sleep 1.5 # 给芯片初始化留出 1.5 秒时间

# 3. 强刷蓝牙芯片物理寄存器为 外设/键鼠 复合类型 (Class: 0x002540)
echo "[*] 正在物理写入蓝牙芯片 Class (0x002540)..."
hciconfig hci0 class 0x002540

# 4. 强制开启物理广播发现模式 (piscan)
echo "[*] 正在物理开启蓝牙广播 (piscan)..."
hciconfig hci0 piscan

# 5. 进入项目目录并启动 Python 模拟器
SCRIPT_DIR="/home/admin/docs/github/nutki/bt-keyboard-switcher"
if [ -d "$SCRIPT_DIR" ]; then
  echo "[+] 状态: 硬件已就绪!正在拉起 Python 键盘切换器服务..."
  echo "[+] 提示: 终端保持开启即可。按 Ctrl+C 可以退出服务并断开连接。"
  cd "$SCRIPT_DIR"
  python3 keyboardswitcher.py
else
  echo "[-] 错误: 未找到项目目录 $SCRIPT_DIR,请检查路径是否正确。"
  exit 1
fi

由于我的实践发现没有正确调整bluetoothd运行参数,所以我改为许下脚本(也就是脚本来调整bluetothd参数):

share-hid.sh 脚本一键配置蓝牙(脚本包含调整bluetoolthd运行参数)
#!/bin/sh
# =====================================================================
# MacBook Air 2010 -> iPad/iPhone 蓝牙键盘鼠标共享一键启动脚本 (自守恒版)
# =====================================================================

# 1. 确保脚本是以 root (sudo) 权限运行
if [ "$(id -u)" -ne 0 ]; then
  echo "[-] 错误: 此脚本需要 root 权限,请使用 'sudo $0' 运行。"
  exit 1
fi

# 2. 定义清理和恢复函数 (当用户按 Ctrl+C 或脚本退出时自动执行)
cleanup() {
  echo ""
  echo "[*] 正在恢复系统环境,请稍候..."

  # 杀死我们手动启动的定制蓝牙进程
  if [ -n "$BT_PID" ]; then
    echo "[*] 正在关闭定制的蓝牙进程 (PID: $BT_PID)..."
    kill "$BT_PID" 2>/dev/null
    wait "$BT_PID" 2>/dev/null
  fi

  # 重新拉起系统默认的蓝牙后台服务 (此时 input 插件重新加载,不影响正常设备使用)
  echo "[*] 正在恢复默认的系统蓝牙服务..."
  rc-service bluetooth start >/dev/null 2>&1

  echo "[+] 恢复完毕,系统已回到默认状态。再见!"
  exit 0
}

# 绑定信号捕获 (无论是 Ctrl+C、终端关闭还是脚本异常终止,都会触发 cleanup)
trap cleanup INT TERM EXIT

# 3. 暂停系统的默认蓝牙后台服务
echo "[*] 正在暂时停止系统的默认蓝牙后台服务..."
rc-service bluetooth stop >/dev/null 2>&1
sleep 0.5

# 4. 在后台手动拉起定制参数的蓝牙服务
echo "[*] 正在后台启动专用蓝牙实例 (强制禁用 input,hostname)..."
/usr/lib/bluetooth/bluetoothd --noplugin=input,hostname &
BT_PID=$!

# 等待蓝牙芯片完全初始化
sleep 1.5

# 5. 强刷蓝牙芯片物理寄存器为 外设/键鼠 复合类型 (Class: 0x002540)
echo "[*] 正在物理写入蓝牙芯片 Class (0x002540)..."
hciconfig hci0 class 0x002540

# 6. 强制开启物理广播发现模式 (piscan)
echo "[*] 正在物理开启蓝牙广播 (piscan)..."
hciconfig hci0 piscan

# 7. 进入项目目录并启动 Python 模拟器
SCRIPT_DIR="/home/admin/docs/github/nutki/bt-keyboard-switcher"
if [ -d "$SCRIPT_DIR" ]; then
  echo "[+] 状态: 硬件已就绪!正在启动键盘切换器..."
  echo "[+] 提示: 终端保持开启即可。按 Ctrl+C 可以退出并自动恢复默认蓝牙。"
  cd "$SCRIPT_DIR"
  python3 keyboardswitcher.py
else
  echo "[-] 错误: 未找到项目目录 $SCRIPT_DIR"
  exit 1
fi
sway配置
# 启动快捷键:按下 $mod+Ctrl+b 启动脚本
# 注意sudo要配置NOPASSWD执行,这样才能在后台完成执行并隐藏
bindsym $mod+Ctrl+b exec foot --title="Bluetooth-HID-Share" -e sudo /home/admin/bin/share-hid.sh
# 呼出/隐藏快捷键:按下 $mod + Shift + B 切换显示该窗口
bindsym $mod+Shift+b [title="Bluetooth-HID-Share"] scratchpad show

# 设置该标题的窗口在 Sway 中默认以浮动模式打开,避免打乱你的平铺布局
#    - 开启浮动 (floating enable)
#    - 设置大小 (resize set ...)
#    - 居中显示 (move position center)
#    - 立即收入便签夹隐藏 (move scratchpad)
for_window [title="Bluetooth-HID-Share"] floating enable, resize set 600 px 400 px, move position center, move scratchpad

现在按下 Mod+Ctrl+B 就开启蓝牙连接,按下 Mod+Shift+B 就把隐藏的窗口掉到前台,此时按下 Ctrl+C 就能终止程序。

配对第2个设备

bt-keyboard-switcher 能够配对多个设备,以下是配对第2个设备方法:

  • 继续保持上述运行状态(即 Command+Control+B 已经在后台运行了程序)

  • 在Linux主机键盘上按下内置的 开启配对广播 快捷键 Left Ctrl + Esc (左 Ctrl 键 + Esc 键),脚本在获到此组合键后,会自动向芯片发送指令,使主机再次处于可发现的配对状态

需要注意要在一个终端中执行 bluetoochctl 交互命令,这样才能看到iOS配对数字字符串并进行确认!

完成配对以后,就可以通过 Left Ctrl + F1 连接第一个设备, Left Ctrl + F2 连接第二个设备...最后通过 Left Ctrl + F12 返回本地输入

备注

完成多个设备配对以后,在 bt-keyboard-switcher 目录下有一个名为 keyboardswitcher.ini 的配置文件,包含了连接配对的设备的蓝牙地址,也就是可以微调绑定的F1,F2等快捷键顺序。