为什么不用现成的 JSON 库

PipeMonitor 的上行链路需要把传感器数据打包成 JSON 发给云端,下行链路需要解析云端下发的命令。通常的做法是引入 cJSON 或 jsmn,但这里有两个约束:

  1. newlib-nano 裁剪了 %f 浮点格式化:RT-Thread 默认使用 newlib-nano,printf 族函数不支持 %f/%g,直接 sprintf(buf, "%.3f", value) 会输出空字符串。
  2. 下行命令格式极简:只有 {"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
temp7 路温度数组: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_FLOWUNDER_FLOWOVER_PRESSUREOVER_TEMPSENSOR_FAULTMCU_RESTART。严重级别分为 infowarncritical

确认帧 (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温压温度有效
5PT100 温度有效(任意通道有合法值即置位)

云端和 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"} 中提取 cmdseq。手写了一个约 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 透传

后续阅读