CODE¶
Warning
The following code should be based on the code in the release code, which may have been updated.
offline_sensing.h¶
/**
* @file offline_sensing.h
* @author SHUAIWEN CUI (SHUAIWEN001@e.ntu.edu.sg)
* @brief Offline sensing module - High-frequency batch sensor data acquisition and storage
* @version 1.0
* @date 2025-01-XX
* @copyright Copyright (c) 2025
*
* @details
* This module provides offline sensor data acquisition with configurable:
* - Sampling frequency (default: 100 Hz, higher than online sensing)
* - Sampling duration (default: 10 seconds)
* - Memory buffer storage (enabled/disabled)
* - SD card storage (enabled/disabled)
* - MQTT report after sampling (enabled/disabled)
*
* Features:
* - High-precision timer-based sampling (ESP timer)
* - Data storage in memory buffer and/or SD card
* - Automatic MQTT report after sampling completion
* - Thread-safe operation
*/
#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 Offline sensing configuration
*/
typedef struct
{
float sampling_frequency_hz; ///< Sampling frequency in Hz (default: 100.0)
float sampling_duration_sec; ///< Sampling duration in seconds (default: 10.0)
bool enable_memory; ///< Enable memory buffer storage (default: true)
bool enable_sd; ///< Enable SD card storage (default: true)
bool enable_mqtt_report; ///< Enable MQTT report after sampling (default: true)
const char *sd_file_path; ///< SD card file path (default: NULL uses auto-generated)
const char *mqtt_report_topic; ///< MQTT topic for report (default: NULL uses default)
} offline_sensing_config_t;
/**
* @brief Offline sensing sample data structure
*/
typedef struct
{
float x; ///< X-axis acceleration (g)
float y; ///< Y-axis acceleration (g)
float z; ///< Z-axis acceleration (g)
float temp; ///< Temperature (°C)
uint64_t timestamp_us; ///< Timestamp in microseconds
} offline_sensing_sample_t;
/**
* @brief Offline sensing session report
*/
typedef struct
{
uint32_t total_samples; ///< Total number of samples collected
float actual_frequency_hz; ///< Actual sampling frequency achieved
float duration_sec; ///< Actual duration
bool memory_storage_success; ///< Memory storage success status
bool sd_storage_success; ///< SD card storage success status
char sd_file_path[256]; ///< SD card file path (if saved)
uint64_t start_timestamp_us; ///< Start timestamp
uint64_t end_timestamp_us; ///< End timestamp
} offline_sensing_report_t;
/* ============================================================================
* FUNCTION DECLARATIONS
* ============================================================================ */
/**
* @brief Initialize offline sensing module
* @param config Configuration structure (NULL for default configuration)
* @return ESP_OK on success, error code on failure
*/
esp_err_t offline_sensing_init(const offline_sensing_config_t *config);
/**
* @brief Set sensor handle for offline sensing
* @param handle ADXL355 sensor handle
* @return ESP_OK on success, error code on failure
*/
esp_err_t offline_sensing_set_sensor_handle(adxl355_handle_t *handle);
/**
* @brief Start offline sensing session
* @return ESP_OK on success, error code on failure
* @note This function blocks until sampling is complete
*/
esp_err_t offline_sensing_start(void);
/**
* @brief Stop offline sensing session (if running)
* @return ESP_OK on success, error code on failure
*/
esp_err_t offline_sensing_stop(void);
/**
* @brief Check if offline sensing is currently running
* @param is_running Output parameter, true if running
* @return ESP_OK on success, error code on failure
*/
esp_err_t offline_sensing_is_running(bool *is_running);
/**
* @brief Get the last sampling report
* @param report Output parameter for the report
* @return ESP_OK on success, error code on failure
*/
esp_err_t offline_sensing_get_report(offline_sensing_report_t *report);
/**
* @brief Get memory buffer data (if enabled)
* @param samples Output buffer for samples
* @param max_samples Maximum number of samples to retrieve
* @param actual_samples Output parameter for actual number of samples retrieved
* @return ESP_OK on success, error code on failure
*/
esp_err_t offline_sensing_get_memory_data(offline_sensing_sample_t *samples,
uint32_t max_samples,
uint32_t *actual_samples);
/**
* @brief Clear memory buffer
* @return ESP_OK on success, error code on failure
*/
esp_err_t offline_sensing_clear_memory(void);
/**
* @brief Deinitialize offline sensing module
* @return ESP_OK on success, error code on failure
*/
esp_err_t offline_sensing_deinit(void);
#ifdef __cplusplus
}
#endif
offline_sensing.c¶
Key Implementation Details¶
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++;
}
}
SD Card Storage¶
static esp_err_t save_to_sd_card(const offline_sensing_sample_t *samples, uint32_t count)
{
// Generate file path with timestamp and parameters
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
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);
}
// 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);
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);
return ESP_OK;
}
Start Function¶
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;
}
// Reset state
s_total_samples = 0;
s_memory_buffer_index = 0;
s_start_timestamp = esp_timer_get_time();
s_start_calendar_time = time(NULL);
// Calculate timer period
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;
}
s_is_running = true;
// Perform immediate first sample
sampling_timer_callback(NULL);
// Start periodic timer
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",
s_config.sampling_frequency_hz, s_config.sampling_duration_sec);
// Wait for sampling duration (blocking)
uint32_t duration_ms = (uint32_t)(s_config.sampling_duration_sec * 1000.0f);
vTaskDelay(pdMS_TO_TICKS(duration_ms));
// 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);
}
}
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;
}
Usage Example¶
#include "offline_sensing.h"
#include "node_acc_adxl355.h"
// Initialize sensor
adxl355_handle_t adxl355_handle;
adxl355_init(&adxl355_handle, ADXL355_RANGE_2G, ADXL355_ODR_1000);
// Set sensor handle
offline_sensing_set_sensor_handle(&adxl355_handle);
// Configure offline sensing
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, // Auto-generate filename
.mqtt_report_topic = NULL // Use default topic
};
// Initialize
offline_sensing_init(&config);
// Start sensing (blocks until complete)
offline_sensing_start();
// Get report
offline_sensing_report_t report;
offline_sensing_get_report(&report);
ESP_LOGI(TAG, "Samples: %u, Frequency: %.2f Hz, Duration: %.2f sec",
report.total_samples, report.actual_frequency_hz, report.duration_sec);
// Get memory data (optional)
offline_sensing_sample_t samples[1000];
uint32_t actual_samples;
offline_sensing_get_memory_data(samples, 1000, &actual_samples);
// Cleanup
offline_sensing_deinit();