Files
QT_wuhan_YT/comm/car.cpp
T
2026-10-14 10:33:58 +08:00

865 lines
36 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#include "car.h"
#include <QApplication>
#include <QDebug>
#include <QHostAddress>
#include <QNetworkDatagram>
#include <QSettings>
#include <QUdpSocket>
#include <QtGlobal>
#include <cstring>
namespace {
// ===================== 线格式 =====================
// 一个 UDP 数据报承载 = 32 位帧头 + 32 位 CAN ID + DLC + 8 字节数据 + 8 位校验和,共 18 字节。
// 多字节字段按主机小端排列,与车载端网关一致;帧头与校验和用于确认数据报内容合法。
#pragma pack(push, 1)
struct CanFrame {
quint32 header; // 固定帧头 0x5AA5FF00(线上字节序 00 FF A5 5A)
quint32 canId; // 29 位扩展帧 ID,直接用于分流
quint8 dlc; // 数据长度码,固定 8
quint8 data[8]; // 8 字节 CAN 原生数据段
quint8 checksum; // canId 4 字节 + dlc + data[8],共 13 字节求和取低 8 位
};
#pragma pack(pop)
const quint32 kFrameHeader = 0x5AA5FF00u;
const int kFrameLen = int(sizeof(CanFrame));
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 sum = 0;
for (int i = 0; i < 4; ++i) sum += quint8((canId >> (8 * i)) & 0xFF);
sum += dlc;
for (int i = 0; i < kDataLen; ++i) sum += data[i];
return sum;
}
// ===================== CAN ID(sheet RC) =====================
// 分类一 遥控器控制指令(上行,7 条)
const quint32 kIdChassisCmd = 0x18208081;
const quint32 kIdArmCmd1 = 0x18218081;
const quint32 kIdArmCmd2 = 0x18228081;
const quint32 kIdDecontamCmd = 0x18238081;
const quint32 kIdDecontamDbg = 0x18248081;
const quint32 kIdDetectorCmd = 0x18258081;
const quint32 kIdEmergency = 0x18268081;
// 分类二/三 VCU 状态反馈(下行,12 条)
const quint32 kIdVehicleFb1 = 0x18308180; // 底盘状态1 BMS
const quint32 kIdVehicleFb2 = 0x18318180; // 底盘状态2 BMS
const quint32 kIdArmFb1 = 0x18328180; // 机械臂状态1 末端位置
const quint32 kIdArmFb2 = 0x18338180; // 机械臂状态2 末端姿态/速度
const quint32 kIdArmFb3 = 0x18348180; // 机械臂状态3 关节角 J1~J4
const quint32 kIdArmFb4 = 0x18358180; // 机械臂状态4 关节角 J5~J6
const quint32 kIdDecontamFb1 = 0x18368180; // 洗消工作状态1
const quint32 kIdDecontamFb2 = 0x18378180; // 洗消工作状态2
const quint32 kIdDetectorFb1 = 0x18388180; // 检测仪结果状态1
const quint32 kIdDetectorFb2 = 0x18398180; // 检测仪结果状态2
const quint32 kIdVehicleFb3 = 0x183A8180; // 底盘状态3 EPS/制动故障位
const quint32 kIdVehicleFb4 = 0x183B8180; // 底盘状态4 挡位/驻车/方向盘/制动电机
// ===================== 位运算 =====================
// 分类一/二/三全部是扩展帧、信号一律 Intel(小端):起始位即最低位序号,
// 位序由低到高、可跨字节(如 bit47 长度 2 就横跨 byte5/byte6)。
quint64 intelBits(const quint8* d, int start, int len)
{
quint64 v = 0;
for (int i = 0; i < len; ++i) {
const int bit = start + i;
if (d[bit >> 3] & (1u << (bit & 7)))
v |= quint64(1) << i;
}
return v;
}
qint64 intelSigned(const quint8* d, int start, int len)
{
const quint64 raw = intelBits(d, start, len);
const quint64 signBit = quint64(1) << (len - 1);
return qint64((raw ^ signBit) - signBit); // 符号扩展
}
void setIntelBits(quint8* d, int start, int len, quint64 v)
{
for (int i = 0; i < len; ++i) {
const int bit = start + i;
const quint8 mask = quint8(1u << (bit & 7));
if (v & (quint64(1) << i)) d[bit >> 3] |= mask;
else d[bit >> 3] &= quint8(~mask);
}
}
// 物理值 = 总线原始整数值 × 精度 + 偏移量
double phys(quint64 raw, double scale, double offset = 0.0)
{
return double(raw) * scale + offset;
}
// ===================== 协议配置文件 =====================
// 与 AppContext 用同一份 config.ini(程序目录)。
QString cfgPath()
{
return QApplication::applicationDirPath() + "/config.ini";
}
// ===================== 故障码 → 中文 =====================
struct CodeText { quint8 code; const char* text; };
const char* lookup(const CodeText* tbl, int n, quint8 code)
{
for (int i = 0; i < n; ++i)
if (tbl[i].code == code) return tbl[i].text;
return nullptr;
}
const CodeText kMcuFaults[] = {
{6, "电机缺相预警"}, {17, "控制器过温警告"}, {18, "电机过温警告"},
{19, "控制器温度传感器警告"}, {20, "电机温度传感器警告"}, {21, "控制器过载警告"},
{22, "电机超速警告"}, {23, "直流母线电压过压警告"}, {24, "直流母线电压欠压警告"},
{35, "电流霍尔传感U相器故障"}, {36, "电流霍尔传感W相器故障"}, {37, "VCU报文丢失故障"},
{38, "VCU循环计数错误"}, {39, "VCU校验和错误"}, {40, "CAN BussOff故障"},
{49, "母线硬件过压故障"}, {50, "相电流硬件过流故障"}, {51, "IGBT短路故障"},
{52, "驱动欠压故障"}, {53, "旋变故障"}, {54, "电机超速故障"},
{55, "低压电源过压故障"}, {56, "低压电源欠压故障"}, {57, "直流母线电压过压故障"},
{58, "直流母线电压欠压故障"}, {59, "控制器过温故障"}, {60, "电机过温故障"},
{61, "电机缺相故障"}, {62, "驻坡失效故障"},
};
const CodeText kAirCompFaults[] = {
{2, "加速过流"}, {3, "减速过流"}, {4, "恒速过流"}, {5, "加速过压"},
{6, "减速过压"}, {7, "恒速过压"}, {8, "缓冲过载"}, {9, "欠压"},
{10, "控制器过载"}, {11, "电机过载"}, {12, "输入缺相"}, {13, "输出缺相"},
{14, "模块过热"}, {15, "外部故障"}, {16, "通信异常"}, {17, "接触器异常"},
{18, "电流检测故障"}, {19, "自学习故障"}, {20, "编码器故障"}, {21, "参数读写异常"},
{22, "变频器硬件故障"}, {23, "电机对地短路故障"}, {31, "PID反馈丢失"},
{40, "快速限流超时故障"}, {42, "速度偏差过大"}, {43, "电机超速"}, {45, "电机过温"},
{90, "编码器线数设定错误"}, {91, "未接编码器"}, {92, "初始位置错误"}, {94, "速度反馈错误"},
};
const CodeText kPlungerFaults[] = {
{1, "短路故障"}, {2, "欠压"}, {3, "过压"}, {4, "控制器过温"},
{5, "控制器温度传感器故障"}, {6, "电机过温"}, {7, "电机温度传感器故障"},
};
const CodeText kCommFaults[] = {
{1, "底盘电机通信异常"}, {2, "底盘BMS通信异常"}, {3, "机械臂通信异常"},
{4, "空压机通信异常"}, {5, "柱塞泵通信异常"}, {6, "叶片泵通信异常"},
{7, "气体检测仪通信异常"}, {8, "遥控器通信异常"},
};
const CodeText kDetectorWarns[] = {
{1, "进气嘴未接"}, {2, "氢气流量不足"}, {4, "仪器故障"}, {8, "浓度过高"},
};
// 底盘状态3(0x183A8180)的 46 个 1 bit 故障标志(bit0~bit45)
const char* const kVsb3Bits[46] = {
"EPS转子角未对中", "EPS转子角度断开", "EPS转子角度SPI故障", "EPSEEPROM故障",
"EPS方向盘中点未对中", "EPS方向盘角度超限", "EPS方向盘小齿信号断开", "EPS方向盘中齿信号断开",
"EPS过电流故障", "EPS电机A相电流异常", "EPS电机C相电流异常", "EPS电机B相电流异常",
"EPSVCU信号丢失", "EPS电机无助力", "EPS预驱配置失败", "EPS半堵转保护",
"EPS温度热保护", "EPS电压管理保护", "EPS转角响应不到位", "EPS转子角度PWM故障",
"EPS电压校验异常", "EPS热敏电阻开路或短路", "EPS热敏电阻温差过大",
"制动器过载故障", "制动器过温或过压故障", "制动器过流故障", "制动器电源欠压故障",
"制动器电源过压故障", "制动器压力不足", "制动器缺相故障", "制动器通讯故障",
"制动器电流采样故障", "制动器驱动故障", "制动器磁编故障", "制动器压力传感器故障",
"制动器踏板位置传感器故障", "制动器机械故障", "制动器堵转故障", "制动器急停按键闭合警告",
"制动器驻车时间过长", "制动器CAN1中断警告", "制动器CAN2中断警告", "制动器点火线断开警告",
"制动器电流异常警告", "制动器温度过高警告", "制动器输入电压过高警告",
};
// 各路 *_FaultLevel 统一为 0 正常 / 1~3 故障等级
const char* const kLevels[4] = {"", "一级故障", "二级故障", "三级故障"};
// 制动电机状态(Brake_*_Mot_fault):0x00~0x07
const char* const kBrakeMotFaults[8] = {
"", "电机开路", "电机短路", "制动执行超时", "解除制动执行超时",
"驱动电路故障", "供电电压过低", "供电电压过高",
};
void addFault(QVector<CanFault>& out, const QString& system, const char* codePrefix,
quint8 code, const char* text)
{
if (!text) return;
CanFault f;
f.system = system;
f.code = QString::fromLatin1(codePrefix) + QString::number(code);
f.desc = QString::fromUtf8(text);
out.append(f);
}
// 未在码表中登记的码也上报,避免“有故障但界面无提示”
void addFaultByCode(QVector<CanFault>& out, const QString& system, const char* codePrefix,
const CodeText* tbl, int n, quint8 code)
{
if (code == 0) return;
const char* t = lookup(tbl, n, code);
if (t) addFault(out, system, codePrefix, code, t);
else {
CanFault f;
f.system = system;
f.code = QString::fromLatin1(codePrefix) + QString::number(code);
f.desc = QString::fromLatin1(codePrefix) + QString::number(code);
out.append(f);
}
}
// 故障等级:>0 才上报,描述形如“单体电压过低二级故障”
void addLevelFault(QVector<CanFault>& out, const QString& system, const char* codePrefix,
const char* name, quint8 level)
{
if (level == 0 || level > 3) return;
CanFault f;
f.system = system;
f.code = QString::fromLatin1(codePrefix) + QString::number(level);
f.desc = QString::fromUtf8(name) + QString::fromUtf8(kLevels[level]);
out.append(f);
}
const char* const kSysChassis = "机器人底盘";
const char* const kSysWash = "洗消系统";
const char* const kSysDetector = "气体检测仪";
} // namespace
// ===================== 构造与生命周期 =====================
CarCommunication::CarCommunication(Telemetry* tel, QObject* parent)
: QObject(parent), m_tel(tel)
{
m_sock = new QUdpSocket(this);
connect(m_sock, &QUdpSocket::readyRead, this, &CarCommunication::onDatagram);
// 端口固定为 kCanRxPort/kCanTxPort 常量,config.ini 只剩自检演示开关要读
QSettings s(cfgPath(), QSettings::IniFormat);
m_selfCheckFault = s.value("net/selfcheck_fault", true).toBool();
// 洗消四参数按存盘值初始化,否则开机帧发的是硬编码初值而非用户设定值。
// INI 读回的是文本("6.5"),须用 toDouble 解析;协议字段都是整数,四舍五入取整。
m_plungerFlow = qRound(qBound(0.0, s.value("param/pump_flow", 20).toDouble(), 24.0));
m_foamRatio = qRound(qBound(6.0, s.value("param/foam_ratio", 6).toDouble(), 12.0));
m_foamPercent = qRound(qBound(0.0, s.value("param/foam_percent", 3).toDouble(), 10.0));
m_airFlow = qRound(qBound(0.0, s.value("param/air_flow", 100).toDouble(), 340.0));
// 通信超时判定:最慢的下行报文周期 1000ms,连续 2s 无任何合法帧 → 通信异常
m_hbTimer = new QTimer(this);
m_hbTimer->setInterval(2000);
connect(m_hbTimer, &QTimer::timeout, this, &CarCommunication::onHeartbeatTimeout);
// 上行周期定时器,周期取自协议「报文周期时间」列
m_fastTimer = new QTimer(this); // 50ms:底盘 / 机械臂 / 急停
m_fastTimer->setInterval(50);
connect(m_fastTimer, &QTimer::timeout, this, &CarCommunication::onSendFast);
m_midTimer = new QTimer(this); // 100ms:洗消指令 / 洗消调试
m_midTimer->setInterval(100);
connect(m_midTimer, &QTimer::timeout, this, &CarCommunication::onSendMid);
m_slowTimer = new QTimer(this); // 1000ms:气体检测仪
m_slowTimer->setInterval(1000);
connect(m_slowTimer, &QTimer::timeout, this, &CarCommunication::onSendSlow);
// 本端自检序列(见 onSelfCheckTick 的 TODO)
m_selfCheckTimer = new QTimer(this);
m_selfCheckTimer->setInterval(450);
connect(m_selfCheckTimer, &QTimer::timeout, this, &CarCommunication::onSelfCheckTick);
if (m_tel) m_tel->js.resize(6);
}
CarCommunication::~CarCommunication()
{
stop();
}
bool CarCommunication::start()
{
if (m_sock->state() == QAbstractSocket::BoundState)
return true;
m_sock->abort();
// AnyIPv4:回环与各本机网卡都能收到;若只允许本机回环,改成 QHostAddress::LocalHost
if (!m_sock->bind(QHostAddress::AnyIPv4, kCanRxPort)) {
// 端口被占用时走这里:不启动定时器,界面显示通信异常
qWarning() << "车端 UDP 端口绑定失败:" << kCanRxPort << m_sock->errorString();
setCommOk(false);
return false;
}
m_hbTimer->start();
m_fastTimer->start();
m_midTimer->start();
m_slowTimer->start();
return true;
}
void CarCommunication::stop()
{
m_hbTimer->stop();
m_fastTimer->stop();
m_midTimer->stop();
m_slowTimer->stop();
m_selfCheckTimer->stop();
m_sock->close();
}
void CarCommunication::setCommOk(bool ok)
{
if (ok == m_lastCommOk) return;
m_lastCommOk = ok;
if (m_tel) m_tel->commOk = ok;
emit commOkChanged(ok);
}
// ===================== 收包与分流 =====================
// UDP 是数据报:单个数据报内含一条或多条完整帧,逐条校验即可,
// 不再需要串口时代那套跨读字节流的残帧缓冲与滑窗重同步。
void CarCommunication::onDatagram()
{
while (m_sock->hasPendingDatagrams()) {
const QByteArray d = m_sock->receiveDatagram().data();
int off = 0;
while (off + kFrameLen <= d.size()) {
CanFrame f;
std::memcpy(&f, d.constData() + off, kFrameLen);
if (f.header == kFrameHeader && f.dlc == kDataLen
&& f.checksum == canChecksum(f.canId, f.dlc, f.data)) {
handleFrame(f.canId, f.data);
off += kFrameLen;
} else {
++off; // 数据报内可能有杂字节,向后找下一个帧头
}
}
}
}
void CarCommunication::handleFrame(quint32 canId, const quint8* d)
{
if (!m_tel) return;
// 收到任意合法帧即刷新链路(CAN 无独立心跳报文)
m_hbTimer->start();
setCommOk(true);
switch (canId) {
// ---------- 0x18308180 底盘状态1(BMS):电池总况与故障等级 ----------
case kIdVehicleFb1: {
m_tel->volt = phys(intelBits(d, 0, 16), 0.1);
m_tel->soc = int(phys(intelBits(d, 16, 16), 0.1));
m_vsf1Faults.clear();
addFaultByCode(m_vsf1Faults, QString::fromUtf8(kSysChassis), "MCU",
kMcuFaults, int(sizeof(kMcuFaults) / sizeof(CodeText)),
quint8(intelBits(d, 32, 8)));
addLevelFault(m_vsf1Faults, QString::fromUtf8(kSysChassis), "MCUL",
"底盘电机", quint8(intelBits(d, 40, 2)));
addLevelFault(m_vsf1Faults, QString::fromUtf8(kSysChassis), "CELLL",
"单体电压过低", quint8(intelBits(d, 45, 2)));
addLevelFault(m_vsf1Faults, QString::fromUtf8(kSysChassis), "CELLHL",
"单体电压过高", quint8(intelBits(d, 47, 2)));
addLevelFault(m_vsf1Faults, QString::fromUtf8(kSysChassis), "VOLTL",
"总电压过低", quint8(intelBits(d, 49, 2)));
addLevelFault(m_vsf1Faults, QString::fromUtf8(kSysChassis), "VOLTHL",
"总电压过高", quint8(intelBits(d, 51, 2)));
addLevelFault(m_vsf1Faults, QString::fromUtf8(kSysChassis), "DLTPL",
"放电低温", quint8(intelBits(d, 53, 2)));
addLevelFault(m_vsf1Faults, QString::fromUtf8(kSysChassis), "DHTPL",
"放电高温", quint8(intelBits(d, 55, 2)));
addLevelFault(m_vsf1Faults, QString::fromUtf8(kSysChassis), "CLTPL",
"充电低温", quint8(intelBits(d, 57, 2)));
addLevelFault(m_vsf1Faults, QString::fromUtf8(kSysChassis), "CHTPL",
"充电高温", quint8(intelBits(d, 59, 2)));
addLevelFault(m_vsf1Faults, QString::fromUtf8(kSysChassis), "TIL",
"温度不均衡", quint8(intelBits(d, 61, 2)));
break;
}
// ---------- 0x18318180 底盘状态2(BMS):手刹 / 车速 / 继电器 / 通信 ----------
case kIdVehicleFb2: {
m_vsf2Faults.clear();
addLevelFault(m_vsf2Faults, QString::fromUtf8(kSysChassis), "VIL",
"压差不均衡", quint8(intelBits(d, 0, 2)));
addLevelFault(m_vsf2Faults, QString::fromUtf8(kSysChassis), "DOCL",
"放电过流", quint8(intelBits(d, 2, 2)));
addLevelFault(m_vsf2Faults, QString::fromUtf8(kSysChassis), "COCL",
"充电过流", quint8(intelBits(d, 4, 2)));
addLevelFault(m_vsf2Faults, QString::fromUtf8(kSysChassis), "INCL",
"内部通讯", quint8(intelBits(d, 6, 2)));
addLevelFault(m_vsf2Faults, QString::fromUtf8(kSysChassis), "EXCL",
"外部通讯", quint8(intelBits(d, 8, 2)));
addLevelFault(m_vsf2Faults, QString::fromUtf8(kSysChassis), "SOCL",
"SOC过低", quint8(intelBits(d, 10, 2)));
addLevelFault(m_vsf2Faults, QString::fromUtf8(kSysChassis), "INSL",
"绝缘", quint8(intelBits(d, 12, 2)));
// 继电器粘连:0=正常 非 0=故障
struct { int bit; const char* name; const char* code; } relays[] = {
{14, "加热继电器粘连", "HEATW"},
{16, "充电继电器粘连", "CHGW"},
{18, "总负继电器粘连", "NEGW"},
{20, "总正继电器粘连", "POSW"},
};
for (const auto& r : relays) {
if (intelBits(d, r.bit, 2) != 0)
addFault(m_vsf2Faults, QString::fromUtf8(kSysChassis), r.code, 1, r.name);
}
m_tel->handbrake = intelBits(d, 22, 2) != 0;
// Vehi_spd:协议表为 8 位、精度 0.01(0~2.55 km/h)。其后 Vehi_RdyStatus(32)、
// Reserve(34)、All_Comm_FaultCode(40) 的起始位与 8 位布局自洽,故按协议取值。
m_tel->speed = phys(intelBits(d, 24, 8), 0.01);
m_tel->rdyStatus = int(intelBits(d, 32, 2));
const quint8 comm = quint8(intelBits(d, 40, 8)); // All_Comm_FaultCode
addFaultByCode(m_vsf2Faults, QString::fromUtf8(kSysChassis), "COMM",
kCommFaults, int(sizeof(kCommFaults) / sizeof(CodeText)), comm);
break;
}
// ---------- 0x183A8180 底盘状态3:EPS / 制动故障位 ----------
case kIdVehicleFb3: {
m_vsf3Faults.clear();
for (int i = 0; i < 46; ++i) {
if (intelBits(d, i, 1) != 0)
addFault(m_vsf3Faults, QString::fromUtf8(kSysChassis), "EPS", quint8(i),
kVsb3Bits[i]);
}
const quint8 eps = quint8(intelBits(d, 48, 8));
if (eps != 0) {
CanFault f;
f.system = QString::fromUtf8(kSysChassis);
f.code = QString("EPSC") + QString::number(eps);
f.desc = QString("EPS故障码") + QString::number(eps);
m_vsf3Faults.append(f);
}
break;
}
// ---------- 0x183B8180 底盘状态4:挡位 / 驻车 / 方向盘 / 制动电机 ----------
case kIdVehicleFb4: {
m_vsf4Faults.clear();
m_tel->gearPos = int(intelBits(d, 0, 2)); // 0=N 1=D 2=R
m_tel->parking = int(intelBits(d, 2, 2)); // 0=未驻车 1=左电机 2=右电机 3=完全驻车
m_tel->awdMode = int(intelBits(d, 4, 2)); // 0=二驱 1=四驱 2=四驱锁定
m_tel->steerAngle = double(intelBits(d, 8, 16));
const quint8 align = quint8(intelBits(d, 24, 8));
if (align == 0x11) addFault(m_vsf4Faults, QString::fromUtf8(kSysChassis), "ALIGN", 1, "方向盘对中失败");
else if (align != 0xEE && align != 0x55)
addFault(m_vsf4Faults, QString::fromUtf8(kSysChassis), "ALIGN", 0, "方向盘未对中");
// 注:协议表把两个信号的“信号值描述”写反了(Brake_Right 描述成左电机、
// Brake_Left 描述成右电机),此处以信号名为准。
const quint8 br = quint8(intelBits(d, 32, 8));
if (br != 0) addFault(m_vsf4Faults, QString::fromUtf8(kSysChassis), "BRKR", br,
br < 8 ? kBrakeMotFaults[br] : "制动电机故障");
const quint8 bl = quint8(intelBits(d, 40, 8));
if (bl != 0) addFault(m_vsf4Faults, QString::fromUtf8(kSysChassis), "BRKL", bl,
bl < 8 ? kBrakeMotFaults[bl] : "制动电机故障");
break;
}
// ---------- 0x18328180 机械臂状态1:末端位置 ----------
case kIdArmFb1: {
m_tel->pos = QString("X:%1 Y:%2 Z:%3")
.arg(intelSigned(d, 0, 16))
.arg(intelSigned(d, 16, 16))
.arg(intelSigned(d, 32, 16));
break;
}
// ---------- 0x18338180 机械臂状态2:末端姿态与末端速度 ----------
case kIdArmFb2: {
m_tel->pose = QString("RX:%1 RY:%2 RZ:%3")
.arg(intelSigned(d, 0, 16))
.arg(intelSigned(d, 16, 16))
.arg(intelSigned(d, 32, 16));
m_tel->vel = double(intelSigned(d, 48, 8));
break;
}
// ---------- 0x18348180 机械臂状态3:关节角 J1~J4 ----------
case kIdArmFb3: {
if (m_tel->js.size() < 6) m_tel->js.resize(6);
for (int i = 0; i < 4; ++i)
m_tel->js[i] = double(intelSigned(d, i * 16, 16));
break;
}
// ---------- 0x18358180 机械臂状态4:关节角 J5~J6 ----------
case kIdArmFb4: {
if (m_tel->js.size() < 6) m_tel->js.resize(6);
m_tel->js[4] = double(intelSigned(d, 0, 16));
m_tel->js[5] = double(intelSigned(d, 16, 16));
break;
}
// ---------- 0x18368180 洗消工作状态1:模式 / 泵 / 球阀 / 液位 / 流量 ----------
case kIdDecontamFb1: {
const quint8 mode = quint8(intelBits(d, 0, 2));
m_tel->washMode = (mode == 3) ? QString::fromUtf8("消毒作业")
: (mode == 2) ? QString::fromUtf8("消除作业")
: QString("--");
m_tel->washStart = intelBits(d, 2, 1) != 0;
m_tel->plunger = intelBits(d, 3, 1) != 0;
m_tel->air = intelBits(d, 4, 1) != 0;
m_tel->leaf = intelBits(d, 5, 1) != 0;
m_tel->d1open = intelBits(d, 8, 1) != 0;
m_tel->d1close = !m_tel->d1open;
m_tel->d2open = intelBits(d, 9, 1) != 0;
m_tel->d2close = !m_tel->d2open;
m_washFaults.clear();
if (intelBits(d, 10, 1) != 0) // Sys_fault:无分路码,只报总故障
addFault(m_washFaults, QString::fromUtf8(kSysWash), "SYS", 1, "洗消系统故障");
if (intelBits(d, 6, 1) != 0)
addFault(m_washFaults, QString::fromUtf8(kSysWash), "LVL", 1, "A组分/清水液位低");
if (intelBits(d, 7, 1) != 0)
addFault(m_washFaults, QString::fromUtf8(kSysWash), "LVL", 2, "B组分液位低");
m_tel->levelA = double(intelBits(d, 16, 8));
m_tel->levelFoam = double(intelBits(d, 24, 8));
m_tel->flowA = double(intelBits(d, 32, 8));
m_tel->flowB = double(intelBits(d, 40, 8));
m_tel->gasFlow = double(intelBits(d, 48, 8));
m_tel->foam = double(intelBits(d, 56, 8));
break;
}
// ---------- 0x18378180 洗消工作状态2:压力 / 流量反馈 / 泵故障码 ----------
case kIdDecontamFb2: {
m_tel->press = phys(intelBits(d, 0, 8), 0.1);
m_tel->airPress = phys(intelBits(d, 8, 8), 0.1);
m_tel->flowPlungerFb = double(intelBits(d, 16, 8));
m_tel->flowAirFb = double(intelBits(d, 32, 8));
m_tel->flowVaneFb = double(intelBits(d, 40, 8));
m_pumpFaults.clear();
addFaultByCode(m_pumpFaults, QString::fromUtf8(kSysWash), "AIR",
kAirCompFaults, int(sizeof(kAirCompFaults) / sizeof(CodeText)),
quint8(intelBits(d, 48, 8)));
addFaultByCode(m_pumpFaults, QString::fromUtf8(kSysWash), "PLG",
kPlungerFaults, int(sizeof(kPlungerFaults) / sizeof(CodeText)),
quint8(intelBits(d, 56, 8)));
break;
}
// ---------- 0x18388180 检测仪结果状态1:状态 / 毒物种类与等级 / 告警 ----------
case kIdDetectorFb1: {
const quint8 sta = quint8(intelBits(d, 0, 8));
const quint8 warn = quint8(intelBits(d, 24, 8));
// 2=计算目标基线 3=等待稳定 4=检测 5=等待点火 6=确定初始基线
m_tel->gasHost = (warn & 0x04) ? QString("fault")
: (sta == 4) ? QString("detecting")
: QString("standby");
// Toxic_types 是位组合:1=P类 2=S类 4=HNO类 8=CH类 16=AS类
const quint8 types = quint8(intelBits(d, 8, 8));
static const struct { quint8 bit; const char* name; } kTypes[] = {
{1, "P类"}, {2, "S类"}, {4, "HNO类"}, {8, "CH类"}, {16, "AS类"},
};
QStringList names;
for (const auto& t : kTypes)
if (types & t.bit) names << QString::fromUtf8(t.name);
m_tel->gasType = names.isEmpty() ? QString("--") : names.join("/");
const quint8 lvl = quint8(intelBits(d, 16, 8));
m_tel->gasLevel = (lvl >= 1 && lvl <= 5) ? QString("%1级").arg(lvl) : QString("--");
m_detectorFaults.clear();
for (const CodeText& w : kDetectorWarns)
if (warn & w.code)
m_detectorFaults.append(CanFault{QString::fromUtf8(kSysDetector),
QString::fromLatin1("W") + QString::number(w.code),
QString::fromUtf8(w.text)});
break;
}
// ---------- 0x18398180 检测仪结果状态2:40 位浓度(ppb) ----------
case kIdDetectorFb2: {
m_tel->gasConc = double(intelBits(d, 0, 40));
break;
}
default:
return; // 未知 canId 不触发刷新(分类四本次未实现)
}
cacheFaults();
emit telemetryUpdated();
}
// 把各帧的故障合并进 Telemetry,并把“新出现”的故障写入故障历史
void CarCommunication::cacheFaults()
{
if (!m_tel) return;
const QVector<QVector<CanFault>*> groups = {
&m_vsf1Faults, &m_vsf2Faults, &m_vsf3Faults, &m_vsf4Faults,
};
QStringList chassis;
for (const QVector<CanFault>* g : groups)
for (const CanFault& f : *g) chassis << f.desc;
m_tel->chassisFaults = chassis;
QStringList wash;
for (const CanFault& f : m_washFaults) wash << f.desc;
for (const CanFault& f : m_detectorFaults) wash << f.desc;
m_tel->washFaults = wash;
QStringList pump;
for (const CanFault& f : m_pumpFaults) pump << f.desc;
m_tel->pumpFaults = pump;
// 周期报文会反复报到同一故障,只在首次出现时写故障历史
const QVector<QVector<CanFault>*> all = {
&m_vsf1Faults, &m_vsf2Faults, &m_vsf3Faults, &m_vsf4Faults,
&m_washFaults, &m_pumpFaults, &m_detectorFaults,
};
for (const QVector<CanFault>* g : all) {
for (const CanFault& f : *g) {
const QString key = f.system + "|" + f.code;
if (m_reportedFaults.contains(key)) continue;
m_reportedFaults.insert(key);
emit faultAppended(f.system, f.code, f.desc);
}
}
}
// ===================== 发包 =====================
void CarCommunication::sendFrame(quint32 canId, const quint8* data)
{
if (m_sock->state() != QAbstractSocket::BoundState) return;
CanFrame f;
f.header = kFrameHeader;
f.canId = canId;
f.dlc = kDataLen;
std::memcpy(f.data, data, kDataLen);
f.checksum = canChecksum(canId, f.dlc, f.data);
m_sock->writeDatagram(reinterpret_cast<const char*>(&f), kFrameLen,
QHostAddress(kCanTxHost), kCanTxPort);
}
// 50ms 组:Chassis_ControlCmd / Arm_ControlCmd1 / Arm_ControlCmd2 / Emergency_Cmd
void CarCommunication::onSendFast()
{
{
quint8 d[8] = {0};
setIntelBits(d, 0, 1, 1); // Chassis_modeCmd 有效
setIntelBits(d, 1, 2, 0); // MCU_modeCmd 底盘预留
setIntelBits(d, 3, 1, 1); // MCU_enable 有效
setIntelBits(d, 4, 4, 0); // Reserve 置 0
setIntelBits(d, 8, 16, m_gear);
setIntelBits(d, 24, 16, m_targetSpd);
setIntelBits(d, 40, 16, m_turning);
sendFrame(kIdChassisCmd, d);
}
{
quint8 d[8] = {0};
setIntelBits(d, 0, 1, m_armMode ? 1 : 0); // Arm_modeCmd
setIntelBits(d, 1, 1, m_armMode ? 1 : 0); // Arm_enable
setIntelBits(d, 2, 6, 0); // Reserve
setIntelBits(d, 8, 16, m_updown);
setIntelBits(d, 24, 16, m_leftright);
setIntelBits(d, 40, 16, m_frontrear);
sendFrame(kIdArmCmd1, d);
}
{
quint8 d[8] = {0};
setIntelBits(d, 0, 16, m_roll);
setIntelBits(d, 16, 16, m_pitch);
setIntelBits(d, 32, 16, m_yaw);
setIntelBits(d, 48, 8, m_homingPause);
sendFrame(kIdArmCmd2, d);
}
{
quint8 d[8] = {0};
setIntelBits(d, 0, 8, m_emergStop);
sendFrame(kIdEmergency, d);
}
}
// 100ms 组:Decontam_ControlCmd / Decontam_DebugCmd
void CarCommunication::onSendMid()
{
{
quint8 d[8] = {0};
setIntelBits(d, 0, 2, m_decontamMode);
setIntelBits(d, 2, 1, m_decontamStart ? 1 : 0);
setIntelBits(d, 3, 5, 0); // Reserve 置 0
// 发泡倍数 / 泡沫比例 / 空压机流量 / 柱塞泵流量:均由页面保存时经 sendSetParam
// 写入缓存(构造时先按 config.ini 初始化),这里直接读缓存组帧,不再每帧读文件。
// 取值范围按协议「信号值描述」列收口:发泡倍数 6-12 倍、泡沫比例 0-10%、
// 空压机流量 0-340 L/min;界面 ParamControl 的可调范围与之一致。
setIntelBits(d, 8, 8, quint64(qBound(6, m_foamRatio, 12)));
setIntelBits(d, 16, 8, quint64(qBound(0, m_foamPercent, 10)));
setIntelBits(d, 24, 8, quint64(qBound(0, m_airFlow, 340)));
setIntelBits(d, 32, 8, quint64(qBound(0, m_plungerFlow, 24)));
sendFrame(kIdDecontamCmd, d);
}
{
quint8 d[8] = {0};
setIntelBits(d, 0, 1, m_debugMode ? 1 : 0);
setIntelBits(d, 1, 1, m_plungerOn ? 1 : 0);
setIntelBits(d, 2, 1, m_airOn ? 1 : 0);
setIntelBits(d, 3, 1, m_vaneOn ? 1 : 0);
setIntelBits(d, 4, 1, m_d1Open ? 1 : 0);
setIntelBits(d, 5, 1, m_d2Open ? 1 : 0);
setIntelBits(d, 6, 2, 0); // Debug_Reserve1
setIntelBits(d, 8, 16, quint64(qBound(0, int(m_airRpm), 3000)));
setIntelBits(d, 24, 16, quint64(qBound(0, int(m_plungerRpm), 1450)));
setIntelBits(d, 40, 16, quint64(qBound(0, int(m_vaneRpm), 2000)));
// 界面每个泵只有一组“启动/停止”,上电与启动同源
setIntelBits(d, 56, 1, m_airOn ? 1 : 0);
setIntelBits(d, 57, 1, m_plungerOn ? 1 : 0);
setIntelBits(d, 58, 1, m_vaneOn ? 1 : 0);
sendFrame(kIdDecontamDbg, d);
}
}
// 1000ms 组:Detector_ControlCmd
void CarCommunication::onSendSlow()
{
quint8 d[8] = {0};
setIntelBits(d, 0, 8, m_detectorCmd);
sendFrame(kIdDetectorCmd, d);
}
// ===================== 上行命令 → 控制状态 =====================
void CarCommunication::sendChassisMove(const QString& dir, double speed, const QString& turn)
{
// 档位推杆量:后 0 / 中 2048 / 前 4096
if (dir == "fwd") m_gear = 4096;
else if (dir == "back") m_gear = 0;
else m_gear = kRcAxisMid;
// 转向推杆量:左 0 / 中 2048 / 右 4096
if (turn == "left") m_turning = 0;
else if (turn == "right") m_turning = 4096;
else m_turning = kRcAxisMid;
// Vehi_TargetSpd 精度 0.01 km/h,量程 0~500(即 0~5.00 km/h)
m_targetSpd = quint16(qBound(0, int(qRound(speed * 100.0)), 500));
}
void CarCommunication::sendArmMove(const QString& axis, int dir)
{
// 轴位推杆量:dir +1 / -1 / 0 → 4096 / 0 / 中位
const quint16 v = (dir > 0) ? 4096 : (dir < 0) ? 0 : kRcAxisMid;
if (axis == "z") m_updown = v;
else if (axis == "x") m_leftright = v;
else if (axis == "y") m_frontrear = v;
else if (axis == "roll") m_roll = v;
else if (axis == "pitch") m_pitch = v;
else if (axis == "yaw") m_yaw = v;
}
void CarCommunication::sendArmReturn()
{
m_homingPause = 0xAA; // 系统回位
}
void CarCommunication::sendArmPause()
{
m_homingPause = 0x55; // 暂停回位
}
void CarCommunication::sendGasStart()
{
m_detectorCmd = 0xAA; // 启动检测
}
void CarCommunication::sendGasStop()
{
m_detectorCmd = 0x55; // 停止检测
}
void CarCommunication::sendWashStart(const QString& mode)
{
m_decontamMode = (mode == "disinfect") ? 3 : 2; // 3=手动消毒 2=手动消除
m_decontamStart = true;
}
void CarCommunication::sendWashStop()
{
m_decontamStart = false;
}
void CarCommunication::sendBallValve(int valve, bool open)
{
if (valve == 1) m_d1Open = open;
else m_d2Open = open;
m_debugMode = true; // 球阀属于调试指令
}
void CarCommunication::sendPumpPower(const QString& pump, bool on)
{
if (pump == "plunger") m_plungerOn = on;
else if (pump == "air") m_airOn = on;
else if (pump == "leaf") m_vaneOn = on; // 叶片泵 → VanePump
m_debugMode = true;
}
void CarCommunication::sendPumpRpm(const QString& pump, int rpm)
{
if (pump == "plunger") m_plungerRpm = quint16(qBound(0, rpm, 1450));
else if (pump == "air") m_airRpm = quint16(qBound(0, rpm, 3000));
else if (pump == "leaf") m_vaneRpm = quint16(qBound(0, rpm, 2000));
}
void CarCommunication::sendSetParam(const QString& key, double value)
{
// 参数缓存与 config.ini 同源:页面保存时先落盘再调这里,周期帧只读缓存。
if (key == "param/speed")
m_targetSpd = quint16(qBound(0, int(qRound(value * 100.0)), 500));
else if (key == "param/pump_flow")
// 字段是 0~24 的整数,界面刻度是 0.1:必须四舍五入而不是截断,
// 否则 0.1 累加出的尾差(面板 5.0 实际存 4.9999999999999982)会下发成 4
m_plungerFlow = qRound(qBound(0.0, value, 24.0));
else if (key == "param/foam_ratio")
m_foamRatio = qRound(qBound(6.0, value, 12.0));
else if (key == "param/foam_percent")
m_foamPercent = qRound(qBound(0.0, value, 10.0));
else if (key == "param/air_flow")
m_airFlow = qRound(qBound(0.0, value, 340.0));
}
void CarCommunication::sendSelfCheckStart()
{
m_selfCheckIndex = 0;
m_selfCheckTimer->start();
}
void CarCommunication::sendEstop()
{
m_emergStop = 0x55;
}
void CarCommunication::sendEstopReset()
{
m_emergStop = 0;
}
// TODO:CAN 矩阵(sheet RC)没有自检报文,故暂由本端跑完 10 项序列,仅为维持
// ④自检 → ⑤作业选择 的流程不中断(onSelfCheckDone 是唯一点亮“作业选择”的地方)。
// 拿到真实自检报文后,改为解析该报文并删除此本地驱动。
void CarCommunication::onSelfCheckTick()
{
bool ok = true;
if (m_selfCheckFault && m_selfCheckIndex == 6) ok = false; // 演示:球阀D1关闭故障
emit selfCheckItem(m_selfCheckIndex, ok);
++m_selfCheckIndex;
if (m_selfCheckIndex >= 10) {
m_selfCheckTimer->stop();
emit selfCheckDone(!m_selfCheckFault);
}
}
void CarCommunication::onHeartbeatTimeout()
{
setCommOk(false);
}