设备卡片¶
这是整个章节的核心内容。这里设备在门户和移动应用上获得界面——完全无需它们任何一行代码。
工作原理¶
设备发布**卡片清单** —— 机器可读的描述"什么要显示、什么要控制"。门户和应用读取清单并构建卡片:传感器变成带实时值的单元格,控制变成按钮、输入框和列表。布局也可以从固件指定。
你不需要手动发布任何东西:通过link.card()声明实体,核心自动收集清单并在连接时发送。
1. 声明实体¶
所有声明在setup()中做,s_link.begin()后。我们的过滤器有三个实体:VOC读数、模式列表和阈值字段。我们逐个讲,最后把块整合到一起。
通用原则:id与label¶
每个实体有两个名字,不要混淆:
- id —— 内部、机器名(
"voc"、"mode")。拉丁字母、数字、下划线,无空格。实体就靠id彼此识别,布局、命令和门户都用它。起好一次就不改; - label —— 人类的标签(
"VOC index"、"Mode")。你写什么用户就看什么。可随意改。
传感器:VOC读数¶
s_link.card().sensor(
"voc", // id:实体的内部名
"VOC index", // label:卡片上的标签
"", // unit:单位显示在数字右侧("°C"、"%"、"g");
// VOC指数没有单位——空字符串
"units[0].vocIndex" // path:数值从哪里取——遥测JSON内的路径。
// 这正是我们在第5章添加的字段:
// doc["units"][0]["vocIndex"]。名字必须完全一致,
// 否则卡片上显示破折号。
);
传感器是"只读"单元格:门户用path从遥测中取值并显示。传感器没有命令。
选择列表:工作模式¶
// 列表选项。用户在下拉菜单中看到它们的原样。
static const char* kModes[] = { "auto", "on", "off" };
s_link.card().select(
"mode", // id:实体的内部名
"Mode", // label:卡片上的标签
kModes, // options:选项数组(上面声明的)
3, // 数组中的选项个数——auto、on、off = 三个。
// C++自己不知道数组长度,由我们告诉
[](const char* opt) { // 回调:函数,当用户在门户上选择选项时
// 核心会调用它。opt —— 选中的字符串,比如"on"
onModeSelected(opt); // 传给我们的逻辑(会在第7章写)
}
);
这里出现了机制的第二部分:控制。当用户在门户上选择选项,设备收到命令,核心自己接收并检查(不在options中的陌生字符串不会到你这里),然后用选中的值调用你的回调。不需要手动解析MQTT消息——你的责任从onModeSelected内部开始。
数字字段:启动阈值¶
s_link.card().number(
"threshold", // id:实体的内部名
"VOC threshold", // label:卡片上的标签
100, // min:门户不会让输入更小
400, // max:也不会更大;核心还会
// 在自己一端按这些边界剪裁
10, // step:用上下箭头改变的步长
"", // unit:单位;指数没有
[](float v) { // 回调:用户发送新值时调用;
// v —— min..max范围内的数字
onThresholdChanged(v); // 传给我们的逻辑(会在第7章写)
}
);
整合到一起¶
最终的块应该在setup()中。onModeSelected和onThresholdChanged函数会在第7章写;为了现在代码能编译,在setup()上面声明它们的占位符:
// 占位符:真正的实现会在第7章写(自动逻辑)。
static void onModeSelected(const char* opt) {}
static void onThresholdChanged(float v) {}
void setup() {
Serial.begin(115200);
s_link.begin();
initVocSensor();
// 遥测:自定义vocIndex字段(第5章)。
s_link.onTelemetryPublish([](JsonObject doc) {
if (g_vocIndex >= 0) {
doc["units"][0]["vocIndex"] = g_vocIndex;
}
});
// 卡片:传感器 + 两个控制器。
s_link.card().sensor("voc", "VOC index", "", "units[0].vocIndex");
static const char* kModes[] = { "auto", "on", "off" };
s_link.card().select("mode", "Mode", kModes, 3, [](const char* opt) {
onModeSelected(opt);
});
s_link.card().number("threshold", "VOC threshold", 100, 400, 10, "", [](float v) {
onThresholdChanged(v);
});
}
风机呢?**不用**声明它:Config中的hasFan = true标志已经自动向清单添加了"风机"单元——这是字典功能,核心自己知道关于它的一切。
回调中的方括号总是空的
[](const char* opt) { ... } —— 这是lambda,无名函数;我们在第5章的注记中详细讲过。提醒一下核心规则:捕获括号总是空的([]),lambda不"带走"任何东西,所有需要的存在全局变量中 —— 像下一章的g_mode和g_threshold。
2. 自动卡片布局¶
根本不用指定布局。门户会自己从声明的实体组装卡片——而且组装得很整齐:显示单元格按行分组(一行最多三个,然后折行),控制器排在下面,每个占一行,全部用门户官方风格。对大多数设备这够了——界面看起来整洁而无需考虑排版。
卡片上的实体顺序 —— 它们在setup()中声明的顺序。
3. 自定义卡片布局(可选)¶
先看卡片怎么组织。卡片是竖直的**行**堆。一行是水平条,放一到四个实体;它们平分宽度:一个实体占满宽度,两个各占一半,三个各占三分之一。
前一节的自动布局自己把实体放在这些行中。如果你要自己决定什么和什么并排——手动指定行,用layoutRow调用。一个调用 = 一行,调用顺序 = 行顺序从上到下:
// 行1:两个单元格——VOC指数和风机,各占一半。
s_link.card().layoutRow("voc", "fan");
// 行2:两个控制器——模式和阈值,也各占一半。
s_link.card().layoutRow("mode", "threshold");
layoutRow传的是实体的**id** —— 你给它们起的那些内部名字(这就是id的用处)。"fan"是风机字典实体的id,由hasFan标志创建。
卡片上会得到这样的布局:
┌─ DIY Air Filter ────────────────┐
│ VOC index │ 风机 │ ← 行1:voc、fan
│ 103 │ 关 │
├───────────────────┼─────────────┤
│ 模式 [自动 ▾] │ 阈值 [150] │ ← 行2:mode、threshold
└─────────────────────────────────┘
你没在任何行中提到的实体不会消失 —— 门户会在下面用自动列表补齐。这样你可以只排版"主要"部分,其余交给自动。
4. 发布到MQTT的内容¶
核心会发布到主题idryer/{serial}/card(retained):
{
"v": 1,
"entities": [
{ "id": "fan", "type": "binary_sensor", "device_class": "fan",
"source": "telemetry", "path": "units[0].fanStatus" },
{ "id": "voc", "type": "sensor", "label": "VOC index",
"source": "telemetry", "path": "units[0].vocIndex" },
{ "id": "mode", "type": "select", "label": "Mode",
"options": ["auto", "on", "off"], "action": "card.mode", "arg": "value" },
{ "id": "threshold", "type": "number", "label": "VOC threshold",
"min": 100, "max": 400, "step": 10, "action": "card.threshold", "arg": "value" }
],
"layout": [ ["voc", "fan"], ["mode", "threshold"] ]
}
不必深究这个JSON——核心从你的调用自动生成它。但了解一下有用:如果你写固件**不用**idryer-core(Rust、MicroPython,随意),只需自己发布这样的JSON —— 门户兼容任何格式相符的内容。
5. 检查¶
烧入并在门户上打开设备:
- **VOC index**单元格显示实时指数(对传感器吹气——数字在下次更新时增长);
- **风机**单元格 —— 打开/关闭;
- Mode —— 下拉列表,VOC threshold —— 带发送按钮的字段。
选择模式和阈值现在什么都不做——回调是占位符。我们将在下一章激活它们。
这正是那个概念
注意发生了什么:你在固件中用五行代码描述了界面——它在门户和应用中出现了。同样的手法对你任何设备都行:只改id、标签和回调。