Skip to content

CODE

Warning

The following code should be based on the code in the release code, which may have been updated.

online_sensing.h

/**
 * @file online_sensing.h
 * @author SHUAIWEN CUI (SHUAIWEN001@e.ntu.edu.sg)
 * @brief Online sensing module - Continuous sensor data acquisition and transmission
 * @version 1.0
 * @date 2025-12-17
 * @copyright Copyright (c) 2025
 *
 * @details
 * This module provides online sensor data acquisition with configurable:
 * - Sampling frequency (default: 1.0 Hz)
 * - MQTT transmission (enabled/disabled)
 * - Serial output (enabled/disabled)
 *
 * Features:
 * - Fixed-rate data acquisition using ESP timer
 * - Automatic data formatting (compact CSV format)
 * - Configurable output channels (MQTT, serial, LCD)
 * - 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 Online sensing configuration
 */
typedef struct
{
    float sampling_frequency_hz;  ///< Sampling frequency in Hz (default: 1.0)
    bool enable_mqtt;              ///< Enable MQTT transmission (default: true)
    bool enable_serial;             ///< Enable serial output (default: true)
#ifdef TINY_MEASUREMENT_ENABLE_LCD
    bool enable_lcd;                ///< Enable LCD display output (default: false, only available when TINY_MEASUREMENT_ENABLE_LCD is defined)
#endif
    const char *mqtt_topic;         ///< MQTT topic for publishing (default: NULL uses MQTT_PUBLISH_TOPIC)
} online_sensing_config_t;

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

/**
 * @brief Default configuration values (from tiny_measurement_config.h macros)
 */
#define ONLINE_SENSING_DEFAULT_FREQ_HZ TINY_MEASUREMENT_ONLINE_SENSING_DEFAULT_FREQ_HZ
#define ONLINE_SENSING_DEFAULT_ENABLE_MQTT TINY_MEASUREMENT_ONLINE_SENSING_DEFAULT_ENABLE_MQTT
#define ONLINE_SENSING_DEFAULT_ENABLE_SERIAL TINY_MEASUREMENT_ONLINE_SENSING_DEFAULT_ENABLE_SERIAL
#ifdef TINY_MEASUREMENT_ENABLE_LCD
#define ONLINE_SENSING_DEFAULT_ENABLE_LCD TINY_MEASUREMENT_ONLINE_SENSING_DEFAULT_ENABLE_LCD
#else
#define ONLINE_SENSING_DEFAULT_ENABLE_LCD false
#endif

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

/**
 * @brief Initialize online sensing module
 * @param config Configuration structure (NULL for default configuration)
 * @return ESP_OK on success, error code on failure
 */
esp_err_t online_sensing_init(const online_sensing_config_t *config);

/**
 * @brief Start online sensing task
 * @return ESP_OK on success, error code on failure
 */
esp_err_t online_sensing_start(void);

/**
 * @brief Stop online sensing task
 * @return ESP_OK on success, error code on failure
 */
esp_err_t online_sensing_stop(void);

/**
 * @brief Deinitialize online sensing module
 * @return ESP_OK on success, error code on failure
 */
esp_err_t online_sensing_deinit(void);

/**
 * @brief Update sampling frequency at runtime
 * @param frequency_hz New sampling frequency in Hz
 * @return ESP_OK on success, error code on failure
 */
esp_err_t online_sensing_set_frequency(float frequency_hz);

/**
 * @brief Enable or disable MQTT transmission
 * @param enable true to enable, false to disable
 * @return ESP_OK on success, error code on failure
 */
esp_err_t online_sensing_set_mqtt_enable(bool enable);

/**
 * @brief Enable or disable serial output
 * @param enable true to enable, false to disable
 * @return ESP_OK on success, error code on failure
 */
esp_err_t online_sensing_set_serial_enable(bool enable);

#ifdef TINY_MEASUREMENT_ENABLE_LCD
/**
 * @brief Enable or disable LCD display
 * @param enable true to enable, false to disable
 * @return ESP_OK on success, error code on failure
 * @note Only available when TINY_MEASUREMENT_ENABLE_LCD is defined
 */
esp_err_t online_sensing_set_lcd_enable(bool enable);
#endif

/**
 * @brief Get current configuration
 * @param config Output parameter for current configuration
 * @return ESP_OK on success, error code on failure
 */
esp_err_t online_sensing_get_config(online_sensing_config_t *config);

/**
 * @brief Check if online sensing is running
 * @param is_running Output parameter: true if running, false otherwise
 * @return ESP_OK on success, error code on failure
 */
esp_err_t online_sensing_is_running(bool *is_running);

/**
 * @brief Set ADXL355 sensor handle (must be called before starting)
 * @param handle ADXL355 handle pointer
 * @return ESP_OK on success, error code on failure
 */
esp_err_t online_sensing_set_sensor_handle(adxl355_handle_t *handle);

#ifdef __cplusplus
}
#endif

online_sensing.c

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

#include "online_sensing.h"
#include "node_mqtt.h"
#ifdef TINY_MEASUREMENT_ENABLE_LCD
#include "node_lcd.h"
#endif
#include "esp_log.h"
#include "esp_timer.h"
#include <string.h>
#include <stdio.h>

/* ============================================================================
 * PRIVATE DEFINITIONS
 * ============================================================================ */

static const char *TAG = "OnlineSensing";

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

static online_sensing_config_t s_config = {
    .sampling_frequency_hz = ONLINE_SENSING_DEFAULT_FREQ_HZ,
    .enable_mqtt = ONLINE_SENSING_DEFAULT_ENABLE_MQTT,
    .enable_serial = ONLINE_SENSING_DEFAULT_ENABLE_SERIAL,
    .mqtt_topic = NULL
};

static esp_timer_handle_t s_sampling_timer = NULL;
static bool s_is_running = false;
static adxl355_handle_t *s_adxl355_handle = NULL;

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

static void sampling_timer_callback(void *arg);

/* ============================================================================
 * PRIVATE FUNCTION IMPLEMENTATIONS
 * ============================================================================ */

/**
 * @brief Timer callback for periodic sampling
 * @param arg Timer argument (unused)
 */
static void sampling_timer_callback(void *arg)
{
    if (!s_is_running || s_adxl355_handle == NULL)
    {
        return;
    }

    adxl355_accelerations_t accel;
    float temperature;
    char mqtt_pub_buff[128];

    // Read sensor data
    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)
    {
        // MQTT transmission - compact format: "x,y,z,t" with 20-bit precision (6 decimal places)
        // Note: esp_mqtt_client_publish is non-blocking, but may fail if buffer is full
        if (s_config.enable_mqtt && s_is_mqtt_connected)
        {
            snprintf(mqtt_pub_buff, sizeof(mqtt_pub_buff), "%.6f,%.6f,%.6f,%.2f",
                     accel.x, accel.y, accel.z, temperature);
            const char *topic = s_config.mqtt_topic ? s_config.mqtt_topic : MQTT_PUBLISH_TOPIC;
            int mqtt_ret = esp_mqtt_client_publish(s_mqtt_client, topic,
                                                    mqtt_pub_buff, strlen(mqtt_pub_buff), 1, 0);
            if (mqtt_ret < 0)
            {
                // MQTT publish failed (likely buffer full), but don't block sampling
                ESP_LOGW(TAG, "MQTT publish failed (buffer may be full): %d", mqtt_ret);
            }
        }

        // Serial output - minimal format with 20-bit precision (6 decimal places)
        // Note: printf may block if serial buffer is full, but typically fast enough
        if (s_config.enable_serial)
        {
            printf("%.6f,%.6f,%.6f,%.2f\n", accel.x, accel.y, accel.z, temperature);
            fflush(stdout); // Ensure output is flushed immediately
        }

#ifdef TINY_MEASUREMENT_ENABLE_LCD
        // LCD display - one axis per line, temperature on last line
        // Use 6 decimal places for full 20-bit precision
        if (s_config.enable_lcd)
        {
            char lcd_str[48];
            // Line 1: X-axis
            snprintf(lcd_str, sizeof(lcd_str), "X:%.6f", accel.x);
            lcd_show_string(0, 0, lcd_self.width, 16, 16, lcd_str, BLACK);
            // Line 2: Y-axis
            snprintf(lcd_str, sizeof(lcd_str), "Y:%.6f", accel.y);
            lcd_show_string(0, 16, lcd_self.width, 16, 16, lcd_str, BLACK);
            // Line 3: Z-axis
            snprintf(lcd_str, sizeof(lcd_str), "Z:%.6f", accel.z);
            lcd_show_string(0, 32, lcd_self.width, 16, 16, lcd_str, BLACK);
            // Line 4: Temperature
            snprintf(lcd_str, sizeof(lcd_str), "T:%.2fC", temperature);
            lcd_show_string(0, 48, lcd_self.width, 16, 16, lcd_str, BLUE);
        }
#endif
    }
    else
    {
        if (accel_ret != ESP_OK)
        {
            ESP_LOGE(TAG, "Failed to read acceleration: %s", esp_err_to_name(accel_ret));
        }
        if (temp_ret != ESP_OK)
        {
            ESP_LOGE(TAG, "Failed to read temperature: %s", esp_err_to_name(temp_ret));
        }
    }
}

/* ============================================================================
 * PUBLIC FUNCTION IMPLEMENTATIONS
 * ============================================================================ */

esp_err_t online_sensing_init(const online_sensing_config_t *config)
{
    if (s_is_running)
    {
        ESP_LOGW(TAG, "Online sensing is already running, stop it first");
        return ESP_ERR_INVALID_STATE;
    }

    // Apply configuration
    if (config != NULL)
    {
        memcpy(&s_config, config, sizeof(online_sensing_config_t));
    }
    else
    {
        // Use default configuration
        s_config.sampling_frequency_hz = ONLINE_SENSING_DEFAULT_FREQ_HZ;
        s_config.enable_mqtt = ONLINE_SENSING_DEFAULT_ENABLE_MQTT;
        s_config.enable_serial = ONLINE_SENSING_DEFAULT_ENABLE_SERIAL;
#ifdef TINY_MEASUREMENT_ENABLE_LCD
        s_config.enable_lcd = ONLINE_SENSING_DEFAULT_ENABLE_LCD;
#endif
        s_config.mqtt_topic = NULL;
    }

    // Validate configuration
    // Note: Code accepts up to 300 Hz, but practical hardware limit is ~200 Hz
    // for stable operation. This limitation is primarily due to MQTT transmission
    // constraints - at higher frequencies, MQTT publishing cannot keep up with
    // data generation rate, causing buffer overflows and data loss.
    if (s_config.sampling_frequency_hz <= 0.0f || s_config.sampling_frequency_hz > 300.0f)
    {
        ESP_LOGE(TAG, "Invalid sampling frequency: %.2f Hz (valid range: 0.1 - 300 Hz)",
                 s_config.sampling_frequency_hz);
        return ESP_ERR_INVALID_ARG;
    }

    ESP_LOGI(TAG, "Online sensing initialized:");
    ESP_LOGI(TAG, "  - Sampling frequency: %.2f Hz", s_config.sampling_frequency_hz);
    ESP_LOGI(TAG, "  - MQTT enabled: %s", s_config.enable_mqtt ? "Yes" : "No");
    ESP_LOGI(TAG, "  - Serial output enabled: %s", s_config.enable_serial ? "Yes" : "No");
#ifdef TINY_MEASUREMENT_ENABLE_LCD
    ESP_LOGI(TAG, "  - LCD display enabled: %s", s_config.enable_lcd ? "Yes" : "No");
#endif
    if (s_config.mqtt_topic)
    {
        ESP_LOGI(TAG, "  - MQTT topic: %s", s_config.mqtt_topic);
    }

    return ESP_OK;
}

esp_err_t online_sensing_start(void)
{
    if (s_is_running)
    {
        ESP_LOGW(TAG, "Online sensing is already running");
        return ESP_ERR_INVALID_STATE;
    }

    // Get ADXL355 handle (assuming it's initialized in main)
    // This should be set by the application before starting
    if (s_adxl355_handle == NULL)
    {
        ESP_LOGE(TAG, "ADXL355 handle not set. Call online_sensing_set_sensor_handle() first");
        return ESP_ERR_INVALID_STATE;
    }

    // Calculate timer period in microseconds
    // Note: Practical limit is ~200 Hz (5000 us period) for stable operation.
    // This limit is primarily due to MQTT transmission constraints - MQTT
    // publishing cannot keep up with data generation at higher frequencies.
    uint64_t period_us = (uint64_t)(1000000.0f / s_config.sampling_frequency_hz);

    // Validate period (code-level check: minimum 100 us = 300 Hz max)
    // However, practical hardware limit is ~200 Hz for reliable operation,
    // mainly constrained by MQTT transmission bandwidth
    if (period_us < 100)
    {
        ESP_LOGE(TAG, "Sampling frequency too high: %.2f Hz (max: 300 Hz)", s_config.sampling_frequency_hz);
        return ESP_ERR_INVALID_ARG;
    }

    // Create timer
    esp_timer_create_args_t timer_args = {
        .callback = sampling_timer_callback,
        .arg = NULL,
        .name = "online_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;

#ifdef TINY_MEASUREMENT_ENABLE_LCD
    // Clear LCD screen before starting online sensing
    if (s_config.enable_lcd)
    {
        lcd_clear(WHITE);
    }
#endif

    // Perform immediate first sample (at t=0) before starting periodic timer
    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, "Online sensing started (frequency: %.2f Hz, period: %llu us)",
             s_config.sampling_frequency_hz, period_us);
    return ESP_OK;
}

esp_err_t online_sensing_stop(void)
{
    if (!s_is_running)
    {
        ESP_LOGW(TAG, "Online sensing is not running");
        return ESP_ERR_INVALID_STATE;
    }

    s_is_running = false;

    // Stop and delete timer
    if (s_sampling_timer != NULL)
    {
        esp_err_t ret = esp_timer_stop(s_sampling_timer);
        if (ret != ESP_OK && ret != ESP_ERR_INVALID_STATE)
        {
            ESP_LOGE(TAG, "Failed to stop timer: %s", esp_err_to_name(ret));
        }

        ret = esp_timer_delete(s_sampling_timer);
        if (ret != ESP_OK)
        {
            ESP_LOGE(TAG, "Failed to delete timer: %s", esp_err_to_name(ret));
        }
        s_sampling_timer = NULL;
    }

    ESP_LOGI(TAG, "Online sensing stopped");
    return ESP_OK;
}

esp_err_t online_sensing_deinit(void)
{
    if (s_is_running)
    {
        online_sensing_stop();
    }

    memset(&s_config, 0, sizeof(online_sensing_config_t));
    s_adxl355_handle = NULL;

    ESP_LOGI(TAG, "Online sensing deinitialized");
    return ESP_OK;
}

esp_err_t online_sensing_set_frequency(float frequency_hz)
{
    if (frequency_hz <= 0.0f || frequency_hz > 300.0f)
    {
        ESP_LOGE(TAG, "Invalid sampling frequency: %.2f Hz (valid range: 0.1 - 300 Hz)", frequency_hz);
        return ESP_ERR_INVALID_ARG;
    }

    // If timer is running, need to restart it with new frequency
    bool was_running = s_is_running;
    if (was_running)
    {
        online_sensing_stop();
    }

    s_config.sampling_frequency_hz = frequency_hz;
    ESP_LOGI(TAG, "Sampling frequency updated to %.2f Hz", frequency_hz);

    // Restart if it was running
    if (was_running)
    {
        return online_sensing_start();
    }

    return ESP_OK;
}

esp_err_t online_sensing_set_mqtt_enable(bool enable)
{
    s_config.enable_mqtt = enable;
    ESP_LOGI(TAG, "MQTT transmission %s", enable ? "enabled" : "disabled");
    return ESP_OK;
}

esp_err_t online_sensing_set_serial_enable(bool enable)
{
    s_config.enable_serial = enable;
    ESP_LOGI(TAG, "Serial output %s", enable ? "enabled" : "disabled");
    return ESP_OK;
}

#ifdef TINY_MEASUREMENT_ENABLE_LCD
esp_err_t online_sensing_set_lcd_enable(bool enable)
{
    s_config.enable_lcd = enable;
    ESP_LOGI(TAG, "LCD display %s", enable ? "enabled" : "disabled");
    return ESP_OK;
}
#endif

esp_err_t online_sensing_get_config(online_sensing_config_t *config)
{
    if (config == NULL)
    {
        return ESP_ERR_INVALID_ARG;
    }

    memcpy(config, &s_config, sizeof(online_sensing_config_t));
    return ESP_OK;
}

esp_err_t online_sensing_is_running(bool *is_running)
{
    if (is_running == NULL)
    {
        return ESP_ERR_INVALID_ARG;
    }

    *is_running = s_is_running;
    return ESP_OK;
}

/* ============================================================================
 * ADDITIONAL HELPER FUNCTION
 * ============================================================================ */

/**
 * @brief Set ADXL355 sensor handle (must be called before starting)
 * @param handle ADXL355 handle pointer
 * @return ESP_OK on success, error code on failure
 */
esp_err_t online_sensing_set_sensor_handle(adxl355_handle_t *handle)
{
    if (handle == NULL)
    {
        return ESP_ERR_INVALID_ARG;
    }

    s_adxl355_handle = handle;
    ESP_LOGI(TAG, "ADXL355 sensor handle set");
    return ESP_OK;
}

Key Implementation

Timer Callback

The core of online sensing is the timer callback function sampling_timer_callback(), which executes at fixed intervals:

  • Sensor Reading: Reads acceleration and temperature data via SPI (non-blocking)
  • Data Formatting: Formats data with 6 decimal places for full 20-bit precision
  • Output Channels: Publishes to MQTT, serial, and optionally LCD based on configuration
  • Error Handling: Logs errors but continues sampling to maintain timing

Timer Management

The module uses ESP-IDF's high-precision timer (esp_timer) for periodic sampling:

  • Timer Creation: Creates a periodic timer with period = 1,000,000 / sampling_frequency_hz microseconds
  • Immediate First Sample: Executes callback immediately at t=0 before starting periodic timer
  • State Management: Uses s_is_running flag to prevent callback execution after stop

Configuration Validation

  • Frequency Range: Validates 0.1 - 300 Hz (code-level limit)
  • Practical Limit: ~200 Hz for stable operation (constrained by MQTT transmission bandwidth)
  • Default Values: Uses macros from tiny_measurement_config.h when config is NULL

Runtime Updates

  • Frequency Update: Stops and restarts timer with new frequency if running
  • Channel Control: Can enable/disable MQTT, serial, and LCD outputs at runtime
  • Non-blocking: MQTT publish is non-blocking, but may fail if buffer is full

Usage Example

#include "tiny_measurement.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
online_sensing_set_sensor_handle(&adxl355_handle);

// Initialize with default configuration
online_sensing_init(NULL);

// Start online sensing
online_sensing_start();

// Runtime configuration update
online_sensing_set_frequency(50.0f);  // Change to 50 Hz
online_sensing_set_mqtt_enable(false); // Disable MQTT
online_sensing_set_serial_enable(false); // Disable serial output

// Stop when done
online_sensing_stop();

// Cleanup
online_sensing_deinit();