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_hzmicroseconds - Immediate First Sample: Executes callback immediately at t=0 before starting periodic timer
- State Management: Uses
s_is_runningflag 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.hwhen 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();