Casper 1 week ago
parent
commit
e1ad87911a
46 changed files with 4223 additions and 0 deletions
  1. 17 0
      yancheng-bi/3rdparty/opencv/vcpkg.json
  2. 15 0
      yancheng-bi/3rdparty/qt6/vcpkg.json
  3. 9 0
      yancheng-bi/3rdparty/zmq/vcpkg.json
  4. 142 0
      yancheng-bi/CMakeLists.txt
  5. 48 0
      yancheng-bi/CMakeSettings.json
  6. 4 0
      yancheng-bi/Config.json
  7. 33 0
      yancheng-bi/README-usage.ps1
  8. 6 0
      yancheng-bi/src/Config.json
  9. 101 0
      yancheng-bi/src/UdpVideoSub.cpp
  10. 37 0
      yancheng-bi/src/UdpVideoSub.h
  11. 11 0
      yancheng-bi/src/main.cpp
  12. 147 0
      yancheng-bi/src/widget.cpp
  13. 46 0
      yancheng-bi/src/widget.h
  14. 512 0
      yancheng-bi/src/widget.ui
  15. 17 0
      yancheng-client/3rdparty/opencv/vcpkg.json
  16. 15 0
      yancheng-client/3rdparty/qt6/vcpkg.json
  17. 9 0
      yancheng-client/3rdparty/zmq/vcpkg.json
  18. 142 0
      yancheng-client/CMakeLists.txt
  19. 48 0
      yancheng-client/CMakeSettings.json
  20. 5 0
      yancheng-client/Config.json
  21. 1 0
      yancheng-client/README-q&a.txt
  22. 11 0
      yancheng-client/README-usage-release.ps1
  23. 33 0
      yancheng-client/README-usage.ps1
  24. 101 0
      yancheng-client/src/UdpVideoSub.cpp
  25. 37 0
      yancheng-client/src/UdpVideoSub.h
  26. 119 0
      yancheng-client/src/ZmqImageSubscriber.cpp
  27. 39 0
      yancheng-client/src/ZmqImageSubscriber.h
  28. 11 0
      yancheng-client/src/main.cpp
  29. 339 0
      yancheng-client/src/widget.cpp
  30. 75 0
      yancheng-client/src/widget.h
  31. 789 0
      yancheng-client/src/widget.ui
  32. 31 0
      yancheng-edge/CMakeLists.txt
  33. 66 0
      yancheng-edge/README-usage-for-docker.bash
  34. 11 0
      yancheng-edge/README-usage-for-podman.bash
  35. 50 0
      yancheng-edge/README-usage-for-service.bash
  36. 37 0
      yancheng-edge/config-10.10.10.24.json
  37. 37 0
      yancheng-edge/config-10.10.10.29.json
  38. 37 0
      yancheng-edge/config-release.json
  39. 22 0
      yancheng-edge/docker/Dockerfile
  40. 38 0
      yancheng-edge/docker/compose.yml
  41. 22 0
      yancheng-edge/podman/Dockerfile
  42. 22 0
      yancheng-edge/podman/compose.yml
  43. 6 0
      yancheng-edge/run.sh
  44. 59 0
      yancheng-edge/src/SimpleConfig.cpp
  45. 64 0
      yancheng-edge/src/SimpleConfig.h
  46. 802 0
      yancheng-edge/src/main.cpp

+ 17 - 0
yancheng-bi/3rdparty/opencv/vcpkg.json

@@ -0,0 +1,17 @@
+{
+    "builtin-baseline": "a94dafabcabe2beb28289b39717111ad3d462327",
+    "dependencies": [
+        "quirc",
+        "opencv4"
+    ],
+    "overrides": [
+        {
+            "name": "quirc",
+            "version": "1.2"
+        },
+        {
+            "name": "opencv4",
+            "version": "4.8.0"
+        }
+    ]
+}

+ 15 - 0
yancheng-bi/3rdparty/qt6/vcpkg.json

@@ -0,0 +1,15 @@
+{
+  "builtin-baseline": "c591ac6466a55ef0a05a3d56bb1489ca36e50102",
+  "dependencies": [
+    "qtbase",
+    "qttools",
+    "qtmultimedia",
+    "qttranslations"
+  ],
+  "overrides": [
+    { "name": "qtbase", "version": "6.6.3" },
+    { "name": "qttools", "version": "6.6.3" },
+    { "name": "qtmultimedia", "version": "6.6.3" },
+    { "name": "qttranslations", "version": "6.6.3" }
+  ]
+}

+ 9 - 0
yancheng-bi/3rdparty/zmq/vcpkg.json

@@ -0,0 +1,9 @@
+{
+  "builtin-baseline": "c591ac6466a55ef0a05a3d56bb1489ca36e50102",
+  "dependencies": [
+    "cppzmq"
+  ],
+  "overrides": [
+    { "name": "cppzmq", "version": "4.10.0" }
+  ]
+}

+ 142 - 0
yancheng-bi/CMakeLists.txt

@@ -0,0 +1,142 @@
+# 设置兼容最低版本
+cmake_minimum_required(VERSION 3.20)
+
+# 定义项目名称变量
+set(PROJECT_NAME TEST001)
+project(${PROJECT_NAME} LANGUAGES CXX)
+
+# 设置C++标准
+set(CMAKE_CXX_STANDARD 17)
+set(CMAKE_CXX_STANDARD_REQUIRED ON)
+set(CMAKE_CXX_EXTENSIONS OFF)
+
+# 启用 Qt 的自动处理
+set(CMAKE_AUTOUIC ON)
+set(CMAKE_AUTOMOC ON)
+set(CMAKE_AUTORCC ON)
+
+# --- for vs2022
+set(CMAKE_RUNTIME_OUTPUT_DIRECTORY_DEBUG ${CMAKE_CURRENT_SOURCE_DIR}/bin)
+set(CMAKE_RUNTIME_OUTPUT_DIRECTORY_RELEASE ${CMAKE_CURRENT_SOURCE_DIR}/bin)
+
+# --- protobuf for opencv
+set(Protobuf_DIR "${CMAKE_CURRENT_SOURCE_DIR}/3rdparty/opencv/vcpkg_installed/x64-windows/share/protobuf")
+set(Protobuf_INCLUDE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/3rdparty/opencv/vcpkg_installed/x64-windows/include")
+set(Protobuf_LIBRARIES "${CMAKE_CURRENT_SOURCE_DIR}/3rdparty/opencv/vcpkg_installed/x64-windows/lib/libprotobuf.lib")
+
+# --- tiff for opencv
+set(TIFF_INCLUDE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/3rdparty/opencv/vcpkg_installed/x64-windows/include")
+set(TIFF_LIBRARY "${CMAKE_CURRENT_SOURCE_DIR}/3rdparty/opencv/vcpkg_installed/x64-windows/lib/tiff.lib")
+
+# --- quirc for opencv
+set(quirc_DIR "${CMAKE_CURRENT_SOURCE_DIR}/3rdparty/opencv/vcpkg_installed/x64-windows/share/quirc")
+
+# --- opencv
+set(OpenCV_DIR "${CMAKE_CURRENT_SOURCE_DIR}/3rdparty/opencv/vcpkg_installed/x64-windows/share/opencv4")
+find_package(OpenCV REQUIRED)
+
+# --- qt
+set(Qt6_DIR "${CMAKE_CURRENT_SOURCE_DIR}/3rdparty/qt6/vcpkg_installed/x64-windows/share/Qt6")
+# find_package(Qt6 REQUIRED COMPONENTS Core Gui Widgets)
+find_package(Qt6 REQUIRED COMPONENTS Core Gui Widgets Multimedia MultimediaWidgets)
+
+# --- zeromq
+set(ZeroMQ_DIR "${CMAKE_CURRENT_SOURCE_DIR}/3rdparty/zmq/vcpkg_installed/x64-windows/share/zeromq")
+set(ZeroMQ_INCLUDE_DIRS "${CMAKE_CURRENT_SOURCE_DIR}/3rdparty/zmq/vcpkg_installed/x64-windows/include")
+set(ZeroMQ_LIBRARIES "${CMAKE_CURRENT_SOURCE_DIR}/3rdparty/zmq/vcpkg_installed/x64-windows/lib/libzmq-mt-4_3_5.lib")
+find_package(ZeroMQ REQUIRED)
+
+# 设置源文件和头文件
+file(GLOB SOURCES src/*.cpp)
+file(GLOB HEADERS src/*.h)
+file(GLOB UIS src/*.ui)
+
+# 添加可执行文件
+add_executable(${PROJECT_NAME}
+        ${SOURCES}
+        ${HEADERS}
+        ${UIS}
+)
+
+# 如果是 Windows 平台,则设置为 GUI 应用(无控制台)
+if(WIN32)
+    set_target_properties(${PROJECT_NAME} PROPERTIES
+            WIN32_EXECUTABLE ON
+            OUTPUT_NAME "测试工具"
+    )
+endif()
+
+# --- 包含头文件目录
+target_include_directories(${PROJECT_NAME} PRIVATE
+        ${ZeroMQ_INCLUDE_DIRS}
+)
+
+# --- 链接
+target_link_libraries(${PROJECT_NAME} PRIVATE
+        Qt6::Core
+        Qt6::Gui
+        Qt6::Widgets
+        Qt6::Multimedia
+        Qt6::MultimediaWidgets
+        ${ZeroMQ_LIBRARIES}
+        ${OpenCV_LIBS}
+        ${Protobuf_LIBRARIES}
+        ${TIFF_LIBRARY}
+        ws2_32  # 添加 Winsock 库
+)
+
+# --- for qt 获取 Qt 插件目录路
+set(QT_CORE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/3rdparty/qt6/vcpkg_installed/x64-windows/bin/Qt6Core.dll")
+set(QT_BASE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/3rdparty/qt6/vcpkg_installed")
+
+# --- 根据构建类型设置插件目录
+if(CMAKE_BUILD_TYPE STREQUAL "Debug")
+    set(CV_BIN_DIR "${CMAKE_CURRENT_SOURCE_DIR}/3rdparty/opencv/vcpkg_installed/x64-windows/debug/bin")
+    set(QT_BIN_DIR "${QT_BASE_DIR}/x64-windows/debug/bin")
+    set(QT_PLUGINS_DIR "${QT_BASE_DIR}/x64-windows/debug/Qt6/plugins/platforms")
+    set(QT_MULTIMEDIA_PLUGINS_DIR "${QT_BASE_DIR}/x64-windows/debug/Qt6/plugins/multimedia")
+else()
+    set(CV_BIN_DIR "${CMAKE_CURRENT_SOURCE_DIR}/3rdparty/opencv/vcpkg_installed/x64-windows/bin")
+    set(QT_BIN_DIR "${QT_BASE_DIR}/x64-windows/bin")
+    set(QT_PLUGINS_DIR "${QT_BASE_DIR}/x64-windows/Qt6/plugins/platforms")
+    set(QT_MULTIMEDIA_PLUGINS_DIR "${QT_BASE_DIR}/x64-windows/Qt6/plugins/multimedia")
+endif()
+
+# --- 打包 qt
+file(GLOB QT_DLLS "${QT_BIN_DIR}/*.dll")
+file(GLOB QT_PLUGINS_DLLS "${QT_PLUGINS_DIR}/*.dll")
+file(GLOB QT_MULTIMEDIA_PLUGINS "${QT_MULTIMEDIA_PLUGINS_DIR}/*.dll")
+add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD
+
+        # 复制所有 DLL 文件到目标目录
+        COMMAND ${CMAKE_COMMAND} -E copy_if_different
+        ${QT_DLLS}
+        "$<TARGET_FILE_DIR:${PROJECT_NAME}>/"
+
+        # 创建 platforms 目录并复制文件
+        COMMAND ${CMAKE_COMMAND} -E make_directory "$<TARGET_FILE_DIR:${PROJECT_NAME}>/platforms"
+        COMMAND ${CMAKE_COMMAND} -E copy_if_different
+        ${QT_PLUGINS_DLLS}
+        "$<TARGET_FILE_DIR:${PROJECT_NAME}>/platforms/"
+
+        # 创建 multimedia 目录并复制文件
+        COMMAND ${CMAKE_COMMAND} -E make_directory "$<TARGET_FILE_DIR:${PROJECT_NAME}>/multimedia"
+        COMMAND ${CMAKE_COMMAND} -E copy_if_different
+        ${QT_MULTIMEDIA_PLUGINS}
+        "$<TARGET_FILE_DIR:${PROJECT_NAME}>/multimedia/"
+)
+
+# --- 打包 opencv
+file(GLOB CV_DLLS "${CV_BIN_DIR}/*.dll")
+add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD
+        COMMAND ${CMAKE_COMMAND} -E copy_if_different
+        ${CV_DLLS}
+        "$<TARGET_FILE_DIR:${PROJECT_NAME}>/"
+)
+
+# --- 打包 zeromq
+add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD
+        COMMAND ${CMAKE_COMMAND} -E copy_if_different
+        "${CMAKE_CURRENT_SOURCE_DIR}/3rdparty/zmq/vcpkg_installed/x64-windows/bin/libzmq-mt-4_3_5.dll"
+        "$<TARGET_FILE_DIR:${PROJECT_NAME}>/"
+)

+ 48 - 0
yancheng-bi/CMakeSettings.json

@@ -0,0 +1,48 @@
+{
+  "configurations": [
+    {
+      "name": "x64-Debug",
+      "generator": "Ninja",
+      "configurationType": "Debug",
+      "buildRoot": "${projectDir}/build/${name}",
+      "installRoot": "${projectDir}/install/${name}",
+      "inheritEnvironments": ["msvc_x64_x64"],
+      "cmakeCommandArgs": "",
+      "buildCommandArgs": "",
+      "variables": [
+        {
+          "name": "CMAKE_CXX_COMPILER",
+          "value": "C:/Program Files/Microsoft Visual Studio/2022/Community/VC/Tools/MSVC/14.44.35207/bin/Hostx64/x64/cl.exe",
+          "type": "FILEPATH"
+        },
+        {
+          "name": "CMAKE_CXX_FLAGS",
+          "value": "/utf-8",
+          "type": "STRING"
+        }
+      ]
+    },
+    {
+      "name": "x64-Release",
+      "generator": "Ninja",
+      "configurationType": "Release",
+      "buildRoot": "${projectDir}/build/${name}",
+      "installRoot": "${projectDir}/install/${name}",
+      "inheritEnvironments": ["msvc_x64_x64"],
+      "cmakeCommandArgs": "",
+      "buildCommandArgs": "",
+      "variables": [
+        {
+          "name": "CMAKE_CXX_COMPILER",
+          "value": "C:/Program Files/Microsoft Visual Studio/2022/Community/VC/Tools/MSVC/14.44.35207/bin/Hostx64/x64/cl.exe",
+          "type": "FILEPATH"
+        },
+        {
+          "name": "CMAKE_CXX_FLAGS",
+          "value": "/utf-8",
+          "type": "STRING"
+        }
+      ]
+    }
+  ]
+}

+ 4 - 0
yancheng-bi/Config.json

@@ -0,0 +1,4 @@
+{
+    "Udp_port1": 20001,
+    "Zmq_ip1": "tcp://192.168.30.101:5557"
+}

+ 33 - 0
yancheng-bi/README-usage.ps1

@@ -0,0 +1,33 @@
+## PowerShell7 on win10/win11
+
+# --- 安装 choco | run as administrator
+Set-ExecutionPolicy Bypass -Scope Process -Force `
+&& Invoke-Expression ((New-Object System.Net.WebClient).DownloadString('https://chocolatey.org/install.ps1'))
+
+# --- 安装 cmake | run as administrator
+choco install cmake --version=3.30.5 -y
+
+# --- 安装 IDE C:\Program Files\Microsoft Visual Studio\2022\Community\Common7\IDE\devenv.exe
+choco install visualstudio2022community -y --package-parameters "--add Microsoft.VisualStudio.Component.VC.Tools.x86.x64"
+choco install visualstudio2022-workload-nativedesktop -y
+
+# --- 安装 SDK
+winget install --id Microsoft.WindowsSDK.10.0.19041 --version 10.0.19041.685
+
+# --- 安装 IDE C:\tools\qtcreator\bin\qtcreator.exe
+choco install qtcreator --version=14.0.1 -y
+
+# --- 安装 vcpkg
+Set-Location "E:\casper\repositories\repositories\sri-project.yancheng.master\yancheng-client"
+git clone -c http.proxy="http://127.0.0.1:7890" https://github.com/microsoft/vcpkg.git
+.\vcpkg\bootstrap-vcpkg.bat
+
+# --- 安装依赖
+subst Z: "E:\casper\repositories\repositories\sri-project.yancheng.master"
+Set-Location "Z:\yancheng-client"
+.\vcpkg\vcpkg.exe install --x-manifest-root=".\3rdparty\qt6"
+.\vcpkg\vcpkg.exe install --x-manifest-root=".\3rdparty\opencv"
+.\vcpkg\vcpkg.exe install --x-manifest-root=".\3rdparty\zmq"
+subst Z: /D
+
+

+ 6 - 0
yancheng-bi/src/Config.json

@@ -0,0 +1,6 @@
+{
+   "Udp_port1":20001,
+   "Udp_port2":20001,
+   "Udp_port3":20001,
+   "Udp_port4":20001,
+}

+ 101 - 0
yancheng-bi/src/UdpVideoSub.cpp

@@ -0,0 +1,101 @@
+#include "UdpVideoSub.h"
+#include <opencv2/opencv.hpp>
+#include<QImage>
+
+//#pragma comment(lib, "ws2_32.lib")
+
+UdpVideoSub::UdpVideoSub(int port):port_(port),running(true)
+{
+    // 初始化Winsock
+    if (WSAStartup(MAKEWORD(2, 2), &wsaData_) != 0)
+    {
+        throw std::runtime_error("WSAStartup failed");
+    }
+
+    // 创建UDP套接字
+    sock_ = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
+    if (sock_ == INVALID_SOCKET)
+    {
+        WSACleanup();
+        throw std::runtime_error("socket creation failed");
+    }
+
+    // 配置服务器地址
+    serverAddr_.sin_family = AF_INET;
+    serverAddr_.sin_port = htons(port_);
+    serverAddr_.sin_addr.s_addr = htonl(INADDR_ANY);
+
+    // 绑定套接字
+    if (bind(sock_, (sockaddr*)&serverAddr_, sizeof(serverAddr_)) == SOCKET_ERROR)
+    {
+        int err = WSAGetLastError();
+        std::cerr << "Bind failed with error: " << err << std::endl;
+        closesocket(sock_);
+        WSACleanup();
+        throw std::runtime_error("bind failed");
+    }
+
+    // 设置接收缓冲区大小
+    int recvBufSize = 65536;
+    if (setsockopt(sock_, SOL_SOCKET, SO_RCVBUF, (char*)&recvBufSize, sizeof(recvBufSize)) == SOCKET_ERROR)
+    {
+        std::cerr << "setsockopt failed: " << WSAGetLastError() << "\n";
+    }
+}
+
+UdpVideoSub::~UdpVideoSub()
+{
+    Stop();
+    if (sock_ != INVALID_SOCKET)
+    {
+        closesocket(sock_);
+    }
+    WSACleanup();
+}
+
+void UdpVideoSub::run()
+{
+    std::vector<uchar> buffer_(65536);
+
+    running=true;
+    while (running)
+    {
+        sockaddr_in clientAddr;
+        int clientAddrSize = sizeof(clientAddr);
+        int bytesReceived = recvfrom(sock_, reinterpret_cast<char*>(buffer_.data()),buffer_.size(), 0, (sockaddr*)&clientAddr, &clientAddrSize);
+
+        if (bytesReceived == SOCKET_ERROR)
+        {
+            std::cerr << "recvfrom failed: " << WSAGetLastError() << "\n";
+            continue;
+        }
+
+        cv::Mat frame = cv::imdecode(cv::Mat(1, bytesReceived, CV_8UC1, buffer_.data()), cv::IMREAD_COLOR);
+        if (!frame.empty())
+        {
+            std::lock_guard<std::mutex> lock(gps_mutex);
+            if (!latest_gps_data.empty())
+            {
+                cv::putText(frame, latest_gps_data, cv::Point(10, 30),
+                            cv::FONT_HERSHEY_SIMPLEX, 0.7, cv::Scalar(0, 255, 0), 2);
+            }
+
+            cv::cvtColor(frame, frame, cv::COLOR_BGR2RGB);
+            QImage qimg(frame.data, frame.cols, frame.rows,
+                        frame.step, QImage::Format_RGB888);
+            emit imageReceived(qimg.copy());
+        }
+    }
+}
+
+void UdpVideoSub::Stop()
+{
+    running = false;
+    this->wait();
+}
+
+void UdpVideoSub::UpdateGpsData(const std::string& gps_data)
+{
+    std::lock_guard<std::mutex> lock(gps_mutex);
+    latest_gps_data = gps_data;
+}

+ 37 - 0
yancheng-bi/src/UdpVideoSub.h

@@ -0,0 +1,37 @@
+#ifndef UDPVIDEOSUB_H
+#define UDPVIDEOSUB_H
+
+#include <QObject>
+#include<QThread>
+#include <winsock2.h>
+#include <ws2tcpip.h>
+
+class UdpVideoSub:public QThread
+{
+    Q_OBJECT
+public:
+    UdpVideoSub(int port);
+    ~UdpVideoSub();
+
+    void Stop();
+    void UpdateGpsData(const std::string& gps_data);
+
+signals:
+    void imageReceived(const QImage& image);
+
+protected:
+    void run();
+
+private:
+    int port_;
+
+    WSADATA wsaData_;
+    SOCKET sock_ = INVALID_SOCKET;
+    sockaddr_in serverAddr_{};
+
+    std::mutex gps_mutex;
+    std::string latest_gps_data;
+    bool running;
+};
+
+#endif // UDPVIDEOSUB_H

+ 11 - 0
yancheng-bi/src/main.cpp

@@ -0,0 +1,11 @@
+#include "widget.h"
+
+#include <QApplication>
+
+int main(int argc, char *argv[])
+{
+    QApplication a(argc, argv);
+    Widget w;
+    w.show();
+    return a.exec();
+}

+ 147 - 0
yancheng-bi/src/widget.cpp

@@ -0,0 +1,147 @@
+#include "widget.h"
+#include "ui_widget.h"
+#include <QFile>
+#include <QJsonObject>
+#include <QJsonDocument>
+
+Widget::Widget(QWidget *parent)
+    : QWidget(parent)
+    , ui(new Ui::Widget)
+{
+    ui->setupUi(this);
+
+    setWindowFlags(Qt::FramelessWindowHint);  //设置窗口为无边框状态
+    showFullScreen();    //设置窗口为全屏模式
+
+    readJsonFile();
+
+    m_udpvideo1=new UdpVideoSub(m_udpPort1);
+    m_udpvideo2=new UdpVideoSub(m_udpPort2);
+    m_udpvideo3=new UdpVideoSub(m_udpPort3);
+    m_udpvideo4=new UdpVideoSub(m_udpPort4);
+
+
+    connect(m_udpvideo1, &UdpVideoSub::imageReceived,this, &Widget::displayImage);
+    connect(m_udpvideo2, &UdpVideoSub::imageReceived,this, &Widget::displayImage);
+    connect(m_udpvideo3, &UdpVideoSub::imageReceived,this, &Widget::displayImage);
+    connect(m_udpvideo4, &UdpVideoSub::imageReceived,this, &Widget::displayImage);
+
+    setStyleSheet("QPushButton {border:1px solid black;color:rgb(255,255,255);border-radius:5px}");
+
+}
+
+Widget::~Widget()
+{
+    delete ui;
+}
+
+void Widget::readJsonFile()
+{
+    //打开文件
+    QFile file("./Config.json");
+    if(!file.open(QIODevice::ReadOnly))
+    {
+        qDebug() << "Failed to open file";
+    }
+
+    //读取全部内容
+    QByteArray jsonData = file.readAll();
+    file.close();
+
+    //解析JSON
+    QJsonParseError parseError;
+    QJsonDocument doc = QJsonDocument::fromJson(jsonData, &parseError);
+    if(parseError.error != QJsonParseError::NoError)
+    {
+        qDebug() << "JSON parse error:" << parseError.errorString();
+    }
+
+    //提取数据
+    if(doc.isObject())
+    {
+        QJsonObject rootObj = doc.object();
+        m_udpPort1 = rootObj["Udp_port1"].toInt();
+        m_udpPort2 = rootObj["Udp_port2"].toInt();
+        m_udpPort3 = rootObj["Udp_port3"].toInt();
+        m_udpPort4 = rootObj["Udp_port4"].toInt();
+    }
+}
+
+void Widget::displayImage(const QImage& image)
+{
+    ui->lab_video->setPixmap(QPixmap::fromImage(image).scaled(ui->lab_video->size(), Qt::KeepAspectRatio, Qt::SmoothTransformation));
+}
+
+void Widget::on_btn_road_clicked()
+{
+
+    ui->btn_road->setStyleSheet("background-color: gray;");
+    ui->btn_road->setEnabled(false);
+
+    ui->btn_gps->setStyleSheet("QPushButton {background-color:rgb(85,170,127);}" "QPushButton:hover {background-color: rgb(0,170,0);}");
+    ui->btn_obstacle->setStyleSheet("QPushButton {background-color:rgb(85,170,127);}" "QPushButton:hover {background-color: rgb(0,170,0);}");
+    ui->btn_move->setStyleSheet("QPushButton {background-color:rgb(85,170,127);}" "QPushButton:hover {background-color: rgb(0,170,0);}");
+    ui->btn_gps->setEnabled(true);
+    ui->btn_obstacle->setEnabled(true);
+    ui->btn_move->setEnabled(true);
+
+    //m_udpvideo2->Stop();
+    //m_udpvideo3->Stop();
+    //m_udpvideo4->Stop();
+    //m_udpvideo1->start();
+}
+
+void Widget::on_btn_gps_clicked()
+{
+    ui->btn_gps->setStyleSheet("background-color: gray;");
+    ui->btn_gps->setEnabled(false);
+
+    ui->btn_road->setStyleSheet("QPushButton {background-color:rgb(85,170,127);}" "QPushButton:hover {background-color: rgb(0,170,0);}");
+    ui->btn_obstacle->setStyleSheet("QPushButton {background-color:rgb(85,170,127);}" "QPushButton:hover {background-color: rgb(0,170,0);}");
+    ui->btn_move->setStyleSheet("QPushButton {background-color:rgb(85,170,127);}" "QPushButton:hover {background-color: rgb(0,170,0);}");
+    ui->btn_road->setEnabled(true);
+    ui->btn_obstacle->setEnabled(true);
+    ui->btn_move->setEnabled(true);
+
+    //m_udpvideo1->Stop();
+    //m_udpvideo3->Stop();
+    //m_udpvideo4->Stop();
+    //m_udpvideo2->start();
+}
+
+void Widget::on_btn_obstacle_clicked()
+{
+    ui->btn_obstacle->setStyleSheet("background-color: gray;");
+    ui->btn_obstacle->setEnabled(false);
+
+    ui->btn_road->setStyleSheet("QPushButton {background-color:rgb(85,170,127);}" "QPushButton:hover {background-color: rgb(0,170,0);}");
+    ui->btn_gps->setStyleSheet("QPushButton {background-color:rgb(85,170,127);}" "QPushButton:hover {background-color: rgb(0,170,0);}");
+    ui->btn_move->setStyleSheet("QPushButton {background-color:rgb(85,170,127);}" "QPushButton:hover {background-color: rgb(0,170,0);}");
+    ui->btn_road->setEnabled(true);
+    ui->btn_gps->setEnabled(true);
+    ui->btn_move->setEnabled(true);
+
+    // m_udpvideo1->Stop();
+    // m_udpvideo2->Stop();
+    // m_udpvideo4->Stop();
+    // m_udpvideo3->start();
+}
+
+void Widget::on_btn_move_clicked()
+{
+    ui->btn_move->setStyleSheet("background-color: gray;");
+    ui->btn_move->setEnabled(false);
+
+    ui->btn_road->setStyleSheet("QPushButton {background-color:rgb(85,170,127);}" "QPushButton:hover {background-color: rgb(0,170,0);}");
+    ui->btn_gps->setStyleSheet("QPushButton {background-color:rgb(85,170,127);}" "QPushButton:hover {background-color: rgb(0,170,0);}");
+    ui->btn_obstacle->setStyleSheet("QPushButton {background-color:rgb(85,170,127);}" "QPushButton:hover {background-color: rgb(0,170,0);}");
+    ui->btn_road->setEnabled(true);
+    ui->btn_gps->setEnabled(true);
+    ui->btn_obstacle->setEnabled(true);
+
+    // m_udpvideo1->Stop();
+    // m_udpvideo2->Stop();
+    // m_udpvideo3->Stop();
+    // m_udpvideo4->start();
+}
+

+ 46 - 0
yancheng-bi/src/widget.h

@@ -0,0 +1,46 @@
+#ifndef WIDGET_H
+#define WIDGET_H
+
+#include <QWidget>
+#include"UdpVideoSub.h"
+#include <QDir>
+
+QT_BEGIN_NAMESPACE
+namespace Ui {
+class Widget;
+}
+QT_END_NAMESPACE
+
+class Widget : public QWidget
+{
+    Q_OBJECT
+
+public:
+    Widget(QWidget *parent = nullptr);
+    ~Widget();
+
+    void readJsonFile();
+
+private slots:
+    void displayImage(const QImage& image);
+
+    void on_btn_road_clicked();
+    void on_btn_gps_clicked();
+    void on_btn_obstacle_clicked();
+    void on_btn_move_clicked();
+
+private:
+    Ui::Widget *ui;
+
+    UdpVideoSub* m_udpvideo1;
+    UdpVideoSub* m_udpvideo2;
+    UdpVideoSub* m_udpvideo3;
+    UdpVideoSub* m_udpvideo4;
+
+    int m_udpPort1;
+    int m_udpPort2;
+    int m_udpPort3;
+    int m_udpPort4;
+
+};
+#endif // WIDGET_H

+ 512 - 0
yancheng-bi/src/widget.ui

@@ -0,0 +1,512 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>Widget</class>
+ <widget class="QWidget" name="Widget">
+  <property name="geometry">
+   <rect>
+    <x>0</x>
+    <y>0</y>
+    <width>1920</width>
+    <height>1080</height>
+   </rect>
+  </property>
+  <property name="minimumSize">
+   <size>
+    <width>1920</width>
+    <height>1080</height>
+   </size>
+  </property>
+  <property name="maximumSize">
+   <size>
+    <width>1920</width>
+    <height>1080</height>
+   </size>
+  </property>
+  <property name="windowTitle">
+   <string>Widget</string>
+  </property>
+  <property name="styleSheet">
+   <string notr="true"/>
+  </property>
+  <layout class="QVBoxLayout" name="verticalLayout_4">
+   <property name="spacing">
+    <number>0</number>
+   </property>
+   <property name="leftMargin">
+    <number>0</number>
+   </property>
+   <property name="topMargin">
+    <number>0</number>
+   </property>
+   <property name="rightMargin">
+    <number>0</number>
+   </property>
+   <property name="bottomMargin">
+    <number>0</number>
+   </property>
+   <item>
+    <widget class="QFrame" name="frame">
+     <property name="minimumSize">
+      <size>
+       <width>1920</width>
+       <height>1080</height>
+      </size>
+     </property>
+     <property name="maximumSize">
+      <size>
+       <width>1920</width>
+       <height>1080</height>
+      </size>
+     </property>
+     <property name="styleSheet">
+      <string notr="true">QFrame#frame{
+	background-color: rgb(0, 51, 152);	
+}</string>
+     </property>
+     <property name="frameShape">
+      <enum>QFrame::StyledPanel</enum>
+     </property>
+     <property name="frameShadow">
+      <enum>QFrame::Raised</enum>
+     </property>
+     <layout class="QVBoxLayout" name="verticalLayout_5">
+      <property name="spacing">
+       <number>4</number>
+      </property>
+      <property name="leftMargin">
+       <number>6</number>
+      </property>
+      <property name="topMargin">
+       <number>6</number>
+      </property>
+      <property name="rightMargin">
+       <number>6</number>
+      </property>
+      <property name="bottomMargin">
+       <number>6</number>
+      </property>
+      <item>
+       <spacer name="verticalSpacer_4">
+        <property name="orientation">
+         <enum>Qt::Vertical</enum>
+        </property>
+        <property name="sizeHint" stdset="0">
+         <size>
+          <width>20</width>
+          <height>40</height>
+         </size>
+        </property>
+       </spacer>
+      </item>
+      <item>
+       <layout class="QHBoxLayout" name="horizontalLayout_3">
+        <property name="spacing">
+         <number>4</number>
+        </property>
+        <property name="leftMargin">
+         <number>4</number>
+        </property>
+        <property name="topMargin">
+         <number>4</number>
+        </property>
+        <property name="rightMargin">
+         <number>4</number>
+        </property>
+        <property name="bottomMargin">
+         <number>4</number>
+        </property>
+        <item>
+         <spacer name="horizontalSpacer_2">
+          <property name="orientation">
+           <enum>Qt::Horizontal</enum>
+          </property>
+          <property name="sizeHint" stdset="0">
+           <size>
+            <width>40</width>
+            <height>20</height>
+           </size>
+          </property>
+         </spacer>
+        </item>
+        <item>
+         <widget class="QLabel" name="label_9">
+          <property name="font">
+           <font>
+            <pointsize>50</pointsize>
+           </font>
+          </property>
+          <property name="styleSheet">
+           <string notr="true">color: rgb(255, 170, 0);</string>
+          </property>
+          <property name="text">
+           <string>盐城机器人室外测试场</string>
+          </property>
+          <property name="alignment">
+           <set>Qt::AlignCenter</set>
+          </property>
+         </widget>
+        </item>
+        <item>
+         <spacer name="horizontalSpacer">
+          <property name="orientation">
+           <enum>Qt::Horizontal</enum>
+          </property>
+          <property name="sizeHint" stdset="0">
+           <size>
+            <width>40</width>
+            <height>20</height>
+           </size>
+          </property>
+         </spacer>
+        </item>
+       </layout>
+      </item>
+      <item>
+       <spacer name="verticalSpacer_5">
+        <property name="orientation">
+         <enum>Qt::Vertical</enum>
+        </property>
+        <property name="sizeHint" stdset="0">
+         <size>
+          <width>20</width>
+          <height>40</height>
+         </size>
+        </property>
+       </spacer>
+      </item>
+      <item>
+       <layout class="QHBoxLayout" name="horizontalLayout">
+        <property name="spacing">
+         <number>4</number>
+        </property>
+        <property name="leftMargin">
+         <number>4</number>
+        </property>
+        <property name="topMargin">
+         <number>4</number>
+        </property>
+        <property name="rightMargin">
+         <number>4</number>
+        </property>
+        <property name="bottomMargin">
+         <number>4</number>
+        </property>
+        <item>
+         <spacer name="horizontalSpacer_8">
+          <property name="orientation">
+           <enum>Qt::Horizontal</enum>
+          </property>
+          <property name="sizeHint" stdset="0">
+           <size>
+            <width>40</width>
+            <height>20</height>
+           </size>
+          </property>
+         </spacer>
+        </item>
+        <item>
+         <layout class="QVBoxLayout" name="verticalLayout_2">
+          <property name="spacing">
+           <number>4</number>
+          </property>
+          <property name="leftMargin">
+           <number>4</number>
+          </property>
+          <property name="topMargin">
+           <number>6</number>
+          </property>
+          <property name="rightMargin">
+           <number>4</number>
+          </property>
+          <property name="bottomMargin">
+           <number>250</number>
+          </property>
+          <item>
+           <widget class="QPushButton" name="btn_road">
+            <property name="minimumSize">
+             <size>
+              <width>300</width>
+              <height>80</height>
+             </size>
+            </property>
+            <property name="maximumSize">
+             <size>
+              <width>300</width>
+              <height>80</height>
+             </size>
+            </property>
+            <property name="font">
+             <font>
+              <pointsize>25</pointsize>
+             </font>
+            </property>
+            <property name="mouseTracking">
+             <bool>true</bool>
+            </property>
+            <property name="tabletTracking">
+             <bool>false</bool>
+            </property>
+            <property name="autoFillBackground">
+             <bool>false</bool>
+            </property>
+            <property name="styleSheet">
+             <string notr="true">QPushButton{
+	color: rgb(255, 255, 255);
+	background-color: rgb(85, 170, 127);
+border: 1px solid black; 
+border-radius: 5px;}
+
+QPushButton:hover {
+    background-color: rgb(0, 170, 0); 
+}</string>
+            </property>
+            <property name="text">
+             <string>路面适应性测试区</string>
+            </property>
+           </widget>
+          </item>
+          <item>
+           <spacer name="verticalSpacer_8">
+            <property name="orientation">
+             <enum>Qt::Vertical</enum>
+            </property>
+            <property name="sizeHint" stdset="0">
+             <size>
+              <width>20</width>
+              <height>40</height>
+             </size>
+            </property>
+           </spacer>
+          </item>
+          <item>
+           <widget class="QPushButton" name="btn_gps">
+            <property name="minimumSize">
+             <size>
+              <width>300</width>
+              <height>80</height>
+             </size>
+            </property>
+            <property name="maximumSize">
+             <size>
+              <width>300</width>
+              <height>80</height>
+             </size>
+            </property>
+            <property name="font">
+             <font>
+              <pointsize>25</pointsize>
+             </font>
+            </property>
+            <property name="autoFillBackground">
+             <bool>false</bool>
+            </property>
+            <property name="styleSheet">
+             <string notr="true">QPushButton{
+color: rgb(255, 255, 255);
+	background-color: rgb(85, 170, 127);
+border: 1px solid black; 
+border-radius: 5px;}
+
+QPushButton:hover {
+    background-color: rgb(0, 170, 0); 
+}</string>
+            </property>
+            <property name="text">
+             <string>导航与定位测试区</string>
+            </property>
+           </widget>
+          </item>
+          <item>
+           <spacer name="verticalSpacer_9">
+            <property name="orientation">
+             <enum>Qt::Vertical</enum>
+            </property>
+            <property name="sizeHint" stdset="0">
+             <size>
+              <width>20</width>
+              <height>40</height>
+             </size>
+            </property>
+           </spacer>
+          </item>
+          <item>
+           <widget class="QPushButton" name="btn_obstacle">
+            <property name="sizePolicy">
+             <sizepolicy hsizetype="Minimum" vsizetype="Fixed">
+              <horstretch>150</horstretch>
+              <verstretch>70</verstretch>
+             </sizepolicy>
+            </property>
+            <property name="minimumSize">
+             <size>
+              <width>300</width>
+              <height>80</height>
+             </size>
+            </property>
+            <property name="maximumSize">
+             <size>
+              <width>300</width>
+              <height>80</height>
+             </size>
+            </property>
+            <property name="font">
+             <font>
+              <pointsize>25</pointsize>
+             </font>
+            </property>
+            <property name="styleSheet">
+             <string notr="true">QPushButton{
+color: rgb(255, 255, 255);
+	background-color: rgb(85, 170, 127);
+border: 1px solid black; 
+border-radius: 5px;}
+
+QPushButton:hover {
+    background-color: rgb(0, 170, 0); 
+}</string>
+            </property>
+            <property name="text">
+             <string>障碍物避让测试区</string>
+            </property>
+           </widget>
+          </item>
+          <item>
+           <spacer name="verticalSpacer_10">
+            <property name="orientation">
+             <enum>Qt::Vertical</enum>
+            </property>
+            <property name="sizeHint" stdset="0">
+             <size>
+              <width>20</width>
+              <height>40</height>
+             </size>
+            </property>
+           </spacer>
+          </item>
+          <item>
+           <widget class="QPushButton" name="btn_move">
+            <property name="minimumSize">
+             <size>
+              <width>300</width>
+              <height>80</height>
+             </size>
+            </property>
+            <property name="maximumSize">
+             <size>
+              <width>300</width>
+              <height>80</height>
+             </size>
+            </property>
+            <property name="font">
+             <font>
+              <pointsize>25</pointsize>
+             </font>
+            </property>
+            <property name="styleSheet">
+             <string notr="true">QPushButton{
+color: rgb(255, 255, 255);
+	background-color: rgb(85, 170, 127);
+border: 1px solid black; 
+border-radius: 5px;}
+
+QPushButton:hover {
+    background-color: rgb(0, 170, 0); 
+}</string>
+            </property>
+            <property name="text">
+             <string>移动能力测试区</string>
+            </property>
+           </widget>
+          </item>
+         </layout>
+        </item>
+        <item>
+         <spacer name="horizontalSpacer_9">
+          <property name="orientation">
+           <enum>Qt::Horizontal</enum>
+          </property>
+          <property name="sizeHint" stdset="0">
+           <size>
+            <width>40</width>
+            <height>20</height>
+           </size>
+          </property>
+         </spacer>
+        </item>
+        <item>
+         <layout class="QVBoxLayout" name="verticalLayout_3">
+          <property name="spacing">
+           <number>4</number>
+          </property>
+          <property name="leftMargin">
+           <number>4</number>
+          </property>
+          <property name="topMargin">
+           <number>4</number>
+          </property>
+          <property name="rightMargin">
+           <number>4</number>
+          </property>
+          <property name="bottomMargin">
+           <number>4</number>
+          </property>
+          <item>
+           <widget class="QLabel" name="lab_video">
+            <property name="minimumSize">
+             <size>
+              <width>1200</width>
+              <height>680</height>
+             </size>
+            </property>
+            <property name="maximumSize">
+             <size>
+              <width>1200</width>
+              <height>680</height>
+             </size>
+            </property>
+            <property name="styleSheet">
+             <string notr="true">border: 1px solid rgb(255, 0, 0);</string>
+            </property>
+            <property name="text">
+             <string/>
+            </property>
+           </widget>
+          </item>
+          <item>
+           <spacer name="verticalSpacer_2">
+            <property name="orientation">
+             <enum>Qt::Vertical</enum>
+            </property>
+            <property name="sizeHint" stdset="0">
+             <size>
+              <width>20</width>
+              <height>40</height>
+             </size>
+            </property>
+           </spacer>
+          </item>
+         </layout>
+        </item>
+        <item>
+         <spacer name="horizontalSpacer_11">
+          <property name="orientation">
+           <enum>Qt::Horizontal</enum>
+          </property>
+          <property name="sizeHint" stdset="0">
+           <size>
+            <width>40</width>
+            <height>20</height>
+           </size>
+          </property>
+         </spacer>
+        </item>
+       </layout>
+      </item>
+     </layout>
+    </widget>
+   </item>
+  </layout>
+ </widget>
+ <resources/>
+ <connections/>
+</ui>

+ 17 - 0
yancheng-client/3rdparty/opencv/vcpkg.json

@@ -0,0 +1,17 @@
+{
+    "builtin-baseline": "a94dafabcabe2beb28289b39717111ad3d462327",
+    "dependencies": [
+        "quirc",
+        "opencv4"
+    ],
+    "overrides": [
+        {
+            "name": "quirc",
+            "version": "1.2"
+        },
+        {
+            "name": "opencv4",
+            "version": "4.8.0"
+        }
+    ]
+}

+ 15 - 0
yancheng-client/3rdparty/qt6/vcpkg.json

@@ -0,0 +1,15 @@
+{
+  "builtin-baseline": "c591ac6466a55ef0a05a3d56bb1489ca36e50102",
+  "dependencies": [
+    "qtbase",
+    "qttools",
+    "qtmultimedia",
+    "qttranslations"
+  ],
+  "overrides": [
+    { "name": "qtbase", "version": "6.6.3" },
+    { "name": "qttools", "version": "6.6.3" },
+    { "name": "qtmultimedia", "version": "6.6.3" },
+    { "name": "qttranslations", "version": "6.6.3" }
+  ]
+}

+ 9 - 0
yancheng-client/3rdparty/zmq/vcpkg.json

@@ -0,0 +1,9 @@
+{
+  "builtin-baseline": "c591ac6466a55ef0a05a3d56bb1489ca36e50102",
+  "dependencies": [
+    "cppzmq"
+  ],
+  "overrides": [
+    { "name": "cppzmq", "version": "4.10.0" }
+  ]
+}

+ 142 - 0
yancheng-client/CMakeLists.txt

@@ -0,0 +1,142 @@
+# 设置兼容最低版本
+cmake_minimum_required(VERSION 3.20)
+
+# 定义项目名称变量
+set(PROJECT_NAME TEST001)
+project(${PROJECT_NAME} LANGUAGES CXX)
+
+# 设置C++标准
+set(CMAKE_CXX_STANDARD 17)
+set(CMAKE_CXX_STANDARD_REQUIRED ON)
+set(CMAKE_CXX_EXTENSIONS OFF)
+
+# 启用 Qt 的自动处理
+set(CMAKE_AUTOUIC ON)
+set(CMAKE_AUTOMOC ON)
+set(CMAKE_AUTORCC ON)
+
+# --- for vs2022
+set(CMAKE_RUNTIME_OUTPUT_DIRECTORY_DEBUG ${CMAKE_CURRENT_SOURCE_DIR}/bin)
+set(CMAKE_RUNTIME_OUTPUT_DIRECTORY_RELEASE ${CMAKE_CURRENT_SOURCE_DIR}/bin)
+
+# --- protobuf for opencv
+set(Protobuf_DIR "${CMAKE_CURRENT_SOURCE_DIR}/3rdparty/opencv/vcpkg_installed/x64-windows/share/protobuf")
+set(Protobuf_INCLUDE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/3rdparty/opencv/vcpkg_installed/x64-windows/include")
+set(Protobuf_LIBRARIES "${CMAKE_CURRENT_SOURCE_DIR}/3rdparty/opencv/vcpkg_installed/x64-windows/lib/libprotobuf.lib")
+
+# --- tiff for opencv
+set(TIFF_INCLUDE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/3rdparty/opencv/vcpkg_installed/x64-windows/include")
+set(TIFF_LIBRARY "${CMAKE_CURRENT_SOURCE_DIR}/3rdparty/opencv/vcpkg_installed/x64-windows/lib/tiff.lib")
+
+# --- quirc for opencv
+set(quirc_DIR "${CMAKE_CURRENT_SOURCE_DIR}/3rdparty/opencv/vcpkg_installed/x64-windows/share/quirc")
+
+# --- opencv
+set(OpenCV_DIR "${CMAKE_CURRENT_SOURCE_DIR}/3rdparty/opencv/vcpkg_installed/x64-windows/share/opencv4")
+find_package(OpenCV REQUIRED)
+
+# --- qt
+set(Qt6_DIR "${CMAKE_CURRENT_SOURCE_DIR}/3rdparty/qt6/vcpkg_installed/x64-windows/share/Qt6")
+# find_package(Qt6 REQUIRED COMPONENTS Core Gui Widgets)
+find_package(Qt6 REQUIRED COMPONENTS Core Gui Widgets Multimedia MultimediaWidgets)
+
+# --- zeromq
+set(ZeroMQ_DIR "${CMAKE_CURRENT_SOURCE_DIR}/3rdparty/zmq/vcpkg_installed/x64-windows/share/zeromq")
+set(ZeroMQ_INCLUDE_DIRS "${CMAKE_CURRENT_SOURCE_DIR}/3rdparty/zmq/vcpkg_installed/x64-windows/include")
+set(ZeroMQ_LIBRARIES "${CMAKE_CURRENT_SOURCE_DIR}/3rdparty/zmq/vcpkg_installed/x64-windows/lib/libzmq-mt-4_3_5.lib")
+find_package(ZeroMQ REQUIRED)
+
+# 设置源文件和头文件
+file(GLOB SOURCES src/*.cpp)
+file(GLOB HEADERS src/*.h)
+file(GLOB UIS src/*.ui)
+
+# 添加可执行文件
+add_executable(${PROJECT_NAME}
+    ${SOURCES}
+    ${HEADERS}
+    ${UIS}
+)
+
+# 如果是 Windows 平台,则设置为 GUI 应用(无控制台)
+if(WIN32)
+    set_target_properties(${PROJECT_NAME} PROPERTIES
+        WIN32_EXECUTABLE ON
+        OUTPUT_NAME "测试工具"
+    )
+endif()
+
+# --- 包含头文件目录
+target_include_directories(${PROJECT_NAME} PRIVATE 
+    ${ZeroMQ_INCLUDE_DIRS}
+)
+
+# --- 链接
+target_link_libraries(${PROJECT_NAME} PRIVATE 
+    Qt6::Core
+    Qt6::Gui
+    Qt6::Widgets
+    Qt6::Multimedia
+    Qt6::MultimediaWidgets
+    ${ZeroMQ_LIBRARIES}
+    ${OpenCV_LIBS}
+    ${Protobuf_LIBRARIES}
+    ${TIFF_LIBRARY}
+    ws2_32  # 添加 Winsock 库
+)
+
+# --- for qt 获取 Qt 插件目录路
+set(QT_CORE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/3rdparty/qt6/vcpkg_installed/x64-windows/bin/Qt6Core.dll")
+set(QT_BASE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/3rdparty/qt6/vcpkg_installed")
+
+# --- 根据构建类型设置插件目录
+if(CMAKE_BUILD_TYPE STREQUAL "Debug")
+    set(CV_BIN_DIR "${CMAKE_CURRENT_SOURCE_DIR}/3rdparty/opencv/vcpkg_installed/x64-windows/debug/bin")
+    set(QT_BIN_DIR "${QT_BASE_DIR}/x64-windows/debug/bin")
+    set(QT_PLUGINS_DIR "${QT_BASE_DIR}/x64-windows/debug/Qt6/plugins/platforms")
+    set(QT_MULTIMEDIA_PLUGINS_DIR "${QT_BASE_DIR}/x64-windows/debug/Qt6/plugins/multimedia")
+else()
+    set(CV_BIN_DIR "${CMAKE_CURRENT_SOURCE_DIR}/3rdparty/opencv/vcpkg_installed/x64-windows/bin")
+    set(QT_BIN_DIR "${QT_BASE_DIR}/x64-windows/bin")
+    set(QT_PLUGINS_DIR "${QT_BASE_DIR}/x64-windows/Qt6/plugins/platforms")
+    set(QT_MULTIMEDIA_PLUGINS_DIR "${QT_BASE_DIR}/x64-windows/Qt6/plugins/multimedia")
+endif()
+
+# --- 打包 qt
+file(GLOB QT_DLLS "${QT_BIN_DIR}/*.dll")
+file(GLOB QT_PLUGINS_DLLS "${QT_PLUGINS_DIR}/*.dll")
+file(GLOB QT_MULTIMEDIA_PLUGINS "${QT_MULTIMEDIA_PLUGINS_DIR}/*.dll")
+add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD
+
+    # 复制所有 DLL 文件到目标目录
+    COMMAND ${CMAKE_COMMAND} -E copy_if_different
+        ${QT_DLLS}
+        "$<TARGET_FILE_DIR:${PROJECT_NAME}>/"
+
+    # 创建 platforms 目录并复制文件
+    COMMAND ${CMAKE_COMMAND} -E make_directory "$<TARGET_FILE_DIR:${PROJECT_NAME}>/platforms"
+    COMMAND ${CMAKE_COMMAND} -E copy_if_different
+        ${QT_PLUGINS_DLLS}
+        "$<TARGET_FILE_DIR:${PROJECT_NAME}>/platforms/"
+
+    # 创建 multimedia 目录并复制文件
+    COMMAND ${CMAKE_COMMAND} -E make_directory "$<TARGET_FILE_DIR:${PROJECT_NAME}>/multimedia"
+    COMMAND ${CMAKE_COMMAND} -E copy_if_different
+        ${QT_MULTIMEDIA_PLUGINS}
+        "$<TARGET_FILE_DIR:${PROJECT_NAME}>/multimedia/"
+)
+
+# --- 打包 opencv
+file(GLOB CV_DLLS "${CV_BIN_DIR}/*.dll")
+add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD
+    COMMAND ${CMAKE_COMMAND} -E copy_if_different
+    ${CV_DLLS}
+    "$<TARGET_FILE_DIR:${PROJECT_NAME}>/"
+)
+
+# --- 打包 zeromq
+add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD
+    COMMAND ${CMAKE_COMMAND} -E copy_if_different
+    "${CMAKE_CURRENT_SOURCE_DIR}/3rdparty/zmq/vcpkg_installed/x64-windows/bin/libzmq-mt-4_3_5.dll"
+    "$<TARGET_FILE_DIR:${PROJECT_NAME}>/"
+)

+ 48 - 0
yancheng-client/CMakeSettings.json

@@ -0,0 +1,48 @@
+{
+  "configurations": [
+    {
+      "name": "x64-Debug",
+      "generator": "Ninja",
+      "configurationType": "Debug",
+      "buildRoot": "${projectDir}/build/${name}",
+      "installRoot": "${projectDir}/install/${name}",
+      "inheritEnvironments": ["msvc_x64_x64"],
+      "cmakeCommandArgs": "",
+      "buildCommandArgs": "",
+      "variables": [
+        {
+          "name": "CMAKE_CXX_COMPILER",
+          "value": "C:/Program Files/Microsoft Visual Studio/2022/Community/VC/Tools/MSVC/14.44.35207/bin/Hostx64/x64/cl.exe",
+          "type": "FILEPATH"
+        },
+        {
+          "name": "CMAKE_CXX_FLAGS",
+          "value": "/utf-8",
+          "type": "STRING"
+        }
+      ]
+    },
+    {
+      "name": "x64-Release",
+      "generator": "Ninja",
+      "configurationType": "Release",
+      "buildRoot": "${projectDir}/build/${name}",
+      "installRoot": "${projectDir}/install/${name}",
+      "inheritEnvironments": ["msvc_x64_x64"],
+      "cmakeCommandArgs": "",
+      "buildCommandArgs": "",
+      "variables": [
+        {
+          "name": "CMAKE_CXX_COMPILER",
+          "value": "C:/Program Files/Microsoft Visual Studio/2022/Community/VC/Tools/MSVC/14.44.35207/bin/Hostx64/x64/cl.exe",
+          "type": "FILEPATH"
+        },
+        {
+          "name": "CMAKE_CXX_FLAGS",
+          "value": "/utf-8",
+          "type": "STRING"
+        }
+      ]
+    }
+  ]
+}

+ 5 - 0
yancheng-client/Config.json

@@ -0,0 +1,5 @@
+{
+    "Btn_text": "道路适应性测试区",
+    "Udp_port1": 20001,
+    "Zmq_ip1": "tcp://192.168.30.101:5557"
+}

+ 1 - 0
yancheng-client/README-q&a.txt

@@ -0,0 +1 @@
+---

+ 11 - 0
yancheng-client/README-usage-release.ps1

@@ -0,0 +1,11 @@
+## PowerShell5.1 on win10/win11
+
+# --- 确认版本
+$PSVersionTable.PSVersion
+
+# --- 安装 choco | run as administrator
+Set-ExecutionPolicy Bypass -Scope Process -Force
+Invoke-Expression ((New-Object System.Net.WebClient).DownloadString('https://chocolatey.org/install.ps1'))
+
+# --- 安装 msvc | run as administrator
+choco install vcredist-all -y

+ 33 - 0
yancheng-client/README-usage.ps1

@@ -0,0 +1,33 @@
+## PowerShell7 on win10/win11
+
+# --- 安装 choco | run as administrator
+Set-ExecutionPolicy Bypass -Scope Process -Force `
+&& Invoke-Expression ((New-Object System.Net.WebClient).DownloadString('https://chocolatey.org/install.ps1'))
+
+# --- 安装 cmake | run as administrator
+choco install cmake --version=3.30.5 -y
+
+# --- 安装 IDE C:\Program Files\Microsoft Visual Studio\2022\Community\Common7\IDE\devenv.exe
+choco install visualstudio2022community -y --package-parameters "--add Microsoft.VisualStudio.Component.VC.Tools.x86.x64"
+choco install visualstudio2022-workload-nativedesktop -y
+
+# --- 安装 SDK
+winget install --id Microsoft.WindowsSDK.10.0.19041 --version 10.0.19041.685
+
+# --- 安装 IDE C:\tools\qtcreator\bin\qtcreator.exe
+choco install qtcreator --version=14.0.1 -y
+
+# --- 安装 vcpkg
+Set-Location "E:\casper\repositories\repositories\sri-project.yancheng.master\yancheng-client"
+git clone -c http.proxy="http://127.0.0.1:7890" https://github.com/microsoft/vcpkg.git
+.\vcpkg\bootstrap-vcpkg.bat
+
+# --- 安装依赖
+subst Z: "E:\casper\repositories\repositories\sri-project.yancheng.master"
+Set-Location "Z:\yancheng-client"
+.\vcpkg\vcpkg.exe install --x-manifest-root=".\3rdparty\qt6"
+.\vcpkg\vcpkg.exe install --x-manifest-root=".\3rdparty\opencv"
+.\vcpkg\vcpkg.exe install --x-manifest-root=".\3rdparty\zmq"
+subst Z: /D
+
+

+ 101 - 0
yancheng-client/src/UdpVideoSub.cpp

@@ -0,0 +1,101 @@
+#include "UdpVideoSub.h"
+#include <opencv2/opencv.hpp>
+#include<QImage>
+
+//#pragma comment(lib, "ws2_32.lib")
+
+UdpVideoSub::UdpVideoSub(int port):port_(port),running(true)
+{
+    // 初始化Winsock
+    if (WSAStartup(MAKEWORD(2, 2), &wsaData_) != 0)
+    {
+        throw std::runtime_error("WSAStartup failed");
+    }
+
+    // 创建UDP套接字
+    sock_ = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
+    if (sock_ == INVALID_SOCKET)
+    {
+        WSACleanup();
+        throw std::runtime_error("socket creation failed");
+    }
+
+    // 配置服务器地址
+    serverAddr_.sin_family = AF_INET;
+    serverAddr_.sin_port = htons(port_);
+    serverAddr_.sin_addr.s_addr = htonl(INADDR_ANY);
+
+    // 绑定套接字
+    if (bind(sock_, (sockaddr*)&serverAddr_, sizeof(serverAddr_)) == SOCKET_ERROR)
+    {
+        int err = WSAGetLastError();
+        std::cerr << "Bind failed with error: " << err << std::endl;
+        closesocket(sock_);
+        WSACleanup();
+        throw std::runtime_error("bind failed");
+    }
+
+    // 设置接收缓冲区大小
+    int recvBufSize = 65536;
+    if (setsockopt(sock_, SOL_SOCKET, SO_RCVBUF, (char*)&recvBufSize, sizeof(recvBufSize)) == SOCKET_ERROR)
+    {
+        std::cerr << "setsockopt failed: " << WSAGetLastError() << "\n";
+    }
+}
+
+UdpVideoSub::~UdpVideoSub()
+{
+    Stop();
+    if (sock_ != INVALID_SOCKET)
+    {
+        closesocket(sock_);
+    }
+    WSACleanup();
+}
+
+void UdpVideoSub::run()
+{
+    std::vector<uchar> buffer_(65536);
+
+    running=true;
+    while (running)
+    {
+        sockaddr_in clientAddr;
+        int clientAddrSize = sizeof(clientAddr);
+        int bytesReceived = recvfrom(sock_, reinterpret_cast<char*>(buffer_.data()),buffer_.size(), 0, (sockaddr*)&clientAddr, &clientAddrSize);
+
+        if (bytesReceived == SOCKET_ERROR)
+        {
+            std::cerr << "recvfrom failed: " << WSAGetLastError() << "\n";
+            continue;
+        }
+
+        cv::Mat frame = cv::imdecode(cv::Mat(1, bytesReceived, CV_8UC1, buffer_.data()), cv::IMREAD_COLOR);
+        if (!frame.empty())
+        {
+            std::lock_guard<std::mutex> lock(gps_mutex);
+            if (!latest_gps_data.empty())
+            {
+                cv::putText(frame, latest_gps_data, cv::Point(10, 30),
+                            cv::FONT_HERSHEY_SIMPLEX, 0.7, cv::Scalar(0, 255, 0), 2);
+            }
+
+            cv::cvtColor(frame, frame, cv::COLOR_BGR2RGB);
+            QImage qimg(frame.data, frame.cols, frame.rows,
+                        frame.step, QImage::Format_RGB888);
+            emit imageReceived(qimg.copy());
+        }
+    }
+}
+
+void UdpVideoSub::Stop()
+{
+    running = false;
+    this->wait();
+}
+
+void UdpVideoSub::UpdateGpsData(const std::string& gps_data)
+{
+    std::lock_guard<std::mutex> lock(gps_mutex);
+    latest_gps_data = gps_data;
+}

+ 37 - 0
yancheng-client/src/UdpVideoSub.h

@@ -0,0 +1,37 @@
+#ifndef UDPVIDEOSUB_H
+#define UDPVIDEOSUB_H
+
+#include <QObject>
+#include<QThread>
+#include <winsock2.h>
+#include <ws2tcpip.h>
+
+class UdpVideoSub:public QThread
+{
+    Q_OBJECT
+public:
+    UdpVideoSub(int port);
+    ~UdpVideoSub();
+
+    void Stop();
+    void UpdateGpsData(const std::string& gps_data);
+
+    signals:
+            void imageReceived(const QImage& image);
+
+protected:
+    void run();
+
+private:
+    int port_;
+
+    WSADATA wsaData_;
+    SOCKET sock_ = INVALID_SOCKET;
+    sockaddr_in serverAddr_{};
+
+    std::mutex gps_mutex;
+    std::string latest_gps_data;
+    bool running;
+};
+
+#endif // UDPVIDEOSUB_H

+ 119 - 0
yancheng-client/src/ZmqImageSubscriber.cpp

@@ -0,0 +1,119 @@
+#include "ZmqImageSubscriber.h"
+#include <QDebug>
+#include <QByteArray>
+#include <QImage>
+#include <opencv2/opencv.hpp>
+#include <QJsonDocument>
+#include <QJsonObject>
+
+
+ZmqImageSubscriber::ZmqImageSubscriber(const QString& address,const QString& topic) :
+        m_context(nullptr), m_subscriber(nullptr), m_address(address),m_topic(topic),m_running(false)
+{
+
+}
+
+ZmqImageSubscriber::~ZmqImageSubscriber()
+{
+    stop();
+}
+
+void ZmqImageSubscriber::run()
+{
+    try
+    {
+        m_context = new zmq::context_t(1);
+        m_subscriber = new zmq::socket_t(*m_context, ZMQ_SUB);
+        m_subscriber->connect(m_address.toStdString());
+        m_subscriber->setsockopt(ZMQ_SUBSCRIBE, m_topic.toStdString().c_str(), m_topic.size());
+        m_running = true;
+
+        while (m_running)
+        {
+            //接收主题帧
+            zmq::message_t topic_msg;
+            if (!m_subscriber->recv(topic_msg)) continue;
+
+            std::string topic(static_cast<char*>(topic_msg.data()), topic_msg.size());
+            if (topic == "fused")
+            {
+                //接收数据
+                zmq::message_t fused_msg;
+                if (!m_subscriber->recv(fused_msg)) continue;
+
+                try {
+                    // 解析JSON数据
+                    QByteArray jsonData = QByteArray::fromRawData(static_cast<const char*>(fused_msg.data()), fused_msg.size());
+                    QJsonParseError parseError;
+                    QJsonDocument fusedDoc = QJsonDocument::fromJson(jsonData, &parseError);
+
+                    if (parseError.error != QJsonParseError::NoError)
+                    {
+                        qDebug() << "JSON解析错误:" << parseError.errorString();
+                        return;
+                    }
+
+                    // 提取关键字段
+                    QJsonObject fusedData = fusedDoc.object();
+                    emit FusedReceived(fusedData);
+                    QString output = QString("时间:%1 | 纬度:%2 | 经度:%3 | 高度:%4m | 速度:%5| 速度:%6| 质量:%7 | 卫星:%8 | 偏航:%9 |"
+                                             "imu_timestamp:%10 | accel_x:%11 | accel_y:%12 | accel_z:%13 | roll:%14 | pitch:%15 | yaw:%16 | system_timestamp:%17")
+                            .arg(fusedData["gps_timestamp"].toString())
+                            .arg(fusedData["latitude"].toDouble(), 0, 'f', 6)
+                            .arg(fusedData["longitude"].toDouble(), 0, 'f', 6)
+                            .arg(fusedData["altitude"].toDouble())
+                            .arg(fusedData["speed_knots"].toDouble())
+                            .arg(fusedData["speed_ms"].toDouble())
+                            .arg(fusedData["quality"].toInt())
+                            .arg(fusedData["satellites"].toInt())
+                            .arg(fusedData["gps_yaw"].toDouble())
+                            .arg(fusedData["imu_timestamp"].toString())
+                            .arg(fusedData["accel_x"].toDouble())
+                            .arg(fusedData["accel_y"].toDouble())
+                            .arg(fusedData["accel_z"].toDouble())
+                            .arg(fusedData["roll"].toDouble())
+                            .arg(fusedData["pitch"].toDouble())
+                            .arg(fusedData["yaw"].toDouble())
+                            .arg(fusedData["system_timestamp"].toString());
+
+                    qDebug() << output;
+
+                    {
+                        std::lock_guard<std::mutex> lock(gps_mutex);
+                        latest_gps_data = output.toStdString();
+                    }
+
+                    //std::cout << "GPS数据更新: " << latest_gps_data << std::endl;
+                }
+                catch (const std::exception& e)
+                {
+                    std::cerr << "JSON解析错误: " << e.what() << std::endl;
+                    std::lock_guard<std::mutex> lock(gps_mutex);
+                    latest_gps_data = "GPS数据解析失败";
+                }
+            }
+        }
+    }
+
+    catch (const zmq::error_t& e)
+    {
+        qWarning() << "ZMQ Error:" << e.what();
+    }
+}
+
+void ZmqImageSubscriber::stop()
+{
+    m_running = false;
+    if (m_subscriber)
+    {
+        m_subscriber->close();
+        delete m_subscriber;
+        m_subscriber = nullptr;
+    }
+    if (m_context)
+    {
+        m_context->close();
+        delete m_context;
+        m_context = nullptr;
+    }
+}

+ 39 - 0
yancheng-client/src/ZmqImageSubscriber.h

@@ -0,0 +1,39 @@
+#ifndef ZMQIMAGESUBSCRIBER_H
+#define ZMQIMAGESUBSCRIBER_H
+
+
+#pragma once
+#include <QObject>
+#include<QThread>
+#include <zmq.hpp>
+
+class ZmqImageSubscriber : public QThread
+{
+    Q_OBJECT
+
+public:
+    explicit ZmqImageSubscriber(const QString& address,const QString& topic);
+    ~ZmqImageSubscriber();
+
+    void stop();
+
+protected:
+    void run();      //线程的事件循环
+
+private:
+    zmq::context_t* m_context;
+    zmq::socket_t* m_subscriber;
+
+    QString m_address;
+    QString m_topic;
+
+    bool m_running;
+
+    std::mutex gps_mutex;
+    std::string latest_gps_data;
+
+    signals:
+            void FusedReceived(const QJsonObject &fusedData);
+};
+#endif
+

+ 11 - 0
yancheng-client/src/main.cpp

@@ -0,0 +1,11 @@
+#include "widget.h"
+
+#include <QApplication>
+
+int main(int argc, char *argv[])
+{
+    QApplication a(argc, argv);
+    Widget w;
+    w.show();
+    return a.exec();
+}

+ 339 - 0
yancheng-client/src/widget.cpp

@@ -0,0 +1,339 @@
+#include "widget.h"
+#include "ui_widget.h"
+#include <QFile>
+#include <QStandardPaths>
+#include<QTimer>
+#include <QJsonObject>
+#include <QJsonDocument>
+
+Widget::Widget(QWidget *parent)
+        : QWidget(parent)
+        , ui(new Ui::Widget)
+{
+    ui->setupUi(this);
+
+    readJsonFile();
+
+    timer = new QTimer(this);
+    timer->start(1000);
+
+    ui->btn_road->setText(m_btnText);
+    ui->btn_start->setEnabled(false);
+    ui->btn_pause->setEnabled(false);
+    ui->btn_end->setEnabled(false);
+
+    connect(timer, &QTimer::timeout, this, &Widget::timeNow);
+
+    m_subscriber1 = new ZmqImageSubscriber(m_zmqIp1, "fused");
+    //m_subscriber2 = new ZmqImageSubscriber(m_zmqIp2, "fused");
+    //m_subscriber3 = new ZmqImageSubscriber("m_zmqIp3", "fused");
+    //m_subscriber4 = new ZmqImageSubscriber("m_zmqIp4", "fused");
+
+    m_udpvideo1=new UdpVideoSub(m_udpPort1);
+    //m_udpvideo2=new UdpVideoSub(m_udpPort2);
+    //m_udpvideo3=new UdpVideoSub(m_udpPort3);
+    //m_udpvideo4=new UdpVideoSub(m_udpPort4);
+
+
+    connect(m_udpvideo1, &UdpVideoSub::imageReceived,this, &Widget::displayImage);
+    //connect(m_udpvideo2, &UdpVideoSub::imageReceived,this, &Widget::displayImage);
+    //connect(m_udpvideo3, &UdpVideoSub::imageReceived,this, &Widget::displayImage);
+    //connect(m_udpvideo4, &UdpVideoSub::imageReceived,this, &Widget::displayImage);
+
+    connect(m_subscriber1,&ZmqImageSubscriber::FusedReceived,this,&Widget::saveFusedData);
+
+    setStyleSheet("QPushButton {border:1px solid black;color:rgb(255,255,255);border-radius:5px}");
+
+}
+
+Widget::~Widget()
+{
+    //delete m_subscriber;
+    delete ui;
+}
+
+void Widget::readJsonFile()
+{
+    //打开文件
+    QFile file("../Config.json");
+    if(!file.open(QIODevice::ReadOnly))
+    {
+        qDebug() << "Failed to open file";
+    }
+
+    //读取全部内容
+    QByteArray jsonData = file.readAll();
+    file.close();
+
+    //解析JSON
+    QJsonParseError parseError;
+    QJsonDocument doc = QJsonDocument::fromJson(jsonData, &parseError);
+    if(parseError.error != QJsonParseError::NoError)
+    {
+        qDebug() << "JSON parse error:" << parseError.errorString();
+    }
+
+    //提取数据
+    if(doc.isObject())
+    {
+        QJsonObject rootObj = doc.object();
+        m_btnText = rootObj["Btn_text"].toString();
+        // m_btnText.setFont(QFont().setPointSize(12));
+        ui->btn_road->setFont(QFont("SimHei", 12)); // 再设置字体(指定字体和大小)
+
+        m_zmqIp1 = rootObj["Zmq_ip1"].toString();
+        m_zmqIp2 = rootObj["Zmq_ip2"].toString();
+        m_zmqIp3 = rootObj["Zmq_ip3"].toString();
+        m_zmqIp4 = rootObj["Zmq_ip4"].toString();
+        m_udpPort1 = rootObj["Udp_port1"].toInt();
+        m_udpPort2 = rootObj["Udp_port2"].toInt();
+        m_udpPort3 = rootObj["Udp_port3"].toInt();
+        m_udpPort4 = rootObj["Udp_port4"].toInt();
+    }
+}
+
+void Widget::timeNow()
+{
+    QDateTime currentTime = QDateTime::currentDateTime();
+    QString timeText = currentTime.toString("yyyy-MM-dd HH:mm:ss");
+    ui->lab_time->setText(timeText);
+}
+
+void Widget::displayImage(const QImage& image)
+{
+    ui->lab_video->setPixmap(QPixmap::fromImage(image).scaled(ui->lab_video->size(), Qt::KeepAspectRatio, Qt::SmoothTransformation));
+}
+
+void  Widget::convertToLocalCoordinates(double start_lon, double start_lat,double end_lon, double end_lat)
+{
+    constexpr double EARTH_RADIUS = 6371000.0; // 地球平均半径(米)
+
+    // 将角度转换为弧度
+    auto toRadians = [](double degree)
+    {
+        return degree * M_PI / 180.0;
+    };
+
+    double dLat = toRadians(end_lat - start_lat);
+    double dLon = toRadians(end_lon - start_lon);
+
+    // 计算y轴方向距离(正北为正方向)
+    coordinateY = EARTH_RADIUS * dLat;
+
+    // 计算x轴方向距离(正东为正方向),考虑纬度变化
+    double lat_avg_rad = toRadians((start_lat + end_lat) / 2.0);
+    coordinateX = EARTH_RADIUS * dLon * cos(lat_avg_rad);
+}
+
+void Widget::saveFusedData(const QJsonObject &fusedData)
+{
+    if(m_isfirst)
+    {
+        m_startLon=fusedData.value("longitude").toDouble();
+        m_startLat=fusedData.value("latitude").toDouble();
+        m_isfirst=false;
+    }
+    else
+    {
+        convertToLocalCoordinates(m_startLon, m_startLat,fusedData.value("longitude").toDouble(),fusedData.value("latitude").toDouble());
+    }
+
+    QString coor = QString("(%1, %2)")
+            .arg(coordinateX, 0, 'f', 2)
+            .arg(coordinateY, 0, 'f', 2);
+    ui->lab_coordinate->setText(coor);
+    QString speed = QString("(%1, %2, %3)")
+            .arg(fusedData.value("accel_x").toDouble(), 0, 'f', 2)
+            .arg(fusedData.value("accel_y").toDouble(), 0, 'f', 2)
+            .arg(fusedData.value("accel_z").toDouble(), 0, 'f', 2);
+    ui->lab_speed->setText(speed);
+    QString angle = QString("(%1, %2, %3)")
+            .arg(fusedData.value("roll").toDouble(), 0, 'f', 2)
+            .arg(fusedData.value("pitch").toDouble(), 0, 'f', 2)
+            .arg(fusedData.value("yaw").toDouble(), 0, 'f', 2);
+    ui->lab_angle->setText(angle);
+
+    QDateTime systemTimestamp = QDateTime::fromMSecsSinceEpoch(fusedData.value("system_timestamp").toString().toLongLong());
+
+    // 写入CSV文件
+    QFile file(m_fileFused);
+    if(file.open(QIODevice::WriteOnly | QIODevice::Append))
+    {
+        QTextStream stream(&file);
+        if(file.size() == 0)
+        {
+            // 写入CSV表头
+            stream << "gps时间,X坐标,Y坐标,速度(节),速度(米/秒),定位质量,卫星数量,偏航角(正北方向),"
+                      "imu时间,x轴加速度,y轴加速度,z轴加速度,滚转角,俯仰角,偏航角,系统时间戳\n";
+        }
+        // 写入数据行
+        stream << fusedData.value("gps_timestamp").toString() << ","
+               << fusedData.value("coordinateX").toDouble() << ","
+               << fusedData.value("coordinateY").toDouble() << ","
+               << fusedData.value("speed_knots").toDouble() << ","
+               << fusedData.value("speed_ms").toDouble() << ","
+               << fusedData.value("quality").toInt() << ","
+               << fusedData.value("satellites").toInt() << ","
+               << fusedData.value("gps_yaw").toDouble() << ","
+               << fusedData.value("imu_timestamp").toString() << ","
+               << fusedData.value("accel_x").toDouble() << ","
+               << fusedData.value("accel_y").toDouble() << ","
+               << fusedData.value("accel_z").toDouble() << ","
+               << fusedData.value("roll").toDouble() << ","
+               << fusedData.value("pitch").toDouble() << ","
+               << fusedData.value("yaw").toDouble() << ","
+               << systemTimestamp.toString("yyyy/M/d hh:mm:ss.zzz") << "\n";
+        file.close();
+    }
+}
+
+void Widget::on_btn_start_clicked()
+{
+    ui->btn_start->setStyleSheet("background-color: gray;color:rgb(85, 170, 255)");
+    ui->btn_start->setEnabled(false);
+    ui->btn_pause->setStyleSheet("QPushButton {background-color:rgb(85,255,85);color:rgb(85, 170, 255)}" "QPushButton:hover {background-color: rgb(0,170,0);}");
+    ui->btn_pause->setEnabled(true);
+    ui->btn_end->setStyleSheet("QPushButton {background-color:rgb(85,255,85);color:rgb(85, 170, 255)}" "QPushButton:hover {background-color: rgb(0,170,0);}");
+    ui->btn_end->setEnabled(true);
+
+    ui->btn_road->setEnabled(false);
+    // ui->btn_gps->setEnabled(false);
+    // ui->btn_obstacle->setEnabled(false);
+    // ui->btn_move->setEnabled(false);
+
+    //使用标准路径API获取当前用户的桌面目录路径
+    m_desktopPath = QStandardPaths::writableLocation(QStandardPaths::DesktopLocation);
+    m_dataDir=(m_desktopPath + "/Data"+"/"+QDateTime::currentDateTime().toString("yyyyMMdd_hhmmss"));   //构造QDir对象指向桌面下的Data子目录
+    if(!m_dataDir.exists())
+    {
+        m_dataDir.mkpath(".");
+    }
+    // 构建带时间戳的文件名
+    m_fileFused = m_dataDir.filePath("FUSED" + QDateTime::currentDateTime().toString("yyyyMMdd_hhmmss") + ".csv");
+
+    m_subscriber1->start();
+}
+
+void Widget::on_btn_pause_clicked()
+{
+    if(ui->btn_pause->text()=="暂停测试")
+    {
+        ui->btn_pause->setText("恢复测试");
+        ui->btn_pause->setStyleSheet("QPushButton{background-color:rgb(85,255,85);color:rgb(255,0,0)}" "QPushButton:hover {background-color:rgb(0,170,0);}");
+
+        m_subscriber1->stop();
+    }
+    else
+    {
+        ui->btn_pause->setText("暂停测试");
+        ui->btn_pause->setStyleSheet("QPushButton{background-color:rgb(85,255,85);color:rgb(85,170,255)}" "QPushButton:hover {background-color:rgb(0,170,0);}");
+
+        m_fileFused = m_dataDir.filePath("FUSED" + QDateTime::currentDateTime().toString("yyyyMMdd_hhmmss") + ".csv");
+        m_subscriber1->start();
+    }
+
+
+}
+
+void Widget::on_btn_end_clicked()
+{
+    ui->btn_start->setStyleSheet("QPushButton {background-color:rgb(85,255,85);color:rgb(85,170,255)}" "QPushButton:hover {background-color: rgb(0,170,0);}");
+    ui->btn_start->setEnabled(true);
+    ui->btn_pause->setText("暂停测试");
+    ui->btn_pause->setStyleSheet("background-color: gray;color:rgb(85,170,255)");
+    ui->btn_pause->setEnabled(false);
+    ui->btn_end->setStyleSheet("background-color: gray;color:rgb(85,170,255)");
+    ui->btn_end->setEnabled(false);
+
+    ui->btn_road->setEnabled(true);
+    // ui->btn_gps->setEnabled(true);
+    // ui->btn_obstacle->setEnabled(true);
+    // ui->btn_move->setEnabled(true);
+
+    coordinateX=0.00;
+    coordinateY=0.00;
+    m_isfirst=true;
+    ui->lab_coordinate->setText("");
+    ui->lab_speed->setText("");
+    ui->lab_angle->setText("");
+
+    m_subscriber1->stop();
+}
+
+void Widget::on_btn_road_clicked()
+{
+
+    ui->btn_road->setStyleSheet("background-color: gray;");
+    ui->btn_road->setEnabled(false);
+
+    // ui->btn_gps->setStyleSheet("QPushButton {background-color:rgb(85,170,127);}" "QPushButton:hover {background-color: rgb(0,170,0);}");
+    // ui->btn_obstacle->setStyleSheet("QPushButton {background-color:rgb(85,170,127);}" "QPushButton:hover {background-color: rgb(0,170,0);}");
+    // ui->btn_move->setStyleSheet("QPushButton {background-color:rgb(85,170,127);}" "QPushButton:hover {background-color: rgb(0,170,0);}");
+    // ui->btn_gps->setEnabled(true);
+    // ui->btn_obstacle->setEnabled(true);
+    // ui->btn_move->setEnabled(true);
+
+    ui->btn_start->setEnabled(true);
+    ui->btn_start->setStyleSheet("QPushButton {background-color:rgb(85,255,85);color:rgb(85, 170, 255)}" "QPushButton:hover {background-color: rgb(0,170,0);}");
+
+    //m_udpvideo2->Stop();
+    m_udpvideo1->start();
+    //m_subscriber1->start();
+}
+
+void Widget::on_btn_gps_clicked()
+{
+    // ui->btn_gps->setStyleSheet("background-color: gray;");
+    // ui->btn_gps->setEnabled(false);
+
+    ui->btn_road->setStyleSheet("QPushButton {background-color:rgb(85,170,127);}" "QPushButton:hover {background-color: rgb(0,170,0);}");
+    // ui->btn_obstacle->setStyleSheet("QPushButton {background-color:rgb(85,170,127);}" "QPushButton:hover {background-color: rgb(0,170,0);}");
+    // ui->btn_move->setStyleSheet("QPushButton {background-color:rgb(85,170,127);}" "QPushButton:hover {background-color: rgb(0,170,0);}");
+    ui->btn_road->setEnabled(true);
+    // ui->btn_obstacle->setEnabled(true);
+    // ui->btn_move->setEnabled(true);
+
+    ui->btn_start->setEnabled(true);
+    ui->btn_start->setStyleSheet("QPushButton {background-color:rgb(85,255,85);color:rgb(85, 170, 255)}" "QPushButton:hover {background-color: rgb(0,170,0);}");
+
+    //m_udpvideo1->Stop();
+    //m_udpvideo2->start();
+    //m_subscriber2->start();
+}
+
+void Widget::on_btn_obstacle_clicked()
+{
+    // ui->btn_obstacle->setStyleSheet("background-color: gray;");
+    // ui->btn_obstacle->setEnabled(false);
+
+    ui->btn_road->setStyleSheet("QPushButton {background-color:rgb(85,170,127);}" "QPushButton:hover {background-color: rgb(0,170,0);}");
+    // ui->btn_gps->setStyleSheet("QPushButton {background-color:rgb(85,170,127);}" "QPushButton:hover {background-color: rgb(0,170,0);}");
+    // ui->btn_move->setStyleSheet("QPushButton {background-color:rgb(85,170,127);}" "QPushButton:hover {background-color: rgb(0,170,0);}");
+    ui->btn_road->setEnabled(true);
+    // ui->btn_gps->setEnabled(true);
+    // ui->btn_move->setEnabled(true);
+
+    ui->btn_start->setEnabled(true);
+    ui->btn_start->setStyleSheet("QPushButton {background-color:rgb(85,255,85);color:rgb(85, 170, 255)}" "QPushButton:hover {background-color: rgb(0,170,0);}");
+
+    //m_subscriber3->start();
+}
+
+void Widget::on_btn_move_clicked()
+{
+    // ui->btn_move->setStyleSheet("background-color: gray;");
+    // ui->btn_move->setEnabled(false);
+
+    ui->btn_road->setStyleSheet("QPushButton {background-color:rgb(85,170,127);}" "QPushButton:hover {background-color: rgb(0,170,0);}");
+    // ui->btn_gps->setStyleSheet("QPushButton {background-color:rgb(85,170,127);}" "QPushButton:hover {background-color: rgb(0,170,0);}");
+    // ui->btn_obstacle->setStyleSheet("QPushButton {background-color:rgb(85,170,127);}" "QPushButton:hover {background-color: rgb(0,170,0);}");
+    ui->btn_road->setEnabled(true);
+    // ui->btn_gps->setEnabled(true);
+    // ui->btn_obstacle->setEnabled(true);
+
+    ui->btn_start->setEnabled(true);
+    ui->btn_start->setStyleSheet("QPushButton {background-color:rgb(85,255,85);color:rgb(85, 170, 255)}" "QPushButton:hover {background-color: rgb(0,170,0);}");
+
+    //m_subscriber4->start();
+}
+

+ 75 - 0
yancheng-client/src/widget.h

@@ -0,0 +1,75 @@
+#ifndef WIDGET_H
+#define WIDGET_H
+
+#include <QWidget>
+#include "ZmqImageSubscriber.h"
+#include"UdpVideoSub.h"
+#include <QDir>
+
+QT_BEGIN_NAMESPACE
+namespace Ui {
+    class Widget;
+}
+QT_END_NAMESPACE
+
+class Widget : public QWidget
+{
+    Q_OBJECT
+
+public:
+    Widget(QWidget *parent = nullptr);
+    ~Widget();
+
+    void readJsonFile();
+    void timeNow();
+    void convertToLocalCoordinates(double start_lon, double start_lat,double end_lon, double end_lat);
+
+private slots:
+            void displayImage(const QImage& image);
+    void saveFusedData(const QJsonObject &fusedData);
+
+    void on_btn_start_clicked();
+    void on_btn_pause_clicked();
+    void on_btn_end_clicked();
+
+    void on_btn_road_clicked();
+    void on_btn_gps_clicked();
+    void on_btn_obstacle_clicked();
+    void on_btn_move_clicked();
+
+private:
+    Ui::Widget *ui;
+
+    QTimer *timer;
+
+    ZmqImageSubscriber* m_subscriber1;
+    ZmqImageSubscriber* m_subscriber2;
+    ZmqImageSubscriber* m_subscriber3;
+    ZmqImageSubscriber* m_subscriber4;
+
+    UdpVideoSub* m_udpvideo1;
+    UdpVideoSub* m_udpvideo2;
+    UdpVideoSub* m_udpvideo3;
+    UdpVideoSub* m_udpvideo4;
+
+    QString m_desktopPath;
+    QDir m_dataDir;
+    QString m_fileFused;
+    double m_startLon;
+    double m_startLat;
+    double coordinateX=0.00;
+    double coordinateY=0.00;
+    bool m_isfirst=true;
+
+    QString m_btnText;
+    QString m_zmqIp1;
+    QString m_zmqIp2;
+    QString m_zmqIp3;
+    QString m_zmqIp4;
+    int m_udpPort1;
+    int m_udpPort2;
+    int m_udpPort3;
+    int m_udpPort4;
+
+};
+#endif // WIDGET_H

+ 789 - 0
yancheng-client/src/widget.ui

@@ -0,0 +1,789 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>Widget</class>
+ <widget class="QWidget" name="Widget">
+  <property name="geometry">
+   <rect>
+    <x>0</x>
+    <y>0</y>
+    <width>1000</width>
+    <height>700</height>
+   </rect>
+  </property>
+  <property name="minimumSize">
+   <size>
+    <width>1000</width>
+    <height>700</height>
+   </size>
+  </property>
+  <property name="maximumSize">
+   <size>
+    <width>1000</width>
+    <height>700</height>
+   </size>
+  </property>
+  <property name="windowTitle">
+   <string>Widget</string>
+  </property>
+  <property name="styleSheet">
+   <string notr="true"/>
+  </property>
+  <layout class="QVBoxLayout" name="verticalLayout_4">
+   <property name="spacing">
+    <number>0</number>
+   </property>
+   <property name="leftMargin">
+    <number>0</number>
+   </property>
+   <property name="topMargin">
+    <number>0</number>
+   </property>
+   <property name="rightMargin">
+    <number>0</number>
+   </property>
+   <property name="bottomMargin">
+    <number>0</number>
+   </property>
+   <item>
+    <widget class="QFrame" name="frame">
+     <property name="minimumSize">
+      <size>
+       <width>1000</width>
+       <height>700</height>
+      </size>
+     </property>
+     <property name="maximumSize">
+      <size>
+       <width>1000</width>
+       <height>700</height>
+      </size>
+     </property>
+     <property name="styleSheet">
+      <string notr="true">QFrame#frame{
+       background-color: rgb(0, 51, 152);
+       }</string>
+     </property>
+     <property name="frameShape">
+      <enum>QFrame::StyledPanel</enum>
+     </property>
+     <property name="frameShadow">
+      <enum>QFrame::Raised</enum>
+     </property>
+     <layout class="QVBoxLayout" name="verticalLayout_5">
+      <property name="spacing">
+       <number>4</number>
+      </property>
+      <property name="leftMargin">
+       <number>6</number>
+      </property>
+      <property name="topMargin">
+       <number>6</number>
+      </property>
+      <property name="rightMargin">
+       <number>6</number>
+      </property>
+      <property name="bottomMargin">
+       <number>6</number>
+      </property>
+      <item>
+       <spacer name="verticalSpacer_4">
+        <property name="orientation">
+         <enum>Qt::Vertical</enum>
+        </property>
+        <property name="sizeHint" stdset="0">
+         <size>
+          <width>20</width>
+          <height>40</height>
+         </size>
+        </property>
+       </spacer>
+      </item>
+      <item>
+       <layout class="QHBoxLayout" name="horizontalLayout_3">
+        <property name="spacing">
+         <number>4</number>
+        </property>
+        <property name="leftMargin">
+         <number>4</number>
+        </property>
+        <property name="topMargin">
+         <number>4</number>
+        </property>
+        <property name="rightMargin">
+         <number>4</number>
+        </property>
+        <property name="bottomMargin">
+         <number>4</number>
+        </property>
+        <item>
+         <spacer name="horizontalSpacer_2">
+          <property name="orientation">
+           <enum>Qt::Horizontal</enum>
+          </property>
+          <property name="sizeHint" stdset="0">
+           <size>
+            <width>40</width>
+            <height>20</height>
+           </size>
+          </property>
+         </spacer>
+        </item>
+        <item>
+         <widget class="QLabel" name="label_9">
+          <property name="font">
+           <font>
+            <pointsize>25</pointsize>
+           </font>
+          </property>
+          <property name="styleSheet">
+           <string notr="true">color: rgb(255, 170, 0);</string>
+          </property>
+          <property name="text">
+           <string>盐城机器人室外测试场</string>
+          </property>
+         </widget>
+        </item>
+        <item>
+         <spacer name="horizontalSpacer">
+          <property name="orientation">
+           <enum>Qt::Horizontal</enum>
+          </property>
+          <property name="sizeHint" stdset="0">
+           <size>
+            <width>40</width>
+            <height>20</height>
+           </size>
+          </property>
+         </spacer>
+        </item>
+       </layout>
+      </item>
+      <item>
+       <spacer name="verticalSpacer_3">
+        <property name="orientation">
+         <enum>Qt::Vertical</enum>
+        </property>
+        <property name="sizeType">
+         <enum>QSizePolicy::Expanding</enum>
+        </property>
+        <property name="sizeHint" stdset="0">
+         <size>
+          <width>20</width>
+          <height>40</height>
+         </size>
+        </property>
+       </spacer>
+      </item>
+      <item>
+       <layout class="QHBoxLayout" name="horizontalLayout_2">
+        <property name="spacing">
+         <number>0</number>
+        </property>
+        <property name="leftMargin">
+         <number>0</number>
+        </property>
+        <property name="topMargin">
+         <number>0</number>
+        </property>
+        <property name="rightMargin">
+         <number>0</number>
+        </property>
+        <property name="bottomMargin">
+         <number>0</number>
+        </property>
+        <item>
+         <widget class="QLabel" name="label">
+          <property name="minimumSize">
+           <size>
+            <width>70</width>
+            <height>30</height>
+           </size>
+          </property>
+          <property name="maximumSize">
+           <size>
+            <width>70</width>
+            <height>30</height>
+           </size>
+          </property>
+          <property name="font">
+           <font>
+            <pointsize>15</pointsize>
+           </font>
+          </property>
+          <property name="layoutDirection">
+           <enum>Qt::LeftToRight</enum>
+          </property>
+          <property name="styleSheet">
+           <string notr="true">color: rgb(255, 170, 0);</string>
+          </property>
+          <property name="text">
+           <string>坐标:</string>
+          </property>
+          <property name="alignment">
+           <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
+          </property>
+         </widget>
+        </item>
+        <item>
+         <widget class="QLabel" name="lab_coordinate">
+          <property name="minimumSize">
+           <size>
+            <width>120</width>
+            <height>30</height>
+           </size>
+          </property>
+          <property name="maximumSize">
+           <size>
+            <width>120</width>
+            <height>30</height>
+           </size>
+          </property>
+          <property name="font">
+           <font>
+            <pointsize>15</pointsize>
+           </font>
+          </property>
+          <property name="styleSheet">
+           <string notr="true">color: rgb(255, 255, 255);</string>
+          </property>
+          <property name="text">
+           <string/>
+          </property>
+         </widget>
+        </item>
+        <item>
+         <widget class="QLabel" name="label_6">
+          <property name="minimumSize">
+           <size>
+            <width>70</width>
+            <height>30</height>
+           </size>
+          </property>
+          <property name="maximumSize">
+           <size>
+            <width>70</width>
+            <height>30</height>
+           </size>
+          </property>
+          <property name="font">
+           <font>
+            <pointsize>15</pointsize>
+           </font>
+          </property>
+          <property name="styleSheet">
+           <string notr="true">color: rgb(255, 170, 0);</string>
+          </property>
+          <property name="text">
+           <string>加速度:</string>
+          </property>
+          <property name="alignment">
+           <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
+          </property>
+         </widget>
+        </item>
+        <item>
+         <widget class="QLabel" name="lab_speed">
+          <property name="minimumSize">
+           <size>
+            <width>160</width>
+            <height>30</height>
+           </size>
+          </property>
+          <property name="maximumSize">
+           <size>
+            <width>160</width>
+            <height>30</height>
+           </size>
+          </property>
+          <property name="font">
+           <font>
+            <pointsize>15</pointsize>
+           </font>
+          </property>
+          <property name="styleSheet">
+           <string notr="true">color: rgb(255, 255, 255);</string>
+          </property>
+          <property name="text">
+           <string/>
+          </property>
+         </widget>
+        </item>
+        <item>
+         <widget class="QLabel" name="label_8">
+          <property name="minimumSize">
+           <size>
+            <width>90</width>
+            <height>30</height>
+           </size>
+          </property>
+          <property name="maximumSize">
+           <size>
+            <width>90</width>
+            <height>30</height>
+           </size>
+          </property>
+          <property name="font">
+           <font>
+            <pointsize>15</pointsize>
+           </font>
+          </property>
+          <property name="styleSheet">
+           <string notr="true">color: rgb(255, 170, 0);</string>
+          </property>
+          <property name="text">
+           <string>姿态角度:</string>
+          </property>
+          <property name="alignment">
+           <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
+          </property>
+         </widget>
+        </item>
+        <item>
+         <widget class="QLabel" name="lab_angle">
+          <property name="minimumSize">
+           <size>
+            <width>190</width>
+            <height>30</height>
+           </size>
+          </property>
+          <property name="maximumSize">
+           <size>
+            <width>190</width>
+            <height>30</height>
+           </size>
+          </property>
+          <property name="font">
+           <font>
+            <pointsize>15</pointsize>
+           </font>
+          </property>
+          <property name="styleSheet">
+           <string notr="true">color: rgb(255, 255, 255);</string>
+          </property>
+          <property name="text">
+           <string/>
+          </property>
+         </widget>
+        </item>
+        <item>
+         <spacer name="horizontalSpacer_7">
+          <property name="orientation">
+           <enum>Qt::Horizontal</enum>
+          </property>
+          <property name="sizeHint" stdset="0">
+           <size>
+            <width>40</width>
+            <height>20</height>
+           </size>
+          </property>
+         </spacer>
+        </item>
+        <item>
+         <widget class="QLabel" name="label_3">
+          <property name="minimumSize">
+           <size>
+            <width>50</width>
+            <height>30</height>
+           </size>
+          </property>
+          <property name="maximumSize">
+           <size>
+            <width>50</width>
+            <height>30</height>
+           </size>
+          </property>
+          <property name="font">
+           <font>
+            <pointsize>15</pointsize>
+           </font>
+          </property>
+          <property name="styleSheet">
+           <string notr="true">color: rgb(255, 170, 0);</string>
+          </property>
+          <property name="text">
+           <string>时间:</string>
+          </property>
+          <property name="alignment">
+           <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
+          </property>
+         </widget>
+        </item>
+        <item>
+         <widget class="QLabel" name="lab_time">
+          <property name="minimumSize">
+           <size>
+            <width>210</width>
+            <height>30</height>
+           </size>
+          </property>
+          <property name="maximumSize">
+           <size>
+            <width>210</width>
+            <height>30</height>
+           </size>
+          </property>
+          <property name="font">
+           <font>
+            <pointsize>15</pointsize>
+           </font>
+          </property>
+          <property name="styleSheet">
+           <string notr="true">color: rgb(255, 255, 255);</string>
+          </property>
+          <property name="text">
+           <string/>
+          </property>
+         </widget>
+        </item>
+       </layout>
+      </item>
+      <item>
+       <spacer name="verticalSpacer_5">
+        <property name="orientation">
+         <enum>Qt::Vertical</enum>
+        </property>
+        <property name="sizeHint" stdset="0">
+         <size>
+          <width>20</width>
+          <height>40</height>
+         </size>
+        </property>
+       </spacer>
+      </item>
+      <item>
+       <layout class="QHBoxLayout" name="horizontalLayout">
+        <property name="spacing">
+         <number>4</number>
+        </property>
+        <property name="leftMargin">
+         <number>4</number>
+        </property>
+        <property name="topMargin">
+         <number>4</number>
+        </property>
+        <property name="rightMargin">
+         <number>4</number>
+        </property>
+        <property name="bottomMargin">
+         <number>4</number>
+        </property>
+        <item>
+         <spacer name="horizontalSpacer_8">
+          <property name="orientation">
+           <enum>Qt::Horizontal</enum>
+          </property>
+          <property name="sizeHint" stdset="0">
+           <size>
+            <width>40</width>
+            <height>20</height>
+           </size>
+          </property>
+         </spacer>
+        </item>
+        <item>
+         <layout class="QVBoxLayout" name="verticalLayout_2">
+          <property name="spacing">
+           <number>4</number>
+          </property>
+          <property name="leftMargin">
+           <number>4</number>
+          </property>
+          <property name="topMargin">
+           <number>4</number>
+          </property>
+          <property name="rightMargin">
+           <number>4</number>
+          </property>
+          <property name="bottomMargin">
+           <number>150</number>
+          </property>
+          <item>
+           <widget class="QPushButton" name="btn_road">
+            <property name="minimumSize">
+             <size>
+              <width>150</width>
+              <height>50</height>
+             </size>
+            </property>
+            <property name="maximumSize">
+             <size>
+              <width>150</width>
+              <height>50</height>
+             </size>
+            </property>
+            <property name="font">
+             <font>
+              <pointsize>12</pointsize>
+             </font>
+            </property>
+            <property name="mouseTracking">
+             <bool>true</bool>
+            </property>
+            <property name="tabletTracking">
+             <bool>false</bool>
+            </property>
+            <property name="autoFillBackground">
+             <bool>false</bool>
+            </property>
+            <property name="styleSheet">
+             <string notr="true">QPushButton{
+              color: rgb(255, 255, 255);
+              background-color: rgb(85, 170, 127);
+              border: 1px solid black;
+              border-radius: 5px;}
+
+              QPushButton:hover {
+              background-color: rgb(0, 170, 0);
+              }</string>
+            </property>
+            <property name="text">
+             <string/>
+            </property>
+           </widget>
+          </item>
+          <item>
+           <spacer name="verticalSpacer_6">
+            <property name="orientation">
+             <enum>Qt::Vertical</enum>
+            </property>
+            <property name="sizeHint" stdset="0">
+             <size>
+              <width>20</width>
+              <height>40</height>
+             </size>
+            </property>
+           </spacer>
+          </item>
+         </layout>
+        </item>
+        <item>
+         <spacer name="horizontalSpacer_9">
+          <property name="orientation">
+           <enum>Qt::Horizontal</enum>
+          </property>
+          <property name="sizeHint" stdset="0">
+           <size>
+            <width>40</width>
+            <height>20</height>
+           </size>
+          </property>
+         </spacer>
+        </item>
+        <item>
+         <layout class="QVBoxLayout" name="verticalLayout_3">
+          <property name="spacing">
+           <number>4</number>
+          </property>
+          <property name="leftMargin">
+           <number>4</number>
+          </property>
+          <property name="topMargin">
+           <number>4</number>
+          </property>
+          <property name="rightMargin">
+           <number>4</number>
+          </property>
+          <property name="bottomMargin">
+           <number>4</number>
+          </property>
+          <item>
+           <widget class="QLabel" name="lab_video">
+            <property name="minimumSize">
+             <size>
+              <width>600</width>
+              <height>450</height>
+             </size>
+            </property>
+            <property name="maximumSize">
+             <size>
+              <width>600</width>
+              <height>450</height>
+             </size>
+            </property>
+            <property name="styleSheet">
+             <string notr="true">border: 1px solid rgb(255, 0, 0);</string>
+            </property>
+            <property name="text">
+             <string/>
+            </property>
+           </widget>
+          </item>
+          <item>
+           <spacer name="verticalSpacer_2">
+            <property name="orientation">
+             <enum>Qt::Vertical</enum>
+            </property>
+            <property name="sizeHint" stdset="0">
+             <size>
+              <width>20</width>
+              <height>40</height>
+             </size>
+            </property>
+           </spacer>
+          </item>
+         </layout>
+        </item>
+        <item>
+         <spacer name="horizontalSpacer_10">
+          <property name="orientation">
+           <enum>Qt::Horizontal</enum>
+          </property>
+          <property name="sizeHint" stdset="0">
+           <size>
+            <width>40</width>
+            <height>20</height>
+           </size>
+          </property>
+         </spacer>
+        </item>
+        <item>
+         <layout class="QVBoxLayout" name="verticalLayout">
+          <property name="spacing">
+           <number>20</number>
+          </property>
+          <property name="leftMargin">
+           <number>4</number>
+          </property>
+          <property name="topMargin">
+           <number>4</number>
+          </property>
+          <property name="rightMargin">
+           <number>4</number>
+          </property>
+          <property name="bottomMargin">
+           <number>35</number>
+          </property>
+          <item>
+           <spacer name="verticalSpacer">
+            <property name="orientation">
+             <enum>Qt::Vertical</enum>
+            </property>
+            <property name="sizeHint" stdset="0">
+             <size>
+              <width>20</width>
+              <height>40</height>
+             </size>
+            </property>
+           </spacer>
+          </item>
+          <item>
+           <widget class="QPushButton" name="btn_start">
+            <property name="minimumSize">
+             <size>
+              <width>150</width>
+              <height>50</height>
+             </size>
+            </property>
+            <property name="maximumSize">
+             <size>
+              <width>150</width>
+              <height>50</height>
+             </size>
+            </property>
+            <property name="font">
+             <font>
+              <pointsize>15</pointsize>
+             </font>
+            </property>
+            <property name="styleSheet">
+             <string notr="true">color: rgb(85, 170, 255);
+              background-color: gray;
+              border: 1px solid black;
+              border-radius: 5px;</string>
+            </property>
+            <property name="text">
+             <string>开始测试</string>
+            </property>
+           </widget>
+          </item>
+          <item>
+           <widget class="QPushButton" name="btn_pause">
+            <property name="minimumSize">
+             <size>
+              <width>150</width>
+              <height>50</height>
+             </size>
+            </property>
+            <property name="maximumSize">
+             <size>
+              <width>150</width>
+              <height>50</height>
+             </size>
+            </property>
+            <property name="font">
+             <font>
+              <pointsize>15</pointsize>
+             </font>
+            </property>
+            <property name="styleSheet">
+             <string notr="true">color: rgb(85, 170, 255);
+              background-color: gray;
+              border: 1px solid black;
+              border-radius: 5px;</string>
+            </property>
+            <property name="text">
+             <string>暂停测试</string>
+            </property>
+           </widget>
+          </item>
+          <item>
+           <widget class="QPushButton" name="btn_end">
+            <property name="enabled">
+             <bool>true</bool>
+            </property>
+            <property name="minimumSize">
+             <size>
+              <width>150</width>
+              <height>50</height>
+             </size>
+            </property>
+            <property name="maximumSize">
+             <size>
+              <width>150</width>
+              <height>50</height>
+             </size>
+            </property>
+            <property name="font">
+             <font>
+              <pointsize>15</pointsize>
+             </font>
+            </property>
+            <property name="styleSheet">
+             <string notr="true">color: rgb(85, 170, 255);
+              background-color: gray;
+              border: 1px solid black;
+              border-radius: 5px;</string>
+            </property>
+            <property name="text">
+             <string>结束测试</string>
+            </property>
+            <property name="checkable">
+             <bool>false</bool>
+            </property>
+           </widget>
+          </item>
+         </layout>
+        </item>
+        <item>
+         <spacer name="horizontalSpacer_11">
+          <property name="orientation">
+           <enum>Qt::Horizontal</enum>
+          </property>
+          <property name="sizeHint" stdset="0">
+           <size>
+            <width>40</width>
+            <height>20</height>
+           </size>
+          </property>
+         </spacer>
+        </item>
+       </layout>
+      </item>
+     </layout>
+    </widget>
+   </item>
+  </layout>
+ </widget>
+ <resources/>
+ <connections/>
+</ui>

+ 31 - 0
yancheng-edge/CMakeLists.txt

@@ -0,0 +1,31 @@
+# 修改后的CMakeLists.txt
+cmake_minimum_required(VERSION 3.5)
+project(ServerProject)
+
+# 设置C++标准
+set(CMAKE_CXX_STANDARD 11)
+set(CMAKE_CXX_STANDARD_REQUIRED ON)
+
+# 查找必要的包
+find_package(OpenCV REQUIRED)
+find_package(PkgConfig REQUIRED)
+pkg_check_modules(ZMQ REQUIRED libzmq)
+find_package(nlohmann_json REQUIRED)
+
+# 设置源文件和头文件
+file(GLOB SOURCES src/*.cpp)
+file(GLOB HEADERS src/*.h)
+
+# 添加可执行文件
+add_executable(TEST001
+        ${SOURCES}
+        ${HEADERS}
+)
+
+# 链接库(添加pthread)
+target_link_libraries(TEST001
+        ${OpenCV_LIBS}
+        ${ZMQ_LIBRARIES}
+        nlohmann_json::nlohmann_json
+        pthread
+)

+ 66 - 0
yancheng-edge/README-usage-for-docker.bash

@@ -0,0 +1,66 @@
+## USAGE
+
+sudo apt install openssh-server
+
+sudo apt update \
+&& sudo apt install libzmq3-dev -y \
+&& sudo apt install nlohmann-json3-dev -y \
+&& sudo apt install g++ -y \
+&& sudo apt install libopencv-dev -y \
+&& sudo apt install pkg-config -y \
+&& sudo apt install cmake -y
+
+echo "操作:安装docker" \
+&& wget -qO- http://58.34.94.178:9090/middleware/docker/install-on-u24.sh | sudo bash \
+&& echo "完毕"
+
+echo "操作:安装nomachine" \
+&& wget http://58.34.94.178:9090/software/nomachine/download/nomachine_8.8.1_1_amd64.deb \
+&& sudo dpkg --install nomachine*.deb \
+&& echo "完毕"
+
+
+# --- 锁屏设置
+gsettings set org.gnome.desktop.session idle-delay 0
+gsettings get org.gnome.desktop.session idle-delay
+gsettings set org.gnome.desktop.screensaver lock-enabled false
+gsettings get org.gnome.desktop.screensaver lock-enabled
+
+# --- 跟踪日志
+echo "RUN: $(date)" \
+&& project_path="/home/user/repositories/repositories/sri-project.yancheng.master/yancheng-edge/docker" \
+&& cd "${project_path}" \
+&& sudo -E docker-compose --file compose.yml logs --follow
+
+# --- 构建启动
+echo "RUN: $(date)" \
+&& project_path="/home/user/repositories/repositories/sri-project.yancheng.master/yancheng-edge/docker" \
+&& cd "${project_path}" \
+&& project_dir=$(dirname "$(pwd)") \
+&& export project_dir \
+&& sudo -E docker-compose --file compose.yml down \
+&& sudo -E docker-compose --file compose.yml up --detach --build \
+&& sudo -E docker-compose --file compose.yml logs --follow
+
+# --- 构建调试
+echo "RUN: $(date)" \
+&& project_path="/home/user/repositories/repositories/sri-project.yancheng.master/yancheng-edge/docker" \
+&& cd ${project_path} \
+&& project_dir=$(dirname "$(pwd)") \
+&& sudo -E docker-compose --file compose.yml down \
+&& sudo -E docker-compose --file compose.yml up --detach --build \
+&& sudo docker exec -it yancheng-edge bash
+
+# --- 编译启动
+echo "RUN: $(date)" \
+&& project_path="/home/user/repositories/repositories/sri-project.yancheng.master/yancheng-edge/docker" \
+&& cd "${project_path}" \
+&& project_dir=$(dirname "$(pwd)") \
+&& export project_dir \
+&& sudo -E docker-compose --file compose.yml down \
+&& sudo -E docker-compose --file compose.yml up --detach \
+&& sudo -E docker-compose --file compose.yml logs --follow
+
+
+/home/user/repositories/repositories/sri-project.yancheng.master/yancheng-edge/build
+./TEST001

+ 11 - 0
yancheng-edge/README-usage-for-podman.bash

@@ -0,0 +1,11 @@
+## USAGE
+
+# --- 构建启动
+echo "RUN: $(date)" \
+&& project_path="/home/user/repositories/repositories/sri-project.yancheng.master/yancheng-edge/podman" \
+&& cd "${project_path}" \
+&& project_dir=$(dirname "$(pwd)") \
+&& export project_dir \
+&& sudo -E podman-compose --file compose.yml down \
+&& sudo -E podman-compose --file compose.yml up --detach --build \
+&& sudo podman logs -f yancheng-edge

+ 50 - 0
yancheng-edge/README-usage-for-service.bash

@@ -0,0 +1,50 @@
+## USAGE
+
+# --- 配置自启
+echo "RUN: $(date)" \
+&& username="user" \
+&& servername="yancheng" \
+&& filepath="/usr/lib/systemd/system/${servername}.service" \
+&& projectdir="/home/${username}/repositories/repositories/sri-project.yancheng.master/yancheng-edge" \
+&& sudo rm -rf ${filepath} \
+&& sudo touch ${filepath} \
+&& sudo chmod 777 ${filepath} \
+&& sudo chmod 777 ${projectdir}/run.sh \
+&& text='
+[Unit]
+Description={{servername}} Auto-start Service
+After=network.target
+
+[Service]
+Type=exec
+WorkingDirectory={{projectdir}}
+ExecStart=/bin/bash -c "./run.sh"
+Restart=always
+User={{username}}
+Group={{username}}
+
+[Install]
+WantedBy=multi-user.target
+' \
+&& sudo echo "${text}" | sudo tee ${filepath} \
+&& sudo sed -i '1d' ${filepath} \
+&& sudo sed -i "s|{{username}}|${username}|g" ${filepath} \
+&& sudo sed -i "s|{{servername}}|${servername}|g" ${filepath} \
+&& sudo sed -i "s|{{projectdir}}|${projectdir}|g" ${filepath} \
+&& sudo systemctl daemon-reload \
+&& sudo systemctl start yancheng.service \
+&& sudo systemctl enable yancheng.service \
+&& echo "END: $(date)"
+
+
+
+# --- 自启服务操作
+sudo systemctl list-units --type=service | grep yancheng
+sudo systemctl status yancheng.service
+sudo systemctl stop yancheng.service
+sudo systemctl start yancheng.service
+sudo systemctl restart yancheng.service
+sudo systemctl enable yancheng.service
+sudo journalctl -u yancheng.service -f
+
+

+ 37 - 0
yancheng-edge/config-10.10.10.24.json

@@ -0,0 +1,37 @@
+{
+    "network": {
+        "udp": {
+            "target_ip": "10.10.10.24",
+            "target_port": 20001,
+            "enabled": true
+        },
+        "zmq": {
+            "gps_publish_port": 5555,
+            "imu_publish_port": 5556,
+            "fused_publish_port": 5557,
+            "enabled": true
+        }
+    },
+    "devices": {
+        "gps": {
+            "serial_port": "/dev/ttyS0",
+            "baud_rate": 115200,
+            "timeout_ms": 100,
+            "enabled": true
+        },
+        "imu": {
+            "serial_port": "/dev/ttyUSB0",
+            "baud_rate": 115200,
+            "polling_rate_hz": 20,
+            "enabled": true
+        },
+        "camera": {
+            "device_index": 0,
+            "width": 640,
+            "height": 480,
+            "jpeg_quality": 80,
+            "fps": 30,
+            "enabled": true
+        }
+    }
+}

+ 37 - 0
yancheng-edge/config-10.10.10.29.json

@@ -0,0 +1,37 @@
+{
+    "network": {
+        "udp": {
+            "target_ip": "10.10.10.29",
+            "target_port": 20001,
+            "enabled": true
+        },
+        "zmq": {
+            "gps_publish_port": 5555,
+            "imu_publish_port": 5556,
+            "fused_publish_port": 5557,
+            "enabled": true
+        }
+    },
+    "devices": {
+        "gps": {
+            "serial_port": "/dev/ttyS0",
+            "baud_rate": 115200,
+            "timeout_ms": 100,
+            "enabled": true
+        },
+        "imu": {
+            "serial_port": "/dev/ttyUSB1",
+            "baud_rate": 115200,
+            "polling_rate_hz": 20,
+            "enabled": true
+        },
+        "camera": {
+            "device_index": 0,
+            "width": 640,
+            "height": 480,
+            "jpeg_quality": 50,
+            "fps": 30,
+            "enabled": true
+        }
+    }
+}

+ 37 - 0
yancheng-edge/config-release.json

@@ -0,0 +1,37 @@
+{
+    "network": {
+        "udp": {
+            "target_ip": "192.168.30.202",
+            "target_port": 20001,
+            "enabled": true
+        },
+        "zmq": {
+            "gps_publish_port": 5555,
+            "imu_publish_port": 5556,
+            "fused_publish_port": 5557,
+            "enabled": true
+        }
+    },
+    "devices": {
+        "gps": {
+            "serial_port": "/dev/ttyS0",
+            "baud_rate": 115200,
+            "timeout_ms": 100,
+            "enabled": true
+        },
+        "imu": {
+            "serial_port": "/dev/ttyUSB0",
+            "baud_rate": 115200,
+            "polling_rate_hz": 20,
+            "enabled": true
+        },
+        "camera": {
+            "device_index": 0,
+            "width": 640,
+            "height": 480,
+            "jpeg_quality": 80,
+            "fps": 30,
+            "enabled": true
+        }
+    }
+}

+ 22 - 0
yancheng-edge/docker/Dockerfile

@@ -0,0 +1,22 @@
+FROM docker.m.daocloud.io/ubuntu:24.04
+ENV DEBIAN_FRONTEND=noninteractive
+
+RUN echo "操作:时区设置" \
+    && apt-get dist-upgrade  \
+    && apt-get update \
+    && apt-get install -y tzdata \
+    && echo "完毕"
+
+RUN echo "操作:安装依赖" \
+    && apt install -y  \
+      libzmq3-dev \
+      nlohmann-json3-dev \
+      g++ \
+      libopencv-dev \
+      pkg-config \
+    && echo "完毕"
+
+RUN echo "操作:安装依赖" \
+    && apt install -y  \
+      cmake \
+    && echo "完毕"

+ 38 - 0
yancheng-edge/docker/compose.yml

@@ -0,0 +1,38 @@
+version: '3.5'
+
+services:
+
+    yancheng-edge:
+
+        # --- building ---
+        image: yancheng-edge:debug
+        dns:
+            - 8.8.8.8
+            - 8.8.4.4
+        build:
+            context: ./
+            dockerfile: ./Dockerfile
+        environment:
+            TZ: Asia/Shanghai
+
+        # --- binding ---
+        volumes:
+            - ${project_dir}:${project_dir}
+            - /dev:/dev
+        network_mode: host
+
+        # --- running ---
+        container_name: yancheng-edge
+        cap_add:
+            - SYS_ADMIN
+        privileged: true
+        working_dir: ${project_dir}
+
+        # --- for debug ---
+#        stdin_open: true
+#        tty: true
+
+        # --- for release ---
+        command: bash run.sh
+        restart: always
+

+ 22 - 0
yancheng-edge/podman/Dockerfile

@@ -0,0 +1,22 @@
+FROM docker.m.daocloud.io/ubuntu:24.04
+ENV DEBIAN_FRONTEND=noninteractive
+
+RUN echo "操作:时区设置" \
+    && apt-get dist-upgrade  \
+    && apt-get update \
+    && apt-get install -y tzdata \
+    && echo "完毕"
+
+RUN echo "操作:安装依赖" \
+    && apt install -y  \
+      libzmq3-dev \
+      nlohmann-json3-dev \
+      g++ \
+      libopencv-dev \
+      pkg-config \
+    && echo "完毕"
+
+RUN echo "操作:安装依赖" \
+    && apt install -y  \
+      cmake \
+    && echo "完毕"

+ 22 - 0
yancheng-edge/podman/compose.yml

@@ -0,0 +1,22 @@
+version: '3.5'
+
+services:
+    yancheng-edge:
+        container_name: yancheng-edge
+        image: yancheng-edge:debug
+        dns:
+            - 8.8.8.8
+            - 8.8.4.4
+        build:
+            context: ./
+            dockerfile: ./Dockerfile
+        environment:
+            TZ: Asia/Shanghai
+        privileged: true
+        volumes:
+            - ${project_dir}:${project_dir}
+            - /dev:/dev
+        network_mode: host  # 关键:使用宿主机网络
+        working_dir: ${project_dir}
+        command: bash run.sh
+        restart: always

+ 6 - 0
yancheng-edge/run.sh

@@ -0,0 +1,6 @@
+#!/bin/bash
+
+# --- 重新编译
+echo "RUN: $(date)" \
+&& rm -rf build && mkdir build && cd build && cmake .. && make \
+&& ./TEST001

+ 59 - 0
yancheng-edge/src/SimpleConfig.cpp

@@ -0,0 +1,59 @@
+/*
+ * @Author: error: error: git config user.name & please set dead value or install git && error: git config user.email & please set dead value or install git & please set dead value or install git
+ * @Date: 2025-06-17 13:24:04
+ * @LastEditors: error: error: git config user.name & please set dead value or install git && error: git config user.email & please set dead value or install git & please set dead value or install git
+ * @LastEditTime: 2025-06-17 16:36:57
+ * @FilePath: /yancheng/SimpleConfig.cpp
+ * @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
+ */
+#include "SimpleConfig.h"
+#include <fstream>
+#include <iostream>
+
+SimpleConfig& SimpleConfig::getInstance() {
+    static SimpleConfig instance;
+    return instance;
+}
+
+bool SimpleConfig::load(const std::string& filepath) {
+    try {
+        std::ifstream file(filepath);
+        if (!file.is_open()) {
+            std::cerr << "Error: Cannot open config file: " << filepath << std::endl;
+            return false;
+        }
+
+        nlohmann::json config = nlohmann::json::parse(file);
+
+        // 加载网络配置
+        network_.udp.target_ip = config["network"]["udp"]["target_ip"];
+        network_.udp.target_port = config["network"]["udp"]["target_port"];
+        network_.udp.enabled = config["network"]["udp"]["enabled"];
+
+        network_.zmq.gps_publish_port = config["network"]["zmq"]["gps_publish_port"];
+        network_.zmq.imu_publish_port = config["network"]["zmq"]["imu_publish_port"];
+        network_.zmq.fused_publish_port = config["network"]["zmq"]["fused_publish_port"];
+        network_.zmq.enabled = config["network"]["zmq"]["enabled"];
+
+        // 加载设备配置
+        devices_.gps.serial_port = config["devices"]["gps"]["serial_port"];
+        devices_.gps.timeout_ms = config["devices"]["gps"]["timeout_ms"];
+        devices_.gps.enabled = config["devices"]["gps"]["enabled"];
+
+        devices_.imu.serial_port = config["devices"]["imu"]["serial_port"];
+        devices_.imu.polling_rate_hz = config["devices"]["imu"]["polling_rate_hz"];
+        devices_.imu.enabled = config["devices"]["imu"]["enabled"];
+
+        devices_.camera.device_index = config["devices"]["camera"]["device_index"];
+        devices_.camera.width = config["devices"]["camera"]["width"];
+        devices_.camera.height = config["devices"]["camera"]["height"];
+        devices_.camera.jpeg_quality = config["devices"]["camera"]["jpeg_quality"];
+        devices_.camera.fps = config["devices"]["camera"]["fps"];
+        devices_.camera.enabled = config["devices"]["camera"]["enabled"];
+
+        return true;
+    } catch (const std::exception& e) {
+        std::cerr << "Config loading error: " << e.what() << std::endl;
+        return false;
+    }
+}

+ 64 - 0
yancheng-edge/src/SimpleConfig.h

@@ -0,0 +1,64 @@
+/*
+ * @Author: error: error: git config user.name & please set dead value or install git && error: git config user.email & please set dead value or install git & please set dead value or install git
+ * @Date: 2025-06-11 14:39:19
+ * @LastEditors: error: error: git config user.name & please set dead value or install git && error: git config user.email & please set dead value or install git & please set dead value or install git
+ * @LastEditTime: 2025-06-17 16:36:20
+ * @FilePath: /yancheng/SimpleConfig.h
+ * @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
+ */
+#pragma once
+#include <string>
+#include <nlohmann/json.hpp>
+
+struct NetworkConfig {
+    struct Udp {
+        std::string target_ip;
+        int target_port;
+        bool enabled;
+    } udp;
+
+    struct Zmq {
+        int gps_publish_port;
+        int imu_publish_port;
+        int fused_publish_port;
+        bool enabled;
+    } zmq;
+};
+
+struct DeviceConfig {
+    struct Gps {
+        std::string serial_port;
+        int timeout_ms;
+        bool enabled;
+    } gps;
+
+    struct Imu {
+        std::string serial_port;
+        int polling_rate_hz;
+        bool enabled;
+    } imu;
+
+    struct Camera {
+        int device_index;
+        int width;
+        int height;
+        int jpeg_quality;
+        int fps;
+        bool enabled;
+    } camera;
+};
+
+class SimpleConfig {
+public:
+    SimpleConfig() = default;
+    static SimpleConfig& getInstance();
+    
+    bool load(const std::string& filepath);
+    const NetworkConfig& getNetwork() const { return network_; }
+    const DeviceConfig& getDevices() const { return devices_; }
+
+private:
+
+    NetworkConfig network_;
+    DeviceConfig devices_;
+};

+ 802 - 0
yancheng-edge/src/main.cpp

@@ -0,0 +1,802 @@
+#include <opencv2/opencv.hpp>
+#include <zmq.hpp>
+#include <vector>
+#include <chrono>
+#include <thread>
+#include <iostream>
+#include <fcntl.h>
+#include <termios.h>
+#include <unistd.h>
+#include <sstream>
+#include <iomanip>
+#include <cmath>
+#include <sys/socket.h>
+#include <arpa/inet.h>
+#include <netinet/in.h>
+// #include "ConfigManager.h"  // or the correct path to your ConfigManager header
+#include "SimpleConfig.h" // 确保包含配置文件管理器
+
+// UDP配置
+// const char *UDP_IP = "10.10.60.146"; // 目标IP地址
+// const int UDP_PORT = 1234;           // 目标端口号
+struct GPSData {
+    std::string timestamp; // UTC时间
+    double latitude;       // 纬度
+    double longitude;      // 经度
+    double altitude;       // 海拔
+    double speed_knots;    // 速度(节)
+    double speed_ms;       // 速度(米/秒)
+    int quality;           // 定位质量
+    int satellites;        // 卫星数量
+    double yaw;            // 偏航角(0~359.9°,真北方向)
+};
+
+bool parseGPHDT(const std::string &line, GPSData &gps) {
+    std::vector <std::string> tokens;
+    std::string token;
+    std::istringstream tokenStream(line);
+
+    while (std::getline(tokenStream, token, ',')) {
+        tokens.push_back(token);
+    }
+
+    // 检查是否为有效的 GPHDT 语句
+    if (tokens.size() < 2 || tokens[0] != "$GPHDT") {
+        return false;
+    }
+
+    try {
+        gps.yaw = std::stod(tokens[1]); // 提取偏航角
+        return true;
+    }
+    catch (...) {
+        return false;
+    }
+}
+
+bool parseGPGGA(const std::string &nmea, GPSData &gps) {
+    // 重置GPS数据
+    gps = GPSData();
+
+    if (nmea.empty() || nmea.find("$GPGGA") != 0) {
+        std::cerr << "[GPS] 无效的GPGGA起始符" << std::endl;
+        return false;
+    }
+
+    std::vector <std::string> tokens;
+    std::stringstream ss(nmea);
+    std::string token;
+
+    // std::cout << "Raw " << tokens << std::endl;
+
+    while (getline(ss, token, ',')) {
+        tokens.push_back(token);
+    }
+
+    if (tokens.size() < 15) {
+        std::cerr << "[GPS] 字段不足,实际数量: " << tokens.size() << std::endl;
+        return false;
+    }
+
+    try {
+        // 解析时间戳
+        if (!tokens[1].empty()) {
+            gps.timestamp = tokens[1];
+        }
+
+        // 解析纬度和经度
+        if (!tokens[2].empty() && !tokens[3].empty()) {
+            double lat = std::stod(tokens[2]);
+            double lat_deg = floor(lat / 100);
+            double lat_min = lat - lat_deg * 100;
+            gps.latitude = lat_deg + lat_min / 60.0;
+            if (tokens[3] == "S")
+                gps.latitude *= -1;
+        }
+
+        if (!tokens[4].empty() && !tokens[5].empty()) {
+            double lon = std::stod(tokens[4]);
+            double lon_deg = floor(lon / 100);
+            double lon_min = lon - lon_deg * 100;
+            gps.longitude = lon_deg + lon_min / 60;
+            if (tokens[5] == "W")
+                gps.longitude *= -1;
+        }
+
+        // 解析质量指标和卫星数量
+        if (!tokens[6].empty())
+            gps.quality = std::stoi(tokens[6]);
+        if (!tokens[7].empty())
+            gps.satellites = std::stoi(tokens[7]);
+
+        // 解析海拔高度(MSL)
+        if (!tokens[9].empty()) {
+            gps.altitude = std::stod(tokens[9]);
+        }
+
+        // 解析高程异常值(Geoid Separation)
+        if (!tokens[11].empty()) {
+            double geoid_separation = std::stod(tokens[11]);
+            gps.altitude += geoid_separation; // 修正真实海拔高度
+        }
+
+        // 即使 quality=0(无效定位),仍然返回 true
+        if (gps.quality == 0) {
+            // std::cerr << "[GPS] 警告: 无效定位 (quality=0)" << std::endl;
+        }
+
+        return true; // 总是返回 true,即使定位无效
+    }
+    catch (const std::exception &e) {
+        std::cerr << "[GPS] 解析异常: " << e.what() << std::endl;
+        return false;
+    }
+}
+
+bool parseGPVTG(const std::string &nmea, GPSData &gps) {
+    if (nmea.empty() || nmea.find("$GPVTG") != 0) {
+        std::cerr << "[GPS] 无效的GPVTG起始符" << std::endl;
+        return false;
+    }
+
+    std::vector <std::string> tokens;
+    std::stringstream ss(nmea);
+    std::string token;
+
+    while (getline(ss, token, ',')) {
+        tokens.push_back(token);
+    }
+
+    if (tokens.size() < 10) {
+        std::cerr << "[GPS] GPVTG字段不足,实际数量: " << tokens.size() << std::endl;
+        return false;
+    }
+
+    try {
+        // 解析速度(节)
+        if (!tokens[5].empty()) {
+            gps.speed_knots = std::stod(tokens[5]);
+        }
+
+        // 解析速度(m/s)
+        if (!tokens[7].empty()) {
+            gps.speed_ms = std::stod(tokens[7]) / 3.6;
+        }
+
+        return true;
+    }
+    catch (const std::exception &e) {
+        std::cerr << "[GPS] GPVTG解析异常: " << e.what() << std::endl;
+        return false;
+    }
+}
+
+// 视频流采集和UDP发送函数
+void videoStreamUDP(const SimpleConfig &config) {
+
+    if (!config.getNetwork().udp.enabled || !config.getDevices().camera.enabled) {
+        return;
+    }
+
+    const auto &network = config.getNetwork();
+    const auto &camera = config.getDevices().camera;
+    // 使用配置中的IP和端口
+    std::string UDP_IP = network.udp.target_ip;
+    int UDP_PORT = network.udp.target_port;
+    std::cout << UDP_IP << std::endl;
+    std::cout << UDP_PORT << std::endl;
+    // 创建UDP套接字
+    int sock = socket(AF_INET, SOCK_DGRAM, 0);
+    if (sock < 0) {
+        std::cerr << "无法创建UDP套接字" << std::endl;
+        return;
+    }
+
+    // 设置目标地址
+    struct sockaddr_in dest_addr;
+    memset(&dest_addr, 0, sizeof(dest_addr));
+    dest_addr.sin_family = AF_INET;
+    dest_addr.sin_port = htons(UDP_PORT);
+    if (inet_pton(AF_INET, UDP_IP.c_str(), &dest_addr.sin_addr) <= 0) {
+        std::cerr << "无效的IP地址" << std::endl;
+        close(sock);
+        return;
+    }
+
+    cv::VideoCapture cap(camera.device_index);
+
+    if (!cap.isOpened()) {
+        std::cerr << "无法打开摄像头!" << std::endl;
+        close(sock);
+        return;
+    }
+
+    cap.set(cv::CAP_PROP_FRAME_WIDTH, camera.width);
+    cap.set(cv::CAP_PROP_FRAME_HEIGHT, camera.height);
+    cv::Mat frame;
+    std::vector <uchar> buffer;
+
+    while (true) {
+        cap >> frame;
+        if (frame.empty())
+            break;
+
+        // 压缩图像
+        std::vector<int> params = {cv::IMWRITE_JPEG_QUALITY, camera.jpeg_quality};
+        cv::imencode(".jpg", frame, buffer, params);
+
+        // 通过UDP发送数据
+        ssize_t sent = sendto(sock, buffer.data(), buffer.size(), 0,
+                              (struct sockaddr *) &dest_addr, sizeof(dest_addr));
+        if (sent < 0) {
+            std::cerr << "error232-发送失败: " << strerror(errno) << std::endl;
+        }
+
+        // cv::imshow("Server Video", frame);
+        // if (cv::waitKey(1) == 27)
+        //     break;
+    }
+
+    close(sock);
+    cap.release();
+}
+
+int configureSerialPort(int fd) {
+    struct termios tty;
+    memset(&tty, 0, sizeof(tty));
+
+    // if (tcgetattr(fd, &tty) {
+    //     std::cerr << "[GPS] 从串口获取属性错误: " << strerror(errno) << std::endl;
+    //     return -1;
+    // }
+
+    cfsetospeed(&tty, B115200);
+    cfsetispeed(&tty, B115200);
+
+    tty.c_cflag &= ~PARENB;
+    tty.c_cflag &= ~CSTOPB;
+    tty.c_cflag &= ~CSIZE;
+    tty.c_cflag |= CS8;
+    tty.c_cflag &= ~CRTSCTS;
+    tty.c_cflag |= CREAD | CLOCAL;
+
+    tty.c_lflag &= ~(ICANON | ECHO | ECHOE | ISIG);
+    tty.c_oflag &= ~OPOST;
+
+    tty.c_cc[VMIN] = 1;
+    tty.c_cc[VTIME] = 10;
+
+    if (tcsetattr(fd, TCSANOW, &tty)) {
+        std::cerr << "[GPS] 设置串口属性错误: " << strerror(errno) << std::endl;
+        return -1;
+    }
+
+    return 0;
+}
+
+void gpsDataPublisher(const SimpleConfig &config) {
+
+    if (!config.getDevices().gps.enabled || !config.getNetwork().zmq.enabled) {
+        return;
+    }
+
+    const auto &gps = config.getDevices().gps;
+    int zmq_port = config.getNetwork().zmq.gps_publish_port;
+
+    zmq::context_t ctx(1);
+    zmq::socket_t publisher(ctx, ZMQ_PUB);
+    publisher.bind("tcp://*:" + std::to_string(zmq_port));
+    // const char *port = "/dev/ttyUSB0";
+    int fd = open(gps.serial_port.c_str(), O_RDWR | O_NOCTTY | O_SYNC);
+    if (fd < 0) {
+        std::cerr << "[GPS] 无法打开串口设备 " << gps.serial_port.c_str() << " 错误: " << strerror(errno) << std::endl;
+        return;
+    }
+
+    if (configureSerialPort(fd)) {
+        close(fd);
+        return;
+    }
+
+    std::string buffer;
+    char tempBuffer[256];
+    ssize_t n;
+    GPSData current_gps; // 存储当前完整的GPS数据(位置+速度)
+
+    std::cout << "[GPS] 开始GPS数据采集..." << std::endl;
+
+    while (true) {
+        n = read(fd, tempBuffer, sizeof(tempBuffer));
+        if (n > 0) {
+            buffer.append(tempBuffer, n);
+
+            size_t pos;
+            while ((pos = buffer.find("\r\n")) != std::string::npos) {
+                std::string line = buffer.substr(0, pos);
+                buffer.erase(0, pos + 2);
+
+                if (line.find("$GPGGA") == 0) {
+                    parseGPGGA(line, current_gps);
+                } else if (line.find("$GPVTG") == 0) {
+                    parseGPVTG(line, current_gps);
+                } else if (line.find("$GPHDT") == 0) {
+                    parseGPHDT(line, current_gps); // 解析偏航角
+                }
+
+                // 如果所有数据都已更新,则发送完整数据
+                if (!current_gps.timestamp.empty()) {
+                    std::ostringstream oss;
+                    oss << std::fixed << std::setprecision(9);
+                    oss << "{"
+                        << "\"timestamp\":\"" << current_gps.timestamp << "\","
+                        << "\"latitude\":" << current_gps.latitude << ","
+                        << "\"longitude\":" << current_gps.longitude << ","
+                        << "\"altitude\":" << current_gps.altitude << ","
+                        << "\"speed_knots\":" << current_gps.speed_knots << ","
+                        << "\"speed_ms\":" << current_gps.speed_ms << ","
+                        << "\"quality\":" << current_gps.quality << ","
+                        << "\"satellites\":" << current_gps.satellites << ","
+                        << "\"yaw\":" << current_gps.yaw // 新增偏航角
+                        << "}";
+
+                    zmq::message_t topic("gps", 3);
+                    zmq::message_t data(oss.str().data(), oss.str().size());
+                    publisher.send(topic, ZMQ_SNDMORE);
+                    publisher.send(data, 0);
+
+                    // std::cout << "[LOG347][GPS] 发送完整GPS数据: " << oss.str() << std::endl;
+                }
+            }
+        } else if (n < 0) {
+            std::cerr << "[GPS] 读取串口错误: " << strerror(errno) << std::endl;
+            break;
+        }
+
+        std::this_thread::sleep_for(std::chrono::milliseconds(10)); // 更短的延迟
+    }
+
+    close(fd);
+    std::cout << "[GPS] GPS数据采集结束" << std::endl;
+}
+
+// 寄存器地址定义(根据实际协议调整)
+#define AX 0x34
+// #define GX 0x37
+#define LRoll 0x3D
+#define HRoll 0x3E
+#define LPitch 0x3F
+#define HPitch 0x40
+#define LYaw 0x41
+#define HYaw 0x42
+// #define TEMP    0x??  // 根据实际温度寄存器地址修改
+
+// 数据更新标志
+#define ACC_UPDATE 0x01
+// #define GYRO_UPDATE 0x02
+#define ANGLE_UPDATE 0x04
+// #define TEMP_UPDATE  0x08
+
+uint8_t s_cDataUpdate = 0;
+int16_t sReg[128] = {0}; // 假设最大寄存器地址为127
+
+// CRC16-Modbus 校验计算
+uint16_t crc16_modbus(const uint8_t *data, size_t length) {
+    uint16_t crc = 0xFFFF;
+    for (size_t i = 0; i < length; i++) {
+        crc ^= data[i];
+        for (int j = 0; j < 8; j++) {
+            if (crc & 0x0001) {
+                crc = (crc >> 1) ^ 0xA001;
+            } else {
+                crc >>= 1;
+            }
+        }
+    }
+    return crc;
+}
+
+// 打开串口
+int open_serial_port(const std::string &port_str, int baud_rate) {
+    const char *port = port_str.c_str();
+    int fd = open(port, O_RDWR | O_NOCTTY);
+    if (fd == -1) {
+        std::cerr << "Error: Unable to open serial port " << port << std::endl;
+        return -1;
+    }
+
+    struct termios options;
+    tcgetattr(fd, &options);
+    cfsetispeed(&options, baud_rate);
+    cfsetospeed(&options, baud_rate);
+
+    options.c_cflag |= (CLOCAL | CREAD);
+    options.c_cflag &= ~PARENB;
+    options.c_cflag &= ~CSTOPB;
+    options.c_cflag &= ~CSIZE;
+    options.c_cflag |= CS8;
+    options.c_lflag &= ~(ICANON | ECHO | ECHOE | ISIG);
+    options.c_oflag &= ~OPOST;
+    options.c_cc[VMIN] = 1;
+    options.c_cc[VTIME] = 10;
+
+    if (tcsetattr(fd, TCSANOW, &options) != 0) {
+        std::cerr << "Error: Failed to set serial port attributes" << std::endl;
+        close(fd);
+        return -1;
+    }
+    return fd;
+}
+
+#include <sys/select.h>
+#include <fcntl.h>
+
+// 读取寄存器数据
+bool read_registers(int fd, uint16_t start_reg, uint16_t num_regs) {
+    tcflush(fd, TCIFLUSH); // 每次读取前清空输入缓冲区
+    uint8_t command[] = {
+            0x50, 0x03,
+            static_cast<uint8_t>(start_reg >> 8), static_cast<uint8_t>(start_reg & 0xFF),
+            static_cast<uint8_t>(num_regs >> 8), static_cast<uint8_t>(num_regs & 0xFF),
+            0, 0 // CRC placeholder
+    };
+
+    uint16_t crc = crc16_modbus(command, 6);
+    command[6] = crc & 0xFF;
+    command[7] = crc >> 8;
+
+    if (write(fd, command, 8) != 8) {
+        std::cerr << "Error: Failed to send command" << std::endl;
+        return false;
+    }
+
+    uint8_t response[256];
+    int len = read(fd, response, sizeof(response));
+    if (len < 2) {
+        std::cerr << "Error: Incomplete response" << std::endl;
+        return false;
+    }
+
+    // 校验CRC
+    crc = crc16_modbus(response, len - 2);
+    if (response[len - 2] != (crc & 0xFF) || response[len - 1] != (crc >> 8)) {
+        std::cerr << "Error: CRC mismatch" << std::endl;
+        return false;
+    }
+
+    // 解析寄存器数据
+    uint8_t byte_count = response[2];
+    if (byte_count != num_regs * 2) {
+        std::cerr << "Error: Unexpected byte count" << std::endl;
+        return false;
+    }
+
+    for (int i = 0; i < num_regs; i++) {
+        sReg[start_reg + i] = (response[3 + 2 * i] << 8) | response[4 + 2 * i];
+    }
+
+    // 设置数据更新标志
+    if (start_reg == AX)
+        s_cDataUpdate |= ACC_UPDATE;
+    if (start_reg == LRoll || start_reg == HRoll ||
+        start_reg == LPitch || start_reg == HPitch ||
+        start_reg == LYaw || start_reg == HYaw) {
+        s_cDataUpdate |= ANGLE_UPDATE;
+    }
+    // if (start_reg == TEMP) s_cDataUpdate |= TEMP_UPDATE;
+
+    return true;
+}
+
+// 在文件开头添加
+struct IMUData {
+    double accel_x = 0.0;
+    double accel_y = 0.0;
+    double accel_z = 0.0;
+    // double gyro_x = 0.0;
+    // double gyro_y = 0.0;
+    // double gyro_z = 0.0;
+    double roll = 0.0;
+    double pitch = 0.0;
+    double yaw = 0.0;
+    std::string timestamp;
+};
+
+int imuDataPublisher(const SimpleConfig &config) {
+    if (!config.getDevices().imu.enabled || !config.getNetwork().zmq.enabled) {
+        return 0;
+    }
+
+    const auto &imu = config.getDevices().imu;
+    int zmq_port = config.getNetwork().zmq.imu_publish_port;
+    // 使用配置中的串口参数
+    std::string port = imu.serial_port;
+    // int baud_rate = imu.baud_rate;  // todo 读取json数据不可用的问题
+    int polling_rate = imu.polling_rate_hz;
+    zmq::context_t ctx(1);
+    zmq::socket_t publisher(ctx, ZMQ_PUB);
+    publisher.bind("tcp://*:" + std::to_string(zmq_port));
+    // const char *port = "/dev/ttyUSB1";
+    int baud_rate = B115200;
+
+    // 打开串口
+    int fd = open_serial_port(imu.serial_port.c_str(), baud_rate);
+    if (fd == -1) {
+        std::cerr << "[IMU] 无法打开串口设备" << std::endl;
+        return 1;
+    }
+
+    // 初始化IMU
+    printf("[IMU] 初始化IMU...");
+    std::this_thread::sleep_for(std::chrono::milliseconds(500)); // 等待传感器启动
+    tcflush(fd, TCIOFLUSH);                                      // 清空输入输出缓冲区
+    printf("完成\n");
+
+    // 设置采集频率
+    const int target_hz = polling_rate;
+    const std::chrono::milliseconds interval(1000 / target_hz);
+
+    while (true) {
+        auto start = std::chrono::steady_clock::now();
+
+        // 读取加速度、角速度、角度
+        read_registers(fd, AX, 3); // 读取加速度
+        // read_registers(fd, GX, 3);     // 读取角速度
+        read_registers(fd, LRoll, 1);  // 读取角度(包含高低位)
+        read_registers(fd, HRoll, 1);  // 读取角度(包含高低位)
+        read_registers(fd, LPitch, 1); // 读取角度(包含高低位)
+        read_registers(fd, HPitch, 1); // 读取角度(包含高低位)
+        read_registers(fd, LYaw, 1);   // 读取角度(包含高低位)
+        read_registers(fd, HYaw, 1);   // 读取角度(包含高低位)
+
+        // 数据处理
+        if (s_cDataUpdate) {
+            // 获取当前时间戳
+            auto now = std::chrono::system_clock::now();
+            auto now_ms = std::chrono::time_point_cast<std::chrono::milliseconds>(now);
+            auto epoch = now_ms.time_since_epoch();
+            auto value = std::chrono::duration_cast<std::chrono::milliseconds>(epoch);
+            std::string timestamp = std::to_string(value.count());
+
+            // 准备JSON消息
+            std::ostringstream oss;
+            oss << std::fixed << std::setprecision(9);
+            oss << "{"
+                << "\"timestamp\":\"" << timestamp << "\","
+                << "\"accel_x\":" << sReg[AX] / 32768.0f * 16.0f << ","
+                << "\"accel_y\":" << sReg[AX + 1] / 32768.0f * 16.0f << ","
+                << "\"accel_z\":" << sReg[AX + 2] / 32768.0f * 16.0f << ","
+                // << "\"gyro_x\":" << sReg[GX] / 32768.0f * 2000.0f << ","
+                // << "\"gyro_y\":" << sReg[GX + 1] / 32768.0f * 2000.0f << ","
+                // << "\"gyro_z\":" << sReg[GX + 2] / 32768.0f * 2000.0f << ","
+                << "\"roll\":" << ((int32_t)((sReg[HRoll] << 16) | (uint16_t) sReg[LRoll])) / 1000.0f << ","
+                << "\"pitch\":" << ((int32_t)((sReg[HPitch] << 16) | (uint16_t) sReg[LPitch])) / 1000.0f << ","
+                << "\"yaw\":" << ((int32_t)((sReg[HYaw] << 16) | (uint16_t) sReg[LYaw])) / 1000.0f
+                << "}";
+
+            std::string jsonStr = oss.str();
+
+            // 创建并发送ZMQ消息
+            zmq::message_t topic("imu", 3);
+            zmq::message_t data(jsonStr.data(), jsonStr.size());
+
+            try {
+                publisher.send(topic, ZMQ_SNDMORE);
+                publisher.send(data, 0);
+                // std::cout << "[LOG585][IMU] 发送数据: " << jsonStr << std::endl;
+            }
+            catch (const zmq::error_t &e) {
+                std::cerr << "[IMU] ZMQ发送错误: " << e.what() << std::endl;
+            }
+
+            // 清除更新标志
+            s_cDataUpdate = 0;
+        }
+
+        // 频率控制
+        auto end = std::chrono::steady_clock::now();
+        auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
+        if (elapsed < interval) {
+            std::this_thread::sleep_for(interval - elapsed);
+        } else {
+            std::cerr << "[IMU] 警告: 循环速度低于" << target_hz << "Hz ("
+                      << elapsed.count() << "ms)" << std::endl;
+        }
+    }
+
+    close(fd);
+    return 0;
+}
+
+struct SensorFusionData {
+    // GPS数据
+    std::string gps_timestamp;//gps时间
+    double latitude;// 纬度
+    double longitude;// 经度
+    double altitude;// 海拔
+    double speed_knots;// 速度(节)
+    double speed_ms; // 速度(米/秒)
+    int quality; // 定位质量
+    int satellites; // 卫星数量
+    double gps_yaw;  // 偏航角(0~359.9°,真北方向)
+
+    // IMU数据
+    std::string imu_timestamp;//imu 时间
+    double accel_x; //x轴加速度
+    double accel_y; //y轴加速度
+    double accel_z; //z轴加速度
+    double roll; //滚转角
+    double pitch; //俯仰角
+    double yaw; //偏航角
+    std::string system_timestamp; // 添加系统时间戳
+};
+
+void updateIMUData(const std::string &jsonStr, SensorFusionData &fusedData) {
+    // 解析JSON并更新IMU数据
+    try {
+        auto j = nlohmann::json::parse(jsonStr);
+        fusedData.accel_x = j["accel_x"];
+        fusedData.accel_y = j["accel_y"];
+        fusedData.accel_z = j["accel_z"];
+        fusedData.roll = j["roll"];
+        fusedData.pitch = j["pitch"];
+        fusedData.yaw = j["yaw"];
+        fusedData.imu_timestamp = j["timestamp"];
+    }
+    catch (...) {
+        std::cerr << "Failed to parse IMU data" << std::endl;
+    }
+}
+
+void updateGPSData(const std::string &jsonStr, SensorFusionData &fusedData) {
+    // 解析JSON并更新GPS数据
+    try {
+        auto j = nlohmann::json::parse(jsonStr);
+        fusedData.gps_timestamp = j["timestamp"];
+        fusedData.latitude = j["latitude"];
+        fusedData.longitude = j["longitude"];
+        fusedData.altitude = j["altitude"];
+        fusedData.speed_knots = j["speed_knots"];
+        fusedData.speed_ms = j["speed_ms"];
+        fusedData.quality = j["quality"];
+        fusedData.satellites = j["satellites"];
+        fusedData.gps_yaw = j["yaw"];
+    }
+    catch (...) {
+        std::cerr << "Failed to parse GPS data" << std::endl;
+    }
+}
+
+void publishFusedData(zmq::socket_t &publisher, const SensorFusionData &data) {
+    std::ostringstream oss;
+    oss << std::fixed << std::setprecision(9);
+    oss << "{"
+        << "\"system_timestamp\":\"" << data.system_timestamp << "\","  // 添加系统时间戳
+        << "\"gps_timestamp\":\"" << data.gps_timestamp << "\","
+        << "\"imu_timestamp\":\"" << data.imu_timestamp << "\","
+        << "\"latitude\":" << data.latitude << ","
+        << "\"longitude\":" << data.longitude << ","
+        << "\"altitude\":" << data.altitude << ","
+        << "\"speed_knots\":" << data.speed_knots << ","
+        << "\"speed_ms\":" << data.speed_ms << ","
+        << "\"quality\":" << data.quality << ","
+        << "\"satellites\":" << data.satellites << ","
+        << "\"gps_yaw\":" << data.gps_yaw << ","
+        << "\"accel_x\":" << data.accel_x << ","
+        << "\"accel_y\":" << data.accel_y << ","
+        << "\"accel_z\":" << data.accel_z << ","
+        << "\"roll\":" << data.roll << ","
+        << "\"pitch\":" << data.pitch << ","
+        << "\"yaw\":" << data.yaw
+        << "}";
+
+    std::string jsonStr = oss.str();
+    zmq::message_t topic("fused", 5);
+    zmq::message_t data_msg(jsonStr.data(), jsonStr.size());
+    publisher.send(topic, ZMQ_SNDMORE);
+    publisher.send(data_msg, 0);
+    std::cout << "[LOG697][IMU GPS] 发送数据: " << jsonStr << std::endl;
+}
+
+void fusedDataPublisher(const SimpleConfig &config) {
+    if (!config.getNetwork().zmq.enabled) {
+        return;
+    }
+
+    zmq::context_t ctx(1);
+    zmq::socket_t publisher(ctx, ZMQ_PUB);
+    int zmq_port = config.getNetwork().zmq.fused_publish_port;
+    publisher.bind("tcp://*:" + std::to_string(zmq_port));
+
+    zmq::socket_t subscriber(ctx, ZMQ_SUB);
+    subscriber.connect("tcp://localhost:" + std::to_string(config.getNetwork().zmq.imu_publish_port));
+    subscriber.connect("tcp://localhost:" + std::to_string(config.getNetwork().zmq.gps_publish_port));
+    subscriber.setsockopt(ZMQ_SUBSCRIBE, "", 0);  // 订阅所有消息
+
+    SensorFusionData fusedData;
+    auto lastUpdate = std::chrono::system_clock::now();
+
+    while (true) {
+        zmq::message_t topic;
+        zmq::message_t data;
+
+        // 接收主题
+        if (subscriber.recv(&topic)) {
+            // 接收数据
+            if (subscriber.recv(&data)) {
+                std::string topic_str(static_cast<char *>(topic.data()), topic.size());
+                std::string data_str(static_cast<char *>(data.data()), data.size());
+
+                try {
+                    if (topic_str == "imu") {
+                        updateIMUData(data_str, fusedData);
+                    } else if (topic_str == "gps") {
+                        updateGPSData(data_str, fusedData);
+                    }
+
+                    // 添加系统时间戳
+                    auto now = std::chrono::system_clock::now();
+                    auto duration = now.time_since_epoch();
+                    auto millis = std::chrono::duration_cast<std::chrono::milliseconds>(duration).count();
+                    fusedData.system_timestamp = std::to_string(millis);
+
+                    // 控制发布频率
+                    if (std::chrono::duration_cast<std::chrono::milliseconds>(
+                            now - lastUpdate).count() >= 40) {  // 默认50ms
+                        publishFusedData(publisher, fusedData);
+                        lastUpdate = now;
+                    }
+                } catch (const std::exception &e) {
+                    std::cerr << "[FUSED] 数据处理错误: " << e.what() << std::endl;
+                }
+            }
+        }
+    }
+}
+
+int main() {
+    SimpleConfig config;
+    if (!config.load("../config-release.json")) {
+        return 1;
+    }
+
+    try {
+        std::cout << "[MAIN] 服务器启动..." << std::endl;
+
+        std::vector <std::thread> threads;  // 只保留这一个声明
+
+        // 1. 视频流线程(独立)
+        if (config.getNetwork().udp.enabled && config.getDevices().camera.enabled) {
+            threads.emplace_back(videoStreamUDP, std::ref(config));
+        }
+
+        // 2. 启动传感器数据采集和原始发布线程
+        if (config.getNetwork().zmq.enabled) {
+            // IMU原始数据发布
+            if (config.getDevices().imu.enabled) {
+                threads.emplace_back(imuDataPublisher, std::ref(config));
+                std::this_thread::sleep_for(std::chrono::milliseconds(50)); // 稍等确保绑定完成
+            }
+
+            // GPS原始数据发布
+            if (config.getDevices().gps.enabled) {
+                threads.emplace_back(gpsDataPublisher, std::ref(config));
+                std::this_thread::sleep_for(std::chrono::milliseconds(50));
+            }
+
+            // 3. 融合数据发布线程(订阅上述原始数据)
+            threads.emplace_back(fusedDataPublisher, std::ref(config));
+        }
+
+        // 等待所有线程完成
+        for (auto &t: threads) {
+            if (t.joinable()) {
+                t.join();
+            }
+        }
+
+    } catch (const std::exception &e) {
+        std::cerr << "[MAIN] 错误: " << e.what() << std::endl;
+        return 1;
+    }
+    return 0;
+}