ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

基于Qt的Klipper上位机开发:实时监控与通信解析实践

基于Qt的Klipper上位机开发:实时监控与通信解析实践 做3D打印上位机这件事我是从“用别人的Web界面觉得不过瘾”开始的。Klipper这个固件生态有个特点几乎所有常见前端Mainsail、Fluidd都是浏览器里的Web应用功能很全但想按自己的习惯定制监控界面、做本地化、甚至离线直接连串口调试Web那套反而束手束脚。于是我用C/Qt写了一个基于Klipper的远程上位机系统把通信解析、打印参数设置、实时状态展示全部纳进一个桌面程序里。这套思路也适用于任何想深入Klipper源码、自己接入通信协议的人或者单纯想在桌面上用一个更顺手的打印控制台。下面是我从零搭建这套系统的完整记录含踩坑。1. 整体设计与思路拆解1.1 Klipper生态与上位机的位置Klipper固件跟传统固件比如Marlin最大的区别是真正的运动规划、温度控制策略都跑在主机端树莓派、PC单片机端只负责输出脉冲、读取传感器。所以“上位机”这个词其实要分三层来理解。第一层是用户界面也就是我写的Qt程序。第二层是Klipper自家配套的API服务——Moonraker它把Klipper内部状态包装成HTTP/WebSocket接口是社区事实上的API网关。第三层是Klipper核心进程klippy真正执行G代码、做运动规划和温度控制的程序。我刚开始犯过一个认知错误以为上位机要直接打串口跟主控板的MCU通信。后来翻Klipper源码才明白如果需要频繁读温度、发G代码正确姿势是走Moonraker。上位机通过HTTP请求查询状态通过WebSocket订阅推送把G代码交给klippy执行。这样既安全又稳定也避免了直接接触底层协议带来的兼容性问题。1.2 为什么最终选择Qt技术栈我试过用Python写控制台脚本也试过改Fluidd的界面。最后回桌面程序还是选了Qt。跨平台是首要理由。Windows上做主力监控、Linux服务器上部署、macOS上调试同一套代码都能跑不用为不同系统单独开发。Qt的网络模块很成熟QNetworkAccessManager负责HTTP请求QWebSocket负责订阅推送QSerialPort在需要串口调试时也能直接顶上。自绘控件这块更是Qt的强项。温度曲线用QChart就能画得挺好看进度条可以用QPainter定制比在Web里折腾CSS和Canvas效率高得多。而且Qt发布成独立exe或AppImage很简单不依赖Node服务装完就能用。1.3 系统架构与模块划分我最终把系统拆成四块UI层主窗口、温度面板、打印面板、参数设置面板。客户端层PrinterClient负责HTTP请求、WebSocket订阅、断线重连。数据层JSON解析、QSettings本地配置、打印任务记录。业务层命令构造、参数下发、打印机状态聚合。UI层绝不直接访问网络所有网络请求都通过PrinterClient封装。这样换打印机、换固件版本只改客户端层界面不用动。这是踩过一次坑总结出来的教训。最开始版本我把QWebSocket直接new在MainWindow里结果界面卡顿、析构崩溃。后来把网络通讯独立成对象丢到子线程跑问题才解决。具体线程模型后面会详细说。2. 核心原理Klipper通信协议与源码解析2.1 Moonraker API状态查询、对象订阅与命令下发先说HTTP接口。查询喷嘴目标温度和实时温度请求路径GET /printer/objects/query?extrudertemperature,target返回的JSON结构{ result: { status: { extruder: { temperature: 210.4, target: 220.0 } } } }这种方式适合定时轮询比如每2秒拉一次温度。如果希望实时刷新更优雅的是WebSocket订阅。先连接ws://主机地址:7125/websocket然后发送JSON-RPC订阅请求{ jsonrpc: 2.0, method: printer.objects.subscribe, params: { objects: { extruder: [temperature, target], heater_bed: [temperature, target], print_stats: null } }, id: 10 }之后Moonraker会持续推送notify_status_update消息里面只带变化的字段。订阅时把关键对象挂上上位机就能以事件驱动方式刷新温度面板不用频繁轮询。G代码命令下发的接口是POST /printer/gcode/script参数为scriptM220 S90。同样可以通过WebSocket发送printer.gcode.script方法。我实际测试下来WebSocket通道延迟更低命令执行结果也更完整所以最终下发命令统一走WebSocket。2.2 从Klipper源码理解参数设置背后发生了什么如果只调用API其实不用看源码。但要做一套真正懂打印的上位机比如要做打印速度百分比调节、流量百分比调节、压力提前补偿就必须明白Klipper里这些参数在哪一层生效。M220是设置速度因子的命令代码在klippy/extras/gcode.py里。它会作用到所有后续运动命令最终影响运动规划的速度。M221设置挤出倍率影响挤出电机流量。SET_PRESSURE_ADVANCE设置压强提前值在klippy/extras/pressure_advance/里实现是提高高速打印质量的进阶参数。在源码里搜索cmd_M220、cmd_M221能很快定位到具体实现。跟着源码走一遍比看文档更有底。搞懂这套逻辑之后上位机的参数设置面板就不是盲填数值而是清楚每个参数会带来什么机械特性。比如M220设到200%如果打印机机械结构刚性不足高速打印时就能明显看到振纹。再比如流量因子设到150%打印表面大概率会过挤出产生堆料。上位机开发人员理解这些才能在界面上加合理的限制和提示。2.3 通信数据解析的通用套路Klipper/Moonraker返回的数据统一是JSON。Qt端我统一用QJsonDocument解析但有个细节很容易忽略WebSocket推送的消息里不同事件类型结构完全不一样。notify_status_update的结构和notify_gcode_response就完全不同。前者params[0]是对象状态字段后者params[0]是G代码响应文本。所以不能一进来就取固定字段。我的做法是先判断method字段再进入不同解析器。void PrinterClient::onTextMessageReceived(const QString message) { QJsonDocument doc QJsonDocument::fromJson(message.toUtf8()); QJsonObject root doc.object(); QString method root.value(method).toString(); if (method notify_status_update) { // 先取params[0]对象再逐字段更新 QJsonArray params root.value(params).toArray(); QJsonObject status params[0].toObject(); updatePrinterStatus(status); } else if (method notify_gcode_response) { // 处理G代码响应/错误 QJsonArray params root.value(params).toArray(); QString response params[0].toString(); emit gcodeResponseReceived(response); } else { // 处理带id的RPC响应 int id root.value(id).toInt(); QJsonObject result root.value(result).toObject(); emit rpcResponseReceived(id, result); } }别把一个方法写到底拆成多个槽函数后面维护逻辑清晰得多。这也是我重构过几次之后的教训一开始在图省事所有解析逻辑堆在一个函数里加新功能时改一处坏三处最后只能推倒重来。3. 基于Qt的上位机实现细节3.1 开发环境准备与模块选择我用的Qt版本是5.15.2 LTSWindows上开发、Linux服务器上部署。这里有个容易踩的坑如果只需要网络和UI功能安装Qt时不需要勾选SerialPort模块但很多从旧项目搬过来的代码会有#include QSerialPort残留一编译就报unknown module(s) in qt: serialport。解决方案有两个重新运行Qt安装器勾选SerialPort模块或者把代码里的残留include和.pro里的QT serialport删掉。我的项目初期调试时接串口直连过MCU所以安装了SerialPort模块。如果你的项目也用不到不用装也没关系。工程结构建议klipper-qt/ ├── KlipperQt.pro ├── main.cpp ├── mainwindow.h/cpp ├── printerclient.h/cpp ├── tempchart.h/cpp ├── printpanel.h/cpp └── settings.h/cpp.pro文件里需要添加QT core gui network websockets charts CONFIG c113.2 网络通信层的线程模型与断线重连最开始版本我把QWebSocket直接new在MainWindow里跑起来后发现温度推送每秒钟可能十几条如果UI层数据量大比如在画几百个点的曲线窗口操作会明显掉帧。后来按标准做法把网络对象放到独立线程里PrinterClient *client new PrinterClient(); QThread *netThread new QThread(); client-moveToThread(netThread); netThread-start();注意moveToThread之后PrinterClient里的QWebSocket的创建和连接必须在子线程内完成。我通常用QMetaObject::invokeMethod(client, connectToHost, Qt::QueuedConnection)来触发避免跨线程直接调用出问题。数据从子线程回到UI靠信号槽自动处理用默认的AutoConnection跨线程时会自动切到QueuedConnectionUI线程照样能安全收到数据包。断线重连是远程监控的刚需。我在PrinterClient里放了个QTimer每5秒检查一次连接状态。如果QWebSocket状态不是ConnectedState就尝试重连。连续重连失败计数到3次时降低频率到30秒一次避免日志刷屏。void PrinterClient::checkConnection() { if (m_webSocket.state() ! QAbstractSocket::ConnectedState) { m_reconnectCount; if (m_reconnectCount 3) { m_checkTimer-setInterval(30000); } m_webSocket.open(QUrl(m_serverUrl)); } else { m_reconnectCount 0; if (m_checkTimer-interval() ! 5000) { m_checkTimer-setInterval(5000); } } }3.3 打印参数设置的完整下发流程这里用最常用的例子用户在面板拖动速度因子到85%。整套流程分四步。UI层拿到滑条值通过emit setSpeedFactor(85)信号发给PrinterClient。PrinterClient内部构造M220 S85命令。然后通过WebSocket发送JSON-RPC{ jsonrpc: 2.0, method: printer.gcode.script, params: { script: M220 S85 }, id: 21 }等Klipper返回result后把当前速度因子保存到本地配置下次启动自动恢复。安全校验不能少。速度因子我限制在10~300之间温度限制在0~350执行前在UI层先做一次范围判断在PrinterClient里再做一次双重保险。这里有个很有意思的问题如果打印过程中直接M220修改速度因子Klipper会立刻调整所有后续运动的速度。如果在打印暂停状态下修改则不会影响当次打印只影响后续。上位机状态机设计时要区分清楚不能让用户误以为暂停时调了参数就能生效。我加了一行提示文案“打印中生效暂停时不生效”用户体验好很多。4. 实时监控与打印管理体验4.1 实时温度曲线与打印进度显示温度曲线用Qt Charts的QChart来实现。这里有个关键优化点不要每条推送都append一个新点否则画几千个点之后明显卡顿。我的做法是维护一个固定容量的QLineSeries比如3600个点只保留最近一小时数据。新数据到来时用replace整个序列这样滑动窗口看起来就像不断移动的曲线实际上底层数据量固定CPU占用很低。// 假设 m_series 是 QLineSeries* void TempChart::updateTemperature(double temp, double target) { QDateTime now QDateTime::currentDateTime(); qreal x now.toMSecsSinceEpoch(); m_tempSeries-replace(m_tempSeries-count(), x, temp); m_targetSeries-replace(m_targetSeries-count(), x, target); // 只保留最近3600个点 if (m_tempSeries-count() 3600) { m_tempSeries-remove(0); m_targetSeries-remove(0); } }进度计算可以从Moonraker的virtual_sdcard.progress直接拿到0~1的浮点数也可以从print_stats.state判断是否正在打印。我给进度条做了自定义绘制把百分比、已打印时长、剩余打印时间按已打印时长和进度估算都画上去比自带QProgressBar好看也更有用。4.2 打印任务与文件管理上传、列表、启动上位机还要管打印文件。我实现了三个基础功能。上传G代码文件调用POST /server/files/uploadmultipart/form-data格式Qt里用QHttpMultiPart构造。获取文件列表调用GET /server/files/list?rootgcodes返回JSON数组。启动打印调用POST /printer/print/start参数filenamexxx.gcode。QNetworkRequest request(QUrl(http:// host /server/files/upload)); QHttpMultiPart *multiPart new QHttpMultiPart(QHttpMultiPart::FormDataType); QHttpPart filePart; filePart.setHeader(QNetworkRequest::ContentDispositionHeader, QStringLiteral(form-data; name\file\; filename\%1\).arg(fileName)); filePart.setBody(gcodeContent); multiPart-append(filePart); QNetworkReply *reply m_http.post(request, multiPart);暂停和取消打印分别调用POST /printer/print/pause和POST /printer/print/cancel这里不展开。文件管理这块有个体验细节打印文件名经常带中文Moonraker对URL编码有要求Qt里要用QUrl::toPercentEncoding(filename)转码后再拼URL否则会偶发404。4.3 多打印机配置与本地化既然用了Qt顺手把多打印机配置和界面国际化也做了。QSettings里保存打印机列表每个item包含主机地址、端口、打印机名称。切换打印机时PrinterClient断开旧连接、连到新主机UI所有面板重置为加载状态。这个过程用信号槽串联避免在槽函数里直接操作其他对象造成耦合。国际化这块比较微妙。网上很多Qt中文资料都在强调tr()但实际做的时候要注意动态字符串拼接不要写tr(xxx variable)应该用QStringLiteral或.arg()否则翻译文件匹配不上。statusLabel-setText(tr(已连接%1).arg(printerName));而不是statusLabel-setText(tr(已连接 printerName));4.4 热词里的“qt绘图效率”与曲线刷新最近搜资料还看到很多人问 “qt绘图效率”、“qchart实现图片缩放” 这类问题。确实Qt Charts在大量数据点下性能会下降除了上面说的定长序列还有一个优化策略降低刷新频率。温度推送过来先缓存UI里搞个定时器5秒刷新一次曲线。这样温度数据本身是实时的画面更新是平滑的CPU占用明显下降。如果追求极致流畅可以把QChartView的渲染模式设置为QChart::GLWidget模式直接走OpenGL加速。如果只是简单画线用QPainter自己画也不难固定横轴是时间戳纵轴是温度。QPainter画几千个点的折线非常快比QLineSeries在普通模式下的效率还高。我最终实现时选择了QChart因为要兼顾图例、坐标轴缩放和tooltip省去自己造轮子的工作量。4.5 Qt与嵌入式联动从“上位机”到“远程控制”还有一个场景是远程控制。远程上位机的意义不只是本地监控还包括在局域网甚至互联网里控制打印机。我在这套系统的网络层里预留了远程控制接口Qt上位机先把命令发到本地转发服务再由转发服务转发到Moonraker。这样即使人不在打印机旁边也能通过转发的WebSocket查看状态、修改参数、启停打印。做好远程控制就要考虑权限和并发。我加了一个简单的token校验机制上位机连接时带上token转发服务验证通过才允许订阅和命令。这套机制不复杂但能解决“家里打印机被局域网内其他人乱操作”的尴尬。如果你只是在纯局域网内跑不开放公网端口风险可控不接远程转发也一样用。5. 常见问题与排查技巧实录5.1 通信连接失败的排查清单我遇到过几类典型问题整理成表方便对照现象可能原因处理方式WebSocket握手失败Moonraker端口不对检查moonraker.conf配置默认端口7125HTTP请求超时网络不通或防火墙拦截先ping通主机再telnet 7125端口验证连接正常但订阅无推送订阅请求参数写错了对象名用Moonraker官方控制台手动发一次订阅验证提交G代码没反应klippy处于打印暂停或错误状态调用接口先查询打印状态再决定是否允许提交中文文件名上传失败URL未做百分号编码用QUrl::toPercentEncoding转码后再请求这个排查过程里最有价值的一条经验是遇到WebSocket连接问题先别改代码先用系统自带的工具验证一遍服务是否正常。比如直接curl一下HTTP接口用websocat测一下Ws连接确认问题在服务端还是客户端能省下很多冤枉时间。5.2 Qt编译与打包踩坑unknown module(s) in qt: serialport这种情况大部分是安装Qt时没勾选对应模块。解决方法很简单重新运行Qt安装器勾选需要的模块就行。发布exe时用windeployqt经常漏掉QtCharts的库。需要手动把Qt5Charts.dll、Qt5Network.dll复制到发布目录或者给windeployqt传参--no-system-d3d-compiler之类的选项后面自己仔细核对一遍。跨平台部署时注意Linux上用linuxdeployqt处理或者直接使用系统包管理器安装Qt依赖。打包前ldd检查一下可执行文件依赖缺哪个库就补哪个。我遇到过Linux下跑起来后没有libQt5WebSockets.so.5的情况就是因为漏装了WebSocket模块。5.3 状态显示刷新的细节问题有个细节让我调试了很久温度数值会偶尔跳回0。后来发现不是打印机温度真的降了而是订阅消息解析时把extruder和heater_bed的对象搞混了请求字段写错导致温度清零。经验是做状态聚合时每个字段更新前先判断JSON里是否有这个key没有就跳过不要默认填0。填充走势图时同理缺数据就留空不补0曲线才真实反映打印过程。void updateExtruderTemperature(const QJsonObject status) { QJsonObject extruderObj status.value(extruder).toObject(); if (extruderObj.contains(temperature)) { double temp extruderObj.value(temperature).toDouble(0.0); emit extruderTempChanged(temp); } // 如果没有temperature字段说明未变化或暂不可用不发出信号 }还有一点Qt Charts默认在主线程更新。如果温度推送太频繁建议定时5秒合并一次数据再刷新曲线CPU占用能降下来一大截。5.4 订阅数据量与性能平衡Moonraker的状态推送默认是差量推送但如果你订阅了太多对象每条推送的频率也会跟着涨。我调试时曾一次性订阅了所有Klipper对象温度、打印进度、风扇转速、电机位置全挂上结果WebSocket每秒钟收到几十条消息UI线程直接满负荷。后来做了个取舍实时要求高的对象温度、打印进度走WebSocket订阅低频对象比如风扇转速、传感器状态走HTTP定时轮询每5秒拉一次。这样WebSocket流量下降一大半UI也流畅很多。这个分层设计是这套系统性能优化的核心配置之一。5.5 Qt与Klipper版本兼容性Klipper和Moonraker更新很快API偶尔会变。我在项目中维护了一个小的API适配层把状态查询、命令下发、文件管理这几个核心请求封装成函数内部拿JSON解析结果时代码里写上备注“Moonraker v0.8”如果升级版本后再遇到字段缺失只需要返回这一层去修改。Qt版本如果用的是5.15 LTSQtCharts在Windows下表现没问题Linux下如果遇到缺库用apt安装libqt5charts5-dev就能解决。Qt 6系列里Qt Charts是独立在/opt/Qt里的安装路径别选错。总之不要盲目追新版本稳定最重要。6. 个人体会与扩展方向整套系统做到能稳定跑起来之后最大的感受是Klipper生态的价值不只是打印效果好它的API设计把“监控”和“控制”拆得很干净这让桌面上位机开发难度比想象中低不少。我踩过的坑大多不在Klipper而在Qt的线程模型和模块配置上。如果你也想做一套自己的上位机建议先从WebSocket订阅状态开始一步步把打印、暂停、参数调整加进去。等跑通一遍你对Klipper源码的阅读能力、对参数底层逻辑的理解都会比只看教程快得多。后续拓展方向也有不少可以加入摄像头流Moonraker直接支持mjpeg-streamer接入可以写独立的“打印预测”模块基于历史耗材和层高估算剩余时间甚至可以接语音控制。三思而后行先把通信层和参数层吃透剩下的功能都是水到渠成。最后再分享一个小技巧调试阶段把WebSocket收到的原始消息打印到日志文件里排查问题会非常舒服。
返回列表