先前出于工作需要,接触学习了一下视觉上位机的制作,了解到一般都是使用C#+halcon或者OpenCVSharp,本人基本没有接触过C#,对C++和OpenCV倒是有一点认识,于是尝试用Qt实现一下简易的视觉上位机上学习资料杂乱不堪,学习难度大,故本文综合了一下这些资料,分享一下我自己的学习经验,代码不全,主要演示思路:
本文主要分为:
1.OpenCV,海康相机SDK环境配置,SDK二次封装
2.UI界面功能板块介绍
2.项目代码,实现逻辑
一.OpenCV与海康相机SDK配置,SDK二次封装
这部分内容网上已经有很多教程了,如果使用Qt自带的Mingw编译器,就可以下载Cmake来编译一次OpenCV,这个可以移步B站去找教程,如果使用MSVC编译器的话,就直接在项目文件中引入外部库即可,同样也有教程,请读者自行查阅,这里不再赘述
引入海康相机的SDK需要去官网下载MVS软件,下载后在文件夹内找到的development文件,将includes下的.h文件和lib文件下的.lib文件(WIN32或者WIN64文件下)复制到pro文件同目录下即可完成配置,注意,这时点击构建的时候有可能出现未定义__int64 错误,这是由于在PixelType文件下的__int64数据类型未定义,找到typedef __int64 int64_t;并在其上方写如下,再点击构建即可,编译成功后便完成了海康相机SDK的加载
#define __int64 long long
引入的SDK直接使用会比较麻烦,不太符合面对对象的编程思想,一般会进行二次封装,在Qt中额外定义一个camera类,封装一下SDK中的打开相机,采集相机,关闭相机等常用的函数,这里做主要二次封装的思路演示,具体重写的函数数量与类型视需求而定,可以通过查阅SDK中MvCameraControl.h与中对各函数的解释与定义即可完成二次封装:
//引入SDK文件
class camera
{
public:
camera();
~camera();
public:
int open(MV_CC_DEVICE_INFO *deviceinfoPtr);
int close();
bool IsDeviceConnected();
//抓图
int StartGrabbing();
//停止抓图
int StopGrabbing();
//主动获取一帧的图像数据
int GetImageBuffer(MV_FRAME_OUT *framePtr,int Sec);
//释放图像缓存
int FreeImageBuffer(MV_FRAME_OUT *framePtr);
..........
}
二.UI界面功能
这部分较为简单,本文着重说明如何代码实现,UI根据项目需求自行设计,大体可以分为相机设置,PLC通讯设置,采集图像显示,视觉算法处理,本案例暂不制作该模块,读者自行根据需求加载即可
1.相机设置容器:
主要包括5个功能,加载相机名,查找子网下的所有GIGE和USB3的海康相机并显示名字到combox里面,打开相机,关闭相机,开始采集与停止采集功能
2.采集图像容器:
实时显示从相机中获取的图像
3.PLC通讯容器:
Qt可以通过串口与TCP进行PLC通讯,使用的协议是Modbus协议,本文中QT使用ModbusTCP协议封装数据包(视觉检测结果),PLC端使用FreeTCP(socket)发送视觉检测需求,这样设置的原因是,汇川PLC作为通讯主站时使用FreeTCP比较简单,指令很少,后面细说
三.项目代码
该部分主要是建立TCP服务端,相机进程,视觉算法封装三个模块
1.建立TCP服务端与实现PLC通讯细节
该部分我们需要通过IP地址与端口号进行与PLC的通讯建立,IP地址可以在汇川PLC的编程软件AutoShop中的通讯设置模块进行配置,这里假设设置为192.168.10.10,端口号默认为502,那么Qt中就需要通过这两个建立连接,使用Qt中自带的类,信号与槽机制完成,首先在pro文件中引入如下代码:
QT+= serialbus
QT+= network
构建通过后,在.h文件中引入如下文件,并分别定义ModbusClient,QTcpSocket,QTcpServer类指针,并在主线程(UI)启动时,在构造函数中进行初始化,在析构中完成资源释放
#include<QModbusClient>
#include<QModbusDataUnit>
#include<QModbusReply>
#include<QTcpSocket>
#include<QTcpServer>
......
private:
QModbusClient *myclient=nullptr;
QTcpSocket *mySocket=nullptr;
QTcpServer *myServer=nullptr;
QList<QString>RWMod;//combox显示内容
...
Wiget::Widget(QWidget *parent):QWidget(parent)
,ui(new Ui::Widget)
{
ui->setupUi(this);
//初始化指针,记得释放
myclient=new QModbusClient();
mySocket=new QTcpSocket();
myServet=new QTcpServer();
//初始化选择读取寄存器类型,
//按需求添加K,C,T,X,Y等寄存器
RWMod.push_back("M寄存器");
RWMod.push_back("D寄存器");
for(int i=0;i<RWMod.size();i++)
{
ui->CBPLCdata->addItem(RWMod.at(i));
}
ui->PBdisConnect->setEnable(false);
}
Widget::~widget()
{
connect(socket,&QTcpSocket::disconnected,this,[=]()
{
qDebug()<<"断开客户端连接";
//close and deletelater
});
if(myclient!=null)
{
delete myclient;
}
if(mySocket!=null)
{
delete mySocket;
}
if(myServer!=null)
{
delete myServer;
}
delete ui;
}
下面在cpp文件中实现TCP服务端建立,使用listen方法监听本机的7960端口,监听成功后,server对象作为发送者,发送建立连接的信号,主线程UI接受信号,并执行表达式内的方法由socket完成建立连接,socket发送读取端口数据的信号,并在表达式内解包读取到的TCP报文
//在构造函数中建立连接或者在槽函数中建立
bool result =server->listen(QHostAddress::Any,7960);//监听本机7960端口
if(result)
{
qDebug()<<"服务器启动成功";
}
else{
qDebug()<<"服务器启动失败";
QMessageBox::warning(this,"警告","未能连接上PLC");
return;
}
qDebug()<<ret<<server->errorString();
connect(server,&QTcpServer::newConnection,this,[=]()
{
socket=server->nextPendingConnection();
if(socket!=nullptr)
{
qDebug()<<"已成功与客户端建立连接";
}
connect(socket,&QTcpSocket::readyRead,this,[=]()
{
QByteArray data=socket->readAll();
ui->plainTextEdit_15->insertPlainText(data);
qDebug()<<data;
//执行视觉检测
//上位机会接受到一个TCP报文,数据类型是QByteArray,根据报文选择不同的处理方法
QByteArray byte("0x00000");
if(data==byte)
{
qDebug()<<"检测到PLC视觉检测需求,开始检测";
//实现视觉检测并传递检测结果到PLC
}
});
});
通过上述的代码,我们就可以实现一个简易的TCP服务端,读者只需根据实际需求,设置switch语句对PLC发来的不同TCP报文内容进行选择执行,这里不再演示。
下面开始建立与PLC的连接,通过ip与端口号进行连接,这里必须确保电脑的IP地址与PLC在同一个子网下,如果连接失败可以先去检查电脑的网络配置和PLC是否接入同一个路由器,这里内定了PLC的IP和端口,实际使用过程可以通过遍历当前网络下的所用IP来进行选择连接
void MainWindow::on_PBconnect_clicked()
{
if(myclient==NULL)
{
return ;
//添加异常处理
}
if(myclient->state()==QModbusDevice::ConnectedState)
{
myclient->setConnectionParameter(QModbusDevice::NetworkPortParameter,502);
myclient->setConnectionParameter(QModbusDevice::NetworkAddresstParameter,"192.168.10.10");
myclient->setTimeout(3000);
myclient->setNumberRetties(3);
if(myclient->connectDevice())
{
QMessageBox::information(this," ","connected!");
ui->PBdisconnect->setEnable(true);
//连接后才可点击断开,在ui构造函数中初始设置为false
}
}
}
void Widget::on_PBdisconnect_clicked()
{
if(myclient->state()==QModbusTcpClient::ConnectedState)
{
myclient->disconnectDevice();
QMessageBox::information(this,"information","已断开连接");
}
ui->PBconnect->setEnable(true);
}
接着是构建发送读写请求的报文,我们使用ModbusTCP协议,主要使用QModbusDataUnit封装报文,通过信号与槽机制来完成,这里演示D位寄存器的读取与写入,修改unit的构造即可:
//读取
void Widget::readMethod(int readAddress,quint16 data)
{
//HoldingRegisters:保持寄存器
//Coils:线圈
QModbusDataUnit unit(QModbusDataUnit::HoldingRegisters,readAddress,data);
QModbusReply *reply=client->sendReadRequest(unit,1);
if(reply)
{
if(!reply->isFinished())
{
connect(reply,QModbusReply::finished,this,replyData);
return;
}
reply->deleteLater();
}
}
void Widget::replyData()
{
QModbusReply *reply =(QModbusReply*)(sender());
QModbusDataUnit unit =reply->result();
reply->deleteLater();
if(unit.valueCount()>0)
{
ui->plainTextEdit->insertPlainText(QString::number(unit.value(0)));
}
readunit=unit.value(0);
//qDebug()<<readunit;
}
void Widget::on_pushButton_clicked()
{
readMethod(ui->plainTextEdit_4->toPlainText().toInt());
//传入窗口的值
}
//写入
void Widget::writeMethod(int writeAddress,quint16 data)
{
QModbusDataUnit unit(QModbusDataUnit::HoldingRegisters,writeAddress,data);
unit.setValue(0,data);
QModbusReply *reply=client->sendWriteRequest(unit,1);
if(!reply->isFinished())
{
connect(reply,&QModbusReply::finished,this,[this,reply]()
{
if(reply->error()!=QModbusDevice::NoError)
reply->deleteLater();
});
}
else{
if(reply->error()!=QModbusDevice::NoError)
reply->deleteLater();
}
}
通过上述代码,我们就实现了Qt上位机连接PLC并读取与写入到保持寄存器(D类)的功能,只需要在按钮的clicked()函数中使用这两个方法就可以完成读写操作。
至此我们基本完成了上位机通讯模块的设置,下位机也需要对应的通讯设置,在汇川PLC的编程软件中写入如下代码:
M8000 TCPCON K192 K168 K10 K80 K7960
TCPSTA K192 K168 K10 K80 K7960 D1
X1 DMOV K2 D2
DMOV H30 D3
SET M0
//M8000为常开,X1为上升沿触发
//D1表示通讯状态,-1表示断开连接
//在AutoShop左侧工具栏中的通讯设置找到以太网设置,右键点击生成以太网配置,在其中配置如下:
//从站ip地址:192.168.10.80(假定主机地址)
//通讯方式:触发
//功能:读寄存器(03)
//触发条件:M0
//从站寄存器地址:D2(实际发送下一个地址的寄存器的数据,即D3)
//数据长度:50
//主站缓冲区地址:D500(填写不用的寄存器即可)
//端口号:7960
//站号:默认255
//协议:FreeTCP
至此就完成了该项目demo的全部制作,谢谢观看