Dorso 坐姿监测 AppState 状态机解析:热插拔、断连与锁屏后的自动恢复
【免费下载链接】dorsoA macOS app that blurs your screen when you slouch.项目地址: https://gitcode.com/gh_mirrors/po/dorso
Dorso 是一款 macOS 坐姿监测工具,当你弯腰驼背时会自动模糊屏幕提醒你坐直。它的核心是一个简洁的AppState状态机:面对相机热插拔、外接显示器断开、锁屏休眠等系统事件,如何"暂停—快照—自动恢复"而不让用户手动干预?本文带你完整拆解这套设计。
一、状态机全景:4 个状态 + 6 种暂停原因
整个应用的顶层状态定义在 Sources/Core/AppState.swift 中,非常克制:
| 状态 | 含义 |
|---|---|
disabled | 用户关闭了监测 |
calibrating | 正在校准基准坐姿 |
monitoring | 正常监测中,检测到驼背即模糊屏幕 |
paused(reason) | 因某个原因暂停 |
暂停不是一个笼统的"停",而是携带原因的paused(PauseReason),共 6 种:noProfile(未完成校准)、onTheGo(仅笔记本屏、外出模式)、cameraDisconnected(相机断连)、screenLocked(锁屏)、airPodsRemoved(AirPods 摘下)、onBattery(电池供电)。
这种"原因枚举"是关键设计:每一种暂停都对应一条明确的恢复路径,菜单栏也能精准显示当前为什么暂停。
二、事件从哪来:四个系统观察者
状态机不直接监听系统,而是由System/目录下的观察者类统一收集事件,再转成Action派发:
- 相机插拔:CameraObserver.swift 监听
AVCaptureDeviceWasConnected/Disconnected通知,只处理视频设备。 - 显示器热插拔:DisplayMonitor.swift 注册 CoreGraphics 重排回调,并做了0.5 秒防抖,避免插拔瞬间的抖动事件触发多次状态迁移。
- 锁屏/休眠:ScreenLockObserver.swift 同时监听锁屏通知和屏幕休眠/唤醒通知,覆盖"合盖但不要求密码"的场景。
- 电源切换:PowerSourceObserver.swift 通过 IOKit 监听 AC/电池切换,并去重电池容量抖动。
观察到的事件在 Sources/AppDelegate/AppDelegate+DeviceEvents.swift 中被"翻译"——先查当前显示器配置对应的坐姿配置文件(Profile),再向TrackingFeature派发cameraDisconnected、displayConfigurationChanged等 Action。
三、相机断连:先找备用相机,再决定暂停还是接管
核心决策逻辑在 PostureEngine.stateWhenCameraDisconnects,规则像一棵清晰的决策树:
- 断掉的不是当前选中的相机 → 只同步 UI,状态不变;
- 没有备用相机 → 进入
paused(cameraDisconnected); - 备用相机正好匹配当前显示器配置档案→ 直接切换到备用相机并立即恢复监测,全程无感;
- 备用相机存在但没有匹配档案 → 切换过去,但停在
paused(noProfile),等待用户校准。
反向也对称:相机插入时(stateWhenCameraConnects),若新相机匹配档案则自动选中并开始监测;若不匹配且正处于断连暂停,则转入paused(noProfile)提示用户校准。
四、显示器热插拔:"外出模式"的暂停与回归
外接显示器拔掉后,应用要判断"我是不是在移动场景"。stateWhenDisplayConfigurationChanges 的处理顺序:
- 状态为
disabled或电池暂停时直接忽略(电池暂停是"粘性"的,只有插电/改设置/用户操作才能解除); - 开启了"外出暂停"(Pause on the go)且当前只剩笔记本内置屏→
paused(onTheGo); - 任何相机都消失了 →
paused(cameraDisconnected); - 有匹配档案的相机 → 必要时自动切换到该相机并恢复
monitoring; - 否则 →
paused(noProfile)。
插回外接显示器后,同样的逻辑会重新评估:只要当前配置存在匹配的相机档案,状态机就会自动切回监测,不需要任何人工操作。
五、锁屏与解锁:快照-恢复,而不是盲目重启
这是整个状态机最优雅的部分。stateWhenScreenLocks 在锁屏时做两件事:
- 把当前状态快照进
stateBeforeLock(注意它可能本来就是paused(.airPodsRemoved)这类状态,也一并快照); - 进入
paused(.screenLocked),同时shouldDetectorRun让检测器停转,省资源。
解锁时(stateWhenScreenUnlocks):
- 有快照且非电池暂停 →精确恢复到锁屏前的状态(
monitoring则重启监测,calibrating则回到校准流程); - 若无快照(异常情况)→ 保持在锁屏暂停态,等待下一次事件;
- 特例:如果合盖期间电源被拔掉且开启了"电池暂停",解锁后落到
paused(onBattery)而非盲目重启。
细节值得点赞:handleScreenUnlocked 在派发解锁 Action之前先补发一次powerSourceChanged,因为合盖期间电源可能已变化,保证恢复决策基于真实状态。
六、自动模式与可测试性:纯函数 + 副作用意图
开启自动模式后,resolveAutomaticSource 会在每次相机/AirPods 就绪度变化时重新裁决活动源:首选源就绪就用它,否则用备胎,谁都不就绪就带原因暂停。注意开头的"粘性守卫"——paused(onBattery)不会被任何硬件事件悄悄解除。
架构上有个值得学习的模式:
- PostureEngine.swift 是纯函数状态机,每个场景一个
stateWhenXxx方法,输入当前状态,输出新状态 + 需要的副作用标记; - TrackingFeature.swift 作为 TCA Reducer 只做路由,把副作用描述为
EffectIntent(如startMonitoring、switchCamera),由运行期闭包执行; - 因此所有迁移路径都可以离线断言,Tests/PostureEngineTransitionTests.swift 中就能看到"锁屏快照 monitoring → 解锁精确恢复"这类用例。
小结
Dorso 的AppState状态机用 4 个状态 + 6 种暂停原因覆盖了绝大多数系统事件。三条值得借鉴的经验:暂停永远携带原因(恢复路径明确)、先快照再恢复(锁屏场景精确回滚)、决策纯函数化 + 副作用意图化(迁移逻辑可单测、可回放)。无论你做桌面工具还是嵌入式应用,这套"事件 → 纯决策 → 意图副作用"的三层结构都能直接套用。
更多设计背景可参考 PROFILES.md 中的相机档案说明,以及 Sources/Settings/SettingsProfiles.swift 的实现。
【免费下载链接】dorsoA macOS app that blurs your screen when you slouch.项目地址: https://gitcode.com/gh_mirrors/po/dorso
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考