【嵌入式开发】监测系统——用QT编写下位机


本文为嵌入式课程的课程设计记录。
监测系统整体分为 上位机下位机,上位机使用C#在VS上进行开发,下位机使用Qt进行编写与编译,下载到开发板(itop4412)上运行。 本文主要介绍使用Qt进行嵌入式界面开发的部分
下位机需要完成的 任务为:

  • 监测itop4412开发板上的摄像头,当有移动物体时发送警报信息
  • 记录开发板上的滑动电阻阻值,以曲线的形式记录历史信息
  • 电子相册,显示历史摄像头信息

一、登陆界面

登陆界面
登录界面比较简单,在Qt上对按钮进行了一些修饰,让其变得更加美观了一些。

    QStringList qss;

    qss.append(QString("QPushButton{background-color:rgba(134,183,200,30);border:1px solid #5F92B2;\
                        border-radius:5px;color:black}"));
    qss.append(QString("QPushButton:hover{background-color:rgba(0,130,150,60);border:1px solid #5F92B2;\
                        border-radius:5px;color:black;}"));
    qss.append(QString("QPushButton:hover:pressed{background-color:rgba(85,170,255,90); border:1px solid #3C80B1;\
                        border-radius:5px;color:black;}"));
    this->setStyleSheet(qss.join(""));

注意:Qt中初始化QString时如果内容太长想换行,换行前需要在末尾输入反斜杠‘\’表示行未结束
一个注册按钮,一个登录按钮,密码输入栏可以选择密码可见,当点击了发送按钮的时候,下位机会通过TCP将登录信息发送给上位机,上位机存储了用户信息,将信息比对之后返回给下位机登陆结果。

二、自定义弹出式小键盘

2.1设计思路

为什么需要大费心思地去写一个小键盘?
因为开发板上的按键不够,也没有外接式键盘,好比如今的智能手机,怎么会用键盘来占用屏幕的空间呢?
自定义的弹出式小键盘效果图如下,当双击输入文本框的时候,小键盘会自动弹出到文本框的下方,根据文本框的位置不同小键盘会自动调整自己的位置:
在这里插入图片描述
设计思路:将小键盘绘制成一个独立的ui界面,这个ui上由多个按键进行一定的排列组成,在其他窗口需要调用小键盘的时候,只需要实例化小键盘然后显示就可以实现弹出的功能。这种设计思路具体有两种实现方式:

  • 一个按键对应一个槽函数
  • 所有按键对应同一个槽函数

很显然,第一种实现方式有很明显的弊端:有大量重复的代码、扩展性差。而第二种方法很好地解决了这两个弊端,难点就是如何在这个共同的槽函数中分辨出是哪个按键出发了槽函数。在Qt的信号与槽机制中有一个用法,可以用动态映射查看触发信号的发送者,这样一来就可以完美解决问题了。

那么这动态映射怎么工作的呢?
需要知道的是在Qt中,信号与槽都是建立在Object对象的基础上的,我们通过信号与槽将不同的对象联系起来,或者给对象赋予不同的功能,这种机制简洁易操作。当某个对象emit一个signal的时候,它就是一个sender,系统会记录下当前是谁emit了这个signal,所以你在对应的slot中可以通过Qt的函数sender()得到当前是谁触发了槽函数

比如我们在具体的实现过程中需要知道是哪个按键触发了槽函数:

QPushButton* button = qobject_cast<QPushButton*>(sender());

然后通过该button的信息我们可以判断具体的按键,然后执行相应的工作就可以啦。

2.2具体实现

前面说了小键盘的设计思路是设计成一个独立的ui界面,这个ui界面其实就是一个类,那么我们在某一个界面需要使用小键盘的时候,我们实例化这个类,然后通过show()函数将其显出出来就可以了
小键盘的ui界面如下:
在这里插入图片描述

槽函数内容:

//在ui界面中将每个按键的名称都对应好按键所代表的含义
void keyboard::keyclick_SLOT()
{
    QPushButton* button = qobject_cast<QPushButton*>(sender());
    QString text = button->text();
    qDebug()<<"Key : "<<text;
    if(button->objectName() == QString("pushButton_del"))
    {//删除按键
        //qDebug()<<"del";
        emit sendMsg(QString("del"));
    }
    else if(button->objectName() == QString("pushButton_clo"))
    {//关闭按键
        //qDebug()<<"close";
        emit sendMsg(QString("close"));
    }
    else if(button->objectName() == QString("pushButton_clr"))
    {//清空按键
        emit sendMsg(QString("clear"));
    }
    else
    {//数字和字母按键
        emit sendMsg(text); //信号传递给主窗口
    }
}

在代码中可以看到,我们使用了自定义的消息函数sendMsg(),其定义方法为在相应的h文件中申明signals,

signals:
    void sendMsg(QString);

现在我需要在登陆界面实例化这个小键盘:

//step1在登陆界面头文件里的class中申明变量
keyboard* keyBoard;//keyboard是小键盘类的名称

//step2在登录界面cpp文件中实例化小键盘
keyBoard = new keyboard(this); //键盘实例化
connect(keyBoard,SIGNAL(sendMsg(QString)),this,SLOT(keyboard_REV(QString)));//SIGNAL(sendMsg(QString))信号是我们自定义的消息

//step3当焦点事件变成文本框时,通过槽函数显示小键盘
void LoginWin::editchange_SLOT()
{//这个槽的触发信号是有文本框被选中
    currentEdit = qobject_cast<QLineEdit*>(sender());//使用了动态映射,记录当前被选中的文本框
    qDebug()<<"edit = "<<currentEdit;
    keyBoard->move(this->x()+currentEdit->x(),this->y()+currentEdit->y()+keyBoard->height()/2);
    keyBoard->show();
}

接受槽函数:

void LoginWin::keyboard_REV(QString text)
{
    if(currentEdit)
    {
        if(text == QString::fromLocal8Bit("del"))
        {
            qDebug()<<"del";
            currentEdit->backspace();
        }
        else if(text == QString::fromLocal8Bit("close"))
        {
            qDebug()<<"close";
            keyBoard->hide();
        }
        else if(text == QString::fromLocal8Bit("clear"))
        {
            qDebug()<<"clear";
            currentEdit->clear();
        }
        else
        {
            qDebug()<<"insert";
            currentEdit->insert(text);
        }
    }
}

ps:小键盘还是需要改一下样式的。

三、通讯协议

上位机和下位机的通讯协议使用了TCP协议,通讯内容分了两个部分:

  1. 文件(图片)的传输
  2. 短消息的传输,例如阻值信息、登录信息、心跳包等。

其中图片的传输通过搭建FTP进行传输,短消息的传输通过socket套接字完成。
消息数据帧格式的具体定义见下表:

-第一字节第二字节第三字节第四字节第五字节
查询(阻值、周期)00
查询(心跳包)01
应答(阻值、周期)1阻值字长(≥4)阻值周期字长周期
收到确认11/0
登录信息2账号字长账号密码字长密码
登录结果21/0
动态监测3

消息总共分为四类,分别是查询消息、应答消息、登录消息、动态监测消息,数据帧的第一个字节为消息类型,根据不同的消息,第二字节有些是后面的字节长度,有些是内容。确认消息第二字节的0代表没有收到,1代表收到了阻值消息,登录结果的第二字节1代表了登陆正确,0代表登录失败。
上位机和下位机的消息通讯按照上表所示的消息格式发送消息,接收方同样按照该格式对接受到的消息进行解析。

四、检测界面

监测界面的效果如下:
在这里插入图片描述
监控界面共分为一下几个部分:

  • 图像显示
  • 历史阻值曲线显示
  • 当前阻值显示
  • 警报信息(检测到有物体移动时报警)
  • 设置采样频率

图像显示使用QLabel控件,历史阻值曲线使用Qt的重绘函数paintEvent()。

根据我自己理解,paintEvent这个重绘函数功能很强大,如果是绘制一个ui的部分的话,这个函数个关键是掌握setViewport和setWindow两个函数,及如何设置坐标。目前我们编写Qt程序的时候通常是使用图形界面来绘制ui,而paintEvent函数是直接代码来绘制ui。这个函数一般的触发条件是update(),可以使用定时器来调用update()函数来绘制ui界面。

网上也有一些很好地解释setViewport和setWindow函数的博客,这两个函数当初也是把我整的死去活来,在这里我依据自己的理解通俗地解释一下这两个函数。简单来说,setViewport是用来决定显示在窗口的哪个位置的,例如我只需要绘制整个界面的右上部分,我们不妨称这个右上部分为窗口I;而setWindow是用来设置窗口I的坐标的,我们绘制界面使用QPainter就离不开坐标系。
现在我们假设一些条件:

  • 窗口尺寸为300*200
  • 需要绘制的部分左上角坐标为(200,10),大小为100*100。

setViewport函数原型为:

inline void QPainter::setViewport(int x, int y, int w, int h)
{
    setViewport(QRect(x, y, w, h));
}
//x:右上角水平坐标
//y:右上角垂直坐标
//w:窗口宽度
//h:窗口高度
setViewport(200,10,100,100);

在这里插入图片描述
蓝色窗口就是主窗口,上面代码的意思是设置绘制区域为红色方框区域。这个函数还是很容易理解的,关键是setWindow这个函数,设置自定义的坐标系,这种设定的目的是为了让坐标系符合我们习惯的形式
首先我们需要了解,在Qt中坐标系的设定是这样的:
在这里插入图片描述
这个坐标系我们称之为逻辑坐标系,左上点为坐标的原点,需要注意的是,不管我们如何变换窗口和视角,绘画的参考坐标系都是上面这个坐标系。但是我们很多时候用这个坐标系很不方便,我们可以定义一个我们自己的坐标系,setWindow就是实现这个自定义的坐标系的。已经说过了,不管怎么变换,绘画的参考坐标系都是上面这个坐标系,setWindow也不会改变这个参考坐标系,那么定义自己的坐标系又是什么意思呢?
准确来说,setWindow改变的是
我将逻辑坐标系中哪个部分放到窗口I中
(窗口I是我们上面设定的那个窗口)。假如:painter.setWindow(10, 10, 100, 100);
在这里插入图片描述
上图中红框左上角坐标为(10,10),宽高为(100,100),则上面这个代码的意思就是将红框里的内容进行压缩之后放到我定义的视角窗口中,这里压缩的意思是我的红色窗口的长宽和窗口I的大小可能并不一致,所以需要伸缩变换。
那么我们怎么利用setWindow来定义自己的坐标系呢?再看代码:

painter.setWindow(0, 100, 100, -100);

根据我们上面的解释,这个代码的意思是我们将左上角坐标为(0,100),宽高为(100,-100)的区域显示到我的视角窗口中。这里宽高加负号的意思就是我将x或者y方向进行反向了。也很好理解,高-100就将向下的y轴变成向上的y轴,高的绝对值还是100。这样设置的效果是:
在这里插入图片描述
黑色坐标系是逻辑坐标系,红色坐标系是我们设定的坐标系,红色坐标系是符合我们使用习惯的坐标系。我们在使用画笔绘图时可以以红色坐标系为我们的坐标系,但是我们必须清楚,我们“画在红色坐标系”上的内容实际上通过转换映射到了黑色逻辑坐标系上。而自定义坐标的原点和长宽就决定了我们将逻辑坐标系里哪些部分展示到窗口I内。
总结一下就是:

  • setViewPort设置了显示窗口的位置和大小
  • 所有绘制的内容最后都绘制到了逻辑坐标系下,而setWindow就设置了我们将逻辑坐标系下哪部分内容放到显示窗口中显示(这也解释了为什么setWindow宽高被解释成缩放系数,因为你逻辑坐标系下10001000大小的内容放到100100的显示窗口中不就是缩小了嘛)

ps:有个需要注意的地方,如果你的压缩比例差的很大,比如横坐标压缩了100倍而纵坐标却没有压缩,那么你画竖线的时候需要画的很粗很粗才能显示出来。
Qtc上的运行效果图为;
在这里插入图片描述
为了效果,我把曲线的显色调整了一下,低于3000曲线为蓝色表示过低,高于7000为红色表示过高,在这之间的为绿色表示正常。

五、历史信息显示

在这里插入图片描述
这个做的就比较简单了,左右选择按钮,左右两侧分别有预览窗口(其实就是缩小版的图片显示),按照按钮的逻辑更新QL able的显示内容就可以实现了。
在这里插入图片描述

六、总结

嵌入式开发本是一个软硬件结合的工作,本文主要讲解了软件部分(Qt部分),另外我认为比较重要的还有环境的搭建和通讯,有时间再更。咕~

  • 1
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
好的,下面是一个简单的用 Qt 写的 socket 代理层代码,可以实现两个客户端同时连接下位机并向下位机发送命令: ```cpp #include <QTcpServer> #include <QTcpSocket> class Proxy : public QObject { Q_OBJECT public: Proxy(QObject *parent = nullptr) : QObject(parent) {} void start(int port) { m_server = new QTcpServer(this); connect(m_server, &QTcpServer::newConnection, this, &Proxy::onNewConnection); m_server->listen(QHostAddress::Any, port); } private slots: void onNewConnection() { QTcpSocket *clientSocket = m_server->nextPendingConnection(); if (m_sockets.size() < 2) { connect(clientSocket, &QTcpSocket::readyRead, this, &Proxy::onReadyRead); m_sockets.append(clientSocket); } else { clientSocket->disconnectFromHost(); clientSocket->deleteLater(); } } void onReadyRead() { QTcpSocket *senderSocket = qobject_cast<QTcpSocket*>(sender()); QByteArray data = senderSocket->readAll(); for (QTcpSocket *socket : m_sockets) { if (socket != senderSocket) { socket->write(data); } } } private: QTcpServer *m_server = nullptr; QList<QTcpSocket*> m_sockets; }; ``` 这段代码使用 Qt 中的 `QTcpServer` 和 `QTcpSocket` 类实现了一个简单的 socket 代理层。在 `start` 函数中创建一个 `QTcpServer` 对象,并监听指定的端口。当有新的连接请求时,`QTcpServer` 会发出 `newConnection` 信号,我们在 `onNewConnection` 槽函数中接收该信号,并创建一个新的 `QTcpSocket` 对象来处理连接请求。 由于我们要实现两个客户端同时连接下位机的功能,所以需要在 `onNewConnection` 中对连接数进行判断。如果当前已经有两个客户端连接,那么直接关闭新的连接请求。如果连接数不足两个,那么就将新的 `QTcpSocket` 对象加入到 `m_sockets` 列表中,并连接 `readyRead` 信号。 当某个客户端发送数据时,`QTcpSocket` 会发出 `readyRead` 信号,我们在 `onReadyRead` 槽函数中接收该信号,并将数据转发给所有其他客户端。具体地,我们遍历 `m_sockets` 列表,如果当前 `QTcpSocket` 对象不是发送方,则将数据写入该对象。 需要注意的是,在处理 `readyRead` 信号时,我们没有对数据进行解析或处理,而是直接将其转发给其他客户端。因此,需要根据具体的协议或数据格式来解析和处理数据。 以上是一个简单的用 Qt 写的 socket 代理层代码,可以实现两个客户端同时连接下位机并向下位机发送命令。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值