ARTICLE DETAIL

资讯详情

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

基于Qt的AGV调度系统:从A*路径规划到分布式协作实践

基于Qt的AGV调度系统:从A*路径规划到分布式协作实践 简介面向毕业设计与课程作业场景这份资源是基于Qt框架开发的分布式智能AGV调度系统完整项目。系统由主控界面、PLC与STM32通信模块、多种AGV控制逻辑共同构成覆盖从界面操作到底层协议解析再到车辆调度的完整链路能够帮助学习者理解跨平台GUI开发、分布式通信机制、多类型自动导引车协同调度等工程问题。压缩包共31个文件以C源文件与头文件为主体并包含Qt界面布局文件、工程管理文件、UML建模文件以及通信协议说明文档和仓库平面布局图便于对照代码理解整体设计。整个资源包约2.63MB已有101人学习。阅读源码可以掌握Qt程序架构、通信协议封装、多AGV任务分配与路径规划等关键实现配套的协议文档和布局图则对课程设计或毕业答辩极具参考价值适合作为AGV调度系统开发的学习范本。1. 用Qt做AGV调度系统先想清楚“分布式”到底解决什么问题一个常见的课程毕设错误是用Qt画一个界面再连上SQLite就把“分布式智能AGV调度系统”交付了。但这类项目真正拉开差距的通常是三个问题当多辆AGV同时申请同一条路径时任务表会不会写坏当调度进程退出或断网后车辆是停下来等死还是被其他节点接管以及你如何向评委证明这些行为不是预先编排好的单机演示。分布式在这里不是多开几个窗口而是把调度器、任务源、车辆控制分别拆成独立进程或独立节点让它们通过消息协作完成同一套业务。本文面向已经会Qt信号槽基本用法、能在QGraphicsView上绘图的读者。下面不会绕开路径规划和一致性这类硬问题每一章都给出可编译的最小实现、关键参数和容易踩的坑。先做路径规划再聊多进程协作最后落到可视化和验收指标顺序就是一条可复现的工程路径。2. 调度核心给AGV写A*路径规划与任务分配的最小实现2.1 栅格地图建模与A*搜索的C实现AGV调度的第一步不是写通信而是先把地图变成机器能算的东西。常见做法是把场景切成二维栅格0表示可通行1表示障碍物AGV当前位置和目标位置都用栅格坐标表达。路径搜索选A*因为它在中小尺寸地图上同时满足最优性和实时性100×100以内的地图在普通PC上单次搜索通常不超过10ms。#include QVector #include QPoint #include queue #include vector #include cmath #include algorithm #include utility using Map QVectorQVectorint; // 0 可通行1 为障碍物 using Path std::vectorQPoint; struct SearchNode { int x, y; double g, f; }; struct NodeCompare { bool operator()(const SearchNode a, const SearchNode b) const { return a.f b.f; } }; double heuristic(int x, int y, int gx, int gy) { return std::sqrt(double((x - gx) * (x - gx) (y - gy) * (y - gy))); } Path findPath(const Map grid, const QPoint start, const QPoint goal) { const int rows grid.size(); const int cols grid[0].size(); std::vectorstd::vectordouble best(rows, std::vectordouble(cols, 1e9)); std::vectorstd::vectorstd::pairint, int parent( rows, std::vectorstd::pairint, int(cols, {-1, -1})); std::priority_queueSearchNode, std::vectorSearchNode, NodeCompare open; open.push({start.x(), start.y(), 0.0, heuristic(start.x(), start.y(), goal.x(), goal.y())}); best[start.x()][start.y()] 0.0; const int dx[4] {1, -1, 0, 0}; const int dy[4] {0, 0, 1, -1}; const double stepCost 1.0; while (!open.empty()) { SearchNode cur open.top(); open.pop(); if (cur.x goal.x() cur.y goal.y()) { break; } for (int k 0; k 4; k) { int nx cur.x dx[k]; int ny cur.y dy[k]; if (nx 0 || nx rows || ny 0 || ny cols) continue; if (grid[nx][ny] 1) continue; double ng cur.g stepCost; if (ng best[nx][ny]) { best[nx][ny] ng; parent[nx][ny] {cur.x, cur.y}; open.push({nx, ny, ng, ng heuristic(nx, ny, goal.x(), goal.y())}); } } } if (best[goal.x()][goal.y()] 1e9) return {}; Path path; int x goal.x(), y goal.y(); while (x ! -1) { path.emplace_back(x, y); std::pairint, int p parent[x][y]; x p.first; y p.second; } std::reverse(path.begin(), path.end()); return path; }代码里的关键点有两个best保存从起点到每个节点的最小代价值只有代价值更小才更新父指针heuristic用欧几里得距离保证在四方向栅格中启发式不大于实际代价A*才能给出最短路径。实际部署时如果地图比较大可以把四方向改成八方向对角线移动代价设为sqrt(2)但要注意检查斜向穿墙。这里有一个容易被忽略的边界start或goal本身落在障碍物上时函数会直接返回空路径。调度系统拿到空路径后不能默认为“原地不动”应当进入任务失败分支而不是继续往下发指令。栅格坐标和像素坐标的换算也建议放在视图层处理算法层不要混入QGraphicsView的场景坐标。2.2 多车路径冲突从独立寻路到时间窗预约表单台车跑A*很简单多台车同时跑就会在交叉口撞上。一个常见的错误是逐台车规划路径然后直接下发给多辆车——这只能用于静态障碍物环境。解决思路是给每个栅格维护一段时间表车辆经过某个栅格时先申请“时间窗”只有时间窗不重叠才允许进入。这种分配方式在AGV调度里叫预约表本质是把路径冲突从物理规避提前到规划阶段处理。struct TimeWindow { int x, y; qint64 startMs, endMs; }; class ReservationTable { public: bool tryReserve(const TimeWindow tw) { for (const TimeWindow o : slots_) { if (o.x tw.x o.y tw.y tw.startMs o.endMs o.startMs tw.endMs) { return false; } } slots_.push_back(tw); return true; } private: std::vectorTimeWindow slots_; };tryReserve的冲突条件用两个区间是否相交来判断只要预约时间[startMs, endMs)和已有时间窗重叠就返回失败。任务分配流程变成规划出整条路径 → 按车辆速度折算每个栅格的进入/离开时间 → 逐个调用tryReserve→ 全部成功才下发给车辆。时间窗的参数直接决定系统能力实际调参时建议按下面表格起步参数典型值取值依据安全缓冲5001000 ms大于单次网络RTT和AGV加减速时间的总和栅格边长与车长相仿AGV头部进入格子和完全离开格子都需要占一格路径刷新间隔200500 ms太低会让时间窗碎片化太高导致路径过时车辆最大速度0.51 m/s课程演示取低速便于观察和录像如果某辆车的整条路径里有任意一个栅格预约失败常见做法不是全部重来而是先做“等待”把整条路径的时间起点往后推500ms再试两次仍然失败才重新规划一条绕行路径。这样能避免多车在狭长通道里的一处冲突导致连锁重规划。2.3 死锁识别与低优先级让行时间窗能解决交叉流冲突但解决不了两辆车对向互占的环路死锁。比如车A占用(2,2)想去(2,3)车B占用(2,3)想去(2,2)双方时间窗都不释放就进入互相等待。死锁检测的完整做法是维护锁请求图并检测环路对于课程设计来说太重我一般用两个更工程化的手段。第一个是给每条任务和每辆车都赋予一个数字优先级低优先级车辆在路口前等待高优先级车辆先行。第二条路径冲突时低优先级车的预约窗口自动顺延而不是和高优先级车反复竞争。第二个手段是给A*的g值加拥挤惩罚某个栅格未来2秒内被其他车辆预约的次数越多这次搜索的代价就越高路径会自然地绕开拥堵区域。这两个方法都不能100%消除死锁所以还要在车辆节点上加一个“卡死超时”如果AGV停留时间超过设定值比如10秒上报调度节点并请求重新规划。智能调度的“智能”体现在这里单条路径靠A*保证最优整体效率靠预约表保证安全死锁靠优先级和超时重规划兜底。把这层做完调度器才算有了可演示的核心逻辑。3. 分布式节点协作用Qt信号槽和Socket拼出多进程调度集群3.1 选型为什么用Qt多进程做分布式而不是先上大型软件框架很多同学一看到“分布式”就想引入Spring Cloud或微服务但在AGV调度这个场景里核心组件是实时状态上报和路径指令下发不是Web请求。更合适做法是用Qt自身的网络组件调度端用QTcpServer接收任务车辆端用QTcpSocket连接调度端监控端用QUdpSocket组播接收状态。一台电脑上用QProcess同时拉起调度进程、车辆进程、监控进程就能形成一套真实的多节点分布式系统。节点角色Qt组件分布式职责调度节点QTcpServer QTimer维护地图、任务表、时间窗预约车辆节点QProcess QTcpSocket上报位置、接收移动指令并模拟执行监控节点QUdpSocket QGraphicsView接收心跳、渲染全局状态这种架构的分布式体现在每个节点是独立进程进程之间不共享内存只通过消息协作任何节点崩溃其他节点可以感知并重新组织。这里要注意“分布式组件的设计理念”不是把全部逻辑塞进一个进程再开几个线程而是让每个角色的边界足够清晰。3.2 用QUdpSocket组播写心跳车辆节点上线后无需人工注册节点发现最省力的方式是UDP组播。所有节点加入同一个组播地址车辆节点周期性广播自己的位置和状态监控节点收到后刷新在线表不需要注册中心。我可以给一个最小的心跳发送端#include QUdpSocket #include QTimer #include QHostAddress class HeartbeatNode : public QObject { Q_OBJECT public: HeartbeatNode(const QHostAddress group, quint16 port, QObject* parent nullptr) : QObject(parent), group_(group), port_(port) { timer_.setInterval(1000); connect(timer_, QTimer::timeout, this, HeartbeatNode::sendBeat); } bool start() { if (!socket_.bind(QHostAddress::AnyIPv4, port_, QUdpSocket::ShareAddress)) return false; socket_.joinMulticastGroup(group_); timer_.start(); return true; } private slots: void sendBeat() { QString data QString(BEAT|%1|%2|%3).arg(id_).arg(x_).arg(y_); socket_.writeDatagram(data.toUtf8(), group_, port_); } public: QString id_ AGV-01; int x_ 0; int y_ 0; private: QUdpSocket socket_; QHostAddress group_; quint16 port_; QTimer timer_; };接收端只需要绑定同一个端口并加入组播然后循环读取datagram按|拆字段更新在线表。关键参数是组播地址固定为239.255.0.100端口固定为45678心跳间隔1000ms连续3个周期没收到就认为节点离线。在同一台机器上多开进程验证时必须给socket_.bind传入QUdpSocket::ShareAddress否则第二个进程会因端口占用而绑定失败。注意车辆的位置字段在真实项目里来自编码器或激光雷达在课程设计里完全可以由随机定时器模拟。模拟越接近真实时序分布式部分的说服力越强。3.3 分布式锁任务表并发写不能只靠QMutex同一进程里多个线程抢任务表可以用QMutex但多个进程同时写同一张任务表时QMutex锁不住其他进程。课程设计里最容易翻车的地方就是这里多个车辆节点同时上报“任务完成”调度端更新数据库或内存表时如果没有跨进程互斥会出现重复派单。锁方案适用范围课程设计推荐度QMutex单进程多线程不适用文件锁多进程但仅限单机低Redis SETNX多机可用的全局锁高但需要额外部署调度节点串行化写入所有写操作集中到调度节点推荐只推荐集中串行化写入车辆节点不直接改任务表而是把“完成上报”发给调度节点调度节点在进程内部用QMutex保护任务表。这种设计避免了分布式锁在架构上又比各节点直接操作数据库安全得多。如果一定要展示分布式锁可以引入Redis最小演示命令是redis-cli SET lock:task:128 ownervehicle-7 NX PX 30000参数含义是NX表示只有当key不存在时才设置成功PX 30000表示锁30秒自动过期owner记录持锁者。拿到锁后完成写操作再DEL释放。长任务要每10秒调用一次续期否则锁过期后另一个节点会拿到同一把锁。3.4 主调度节点故障心跳超时之后自动接管单调度节点的分布式系统不是完整的分布式完整系统至少要能容忍调度节点崩溃。常见做法是让多个节点都具备调度能力其中一个作为主节点其余作为备用节点。备用节点持续接收主节点的心跳一旦超时进入接管流程。简易逻辑如下void MonitorNode::checkLeaderTimeout() { qint64 nowMs QDateTime::currentMSecsSinceEpoch(); if (nowMs - lastLeaderBeatMs_ 3000 !acting_) { acting_ true; socket_.writeDatagram( QString(ELECT|%1).arg(nodeId_).toUtf8(), group_, port_); } }这里用ELECT消息发起选举所有备用节点收到后选择节点编号最小的那个成为新主节点。这个算法不严谨但足够支撑答辩时的故障演示。演员式的操作很简单启动调度节点、两个备用节点、三辆AGV然后手动杀掉调度节点进程观察监控端是否在3秒内把主节点标记切换到备用节点。这样比任何架构图都直观。4. Qt可视化把调度过程和AGV移动画出来还要跑得流畅4.1 用QGraphicsView绘制AGV核心是重写boundingRect和paint调度系统的展示层通常用QGraphicsView因为它的场景管理器天然适合大量Item。给每辆AGV设计一个QGraphicsObject子类位置由业务层定时更新#include QGraphicsObject #include QPainter class AgvItem : public QGraphicsObject { Q_OBJECT public: AgvItem(const QString id, const QPointF pos) : id_(id) { setPos(pos); } QRectF boundingRect() const override { return QRectF(-20, -20, 40, 40); } void paint(QPainter* painter, const QStyleOptionGraphicsItem*, QWidget*) override { painter-setRenderHint(QPainter::Antialiasing); if (status_ running) painter-setBrush(QColor(#42A5F5)); else if (status_ waiting) painter-setBrush(QColor(#FFCA28)); else painter-setBrush(QColor(#BDBDBD)); painter-drawEllipse(-14, -14, 28, 28); painter-drawText(boundingRect(), Qt::AlignCenter, id_); } void updateStatus(const QString s) { status_ s; update(); } private: QString id_; QString status_ idle; };业务层拿到车辆位置后只需调用item-setPos(scenePos)并update()。地图数组坐标乘以一个cellSize换算成场景坐标例如地图坐标(2,3)对应场景点QPointF(2 * cellSize, 3 * cellSize)。路径不是直接画在AGV身上而是单独用QGraphicsPathItem或QPainterPath作为底层图层AGV在路径上移动时只需要改变坐标。4.2 跨线程刷新UI的三个约定QueuedConnection、批量刷新、崩溃排查调度计算如果放在UI线程任务一多界面就会卡。正确做法是把A*和时间窗预约放到QThread工作对象中结果通过信号发给界面。跨线程改变QGraphicsItem最常见的问题是程序直接崩溃崩溃栈往往停在QGraphicsScene::drawItems。避免崩溃的约定有三个第一跨线程connect时必须明确使用Qt::QueuedConnection。例如工作线程里的位置更新信号接到AgvItem::updateStatus一旦写成直连等于是让工作线程去操作UI对象。第二槽函数不要有返回值计算结果通过信号参数传回。Qt的跨线程调用本质是投递事件到接收者线程返回值机制在这条链路上不可靠强行等待返回值会让界面线程卡死。第三批量刷新时用一次开关控制重绘。几百辆AGV同时运动时每辆车每帧都调scene-update()会造成大量重复绘制。批量更新模式下先执行view-setUpdatesEnabled(false)等这批位置全部设置完再重新开启重绘次数从N次降到1次。对于地图上少于50辆车的课程项目这套方案比自定义OpenGL渲染省力得多效果也足够。4.3 用自定义进度环和状态灯表达多车任务进度QProgressBar在设备面板上不够直观我一般会在每辆AGV的Item上方加一个进度环。继承QWidget重写paintEvent用drawArc画出任务完成百分比void ProgressRing::paintEvent(QPaintEvent*) { QPainter p(this); p.setRenderHint(QPainter::Antialiasing); p.setPen(QPen(QColor(#E0E0E0), 6)); p.drawArc(rect().adjusted(2, 2, -2, -2), 0, 360 * 16); p.setPen(QPen(QColor(#42A5F5), 6)); p.drawArc(rect().adjusted(2, 2, -2, -2), 90 * 16, -percent_ * 360 * 16); }drawArc的角度单位是1/16度所以完整圆要写360 * 16起始角90度表示从正上方开始。进度环可以直接嵌入AgvItem的paint在车辆图标的四角再画四个小色点表示等待、运行、故障、充电四种状态。这样评委看监控界面时一辆车的状态一目了然不需要读表格。5. 验收与答辩用压测脚本和指标让评委相信它不是单机演示5.1 用Python脚本做一次100任务的并发压测调度系统的验收不能只靠界面演示需要有一个可重复执行的压测脚本。下面的脚本模拟100个客户端同时向调度节点提交任务验证任务接收接口的并发能力import socket import threading import time TASKS 100 done 0 lock threading.Lock() def submit(i): global done try: s socket.create_connection((127.0.0.1, 9000), timeout3) s.sendall(fTASK|{i}|10,10 49,49.encode()) s.close() with lock: done 1 except OSError: pass start time.time() threads [threading.Thread(targetsubmit, args(i,)) for i in range(TASKS)] for t in threads: t.start() for t in threads: t.join() print(fsubmitted{done}/{TASKS}, cost{time.time()-start:.2f}s)脚本里的9000端口要和调度节点的QTcpServer监听端口一致。每个线程创建独立Socket连接能同时触发调度端多线程接收逻辑。压测时观察调度端日志中每个任务的分配耗时如果出现大量排队说明任务分配链路存在瓶颈如果出现同一栅格时间窗大量冲突则要调大安全缓冲时间或改进绕行策略。5.2 三个评委不会拒绝的量化指标答辩时与其说“系统很稳定”不如直接展示三个数字。第一个是任务完成率压测脚本发出100个任务车辆节点全部回到指定位置后上报完成完成数除以接受数。如果低于95%先检查死锁超时时间和预约表冲突逻辑。第二个是平均周转时间从任务下发到AGV完成的时间这个指标直接反映调度质量周转时间过长说明路径规划或预约参数不合理。第三个是故障恢复时间手动结束主调度节点进程监控端看到从“主节点离线”到“备用节点接管”的时间差课程项目做到5秒以内已经能说明问题。答辩现场最有说服力的演示不是PPT而是准备工作目录里的两个批处理脚本第一个脚本批量启动调度节点、备用节点、三辆AGV和监控端第二个脚本直接结束主调度进程。监控窗口里的车辆状态和任务进度会按预期切换配合压测脚本打印出的任务完成数比口头解释“分布式架构”可靠得多。本文还有配套的精品资源点击获取
返回列表