24、建筑监控与管理系统及Qt框架开发应用解析

建筑监控与管理系统及Qt框架开发应用解析

1. 管理工具与空调服务
  • 管理工具 :借助C&C服务器实现的API,利用Qt5框架和Mosquitto MQTT客户端库开发了基于GUI的管理工具,可对节点进行基本管理,并将其覆盖在建筑物布局图上。不过,图形工具开发较为复杂,且通常局限于建筑物的单层,除非使用包含所有楼层及节点映射的超大地图,而这显然不够便捷。
  • 空调服务 :为控制空调机组,开发了类似C&C的服务,其核心代码如下:
#include <string> 
#include <vector> 

using namespace std; 

#include <Poco/Data/Session.h> 
#include <Poco/Data/SQLite/Connector.h> 

#include <Poco/Net/HTTPClientSession.h> 
#include <Poco/Net/HTTPSClientSession.h> 

#include <Poco/Timer.h> 

using namespace Poco; 
using namespace Poco::Net; 

class Listener; 

struct NodeInfo { 
    string uid; 
    float posx; 
    float posy; 
    float current;     
    float target; 
    bool ch0_state; 
    UInt8 ch0_duty; 
    bool ch0_valid; 
    bool ch1_state; 
    UInt8 ch1_duty; 
    bool ch1_valid; 
    bool ch2_state; 
    UInt8 ch2_duty; 
    bool ch2_valid; 
    bool ch3_state; 
    UInt8 ch3_duty; 
    bool ch3_valid; 
    UInt8 validate; 
}; 

struct ValveInfo { 
    string uid; 
    UInt8 ch0_valve; 
    UInt8 ch1_valve; 
    UInt8 ch2_valve; 
    UInt8 ch3_valve; 
}; 

struct SwitchInfo { 
    string uid; 
    bool state; 
}; 

#include "listener.h" 

class Nodes { 
    static Data::Session* session; 
    static bool initialized; 
    static HTTPClientSession* influxClient; 
    static string influxDb; 
    static bool secure; 
    static Listener* listener; 
    static Timer* tempTimer; 
    static Timer* nodesTimer; 
    static Timer* switchTimer; 
    static Nodes* selfRef; 

public: 
    static void init(string influxHost, int influxPort, string influxDb, string influx_sec, Listener* listener);
    static void stop(); 
    static bool getNodeInfo(string uid, NodeInfo &info); 
    static bool getValveInfo(string uid, ValveInfo &info); 
    static bool getSwitchInfo(string uid, SwitchInfo &info); 
    static bool setTargetTemperature(string uid, float temp); 
    static bool setCurrentTemperature(string uid, float temp); 
    static bool setDuty(string uid, UInt8 ch0, UInt8 ch1, UInt8 ch2, UInt8 ch3); 
    static bool setValves(string uid, bool ch0, bool ch1, bool ch2, bool ch3); 
    static bool setSwitch(string uid, bool state); 
    void updateCurrentTemperatures(Timer& timer); 
    void checkNodes(Timer& timer);
    void checkSwitch(Timer& timer); 
    static bool getUIDs(vector<string> &uids); 
    static bool getSwitchUIDs(vector<string> &uids); 
};

Nodes类本质上是SQLite数据库的包装器,包含节点、阀门和制冷/制热开关的信息,还设有定时器,用于不断触发应用程序检查系统状态,与目标状态进行比较,并在必要时进行调整。

Listener类则广泛使用Nodes类来跟踪节点和连接的空调机组的状态,以及控制水流的开关和阀门的状态,代码如下:

#include <mosquittopp.h> 

#include <string> 
#include <map> 

using namespace std; 

#include <Poco/Mutex.h> 

using namespace Poco; 

struct NodeInfo; 
struct ValveInfo; 
struct SwitchInfo; 

#include "nodes.h" 

class Listener : public mosqpp::mosquittopp { 
    map<string, NodeInfo> nodes; 
    map<string, ValveInfo> valves; 
    map<string, SwitchInfo> switches; 
    Mutex nodesLock; 
    Mutex valvesLock; 
    Mutex switchesLock; 
    bool heating; 
    Mutex heatingLock; 

public: 
    Listener(string clientId, string host, int port); 
    ~Listener(); 

    void on_connect(int rc); 
    void on_message(const struct mosquitto_message* message); 
    void on_subscribe(int mid, int qos_count, const int* granted_qos); 
    bool checkNodes(); 
    bool checkSwitch(); 
};

该应用程序的工作方式是,Nodes类的定时器会使Listener类在PWM、IO和开关模块的主题上发布消息,查询应处于活动状态的设备的状态。这种主动循环系统在工业应用中很常见,可持续验证系统,快速检测是否有异常。

2. InfluxDB记录传感器读数

从一开始,记录传感器读数和咖啡机统计信息就是重点。对于这类数据,理想的数据库是时间序列数据库,Influx是常见的选择。但它不支持MQTT,仅提供HTTP和本地接口。为解决此问题,编写了一个简单的MQTT到Influx HTTP行协议的桥接器,代码如下:

#include "mth.h" 

#include <iostream> 

using namespace std; 

#include <Poco/Net/HTTPRequest.h> 
#include <Poco/Net/HTTPResponse.h> 
#include <Poco/StringTokenizer.h> 
#include <Poco/String.h> 

using namespace Poco; 

MtH::MtH(string clientId, string host, int port, string topics, string influxHost,  
                int influxPort, string influxDb, string influx_sec) : mosquittopp(clientId.c_str()) { 
    this->topics  = topics; 
    this->influxDb = influxDb; 
    if (influx_sec == "true") {  
          cout << "Connecting with HTTPS..." << std::endl; 
          influxClient = new Net::HTTPSClientSession(influxHost, influxPort); 
          secure = true;  
    }  
    else { 
          cout << "Connecting with HTTP..." << std::endl; 
          influxClient = new Net::HTTPClientSession(influxHost, influxPort); 
          secure = false;  
    } 

    int keepalive = 60; 
    connect(host.c_str(), port, keepalive); 
}

MtH::~MtH() { 
    delete influxClient; 
} 

void MtH::on_connect(int rc) { 
    cout << "Connected. Subscribing to topics...\n"; 

    if (rc == 0) { 
          StringTokenizer st(topics, ",", StringTokenizer::TOK_TRIM | StringTokenizer::TOK_IGNORE_EMPTY);
          for (StringTokenizer::Iterator it = st.begin(); it != st.end(); ++it) { 
                string topic = string(*it); 
                cout << "Subscribing to: " << topic << "\n"; 
                subscribe(0, topic.c_str()); 

                // Add name of the series to the 'series' map. 
                StringTokenizer st1(topic, "/", StringTokenizer::TOK_TRIM | StringTokenizer::TOK_IGNORE_EMPTY);
                string s = st1[st1.count() - 1]; // Get last item. 
                series.insert(std::pair<string, string>(topic, s)); 
          } 
    } 
    else { 
          cerr << "Connection failed. Aborting subscribing.\n"; 
    } 
}

void MtH::on_message(const struct mosquitto_message* message) { 
    string topic = message->topic;       
    map<string, string>::iterator it = series.find(topic); 
    if (it == series.end()) {  
          cerr << "Topic not found: " << topic << "\n"; 
          return;  
    } 

    if (message->payloadlen < 1) { 
          cerr << "No payload found. Returning...\n"; 
          return; 
    } 

    string payload = string((const char*) message->payload, message->payloadlen); 
    size_t pos = payload.find(";"); 
    if (pos == string::npos || pos == 0) { 
        cerr << "Invalid payload: " << payload << ". Reject.\n"; 
        return; 
    } 

    string uid = payload.substr(0, pos); 
    string value = payload.substr(pos + 1); 
    string influxMsg;  
    influxMsg = series[topic]; 
    influxMsg += ",location=" + uid; 
    influxMsg += " value=" + value; 
    try { 
        Net::HTTPRequest request(Net::HTTPRequest::HTTP_POST,  
        "/write?db=" + influxDb, Net::HTTPMessage::HTTP_1_1); 
        request.setContentLength(influxMsg.length()); 
        request.setContentType("application/x-www-form-urlencoded"); 
        influxClient->sendRequest(request) << influxMsg; 

        Net::HTTPResponse response; 
        influxClient->receiveResponse(response); 
    } 
    catch (Exception& exc) { 
        cout << "Exception caught while attempting to connect." <<  
        std::endl; 
        cerr << exc.displayText() << std::endl; 
        return; 
    }
}

当收到新的MQTT消息时,会查找对应的Influx时间序列名称,创建要发送到InfluxDB服务器的字符串。假设消息负载由发送消息的节点的MAC地址后跟分号组成,取分号后的部分作为值,MAC作为位置,然后将其发送到数据库服务器。

3. 安全方面

在系统开发过程中,安全至关重要。因此考虑添加传输层安全(TLS)加密,利用Sming框架中的集成axTLS加密库和AES证书(主机和客户端),不仅可验证主机(服务器)和客户端(节点)的身份,还能提供安全的加密链接。
然而,ESP8266内存不足以分配默认的TLS握手缓冲区,需要服务器(主机)端使用SSL片段大小扩展。但常用的MQTT代理Mosquitto不支持此扩展,客户端需使用默认的双16 kB缓冲区。一种解决方案是修改Mosquitto代理的源代码并重新编译,更好的方案是安装代理软件HAProxy,它作为TLS端点,处理证书并将解密后的流量通过本地回环(localhost)接口重定向到MQTT代理。将SSL片段大小选项设置为1 - 2 kB后,系统可正常工作,实现了建筑物范围内的无线监控和控制系统,确保敏感信息和精细控制命令的安全通信。

4. Qt框架开发嵌入式系统
  • 框架的力量 :框架是为简化特定应用程序开发而设计的代码集合,为开发者提供一系列类,使其无需担心底层硬件接口或操作系统API,即可实现应用逻辑。之前使用过多种框架,如No date框架、CMSIS、Arduino、POCO框架和Qt框架等。不同框架有不同的目标系统,No date、CMSIS和Arduino针对微控制器(MCUs),实时操作系统框架(RTOS)包含完整的操作系统,而POCO和Qt则针对操作系统,作为操作系统特定API的抽象层,同时提供额外功能,便于快速构建功能齐全的应用程序。
  • Qt用于命令行应用 :虽然Qt框架以图形用户界面(GUI)为卖点,但也可用于开发仅命令行的应用程序。示例代码如下:
#include <QCoreApplication>  
#include <core.h>  

int main(int argc, char *argv[]) {  
   QCoreApplication app(argc, argv);  
   Core core;  

   connect(&core, &Core::done, &app, &app::quit, Qt::QueuedConnection);  
   core.start();  

   return app.exec();  
} 

#include <QObject>  

class Core : public QObject {  
   Q_OBJECT  
public:  
   explicit Core(QObject *parent = 0);  

signals:  
   void done();  
public slots:  
   void start();  
}; 

#include "core.h"  
#include <iostream>  

Core::Core(QObject *parent) : QObject(parent) {  
   //  
}  

void Core::start() {  
   std::cout << "Start emitting done()" << std::endl;  
   emit done();  
} 

在这个例子中,使用QCoreApplication类创建输入和事件循环处理程序。每个想使用Qt信号 - 槽架构的类都需从QObject类派生,并在类声明中包含Q_OBJECT宏。

5. GUI-based Qt applications
  • 代码对比与变化 :将基于Qt的示例项目(来自之前的Linux - 基于信息娱乐系统示例)的主函数与前面的仅命令行版本进行对比,当添加GUI时,有明显变化。
#include "mainwindow.h"  
#include <QApplication>  

int main(int argc, char *argv[]) {  
    QApplication a(argc, argv);  
    MainWindow w;  
    w.show();  

    return a.exec();  
} 

最明显的变化是使用 QApplication 而不是 QCoreApplication ,并且使用从 QMainWindow 派生的类。
- MainWindow类的定义与功能

#include <QMainWindow>  

#include <QAudioRecorder>  
#include <QAudioProbe>  
#include <QMediaPlayer>  


namespace Ui {  
    class MainWindow;  
}  

class MainWindow : public QMainWindow {  
    Q_OBJECT  

public:  
    explicit MainWindow(QWidget *parent = nullptr);  
    ~MainWindow();  

public slots:  
    void playBluetooth();  
    void stopBluetooth();  
    void playOnlineStream();  
    void stopOnlineStream();  
    void playLocalFile();  
    void stopLocalFile();  
    void recordMessage();  
    void playMessage();  

    void errorString(QString err);  

    void quit();  

private:  
    Ui::MainWindow *ui;  

    QMediaPlayer* player;  
    QAudioRecorder* audioRecorder;  
    QAudioProbe* audioProbe;  

    qint64 silence;  

private slots:  
    void processBuffer(QAudioBuffer);  
}; 

MainWindow 类从 QMainWindow 派生,拥有 show() 方法。 MainWindow 实例在 UI 命名空间中声明,这与运行 qmake 工具生成的自动代码相关。
- 构造函数的操作与功能

MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent),  
    ui(new Ui::MainWindow) {  
    ui->setupUi(this); 
    connect(ui->actionQuit, SIGNAL(triggered()), this, SLOT(quit())); 
    connect(ui->playBluetoothButton, SIGNAL(pressed), this, SLOT(playBluetooth));  
    connect(ui->stopBluetoothButton, SIGNAL(pressed), this, SLOT(stopBluetooth));  
    connect(ui->playLocalAudioButton, SIGNAL(pressed), this, SLOT(playLocalFile));  
    connect(ui->stopLocalAudioButton, SIGNAL(pressed), this, SLOT(stopLocalFile));  
    connect(ui->playOnlineStreamButton, SIGNAL(pressed), this, SLOT(playOnlineStream));  
    connect(ui->stopOnlineStreamButton, SIGNAL(pressed), this, SLOT(stopOnlineStream));  
    connect(ui->recordMessageButton, SIGNAL(pressed), this, SLOT(recordMessage));  
    connect(ui->playBackMessage, SIGNAL(pressed), this, SLOT(playMessage)); 
    silence = 0;  

    // Create the audio interface instances.  
    player = new QMediaPlayer(this);  
    audioRecorder = new QAudioRecorder(this);  
    audioProbe = new QAudioProbe(this);  

    // Configure the audio recorder.  
    QAudioEncoderSettings audioSettings;  
    audioSettings.setCodec("audio/amr");  
    audioSettings.setQuality(QMultimedia::HighQuality);      
    audioRecorder->setEncodingSettings(audioSettings);      
    audioRecorder->setOutputLocation(QUrl::fromLocalFile("message/last_message.amr"));  

    // Configure audio probe.  
    connect(audioProbe, SIGNAL(audioBufferProbed(QAudioBuffer)), this, SLOT(processBuffer(QAudioBuffer))); 
    audioProbe->setSource(audioRecorder); 
    QThread* thread = new QThread;  
    VoiceInput* vi = new VoiceInput();  
    vi->moveToThread(thread);  
    connect(thread, SIGNAL(started()), vi, SLOT(run()));  
    connect(vi, SIGNAL(finished()), thread, SLOT(quit()));  
    connect(vi, SIGNAL(finished()), vi, SLOT(deleteLater()));  
    connect(thread, SIGNAL(finished()), thread, SLOT(deleteLater()));  

    connect(vi, SIGNAL(error(QString)), this, SLOT(errorString(QString)));  
    connect(vi, SIGNAL(playBluetooth), this, SLOT(playBluetooth));  
    connect(vi, SIGNAL(stopBluetooth), this, SLOT(stopBluetooth));  
    connect(vi, SIGNAL(playLocal), this, SLOT(playLocalFile));  
    connect(vi, SIGNAL(stopLocal), this, SLOT(stopLocalFile));  
    connect(vi, SIGNAL(playRemote), this, SLOT(playOnlineStream));  
    connect(vi, SIGNAL(stopRemote), this, SLOT(stopOnlineStream));  
    connect(vi, SIGNAL(recordMessage), this, SLOT(recordMessage));  
    connect(vi, SIGNAL(playMessage), this, SLOT(playMessage));  

    thread->start();  
} 

构造函数首先从UI描述文件中加载GUI,该文件通常使用Qt Designer工具创建。然后,将GUI中的菜单动作和按钮小部件连接到内部槽。还进行了音频接口实例的创建和配置,以及线程的创建和连接,将一些耗时操作移到单独的线程中执行,避免阻塞UI线程。
- 析构函数的作用

MainWindow::~MainWindow() {  
    delete ui;  
} 

析构函数用于删除UI和所有关联元素。

6. 未来发展展望

该系统仍有许多可改进之处,例如增加支持的传感器数量、使用更多的GPIO扩展芯片、优化空调系统配置、实现与日历后端关联的房间占用检测,以及清理无人参加的办公室会议安排等。此外,还可以考虑将MCU从ESP8266更换为基于ARM的MCU,以获得有线以太网选项和更好的调试与开发工具。虽然带有Wi - Fi的MCU使用方便,但ESP8266的开发工具不够完善,且缺乏有线通信选项(不使用外部芯片时),系统的可靠性依赖于Wi - Fi网络质量。对于建筑自动化系统,可靠性很重要,因此未来可能会采用有线和无线相结合的混合网络。

建筑监控与管理系统及Qt框架开发应用解析

7. 系统架构与功能总结

为了更清晰地理解整个系统的架构和功能,我们可以通过以下表格进行总结:
| 系统组件 | 功能描述 | 关键代码类 |
| — | — | — |
| 管理工具 | 基于GUI对节点进行基本管理,覆盖在建筑物布局图上 | 无(使用Qt5和Mosquitto MQTT库开发) |
| 空调服务 | 控制空调机组,管理节点、阀门和开关信息,检查系统状态 | Nodes类、Listener类 |
| InfluxDB桥接器 | 将MQTT消息转换为InfluxDB可接受的格式并记录传感器数据 | MtH类 |
| 安全机制 | 提供传输层安全(TLS)加密,确保通信安全 | 使用HAProxy作为TLS端点 |
| Qt命令行应用 | 使用QCoreApplication创建命令行应用程序,利用信号 - 槽机制 | Core类、QCoreApplication |
| Qt GUI应用 | 创建具有图形用户界面的应用程序,处理音频、播放等功能 | MainWindow类、QApplication |

下面是系统的整体工作流程的mermaid流程图:

graph LR;
    A[管理工具] -->|管理节点| B[空调服务];
    B -->|记录数据| C[InfluxDB桥接器];
    C -->|存储数据| D[InfluxDB];
    E[安全机制] -->|加密通信| A;
    E -->|加密通信| B;
    E -->|加密通信| C;
    F[Qt命令行应用] -->|独立运行| G[系统功能];
    H[Qt GUI应用] -->|用户交互| G;
8. 各组件详细分析
  • 管理工具 :虽然提供了直观的节点管理方式,但受限于图形工具开发的复杂性和地图展示的局限性,在多层建筑管理方面存在不足。未来可以考虑开发更灵活的地图展示方式,如分层切换或3D地图,以提高管理效率。
  • 空调服务 :Nodes类和Listener类的设计使得系统能够有效地管理空调机组的各种参数和状态。通过定时器的使用,实现了对系统状态的实时监测和调整。然而,随着系统规模的扩大,定时器的管理可能会变得复杂,需要考虑更高效的调度算法。
  • InfluxDB桥接器 :解决了InfluxDB不支持MQTT的问题,实现了传感器数据的有效记录。但在处理大量消息时,可能会出现性能瓶颈,需要优化消息处理和数据库写入的逻辑。
  • 安全机制 :采用HAProxy作为TLS端点是一个有效的解决方案,确保了系统通信的安全性。但在证书管理和更新方面,需要建立完善的机制,以应对证书过期等问题。
  • Qt命令行应用 :展示了Qt框架在命令行开发中的灵活性,通过信号 - 槽机制实现了程序的简洁和高效。但对于复杂的命令行交互,可能需要进一步扩展和优化。
  • Qt GUI应用 :利用Qt的强大功能创建了具有丰富交互性的图形用户界面。但在多线程处理方面,需要注意线程安全问题,避免出现数据竞争和死锁等情况。
9. 开发建议与实践技巧
  • 框架选择 :根据项目需求选择合适的框架是关键。对于微控制器开发,可以优先考虑No date、CMSIS和Arduino等框架;对于操作系统相关的开发,POCO和Qt框架能提供更强大的功能和更好的抽象层。
  • 代码结构 :保持良好的代码结构和注释,有助于提高代码的可维护性和可读性。例如,在Nodes类和Listener类中,将不同的功能模块分开实现,便于后续的扩展和修改。
  • 安全意识 :在开发过程中始终保持安全意识,特别是在涉及敏感信息和控制命令的通信中,要采用加密和认证机制,确保系统的安全性。
  • 性能优化 :对于可能出现性能瓶颈的部分,如InfluxDB桥接器和多线程处理,要进行性能测试和优化,采用合适的算法和数据结构提高系统的响应速度。
10. 总结与启示

通过对建筑监控与管理系统及Qt框架开发应用的分析,我们可以得到以下启示:
- 一个复杂的系统通常由多个组件组成,每个组件都有其特定的功能和作用,需要合理设计和集成,以实现系统的整体目标。
- 框架的使用可以大大简化开发过程,但也需要根据具体需求进行选择和定制,充分发挥框架的优势。
- 安全是系统开发中不可忽视的重要因素,要采取有效的安全措施,保障系统的稳定运行和数据安全。
- 随着技术的不断发展,系统需要不断进行改进和升级,以适应新的需求和挑战,未来的系统可能会采用更先进的技术和架构。

总之,建筑监控与管理系统及Qt框架开发应用是一个综合性的领域,涉及到硬件、软件、网络和安全等多个方面。通过深入学习和实践,我们可以不断提高自己的开发能力,开发出更加高效、安全和可靠的系统。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值