Skip to content

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