Files
2026-10-16 09:06:03 +08:00

318 lines
11 KiB
C++
Raw Permalink 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 "appcontext.h"
#include "car.h"
#include <QApplication>
#include <QDateTime>
namespace {
// 遥控器摇杆满量程 0~4096、中位 2048;取 ±10% 死区,即 1638~2458 视为中立
const int kAxisMid = 2048;
const int kAxisDead = 410;
int axisDir(int v)
{
if (v > kAxisMid + kAxisDead) return +1;
if (v < kAxisMid - kAxisDead) return -1;
return 0;
}
// 六个机械臂轴,顺序与 sendArmMove 的轴名一致(见 pages/arm.cpp)
const char* const kArmAxes[6] = {"z", "x", "y", "roll", "pitch", "yaw"};
} // namespace
AppContext* AppContext::instance()
{
static AppContext* s = nullptr;
if (!s) s = new AppContext();
return s;
}
AppContext::AppContext(QObject* parent)
: QObject(parent)
{
// 参数与密码持久化:QSettings 写入程序目录 config.ini
m_settings = new QSettings(QApplication::applicationDirPath() + "/config.ini",
QSettings::IniFormat, this);
// 首次运行写入默认值
if (!m_settings->contains(keySpeed())) m_settings->setValue(keySpeed(), 4.5);
if (!m_settings->contains(keyPumpFlow())) m_settings->setValue(keyPumpFlow(), 20);
if (!m_settings->contains(keyFoamRatio())) m_settings->setValue(keyFoamRatio(), 6);
if (!m_settings->contains(keyFoamPercent()))m_settings->setValue(keyFoamPercent(), 3);
if (!m_settings->contains(keyAirFlow())) m_settings->setValue(keyAirFlow(), 100);
if (!m_settings->contains("param/pwd")) m_settings->setValue("param/pwd", "6789");
// 通信层(19 条 CAN 报文经 UDP 收发:收 9000 / 发 9001,见 comm/car.h)
m_comm = new CarCommunication(&telemetry, this);
// 遥控器(串口 /dev/ttyS7,24 字节二进制帧,见 comm/uart.h)
m_rc = new RcCommunication(this);
// 通信层故障信号 → 写入故障历史
connect(m_comm, &CarCommunication::faultAppended,
this, &AppContext::appendFault);
// 遥控器报文 → 模式 / 急停 / 动作 / 显示
connect(m_rc, &RcCommunication::frameReceived, this, &AppContext::onRcFrame);
connect(m_rc, &RcCommunication::linkChanged, this, &AppContext::onRcLinkChanged);
#if 0
// 行车/机械臂模式改由底盘报文的 mode 字段决定(遥控器在线时让位给遥控器报文)。
// 停用原因:协议 sheet RC 的 12 条下行报文里没有该 mode 字段,telemetry.driveMode
// 无处可填,此连接只会一直把模式按 0(行车)写。待协议补充该信号后恢复。
connect(m_comm, &CarCommunication::telemetryUpdated, this, [this]() {
if (m_rc->linkOk()) return;
setMode(telemetry.driveMode == 0 ? Mode::Drive : Mode::Arm);
});
#endif
seedFaultHistory();
}
void AppContext::seedFaultHistory()
{
// 演示用历史故障汇总(协议示例数据 + 补充)
struct Seed { const char* t; const char* sys; const char* code; const char* d; };
const Seed seeds[] = {
{"2026-05-16 10:08", "机器人底盘", "C01", "底盘电量低"},
{"2026-05-16 09:58", "机器人底盘", "C02", "底盘电机温度高"},
{"2026-05-16 09:41", "机器人底盘", "C03", "总电压低"},
{"2026-05-15 09:08", "机械臂", "A01", "机械臂通信故障"},
{"2026-05-15 08:55", "机械臂", "A02", "机械臂自检故障"},
{"2026-05-15 08:30", "机械臂", "A03", "关节J2过载"},
{"2026-05-14 09:58", "洗消系统", "W01", "柱塞泵故障"},
{"2026-05-14 09:20", "洗消系统", "W02", "空压机压力异常"},
{"2026-05-14 08:45", "洗消系统", "W03", "电动球阀D1开启故障"},
{"2026-05-13 09:08", "洗消系统", "W04", "清水液位低"},
{"2026-05-13 08:18", "洗消系统", "W05", "药剂液位低"},
{"2026-05-13 08:02", "洗消系统", "W06", "叶片泵故障"},
{"2026-05-12 11:20", "气体检测仪", "G01", "检测仪通信故障"},
{"2026-05-12 10:50", "气体检测仪", "G02", "有毒物浓度超阈值"},
{"2026-05-11 09:30", "气体检测仪", "G03", "主机标定异常"},
{"2026-05-11 09:10", "机器人底盘", "C04", "手刹失效"},
{"2026-05-10 08:40", "机械臂", "A04", "末端速度超限"},
{"2026-05-10 08:15", "气体检测仪", "G04", "传感器信号异常"},
};
for (const Seed& s : seeds) {
FaultRecord r;
r.time = QString::fromUtf8(s.t);
r.system = QString::fromUtf8(s.sys);
r.code = QString::fromUtf8(s.code);
r.desc = QString::fromUtf8(s.d);
faultHistory.append(r); // 种子数据按“最新在前”顺序直接追加
}
}
void AppContext::appendFault(const QString& system, const QString& code, const QString& desc)
{
FaultRecord r;
r.time = QDateTime::currentDateTime().toString("yyyy-MM-dd hh:mm");
r.system = system;
r.code = code;
r.desc = desc;
faultHistory.prepend(r); // 最新在前
}
double AppContext::getDouble(const QString& key, double def) const
{
return m_settings->value(key, def).toDouble();
}
void AppContext::setDouble(const QString& key, double val)
{
m_settings->setValue(key, val);
m_settings->sync();
}
QString AppContext::getString(const QString& key, const QString& def) const
{
return m_settings->value(key, def).toString();
}
void AppContext::setString(const QString& key, const QString& val)
{
m_settings->setValue(key, val);
m_settings->sync();
}
QString AppContext::password() const
{
return m_settings->value("param/pwd", "6789").toString();
}
void AppContext::setPassword(const QString& pwd)
{
m_settings->setValue("param/pwd", pwd);
m_settings->sync();
}
// 管理员密码为固定值,不持久化、不可修改
QString AppContext::adminPassword() const
{
return "9527";
}
void AppContext::resetEstop()
{
estopTriggered = false;
telemetry.estop = false;
emit estopChanged(false);
// 通知车载端复位
if (m_comm) m_comm->sendEstopReset();
}
void AppContext::triggerEstop()
{
estopTriggered = true;
telemetry.estop = true;
emit estopChanged(true);
if (m_comm) m_comm->sendEstop();
}
void AppContext::setMode(Mode m)
{
// 洗消作业期间强制机械臂模式:作业时末端要自动俯仰摆动,而 Arm_ControlCmd1 的
// 使能位就挂在这个模式上。遥控器**每帧**都会调进来,所以这个覆盖只能放在这里——
// 放到 sendWashStart 之类的地方,会被下一帧遥控器报文冲掉。
if (washRunning) m = Mode::Arm;
if (mode == m) return;
mode = m;
emit modeChanged(m);
if (m_comm) m_comm->setArmMode(m == Mode::Arm); // 同步到 Arm_ControlCmd1 的 Arm_modeCmd/Arm_enable
}
void AppContext::setWashRunning(bool on)
{
if (on == washRunning) return;
washRunning = on;
if (on) {
m_modeBeforeWash = mode;
setMode(Mode::Arm); // 覆盖生效,进入洗消状态
} else {
setMode(m_modeBeforeWash); // 作业结束,还原作业前的模式
}
}
// ===================== 遥控器(24 字节二进制帧) =====================
void AppContext::onRcFrame(const RcFrame& f)
{
// 存入遥测快照,供界面显示
telemetry.rcBattery = f.battery;
telemetry.rcDriveMode = f.driveMode;
telemetry.rcArmMode = f.armMode;
telemetry.rcArmEnabled = f.armEnabled;
telemetry.rcChassisEnabled = f.chassisEnabled;
telemetry.rcEstop = f.estop;
telemetry.rcGear = f.gear;
telemetry.rcSteer = f.steer;
telemetry.rcPosZ = f.posZ;
telemetry.rcPosX = f.posX;
telemetry.rcPosY = f.posY;
telemetry.rcRoll = f.roll;
telemetry.rcPitch = f.pitch;
telemetry.rcYaw = f.yaw;
// 模式:bit1 机械臂模式须与 bit2 机械臂使能同时为 1 才视为机械臂
setMode((f.armMode && f.armEnabled) ? Mode::Arm : Mode::Drive);
// 急停与复位都由遥控器 bit4 下发:上升沿触发,下降沿复位。面板只显示横幅提示。
if (f.estop != m_rcEstopPrev) {
m_rcEstopPrev = f.estop;
if (f.estop) triggerEstop();
else resetEstop();
}
// 动作:与界面按钮同一套安全联锁(模式 + 使能 + 未急停)
applyRcChassis(f.gear, f.steer, f.chassisEnabled);
applyRcArm(f);
updateRcDriving();
}
void AppContext::applyRcChassis(int gear, int steer, bool enabled)
{
const bool allowed = enabled && !estopTriggered && (mode == Mode::Drive);
const int g = allowed ? axisDir(gear) : 0;
const int s = allowed ? axisDir(steer) : 0;
if (g == m_rcGearDir && s == m_rcSteerDir) return; // 方向未变,不重复发包
m_rcGearDir = g;
m_rcSteerDir = s;
sendChassisCommand();
}
void AppContext::applyRcArm(const RcFrame& f)
{
const bool allowed = f.armEnabled && !estopTriggered && (mode == Mode::Arm);
const int vals[6] = {f.posZ, f.posX, f.posY, f.roll, f.pitch, f.yaw};
for (int i = 0; i < 6; ++i) {
const int d = allowed ? axisDir(vals[i]) : 0;
if (d == m_rcAxisDir[i]) continue; // 方向未变,不重复发包
m_rcAxisDir[i] = d;
m_comm->sendArmMoveRc(QString::fromLatin1(kArmAxes[i]), d);
}
}
// 档位与转向可同时下发(边前进/后退边转向);两者都回中才停车。
// 仅转向(档位回中)时发 stop + turn,由车载端决定是否原地转向。
void AppContext::sendChassisCommand()
{
QString dir = "stop";
if (m_rcGearDir > 0) dir = "fwd";
else if (m_rcGearDir < 0) dir = "back";
QString turn;
if (m_rcSteerDir > 0) turn = "right";
else if (m_rcSteerDir < 0) turn = "left";
if (dir == "stop" && turn.isEmpty()) {
m_comm->sendChassisMoveRc("stop", 0);
return;
}
const double spd = (dir == "stop") ? 0.0 : getDouble(keySpeed(), 4.5);
m_comm->sendChassisMoveRc(dir, spd, turn);
}
void AppContext::updateRcDriving()
{
// 遥控器正在操作:链路在线且任一已生效轴偏离中位
bool driving = (m_rcGearDir != 0) || (m_rcSteerDir != 0);
for (int i = 0; i < 6; ++i) {
if (m_rcAxisDir[i] != 0) { driving = true; break; }
}
if (driving == m_rcDriving) return;
m_rcDriving = driving;
// 先告诉通信层再发信号:页面收到 rcDrivingChanged(false) 会重发按住的方向,
// 那一下必须畅通(让位标志得已经落下)
if (m_comm) m_comm->setRcActive(driving);
emit rcDrivingChanged(driving);
}
void AppContext::onRcLinkChanged(bool ok)
{
telemetry.rcLinkOk = ok;
if (!ok) {
// 链路中断:撤销所有遥控器动作,否则最后一条指令会被一直挂住。
// 不动急停状态——丢包导致的 bit4 读为 0 不应被当成遥控器下发的复位。
// 这里必须走 Rc 入口:此刻 rcActive 还是真,走面板入口会被让位挡掉。
if (m_rcGearDir != 0 || m_rcSteerDir != 0) {
m_rcGearDir = 0;
m_rcSteerDir = 0;
m_comm->sendChassisMoveRc("stop", 0);
}
for (int i = 0; i < 6; ++i) {
if (m_rcAxisDir[i] == 0) continue;
m_rcAxisDir[i] = 0;
m_comm->sendArmMoveRc(QString::fromLatin1(kArmAxes[i]), 0);
}
}
updateRcDriving();
}