加速度传感¶
采样可以说是本项目最重要的功能之一。它允许我们收集和存储来自传感器的数据,以便后续分析和处理。由于Arduino性能非常有限,本项目采用一边采样一边存储的方式来实现数据采集。由于没有引入实时操作系统,存储的过程会对采样造成一定的影响,所以无法实现很高的采样频率,但是由于本项目的展示和教学性质,采样频率不需要很高。经过测试100Hz的采样频率完全可以实现,而且由于是一边采样一边存储,所以数据上限基本上等同于SD卡的容量。
sensing.hpp
#pragma once
#include <stdint.h>
#define SENSING_PREPARING_DUR_MS 5000 // Duration for preparing sensing in milliseconds
extern uint64_t sensing_scheduled_start_ms; // Scheduled sensing start time (Unix ms)
extern uint64_t sensing_scheduled_end_ms; // Scheduled sensing end time (Unix ms)
extern uint32_t sensing_rate_hz; // Sensing rate in Hz
extern uint32_t sensing_duration_s; // Sensing duration in seconds
typedef struct {
uint16_t elapsed_ms; // Elapsed time since sensing started (ms)
int16_t ax;
int16_t ay;
int16_t az;
} SamplePoint;
bool sensing_start(); // Called once at the beginning of SAMPLING state
void sensing_sample_once(); // Called repeatedly during SAMPLING state
void sensing_stop(); // Called once at the end of SAMPLING state
void sensing_retrieve_file(); // Retrieve file from SD card
sensing.cpp
#include <Arduino.h>
#include "config.hpp"
#include "nodestate.hpp"
#include "time.hpp"
#include "rgbled.hpp"
#include "mpu6050.hpp"
#include "sensing.hpp"
#include "mqtt.hpp"
#include "sdcard.hpp"
#include "logging.hpp"
uint64_t sensing_scheduled_start_ms = 0;
uint64_t sensing_scheduled_end_ms = 0;
uint32_t sensing_rate_hz = 0;
uint32_t sensing_duration_s = 0;
static File data_file;
static uint32_t last_sample_time = 0;
static uint32_t t_start_ms = 0;
static uint32_t sample_count = 0;
static char filename[32];
bool sensing_start()
{
t_start_ms = millis();
last_sample_time = t_start_ms;
sample_count = 0;
load_log_number(); // Load current log number from persistent storage
snprintf(filename, sizeof(filename), "N%03d_%03d.txt", NODE_ID, log_number + 1);
Serial.print("[SD] Opening file for streaming: ");
Serial.println(filename);
data_file = SD.open(filename, FILE_WRITE);
if (!data_file)
{
Serial.println("[SD] Failed to open file.");
return false;
}
data_file.println("=============== Sampling Metadata ===============");
#ifdef NODE_ID
data_file.print("Node ID: ");
data_file.println(NODE_ID);
#endif
data_file.print("Start Time: ");
data_file.println(SensingSchedule.to_string());
data_file.print("Sampling Rate: ");
data_file.print(sensing_rate_hz);
data_file.println(" Hz");
data_file.print("Duration: ");
data_file.print(sensing_duration_s);
data_file.println(" s");
data_file.println("================= Sampling Data =================");
data_file.println("time_ms,ax,ay,az");
Serial.println("[SENSING] Sensing started (streaming mode).");
return true;
}
void sensing_sample_once()
{
uint32_t now_ms = millis();
if (now_ms - last_sample_time >= (1000 / sensing_rate_hz))
{
last_sample_time += (1000 / sensing_rate_hz);
int16_t ax, ay, az;
imu_get_acceleration(ax, ay, az);
uint16_t elapsed = (uint16_t)(now_ms - t_start_ms);
float ax_g = ax / 16384.0f;
float ay_g = ay / 16384.0f;
float az_g = az / 16384.0f;
char line[64];
snprintf(line, sizeof(line), "%u,%.6f,%.6f,%.6f", elapsed, ax_g, ay_g, az_g);
data_file.println(line);
sample_count++;
}
}
void sensing_stop()
{
Serial.print("[SENSING] Sampling completed. ");
Serial.print(sample_count);
Serial.println(" samples collected.");
if (data_file)
{
data_file.close();
Serial.print("[SD] File saved: ");
Serial.println(filename);
log_number++;
save_log_number();
}
// Reopen and print file content
File f = SD.open(filename, FILE_READ);
if (f)
{
Serial.println("[SD] Dumping file content:");
while (f.available())
{
Serial.write(f.read());
}
f.close();
}
else
{
Serial.println("[SD] Failed to reopen file for reading.");
}
sample_count = 0;
}
void sensing_retrieve_file()
{
File file = SD.open(retrieval_filename, FILE_READ);
if (!file)
{
Serial.print("[Error] File not found: ");
Serial.println(retrieval_filename);
return;
}
Serial.print("[Retrieval] Reading file: ");
Serial.println(retrieval_filename);
size_t total_size = file.size();
size_t bytes_sent = 0;
size_t chunk_size = 850;
size_t chunk_index = 1;
size_t chunk_total = (total_size + chunk_size - 1) / chunk_size;
char prefix[32];
snprintf(prefix, sizeof(prefix), "%s", retrieval_filename + 1); // Remove leading '/'
char topic[64];
while (file.available())
{
char buffer[851]; // chunk_size + 1 for null terminator
size_t len = file.readBytes(buffer, chunk_size);
buffer[len] = '\0';
snprintf(topic, sizeof(topic), "%s[%d/%d]:", prefix, chunk_index, chunk_total);
String payload = String(topic) + String(buffer);
bool ok = mqtt_client.publish(MQTT_TOPIC_PUB, payload.c_str());
if (ok)
{
bytes_sent += len;
Serial.print("[MQTT] Sent chunk ");
Serial.print(chunk_index);
Serial.print(" / ");
Serial.print(chunk_total);
Serial.print(" (");
Serial.print(bytes_sent);
Serial.print(" / ");
Serial.print(total_size);
Serial.println(" bytes)");
}
else
{
Serial.print("[Error] Failed to send chunk ");
Serial.println(chunk_index);
}
chunk_index++;
mqtt_loop(); // keep MQTT alive
delay(50); // throttle
}
file.close();
String done_msg = String(prefix) + "[done]";
mqtt_client.publish(MQTT_TOPIC_PUB, done_msg.c_str());
Serial.println("[MQTT] File upload completed.");
node_status.node_flags.data_retrieval_requested = false;
node_status.node_flags.data_retrieval_sent = true;
}
如上面代码所示,采样过程分为几个阶段:
-
采样开始时间和结束时间:这一部分是在MQTT命令回调时就已经完成。
-
calling
sensing_start()
:在采样状态开始时调用,打开SD卡文件并写入采样元数据。在这个函数中调用了一个load_log_number()
函数来加载当前日志编号,并在文件名中使用它。文件名格式为N001_001.txt
,其中N001
是节点ID,001
是日志编号。在SD卡中有个文件记录了当前日志编号,采样完成后会自动增加。 -
calling
sensing_sample_once()
:在采样状态中重复调用,读取传感器数据并写入SD卡文件。每次采样都会检查是否达到了设定的采样频率(sensing_rate_hz
),如果达到了,就读取传感器数据并写入文件。在主程序的loop中,当当前时间减去上次采样时间大于等于1000 / sensing_rate_hz
时,就进行一次采样。采样数据包括时间戳和加速度传感器的三个轴向数据(ax, ay, az),并将其写入SD卡文件。 -
calling
sensing_stop()
:在采样状态结束时调用,关闭SD卡文件并打印采样结果。这个函数会打印采样的总数,并将文件内容重新打开以便打印到串口。
Info
本项目中,由于串口输出速度很慢,会拖累采样和存储,所以在采样过程中,我们只做存储而不做串口输出。采样完成后会重新打开文件并打印内容到串口。