570 lines
20 KiB
C++
Raw Normal View History

2026-07-30 12:06:02 +08:00
#include "RemoteGuideClient.h"
#include <QByteArray>
#include <QFutureWatcher>
#include <QHostAddress>
#include <QJsonDocument>
#include <QJsonParseError>
#include <QMetaObject>
#include <QNetworkAddressEntry>
#include <QNetworkInterface>
#include <QPointer>
#include <QSet>
#include <QStringList>
#include <QTimer>
#include <QUdpSocket>
#include <QtConcurrent/QtConcurrentRun>
#include "IVrZeroMQClient.h"
#include "IVrZeroMQPubSub.h"
#include "VrLog.h"
namespace {
const char kProtocol[] = "ParkingSpaceGuideRemote";
const char kService[] = "ParkingSpaceGuide";
bool IsValidPort(int port)
{
return port > 0 && port <= 65535;
}
QString ResponseMessage(const QJsonObject& response,
const QString& fallback)
{
const QString message = response.value("message").toString().trimmed();
return message.isEmpty() ? fallback : message;
}
} // namespace
RemoteGuideClient::RemoteGuideClient(int discoveryPort, QObject* parent)
: QObject(parent)
, m_discoveryPort(IsValidPort(discoveryPort) ? discoveryPort : 5555)
, m_discoveryTimer(new QTimer(this))
, m_heartbeatTimer(new QTimer(this))
, m_requestWatcher(new QFutureWatcher<ControlReply>(this))
{
m_discoveryTimer->setInterval(1000);
m_heartbeatTimer->setInterval(3000);
connect(m_discoveryTimer, &QTimer::timeout,
this, &RemoteGuideClient::SendDiscovery);
connect(m_heartbeatTimer, &QTimer::timeout, this, [this]() {
if (m_connected && !m_requestInFlight) {
SendControlCommand(QStringLiteral("ping"));
}
});
connect(m_requestWatcher, &QFutureWatcher<ControlReply>::finished,
this, &RemoteGuideClient::HandleControlFinished);
EnsureDiscoverySocket();
LOG_INFO("ParkingSpaceGuideView discovery started localPort=%u targetPort=%d\n",
m_discoverySocket
? static_cast<unsigned int>(m_discoverySocket->localPort())
: 0U,
m_discoveryPort);
m_discoveryTimer->start();
QTimer::singleShot(0, this, &RemoteGuideClient::SendDiscovery);
}
RemoteGuideClient::~RemoteGuideClient()
{
++m_generation;
m_discoveryTimer->stop();
m_heartbeatTimer->stop();
disconnect(m_requestWatcher, nullptr, this, nullptr);
if (m_requestWatcher->isRunning()) {
m_requestWatcher->waitForFinished();
}
if (m_subscriber) {
m_subscriber->UnInit();
delete m_subscriber;
m_subscriber = nullptr;
}
}
bool RemoteGuideClient::IsConnected() const
{
return m_connected;
}
bool RemoteGuideClient::IsGuidanceRunning() const
{
return m_guidanceRunning;
}
QString RemoteGuideClient::ServerAddress() const
{
return m_serverAddress;
}
void RemoteGuideClient::SelectModel(const QString& modelType)
{
QJsonObject arguments;
arguments["modelType"] = modelType.trimmed().toUpper();
SendControlCommand(QStringLiteral("select_model"), arguments);
}
void RemoteGuideClient::StartGuidance(const QString& modelType)
{
QJsonObject arguments;
arguments["modelType"] = modelType.trimmed().toUpper();
SendControlCommand(QStringLiteral("start"), arguments);
}
void RemoteGuideClient::StopGuidance()
{
SendControlCommand(QStringLiteral("stop"));
}
void RemoteGuideClient::RequestStatus()
{
SendControlCommand(QStringLiteral("get_status"));
}
void RemoteGuideClient::EnsureDiscoverySocket()
{
if (!m_discoverySocket) {
m_discoverySocket = new QUdpSocket(this);
connect(m_discoverySocket, &QUdpSocket::readyRead,
this, &RemoteGuideClient::ReadDiscoveryDatagrams);
}
if (m_discoverySocket->state() == QAbstractSocket::UnconnectedState) {
const bool bound = m_discoverySocket->bind(
QHostAddress::AnyIPv4,
0,
QUdpSocket::ShareAddress | QUdpSocket::ReuseAddressHint);
if (!bound) {
const QByteArray errorText = m_discoverySocket->errorString()
.toUtf8();
LOG_ERROR("ParkingSpaceGuideView discovery bind failed error=%s\n",
errorText.constData());
}
}
}
void RemoteGuideClient::SendDiscovery()
{
if (m_connected) {
return;
}
EnsureDiscoverySocket();
QJsonObject request;
request["cmd"] = QStringLiteral("discover");
request["service"] = QString::fromLatin1(kService);
request["protocol"] = QString::fromLatin1(kProtocol);
const QByteArray payload = QJsonDocument(request)
.toJson(QJsonDocument::Compact);
QSet<QString> broadcastAddresses;
broadcastAddresses.insert(QHostAddress(QHostAddress::LocalHost).toString());
broadcastAddresses.insert(QHostAddress(QHostAddress::Broadcast).toString());
const QList<QNetworkInterface> interfaces =
QNetworkInterface::allInterfaces();
for (const QNetworkInterface& networkInterface : interfaces) {
const QNetworkInterface::InterfaceFlags flags = networkInterface.flags();
if (!flags.testFlag(QNetworkInterface::IsUp) ||
!flags.testFlag(QNetworkInterface::IsRunning) ||
flags.testFlag(QNetworkInterface::IsLoopBack)) {
continue;
}
for (const QNetworkAddressEntry& entry :
networkInterface.addressEntries()) {
if (entry.ip().protocol() != QAbstractSocket::IPv4Protocol ||
entry.broadcast().isNull()) {
continue;
}
broadcastAddresses.insert(entry.broadcast().toString());
}
}
++m_discoveryAttempt;
bool sent = false;
int sentCount = 0;
for (const QString& address : broadcastAddresses) {
const qint64 sentBytes = m_discoverySocket->writeDatagram(
payload,
QHostAddress(address),
static_cast<quint16>(m_discoveryPort));
if (sentBytes >= 0) {
sent = true;
++sentCount;
} else {
const QByteArray addressText = address.toUtf8();
const QByteArray errorText = m_discoverySocket->errorString()
.toUtf8();
LOG_WARNING("ParkingSpaceGuideView discovery send failed target=%s:%d error=%s\n",
addressText.constData(),
m_discoveryPort,
errorText.constData());
}
}
const QByteArray targetsText = QStringList(broadcastAddresses.values())
.join(QStringLiteral(","))
.toUtf8();
LOG_INFO("ParkingSpaceGuideView discovery attempt=%d localPort=%u targetPort=%d sent=%d targets=%s\n",
m_discoveryAttempt,
static_cast<unsigned int>(m_discoverySocket->localPort()),
m_discoveryPort,
sentCount,
targetsText.constData());
SetConnectionMessage(sent
? QStringLiteral("正在搜索停机引导设备……")
: QStringLiteral("UDP搜索发送失败将继续重试%1")
.arg(m_discoverySocket->errorString()));
}
void RemoteGuideClient::ReadDiscoveryDatagrams()
{
while (m_discoverySocket && m_discoverySocket->hasPendingDatagrams()) {
QByteArray payload;
payload.resize(static_cast<int>(
m_discoverySocket->pendingDatagramSize()));
QHostAddress sender;
quint16 senderPort = 0;
const qint64 bytes = m_discoverySocket->readDatagram(
payload.data(), payload.size(), &sender, &senderPort);
if (bytes <= 0) {
continue;
}
payload.resize(static_cast<int>(bytes));
const QByteArray senderText = sender.toString().toUtf8();
LOG_INFO("ParkingSpaceGuideView discovery datagram peer=%s:%u bytes=%lld\n",
senderText.constData(),
static_cast<unsigned int>(senderPort),
static_cast<long long>(bytes));
QJsonParseError error;
const QJsonDocument document = QJsonDocument::fromJson(payload, &error);
if (error.error != QJsonParseError::NoError ||
!document.isObject()) {
const QByteArray errorText = error.errorString().toUtf8();
LOG_WARNING("ParkingSpaceGuideView invalid discovery JSON peer=%s:%u error=%s\n",
senderText.constData(),
static_cast<unsigned int>(senderPort),
errorText.constData());
continue;
}
const QJsonObject object = document.object();
if (!object.value("ok").toBool() ||
object.value("cmd").toString() != QStringLiteral("discover") ||
object.value("protocol").toString() !=
QString::fromLatin1(kProtocol) ||
object.value("service").toString() !=
QString::fromLatin1(kService)) {
LOG_WARNING("ParkingSpaceGuideView ignored discovery response peer=%s:%u protocol/service mismatch\n",
senderText.constData(),
static_cast<unsigned int>(senderPort));
continue;
}
if (!m_connected) {
LOG_INFO("ParkingSpaceGuideView discovered service peer=%s control=%d publish=%d\n",
senderText.constData(),
object.value("controlPort").toInt(),
object.value("publishPort").toInt());
ConnectToServer(sender.toString(), object);
}
}
}
bool RemoteGuideClient::ConnectToServer(const QString& address,
const QJsonObject& serverInfo)
{
const int controlPort = serverInfo.value("controlPort").toInt();
const int publishPort = serverInfo.value("publishPort").toInt();
const QString topic = serverInfo.value("topic").toString().trimmed();
if (address.isEmpty() || !IsValidPort(controlPort) ||
!IsValidPort(publishPort) || topic.isEmpty()) {
const QByteArray addressText = address.toUtf8();
LOG_ERROR("ParkingSpaceGuideView invalid service info peer=%s control=%d publish=%d topicEmpty=%d\n",
addressText.constData(),
controlPort,
publishPort,
topic.isEmpty() ? 1 : 0);
return false;
}
IVrZeroMQSubscriber* subscriber = nullptr;
if (!IVrZeroMQSubscriber::CreateObject(&subscriber) || !subscriber) {
SetConnectionMessage(QStringLiteral("创建状态订阅器失败,将继续搜索"));
return false;
}
const QByteArray expectedTopic = topic.toUtf8();
const int connectionGeneration = m_generation + 1;
QPointer<RemoteGuideClient> self(this);
const FunSubRecv callback =
[self, expectedTopic, connectionGeneration](const char* topicData,
size_t topicLength,
const char* data,
size_t dataLength) {
if (!self || !topicData || (!data && dataLength > 0)) {
return;
}
const QByteArray receivedTopic(
topicData, static_cast<int>(topicLength));
if (receivedTopic != expectedTopic) {
return;
}
const QByteArray payload(data, static_cast<int>(dataLength));
QMetaObject::invokeMethod(
self,
[self, payload, connectionGeneration]() {
if (self && self->m_connected &&
self->m_generation == connectionGeneration) {
self->HandleStatusPayload(payload);
}
},
Qt::QueuedConnection);
};
const QByteArray addressUtf8 = address.toUtf8();
const QByteArray topicUtf8 = topic.toUtf8();
if (subscriber->Init(addressUtf8.constData(),
publishPort,
topicUtf8.constData(),
callback) != 0) {
subscriber->UnInit();
delete subscriber;
SetConnectionMessage(QStringLiteral("连接状态发布端口失败,将继续搜索"));
return false;
}
if (m_subscriber) {
m_subscriber->UnInit();
delete m_subscriber;
}
m_subscriber = subscriber;
m_serverAddress = address;
m_deviceName = serverInfo.value("deviceName")
.toString(QStringLiteral("ParkingSpaceGuideApp"));
m_topic = topic;
m_controlPort = controlPort;
m_publishPort = publishPort;
m_guidanceRunning = serverInfo.value("guidanceRunning").toBool();
m_connected = true;
m_generation = connectionGeneration;
m_discoveryTimer->stop();
m_heartbeatTimer->start();
m_connectionMessage = QStringLiteral("已连接");
const QByteArray serverText = m_serverAddress.toUtf8();
const QByteArray topicText = m_topic.toUtf8();
LOG_INFO("ParkingSpaceGuideView connected server=%s control=%d publish=%d topic=%s\n",
serverText.constData(),
m_controlPort,
m_publishPort,
topicText.constData());
emit ConnectionStateChanged(true,
m_connectionMessage,
m_deviceName,
m_serverAddress);
RequestStatus();
return true;
}
void RemoteGuideClient::DisconnectServer(const QString& reason)
{
++m_generation;
m_connected = false;
m_guidanceRunning = false;
m_requestInFlight = false;
m_heartbeatTimer->stop();
if (m_subscriber) {
m_subscriber->UnInit();
delete m_subscriber;
m_subscriber = nullptr;
}
m_serverAddress.clear();
m_deviceName.clear();
m_topic.clear();
m_controlPort = 0;
m_publishPort = 0;
m_connectionMessage = reason;
emit ConnectionStateChanged(false,
reason,
QString(),
QString());
if (!m_discoveryTimer->isActive()) {
m_discoveryTimer->start();
}
QTimer::singleShot(0, this, &RemoteGuideClient::SendDiscovery);
}
void RemoteGuideClient::SendControlCommand(
const QString& command,
const QJsonObject& arguments)
{
if (!m_connected) {
emit CommandFinished(command,
false,
QStringLiteral("尚未连接停机引导设备"),
QJsonObject());
return;
}
if (m_requestInFlight) {
if (command != QStringLiteral("ping")) {
QTimer::singleShot(100, this,
[this, command, arguments]() {
SendControlCommand(command, arguments);
});
}
return;
}
QJsonObject request = arguments;
request["cmd"] = command;
request["protocol"] = QString::fromLatin1(kProtocol);
const QByteArray payload = QJsonDocument(request)
.toJson(QJsonDocument::Compact);
const QByteArray address = m_serverAddress.toUtf8();
const int port = m_controlPort;
const int generation = m_generation;
m_requestInFlight = true;
m_requestWatcher->setFuture(QtConcurrent::run(
[address, port, payload, command, generation]() {
ControlReply reply;
reply.command = command;
reply.generation = generation;
IVrZeroMQClient* client = nullptr;
if (!IVrZeroMQClient::CreateObject(&client) || !client) {
reply.errorMessage = QStringLiteral("创建控制客户端失败");
return reply;
}
const int initResult = client->Init(address.constData(), port);
if (initResult != 0) {
reply.errorMessage = QStringLiteral("连接控制端口失败:%1")
.arg(initResult);
client->UnInit();
delete client;
return reply;
}
char* responseData = nullptr;
size_t responseLength = 0;
const int sendResult = client->SendAndWaitBack(
payload.constData(),
payload.size(),
&responseData,
responseLength);
QByteArray response;
if (sendResult == 0 && responseData) {
response = QByteArray(responseData,
static_cast<int>(responseLength));
}
client->UnInit();
delete client;
if (sendResult != 0) {
reply.errorMessage = QStringLiteral("控制请求超时或失败:%1")
.arg(sendResult);
return reply;
}
QJsonParseError parseError;
const QJsonDocument document = QJsonDocument::fromJson(
response, &parseError);
if (parseError.error != QJsonParseError::NoError ||
!document.isObject()) {
reply.errorMessage = QStringLiteral("服务端返回了无效JSON");
return reply;
}
reply.response = document.object();
if (reply.response.value("protocol").toString() !=
QString::fromLatin1(kProtocol)) {
reply.errorMessage = QStringLiteral("服务端协议不匹配");
return reply;
}
reply.transportOk = true;
return reply;
}));
}
void RemoteGuideClient::HandleControlFinished()
{
const ControlReply reply = m_requestWatcher->result();
if (reply.generation != m_generation) {
return;
}
m_requestInFlight = false;
if (!reply.transportOk) {
if (reply.command != QStringLiteral("ping")) {
emit CommandFinished(reply.command,
false,
reply.errorMessage,
QJsonObject());
}
DisconnectServer(QStringLiteral("通信中断:%1正在重新搜索设备")
.arg(reply.errorMessage));
return;
}
if (reply.response.contains("guidanceRunning")) {
const bool running =
reply.response.value("guidanceRunning").toBool();
if (running != m_guidanceRunning) {
m_guidanceRunning = running;
emit GuidanceRunningChanged(running);
}
}
const QJsonObject status = reply.response.value("status").toObject();
if (!status.isEmpty()) {
emit StatusReceived(status);
}
const bool ok = reply.response.value("ok").toBool();
if (reply.command != QStringLiteral("ping")) {
emit CommandFinished(
reply.command,
ok,
ResponseMessage(reply.response,
ok ? QStringLiteral("操作成功")
: QStringLiteral("服务端拒绝了操作")),
reply.response);
}
if (ok && (reply.command == QStringLiteral("select_model") ||
reply.command == QStringLiteral("start") ||
reply.command == QStringLiteral("stop"))) {
QTimer::singleShot(0, this, &RemoteGuideClient::RequestStatus);
}
}
void RemoteGuideClient::HandleStatusPayload(const QByteArray& payload)
{
QJsonParseError error;
const QJsonDocument document = QJsonDocument::fromJson(payload, &error);
if (error.error != QJsonParseError::NoError || !document.isObject()) {
return;
}
const QJsonObject status = document.object();
const QString protocol = status.value("protocol").toString();
if (!protocol.isEmpty() &&
protocol != QStringLiteral("ParkingSpaceGuideResult")) {
return;
}
emit StatusReceived(status);
}
void RemoteGuideClient::SetConnectionMessage(const QString& message)
{
if (m_connected || message == m_connectionMessage) {
return;
}
m_connectionMessage = message;
const QByteArray messageText = message.toUtf8();
if (message.contains(QStringLiteral("失败")) ||
message.contains(QStringLiteral("中断"))) {
LOG_WARNING("ParkingSpaceGuideView connection state: %s\n",
messageText.constData());
} else {
LOG_INFO("ParkingSpaceGuideView connection state: %s\n",
messageText.constData());
}
emit ConnectionStateChanged(false,
message,
QString(),
QString());
}