调整通讯协议udp+uart

This commit is contained in:
xu
2026-10-14 10:33:58 +08:00
parent 950e9e6c9e
commit 124e92b95f
7 changed files with 61 additions and 69 deletions
+11 -12
View File
@@ -3,7 +3,7 @@
依据《QT界面详细设计说明书.md》实现的“玉兔工业无线遥控器(YTZT6K8EART2-aL25)”遥控端界面, 依据《QT界面详细设计说明书.md》实现的“玉兔工业无线遥控器(YTZT6K8EART2-aL25)”遥控端界面,
覆盖 ③开机密码(含密码警告/密码管理登录/密码管理/密码重置)④自检 ⑤系统首页面 ⑥作业选择 ⑦机器人底盘 覆盖 ③开机密码(含密码警告/密码管理登录/密码管理/密码重置)④自检 ⑤系统首页面 ⑥作业选择 ⑦机器人底盘
⑧机械臂 ⑨气体检测仪 ⑩消除作业 ⑪消毒作业 ⑫调试维护 ⑬故障查询 共 11 个界面, ⑧机械臂 ⑨气体检测仪 ⑩消除作业 ⑪消毒作业 ⑫调试维护 ⑬故障查询 共 11 个界面,
并配套 **CAN 报文收发**的通信层(19 条报文经 `/dev/ttyS7` 串口收发,遥控器另占 `/dev/ttyS5`)。 并配套 **CAN 报文收发**的通信层(19 条报文经 UDP 收发,遥控器另占串口 `/dev/ttyS7`)。
## 环境与构建 ## 环境与构建
@@ -83,16 +83,15 @@ mingw32-make -j4
0~24 L/min;调试维护页三泵转速上限各按协议(空压机 3000 / 柱塞泵 1450 / 叶片泵 2000 rpm)。 0~24 L/min;调试维护页三泵转速上限各按协议(空压机 3000 / 柱塞泵 1450 / 叶片泵 2000 rpm)。
顶部「通信正常/异常」按收帧判定:连续 **2s** 未收到任何合法帧即判为异常(最慢的下行周期 顶部「通信正常/异常」按收帧判定:连续 **2s** 未收到任何合法帧即判为异常(最慢的下行周期
1000ms)。串口打开失败(如开发机 Windows 上没有 `/dev/ttyS7`)时不启动定时器、不崩溃, 1000ms)。UDP 端口绑定失败(如 9000 已被占用)时不启动定时器、不崩溃,界面显示「通信异常」。
界面显示「通信异常」。
可在 `config.ini`(程序目录)中配置 `car/device`(默认 `/dev/ttyS7`)、`car/baud`(默认 车端 CAN 走 UDP,端口在 `comm/car.cpp` 中以常量固定:本机 **9000** 收、上行命令发往 **127.0.0.1:9001**。
`500000`)、`rc/device`(默认 `/dev/ttyS5`)、`rc/baud`(默认 `115200`)、`net/selfcheck_fault` 遥控器仍走串口:`comm/uart.cpp` 打开 `rc/device`(默认 `/dev/ttyS7`)、`rc/baud`(默认 `115200`)。
(自检故障演示)。 `config.ini`(程序目录)中与通信相关的配置还有 `net/selfcheck_fault`(自检故障演示)。
参数(车速、柱塞泵流量、发泡倍数、泡沫比例、空压机流量)与密码经 `QSettings` 持久化,上电自动读取。 参数(车速、柱塞泵流量、发泡倍数、泡沫比例、空压机流量)与密码经 `QSettings` 持久化,上电自动读取。
`tools/can_sender.py` / `tools/rc_sender.py` 是旧的 UDP 联调工具,**已不适用于当前串口通道**, `tools/can_sender.py` 是车端 CAN 的 UDP 联调工具(给 UI 发反馈时目标端口用 **9000**);
留待需要时另写。 遥控器已回到串口,`tools/rc_sender.py` 发送的 UDP 报文不再被接收。
### 已知缺口 ### 已知缺口
@@ -108,11 +107,11 @@ mingw32-make -j4
(0,1)/(2,3)/(4,5)/(6,7),`value = data[起/8-1]<<8 | data[起/8]`,恰好占满 64 位、 (0,1)/(2,3)/(4,5)/(6,7),`value = data[起/8-1]<<8 | data[起/8]`,恰好占满 64 位、
无溢出。实现分类四时勿按 Intel 连续位序去读。 无溢出。实现分类四时勿按 Intel 连续位序去读。
## 遥控器(串口 /dev/ttyS5,24 字节二进制帧) ## 遥控器(串口 /dev/ttyS7,24 字节二进制帧)
遥控器不走车端 CAN 通道,独立占一路串口:`comm/uart.cpp` 打开 `rc/device` 遥控器不走车端 CAN 通道,独立占一路串口:`comm/uart.cpp` 打开 `rc/device`
(默认 **`/dev/ttyS5`**,115200 / 8 数据位 / 1 停止位 / 无校验),与车端 `/dev/ttyS7` (默认 **`/dev/ttyS7`**,115200 / 8 数据位 / 1 停止位 / 无校验),与改走 UDP 的车端 CAN
(见 `comm/car.cpp`)互不干扰。 (`comm/car.cpp`,收 9000 / 发 9001)互不干扰。
帧长 **24 字节**、1 字节对齐、多字节字段**小端**,发送周期 100ms。帧头 `BYTE0=0x11`、 帧长 **24 字节**、1 字节对齐、多字节字段**小端**,发送周期 100ms。帧头 `BYTE0=0x11`、
帧尾 `BYTE22=0x22`、`BYTE23 = BYTE0~BYTE22 之和的低 8 位`;校验不过即丢弃。 帧尾 `BYTE22=0x22`、`BYTE23 = BYTE0~BYTE22 之和的低 8 位`;校验不过即丢弃。
@@ -157,7 +156,7 @@ mingw32-make -j4
## 目录结构 ## 目录结构
- `app/` 全局常量、配色、应用上下文(AppContext)、QSS 样式、闪烁工具 - `app/` 全局常量、配色、应用上下文(AppContext)、QSS 样式、闪烁工具
- `comm/` 车端 CAN 通信层(`car`,19 条报文经 `/dev/ttyS7`)+ 遥控器串口通道(`uart`,`/dev/ttyS5`) - `comm/` 车端 CAN 通信层(`car`,19 条报文经 UDP 收 9000 / 发 9001)+ 遥控器串口通道(`uart`,`/dev/ttyS7`)
- `widgets/` 公共组件:TitleBar / StatusStrip / LedIndicator / NumPadDialog / - `widgets/` 公共组件:TitleBar / StatusStrip / LedIndicator / NumPadDialog /
ConfirmDialog / AlarmBanner / ParamControl / DataCard / FaultTable ConfirmDialog / AlarmBanner / ParamControl / DataCard / FaultTable
- `pages/` 各界面(继承 BasePage),对应关系如下: - `pages/` 各界面(继承 BasePage),对应关系如下:
+2 -2
View File
@@ -43,10 +43,10 @@ AppContext::AppContext(QObject* parent)
if (!m_settings->contains(keyAirFlow())) m_settings->setValue(keyAirFlow(), 100); if (!m_settings->contains(keyAirFlow())) m_settings->setValue(keyAirFlow(), 100);
if (!m_settings->contains("param/pwd")) m_settings->setValue("param/pwd", "6789"); if (!m_settings->contains("param/pwd")) m_settings->setValue("param/pwd", "6789");
// 通信层(19 条 CAN 报文经 /dev/ttyS7 收发,见 comm/car.h) // 通信层(19 条 CAN 报文经 UDP 收发:收 9000 / 发 9001,见 comm/car.h)
m_comm = new CarCommunication(&telemetry, this); m_comm = new CarCommunication(&telemetry, this);
// 遥控器(串口 /dev/ttyS5,24 字节二进制帧,见 comm/uart.h) // 遥控器(串口 /dev/ttyS7,24 字节二进制帧,见 comm/uart.h)
m_rc = new RcCommunication(this); m_rc = new RcCommunication(this);
// 通信层故障信号 → 写入故障历史 // 通信层故障信号 → 写入故障历史
+39 -43
View File
@@ -1,16 +1,18 @@
#include "car.h" #include "car.h"
#include <QApplication> #include <QApplication>
#include <QDebug> #include <QDebug>
#include <QSerialPort> #include <QHostAddress>
#include <QNetworkDatagram>
#include <QSettings> #include <QSettings>
#include <QUdpSocket>
#include <QtGlobal> #include <QtGlobal>
#include <cstring> #include <cstring>
namespace { namespace {
// ===================== 线格式 ===================== // ===================== 线格式 =====================
// 一帧 = 32 位帧头 + 32 位 CAN ID + DLC + 8 字节数据 + 8 位校验和,共 18 字节。 // 一个 UDP 数据报承载 = 32 位帧头 + 32 位 CAN ID + DLC + 8 字节数据 + 8 位校验和,共 18 字节。
// 多字节字段按主机小端排列,与车载端网关一致;帧头用于在字节流中定位报文起始。 // 多字节字段按主机小端排列,与车载端网关一致;帧头与校验和用于确认数据报内容合法。
#pragma pack(push, 1) #pragma pack(push, 1)
struct CanFrame { struct CanFrame {
quint32 header; // 固定帧头 0x5AA5FF00(线上字节序 00 FF A5 5A) quint32 header; // 固定帧头 0x5AA5FF00(线上字节序 00 FF A5 5A)
@@ -25,6 +27,11 @@ const quint32 kFrameHeader = 0x5AA5FF00u;
const int kFrameLen = int(sizeof(CanFrame)); const int kFrameLen = int(sizeof(CanFrame));
const int kDataLen = 8; const int kDataLen = 8;
// 车端 CAN 的 UDP 端口:本机 9000 收(车端反馈),上行命令发往 9001
const quint16 kCanRxPort = 9000;
const quint16 kCanTxPort = 9001;
const char* kCanTxHost = "127.0.0.1";
quint8 canChecksum(quint32 canId, quint8 dlc, const quint8* data) quint8 canChecksum(quint32 canId, quint8 dlc, const quint8* data)
{ {
quint8 sum = 0; quint8 sum = 0;
@@ -224,13 +231,11 @@ const char* const kSysDetector = "气体检测仪";
CarCommunication::CarCommunication(Telemetry* tel, QObject* parent) CarCommunication::CarCommunication(Telemetry* tel, QObject* parent)
: QObject(parent), m_tel(tel) : QObject(parent), m_tel(tel)
{ {
m_serial = new QSerialPort(this); m_sock = new QUdpSocket(this);
connect(m_serial, &QSerialPort::readyRead, this, &CarCommunication::onReadyRead); connect(m_sock, &QUdpSocket::readyRead, this, &CarCommunication::onDatagram);
// 串口配置与 AppContext 共用 config.ini(默认 /dev/ttyS7,500000 8N1) // 端口固定为 kCanRxPort/kCanTxPort 常量,config.ini 只剩自检演示开关要读
QSettings s(cfgPath(), QSettings::IniFormat); QSettings s(cfgPath(), QSettings::IniFormat);
m_device = s.value("car/device", "/dev/ttyS7").toString();
m_baud = s.value("car/baud", 500000).toInt();
m_selfCheckFault = s.value("net/selfcheck_fault", true).toBool(); m_selfCheckFault = s.value("net/selfcheck_fault", true).toBool();
// 洗消四参数按存盘值初始化,否则开机帧发的是硬编码初值而非用户设定值。 // 洗消四参数按存盘值初始化,否则开机帧发的是硬编码初值而非用户设定值。
@@ -273,23 +278,17 @@ CarCommunication::~CarCommunication()
bool CarCommunication::start() bool CarCommunication::start()
{ {
if (m_serial->isOpen()) if (m_sock->state() == QAbstractSocket::BoundState)
return true; return true;
m_serial->setPortName(m_device); m_sock->abort();
m_serial->setBaudRate(m_baud); // AnyIPv4:回环与各本机网卡都能收到;若只允许本机回环,改成 QHostAddress::LocalHost
m_serial->setDataBits(QSerialPort::Data8); if (!m_sock->bind(QHostAddress::AnyIPv4, kCanRxPort)) {
m_serial->setStopBits(QSerialPort::OneStop); // 端口被占用时走这里:不启动定时器,界面显示通信异常
m_serial->setParity(QSerialPort::NoParity); qWarning() << "车端 UDP 端口绑定失败:" << kCanRxPort << m_sock->errorString();
m_serial->setFlowControl(QSerialPort::NoFlowControl);
if (!m_serial->open(QIODevice::ReadWrite)) {
// 开发机(Windows)没有 /dev/ttyS7 时走这里:不启动定时器,界面显示通信异常
qWarning() << "车端串口打开失败:" << m_device << m_serial->errorString();
setCommOk(false); setCommOk(false);
return false; return false;
} }
m_buf.clear();
m_hbTimer->start(); m_hbTimer->start();
m_fastTimer->start(); m_fastTimer->start();
m_midTimer->start(); m_midTimer->start();
@@ -304,8 +303,7 @@ void CarCommunication::stop()
m_midTimer->stop(); m_midTimer->stop();
m_slowTimer->stop(); m_slowTimer->stop();
m_selfCheckTimer->stop(); m_selfCheckTimer->stop();
if (m_serial->isOpen()) m_serial->close(); m_sock->close();
m_buf.clear();
} }
void CarCommunication::setCommOk(bool ok) void CarCommunication::setCommOk(bool ok)
@@ -317,28 +315,25 @@ void CarCommunication::setCommOk(bool ok)
} }
// ===================== 收包与分流 ===================== // ===================== 收包与分流 =====================
void CarCommunication::onReadyRead() // UDP 是数据报:单个数据报内含一条或多条完整帧,逐条校验即可,
// 不再需要串口时代那套跨读字节流的残帧缓冲与滑窗重同步。
void CarCommunication::onDatagram()
{ {
m_buf.append(m_serial->readAll()); while (m_sock->hasPendingDatagrams()) {
const QByteArray d = m_sock->receiveDatagram().data();
// 串口是字节流:逐字节找帧头,校验不过则右移一字节继续找(容忍粘包、拆包与杂包)。 int off = 0;
// 尾部不足一帧的残段留在 m_buf 里等下次数据。 while (off + kFrameLen <= d.size()) {
int off = 0; CanFrame f;
while (off + kFrameLen <= m_buf.size()) { std::memcpy(&f, d.constData() + off, kFrameLen);
CanFrame f; if (f.header == kFrameHeader && f.dlc == kDataLen
std::memcpy(&f, m_buf.constData() + off, kFrameLen); && f.checksum == canChecksum(f.canId, f.dlc, f.data)) {
if (f.header == kFrameHeader && f.dlc == kDataLen handleFrame(f.canId, f.data);
&& f.checksum == canChecksum(f.canId, f.dlc, f.data)) { off += kFrameLen;
handleFrame(f.canId, f.data); } else {
off += kFrameLen; ++off; // 数据报内可能有杂字节,向后找下一个帧头
} else { }
++off;
} }
} }
if (off > 0) m_buf.remove(0, off);
// 防御:长期收不到帧头时缓冲会一直涨,超过上限只保留尾部一帧长度
if (m_buf.size() > 4096) m_buf.remove(0, m_buf.size() - kFrameLen);
} }
void CarCommunication::handleFrame(quint32 canId, const quint8* d) void CarCommunication::handleFrame(quint32 canId, const quint8* d)
@@ -633,7 +628,7 @@ void CarCommunication::cacheFaults()
// ===================== 发包 ===================== // ===================== 发包 =====================
void CarCommunication::sendFrame(quint32 canId, const quint8* data) void CarCommunication::sendFrame(quint32 canId, const quint8* data)
{ {
if (!m_serial->isOpen()) return; if (m_sock->state() != QAbstractSocket::BoundState) return;
CanFrame f; CanFrame f;
f.header = kFrameHeader; f.header = kFrameHeader;
@@ -641,7 +636,8 @@ void CarCommunication::sendFrame(quint32 canId, const quint8* data)
f.dlc = kDataLen; f.dlc = kDataLen;
std::memcpy(f.data, data, kDataLen); std::memcpy(f.data, data, kDataLen);
f.checksum = canChecksum(canId, f.dlc, f.data); f.checksum = canChecksum(canId, f.dlc, f.data);
m_serial->write(reinterpret_cast<const char*>(&f), kFrameLen); m_sock->writeDatagram(reinterpret_cast<const char*>(&f), kFrameLen,
QHostAddress(kCanTxHost), kCanTxPort);
} }
// 50ms 组:Chassis_ControlCmd / Arm_ControlCmd1 / Arm_ControlCmd2 / Emergency_Cmd // 50ms 组:Chassis_ControlCmd / Arm_ControlCmd1 / Arm_ControlCmd2 / Emergency_Cmd
+5 -8
View File
@@ -9,7 +9,7 @@
#include <QVector> #include <QVector>
#include "global.h" #include "global.h"
class QSerialPort; class QUdpSocket;
// 摇杆量协议范围 0~4096,中位 2048(分类一各轴、Vehi_gear/Vehi_turning 通用) // 摇杆量协议范围 0~4096,中位 2048(分类一各轴、Vehi_gear/Vehi_turning 通用)
static const int kRcAxisMid = 2048; static const int kRcAxisMid = 2048;
@@ -30,7 +30,8 @@ struct CanFault {
// 下行 分类二/三 12 条(车端 → 遥控器):底盘 4 条、机械臂 4 条、洗消 2 条、气体检测仪 2 条, // 下行 分类二/三 12 条(车端 → 遥控器):底盘 4 条、机械臂 4 条、洗消 2 条、气体检测仪 2 条,
// 按 canId 分流转 handleFrame() 写入 Telemetry。 // 按 canId 分流转 handleFrame() 写入 Telemetry。
// 全部为 29 位扩展帧、信号一律 Intel(小端)位序。 // 全部为 29 位扩展帧、信号一律 Intel(小端)位序。
// 串口是字节流,故线上帧统一为 18 字节:帧头 0x5AA5FF00 + canId + DLC + 8 字节数据 + 校验和。 // 传输走 UDP:bind 本机 9000 端口收下行反馈,上行命令发往 127.0.0.1:9001。
// UDP 载荷统一为 18 字节:帧头 0x5AA5FF00 + canId + DLC + 8 字节数据 + 校验和。
// 通信超时判定:连续 2s 未收到任何合法帧则判为通信异常(最慢的下行周期 1000ms)。 // 通信超时判定:连续 2s 未收到任何合法帧则判为通信异常(最慢的下行周期 1000ms)。
class CarCommunication : public QObject class CarCommunication : public QObject
{ {
@@ -41,7 +42,6 @@ public:
bool start(); bool start();
void stop(); void stop();
QString deviceName() const { return m_device; }
// 行车/机械臂模式(面板旋钮或遥控器报文)→ Arm_ControlCmd1 的 Arm_modeCmd / Arm_enable // 行车/机械臂模式(面板旋钮或遥控器报文)→ Arm_ControlCmd1 的 Arm_modeCmd / Arm_enable
void setArmMode(bool arm) { m_armMode = arm; } void setArmMode(bool arm) { m_armMode = arm; }
@@ -73,7 +73,7 @@ signals:
void faultAppended(const QString& system, const QString& code, const QString& desc); void faultAppended(const QString& system, const QString& code, const QString& desc);
private slots: private slots:
void onReadyRead(); void onDatagram();
void onSendFast(); // 50ms 组:底盘 / 机械臂 / 急停 void onSendFast(); // 50ms 组:底盘 / 机械臂 / 急停
void onSendMid(); // 100ms 组:洗消指令 / 洗消调试 void onSendMid(); // 100ms 组:洗消指令 / 洗消调试
void onSendSlow(); // 1000ms 组:气体检测仪 void onSendSlow(); // 1000ms 组:气体检测仪
@@ -88,16 +88,13 @@ private:
void setCommOk(bool ok); void setCommOk(bool ok);
Telemetry* m_tel = nullptr; Telemetry* m_tel = nullptr;
QSerialPort* m_serial = nullptr; QUdpSocket* m_sock = nullptr;
QByteArray m_buf; // 串口字节流缓冲:帧头滑窗重同步用
QTimer* m_hbTimer = nullptr; QTimer* m_hbTimer = nullptr;
QTimer* m_fastTimer = nullptr; QTimer* m_fastTimer = nullptr;
QTimer* m_midTimer = nullptr; QTimer* m_midTimer = nullptr;
QTimer* m_slowTimer = nullptr; QTimer* m_slowTimer = nullptr;
QTimer* m_selfCheckTimer = nullptr; QTimer* m_selfCheckTimer = nullptr;
QString m_device = "/dev/ttyS7";
int m_baud = 500000;
bool m_lastCommOk = true; bool m_lastCommOk = true;
bool m_armMode = false; bool m_armMode = false;
+1 -1
View File
@@ -68,7 +68,7 @@ RcCommunication::RcCommunication(QObject* parent)
// 与 AppContext 共用程序目录的 config.ini:默认构造读不到 rc/* 这些键 // 与 AppContext 共用程序目录的 config.ini:默认构造读不到 rc/* 这些键
// (本工程没有 setOrganizationName/setApplicationName) // (本工程没有 setOrganizationName/setApplicationName)
QSettings s(QApplication::applicationDirPath() + "/config.ini", QSettings::IniFormat); QSettings s(QApplication::applicationDirPath() + "/config.ini", QSettings::IniFormat);
m_device = s.value("rc/device", "/dev/ttyS5").toString(); m_device = s.value("rc/device", "/dev/ttyS7").toString();
m_baud = s.value("rc/baud", 115200).toInt(); m_baud = s.value("rc/baud", 115200).toInt();
// 协议发送周期 100ms,连续 500ms 无合法帧判为链路断开 // 协议发送周期 100ms,连续 500ms 无合法帧判为链路断开
+2 -2
View File
@@ -36,8 +36,8 @@ struct RcFrame {
Q_DECLARE_METATYPE(RcFrame) Q_DECLARE_METATYPE(RcFrame)
// ===================== 遥控器串口接收层 ===================== // ===================== 遥控器串口接收层 =====================
// 独占 rc/device(默认 /dev/ttyS5),115200 / 8 数据位 / 1 停止位 / 无校验, // 独占 rc/device(默认 /dev/ttyS7),115200 / 8 数据位 / 1 停止位 / 无校验。
// 与车端 /dev/ttyS7(见 car.h)互不干扰。 // 车端 CAN 改走 UDP(见 car.h,收 9000 / 发 9001),不再占用串口。
// 只负责收包、校验、解析与链路超时判定,不决定遥控器数据的用途。 // 只负责收包、校验、解析与链路超时判定,不决定遥控器数据的用途。
class RcCommunication : public QObject class RcCommunication : public QObject
{ {
+1 -1
View File
@@ -1,4 +1,4 @@
QT += core gui widgets serialport QT += core gui widgets network serialport
greaterThan(QT_MAJOR_VERSION, 4): QT += widgets greaterThan(QT_MAJOR_VERSION, 4): QT += widgets