跳转至

代码

Warning

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

offline_sensing.h

/**
 * @file offline_sensing.h
 * @author SHUAIWEN CUI (SHUAIWEN001@e.ntu.edu.sg)
 * @brief 离线传感模块 - 高频批量传感器数据采集和存储
 * @version 1.0
 * @date 2025-12-17
 * @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;

/* ============================================================================
 * DEFAULT CONFIGURATION
 * ============================================================================ */

/**
 * @brief 默认配置值(来自tiny_measurement_config.h宏)
 */
#define OFFLINE_SENSING_DEFAULT_FREQ_HZ TINY_MEASUREMENT_OFFLINE_SENSING_DEFAULT_FREQ_HZ
#define OFFLINE_SENSING_DEFAULT_DURATION_SEC TINY_MEASUREMENT_OFFLINE_SENSING_DEFAULT_DURATION_SEC
#define OFFLINE_SENSING_DEFAULT_ENABLE_MEMORY TINY_MEASUREMENT_OFFLINE_SENSING_DEFAULT_ENABLE_MEMORY
#define OFFLINE_SENSING_DEFAULT_ENABLE_SD TINY_MEASUREMENT_OFFLINE_SENSING_DEFAULT_ENABLE_SD
#define OFFLINE_SENSING_DEFAULT_ENABLE_MQTT_REPORT TINY_MEASUREMENT_OFFLINE_SENSING_DEFAULT_ENABLE_MQTT_REPORT

/* ============================================================================
 * 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

/**
 * @file offline_sensing.c
 * @author SHUAIWEN CUI (SHUAIWEN001@e.ntu.edu.sg)
 * @brief Offline sensing module implementation
 * @version 1.0
 * @date 2025-12-17
 * @copyright Copyright (c) 2025
 *
 */

#include "offline_sensing.h"
#include "node_mqtt.h"
#include "node_sdcard.h"
#include "esp_log.h"
#include "esp_timer.h"
#include "esp_heap_caps.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/semphr.h"
#include <string.h>
#include <stdio.h>
#include <time.h>
#include <inttypes.h>
#include <errno.h>

static const char *TAG = "OfflineSensing";

/* ============================================================================
 * PRIVATE VARIABLES
 * ============================================================================ */

static offline_sensing_config_t s_config;
static adxl355_handle_t *s_adxl355_handle = NULL;
static bool s_is_running = false;
static bool s_is_initialized = false;

// Memory buffer
static offline_sensing_sample_t *s_memory_buffer = NULL;
static uint32_t s_memory_buffer_size = 0;
static uint32_t s_memory_buffer_index = 0;
static SemaphoreHandle_t s_memory_mutex = NULL;

// Timer
static esp_timer_handle_t s_sampling_timer = NULL;

// Sampling state
static uint32_t s_total_samples = 0;
static uint32_t s_expected_samples = 0;  // Expected number of samples (freq * duration + 1)
static uint64_t s_start_timestamp = 0;  // Microseconds since boot (from esp_timer_get_time)
static time_t s_start_calendar_time = 0; // Calendar time at sampling start (from time(NULL))
static uint64_t s_end_timestamp = 0;
static offline_sensing_report_t s_last_report;

// MQTT client (from node_mqtt.h)
extern esp_mqtt_client_handle_t s_mqtt_client;
extern bool s_is_mqtt_connected;

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

static void sampling_timer_callback(void *arg);
static esp_err_t save_to_sd_card(const offline_sensing_sample_t *samples, uint32_t count);
static esp_err_t send_mqtt_report(const offline_sensing_report_t *report);

/* ============================================================================
 * TIMER CALLBACK
 * ============================================================================ */

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();

        // Store in memory buffer if enabled
        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++;

        // Auto-stop when expected number of samples is reached
        // Expected samples = (freq * duration) + 1 (for t=0 sample)
        if (s_expected_samples > 0 && s_total_samples >= s_expected_samples)
        {
            s_is_running = false; // Stop further sampling
        }
    }
}

/* ============================================================================
 * SD CARD STORAGE
 * ============================================================================ */

static esp_err_t save_to_sd_card(const offline_sensing_sample_t *samples, uint32_t count)
{
    if (samples == NULL || count == 0)
    {
        return ESP_ERR_INVALID_ARG;
    }

    // Generate file path with sampling start time, frequency, and duration
    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
    {
        // Auto-generate filename with sampling start time, frequency, and duration
        // Format: offline_YYYYMMDD_HHMMSS_FreqXXXHz_DurXXs.csv
        struct tm timeinfo;
        bool use_relative_time = false;

        if (s_start_calendar_time != 0)
        {
            localtime_r(&s_start_calendar_time, &timeinfo);
            // Check if time is valid (not 1970-01-01, which indicates unset time)
            if (timeinfo.tm_year < 70)  // tm_year is years since 1900, so 70 means 1970
            {
                ESP_LOGW(TAG, "System time appears unset (1970), using relative timestamp");
                use_relative_time = true;
            }
        }
        else
        {
            // Fallback to current time if calendar time not set
            time_t now = time(NULL);
            localtime_r(&now, &timeinfo);
            if (timeinfo.tm_year < 70)
            {
                ESP_LOGW(TAG, "System time appears unset (1970), using relative timestamp");
                use_relative_time = true;
            }
        }

        // Format frequency and duration for filename (4 digits each, zero-padded)
        int freq_int = (int)(s_config.sampling_frequency_hz + 0.5f);
        int dur_int = (int)(s_config.sampling_duration_sec + 0.5f);

        if (use_relative_time)
        {
            // Use relative timestamp (seconds since boot) if system time is not set
            // Format: BootXXXXX_FXXXX_DXXXX.csv
            uint64_t boot_time_sec = s_start_timestamp / 1000000ULL;
            snprintf(file_path, sizeof(file_path), "%s/Boot%05llu_F%04d_D%04d.csv",
                     MOUNT_POINT,
                     (unsigned long long)boot_time_sec % 100000,  // Last 5 digits of boot time
                     freq_int,                                   // Frequency (4 digits, zero-padded)
                     dur_int);                                   // Duration (4 digits, zero-padded)
        }
        else
        {
            // Format: YYYYMMDDHHMMSS_FXXXX_DXXXX.csv
            // Includes full date/time (year, month, day, hour, minute, second)
            // and sampling parameters (frequency and duration, both 4 digits zero-padded)
            snprintf(file_path, sizeof(file_path), "%s/%04d%02d%02d%02d%02d%02d_F%04d_D%04d.csv",
                     MOUNT_POINT,
                     timeinfo.tm_year + 1900,  // YYYY (4 digits)
                     timeinfo.tm_mon + 1,       // MM (2 digits)
                     timeinfo.tm_mday,          // DD (2 digits)
                     timeinfo.tm_hour,          // HH (2 digits)
                     timeinfo.tm_min,           // MM (2 digits)
                     timeinfo.tm_sec,           // SS (2 digits)
                     freq_int,                 // Frequency (4 digits, zero-padded)
                     dur_int);                 // Duration (4 digits, zero-padded)
        }
    }

    // Open file for writing
    FILE *f = fopen(file_path, "w");
    if (f == NULL)
    {
        ESP_LOGE(TAG, "Failed to open file for writing: %s", file_path);
        ESP_LOGE(TAG, "Error: %s (errno: %d)", strerror(errno), errno);

        // Try to check if directory exists and is writable
        FILE *test_f = fopen(MOUNT_POINT "/.test_write", "w");
        if (test_f == NULL)
        {
            ESP_LOGE(TAG, "SD card directory may not be writable. Check SD card mount status.");
        }
        else
        {
            fclose(test_f);
            remove(MOUNT_POINT "/.test_write");
            ESP_LOGE(TAG, "Directory is writable, but file creation failed. Check filename length or invalid characters.");
        }

        return ESP_FAIL;
    }

    // Write CSV header
    fprintf(f, "timestamp_us,x,y,z,temp\n");

    // Write samples
    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 samples -> %s", count, file_path);

    // Update report with file path
    strncpy(s_last_report.sd_file_path, file_path, sizeof(s_last_report.sd_file_path) - 1);
    s_last_report.sd_file_path[sizeof(s_last_report.sd_file_path) - 1] = '\0';

    return ESP_OK;
}

/* ============================================================================
 * MQTT REPORT
 * ============================================================================ */

static esp_err_t send_mqtt_report(const offline_sensing_report_t *report)
{
    if (report == NULL)
    {
        return ESP_ERR_INVALID_ARG;
    }

    if (!s_is_mqtt_connected || s_mqtt_client == NULL)
    {
        ESP_LOGW(TAG, "MQTT not connected, skipping report");
        return ESP_ERR_INVALID_STATE;
    }

    // Format JSON report
    char report_buff[512];
    snprintf(report_buff, sizeof(report_buff),
             "{\"samples\":%" PRIu32 ",\"freq_hz\":%.2f,\"duration_sec\":%.2f,"
             "\"memory_ok\":%s,\"sd_ok\":%s,\"sd_path\":\"%s\","
             "\"start_us\":%" PRIu64 ",\"end_us\":%" PRIu64 "}",
             report->total_samples,
             report->actual_frequency_hz,
             report->duration_sec,
             report->memory_storage_success ? "true" : "false",
             report->sd_storage_success ? "true" : "false",
             report->sd_file_path[0] != '\0' ? report->sd_file_path : "",
             report->start_timestamp_us,
             report->end_timestamp_us);

    const char *topic = s_config.mqtt_report_topic ? s_config.mqtt_report_topic : "/offline_sensing/report";
    int mqtt_ret = esp_mqtt_client_publish(s_mqtt_client, topic, report_buff, strlen(report_buff), 1, 0);

    if (mqtt_ret < 0)
    {
        ESP_LOGE(TAG, "MQTT publish failed: %d", mqtt_ret);
        return ESP_FAIL;
    }

    ESP_LOGD(TAG, "MQTT report sent");
    return ESP_OK;
}

/* ============================================================================
 * PUBLIC FUNCTIONS
 * ============================================================================ */

esp_err_t offline_sensing_init(const offline_sensing_config_t *config)
{
    if (s_is_initialized)
    {
        ESP_LOGW(TAG, "Offline sensing already initialized");
        return ESP_ERR_INVALID_STATE;
    }

    // Apply configuration
    if (config != NULL)
    {
        memcpy(&s_config, config, sizeof(offline_sensing_config_t));
    }
    else
    {
        // Use default configuration
        s_config.sampling_frequency_hz = OFFLINE_SENSING_DEFAULT_FREQ_HZ;
        s_config.sampling_duration_sec = OFFLINE_SENSING_DEFAULT_DURATION_SEC;
        s_config.enable_memory = OFFLINE_SENSING_DEFAULT_ENABLE_MEMORY;
        s_config.enable_sd = OFFLINE_SENSING_DEFAULT_ENABLE_SD;
        s_config.enable_mqtt_report = OFFLINE_SENSING_DEFAULT_ENABLE_MQTT_REPORT;
        s_config.sd_file_path = NULL;
        s_config.mqtt_report_topic = NULL;
    }

    // Validate configuration
    // Note: Maximum frequency of 4000 Hz is limited by ADXL355 sensor's maximum output data rate (ODR)
    // ADXL355 supports up to 4000 Hz ODR, which is the hardware limit for this sensor
    if (s_config.sampling_frequency_hz < 1.0f || s_config.sampling_frequency_hz > 4000.0f)
    {
        ESP_LOGE(TAG, "Invalid sampling frequency: %.2f Hz (valid range: 1 - 4000 Hz, max limited by ADXL355 ODR)",
                 s_config.sampling_frequency_hz);
        return ESP_ERR_INVALID_ARG;
    }

    if (s_config.sampling_duration_sec < 0.1f || s_config.sampling_duration_sec > 3600.0f)
    {
        ESP_LOGE(TAG, "Invalid sampling duration: %.2f sec (valid range: 0.1 - 3600 sec)",
                 s_config.sampling_duration_sec);
        return ESP_ERR_INVALID_ARG;
    }

    // Allocate memory buffer if enabled
    // Use PSRAM (external RAM) to support larger buffer sizes for high-frequency/long-duration sampling
    if (s_config.enable_memory)
    {
        uint32_t buffer_size = (uint32_t)(s_config.sampling_frequency_hz * s_config.sampling_duration_sec) + 100; // Add margin
        size_t required_bytes = buffer_size * sizeof(offline_sensing_sample_t);

        // Check PSRAM availability
        size_t psram_free = heap_caps_get_free_size(MALLOC_CAP_SPIRAM);
        if (psram_free < required_bytes)
        {
            ESP_LOGE(TAG, "PSRAM insufficient: available %zu bytes, need %zu bytes",
                     psram_free, required_bytes);
            return ESP_ERR_NO_MEM;
        }

        // Allocate from PSRAM explicitly to utilize external memory (typically 8MB)
        s_memory_buffer = (offline_sensing_sample_t *)heap_caps_malloc(
            required_bytes,
            MALLOC_CAP_SPIRAM  // Use PSRAM for large buffer
        );
        if (s_memory_buffer == NULL)
        {
            ESP_LOGE(TAG, "Failed to allocate memory buffer from PSRAM");
            ESP_LOGE(TAG, "Available PSRAM: %zu bytes", heap_caps_get_free_size(MALLOC_CAP_SPIRAM));
            return ESP_ERR_NO_MEM;
        }
        s_memory_buffer_size = buffer_size;
        s_memory_buffer_index = 0;
        ESP_LOGD(TAG, "Allocated memory buffer for %u samples", buffer_size);
    }

    // Create mutex for memory buffer
    s_memory_mutex = xSemaphoreCreateMutex();
    if (s_memory_mutex == NULL)
    {
        ESP_LOGE(TAG, "Failed to create mutex");
        if (s_memory_buffer != NULL)
        {
            heap_caps_free(s_memory_buffer);
            s_memory_buffer = NULL;
        }
        return ESP_ERR_NO_MEM;
    }

    // Initialize report
    memset(&s_last_report, 0, sizeof(offline_sensing_report_t));
    s_last_report.sd_file_path[0] = '\0';

    s_is_initialized = true;

    return ESP_OK;
}

esp_err_t offline_sensing_set_sensor_handle(adxl355_handle_t *handle)
{
    if (handle == NULL)
    {
        return ESP_ERR_INVALID_ARG;
    }
    s_adxl355_handle = handle;
    return ESP_OK;
}

esp_err_t offline_sensing_start(void)
{
    if (!s_is_initialized)
    {
        ESP_LOGE(TAG, "Offline sensing not initialized");
        return ESP_ERR_INVALID_STATE;
    }

    if (s_is_running)
    {
        ESP_LOGW(TAG, "Offline sensing already running");
        return ESP_ERR_INVALID_STATE;
    }

    if (s_adxl355_handle == NULL)
    {
        ESP_LOGE(TAG, "Sensor handle not set");
        return ESP_ERR_INVALID_STATE;
    }

    // Reset state
    s_total_samples = 0;
    s_memory_buffer_index = 0;
    // Calculate expected number of samples: (freq * duration) + 1 (for t=0 sample)
    s_expected_samples = (uint32_t)(s_config.sampling_frequency_hz * s_config.sampling_duration_sec) + 1;
    s_start_timestamp = esp_timer_get_time();  // Microseconds since boot
    s_start_calendar_time = time(NULL);        // Calendar time for filename generation

    // Calculate timer period in microseconds
    uint64_t period_us = (uint64_t)(1000000.0f / s_config.sampling_frequency_hz);

    // Create timer
    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, "Failed to create timer: %s", esp_err_to_name(ret));
        return ret;
    }

    // Set running flag before starting timer (so callback can execute)
    s_is_running = true;

    // Perform immediate first sample (at t=0) before starting periodic timer
    // This ensures we get exactly N samples for N seconds at N Hz
    sampling_timer_callback(NULL);

    // Start periodic timer (first periodic sample will be at t=period_us)
    ret = esp_timer_start_periodic(s_sampling_timer, period_us);
    if (ret != ESP_OK)
    {
        ESP_LOGE(TAG, "Failed to start timer: %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, "Offline sensing started: %.2f Hz for %.2f sec (expected samples: %u)", 
             s_config.sampling_frequency_hz, s_config.sampling_duration_sec, s_expected_samples);

    // Wait for sampling duration
    // The timer callback will auto-stop when expected_samples is reached
    uint32_t duration_ms = (uint32_t)(s_config.sampling_duration_sec * 1000.0f);
    // Add small margin to ensure all samples are collected
    vTaskDelay(pdMS_TO_TICKS(duration_ms + 100));

    // Ensure sampling is stopped
    s_is_running = false;

    // Small delay to ensure any in-flight callback completes
    vTaskDelay(pdMS_TO_TICKS(10));

    // Stop sampling
    offline_sensing_stop();

    // Process and save data
    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;

    // Prepare report
    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;

    // Save to SD card if enabled
    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);
        }
        else
        {
            // Would need to re-read from sensor or use a different approach
            ESP_LOGW(TAG, "SD card storage requires memory buffer");
            s_last_report.sd_storage_success = false;
        }
    }
    else
    {
        s_last_report.sd_storage_success = false;
    }

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

    // Send MQTT report if enabled
    if (s_config.enable_mqtt_report)
    {
        send_mqtt_report(&s_last_report);
    }

    ESP_LOGI(TAG, "Completed: %u samples, %.2f Hz, %.2f sec", 
             s_total_samples, s_config.sampling_frequency_hz, s_config.sampling_duration_sec);

    return ESP_OK;
}

esp_err_t offline_sensing_stop(void)
{
    if (!s_is_running)
    {
        return ESP_OK;
    }

    if (s_sampling_timer != NULL)
    {
        esp_timer_stop(s_sampling_timer);
        esp_timer_delete(s_sampling_timer);
        s_sampling_timer = NULL;
    }

    s_is_running = false;
    return ESP_OK;
}

esp_err_t offline_sensing_is_running(bool *is_running)
{
    if (is_running == NULL)
    {
        return ESP_ERR_INVALID_ARG;
    }
    *is_running = s_is_running;
    return ESP_OK;
}

esp_err_t offline_sensing_get_report(offline_sensing_report_t *report)
{
    if (report == NULL)
    {
        return ESP_ERR_INVALID_ARG;
    }
    memcpy(report, &s_last_report, sizeof(offline_sensing_report_t));
    return ESP_OK;
}

esp_err_t offline_sensing_get_memory_data(offline_sensing_sample_t *samples, uint32_t max_samples, uint32_t *actual_samples)
{
    if (samples == NULL || actual_samples == NULL)
    {
        return ESP_ERR_INVALID_ARG;
    }

    if (!s_config.enable_memory || s_memory_buffer == NULL)
    {
        *actual_samples = 0;
        return ESP_ERR_INVALID_STATE;
    }

    if (xSemaphoreTake(s_memory_mutex, pdMS_TO_TICKS(100)) == pdTRUE)
    {
        uint32_t copy_count = (s_memory_buffer_index < max_samples) ? s_memory_buffer_index : max_samples;
        memcpy(samples, s_memory_buffer, copy_count * sizeof(offline_sensing_sample_t));
        *actual_samples = copy_count;
        xSemaphoreGive(s_memory_mutex);
        return ESP_OK;
    }

    return ESP_ERR_TIMEOUT;
}

esp_err_t offline_sensing_clear_memory(void)
{
    if (s_memory_buffer == NULL)
    {
        return ESP_ERR_INVALID_STATE;
    }

    if (xSemaphoreTake(s_memory_mutex, pdMS_TO_TICKS(100)) == pdTRUE)
    {
        s_memory_buffer_index = 0;
        xSemaphoreGive(s_memory_mutex);
        return ESP_OK;
    }

    return ESP_ERR_TIMEOUT;
}

esp_err_t offline_sensing_deinit(void)
{
    if (!s_is_initialized)
    {
        return ESP_OK;
    }

    // Stop if running
    offline_sensing_stop();

    // Free memory buffer
    if (s_memory_buffer != NULL)
    {
        heap_caps_free(s_memory_buffer);
        s_memory_buffer = NULL;
    }

    // Delete mutex
    if (s_memory_mutex != NULL)
    {
        vSemaphoreDelete(s_memory_mutex);
        s_memory_mutex = NULL;
    }

    // Clear report file path
    s_last_report.sd_file_path[0] = '\0';

    s_is_initialized = false;
    return ESP_OK;
}

关键实现

采样频率限制

4000 Hz的最大采样频率由**ADXL355传感器的硬件能力**(最大ODR为4000 Hz)决定。离线感知模块架构本身是传感器无关的,如果使用具有更高ODR能力的传感器,理论上可以支持更高的频率。ESP定时器系统和内存缓冲区设计能够处理超过4000 Hz的频率。

定时器回调

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++;

        // 达到预期样本数时自动停止
        // 预期样本数 = (频率 × 时长) + 1(包括t=0的样本)
        if (s_expected_samples > 0 && s_total_samples >= s_expected_samples)
        {
            s_is_running = false; // 停止进一步采样
        }
    }
}

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
    {
        // 自动生成带采样开始时间、频率和时长的文件名
        // Format: offline_YYYYMMDD_HHMMSS_FreqXXXHz_DurXXs.csv
        struct tm timeinfo;
        bool use_relative_time = false;

        if (s_start_calendar_time != 0)
        {
            localtime_r(&s_start_calendar_time, &timeinfo);
            // 检查时间是否有效(不是1970-01-01,表示未设置时间)
            if (timeinfo.tm_year < 70)  // tm_year是自1900年以来的年数,所以70表示1970
            {
                ESP_LOGW(TAG, "系统时间似乎未设置(1970),使用相对时间戳");
                use_relative_time = true;
            }
        }
        else
        {
            // 如果日历时间未设置,回退到当前时间
            time_t now = time(NULL);
            localtime_r(&now, &timeinfo);
            if (timeinfo.tm_year < 70)
            {
                ESP_LOGW(TAG, "系统时间似乎未设置(1970),使用相对时间戳");
                use_relative_time = true;
            }
        }

        // 格式化频率和时长用于文件名(各4位数字,零填充)
        int freq_int = (int)(s_config.sampling_frequency_hz + 0.5f);
        int dur_int = (int)(s_config.sampling_duration_sec + 0.5f);

        if (use_relative_time)
        {
            // 如果系统时间未设置,使用相对时间戳(自启动以来的秒数)
            // 格式:BootXXXXX_FXXXX_DXXXX.csv
            uint64_t boot_time_sec = s_start_timestamp / 1000000ULL;
            snprintf(file_path, sizeof(file_path), "%s/Boot%05llu_F%04d_D%04d.csv",
                     MOUNT_POINT,
                     (unsigned long long)boot_time_sec % 100000,  // 启动时间的最后5位数字
                     freq_int,                                   // 频率(4位数字,零填充)
                     dur_int);                                   // 时长(4位数字,零填充)
        }
        else
        {
            // 格式:YYYYMMDDHHMMSS_FXXXX_DXXXX.csv
            // 包括完整日期/时间(年、月、日、时、分、秒)
            // 和采样参数(频率和时长,均为4位数字零填充)
            snprintf(file_path, sizeof(file_path), "%s/%04d%02d%02d%02d%02d%02d_F%04d_D%04d.csv",
                     MOUNT_POINT,
                     timeinfo.tm_year + 1900,  // YYYY(4位数字)
                     timeinfo.tm_mon + 1,       // MM(2位数字)
                     timeinfo.tm_mday,          // DD(2位数字)
                     timeinfo.tm_hour,          // HH(2位数字)
                     timeinfo.tm_min,           // MM(2位数字)
                     timeinfo.tm_sec,           // SS(2位数字)
                     freq_int,                 // 频率(4位数字,零填充)
                     dur_int);                 // 时长(4位数字,零填充)
        }
    }

    // 打开文件进行写入
    FILE *f = fopen(file_path, "w");
    if (f == NULL)
    {
        ESP_LOGE(TAG, "打开文件写入失败: %s", file_path);
        ESP_LOGE(TAG, "错误: %s (errno: %d)", strerror(errno), errno);

        // 尝试检查目录是否存在且可写
        FILE *test_f = fopen(MOUNT_POINT "/.test_write", "w");
        if (test_f == NULL)
        {
            ESP_LOGE(TAG, "SD卡目录可能不可写。请检查SD卡挂载状态。");
        }
        else
        {
            fclose(test_f);
            remove(MOUNT_POINT "/.test_write");
            ESP_LOGE(TAG, "目录可写,但文件创建失败。请检查文件名长度或无效字符。");
        }

        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);

    // 更新报告中的文件路径
    strncpy(s_last_report.sd_file_path, file_path, sizeof(s_last_report.sd_file_path) - 1);
    s_last_report.sd_file_path[sizeof(s_last_report.sd_file_path) - 1] = '\0';

    return ESP_OK;
}

MQTT报告函数

static esp_err_t send_mqtt_report(const offline_sensing_report_t *report)
{
    if (report == NULL)
    {
        return ESP_ERR_INVALID_ARG;
    }

    if (!s_is_mqtt_connected || s_mqtt_client == NULL)
    {
        ESP_LOGW(TAG, "MQTT未连接,跳过报告");
        return ESP_ERR_INVALID_STATE;
    }

    // 格式化JSON报告
    char report_buff[512];
    snprintf(report_buff, sizeof(report_buff),
             "{\"samples\":%" PRIu32 ",\"freq_hz\":%.2f,\"duration_sec\":%.2f,"
             "\"memory_ok\":%s,\"sd_ok\":%s,\"sd_path\":\"%s\","
             "\"start_us\":%" PRIu64 ",\"end_us\":%" PRIu64 "}",
             report->total_samples,
             report->actual_frequency_hz,
             report->duration_sec,
             report->memory_storage_success ? "true" : "false",
             report->sd_storage_success ? "true" : "false",
             report->sd_file_path[0] != '\0' ? report->sd_file_path : "",
             report->start_timestamp_us,
             report->end_timestamp_us);

    const char *topic = s_config.mqtt_report_topic ? s_config.mqtt_report_topic : "/offline_sensing/report";
    int mqtt_ret = esp_mqtt_client_publish(s_mqtt_client, topic, report_buff, strlen(report_buff), 1, 0);

    if (mqtt_ret < 0)
    {
        ESP_LOGE(TAG, "MQTT发布失败: %d", mqtt_ret);
        return ESP_FAIL;
    }

    ESP_LOGD(TAG, "MQTT报告已发送");
    return ESP_OK;
}

初始化函数

esp_err_t offline_sensing_init(const offline_sensing_config_t *config)
{
    if (s_is_initialized)
    {
        ESP_LOGW(TAG, "离线传感已初始化");
        return ESP_ERR_INVALID_STATE;
    }

    // 应用配置
    if (config != NULL)
    {
        memcpy(&s_config, config, sizeof(offline_sensing_config_t));
    }
    else
    {
        // 使用默认配置
        s_config.sampling_frequency_hz = OFFLINE_SENSING_DEFAULT_FREQ_HZ;
        s_config.sampling_duration_sec = OFFLINE_SENSING_DEFAULT_DURATION_SEC;
        s_config.enable_memory = OFFLINE_SENSING_DEFAULT_ENABLE_MEMORY;
        s_config.enable_sd = OFFLINE_SENSING_DEFAULT_ENABLE_SD;
        s_config.enable_mqtt_report = OFFLINE_SENSING_DEFAULT_ENABLE_MQTT_REPORT;
        s_config.sd_file_path = NULL;
        s_config.mqtt_report_topic = NULL;
    }

    // 验证配置
    // 注意:4000 Hz的最大频率受ADXL355传感器的最大输出数据率(ODR)限制
    // ADXL355支持高达4000 Hz的ODR,这是该传感器的硬件限制
    if (s_config.sampling_frequency_hz < 1.0f || s_config.sampling_frequency_hz > 4000.0f)
    {
        ESP_LOGE(TAG, "无效的采样频率: %.2f Hz (有效范围: 1 - 4000 Hz,最大值受ADXL355 ODR限制)",
                 s_config.sampling_frequency_hz);
        return ESP_ERR_INVALID_ARG;
    }

    if (s_config.sampling_duration_sec < 0.1f || s_config.sampling_duration_sec > 3600.0f)
    {
        ESP_LOGE(TAG, "无效的采样时长: %.2f 秒 (有效范围: 0.1 - 3600 秒)",
                 s_config.sampling_duration_sec);
        return ESP_ERR_INVALID_ARG;
    }

    // 如果启用,分配内存缓冲区
    // 使用PSRAM(外部RAM)支持高频/长时间采样的大缓冲区
    if (s_config.enable_memory)
    {
        uint32_t buffer_size = (uint32_t)(s_config.sampling_frequency_hz * s_config.sampling_duration_sec) + 100; // 添加余量
        size_t required_bytes = buffer_size * sizeof(offline_sensing_sample_t);

        // 检查PSRAM可用性
        size_t psram_free = heap_caps_get_free_size(MALLOC_CAP_SPIRAM);
        if (psram_free < required_bytes)
        {
            ESP_LOGE(TAG, "PSRAM不足: 可用 %zu 字节,需要 %zu 字节",
                     psram_free, required_bytes);
            return ESP_ERR_NO_MEM;
        }

        // 从PSRAM显式分配以利用外部内存(通常为8MB)
        s_memory_buffer = (offline_sensing_sample_t *)heap_caps_malloc(
            required_bytes,
            MALLOC_CAP_SPIRAM  // 使用PSRAM作为大缓冲区
        );
        if (s_memory_buffer == NULL)
        {
            ESP_LOGE(TAG, "从PSRAM分配内存缓冲区失败");
            ESP_LOGE(TAG, "可用PSRAM: %zu 字节", heap_caps_get_free_size(MALLOC_CAP_SPIRAM));
            return ESP_ERR_NO_MEM;
        }
        s_memory_buffer_size = buffer_size;
        s_memory_buffer_index = 0;
        ESP_LOGD(TAG, "为 %u 个样本分配内存缓冲区", buffer_size);
    }

    // 为内存缓冲区创建互斥锁
    s_memory_mutex = xSemaphoreCreateMutex();
    if (s_memory_mutex == NULL)
    {
        ESP_LOGE(TAG, "创建互斥锁失败");
        if (s_memory_buffer != NULL)
        {
            heap_caps_free(s_memory_buffer);
            s_memory_buffer = NULL;
        }
        return ESP_ERR_NO_MEM;
    }

    // 初始化报告
    memset(&s_last_report, 0, sizeof(offline_sensing_report_t));
    s_last_report.sd_file_path[0] = '\0';

    s_is_initialized = true;

    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;
    }

    if (s_adxl355_handle == NULL)
    {
        ESP_LOGE(TAG, "传感器句柄未设置");
        return ESP_ERR_INVALID_STATE;
    }

    // 重置状态
    s_total_samples = 0;
    s_memory_buffer_index = 0;
    // 计算预期样本数:(频率 × 时长) + 1(包括t=0的样本)
    s_expected_samples = (uint32_t)(s_config.sampling_frequency_hz * s_config.sampling_duration_sec) + 1;
    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;

    // 在启动周期定时器前立即执行第一次采样(t=0)
    // 这确保我们在N秒内以N Hz获得恰好N个样本
    sampling_timer_callback(NULL);

    // 启动周期定时器(第一次周期采样将在t=period_us时执行)
    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 秒(预期样本数: %u)", 
             s_config.sampling_frequency_hz, s_config.sampling_duration_sec, s_expected_samples);

    // 等待采样时长
    // 定时器回调将在达到expected_samples时自动停止
    uint32_t duration_ms = (uint32_t)(s_config.sampling_duration_sec * 1000.0f);
    // 添加小余量以确保所有样本都已采集
    vTaskDelay(pdMS_TO_TICKS(duration_ms + 100));

    // 确保采样已停止
    s_is_running = false;

    // 小延迟以确保任何正在执行的回调完成
    vTaskDelay(pdMS_TO_TICKS(10));

    // 停止采样
    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);
        }
        else
        {
            // 需要从传感器重新读取或使用不同的方法
            ESP_LOGW(TAG, "SD卡存储需要内存缓冲区");
            s_last_report.sd_storage_success = false;
        }
    }
    else
    {
        s_last_report.sd_storage_success = false;
    }

    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;
}

停止函数

esp_err_t offline_sensing_stop(void)
{
    if (!s_is_running)
    {
        return ESP_OK;
    }

    if (s_sampling_timer != NULL)
    {
        esp_timer_stop(s_sampling_timer);
        esp_timer_delete(s_sampling_timer);
        s_sampling_timer = NULL;
    }

    s_is_running = false;
    return ESP_OK;
}

反初始化函数

esp_err_t offline_sensing_deinit(void)
{
    if (!s_is_initialized)
    {
        return ESP_OK;
    }

    // 如果正在运行,停止
    offline_sensing_stop();

    // 释放内存缓冲区
    if (s_memory_buffer != NULL)
    {
        heap_caps_free(s_memory_buffer);
        s_memory_buffer = NULL;
    }

    // 删除互斥锁
    if (s_memory_mutex != NULL)
    {
        vSemaphoreDelete(s_memory_mutex);
        s_memory_mutex = NULL;
    }

    // 清除报告文件路径
    s_last_report.sd_file_path[0] = '\0';

    s_is_initialized = false;
    return ESP_OK;
}

获取内存数据函数

esp_err_t offline_sensing_get_memory_data(offline_sensing_sample_t *samples, uint32_t max_samples, uint32_t *actual_samples)
{
    if (samples == NULL || actual_samples == NULL)
    {
        return ESP_ERR_INVALID_ARG;
    }

    if (!s_config.enable_memory || s_memory_buffer == NULL)
    {
        *actual_samples = 0;
        return ESP_ERR_INVALID_STATE;
    }

    if (xSemaphoreTake(s_memory_mutex, pdMS_TO_TICKS(100)) == pdTRUE)
    {
        uint32_t copy_count = (s_memory_buffer_index < max_samples) ? s_memory_buffer_index : max_samples;
        memcpy(samples, s_memory_buffer, copy_count * sizeof(offline_sensing_sample_t));
        *actual_samples = copy_count;
        xSemaphoreGive(s_memory_mutex);
        return ESP_OK;
    }

    return ESP_ERR_TIMEOUT;
}

清除内存函数

esp_err_t offline_sensing_clear_memory(void)
{
    if (s_memory_buffer == NULL)
    {
        return ESP_ERR_INVALID_STATE;
    }

    if (xSemaphoreTake(s_memory_mutex, pdMS_TO_TICKS(100)) == pdTRUE)
    {
        s_memory_buffer_index = 0;
        xSemaphoreGive(s_memory_mutex);
        return ESP_OK;
    }

    return ESP_ERR_TIMEOUT;
}

检查运行状态函数

esp_err_t offline_sensing_is_running(bool *is_running)
{
    if (is_running == NULL)
    {
        return ESP_ERR_INVALID_ARG;
    }
    *is_running = s_is_running;
    return ESP_OK;
}

获取报告函数

esp_err_t offline_sensing_get_report(offline_sensing_report_t *report)
{
    if (report == NULL)
    {
        return ESP_ERR_INVALID_ARG;
    }
    memcpy(report, &s_last_report, sizeof(offline_sensing_report_t));
    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);
ESP_LOGI(TAG, "SD文件: %s", report.sd_file_path);

// 获取内存数据(可选)
offline_sensing_sample_t samples[1000];
uint32_t actual_samples;
offline_sensing_get_memory_data(samples, 1000, &actual_samples);
ESP_LOGI(TAG, "从内存检索到 %u 个样本", actual_samples);

// 清除内存缓冲区(可选)
offline_sensing_clear_memory();

// 检查是否正在运行(可选)
bool is_running;
offline_sensing_is_running(&is_running);

// 清理
offline_sensing_deinit();