双层存储需求

PipeMonitor 有两种存储需求:

  1. CSV 历史记录:供现场人员用读卡器导出,做离线分析和溯源
  2. 上行帧缓存:网络中断时暂存未送达的遥测/报警帧,恢复后自动补传

两者都用 LittleFS 文件系统,挂在 SPI NOR Flash 上,通过 FAL(Flash Abstraction Layer)管理分区。

存储线程:批量刷盘

采集线程每 2 秒产生一条测量快照,如果每次都写 Flash,写入频率太高会加速 NOR Flash 磨损。所以引入一个独立的存储线程,做批量刷盘:

#define STORAGE_BATCH_COUNT          4U    /* 最多攒 4 条一起写 */
#define STORAGE_FLUSH_PERIOD_MS      5000U /* 最长 5 秒刷一次 */

存储线程从消息队列里收快照,攒够 4 条或超时 5 秒后,一次性写入 CSV:

while (1)
{
    received = rt_mq_recv(&g_storage_mq, &measurement, sizeof(measurement),
                          rt_tick_from_millisecond(STORAGE_FLUSH_PERIOD_MS));

    if (received == sizeof(measurement))
    {
        /* 攒到 batch_buffer 里 */
        memcpy(&batch_buffer[batch_length], line_buffer, line_length);
        batch_length += line_length;
        batch_count++;

        if (batch_count >= STORAGE_BATCH_COUNT)
        {
            app_storage_write_batch(batch_buffer, batch_length);
            batch_length = 0;
            batch_count = 0;
        }
    }
    else if (batch_length > 0)
    {
        /* 超时,把剩余的刷出去 */
        app_storage_write_batch(batch_buffer, batch_length);
        batch_length = 0;
        batch_count = 0;
    }
}

这样 Flash 写入频率从每 2 秒一次降低到每 8-10 秒一次,同时保证断电最多丢 4 条数据。

CSV 文件格式

CSV 文件路径 /data/measurements.csv,表头:

timestamp,uptime_ms,flow_lpm,total_l,velocity_ms,pxw_temp_c,pxw_pres_mpa,
temp_4_1_c,temp_4_2_c,temp_5_1_c,temp_5_2_c,temp_5_3_c,temp_5_4_c

每行一条采样记录,时间戳用 YYYY-MM-DD HH:MM:SS 格式。如果 RTC 没同步,用 boot+Ns 格式表示启动后秒数。

static void app_storage_format_timestamp(char *buffer, rt_size_t size,
                                          time_t timestamp, rt_tick_t tick)
{
    if (timestamp <= 0 || local_tm.tm_year < (2024 - 1900))
    {
        rt_snprintf(buffer, size, "boot+%lus", app_tick_to_millisecond(tick) / 1000);
        return;
    }
    strftime(buffer, size, "%Y-%m-%d %H:%M:%S", &local_tm);
}

无效传感器通道输出空字段,用连续逗号表示缺失值。

CSV Schema 迁移

如果表头格式变了(比如新增传感器通道),旧文件的表头和新代码不匹配。启动时会检查:

static int app_storage_prepare_file_schema(const char *csv_header)
{
    /* 读文件前几个字节,和期望的表头比较 */
    read(fd, header_buffer, expected_length);

    if (memcmp(header_buffer, csv_header, expected_length) == 0)
        return 0;  /* 匹配,继续追加 */

    /* 不匹配,把旧文件重命名为 legacy */
    app_storage_rotate_legacy_file();
    return 1;  /* 需要写新表头 */
}

旧文件重命名为 measurements_legacy.csvmeasurements_legacy_1.csv……最多保留 10 个。

离线上行缓存

当云端 ACK 超时或串口写入失败时,上行帧被追加到 /data/pending.jsonl

int storage_pending_append(const char *line)
{
    fd = open("/data/pending.jsonl", O_WRONLY | O_CREAT | O_APPEND, 0);
    write(fd, line, len);
    if (line[len-1] != '\n')
        write(fd, "\n", 1);  /* 确保每行以换行结尾 */
    close(fd);
}

每行一条完整的 JSON 帧,和上行时发送的格式完全一样。

补传策略

网络恢复后,上行线程每轮循环补传一条缓存帧:

int storage_pending_replay_one(storage_pending_send_cb_t send_cb, void *user_data)
{
    /* 读第一条 */
    storage_pending_read_first_line(line, sizeof(line));

    /* 尝试发送 */
    if (send_cb(line, user_data) != 0)
        return -1;  /* 发送失败,保留缓存 */

    /* 发送成功,删除第一条 */
    storage_pending_drop_first_line();
    return 1;
}

删除采用”读剩余 → 写临时文件 → rename 替换”的方式:

static int storage_pending_drop_first_line(void)
{
    /* 跳过第一行 */
    while (read(in_fd, &ch, 1) == 1 && ch != '\n') { }

    /* 把剩余内容写到 pending.tmp */
    while ((n = read(in_fd, buf, sizeof(buf))) > 0)
        write(out_fd, buf, n);

    close(in_fd);
    close(out_fd);

    /* LittleFS 的 rename 是断电安全的 */
    rename("/data/pending.tmp", "/data/pending.jsonl");
}

LittleFS 的 rename 操作是原子性的,即使在 rename 过程中断电,文件系统也能保持一致。

断电安全分析

操作断电风险后果
CSV 追加写入丢最后一批未刷盘的数据最多丢 4 条采样
pending 追加写入帧可能不完整补传时 JSON 解析失败,丢弃该行
pending 删除第一条rename 中断LittleFS 保证一致性,不会损坏

msh 调试命令

  • storage_pending — 查看缓存行数
  • storage_pending clear — 清空缓存
  • storage_rawtest — NOR Flash 原始读写测试
  • storage_fstest — LittleFS 文件读写测试

这些命令在部署时很有用,可以验证 Flash 和文件系统是否正常工作。

存储容量估算

假设每条 JSON 帧 ~200 字节,每 10 秒一帧:

  • 1 小时:360 帧 × 200B = 72KB
  • 24 小时:8640 帧 × 200B = 1.7MB
  • 7 天:60480 帧 × 200B = 12MB

如果网络中断 7 天,pending 缓存会占用约 12MB。通常 SPI NOR Flash 容量在 1-16MB 之间,需要根据实际 Flash 大小评估缓存上限。

CSV 文件增长速度类似,可以通过 measurements_legacy 轮转机制控制单文件大小。

小结

双层存储设计:

  • CSV:面向人,供离线分析,批量写入减少 Flash 磨损
  • pending.jsonl:面向机器,供自动补传,逐条追加逐条删除

两者都依赖 LittleFS 的断电安全特性,保证在任何时刻断电都不会损坏文件系统。补传机制配合云端 ACK,实现了”至少一次”的送达语义。

后续阅读