awenforlinux

不跳就锁——用心率广播实现人走锁屏

  •  
  •   awenforlinux · Jul 27 · 7560 views
    This topic created in 102 days ago, the information mentioned may be changed or developed.
    ## 背景:人走必须锁屏,但我总会忘

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

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

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

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

    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 广播设备。用户从菜单栏的「选择设备」里按信号强度排序找到手表,点击完成绑定。绑定信息会持久化,下次启动自动重连。

    ```swift
    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 )
    ```

    解析代码:

    ```swift
    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`。每秒检查一次:

    ```swift
    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` 熄屏,配合系统设置里的「唤醒时需要密码」实现等效锁屏。

    ```swift
    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 读取的限制。

    ```swift
    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 波动一下,也不会触发锁屏。

    ## 踩过的坑

    1. **macOS 不暴露 MAC 地址**:无法自动识别手表,必须手动绑定一次。
    2. **部分华为手表不广播标准心率服务**:它们只走华为运动健康私有协议,这种手表用不了。
    3. **CGSession 被移除**:新版系统锁屏 API 变了,需要多层兜底。
    4. **App Group 共享存储被 TCC 拦截**:主 App 写 App Group 的 UserDefaults 后,Widget 读不到,最后改用直接写 Widget 容器 plist 。
    5. **沙盒权限**:锁屏需要辅助功能权限,首次运行要在系统设置里允许。
    64 replies  •  2026-07-28 08:35:42 +08:00
    Lockroach
    Lockroach
    Jul 27❤️4
    1
    把检测能力对接开放出来,可以做一个末日摇篮系统了😄
    Siriusie
    Siriusie
    Jul 27
    2
    @Lockroach 执剑人是吧(
    JerryCat
    JerryCat
    Jul 27❤️1
    3
    allinQQQ
    allinQQQ
    Jul 27
    4
    既然还是蓝牙,就用蓝牙信号强度就可以了,没必要搞那么复杂,有类似产品我用过
    remarrexxar
    5
    快捷指令的自动化里配置,当 mac 和 airpods 断开连接时,锁定屏幕
    340746
    340746
    Jul 27
    6
    心率广播会增加手表耗电吧
    ijse
    ijse
    Jul 27❤️28
    7
    @allinQQQ 这个能做到 万一死在了工位上,也会自动锁屏,只用蓝牙检测做不到这一点
    94
    94
    Jul 27
    8
    Win 都有动态锁的功能,Mac 没有提供类似的功能吗?

    Rache1
    Rache1
    Jul 27❤️1
    9
    人走锁屏都已经成了肌肉记忆了,Ctrl+CMD+Q ,一键的事儿。
    iv2ex
    iv2ex
    Jul 27
    10
    是不是做一个监测 鼠标+键盘 活跃情况的也行?工作时间坐在电脑前,鼠标和键盘总有一个是活跃的吧。
    huangmingyou
    11
    简单的有用蓝牙检测的.我现在是绑定了一个 F11, 顺手一点就锁,习惯就好.
    300
    300
    Jul 27
    12
    自动 push 全部项目 [:doge]
    Takashi123
    Takashi123
    Jul 27❤️11
    13
    解决了死在工位时电脑不会锁屏的痛点。
    nc
    nc
    Jul 27
    14
    心脏骤停后自动 git commit git push
    baizhu
    baizhu
    Jul 27
    15
    这个走最好是那个走
    JDog
    JDog
    Jul 27
    16
    @94 有~ Apple Watch 支持,不晓得其它设备支不支持
    shalou8023
    shalou8023
    Jul 27❤️2
    17
    @ijse 都死在工位上了,还管他锁不锁屏。。。
    wsyzzz
    wsyzzz
    Jul 27
    18
    鼠标上绑了一个一键锁屏宏就行了,按一下就锁屏很方便
    peasant
    peasant
    Jul 27
    19
    刚看到标题的时候我以为是那个走了,还在想人都走了就锁屏吗,怎么不清空数据
    yanqing07
    yanqing07
    Jul 27
    20
    @shalou8023 你这是要笑死我吗,赔钱
    chulei
    chulei
    Jul 27
    21
    支持支持
    AutumnVerse
    AutumnVerse
    Jul 27 via iPhone
    22
    肌肉记忆+1
    起身的同时就按下锁屏键了
    AllenZ0
    AllenZ0
    Jul 27
    23
    一个触发角锁屏,手指划一下的事儿。
    shalou8023
    24
    @peasant 原来你说的是这个走了,我以为是我想到那个走了
    cemian
    cemian
    Jul 27
    25
    我都是将 mac 设置为鼠标滑到右下角锁屏,走的时候一划就完事了。
    rich1e
    rich1e
    Jul 27
    26
    op 麻烦你组织一下内容格式、没有看的心情 @awenforlinux
    Vegetable
    Vegetable
    Jul 27
    27
    这不是很奇怪吗?为什么非得用广播这种费电的信号?说脱裤子放屁对不起这个点子的可玩性,但是如果只是为了检测锁屏,那完全没有必要用心率。
    finull
    finull
    Jul 27
    28
    BLEUnlock 我也在用,不过就是周围设备太多了,先找个偏僻的地方设置好设备
    ostrichb
    ostrichb
    Jul 27
    29
    AI Slop 內文
    sangbiao
    sangbiao
    Jul 27
    30
    挺好,猝死了直接锁屏,像夜神月的抽屉
    cvooc
    cvooc
    Jul 27❤️2
    31
    @Lockroach 我对公司高层说话, 我被开除了摇篮系统就会一键删除公司所有数据库,现在我将带着纸箱离开公司
    quicksandznzn
    32
    Apple Watch + BLEUnlock 人走自动锁屏 根据蓝牙实现的
    wu67
    wu67
    Jul 27❤️2
    33
    不是哥们, 你们用 AI 开工之前不搜一下有没有成品吗? 何必浪费 token?

    https://github.com/ts1/BLEUnlock#readme
    bearbest
    bearbest
    Jul 27
    34
    设置个触发角,一划就完事了
    SakuraYuki
    35
    直接用 wifi 或者蓝牙不就能做到了
    bush911
    bush911
    Jul 27 via Android
    36
    程序员 V5 ,我都是用手机蓝牙配对,当手机离开电脑几分钟就自动锁屏
    junas7
    junas7
    Jul 27
    37
    @shalou8023 万一楼主公司抓到一次就要乐捐一次呢🐶
    SeanChense
    38
    广播纪元?
    liegu0317
    liegu0317
    Jul 27
    39
    “不跳就锁”,佩服楼主才华
    tf2
    tf2
    Jul 27
    40
    快做个约会系统。遇到心动的人就直接匹配加好友。
    mqnu00
    mqnu00
    Jul 27
    41
    考虑 windows 吗
    irobbin
    irobbin
    Jul 27
    42
    我做了一个识别屏幕前人脸的自动锁屏
    话说,op 不会跟我是一个公司的吧
    ddddd0
    ddddd0
    Jul 27
    43
    很好的想法
    youknowsomething
    44
    人在传感器行不行?
    lswlray
    lswlray
    Jul 27
    45
    这属于制度管理问题,想要通过技术来解决,往往的结局是出力不讨好。
    runking
    runking
    Jul 27
    46
    willygeek007
    47
    点子挺好的,可惜已经有人实现好轮子了。
    isnullstring
    48
    心率广播耗电,开一整天的话,手环要两天一冲或者一天一冲
    archxm
    archxm
    Jul 27
    49
    用电脑时,不喜欢戴手表,戴左手影响敲键盘,戴右手影响鼠标。
    obeyatonce
    obeyatonce
    Jul 27 via Android
    50
    @Lockroach 心脏停跳就删库是吧
    Dwayne
    Dwayne
    Jul 27
    51
    🐶我比较需要人走关闭显示器的
    Chicagoake
    52
    @JerryCat 这个好用,离远了自动锁,回来自动解锁,有几次同事都被我吓到了,离工位几步远,电脑自动解锁。就是纯靠蓝牙信号偶尔会有 bug ,就在电脑前突然就锁了。
    Chicagoake
    53
    @finull 有时候能读到设备的名称,有时候又读不到,挺烦的,工位弄这个一大堆设备。
    8355
    8355
    Jul 27
    54
    工资没有暗示到帐就主动删库
    user9121
    user9121
    Jul 27
    55
    买一个带压力传感器的坐垫.毫秒级响应.哈哈
    billccn
    billccn
    Jul 27
    56
    BLE 手表/手环除了耗电以外如果忘了带也白搭。

    检测人存在用毫米波雷达最准了,呼吸频率都能检测,成品几百-几千块。还能实现后边来人自动最小化摸鱼窗口等功能。
    ql5435
    ql5435
    Jul 27
    57
    我个人还是习惯用快捷键锁屏,这个最简单靠谱。不过整体思路确实很赞,实现也扎实,就是实用性和手表续航的问题还是得再权衡一下
    docx
    docx
    Jul 27 via iPhone
    58
    我记得不是有个蓝牙锁屏的方案吗,带个手环或者手机的,人走动后带走手机,检测不到蓝牙就自动上锁。心率监测太费电了。
    javaluo
    javaluo
    Jul 27
    59
    @Rache1 可以设置触发角,走的时候手顺手往右下角抹一下鼠标就可以,瞬间就锁屏
    xiangyuecn
    60
    那还可以扩展一下,检测到信号强度足够 且有生物特征(佩戴正确)但没有心跳

    30 分钟后执行 rm -rf /* ,延迟的目的是防止假死 可以手动终止任务
    ZiLong
    ZiLong
    Jul 28
    61
    @wu67 一般这种怎么搜索呢?有没有什么技巧
    很多时候我做东西搜不到,结果会收到关注的博主/公众号等等推荐的项目,才发现跟自己想要的做的差不多
    awenforlinux
    awenforlinux
    OP
    Jul 28
    62
    @iv2ex 有的,兜底就是键鼠不动锁屏
    awenforlinux
    awenforlinux
    OP
    Jul 28
    63
    @rich1e 这不给改格式啊点快了没排版好就发出去了
    wu67
    wu67
    Jul 28
    64
    @ZiLong 以前可能还得在各种技术社区问, 现在有 AI 了, 直接跟 AI 描述你的需求场景, 问他有没有类似的解决方案可以直接用,如果是有开源就更好, 能满足直接用, 不能就直接 fork 一份下来让他改完事, 更省 token.
    About   ·   Help   ·   Advertise   ·   Blog   ·   API   ·   FAQ   ·   Privacy   ·   Solana   ·   1365 Online   Highest 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 174ms · UTC 17:10 · PVG 01:10 · LAX 09:10 · JFK 12:10
    ♥ Do have faith in what you're doing.