3. LED 控制协议设计:让设备听懂你的指令

协议设计这事儿,说白了就是给设备和上位机定一套「共同语言」。我早年做智能家居项目时吃过亏——当时图省事,直接用二进制位定义控制指令,结果后期要加新功能,整个协议都得推倒重来。从那以后,我养成了习惯:但凡涉及设备控制,先拿 JSON 把协议框架搭好。

你想想看,ESP32 通过 WebSocket 收到的是一串字符串,如果这串字符串没有固定格式,设备怎么知道你想开灯还是调亮度?所以这一节,我们就来定义这套「LED 控制语言」。

3.1 为什么选 JSON?

市面上常见的控制协议格式有几种:纯文本、XML、JSON、二进制流。我个人偏好 JSON,原因很简单——

格式 可读性 解析开销 扩展性 适用场景
纯文本 高 极低 差 简单开关
XML 中 高 好 复杂配置
JSON 高 低 好 物联网控制
二进制 差 极低 差 低功耗/高实时

ESP32 的 Arduino 生态里,ArduinoJson 库非常成熟,解析一条 JSON 指令也就几十微秒。对于 LED 控制这种毫秒级响应的场景,完全够用。

我的习惯:协议设计时,我会先画一张「指令结构图」,把字段、类型、取值范围全部列清楚,再动手写代码。这样后期改协议时,改图比改代码快得多。

3.2 指令结构总览

我们先从整体上看看这套协议长什么样。下面这张图,是我在设计协议时画的骨架——

LED 控制协议 JSON 结构 根对象 {} "cmd": 命令类型 "target": 控制目标 "params": 参数对象 "cmd" 取值: "switch" — 开关指令 "brightness" — 亮度指令 "target" 取值: "led" — 主灯 "params" 子字段: "state": 0/1 (开关) "level": 0-100 (亮度) "transition": 毫秒 (渐变) 每条指令包含三个核心字段:命令类型、控制目标、参数 参数对象根据 cmd 不同,包含不同的子字段 示例:{"cmd":"switch","target":"led","params":{"state":1}}

嗯,这里要注意:params 是一个嵌套对象,不是简单值。这样做的好处是——以后要给某个指令加参数,直接在 params 里加字段就行,不影响整体结构。

3.3 开关指令设计

开关指令是最基础的控制。我在项目中见过有人用 "on": true 这种写法,但说实话,布尔值在 JSON 解析时容易踩坑——有些库会把 true 解析成字符串。所以我习惯用整数 0/1。

开关指令的完整格式:

{
    "cmd": "switch",
    "target": "led",
    "params": {
        "state": 1,
        "transition": 200
    }
}

字段说明:

  • cmd:固定为 "switch",标识这是一条开关指令
  • target:控制目标,目前只有 "led",预留以后扩展多路灯光
  • params.state:1 开灯,0 关灯
  • params.transition:渐变时间,单位毫秒。设为 0 表示立即切换
我曾经踩过的坑:transition 字段如果缺失,ESP32 解析时会得到 0,导致灯光「啪」一下瞬间切换,用户体验很差。我建议在代码里给 transition 设个默认值,比如 300ms,这样即使前端忘了传,灯光也有个柔和的渐变效果。

3.4 亮度调节指令

亮度调节比开关复杂一点。你需要考虑两个问题:亮度范围怎么定?渐变过程怎么控制?

我个人习惯把亮度映射到 0-100 的整数范围,这样前端 slider 组件可以直接用百分比。ESP32 底层 PWM 占空比是 0-255,中间做个映射就行。

亮度指令的完整格式:

{
    "cmd": "brightness",
    "target": "led",
    "params": {
        "level": 75,
        "transition": 500
    }
}

字段说明:

  • cmd:固定为 "brightness"
  • params.level:目标亮度,取值范围 0-100。注意 0 不等于关灯,只是最暗
  • params.transition:渐变时间,单位毫秒。亮度调节建议设 300-800ms,太短会闪烁,太长响应迟钝

你可能会问:level 为 0 和 state 为 0 有什么区别?

区别大了。level=0 只是把亮度调到最低,PWM 还在工作;state=0 是彻底关断,PWM 输出低电平。前者适合「夜灯模式」,后者才是真正的关灯。

3.5 指令解析与校验

协议定好了,接下来就是 ESP32 怎么解析。我一般分三步走:

  1. 语法解析:检查 JSON 格式是否正确,能不能被 ArduinoJson 解析
  2. 字段校验:检查必要字段是否存在,类型对不对
  3. 值域校验:检查数值是否在合法范围内

下面是我在实际项目中用的解析函数骨架:

#include <ArduinoJson.h>

typedef struct {
    String cmd;
    String target;
    int state;        // -1 表示未设置
    int level;        // -1 表示未设置
    int transition;   // -1 表示未设置
} LedCommand;

bool parseLedCommand(const char* json, LedCommand* cmd) {
    // 第一步:语法解析
    StaticJsonDocument<256> doc;
    DeserializationError error = deserializeJson(doc, json);
    if (error) {
        Serial.println("JSON 解析失败");
        return false;
    }
    
    // 第二步:字段校验
    if (!doc.containsKey("cmd") || !doc.containsKey("target")) {
        Serial.println("缺少必要字段");
        return false;
    }
    
    cmd->cmd = doc["cmd"].as<String>();
    cmd->target = doc["target"].as<String>();
    
    // 第三步:根据 cmd 类型解析 params
    if (cmd->cmd == "switch") {
        if (!doc["params"].containsKey("state")) {
            Serial.println("开关指令缺少 state");
            return false;
        }
        cmd->state = doc["params"]["state"];
        cmd->transition = doc["params"]["transition"] | 300;  // 默认 300ms
        
        // 值域校验
        if (cmd->state != 0 && cmd->state != 1) {
            Serial.println("state 只能为 0 或 1");
            return false;
        }
    } 
    else if (cmd->cmd == "brightness") {
        if (!doc["params"].containsKey("level")) {
            Serial.println("亮度指令缺少 level");
            return false;
        }
        cmd->level = doc["params"]["level"];
        cmd->transition = doc["params"]["transition"] | 500;  // 默认 500ms
        
        // 值域校验
        if (cmd->level < 0 || cmd->level > 100) {
            Serial.println("level 范围 0-100");
            return false;
        }
    } 
    else {
        Serial.println("未知命令类型");
        return false;
    }
    
    return true;
}
核心要点:解析函数一定要做「防御性编程」。你永远不知道前端会发什么乱七八糟的数据过来。我见过有人把 "state": "on" 传过来,也见过 "level": 999 这种离谱值。校验做扎实了,设备才不会抽风。

3.6 协议扩展性设计

协议设计时,我习惯留一些「扩展位」。比如:

  • 多目标扩展:target 字段以后可以支持 "led1"、"led2",甚至 "all"
  • 命令扩展:cmd 字段可以加 "color"(RGB 调色)、"mode"(呼吸灯模式)
  • 参数扩展:params 里可以加 "count"(闪烁次数)、"interval"(闪烁间隔)

举个例子,以后要加 RGB 调色,指令可以这样:

{
    "cmd": "color",
    "target": "led",
    "params": {
        "r": 255,
        "g": 128,
        "b": 64,
        "transition": 400
    }
}

你看,整体结构没变,只是 params 里多了三个颜色通道。这就是 JSON 协议的好处——扩展成本极低。

我的建议:协议文档一定要写清楚每个字段的「合法值范围」和「默认值」。我习惯在代码注释里直接写一个 JSON 示例,这样别人看代码时一眼就能明白协议长什么样。

3.7 本章小结

这一节我们干了三件事:

  • 定义了 JSON 指令的通用结构:cmd + target + params
  • 设计了开关指令和亮度调节指令的具体字段和取值范围
  • 写了一套完整的解析校验函数,确保 ESP32 能安全地处理前端发来的指令