基本通讯框架构建

This commit is contained in:
2025-09-16 19:08:32 +08:00
commit 6d540f8302
19 changed files with 665 additions and 0 deletions
+25
View File
@@ -0,0 +1,25 @@
#pragma once
#include <QObject>
#include <QByteArray>
#include <functional>
class Endpoint : public QObject {
Q_OBJECT
public:
using RxHandler = std::function<void(const QByteArray&)>;
explicit Endpoint(QObject* parent = nullptr) : QObject(parent) {}
virtual ~Endpoint() = default;
virtual bool open() = 0;
virtual void close() = 0;
virtual bool isOpen() const = 0;
virtual qint64 writeBytes(const QByteArray& data) = 0;
void setRxHandler(RxHandler cb) { rx_ = std::move(cb); }
protected:
void emitRx(const QByteArray& data) { if (rx_) rx_(data); }
private:
RxHandler rx_;
};
+14
View File
@@ -0,0 +1,14 @@
#include "Injector.hpp"
Injector::Injector(Endpoint* target, uint8_t sysid, uint8_t compid, QObject* parent)
: QObject(parent), target_(target), sysid_(sysid), compid_(compid) {}
bool Injector::sendHeartbeat(uint8_t type, uint8_t autopilot, uint8_t base_mode,
uint32_t custom_mode, uint8_t system_status) {
if (!target_ || !target_->isOpen()) return false;
mavlink_message_t msg;
mavlink_msg_heartbeat_pack(sysid_, compid_, &msg, type, autopilot, base_mode, custom_mode, system_status);
uint8_t buf[MAVLINK_MAX_PACKET_LEN];
const int len = mavlink_msg_to_send_buffer(buf, &msg);
return target_->writeBytes(QByteArray(reinterpret_cast<const char*>(buf), len)) == len;
}
+19
View File
@@ -0,0 +1,19 @@
#pragma once
#include <QObject>
#include "Endpoint.hpp"
extern "C" {
#include "mavlink/common/mavlink.h"
}
class Injector : public QObject {
Q_OBJECT
public:
Injector(Endpoint* target, uint8_t sysid, uint8_t compid, QObject* parent = nullptr);
bool sendHeartbeat(uint8_t type, uint8_t autopilot, uint8_t base_mode,
uint32_t custom_mode, uint8_t system_status);
private:
Endpoint* target_;
uint8_t sysid_;
uint8_t compid_;
};
+16
View File
@@ -0,0 +1,16 @@
#include "MavlinkTap.hpp"
#include <cstring>
MavlinkTap::MavlinkTap(QObject* parent) : QObject(parent) {
std::memset(&status_, 0, sizeof(status_));
}
void MavlinkTap::feed(const QByteArray& data) {
QMutexLocker locker(&mtx_);
mavlink_message_t msg;
for (unsigned char c : data) {
if (mavlink_parse_char(MAVLINK_COMM_0, c, &msg, &status_)) {
emit messageParsed(msg);
}
}
}
+22
View File
@@ -0,0 +1,22 @@
#pragma once
#include <QObject>
#include <QByteArray>
#include <QMutex>
extern "C" {
#include "mavlink/common/mavlink.h"
}
class MavlinkTap : public QObject {
Q_OBJECT
public:
explicit MavlinkTap(QObject* parent = nullptr);
void feed(const QByteArray& data); // 喂入原始字节(任意方向)
signals:
void messageParsed(const mavlink_message_t& msg);
private:
QMutex mtx_;
mavlink_status_t status_;
};
+36
View File
@@ -0,0 +1,36 @@
#include "ProxyConnection.hpp"
#include <QDebug>
ProxyConnection::ProxyConnection(Endpoint* a, Endpoint* b, MavlinkTap* tap, QObject* parent)
: QObject(parent), A_(a), B_(b), tap_(tap) {}
bool ProxyConnection::start() {
if (!A_ || !B_) return false;
if (!A_->open() || !B_->open()) {
qWarning() << "ProxyConnection: failed to open endpoints";
return false;
}
running_ = true;
A_->setRxHandler([this](const QByteArray& data){
if (!running_) return;
if (tap_) tap_->feed(data); // 仅监控解析
auto n = B_->writeBytes(data); // 原样转发
emit bytesForwardedAB(n);
});
B_->setRxHandler([this](const QByteArray& data){
if (!running_) return;
if (tap_) tap_->feed(data);
auto n = A_->writeBytes(data);
emit bytesForwardedBA(n);
});
return true;
}
void ProxyConnection::stop() {
running_ = false;
if (A_->isOpen()) A_->close();
if (B_->isOpen()) B_->close();
}
+24
View File
@@ -0,0 +1,24 @@
#pragma once
#include <QObject>
#include "Endpoint.hpp"
#include "MavlinkTap.hpp"
class ProxyConnection : public QObject {
Q_OBJECT
public:
ProxyConnection(Endpoint* a, Endpoint* b, MavlinkTap* tap = nullptr, QObject* parent = nullptr);
~ProxyConnection() override = default;
bool start();
void stop();
signals:
void bytesForwardedAB(qint64 n);
void bytesForwardedBA(qint64 n);
private:
Endpoint* A_;
Endpoint* B_;
MavlinkTap* tap_;
bool running_ = false;
};
+117
View File
@@ -0,0 +1,117 @@
#include "ProxyService.hpp"
#include <QMetaObject>
#include <QDebug>
ProxyService::ProxyService(const QString& serialPort, qint32 baudRate, quint16 gcsListenPort, QObject* parent)
: QObject(parent),
serialPort_(serialPort),
baudRate_(baudRate),
gcsPort_(gcsListenPort)
{
// 预创建对象(仍在主线程),随后统一迁移到IO线程
serial_ = new SerialEndpoint(serialPort_, baudRate_);
udpHub_ = new UdpHubEndpoint(gcsPort_);
tap_ = new MavlinkTap;
proxy_ = new ProxyConnection(serial_, udpHub_, tap_);
injector_ = new Injector(udpHub_, /*sysid*/245, /*compid*/190);
// 迁移到 IO 线程(这里只是“归属”变更,不做 open/bind)
serial_->moveToThread(&ioThread_);
udpHub_->moveToThread(&ioThread_);
tap_->moveToThread(&ioThread_);
proxy_->moveToThread(&ioThread_);
injector_->moveToThread(&ioThread_);
// 将 IO 线程产生的信号桥接到主线程
connect(tap_, &MavlinkTap::messageParsed, this, &ProxyService::messageParsed, Qt::QueuedConnection);
connect(udpHub_, &UdpHubEndpoint::peerAdded, this, &ProxyService::peerAdded, Qt::QueuedConnection);
connect(udpHub_, &UdpHubEndpoint::peerRemoved,this,&ProxyService::peerRemoved, Qt::QueuedConnection);
connect(proxy_, &ProxyConnection::bytesForwardedAB, this, &ProxyService::bytesForwardedAB, Qt::QueuedConnection);
connect(proxy_, &ProxyConnection::bytesForwardedBA, this, &ProxyService::bytesForwardedBA, Qt::QueuedConnection);
// 线程结束时通知
connect(&ioThread_, &QThread::finished, this, [this](){
emit stopped();
});
// 线程启动时在 IO 线程执行 ioStart_
connect(&ioThread_, &QThread::started, this, &ProxyService::ioStart_, Qt::QueuedConnection);
}
ProxyService::~ProxyService() {
stop();
ioThread_.wait();
// 对象都在IO线程归属;此时线程已退出,可安全删除
delete serial_;
delete udpHub_;
delete tap_;
delete proxy_;
delete injector_;
}
void ProxyService::start() {
if (running_) return;
ioThread_.start();
}
void ProxyService::stop() {
if (!running_) return;
// 在 IO 线程里做关闭
QMetaObject::invokeMethod(this, "ioStop_", Qt::QueuedConnection);
ioThread_.quit();
}
void ProxyService::setPeerIdleTimeoutMs(int ms) {
peerIdleMs_ = ms;
if (udpHub_) {
QMetaObject::invokeMethod(udpHub_, [this](){
udpHub_->setPeerIdleTimeoutMs(peerIdleMs_);
}, Qt::QueuedConnection);
}
}
void ProxyService::addStaticPeer(const QHostAddress& addr, quint16 port) {
if (udpHub_) {
QMetaObject::invokeMethod(udpHub_, [this, addr, port](){
udpHub_->addStaticPeer(addr, port);
}, Qt::QueuedConnection);
}
}
void ProxyService::sendHeartbeat(uint8_t type, uint8_t autopilot,
uint8_t base_mode, uint32_t custom_mode,
uint8_t system_status) {
if (injector_) {
QMetaObject::invokeMethod(injector_, [=](){
injector_->sendHeartbeat(type, autopilot, base_mode, custom_mode, system_status);
}, Qt::QueuedConnection);
}
}
// ---------------- IO 线程内的实际开启/关闭 ----------------
void ProxyService::ioStart_() {
// 应用 UDP Hub 的超时配置
udpHub_->setPeerIdleTimeoutMs(peerIdleMs_);
// 在 IO 线程里启动透明转发(内部会 open 串口/绑定 UDP)
const bool ok = proxy_->start();
running_ = ok;
if (!ok) {
qCritical() << "[ProxyService] start failed (serial or udp bind open failed)";
emit started(false);
return;
}
// 可选:给所有 GCS 发送一个心跳,证明代理在线(可注释)
injector_->sendHeartbeat(6 /*MAV_TYPE_GCS*/, 8 /*MAV_AUTOPILOT_INVALID*/,
0 /*base_mode*/, 0 /*custom*/, 4 /*system_status:active*/);
emit started(true);
}
void ProxyService::ioStop_() {
if (!running_) return;
proxy_->stop(); // 内部会 close 串口与 UDP
running_ = false;
}
+72
View File
@@ -0,0 +1,72 @@
#pragma once
#include <QObject>
#include <QThread>
#include <QHostAddress>
#include <QString>
#include "SerialEndpoint.hpp"
#include "UdpHubEndpoint.hpp"
#include "ProxyConnection.hpp"
#include "MavlinkTap.hpp"
#include "Injector.hpp"
class ProxyService : public QObject {
Q_OBJECT
public:
// 构造:只在主线程调用;不做IO操作
explicit ProxyService(const QString& serialPort,
qint32 baudRate,
quint16 gcsListenPort,
QObject* parent = nullptr);
~ProxyService() override;
// 在调用线程(通常主线程)触发启动/停止;真正的开启关闭在IO线程里执行
Q_INVOKABLE void start();
Q_INVOKABLE void stop();
// 运行时控制(跨线程排队到IO线程)
Q_INVOKABLE void setPeerIdleTimeoutMs(int ms);
Q_INVOKABLE void addStaticPeer(const QHostAddress& addr, quint16 port);
// 可选:注入一个心跳(排队到IO线程执行)
Q_INVOKABLE void sendHeartbeat(uint8_t type, uint8_t autopilot,
uint8_t base_mode, uint32_t custom_mode,
uint8_t system_status);
signals:
// 生命周期
void started(bool ok);
void stopped();
// 观察数据
void messageParsed(const mavlink_message_t& msg); // 从IO线程跨到主线程(Queued
void bytesForwardedAB(qint64 n); // drone->GCS
void bytesForwardedBA(qint64 n); // GCS->drone
// GCS 动态
void peerAdded(const QHostAddress& addr, quint16 port);
void peerRemoved(const QHostAddress& addr, quint16 port);
private slots:
// 这些槽运行在 IO 线程中
void ioStart_(); // 打开端口/绑定socket/建立转发
void ioStop_(); // 停止转发/关闭设备
private:
// 仅在主线程访问这些裸指针的“拥有关系”;实际方法调用都在IO线程中
QThread ioThread_;
SerialEndpoint* serial_ = nullptr;
UdpHubEndpoint* udpHub_ = nullptr;
MavlinkTap* tap_ = nullptr;
ProxyConnection* proxy_ = nullptr;
Injector* injector_ = nullptr;
// 构造参数
const QString serialPort_;
const qint32 baudRate_;
const quint16 gcsPort_;
// 配置参数(通过Queued修改)
int peerIdleMs_ = 30'000;
bool running_ = false;
};
+36
View File
@@ -0,0 +1,36 @@
#include "SerialEndpoint.hpp"
#include <QDebug>
SerialEndpoint::SerialEndpoint(const QString& portName, qint32 baudRate, QObject* parent)
: Endpoint(parent)
{
port_.setPortName(portName);
port_.setBaudRate(baudRate);
port_.setDataBits(QSerialPort::Data8);
port_.setParity(QSerialPort::NoParity);
port_.setStopBits(QSerialPort::OneStop);
port_.setFlowControl(QSerialPort::NoFlowControl);
QObject::connect(&port_, &QSerialPort::readyRead, this, [this](){
QByteArray data = port_.readAll();
emitRx(data);
});
}
bool SerialEndpoint::open() {
if (port_.isOpen()) return true;
bool ok = port_.open(QIODevice::ReadWrite);
if (!ok) qWarning() << "SerialEndpoint: failed to open" << port_.portName();
return ok;
}
void SerialEndpoint::close() {
if (port_.isOpen()) port_.close();
}
qint64 SerialEndpoint::writeBytes(const QByteArray& data) {
if (!port_.isOpen()) return -1;
qint64 n = port_.write(data);
port_.waitForBytesWritten(200);
return n;
}
+18
View File
@@ -0,0 +1,18 @@
#pragma once
#include "Endpoint.hpp"
#include <QSerialPort>
class SerialEndpoint : public Endpoint {
Q_OBJECT
public:
SerialEndpoint(const QString& portName, qint32 baudRate, QObject* parent = nullptr);
~SerialEndpoint() override = default;
bool open() override;
void close() override;
bool isOpen() const override { return port_.isOpen(); }
qint64 writeBytes(const QByteArray& data) override;
private:
QSerialPort port_;
};
+86
View File
@@ -0,0 +1,86 @@
#include "UdpHubEndpoint.hpp"
#include <QDateTime>
#include <QDebug>
UdpHubEndpoint::UdpHubEndpoint(quint16 bindPort, QObject* parent)
: Endpoint(parent), bindPort_(bindPort)
{
QObject::connect(&gcTimer_, &QTimer::timeout, this, &UdpHubEndpoint::gcPeers_);
gcTimer_.setInterval(2000);
}
bool UdpHubEndpoint::open() {
if (sock_.state() == QAbstractSocket::BoundState) return true;
bool ok = sock_.bind(QHostAddress::AnyIPv4, bindPort_, QUdpSocket::ShareAddress);
if (!ok) {
qWarning() << "UdpHubEndpoint: bind failed on port" << bindPort_;
return false;
}
QObject::connect(&sock_, &QUdpSocket::readyRead, this, &UdpHubEndpoint::onReadyRead_);
gcTimer_.start();
return true;
}
void UdpHubEndpoint::close() {
gcTimer_.stop();
sock_.close();
peers_.clear();
lastSeenMs_.clear();
}
bool UdpHubEndpoint::isOpen() const {
return sock_.state() == QAbstractSocket::BoundState;
}
qint64 UdpHubEndpoint::writeBytes(const QByteArray& data) {
qint64 total = 0;
for (const auto& p : peers_) {
qint64 n = sock_.writeDatagram(data, p.addr, p.port);
if (n > 0) total += n;
}
return total;
}
void UdpHubEndpoint::addStaticPeer(const QHostAddress& addr, quint16 port) {
upsertPeer_(addr, port);
}
void UdpHubEndpoint::onReadyRead_() {
while (sock_.hasPendingDatagrams()) {
QByteArray buf;
buf.resize(int(sock_.pendingDatagramSize()));
QHostAddress sender; quint16 senderPort = 0;
sock_.readDatagram(buf.data(), buf.size(), &sender, &senderPort);
upsertPeer_(sender, senderPort); // 记录/刷新该 GCS
emitRx(buf); // 把字节给上层转发到串口
}
}
void UdpHubEndpoint::gcPeers_() {
const qint64 now = QDateTime::currentMSecsSinceEpoch();
QList<UdpPeer> expired;
for (auto it = lastSeenMs_.cbegin(); it != lastSeenMs_.cend(); ++it) {
if (now - it.value() > idleMs_) expired.push_back(it.key());
}
for (const auto& p : expired) {
peers_.remove(p);
lastSeenMs_.remove(p);
emit peerRemoved(p.addr, p.port);
}
}
void UdpHubEndpoint::upsertPeer_(const QHostAddress& a, quint16 p) {
UdpPeer peer{a, p};
bool inserted = false;
if (!peers_.contains(peer)) {
peers_.insert(peer); // 返回迭代器,但我们不用
inserted = true;
}
lastSeenMs_[peer] = QDateTime::currentMSecsSinceEpoch();
if (inserted) {
emit peerAdded(a, p);
}
}
+51
View File
@@ -0,0 +1,51 @@
#pragma once
#include "Endpoint.hpp"
#include <QUdpSocket>
#include <QHostAddress>
#include <QSet>
#include <QMap>
#include <QTimer>
struct UdpPeer {
QHostAddress addr;
quint16 port;
bool operator==(const UdpPeer& o) const { return addr == o.addr && port == o.port; }
};
inline uint qHash(const UdpPeer& p, uint seed = 0) {
return qHash(p.addr.toString(), seed) ^ qHash(p.port, seed);
}
class UdpHubEndpoint : public Endpoint {
Q_OBJECT
public:
explicit UdpHubEndpoint(quint16 bindPort, QObject* parent = nullptr);
bool open() override;
void close() override;
bool isOpen() const override;
// 广播:把 data 下发给所有已知 GCS peers
qint64 writeBytes(const QByteArray& data) override;
// 可选:预置白名单
void addStaticPeer(const QHostAddress& addr, quint16 port);
void setPeerIdleTimeoutMs(int ms) { idleMs_ = ms; }
signals:
void peerAdded(const QHostAddress& addr, quint16 port);
void peerRemoved(const QHostAddress& addr, quint16 port);
private slots:
void onReadyRead_();
void gcPeers_();
private:
void upsertPeer_(const QHostAddress& a, quint16 p);
QUdpSocket sock_;
quint16 bindPort_;
QSet<UdpPeer> peers_;
QMap<UdpPeer, qint64> lastSeenMs_;
QTimer gcTimer_;
int idleMs_ = 30'000; // 30s 无流量剔除
};
+11
View File
@@ -0,0 +1,11 @@
#include "mainwindow.h"
#include <QApplication>
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
MainWindow w;
w.show();
return a.exec();
}
+14
View File
@@ -0,0 +1,14 @@
#include "mainwindow.h"
#include "ui_mainwindow.h"
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
, ui(new Ui::MainWindow)
{
ui->setupUi(this);
}
MainWindow::~MainWindow()
{
delete ui;
}
+23
View File
@@ -0,0 +1,23 @@
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include <QMainWindow>
QT_BEGIN_NAMESPACE
namespace Ui {
class MainWindow;
}
QT_END_NAMESPACE
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
MainWindow(QWidget *parent = nullptr);
~MainWindow();
private:
Ui::MainWindow *ui;
};
#endif // MAINWINDOW_H
+31
View File
@@ -0,0 +1,31 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>MainWindow</class>
<widget class="QMainWindow" name="MainWindow">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>800</width>
<height>600</height>
</rect>
</property>
<property name="windowTitle">
<string>MainWindow</string>
</property>
<widget class="QWidget" name="centralwidget"/>
<widget class="QMenuBar" name="menubar">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>800</width>
<height>21</height>
</rect>
</property>
</widget>
<widget class="QStatusBar" name="statusbar"/>
</widget>
<resources/>
<connections/>
</ui>