本节将会介绍如何实现示波器上位机的网络数据共享,使得用户可以通过PC机、平板电脑等设备远程查看波形测量结果并配置示波器参数。下文将介绍如何在旭日X3派的上位机界面中添加TCP服务器,实现上述功能;并将客户端和服务端融合,

#include <QTcpSocket>
#include <QTcpServer>

在同一个界面上实现远程测量显示功能和本地服务器功能切换。

Part 1:服务器框架设计

本小节将会介绍如何编写服务器框架,使得客户端能够实连接到此上位机,并实现数据回环测试。

Step 1:在mainwindow.h中添加Socket网络库,并建立Socket对象指针(私有变量)

#include <QTcpSocket>
#include <QTcpServer>
QTcpServer *tcpServer=nullptr;
QList<QTcpSocket*> tcpSocket;

Step 2:为mainwindow.c添加TCP服务端打开函数:

bool MainWindow::openSocketServer()//Open SocketServer
{
    if(tcpServer)
    {
        ;//Doing Nothing if Server Object was settled
    }
    else
    {
        tcpServer=new QTcpServer(this);
        connect(tcpServer,&QTcpServer::newConnection,this,&MainWindow::on_TCPserver_Connect);
    }
    if(tcpServer==nullptr)return false;
    if(tcpServer->isListening())
    {
        for(auto itor=tcpSocket.begin();itor!=tcpSocket.end();itor++)
            (*itor)->disconnectFromHost();
        tcpServer->close();
        for(auto itor=tcpSocket.begin();itor!=tcpSocket.end();itor=tcpSocket.erase(itor));
    }
    return tcpServer->listen(QHostAddress::Any ,11401);
}

本段中主要根据tcpServer指针状态创建了一个监听11401端口的TCP服务端,如果先前已经创建了TCP服务端,则把服务端的连接关闭后再重新创建。

Step 3:添加连接建立回调函数

void MainWindow::on_TCPserver_Connect(void)//TCP Connection Enstablish slot
{
    printf("%s",QString("Got Connection\r\n").toLocal8Bit().data());
    QTcpSocket *socket=tcpServer->nextPendingConnection();
    tcpSocket.append(socket); //Append Socket Object to List
    connect(socket,&QTcpSocket::readyRead,this,&MainWindow::on_TCPserver_Read);
    connect(socket,&QTcpSocket::disconnected,this,&MainWindow::on_TCPserver_Disconnect);
} 

本段代码主要在有TCP客户端连接服务器时执行,服务器会将此设备的套接字(Socket)对象加入到tcpSocket链表中,并针对此套接字对象链接对应的数据接收槽函数(当客户端发来数据时会触发此函数)。

Step 4:添加断开连接回调函数:

void MainWindow::on_TCPserver_Disconnect(void)//TCP Disconnect Opt
{
    int cnt=0;
    for(auto itor=tcpSocket.begin();itor!=tcpSocket.end();)
    {
        QTcpSocket *socket=(QTcpSocket*)*itor;
        if(socket->state()==QTcpSocket::ConnectedState)
            itor++;
        else
            itor=tcpSocket.erase(itor);
        cnt++;
    }
}

本段代码主要在有TCP客户端断开服务器连接时执行,服务器会将此设备的套接字对象从tcpSocket链表中移除,不再对其数据进行处理。

Step 5:添加接收回调函数

void MainWindow::on_TCPserver_Read(void)//TCP Server Recv Data
{
    for(auto itor=tcpSocket.begin();itor!=tcpSocket.end();itor++)
    {
        QTcpSocket *socket=(QTcpSocket*)*itor;
        QByteArray RxBuff=socket->readAll();
        if(RxBuff.length()==0)continue;
        socket->write(RxBuff);//Loopback
        QString RxStr=QString::fromLocal8Bit(RxBuff);
        if(tcpSocket.length()>1)
            RxStr="<"+socket->peerAddress().toString()+":"+QString::number(socket->peerPort())+">"+RxStr+"\n";
        qDebug()<<RxStr;
        int spilt=RxStr.indexOf(".");
        int end=RxStr.indexOf(";");
        if((spilt==-1)||(end==-1))continue;//if Not A Currect CMD, PASS this Node
        qDebug()<<"GOT CMD";
    }
}

本段代码主要在有TCP客户端向服务器发送数据时执行,服务器会遍历所有连接的Socket对象,处理发来的数据。

本段代码主要执行了2部分操作:数据回环、命令解析,服务端会将收到的数据原样返回给客户端,并且通过调试窗口输出出来,如果接收到命令格式的数据,服务端还会从调试窗口输出“GOT CMD”字样。

Step 6:为槽函数添加声明

    bool openSocketServer();//Open SocketServer
    void on_TCPserver_Connect(void);//TCP Connection Enstablish slot
    void on_TCPserver_Read(void);//TCP Server Recv Data
    void on_TCPserver_Disconnect(void);//TCP Disconnect Opt

Step 7:构造函数添加初始化代码

    if(!openSocketServer())
    {
        delete tcpServer;
        tcpServer=nullptr;
        qDebug()<<"TCP Server Start Failed!";
        QMessageBox::warning(this,tr("TCP Server Start Failed!"),tr("Cannot Start OSC server, Port was Listen aready."),QMessageBox::Close);
    } 

本段代码主要调用了先前的服务器开启代码,如果开启失败则会删除服务器对象,并弹窗提示。

Step 8:构析函数添加

MainWindow::~MainWindow()
{
    delete ui;
    if(myserial)
    {
        if(myserial->isOpen()) myserial->close();
        delete myserial;
    }
    if(tcpServer)
    {
        if(tcpServer->isListening()) tcpServer->close();
        delete tcpServer;
    }
}

Step 9:测试验证

经过上述代码编写,已经初步实现一个可以实现数据回环的TCP服务端,可以使用TCP测试工具进行测试(工具链接见附录)。

先使用测试工具建立一个TCP客户端目标端口填写为11401IP地址根据旭日X3派实际情况填写。

配置发送缓冲区为“非16进制模式”(上侧不勾选),并点击连接按钮。

此时发送数据可以看到右侧屏幕下侧的接收数据结果

Part 2:添加数据转发代码

接下来将从串口数据接收回调和TCP接收回调函数中分别添加代码,实现数据转发。

Step 1:添加串口数据广播代码

void MainWindow::on_SerialRecv(void)
{
    static QByteArray RecvBuff;
    if(myserial->isOpen())
    {
        QByteArray QBArecv =myserial->readAll();
        RecvBuff.append(QBArecv);
        char SyncArray[]={0x53,0x59,0x4E,0x43,0x00,0xFF,0x00,0xFF};
        QByteArray Sync=QByteArray(SyncArray,8);
        if(RecvBuff.indexOf(Sync)!=-1)
        {
            QByteArray Packed=RecvBuff.left(RecvBuff.indexOf(Sync)+8);
            RecvBuff=RecvBuff.mid(RecvBuff.indexOf(Sync)+8);
//省略了绘图代码,实际上需要保留
            if(tcpServer)
            {
                for(auto itor=tcpSocket.begin();itor!=tcpSocket.end();itor++)
                {
                    QTcpSocket *socket=(QTcpSocket*)*itor;
                    socket->write(QBArecv);//TransMit
                }
            }
        }
    }
}

Step 2:修改TCP读取回调

void MainWindow::on_TCPserver_Read(void)//TCP Server Recv Data
{
    for(auto itor=tcpSocket.begin();itor!=tcpSocket.end();itor++)
    {
        QTcpSocket *socket=(QTcpSocket*)*itor;
        QByteArray RxBuff=socket->readAll();
        if(RxBuff.length()==0)continue; 
//省略了回环,按需保留
        int spilt=RxStr.indexOf(".");
        int end=RxStr.indexOf(";");
        if((spilt==-1)||(end==-1))continue;//if Not A Currect CMD, PASS this Node
        qDebug()<<"GOT CMD";
        if(myserial->isOpen())myserial->write(RxBuff);
    }
}

Step 3:数据接收测试

TCP测试工具的接收窗口改为“16进制显示”(下侧勾选16进制)

连接服务后,可以看到有源源不断的数据从服务端发出;发送字符串形式的控制命令,可以看到右侧调试输出可以正常识别运行模式。

Part 3:添加客户端功能

Step 1:修改界面

按照下图修改界面,添加网络配置框,以及最下侧的“Remote Mode”复选框,并且修改对象名如图右所示。

Step 2:修改mainwindow.h,添加客户端回调函数及对象指针。

    void on_TCP_Connect();
    void on_TCP_Read();
    void on_TCP_Disconnect();

    QTcpSocket *tcpClient=nullptr;

Step 3:为“Remote Mode”复选框添加stateChanged(int)回调函数

void MainWindow::on_checkBoxLocal_stateChanged(int arg1)
{
    ui->groupBoxPortSel->setVisible(!arg1);
    ui->groupBoxNetSel->setVisible(arg1);
    if(arg1)
    {
        if(myserial)if(myserial->isOpen())myserial->close();
        if(tcpClient)//Opened to close
        {
            ;
        }
        else//Closed to open
        {
            ui->textEditIP->setText("localhost");
            ui->textEditPort->setText("11401");
            tcpClient=new QTcpSocket(this);

            connect(tcpClient,&QTcpSocket::connected,this,&MainWindow::on_TCP_Connect);
            connect(tcpClient,&QTcpSocket::readyRead,this,&MainWindow::on_TCP_Read);
        }
    }
    else
    {
        if(tcpClient)
        {
            disconnect(tcpClient,&QTcpSocket::connected,this,&MainWindow::on_TCP_Connect);
            disconnect(tcpClient,&QTcpSocket::readyRead,this,&MainWindow::on_TCP_Read);
            disconnect(tcpClient,&QTcpSocket::disconnected,this,&MainWindow::on_TCP_Disconnect);
            if(tcpClient->isOpen())
            {
                tcpClient->disconnectFromHost();
                tcpClient->close();
            }
            ui->pushButtonOpenConnect->setText("Connect");
            delete tcpClient;
            tcpClient=nullptr;
        }
    }
}

此段代码主要是根据模式,选择性开启TCP Client。远程模式会自动关闭串口,打开TCP Client;非远程模式下会自动关闭TCP Client

Step 4:为连接按钮添加回调函数

void MainWindow::on_pushButtonOpenConnect_clicked()
{
    if(ui->checkBoxLocal->isChecked())
    {
        if(tcpClient==nullptr)return;
        if(tcpClient->isOpen())
        {
            tcpClient->disconnectFromHost();
            tcpClient->close();
            ui->pushButtonOpenConnect->setText("Connect");
        }
        else
        {
            QString ip=ui->textEditIP->toPlainText();
            int port=ui->textEditPort->toPlainText().toInt();
            tcpClient->connectToHost(ip,quint16(port));
        }
    }
}

Step 5:添加TCP Client连接、断开连接回调函数

void MainWindow::on_TCP_Connect(void)
{
    printf("%s",QString("Connectted\r\n").toLocal8Bit().data());
    connect(tcpClient,&QTcpSocket::disconnected,this,&MainWindow::on_TCP_Disconnect);
    ui->pushButtonOpenConnect->setText("DisConnect");
}
void MainWindow::on_TCP_Disconnect(void)
{
    printf("%s",QString("Disconnect\r\n").toLocal8Bit().data());
    tcpClient->close();
    if(ui->pushButtonOpenConnect)ui->pushButtonOpenConnect->setText("Connect");
}

 Step 6:参照串口函数添加读取与绘图函数

void MainWindow::on_TCP_Read(void)
{
    static QByteArray RecvBuff;
    QByteArray QBArecv=tcpClient->readAll();
    {
        RecvBuff.append(QBArecv);
        char SyncArray[]={0x53,0x59,0x4E,0x43,0x00,0xFF,0x00,0xFF};
        QByteArray Sync=QByteArray(SyncArray,8);
        if(RecvBuff.indexOf(Sync)!=-1)
        {
            QByteArray Packed=RecvBuff.left(RecvBuff.indexOf(Sync)+8);
            RecvBuff=RecvBuff.mid(RecvBuff.indexOf(Sync)+8);

            QBArecv=Packed;
            QString Recv="";
            if(1)
            {
                QString Temp;
                int cnt=0;
                QList<QPointF> points;
                foreach(QChar dat,QBArecv)
                {
                    Recv+=Temp.sprintf("%02X ",(unsigned int)dat.unicode());
                    if(cnt<(QBArecv.length()-8))points.append(QPointF(cnt++,(unsigned int)dat.unicode()));
                }
                series->replace(points);
                chart->axisX()->setRange(0,cnt-1);
            }
            else Recv+=QString::fromLocal8Bit(QBArecv);
            qDebug()<<Recv;
            qDebug()<<"Lenth="<<Packed.length();
            if(tcpServer)
            {
                for(auto itor=tcpSocket.begin();itor!=tcpSocket.end();itor++)
                {
                    QTcpSocket *socket=(QTcpSocket*)*itor;
                    socket->write(QBArecv);//TransMit
                }
            }
        }
    }
}

Step 7:构造构析函数添加(目前代码的最后一行):
为构造函数和构析函数分别加入下列代码,实现TCP客户端初始化。

on_checkBoxLocal_stateChanged(0);

Step 8:波形同步显示测试
编译代码并运行,可以得到具有远程查看功能的示波器界面,分别建立2个示波器界面:
第一个按照通常的使用模式,通过串口连接示波器下位机即可。
第二个界面,勾选“Remote Mode” 复选框,并点击“Connect”按钮便可实时获取波形数据。

Part 4:完善界面功能

Step 1:修改下拉框回调函数,添加TCP命令发送代码

void MainWindow::on_comboBoxBuffer_currentIndexChanged(const QString &arg1)
{
     QString Tx;
     Tx.sprintf("function:WindowLenth.%d;\r\n", (int)arg1.toInt());
     if(myserial->isOpen())myserial->write(Tx.toLocal8Bit());
     if(tcpClient)if(tcpClient->isOpen())tcpClient->write(Tx.toLocal8Bit());
}
void MainWindow::on_comboBoxSampleRate_currentIndexChanged(int index)
{
    int SampleMap[]={500,1000,2500,5000,10000,25000,50000,100000,250000,500000,1000000};
    QString Tx;
    Tx.sprintf("function:SimpleRate.%d;\r\n", SampleMap[index]);
    if(myserial->isOpen())myserial->write(Tx.toLocal8Bit());
    if(tcpClient)if(tcpClient->isOpen())tcpClient->write(Tx.toLocal8Bit());
}

Step 2:修改服务端命令解析代码

void MainWindow::on_TCPserver_Read(void)//TCP Server Recv Data
{
    for(auto itor=tcpSocket.begin();itor!=tcpSocket.end();itor++)
    {
        QTcpSocket *socket=(QTcpSocket*)*itor;
        QByteArray RxBuff=socket->readAll();
        if(RxBuff.length()==0)continue;
        socket->write(RxBuff);//Loopback
        QString RxStr=QString::fromLocal8Bit(RxBuff);
        if(tcpSocket.length()>1)
            RxStr="<"+socket->peerAddress().toString()+":"+QString::number(socket->peerPort())+">"+RxStr+"\n";
        qDebug()<<RxStr;
        int spilt=RxStr.indexOf(".");
        int end=RxStr.indexOf(";");
        if((spilt==-1)||(end==-1))continue;//if Not A Currect CMD, PASS this Node
        qDebug()<<"GOT CMD";
        if(myserial->isOpen())myserial->write(RxBuff);
        int paraI=RxStr.indexOf("function:SimpleRate.");
        if(paraI!=-1)
        {
            int Len=strlen("function:SimpleRate.");
            QString Para=RxStr.mid(paraI+Len,end-paraI-Len);
            int SRate=Para.toInt();
            int SampleMap[]={500,1000,2500,5000,10000,25000,50000,100000,250000,500000,1000000};
            for(int i=0;i<11;i++)
            {
                if(SampleMap[i]==SRate)
                {
                    ui->comboBoxSampleRate->setCurrentIndex(i);
                    SRate=0;
                    break;
                }
            }
            if(SRate)
                ui->comboBoxSampleRate->setEditText(QString::number(SRate));
        }
        paraI=RxStr.indexOf("function:WindowLenth.");
        if(paraI!=-1)
        {
            int Len=strlen("function:WindowLenth.");
            QString Para=RxStr.mid(paraI+Len,end-paraI-Len);
            int SRate=Para.toInt();
            int WindowMap[]={128,256,512,1024,2048,4096};
            for(int i=0;i<6;i++)
            {
                if(WindowMap[i]==SRate)
                {
                    ui->comboBoxBuffer->setCurrentIndex(i);
                    SRate=0;
                    break;
                }
            }
            if(SRate)
                ui->comboBoxBuffer->setEditText(QString::number(SRate));
        }
    }
}

本段代码主要实现了根据数组内容匹配命令参数,并将示波器客户端的设置数据同步给服务端的下拉框。

Step 3:最终演示

本项目最终实现了PCAndroid客户端,其联调效果如下:

旭日X3派上位机(左下角,服务器)

PC界面(背景,客户端)

Android界面(背景,客户端)

Part 5:代码与工具下载

界面源码:https://pan.baidu.com/s/11Imr6sGIyLAp00uvlTLqGQ?pwd=Yuki

TCP调试工具:https://pan.baidu.com/s/1eEffRw492zcfHN0iZkUcTw?pwd=Yuki