糖包拆线 & 查看点云软件增加姿态,线条的显示

This commit is contained in:
yiyi 2026-02-07 23:46:44 +08:00
parent 99881767bb
commit 1641a0e029
17 changed files with 1710 additions and 250 deletions

View File

@ -135,17 +135,22 @@ INCLUDEPATH += ../../../SDK/Device/gl_linelaser_sdk/include
win32:CONFIG(release, debug|release): {
LIBS += -L$$PWD/../../../SDK/Device/VzNLSDK/Windows/x64/Release
LIBS += -lVzKernel -lVzNLDetect -lVzNLGraphics
# gl_linelaser_sdk
LIBS += -L$$PWD/../../../SDK/Device/gl_linelaser_sdk/x64
LIBS += -lgl_linelaser_sdk
}
else:win32:CONFIG(debug, debug|release): {
LIBS += -L$$PWD/../../../SDK/Device/VzNLSDK/Windows/x64/Debug
LIBS += -lVzKerneld -lVzNLDetectd -lVzNLGraphicsd
# gl_linelaser_sdk
LIBS += -L$$PWD/../../../SDK/Device/gl_linelaser_sdk/x64
LIBS += -lgl_linelaser_sdk
}
else:unix:!macx: {
LIBS += -L$$PWD/../../../SDK/Device/VzNLSDK/Arm/aarch64
LIBS += -lVzEyeSecurityLoader-shared -lVzKernel -lVzNLDetect -lVzNLGraphics
# gl_linelaser_sdk for Linux aarch64
LIBS += -L$$PWD/../../../SDK/Device/gl_linelaser_sdk/aarch64_linux -lgl_linelaser_sdk
}

View File

@ -146,6 +146,15 @@ protected:
*/
int CreateDevice(IVrEyeDevice** ppDevice) override;
/**
* @brief Modbus写寄存器回调
*
* - 0:
* - 2: 12-
* - 4: xyzu数据线8
*/
void OnModbusWriteCallback(uint16_t startAddress, const uint16_t* data, uint16_t count) override;
private:
// TCP服务器相关方法
int InitTcpServer(int nPort);
@ -188,6 +197,10 @@ private:
// 手眼标定矩阵列表从独立文件加载暂时保留在Presenter中
std::vector<CalibMatrix> m_clibMatrixList;
// ModbusTCP协议相关
DetectionResult m_lastDetectionResult; // 最新的检测结果
std::mutex m_modbusResultMutex; // 保护检测结果的互斥锁
};
#endif // BAGTHREADPOSITIONPRESENTER_H

View File

@ -252,6 +252,14 @@ int BagThreadPositionPresenter::ProcessAlgoDetection(std::vector<std::pair<EVzRe
if (GetStatusCallback<IYBagThreadPositionStatus>()) {
if (auto pStatus = GetStatusCallback<IYBagThreadPositionStatus>()) pStatus->OnStatusUpdate("检测处理器未初始化");
}
// 更新ModbusTCP寄存器地址2 = 2检测失败
if (IsModbusServerRunning()) {
uint16_t statusValue = 2; // 失败
WriteModbusRegisters(2, &statusValue, 1);
LOG_INFO("ModbusTCP: 检测失败处理器未初始化地址2写入2\n");
}
return ERR_CODE(DEV_NOT_FIND);
}
@ -277,6 +285,16 @@ int BagThreadPositionPresenter::ProcessAlgoDetection(std::vector<std::pair<EVzRe
}
LOG_INFO("[Algo Thread] sx_bagThreadMeasure detected %zu objects time : %.2f ms\n", detectionResult.positions.size(), oTimeUtils.GetElapsedTimeInMilliSec());
// 如果检测失败更新ModbusTCP状态
if (nRet != SUCCESS) {
if (IsModbusServerRunning()) {
uint16_t statusValue = 2; // 失败
WriteModbusRegisters(2, &statusValue, 1);
LOG_INFO("ModbusTCP: 检测失败地址2写入2\n");
}
}
ERR_CODE_RETURN(nRet);
// 8. 通知UI检测结果
@ -285,6 +303,69 @@ int BagThreadPositionPresenter::ProcessAlgoDetection(std::vector<std::pair<EVzRe
pStatus->OnDetectionResult(detectionResult);
}
// 保存检测结果用于ModbusTCP输出
{
std::lock_guard<std::mutex> lock(m_modbusResultMutex);
m_lastDetectionResult = detectionResult;
}
// 更新ModbusTCP寄存器地址2 = 1检测成功并输出xyzu数据到地址4
if (IsModbusServerRunning()) {
uint16_t statusValue = 1; // 成功
WriteModbusRegisters(2, &statusValue, 1);
LOG_INFO("ModbusTCP: 检测成功地址2写入1\n");
// 直接输出xyzu数据到地址4
if (!detectionResult.threadInfoList.empty()) {
size_t threadCount = detectionResult.threadInfoList.size();
std::vector<uint16_t> outputData;
outputData.reserve(threadCount * 8);
LOG_INFO("ModbusTCP: 准备输出 %zu 条拆线的xyzu数据\n", threadCount);
// 将每个拆线的 x, y, z, u 转换为寄存器数据
for (const auto& thread : detectionResult.threadInfoList) {
// x (centerX)
float x = static_cast<float>(thread.centerX);
uint16_t* xPtr = reinterpret_cast<uint16_t*>(&x);
outputData.push_back(xPtr[0]);
outputData.push_back(xPtr[1]);
// y (centerY)
float y = static_cast<float>(thread.centerY);
uint16_t* yPtr = reinterpret_cast<uint16_t*>(&y);
outputData.push_back(yPtr[0]);
outputData.push_back(yPtr[1]);
// z (centerZ)
float z = static_cast<float>(thread.centerZ);
uint16_t* zPtr = reinterpret_cast<uint16_t*>(&z);
outputData.push_back(zPtr[0]);
outputData.push_back(zPtr[1]);
// u (rotateAngle)
float u = static_cast<float>(thread.rotateAngle);
uint16_t* uPtr = reinterpret_cast<uint16_t*>(&u);
outputData.push_back(uPtr[0]);
outputData.push_back(uPtr[1]);
LOG_DEBUG("ModbusTCP: 拆线数据: x=%.3f, y=%.3f, z=%.3f, u=%.3f\n",
thread.centerX, thread.centerY, thread.centerZ, thread.rotateAngle);
}
// 从地址4开始写入数据
if (!outputData.empty()) {
int ret = WriteModbusRegisters(4, outputData.data(), static_cast<uint16_t>(outputData.size()));
if (ret == 0) {
LOG_INFO("ModbusTCP: 成功输出 %zu 条拆线的xyzu数据到寄存器地址4起共%zu个寄存器\n",
threadCount, outputData.size());
} else {
LOG_ERROR("ModbusTCP: 输出xyzu数据失败错误码: %d\n", ret);
}
}
}
}
// 更新状态
QString statusMsg = QString("检测完成,发现%1条拆线").arg(detectionResult.positions.size() / 2);
if (auto pStatus = GetStatusCallback<IYBagThreadPositionStatus>()) pStatus->OnStatusUpdate(statusMsg.toStdString());
@ -695,3 +776,38 @@ int BagThreadPositionPresenter::CreateDevice(IVrEyeDevice** ppDevice)
LOG_ERROR("[BagThreadPositionPresenter] Failed to create GlLineLaser device, error: %d\n", nRet);
return ERR_CODE(DEV_OPEN_ERR);
}
// ============ ModbusTCP 协议实现 ============
void BagThreadPositionPresenter::OnModbusWriteCallback(uint16_t startAddress, const uint16_t* data, uint16_t count)
{
if (!data || count == 0) {
LOG_WARNING("[ModbusTCP] 无效的写入参数\n");
return;
}
LOG_INFO("[ModbusTCP] 收到写寄存器请求: 地址=%d, 数量=%d, 值=%d\n", startAddress, count, data[0]);
// 地址0: 请求启动相机扫描
if (startAddress == 0) {
if (data[0] == 1) {
LOG_INFO("[ModbusTCP] 收到启动扫描请求\n");
// 重置检测状态寄存器地址2为0
uint16_t statusValue = 0;
WriteModbusRegisters(2, &statusValue, 1);
// 触发检测
bool success = TriggerDetection(m_currentCameraIndex);
if (!success) {
LOG_ERROR("[ModbusTCP] 启动扫描失败\n");
// 写入失败状态到地址2
statusValue = 2; // 失败
WriteModbusRegisters(2, &statusValue, 1);
} else {
LOG_INFO("[ModbusTCP] 扫描已启动\n");
}
}
}
}

View File

@ -0,0 +1,101 @@
# 糖包拆线 ModbusTCP 协议说明
**应用名称**: BagThreadPosition糖包拆线
**版本**: v1.0
**日期**: 2026-02-07
## 概述
糖包拆线应用作为 ModbusTCP 服务端,使用标准 ModbusTCP 协议(端口 502与外部设备客户端通信实现相机扫描控制和检测结果输出。
### 基本信息
- **服务端**: 糖包拆线应用BagThreadPosition
- **客户端**: PLC、机器人控制器或其他支持 ModbusTCP 的设备
- **协议**: ModbusTCP基于 TCP/IP 的 Modbus 协议)
- **端口**: 502
- **功能码**: 0x03读保持寄存器、0x10写多个保持寄存器
## 寄存器地址映射
### 控制寄存器
| 地址 | 功能 | 读/写 | 数据类型 | 说明 |
|------|------|-------|----------|------|
| 0 | 启动扫描 | 写 | uint16 | 写入1启动相机扫描 |
| 2 | 检测状态 | 读 | uint16 | 0=未完成, 1=成功, 2=失败 |
### 数据寄存器
| 地址 | 功能 | 读/写 | 数据类型 | 说明 |
|------|------|-------|----------|------|
| 4+ | xyzu数据 | 读 | float[] | 拆线位置数据每条拆线8个寄存器 |
## 使用流程
### 客户端操作步骤
### 1. 启动扫描
客户端写入寄存器[0] = 1
- 服务端启动相机扫描
- 寄存器[2]被重置为0检测中
### 2. 等待检测完成
客户端循环读取寄存器[2]直到值不为0
- 值为1检测成功xyzu数据已自动写入寄存器[4]开始的位置
- 值为2检测失败
### 3. 读取检测结果
当寄存器[2] = 1成功客户端直接从寄存器[4]开始读取 xyzu 数据。
## 数据格式
### 寄存器布局
每条拆线占用 8 个寄存器4个float每个float占2个寄存器
```
寄存器[4+i*8+0:1] -> x (centerX, float)
寄存器[4+i*8+2:3] -> y (centerY, float)
寄存器[4+i*8+4:5] -> z (centerZ, float)
寄存器[4+i*8+6:7] -> u (rotateAngle, float)
```
其中 i 为拆线索引从0开始
### xyzu 含义
- **x (centerX)**: 拆线端部中心点的X坐标毫米
- **y (centerY)**: 拆线端部中心点的Y坐标毫米
- **z (centerZ)**: 拆线端部中心点的Z坐标毫米
- **u (rotateAngle)**: 拆线旋转角度(度,范围 -30° ~ 30°
### Float 数据格式
每个 float 值占用 2 个 uint16 寄存器IEEE 754 单精度浮点数):
- 低位寄存器float 的低16位
- 高位寄存器float 的高16位
## 注意事项
1. **服务端**: 糖包拆线应用启动后自动开启 ModbusTCP 服务端,监听端口 502
2. **客户端**: 支持标准 ModbusTCP 协议的设备均可连接PLC、机器人控制器等
3. **数据顺序**: 拆线数据按检测顺序排列
4. **自动输出**: 检测成功后,服务端自动将 xyzu 数据写入寄存器[4]开始的位置,客户端无需额外请求
5. **字节序**: Float 数据使用小端字节序Little Endian
6. **连接数**: 支持多个客户端同时连接,但建议单客户端操作
7. **超时处理**: 建议客户端设置合理的超时时间如30秒等待检测完成
## 错误处理
如果寄存器[2]返回2失败可能的原因
- 相机未连接
- 检测算法失败
- 检测处理器未初始化
建议客户端在失败后重新启动扫描或通知操作人员检查设备状态。

View File

@ -53,10 +53,10 @@ public:
/**
* @brief TCP服务器
* @param port TCP端口号5020
* @param port TCP端口号
* @return 0--
*/
int Initialize(uint16_t port = 5020);
int Initialize(uint16_t port = 6800);
/**
* @brief

View File

@ -624,11 +624,19 @@ void BasePresenter::_StaticDetectionCallback(EVzResultDataType eDataType, SVzLas
if (pLaserLinePoint->p3DPoint && pLaserLinePoint->nPointCount > 0) {
lineData.p3DPoint = new SVzNL3DPosition[pLaserLinePoint->nPointCount];
if (lineData.p3DPoint) {
memcpy(lineData.p3DPoint, pLaserLinePoint->p3DPoint, sizeof(SVzNL3DPosition) * pLaserLinePoint->nPointCount);
if(pLaserLinePoint->p3DPoint){
memcpy(lineData.p3DPoint, pLaserLinePoint->p3DPoint, sizeof(SVzNL3DPosition) * pLaserLinePoint->nPointCount);
} else {
memset(lineData.p3DPoint, 0, sizeof(SVzNL3DPosition) * pLaserLinePoint->nPointCount);
}
}
lineData.p2DPoint = new SVzNL2DPosition[pLaserLinePoint->nPointCount];
if (lineData.p2DPoint) {
memcpy(lineData.p2DPoint, pLaserLinePoint->p2DPoint, sizeof(SVzNL2DPosition) * pLaserLinePoint->nPointCount);
if (lineData.p2DPoint){
if(pLaserLinePoint->p2DPoint) {
memcpy(lineData.p2DPoint, pLaserLinePoint->p2DPoint, sizeof(SVzNL2DPosition) * pLaserLinePoint->nPointCount);
} else {
memset(lineData.p2DPoint, 0, sizeof(SVzNL2DPosition) * pLaserLinePoint->nPointCount);
}
}
}
} else if (eDataType == keResultDataType_PointXYZRGBA) {
@ -636,11 +644,19 @@ void BasePresenter::_StaticDetectionCallback(EVzResultDataType eDataType, SVzLas
if (pLaserLinePoint->p3DPoint && pLaserLinePoint->nPointCount > 0) {
lineData.p3DPoint = new SVzNLPointXYZRGBA[pLaserLinePoint->nPointCount];
if (lineData.p3DPoint) {
memcpy(lineData.p3DPoint, pLaserLinePoint->p3DPoint, sizeof(SVzNLPointXYZRGBA) * pLaserLinePoint->nPointCount);
if(pLaserLinePoint->p3DPoint){
memcpy(lineData.p3DPoint, pLaserLinePoint->p3DPoint, sizeof(SVzNLPointXYZRGBA) * pLaserLinePoint->nPointCount);
} else {
memset(lineData.p3DPoint, 0, sizeof(SVzNLPointXYZRGBA) * pLaserLinePoint->nPointCount);
}
}
lineData.p2DPoint = new SVzNL2DLRPoint[pLaserLinePoint->nPointCount];
if (lineData.p2DPoint) {
memcpy(lineData.p2DPoint, pLaserLinePoint->p2DPoint, sizeof(SVzNL2DLRPoint) * pLaserLinePoint->nPointCount);
if(pLaserLinePoint->p2DPoint) {
memcpy(lineData.p2DPoint, pLaserLinePoint->p2DPoint, sizeof(SVzNL2DLRPoint) * pLaserLinePoint->nPointCount);
} else {
memset(lineData.p2DPoint, 0, sizeof(SVzNL2DLRPoint) * pLaserLinePoint->nPointCount);
}
}
}
}
@ -708,7 +724,7 @@ void BasePresenter::_StaticCameraStatusCallback(EVzDeviceWorkStatus eStatus, voi
}
}
// 相机一直重联
void BasePresenter::StartCameraReconnectTimer()
{
LOG_DEBUG("[BasePresenter] StartCameraReconnectTimer called\n");

View File

@ -6,6 +6,9 @@
#include <chrono>
#include <algorithm>
// 静态实例指针供SDK回调访问
CGlLineLaserDevice* CGlLineLaserDevice::s_pInstance = nullptr;
CGlLineLaserDevice::CGlLineLaserDevice()
: m_nDeviceId(0)
, m_bDeviceOpen(false)
@ -15,6 +18,7 @@ CGlLineLaserDevice::CGlLineLaserDevice()
, m_nBatchLines(200)
{
memset(&m_modelInfo, 0, sizeof(GLX8_2_ModelInfo));
s_pInstance = this;
}
CGlLineLaserDevice::~CGlLineLaserDevice()
@ -22,11 +26,13 @@ CGlLineLaserDevice::~CGlLineLaserDevice()
if (m_bDeviceOpen) {
CloseDevice();
}
if (s_pInstance == this) {
s_pInstance = nullptr;
}
}
int CGlLineLaserDevice::InitDevice()
{
// 初始化 gl_linelaser_sdk
int ret = GLX8_2_Initialize();
if (ret != 0) {
LOG_ERROR("GLX8_2_Initialize failed: %d\n", ret);
@ -46,7 +52,6 @@ int CGlLineLaserDevice::SetStatusCallback(VzNL_OnNotifyStatusCBEx fNotify, void
int CGlLineLaserDevice::OpenDevice(const char* sIP, bool bRGBD, bool bSwing, bool bFillLaser)
{
// gl_linelaser_sdk 不支持 RGBD 和摆动模式,忽略这些参数
(void)bRGBD;
(void)bSwing;
(void)bFillLaser;
@ -56,14 +61,12 @@ int CGlLineLaserDevice::OpenDevice(const char* sIP, bool bRGBD, bool bSwing, boo
return SUCCESS;
}
// 解析IP地址
GLX8_2_ETHERNET_CONFIG ethConfig;
memset(&ethConfig, 0, sizeof(ethConfig));
if (sIP && strlen(sIP) > 0) {
LOG_DEBUG("open IP address format: %s\n", sIP);
// 解析IP字符串 "x.x.x.x"
int ip[4];
if (sscanf(sIP, "%d.%d.%d.%d", &ip[0], &ip[1], &ip[2], &ip[3]) == 4) {
ethConfig.abyIpAddress[0] = (unsigned char)ip[0];
@ -84,7 +87,6 @@ int CGlLineLaserDevice::OpenDevice(const char* sIP, bool bRGBD, bool bSwing, boo
return ERR_CODE(DEV_NOT_FIND);
}
// 使用第一个找到的设备
memcpy(&ethConfig, &pDevices[0], sizeof(GLX8_2_ETHERNET_CONFIG));
char ipStr[32];
sprintf(ipStr, "%d.%d.%d.%d", ethConfig.abyIpAddress[0], ethConfig.abyIpAddress[1], ethConfig.abyIpAddress[2], ethConfig.abyIpAddress[3]);
@ -95,7 +97,7 @@ int CGlLineLaserDevice::OpenDevice(const char* sIP, bool bRGBD, bool bSwing, boo
// 打开设备
int ret = GLX8_2_EthernetOpen(m_nDeviceId, &ethConfig);
if (ret != 0) {
LOG_ERROR("GLX8_2_Ethernet Open failed: %d\n", ret);
LOG_ERROR("GLX8_2_EthernetOpen failed: %d\n", ret);
return ERR_CODE(DEV_OPEN_ERR);
}
@ -114,14 +116,186 @@ int CGlLineLaserDevice::OpenDevice(const char* sIP, bool bRGBD, bool bSwing, boo
m_modelInfo.Model, m_nProfileWidth, m_dXPitch, m_dYPitch);
}
// 分配数据缓存
m_profileBuffer.resize(static_cast<size_t>(m_nProfileWidth) * m_nBatchLines);
m_intensityBuffer.resize(static_cast<size_t>(m_nProfileWidth) * m_nBatchLines);
m_positionBuffer.resize(m_nProfileWidth);
// 注册SDK批处理回调批处理参数在设备端软件预先配置
ret = RegisterBatchCallback();
if (ret != SUCCESS) {
LOG_ERROR("RegisterBatchCallback failed: %d\n", ret);
return ret;
}
LOG_DEBUG("Device initialized successfully (callback mode)\n");
return SUCCESS;
}
// 配置批处理模式
int CGlLineLaserDevice::ConfigureBatchMode()
{
LOG_DEBUG("Configuring batch mode...\n");
char inval[4];
uint32_t ival;
// 1. 开启批处理测量
ival = 1;
memcpy(inval, &ival, 4);
int ret = GLX8_2_SetSetting(m_nDeviceId, 1, 0, 0, 3, nullptr, inval, 4);
if (ret != 0) {
LOG_ERROR("Failed to enable batch mode: %d\n", ret);
return ERR_CODE(DEV_CTRL_ERR);
}
LOG_DEBUG("Batch mode enabled\n");
// 2. 设置批处理数量
ival = m_nBatchLines;
memcpy(inval, &ival, 4);
ret = GLX8_2_SetSetting(m_nDeviceId, 1, 0, 0, 0x0a, nullptr, inval, 4);
if (ret != 0) {
LOG_ERROR("Failed to set batch lines: %d\n", ret);
return ERR_CODE(DEV_CTRL_ERR);
}
LOG_DEBUG("Batch lines set to %d\n", m_nBatchLines);
// 3. 设置带亮度输出
ival = 1;
memcpy(inval, &ival, 4);
ret = GLX8_2_SetSetting(m_nDeviceId, 1, 2, 0, 0x0b, nullptr, inval, 4);
if (ret != 0) {
LOG_ERROR("Failed to enable intensity output: %d\n", ret);
return ERR_CODE(DEV_CTRL_ERR);
}
LOG_DEBUG("Intensity output enabled\n");
return SUCCESS;
}
// 注册SDK批处理回调
int CGlLineLaserDevice::RegisterBatchCallback()
{
s_pInstance = this;
int ret = GLX8_2_SetBatchOneTimeDataHandler(m_nDeviceId, BatchOneTimeCallback);
if (ret != 0) {
LOG_ERROR("GLX8_2_SetBatchOneTimeDataHandler failed: %d\n", ret);
return ERR_CODE(DEV_CTRL_ERR);
}
LOG_DEBUG("Batch callback registered\n");
return SUCCESS;
}
// SDK批处理回调静态函数由SDK线程调用
void CGlLineLaserDevice::BatchOneTimeCallback(const GLX8_2_STR_CALLBACK_INFO* info, const GLX8_2_Data DataObj)
{
if (s_pInstance == nullptr) {
return;
}
if (!s_pInstance->m_bDetecting) {
return;
}
s_pInstance->ProcessBatchData(info, DataObj);
}
// 处理一次批处理回调数据
void CGlLineLaserDevice::ProcessBatchData(const GLX8_2_STR_CALLBACK_INFO* info, const GLX8_2_Data DataObj)
{
if (info == nullptr || DataObj == nullptr) {
LOG_ERROR("ProcessBatchData: null parameter\n");
return;
}
// 检查批处理状态
if (info->returnStatus != 0) {
LOG_WARNING("Batch returnStatus: %d\n", info->returnStatus);
return;
}
int batchCount = info->BatchPoints;
int width = info->xPoints;
if (batchCount <= 0 || width <= 0) {
LOG_WARNING("Invalid batch data: batchCount=%d, width=%d\n", batchCount, width);
return;
}
LOG_DEBUG("Received batch: %d lines, width: %d, startEncoder: %d, batchTimes: %d\n", batchCount, width, info->startEncoder, info->BatchTimes);
// 使用回调info中的实时参数
double xPitch = info->xPixth;
if (xPitch <= 0) {
xPitch = m_dXPitch; // 回退到设备初始化时获取的值
}
// 从SDK获取数据指针SDK内部管理内存无需自己分配
int32_t* profileData = GLX8_2_GetBatchProfilePoint(DataObj, 0);
uint32_t* encoderData = GLX8_2_GetBatchEncoderPoint(DataObj, 0);
if (profileData == nullptr) {
LOG_ERROR("GLX8_2_GetBatchProfilePoint returned null\n");
return;
}
// 局部位置缓存
std::vector<SVzNL3DPosition> positionBuffer(width);
// 逐行处理数据并回调给上层
for (int lineIdx = 0; lineIdx < batchCount; lineIdx++) {
if (!m_bDetecting) {
break;
}
// 计算当前行在批处理数据中的偏移
const int32_t* lineProfile = profileData + static_cast<size_t>(lineIdx) * width;
// 转换为xyz坐标
double yOffset = static_cast<double>(m_ullFrameIndex) * m_dYPitch;
for (int i = 0; i < width; i++) {
SVzNL3DPosition& pos = positionBuffer[i];
pos.nPointIdx = i;
pos.pt3D.x = (static_cast<double>(i) - static_cast<double>(width) / 2.0) * xPitch;
pos.pt3D.y = yOffset;
int32_t rawZ = lineProfile[i];
if (rawZ == 0x7FFFFFFF || rawZ < -100000000) {
pos.pt3D.z = 0.0;
} else {
pos.pt3D.z = static_cast<double>(rawZ) * 0.00001; // 0.01um -> mm
}
}
// 填充 SVzLaserLineData 结构
SVzLaserLineData laserLineData;
memset(&laserLineData, 0, sizeof(SVzLaserLineData));
laserLineData.p3DPoint = positionBuffer.data();
laserLineData.p2DPoint = nullptr;
laserLineData.nPointCount = width;
laserLineData.dTotleOffset = yOffset;
laserLineData.dStep = m_dYPitch;
laserLineData.llFrameIdx = m_ullFrameIndex;
laserLineData.llTimeStamp = std::chrono::duration_cast<std::chrono::microseconds>(
std::chrono::steady_clock::now().time_since_epoch()).count();
laserLineData.nEncodeNo = encoderData ? encoderData[lineIdx] : 0;
laserLineData.fSwingAngle = 0.0f;
laserLineData.bEndOnceScan = (lineIdx == batchCount - 1) ? VzTrue : VzFalse;
// 回调给上层应用
if (m_pDetectCallback) {
m_pDetectCallback(m_eDataType, &laserLineData, m_pDetectCallbackParam);
}
m_ullFrameIndex++;
}
LOG_DEBUG("Processed %d lines, total frames: %llu\n", batchCount, m_ullFrameIndex);
// 通知上层本次批处理数据处理完成
if (m_pStatusCallback) {
m_pStatusCallback(keDeviceWorkStatus_Device_Swing_Finish, nullptr, 0, m_pStatusCallbackParam);
}
}
int CGlLineLaserDevice::GetVersion(SVzNLVersionInfo& sVersionInfo)
{
memset(&sVersionInfo, 0, sizeof(SVzNLVersionInfo));
@ -131,7 +305,6 @@ int CGlLineLaserDevice::GetVersion(SVzNLVersionInfo& sVersionInfo)
strncpy(sVersionInfo.szSDKVersion, sdkVersion, VZNL_VERSION_LENGTH - 1);
}
// 填充其他版本信息
strncpy(sVersionInfo.szAppVersion, "GlLineLaser", VZNL_VERSION_LENGTH - 1);
return SUCCESS;
@ -141,12 +314,10 @@ int CGlLineLaserDevice::GetDevInfo(SVzNLEyeDeviceInfoEx& sDeviceInfo)
{
memset(&sDeviceInfo, 0, sizeof(SVzNLEyeDeviceInfoEx));
// 填充设备信息
strncpy(sDeviceInfo.sEyeCBInfo.byServerIP, m_strDeviceIP.c_str(), VZNL_SDK_NETWORK_IPv4_LENGTH - 1);
strncpy(sDeviceInfo.sEyeCBInfo.szDeviceName, m_modelInfo.Model, VZNL_DEVICE_NAME_LENGTH - 1);
strncpy(sDeviceInfo.sEyeCBInfo.szDeviceID, m_modelInfo.HeaderSerial, VZNL_GUID_LENGTH - 1);
// 设置分辨率
sDeviceInfo.sVideoRes.nFrameWidth = m_nProfileWidth;
sDeviceInfo.sVideoRes.nFrameHeight = m_nBatchLines;
@ -191,14 +362,19 @@ int CGlLineLaserDevice::StartDetect(VzNL_AutoOutputLaserLineExCB fCallFunc, EVzR
m_pDetectCallback = fCallFunc;
m_pDetectCallbackParam = param;
m_eDataType = eDataType;
m_bStopDetect = false;
m_ullFrameIndex = 0;
// 启动数据采集线程(主动轮询模式)
m_bDetecting = true;
m_detectThread = std::thread(&CGlLineLaserDevice::DetectThreadFunc, this);
LOG_DEBUG("Detection started\n");
// 启动回调模式批处理0=立即开始)
int ret = GLX8_2_StartMeasureWithCallback(m_nDeviceId, 0);
if (ret != 0) {
LOG_ERROR("GLX8_2_StartMeasureWithCallback failed: %d\n", ret);
m_bDetecting = false;
return ERR_CODE(DEV_CTRL_ERR);
}
LOG_DEBUG("Detection started (callback mode)\n");
return SUCCESS;
}
@ -214,7 +390,7 @@ int CGlLineLaserDevice::StopDetect()
return SUCCESS;
}
m_bStopDetect = true;
m_bDetecting = false;
// 停止批处理
int ret = GLX8_2_StopMeasure(m_nDeviceId);
@ -222,13 +398,6 @@ int CGlLineLaserDevice::StopDetect()
LOG_ERROR("GLX8_2_StopMeasure failed: %d\n", ret);
}
// 等待线程结束
if (m_detectThread.joinable()) {
m_detectThread.join();
}
m_bDetecting = false;
// 通知状态变化
if (m_pStatusCallback) {
m_pStatusCallback(keDeviceWorkStatus_Device_Swing_Finish, nullptr, 0, m_pStatusCallbackParam);
@ -240,163 +409,12 @@ int CGlLineLaserDevice::StopDetect()
return SUCCESS;
}
// 数据采集线程函数(主动轮询模式,参考 test_deal_atch_datas
void CGlLineLaserDevice::DetectThreadFunc()
{
LOG_DEBUG("Detect thread started\n");
while (!m_bStopDetect) {
// 启动批处理(参考 test_deal_atch_datas
int ret = GLX8_2_StartMeasure(m_nDeviceId, 5000); // 5秒超时
if (ret != 0) {
LOG_ERROR("GLX8_2_StartMeasure failed: %d\n", ret);
std::this_thread::sleep_for(std::chrono::milliseconds(100));
continue;
}
// 等待一小段时间让设备准备好
std::this_thread::sleep_for(std::chrono::milliseconds(50));
// 获取批处理数据(参考 test_batch_datas
GetBatchData();
// 批处理完成后等待一段时间再开始下一次
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
LOG_DEBUG("Detect thread stopped\n");
}
// 获取批处理数据(参考 test_batch_datas 的流程)
void CGlLineLaserDevice::GetBatchData()
{
GLX8_2_STR_CALLBACK_INFO info;
memset(&info, 0, sizeof(info));
const int maxBatchLines = m_nBatchLines;
// 确保缓存足够大
size_t totalPoints = static_cast<size_t>(m_nProfileWidth) * maxBatchLines;
if (m_profileBuffer.size() < totalPoints) {
m_profileBuffer.resize(totalPoints);
}
if (m_intensityBuffer.size() < totalPoints) {
m_intensityBuffer.resize(totalPoints);
}
std::vector<uint32_t> encoderBuffer(maxBatchLines);
// 使用 GLX8_2_ReceiveDataAuto 顺序获取批处理数据
int ret = GLX8_2_ReceiveDataAuto(m_nDeviceId, &info,
m_profileBuffer.data(),
m_intensityBuffer.data(),
encoderBuffer.data());
if (ret != 0) {
LOG_WARNING("GLX8_2_ReceiveDataAuto failed: %d\n", ret);
return;
}
int batchCount = info.BatchPoints;
int width = info.xPoints;
if (batchCount <= 0 || width <= 0) {
LOG_WARNING("Invalid batch data: batchCount=%d, width=%d\n", batchCount, width);
return;
}
LOG_DEBUG("Received batch: %d lines, width: %d, startEncoder: %d\n",
batchCount, width, info.startEncoder);
// 确保位置缓存足够大
if (m_positionBuffer.size() < static_cast<size_t>(width)) {
m_positionBuffer.resize(width);
}
// 逐行处理数据并回调转换为xyz点云数据
for (int lineIdx = 0; lineIdx < batchCount; lineIdx++) {
if (m_bStopDetect) {
break;
}
// 计算当前行在缓存中的偏移
const int32_t* lineProfile = m_profileBuffer.data() + static_cast<size_t>(lineIdx) * width;
// 转换为xyz坐标
ConvertProfileToPosition(lineProfile, width, lineIdx);
// 填充 SVzLaserLineData 结构
SVzLaserLineData laserLineData;
memset(&laserLineData, 0, sizeof(SVzLaserLineData));
laserLineData.p3DPoint = m_positionBuffer.data();
laserLineData.p2DPoint = nullptr; // 线激光没有2D数据
laserLineData.nPointCount = width;
laserLineData.dTotleOffset = static_cast<double>(m_ullFrameIndex) * m_dYPitch;
laserLineData.dStep = m_dYPitch;
laserLineData.llFrameIdx = m_ullFrameIndex;
laserLineData.llTimeStamp = std::chrono::duration_cast<std::chrono::microseconds>(
std::chrono::steady_clock::now().time_since_epoch()).count();
laserLineData.nEncodeNo = encoderBuffer[lineIdx]; // 使用实际的编码器值
laserLineData.fSwingAngle = 0.0f; // 线激光没有摆动角度
laserLineData.bEndOnceScan = (lineIdx == batchCount - 1) ? VzTrue : VzFalse;
// 回调给上层应用
if (m_pDetectCallback) {
m_pDetectCallback(m_eDataType, &laserLineData, m_pDetectCallbackParam);
}
m_ullFrameIndex++;
}
LOG_DEBUG("Processed %d lines, total frames: %llu\n", batchCount, m_ullFrameIndex);
}
// 将轮廓数据转换为位置数据
void CGlLineLaserDevice::ConvertProfileToPosition(const int32_t* profileData, int count, int lineIndex)
{
// lineIndex 参数保留用于未来扩展,当前使用 m_ullFrameIndex 计算全局偏移
(void)lineIndex;
// 确保缓存足够大
if (m_positionBuffer.size() < static_cast<size_t>(count)) {
m_positionBuffer.resize(count);
}
// 计算Y偏移基于全局帧索引
double yOffset = static_cast<double>(m_ullFrameIndex) * m_dYPitch;
// 转换每个点
for (int i = 0; i < count; i++) {
SVzNL3DPosition& pos = m_positionBuffer[i];
pos.nPointIdx = i;
// X 坐标根据点索引和X间距计算
// X范围居中从 -width/2 * xPitch 到 width/2 * xPitch
pos.pt3D.x = (static_cast<double>(i) - static_cast<double>(count) / 2.0) * m_dXPitch;
// Y 坐标:扫描方向偏移
pos.pt3D.y = yOffset;
// Z 坐标高度值gl_linelaser_sdk 单位是 0.01um,转换为 mm
// 无效值通常是最大值或特定值,这里假设 0x7FFFFFFF 或负值为无效
int32_t rawZ = profileData[i];
if (rawZ == 0x7FFFFFFF || rawZ < -100000000) {
pos.pt3D.z = 0.0; // 无效值设为0
} else {
pos.pt3D.z = static_cast<double>(rawZ) * 0.00001; // 0.01um -> mm
}
}
}
// ============ ROI相关线激光不支持============
int CGlLineLaserDevice::SetDetectROI(SVzNLRect& leftROI, SVzNLRect& rightROI)
{
(void)leftROI;
(void)rightROI;
// 线激光不支持ROI设置
return SUCCESS;
}
@ -407,19 +425,17 @@ int CGlLineLaserDevice::GetDetectROI(SVzNLRect& leftROI, SVzNLRect& rightROI)
return SUCCESS;
}
// ============ 曝光/增益相关线激光通过SDK设置============
// ============ 曝光/增益相关 ============
int CGlLineLaserDevice::SetEyeExpose(unsigned int& exposeTime)
{
(void)exposeTime;
// gl_linelaser_sdk 通过 GLX8_2_SetSetting 设置曝光
// 暂不实现详细接口
return SUCCESS;
}
int CGlLineLaserDevice::GetEyeExpose(unsigned int& exposeTime)
{
exposeTime = 1000; // 默认值
exposeTime = 1000;
return SUCCESS;
}
@ -431,7 +447,7 @@ int CGlLineLaserDevice::SetEyeGain(unsigned int& gain)
int CGlLineLaserDevice::GetEyeGain(unsigned int& gain)
{
gain = 100; // 默认值
gain = 100;
return SUCCESS;
}
@ -440,13 +456,12 @@ int CGlLineLaserDevice::GetEyeGain(unsigned int& gain)
int CGlLineLaserDevice::SetFrame(int& frame)
{
(void)frame;
// 线激光帧率由硬件决定
return SUCCESS;
}
int CGlLineLaserDevice::GetFrame(int& frame)
{
frame = 100; // 默认帧率
frame = 100;
return SUCCESS;
}
@ -454,7 +469,7 @@ int CGlLineLaserDevice::GetFrame(int& frame)
bool CGlLineLaserDevice::IsSupport()
{
return false; // 不支持RGBD
return false;
}
int CGlLineLaserDevice::SetRGBDExposeThres(float& value)
@ -520,7 +535,6 @@ int CGlLineLaserDevice::SetWorkRange(double& dMin, double& dMax)
int CGlLineLaserDevice::GetWorkRange(double& dMin, double& dMax)
{
// 从设备信息获取工作范围
dMin = m_modelInfo.zRangmin;
dMax = m_modelInfo.zRangmax;
return SUCCESS;
@ -558,11 +572,6 @@ int CGlLineLaserDevice::SetBatchLines(unsigned int batchLines)
return ERR_CODE(DEV_ARG_INVAILD);
}
m_nBatchLines = batchLines;
// 重新分配缓存
m_profileBuffer.resize(static_cast<size_t>(m_nProfileWidth) * m_nBatchLines);
m_intensityBuffer.resize(static_cast<size_t>(m_nProfileWidth) * m_nBatchLines);
return SUCCESS;
}
@ -602,8 +611,6 @@ int CGlLineLaserDevice::GetMeasureRange(double& xMin, double& xMax, double& zMin
}
// ============ 工厂方法实现 ============
// 注意IVrEyeDevice::CreateObject 在 VrEyeDevice 中实现,创建 VzNLSDK 设备
// GlLineLaserDevice 只提供 IGlLineLaserDevice::CreateGlLineLaserObject 工厂方法
int IGlLineLaserDevice::CreateGlLineLaserObject(IGlLineLaserDevice** ppDevice)
{

View File

@ -4,10 +4,7 @@
#include "IGlLineLaserDevice.h"
#include "phoskey_ss.h"
#include <thread>
#include <atomic>
#include <mutex>
#include <condition_variable>
#include <vector>
#include <string>
@ -15,6 +12,15 @@
* @brief CGlLineLaserDevice
*
* gl_linelaser_sdk (phoskey_ss) IVrEyeDevice
* 使 SDK GLX8_2_SetBatchOneTimeDataHandler
*
*
* OpenDevice + SDK回调
* StartDetect GLX8_2_StartMeasureWithCallback
* SDK回调 BatchOneTimeCallback ProcessBatchData
* StopDetect GLX8_2_StopMeasure
* CloseDevice GLX8_2_CommClose
*
* gl_linelaser_sdk int32_t 0.01um
* SVzLaserLineData SVzNL3DPosition mm
*/
@ -100,7 +106,7 @@ private:
// 批处理参数
unsigned int m_nBatchLines = 200; // 批处理行数
// 回调相关
// 上层回调相关
VzNL_AutoOutputLaserLineExCB m_pDetectCallback = nullptr;
void* m_pDetectCallbackParam = nullptr;
EVzResultDataType m_eDataType = keResultDataType_Position;
@ -108,42 +114,40 @@ private:
VzNL_OnNotifyStatusCBEx m_pStatusCallback = nullptr;
void* m_pStatusCallbackParam = nullptr;
// 数据采集线程
std::thread m_detectThread;
// 检测状态
std::atomic<bool> m_bDetecting{false};
std::atomic<bool> m_bStopDetect{false};
std::mutex m_detectMutex;
std::condition_variable m_detectCondition;
// 数据缓存
std::vector<int32_t> m_profileBuffer; // 轮廓数据缓存
std::vector<uint8_t> m_intensityBuffer; // 亮度数据缓存
std::vector<SVzNL3DPosition> m_positionBuffer; // 转换后的位置数据
// 帧计数
unsigned long long m_ullFrameIndex = 0;
unsigned long long m_ullTimeStamp = 0;
// 内部方法
/**
* @brief 线 test_deal_atch_datas
* @brief
*/
void DetectThreadFunc();
int ConfigureBatchMode();
/**
* @brief test_batch_datas
* @brief SDK批处理回调
*/
void GetBatchData();
int RegisterBatchCallback();
/**
* @brief gl_linelaser_sdk SVzNL3DPosition
* @param profileData int32_t0.01um
* @param count
* @param lineIndex
* @return
* @brief SDK SDK线程调用
* @param info
* @param DataObj //
*/
void ConvertProfileToPosition(const int32_t* profileData, int count, int lineIndex);
static void BatchOneTimeCallback(const GLX8_2_STR_CALLBACK_INFO* info, const GLX8_2_Data DataObj);
/**
* @brief
* @param info
* @param DataObj
*/
void ProcessBatchData(const GLX8_2_STR_CALLBACK_INFO* info, const GLX8_2_Data DataObj);
// 静态回调需要通过实例指针访问成员保存this指针供回调使用
static CGlLineLaserDevice* s_pInstance;
};
#endif // CGLLINELASERDEVICE_H

View File

@ -1,7 +1,7 @@
#!/bin/bash
# 可用的 App 列表
AVAILABLE_APPS="GrabBag BeltTearing LapWeld Workpiece ParticleSize BinocularMark WorkpieceProject TunnelChannel WheelMeasure ScrewPosition WorkpieceHole"
AVAILABLE_APPS="GrabBag BeltTearing LapWeld Workpiece ParticleSize BinocularMark WorkpieceProject TunnelChannel WheelMeasure ScrewPosition WorkpieceHole BagThreadPosition"
# 显示帮助信息
show_help() {

View File

@ -0,0 +1,410 @@
#!/bin/bash
#BagThreadPosition 版本配置
PKG_NAME="BagThreadPosition"
PKG_ARCH="arm64"
# 从Version.h文件中读取版本信息
VERSION_FILE="../App/BagThreadPosition/BagThreadPositionApp/Version.h"
if [ -f "${VERSION_FILE}" ]; then
# 读取版本号(从 BAGTHREADPOSITION_VERSION_STRING 中提取)
PKG_VERSION=$(grep '#define BAGTHREADPOSITION_VERSION_STRING' ${VERSION_FILE} | sed 's/.*"\(.*\)".*/\1/')
# 读取构建号(从 BAGTHREADPOSITION_BUILD_STRING 中提取)
BUILD_NUMBER=$(grep '#define BAGTHREADPOSITION_BUILD_STRING' ${VERSION_FILE} | sed 's/.*"\(.*\)".*/\1/')
echo "${VERSION_FILE} 读取版本信息:"
echo " 版本号: ${PKG_VERSION}"
echo " 构建号: ${BUILD_NUMBER}"
# 如果读取失败,使用默认值
if [ -z "${PKG_VERSION}" ]; then
PKG_VERSION="1.0.0"
echo "警告: 无法读取版本号,使用默认值: ${PKG_VERSION}"
fi
if [ -z "${BUILD_NUMBER}" ]; then
BUILD_NUMBER="1"
echo "警告: 无法读取构建号,使用默认值: ${BUILD_NUMBER}"
fi
else
# Version.h文件不存在时的默认值
PKG_VERSION="1.0.0"
BUILD_NUMBER="1"
echo "警告: ${VERSION_FILE} 文件不存在,使用默认版本信息"
echo " 版本号: ${PKG_VERSION}"
echo " 构建号: ${BUILD_NUMBER}"
fi
PKG_PATH=$HOME/BagThreadPositionPkg
CODE_PATH=../
RELEASE_PATH=../Publish
echo "=========================================="
echo "开始打包 BagThreadPosition 应用程序 v${PKG_VERSION}..."
echo "=========================================="
echo "清理所有旧的打包目录..."
rm -rf $HOME/*Pkg
#QT depend
QT_PKG_PATH=/opt/firefly_qt5.15_arm64_20.04
QT_LIB_PATH=/opt/sysroot/firefly-arm64-sysroot-20.04/lib/aarch64-linux-gnu
echo "创建打包目录结构..."
mkdir -p ${PKG_PATH}/DEBIAN
mkdir -p ${PKG_PATH}/etc/profile.d
mkdir -p ${PKG_PATH}/etc/xdg/autostart
mkdir -p ${PKG_PATH}/opt/sysroot/lib
mkdir -p ${PKG_PATH}/usr/local/bin
mkdir -p ${PKG_PATH}/usr/lib
mkdir -p ${PKG_PATH}/usr/share/applications
mkdir -p ${PKG_PATH}/usr/share/pixmaps
echo "复制 Qt 运行时环境..."
cp -rfd ${QT_PKG_PATH}/ext ${PKG_PATH}/opt/firefly_qt5.15
cp ${QT_PKG_PATH}/target_qtEnv.sh ${PKG_PATH}/etc/profile.d/
# 复制 Qt 库文件
for libfile in ${QT_LIB_PATH}/*.so*; do
# 获取文件名用于比较
filename=$(basename "$libfile")
# 跳过 LLVM、flite、clang 和 X11 相关库文件
if [[ "$filename" == *icu* ]]; then
# 复制其他库文件,保持符号链接
cp -rfd "$libfile" ${PKG_PATH}/opt/sysroot/lib/
continue
fi
done
echo "复制依赖库文件..."
#depend
cp -a ${CODE_PATH}/SDK/OpenCV320/Arm/aarch64/*opencv_core*.so* ${PKG_PATH}/usr/lib/
cp -a ${CODE_PATH}/SDK/OpenCV320/Arm/aarch64/*opencv_imgproc*.so* ${PKG_PATH}/usr/lib/
cp -a ${CODE_PATH}/SDK/OpenCV320/Arm/aarch64/*opencv_highgui*.so* ${PKG_PATH}/usr/lib/
cp ${CODE_PATH}/AppAlgo/bagThreadPositioning/Arm/aarch64/*.so ${PKG_PATH}/usr/lib/
cp ${CODE_PATH}/SDK/Device/VzNLSDK/Arm/aarch64/*.so ${PKG_PATH}/usr/lib/
cp ${CODE_PATH}/SDK/Device/gl_linelaser_sdk/aarch64_linux/*.so ${PKG_PATH}/usr/lib/
echo "复制应用程序主文件..."
#APP
cp ${CODE_PATH}/GrabBagPrj/buildarm/App/BagThreadPosition/BagThreadPositionApp/BagThreadPositionApp ${PKG_PATH}/usr/local/bin/
echo "复制应用程序图标..."
#LOGO
cp ${CODE_PATH}/App/BagThreadPosition/BagThreadPositionApp/resource/logo.png ${PKG_PATH}/usr/share/pixmaps/bagthreadposition.png
echo "生成桌面自启动配置文件..."
#desktop autostart configuration
AUTOSTART_PATH=${PKG_PATH}/etc/xdg/autostart/bagthreadposition.desktop
cat > ${AUTOSTART_PATH} << EOF
[Desktop Entry]
Type=Application
Name=BagThreadPosition
Comment=BagThreadPosition Application Auto Start
Exec=/usr/local/bin/BagThreadPositionApp
Icon=/usr/share/pixmaps/bagthreadposition.png
Hidden=false
NoDisplay=false
X-GNOME-Autostart-enabled=true
AutostartCondition=GNOME3 unless-session gnome
EOF
echo "生成 control 文件..."
#control
CONTROL_PATH=${PKG_PATH}/DEBIAN/control
echo "Package: ${PKG_NAME}" > ${CONTROL_PATH}
echo "Version: ${PKG_VERSION}" >> ${CONTROL_PATH}
echo "Section: bagthreadpositionapp" >> ${CONTROL_PATH}
echo "Architecture: ${PKG_ARCH}" >> ${CONTROL_PATH}
echo "Priority: optional" >> ${CONTROL_PATH}
echo "Maintainer: BagThreadPosition Team <support@bagthreadposition.com>" >> ${CONTROL_PATH}
echo "Description: bagthreadposition app" >> ${CONTROL_PATH}
echo "生成安装后脚本..."
#postinst install exec script
POSTINST_PATH=${PKG_PATH}/DEBIAN/postinst
cat > ${POSTINST_PATH} << 'EOF'
#!/bin/bash
echo "配置 BagThreadPosition 应用程序..."
# 创建应用注册目录和文件
REGISTRY_DIR="/var/lib/vr-apps"
REGISTRY_FILE="${REGISTRY_DIR}/installed-apps.list"
APP_NAME="BagThreadPosition"
mkdir -p "${REGISTRY_DIR}"
touch "${REGISTRY_FILE}"
# 检查是否是首次安装VR应用
FIRST_INSTALL=false
if [ ! -s "${REGISTRY_FILE}" ]; then
FIRST_INSTALL=true
echo "检测到首次安装VR应用将安装公共资源..."
fi
# 设置库文件路径(每个应用独立配置)
echo "/usr/lib" > /etc/ld.so.conf.d/bagthreadposition.conf
echo "/opt/sysroot/lib/" >> /etc/ld.so.conf.d/bagthreadposition.conf
# 如果不是首次安装,公共库已经配置过,只需要更新
if [ "${FIRST_INSTALL}" = true ]; then
echo "配置公共库路径..."
ldconfig
else
echo "公共库已存在,仅更新配置..."
ldconfig
fi
# 确保应用程序可执行
chmod +x /usr/local/bin/BagThreadPositionApp
# 配置端口映射 502 -> 5020
echo "配置端口映射 502 -> 5020..."
# 检查并创建systemd服务确保重启后规则生效
if [ ! -f /etc/systemd/system/vr-port-mapping.service ]; then
echo "创建端口映射服务..."
cat > /etc/systemd/system/vr-port-mapping.service << 'PORTEOF'
[Unit]
Description=VR Common Port Mapping Service
After=network.target
[Service]
Type=oneshot
ExecStart=/bin/bash -c 'iptables-legacy -t nat -A PREROUTING -p tcp -m tcp --dport 502 -j REDIRECT --to-port 5020'
RemainAfterExit=yes
[Install]
WantedBy=multi-user.target
PORTEOF
systemctl enable vr-port-mapping.service
systemctl start vr-port-mapping.service
else
echo "端口映射服务已存在,跳过创建..."
# 确保服务处于启用状态
systemctl enable vr-port-mapping.service 2>/dev/null || true
systemctl start vr-port-mapping.service 2>/dev/null || true
fi
# 检查iptables-legacy规则是否已存在
if ! iptables-legacy -t nat -C PREROUTING -p tcp -m tcp --dport 502 -j REDIRECT --to-port 5020 2>/dev/null; then
echo "添加iptables-legacy端口映射规则..."
iptables-legacy -t nat -A PREROUTING -p tcp -m tcp --dport 502 -j REDIRECT --to-port 5020
else
echo "iptables-legacy规则已存在跳过添加..."
fi
# 检查并创建当前用户的桌面快捷方式
echo "检查当前用户的桌面快捷方式..."
# 获取当前执行安装的用户信息
if [ -n "$SUDO_USER" ]; then
# 如果是通过sudo执行的获取真实用户
current_user="$SUDO_USER"
current_home=$(getent passwd "$current_user" | cut -d: -f6)
else
# 直接执行的情况
current_user=$(whoami)
current_home="$HOME"
fi
echo "当前用户: $current_user"
echo "用户主目录: $current_home"
# 检查多种可能的桌面目录名称
desktop_dirs=("Desktop" "桌面" "desktop")
desktop_dir=""
desktop_shortcut=""
for dir_name in "${desktop_dirs[@]}"; do
potential_dir="$current_home/$dir_name"
if [ -d "$potential_dir" ]; then
desktop_dir="$potential_dir"
desktop_shortcut="$desktop_dir/bagthreadposition.desktop"
echo "找到桌面目录: $desktop_dir"
break
fi
done
# 检查是否找到桌面目录
if [ -n "$desktop_dir" ]; then
# 检查桌面上是否已有快捷方式
if [ ! -f "$desktop_shortcut" ]; then
echo "为当前用户创建桌面快捷方式..."
# 复制桌面文件到用户桌面
cp /usr/share/applications/bagthreadposition.desktop "$desktop_shortcut"
# 设置正确的所有者和权限
chown $current_user:$current_user "$desktop_shortcut" 2>/dev/null || true
chmod 755 "$desktop_shortcut"
echo "已创建桌面快捷方式: $desktop_shortcut"
else
echo "桌面快捷方式已存在,跳过创建"
fi
else
echo "当前用户没有找到桌面目录(Desktop/桌面/desktop),跳过桌面快捷方式创建"
fi
# 注册应用到系统
if ! grep -q "^${APP_NAME}$" "${REGISTRY_FILE}"; then
echo "${APP_NAME}" >> "${REGISTRY_FILE}"
echo "应用 ${APP_NAME} 已注册到系统"
else
echo "应用 ${APP_NAME} 已经注册,跳过..."
fi
echo "BagThreadPosition 应用程序安装完成!"
echo "应用程序将在用户登录桌面后自动启动。"
echo "端口映射已配置502 -> 5020"
echo "桌面快捷方式已创建如果用户有Desktop目录"
echo "如需立即启动,请运行: /usr/local/bin/BagThreadPositionApp"
echo "如需禁用自启动,请删除文件: ~/.config/autostart/bagthreadposition.desktop"
echo "已安装的VR应用: $(cat ${REGISTRY_FILE} | tr '\n' ' ')"
EOF
chmod +x ${POSTINST_PATH}
echo "生成卸载脚本..."
#postrm uninstall exec script
POSTRM_PATH=${PKG_PATH}/DEBIAN/postrm
cat > ${POSTRM_PATH} << 'EOF'
#!/bin/bash
echo "卸载 BagThreadPosition 应用程序..."
# 应用注册信息
REGISTRY_DIR="/var/lib/vr-apps"
REGISTRY_FILE="${REGISTRY_DIR}/installed-apps.list"
APP_NAME="BagThreadPosition"
# 从注册表中移除应用
if [ -f "${REGISTRY_FILE}" ]; then
sed -i "/^${APP_NAME}$/d" "${REGISTRY_FILE}"
echo "应用 ${APP_NAME} 已从系统注册表中移除"
fi
# 清理应用专属的库文件配置
rm -f /etc/ld.so.conf.d/bagthreadposition.conf
# 检查是否还有其他VR应用在使用公共资源
REMAINING_APPS=0
if [ -f "${REGISTRY_FILE}" ]; then
REMAINING_APPS=$(grep -c . "${REGISTRY_FILE}" 2>/dev/null || echo "0")
fi
echo "系统中剩余的VR应用数量: ${REMAINING_APPS}"
# 如果没有其他应用了,清理公共资源
if [ "${REMAINING_APPS}" -eq 0 ]; then
echo "没有其他VR应用开始清理公共资源..."
# 清理端口映射配置
echo "清理端口映射配置..."
systemctl stop vr-port-mapping.service 2>/dev/null || true
systemctl disable vr-port-mapping.service 2>/dev/null || true
rm -f /etc/systemd/system/vr-port-mapping.service
# 清理iptables-legacy规则
iptables-legacy -t nat -D PREROUTING -p tcp -m tcp --dport 502 -j REDIRECT --to-port 5020 2>/dev/null || true
# 清理注册目录
rm -rf "${REGISTRY_DIR}"
echo "公共资源已清理"
else
echo "系统中还有 ${REMAINING_APPS} 个VR应用在运行保留公共资源"
if [ -f "${REGISTRY_FILE}" ]; then
echo "剩余应用: $(cat ${REGISTRY_FILE} | tr '\n' ' ')"
fi
fi
# 更新库缓存
ldconfig
# 重新加载systemd
systemctl daemon-reload
# 清理当前用户的桌面快捷方式
echo "清理当前用户的桌面快捷方式..."
# 获取当前执行卸载的用户信息
if [ -n "$SUDO_USER" ]; then
# 如果是通过sudo执行的获取真实用户
current_user="$SUDO_USER"
current_home=$(getent passwd "$current_user" | cut -d: -f6)
else
# 直接执行的情况
current_user=$(whoami)
current_home="$HOME"
fi
# 检查多种可能的桌面目录名称并清理快捷方式
desktop_dirs=("Desktop" "桌面" "desktop")
shortcut_found=false
for dir_name in "${desktop_dirs[@]}"; do
desktop_shortcut="$current_home/$dir_name/bagthreadposition.desktop"
if [ -f "$desktop_shortcut" ]; then
echo "删除当前用户的桌面快捷方式: $desktop_shortcut"
rm -f "$desktop_shortcut"
echo "已删除桌面快捷方式: $desktop_shortcut"
shortcut_found=true
fi
done
if [ "$shortcut_found" = false ]; then
echo "当前用户没有找到桌面快捷方式,跳过清理"
fi
echo "BagThreadPosition 应用程序卸载完成!"
if [ "${REMAINING_APPS}" -eq 0 ]; then
echo "所有VR应用已卸载公共资源已清理"
else
echo "公共资源已保留供其他VR应用使用"
fi
echo "桌面快捷方式已清理"
echo "如需彻底清理自启动配置,请手动删除: ~/.config/autostart/bagthreadposition.desktop"
EOF
chmod +x ${POSTRM_PATH}
echo "生成桌面快捷方式..."
#desktop
DESKTOP_PATH=${PKG_PATH}/usr/share/applications/bagthreadposition.desktop
echo "[Desktop Entry]" > ${DESKTOP_PATH}
echo "Version=${PKG_VERSION}" >> ${DESKTOP_PATH}
echo "Name=BagThreadPosition" >> ${DESKTOP_PATH}
echo "Type=Application" >> ${DESKTOP_PATH}
echo "Comment=BagThreadPosition App" >> ${DESKTOP_PATH}
echo "Terminal=false" >> ${DESKTOP_PATH}
echo "Exec=/usr/local/bin/BagThreadPositionApp" >> ${DESKTOP_PATH}
echo "Icon=/usr/share/pixmaps/bagthreadposition.png" >> ${DESKTOP_PATH}
echo "Categories=Development;" >> ${DESKTOP_PATH}
echo "GenericName=BagThreadPosition App" >> ${DESKTOP_PATH}
echo "Keywords=bagthreadposition;app;" >> ${DESKTOP_PATH}
echo "StartupNotify=true" >> ${DESKTOP_PATH}
echo "设置文件权限..."
# 设置usr目录权限不包括DEBIAN
chmod -R 755 ${PKG_PATH}/usr
chmod -R 755 ${PKG_PATH}/etc
chmod -R 755 ${PKG_PATH}/opt
echo "开始构建 DEB 包..."
# 生成带时间戳和构建号的包文件名
TIMESTAMP=$(date +%Y%m%d%H%M%S)
DEB_FILENAME="${RELEASE_PATH}/${PKG_NAME}_${PKG_VERSION}_${BUILD_NUMBER}_${PKG_ARCH}_${TIMESTAMP}.deb"
fakeroot dpkg -b ${PKG_PATH} ${DEB_FILENAME}
echo "=========================================="
echo "打包完成!"
echo "生成的包文件: ${DEB_FILENAME}"
echo "文件大小: $(ls -lh ${DEB_FILENAME} | awk '{print $5}')"
echo "=========================================="

View File

@ -85,7 +85,7 @@ cp -a ${CODE_PATH}/SDK/OpenCV320/Arm/aarch64/*opencv_imgproc*.so* ${PKG_PATH}/u
cp -a ${CODE_PATH}/SDK/OpenCV320/Arm/aarch64/*opencv_highgui*.so* ${PKG_PATH}/usr/lib/
cp ${CODE_PATH}/AppAlgo/workpieceHolePositioning/Arm/aarch64/*.so ${PKG_PATH}/usr/lib/
cp ${CODE_PATH}/SDK/Device/VzNLSDK/Arm/aarch64/*.so ${PKG_PATH}/usr/lib/
cp ${CODE_PATH}/SDK/Device/VzNLSDK/Arm/aarch64/*.so ${PKG_PATH}/usr/lib/
echo "复制应用程序主文件..."
#APP

View File

@ -43,6 +43,16 @@ private slots:
*/
void onOpenFile();
/**
* @brief 线
*/
void onOpenSegmentFile();
/**
* @brief 姿
*/
void onOpenPoseFile();
/**
* @brief
*/
@ -98,6 +108,26 @@ private slots:
*/
void onLinePointTableClicked(int row, int column);
/**
* @brief 1姿
*/
void onShowPose1();
/**
* @brief 2姿
*/
void onShowPose2();
/**
* @brief 线
*/
void onShowInputLine();
/**
* @brief 线
*/
void onClearInputLine();
private:
/**
* @brief
@ -124,6 +154,16 @@ private:
*/
QGroupBox* createMeasureGroup();
/**
* @brief
*/
QWidget* createMeasurePage();
/**
* @brief 线
*/
QWidget* createLinePage();
/**
* @brief 线
*/
@ -157,6 +197,8 @@ private:
// 文件操作控件
QPushButton* m_btnOpenFile;
QPushButton* m_btnOpenSegment;
QPushButton* m_btnOpenPose;
QPushButton* m_btnClearAll;
QPushButton* m_btnResetView;
@ -167,6 +209,18 @@ private:
QLabel* m_lblPoint2;
QLabel* m_lblDistance;
// 点1姿态输入控件
QLineEdit* m_editRx1;
QLineEdit* m_editRy1;
QLineEdit* m_editRz1;
QPushButton* m_btnShowPose1;
// 点2姿态输入控件
QLineEdit* m_editRx2;
QLineEdit* m_editRy2;
QLineEdit* m_editRz2;
QPushButton* m_btnShowPose2;
// 选线拟合控件
QPushButton* m_btnClearLine;
QPushButton* m_btnShowLinePoints;
@ -177,6 +231,16 @@ private:
QLabel* m_lblLineIndex;
QLabel* m_lblLinePointCount;
// 输入线段控件
QLineEdit* m_editLineX1;
QLineEdit* m_editLineY1;
QLineEdit* m_editLineZ1;
QLineEdit* m_editLineX2;
QLineEdit* m_editLineY2;
QLineEdit* m_editLineZ2;
QPushButton* m_btnShowLine;
QPushButton* m_btnClearLine2;
// 点云列表
QListWidget* m_cloudList;

View File

@ -65,6 +65,34 @@ struct SelectedLineInfo
SelectedLineInfo() : valid(false), cloudIndex(-1), lineIndex(-1), pointIndex(-1), pointCount(0), mode(LineSelectMode::Vertical) {}
};
/**
* @brief 线
*/
struct LineSegment
{
float x1, y1, z1; // 起点
float x2, y2, z2; // 终点
float r, g, b; // 颜色 (0-1)
LineSegment() : x1(0), y1(0), z1(0), x2(0), y2(0), z2(0), r(1), g(1), b(1) {}
LineSegment(float _x1, float _y1, float _z1, float _x2, float _y2, float _z2, float _r = 1.0f, float _g = 1.0f, float _b = 1.0f)
: x1(_x1), y1(_y1), z1(_z1), x2(_x2), y2(_y2), z2(_z2), r(_r), g(_g), b(_b) {}
};
/**
* @brief 姿
*/
struct PosePoint
{
float x, y, z; // 位置
float rx, ry, rz; // 欧拉角(度)
float scale; // 坐标系大小
PosePoint() : x(0), y(0), z(0), rx(0), ry(0), rz(0), scale(10.0f) {}
PosePoint(float _x, float _y, float _z, float _rx, float _ry, float _rz, float _scale = 10.0f)
: x(_x), y(_y), z(_z), rx(_rx), ry(_ry), rz(_rz), scale(_scale) {}
};
/**
* @brief OpenGL
*/
@ -145,6 +173,26 @@ public:
*/
size_t getCloudCount() const { return m_pointClouds.size(); }
/**
* @brief 线
*/
void addLineSegments(const QVector<LineSegment>& segments);
/**
* @brief 线
*/
void clearLineSegments();
/**
* @brief 姿
*/
void addPosePoints(const QVector<PosePoint>& poses);
/**
* @brief 姿
*/
void clearPosePoints();
signals:
void pointSelected(const SelectedPointInfo& point);
void twoPointsSelected(const SelectedPointInfo& p1, const SelectedPointInfo& p2, float distance);
@ -168,6 +216,8 @@ private:
void drawMeasurementLine();
void drawAxis();
void drawSelectedLine(); // 绘制选中的线
void drawLineSegments(); // 绘制线段
void drawPosePoints(); // 绘制姿态点
struct PointCloudData
{
@ -216,6 +266,10 @@ private:
int m_colorIndex; // 颜色轮换索引
static const int COLOR_COUNT = 7; // 可用颜色数量
// 线段和姿态点数据
QVector<LineSegment> m_lineSegments;
QVector<PosePoint> m_posePoints;
};
#endif // POINT_CLOUD_GL_WIDGET_H

View File

@ -5,6 +5,12 @@
#include <QTableWidget>
#include <QHeaderView>
#include <QVector3D>
#include <QFile>
#include <QTextStream>
#include <QRegExp>
#include <QFrame>
#include <QTabWidget>
#include <cmath>
#include "VrLog.h"
CloudViewMainWindow::CloudViewMainWindow(QWidget* parent)
@ -81,7 +87,7 @@ QWidget* CloudViewMainWindow::createViewerArea()
QWidget* CloudViewMainWindow::createControlPanel()
{
QWidget* widget = new QWidget(this);
widget->setMaximumWidth(300);
widget->setMaximumWidth(400); // 加宽到400
QVBoxLayout* layout = new QVBoxLayout(widget);
layout->setContentsMargins(5, 5, 5, 5);
layout->setSpacing(10);
@ -89,11 +95,11 @@ QWidget* CloudViewMainWindow::createControlPanel()
// 文件操作组
layout->addWidget(createFileGroup());
// 选点测距组
layout->addWidget(createMeasureGroup());
// 选线拟合组
layout->addWidget(createLineGroup());
// 创建 Tab 控件
QTabWidget* tabWidget = new QTabWidget(widget);
tabWidget->addTab(createMeasurePage(), "选点测距");
tabWidget->addTab(createLinePage(), "选线");
layout->addWidget(tabWidget);
// 点云列表组
layout->addWidget(createCloudListGroup());
@ -107,14 +113,22 @@ QWidget* CloudViewMainWindow::createControlPanel()
QGroupBox* CloudViewMainWindow::createFileGroup()
{
QGroupBox* group = new QGroupBox("文件操作", this);
group->setMaximumWidth(300);
group->setMaximumWidth(400);
QVBoxLayout* layout = new QVBoxLayout(group);
m_btnOpenFile = new QPushButton("打开文件", group);
m_btnOpenFile = new QPushButton("打开点云", group);
m_btnOpenFile->setMinimumHeight(30);
connect(m_btnOpenFile, &QPushButton::clicked, this, &CloudViewMainWindow::onOpenFile);
layout->addWidget(m_btnOpenFile);
m_btnOpenSegment = new QPushButton("打开线段 {x,y,z}-{x,y,z}", group);
connect(m_btnOpenSegment, &QPushButton::clicked, this, &CloudViewMainWindow::onOpenSegmentFile);
layout->addWidget(m_btnOpenSegment);
m_btnOpenPose = new QPushButton("打开姿态点 {x,y,z}-{r,p,y}", group);
connect(m_btnOpenPose, &QPushButton::clicked, this, &CloudViewMainWindow::onOpenPoseFile);
layout->addWidget(m_btnOpenPose);
m_btnClearAll = new QPushButton("清除所有", group);
connect(m_btnClearAll, &QPushButton::clicked, this, &CloudViewMainWindow::onClearAll);
layout->addWidget(m_btnClearAll);
@ -126,10 +140,98 @@ QGroupBox* CloudViewMainWindow::createFileGroup()
return group;
}
QWidget* CloudViewMainWindow::createMeasurePage()
{
QWidget* page = new QWidget(this);
QVBoxLayout* layout = new QVBoxLayout(page);
layout->setContentsMargins(0, 5, 0, 0);
layout->addWidget(createMeasureGroup());
layout->addStretch();
return page;
}
QWidget* CloudViewMainWindow::createLinePage()
{
QWidget* page = new QWidget(this);
QVBoxLayout* layout = new QVBoxLayout(page);
layout->setContentsMargins(0, 5, 0, 0);
// 选线拟合组
layout->addWidget(createLineGroup());
// 输入线段组
QGroupBox* inputLineGroup = new QGroupBox("输入线段", page);
QVBoxLayout* inputLayout = new QVBoxLayout(inputLineGroup);
// 提示
QLabel* lblTip = new QLabel("输入两点坐标显示线段", inputLineGroup);
lblTip->setStyleSheet("color: gray; font-size: 10px;");
inputLayout->addWidget(lblTip);
// 点1坐标
QLabel* lblPoint1 = new QLabel("点1:", inputLineGroup);
lblPoint1->setStyleSheet("font-weight: bold;");
inputLayout->addWidget(lblPoint1);
QHBoxLayout* p1Layout = new QHBoxLayout();
p1Layout->addWidget(new QLabel("X:", inputLineGroup));
m_editLineX1 = new QLineEdit("0.0", inputLineGroup);
m_editLineX1->setMaximumWidth(70);
p1Layout->addWidget(m_editLineX1);
p1Layout->addWidget(new QLabel("Y:", inputLineGroup));
m_editLineY1 = new QLineEdit("0.0", inputLineGroup);
m_editLineY1->setMaximumWidth(70);
p1Layout->addWidget(m_editLineY1);
p1Layout->addWidget(new QLabel("Z:", inputLineGroup));
m_editLineZ1 = new QLineEdit("0.0", inputLineGroup);
m_editLineZ1->setMaximumWidth(70);
p1Layout->addWidget(m_editLineZ1);
inputLayout->addLayout(p1Layout);
// 点2坐标
QLabel* lblPoint2 = new QLabel("点2:", inputLineGroup);
lblPoint2->setStyleSheet("font-weight: bold;");
inputLayout->addWidget(lblPoint2);
QHBoxLayout* p2Layout = new QHBoxLayout();
p2Layout->addWidget(new QLabel("X:", inputLineGroup));
m_editLineX2 = new QLineEdit("100.0", inputLineGroup);
m_editLineX2->setMaximumWidth(70);
p2Layout->addWidget(m_editLineX2);
p2Layout->addWidget(new QLabel("Y:", inputLineGroup));
m_editLineY2 = new QLineEdit("100.0", inputLineGroup);
m_editLineY2->setMaximumWidth(70);
p2Layout->addWidget(m_editLineY2);
p2Layout->addWidget(new QLabel("Z:", inputLineGroup));
m_editLineZ2 = new QLineEdit("100.0", inputLineGroup);
m_editLineZ2->setMaximumWidth(70);
p2Layout->addWidget(m_editLineZ2);
inputLayout->addLayout(p2Layout);
// 按钮
QHBoxLayout* btnLayout = new QHBoxLayout();
m_btnShowLine = new QPushButton("显示线段", inputLineGroup);
connect(m_btnShowLine, &QPushButton::clicked, this, &CloudViewMainWindow::onShowInputLine);
btnLayout->addWidget(m_btnShowLine);
m_btnClearLine2 = new QPushButton("清除线段", inputLineGroup);
connect(m_btnClearLine2, &QPushButton::clicked, this, &CloudViewMainWindow::onClearInputLine);
btnLayout->addWidget(m_btnClearLine2);
inputLayout->addLayout(btnLayout);
layout->addWidget(inputLineGroup);
layout->addStretch();
return page;
}
QGroupBox* CloudViewMainWindow::createMeasureGroup()
{
QGroupBox* group = new QGroupBox("选点测距", this);
group->setMaximumWidth(300);
group->setMaximumWidth(400);
QVBoxLayout* layout = new QVBoxLayout(group);
// 操作说明
@ -156,23 +258,91 @@ QGroupBox* CloudViewMainWindow::createMeasureGroup()
connect(m_btnClearPoints, &QPushButton::clicked, this, &CloudViewMainWindow::onClearSelectedPoints);
layout->addWidget(m_btnClearPoints);
// 点1信息
QHBoxLayout* point1Layout = new QHBoxLayout();
// 分隔线
QFrame* line1 = new QFrame(group);
line1->setFrameShape(QFrame::HLine);
line1->setFrameShadow(QFrame::Sunken);
layout->addWidget(line1);
// ========== 点1信息和姿态 ==========
QLabel* lblPoint1Title = new QLabel("点1:", group);
lblPoint1Title->setStyleSheet("font-weight: bold;");
layout->addWidget(lblPoint1Title);
m_lblPoint1 = new QLabel("--", group);
m_lblPoint1->setWordWrap(true);
point1Layout->addWidget(lblPoint1Title);
point1Layout->addWidget(m_lblPoint1, 1);
layout->addLayout(point1Layout);
layout->addWidget(m_lblPoint1);
// 点2信息
QHBoxLayout* point2Layout = new QHBoxLayout();
// 点1姿态输入
QHBoxLayout* pose1Layout = new QHBoxLayout();
pose1Layout->addWidget(new QLabel("RX:", group));
m_editRx1 = new QLineEdit("0.0", group);
m_editRx1->setMaximumWidth(60);
pose1Layout->addWidget(m_editRx1);
pose1Layout->addWidget(new QLabel("°", group));
pose1Layout->addWidget(new QLabel("RY:", group));
m_editRy1 = new QLineEdit("0.0", group);
m_editRy1->setMaximumWidth(60);
pose1Layout->addWidget(m_editRy1);
pose1Layout->addWidget(new QLabel("°", group));
pose1Layout->addWidget(new QLabel("RZ:", group));
m_editRz1 = new QLineEdit("0.0", group);
m_editRz1->setMaximumWidth(60);
pose1Layout->addWidget(m_editRz1);
pose1Layout->addWidget(new QLabel("°", group));
layout->addLayout(pose1Layout);
m_btnShowPose1 = new QPushButton("显示点1姿态", group);
connect(m_btnShowPose1, &QPushButton::clicked, this, &CloudViewMainWindow::onShowPose1);
layout->addWidget(m_btnShowPose1);
// 分隔线
QFrame* line2 = new QFrame(group);
line2->setFrameShape(QFrame::HLine);
line2->setFrameShadow(QFrame::Sunken);
layout->addWidget(line2);
// ========== 点2信息和姿态 ==========
QLabel* lblPoint2Title = new QLabel("点2:", group);
lblPoint2Title->setStyleSheet("font-weight: bold;");
layout->addWidget(lblPoint2Title);
m_lblPoint2 = new QLabel("--", group);
m_lblPoint2->setWordWrap(true);
point2Layout->addWidget(lblPoint2Title);
point2Layout->addWidget(m_lblPoint2, 1);
layout->addLayout(point2Layout);
layout->addWidget(m_lblPoint2);
// 点2姿态输入
QHBoxLayout* pose2Layout = new QHBoxLayout();
pose2Layout->addWidget(new QLabel("RX:", group));
m_editRx2 = new QLineEdit("0.0", group);
m_editRx2->setMaximumWidth(60);
pose2Layout->addWidget(m_editRx2);
pose2Layout->addWidget(new QLabel("°", group));
pose2Layout->addWidget(new QLabel("RY:", group));
m_editRy2 = new QLineEdit("0.0", group);
m_editRy2->setMaximumWidth(60);
pose2Layout->addWidget(m_editRy2);
pose2Layout->addWidget(new QLabel("°", group));
pose2Layout->addWidget(new QLabel("RZ:", group));
m_editRz2 = new QLineEdit("0.0", group);
m_editRz2->setMaximumWidth(60);
pose2Layout->addWidget(m_editRz2);
pose2Layout->addWidget(new QLabel("°", group));
layout->addLayout(pose2Layout);
m_btnShowPose2 = new QPushButton("显示点2姿态", group);
connect(m_btnShowPose2, &QPushButton::clicked, this, &CloudViewMainWindow::onShowPose2);
layout->addWidget(m_btnShowPose2);
// 分隔线
QFrame* line3 = new QFrame(group);
line3->setFrameShape(QFrame::HLine);
line3->setFrameShadow(QFrame::Sunken);
layout->addWidget(line3);
// 距离信息
QHBoxLayout* distLayout = new QHBoxLayout();
@ -189,7 +359,7 @@ QGroupBox* CloudViewMainWindow::createMeasureGroup()
QGroupBox* CloudViewMainWindow::createLineGroup()
{
QGroupBox* group = new QGroupBox("选线", this);
group->setMaximumWidth(300);
group->setMaximumWidth(400);
QVBoxLayout* layout = new QVBoxLayout(group);
// 操作说明
@ -250,7 +420,7 @@ QGroupBox* CloudViewMainWindow::createLineGroup()
QGroupBox* CloudViewMainWindow::createCloudListGroup()
{
QGroupBox* group = new QGroupBox("点云列表", this);
group->setMaximumWidth(300);
group->setMaximumWidth(400);
QVBoxLayout* layout = new QVBoxLayout(group);
m_cloudList = new QListWidget(group);
@ -317,6 +487,168 @@ void CloudViewMainWindow::onOpenFile()
statusBar()->showMessage(QString("已加载 %1 个点").arg(m_converter->getLoadedPointCount()));
}
void CloudViewMainWindow::onOpenSegmentFile()
{
QString fileName = QFileDialog::getOpenFileName(
this,
"打开线段文件",
QString(),
"文本文件 (*.txt);;所有文件 (*.*)"
);
if (fileName.isEmpty()) {
return;
}
statusBar()->showMessage("正在加载线段...");
QFile file(fileName);
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
QMessageBox::critical(this, "错误", QString("无法打开文件: %1").arg(fileName));
statusBar()->showMessage("加载失败");
return;
}
QVector<LineSegment> segments;
QTextStream in(&file);
int lineNum = 0;
int validCount = 0;
while (!in.atEnd()) {
QString line = in.readLine().trimmed();
lineNum++;
// 跳过空行和注释
if (line.isEmpty() || line.startsWith('#')) {
continue;
}
// 解析格式:{x,y,z}-{x,y,z}
QRegExp regex("\\{([^}]+)\\}-\\{([^}]+)\\}");
if (regex.indexIn(line) == -1) {
LOG_WARN("[CloudView] Line %d: Invalid format, expected {x,y,z}-{x,y,z}\n", lineNum);
continue;
}
QString point1Str = regex.cap(1);
QString point2Str = regex.cap(2);
QStringList p1 = point1Str.split(',');
QStringList p2 = point2Str.split(',');
if (p1.size() != 3 || p2.size() != 3) {
LOG_WARN("[CloudView] Line %d: Invalid point format\n", lineNum);
continue;
}
bool ok = true;
float x1 = p1[0].toFloat(&ok); if (!ok) continue;
float y1 = p1[1].toFloat(&ok); if (!ok) continue;
float z1 = p1[2].toFloat(&ok); if (!ok) continue;
float x2 = p2[0].toFloat(&ok); if (!ok) continue;
float y2 = p2[1].toFloat(&ok); if (!ok) continue;
float z2 = p2[2].toFloat(&ok); if (!ok) continue;
// 默认白色
segments.append(LineSegment(x1, y1, z1, x2, y2, z2, 1.0f, 1.0f, 1.0f));
validCount++;
}
file.close();
if (segments.isEmpty()) {
QMessageBox::warning(this, "警告", "文件中没有有效的线段数据");
statusBar()->showMessage("加载失败");
return;
}
m_glWidget->addLineSegments(segments);
statusBar()->showMessage(QString("已加载 %1 条线段").arg(validCount));
LOG_INFO("[CloudView] Loaded %d line segments from %s\n", validCount, fileName.toStdString().c_str());
}
void CloudViewMainWindow::onOpenPoseFile()
{
QString fileName = QFileDialog::getOpenFileName(
this,
"打开姿态点文件",
QString(),
"文本文件 (*.txt);;所有文件 (*.*)"
);
if (fileName.isEmpty()) {
return;
}
statusBar()->showMessage("正在加载姿态点...");
QFile file(fileName);
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
QMessageBox::critical(this, "错误", QString("无法打开文件: %1").arg(fileName));
statusBar()->showMessage("加载失败");
return;
}
QVector<PosePoint> poses;
QTextStream in(&file);
int lineNum = 0;
int validCount = 0;
while (!in.atEnd()) {
QString line = in.readLine().trimmed();
lineNum++;
// 跳过空行和注释
if (line.isEmpty() || line.startsWith('#')) {
continue;
}
// 解析格式:{x,y,z}-{r,p,y}
QRegExp regex("\\{([^}]+)\\}-\\{([^}]+)\\}");
if (regex.indexIn(line) == -1) {
LOG_WARN("[CloudView] Line %d: Invalid format, expected {x,y,z}-{r,p,y}\n", lineNum);
continue;
}
QString posStr = regex.cap(1);
QString rotStr = regex.cap(2);
QStringList pos = posStr.split(',');
QStringList rot = rotStr.split(',');
if (pos.size() != 3 || rot.size() != 3) {
LOG_WARN("[CloudView] Line %d: Invalid point format\n", lineNum);
continue;
}
bool ok = true;
float x = pos[0].toFloat(&ok); if (!ok) continue;
float y = pos[1].toFloat(&ok); if (!ok) continue;
float z = pos[2].toFloat(&ok); if (!ok) continue;
float roll = rot[0].toFloat(&ok); if (!ok) continue;
float pitch = rot[1].toFloat(&ok); if (!ok) continue;
float yaw = rot[2].toFloat(&ok); if (!ok) continue;
// 固定大小为10
float scale = 10.0f;
poses.append(PosePoint(x, y, z, roll, pitch, yaw, scale));
validCount++;
}
file.close();
if (poses.isEmpty()) {
QMessageBox::warning(this, "警告", "文件中没有有效的姿态点数据");
statusBar()->showMessage("加载失败");
return;
}
m_glWidget->addPosePoints(poses);
statusBar()->showMessage(QString("已加载 %1 个姿态点").arg(validCount));
LOG_INFO("[CloudView] Loaded %d pose points from %s\n", validCount, fileName.toStdString().c_str());
}
void CloudViewMainWindow::onClearAll()
{
m_glWidget->clearPointClouds();
@ -335,7 +667,7 @@ void CloudViewMainWindow::onClearAll()
m_lblLineIndex->setText("--");
m_lblLinePointCount->setText("--");
statusBar()->showMessage("已清除所有点云");
statusBar()->showMessage("已清除所有数据");
}
void CloudViewMainWindow::onResetView()
@ -347,6 +679,7 @@ void CloudViewMainWindow::onResetView()
void CloudViewMainWindow::onClearSelectedPoints()
{
m_glWidget->clearSelectedPoints();
m_glWidget->clearPosePoints(); // 清除选点时也清除姿态
m_lblPoint1->setText("--");
m_lblPoint2->setText("--");
m_lblDistance->setText("--");
@ -359,6 +692,9 @@ void CloudViewMainWindow::onPointSelected(const SelectedPointInfo& point)
return;
}
// 选择新点时清除之前的姿态显示
m_glWidget->clearPosePoints();
updateSelectedPointsDisplay();
// 状态栏显示:坐标、线号、索引号
@ -705,3 +1041,205 @@ void CloudViewMainWindow::onLinePointTableClicked(int row, int column)
}
}
void CloudViewMainWindow::onShowPose1()
{
auto selectedPoints = m_glWidget->getSelectedPoints();
if (selectedPoints.isEmpty() || !selectedPoints[0].valid) {
QMessageBox::warning(this, "提示", "请先选择点1Ctrl+左键点击点云)");
return;
}
const auto& point = selectedPoints[0];
// 读取姿态参数
bool ok = true;
float rx = m_editRx1->text().toFloat(&ok);
if (!ok) {
QMessageBox::warning(this, "错误", "点1 RX 值无效");
return;
}
float ry = m_editRy1->text().toFloat(&ok);
if (!ok) {
QMessageBox::warning(this, "错误", "点1 RY 值无效");
return;
}
float rz = m_editRz1->text().toFloat(&ok);
if (!ok) {
QMessageBox::warning(this, "错误", "点1 RZ 值无效");
return;
}
// 固定大小为10
float scale = 10.0f;
// 清除之前的姿态点
m_glWidget->clearPosePoints();
// 创建点1的姿态点
PosePoint pose1(point.x, point.y, point.z, rx, ry, rz, scale);
QVector<PosePoint> poses;
poses.append(pose1);
// 如果点2也存在添加点2的姿态
if (selectedPoints.size() >= 2 && selectedPoints[1].valid) {
const auto& point2 = selectedPoints[1];
float rx2 = m_editRx2->text().toFloat(&ok);
float ry2 = m_editRy2->text().toFloat(&ok);
float rz2 = m_editRz2->text().toFloat(&ok);
if (ok) {
PosePoint pose2(point2.x, point2.y, point2.z, rx2, ry2, rz2, scale);
poses.append(pose2);
}
}
// 添加到显示
m_glWidget->addPosePoints(poses);
statusBar()->showMessage(QString("已显示点1姿态 (%.3f, %.3f, %.3f) 旋转(%.1f°, %.1f°, %.1f°)")
.arg(point.x).arg(point.y).arg(point.z)
.arg(rx).arg(ry).arg(rz));
LOG_INFO("[CloudView] Show pose1 at (%.3f, %.3f, %.3f) with rotation (%.1f, %.1f, %.1f)\n",
point.x, point.y, point.z, rx, ry, rz);
}
void CloudViewMainWindow::onShowPose2()
{
auto selectedPoints = m_glWidget->getSelectedPoints();
if (selectedPoints.size() < 2 || !selectedPoints[1].valid) {
QMessageBox::warning(this, "提示", "请先选择点2启用测距后Ctrl+左键点击第二个点)");
return;
}
const auto& point = selectedPoints[1];
// 读取姿态参数
bool ok = true;
float rx = m_editRx2->text().toFloat(&ok);
if (!ok) {
QMessageBox::warning(this, "错误", "点2 RX 值无效");
return;
}
float ry = m_editRy2->text().toFloat(&ok);
if (!ok) {
QMessageBox::warning(this, "错误", "点2 RY 值无效");
return;
}
float rz = m_editRz2->text().toFloat(&ok);
if (!ok) {
QMessageBox::warning(this, "错误", "点2 RZ 值无效");
return;
}
// 固定大小为10
float scale = 10.0f;
// 清除之前的姿态点
m_glWidget->clearPosePoints();
// 创建点2的姿态点
PosePoint pose2(point.x, point.y, point.z, rx, ry, rz, scale);
QVector<PosePoint> poses;
// 如果点1也存在添加点1的姿态
if (selectedPoints[0].valid) {
const auto& point1 = selectedPoints[0];
float rx1 = m_editRx1->text().toFloat(&ok);
float ry1 = m_editRy1->text().toFloat(&ok);
float rz1 = m_editRz1->text().toFloat(&ok);
if (ok) {
PosePoint pose1(point1.x, point1.y, point1.z, rx1, ry1, rz1, scale);
poses.append(pose1);
}
}
poses.append(pose2);
// 添加到显示
m_glWidget->addPosePoints(poses);
statusBar()->showMessage(QString("已显示点2姿态 (%.3f, %.3f, %.3f) 旋转(%.1f°, %.1f°, %.1f°)")
.arg(point.x).arg(point.y).arg(point.z)
.arg(rx).arg(ry).arg(rz));
LOG_INFO("[CloudView] Show pose2 at (%.3f, %.3f, %.3f) with rotation (%.1f, %.1f, %.1f)\n",
point.x, point.y, point.z, rx, ry, rz);
}
void CloudViewMainWindow::onShowInputLine()
{
// 读取点1坐标
bool ok = true;
float x1 = m_editLineX1->text().toFloat(&ok);
if (!ok) {
QMessageBox::warning(this, "错误", "点1 X 值无效");
return;
}
float y1 = m_editLineY1->text().toFloat(&ok);
if (!ok) {
QMessageBox::warning(this, "错误", "点1 Y 值无效");
return;
}
float z1 = m_editLineZ1->text().toFloat(&ok);
if (!ok) {
QMessageBox::warning(this, "错误", "点1 Z 值无效");
return;
}
// 读取点2坐标
float x2 = m_editLineX2->text().toFloat(&ok);
if (!ok) {
QMessageBox::warning(this, "错误", "点2 X 值无效");
return;
}
float y2 = m_editLineY2->text().toFloat(&ok);
if (!ok) {
QMessageBox::warning(this, "错误", "点2 Y 值无效");
return;
}
float z2 = m_editLineZ2->text().toFloat(&ok);
if (!ok) {
QMessageBox::warning(this, "错误", "点2 Z 值无效");
return;
}
// 清除之前的线段
m_glWidget->clearLineSegments();
// 创建线段(红色)
LineSegment segment(x1, y1, z1, x2, y2, z2, 1.0f, 0.0f, 0.0f);
QVector<LineSegment> segments;
segments.append(segment);
// 添加到显示
m_glWidget->addLineSegments(segments);
// 计算距离
float dx = x2 - x1;
float dy = y2 - y1;
float dz = z2 - z1;
float distance = std::sqrt(dx * dx + dy * dy + dz * dz);
statusBar()->showMessage(QString("已显示线段 (%1,%2,%3) → (%4,%5,%6) 长度: %7")
.arg(x1).arg(y1).arg(z1)
.arg(x2).arg(y2).arg(z2)
.arg(distance));
LOG_INFO("[CloudView] Show input line from (%.3f, %.3f, %.3f) to (%.3f, %.3f, %.3f) length=%.3f\n",
x1, y1, z1, x2, y2, z2, distance);
}
void CloudViewMainWindow::onClearInputLine()
{
m_glWidget->clearLineSegments();
statusBar()->showMessage("已清除输入的线段");
}

View File

@ -116,6 +116,8 @@ void PointCloudGLWidget::paintGL()
drawSelectedPoints();
drawMeasurementLine();
drawSelectedLine();
drawLineSegments();
drawPosePoints();
// 最后绘制坐标系指示器(覆盖在所有内容之上)
drawAxis();
@ -248,6 +250,8 @@ void PointCloudGLWidget::clearPointClouds()
m_pointClouds.clear();
m_selectedPoints.clear();
m_selectedLine = SelectedLineInfo();
m_lineSegments.clear();
m_posePoints.clear();
m_minBound = QVector3D(-50, -50, -50);
m_maxBound = QVector3D(50, 50, 50);
m_center = QVector3D(0, 0, 0);
@ -978,3 +982,91 @@ void PointCloudGLWidget::replaceFirstCloud(const PointCloudXYZ& cloud, const QSt
resetView();
update();
}
void PointCloudGLWidget::addLineSegments(const QVector<LineSegment>& segments)
{
m_lineSegments.append(segments);
update();
}
void PointCloudGLWidget::clearLineSegments()
{
m_lineSegments.clear();
update();
}
void PointCloudGLWidget::addPosePoints(const QVector<PosePoint>& poses)
{
m_posePoints.append(poses);
update();
}
void PointCloudGLWidget::clearPosePoints()
{
m_posePoints.clear();
update();
}
void PointCloudGLWidget::drawLineSegments()
{
if (m_lineSegments.isEmpty()) {
return;
}
glLineWidth(2.0f);
glBegin(GL_LINES);
for (const auto& seg : m_lineSegments) {
glColor3f(seg.r, seg.g, seg.b);
glVertex3f(seg.x1, seg.y1, seg.z1);
glVertex3f(seg.x2, seg.y2, seg.z2);
}
glEnd();
glLineWidth(1.0f);
}
void PointCloudGLWidget::drawPosePoints()
{
if (m_posePoints.isEmpty()) {
return;
}
glLineWidth(2.0f);
for (const auto& pose : m_posePoints) {
glPushMatrix();
// 移动到姿态点位置
glTranslatef(pose.x, pose.y, pose.z);
// 应用欧拉角旋转ZYX顺序
glRotatef(pose.rz, 0, 0, 1); // Z轴旋转
glRotatef(pose.ry, 0, 1, 0); // Y轴旋转
glRotatef(pose.rx, 1, 0, 0); // X轴旋转
// 绘制坐标系
glBegin(GL_LINES);
// X轴 - 红色
glColor3f(1.0f, 0.0f, 0.0f);
glVertex3f(0.0f, 0.0f, 0.0f);
glVertex3f(pose.scale, 0.0f, 0.0f);
// Y轴 - 绿色
glColor3f(0.0f, 1.0f, 0.0f);
glVertex3f(0.0f, 0.0f, 0.0f);
glVertex3f(0.0f, pose.scale, 0.0f);
// Z轴 - 蓝色
glColor3f(0.0f, 0.0f, 1.0f);
glVertex3f(0.0f, 0.0f, 0.0f);
glVertex3f(0.0f, 0.0f, pose.scale);
glEnd();
glPopMatrix();
}
glLineWidth(1.0f);
}

17
Tools/CloudView/poses.txt Normal file
View File

@ -0,0 +1,17 @@
# 姿态点示例
# 格式:{x,y,z}-{r,p,y}
# 原点
{0,0,0}-{0,0,0}
# X轴上
{100,0,0}-{0,0,45}
# Y轴上
{0,100,0}-{0,0,90}
# Z轴上
{0,0,100}-{0,45,0}
# 对角线
{50,50,50}-{30,30,30}

View File

@ -0,0 +1,23 @@
# 线段示例
# 格式:{x,y,z}-{x,y,z}
# 坐标轴
{0,0,0}-{100,0,0}
{0,0,0}-{0,100,0}
{0,0,0}-{0,0,100}
# 立方体
{50,50,50}-{150,50,50}
{150,50,50}-{150,150,50}
{150,150,50}-{50,150,50}
{50,150,50}-{50,50,50}
{50,50,150}-{150,50,150}
{150,50,150}-{150,150,150}
{150,150,150}-{50,150,150}
{50,150,150}-{50,50,150}
{50,50,50}-{50,50,150}
{150,50,50}-{150,50,150}
{150,150,50}-{150,150,150}
{50,150,50}-{50,150,150}