570 lines
20 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 "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());
}