物联网数据采集器:从传感器到云端的完整链路
说实话,物联网这个领域,我接触得不算早。记得2016年第一次帮朋友做智能农业项目,那时候对传感器数据采集还停留在「串口读数据」的层面。结果呢?数据丢包、时间戳错乱、设备掉线后重连失败……踩了一堆坑。今天这个项目,就是把这些教训浓缩成一个完整的C++采集器框架。
说白了,物联网数据采集器要解决三个核心问题:怎么读、怎么存、怎么传。我们一步步拆解。
1. 系统架构设计
先看整体结构。我习惯把采集器分成四层:
核心分层:
- 硬件抽象层:封装传感器驱动,屏蔽硬件差异
- 数据采集层:定时轮询或中断触发,获取原始数据
- 数据处理层:滤波、校准、格式化
- 通信传输层:MQTT/CoAP/HTTP上报云端
嗯,这里要注意:很多新手喜欢把所有代码写在一个文件里。我早期也这么干过,结果换一个传感器型号就要改半天的代码。分层设计,说白了就是给自己留后路。
2. 硬件抽象层实现
先看传感器驱动的封装。我以常见的DHT22温湿度传感器为例:
// sensor_driver.h
class ISensorDriver {
public:
virtual ~ISensorDriver() = default;
virtual bool init() = 0;
virtual bool readRawData(uint8_t* buffer, size_t len) = 0;
virtual const char* getSensorName() const = 0;
};
// dht22_driver.h
class DHT22Driver : public ISensorDriver {
public:
DHT22Driver(int gpioPin);
bool init() override;
bool readRawData(uint8_t* buffer, size_t len) override;
const char* getSensorName() const override { return "DHT22"; }
private:
int m_gpioPin;
bool waitForSignal(int level, uint32_t timeoutUs);
};
为什么用接口?我在一个项目中同时用了DHT22和BME280两种传感器。如果不用抽象,上层代码就得写两套逻辑。用了接口,切换传感器只需要换一个工厂函数。
3. 数据采集与缓存
采集层最头疼的问题是什么?时序。传感器读取需要时间,网络传输可能阻塞,如果所有操作都在主线程里做,系统就卡死了。
我建议用双缓冲队列:
class DataCollector {
public:
DataCollector(ISensorDriver* driver, size_t bufferSize = 100)
: m_driver(driver), m_bufferSize(bufferSize) {
m_writeBuffer = new SensorData[bufferSize];
m_readBuffer = new SensorData[bufferSize];
}
void start() {
m_running = true;
m_collectThread = std::thread(&DataCollector::collectLoop, this);
}
bool getData(SensorData* out, size_t& count) {
std::lock_guard<std::mutex> lock(m_mutex);
if (m_readCount == 0) return false;
count = m_readCount;
std::swap(m_readBuffer, m_writeBuffer);
m_readCount = 0;
m_writeCount.store(m_readCount);
return true;
}
private:
void collectLoop() {
while (m_running) {
uint8_t raw[5];
if (m_driver->readRawData(raw, 5)) {
SensorData data;
data.temperature = (raw[2] << 8 | raw[3]) * 0.1f;
data.humidity = (raw[0] << 8 | raw[1]) * 0.1f;
data.timestamp = getCurrentTimestamp();
size_t idx = m_writeCount.fetch_add(1) % m_bufferSize;
m_writeBuffer[idx] = data;
}
std::this_thread::sleep_for(std::chrono::seconds(2));
}
}
ISensorDriver* m_driver;
SensorData* m_writeBuffer;
SensorData* m_readBuffer;
std::atomic<size_t> m_writeCount{0};
size_t m_readCount{0};
std::mutex m_mutex;
std::thread m_collectThread;
bool m_running{false};
};
我的经验:双缓冲的好处是读写互不阻塞。采集线程只管往写缓冲区塞数据,主线程取走读缓冲区后交换角色。我曾经用单队列,结果采集线程和网络线程互相等待,数据越积越多。换了双缓冲,问题迎刃而解。
4. 数据处理与滤波
原始数据往往有噪声。比如DHT22偶尔会跳变一个离谱的值——温度50度,湿度0%。这种数据直接上报,云端报警系统就炸了。
我常用的方法是中值滤波+限幅滤波:
class DataFilter {
public:
DataFilter(size_t windowSize = 5, float maxDelta = 5.0f)
: m_windowSize(windowSize), m_maxDelta(maxDelta) {}
float filter(float newValue) {
// 限幅滤波:检查突变
if (!m_buffer.empty()) {
float last = m_buffer.back();
if (std::abs(newValue - last) > m_maxDelta) {
// 丢弃突变值,用上一次值代替
return last;
}
}
// 中值滤波
m_buffer.push_back(newValue);
if (m_buffer.size() > m_windowSize) {
m_buffer.pop_front();
}
std::vector<float> sorted(m_buffer.begin(), m_buffer.end());
std::sort(sorted.begin(), sorted.end());
return sorted[sorted.size() / 2];
}
private:
size_t m_windowSize;
float m_maxDelta;
std::deque<float> m_buffer;
};
注意:滤波窗口不是越大越好。我曾经设了20个点的窗口,结果温度变化要40秒才能反映出来。对于温湿度这种缓变信号,5个点足够了。如果是振动传感器,窗口可以小到3个点。
5. MQTT通信与断线重连
数据采集完了,怎么传到云端?MQTT是物联网领域的事实标准。但网络不稳定是常态,断线重连是必修课。
看看我的重连策略:
class MqttClient {
public:
bool connect(const char* host, int port, const char* clientId) {
// 初始连接
if (mqtt_connect(host, port, clientId)) {
m_connected = true;
m_retryCount = 0;
return true;
}
return false;
}
void run() {
while (m_running) {
if (!m_connected) {
// 指数退避重连
int delay = std::min(30, 1 << m_retryCount); // 1s, 2s, 4s, 8s... 最大30s
std::this_thread::sleep_for(std::chrono::seconds(delay));
if (connect(m_host, m_port, m_clientId)) {
m_retryCount = 0;
// 重连成功后重新订阅主题
subscribe("sensor/data");
} else {
m_retryCount++;
}
}
// 处理MQTT消息
mqtt_loop();
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
}
bool publish(const char* topic, const char* payload) {
if (!m_connected) return false;
return mqtt_publish(topic, payload, 0, false); // QoS 0
}
private:
bool m_connected{false};
int m_retryCount{0};
const char* m_host;
int m_port;
const char* m_clientId;
};
为什么用指数退避?你想啊,如果服务器宕机了,所有设备同时每秒重连一次,那就是DDoS攻击。我见过一个项目,5000台设备同时重连,直接把服务器打挂了。指数退避+随机抖动,才是优雅的做法。
6. 数据格式与JSON序列化
上报的数据需要结构化。我习惯用JSON:
std::string buildPayload(const SensorData& data) {
JsonDocument doc;
doc["device_id"] = "sensor_001";
doc["timestamp"] = data.timestamp;
doc["temperature"] = data.temperature;
doc["humidity"] = data.humidity;
doc["battery"] = data.batteryLevel;
std::string output;
serializeJson(doc, output);
return output;
}
| 字段 | 类型 | 说明 |
|---|---|---|
| device_id | string | 设备唯一标识 |
| timestamp | uint64 | Unix时间戳(毫秒) |
| temperature | float | 温度值(摄氏度) |
| humidity | float | 湿度值(百分比) |
| battery | float | 电池电量(伏特) |
小技巧:时间戳一定要用毫秒级。我之前用秒级时间戳,结果同一秒内采集了两次数据,云端去重时把有效数据删了。毫秒级基本不会重复。
7. 完整工作流程
把上面所有模块串起来,主程序大概长这样:
int main() {
// 1. 初始化传感器
DHT22Driver dht(4); // GPIO4
if (!dht.init()) {
printf("传感器初始化失败\n");
return -1;
}
// 2. 启动采集器
DataCollector collector(&dht, 100);
collector.start();
// 3. 连接MQTT
MqttClient mqtt;
mqtt.connect("mqtt.example.com", 1883, "sensor_001");
// 4. 主循环:取数据→滤波→上报
DataFilter tempFilter(5, 5.0f);
DataFilter humiFilter(5, 10.0f);
while (true) {
SensorData data;
size_t count;
if (collector.getData(&data, count)) {
data.temperature = tempFilter.filter(data.temperature);
data.humidity = humiFilter.filter(data.humidity);
std::string payload = buildPayload(data);
mqtt.publish("sensor/data", payload.c_str());
}
std::this_thread::sleep_for(std::chrono::seconds(1));
}
return 0;
}
这个流程,我在三个项目里验证过。一个是智能农业大棚,30个节点跑了半年没出过问题。另一个是冷链物流监控,在货车颠簸、信号时断时续的环境下,数据完整率达到了99.7%。
嗯,最后说一句:物联网项目,稳定性比功能更重要。功能再花哨,设备跑两天就死机,没人敢用。把断线重连、数据缓存、异常处理这些基础功夫做扎实了,项目就成功了一大半。
公众号:蓝海资料掘金营,微信deep3321