跳转至

代码

Warning

以下代码应基于发布代码中的代码,可能已更新。

online_sensing.h

/**
 * @file online_sensing.h
 * @author SHUAIWEN CUI (SHUAIWEN001@e.ntu.edu.sg)
 * @brief 在线传感模块 - 连续传感器数据采集和传输
 * @version 1.0
 * @date 2025-12-17
 * @copyright Copyright (c) 2025
 *
 * @details
 * 该模块提供在线传感器数据采集,可配置:
 * - 采样频率(默认:1.0 Hz)
 * - MQTT传输(启用/禁用)
 * - 串口输出(启用/禁用)
 *
 * 功能特性:
 * - 使用ESP定时器进行固定频率数据采集
 * - 自动数据格式化(紧凑CSV格式)
 * - 可配置输出通道(MQTT、串口、LCD)
 * - 线程安全操作
 */

#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,默认:1.0)
    bool enable_mqtt;              ///< 启用MQTT传输(默认:true)
    bool enable_serial;             ///< 启用串口输出(默认:true)
#ifdef TINY_MEASUREMENT_ENABLE_LCD
    bool enable_lcd;                ///< 启用LCD显示输出(默认:false,仅在定义TINY_MEASUREMENT_ENABLE_LCD时可用)
#endif
    const char *mqtt_topic;         ///< MQTT发布主题(默认:NULL使用MQTT_PUBLISH_TOPIC)
} online_sensing_config_t;

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

/**
 * @brief 默认配置值(来自tiny_measurement_config.h宏)
 */
#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 初始化在线传感模块
 * @param config 配置结构(NULL使用默认配置)
 * @return 成功返回ESP_OK,失败返回错误码
 */
esp_err_t online_sensing_init(const online_sensing_config_t *config);

/**
 * @brief 启动在线传感任务
 * @return 成功返回ESP_OK,失败返回错误码
 */
esp_err_t online_sensing_start(void);

/**
 * @brief 停止在线传感任务
 * @return 成功返回ESP_OK,失败返回错误码
 */
esp_err_t online_sensing_stop(void);

/**
 * @brief 反初始化在线传感模块
 * @return 成功返回ESP_OK,失败返回错误码
 */
esp_err_t online_sensing_deinit(void);

/**
 * @brief 运行时更新采样频率
 * @param frequency_hz 新的采样频率(Hz)
 * @return 成功返回ESP_OK,失败返回错误码
 */
esp_err_t online_sensing_set_frequency(float frequency_hz);

/**
 * @brief 启用或禁用MQTT传输
 * @param enable true启用,false禁用
 * @return 成功返回ESP_OK,失败返回错误码
 */
esp_err_t online_sensing_set_mqtt_enable(bool enable);

/**
 * @brief 启用或禁用串口输出
 * @param enable true启用,false禁用
 * @return 成功返回ESP_OK,失败返回错误码
 */
esp_err_t online_sensing_set_serial_enable(bool enable);

#ifdef TINY_MEASUREMENT_ENABLE_LCD
/**
 * @brief 启用或禁用LCD显示
 * @param enable true启用,false禁用
 * @return 成功返回ESP_OK,失败返回错误码
 * @note 仅在定义TINY_MEASUREMENT_ENABLE_LCD时可用
 */
esp_err_t online_sensing_set_lcd_enable(bool enable);
#endif

/**
 * @brief 获取当前配置
 * @param config 输出参数,当前配置
 * @return 成功返回ESP_OK,失败返回错误码
 */
esp_err_t online_sensing_get_config(online_sensing_config_t *config);

/**
 * @brief 检查在线传感是否正在运行
 * @param is_running 输出参数:true表示正在运行,false表示未运行
 * @return 成功返回ESP_OK,失败返回错误码
 */
esp_err_t online_sensing_is_running(bool *is_running);

/**
 * @brief 设置ADXL355传感器句柄(必须在启动前调用)
 * @param handle ADXL355句柄指针
 * @return 成功返回ESP_OK,失败返回错误码
 */
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 在线传感模块实现
 * @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 定时器回调函数,用于周期性采样
 * @param arg 定时器参数(未使用)
 */
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];

    // 读取传感器数据
    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传输 - 紧凑格式:"x,y,z,t",20位精度(6位小数)
        // 注意:esp_mqtt_client_publish是非阻塞的,但如果缓冲区满可能会失败
        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发布失败(可能是缓冲区满),但不阻塞采样
                ESP_LOGW(TAG, "MQTT发布失败(缓冲区可能已满): %d", mqtt_ret);
            }
        }

        // 串口输出 - 最小格式,20位精度(6位小数)
        // 注意:如果串口缓冲区满,printf可能会阻塞,但通常足够快
        if (s_config.enable_serial)
        {
            printf("%.6f,%.6f,%.6f,%.2f\n", accel.x, accel.y, accel.z, temperature);
            fflush(stdout); // 确保立即刷新输出
        }

#ifdef TINY_MEASUREMENT_ENABLE_LCD
        // LCD显示 - 每行一个轴,最后一行显示温度
        // 使用6位小数以支持完整的20位精度
        if (s_config.enable_lcd)
        {
            char lcd_str[48];
            // 第1行:X轴
            snprintf(lcd_str, sizeof(lcd_str), "X:%.6f", accel.x);
            lcd_show_string(0, 0, lcd_self.width, 16, 16, lcd_str, BLACK);
            // 第2行:Y轴
            snprintf(lcd_str, sizeof(lcd_str), "Y:%.6f", accel.y);
            lcd_show_string(0, 16, lcd_self.width, 16, 16, lcd_str, BLACK);
            // 第3行:Z轴
            snprintf(lcd_str, sizeof(lcd_str), "Z:%.6f", accel.z);
            lcd_show_string(0, 32, lcd_self.width, 16, 16, lcd_str, BLACK);
            // 第4行:温度
            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, "读取加速度失败: %s", esp_err_to_name(accel_ret));
        }
        if (temp_ret != ESP_OK)
        {
            ESP_LOGE(TAG, "读取温度失败: %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, "在线传感已在运行,请先停止");
        return ESP_ERR_INVALID_STATE;
    }

    // 应用配置
    if (config != NULL)
    {
        memcpy(&s_config, config, sizeof(online_sensing_config_t));
    }
    else
    {
        // 使用默认配置
        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;
    }

    // 验证配置
    // 注意:代码接受高达300 Hz,但实际硬件稳定运行上限约为200 Hz
    // 此限制主要是由于MQTT传输的约束 - 在更高频率下,MQTT发布无法跟上
    // 数据生成速率,导致缓冲区溢出和数据丢失
    if (s_config.sampling_frequency_hz <= 0.0f || s_config.sampling_frequency_hz > 300.0f)
    {
        ESP_LOGE(TAG, "无效的采样频率: %.2f Hz (有效范围: 0.1 - 300 Hz)",
                 s_config.sampling_frequency_hz);
        return ESP_ERR_INVALID_ARG;
    }

    ESP_LOGI(TAG, "在线传感已初始化:");
    ESP_LOGI(TAG, "  - 采样频率: %.2f Hz", s_config.sampling_frequency_hz);
    ESP_LOGI(TAG, "  - MQTT启用: %s", s_config.enable_mqtt ? "是" : "否");
    ESP_LOGI(TAG, "  - 串口输出启用: %s", s_config.enable_serial ? "是" : "否");
#ifdef TINY_MEASUREMENT_ENABLE_LCD
    ESP_LOGI(TAG, "  - LCD显示启用: %s", s_config.enable_lcd ? "是" : "否");
#endif
    if (s_config.mqtt_topic)
    {
        ESP_LOGI(TAG, "  - MQTT主题: %s", s_config.mqtt_topic);
    }

    return ESP_OK;
}

esp_err_t online_sensing_start(void)
{
    if (s_is_running)
    {
        ESP_LOGW(TAG, "在线传感已在运行");
        return ESP_ERR_INVALID_STATE;
    }

    // 获取ADXL355句柄(假设在main中已初始化)
    // 这应该在启动前由应用程序设置
    if (s_adxl355_handle == NULL)
    {
        ESP_LOGE(TAG, "ADXL355句柄未设置。请先调用 online_sensing_set_sensor_handle()");
        return ESP_ERR_INVALID_STATE;
    }

    // 计算定时器周期(微秒)
    // 注意:实际稳定运行上限约为200 Hz(5000微秒周期)
    // 此限制主要是由于MQTT传输的约束 - 在更高频率下,MQTT发布无法跟上数据生成速率
    uint64_t period_us = (uint64_t)(1000000.0f / s_config.sampling_frequency_hz);

    // 验证周期(代码层面检查:最小100微秒 = 最大300 Hz)
    // 但实际硬件稳定运行上限约为200 Hz,主要受MQTT传输带宽限制
    if (period_us < 100)
    {
        ESP_LOGE(TAG, "采样频率过高: %.2f Hz (最大: 300 Hz)", s_config.sampling_frequency_hz);
        return ESP_ERR_INVALID_ARG;
    }

    // 创建定时器
    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, "创建定时器失败: %s", esp_err_to_name(ret));
        return ret;
    }

    // 在启动定时器前设置运行标志(以便回调可以执行)
    s_is_running = true;

#ifdef TINY_MEASUREMENT_ENABLE_LCD
    // 在启动在线传感前清除LCD屏幕
    if (s_config.enable_lcd)
    {
        lcd_clear(WHITE);
    }
#endif

    // 在启动周期定时器前立即执行第一次采样(t=0)
    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, 周期: %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, "在线传感未运行");
        return ESP_ERR_INVALID_STATE;
    }

    s_is_running = false;

    // 停止并删除定时器
    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, "停止定时器失败: %s", esp_err_to_name(ret));
        }

        ret = esp_timer_delete(s_sampling_timer);
        if (ret != ESP_OK)
        {
            ESP_LOGE(TAG, "删除定时器失败: %s", esp_err_to_name(ret));
        }
        s_sampling_timer = NULL;
    }

    ESP_LOGI(TAG, "在线传感已停止");
    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, "在线传感已反初始化");
    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, "无效的采样频率: %.2f Hz (有效范围: 0.1 - 300 Hz)", frequency_hz);
        return ESP_ERR_INVALID_ARG;
    }

    // 如果定时器正在运行,需要用新频率重启它
    bool was_running = s_is_running;
    if (was_running)
    {
        online_sensing_stop();
    }

    s_config.sampling_frequency_hz = frequency_hz;
    ESP_LOGI(TAG, "采样频率已更新为 %.2f Hz", frequency_hz);

    // 如果之前正在运行,则重启
    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传输 %s", enable ? "已启用" : "已禁用");
    return ESP_OK;
}

esp_err_t online_sensing_set_serial_enable(bool enable)
{
    s_config.enable_serial = enable;
    ESP_LOGI(TAG, "串口输出 %s", enable ? "已启用" : "已禁用");
    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显示 %s", enable ? "已启用" : "已禁用");
    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 设置ADXL355传感器句柄(必须在启动前调用)
 * @param handle ADXL355句柄指针
 * @return 成功返回ESP_OK,失败返回错误码
 */
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传感器句柄已设置");
    return ESP_OK;
}

关键实现

定时器回调

在线传感的核心是定时器回调函数 sampling_timer_callback(),它以固定间隔执行:

  • 传感器读取:通过SPI读取加速度和温度数据(非阻塞)
  • 数据格式化:使用6位小数格式化数据,支持完整的20位精度
  • 输出通道:根据配置发布到MQTT、串口和可选的LCD
  • 错误处理:记录错误但继续采样以维持时序

定时器管理

模块使用ESP-IDF的高精度定时器(esp_timer)进行周期性采样:

  • 定时器创建:创建周期定时器,周期 = 1,000,000 / 采样频率_hz 微秒
  • 立即首次采样:在启动周期定时器前立即执行回调(t=0)
  • 状态管理:使用 s_is_running 标志防止停止后回调执行

配置验证

  • 频率范围:验证 0.1 - 300 Hz(代码层面限制)
  • 实测限制:稳定运行约200 Hz(受MQTT传输带宽限制)
  • 默认值:当config为NULL时使用 tiny_measurement_config.h 中的宏

运行时更新

  • 频率更新:如果正在运行,停止并重启定时器以使用新频率
  • 通道控制:可以在运行时启用/禁用MQTT、串口和LCD输出
  • 非阻塞:MQTT发布是非阻塞的,但如果缓冲区满可能会失败

使用示例

#include "tiny_measurement.h"
#include "node_acc_adxl355.h"

// 初始化传感器
adxl355_handle_t adxl355_handle;
adxl355_init(&adxl355_handle, ADXL355_RANGE_2G, ADXL355_ODR_1000);

// 设置传感器句柄
online_sensing_set_sensor_handle(&adxl355_handle);

// 使用默认配置初始化
online_sensing_init(NULL);

// 启动在线传感
online_sensing_start();

// 运行时配置更新
online_sensing_set_frequency(50.0f);  // 改为50 Hz
online_sensing_set_mqtt_enable(false); // 禁用MQTT
online_sensing_set_serial_enable(false); // 禁用串口输出

// 完成后停止
online_sensing_stop();

// 清理
online_sensing_deinit();