diff --git a/App/RodAndBarPosition/RodAndBarPositionApp/Version.h b/App/RodAndBarPosition/RodAndBarPositionApp/Version.h index 647371cf..ba672e3f 100644 --- a/App/RodAndBarPosition/RodAndBarPositionApp/Version.h +++ b/App/RodAndBarPosition/RodAndBarPositionApp/Version.h @@ -6,7 +6,7 @@ #define RODANDBARPOSITION_APP_NAME "棒材定位" #define RODANDBARPOSITION_VERSION_STRING "1.1.4" -#define RODANDBARPOSITION_BUILD_STRING "1" +#define RODANDBARPOSITION_BUILD_STRING "2" #define RODANDBARPOSITION_FULL_VERSION_STRING "V" RODANDBARPOSITION_VERSION_STRING "_" RODANDBARPOSITION_BUILD_STRING // 获取版本信息的便捷函数 diff --git a/App/RodAndBarPosition/RodAndBarPositionApp/Version.md b/App/RodAndBarPosition/RodAndBarPositionApp/Version.md index 39337722..86e2792c 100644 --- a/App/RodAndBarPosition/RodAndBarPositionApp/Version.md +++ b/App/RodAndBarPosition/RodAndBarPositionApp/Version.md @@ -1,4 +1,7 @@ # 1.1.4 2026-06-26 +## build_2 +1. 限定了欧拉角的输出范围 + ## build_1 1. 修改工具的参数配置 diff --git a/GrabBagPrj/AppList.md b/GrabBagPrj/AppList.md index d50a1aba..653b40b8 100644 --- a/GrabBagPrj/AppList.md +++ b/GrabBagPrj/AppList.md @@ -7,17 +7,21 @@ | 1 | 自动拆包 | GrabBag | 1.3.6.2 | | 2 | 皮带撕裂 | BeltTearingServer | 2.0.8.1 | | 3 | 孔洞检测 | HoleDetection | 1.1.6.2 | -| 4 | 棒材定位 | RodAndBarPosition | 1.1.0.1 | -| 5 | 车轮拱高测量 | WheelMeasure | 1.2.0.1 | +| 4 | 棒材定位 | RodAndBarPosition | 1.1.4.2 | +| 5 | 车轮拱高测量 | WheelMeasure | 1.2.2.2 | | 6 | 定子定位 | StatorPosition | 1.0.0.1 | | 7 | 焊接 | LapWeld | 1.0.0.1 | | 8 | 工件定位 | Workpiece | 1.0.3.0 | | 9 | 颗粒尺寸检测 | ParticleSize | 1.0.0.0 | | 10 | 双目标记检测 | BinocularMarkServer | 1.0.0.4 | | 11 | 铁路隧道槽道测量 | TunnelChannel | 1.0.0.3 | -| 12 | 螺杆定位 | ScrewPosition | 1.1.10.1 | +| 12 | 螺杆定位 | ScrewPosition | 1.2.4.1 | | 13 | 包裹拆线位置定位 | BagThreadPosition | 1.0.0.4 | -| 14 | 工件孔定位 | WorkpieceHole | 1.1.5.1 | +| 14 | 工件孔定位 | WorkpieceHole | 1.1.6.1 | +| 15 | 轮胎孔定位 | TireHolePose | 1.0.1.1 | | 16 | 坑孔定位 | HolePitPosition | 无 | | 17 | 钢筋焊缝定位 | RodWeldSeam | 1.0.0.1 | +| 18 | 无人机螺杆检测服务端 | DroneScrewServer | 1.0.0.1 | +| 19 | 无人机螺杆检测控制端 | DroneScrewCtrlApp | 1.0.0.1 | +| 20 | 砂轮盘孔定位 | DiscHolePose | 1.0.0.1 | diff --git a/GrabBagPrj/CloudView.iss b/GrabBagPrj/CloudView.iss index 762609df..799f8238 100644 --- a/GrabBagPrj/CloudView.iss +++ b/GrabBagPrj/CloudView.iss @@ -2,7 +2,7 @@ ; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES! #define MyAppName "点云查看器" -#define MyAppVersion "1.1.4" +#define MyAppVersion "1.1.6" #define MyAppPublisher "" #define MyAppURL "" #define MyAppExeName "CloudView.exe" diff --git a/GrabBagPrj/GrabBagPrj.pro b/GrabBagPrj/GrabBagPrj.pro index 2eed2289..68f84d13 100644 --- a/GrabBagPrj/GrabBagPrj.pro +++ b/GrabBagPrj/GrabBagPrj.pro @@ -27,7 +27,7 @@ Device.depends = Utils AppUtils AppUtils.depends = Utils Module App.depends = Utils VrNets Module Device AppUtils Tools.depends = Module Utils Robot -Test.depends = Device Module +Test.depends = Utils Module Device # Test 测试 Test.file = ../Test/Test.pro diff --git a/GrabBagPrj/GrabBagPrj.sln b/GrabBagPrj/GrabBagPrj.sln new file mode 100644 index 00000000..074c3b43 --- /dev/null +++ b/GrabBagPrj/GrabBagPrj.sln @@ -0,0 +1,44 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.5.33516.290 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "ThreadPool", "..\Module\ThreadPool\ThreadPool.vcxproj", "{881F41B5-0FB2-4890-AB1F-F5F2013A22F7}" +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "TestThreadPool", "..\Test\TestThreadPool\TestThreadPool.vcxproj", "{0DBD887F-4AAC-4DB6-9706-4C6A43C3BB38}" + ProjectSection(ProjectDependencies) = postProject + {881F41B5-0FB2-4890-AB1F-F5F2013A22F7} = {881F41B5-0FB2-4890-AB1F-F5F2013A22F7} + EndProjectSection +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {881F41B5-0FB2-4890-AB1F-F5F2013A22F7}.Debug|x64.ActiveCfg = Debug|x64 + {881F41B5-0FB2-4890-AB1F-F5F2013A22F7}.Debug|x64.Build.0 = Debug|x64 + {881F41B5-0FB2-4890-AB1F-F5F2013A22F7}.Debug|x86.ActiveCfg = Debug|Win32 + {881F41B5-0FB2-4890-AB1F-F5F2013A22F7}.Debug|x86.Build.0 = Debug|Win32 + {881F41B5-0FB2-4890-AB1F-F5F2013A22F7}.Release|x64.ActiveCfg = Release|x64 + {881F41B5-0FB2-4890-AB1F-F5F2013A22F7}.Release|x64.Build.0 = Release|x64 + {881F41B5-0FB2-4890-AB1F-F5F2013A22F7}.Release|x86.ActiveCfg = Release|Win32 + {881F41B5-0FB2-4890-AB1F-F5F2013A22F7}.Release|x86.Build.0 = Release|Win32 + {0DBD887F-4AAC-4DB6-9706-4C6A43C3BB38}.Debug|x64.ActiveCfg = Debug|x64 + {0DBD887F-4AAC-4DB6-9706-4C6A43C3BB38}.Debug|x64.Build.0 = Debug|x64 + {0DBD887F-4AAC-4DB6-9706-4C6A43C3BB38}.Debug|x86.ActiveCfg = Debug|Win32 + {0DBD887F-4AAC-4DB6-9706-4C6A43C3BB38}.Debug|x86.Build.0 = Debug|Win32 + {0DBD887F-4AAC-4DB6-9706-4C6A43C3BB38}.Release|x64.ActiveCfg = Release|x64 + {0DBD887F-4AAC-4DB6-9706-4C6A43C3BB38}.Release|x64.Build.0 = Release|x64 + {0DBD887F-4AAC-4DB6-9706-4C6A43C3BB38}.Release|x86.ActiveCfg = Release|Win32 + {0DBD887F-4AAC-4DB6-9706-4C6A43C3BB38}.Release|x86.Build.0 = Release|Win32 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {9CA02FFA-9326-4C71-A14C-B54AB2F579D9} + EndGlobalSection +EndGlobal diff --git a/GrabBagPrj/TireHolePose.iss b/GrabBagPrj/TireHolePose.iss new file mode 100644 index 00000000..da2ba051 --- /dev/null +++ b/GrabBagPrj/TireHolePose.iss @@ -0,0 +1,23 @@ +; TireHolePose Inno Setup 脚本 +; 轮胎孔定位应用程序打包配置 + +[Setup] +AppName=轮胎孔定位 +AppVersion=1.0.0 +AppPublisher=ShangGu +DefaultDirName={pf}\TireHolePose +DefaultGroupName=轮胎孔定位 +OutputBaseFilename=TireHolePose_Setup +Compression=lzma +SolidCompression=yes +UninstallDisplayIcon={app}\TireHolePoseApp.exe + +[Files] +Source: "buildwin\TireHolePose\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs + +[Icons] +Name: "{group}\轮胎孔定位"; Filename: "{app}\TireHolePoseApp.exe" +Name: "{group}\卸载轮胎孔定位"; Filename: "{uninstallexe}" + +[Run] +Filename: "{app}\TireHolePoseApp.exe"; Description: "运行轮胎孔定位"; Flags: nowait postinstall skipifsilent diff --git a/GrabBagPrj/buildArmPrj.sh b/GrabBagPrj/buildArmPrj.sh index 659eecb1..b42d68fc 100644 --- a/GrabBagPrj/buildArmPrj.sh +++ b/GrabBagPrj/buildArmPrj.sh @@ -110,8 +110,10 @@ fi JOBS="${JOBS:-$(getconf _NPROCESSORS_ONLN 2>/dev/null || echo 8)}" echo "==========================================" +echo "一次性编译,并行度 ${JOBS}" +echo "(若内存不足出现 cc1plus 被 Killed,降低并行度重试:JOBS=4 ./buildArmPrj.sh 或 JOBS=2 ./buildArmPrj.sh)" -echo "执行 make -j${JOBS}" +# 一次性编译全部目标(并行度由 JOBS 控制,默认取 CPU 核心数) BUILD_RESULT=0 make -j"${JOBS}" || BUILD_RESULT=$? diff --git a/GrabBagPrj/pkg_authregister.bat b/GrabBagPrj/pkg_authregister.bat index bd134d58..0cdef729 100644 --- a/GrabBagPrj/pkg_authregister.bat +++ b/GrabBagPrj/pkg_authregister.bat @@ -1,7 +1,8 @@ @echo off setlocal -set PRJ_PATH=c:\project\QT\GrabBag\GrabBagPrj +set PRJ_PATH=%~dp0 +set PRJ_PATH=%PRJ_PATH:~0,-1% if not exist "%PRJ_PATH%\buildwin\AuthRegister" mkdir "%PRJ_PATH%\buildwin\AuthRegister" diff --git a/GrabBagPrj/pkg_belttearingapp.bat b/GrabBagPrj/pkg_belttearingapp.bat index e3e40293..46fa89f6 100644 --- a/GrabBagPrj/pkg_belttearingapp.bat +++ b/GrabBagPrj/pkg_belttearingapp.bat @@ -1,7 +1,8 @@ @echo off setlocal -set PRJ_PATH=c:\project\QT\GrabBag\GrabBagPrj +set PRJ_PATH=%~dp0 +set PRJ_PATH=%PRJ_PATH:~0,-1% if not exist "%PRJ_PATH%\buildwin\BeltTearingApp" mkdir "%PRJ_PATH%\buildwin\BeltTearingApp" diff --git a/GrabBagPrj/pkg_binocularMarkTest.bat b/GrabBagPrj/pkg_binocularMarkTest.bat index 6bb1719d..2d7a042b 100644 --- a/GrabBagPrj/pkg_binocularMarkTest.bat +++ b/GrabBagPrj/pkg_binocularMarkTest.bat @@ -1,7 +1,8 @@ @echo off setlocal -set PRJ_PATH=c:\project\QT\GrabBag\GrabBagPrj +set PRJ_PATH=%~dp0 +set PRJ_PATH=%PRJ_PATH:~0,-1% cd /d "%PRJ_PATH%\buildwin\BinocularMarkTest" diff --git a/GrabBagPrj/pkg_calibview.bat b/GrabBagPrj/pkg_calibview.bat index 92b1da69..6dd912f0 100644 --- a/GrabBagPrj/pkg_calibview.bat +++ b/GrabBagPrj/pkg_calibview.bat @@ -1,7 +1,8 @@ @echo off setlocal -set PRJ_PATH=c:\project\QT\GrabBag\GrabBagPrj +set PRJ_PATH=%~dp0 +set PRJ_PATH=%PRJ_PATH:~0,-1% if not exist "%PRJ_PATH%\buildwin\CalibView" mkdir "%PRJ_PATH%\buildwin\CalibView" diff --git a/GrabBagPrj/pkg_cloudview.bat b/GrabBagPrj/pkg_cloudview.bat index a7228885..989b045a 100644 --- a/GrabBagPrj/pkg_cloudview.bat +++ b/GrabBagPrj/pkg_cloudview.bat @@ -1,7 +1,8 @@ @echo off setlocal -set PRJ_PATH=c:\project\QT\GrabBag\GrabBagPrj +set PRJ_PATH=%~dp0 +set PRJ_PATH=%PRJ_PATH:~0,-1% if not exist "%PRJ_PATH%\buildwin\CloudView" mkdir "%PRJ_PATH%\buildwin\CloudView" diff --git a/GrabBagPrj/pkg_controlapp.bat b/GrabBagPrj/pkg_controlapp.bat index c653fd42..739a9b7f 100644 --- a/GrabBagPrj/pkg_controlapp.bat +++ b/GrabBagPrj/pkg_controlapp.bat @@ -1,7 +1,8 @@ @echo off setlocal -set PRJ_PATH=c:\project\QT\GrabBag\GrabBagPrj +set PRJ_PATH=%~dp0 +set PRJ_PATH=%PRJ_PATH:~0,-1% if not exist "%PRJ_PATH%\buildwin\ControlApp" mkdir "%PRJ_PATH%\buildwin\ControlApp" diff --git a/GrabBagPrj/pkg_threadpool.bat b/GrabBagPrj/pkg_threadpool.bat new file mode 100644 index 00000000..43d909f2 --- /dev/null +++ b/GrabBagPrj/pkg_threadpool.bat @@ -0,0 +1,42 @@ +@echo off +setlocal + +REM project root = parent of this script's directory +cd /d "%~dp0.." +set PRJ_PATH=%cd% + +set PUB_PATH=%PRJ_PATH%\Publish\ThreadPool + +echo ======================================== +echo pkg ThreadPool +echo ======================================== + +if exist "%PUB_PATH%" ( + echo [WARN] dir already exists: %PUB_PATH% + echo [WARN] please delete it manually, then re-run. + pause + exit /b 1 +) +mkdir "%PUB_PATH%" + +REM 1. copy ThreadPool source (keep Inc / _Inc / Src structure) +echo [copy] ThreadPool source... +xcopy "%PRJ_PATH%\Module\ThreadPool\Inc\*.h" "%PUB_PATH%\Inc\" /Y /I /Q +xcopy "%PRJ_PATH%\Module\ThreadPool\_Inc\*.h" "%PUB_PATH%\_Inc\" /Y /I /Q +xcopy "%PRJ_PATH%\Module\ThreadPool\Src\*.cpp" "%PUB_PATH%\Src\" /Y /I /Q + +REM 2. copy demo +echo [copy] demo... +xcopy "%PRJ_PATH%\Test\TestThreadPool\main.cpp" "%PUB_PATH%\demo\" /Y /I /Q + +REM 3. copy project files +echo [copy] project files... +xcopy "%PRJ_PATH%\Module\ThreadPool\ThreadPool.pro" "%PUB_PATH%\" /Y /I /Q +xcopy "%PRJ_PATH%\Module\ThreadPool\ThreadPool.vcxproj" "%PUB_PATH%\" /Y /I /Q +xcopy "%PRJ_PATH%\Module\ThreadPool\ThreadPool.vcxproj.filters" "%PUB_PATH%\" /Y /I /Q + +echo ======================================== +echo done: %PUB_PATH% +echo ======================================== +dir /s /b "%PUB_PATH%" +endlocal diff --git a/GrabBagPrj/pkg_tireholepose.bat b/GrabBagPrj/pkg_tireholepose.bat new file mode 100644 index 00000000..bb1cdb1e --- /dev/null +++ b/GrabBagPrj/pkg_tireholepose.bat @@ -0,0 +1,25 @@ +@echo off +setlocal + +set PRJ_PATH=%~dp0 +set PRJ_PATH=%PRJ_PATH:~0,-1% + +if not exist "%PRJ_PATH%\buildwin\TireHolePose" mkdir "%PRJ_PATH%\buildwin\TireHolePose" + +cd /d "%PRJ_PATH%\buildwin\TireHolePose" + +powershell -Command "Remove-Item * -Recurse -Force" + +copy "..\..\build\App\TireHolePose\TireHolePoseApp\release\TireHolePoseApp.exe" .\ + +"C:\tools\Qt\5.15.2\msvc2019_64\bin\windeployqt.exe" "TireHolePoseApp.exe" + +cd ..\.. + +set ISCC_PATH=C:\Program Files (x86)\Inno Setup 6\ISCC.exe + +"%ISCC_PATH%" "TireHolePose.iss" + +echo "finish" + +endlocal diff --git a/GrabBagPrj/pkg_wdremotereceivertest.bat b/GrabBagPrj/pkg_wdremotereceivertest.bat new file mode 100644 index 00000000..9ec34983 --- /dev/null +++ b/GrabBagPrj/pkg_wdremotereceivertest.bat @@ -0,0 +1,52 @@ +@echo off +setlocal + +REM ============================================================ +REM WDRemoteReceiver 模块测试工具 - Windows 打包脚本 +REM +REM 输出:%PRJ_PATH%\buildwin\WDRemoteReceiverTest\ +REM ============================================================ + +set PRJ_PATH=%~dp0 +set PRJ_PATH=%PRJ_PATH:~0,-1% +set OUT_DIR=%PRJ_PATH%\buildwin\WDRemoteReceiverTest +set BUILD_DIR=%PRJ_PATH%\build\Test\WDRemoteReceiverTest\release +set WD_RECEIVER_DIR=%PRJ_PATH%\build\Module\WDRemoteReceiver\release + +if not exist "%OUT_DIR%" mkdir "%OUT_DIR%" +cd /d "%OUT_DIR%" + +REM 1) 拷贝可执行 +copy "%BUILD_DIR%\WDRemoteReceiverTest.exe" .\ +if errorlevel 1 ( + echo [ERROR] not found: %BUILD_DIR%\WDRemoteReceiverTest.exe + exit /b 1 +) + +REM 2) Qt 依赖部署 +REM 2) WDRemoteReceiver runtime +copy "%WD_RECEIVER_DIR%\WDRemoteReceiver.dll" .\ +if errorlevel 1 ( + echo [ERROR] not found: %WD_RECEIVER_DIR%\WDRemoteReceiver.dll + exit /b 1 +) + +"C:\tools\Qt\5.15.2\msvc2019_64\bin\windeployqt.exe" "WDRemoteReceiverTest.exe" + +REM 3) 拷贝 ZeroMQ Runtime +copy "%PRJ_PATH%\..\SDK\ZeroMQ\Windows\x64\Release\libzmq*.dll" .\ + +REM 4) 拷贝 Protobuf Runtime(如果需要) +REM 5) OpenCV runtime used by WDRemoteReceiver +copy "%PRJ_PATH%\..\SDK\OpenCV320\Windows\vc14\Release\opencv_world320.dll" .\ + +copy "%PRJ_PATH%\..\SDK\Protobuf_3_20\Windows\x64\Release\libprotobuf*.dll" .\ 2>nul + +cd /d "%PRJ_PATH%" + +echo. +echo === WDRemoteReceiverTest Windows 打包完成 === +echo 输出目录: %OUT_DIR% +echo. + +endlocal diff --git a/GrabBagPrj/project_meta.sh b/GrabBagPrj/project_meta.sh index 73d378a2..cff01fcc 100644 --- a/GrabBagPrj/project_meta.sh +++ b/GrabBagPrj/project_meta.sh @@ -27,6 +27,21 @@ _array_contains() { _trim_leading_spaces() { printf '%s' "${1#"${1%%[![:space:]]*}"}"; } +# 将逗号分隔的多条 spec 字符串拆分到指定数组里(空段忽略,自动去前导空格) +# 使用 read -ra 避免对 spec 中的 glob (*) 触发路径名展开 +# 用法: _split_csv_specs "" out_array +_split_csv_specs() { + local _csv=$1 + local -n _out=$2 + local _items _item _trimmed + [ -z "$_csv" ] && return 0 + IFS=',' read -ra _items <<< "$_csv" + for _item in "${_items[@]}"; do + _trimmed="$(_trim_leading_spaces "$_item")" + [ -n "$_trimmed" ] && _out+=("$_trimmed") + done +} + set_project_meta() { PROJECT_META["$1:$2"]=${3-}; } get_project_meta() { printf '%s' "${PROJECT_META["$1:$2"]:-}"; } @@ -71,6 +86,7 @@ load_package_project_meta() { PACKAGE_SERVICE_DEST_REL="$(get_project_meta "$project" package_service_dest_rel)" PACKAGE_LD_CONF_EXTRA_LINES="$(get_project_meta "$project" package_ld_conf_extra_lines)" PACKAGE_OPENCV_PROFILE="$(get_project_meta "$project" package_opencv_profile)" + PACKAGE_DEPENDS="$(get_project_meta "$project" package_depends)" local serialized serialized="${PROJECT_COPY_SPECS[$project]:-}" @@ -154,14 +170,25 @@ _finalize_project_meta() { fi fi - : "${meta_ref[package_icon_source_rel]:=${meta_ref[app_dir_rel]}/resource/logo.png}" - if [ "${meta_ref[package_kind]}" = "desktop" ]; then - : "${meta_ref[package_icon_dest_rel]:=usr/share/pixmaps/${package_slug}.png}" + local default_icon_rel="${meta_ref[app_dir_rel]}/resource/logo.png" + if [ -z "${meta_ref[package_icon_source_rel]}" ] && [ -f "${SCRIPT_DIR}/../${default_icon_rel}" ]; then + meta_ref[package_icon_source_rel]="${default_icon_rel}" + fi + if [ -n "${meta_ref[package_icon_source_rel]}" ]; then + : "${meta_ref[package_icon_dest_rel]:=usr/share/pixmaps/${package_slug}.png}" + fi fi if [ "${meta_ref[package_kind]}" = "service" ]; then - : "${meta_ref[package_service_source_rel]:=${meta_ref[app_dir_rel]}/${package_slug}.service}" + # 优先使用 service/ 子目录下的 service 文件,不存在则回退到 app_dir 根目录 + local _svc_in_service_dir="${meta_ref[app_dir_rel]}/service/${package_slug}.service" + local _svc_in_root="${meta_ref[app_dir_rel]}/${package_slug}.service" + if [ -f "${SCRIPT_DIR}/../${_svc_in_service_dir}" ]; then + : "${meta_ref[package_service_source_rel]:=$_svc_in_service_dir}" + else + : "${meta_ref[package_service_source_rel]:=$_svc_in_root}" + fi : "${meta_ref[package_service_dest_rel]:=opt/${package_slug}/${package_slug}.service}" fi fi @@ -196,6 +223,7 @@ _register_project() { [package_service_dest_rel]="" [package_ld_conf_extra_lines]="" [package_opencv_profile]="" + [package_depends]="" ) local kv key value @@ -269,7 +297,7 @@ _parse_kv_overrides() { _register_desktop_projects() { local entry fields name algo - local app_dir version_macro opencv algo_copy device_copy extra_copy optional_copy ld_conf description comment + local app_dir version_macro opencv algo_copy device_copy extra_copy optional_copy ld_conf description comment package_depends for entry in "${DESKTOP_PROJECTS[@]}"; do IFS=';' read -ra fields <<< "$entry" @@ -280,12 +308,14 @@ _register_desktop_projects() { app_dir="App/${name}/${name}App" version_macro=$(printf '%s' "$name" | tr '[:lower:]' '[:upper:]') opencv="" algo_copy="" device_copy="$COPY_VZNLSDK" extra_copy="" optional_copy="" - ld_conf="" description="" comment="" + ld_conf="" description="" comment="" package_depends="" _parse_kv_overrides fields 2 \ - app_dir version_macro opencv algo_copy device_copy extra_copy optional_copy ld_conf description comment + app_dir version_macro opencv algo_copy device_copy extra_copy optional_copy ld_conf description comment package_depends - : "${algo_copy:=glob|AppAlgo/${algo}/Arm/aarch64/*.so|usr/lib}" + if [ -z "$algo_copy" ] && [ "$algo" != "none" ]; then + algo_copy="glob|AppAlgo/${algo}/Arm/aarch64/*.so|usr/lib" + fi # register local reg_args=() @@ -293,18 +323,22 @@ _register_desktop_projects() { [ -n "$ld_conf" ] && reg_args+=("package_ld_conf_extra_lines=$ld_conf") [ -n "$description" ] && reg_args+=("package_description=$description") [ -n "$comment" ] && reg_args+=("package_comment=$comment") + [ -n "$package_depends" ] && reg_args+=("package_depends=$package_depends") _register_desktop_project "$name" "$app_dir" "$version_macro" "${reg_args[@]}" # copy specs - local specs=("$algo_copy") + local specs=() + [ -n "$algo_copy" ] && [ "$algo_copy" != "none" ] && specs+=("$algo_copy") [ -n "$device_copy" ] && [ "$device_copy" != "none" ] && specs+=("$device_copy") - [ -n "$extra_copy" ] && specs+=("$extra_copy") + _split_csv_specs "$extra_copy" specs _set_copy_specs "$name" "${specs[@]}" # optional copy - if [ -n "$optional_copy" ]; then - _set_optional_copy_specs "$name" "$optional_copy" + local opt_specs=() + _split_csv_specs "$optional_copy" opt_specs + if [ ${#opt_specs[@]} -gt 0 ]; then + _set_optional_copy_specs "$name" "${opt_specs[@]}" elif [ "$device_copy" != "$COPY_VZNLSDK" ]; then _set_optional_copy_specs "$name" "$COPY_VZNLSDK" fi @@ -313,7 +347,7 @@ _register_desktop_projects() { _register_service_projects() { local entry fields name algo - local app_suffix version_macro opencv device_copy description + local app_dir app_suffix version_macro opencv device_copy extra_copy optional_copy description package_depends for entry in "${SERVICE_PROJECTS[@]}"; do IFS=';' read -ra fields <<< "$entry" @@ -321,30 +355,45 @@ _register_service_projects() { algo="$(_trim_leading_spaces "${fields[1]}")" # 默认值 - app_suffix="Server" version_macro="" opencv="" - device_copy="$COPY_VZNLSDK" description="" + app_dir="" app_suffix="Server" version_macro="" opencv="" + device_copy="$COPY_VZNLSDK" extra_copy="" optional_copy="" description="" package_depends="" _parse_kv_overrides fields 2 \ - app_suffix version_macro opencv device_copy description + app_dir app_suffix version_macro opencv device_copy extra_copy optional_copy description package_depends + : "${app_dir:=App/${name}/${name}${app_suffix}}" : "${version_macro:=$(printf '%s' "$name" | tr '[:lower:]' '[:upper:]')}" # register local reg_args=() [ -n "$opencv" ] && reg_args+=("package_opencv_profile=$opencv") [ -n "$description" ] && reg_args+=("package_description=$description") + [ -n "$package_depends" ] && reg_args+=("package_depends=$package_depends") - _register_service_project "$name" "App/${name}/${name}${app_suffix}" "$version_macro" "${reg_args[@]}" - _set_copy_specs "$name" "glob|AppAlgo/${algo}/Arm/aarch64/*.so|usr/lib" "$device_copy" + _register_service_project "$name" "$app_dir" "$version_macro" "${reg_args[@]}" + + # copy specs + local specs=() + [ "$algo" != "none" ] && specs+=("glob|AppAlgo/${algo}/Arm/aarch64/*.so|usr/lib") + [ -n "$device_copy" ] && [ "$device_copy" != "none" ] && specs+=("$device_copy") + _split_csv_specs "$extra_copy" specs + _set_copy_specs "$name" "${specs[@]}" + + # optional copy + local opt_specs=() + _split_csv_specs "$optional_copy" opt_specs # 自动检测 config 目录 - local app_dir="App/${name}/${name}${app_suffix}" if [ -d "${SCRIPT_DIR}/../${app_dir}/config" ]; then local pkg_name pkg_name=$(get_project_meta "$name" package_name) local slug slug=$(printf '%s' "$pkg_name" | tr '[:upper:]' '[:lower:]') - _set_optional_copy_specs "$name" "dir|${app_dir}/config|opt/${slug}/config" + opt_specs+=("dir|${app_dir}/config|opt/${slug}/config") + fi + + if [ ${#opt_specs[@]} -gt 0 ]; then + _set_optional_copy_specs "$name" "${opt_specs[@]}" fi done } diff --git a/GrabBagPrj/project_pkg_common.sh b/GrabBagPrj/project_pkg_common.sh index 8ef77a33..895967e7 100644 --- a/GrabBagPrj/project_pkg_common.sh +++ b/GrabBagPrj/project_pkg_common.sh @@ -16,6 +16,21 @@ QT_LIB_PATH="/opt/sysroot/firefly-arm64-sysroot-20.04/lib/aarch64-linux-gnu" DEFAULT_OPENCV_PROFILE="opencv320" OPENCV320_LIB_GLOB="SDK/OpenCV320/Arm/aarch64/libopencv_*.so*" OPENCV480_LIB_GLOB="SDK/OpenCV480/Arm/aarch64/lib/libopencv_*.so*" +OPENCV450_LIB_DIR_CANDIDATES=( + "AppAlgo/stereo_bolt_delivery/lib/opencv" + "AppAlgo/stereo_bolt_delivery/lib" + "SDK/OpenCV450/Arm/aarch64/lib" + "SDK/OpenCV45D/Arm/aarch64/lib" + "SDK/OpenCV454/Arm/aarch64/lib" + "SDK/OpenCV45/Arm/aarch64/lib" +) +OPENCV450_REQUIRED_LIBS=( + "libopencv_calib3d.so.4.5" + "libopencv_imgcodecs.so.4.5" + "libopencv_dnn.so.4.5" + "libopencv_imgproc.so.4.5" + "libopencv_core.so.4.5" +) FIXED_PACKAGE_MAINTAINER="visionrobot" FIXED_QT_REMOVE_WEBENGINE=true FIXED_QT_REMOVE_NFC=true @@ -195,6 +210,16 @@ process_specs() { done } +resolve_package_source_path() { + local src=$1 + + if [[ "${src}" = /* ]]; then + printf '%s\n' "${src}" + else + printf '%s/%s\n' "${REPO_DIR}" "${src}" + fi +} + apply_package_defaults() { PACKAGE_SLUG=$(printf '%s' "${PACKAGE_NAME}" | tr '[:upper:]' '[:lower:]') PACKAGE_SECTION="${PACKAGE_SLUG}app" @@ -258,6 +283,10 @@ copy_opencv_runtime() { opencv480) src_pattern="${OPENCV480_LIB_GLOB}" ;; + opencv450|opencv45d) + copy_opencv450_runtime + return 0 + ;; none) return 0 ;; @@ -271,6 +300,60 @@ copy_opencv_runtime() { copy_glob_items "${src_pattern}" "usr/lib" "globa" true } +copy_opencv450_runtime() { + local candidates=() + local candidate="" + local candidate_abs="" + local selected_dir="" + local lib="" + local missing=false + + if [ -n "${DRONESCREW_OPENCV450_LIB_DIR:-}" ]; then + candidates+=("${DRONESCREW_OPENCV450_LIB_DIR}") + fi + candidates+=("${OPENCV450_LIB_DIR_CANDIDATES[@]}") + + for candidate in "${candidates[@]}"; do + candidate_abs="$(resolve_package_source_path "${candidate}")" + [ -d "${candidate_abs}" ] || continue + + missing=false + for lib in "${OPENCV450_REQUIRED_LIBS[@]}"; do + if [ ! -e "${candidate_abs}/${lib}" ]; then + missing=true + break + fi + done + + if [ "${missing}" = false ]; then + selected_dir="${candidate_abs}" + break + fi + done + + if [ -z "${selected_dir}" ]; then + { + echo "Missing OpenCV 4.5.0 runtime for ${PACKAGE_NAME}." + echo "libstereo_bolt.so requires these exact sonames:" + printf ' %s\n' "${OPENCV450_REQUIRED_LIBS[@]}" + echo "Put them in AppAlgo/stereo_bolt_delivery/lib/opencv," + echo "or set DRONESCREW_OPENCV450_LIB_DIR to the directory that contains them." + } >&2 + exit 1 + fi + + log_info "Copy OpenCV runtime (opencv450) from ${selected_dir}" + mkdir -p "${PKG_PATH}/usr/lib" + + shopt -s nullglob + local opencv_libs=( "${selected_dir}"/libopencv_*.so.4.5* ) + shopt -u nullglob + + for lib in "${opencv_libs[@]}"; do + cp -a "${lib}" "${PKG_PATH}/usr/lib/" + done +} + copy_required_files() { copy_opencv_runtime process_specs true "${PACKAGE_COPY_SPECS[@]}" @@ -297,6 +380,10 @@ Priority: optional Maintainer: ${PACKAGE_MAINTAINER} Description: ${PACKAGE_DESCRIPTION} EOF + + if [ -n "${PACKAGE_DEPENDS:-}" ]; then + printf 'Depends: %s\n' "${PACKAGE_DEPENDS}" >> "${PKG_PATH}/DEBIAN/control" + fi } set_permissions() { @@ -366,7 +453,11 @@ load_package_context() { DEB_FILENAME="${RELEASE_PATH}/${PACKAGE_NAME}_${PKG_VERSION}_${BUILD_NUMBER}_${PKG_ARCH}_${TIMESTAMP}.deb" DESKTOP_FILE_NAME="${PACKAGE_SLUG}.desktop" EXEC_INSTALL_PATH="/${PACKAGE_BINARY_DEST_REL}" - ICON_INSTALL_PATH="/${PACKAGE_ICON_DEST_REL}" + if [ -n "${PACKAGE_ICON_DEST_REL}" ]; then + ICON_INSTALL_PATH="/${PACKAGE_ICON_DEST_REL}" + else + ICON_INSTALL_PATH="application-x-executable" + fi SERVICE_INSTALL_DIR="/opt/${PACKAGE_SLUG}" echo "加载项目 ${PACKAGE_TARGET} 上下文完成" @@ -401,7 +492,11 @@ finalize_package() { log_info "设置文件权限" set_permissions log_info "生成 DEB 安装包" - fakeroot dpkg -b "${PKG_PATH}" "${DEB_FILENAME}" >/dev/null + + # 使用低压缩等级避免 OOM(大包 + xz 默认等级 6 会吃几百 MB 内存) + # 可通过 DEB_COMPRESS_OPTS 环境变量覆盖,例如 DEB_COMPRESS_OPTS="-Zgzip" 或 "-Zxz -z9" + local compress_opts="${DEB_COMPRESS_OPTS:--Zxz -z1}" + fakeroot dpkg-deb ${compress_opts} --build "${PKG_PATH}" "${DEB_FILENAME}" >/dev/null log_info "安装包已生成: ${DEB_FILENAME}" log_info "安装包大小: $(ls -lh "${DEB_FILENAME}" | awk '{print $5}')" diff --git a/GrabBagPrj/project_pkg_service.sh b/GrabBagPrj/project_pkg_service.sh index d144db54..44117a44 100644 --- a/GrabBagPrj/project_pkg_service.sh +++ b/GrabBagPrj/project_pkg_service.sh @@ -13,27 +13,114 @@ set -e SERVICE_SOURCE="${SERVICE_INSTALL_PATH}" SERVICE_UNIT="${SERVICE_UNIT_NAME}" SERVICE_TARGET="/etc/systemd/system/\${SERVICE_UNIT}" +LD_CONF_FILE="/etc/ld.so.conf.d/${PACKAGE_SLUG}.conf" +PKG_NAME="${PACKAGE_NAME}" +PKG_VERSION="${PKG_RELEASE_VERSION}" +INSTALL_DIR="${SERVICE_INSTALL_DIR}" +log() { echo "[\${PKG_NAME}] \$*"; } +fail() { echo "[\${PKG_NAME}] [FAIL] \$*" >&2; exit 1; } + +log "==========================================" +log "开始安装 \${PKG_NAME} \${PKG_VERSION}" +log " 安装目录: \${INSTALL_DIR}" +log " 服务单元: \${SERVICE_UNIT}" +log "==========================================" + +# 1. 配置动态库搜索路径 +log "[1/6] 配置动态库搜索路径 \${LD_CONF_FILE}" +cat > "\${LD_CONF_FILE}" <<'LDEOF' +/usr/lib +/opt/sysroot/lib/ +LDEOF ldconfig +log " ldconfig 完成" -if [ ! -f "\${SERVICE_SOURCE}" ]; then - echo "缺少服务文件: \${SERVICE_SOURCE}" >&2 - exit 1 +# 2. DroneScrewServer 专用:启动 mediamtx +if [ "\${PKG_NAME}" = "DroneScrewServer" ]; then + mkdir -p "\${INSTALL_DIR}/logs" + log "[2/6] 检测并启动 mediamtx RTSP 服务器" + MEDIAMTX_BIN="\${INSTALL_DIR}/bin/mediamtx" + MEDIAMTX_CFG="\${INSTALL_DIR}/etc/mediamtx_dronescrew.yml" + MEDIAMTX_SERVICE="\${INSTALL_DIR}/etc/mediamtx-dronescrew.service" + + if [ -f "\${MEDIAMTX_BIN}" ] && [ -f "\${MEDIAMTX_CFG}" ] && [ -f "\${MEDIAMTX_SERVICE}" ]; then + chmod +x "\${MEDIAMTX_BIN}" + + # 复制 service 文件到 systemd 目录 + cp "\${MEDIAMTX_SERVICE}" /etc/systemd/system/mediamtx-dronescrew.service + chmod 644 /etc/systemd/system/mediamtx-dronescrew.service + + systemctl daemon-reload + systemctl enable mediamtx-dronescrew.service >/dev/null 2>&1 || true + systemctl restart mediamtx-dronescrew.service + sleep 1 + + if systemctl is-active --quiet mediamtx-dronescrew.service; then + log " [OK] mediamtx 已启动 (端口 8554)" + else + log " [WARN] mediamtx 启动失败,DroneScrewServer RTSP 推流可能不可用" + log " 手动检查: journalctl -u mediamtx-dronescrew.service -n 20" + fi + else + log " [WARN] mediamtx 文件缺失,跳过启动" + fi +else + log "[2/6] 跳过 mediamtx(非 DroneScrewServer)" fi +# 3. 校验服务文件 +log "[3/6] 校验服务文件" +[ -f "\${SERVICE_SOURCE}" ] || fail "缺少服务文件: \${SERVICE_SOURCE}" +log " OK: \${SERVICE_SOURCE}" + +# 4. 停止/禁用旧服务 +log "[4/6] 停止旧服务(如在运行)" if systemctl is-active --quiet "\${SERVICE_UNIT}" 2>/dev/null; then systemctl stop "\${SERVICE_UNIT}" + log " 已停止 \${SERVICE_UNIT}" +else + log " 旧服务未运行,跳过" fi - if systemctl is-enabled --quiet "\${SERVICE_UNIT}" 2>/dev/null; then - systemctl disable "\${SERVICE_UNIT}" + systemctl disable "\${SERVICE_UNIT}" >/dev/null 2>&1 || true + log " 已禁用旧 unit" fi +# 5. 注册新 unit +log "[5/6] 注册并启用新 unit" cp "\${SERVICE_SOURCE}" "\${SERVICE_TARGET}" chmod 644 "\${SERVICE_TARGET}" systemctl daemon-reload -systemctl enable "\${SERVICE_UNIT}" +systemctl enable "\${SERVICE_UNIT}" >/dev/null 2>&1 +log " 已 enable \${SERVICE_UNIT}" + +# 6. 启动并验证 +log "[6/6] 启动服务" +set +e systemctl start "\${SERVICE_UNIT}" +START_RC=\$? +set -e +sleep 1 + +if systemctl is-active --quiet "\${SERVICE_UNIT}"; then + log " [OK] \${SERVICE_UNIT} 已在运行" + systemctl status "\${SERVICE_UNIT}" --no-pager --lines=3 || true + log "==========================================" + log "\${PKG_NAME} 安装成功" + log " 查看实时日志: journalctl -u \${SERVICE_UNIT} -f" + log " 停止服务: systemctl stop \${SERVICE_UNIT}" + if [ "\${PKG_NAME}" = "DroneScrewServer" ]; then + log " mediamtx 日志: journalctl -u mediamtx-dronescrew.service -f" + log " RTSP 拉流地址: rtsp://<板子IP>:8554/live/dronescrew" + fi + log "==========================================" +else + log " [FAIL] \${SERVICE_UNIT} 未能启动 (start rc=\${START_RC})" + log " 最近 20 行日志:" + journalctl -u "\${SERVICE_UNIT}" --no-pager --lines=20 || true + fail "服务启动失败,请检查上面的日志" +fi EOF chmod +x "${PKG_PATH}/DEBIAN/postinst" @@ -45,14 +132,50 @@ write_service_postrm() { set -e SERVICE_UNIT="${SERVICE_UNIT_NAME}" +LD_CONF_FILE="/etc/ld.so.conf.d/${PACKAGE_SLUG}.conf" +PKG_NAME="${PACKAGE_NAME}" +INSTALL_DIR="${SERVICE_INSTALL_DIR}" +ACTION="\${1:-}" -systemctl stop "\${SERVICE_UNIT}" 2>/dev/null || true -systemctl disable "\${SERVICE_UNIT}" 2>/dev/null || true +log() { echo "[\${PKG_NAME}] \$*"; } + +log "==========================================" +log "卸载 \${PKG_NAME} (动作: \${ACTION})" +log "==========================================" + +log "[1/4] 停止并禁用 \${SERVICE_UNIT}" +systemctl stop "\${SERVICE_UNIT}" 2>/dev/null && log " 已停止" || log " 未在运行" +systemctl disable "\${SERVICE_UNIT}" >/dev/null 2>&1 && log " 已禁用" || true rm -f "/etc/systemd/system/\${SERVICE_UNIT}" systemctl daemon-reload -if [ "\${1:-}" = "remove" ]; then - rm -rf "${SERVICE_INSTALL_DIR}" +# DroneScrewServer 专用:停止 mediamtx +if [ "\${PKG_NAME}" = "DroneScrewServer" ]; then + log "[2/4] 停止并删除 mediamtx 服务" + if systemctl is-active --quiet mediamtx-dronescrew.service 2>/dev/null; then + systemctl stop mediamtx-dronescrew.service + log " 已停止 mediamtx" + fi + systemctl disable mediamtx-dronescrew.service >/dev/null 2>&1 || true + rm -f /etc/systemd/system/mediamtx-dronescrew.service + systemctl daemon-reload + log " mediamtx 服务已移除" +else + log "[2/4] 跳过 mediamtx(非 DroneScrewServer)" +fi + +if [ "\${ACTION}" = "remove" ] || [ "\${ACTION}" = "purge" ]; then + log "[3/4] 清理安装目录 \${INSTALL_DIR}" + rm -rf "\${INSTALL_DIR}" + log "[4/4] 清理 ld.so.conf 并刷新缓存" + rm -f "\${LD_CONF_FILE}" + ldconfig + log "==========================================" + log "\${PKG_NAME} 卸载完成" + log "==========================================" +else + log "[3/4] 跳过目录清理(仅升级阶段,保留安装目录)" + log "[4/4] 跳过 ld.so.conf 清理" fi EOF diff --git a/GrabBagPrj/project_registry.sh b/GrabBagPrj/project_registry.sh index 2ca0d992..e6819ef6 100644 --- a/GrabBagPrj/project_registry.sh +++ b/GrabBagPrj/project_registry.sh @@ -25,8 +25,12 @@ # # 可选覆盖 (key=value): # 通用: app_dir version_macro opencv device_copy(none=不复制) description comment +# package_depends(DEB Depends 字段,逗号分隔) # 桌面: algo_copy extra_copy optional_copy ld_conf -# 服务: app_suffix(默认 Server) +# 服务: app_dir app_suffix(默认 Server) extra_copy optional_copy +# +# 多条 spec: extra_copy 与 optional_copy 支持以英文逗号分隔多条复制规则 +# 例: extra_copy=glob|A/*.so|usr/lib,glob|B/*.so|usr/lib # ============================================================================= SCRIPT_DIR="${SCRIPT_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)}" @@ -34,51 +38,105 @@ source "${SCRIPT_DIR}/project_meta.sh" COPY_VZNLSDK="glob|SDK/Device/VzNLSDK/Arm/aarch64/*.so|usr/lib" +# 复制编译生成的动态库 +# 说明:项目内部库绝大多数是静态库(CONFIG += staticlib),已在链接阶段 +# 打进可执行文件,运行时不需要 .so。实测各目录产出: +# Device / Utils / VrNets —— 全为静态库,不产出 .so +# Module —— 含 3 个动态库(BinocularMarkReceiver / +# FFMediaStream / WDRemoteReceiver),需随包部署 +COPY_DEVICE_LIBS="globa|GrabBagPrj/buildarm/Device/*/*.so*|usr/lib" +COPY_MODULE_LIBS="globa|GrabBagPrj/buildarm/Module/*/*.so*|usr/lib" +COPY_UTILS_LIBS="globa|GrabBagPrj/buildarm/Utils/*/*.so*|usr/lib" +COPY_VRNETS_LIBS="globa|GrabBagPrj/buildarm/VrNets/*.so*|usr/lib" + +# 合并编译生成的动态库:当前仅 Module 实际产出 .so。 +# 这些规则走 required(extra_copy),空匹配会导致打包失败, +# 因此只能收集确实存在 .so 的目录。若将来 Device/Utils/VrNets +# 某目录新增动态库(CONFIG += shared),把对应 COPY_*_LIBS 追加到下方即可。 +COPY_ALL_BUILD_LIBS="${COPY_MODULE_LIBS}" + # ===== 桌面项目 ===== DESKTOP_PROJECTS=( - "GrabBag;bagPosition" - "LapWeld;lapWeldDetection" - "Workpiece;workpieceCornerExtraction" - "ParticleSize;particleSizeMeasure" - "ScrewPosition;rodAndBarDetection" - "WorkpieceHole;workpieceHolePositioning" - "StatorPosition;motorStatorPosition" - "RodAndBarPosition;rodAndBarDetection" - "RodWeldSeam;rodAndBarDetection" - "HolePitPosition;workpieceHolePositioning" - "WheelMeasure;wheelArchHeigthMeasure" + "GrabBag;bagPosition;\ + extra_copy=${COPY_ALL_BUILD_LIBS}" + "LapWeld;lapWeldDetection;\ + extra_copy=${COPY_ALL_BUILD_LIBS}" + "Workpiece;workpieceCornerExtraction;\ + extra_copy=${COPY_ALL_BUILD_LIBS}" + "ParticleSize;particleSizeMeasure;\ + extra_copy=${COPY_ALL_BUILD_LIBS}" + "ScrewPosition;rodAndBarDetection;\ + extra_copy=${COPY_ALL_BUILD_LIBS}" + "WorkpieceHole;workpieceHolePositioning;\ + extra_copy=${COPY_ALL_BUILD_LIBS}" + "DiscHolePose;workpieceHolePositioning;\ + extra_copy=${COPY_ALL_BUILD_LIBS}" + "TireHolePose;workpieceHolePositioning;\ + extra_copy=${COPY_ALL_BUILD_LIBS}" + "StatorPosition;motorStatorPosition;\ + extra_copy=${COPY_ALL_BUILD_LIBS}" + "RodAndBarPosition;rodAndBarDetection;\ + extra_copy=${COPY_ALL_BUILD_LIBS}" + "RodWeldSeam;rodAndBarDetection;\ + extra_copy=${COPY_ALL_BUILD_LIBS}" + "HolePitPosition;workpieceHolePositioning;\ + extra_copy=${COPY_ALL_BUILD_LIBS}" + "WheelMeasure;wheelArchHeigthMeasure;\ + extra_copy=${COPY_ALL_BUILD_LIBS}" "BagThreadPosition;bagThreadPositioning;\ - extra_copy=glob|SDK/Device/gl_linelaser_sdk/aarch64_linux/*.so|usr/lib" + extra_copy=${COPY_ALL_BUILD_LIBS},glob|SDK/Device/gl_linelaser_sdk/aarch64_linux/*.so|usr/lib" "HoleDetection;holeDetection;\ - algo_copy=globa|AppAlgo/holeDetection/arm/release/*.so*|usr/lib" + algo_copy=globa|AppAlgo/holeDetection/arm/release/*.so*|usr/lib;\ + extra_copy=${COPY_ALL_BUILD_LIBS}" "TunnelChannel;channelSpaceMeasure;\ ld_conf=/usr/lib/HCNetSDKCom;\ - extra_copy=globa|SDK/Device/HCNetSDK/Arm64/lib/*.so*|usr/lib;\ + extra_copy=${COPY_ALL_BUILD_LIBS},globa|SDK/Device/HCNetSDK/Arm64/lib/*.so*|usr/lib;\ optional_copy=globa|SDK/Device/HCNetSDK/Arm64/lib/HCNetSDKCom/*.so*|usr/lib/HCNetSDKCom" "WorkpiecePosition;binocularMark;\ app_dir=App/WorkpieceProject/WorkpiecePositionApp;\ version_macro=WORKPIECEPOSITION;\ opencv=opencv480;\ - device_copy=globa|SDK/Device/EpicEye/lib/arm/*.so*|usr/lib" + device_copy=globa|SDK/Device/EpicEye/lib/arm/*.so*|usr/lib;\ + extra_copy=${COPY_ALL_BUILD_LIBS}" "WorkpieceSplice;binocularMark;\ app_dir=App/WorkpieceProject/WorkpieceSpliceApp;\ version_macro=WORKPIECESPLICE;\ opencv=opencv480;\ - device_copy=none" + device_copy=none;\ + extra_copy=${COPY_ALL_BUILD_LIBS}" + + "DroneScrewCtrlApp;none;\ + app_dir=App/DroneScrewbolt/DroneScrewCtrlApp;\ + version_macro=DRONESCREWCTRL;\ + device_copy=none;\ + extra_copy=${COPY_ALL_BUILD_LIBS},globa|SDK/ZeroMQ/Arm/aarch64/libzmq.so*|usr/lib;\ + description=DroneScrew Ctrl (board 2)" ) # ===== 服务项目 ===== SERVICE_PROJECTS=( - "BeltTearing;beltTearing" + "BeltTearing;beltTearing;\ + extra_copy=${COPY_ALL_BUILD_LIBS}" "BinocularMark;binocularMark;\ opencv=opencv480;\ - device_copy=glob|SDK/Device/Galaxy/aarch64/lib/armv8/*|usr/lib" + device_copy=glob|SDK/Device/Galaxy/aarch64/lib/armv8/*|usr/lib;\ + extra_copy=${COPY_ALL_BUILD_LIBS}" + + "DroneScrewServer;none;\ + app_dir=App/DroneScrewbolt/DroneScrewServer;\ + version_macro=DRONESCREWSERVER;\ + app_suffix=Server;\ + opencv=opencv450;\ + device_copy=globa|SDK/Device/MVS/MVS_STD_V3.0.1_251113/MVS-3.0.1_aarch64_20251113/lib/aarch64/*.so*|usr/lib;\ + extra_copy=${COPY_ALL_BUILD_LIBS},globa|SDK/ZeroMQ/Arm/aarch64/libzmq.so*|usr/lib,file|SDK/mediamtx/arm64/mediamtx|opt/dronescrewserver/bin/mediamtx,file|App/DroneScrewbolt/DroneScrewServer/service/mediamtx_dronescrew.yml|opt/dronescrewserver/etc/mediamtx_dronescrew.yml,file|App/DroneScrewbolt/DroneScrewServer/service/mediamtx-dronescrew.service|opt/dronescrewserver/etc/mediamtx-dronescrew.service,globa|AppAlgo/stereo_bolt_delivery/lib/*.so*|usr/lib,dir|AppAlgo/stereo_bolt_delivery/config|opt/dronescrewserver/config,dir|AppAlgo/stereo_bolt_delivery/calib|opt/dronescrewserver/calib,dir|AppAlgo/stereo_bolt_delivery/weights|opt/dronescrewserver/weights;\ + optional_copy=globa|SDK/Protobuf_3_20/Arm/aarch64/libprotobuf.so*|usr/lib,globa|SDK/Device/MVS/MVS_STD_V3.0.1_251113/MVS-3.0.1_aarch64_20251113/lib/aarch64/ThirdParty/*.so*|usr/lib;\ + description=DroneScrew Server (board 1)" ) # ===== 执行注册 ===== diff --git a/GrabBagPrj/project_updateversion.sh b/GrabBagPrj/project_updateversion.sh index 0b28e34d..382c71f3 100644 --- a/GrabBagPrj/project_updateversion.sh +++ b/GrabBagPrj/project_updateversion.sh @@ -58,8 +58,27 @@ echo "版本文件: ${VERSION_FILE}" echo "完整版本号: ${FULL_VERSION}" if ! grep -qE "\\|[[:space:]]*${PUBLISH_NAME}[[:space:]]*\\|" "${APP_LIST_FILE}"; then - echo "错误: AppList.md 中未找到 ${PUBLISH_NAME}" >&2 - exit 1 + echo "警告: AppList.md 中未找到 ${PUBLISH_NAME},自动添加..." + + # 获取当前最大序号 + max_num=$(awk -F'|' 'NF >= 6 && $2 ~ /^[[:space:]]*[0-9]+[[:space:]]*$/ { + num = $2 + gsub(/^[[:space:]]+|[[:space:]]+$/, "", num) + if (num > max) max = num + } END { print max+0 }' "${APP_LIST_FILE}") + + next_num=$((max_num + 1)) + + # 获取项目描述(如果有) + project_desc=$(get_project_meta "${VERSION_PROJECT}" package_description) + if [ -z "$project_desc" ]; then + project_desc="${VERSION_PROJECT}" + fi + + # 在表格末尾(最后一个空行之前)添加新行 + sed -i "/^$/i | ${next_num} | ${project_desc} | ${PUBLISH_NAME} | ${FULL_VERSION} |" "${APP_LIST_FILE}" + + echo "已添加 ${PUBLISH_NAME} 到 AppList.md (序号: ${next_num})" fi tmp_file="$(mktemp)" diff --git a/Module/HandEyeCalib/Src/HandEyeCalib.cpp b/Module/HandEyeCalib/Src/HandEyeCalib.cpp index f4c7ac83..c12dfdbc 100644 --- a/Module/HandEyeCalib/Src/HandEyeCalib.cpp +++ b/Module/HandEyeCalib/Src/HandEyeCalib.cpp @@ -5,6 +5,53 @@ #include #include +namespace +{ + +double NormalizeAnglePi(double angle) +{ + double normalized = std::fmod(angle + M_PI, 2.0 * M_PI); + if (normalized < 0.0) { + normalized += 2.0 * M_PI; + } + return normalized - M_PI; +} + +void GetEulerOrderAxes(HECEulerOrder order, int axes[3]) +{ + switch (order) { + case HECEulerOrder::XYZ: axes[0] = 0; axes[1] = 1; axes[2] = 2; break; + case HECEulerOrder::XZY: axes[0] = 0; axes[1] = 2; axes[2] = 1; break; + case HECEulerOrder::YXZ: axes[0] = 1; axes[1] = 0; axes[2] = 2; break; + case HECEulerOrder::YZX: axes[0] = 1; axes[1] = 2; axes[2] = 0; break; + case HECEulerOrder::ZXY: axes[0] = 2; axes[1] = 0; axes[2] = 1; break; + case HECEulerOrder::ZYX: axes[0] = 2; axes[1] = 1; axes[2] = 0; break; + } +} + +void NormalizeEulerBranch(HECEulerOrder order, HECEulerAngles& angles) +{ + double values[3] = { angles.roll, angles.pitch, angles.yaw }; + int axes[3] = { 0, 1, 2 }; + GetEulerOrderAxes(order, axes); + + if (std::abs(values[axes[1]]) > M_PI / 2.0) { + values[axes[0]] = NormalizeAnglePi(values[axes[0]] + M_PI); + values[axes[1]] = NormalizeAnglePi(M_PI - values[axes[1]]); + values[axes[2]] = NormalizeAnglePi(values[axes[2]] + M_PI); + } else { + values[0] = NormalizeAnglePi(values[0]); + values[1] = NormalizeAnglePi(values[1]); + values[2] = NormalizeAnglePi(values[2]); + } + + angles.roll = values[0]; + angles.pitch = values[1]; + angles.yaw = values[2]; +} + +} // namespace + HandEyeCalib::HandEyeCalib() { } @@ -195,6 +242,8 @@ void HandEyeCalib::RotationMatrixToEuler( angles.roll = ea[0]; angles.pitch = ea[1]; angles.yaw = ea[2]; break; } + + NormalizeEulerBranch(order, angles); } void HandEyeCalib::EulerToRotationMatrix( diff --git a/Module/WDRemoteReceiver/Inc/IWDRemoteReceiver.h b/Module/WDRemoteReceiver/Inc/IWDRemoteReceiver.h new file mode 100644 index 00000000..e70bc28e --- /dev/null +++ b/Module/WDRemoteReceiver/Inc/IWDRemoteReceiver.h @@ -0,0 +1,118 @@ +#ifndef IWDREMOTERECEIVER_H +#define IWDREMOTERECEIVER_H + +#include "WDRemoteDataTypes.h" +#include +#include +#include + +// 导出宏定义 +#ifdef _WIN32 +# ifdef WDREMOTERECEIVER_LIBRARY +# define WDREMOTERECEIVER_EXPORT __declspec(dllexport) +# else +# define WDREMOTERECEIVER_EXPORT __declspec(dllimport) +# endif +#else +# define WDREMOTERECEIVER_EXPORT __attribute__((visibility("default"))) +#endif + +/** + * @brief 远程接收器接口 + * + * 把对 ZMQ 控制通道 + 结果订阅 + 原始图像订阅的访问封装为一个动态库, + * 不绑定到具体业务,下游项目可以直接复用。 + * + * 内部使用: + * - IVrZeroMQClient (控制 REQ-REP) + * - IVrZeroMQSubscriber (结果 PUB-SUB,topic="result") + * - IVrZeroMQSubscriber (原始图像 PUB-SUB,topic="raw") + * + * 典型用法: + * IWDRemoteReceiver* p = nullptr; + * IWDRemoteReceiver::CreateInstance(&p); + * p->SetEventCallback(...); + * p->SetDetectionCallback(...); + * p->SetRawImageCallback(...); // 双目图像 + * std::vector devices; + * p->SearchDevices(devices); // UDP 广播发现设备 + * p->Open(devices[0].machineCode); // 按机器码打开指定设备 + * p->StartWork(WDRemoteWorkMode::Detection); + * ... + * p->StopWork(); // 发送停止指令 + * p->Disconnect(); // 断开所有通道 + */ +class WDREMOTERECEIVER_EXPORT IWDRemoteReceiver +{ +public: + virtual ~IWDRemoteReceiver() = default; + + /** + * @brief 连接参数 + */ + struct ConnectConfig + { + std::string serverIp{"127.0.0.1"}; + int controlPort{5555}; // ZMQ REQ-REP 控制 + int resultPort{5556}; // ZMQ PUB 检测结果 + int rawImagePort{5557}; // ZMQ PUB 原始图像,0 表示不启用 + }; + + // 回调类型 + using DetectionCallback = std::function; + using RawImageCallback = std::function; + using EventCallback = std::function; + + // 连接 / 断开。推荐使用 SearchDevices + Open;Connect 保留给兼容旧调用。 + virtual int Connect(const ConnectConfig& cfg) = 0; + virtual int SearchDevices(std::vector& devices, + int timeoutMs = 150) = 0; + virtual int Open(const std::string& machineCode, + int timeoutMs = 3000) = 0; + virtual int Disconnect() = 0; + virtual bool IsConnected() const = 0; + + // 控制接口(内部同步走 ZMQ REQ-REP) + virtual int StartWork(int timeoutMs = 3000) = 0; + virtual int StartWork(WDRemoteWorkMode mode, int timeoutMs = 3000) = 0; + + virtual int StopWork (int timeoutMs = 3000) = 0; + virtual int StopWork (WDRemoteWorkMode mode, int timeoutMs = 3000) = 0; + virtual int RequestSingleDetection(int timeoutMs = 15000) = 0; + virtual int SetExposure(double exposureTime, int timeoutMs = 3000) = 0; + virtual int SetGain (double gain, int timeoutMs = 3000) = 0; + virtual int SetAlgoParams(const WDRemoteAlgoParams& params, int timeoutMs = 3000) = 0; + virtual WDRemoteServerInfo GetServerInfo(int timeoutMs = 3000) = 0; + + /** + * @brief 设置检测模式下的图像传输模式 + * @param mode "binocular"(默认,发送左右目)或 "mono"(仅发送左目) + * + * 在调用 StartWork(Detection) 之前设置; + * 仅在检测模式下生效,实时传图模式不受影响。 + */ + virtual void SetDetectMode(const std::string& mode) = 0; + + /** + * @brief 自定义控制命令(透传 JSON 给服务端 ZMQ REP socket) + * + * 服务端目前不识别的命令会回 "unknown cmd"。 + * 上层可借此扩展,无需修改库本身。 + */ + virtual int SendCustomCommand(const std::string& jsonReq, + std::string& jsonResp, + int timeoutMs = 3000) = 0; + + // 回调注入 + virtual void SetDetectionCallback(DetectionCallback cb) = 0; + virtual void SetRawImageCallback (RawImageCallback cb) = 0; // 双目图像 + virtual void SetEventCallback (EventCallback cb) = 0; + + /** + * @brief 创建实例 + */ + static int CreateInstance(IWDRemoteReceiver** ppReceiver); +}; + +#endif // IWDREMOTERECEIVER_H diff --git a/Module/WDRemoteReceiver/Inc/WDRemoteDataTypes.h b/Module/WDRemoteReceiver/Inc/WDRemoteDataTypes.h new file mode 100644 index 00000000..0b4bf01e --- /dev/null +++ b/Module/WDRemoteReceiver/Inc/WDRemoteDataTypes.h @@ -0,0 +1,357 @@ +#ifndef WDREMOTEDATATYPES_H +#define WDREMOTEDATATYPES_H + +#include +#include +#include +#include +#include + +/** + * @file WDRemoteDataTypes.h + * @brief WDRemoteReceiver 数据类型定义 + * + * 本文件包含所有 WDRemoteReceiver 使用的数据结构、枚举和常量定义。 + */ + +/** + * @brief 远程图像像素格式 + * + * 取值与服务端 ZMQ 原始图像 PUB 的 pixelFormat 字段保持一致。 + */ +enum class WDRemotePixelFormat : uint32_t +{ + UNKNOWN = 0xFFFFFFFFu, + MONO8 = 0, + BGR8 = 1, + RGB8 = 2, + BAYERRG8 = 3, + NV12 = 4, +}; + +/** + * @brief 事件类型 + */ +enum class WDRemoteEventType +{ + UNKNOWN, + CONNECTED, // 控制通道已建立 + DISCONNECTED, // 控制通道已断开 + CONNECTION_ERROR, // 连接错误 + REQUEST_TIMEOUT, // 同步请求超时 +}; + +/** + * @brief 远程服务端的检测目标矩形框(图像像素坐标) + * + * 字段与服务端 DroneScrewBox 一一对应,但作为对外通用结构使用, + * 不绑定到任何具体业务结构体。 + */ +struct WDRemoteDetectionBox +{ + int classId{0}; + float score{0.0f}; + int x{0}; + int y{0}; + int width{0}; + int height{0}; +}; + +/** + * @brief 远程单帧检测结果 + */ +// Stereo distance result in millimeters. +struct WDRemoteDetectionDistance +{ + int fromId{0}; + int toId{0}; + float distanceMm{0.0f}; +}; + +struct WDRemoteDetectionFrame +{ + uint64_t frameId{0}; + int64_t timestampUs{0}; + int imageWidth{0}; + int imageHeight{0}; + bool success{true}; + int errorCode{0}; + std::string message; + std::vector boxes; + std::vector distances; +}; + +/** + * @brief 远程算法参数(透传给服务端) + */ +struct WDRemoteAlgoParams +{ + float scoreThreshold{0.5f}; + float nmsThreshold{0.45f}; + int inputWidth{640}; + int inputHeight{640}; + std::string modelPath; + int modelType{0}; // 0=YOLO, 1=自定义 +}; + +struct WDRemoteMatrix +{ + int rows{0}; + int cols{0}; + std::vector data; + + bool empty() const { return rows <= 0 || cols <= 0 || data.empty(); } +}; + +struct WDRemoteCameraCalibration +{ + bool valid{false}; + int imageWidth{0}; + int imageHeight{0}; + double rms{0.0}; + WDRemoteMatrix cameraMatrix; + WDRemoteMatrix distCoeffs; +}; + +struct WDRemoteStereoCalibration +{ + bool valid{false}; + double rms{0.0}; + double baselineMm{0.0}; + WDRemoteMatrix R; + WDRemoteMatrix T; + WDRemoteMatrix E; + WDRemoteMatrix F; + WDRemoteMatrix R1; + WDRemoteMatrix R2; + WDRemoteMatrix P1; + WDRemoteMatrix P2; + WDRemoteMatrix Q; +}; + +struct WDRemoteCalibrationInfo +{ + bool valid{false}; + std::string sourcePath; + std::string algoConfigPath; + std::string message; + WDRemoteCameraCalibration left; + WDRemoteCameraCalibration right; + WDRemoteStereoCalibration stereo; +}; + +/** + * @brief 服务端运行时信息(GetServerInfo 返回) + */ +struct WDRemoteServerInfo +{ + bool ok{false}; + std::string rtspUrl; + std::string version; + int controlPort{0}; + int resultPort{0}; + int rawImagePort{0}; + double detectFps{0.0}; + double streamFps{0.0}; + double leftExposure{0.0}; + double rightExposure{0.0}; + double leftGain{0.0}; + double rightGain{0.0}; + int leftCameraIndex{0}; + int rightCameraIndex{1}; + std::string leftCameraSerial; + std::string rightCameraSerial; + int streamWidth{0}; + int streamHeight{0}; + std::string streamCamera; + int binningHorizontal{0}; + int binningVertical{0}; + std::string mode; + std::string detectMode; + // 算法参数 + float algoScore{0.5f}; + float algoNms{0.45f}; + int algoWidth{640}; + int algoHeight{640}; + std::string algoModel; + int algoModelType{0}; + WDRemoteCalibrationInfo calibration; + std::string rawJson; // 原始 JSON 应答,方便扩展字段 +}; + +/** + * @brief 远程服务设备信息(SearchDevices 返回) + */ +struct WDRemoteDeviceInfo +{ + std::string machineCode; + std::string clientMachineCode; + std::string serverIp; + std::string deviceName; + int controlPort{5555}; + int resultPort{5556}; + int rawImagePort{5557}; + std::string rtspUrl; + double detectFps{0.0}; + double streamFps{0.0}; + double leftExposure{0.0}; + double rightExposure{0.0}; + double leftGain{0.0}; + double rightGain{0.0}; + int leftCameraIndex{0}; + int rightCameraIndex{1}; + std::string leftCameraSerial; + std::string rightCameraSerial; + int streamWidth{0}; + int streamHeight{0}; + std::string streamCamera; + int binningHorizontal{0}; + int binningVertical{0}; + std::string mode; + std::string detectMode; + std::string rawJson; +}; + +constexpr uint16_t WD_REMOTE_DISCOVERY_PORT = 5554; + +/** + * @brief StartWork 工作模式 + */ +enum class WDRemoteWorkMode +{ + LiveStream = 0, // 实时左目传图 + Detection = 1, // 检测模式,订阅检测结果和 raw 图像 +}; + +/** + * @brief 远程原始图像数据(单目) + * + * - data / dataSize 在回调内有效,回调返回后被库回收 + * - 上层若需要保存,请深拷贝 + */ +struct WDRemoteRawImage +{ + uint64_t frameId{0}; + int64_t timestampUs{0}; + int width{0}; + int height{0}; + int stride{0}; + WDRemotePixelFormat pixelFormat{WDRemotePixelFormat::UNKNOWN}; + const uint8_t* data{nullptr}; + size_t dataSize{0}; +}; + +/** + * @brief 远程双目原始图像数据 + * + * 用于双目相机系统,同时包含左右目图像。 + * - leftData / rightData / dataSize 在回调内有效,回调返回后被库回收 + * - 上层若需要保存,请深拷贝 + */ +struct WDRemoteBinocularRawImage +{ + uint64_t frameId{0}; + int64_t timestampUs{0}; + + // 左目图像 + int leftWidth{0}; + int leftHeight{0}; + int leftStride{0}; + WDRemotePixelFormat leftPixelFormat{WDRemotePixelFormat::UNKNOWN}; + const uint8_t* leftData{nullptr}; + size_t leftDataSize{0}; + + // 右目图像 + int rightWidth{0}; + int rightHeight{0}; + int rightStride{0}; + WDRemotePixelFormat rightPixelFormat{WDRemotePixelFormat::UNKNOWN}; + const uint8_t* rightData{nullptr}; + size_t rightDataSize{0}; +}; + +/** + * @brief 原始图像 ZMQ PUB 协议头(48 字节,紧凑布局) + * + * 服务端 PUB 数据 = topic(独立 ZMQ 分帧) + payload + * payload = WDRemoteRawImageHeader (48B) + raw bytes + * + * 这里同时给到客户端用于序列化/反序列化,便于第三方接入。 + */ +#pragma pack(push, 1) +struct WDRemoteRawImageHeader +{ + uint32_t magic; // 0x52415749 ('RAWI') + uint32_t version; // 1 + uint64_t frameId; + int64_t timestampUs; + uint32_t width; + uint32_t height; + uint32_t stride; + uint32_t pixelFormat; // WDRemotePixelFormat + uint32_t dataSize; + uint32_t reserved; +}; +#pragma pack(pop) + +static_assert(sizeof(WDRemoteRawImageHeader) == 48, + "WDRemoteRawImageHeader must be 48 bytes"); + +constexpr uint32_t WD_REMOTE_RAW_IMAGE_MAGIC = 0x52415749u; // 'RAWI' +constexpr uint32_t WD_REMOTE_RAW_IMAGE_VERSION = 1u; + +/** + * @brief 双目原始图像 ZMQ PUB 协议头(88 字节,紧凑布局) + * + * 服务端 PUB 数据 = topic(独立 ZMQ 分帧) + payload + * payload = WDRemoteBinocularRawImageHeader (88B) + left raw bytes + right raw bytes + */ +#pragma pack(push, 1) +struct WDRemoteBinocularRawImageHeader +{ + uint32_t magic; // 0x42495749 ('BIWI' - Binocular Image) 4B + uint32_t version; // 1 4B + uint64_t frameId; // 8B + int64_t timestampUs; // 8B + + // 左目图像信息 (20B) + uint32_t leftWidth; + uint32_t leftHeight; + uint32_t leftStride; + uint32_t leftPixelFormat; + uint32_t leftDataSize; + + // 右目图像信息 (20B) + uint32_t rightWidth; + uint32_t rightHeight; + uint32_t rightStride; + uint32_t rightPixelFormat; + uint32_t rightDataSize; + + uint32_t reserved[6]; // 24B padding (total: 4+4+8+8+20+20+24=88) +}; +#pragma pack(pop) + +static_assert(sizeof(WDRemoteBinocularRawImageHeader) == 88, + "WDRemoteBinocularRawImageHeader must be 88 bytes"); + +constexpr uint32_t WD_REMOTE_BINOCULAR_RAW_IMAGE_MAGIC = 0x42495749u; // 'BIWI' +constexpr uint32_t WD_REMOTE_BINOCULAR_RAW_IMAGE_VERSION = 1u; + +/** + * @brief 双目原始图像 payload 压缩方式 + * + * WDRemoteBinocularRawImageHeader::reserved[0] 记录该值: + * - 0: payload 中 left/right 数据就是原始图像字节 + * - 1: payload 中 left/right 数据是 PNG 压缩后的图像字节 + * - 2: payload 中 left/right 数据是 MJPEG/JPEG 压缩后的图像字节(MPP 硬件编码,有损) + * - 3: payload 中 left/right 数据是 zlib/deflate 压缩后的原始字节(真·无损,bit-exact) + * + * reserved[1] / reserved[2] 在压缩模式下分别记录左右目压缩前的原始字节数。 + */ +constexpr uint32_t WD_REMOTE_IMAGE_COMPRESSION_RAW = 0u; +constexpr uint32_t WD_REMOTE_IMAGE_COMPRESSION_PNG = 1u; +constexpr uint32_t WD_REMOTE_IMAGE_COMPRESSION_MJPEG = 2u; +constexpr uint32_t WD_REMOTE_IMAGE_COMPRESSION_ZLIB = 3u; + +#endif // WDREMOTEDATATYPES_H diff --git a/Module/WDRemoteReceiver/Src/WDRemoteReceiver.cpp b/Module/WDRemoteReceiver/Src/WDRemoteReceiver.cpp new file mode 100644 index 00000000..19cf13aa --- /dev/null +++ b/Module/WDRemoteReceiver/Src/WDRemoteReceiver.cpp @@ -0,0 +1,1241 @@ +#include "WDRemoteReceiver.h" + +#include "IVrZeroMQClient.h" +#include "IVrZeroMQPubSub.h" +#include "VrLog.h" +#include "VrError.h" +#include "AuthManager.h" + +#include "json/json.h" + +#include +#include +#include +#include +#include + +#include +#include + + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace VA; // 项目内 jsoncpp 包在 VA 命名空间内 + +namespace +{ +std::string logPreview(const std::string& text, size_t maxLen = 512) +{ + if (text.size() <= maxLen) + return text; + return text.substr(0, maxLen) + "..."; +} + +int sendZmqJsonRequest(const std::string& ip, + int port, + const std::string& jsonReq, + std::string& jsonResp, + int timeoutMs) +{ + if (ip.empty() || port <= 0) return ERR_CODE(NET_ERR_ARG); + if (jsonReq.find("\"cmd\":\"discover\"") != std::string::npos || + jsonReq.find("\"cmd\" : \"discover\"") != std::string::npos) + { + LOG_ERROR("[ZMQ-REQ] discover over ZMQ is disabled; use UDP discovery port=%u\n", + static_cast(WD_REMOTE_DISCOVERY_PORT)); + return ERR_CODE(DRONESCREW_ERR_UDP_DISCOVERY); + } + + try + { + zmq::context_t ctx(1); + zmq::socket_t sock(ctx, ZMQ_REQ); + const int linger = 0; + sock.set(zmq::sockopt::linger, linger); + sock.set(zmq::sockopt::rcvtimeo, timeoutMs); + sock.set(zmq::sockopt::sndtimeo, timeoutMs); + sock.set(zmq::sockopt::connect_timeout, timeoutMs); + + const std::string addr = "tcp://" + ip + ":" + std::to_string(port); + sock.connect(addr); + + zmq::message_t req(jsonReq.data(), jsonReq.size()); + const std::string reqPreview = logPreview(jsonReq); + LOG_DEBUG("[ZMQ-REQ] send %s:%d bytes=%zu preview=%s\n", + ip.c_str(), port, jsonReq.size(), reqPreview.c_str()); + auto sent = sock.send(req, zmq::send_flags::none); + if (!sent.has_value()) + { + LOG_DEBUG("[ZMQ-REQ] send failed %s:%d\n", ip.c_str(), port); + return ERR_CODE(NET_ERR_SEND_DATA); + } + + zmq::message_t reply; + auto recved = sock.recv(reply, zmq::recv_flags::none); + if (!recved.has_value()) + { + LOG_DEBUG("[ZMQ-REQ] recv timeout %s:%d timeout=%d\n", + ip.c_str(), port, timeoutMs); + return ERR_CODE(NET_ERR_RECV_DATA); + } + + jsonResp.assign(static_cast(reply.data()), reply.size()); + const std::string respPreview = logPreview(jsonResp); + LOG_DEBUG("[ZMQ-REQ] recv %s:%d bytes=%zu preview=%s\n", + ip.c_str(), port, jsonResp.size(), respPreview.c_str()); + return 0; + } + catch (const std::exception& e) + { + LOG_DEBUG("[ZMQ-REQ] exception %s:%d %s\n", ip.c_str(), port, e.what()); + return ERR_CODE(NET_ERR_CONNECT); + } +} + +std::vector buildBroadcastAddresses() +{ + std::vector addrs; + std::set seen; + + auto addAddr = [&](const QHostAddress& addr) { + if (addr.isNull()) return; + const QString key = addr.toString(); + if (key.isEmpty()) return; + if (seen.insert(key).second) + addrs.push_back(addr); + }; + + addAddr(QHostAddress::Broadcast); + + const QList ifaces = QNetworkInterface::allInterfaces(); + for (const QNetworkInterface& iface : ifaces) + { + const QNetworkInterface::InterfaceFlags flags = iface.flags(); + if (!(flags & QNetworkInterface::IsUp) || + !(flags & QNetworkInterface::CanBroadcast) || + (flags & QNetworkInterface::IsLoopBack)) + continue; + + const QList entries = iface.addressEntries(); + for (const QNetworkAddressEntry& entry : entries) + { + if (entry.ip().protocol() != QAbstractSocket::IPv4Protocol) + continue; + addAddr(entry.broadcast()); + } + } + + return addrs; +} + +std::string hostAddressToIpv4String(const QHostAddress& addr) +{ + bool ok = false; + const quint32 ipv4 = addr.toIPv4Address(&ok); + if (ok) + return QHostAddress(ipv4).toString().toStdString(); + return addr.toString().toStdString(); +} + +bool decodePngImage(const uint8_t* data, + size_t dataSize, + int expectedWidth, + int expectedHeight, + WDRemotePixelFormat& pixelFormat, + int& stride, + std::vector& decodedBytes) +{ + if (!data || dataSize == 0) return false; + + std::vector encoded(data, data + dataSize); + cv::Mat decoded = cv::imdecode(encoded, cv::IMREAD_UNCHANGED); + if (decoded.empty()) return false; + if (decoded.depth() != CV_8U) return false; + if (expectedWidth > 0 && decoded.cols != expectedWidth) return false; + if (expectedHeight > 0 && decoded.rows != expectedHeight) return false; + + const int channels = decoded.channels(); + if (channels == 1) + { + pixelFormat = WDRemotePixelFormat::MONO8; + stride = decoded.cols; + } + else if (channels == 3) + { + pixelFormat = WDRemotePixelFormat::BGR8; + stride = decoded.cols * 3; + } + else + { + return false; + } + + if (!decoded.isContinuous()) + decoded = decoded.clone(); + + const size_t bytes = decoded.total() * decoded.elemSize(); + decodedBytes.assign(decoded.data, decoded.data + bytes); + return true; +} + +int responseErrorCode(const std::string& jsonResp) +{ + if (jsonResp.empty()) + return ERR_CODE(DRONESCREW_ERR_RESPONSE_NOT_OK); + + Json::Value root; + Json::Reader reader; + if (!reader.parse(jsonResp, root)) + return ERR_CODE(DRONESCREW_ERR_INVALID_JSON); + + if (root.get("ok", false).asBool()) + return 0; + + const int code = root.get("code", 0).asInt(); + return code != 0 ? code : ERR_CODE(DRONESCREW_ERR_RESPONSE_NOT_OK); +} + +void fillMatrix(const Json::Value& root, WDRemoteMatrix& matrix) +{ + matrix.rows = root.get("rows", 0).asInt(); + matrix.cols = root.get("cols", 0).asInt(); + matrix.data.clear(); + + const Json::Value& data = root["data"]; + if (!data.isArray()) + return; + + matrix.data.reserve(data.size()); + for (Json::ArrayIndex i = 0; i < data.size(); ++i) + matrix.data.push_back(data[i].asDouble()); +} + +void fillCameraCalibration(const Json::Value& root, + WDRemoteCameraCalibration& calibration) +{ + if (!root.isObject()) + return; + + calibration.valid = root.get("valid", false).asBool(); + calibration.imageWidth = root.get("imageWidth", 0).asInt(); + calibration.imageHeight = root.get("imageHeight", 0).asInt(); + calibration.rms = root.get("rms", 0.0).asDouble(); + fillMatrix(root["cameraMatrix"], calibration.cameraMatrix); + fillMatrix(root["distCoeffs"], calibration.distCoeffs); +} + +void fillStereoCalibration(const Json::Value& root, + WDRemoteStereoCalibration& calibration) +{ + if (!root.isObject()) + return; + + calibration.valid = root.get("valid", false).asBool(); + calibration.rms = root.get("rms", 0.0).asDouble(); + calibration.baselineMm = root.get("baselineMm", 0.0).asDouble(); + fillMatrix(root["R"], calibration.R); + fillMatrix(root["T"], calibration.T); + fillMatrix(root["E"], calibration.E); + fillMatrix(root["F"], calibration.F); + fillMatrix(root["R1"], calibration.R1); + fillMatrix(root["R2"], calibration.R2); + fillMatrix(root["P1"], calibration.P1); + fillMatrix(root["P2"], calibration.P2); + fillMatrix(root["Q"], calibration.Q); +} + +void fillCalibrationInfo(const Json::Value& root, + WDRemoteCalibrationInfo& calibration) +{ + const Json::Value& node = root["calibration"]; + if (!node.isObject()) + return; + + calibration.valid = node.get("valid", false).asBool(); + calibration.sourcePath = node.get("sourcePath", "").asString(); + calibration.algoConfigPath = node.get("algoConfigPath", "").asString(); + calibration.message = node.get("message", "").asString(); + fillCameraCalibration(node["left"], calibration.left); + fillCameraCalibration(node["right"], calibration.right); + fillStereoCalibration(node["stereo"], calibration.stereo); +} + +void fillRuntimeFields(const Json::Value& root, WDRemoteServerInfo& info) +{ + info.detectFps = root.get("detectFps", 0.0).asDouble(); + info.streamFps = root.get("streamFps", 0.0).asDouble(); + info.leftExposure = root.get("leftExposure", 0.0).asDouble(); + info.rightExposure = root.get("rightExposure", 0.0).asDouble(); + info.leftGain = root.get("leftGain", 0.0).asDouble(); + info.rightGain = root.get("rightGain", 0.0).asDouble(); + info.leftCameraIndex = root.get("leftCameraIndex", 0).asInt(); + info.rightCameraIndex = root.get("rightCameraIndex", 1).asInt(); + info.leftCameraSerial = root.get("leftCameraSerial", "").asString(); + info.rightCameraSerial = root.get("rightCameraSerial", "").asString(); + info.streamWidth = root.get("streamWidth", 0).asInt(); + info.streamHeight = root.get("streamHeight", 0).asInt(); + info.streamCamera = root.get("streamCamera", "").asString(); + info.binningHorizontal = root.get("binningHorizontal", 0).asInt(); + info.binningVertical = root.get("binningVertical", 0).asInt(); + info.mode = root.get("mode", "").asString(); + info.detectMode = root.get("detectMode", "").asString(); + // 算法参数 + info.algoScore = static_cast(root.get("algoScore", 0.5).asDouble()); + info.algoNms = static_cast(root.get("algoNms", 0.45).asDouble()); + info.algoWidth = root.get("algoWidth", 640).asInt(); + info.algoHeight = root.get("algoHeight", 640).asInt(); + info.algoModel = root.get("algoModel", "").asString(); + info.algoModelType = root.get("algoModelType", 0).asInt(); + fillCalibrationInfo(root, info.calibration); +} + +void fillRuntimeFields(const Json::Value& root, WDRemoteDeviceInfo& info) +{ + info.detectFps = root.get("detectFps", 0.0).asDouble(); + info.streamFps = root.get("streamFps", 0.0).asDouble(); + info.leftExposure = root.get("leftExposure", 0.0).asDouble(); + info.rightExposure = root.get("rightExposure", 0.0).asDouble(); + info.leftGain = root.get("leftGain", 0.0).asDouble(); + info.rightGain = root.get("rightGain", 0.0).asDouble(); + info.leftCameraIndex = root.get("leftCameraIndex", 0).asInt(); + info.rightCameraIndex = root.get("rightCameraIndex", 1).asInt(); + info.leftCameraSerial = root.get("leftCameraSerial", "").asString(); + info.rightCameraSerial = root.get("rightCameraSerial", "").asString(); + info.streamWidth = root.get("streamWidth", 0).asInt(); + info.streamHeight = root.get("streamHeight", 0).asInt(); + info.streamCamera = root.get("streamCamera", "").asString(); + info.binningHorizontal = root.get("binningHorizontal", 0).asInt(); + info.binningVertical = root.get("binningVertical", 0).asInt(); + info.mode = root.get("mode", "").asString(); + info.detectMode = root.get("detectMode", "").asString(); +} +} + +// ===================================================================== +// 工厂 +// ===================================================================== +int IWDRemoteReceiver::CreateInstance(IWDRemoteReceiver** ppReceiver) +{ + if (!ppReceiver) return ERR_CODE(DEV_ARG_INVAILD); + *ppReceiver = new (std::nothrow) WDRemoteReceiver(); + return *ppReceiver ? 0 : ERR_CODE(DATA_ERR_MEM); +} + +// ===================================================================== +// ctor / dtor +// ===================================================================== +WDRemoteReceiver::WDRemoteReceiver() +{ +} + +WDRemoteReceiver::~WDRemoteReceiver() +{ + Disconnect(); +} + +// ===================================================================== +// 连接 / 断开 +// ===================================================================== +int WDRemoteReceiver::Connect(const ConnectConfig& cfg) +{ + if (m_bConnected.load()) + { + if (m_cfg.serverIp == cfg.serverIp && + m_cfg.controlPort == cfg.controlPort && + m_cfg.resultPort == cfg.resultPort && + m_cfg.rawImagePort == cfg.rawImagePort) + return 0; + Disconnect(); + } + + if (cfg.serverIp.empty() || cfg.controlPort <= 0) + return ERR_CODE(NET_ERR_ARG); + + Json::Value probeReq; + probeReq["cmd"] = "get_info"; + Json::FastWriter probeWriter; + std::string probeResp; + const int probeRet = sendZmqJsonRequest(cfg.serverIp, cfg.controlPort, + probeWriter.write(probeReq), + probeResp, 1000); + if (probeRet != 0) + { + LOG_DEBUG("[CLIENT] Connect probe failed: ctrl=%s:%d ret=%d\n", + cfg.serverIp.c_str(), cfg.controlPort, probeRet); + emitEvent(WDRemoteEventType::CONNECTION_ERROR, "server probe failed"); + return probeRet; + } + + Json::Value probeRoot; + Json::Reader probeReader; + if (!probeReader.parse(probeResp, probeRoot) || + !probeRoot.get("ok", false).asBool()) + { + LOG_DEBUG("[CLIENT] Connect probe invalid response: ctrl=%s:%d resp=%s\n", + cfg.serverIp.c_str(), cfg.controlPort, probeResp.c_str()); + emitEvent(WDRemoteEventType::CONNECTION_ERROR, "server probe invalid response"); + return ERR_CODE(DRONESCREW_ERR_RESPONSE_NOT_OK); + } + + m_cfg = cfg; + + // 1) 控制通道 + if (!IVrZeroMQClient::CreateObject(&m_pCtrlClient) || !m_pCtrlClient) + { + emitEvent(WDRemoteEventType::CONNECTION_ERROR, + "create ZeroMQ client failed"); + return ERR_CODE(NET_ERR_CREAT_INIT); + } + + int r = m_pCtrlClient->Init(m_cfg.serverIp.c_str(), m_cfg.controlPort); + if (r != 0) + { + emitEvent(WDRemoteEventType::CONNECTION_ERROR, + "ZeroMQ client init failed"); + delete m_pCtrlClient; + m_pCtrlClient = nullptr; + return r; + } + + LOG_DEBUG("[CLIENT] Connect OK: ctrl=%s:%d result=%d raw=%d\n", + m_cfg.serverIp.c_str(), m_cfg.controlPort, + m_cfg.resultPort, m_cfg.rawImagePort); + + m_bConnected = true; + emitEvent(WDRemoteEventType::CONNECTED, ""); + + return 0; +} + +bool WDRemoteReceiver::parseDeviceInfoJson(const std::string& jsonStr, + const std::string& serverIp, + WDRemoteDeviceInfo& outDevice) +{ + Json::Value root; + Json::Reader reader; + if (!reader.parse(jsonStr, root)) return false; + if (!root.get("ok", false).asBool()) return false; + + outDevice.machineCode = root.get("machineCode", "").asString(); + outDevice.clientMachineCode = root.get("clientMachineCode", "").asString(); + outDevice.serverIp = serverIp.empty() + ? root.get("serverIp", "").asString() + : serverIp; + outDevice.deviceName = root.get("deviceName", "DroneScrewServer").asString(); + outDevice.controlPort = root.get("controlPort", 5555).asInt(); + outDevice.resultPort = root.get("resultPort", 5556).asInt(); + outDevice.rawImagePort = root.get("rawImagePort", 5557).asInt(); + outDevice.rtspUrl = root.get("rtsp", "").asString(); + fillRuntimeFields(root, outDevice); + outDevice.rawJson = jsonStr; + return !outDevice.machineCode.empty(); +} + +int WDRemoteReceiver::SearchDevices(std::vector& devices, + int timeoutMs) +{ + devices.clear(); + m_lastDevices.clear(); + LOG_DEBUG("[CLIENT-DISCOVER] UDP discovery begin port=%u timeout=%d\n", + static_cast(WD_REMOTE_DISCOVERY_PORT), timeoutMs); + + const std::string clientMachineCode = AuthManager::GetMachineCode(); + Json::Value req; + req["cmd"] = "discover"; + req["machineCode"] = clientMachineCode; + Json::FastWriter writer; + const std::string jsonReq = writer.write(req); + + QUdpSocket socket; + if (!socket.bind(QHostAddress::AnyIPv4, 0, + QUdpSocket::ShareAddress | QUdpSocket::ReuseAddressHint)) + { + LOG_DEBUG("[CLIENT-DISCOVER] UDP bind failed: %s\n", + socket.errorString().toStdString().c_str()); + return ERR_CODE(NET_ERR_CREAT_BIND); + } + + const QByteArray datagram(jsonReq.data(), static_cast(jsonReq.size())); + const std::vector broadcasts = buildBroadcastAddresses(); + for (const QHostAddress& addr : broadcasts) + { + const qint64 sent = socket.writeDatagram( + datagram, addr, static_cast(WD_REMOTE_DISCOVERY_PORT)); + LOG_DEBUG("[CLIENT-DISCOVER] send udp %s:%u bytes=%lld\n", + addr.toString().toStdString().c_str(), + static_cast(WD_REMOTE_DISCOVERY_PORT), + static_cast(sent)); + } + + std::set seenMachineCodes; + QElapsedTimer timer; + timer.start(); + while (timer.elapsed() < timeoutMs) + { + const int remain = timeoutMs - static_cast(timer.elapsed()); + if (remain <= 0) break; + + if (!socket.waitForReadyRead(remain)) + break; + + while (socket.hasPendingDatagrams()) + { + QByteArray resp; + resp.resize(static_cast(socket.pendingDatagramSize())); + + QHostAddress sender; + quint16 senderPort = 0; + const qint64 readBytes = + socket.readDatagram(resp.data(), resp.size(), &sender, &senderPort); + if (readBytes <= 0) + continue; + resp.resize(static_cast(readBytes)); + + const std::string senderIp = hostAddressToIpv4String(sender); + const std::string respJson(resp.constData(), static_cast(resp.size())); + + WDRemoteDeviceInfo info; + if (!parseDeviceInfoJson(respJson, senderIp, info)) + continue; + if (!seenMachineCodes.insert(info.machineCode).second) + continue; + + devices.push_back(info); + m_lastDevices.push_back(info); + LOG_DEBUG("[CLIENT-DISCOVER] recv device machine=%s ip=%s ctrl=%d result=%d raw=%d from=%s:%u\n", + info.machineCode.c_str(), info.serverIp.c_str(), + info.controlPort, info.resultPort, info.rawImagePort, + senderIp.c_str(), static_cast(senderPort)); + } + } + + return devices.empty() ? ERR_CODE(NET_DEV_NOT_FIND) : 0; +} + +int WDRemoteReceiver::Open(const std::string& machineCode, int timeoutMs) +{ + if (machineCode.empty()) return ERR_CODE(DEV_ARG_INVAILD); + + std::vector devices = m_lastDevices; + if (devices.empty()) + SearchDevices(devices, timeoutMs); + + for (const WDRemoteDeviceInfo& info : devices) + { + if (info.machineCode != machineCode) + continue; + + Json::Value req; + req["cmd"] = "open"; + req["machineCode"] = AuthManager::GetMachineCode(); + req["targetMachineCode"] = machineCode; + Json::FastWriter writer; + + std::string resp; + const int r = sendZmqJsonRequest(info.serverIp, info.controlPort, + writer.write(req), resp, timeoutMs); + if (r != 0) + return r; + + WDRemoteDeviceInfo confirmed; + if (!parseDeviceInfoJson(resp, info.serverIp, confirmed)) + return ERR_CODE(DATA_ERR_INVALID); + if (confirmed.machineCode != machineCode) + return ERR_CODE(DRONESCREW_ERR_MACHINE_CODE_MISMATCH); + + ConnectConfig cfg; + cfg.serverIp = confirmed.serverIp; + cfg.controlPort = confirmed.controlPort; + cfg.resultPort = confirmed.resultPort; + cfg.rawImagePort = confirmed.rawImagePort; + return Connect(cfg); + } + + return ERR_CODE(NET_DEV_NOT_FIND); +} + +int WDRemoteReceiver::Disconnect() +{ + if (!m_bConnected.load() && !m_pCtrlClient && !m_pSubResult && !m_pSubRaw) + return 0; + + StopSubscribeDetection(); + StopSubscribeRawImage(); + + if (m_pCtrlClient) + { + m_pCtrlClient->UnInit(); + delete m_pCtrlClient; + m_pCtrlClient = nullptr; + } + + bool was = m_bConnected.exchange(false); + if (was) emitEvent(WDRemoteEventType::DISCONNECTED, ""); + return 0; +} + +bool WDRemoteReceiver::IsConnected() const +{ + return m_bConnected.load(); +} + +// ===================================================================== +// 回调注入 +// ===================================================================== +void WDRemoteReceiver::SetDetectionCallback(DetectionCallback cb) +{ + { + std::lock_guard lk(m_mtxCallback); + m_cbDetection = std::move(cb); + } +} + +void WDRemoteReceiver::SetRawImageCallback(RawImageCallback cb) +{ + { + std::lock_guard lk(m_mtxCallback); + m_cbRawImage = std::move(cb); + } +} + +void WDRemoteReceiver::SetEventCallback(EventCallback cb) +{ + std::lock_guard lk(m_mtxCallback); + m_cbEvent = std::move(cb); +} + +void WDRemoteReceiver::emitEvent(WDRemoteEventType ev, const std::string& msg) +{ + EventCallback cb; + { + std::lock_guard lk(m_mtxCallback); + cb = m_cbEvent; + } + if (cb) cb(ev, msg); +} + +// ===================================================================== +// 控制:REQ-REP 通用发送 +// ===================================================================== +int WDRemoteReceiver::sendCommandAndParse(const std::string& jsonReq, + std::string& jsonResp, + int timeoutMs) +{ + if (!m_bConnected.load()) + { + LOG_DEBUG("[CLIENT] sendCommand: not connected\n"); + return ERR_CODE(NET_ERR_NOTINIT); + } + + const std::string reqPreview = logPreview(jsonReq); + LOG_DEBUG("[CLIENT] sendCommand bytes=%zu preview=%s\n", + jsonReq.size(), reqPreview.c_str()); + + std::lock_guard lk(m_mtxCtrl); + + int r = sendZmqJsonRequest(m_cfg.serverIp, m_cfg.controlPort, + jsonReq, jsonResp, timeoutMs); + if (r != 0) + { + LOG_DEBUG("[CLIENT] sendCommand FAIL r=%d\n", r); + emitEvent(WDRemoteEventType::REQUEST_TIMEOUT, "SendAndWaitBack failed"); + return r; + } + const std::string respPreview = logPreview(jsonResp); + LOG_DEBUG("[CLIENT] sendCommand recv bytes=%zu preview=%s\n", + jsonResp.size(), respPreview.c_str()); + return 0; +} + +int WDRemoteReceiver::SendCustomCommand(const std::string& jsonReq, + std::string& jsonResp, + int timeoutMs) +{ + return sendCommandAndParse(jsonReq, jsonResp, timeoutMs); +} + +int WDRemoteReceiver::StartWork(int timeoutMs) +{ + return StartWork(WDRemoteWorkMode::Detection, timeoutMs); +} + +int WDRemoteReceiver::StartWork(WDRemoteWorkMode mode, int timeoutMs) +{ + if (mode == WDRemoteWorkMode::LiveStream) + { + StopSubscribeDetection(); + StopSubscribeRawImage(); + + Json::Value stopReq; + stopReq["cmd"] = "stop"; + Json::FastWriter w; + std::string stopResp; + sendCommandAndParse(w.write(stopReq), stopResp, timeoutMs); + + Json::Value req; + req["cmd"] = "start_stream"; + std::string resp; + const int r = sendCommandAndParse(w.write(req), resp, timeoutMs); + if (r != 0) return r; + return responseErrorCode(resp); + } + + Json::Value stopStreamReq; + stopStreamReq["cmd"] = "stop_stream"; + Json::FastWriter w; + std::string stopStreamResp; + sendCommandAndParse(w.write(stopStreamReq), stopStreamResp, timeoutMs); + + int subRet = StartSubscribeDetection(); + if (subRet != 0) + return subRet; + + subRet = StartSubscribeRawImage(); + if (subRet != 0) + { + StopSubscribeDetection(); + return subRet; + } + + Json::Value req; + req["cmd"] = "start"; + req["mode"] = "detection"; + std::string detectMode; + { + std::lock_guard lk(m_mtxDetectMode); + detectMode = m_detectMode; + } + req["detectMode"] = detectMode; + std::string resp; + int r = sendCommandAndParse(w.write(req), resp, timeoutMs); + if (r != 0) + { + StopSubscribeRawImage(); + StopSubscribeDetection(); + return r; + } + const int respCode = responseErrorCode(resp); + if (respCode != 0) + { + StopSubscribeRawImage(); + StopSubscribeDetection(); + return respCode; + } + return 0; +} + +int WDRemoteReceiver::StopWork(int timeoutMs) +{ + const int streamRet = StopWork(WDRemoteWorkMode::LiveStream, timeoutMs); + const int detectRet = StopWork(WDRemoteWorkMode::Detection, timeoutMs); + if (streamRet != 0 && detectRet != 0) + return detectRet; + if (detectRet != 0) return detectRet; + if (streamRet != 0) return streamRet; + return 0; +} + +int WDRemoteReceiver::StopWork(WDRemoteWorkMode mode, int timeoutMs) +{ + Json::FastWriter w; + if (mode == WDRemoteWorkMode::LiveStream) + { + Json::Value streamReq; + streamReq["cmd"] = "stop_stream"; + std::string streamResp; + const int streamRet = sendCommandAndParse(w.write(streamReq), streamResp, timeoutMs); + if (streamRet != 0) return streamRet; + return responseErrorCode(streamResp); + } + + Json::Value detectReq; + detectReq["cmd"] = "stop"; + std::string detectResp; + const int detectRet = sendCommandAndParse(w.write(detectReq), detectResp, timeoutMs); + + StopSubscribeRawImage(); + StopSubscribeDetection(); + + if (detectRet != 0) return detectRet; + const int detectCode = responseErrorCode(detectResp); + if (detectCode != 0) return detectCode; + return 0; +} + +int WDRemoteReceiver::RequestSingleDetection(int timeoutMs) +{ + Json::Value req; + req["cmd"] = "single"; + Json::FastWriter w; + std::string resp; + int r = sendCommandAndParse(w.write(req), resp, timeoutMs); + if (r != 0) return r; + return responseErrorCode(resp); +} + +int WDRemoteReceiver::SetExposure(double exposureTime, int timeoutMs) +{ + Json::Value req; + req["cmd"] = "set_exposure"; + req["value"] = exposureTime; + Json::FastWriter w; + std::string resp; + int r = sendCommandAndParse(w.write(req), resp, timeoutMs); + if (r != 0) return r; + return responseErrorCode(resp); +} + +int WDRemoteReceiver::SetGain(double gain, int timeoutMs) +{ + Json::Value req; + req["cmd"] = "set_gain"; + req["value"] = gain; + Json::FastWriter w; + std::string resp; + int r = sendCommandAndParse(w.write(req), resp, timeoutMs); + if (r != 0) return r; + return responseErrorCode(resp); +} + +int WDRemoteReceiver::SetAlgoParams(const WDRemoteAlgoParams& params, int timeoutMs) +{ + Json::Value req; + req["cmd"] = "set_algo_params"; + req["score"] = params.scoreThreshold; + req["nms"] = params.nmsThreshold; + req["width"] = params.inputWidth; + req["height"] = params.inputHeight; + req["model"] = params.modelPath; + req["modelType"] = params.modelType; + Json::FastWriter w; + std::string resp; + int r = sendCommandAndParse(w.write(req), resp, timeoutMs); + if (r != 0) return r; + return responseErrorCode(resp); +} + +void WDRemoteReceiver::SetDetectMode(const std::string& mode) +{ + if (mode == "mono" || mode == "binocular") + { + { + std::lock_guard lk(m_mtxDetectMode); + m_detectMode = mode; + } + LOG_DEBUG("[CLIENT] detectMode set to: %s\n", mode.c_str()); + if (m_bConnected.load()) + { + Json::Value req; + req["cmd"] = "set_detect_mode"; + req["detectMode"] = mode; + Json::FastWriter w; + std::string resp; + const int r = sendCommandAndParse(w.write(req), resp, 1000); + if (r != 0) + { + LOG_DEBUG("[CLIENT] set_detect_mode sync failed ret=%d mode=%s\n", + r, m_detectMode.c_str()); + } + else + { + const int code = responseErrorCode(resp); + if (code != 0) + LOG_DEBUG("[CLIENT] set_detect_mode rejected code=%d mode=%s\n", + code, m_detectMode.c_str()); + } + } + } + else + { + std::string current; + { + std::lock_guard lk(m_mtxDetectMode); + current = m_detectMode; + } + LOG_WARN("[CLIENT] unknown detectMode: %s, keep: %s\n", + mode.c_str(), current.c_str()); + } +} + +WDRemoteServerInfo WDRemoteReceiver::GetServerInfo(int timeoutMs) +{ + WDRemoteServerInfo info; + Json::Value req; + req["cmd"] = "get_info"; + Json::FastWriter w; + std::string resp; + int r = sendCommandAndParse(w.write(req), resp, timeoutMs); + info.rawJson = resp; + if (r != 0) return info; + + Json::Value root; + Json::Reader reader; + if (!reader.parse(resp, root)) return info; + if (!root.get("ok", false).asBool()) return info; + + info.ok = true; + info.rtspUrl = root.get("rtsp", "").asString(); + info.version = root.get("version", "").asString(); + info.controlPort = root.get("controlPort", 0 ).asInt(); + info.resultPort = root.get("resultPort", 0 ).asInt(); + info.rawImagePort = root.get("rawImagePort", 0 ).asInt(); + fillRuntimeFields(root, info); + return info; +} + +// ===================================================================== +// PUB-SUB 订阅启停 +// ===================================================================== +int WDRemoteReceiver::StartSubscribeDetection() +{ + if (m_pSubResult) return 0; + if (!IVrZeroMQSubscriber::CreateObject(&m_pSubResult) || !m_pSubResult) + return ERR_CODE(NET_ERR_CREAT_INIT); + + auto onMsg = [this](const char* topic, const size_t topicLen, + const char* data, const size_t dataLen) { + this->onResultMessage(topic, topicLen, data, dataLen); + }; + + int r = m_pSubResult->Init(m_cfg.serverIp.c_str(), m_cfg.resultPort, + "result", onMsg); + if (r != 0) + { + delete m_pSubResult; + m_pSubResult = nullptr; + return r; + } + LOG_DEBUG("[CLIENT-SUB] result subscribe tcp://%s:%d topic=result\n", + m_cfg.serverIp.c_str(), m_cfg.resultPort); + return 0; +} + +int WDRemoteReceiver::StopSubscribeDetection() +{ + if (!m_pSubResult) return 0; + m_pSubResult->UnInit(); + delete m_pSubResult; + m_pSubResult = nullptr; + return 0; +} + +int WDRemoteReceiver::StartSubscribeRawImage() +{ + if (m_pSubRaw) return 0; + if (m_cfg.rawImagePort <= 0) return ERR_CODE(NET_ERR_ARG); + if (!IVrZeroMQSubscriber::CreateObject(&m_pSubRaw) || !m_pSubRaw) + return ERR_CODE(NET_ERR_CREAT_INIT); + + auto onMsg = [this](const char* topic, const size_t topicLen, + const char* data, const size_t dataLen) { + this->onRawImageMessage(topic, topicLen, data, dataLen); + }; + + int r = m_pSubRaw->Init(m_cfg.serverIp.c_str(), m_cfg.rawImagePort, + "raw", onMsg); + if (r != 0) + { + delete m_pSubRaw; + m_pSubRaw = nullptr; + return r; + } + LOG_DEBUG("[CLIENT-SUB] raw subscribe tcp://%s:%d topic=raw\n", + m_cfg.serverIp.c_str(), m_cfg.rawImagePort); + return 0; +} + +int WDRemoteReceiver::StopSubscribeRawImage() +{ + if (!m_pSubRaw) return 0; + m_pSubRaw->UnInit(); + delete m_pSubRaw; + m_pSubRaw = nullptr; + return 0; +} + +// ===================================================================== +// 订阅消息分发 +// ===================================================================== +void WDRemoteReceiver::onResultMessage(const char* /*topic*/, size_t /*topicLen*/, + const char* data, size_t dataLen) +{ + if (!data || dataLen == 0) return; + static int resultCnt = 0; + ++resultCnt; + if (resultCnt == 1 || resultCnt % 100 == 0) + LOG_DEBUG("[CLIENT-SUB] result #%d size=%zu\n", resultCnt, dataLen); + std::string json(data, dataLen); + + WDRemoteDetectionFrame frame; + if (!parseDetectionJson(json, frame)) return; + + DetectionCallback cb; + { + std::lock_guard lk(m_mtxCallback); + cb = m_cbDetection; + } + if (cb) cb(frame); +} + +void WDRemoteReceiver::onRawImageMessage(const char* /*topic*/, size_t /*topicLen*/, + const char* data, size_t dataLen) +{ + if (!data || dataLen < sizeof(WDRemoteRawImageHeader)) return; + static int rawCnt = 0; + ++rawCnt; + if (rawCnt == 1 || rawCnt % 100 == 0) + LOG_DEBUG("[CLIENT-SUB] raw #%d size=%zu\n", rawCnt, dataLen); + + // 先尝试解析为双目图像 + if (dataLen >= sizeof(WDRemoteBinocularRawImageHeader)) + { + WDRemoteBinocularRawImageHeader binocHdr{}; + std::memcpy(&binocHdr, data, sizeof(binocHdr)); + + if (binocHdr.magic == WD_REMOTE_BINOCULAR_RAW_IMAGE_MAGIC && + binocHdr.version == WD_REMOTE_BINOCULAR_RAW_IMAGE_VERSION) + { + // 双目图像 + size_t expectedSize = sizeof(binocHdr) + binocHdr.leftDataSize + binocHdr.rightDataSize; + if (dataLen < expectedSize) return; + + const uint8_t* leftPayload = + reinterpret_cast(data + sizeof(binocHdr)); + const uint8_t* rightPayload = + reinterpret_cast(data + sizeof(binocHdr) + binocHdr.leftDataSize); + const uint32_t compression = binocHdr.reserved[0]; + std::string detectMode; + { + std::lock_guard lk(m_mtxDetectMode); + detectMode = m_detectMode; + } + const bool localMono = (detectMode == "mono"); + std::vector leftDecoded; + std::vector rightDecoded; + + WDRemoteBinocularRawImage img; + img.frameId = binocHdr.frameId; + img.timestampUs = binocHdr.timestampUs; + + img.leftWidth = static_cast(binocHdr.leftWidth); + img.leftHeight = static_cast(binocHdr.leftHeight); + img.leftStride = static_cast(binocHdr.leftStride); + img.leftPixelFormat = static_cast(binocHdr.leftPixelFormat); + img.leftData = leftPayload; + img.leftDataSize = binocHdr.leftDataSize; + + img.rightWidth = static_cast(binocHdr.rightWidth); + img.rightHeight = static_cast(binocHdr.rightHeight); + img.rightStride = static_cast(binocHdr.rightStride); + img.rightPixelFormat = static_cast(binocHdr.rightPixelFormat); + img.rightData = binocHdr.rightDataSize > 0 ? rightPayload : nullptr; + img.rightDataSize = binocHdr.rightDataSize; + + if (compression == WD_REMOTE_IMAGE_COMPRESSION_PNG || + compression == WD_REMOTE_IMAGE_COMPRESSION_MJPEG) + { + // PNG 与 MJPEG/JPEG 都用 cv::imdecode 解码(IMREAD_UNCHANGED 按内容自动识别格式) + if (!decodePngImage(leftPayload, binocHdr.leftDataSize, + img.leftWidth, img.leftHeight, + img.leftPixelFormat, img.leftStride, + leftDecoded)) + { + LOG_DEBUG("[CLIENT-SUB] raw decode left failed comp=%u frame=%llu size=%u\n", + compression, + static_cast(binocHdr.frameId), + binocHdr.leftDataSize); + return; + } + img.leftData = leftDecoded.data(); + img.leftDataSize = leftDecoded.size(); + + if (!localMono && binocHdr.rightDataSize > 0) + { + if (!decodePngImage(rightPayload, binocHdr.rightDataSize, + img.rightWidth, img.rightHeight, + img.rightPixelFormat, img.rightStride, + rightDecoded)) + { + LOG_DEBUG("[CLIENT-SUB] raw decode right failed comp=%u frame=%llu size=%u\n", + compression, + static_cast(binocHdr.frameId), + binocHdr.rightDataSize); + return; + } + img.rightData = rightDecoded.data(); + img.rightDataSize = rightDecoded.size(); + } + } + else if (compression == WD_REMOTE_IMAGE_COMPRESSION_ZLIB) + { + // 真·无损:Qt qUncompress 解回原始字节(与服务端 qCompress 配对,跨平台无额外依赖)。 + // 图像格式/尺寸/stride 沿用包头(解出的就是原始裸帧)。 + QByteArray leftRaw = qUncompress(reinterpret_cast(leftPayload), + static_cast(binocHdr.leftDataSize)); + if (leftRaw.isEmpty()) + { + LOG_DEBUG("[CLIENT-SUB] zlib inflate left failed frame=%llu size=%u\n", + static_cast(binocHdr.frameId), + binocHdr.leftDataSize); + return; + } + leftDecoded.assign(reinterpret_cast(leftRaw.constData()), + reinterpret_cast(leftRaw.constData()) + leftRaw.size()); + img.leftData = leftDecoded.data(); + img.leftDataSize = leftDecoded.size(); + + if (!localMono && binocHdr.rightDataSize > 0) + { + QByteArray rightRaw = qUncompress(reinterpret_cast(rightPayload), + static_cast(binocHdr.rightDataSize)); + if (rightRaw.isEmpty()) + { + LOG_DEBUG("[CLIENT-SUB] zlib inflate right failed frame=%llu size=%u\n", + static_cast(binocHdr.frameId), + binocHdr.rightDataSize); + return; + } + rightDecoded.assign(reinterpret_cast(rightRaw.constData()), + reinterpret_cast(rightRaw.constData()) + rightRaw.size()); + img.rightData = rightDecoded.data(); + img.rightDataSize = rightDecoded.size(); + } + } + else if (compression != WD_REMOTE_IMAGE_COMPRESSION_RAW) + { + LOG_DEBUG("[CLIENT-SUB] raw unknown compression=%u frame=%llu\n", + compression, + static_cast(binocHdr.frameId)); + return; + } + + if (localMono) + { + img.rightWidth = 0; + img.rightHeight = 0; + img.rightStride = 0; + img.rightPixelFormat = WDRemotePixelFormat::UNKNOWN; + img.rightData = nullptr; + img.rightDataSize = 0; + } + + if (rawCnt == 1 || rawCnt % 100 == 0) + { + LOG_DEBUG("[CLIENT-SUB] raw #%d compression=%s left=%dx%d size=%zu right=%dx%d size=%zu\n", + rawCnt, + compression == WD_REMOTE_IMAGE_COMPRESSION_PNG ? "png" : "raw", + img.leftWidth, img.leftHeight, img.leftDataSize, + img.rightWidth, img.rightHeight, img.rightDataSize); + } + + RawImageCallback cb; + { + std::lock_guard lk(m_mtxCallback); + cb = m_cbRawImage; + } + if (cb) cb(img); + return; + } + } + + // 单目图像 → 包装为双目(仅左眼有效) + WDRemoteRawImageHeader hdr{}; + std::memcpy(&hdr, data, sizeof(hdr)); + + if (hdr.magic != WD_REMOTE_RAW_IMAGE_MAGIC) return; + if (hdr.version != WD_REMOTE_RAW_IMAGE_VERSION) return; + + if (dataLen < sizeof(hdr) + hdr.dataSize) return; + + WDRemoteBinocularRawImage img; + img.frameId = hdr.frameId; + img.timestampUs = hdr.timestampUs; + + img.leftWidth = static_cast(hdr.width); + img.leftHeight = static_cast(hdr.height); + img.leftStride = static_cast(hdr.stride); + img.leftPixelFormat = static_cast(hdr.pixelFormat); + img.leftData = reinterpret_cast(data + sizeof(hdr)); + img.leftDataSize = hdr.dataSize; + + // 右眼置空 + img.rightWidth = 0; + img.rightHeight = 0; + img.rightStride = 0; + img.rightPixelFormat = WDRemotePixelFormat::UNKNOWN; + img.rightData = nullptr; + img.rightDataSize = 0; + + { + RawImageCallback cb; + { + std::lock_guard lk(m_mtxCallback); + cb = m_cbRawImage; + } + if (cb) cb(img); + } +} + +// ===================================================================== +// JSON → WDRemoteDetectionFrame +// ===================================================================== +bool WDRemoteReceiver::parseDetectionJson(const std::string& jsonStr, + WDRemoteDetectionFrame& outFrame) +{ + Json::Value root; + Json::Reader reader; + if (!reader.parse(jsonStr, root)) return false; + + outFrame.frameId = static_cast(root.get("frameId", 0).asInt64()); + outFrame.timestampUs = static_cast (root.get("ts", 0).asInt64()); + outFrame.imageWidth = root.get("width", 0).asInt(); + outFrame.imageHeight = root.get("height", 0).asInt(); + outFrame.success = root.get("success", true).asBool(); + outFrame.errorCode = root.get("errorCode", 0).asInt(); + outFrame.message = root.get("msg", "").asString(); + + outFrame.boxes.clear(); + outFrame.distances.clear(); + + const Json::Value& bs = root["boxes"]; + if (bs.isArray()) + { + outFrame.boxes.reserve(bs.size()); + for (unsigned i = 0; i < bs.size(); ++i) + { + const Json::Value& b = bs[i]; + WDRemoteDetectionBox box; + box.classId = b.get("cls", 0 ).asInt(); + box.score = static_cast(b.get("score", 0.0).asDouble()); + box.x = b.get("x", 0 ).asInt(); + box.y = b.get("y", 0 ).asInt(); + box.width = b.get("w", 0 ).asInt(); + box.height = b.get("h", 0 ).asInt(); + outFrame.boxes.push_back(box); + } + } + + const Json::Value& ds = root["distances"]; + if (ds.isArray()) + { + outFrame.distances.reserve(ds.size()); + for (unsigned i = 0; i < ds.size(); ++i) + { + const Json::Value& d = ds[i]; + WDRemoteDetectionDistance distance; + distance.fromId = d.get("fromId", 0).asInt(); + distance.toId = d.get("toId", 0).asInt(); + distance.distanceMm = static_cast(d.get("distanceMm", 0.0).asDouble()); + outFrame.distances.push_back(distance); + } + } + return true; +} diff --git a/Module/WDRemoteReceiver/WDRemoteReceiver.pro b/Module/WDRemoteReceiver/WDRemoteReceiver.pro new file mode 100644 index 00000000..3322832b --- /dev/null +++ b/Module/WDRemoteReceiver/WDRemoteReceiver.pro @@ -0,0 +1,104 @@ +# WDRemoteReceiver.pro +# 远程设备控制 + 结果订阅 + 原始图像订阅 封装库 +# 对外提供 IWDRemoteReceiver 接口;下游项目可直接复用,不与 DroneScrew 业务绑定 + +QT += core network +QT -= gui + +TARGET = WDRemoteReceiver +TEMPLATE = lib +CONFIG += c++17 + +# 全平台使用动态库 +CONFIG += shared + +# 宏定义 +DEFINES += QT_DEPRECATED_WARNINGS +DEFINES += WDREMOTERECEIVER_LIBRARY + +# 让本库内部能使用 IVrZeroMQClient / IVrZeroMQSubscriber 的导入宏 +win32 { + DEFINES += VR_MQCLIENT_EXPORTS=0 + DEFINES += VR_MQPUBSUB_EXPORTS=0 +} + +unix { + DEFINES += VR_MQCLIENT_EXPORTS=0 + DEFINES += VR_MQPUBSUB_EXPORTS=0 +} + +# UTF-8 编码 +win32-msvc { + QMAKE_CXXFLAGS += /utf-8 + QMAKE_CFLAGS += /utf-8 +} + +# 头文件路径 +INCLUDEPATH += $$PWD/Inc +INCLUDEPATH += $$PWD/_Inc +INCLUDEPATH += $$PWD/../../Utils/VrUtils/Inc +INCLUDEPATH += $$PWD/../../Utils/VrUtils/jsoncpp +INCLUDEPATH += $$PWD/../../Utils/VrCommon/Inc +INCLUDEPATH += $$PWD/../AuthModule/Inc + +# 依赖模块(ZeroMQ 控制 + 结果订阅 + 原始图像订阅) +INCLUDEPATH += $$PWD/../ZeroMQClient/Inc +INCLUDEPATH += $$PWD/../ZeroMQPubSub/Inc + +# SDK 头(ZeroMQ) +INCLUDEPATH += $$PWD/../../SDK/ZeroMQ/include +INCLUDEPATH += $$PWD/../../SDK/OpenCV320/include + +HEADERS += \ + Inc/IWDRemoteReceiver.h \ + _Inc/WDRemoteReceiver.h + +SOURCES += \ + Src/WDRemoteReceiver.cpp + +# 库链接 +win32 { + CONFIG(debug, debug|release) { + LIBS += -L../ZeroMQClient/debug -lZeroMQClient + LIBS += -L../ZeroMQPubSub/debug -lZeroMQPubSub + LIBS += -L../AuthModule/debug -lAuthModule + LIBS += -L../../Utils/VrUtils/debug -lVrUtils + } else { + LIBS += -L../ZeroMQClient/release -lZeroMQClient + LIBS += -L../ZeroMQPubSub/release -lZeroMQPubSub + LIBS += -L../AuthModule/release -lAuthModule + LIBS += -L../../Utils/VrUtils/release -lVrUtils + } + + # ZeroMQ SDK + CONFIG(debug, debug|release) { + LIBS += -L$$PWD/../../SDK/ZeroMQ/Windows/x64/Debug -llibzmq-v142-mt-gd-4_3_5 + } else { + LIBS += -L$$PWD/../../SDK/ZeroMQ/Windows/x64/Release -llibzmq-v142-mt-4_3_5 + } + + LIBS += -lws2_32 + + CONFIG(debug, debug|release) { + LIBS += -L$$PWD/../../SDK/OpenCV320/Windows/vc14/Release -lopencv_world320d + } else { + LIBS += -L$$PWD/../../SDK/OpenCV320/Windows/vc14/Release -lopencv_world320 + } +} + +unix:!macx { + LIBS += -L../ZeroMQClient -lZeroMQClient + LIBS += -L../ZeroMQPubSub -lZeroMQPubSub + LIBS += -L../AuthModule -lAuthModule + LIBS += -L../../Utils/VrUtils -lVrUtils + + LIBS += -L$$PWD/../../SDK/ZeroMQ/Arm/aarch64 -lzmq + LIBS += -L$$PWD/../../SDK/OpenCV320/Arm/aarch64 -lopencv_core -lopencv_imgcodecs + LIBS += -lpthread + + QMAKE_CXXFLAGS += -fPIC + + target.path = /usr/lib +} + +!isEmpty(target.path): INSTALLS += target diff --git a/Module/WDRemoteReceiver/_Inc/WDRemoteReceiver.h b/Module/WDRemoteReceiver/_Inc/WDRemoteReceiver.h new file mode 100644 index 00000000..7356df12 --- /dev/null +++ b/Module/WDRemoteReceiver/_Inc/WDRemoteReceiver.h @@ -0,0 +1,96 @@ +#ifndef WDREMOTERECEIVER_H +#define WDREMOTERECEIVER_H + +#include "IWDRemoteReceiver.h" +#include +#include +#include +#include + +class IVrZeroMQClient; +class IVrZeroMQSubscriber; + +/** + * @brief IWDRemoteReceiver 实现 + * + * - 控制:IVrZeroMQClient (REQ-REP) + * - 结果订阅:IVrZeroMQSubscriber on topic="result" + * - 原始图像订阅:IVrZeroMQSubscriber on topic="raw" + */ +class WDRemoteReceiver : public IWDRemoteReceiver +{ +public: + WDRemoteReceiver(); + ~WDRemoteReceiver() override; + + // IWDRemoteReceiver + int Connect(const ConnectConfig& cfg) override; + int SearchDevices(std::vector& devices, + int timeoutMs) override; + int Open(const std::string& machineCode, + int timeoutMs) override; + int Disconnect() override; + bool IsConnected() const override; + + int StartWork(int timeoutMs) override; + int StartWork(WDRemoteWorkMode mode, int timeoutMs) override; + int StopWork (int timeoutMs) override; + int StopWork (WDRemoteWorkMode mode, int timeoutMs) override; + int RequestSingleDetection(int timeoutMs) override; + int SetExposure(double exposureTime, int timeoutMs) override; + int SetGain (double gain, int timeoutMs) override; + int SetAlgoParams(const WDRemoteAlgoParams& params, int timeoutMs) override; + WDRemoteServerInfo GetServerInfo(int timeoutMs) override; + int SendCustomCommand(const std::string& jsonReq, + std::string& jsonResp, + int timeoutMs) override; + void SetDetectMode(const std::string& mode) override; + + void SetDetectionCallback(DetectionCallback cb) override; + void SetRawImageCallback (RawImageCallback cb) override; // 双目图像 + void SetEventCallback (EventCallback cb) override; + +private: + int sendCommandAndParse(const std::string& jsonReq, + std::string& jsonResp, + int timeoutMs); + + // 内部订阅管理(StartWork/StopWork 自动调用) + int StartSubscribeDetection(); + int StopSubscribeDetection(); + int StartSubscribeRawImage(); + int StopSubscribeRawImage(); + + void onResultMessage (const char* topic, size_t topicLen, + const char* data, size_t dataLen); + void onRawImageMessage(const char* topic, size_t topicLen, + const char* data, size_t dataLen); + + void emitEvent(WDRemoteEventType ev, const std::string& msg); + + static bool parseDetectionJson(const std::string& jsonStr, + WDRemoteDetectionFrame& outFrame); + static bool parseDeviceInfoJson(const std::string& jsonStr, + const std::string& serverIp, + WDRemoteDeviceInfo& outDevice); + +private: + ConnectConfig m_cfg; + std::atomic m_bConnected{false}; + + IVrZeroMQClient* m_pCtrlClient{nullptr}; + IVrZeroMQSubscriber* m_pSubResult{nullptr}; + IVrZeroMQSubscriber* m_pSubRaw{nullptr}; + + std::mutex m_mtxCtrl; // 串行化 REQ-REP + mutable std::mutex m_mtxCallback; + mutable std::mutex m_mtxDetectMode; + + DetectionCallback m_cbDetection; + RawImageCallback m_cbRawImage; // 双目图像回调 + EventCallback m_cbEvent; + std::vector m_lastDevices; + std::string m_detectMode{"binocular"}; // "binocular" | "mono" +}; + +#endif // WDREMOTERECEIVER_H diff --git a/Module/ZeroMQServer/Src/CVrZeroMQServer.cpp b/Module/ZeroMQServer/Src/CVrZeroMQServer.cpp index 00cfdde6..c8b1b0ad 100644 --- a/Module/ZeroMQServer/Src/CVrZeroMQServer.cpp +++ b/Module/ZeroMQServer/Src/CVrZeroMQServer.cpp @@ -175,7 +175,7 @@ void CVrZeroMQServer::_WorkTask(zmq::socket_t *socket) std::string strResponse = m_funRecvAck(static_cast(msg.data()), msg.size()); zmq::message_t response(strResponse.c_str(), strResponse.size()); socket->send(response, zmq::send_flags::none); - LOG_DEBUG("[ZMQ-REP] _WorkTask send resp: %s\n", strResponse.c_str()); + LOG_DEBUG("[ZMQ-REP] _WorkTask send resp bytes=%zu\n", strResponse.size()); } else { socket->send(msg, zmq::send_flags::none); }