代码¶
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();