物联网数据采集器:从传感器到云端的完整链路

说实话,物联网这个领域,我接触得不算早。记得2016年第一次帮朋友做智能农业项目,那时候对传感器数据采集还停留在「串口读数据」的层面。结果呢?数据丢包、时间戳错乱、设备掉线后重连失败……踩了一堆坑。今天这个项目,就是把这些教训浓缩成一个完整的C++采集器框架。

说白了,物联网数据采集器要解决三个核心问题:怎么读、怎么存、怎么传。我们一步步拆解。

1. 系统架构设计

先看整体结构。我习惯把采集器分成四层:

核心分层:

  • 硬件抽象层:封装传感器驱动,屏蔽硬件差异
  • 数据采集层:定时轮询或中断触发,获取原始数据
  • 数据处理层:滤波、校准、格式化
  • 通信传输层:MQTT/CoAP/HTTP上报云端

嗯,这里要注意:很多新手喜欢把所有代码写在一个文件里。我早期也这么干过,结果换一个传感器型号就要改半天的代码。分层设计,说白了就是给自己留后路。

物联网数据采集器架构图 硬件抽象层 传感器驱动 | GPIO控制 | ADC读取 | I2C/SPI/UART接口 数据采集层 定时轮询 | 中断处理 | 数据缓存 | 时间戳标记 数据处理层 滤波算法 | 数据校准 | 单位转换 | 异常检测 通信传输层 MQTT协议 | JSON序列化 | 断线重连 | 数据压缩

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