配置驱动的洗衣机动态设计:从JSON到状态机实践
2026/8/27 20:43:37 网站建设 项目流程

在家电产品开发中,动态设计并不是指界面动画,而是指一个系统能否通过配置而不是改代码,去适应不同型号、不同市场、不同用户的洗涤需求。Schulthess 这类高端洗衣机产品,面板上往往有很多程序,不同程序又有温度、转速、漂洗次数、预约时间等可变参数。如果每加一个程序就改一次界面代码和执行逻辑,开发和测试成本会随着型号增长越来越难控制。把洗涤程序抽成配置,让界面和执行引擎按配置动态生成,是更可维护的做法。

这篇文章会从需求出发,拆解动态设计在洗衣机控制场景里的核心概念,给出一个“配置定义程序 -> 校验配置 -> 渲染面板 -> 状态机执行”的最小可运行设计,并说明常见问题、排查路径和适合生产环境的扩展方向。适合正在做家电嵌入式 HMI、物联网前端,或者想理解配置驱动 UI 和状态机设计的开发者阅读。

1. 理清“动态设计”在洗衣机场景中的含义

1.1 洗衣机控制面板不只是按钮

传统洗衣机面板是一组固定按钮加一个旋钮,程序列表、参数范围都写在硬件里。用户选择某个程序后,面板显示对应的温度、转速和漂洗次数,允许用户做有限调节。这种设计在产品型号少、功能固定时没有问题。

但当产品线变大以后,问题就出现了。不同型号可能有不同的最大转速、不同的加热方式、不同的脱水安全策略,甚至不同销售地区的洗涤程序命名和默认参数都不一样。如果每个型号都单独维护一套界面代码和执行逻辑,任何一个参数调整都要重新编译、重新烧录、重新测试,版本管理也会变得很痛苦。

动态设计要解决的核心问题是:程序列表、参数范围、显示文案、执行流程这些内容,能不能和数据分离,能不能在设备不重新编译的情况下通过配置调整。

1.2 动态设计的核心:程序与界面分离

在洗衣机这类嵌入式产品里,动态设计通常包含两个层面:

第一层是“程序配置化”。洗涤程序不再是一段写死在代码里的 case 分支,而是一条包含多个参数和多个阶段的对象。程序有哪些环节、每个环节持续多久、目标温度是多少、漂流比例是多少,都从这个对象里读取。

第二层是“界面配置化”。控制面板上的程序列表、参数控件、取值范围、单位显示,都由配置内容驱动。配置里有温度控件,界面就渲染温度调节;配置里没有漂洗次数,界面就不显示漂洗次数。这样不同型号可以复用同一套界面框架。

这两个层面合起来,才是完整的动态设计。只有程序配置化、没有界面配置化,程序再动态,界面还是固定模板,扩展程序时依然要改面板代码。

1.3 从 Schulthess 型多程序洗衣机看动态设计解决的问题

以 Schulthess 这类提供多程序的机型为例,用户可以选择的程序通常不止一两个,而是包含棉麻、化纤、羊毛、快速洗、大件、衬衫、羽绒服等不同场景。每个场景对温度、转速、水位、漂洗次数、脱水方式的要求都不同。

如果不做动态设计,开发一个新程序的过程是这样的:在程序列表里加一个枚举,在界面里加一个图标,在控件区加一段 if 分支,在执行流程里加一个 case,再写一大堆联动逻辑。只要其中一步忘改,就会出现“界面能选程序,但执行时参数不对”或者“程序能跑,但界面上没有对应控件”的问题。

如果做动态设计,开发新程序的过程就变成:写一段 JSON 配置,描述程序名称、参数控件和执行阶段,然后由界面引擎解析生成面板,由执行引擎解析生成状态机。代码的改动量从多处修改收敛成一个配置文件,回归测试的范围也变得更清晰。文章后面的案例就是围绕这条主线展开的。

2. 需求分析与系统架构拆分

2.1 功能需求清单

在开始写代码之前,先列出动态洗涤程序系统应该满足的功能需求。不要一上来就选择框架,先确定边界。

  • 支持程序列表动态加载,新增程序不用改主界面代码。
  • 支持参数控件动态渲染,不同程序展示不同参数类型。
  • 支持参数范围配置,例如温度最小值、最大值、步进值。
  • 支持执行阶段配置,例如进水、加热、洗涤、漂洗、脱水。
  • 支持配置合法性校验,非法配置不能进入执行流程。
  • 支持状态机按阶段顺序执行,并能在每个阶段输出日志。
  • 支持在没有硬件环境时先做软件模拟,方便联调 UI 和执行逻辑。

从这些需求可以看出,这个系统不是简单的“取配置文件然后显示出来”,而是要把配置同时驱动界面和执行引擎。前者是运行时读取问题,后者是状态流转问题。

2.2 整体架构:配置模块、解析模块、UI 模块、执行模块

根据上面的需求,可以把系统拆成四个模块:

模块职责关键输出
配置模块提供程序定义数据,可以是 JSON 文件、数据库记录或云端下发结果原始配置对象
解析模块对配置做结构校验、字段校验、默认值补全标准化的 ProgramDefinition
UI 模块根据 ProgramDefinition 渲染程序列表和参数控件可交互的控制面板
执行模块读取 ProgramDefinition 的阶段列表,按状态机推进洗涤流程当前阶段、阶段日志、完成状态

这种分层方式的好处是依赖方向清晰:UI 模块不直接读文件,执行模块不关心界面显示。配置解析结果是中间的稳定契约,只要这个契约不破坏,内部实现可以替换。

在真实嵌入式设备中,配置模块可能要适配 EEPROM、Flash 或远程服务器,UI 模块可能是 LVGL、TouchGFX 或 H5 页面,执行模块直接对接电机、水泵、加热器。但无论是嵌入式和 H5,四层拆法都成立。

2.3 数据模型设计

洗涤程序的数据模型是动态设计的关键。模型设计得不好,后面扩展一个新参数都会很痛苦。先定义最核心的实体:

  • Program:一个洗涤程序。
  • ControlParam:程序面板上的一个可调参数。
  • Stage:执行流程中的一个阶段。
  • StageAction:阶段内需要执行的具体动作。

用 JSON 来表示一个程序时,可以这样设计结构:

{ "id": "cotton_60", "name": "棉麻 60°C", "category": "cotton", "controls": [ { "key": "temperature", "label": "温度", "type": "range", "unit": "°C", "min": 20, "max": 95, "step": 5, "default": 60 }, { "key": "spinSpeed", "label": "脱水转速", "type": "range", "unit": "rpm", "min": 0, "max": 1600, "step": 100, "default": 1200 } ], "stages": [ { "action": "fill", "target": "high" }, { "action": "heat", "targetTemp": 60 }, { "action": "wash", "durationMin": 45 }, { "action": "rinse", "count": 3 }, { "action": "spin", "speed": 1200 } ] }

这个结构的优点在于:界面需要的信息在 controls 数组里,执行流程需要的信息在 stages 数组里。两边的消费方各取所需,又共享同一个 Program 根对象。如果以后再增加“预约时间”“洗涤液用量”,只需要在 controls 中新增一项,并在执行阶段增加对应的字段引用。

3. 用 JSON Schema 定义洗涤程序

3.1 洗涤程序配置示例

为了让配置可校验、可解释,不应在代码里手写一堆 if 判断配置文件是否合法。推荐用一个 JSON Schema 来描述洗涤程序的约束,然后让解析模块统一校验。

下面是一个精简的 Schema 示例:

{ "$schema": "https://json-schema.org/draft/2020-12/schema", "type": "object", "required": ["id", "name", "controls", "stages"], "properties": { "id": { "type": "string", "pattern": "^[a-z0-9_]+$" }, "name": { "type": "string" }, "category": { "type": "string" }, "controls": { "type": "array", "items": { "type": "object", "required": ["key", "label", "type"], "properties": { "key": { "type": "string" }, "label": { "type": "string" }, "type": { "enum": ["range", "select", "toggle"] }, "unit": { "type": "string" }, "min": { "type": "number" }, "max": { "type": "number" }, "step": { "type": "number" }, "default": { "type": "number" }, "options": { "type": "array", "items": { "type": "string" } } } } }, "stages": { "type": "array", "minItems": 1, "items": { "type": "object", "required": ["action"], "properties": { "action": { "type": "string" }, "target": { "type": "string" }, "targetTemp": { "type": "number" }, "durationMin": { "type": "integer" }, "count": { "type": "integer" }, "speed": { "type": "integer" } } } } } }

编写 Schema 的重点不是“格式好看”,而是把规则提前声明。这样配置编写者在配置被加载之前就能发现问题。

3.2 参数说明与约束

在动态方案里,参数控件类型决定了界面渲染方式。常见类型和约束如下:

类型界面表现主要字段使用场景
range滑杆或步进器min、max、step、default温度、转速、预约时间
select下拉或胶囊选择options、default洗涤模式、水位等级
toggle开关default额外漂洗、防皱、静音

不要在配置里直接写"min": 100, "max": 50这种颠倒范围。解析模块要负责做一致性校验,否则 UI 渲染会出现滑杆无法拖动、执行模块计算出负数时间等问题。

3.3 参数校验:为什么不能直接信任配置

配置文件是运行时数据,不应该被当成可信代码。即使是内部工程维护的 JSON,也可能出现字段拼错、范围写错、阶段顺序不合理等问题。如果解析模块不做校验,错误会在运行到某个阶段时才暴露,这时候排查成本已经很高。

建议至少校验三类规则:

  • 结构规则:必填字段是否齐全,类型是否正确。
  • 范围规则:min 是否小于 max,default 是否落在范围内,step 是否能整除范围。
  • 业务规则:温度阶段的目标温度是否在 controls 可调范围内,漂洗次数是否为正数,阶段列表是否为空。

校验失败时,不要只返回“配置错误”这样的提示,要返回具体到字段的错误信息。例如:controls.spinSpeed.default 不在 min 和 max 范围内。这样配置维护者能快速定位。

4. 动态渲染与状态机实现

4.1 根据配置动态生成控制面板

UI 模块的核心是根据 ProgramDefinition.controls 数组渲染控件。界面不需要知道每一个具体程序,只需要知道“某个参数是什么类型”“取值范围是多少”。

以浏览器端简化示例为参考,可以写一个渲染函数:

function renderControls(container, controls, state) { container.innerHTML = ''; controls.forEach((control) => { const group = document.createElement('div'); group.className = 'control-group'; const label = document.createElement('label'); label.textContent = control.label; group.appendChild(label); if (control.type === 'range') { const input = document.createElement('input'); input.type = 'range'; input.min = control.min; input.max = control.max; input.step = control.step; input.value = state[control.key] ?? control.default; input.addEventListener('input', () => { state[control.key] = Number(input.value); }); group.appendChild(input); } else if (control.type === 'select') { const select = document.createElement('select'); control.options.forEach((option) => { const optionEl = document.createElement('option'); optionEl.value = option; optionEl.textContent = option; select.appendChild(optionEl); }); select.value = state[control.key] ?? control.default; select.addEventListener('change', () => { state[control.key] = select.value; }); group.appendChild(select); } container.appendChild(group); }); }

这个函数只关心数据,不关心具体是哪个洗涤程序。以后新增一种“text”控件类型,只需要在if分支中增加一个渲染分支,不需要为每个程序分别写代码。

关键点在于 state 对象。界面控件的改变只写入 state,执行模块启动时再从 state 读取最终参数,而不是在执行过程中反过来查询 DOM 控件的值。这样可以避免 UI 状态和执行逻辑耦合。

4.2 洗涤状态机设计

洗涤流程天然适合用状态机建模。进水、加热、洗涤、漂洗、脱水之间有明确的先后关系,每个阶段都可能因为水位、温度、时间条件进入下一个阶段。动态设计里,状态机的阶段列表来自配置,而不是写死的函数调用序列。

用 Python 写一个最小模拟状态机:

class WashStage: def __init__(self, action, **params): self.action = action self.params = params def __repr__(self): return f"<{self.action} {self.params}>" class WashStateMachine: def __init__(self, stages): self.stages = stages self.index = 0 self.status = "idle" def start(self): if not self.stages: raise ValueError("stage list is empty") self.status = "running" return self.next() def next(self): if self.index >= len(self.stages): self.status = "finished" return None stage = self.stages[self.index] self.index += 1 return stage

这个模拟版本没有接真实硬件,但已经表达出核心逻辑:执行引擎只按配置的阶段列表推进,每个阶段是一个对象。真实设备中,每个 action 会对应一个具体执行函数,例如fill会打开进水阀,直到水位传感器到达目标水位。

4.3 配置变更与热更新策略

在软件模拟阶段,配置可以通过文件热加载来调试。在生产设备上,热更新要考虑安全性。如果洗衣机正在运行,此时收到一个新配置,不能立刻替换正在执行的 stages。否则会出现本次洗涤还没完成,阶段列表已经被换掉的情况。

推荐策略是“执行中锁定配置”。只有在 idle 状态才允许更新程序配置;running 状态只记录待交付的配置,等设备回到 idle 后再切换。这个策略在状态机里非常容易实现,只需要在切换配置前检查status字段。

5. 最小可运行验证

5.1 准备本地运行环境

这个最小案例只需要 Python 3.9 或以上版本,不需要安装任何第三方库。如果希望用 JSON Schema 做校验,再安装一个jsonschema库即可。这里为了减少环境依赖,先用手写函数完成基本校验。

项目目录建议如下:

washing-machine-dynamic-design/ ├── config/ │ └── cotton_60.json ├── core/ │ ├── __init__.py │ ├── parser.py │ ├── state_machine.py │ └── validator.py └── main.py

这个结构方便后面扩展。config 目录放程序配置,core 目录放解析、校验和执行逻辑,main.py 做入口演示。

5.2 运行配置解析与状态模拟

core/validator.py中实现一个基础校验函数:

def validate_program(data): required = ["id", "name", "controls", "stages"] for field in required: if field not in data: raise ValueError(f"missing field: {field}") for control in data["controls"]: if control["type"] == "range": if control["min"] >= control["max"]: raise ValueError( f"control {control['key']}: min must be less than max" ) if not (control["min"] <= control["default"] <= control["max"]): raise ValueError( f"control {control['key']}: default out of range" ) if not data["stages"]: raise ValueError("stages must not be empty")

main.py中加载配置文件并运行:

import json from core.validator import validate_program from core.state_machine import WashStateMachine, WashStage def load_program(path): with open(path, "r", encoding="utf-8") as f: data = json.load(f) validate_program(data) stages = [WashStage(stage["action"], **{ k: v for k, v in stage.items() if k != "action" }) for stage in data["stages"]] return data, stages if __name__ == "__main__": program, stages = load_program("config/cotton_60.json") print("program:", program["name"]) print("controls:", [c["key"] for c in program["controls"]]) machine = WashStateMachine(stages) print("machine status:", machine.status) while True: stage = machine.next() if stage is None: break print("execute:", stage) print("machine status:", machine.status)

运行命令:

python main.py

预期输出类似:

program: 棉麻 60°C controls: ['temperature', 'spinSpeed'] machine status: idle execute: <fill {'target': 'high'}> execute: <heat {'targetTemp': 60}> execute: <wash {'durationMin': 45}> execute: <rinse {'count': 3}> execute: <spin {'speed': 1200}> machine status: finished

这样,一个配置驱动洗涤程序的最小闭环就跑通了。这里没有接真实电机和水泵,但验证了“配置 -> 校验 -> 状态机执行”这条主线。

5.3 验证结果是否可扩展

跑通最小案例后,可以做一个扩展实验:在 config 目录下新增一个quick_wash.json,然后重新运行入口脚本。程序列表和处理逻辑都不需要改,只需要新配置能被正确解析和校验。这个实验能直接验证动态设计是否做到“数据与逻辑分离”。

如果修改一行配置后发现运行错误,排查时先看校验函数是否报错,再看状态机阶段列表是否和预期一致。只要错误信息是具体字段级别的,定位速度会比在几十个 case 分支里查找快很多。

6. 常见问题与排查链路

6.1 程序没有出现在面板上

现象:程序配置文件已添加,但界面列表里看不到新程序。

可能原因和检查顺序:

  1. 配置文件是否放在程序加载目录下。
  2. 配置的 id 是否与其他程序重名,导致被覆盖。
  3. 配置是否通过了 Schema 校验,校验失败时加载模块可能直接跳过。
  4. 列表渲染使用的数据源是原始文件内容,还是解析后的 ProgramDefinition;如果 UI 读取的是旧数据源,自然不会出现新程序。

排查时先在加载入口打日志,输出“加载到的程序数量”和“每个程序 id”。如果加载数量正确,再检查 UI 层的过滤条件,例如是否误加了状态过滤或分类过滤。

6.2 参数调节范围不对

现象:界面上温度滑杆最小值是 0,最大值是 100,但程序要求 20 到 95。

可能原因:UI 没有读取minmax,而是写死了通用范围。很多动态界面首次实现时,容易在控件代码里硬编码一个兜底范围,导致配置里的范围没有生效。

排查方式:

  • 打开调试面板,查看渲染函数是否拿到 control 对象的完整属性。
  • 检查input.mininput.max是否被赋值为数字而不是字符串。
  • 检查解析模块是否丢失了数字类型。JSON 中字段可能是字符串,范围计算时要转成 Number 或 float。

处理方案是统一在解析模块标准化数字字段,不让 UI 层做类型转换。

6.3 状态机执行卡在某个阶段

现象:程序启动后一直处于加热阶段,不进入洗涤阶段。

可能原因:

  • 阶段之间缺少状态推进的触发条件。例如加热阶段需要检测温度到达targetTemp,但传感器数据没有更新或者条件判断写反。
  • 执行模块只从配置读取了阶段列表,但没有读取用户修改后的参数。如果用户在界面上把目标温度调成了 40,执行时仍然使用配置里的 60,就会出现温度一直不到目标值。
  • 状态机没有处理异常和超时,触发条件永远不满足时没有退出机制。

排查顺序是先看状态机日志:当前在哪一个阶段,阶段开始时的参数是什么,等待条件是什么。再看实际温度是否到达目标值,最后看是否配置了最大等待时间或超时保护。

6.4 配置热更新导致界面闪烁或数据丢失

现象:设备在运行时热更新配置,界面重新渲染,用户刚调整的转速值被重置为默认值。

原因:渲染函数在每次更新时都使用配置里的 default 初始化控件,而不是保留 state 中已有值。

解决方案:初始化控件时优先读取当前 state 的值,只有 state 中没有对应 key 时才使用 default:

input.value = state[control.key] ?? control.default;

这行代码在前面示例中已经体现。如果要在生产环境支持配置热更新,状态保持是非常重要的一环。可以把用户参数保存在一个独立对象中,渲染只同步控件,不回填默认值。

6.5 配置校验错误定位困难

现象:加载配置时提示 “invalid config”,但不知道是哪一段写错了。

原因:校验函数返回了笼统的错误,没有给出字段路径。

改进方式:在报错信息中附带 JSON Pointer 风格的路径,例如controls[1].min。如果使用 jsonschema 库,默认错误信息可以转换成更友好的格式。不要吞掉原始异常,要同时记录配置文件名和字段路径。

7. 最佳实践与扩展方向

7.1 可复用检查清单

在发布一份洗涤程序配置前,建议按下面清单检查:

  • id 是否唯一,是否符合命名规范。
  • name 是否在目标市场语言下显示正确,长度是否合适。
  • controls 数组中是否每个参数都有 label 和 type。
  • range 类型的 min、max、step、default 是否全部存在,default 是否在校验范围内。
  • select 类型的 options 是否为空,default 是否在 options 中。
  • stages 是否至少包含一个阶段,阶段顺序是否符合“进水 -> 洗涤 -> 漂洗 -> 脱水”的物理约束。
  • 每个阶段依赖的参数是否已在 controls 中暴露给用户,如果没有暴露,是否满足默认值逻辑。
  • 解析模块和 UI 模块的版本是否匹配,字段名是否有兼容性变化。

这份清单可以写成脚本,在 CI 阶段自动执行。尤其当配置会通过云端下发到设备时,配置检查应该被当作发布流程的一部分。

7.2 学习环境与生产环境差异

上面的最小案例用于说明思路,和真实洗衣机系统还有明显差距。如果要在生产环境落地,需要考虑:

  • 嵌入式端是否具备 JSON 解析能力,内存和 Flash 是否足够。如果资源紧张,可以使用二进制配置协议,但设计思路一致。
  • 真实硬件控制必须增加安全保护。例如加热阶段温度传感器异常时立即停止加热,脱水阶段门锁状态异常时不能启动电机。
  • UI 层要考虑低端屏幕的渲染性能,不能在每次渲染时重建全部控件。
  • 配置文件要有版本号,避免设备收到旧版本配置后被回退。
  • 所有状态机和配置变更都要有日志,方便远程诊断。

学习环境可以只关注功能正确性,生产环境要额外关注安全、异常、日志、权限和回滚。这些不是附加功能,而是家电设备的基本要求。

7.3 扩展方向:云端下发、远程诊断、语音控制

动态设计做好之后,扩展能力会有明显提升。常见方向包括:

  • 云端配置下发:不同地区可以在同一天更新洗涤程序,不需要用户刷固件。
  • A/B 测试:对不同用户下发不同程序交互,观察使用率。
  • 远程诊断:用户反馈问题后,从云端拉取设备端配置版本和状态机日志。
  • 语音控制:语音意图解析后,将意图转换成一组控制参数,再写入同一个 state 对象。
  • 能耗预测:把洗涤阶段参数导出给云平台,结合历史数据做能耗预测。

这些扩展方向都依赖同一个前提:程序数据、界面描述和执行流程是结构化、可校验、可版本管理的。如果一开始就把数据和逻辑写死,后续所有扩展都会受制于硬编码。

动态设计不是把简单问题复杂化,而是把复杂问题边界化。通过配置驱动界面和执行,新增程序从“改动多个模块”变成“新增一份配置”。对于多程序、多型号、多市场的家电产品来说,这个设计思路值得在项目早期就引入,而不是等到型号越来越多之后再重构。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询