跳转至

代码

Warning

以下代码应基于发布代码中的代码,可能已更新。

offline_sensing.h

/**
 * @file offline_sensing.h
 * @author SHUAIWEN CUI (SHUAIWEN001@e.ntu.edu.sg)
 * @brief 离线传感模块 - 高频批量传感器数据采集和存储
 * @version 1.0
 * @date 2025-01-XX
 * @copyright Copyright (c) 2025
 *
 * @details
 * 该模块提供离线传感器数据采集,可配置:
 * - 采样频率(默认:100 Hz,高于在线传感)
 * - 采样时长(默认:10秒)
 * - 内存缓冲区存储(启用/禁用)
 * - SD卡存储(启用/禁用)
 * - 采样完成后MQTT报告(启用/禁用)
 *
 * 功能特性:
 * - 基于高精度定时器的采样(ESP定时器)
 * - 在内存缓冲区和/或SD卡中存储数据
 * - 采样完成后自动MQTT报告
 * - 线程安全操作
 */

#pragma once

#include <stdint.h>
#include <stdbool.h>
#include "esp_err.h"
#include "node_acc_adxl355.h"
#include "tiny_measurement_config.h"

#ifdef __cplusplus
extern "C"
{
#endif

/* ============================================================================
 * CONFIGURATION STRUCTURE
 * ============================================================================ */

/**
 * @brief 离线传感配置
 */
typedef struct
{
    float sampling_frequency_hz;      ///< 采样频率(Hz,默认:100.0)
    float sampling_duration_sec;       ///< 采样时长(秒,默认:10.0)
    bool enable_memory;                ///< 启用内存缓冲区存储(默认:true)
    bool enable_sd;                    ///< 启用SD卡存储(默认:true)
    bool enable_mqtt_report;          ///< 采样完成后启用MQTT报告(默认:true)
    const char *sd_file_path;         ///< SD卡文件路径(默认:NULL自动生成)
    const char *mqtt_report_topic;    ///< MQTT报告主题(默认:NULL使用默认)
} offline_sensing_config_t;

/**
 * @brief 离线传感样本数据结构
 */
typedef struct
{
    float x;                    ///< X轴加速度(g)
    float y;                    ///< Y轴加速度(g)
    float z;                    ///< Z轴加速度(g)
    float temp;                 ///< 温度(°C)
    uint64_t timestamp_us;      ///< 时间戳(微秒)
} offline_sensing_sample_t;

/**
 * @brief 离线传感会话报告
 */
typedef struct
{
    uint32_t total_samples;          ///< 采集的样本总数
    float actual_frequency_hz;       ///< 实际达到的采样频率
    float duration_sec;               ///< 实际时长
    bool memory_storage_success;      ///< 内存存储成功状态
    bool sd_storage_success;         ///< SD卡存储成功状态
    char sd_file_path[256];          ///< SD卡文件路径(如果已保存)
    uint64_t start_timestamp_us;     ///< 开始时间戳
    uint64_t end_timestamp_us;       ///< 结束时间戳
} offline_sensing_report_t;

/* ============================================================================
 * FUNCTION DECLARATIONS
 * ============================================================================ */

/**
 * @brief 初始化离线传感模块
 * @param config 配置结构(NULL使用默认配置)
 * @return 成功返回ESP_OK,失败返回错误码
 */
esp_err_t offline_sensing_init(const offline_sensing_config_t *config);

/**
 * @brief 为离线传感设置传感器句柄
 * @param handle ADXL355传感器句柄
 * @return 成功返回ESP_OK,失败返回错误码
 */
esp_err_t offline_sensing_set_sensor_handle(adxl355_handle_t *handle);

/**
 * @brief 启动离线传感会话
 * @return 成功返回ESP_OK,失败返回错误码
 * @note 此函数阻塞直到采样完成
 */
esp_err_t offline_sensing_start(void);

/**
 * @brief 停止离线传感会话(如果正在运行)
 * @return 成功返回ESP_OK,失败返回错误码
 */
esp_err_t offline_sensing_stop(void);

/**
 * @brief 检查离线传感是否正在运行
 * @param is_running 输出参数,true表示正在运行
 * @return 成功返回ESP_OK,失败返回错误码
 */
esp_err_t offline_sensing_is_running(bool *is_running);

/**
 * @brief 获取最后一次采样报告
 * @param report 输出参数,报告
 * @return 成功返回ESP_OK,失败返回错误码
 */
esp_err_t offline_sensing_get_report(offline_sensing_report_t *report);

/**
 * @brief 获取内存缓冲区数据(如果启用)
 * @param samples 样本的输出缓冲区
 * @param max_samples 要检索的最大样本数
 * @param actual_samples 输出参数,实际检索的样本数
 * @return 成功返回ESP_OK,失败返回错误码
 */
esp_err_t offline_sensing_get_memory_data(offline_sensing_sample_t *samples, 
                                          uint32_t max_samples, 
                                          uint32_t *actual_samples);

/**
 * @brief 清除内存缓冲区
 * @return 成功返回ESP_OK,失败返回错误码
 */
esp_err_t offline_sensing_clear_memory(void);

/**
 * @brief 反初始化离线传感模块
 * @return 成功返回ESP_OK,失败返回错误码
 */
esp_err_t offline_sensing_deinit(void);

#ifdef __cplusplus
}
#endif

offline_sensing.c

关键实现细节

定时器回调

static void sampling_timer_callback(void *arg)
{
    if (!s_is_running || s_adxl355_handle == NULL)
    {
        return;
    }

    adxl355_accelerations_t accel;
    float temperature;
    esp_err_t accel_ret = adxl355_read_accelerations(s_adxl355_handle, &accel);
    esp_err_t temp_ret = adxl355_read_temperature(s_adxl355_handle, &temperature);

    if (accel_ret == ESP_OK && temp_ret == ESP_OK)
    {
        uint64_t timestamp = esp_timer_get_time();

        // 如果启用,存储在内存缓冲区
        if (s_config.enable_memory && s_memory_buffer != NULL)
        {
            if (xSemaphoreTake(s_memory_mutex, pdMS_TO_TICKS(10)) == pdTRUE)
            {
                if (s_memory_buffer_index < s_memory_buffer_size)
                {
                    s_memory_buffer[s_memory_buffer_index].x = accel.x;
                    s_memory_buffer[s_memory_buffer_index].y = accel.y;
                    s_memory_buffer[s_memory_buffer_index].z = accel.z;
                    s_memory_buffer[s_memory_buffer_index].temp = temperature;
                    s_memory_buffer[s_memory_buffer_index].timestamp_us = timestamp;
                    s_memory_buffer_index++;
                }
                xSemaphoreGive(s_memory_mutex);
            }
        }

        s_total_samples++;
    }
}

SD卡存储

static esp_err_t save_to_sd_card(const offline_sensing_sample_t *samples, uint32_t count)
{
    // 生成带时间戳和参数的文件路径
    char file_path[256];
    if (s_config.sd_file_path != NULL)
    {
        snprintf(file_path, sizeof(file_path), "%s/%s", MOUNT_POINT, s_config.sd_file_path);
    }
    else
    {
        // 自动生成文件名
        struct tm timeinfo;
        localtime_r(&s_start_calendar_time, &timeinfo);

        int freq_int = (int)(s_config.sampling_frequency_hz + 0.5f);
        int dur_int = (int)(s_config.sampling_duration_sec + 0.5f);

        snprintf(file_path, sizeof(file_path), 
                 "%s/%04d%02d%02d%02d%02d%02d_F%04d_D%04d.csv",
                 MOUNT_POINT,
                 timeinfo.tm_year + 1900,
                 timeinfo.tm_mon + 1,
                 timeinfo.tm_mday,
                 timeinfo.tm_hour,
                 timeinfo.tm_min,
                 timeinfo.tm_sec,
                 freq_int,
                 dur_int);
    }

    // 打开文件进行写入
    FILE *f = fopen(file_path, "w");
    if (f == NULL)
    {
        ESP_LOGE(TAG, "打开文件写入失败: %s", file_path);
        return ESP_FAIL;
    }

    // 写入CSV头
    fprintf(f, "timestamp_us,x,y,z,temp\n");

    // 写入样本
    for (uint32_t i = 0; i < count; i++)
    {
        fprintf(f, "%llu,%.6f,%.6f,%.6f,%.2f\n",
                (unsigned long long)samples[i].timestamp_us,
                samples[i].x, samples[i].y, samples[i].z, samples[i].temp);
    }

    fclose(f);
    ESP_LOGI(TAG, "SD: %u 样本 -> %s", count, file_path);
    return ESP_OK;
}

启动函数

esp_err_t offline_sensing_start(void)
{
    if (!s_is_initialized)
    {
        ESP_LOGE(TAG, "离线传感未初始化");
        return ESP_ERR_INVALID_STATE;
    }

    if (s_is_running)
    {
        ESP_LOGW(TAG, "离线传感已在运行");
        return ESP_ERR_INVALID_STATE;
    }

    // 重置状态
    s_total_samples = 0;
    s_memory_buffer_index = 0;
    s_start_timestamp = esp_timer_get_time();
    s_start_calendar_time = time(NULL);

    // 计算定时器周期
    uint64_t period_us = (uint64_t)(1000000.0f / s_config.sampling_frequency_hz);

    // 创建定时器
    esp_timer_create_args_t timer_args = {
        .callback = sampling_timer_callback,
        .arg = NULL,
        .name = "offline_sampling_timer"
    };

    esp_err_t ret = esp_timer_create(&timer_args, &s_sampling_timer);
    if (ret != ESP_OK)
    {
        ESP_LOGE(TAG, "创建定时器失败: %s", esp_err_to_name(ret));
        return ret;
    }

    s_is_running = true;

    // 立即执行第一次采样
    sampling_timer_callback(NULL);

    // 启动周期定时器
    ret = esp_timer_start_periodic(s_sampling_timer, period_us);
    if (ret != ESP_OK)
    {
        ESP_LOGE(TAG, "启动定时器失败: %s", esp_err_to_name(ret));
        esp_timer_delete(s_sampling_timer);
        s_sampling_timer = NULL;
        s_is_running = false;
        return ret;
    }

    ESP_LOGI(TAG, "离线传感已启动: %.2f Hz,持续 %.2f 秒", 
             s_config.sampling_frequency_hz, s_config.sampling_duration_sec);

    // 等待采样时长(阻塞)
    uint32_t duration_ms = (uint32_t)(s_config.sampling_duration_sec * 1000.0f);
    vTaskDelay(pdMS_TO_TICKS(duration_ms));

    // 停止采样
    offline_sensing_stop();

    // 处理并保存数据
    s_end_timestamp = esp_timer_get_time();
    float actual_duration = (s_end_timestamp - s_start_timestamp) / 1000000.0f;
    float actual_frequency = s_total_samples / actual_duration;

    // 准备报告
    s_last_report.total_samples = s_total_samples;
    s_last_report.actual_frequency_hz = actual_frequency;
    s_last_report.duration_sec = actual_duration;
    s_last_report.start_timestamp_us = s_start_timestamp;
    s_last_report.end_timestamp_us = s_end_timestamp;

    // 如果启用,保存到SD卡
    if (s_config.enable_sd && s_total_samples > 0)
    {
        if (s_config.enable_memory && s_memory_buffer != NULL)
        {
            s_last_report.sd_storage_success = (save_to_sd_card(s_memory_buffer, 
                                                                 s_memory_buffer_index) == ESP_OK);
        }
    }

    s_last_report.memory_storage_success = (s_config.enable_memory && s_memory_buffer != NULL);

    // 如果启用,发送MQTT报告
    if (s_config.enable_mqtt_report)
    {
        send_mqtt_report(&s_last_report);
    }

    ESP_LOGI(TAG, "完成: %u 样本,%.2f Hz,%.2f 秒", 
             s_total_samples, s_config.sampling_frequency_hz, s_config.sampling_duration_sec);

    return ESP_OK;
}

使用示例

#include "offline_sensing.h"
#include "node_acc_adxl355.h"

// 初始化传感器
adxl355_handle_t adxl355_handle;
adxl355_init(&adxl355_handle, ADXL355_RANGE_2G, ADXL355_ODR_1000);

// 设置传感器句柄
offline_sensing_set_sensor_handle(&adxl355_handle);

// 配置离线传感
offline_sensing_config_t config = {
    .sampling_frequency_hz = 100.0f,
    .sampling_duration_sec = 10.0f,
    .enable_memory = true,
    .enable_sd = true,
    .enable_mqtt_report = true,
    .sd_file_path = NULL,  // 自动生成文件名
    .mqtt_report_topic = NULL  // 使用默认主题
};

// 初始化
offline_sensing_init(&config);

// 启动传感(阻塞直到完成)
offline_sensing_start();

// 获取报告
offline_sensing_report_t report;
offline_sensing_get_report(&report);
ESP_LOGI(TAG, "样本: %u,频率: %.2f Hz,时长: %.2f 秒",
         report.total_samples, report.actual_frequency_hz, report.duration_sec);

// 获取内存数据(可选)
offline_sensing_sample_t samples[1000];
uint32_t actual_samples;
offline_sensing_get_memory_data(samples, 1000, &actual_samples);

// 清理
offline_sensing_deinit();