3. LED 控制协议设计:让设备听懂你的指令
协议设计这事儿,说白了就是给设备和上位机定一套「共同语言」。我早年做智能家居项目时吃过亏——当时图省事,直接用二进制位定义控制指令,结果后期要加新功能,整个协议都得推倒重来。从那以后,我养成了习惯:但凡涉及设备控制,先拿 JSON 把协议框架搭好。
你想想看,ESP32 通过 WebSocket 收到的是一串字符串,如果这串字符串没有固定格式,设备怎么知道你想开灯还是调亮度?所以这一节,我们就来定义这套「LED 控制语言」。
3.1 为什么选 JSON?
市面上常见的控制协议格式有几种:纯文本、XML、JSON、二进制流。我个人偏好 JSON,原因很简单——
| 格式 | 可读性 | 解析开销 | 扩展性 | 适用场景 |
|---|---|---|---|---|
| 纯文本 | 高 | 极低 | 差 | 简单开关 |
| XML | 中 | 高 | 好 | 复杂配置 |
| JSON | 高 | 低 | 好 | 物联网控制 |
| 二进制 | 差 | 极低 | 差 | 低功耗/高实时 |
ESP32 的 Arduino 生态里,ArduinoJson 库非常成熟,解析一条 JSON 指令也就几十微秒。对于 LED 控制这种毫秒级响应的场景,完全够用。
3.2 指令结构总览
我们先从整体上看看这套协议长什么样。下面这张图,是我在设计协议时画的骨架——
嗯,这里要注意: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表示立即切换
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 怎么解析。我一般分三步走:
- 语法解析:检查 JSON 格式是否正确,能不能被
ArduinoJson解析 - 字段校验:检查必要字段是否存在,类型对不对
- 值域校验:检查数值是否在合法范围内
下面是我在实际项目中用的解析函数骨架:
#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 协议的好处——扩展成本极低。
3.7 本章小结
这一节我们干了三件事:
- 定义了 JSON 指令的通用结构:
cmd+target+params - 设计了开关指令和亮度调节指令的具体字段和取值范围
- 写了一套完整的解析校验函数,确保 ESP32 能安全地处理前端发来的指令