配置驱动的洗衣机动态设计:从JSON到状态机实践

📅 2026/8/27 20:43:43
配置驱动的洗衣机动态设计:从JSON到状态机实践
在家电产品开发中动态设计并不是指界面动画而是指一个系统能否通过配置而不是改代码去适应不同型号、不同市场、不同用户的洗涤需求。Schulthess 这类高端洗衣机产品面板上往往有很多程序不同程序又有温度、转速、漂洗次数、预约时间等可变参数。如果每加一个程序就改一次界面代码和执行逻辑开发和测试成本会随着型号增长越来越难控制。把洗涤程序抽成配置让界面和执行引擎按配置动态生成是更可维护的做法。这篇文章会从需求出发拆解动态设计在洗衣机控制场景里的核心概念给出一个“配置定义程序 - 校验配置 - 渲染面板 - 状态机执行”的最小可运行设计并说明常见问题、排查路径和适合生产环境的扩展方向。适合正在做家电嵌入式 HMI、物联网前端或者想理解配置驱动 UI 和状态机设计的开发者阅读。1. 理清“动态设计”在洗衣机场景中的含义1.1 洗衣机控制面板不只是按钮传统洗衣机面板是一组固定按钮加一个旋钮程序列表、参数范围都写在硬件里。用户选择某个程序后面板显示对应的温度、转速和漂洗次数允许用户做有限调节。这种设计在产品型号少、功能固定时没有问题。但当产品线变大以后问题就出现了。不同型号可能有不同的最大转速、不同的加热方式、不同的脱水安全策略甚至不同销售地区的洗涤程序命名和默认参数都不一样。如果每个型号都单独维护一套界面代码和执行逻辑任何一个参数调整都要重新编译、重新烧录、重新测试版本管理也会变得很痛苦。动态设计要解决的核心问题是程序列表、参数范围、显示文案、执行流程这些内容能不能和数据分离能不能在设备不重新编译的情况下通过配置调整。1.2 动态设计的核心程序与界面分离在洗衣机这类嵌入式产品里动态设计通常包含两个层面第一层是“程序配置化”。洗涤程序不再是一段写死在代码里的 case 分支而是一条包含多个参数和多个阶段的对象。程序有哪些环节、每个环节持续多久、目标温度是多少、漂流比例是多少都从这个对象里读取。第二层是“界面配置化”。控制面板上的程序列表、参数控件、取值范围、单位显示都由配置内容驱动。配置里有温度控件界面就渲染温度调节配置里没有漂洗次数界面就不显示漂洗次数。这样不同型号可以复用同一套界面框架。这两个层面合起来才是完整的动态设计。只有程序配置化、没有界面配置化程序再动态界面还是固定模板扩展程序时依然要改面板代码。1.3 从 Schulthess 型多程序洗衣机看动态设计解决的问题以 Schulthess 这类提供多程序的机型为例用户可以选择的程序通常不止一两个而是包含棉麻、化纤、羊毛、快速洗、大件、衬衫、羽绒服等不同场景。每个场景对温度、转速、水位、漂洗次数、脱水方式的要求都不同。如果不做动态设计开发一个新程序的过程是这样的在程序列表里加一个枚举在界面里加一个图标在控件区加一段 if 分支在执行流程里加一个 case再写一大堆联动逻辑。只要其中一步忘改就会出现“界面能选程序但执行时参数不对”或者“程序能跑但界面上没有对应控件”的问题。如果做动态设计开发新程序的过程就变成写一段 JSON 配置描述程序名称、参数控件和执行阶段然后由界面引擎解析生成面板由执行引擎解析生成状态机。代码的改动量从多处修改收敛成一个配置文件回归测试的范围也变得更清晰。文章后面的案例就是围绕这条主线展开的。2. 需求分析与系统架构拆分2.1 功能需求清单在开始写代码之前先列出动态洗涤程序系统应该满足的功能需求。不要一上来就选择框架先确定边界。支持程序列表动态加载新增程序不用改主界面代码。支持参数控件动态渲染不同程序展示不同参数类型。支持参数范围配置例如温度最小值、最大值、步进值。支持执行阶段配置例如进水、加热、洗涤、漂洗、脱水。支持配置合法性校验非法配置不能进入执行流程。支持状态机按阶段顺序执行并能在每个阶段输出日志。支持在没有硬件环境时先做软件模拟方便联调 UI 和执行逻辑。从这些需求可以看出这个系统不是简单的“取配置文件然后显示出来”而是要把配置同时驱动界面和执行引擎。前者是运行时读取问题后者是状态流转问题。2.2 整体架构配置模块、解析模块、UI 模块、执行模块根据上面的需求可以把系统拆成四个模块模块职责关键输出配置模块提供程序定义数据可以是 JSON 文件、数据库记录或云端下发结果原始配置对象解析模块对配置做结构校验、字段校验、默认值补全标准化的 ProgramDefinitionUI 模块根据 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 是否小于 maxdefault 是否落在范围内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(fmissing field: {field}) for control in data[controls]: if control[type] range: if control[min] control[max]: raise ValueError( fcontrol {control[key]}: min must be less than max ) if not (control[min] control[default] control[max]): raise ValueError( fcontrol {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, encodingutf-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 程序没有出现在面板上现象程序配置文件已添加但界面列表里看不到新程序。可能原因和检查顺序配置文件是否放在程序加载目录下。配置的 id 是否与其他程序重名导致被覆盖。配置是否通过了 Schema 校验校验失败时加载模块可能直接跳过。列表渲染使用的数据源是原始文件内容还是解析后的 ProgramDefinition如果 UI 读取的是旧数据源自然不会出现新程序。排查时先在加载入口打日志输出“加载到的程序数量”和“每个程序 id”。如果加载数量正确再检查 UI 层的过滤条件例如是否误加了状态过滤或分类过滤。6.2 参数调节范围不对现象界面上温度滑杆最小值是 0最大值是 100但程序要求 20 到 95。可能原因UI 没有读取min、max而是写死了通用范围。很多动态界面首次实现时容易在控件代码里硬编码一个兜底范围导致配置里的范围没有生效。排查方式打开调试面板查看渲染函数是否拿到 control 对象的完整属性。检查input.min、input.max是否被赋值为数字而不是字符串。检查解析模块是否丢失了数字类型。JSON 中字段可能是字符串范围计算时要转成 Number 或 float。处理方案是统一在解析模块标准化数字字段不让 UI 层做类型转换。6.3 状态机执行卡在某个阶段现象程序启动后一直处于加热阶段不进入洗涤阶段。可能原因阶段之间缺少状态推进的触发条件。例如加热阶段需要检测温度到达targetTemp但传感器数据没有更新或者条件判断写反。执行模块只从配置读取了阶段列表但没有读取用户修改后的参数。如果用户在界面上把目标温度调成了 40执行时仍然使用配置里的 60就会出现温度一直不到目标值。状态机没有处理异常和超时触发条件永远不满足时没有退出机制。排查顺序是先看状态机日志当前在哪一个阶段阶段开始时的参数是什么等待条件是什么。再看实际温度是否到达目标值最后看是否配置了最大等待时间或超时保护。6.4 配置热更新导致界面闪烁或数据丢失现象设备在运行时热更新配置界面重新渲染用户刚调整的转速值被重置为默认值。原因渲染函数在每次更新时都使用配置里的 default 初始化控件而不是保留 state 中已有值。解决方案初始化控件时优先读取当前 state 的值只有 state 中没有对应 key 时才使用 defaultinput.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 对象。能耗预测把洗涤阶段参数导出给云平台结合历史数据做能耗预测。这些扩展方向都依赖同一个前提程序数据、界面描述和执行流程是结构化、可校验、可版本管理的。如果一开始就把数据和逻辑写死后续所有扩展都会受制于硬编码。动态设计不是把简单问题复杂化而是把复杂问题边界化。通过配置驱动界面和执行新增程序从“改动多个模块”变成“新增一份配置”。对于多程序、多型号、多市场的家电产品来说这个设计思路值得在项目早期就引入而不是等到型号越来越多之后再重构。