为什么不用现成的 JSON 库
PipeMonitor 的上行链路需要把传感器数据打包成 JSON 发给云端,下行链路需要解析云端下发的命令。通常的做法是引入 cJSON 或 jsmn,但这里有两个约束:
- newlib-nano 裁剪了
%f浮点格式化:RT-Thread 默认使用 newlib-nano,printf族函数不支持%f/%g,直接sprintf(buf, "%.3f", value)会输出空字符串。 - 下行命令格式极简:只有
{"t":"cmd","seq":42,"cmd":"reboot"}这种固定结构,不需要完整的 JSON 解析能力。
因此上行编码用 rt_snprintf + 定点数格式化实现,下行解析手写了一个约 130 行的极简字段提取器。
上行帧格式
三种帧类型,每条以 \n 结尾:
遥测帧 (tele)
{"t":"tele","ts":1712345678,"seq":123,"dev":"FM001",
"flow":12.345,"total":1234.567,"v":0.850,"pres":0.520,
"temp":[23.1,23.2,22.9,23.0,22.8,23.1,23.4],
"heart_count":10,"valid":63}
字段说明:
| 字段 | 含义 |
|---|---|
t | 帧类型:tele / alarm / ack |
ts | 设备时间戳(Unix 秒) |
seq | 设备侧自增序号,用于云端去重和乱序检测 |
dev | 设备 ID |
flow | 瞬时流量(L/min),无效时为 null |
total | 累计流量(L),无效时为 null |
v | 流速(m/s),无效时为 null |
pres | 压力(MPa),无效时为 null |
temp | 7 路温度数组:T0 为温压一体传感器温度,T1-T6 为 PT100 通道 |
heart_count | 采集心跳计数 |
valid | 字段有效位掩码 |
告警帧 (alarm)
{"t":"alarm","ts":1712345678,"seq":124,"dev":"FM001",
"code":"OVER_FLOW","val":150.000,"severity":"warn"}
告警码包括 OVER_FLOW、UNDER_FLOW、OVER_PRESSURE、OVER_TEMP、SENSOR_FAULT、MCU_RESTART。严重级别分为 info、warn、critical。
确认帧 (ack)
{"t":"ack","ts":1712345678,"seq":125,"dev":"FM001",
"cmd_seq":42,"cmd":"reboot","result":"ok"}
设备收到下行命令后,执行并回复 ack。cmd_seq 回显命令的序号,实现请求-响应匹配。
valid 位图
valid 字段用一个 uint32 编码哪些传感器数据是有效的:
| bit | 含义 |
|---|---|
| 0 | 瞬时流量有效 |
| 1 | 累计流量有效 |
| 2 | 流速有效 |
| 3 | 压力有效 |
| 4 | 温压温度有效 |
| 5 | PT100 温度有效(任意通道有合法值即置位) |
云端和 Flutter 端可以根据位图跳过无效字段,比如 PT100 模块没接时,temp[1]~temp[6] 都是 null,UI 不会误显示 0.0。
newlib-nano 浮点格式化兼容
newlib-nano 裁剪了 %f 支持,rt_snprintf(buf, size, "%.3f", value) 什么也输出不了。解决方案是手动定点格式化:
/* 把 float 按 3 位小数输出;buf 至少 16B。无效时输出 "null"。 */
static int fmt_fixed3(char *buf, int buf_sz, rt_uint8_t valid, float value)
{
long whole;
long frac;
int neg = 0;
if (!valid) {
return rt_snprintf(buf, buf_sz, "null");
}
if (value < 0.0f) { neg = 1; value = -value; }
/* 四舍五入到小数三位 */
value += 0.0005f;
whole = (long)value;
frac = (long)((value - (float)whole) * 1000.0f);
return rt_snprintf(buf, buf_sz, "%s%ld.%03ld",
neg ? "-" : "", whole, frac);
}
先把浮点数乘以 1000 取整,再用 %ld 输出整数部分和小数部分。fmt_fixed1 同理,用于 1 位小数的温度值。
PT100 原始值是 *10 的 int16,格式化为 23.1 这种形式:
static int fmt_pt100(char *buf, int buf_sz, int16_t raw)
{
if (raw == INT16_MIN)
return rt_snprintf(buf, buf_sz, "null");
int val = (int)raw;
int neg = (val < 0) ? 1 : 0;
if (neg) val = -val;
return rt_snprintf(buf, buf_sz, "%s%d.%d",
neg ? "-" : "", val / 10, val % 10);
}
下行命令解析器
下行命令格式固定,只需要从 {"t":"cmd","seq":42,"cmd":"reboot"} 中提取 cmd 和 seq。手写了一个约 130 行的极简字段提取器,核心逻辑:
/* 在 JSON 字符串中查找 "key" 后面的值起始位置 */
static const char *json_find_value_start(const char *src, const char *key)
{
char needle[40];
rt_snprintf(needle, sizeof(needle), "\"%s\"", key);
const char *p = src;
while ((p = strstr(p, needle)) != NULL) {
const char *v = p + strlen(needle);
while (*v == ' ' || *v == '\t') v++;
if (*v == ':') {
v++;
while (*v == ' ' || *v == '\t') v++;
return v;
}
p += strlen(needle); /* 跳过字符串值里可能出现的同名文本 */
}
return NULL;
}
在此基础上封装了 json_find_string(提取引号字符串值)和 json_find_uint(提取整数值),整个解析器不到 150 行,零依赖、零动态内存分配。
上行服务中的帧发送
遥测帧以 10 秒为默认周期发送(可通过 set_upload_period 命令调整为 2/10/30/60 秒)。告警帧立即发送,不等待周期。
每帧发送后等待云端 ACK,超时 8 秒。ACK 成功才计数为 tele_sent,超时则计入 cloud_ack_timeout。传感器数据不落 flash 缓存——超时就丢弃,等待下一次实时上行。
小结
- 上行编码用定点格式化绕过 newlib-nano 的
%f裁剪,保持零外部依赖 - valid 位图让云端和 App 能正确区分”值为 0”和”传感器未接”
- 下行解析器 150 行手写代码替代 cJSON,节省 ROM 空间
- 三种帧类型(tele/alarm/ack)统一用
\n分隔,方便 DTU 透传