[控场AI]
· 6 分钟阅读· 3,047 字

用状态图模拟卡西欧F-91W:一次硬核的嵌入式状态机实践

用状态图模拟卡西欧F-91W:一次硬核的嵌入式状态机实践

卡西欧 F-91W 的多模式交互天然是状态机,用状态图建模比 if-else 更清晰可测。

文章以卡西欧 F-91W 电子表为例,说明为何这类多模式、多层级、带编辑流程的设备适合用状态图(Statecharts)建模。作者首先区分扁平 FSM 与 Harel 提出的 Statecharts,指出层级状态、正交区域、历史状态和守卫条件能避免状态爆炸。随后介绍项目的关键设计:把 Mode/Adjust/Light 三键的短按与长按翻译成语义化事件,将消抖和长按判定隔离在输入层,并用「编辑会话」复合状态配合延迟事件优雅实现超时提交。最后归纳状态图的两大收益——结构化测试与行为可追溯,并对做智能手表、车载中控等交互系统的开发者给出渐进建模与选型建议。

一块手表为什么值得用状态图来建模

卡西欧 F-91W 是电子表界的传奇。它便宜、耐用、功能简单到几乎不需要说明书,但真正把它拆解成一个软件系统时,你会发现它的交互逻辑远比表面复杂:计时、闹钟、秒表、整点报时、12/24 小时制、日期与星期切换,这些模式之间通过左下角的 Mode 键和左上角的 Adjust 键进行切换和编辑。

最近在 Hacker News 上引发讨论的一个项目,尝试用 Statecharts(状态图) 来建模 F-91W 的行为逻辑。项目的核心观点是:像 F-91W 这种多模式、多层级、带编辑状态的设备,本质上就是一个天然的状态机,用状态图来描述它,比用一堆 if-else 更能贴近真实交互。

卡西欧F-91W状态图建模项目来源截图

Statecharts 不是普通状态机

理解这个项目的前提,是先分清「有限状态机(FSM)」和「状态图(Statechart)」的区别。

普通的 FSM 是扁平的:一组状态,一组事件,事件触发状态迁移。但 F-91W 的交互是嵌套的——你在「闹钟模式」里,可能处于「小时编辑」子状态;按下 Adjust 后切换到「分钟编辑」,再按 Light 又回到浏览状态。这种层级关系用扁平 FSM 表达会迅速膨胀成状态爆炸。

Statecharts 由 David Harel 在 1987 年提出,核心扩展包括:

  • 层级状态(Hierarchical States):状态可以嵌套子状态,父状态的迁移可以被所有子状态继承
  • 正交区域(Orthogonal Regions):一个系统可以同时处于多个并行状态,比如「显示刷新」和「按键处理」并行运行
  • 历史状态(History States):回到某个复合状态时,可以记住上次退出的子状态
  • 守卫条件与动作(Guards & Actions):迁移可以带条件和副作用

F-91W 的模式切换非常适合用层级状态来抽象:顶层是「时间模式 / 闹钟模式 / 秒表模式 / 整点报时设置」,每个模式内部再定义各自的子状态和编辑流程。Mode 键负责顶层迁移,Adjust 键负责进入和穿行编辑子状态,Light 键则是一个全局的、不改变模式状态的瞬时动作——这正是正交区域或全局事件的典型用例。

状态爆炸(State Explosion) 是指用扁平 FSM 建模复杂系统时,状态数量随独立维度呈指数增长的现象。假设 F-91W 只有「当前模式」「是否在编辑」「编辑哪个字段」「12/24 小时制」四个彼此独立的维度,每个维度各有若干取值,扁平表达就必须把它们的笛卡尔积全部枚举成互不相关的状态,数量迅速突破人力可维护的极限。更麻烦的是,同一个逻辑动作(比如按 Light 点亮背光)要在几十个状态上各自复制一条迁移,一旦需求变更就要成片修改,极易遗漏。层级状态和正交区域正是为消除这种冗余而设计的:公共行为提升到父状态或并行区域只需定义一次,状态总数从乘积级回落到求和级,这也是状态图相对于 FSM 最本质的工程价值。

把按键语义翻译成事件

F-91W 只有三个按键:Mode、Adjust、Light。项目的一个关键设计决策,是把每个按键的「短按」和「长按」区分成不同事件,而不是在状态迁移里到处判断按下时长。

这种做法的好处是:状态图里的迁移全部由语义化事件驱动,比如 MODE_PRESS、ADJUST_PRESS、ADJUST_LONG_PRESS、LIGHT_PRESS。按键消抖、长按判定这些和硬件强相关的逻辑,被隔离在状态机之外的输入层。

编辑状态的迁移逻辑也因此变得清晰:在闹钟模式下按 Adjust 进入小时编辑,再按 Adjust 切到分钟编辑,再次按 Adjust 或等待超时则提交修改并回到浏览状态。用一个「编辑会话」复合状态来承载这套流程,比在全局维护一个 editingField 变量要可靠得多——超时提交这种需求,用延迟事件(delayed event)在状态图里表达几乎是免费的。

延迟事件(Delayed Event) 是 SCXML、XState 等状态图实现中的标准机制:进入某个状态时可以安排一个在 N 毫秒后自动触发的事件,若在该时间内发生其他迁移而离开此状态,这个定时器会被自动取消。它天然对应「超时提交」这类需求——进入「编辑会话」复合状态时挂上一个 TIMEOUT 延迟事件,用户每次按键都会重置或推进编辑流程,一旦静默超时便自动触发提交并退回浏览状态。相比手写 setTimeout 再在回调里判断当前是否仍处于编辑态,延迟事件的取消语义由状态机保证,不会出现定时器泄漏或状态错配。按键消抖(Debounce) 则是另一层:机械按键在按下和松开的几毫秒内会产生多次电平抖动,若不处理会被识别成连续多次按键。把消抖和长按判定放在输入层,状态图收到的每个事件都已是干净、有明确语义的离散信号,迁移逻辑无需再关心时序细节。

状态图真正的价值:可测试与可追溯

这个项目在 Hacker News 上被讨论,很大程度上不是因为 F-91W 本身,而是它示范了状态图在嵌入式与前端交互中的实用价值。

把设备的全部行为收敛到一张状态图后,会得到两个直接收益:

一是测试变得结构化。 你不需要穷举各种按键组合去碰运气,而是可以针对每条迁移写单元测试:给定当前状态和事件,断言下一状态和输出。状态覆盖率可以直接度量。

二是行为可追溯。 当用户报告「按了 Adjust 之后表盘卡住了」,你可以沿着状态图回溯是哪条迁移缺了守卫条件、哪个子状态没有出口。相比在读代码时脑内模拟一堆布尔标志位,状态图把「在什么情况下会发生什么」变成了可以肉眼检查的图。

状态覆盖率(State Coverage) 是衡量测试完整性的常用指标,指测试用例执行到了状态图中多少个状态与迁移。它比代码行覆盖率更贴近交互系统的真实风险:一段被覆盖的代码可能对应着从未被触发过的迁移路径,而用户报告的 bug 往往就藏在这些路径上。实践中可以要求每条迁移至少被一个测试触达,对守卫条件为真的分支与为假的分支分别断言,这样测试集规模与状态图的规模线性相关,而不会像穷举按键组合那样组合爆炸。状态图还可被工具直接导出为测试脚本或可视化图示,使「测试用例」与「设计文档」共享同一份事实来源,避免两者随时间推移而各自漂移。

对国内开发者的启发

如果你在做智能手表、车载中控、或者任何带多级菜单和编辑流程的交互系统,F-91W 这个案例值得参考。它的复杂度刚好介于「玩具 demo」和「真实产品」之间——足够简单到能在一篇文章里讲清楚,又足够真实到能暴露状态图的实际收益。

常用的状态图实现包括 XState(JavaScript/TypeScript)、SCXML 规范以及各类嵌入式状态机框架。选型时不必追求「一步到位」,先从一个模式内部的子状态开始画,逐步把 Mode 键的顶层迁移补全,比一开始就试图画出全图要务实得多。

F-91W 证明了一件朴素的事:越是看起来简单的设备,其交互状态越值得被认真建模。

#Statecharts#状态图#卡西欧F-91W#状态机#XState#嵌入式交互#层级状态
分享:

相关推荐