Qt5开发金橙子打标卡上位机:SDK封装与多线程实战
1. 项目缘起与整体设计思路1.1 为什么选择Qt5来做打标卡上位机金橙子打标卡在激光打标行业里算是老牌选手了配套的EzCad软件功能确实全但真到了产线集成阶段问题就来了——客户要的是MES对接、要的是自动上下料联动、要的是自定义报表EzCad那套封闭界面根本改不动。这时候二次开发就成了刚需。选Qt5做上位机框架理由很直接。第一打标卡SDK给的是C接口的动态库Qt5的C原生调用毫无障碍不需要像C#那样折腾P/Invoke。第二Qt5的信号槽机制处理打标完成回调、轴运动状态更新这类异步事件特别顺手比MFC的消息映射清爽太多。第三跨平台。虽然打标卡驱动目前只在Windows下跑但上位机界面用Qt写以后想迁到Linux工控机上也不是没可能。我见过不少同行用C# WinForm做也能跑但涉及到多线程打标和界面刷新同步的时候Invoke嵌套Invoke的代码写起来实在难受。Qt5的QMetaObject::invokeMethod配合Qt::QueuedConnection跨线程更新UI就是一行代码的事。1.2 整体架构怎么搭整个项目我分成三层来设计这个分层逻辑是踩过坑之后定下来的。底层是SDK封装层。金橙子给的MarkEzd.dll导出的是纯C函数直接散落在业务代码里调用会非常乱。我建了一个LaserController单例类把lmc1_Initial、lmc1_LoadEzdFile、lmc1_Mark这些接口全部包一层统一做错误码转换和日志记录。这样以后换打标卡品牌只需要重写这一个类。中间是业务逻辑层。打标流程控制、参数管理、文件队列这些放在这里。关键点是打标操作必须放在独立线程因为lmc1_Mark是阻塞调用放主线程界面直接卡死。上层是UI层。主界面用QMainWindow左边放文件列表中间是打标预览区域右边是参数面板底部状态栏显示打标进度和卡状态。预览区域我一开始想用OpenGL自己画后来发现直接用SDK提供的lmc1_GetPrevBitmap拿到预览位图转成QImage显示在QLabel上就够了省事且准确。注意SDK的预览位图是BGR格式还是RGB格式不同版本有差异拿到之后先存成bmp文件用画图打开确认一下不然颜色会反。1.3 开发环境与依赖清单环境这块我列一下实际用的版本避免大家踩版本兼容的坑组件版本说明Qt5.12.95.15也行但5.12的MSVC2017包最稳编译器MSVC2017 64位必须和SDK的位数一致打标卡SDKMarkEzd 2.x随卡附赠注意区分32/64位操作系统Win10 专业版关闭驱动签名强制IDEQt Creator 4.11或者VS2017Qt插件这里有个大坑金橙子早期SDK只有32位版本如果你的Qt装的是64位链接的时候会报LNK1112模块计算机类型冲突。解决办法要么换32位Qt要么找厂家要64位SDK。我建议直接上64位现在新卡基本都支持了。2. 核心细节解析与实操要点2.1 SDK接口封装的关键细节金橙子SDK的函数命名有规律lmc1_开头的是基础控制lmc1_后面跟动词。我挑几个最核心的说。初始化函数lmc1_Initial有两个参数第一个是卡号单卡填0第二个是回调函数指针。这个回调很重要打标完成、卡异常都会通过它通知。回调函数签名是void __stdcall CallBack(int nCard, int nMsg, int nParam)注意__stdcall不能少否则栈会乱。// LaserController.h class LaserController : public QObject { Q_OBJECT public: static LaserController instance(); bool initCard(int cardNo 0); bool loadFile(const QString ezdPath); bool startMark(); void stopMark(); bool isMarking() const; signals: void markFinished(int cardNo); void cardError(int cardNo, int errorCode); private: LaserController() default; static void __stdcall sdkCallback(int nCard, int nMsg, int nParam); std::atomic_bool m_marking{false}; };回调里绝对不能直接操作UI也不能做耗时操作。我的做法是在回调里只发一个Qt信号用Qt::QueuedConnection连到主线程的槽函数。这里有个细节回调是SDK内部线程调用的而LaserController对象活在主线程跨线程发信号Qt会自动排队安全。lmc1_LoadEzdFile加载的是EzCad生成的.ezd文件这个文件里包含了图形、图层参数、打标速度功率等全部信息。加载成功返回0失败返回错误码。常见错误是文件路径含中文SDK内部用的是ANSI编码中文路径会乱码导致加载失败。解决办法是把ezd文件复制到临时目录用英文路径加载或者用GetShortPathName转短路径。2.2 打标线程的设计与信号槽传递打标必须异步这是铁律。我设计了一个MarkWorker类继承QObject然后moveToThread到一个QThread上。// MarkWorker.cpp void MarkWorker::doMark(const QString filePath) { if (!LaserController::instance().loadFile(filePath)) { emit markFailed(filePath, 加载文件失败); return; } LaserController::instance().startMark(); // startMark内部是阻塞的直到打标完成或出错才返回 emit markFinished(filePath); }信号槽传递结构体的时候要注意如果结构体不是Qt元类型跨线程队列连接会失败。比如我想传递一个包含文件路径、打标时间、结果的MarkResult结构体必须用Q_DECLARE_METATYPE(MarkResult)声明并且在连接前调用qRegisterMetaTypeMarkResult(MarkResult)。这个坑我踩过当时信号发出去了槽函数死活不进调试了半天才发现是元类型没注册。提示Qt5.12之后如果结构体有默认构造函数和拷贝构造函数Q_DECLARE_METATYPE基本就够了但qRegisterMetaType还是建议加上尤其是用QVariant包装的时候。2.3 参数管理与配置文件打标参数分两类一类是EzCad文件里带的图层参数另一类是上位机自己控制的比如打标次数、延时、报警阈值。后者我用QSettings存ini文件路径放在QStandardPaths::AppConfigLocation下避免权限问题。参数面板上我做了个实时校验比如打标速度范围是1到10000功率是0到100超出范围输入框变红并且禁用开始按钮。这个校验逻辑写在QValidator的子类里比在点击按钮时再弹窗提示体验好得多。3. 实操过程与核心环节实现3.1 从零搭建工程的具体步骤第一步Qt Creator新建项目选Qt Widgets Application构建套件选MSVC2017 64位。项目名随便起比如LaserMarker。第二步把SDK的头文件和lib文件放到项目目录下建个thirdparty文件夹。在.pro文件里加INCLUDEPATH $$PWD/thirdparty/include LIBS -L$$PWD/thirdparty/lib -lMarkEzd如果是VS工程就在项目属性里配附加包含目录和附加库目录。第三步把SDK的dll复制到输出目录。我写了个QMAKE_POST_LINK自动拷贝QMAKE_POST_LINK $$quote(cmd /c copy /y $$shell_path($$PWD/thirdparty/bin/*.dll) $$shell_path($$OUT_PWD/$$DESTDIR))第四步实现LaserController单例。初始化的时候调用lmc1_Initial如果返回非0弹窗提示检查卡是否插好、驱动是否安装。驱动安装包在SDK目录下装完设备管理器里能看到USB Device或者PCI Device。3.2 打标流程的完整代码实现主界面的打标按钮点击后流程是这样的void MainWindow::onStartMarkClicked() { if (m_fileList-count() 0) { QMessageBox::warning(this, 提示, 请先添加打标文件); return; } m_markThread new QThread(this); m_worker new MarkWorker; m_worker-moveToThread(m_markThread); connect(m_markThread, QThread::started, [](){ m_worker-doMark(m_fileList-currentItem()-text()); }); connect(m_worker, MarkWorker::markFinished, this, MainWindow::onMarkFinished); connect(m_worker, MarkWorker::markFailed, this, MainWindow::onMarkFailed); connect(m_markThread, QThread::finished, m_worker, QObject::deleteLater); m_markThread-start(); ui-btnStart-setEnabled(false); ui-btnStop-setEnabled(true); }onMarkFinished里更新界面、记录日志、如果还有下一个文件就自动继续。这里有个细节连续打标的时候不要每次都新建线程线程创建销毁有开销。我的做法是线程常驻用信号触发doMark打标完成后线程进入事件循环等待下一次触发。停止打标调用lmc1_StopMark这个函数是线程安全的可以在主线程直接调。调用后lmc1_Mark会返回一个非零值doMark里判断返回值就知道是被停止的还是正常完成的。3.3 预览功能的实现细节预览用lmc1_GetPrevBitmap函数原型是int lmc1_GetPrevBitmap(int nCard, HBITMAP* hBitmap)。拿到HBITMAP后转成QImageHBITMAP hBmp nullptr; int ret lmc1_GetPrevBitmap(0, hBmp); if (ret 0 hBmp) { QImage img QtWin::fromHBITMAP(hBmp, QtWin::HBitmapFormat_ARGB32); ui-labelPreview-setPixmap(QPixmap::fromImage(img).scaled( ui-labelPreview-size(), Qt::KeepAspectRatio, Qt::SmoothTransformation)); DeleteObject(hBmp); }QtWin::fromHBITMAP需要加QT winextras。如果不想依赖winextras可以用GDI手动转换代码长一点但没额外依赖。预览刷新时机加载文件后刷一次参数修改后刷一次。不要频繁刷lmc1_GetPrevBitmap内部有渲染开销每秒超过10次界面会卡。4. 常见问题与排查技巧实录4.1 打标卡初始化失败排查表现象可能原因排查方法返回错误码-1卡未插好或驱动未装设备管理器查看是否有未知设备返回错误码-2卡被其他程序占用关闭EzCad和其他打标软件返回错误码-5SDK版本与卡固件不匹配找厂家确认版本对应关系程序崩溃dll位数不匹配用Dependency Walker检查回调不触发回调函数未加__stdcall检查函数签名4.2 打标位置偏移的校准方法打标位置偏了是高频问题。先排除图形本身的问题——用EzCad打开同一个ezd文件打一遍如果EzCad打出来是正的那就是上位机的问题。上位机导致偏移的原因通常有两个一是lmc1_LoadEzdFile之后调用了lmc1_SetOffset之类的函数改了偏移量二是打标区域的原点设置和EzCad不一致。我的做法是加载文件后不碰任何位置参数完全依赖ezd文件里的设置。如果确实需要偏移用lmc1_SetMarkOffset单位是毫米正负号对应方向。校准的时候拿一张纸打一个十字量实际位置和理论位置的差值填到偏移参数里。反复两次基本就准了。4.3 多线程打标的稳定性经验连续打标几百个文件之后偶尔会出现卡死。排查下来是SDK内部的状态机没有正确复位。解决办法是在每次lmc1_Mark返回后调用一次lmc1_Reset然后再加载下一个文件。这个lmc1_Reset文档里没写是问厂家技术支持要到的。另外打标线程的优先级建议设为QThread::HighPriority避免被界面刷新抢占CPU导致打标时序抖动。但也不要设TimeCritical那个会影响系统其他进程。注意lmc1_Reset会清除当前加载的文件所以顺序必须是打标完成 - Reset - 加载下一个文件 - 打标。4.4 文件拖拽功能的实现与坑Qt5的拖拽很简单主窗口构造函数里setAcceptDrops(true)然后重写dragEnterEvent和dropEvent。但有个坑如果主窗口里放了QListWidget拖拽事件会被列表控件拦截。解决办法是在列表控件上也设置setAcceptDrops(false)让事件冒泡到主窗口。void MainWindow::dragEnterEvent(QDragEnterEvent* event) { if (event-mimeData()-hasUrls()) { event-acceptProposedAction(); } } void MainWindow::dropEvent(QDropEvent* event) { for (const QUrl url : event-mimeData()-urls()) { QString path url.toLocalFile(); if (path.endsWith(.ezd, Qt::CaseInsensitive)) { m_fileList-addItem(path); } } }拖拽进来的文件路径如果是中文toLocalFile在Windows下没问题但传给SDK之前还是要转成短路径或者复制到英文目录。5. 工具选型与扩展方向5.1 为什么不用现成的EzCad二次开发包金橙子其实提供了一个ActiveX控件版的二次开发包可以在C#或VB里直接拖控件。我试过优点是快缺点是丑且不灵活。控件自带的界面元素改不了和产线MES系统对接的时候数据传递很别扭。用SDK纯C接口虽然前期封装麻烦但后期想怎么改就怎么改自由度完全不一样。5.2 和MES系统对接的扩展思路产线集成通常需要把打标结果上传MES。我的做法是在MarkWorker里打标完成后发一个信号主线程收到后调用HTTP接口上传。用QNetworkAccessManager发POST请求JSON格式的数据体。注意网络请求也要异步不要阻塞打标线程。数据内容一般包括产品序列号、打标文件路径、打标时间、操作员、结果状态。MES那边返回成功或失败失败的话本地记录到SQLite数据库等网络恢复了再补传。5.3 后续可以加的功能一个是打标计数和产量统计用QSqlTableModel存SQLite界面上放个QTableView显示。另一个是权限管理操作员只能打标工程师才能改参数用简单的配置文件存密码哈希就行不用上数据库。还有个实用的功能是打标日志回放把每次打标的参数和结果存下来出问题的时候可以查。我一般存成CSV方便用Excel打开分析。6. 实操心得与避坑清单6.1 调试阶段的几个关键技巧调试打标程序的时候不要一上来就接激光器。先用打标卡自带的红光指示功能lmc1_RedLight可以控制红光开关把红光打在纸上确认位置和图形对不对。确认无误再开激光功率调到最低先试。SDK的错误码不要只看返回值有些错误是通过回调的nMsg传的。我在回调里把所有消息都打到日志文件出问题的时候翻日志比猜快得多。6.2 代码组织上的建议LaserController单例不要做成全局变量到处include用instance()方法获取。UI层不要直接调SDK函数全部通过LaserController走。这样以后换卡或者加卡改动范围可控。信号槽连接尽量用新式语法Class::signal编译期就能检查参数匹配。老式SIGNAL/SLOT宏写错了编译不报错运行时才警告浪费时间。6.3 性能相关的注意事项打标文件加载是IO操作如果文件在机械硬盘上加载大文件可能要几百毫秒。我的做法是提前把下一个要打的文件预加载到内存用QFile读成QByteArray打标的时候写临时文件再加载。虽然多一次写但比直接读机械硬盘快。界面刷新频率控制在30fps以内QTimer设33毫秒。打标进度条用QProgressBar不要自己画Qt自带的性能足够。6.4 版本管理与部署SDK的dll和lib一定要纳入版本管理不同版本的SDK接口可能有细微差异。我在项目里建了个doc文件夹把每次拿到的SDK版本号和变更说明记下来。部署的时候用windeployqt自动拷贝Qt依赖然后手动把SDK的dll复制过去。注意VC运行库也要装目标机器没装VS的话把vcredist_x64.exe一起打包。提示如果目标机器是Win7Qt5.12是最后支持Win7的版本5.15开始就不支持了。工控机很多还是Win7选版本的时候注意。6.5 一个容易被忽略的细节打标卡的USB线不要插在USB Hub上直接插主板USB口。Hub供电不稳会导致打标过程中卡掉线现象是打标打一半停了回调报通信错误。这个坑我调了两天才发现换了根线直接插主板就好了。另外工控环境电磁干扰大USB线最好用带屏蔽的长度不要超过1.5米。如果卡和电脑距离远用带信号放大的延长线。7. 完整代码结构速览项目文件结构如下LaserMarker/ ├── LaserMarker.pro ├── main.cpp ├── mainwindow.h/cpp/ui ├── lasercontroller.h/cpp ├── markworker.h/cpp ├── markresult.h ├── thirdparty/ │ ├── include/MarkEzd.h │ ├── lib/MarkEzd.lib │ └── bin/MarkEzd.dll └── config/ └── settings.inimain.cpp里注册元类型int main(int argc, char *argv[]) { QApplication a(argc, argv); qRegisterMetaTypeMarkResult(MarkResult); MainWindow w; w.show(); return a.exec(); }MarkResult结构体定义struct MarkResult { QString filePath; QDateTime startTime; QDateTime endTime; int errorCode; QString errorMsg; }; Q_DECLARE_METATYPE(MarkResult)这套代码我在三个产线项目上用过累计打了上百万个产品稳定性没问题。核心就是线程分离、错误处理、状态复位这三件事做到位。SDK本身不难难的是把异常情况都考虑到产线上可没有重启的机会。
上一篇/下一篇内容由系统自动关联
返回资讯列表 →