898 lines
37 KiB
C++
898 lines
37 KiB
C++
#include "MainWindow.h"
|
|
|
|
#include <array>
|
|
|
|
#include <QAbstractItemView>
|
|
#include <QComboBox>
|
|
#include <QDateTime>
|
|
#include <QGridLayout>
|
|
#include <QHBoxLayout>
|
|
#include <QJsonArray>
|
|
#include <QJsonObject>
|
|
#include <QJsonValue>
|
|
#include <QLabel>
|
|
#include <QLineEdit>
|
|
#include <QMessageBox>
|
|
#include <QPushButton>
|
|
#include <QRegularExpression>
|
|
#include <QRegularExpressionValidator>
|
|
#include <QResizeEvent>
|
|
#include <QScroller>
|
|
#include <QScrollerProperties>
|
|
#include <QSignalBlocker>
|
|
#include <QStatusBar>
|
|
#include <QTimer>
|
|
#include <QVariant>
|
|
#include <QVBoxLayout>
|
|
|
|
#include "RemoteGuideClient.h"
|
|
#include "dialogcontrolpassword.h"
|
|
#include "IVrUtils.h"
|
|
#include "Version.h"
|
|
#include "ui_MainWindow.h"
|
|
|
|
namespace {
|
|
|
|
void EnableComboBoxTouchScroll(QComboBox* combo)
|
|
{
|
|
if (!combo) {
|
|
return;
|
|
}
|
|
|
|
combo->setMaxVisibleItems(8);
|
|
QAbstractItemView* view = combo->view();
|
|
if (!view) {
|
|
return;
|
|
}
|
|
view->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOn);
|
|
view->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
|
|
view->setVerticalScrollMode(QAbstractItemView::ScrollPerPixel);
|
|
view->setTextElideMode(Qt::ElideNone);
|
|
QWidget* viewport = view->viewport();
|
|
viewport->setAttribute(Qt::WA_AcceptTouchEvents, true);
|
|
QScroller::grabGesture(viewport, QScroller::LeftMouseButtonGesture);
|
|
QScroller* scroller = QScroller::scroller(viewport);
|
|
if (scroller) {
|
|
QScrollerProperties properties = scroller->scrollerProperties();
|
|
properties.setScrollMetric(
|
|
QScrollerProperties::VerticalOvershootPolicy,
|
|
QVariant(static_cast<int>(QScrollerProperties::OvershootAlwaysOff)));
|
|
scroller->setScrollerProperties(properties);
|
|
}
|
|
}
|
|
|
|
QString NonEmpty(const QString& value, const QString& fallback)
|
|
{
|
|
const QString trimmed = value.trimmed();
|
|
return trimmed.isEmpty() ? fallback : trimmed;
|
|
}
|
|
|
|
bool IsKnownAircraftModel(const QString& modelType)
|
|
{
|
|
const QString normalized = modelType.trimmed().toUpper();
|
|
return !normalized.isEmpty() &&
|
|
normalized != QStringLiteral("未知") &&
|
|
normalized != QStringLiteral("----");
|
|
}
|
|
|
|
QString FormatModelVerification(const QString& expectedModel,
|
|
const QString& detectedModel,
|
|
bool modelMatched)
|
|
{
|
|
const QString expected = expectedModel.trimmed().toUpper();
|
|
const QString detected = detectedModel.trimmed().toUpper();
|
|
if (!IsKnownAircraftModel(detected)) {
|
|
return QStringLiteral("未知");
|
|
}
|
|
if (!IsKnownAircraftModel(expected)) {
|
|
return QStringLiteral("实测(%1)").arg(detected);
|
|
}
|
|
if (modelMatched) {
|
|
return QStringLiteral("匹配(%1)").arg(detected);
|
|
}
|
|
if (detected.compare(expected, Qt::CaseInsensitive) != 0) {
|
|
return QStringLiteral("不匹配(%1)").arg(detected);
|
|
}
|
|
return QStringLiteral("未知");
|
|
}
|
|
|
|
void ApplyActivatedLabel(QLabel* label, bool activated)
|
|
{
|
|
if (!label) {
|
|
return;
|
|
}
|
|
label->setText(activated
|
|
? QStringLiteral("系统已激活")
|
|
: QStringLiteral("未激活"));
|
|
label->setStyleSheet(activated
|
|
? QStringLiteral("color: rgb(0, 255, 0); font-size: 26px; font-weight: 700;")
|
|
: QStringLiteral("color: rgb(172, 181, 194); font-size: 26px; font-weight: 700;"));
|
|
}
|
|
|
|
QJsonObject RuntimeStatusObject(const QJsonObject& root)
|
|
{
|
|
const QJsonValue value = root.value(QStringLiteral("status"));
|
|
return value.isObject() ? value.toObject() : QJsonObject();
|
|
}
|
|
|
|
bool ActivatedOf(const QJsonObject& root)
|
|
{
|
|
const QJsonObject status = RuntimeStatusObject(root);
|
|
if (status.contains(QStringLiteral("activated"))) {
|
|
return status.value("activated").toBool();
|
|
}
|
|
return root.value("activated").toBool();
|
|
}
|
|
|
|
QString WorkModeOf(const QJsonObject& root)
|
|
{
|
|
const QJsonObject status = RuntimeStatusObject(root);
|
|
if (status.contains(QStringLiteral("workMode"))) {
|
|
return status.value("workMode").toString();
|
|
}
|
|
return root.value("workMode").toString();
|
|
}
|
|
|
|
QString FormatProtocolTime(const QJsonValue& value)
|
|
{
|
|
if (value.isString()) {
|
|
const QString text = value.toString().trimmed();
|
|
return text.isEmpty() ? QStringLiteral("--") : text;
|
|
}
|
|
const qint64 epoch = value.toVariant().toLongLong();
|
|
if (epoch <= 0) {
|
|
return QStringLiteral("--");
|
|
}
|
|
return QDateTime::fromMSecsSinceEpoch(epoch).toString(
|
|
QStringLiteral("yyyy-MM-dd HH:mm:ss"));
|
|
}
|
|
|
|
} // namespace
|
|
|
|
MainWindow::MainWindow(int discoveryPort, QWidget* parent)
|
|
: QMainWindow(parent)
|
|
, ui(new Ui::MainWindow)
|
|
{
|
|
ui->setupUi(this);
|
|
EnableComboBoxTouchScroll(ui->modelCombo);
|
|
setWindowTitle(QStringLiteral("停机引导远程显示 %1")
|
|
.arg(QString::fromLatin1(
|
|
PARKINGSPACEGUIDEVIEW_FULL_VERSION_STRING)));
|
|
const QString versionWithBuildTime = QStringLiteral("%1_%2%3%4%5%6%7")
|
|
.arg(QString::fromLatin1(
|
|
PARKINGSPACEGUIDEVIEW_FULL_VERSION_STRING))
|
|
.arg(YEAR)
|
|
.arg(MONTH, 2, 10, QChar('0'))
|
|
.arg(DAY, 2, 10, QChar('0'))
|
|
.arg(HOUR, 2, 10, QChar('0'))
|
|
.arg(MINUTE, 2, 10, QChar('0'))
|
|
.arg(SECOND, 2, 10, QChar('0'));
|
|
ui->versionLabel->setText(versionWithBuildTime);
|
|
statusBar()->addPermanentWidget(ui->versionLabel);
|
|
|
|
connect(ui->windowButton, &QPushButton::clicked,
|
|
this, &QWidget::showMinimized);
|
|
connect(ui->closeWindowButton, &QPushButton::clicked,
|
|
this, &QWidget::close);
|
|
|
|
m_client = new RemoteGuideClient(discoveryPort, this);
|
|
connect(m_client, &RemoteGuideClient::ConnectionStateChanged,
|
|
this, &MainWindow::HandleConnectionState);
|
|
connect(m_client, &RemoteGuideClient::StatusReceived,
|
|
this, &MainWindow::ApplyStatus);
|
|
connect(m_client, &RemoteGuideClient::GuidanceRunningChanged,
|
|
this, [this](bool running) {
|
|
const bool wasRunning = m_guidanceRunning;
|
|
m_guidanceRunning = running;
|
|
if (wasRunning && !running) {
|
|
const QSignalBlocker blocker(ui->modelCombo);
|
|
ui->modelCombo->setCurrentIndex(0);
|
|
}
|
|
UpdateControls();
|
|
});
|
|
connect(m_client, &RemoteGuideClient::CommandFinished,
|
|
this, &MainWindow::HandleCommandFinished);
|
|
|
|
connect(ui->modelCombo,
|
|
QOverload<int>::of(&QComboBox::currentIndexChanged),
|
|
this,
|
|
[this](int index) {
|
|
if (index > 0 && m_client && m_client->IsConnected()) {
|
|
m_commandPending = true;
|
|
UpdateControls();
|
|
m_client->SelectModel(ui->modelCombo->currentText());
|
|
}
|
|
UpdateControls();
|
|
});
|
|
connect(ui->startButton, &QPushButton::clicked, this, [this]() {
|
|
const QString password = PromptControlPassword(QStringLiteral("开始引导"));
|
|
if (m_requireLogin && password.isEmpty()) {
|
|
return;
|
|
}
|
|
m_commandPending = true;
|
|
UpdateControls();
|
|
m_client->StartGuidance(
|
|
ui->modelCombo->currentIndex() > 0
|
|
? ui->modelCombo->currentText()
|
|
: QString(),
|
|
password);
|
|
});
|
|
connect(ui->stopButton, &QPushButton::clicked, this, [this]() {
|
|
const QString password = PromptControlPassword(QStringLiteral("停止引导"));
|
|
if (m_requireLogin && password.isEmpty()) {
|
|
return;
|
|
}
|
|
m_commandPending = true;
|
|
UpdateControls();
|
|
m_client->StopGuidance(password);
|
|
});
|
|
|
|
AddStatusRow(QStringLiteral("系统状态"), m_activatedLabel);
|
|
AddStatusRow(QStringLiteral("自检"), m_selfCheckLabel);
|
|
AddStatusRow(QStringLiteral("工作模式"), m_workModeLabel);
|
|
AddStatusRow(QStringLiteral("人员轮挡"), m_personChockLabel);
|
|
AddStatusRow(QStringLiteral("上轮挡"), m_chockOnLabel);
|
|
AddStatusRow(QStringLiteral("下轮挡"), m_chockOffLabel);
|
|
|
|
auto* passwordLayout = new QHBoxLayout();
|
|
m_passwordEdit = new QLineEdit(ui->sidePanel);
|
|
m_passwordEdit->setEchoMode(QLineEdit::Password);
|
|
m_passwordEdit->setMaxLength(DialogControlPassword::kPasswordLength);
|
|
m_passwordEdit->setValidator(new QRegularExpressionValidator(
|
|
QRegularExpression(QStringLiteral("^[0-9]{0,6}$")),
|
|
m_passwordEdit));
|
|
m_passwordEdit->setInputMethodHints(Qt::ImhDigitsOnly);
|
|
m_passwordEdit->setPlaceholderText(QStringLiteral("6位数字密码"));
|
|
m_passwordEdit->setStyleSheet(
|
|
QStringLiteral("color: rgb(239, 241, 245); background-color: rgb(37, 38, 42);"
|
|
"border: 1px solid rgb(92, 95, 104); padding-left: 12px;"
|
|
"font-size: 18px; min-height: 42px;"));
|
|
m_loginButton = new QPushButton(QStringLiteral("登录"), ui->sidePanel);
|
|
m_loginButton->setMinimumHeight(42);
|
|
passwordLayout->addWidget(m_passwordEdit, 1);
|
|
passwordLayout->addWidget(m_loginButton);
|
|
ui->sideLayout->insertLayout(ui->sideLayout->count() - 1, passwordLayout);
|
|
|
|
auto* actionLayout = new QHBoxLayout();
|
|
m_maintenanceButton = new QPushButton(QStringLiteral("维护模式"), ui->sidePanel);
|
|
actionLayout->addWidget(m_maintenanceButton);
|
|
ui->sideLayout->insertLayout(ui->sideLayout->count() - 1, actionLayout);
|
|
|
|
const QString chockStyle =
|
|
QStringLiteral("QPushButton { color: rgb(239, 241, 245); border: none; "
|
|
"border-radius: 4px; font-size: 16px; }"
|
|
"QPushButton:disabled { background-color: rgb(65, 67, 73); "
|
|
"color: rgb(130, 132, 138); }");
|
|
m_chockOnButton = new QPushButton(QStringLiteral("上轮挡"), ui->header);
|
|
m_chockOnButton->setGeometry(1525, 112, 80, 80);
|
|
m_chockOnButton->setToolTip(QStringLiteral("上轮挡"));
|
|
m_chockOnButton->setStyleSheet(
|
|
chockStyle + QStringLiteral("QPushButton { background-color: rgb(60, 125, 80); }"));
|
|
m_chockOffButton = new QPushButton(QStringLiteral("下轮挡"), ui->header);
|
|
m_chockOffButton->setGeometry(1645, 112, 80, 80);
|
|
m_chockOffButton->setToolTip(QStringLiteral("下轮挡"));
|
|
m_chockOffButton->setStyleSheet(
|
|
chockStyle + QStringLiteral("QPushButton { background-color: rgb(70, 110, 160); }"));
|
|
m_chockOnButton->show();
|
|
m_chockOffButton->show();
|
|
|
|
auto* replayLayout = new QHBoxLayout();
|
|
m_sessionCombo = new QComboBox(ui->sidePanel);
|
|
m_sessionCombo->addItem(QStringLiteral("选择历史会话"));
|
|
m_sessionCombo->setStyleSheet(
|
|
QStringLiteral("color: rgb(239, 241, 245); background-color: rgb(37, 38, 42);"
|
|
"border: 1px solid rgb(92, 95, 104); padding-left: 12px;"
|
|
"font-size: 16px; min-height: 42px;"));
|
|
m_replayButton = new QPushButton(QStringLiteral("回放"), ui->sidePanel);
|
|
m_replayButton->setMinimumHeight(42);
|
|
replayLayout->addWidget(m_sessionCombo, 1);
|
|
replayLayout->addWidget(m_replayButton);
|
|
ui->sideLayout->insertLayout(ui->sideLayout->count() - 1, replayLayout);
|
|
|
|
m_replayTimer = new QTimer(this);
|
|
m_replayTimer->setSingleShot(true);
|
|
connect(m_replayTimer, &QTimer::timeout,
|
|
this, &MainWindow::PlayNextReplaySample);
|
|
m_passwordExpireTimer = new QTimer(this);
|
|
m_passwordExpireTimer->setSingleShot(true);
|
|
connect(m_passwordExpireTimer, &QTimer::timeout, this, [this]() {
|
|
ExpirePasswordSession();
|
|
});
|
|
connect(m_loginButton, &QPushButton::clicked, this, [this]() {
|
|
if (!m_client || !m_client->IsConnected()) {
|
|
return;
|
|
}
|
|
m_commandPending = true;
|
|
UpdateControls();
|
|
m_client->Login(m_passwordEdit->text());
|
|
});
|
|
connect(m_maintenanceButton, &QPushButton::clicked, this, [this]() {
|
|
if (m_guidanceRunning) {
|
|
return;
|
|
}
|
|
const QString password = PromptControlPassword(
|
|
m_maintenanceMode
|
|
? QStringLiteral("退出维护模式")
|
|
: QStringLiteral("进入维护模式"));
|
|
if (m_requireLogin && password.isEmpty()) {
|
|
return;
|
|
}
|
|
m_commandPending = true;
|
|
UpdateControls();
|
|
m_client->SetMaintenanceMode(!m_maintenanceMode, password);
|
|
});
|
|
connect(m_chockOnButton, &QPushButton::clicked, this, [this]() {
|
|
RecordChockWithPassword(true);
|
|
});
|
|
connect(m_chockOffButton, &QPushButton::clicked, this, [this]() {
|
|
RecordChockWithPassword(false);
|
|
});
|
|
connect(m_replayButton, &QPushButton::clicked, this, [this]() {
|
|
if (m_replaying) {
|
|
StopReplay();
|
|
UpdateControls();
|
|
return;
|
|
}
|
|
const int index = m_sessionCombo->currentIndex();
|
|
const QString sessionUuid =
|
|
m_sessionCombo->itemData(index).toString();
|
|
if (sessionUuid.isEmpty()) {
|
|
return;
|
|
}
|
|
const QString password = PromptControlPassword(QStringLiteral("回放"));
|
|
if (m_requireLogin && password.isEmpty()) {
|
|
return;
|
|
}
|
|
m_commandPending = true;
|
|
UpdateControls();
|
|
m_client->RequestSessionSamples(sessionUuid, password);
|
|
});
|
|
|
|
ApplyStatus(QJsonObject());
|
|
HandleConnectionState(false,
|
|
QStringLiteral("正在搜索停机引导设备……"),
|
|
QString(),
|
|
QString());
|
|
}
|
|
|
|
MainWindow::~MainWindow()
|
|
{
|
|
delete ui;
|
|
}
|
|
|
|
void MainWindow::ApplyStatus(const QJsonObject& status)
|
|
{
|
|
if (m_replaying && m_guidanceRunning) {
|
|
StopReplay();
|
|
}
|
|
if (m_replaying) {
|
|
return;
|
|
}
|
|
|
|
ui->stateImage->SetStatus(status);
|
|
int stateCode = status.value("guideStateCode").toInt(12);
|
|
if (stateCode < 1 || stateCode > 24) {
|
|
stateCode = 12;
|
|
}
|
|
ApplyGuideState(stateCode, status.value("guideText").toString());
|
|
|
|
ui->targetIdLabel->setText(NonEmpty(
|
|
status.value("targetId").toString(), QStringLiteral("T001")));
|
|
ui->parkIdLabel->setText(NonEmpty(
|
|
status.value("parkId").toString(), QStringLiteral("未知")));
|
|
|
|
QString expectedModel = status.value("expectedModelType")
|
|
.toString().trimmed();
|
|
if (m_guidanceRunning && IsKnownAircraftModel(expectedModel) &&
|
|
ui->modelCombo->currentText() != expectedModel) {
|
|
const QSignalBlocker blocker(ui->modelCombo);
|
|
ui->modelCombo->setCurrentText(expectedModel);
|
|
}
|
|
if (expectedModel.isEmpty() && ui->modelCombo->currentIndex() > 0) {
|
|
expectedModel = ui->modelCombo->currentText();
|
|
}
|
|
const QString detectedModel = NonEmpty(
|
|
status.value("modelType").toString(), QStringLiteral("未知"));
|
|
ui->modelMatchLabel->setText(FormatModelVerification(
|
|
expectedModel,
|
|
detectedModel,
|
|
status.value("modelMatched").toBool()));
|
|
|
|
const int personState = status.value("guideStateCode").toInt(0);
|
|
const QString personChockTime =
|
|
FormatProtocolTime(status.value("personChockTime"));
|
|
ui->personLabel->setText(
|
|
personState == 21 || personChockTime != QStringLiteral("--")
|
|
? QStringLiteral("已确认")
|
|
: QStringLiteral("--"));
|
|
ui->personCountLabel->setText(QStringLiteral("--"));
|
|
|
|
SetMetric(ui->distanceLabel, status, "distanceToStop",
|
|
QStringLiteral("毫米"), 0);
|
|
SetMetric(ui->offsetLabel, status, "lateralOffset",
|
|
QStringLiteral("毫米"), 0);
|
|
SetMetric(ui->yawLabel, status, "bodyYawAngle", QStringLiteral("度"));
|
|
SetMetric(ui->speedLabel, status, "aircraftSpeed", QStringLiteral("米/秒"));
|
|
|
|
const bool activated = ActivatedOf(status);
|
|
ApplyActivatedLabel(m_activatedLabel, activated);
|
|
const QJsonObject selfCheck = status.value("selfCheck").toObject();
|
|
if (m_selfCheckLabel) {
|
|
if (selfCheck.value("running").toBool()) {
|
|
m_selfCheckLabel->setText(QStringLiteral("自检中"));
|
|
} else if (selfCheck.isEmpty()) {
|
|
m_selfCheckLabel->setText(QStringLiteral("--"));
|
|
} else {
|
|
m_selfCheckLabel->setText(NonEmpty(
|
|
selfCheck.value("message").toString(),
|
|
selfCheck.value("ok").toBool()
|
|
? QStringLiteral("通过")
|
|
: QStringLiteral("失败")));
|
|
}
|
|
}
|
|
const QString workMode = WorkModeOf(status);
|
|
m_maintenanceMode = workMode == QStringLiteral("maintenance");
|
|
if (m_workModeLabel) {
|
|
m_workModeLabel->setText(m_maintenanceMode
|
|
? QStringLiteral("维护模式")
|
|
: QStringLiteral("正常工作模式"));
|
|
}
|
|
if (m_personChockLabel) {
|
|
m_personChockLabel->setText(
|
|
FormatProtocolTime(status.value("personChockTime")));
|
|
}
|
|
if (m_chockOnLabel) {
|
|
m_chockOnLabel->setText(
|
|
FormatProtocolTime(status.value("chockOnTime")));
|
|
}
|
|
if (m_chockOffLabel) {
|
|
m_chockOffLabel->setText(
|
|
FormatProtocolTime(status.value("chockOffTime")));
|
|
}
|
|
if (m_maintenanceButton) {
|
|
m_maintenanceButton->setText(m_maintenanceMode
|
|
? QStringLiteral("退出维护")
|
|
: QStringLiteral("维护模式"));
|
|
}
|
|
}
|
|
|
|
void MainWindow::ApplyGuideState(int stateCode, const QString& guideText)
|
|
{
|
|
const GuidePresentation& presentation = PresentationForState(stateCode);
|
|
ui->stateNameLabel->setText(presentation.name);
|
|
QString stateText = QStringLiteral("%1 %2")
|
|
.arg(stateCode)
|
|
.arg(presentation.name);
|
|
const QString trimmedGuideText = guideText.trimmed();
|
|
if (!trimmedGuideText.isEmpty()) {
|
|
stateText += QStringLiteral(" / %1").arg(trimmedGuideText);
|
|
}
|
|
ui->guideStateLabel->setText(stateText);
|
|
|
|
if (stateCode != m_currentStateCode) {
|
|
m_currentStateCode = stateCode;
|
|
ui->stateCodeLabel->setText(
|
|
QStringLiteral("状态 #%1").arg(stateCode, 2, 10, QChar('0')));
|
|
}
|
|
}
|
|
|
|
void MainWindow::UpdateControls()
|
|
{
|
|
const bool canOperate = m_connected &&
|
|
m_loggedIn &&
|
|
!m_commandPending;
|
|
ui->modelCombo->setEnabled(canOperate &&
|
|
!m_guidanceRunning &&
|
|
!m_replaying);
|
|
ui->startButton->setEnabled(canOperate &&
|
|
!m_guidanceRunning &&
|
|
!m_replaying);
|
|
ui->stopButton->setEnabled(canOperate &&
|
|
m_guidanceRunning);
|
|
if (m_passwordEdit) {
|
|
m_passwordEdit->setEnabled(m_connected &&
|
|
m_requireLogin &&
|
|
!m_loggedIn &&
|
|
!m_commandPending);
|
|
}
|
|
if (m_loginButton) {
|
|
m_loginButton->setEnabled(m_connected &&
|
|
m_requireLogin &&
|
|
!m_loggedIn &&
|
|
!m_commandPending);
|
|
}
|
|
if (m_maintenanceButton) {
|
|
m_maintenanceButton->setEnabled(canOperate && !m_guidanceRunning);
|
|
}
|
|
if (m_chockOnButton) {
|
|
m_chockOnButton->setEnabled(m_connected && !m_commandPending);
|
|
}
|
|
if (m_chockOffButton) {
|
|
m_chockOffButton->setEnabled(m_connected && !m_commandPending);
|
|
}
|
|
if (m_sessionCombo) {
|
|
m_sessionCombo->setEnabled(canOperate && !m_guidanceRunning);
|
|
}
|
|
if (m_replayButton) {
|
|
m_replayButton->setEnabled(canOperate && !m_guidanceRunning);
|
|
m_replayButton->setText(m_replaying
|
|
? QStringLiteral("停止")
|
|
: QStringLiteral("回放"));
|
|
}
|
|
}
|
|
|
|
void MainWindow::SetMetric(QLabel* label,
|
|
const QJsonObject& status,
|
|
const char* field,
|
|
const QString& unit,
|
|
int precision,
|
|
double divisor)
|
|
{
|
|
const double safeDivisor = divisor == 0.0 ? 1.0 : divisor;
|
|
const double value = status.value(QString::fromLatin1(field))
|
|
.toDouble(0.0) / safeDivisor;
|
|
label->setText(QString::number(value, 'f', precision) + unit);
|
|
}
|
|
|
|
void MainWindow::HandleConnectionState(bool connected,
|
|
const QString& message,
|
|
const QString& deviceName,
|
|
const QString& address)
|
|
{
|
|
Q_UNUSED(message);
|
|
Q_UNUSED(deviceName);
|
|
Q_UNUSED(address);
|
|
m_connected = connected;
|
|
m_commandPending = false;
|
|
m_guidanceRunning = connected && m_client
|
|
? m_client->IsGuidanceRunning()
|
|
: false;
|
|
if (!connected) {
|
|
StopReplay();
|
|
ExpirePasswordSession();
|
|
m_requireLogin = true;
|
|
const QSignalBlocker blocker(ui->modelCombo);
|
|
ui->modelCombo->clear();
|
|
ui->modelCombo->addItem(QStringLiteral("请选择机型"));
|
|
if (m_sessionCombo) {
|
|
const QSignalBlocker sessionBlocker(m_sessionCombo);
|
|
m_sessionCombo->clear();
|
|
m_sessionCombo->addItem(QStringLiteral("选择历史会话"));
|
|
}
|
|
if (m_passwordEdit) {
|
|
m_passwordEdit->clear();
|
|
}
|
|
}
|
|
ui->deviceLabel->setText(connected
|
|
? QStringLiteral("已连接")
|
|
: QStringLiteral("未连接"));
|
|
UpdateControls();
|
|
}
|
|
|
|
void MainWindow::HandleCommandFinished(const QString& command,
|
|
bool ok,
|
|
const QString& message,
|
|
const QJsonObject& response)
|
|
{
|
|
Q_UNUSED(message);
|
|
m_commandPending = false;
|
|
if (ok && command == QStringLiteral("start")) {
|
|
m_guidanceRunning = true;
|
|
StopReplay();
|
|
} else if (ok && command == QStringLiteral("stop")) {
|
|
m_guidanceRunning = false;
|
|
const QSignalBlocker blocker(ui->modelCombo);
|
|
ui->modelCombo->setCurrentIndex(0);
|
|
} else if (!ok && command == QStringLiteral("select_model")) {
|
|
const QSignalBlocker blocker(ui->modelCombo);
|
|
ui->modelCombo->setCurrentIndex(0);
|
|
} else if (command == QStringLiteral("login")) {
|
|
if (ok) {
|
|
BeginPasswordSession();
|
|
m_client->RequestSessions(50);
|
|
} else {
|
|
ExpirePasswordSession();
|
|
if (m_passwordEdit) {
|
|
m_passwordEdit->clear();
|
|
}
|
|
}
|
|
} else if (ok && command == QStringLiteral("get_panel_config")) {
|
|
ApplyPanelConfig(response.value("panel").toObject());
|
|
} else if (ok && command == QStringLiteral("list_sessions")) {
|
|
ApplySessionList(response.value("sessions").toArray());
|
|
} else if (ok && command == QStringLiteral("get_session_samples")) {
|
|
StartReplay(response.value("samples").toArray());
|
|
} else if (ok && command == QStringLiteral("set_maintenance")) {
|
|
if (response.contains(QStringLiteral("workMode"))) {
|
|
m_maintenanceMode = response.value("workMode").toString() ==
|
|
QStringLiteral("maintenance");
|
|
} else {
|
|
m_maintenanceMode = response.value("maintenanceMode").toBool();
|
|
}
|
|
}
|
|
if (m_client && m_client->IsConnected()) {
|
|
m_guidanceRunning = m_client->IsGuidanceRunning();
|
|
}
|
|
UpdateControls();
|
|
}
|
|
|
|
void MainWindow::resizeEvent(QResizeEvent* event)
|
|
{
|
|
QMainWindow::resizeEvent(event);
|
|
}
|
|
|
|
const MainWindow::GuidePresentation&
|
|
MainWindow::PresentationForState(int stateCode)
|
|
{
|
|
static const std::array<GuidePresentation, 24> states = {{
|
|
{QStringLiteral("泊位引导启动"), QStringLiteral("START-OF-DOCKING"), QStringLiteral("WAIT"), QStringLiteral("系统已进入泊位引导等待状态。"), QStringLiteral("等待运行箭头或接近率条出现,并确认机型正确。"), QStringLiteral("warn")},
|
|
{QStringLiteral("捕获"), QStringLiteral("CAPTURE"), QStringLiteral("CAPTURE"), QStringLiteral("运行箭头和飞机类型表示系统正在搜索接近中的飞机。"), QStringLiteral("确认机型正确,沿引导线低速滑行。"), QStringLiteral("good")},
|
|
{QStringLiteral("跟踪"), QStringLiteral("TRACKING"), QStringLiteral("TRACKING"), QStringLiteral("中心线和转向箭头提示飞机位置及修正方向。"), QStringLiteral("按红色箭头小幅修正方向,保持低速。"), QStringLiteral("good")},
|
|
{QStringLiteral("接近率"), QStringLiteral("CLOSING RATE"), QStringLiteral("CLOSING"), QStringLiteral("数字和接近率条显示距停止位置的剩余距离。"), QStringLiteral("保持低速并按箭头修正,准备在 STOP 出现时停车。"), QStringLiteral("good")},
|
|
{QStringLiteral("对准中心线"), QStringLiteral("ALIGNED TO CENTRE"), QStringLiteral("ALIGNED"), QStringLiteral("显示距离但无转向箭头,表示飞机已对准中心线。"), QStringLiteral("继续低速向停止位滑行。"), QStringLiteral("good")},
|
|
{QStringLiteral("慢速"), QStringLiteral("SLOW"), QStringLiteral("SLOW"), QStringLiteral("飞机接近速度高于当前区域的允许速度。"), QStringLiteral("立即减速,观察是否恢复正常接近率显示。"), QStringLiteral("warn")},
|
|
{QStringLiteral("方位引导"), QStringLiteral("AZIMUTH GUIDANCE"), QStringLiteral("GUIDANCE"), QStringLiteral("位置箭头和转向箭头共同提示方位修正。"), QStringLiteral("按转向箭头修正并保持低速。"), QStringLiteral("good")},
|
|
{QStringLiteral("到达停止位置"), QStringLiteral("STOP POSITION"), QStringLiteral("STOP"), QStringLiteral("飞机已经到达正确停止位置。"), QStringLiteral("立即停车,等待地面确认。"), QStringLiteral("stop")},
|
|
{QStringLiteral("泊位引导完成"), QStringLiteral("DOCKING COMPLETED"), QStringLiteral("OK"), QStringLiteral("泊位引导流程已经完成。"), QStringLiteral("保持停稳,等待地面服务。"), QStringLiteral("good")},
|
|
{QStringLiteral("越位"), QStringLiteral("OVERSHOOT"), QStringLiteral("TOO FAR"), QStringLiteral("飞机已经越过目标停止位置。"), QStringLiteral("立即停止,等待人工处置。"), QStringLiteral("stop")},
|
|
{QStringLiteral("提前停止"), QStringLiteral("STOP SHORT"), QStringLiteral("STOP OK"), QStringLiteral("飞机已静止,但尚未到达预定停止位置。"), QStringLiteral("等待地面确认是否继续或结束。"), QStringLiteral("warn")},
|
|
{QStringLiteral("等待"), QStringLiteral("WAIT"), QStringLiteral("WAIT"), QStringLiteral("系统等待视野恢复或重新检测到飞机。"), QStringLiteral("保持等待,在接近率条恢复前不要继续。"), QStringLiteral("warn")},
|
|
{QStringLiteral("异常慢速-恶劣天气"), QStringLiteral("SLOW - WEATHER"), QStringLiteral("SLOW"), QStringLiteral("天气原因导致系统进入异常慢速捕获模式。"), QStringLiteral("低速等待接近率条出现。"), QStringLiteral("warn")},
|
|
{QStringLiteral("异常慢速-飞机丢失"), QStringLiteral("SLOW - LOST"), QStringLiteral("SLOW"), QStringLiteral("引导过程中暂时丢失飞机目标。"), QStringLiteral("低速等待系统重新检测飞机。"), QStringLiteral("warn")},
|
|
{QStringLiteral("飞机验证失败"), QStringLiteral("ID FAIL"), QStringLiteral("STOP ID FAIL"), QStringLiteral("检测机型与选择机型验证失败。"), QStringLiteral("停止飞机,等待人工复核,不得自行继续。"), QStringLiteral("stop")},
|
|
{QStringLiteral("机位受阻"), QStringLiteral("GATE BLOCKED"), QStringLiteral("WAIT GATE BLOCK"), QStringLiteral("机位前方或进近视野存在障碍。"), QStringLiteral("等待障碍物清除,未恢复前不要继续。"), QStringLiteral("warn")},
|
|
{QStringLiteral("视野受阻"), QStringLiteral("VIEW BLOCKED"), QStringLiteral("WAIT VIEW BLOCK"), QStringLiteral("传感器视野受阻,当前无法可靠检测飞机。"), QStringLiteral("等待视野恢复,必要时切换人工引导。"), QStringLiteral("warn")},
|
|
{QStringLiteral("SBU停止"), QStringLiteral("SBU STOP"), QStringLiteral("STOP SBU"), QStringLiteral("自动引导因不可恢复错误进入安全备份状态。"), QStringLiteral("立即停止并使用人工引导,不得自行恢复。"), QStringLiteral("stop")},
|
|
{QStringLiteral("速度过快"), QStringLiteral("TOO FAST"), QStringLiteral("STOP TOO FAST"), QStringLiteral("飞机速度超出系统可以安全处理的范围。"), QStringLiteral("立即停止飞机,等待系统重启或人工引导。"), QStringLiteral("stop")},
|
|
{QStringLiteral("紧急停止"), QStringLiteral("EMERGENCY STOP"), QStringLiteral("STOP"), QStringLiteral("现场急停或急停线路断开已经触发。"), QStringLiteral("立即停止,等待现场确认和处置。"), QStringLiteral("stop")},
|
|
{QStringLiteral("挡轮挡"), QStringLiteral("CHOCKS ON"), QStringLiteral("CHOCK ON"), QStringLiteral("轮挡区域人员检测已完成。"), QStringLiteral("保持停稳,进入地面保障流程。"), QStringLiteral("good")},
|
|
{QStringLiteral("系统错误"), QStringLiteral("ERROR"), QStringLiteral("ERROR"), QStringLiteral("系统发生错误,自动引导不可继续。"), QStringLiteral("停止自动引导并通知维护人员。"), QStringLiteral("stop")},
|
|
{QStringLiteral("系统故障"), QStringLiteral("SYSTEM BREAKDOWN"), QStringLiteral("BLACK"), QStringLiteral("系统发生严重故障,自动引导不可用。"), QStringLiteral("使用人工备份程序。"), QStringLiteral("maint")},
|
|
{QStringLiteral("电源故障"), QStringLiteral("POWER FAILURE"), QStringLiteral("BLACK"), QStringLiteral("显示和引导系统电源中断。"), QStringLiteral("使用人工备份程序并检查供电。"), QStringLiteral("maint")}
|
|
}};
|
|
|
|
if (stateCode < 1 || stateCode > static_cast<int>(states.size())) {
|
|
stateCode = 12;
|
|
}
|
|
return states[static_cast<size_t>(stateCode - 1)];
|
|
}
|
|
|
|
void MainWindow::AddStatusRow(const QString& caption, QLabel*& valueLabel)
|
|
{
|
|
const int row = ui->resultGrid->rowCount();
|
|
auto* captionLabel = new QLabel(caption, ui->sidePanel);
|
|
captionLabel->setStyleSheet(
|
|
QStringLiteral("color: rgb(153, 164, 178); font-size: 24px;"));
|
|
valueLabel = new QLabel(ui->sidePanel);
|
|
valueLabel->setAlignment(Qt::AlignRight | Qt::AlignVCenter);
|
|
valueLabel->setTextInteractionFlags(Qt::TextSelectableByMouse);
|
|
valueLabel->setStyleSheet(
|
|
QStringLiteral("color: rgb(239, 242, 246); font-size: 26px; font-weight: 500;"));
|
|
valueLabel->setText(QStringLiteral("--"));
|
|
ui->resultGrid->addWidget(captionLabel, row, 0);
|
|
ui->resultGrid->addWidget(valueLabel, row, 1);
|
|
}
|
|
|
|
void MainWindow::ApplyPanelConfig(const QJsonObject& panel)
|
|
{
|
|
m_requireLogin = panel.value("requireLogin").toBool(true);
|
|
m_passwordValidSeconds = panel.value("passwordValidSeconds").toInt(120);
|
|
if (m_passwordValidSeconds < 1) {
|
|
m_passwordValidSeconds = 120;
|
|
}
|
|
if (panel.contains(QStringLiteral("workMode"))) {
|
|
m_maintenanceMode = panel.value("workMode").toString() ==
|
|
QStringLiteral("maintenance");
|
|
} else {
|
|
m_maintenanceMode = panel.value("maintenanceMode").toBool();
|
|
}
|
|
if (!m_requireLogin) {
|
|
m_loggedIn = true;
|
|
if (m_client) {
|
|
m_client->RequestSessions(50);
|
|
}
|
|
}
|
|
RebuildModelCombo(panel.value("models").toArray());
|
|
if (panel.contains(QStringLiteral("activated"))) {
|
|
ApplyActivatedLabel(m_activatedLabel,
|
|
panel.value("activated").toBool());
|
|
}
|
|
if (m_workModeLabel) {
|
|
m_workModeLabel->setText(m_maintenanceMode
|
|
? QStringLiteral("维护模式")
|
|
: QStringLiteral("正常工作模式"));
|
|
}
|
|
UpdateControls();
|
|
}
|
|
|
|
void MainWindow::RebuildModelCombo(const QJsonArray& models)
|
|
{
|
|
const QString current = ui->modelCombo->currentText();
|
|
const QSignalBlocker blocker(ui->modelCombo);
|
|
ui->modelCombo->clear();
|
|
ui->modelCombo->addItem(QStringLiteral("请选择机型"));
|
|
for (const QJsonValue& value : models) {
|
|
const QString name = value.isObject()
|
|
? value.toObject().value("name").toString().trimmed()
|
|
: value.toString().trimmed();
|
|
if (!name.isEmpty()) {
|
|
ui->modelCombo->addItem(name);
|
|
}
|
|
}
|
|
if (ui->modelCombo->count() <= 1) {
|
|
ui->modelCombo->addItem(QStringLiteral("A320"));
|
|
ui->modelCombo->addItem(QStringLiteral("B737"));
|
|
ui->modelCombo->addItem(QStringLiteral("其他"));
|
|
}
|
|
const int index = ui->modelCombo->findText(current);
|
|
ui->modelCombo->setCurrentIndex(index > 0 ? index : 0);
|
|
}
|
|
|
|
void MainWindow::ApplySessionList(const QJsonArray& sessions)
|
|
{
|
|
if (!m_sessionCombo) {
|
|
return;
|
|
}
|
|
const QSignalBlocker blocker(m_sessionCombo);
|
|
m_sessionCombo->clear();
|
|
m_sessionCombo->addItem(QStringLiteral("选择历史会话"));
|
|
for (const QJsonValue& value : sessions) {
|
|
const QJsonObject object = value.toObject();
|
|
const QString uuid = object.value("sessionUuid").toString();
|
|
if (uuid.isEmpty()) {
|
|
continue;
|
|
}
|
|
const QString model = NonEmpty(
|
|
object.value("expectedModelType").toString(),
|
|
NonEmpty(object.value("modelType").toString(),
|
|
NonEmpty(object.value("expectedModel").toString(),
|
|
object.value("recognizedModel").toString())));
|
|
const QString label = QStringLiteral("%1 %2")
|
|
.arg(FormatProtocolTime(object.value("startTime")),
|
|
NonEmpty(model, QStringLiteral("未选机型")));
|
|
m_sessionCombo->addItem(label, uuid);
|
|
}
|
|
}
|
|
|
|
void MainWindow::StartReplay(const QJsonArray& samples)
|
|
{
|
|
StopReplay();
|
|
m_replaySamples = samples;
|
|
m_replayIndex = 0;
|
|
if (m_replaySamples.isEmpty()) {
|
|
return;
|
|
}
|
|
m_replaying = true;
|
|
PlayNextReplaySample();
|
|
}
|
|
|
|
void MainWindow::StopReplay()
|
|
{
|
|
m_replaying = false;
|
|
m_replaySamples = QJsonArray();
|
|
m_replayIndex = 0;
|
|
if (m_replayTimer) {
|
|
m_replayTimer->stop();
|
|
}
|
|
}
|
|
|
|
void MainWindow::PlayNextReplaySample()
|
|
{
|
|
if (!m_replaying || m_replayIndex >= m_replaySamples.size()) {
|
|
StopReplay();
|
|
UpdateControls();
|
|
return;
|
|
}
|
|
|
|
const QJsonObject sample = m_replaySamples.at(m_replayIndex).toObject();
|
|
m_replaying = false;
|
|
ApplyStatus(sample);
|
|
m_replaying = true;
|
|
++m_replayIndex;
|
|
if (m_replayIndex >= m_replaySamples.size()) {
|
|
StopReplay();
|
|
UpdateControls();
|
|
return;
|
|
}
|
|
|
|
const qint64 currentTs =
|
|
sample.value("timestamp").toVariant().toLongLong();
|
|
const qint64 nextTs = m_replaySamples.at(m_replayIndex)
|
|
.toObject()
|
|
.value("timestamp")
|
|
.toVariant()
|
|
.toLongLong();
|
|
int intervalMs = static_cast<int>(nextTs - currentTs);
|
|
if (intervalMs < 50) {
|
|
intervalMs = 50;
|
|
} else if (intervalMs > 1000) {
|
|
intervalMs = 1000;
|
|
}
|
|
m_replayTimer->start(intervalMs);
|
|
}
|
|
|
|
QString MainWindow::PromptControlPassword(const QString& title)
|
|
{
|
|
if (!m_requireLogin) {
|
|
return QStringLiteral("");
|
|
}
|
|
if (IsPasswordSessionValid() && m_client) {
|
|
const QString cached = m_client->SessionPassword();
|
|
if (!cached.isEmpty()) {
|
|
return cached;
|
|
}
|
|
}
|
|
DialogControlPassword dialog(this);
|
|
dialog.SetTitle(title);
|
|
if (m_client) {
|
|
const QString expected = m_client->SessionPassword();
|
|
if (!expected.isEmpty()) {
|
|
dialog.SetPasswordChecker([expected](const QString& password) {
|
|
return password == expected;
|
|
});
|
|
}
|
|
}
|
|
if (dialog.exec() != QDialog::Accepted) {
|
|
return QString();
|
|
}
|
|
if (m_client) {
|
|
m_client->SetSessionPassword(dialog.Password());
|
|
}
|
|
BeginPasswordSession();
|
|
return dialog.Password();
|
|
}
|
|
|
|
bool MainWindow::IsPasswordSessionValid() const
|
|
{
|
|
return m_loggedIn &&
|
|
m_passwordValidUntilMs > QDateTime::currentMSecsSinceEpoch();
|
|
}
|
|
|
|
void MainWindow::BeginPasswordSession()
|
|
{
|
|
// 从本次成功输入起计算有效期,到期后必须重新输入密码。
|
|
m_loggedIn = true;
|
|
const int validSeconds = m_passwordValidSeconds < 1
|
|
? 120
|
|
: m_passwordValidSeconds;
|
|
m_passwordValidUntilMs = QDateTime::currentMSecsSinceEpoch() +
|
|
static_cast<qint64>(validSeconds) * 1000;
|
|
if (m_passwordExpireTimer) {
|
|
m_passwordExpireTimer->start(validSeconds * 1000);
|
|
}
|
|
UpdateControls();
|
|
}
|
|
|
|
void MainWindow::ExpirePasswordSession()
|
|
{
|
|
m_loggedIn = false;
|
|
m_passwordValidUntilMs = 0;
|
|
if (m_passwordExpireTimer) {
|
|
m_passwordExpireTimer->stop();
|
|
}
|
|
if (m_client) {
|
|
m_client->ClearSessionPassword();
|
|
}
|
|
if (m_passwordEdit) {
|
|
m_passwordEdit->clear();
|
|
}
|
|
UpdateControls();
|
|
}
|
|
|
|
void MainWindow::RecordChockWithPassword(bool chockOn)
|
|
{
|
|
if (!m_client || !m_client->IsConnected()) {
|
|
return;
|
|
}
|
|
const QString title = chockOn
|
|
? QStringLiteral("上轮挡")
|
|
: QStringLiteral("下轮挡");
|
|
const QString password = PromptControlPassword(title);
|
|
if (m_requireLogin && password.isEmpty()) {
|
|
return;
|
|
}
|
|
m_commandPending = true;
|
|
UpdateControls();
|
|
m_client->RecordChock(chockOn, password);
|
|
}
|