爱意满满的作品展示区。
awenforlinux

我用 AI 开发了一款三体世界中的摇篮系统

  •  
  •   awenforlinux · 7h 45m ago · 235 views

    《三体》中,面壁者雷迪亚兹为了实施水星核弹威慑计划,佩戴了一块特殊的黑色腕表。这块表实时监测他的心跳、血压、体温等生命体征,并持续向外发送抑制信号。只要雷迪亚兹活着,信号就不停;一旦他死亡或失去生命体征,信号中断,水星的氢弹就会立即引爆,导致水星坠入太阳,最终毁灭整个太阳系。

    三体里的"摇篮系统":生命体征即威慑

    《三体》中,面壁者雷迪亚兹为了实施水星核弹威慑计划,佩戴了一块特殊的黑色腕表。这块表实时监测他的心跳、血压、体温等生命体征,并持续向外发送抑制信号。只要雷迪亚兹活着,信号就不停;一旦他死亡或失去生命体征,信号中断,水星的氢弹就会立即引爆,导致水星坠入太阳,最终毁灭整个太阳系。

    clip_1785158399671_ua8nyj.png

    这就是"摇篮系统"名字的由来:

    • 手表持续发信号 = 摇篮不停摇晃 → 氢弹休眠、锁死;
    • 雷迪亚兹死亡 → 信号中断 = 摇篮停摇 → 氢弹瞬间全部引爆。

    听证会上,破壁人揭穿了雷迪亚兹的计划,联合国本要审判他。雷迪亚兹举起左手展示这块摇篮手表,威胁只要自己被杀,水星核弹就会毁灭全人类。各国代表在恐惧中只能放他返回委内瑞拉。

    我手里正好有一块支持心率广播的智能手表,每天戴着它跑步和监测睡眠。既然它已经 24 小时戴在手上,能不能让它充当"人体存在传感器"?答案是可以。

    这让我立刻想到《三体》里的"摇篮系统"。

    后来我做的这个"不跳就锁",其实也是一个微缩版的摇篮系统:Mac 持续监听我的心率广播,心跳在,屏幕就亮着;心跳中断,屏幕立刻锁定。只不过它威慑的不是三体人,而是我自己的安全意识。

    这个项目我已经开源在 GitHub:**monkey-wenjun/HeartRateLockWidget**,有兴趣可以直接下载试用或提 PR 。

    背景:人走必须锁屏,但我总会忘

    当然更重要的是🐶公司办公区有比较严格的安全规范:离开工位必须锁屏。道理都懂,可实际执行起来总是磕磕绊绊——去洗手间、接水、开会,经常起身就走了,屏幕还亮着。 clip_1784758448046_n4br1c.png 理想中的人走锁屏应该满足几个条件:

    • 无感:不需要我手动操作,也不需要我佩戴额外硬件。
    • 及时:人离开工位后几秒内就要锁屏。
    • 不误判:坐在电脑前正常工作时不应该突然锁屏。
    • 隐私:不要把我的生物特征上传到云端。

    为什么不用 BLEUnlock ,还要自己造轮子?

    在做这个应用之前,我已经调研过开源方案,比如 BLEUnlock。它的思路很简单:让 Mac 持续扫描你随身 BLE 设备的信号强度( RSSI ),信号弱到一定程度就认为人离开了,然后锁屏。

    这个方案听起来很美好,但有一个本质问题:**它只判断"设备在不在附近",不判断"人还在不在"**。实际使用中会碰到很多误触发场景:

    • 手机或手表放在桌上充电,人走开几步去倒水,RSSI 波动一下就被判定为"离开";
    • 设备在口袋里,身体遮挡导致 RSSI 短暂下降,屏幕突然锁定;
    • 开放式办公区蓝牙设备很多,信号干扰造成误判。

    换句话说,BLEUnlock 是靠"蓝牙信号"猜人在不在,而我的手表能广播心率数据。心率是活的、持续变化的生理信号——只有戴在活人手腕上,才会每秒产生一次真实的心跳数值。只要心率广播在,就说明佩戴者大概率还在;心率中断,才意味着人真的走远或摘下手表。

    所以我的方案核心是:用心率这种"生命体征"来做存在判定,而不是单纯依赖蓝牙信号强度。RSSI 只作为辅助参考,真正触发锁屏的是"超过一定时间没有收到心率"。这样误触发率会低很多。

    核心思路:用心率广播 + 信号强度判断"人还在不在"

    BLE ( Bluetooth Low Energy )心率设备会广播标准心率服务 0x180D,其中的心率测量特征 0x2A37 每秒推送一次当前心率。只要手表在 Mac 的蓝牙覆盖范围内,Mac 就能收到稳定的心率数据。

    当我的身体离开 Mac 一段距离后:

    1. 心率广播中断,Mac 超过一定时间收不到心率。
    2. 即使还在广播,信号强度 RSSI 也会显著下降。

    于是判定逻辑很简单:

    • 心率超时:超过 10 秒没收到心率,认为设备失联,锁屏。
    • 弱信号:连续 3 秒 RSSI 低于 -75 dBm ,认为人已经走远,锁屏。

    这两种条件互为补充:前者应对手表被取下或蓝牙关闭,后者应对人走远但广播仍在的情况。

    项目结构

    我把应用拆成了三个部分:

    HeartRateLockWidget/
    ├── HeartRateLock/              # macOS 状态栏后台 App
    │   ├── HeartRateLockApp.swift      # 应用入口、状态栏菜单、设置窗口
    │   ├── BLEHeartRateManager.swift   # CoreBluetooth 扫描/绑定/连接/心率订阅
    │   └── LockScreenManager.swift     # 触发 macOS 锁屏
    ├── HeartRateWidget/            # 桌面/侧边小组件
    │   ├── HeartRateWidget.swift       # 实时心率 Widget
    │   ├── HeartRateCurveWidget.swift  # 心率曲线 Widget
    │   └── HeartRateWidgetBundle.swift # Widget 入口
    └── HeartRateLockShared/        # App 与 Widget 共享代码
        └── SharedConfig.swift          # App Group 、UserDefaults Key 、设置项
    

    HeartRateLock 是一个没有 Dock 图标的状态栏应用,启动后躲在菜单栏里持续扫描和监听心率。HeartRateWidget 是 WidgetKit 扩展,负责把心率展示在桌面或通知中心。

    关键实现

    1. 扫描与绑定设备

    macOS 的 CoreBluetooth 扫描时不指定服务 UUID ,这样可以列出周围所有 BLE 广播设备。用户从菜单栏的「选择设备」里按信号强度排序找到手表,点击完成绑定。绑定信息会持久化,下次启动自动重连。

    private let heartRateServiceUUID = CBUUID(string: "0x180D")
    private let heartRateMeasurementUUID = CBUUID(string: "0x2A37")
    
    func centralManager(
        _ central: CBCentralManager,
        didDiscover peripheral: CBPeripheral,
        advertisementData: [String: Any],
        rssi RSSI: NSNumber
    ) {
        // 收集设备名、RSSI ,按信号强度排序展示
    }
    

    这里有一个坑:macOS 出于隐私保护,不会把 BLE 设备的 MAC 地址暴露给 App ,只会给一个随机生成的 CBPeripheral.identifier。所以无法通过 MAC 地址过滤,只能让用户手动选择。另外很多手表在广播里不暴露名字,首次绑定可能显示为「未知设备」,连接成功后我会再读取 GAP 服务 0x2A00 里的真实设备名来校正。

    2. 解析心率数据

    心率测量特征的通知数据格式是 BLE 标准:

    byte 0: flags
      bit 0: 心率值是 uint8 还是 uint16
    byte 1: 心率值( uint8 )
    byte 1-2: 心率值( uint16 )
    

    解析代码:

    private func parseHeartRateMeasurement(_ data: Data) -> Int? {
        guard !data.isEmpty else { return nil }
        let flags = data[0]
        let isUInt16 = (flags & 0x01) != 0
        if isUInt16, data.count >= 3 {
            return Int(data[1]) | (Int(data[2]) << 8)
        } else if data.count >= 2 {
            return Int(data[1])
        }
        return nil
    }
    

    3. 锁屏触发

    每次收到心率或读到 RSSI 时,都会更新 lastAliveAt。每秒检查一次:

    private func checkTimeout() {
        guard let reference = lastAliveAt ?? connectAttemptAt else { return }
        if Date().timeIntervalSince(reference) > SharedConfig.timeoutSeconds, !didLockForTimeout {
            didLockForTimeout = true
            LockScreenManager.lockScreen()
        }
    }
    

    弱信号判定类似:连续低于阈值秒数达到设定值就锁屏,避免 RSSI 抖动导致误锁。

    4. 触发 macOS 锁屏

    最早我用的是 /System/Library/CoreServices/Menu Extras/User.menu/Contents/Resources/CGSession -suspend,但新版 macOS 已经移除了这个路径。为了兼容不同系统版本,我做了三层兜底:

    1. 调用 login.framework 的私有接口 SACLockScreenImmediate
    2. 如果找不到,再尝试 CGSession -suspend
    3. 如果都不行,最后用 pmset displaysleepnow 熄屏,配合系统设置里的「唤醒时需要密码」实现等效锁屏。
    enum LockScreenManager {
        static func lockScreen() {
            if lockViaPrivateFramework() { return }
            if FileManager.default.fileExists(atPath: lockCommand) {
                // CGSession -suspend
                return
            }
            // pmset displaysleepnow 兜底
        }
    }
    

    5. 小组件与 App 共享数据

    Widget 扩展和主 App 属于不同进程,不能直接共享内存。我用了一个小技巧:主 App (非沙盒)直接写 Widget 容器内的 plist 文件,Widget 读取自己的容器。这样比 App Group 的 UserDefaults(suiteName:) 更稳定,也绕过了 macOS AppData TCC 策略对 Widget 读取的限制。

    public enum SharedStore {
        private static let plistURL: URL = {
            if isSandboxed {
                // Widget:自己容器内的 Application Support
            } else {
                // 主 App:写 Widget 容器内的同一个文件
            }
        }()
    }
    

    Widget 每秒请求一次时间线刷新,实际刷新频率由系统调度决定,所以主 App 负责实时性,Widget 只是显示层。

    还加了哪些功能

    空闲锁屏兜底

    万一哪天没戴手表,应用还内置了键鼠空闲检测。通过 CGEventSource.secondsSinceLastEventType 读取键盘、鼠标最后一次活动时间,超过设定秒数也锁屏。这个兜底还可以设置只在指定时间段生效,比如只在工作时间开启。

    开机自启

    SMAppService.mainApp.register() 注册登录项,实现开机自动启动,不需要在「系统设置 > 通用 > 登录项」里手动添加。

    设置面板

    状态栏点击「设置…」可以调整:

    • 信号弱锁屏阈值(-90 ~ -40 dBm )
    • 连续弱信号秒数
    • 心率超时秒数
    • 键鼠空闲锁屏秒数及时段
    • 开机自启开关

    实际使用效果

    戴上手表坐在工位,状态栏会显示实时心率。起身去洗手间,走出两三米后 RSSI 开始下降,大约 3 秒后屏幕自动锁定。回来后解锁电脑,手表会重新连上,继续显示心率。

    误触发的情况极少,因为同时满足「信号持续弱」和「心率中断」才会锁屏。坐在椅子上正常打字时,即使偶尔 RSSI 波动一下,也不会触发锁屏。

    还能怎么玩:真正的"摇篮系统"应该更进一步

    现在的逻辑是"心率没了 → 锁屏",这其实只是一个开始。如果你真想把它做成三体世界里那种"生命体征即威慑"的系统,可以基于这个开源方案继续加戏。

    比如一个更严肃的状态组合:手表明明还戴在手上,蓝牙信号也不弱,但心率突然停止了。这意味着什么?大概率不是人走开了,而是心脏不跳了。这种情况下再锁屏就显得不够有仪式感,你可以触发一些更有"摇篮系统"味道的自动化:

    • 自动提交代码:检测到心率归零且信号正常,立刻执行 git add -A && git commit -m "chore: 最后的提交" && git push,确保代码不丢失,做一个负责任的程序员。
    • 自动发消息:给同事群发一条 Slack/飞书消息:"我去参加黑暗森林战役了,需求下次迭代再说。"。
    • 删库但不跑路:既然人都没了,服务器留着也没意义。可以设计一个"温柔删库"流程——先备份到对象存储,再清空数据库,最后注销所有云账号。真正做到"删库不跑路",因为已经跑不动了。
    • 触发遗嘱脚本:打开提前写好的 last_will.sh,把比特币私钥、未完成的 side project 、暗恋对象的名字一并分发出去。

    clip_1785158635597_yz7imr.png 当然,这些功能建议只作为彩蛋存在,默认不要开启。毕竟心脏停跳不一定是真的心脏停跳,也可能是手表没电了、蓝牙断流了,或者你只是把手表摘下来放在了桌上。真触发一次"自动提交代码"还可以接受,要是误触发"删库",那你可能就要先一步去见雷迪亚兹了。

    踩过的坑

    1. macOS 不暴露 MAC 地址:无法自动识别手表,必须手动绑定一次。
    2. 部分华为手表不广播标准心率服务:它们只走华为运动健康私有协议,这种手表用不了。
    3. CGSession 被移除:新版系统锁屏 API 变了,需要多层兜底。
    4. App Group 共享存储被 TCC 拦截:主 App 写 App Group 的 UserDefaults 后,Widget 读不到,最后改用直接写 Widget 容器 plist 。
    5. 沙盒权限:锁屏需要辅助功能权限,首次运行要在系统设置里允许。
    awenforlinux
        1
    awenforlinux  
    OP
       7h 45m ago
    https://www.v2ex.com/t/1229992#reply58 原文档格式问题无法修改,重新发布下
    About   ·   Help   ·   Advertise   ·   Blog   ·   API   ·   FAQ   ·   Solana   ·   926 Online   Highest 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 32ms · UTC 21:11 · PVG 05:11 · LAX 14:11 · JFK 17:11
    ♥ Do have faith in what you're doing.