Compare commits
7 Commits
354552dc88
...
0.3.0
| Author | SHA1 | Date | |
|---|---|---|---|
| c9495680d6 | |||
| 4882dc1a67 | |||
| bc9f2824ed | |||
|
|
54d285cbc8 | ||
| 59564fd312 | |||
| 053e247380 | |||
| 02f5368b89 |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -11,7 +11,7 @@ TactileIpc3D_autogen/
|
||||
*.ninja
|
||||
*.ninja_deps
|
||||
*.ninja_log
|
||||
|
||||
OpenCV/
|
||||
# Qt generated files
|
||||
*.moc
|
||||
moc_*.cpp
|
||||
|
||||
@@ -1,151 +0,0 @@
|
||||
# 自动适配:点阵不越界 + 面板尺寸跟随点阵
|
||||
|
||||
这份文档说明本项目里为了实现:
|
||||
|
||||
1. **圆点(点阵)始终落在面板顶面上,不越界**
|
||||
2. **面板的宽/深(W/D)根据点阵规格自动缩放**
|
||||
3. **面板厚度(H)保持独立可控(不随点阵改变)**
|
||||
|
||||
所做的代码改动和使用方式。
|
||||
|
||||
---
|
||||
|
||||
## 现象 & 根因
|
||||
|
||||
当前点阵的摆放逻辑(`src/glwidget.cpp` 的 `GLWidget::updateInstanceBufferIfNeeded_()`)是:
|
||||
|
||||
- 点阵中心围绕原点居中摆放
|
||||
- 每个点的中心坐标:
|
||||
- `x = c * pitch - (cols-1)*pitch/2`
|
||||
- `z = r * pitch - (rows-1)*pitch/2`
|
||||
- 所以 **点中心** 的范围一定在:
|
||||
- `x ∈ [-gridW/2, +gridW/2]`,其中 `gridW = (cols-1)*pitch`
|
||||
- `z ∈ [-gridD/2, +gridD/2]`,其中 `gridD = (rows-1)*pitch`
|
||||
|
||||
但每个点是“有半径”的(`dotRadius`),在 shader 里实际画出来的圆会占据:
|
||||
|
||||
- `x` 方向还会向外多出 `dotRadius`
|
||||
- `z` 方向还会向外多出 `dotRadius`
|
||||
|
||||
如果面板的 `W/D` 没有随点阵变化而调整,就会出现你截图里那种:**外圈点的圆形被画到了面板外**(看起来像“越界”)。
|
||||
|
||||
---
|
||||
|
||||
## 解决思路(数学上怎么保证不越界)
|
||||
|
||||
要让“圆点的边缘”也不越界,需要满足:
|
||||
|
||||
- 面板半宽 `panelW/2 >= gridW/2 + dotRadius`
|
||||
- 面板半深 `panelD/2 >= gridD/2 + dotRadius`
|
||||
|
||||
等价于:
|
||||
|
||||
- `panelW >= gridW + 2*dotRadius`
|
||||
- `panelD >= gridD + 2*dotRadius`
|
||||
|
||||
本项目采用的就是这条公式。
|
||||
|
||||
> 如果你希望四周再留白一点,只要把公式改成:`+ 2*(dotRadius + margin)` 即可。
|
||||
|
||||
---
|
||||
|
||||
## 代码改动点(做了什么)
|
||||
|
||||
### 1) `setSpec()` 里自动计算面板 W/D
|
||||
|
||||
文件:`src/glwidget.cpp`,函数:`GLWidget::setSpec(...)`
|
||||
|
||||
做的事:
|
||||
|
||||
- 对 `rows/cols/pitch/dotRadius` 做了下限保护(避免负数)
|
||||
- 根据点阵规格计算面板顶面 `m_panelW/m_panelD`:
|
||||
- `gridW = (cols-1) * pitch`
|
||||
- `gridD = (rows-1) * pitch`
|
||||
- `m_panelW = gridW + 2*dotRadius`
|
||||
- `m_panelD = gridD + 2*dotRadius`
|
||||
- 标记面板几何和 dots 几何需要重建(dirty flag)
|
||||
|
||||
这样一旦你改点阵规格,面板会自动缩放到刚好包住点阵。
|
||||
|
||||
---
|
||||
|
||||
### 2) 厚度单独控制:`setPanelThickness()`
|
||||
|
||||
文件:`src/glwidget.h` / `src/glwidget.cpp`
|
||||
|
||||
新增接口:
|
||||
|
||||
- `void setPanelThickness(float h);`
|
||||
|
||||
它只修改 `m_panelH`(厚度),并标记面板几何需要重建。
|
||||
|
||||
---
|
||||
|
||||
### 3) 由于 W/D 会变:需要“重建几何体 buffer”
|
||||
|
||||
原因:面板顶面矩形的顶点坐标依赖 `m_panelW/m_panelD`,改变后必须重建 VBO/IBO/VAO 才能反映到 GPU。
|
||||
|
||||
做法:
|
||||
|
||||
- 新增两个 dirty flag:
|
||||
- `m_panelGeometryDirty`
|
||||
- `m_dotsGeometryDirty`
|
||||
- 在 `GLWidget::paintGL()` 的开头判断 dirty:
|
||||
- dirty 就调用 `initPanelGeometry_()` / `initDotGeometry_()` 重建
|
||||
- dots 重建后会重新分配 instanceVBO,所以把 `m_valuesDirty = true`,确保下一帧重新上传 instance 数据
|
||||
|
||||
---
|
||||
|
||||
### 4) `initPanelGeometry_()` / `initDotGeometry_()` 现在会先删旧 buffer
|
||||
|
||||
因为会重复调用重建,所以这两个函数里加入了:
|
||||
|
||||
- 先 `glDeleteBuffers` / `glDeleteVertexArrays`
|
||||
- 再重新 `glGen*` 创建新资源
|
||||
|
||||
保证不会重复堆积资源(泄漏)或引用旧 VAO。
|
||||
|
||||
---
|
||||
|
||||
### 5) `main.cpp` 不再手动写死面板 W/D
|
||||
|
||||
文件:`main.cpp`
|
||||
|
||||
把原来的:
|
||||
|
||||
- `glw->setPanelSize(1.2f, 0.08f, 1.2f);`
|
||||
|
||||
改为:
|
||||
|
||||
- `glw->setPanelThickness(0.08f);`
|
||||
|
||||
原因:如果继续调用 `setPanelSize()`,你会把 `setSpec()` 自动算出来的 W/D 覆盖掉,越界问题就会回来。
|
||||
|
||||
---
|
||||
|
||||
## 使用方式(你应该怎么调用)
|
||||
|
||||
推荐顺序:
|
||||
|
||||
1. `glw->setSpec(rows, cols, pitch, dotRadius);`
|
||||
- 会自动算出 `panelW/panelD`,保证点不越界
|
||||
2. `glw->setPanelThickness(panelH);`
|
||||
- 只改厚度,不影响自动宽深
|
||||
3. `glw->setRange(minV, maxV);`
|
||||
4. `glw->submitValues(values);`
|
||||
|
||||
如果你确实想手动指定面板大小(不走自动适配),可以再调用:
|
||||
|
||||
- `glw->setPanelSize(w, h, d);`
|
||||
|
||||
但要理解:这会覆盖自动计算的 `W/D`,点可能再次越界(除非你按本文公式算好)。
|
||||
|
||||
---
|
||||
|
||||
## 可选改进(按你需求继续拓展)
|
||||
|
||||
- **加边距(不要贴边)**:在 `setSpec()` 里引入 `margin`,把公式改成:
|
||||
`m_panelW = gridW + 2*(m_dotRadius + margin)`
|
||||
`m_panelD = gridD + 2*(m_dotRadius + margin)`
|
||||
- **让点阵填满面板但不改变 pitch**:目前面板跟着 pitch 走;如果你希望面板固定而 pitch 自适配,需要反过来解 pitch。
|
||||
|
||||
@@ -1,134 +0,0 @@
|
||||
# 屏幕空间背景(白底 + 灰色网格线)说明
|
||||
|
||||
这份文档解释项目里新增的“3D 软件常见的白色背景 + 灰色分割线网格”是怎么实现的,以及如何调整效果。
|
||||
|
||||
需求点:
|
||||
|
||||
- 背景看起来更“科技感”(轻微渐变 + 网格线 + vignette)
|
||||
- **背景不随相机旋转**(不受 yaw/pitch/zoom 影响)
|
||||
|
||||
---
|
||||
|
||||
## 实现方式概览(为什么它不会旋转)
|
||||
|
||||
我们采用“屏幕空间(screen-space)”绘制方式:
|
||||
|
||||
1. **画一个全屏 quad**(两个三角形),顶点坐标直接写在裁剪空间 NDC([-1,1])
|
||||
2. fragment shader 使用 `gl_FragCoord`(屏幕像素坐标)生成网格线
|
||||
|
||||
由于整个背景是直接画在屏幕坐标系里,不使用 `uMVP`,所以它不会跟着相机旋转或移动。
|
||||
|
||||
---
|
||||
|
||||
## 关键文件
|
||||
|
||||
- 资源注册:`resources.qrc`
|
||||
- 新增:`shaders/bg.vert`、`shaders/bg.frag`
|
||||
- Shader:
|
||||
- `shaders/bg.vert`:全屏 quad 的顶点 shader(裁剪空间直出)
|
||||
- `shaders/bg.frag`:背景颜色 + 抗锯齿网格线(基于 `gl_FragCoord`)
|
||||
- C++(OpenGLWidget):
|
||||
- `src/glwidget.h`:新增 `m_bgProg`、`m_bgVao/m_bgVbo`、`initBackgroundGeometry_()`
|
||||
- `src/glwidget.cpp`
|
||||
- `GLWidget::initPrograms_()`:编译/链接背景 program
|
||||
- `GLWidget::initBackgroundGeometry_()`:创建全屏 quad VAO/VBO
|
||||
- `GLWidget::paintGL()`:先画背景(关闭深度写入),再画 3D 内容
|
||||
|
||||
---
|
||||
|
||||
## 渲染顺序(为什么不会影响 3D 深度)
|
||||
|
||||
背景绘制发生在 `GLWidget::paintGL()` 的最前面:
|
||||
|
||||
1. `glClear(...)` 清空颜色/深度
|
||||
2. **绘制背景**(屏幕空间)
|
||||
- `glDisable(GL_DEPTH_TEST);`
|
||||
- `glDepthMask(GL_FALSE);`(不写深度)
|
||||
3. 恢复深度状态
|
||||
- `glDepthMask(GL_TRUE);`
|
||||
- `glEnable(GL_DEPTH_TEST);`
|
||||
4. 再绘制 panel / dots(正常 3D 深度测试)
|
||||
|
||||
因此:背景永远在最底层,而且不会把深度缓冲弄脏。
|
||||
|
||||
---
|
||||
|
||||
## 全屏 quad(背景几何)
|
||||
|
||||
`GLWidget::initBackgroundGeometry_()` 创建一个覆盖整个屏幕的矩形(两个三角形):
|
||||
|
||||
- 顶点坐标是 NDC(裁剪空间):
|
||||
- (-1,-1) 到 (1,1)
|
||||
- 顶点 shader(`shaders/bg.vert`)仅仅把它输出到 `gl_Position`
|
||||
|
||||
这样不需要任何相机矩阵,也不会“跟着相机”动。
|
||||
|
||||
---
|
||||
|
||||
## 网格线怎么画出来的(bg.frag)
|
||||
|
||||
`shaders/bg.frag` 主要做了三件事:
|
||||
|
||||
### 1) 背景渐变底色
|
||||
|
||||
- 使用 `uv = gl_FragCoord.xy / uViewport` 得到 0..1 的屏幕坐标
|
||||
- 在 y 方向做一个轻微渐变(上更亮、下稍灰)
|
||||
|
||||
### 2) 细网格 + 粗网格(分割线)
|
||||
|
||||
用 `uMinorStep` / `uMajorStep`(单位:像素)控制网格间距:
|
||||
|
||||
- `uMinorStep`:细分格子(更淡)
|
||||
- `uMajorStep`:粗分格子(更明显)
|
||||
|
||||
网格线本质是对 `fract(coord / step)` 做“距离最近线”的计算,然后用 `fwidth` 做抗锯齿:
|
||||
|
||||
- `fwidth` 会随着屏幕像素密度和视角变化自动调整边缘过渡,避免锯齿
|
||||
|
||||
### 3) 轻微 vignette
|
||||
|
||||
四角略暗、中心略亮,让画面更聚焦、更像 3D 软件视口。
|
||||
|
||||
---
|
||||
|
||||
## HiDPI 注意点(为什么要乘 devicePixelRatio)
|
||||
|
||||
Qt 的 `width()/height()` 是“逻辑像素”;而 `gl_FragCoord` 是“物理像素”。
|
||||
|
||||
所以背景在 C++ 里传入:
|
||||
|
||||
- `uViewport = (width * dpr, height * dpr)`
|
||||
- 网格 step 也乘 `dpr`
|
||||
|
||||
否则在高 DPI 屏幕上网格会显得“变密/变粗”。
|
||||
|
||||
---
|
||||
|
||||
## 如何调整外观(常用参数)
|
||||
|
||||
### 调整网格密度
|
||||
|
||||
在 `src/glwidget.cpp` 里设置了:
|
||||
|
||||
- `uMinorStep = 24 px`(细线间距)
|
||||
- `uMajorStep = 120 px`(粗线间距)
|
||||
|
||||
改这两个值就能让网格更密/更稀。
|
||||
|
||||
### 调整颜色/强度/科技感
|
||||
|
||||
在 `shaders/bg.frag` 里可以改:
|
||||
|
||||
- `topCol/botCol`:背景渐变颜色
|
||||
- `minorCol/majorCol`:网格线颜色
|
||||
- `mix(...)` 的系数:线条深浅
|
||||
- vignette 强度:`dot(p,p)` 前面的系数
|
||||
|
||||
---
|
||||
|
||||
## 可选增强(如果你想更像 Blender/Unity)
|
||||
|
||||
- 加“坐标轴线”(X 红、Z 蓝或灰色加深)并在中心画十字
|
||||
- 增加 UI 开关:显示/隐藏网格、调 step、调强度
|
||||
- 增加“地平线”或 “ground plane” 的淡淡雾化效果
|
||||
|
||||
@@ -9,9 +9,6 @@ set(CMAKE_AUTOUIC ON)
|
||||
|
||||
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
|
||||
|
||||
add_subdirectory(3rdpart/QXlsx/QXlsx)
|
||||
|
||||
include_directories(${CMAKE_CURRENT_SOURCE_DIR}/src)
|
||||
|
||||
find_package(Qt6 COMPONENTS
|
||||
Core
|
||||
@@ -28,6 +25,10 @@ find_package(Qt6 COMPONENTS
|
||||
LinguistTools
|
||||
)
|
||||
|
||||
set(QT_VERSION_MAJOR 6)
|
||||
add_subdirectory(3rdpart/QXlsx/QXlsx)
|
||||
include_directories(${CMAKE_CURRENT_SOURCE_DIR}/src)
|
||||
|
||||
qt_standard_project_setup()
|
||||
|
||||
add_executable(TactileIpc3D
|
||||
@@ -62,8 +63,10 @@ add_executable(TactileIpc3D
|
||||
src/ringbuffer.cpp
|
||||
src/sparkline_plotitem.h
|
||||
src/sparkling_plotitem.cpp
|
||||
src/globalhelper.h
|
||||
src/globalhelper.h
|
||||
)
|
||||
target_link_libraries(TactileIpc3D
|
||||
target_link_libraries(TactileIpc3D PRIVATE
|
||||
Qt6::Core
|
||||
Qt6::Gui
|
||||
Qt6::Widgets
|
||||
@@ -76,7 +79,9 @@ target_link_libraries(TactileIpc3D
|
||||
Qt6::QuickDialogs2
|
||||
QXlsx::QXlsx
|
||||
)
|
||||
|
||||
target_include_directories(TactileIpc3D PRIVATE
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/3rdpart/OpenCV/include
|
||||
)
|
||||
set(TS_FILES
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/i18n/app_zh_CN.ts
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/i18n/app_en_US.ts
|
||||
@@ -109,31 +114,17 @@ qt_add_resources(TactileIpc3D i18n_resources
|
||||
FILES ${QM_FILES}
|
||||
)
|
||||
|
||||
#if (WIN32 AND NOT DEFINED CMAKE_TOOLCHAIN_FILE)
|
||||
# set(DEBUG_SUFFIX)
|
||||
# if (MSVC AND CMAKE_BUILD_TYPE MATCHES "Debug")
|
||||
# set(DEBUG_SUFFIX "d")
|
||||
# endif ()
|
||||
# set(QT_INSTALL_PATH "${CMAKE_PREFIX_PATH}")
|
||||
# if (NOT EXISTS "${QT_INSTALL_PATH}/bin")
|
||||
# set(QT_INSTALL_PATH "${QT_INSTALL_PATH}/..")
|
||||
# if (NOT EXISTS "${QT_INSTALL_PATH}/bin")
|
||||
# set(QT_INSTALL_PATH "${QT_INSTALL_PATH}/..")
|
||||
# endif ()
|
||||
# endif ()
|
||||
# if (EXISTS "${QT_INSTALL_PATH}/plugins/platforms/qwindows${DEBUG_SUFFIX}.dll")
|
||||
# add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD
|
||||
# COMMAND ${CMAKE_COMMAND} -E make_directory
|
||||
# "$<TARGET_FILE_DIR:${PROJECT_NAME}>/plugins/platforms/")
|
||||
# add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD
|
||||
# COMMAND ${CMAKE_COMMAND} -E copy
|
||||
# "${QT_INSTALL_PATH}/plugins/platforms/qwindows${DEBUG_SUFFIX}.dll"
|
||||
# "$<TARGET_FILE_DIR:${PROJECT_NAME}>/plugins/platforms/")
|
||||
# endif ()
|
||||
# foreach (QT_LIB Core Gui Widgets)
|
||||
# add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD
|
||||
# COMMAND ${CMAKE_COMMAND} -E copy
|
||||
# "${QT_INSTALL_PATH}/bin/Qt6${QT_LIB}${DEBUG_SUFFIX}.dll"
|
||||
# "$<TARGET_FILE_DIR:${PROJECT_NAME}>")
|
||||
# endforeach (QT_LIB)
|
||||
#endif ()
|
||||
set(runtime_out_dir "${CMAKE_BINARY_DIR}/out")
|
||||
set_target_properties(TactileIpc3D PROPERTIES
|
||||
RUNTIME_OUTPUT_DIRECTORY "${runtime_out_dir}"
|
||||
RUNTIME_OUTPUT_DIRECTORY_DEBUG "${runtime_out_dir}/Debug"
|
||||
RUNTIME_OUTPUT_DIRECTORY_RELEASE "${runtime_out_dir}/Release"
|
||||
RUNTIME_OUTPUT_DIRECTORY_RELWITHDEBINFO "${runtime_out_dir}/RelWithDebInfo"
|
||||
RUNTIME_OUTPUT_DIRECTORY_MINSIZEREL "${runtime_out_dir}/MinSizeRel"
|
||||
)
|
||||
|
||||
include(GNUInstallDirs)
|
||||
install(TARGETS TactileIpc3D
|
||||
RUNTIME DESTINATION bin
|
||||
)
|
||||
|
||||
|
||||
66
README.md
66
README.md
@@ -257,29 +257,38 @@ m_dotsProg->release();
|
||||
- 想要更柔和的圆边:用 `smoothstep` 做 alpha 边缘 + 开启 blending
|
||||
- 想要真正的 3D 圆柱/球:就不能靠 discard 了,需要真正的几何或法线光照
|
||||
|
||||
## Metrics ????
|
||||
## Metrics(指标计算)
|
||||
|
||||
Metrics ?????????? payload?`DataFrame.data`??????? `metricsChanged` ???
|
||||
Metrics 是对**单帧 payload**的统计结果,payload 即 `DataFrame.data`。每次接收新帧或回放 tick,
|
||||
`DataBackend::emitFrame_()` 会先调用 `updateMetrics_()`,然后发出 `metricsChanged`,QML 端通过
|
||||
`Backend.data.metric*` 读取这些值。
|
||||
|
||||
- ??????
|
||||
- ????sqrt(mean(v^2))
|
||||
- ????mean(v)
|
||||
- ??????? - ???
|
||||
- Peak:`max(v)`
|
||||
- RMS:`sqrt(mean(v^2))`
|
||||
- Avg:`mean(v)`
|
||||
- Delta:`max(v) - min(v)`
|
||||
- Sum:`sum(v)`
|
||||
|
||||
## Live Trend ??
|
||||
代码位置:`src/data_backend.cpp`(`updateMetrics_()`)
|
||||
|
||||
Live Trend ????? payload ???sum of `DataFrame.data`???????????????? `src/data_backend.cpp` ? `updateMetrics_()` ???? `metricSum`??? `qml/content/RightPanel.qml` ??? `Backend.data.metricSum` ??????
|
||||
## Live Trend(Payload Sum)
|
||||
|
||||
## Live Trend ??????????
|
||||
右侧面板的 **Payload Sum** 折线图来自 `metricSum`:每次 `metricsChanged` 时,如果已有帧数据,
|
||||
就会执行 `card.plot.append(Backend.data.metricSum)`。
|
||||
|
||||
????????? `QImage` ?????????? DPI?devicePixelRatio??????? DPI ??????????????????????? y ???????????????????????
|
||||
- 计算来源:`DataBackend::updateMetrics_()`(`metricSum = sum(DataFrame.data)`)
|
||||
- 触发位置:`qml/content/RightPanel.qml`(`Connections { onMetricsChanged { ... } }`)
|
||||
- 绘图组件:`qml/content/LiveTrendCard.qml` + `SparklinePlot`(`LiveTrend` 模块)
|
||||
|
||||
?????
|
||||
- ???????? `devicePixelRatio` ???????
|
||||
- ????????????????????
|
||||
- ???????? y ????????????????
|
||||
## Live Trend 的 HiDPI 对齐(文字清晰度/位置)
|
||||
|
||||
?????????
|
||||
SparklinePlot 的刻度文字通过 `QImage -> QSGTexture` 绘制。为避免高 DPI 下文字发虚或 y 方向偏移,
|
||||
实现上做了两件事:
|
||||
|
||||
- 按 `devicePixelRatio` 生成缓存 key 与 QImage 像素尺寸
|
||||
- `QImage::setDevicePixelRatio(dpr)`,并在布局时按像素网格对齐
|
||||
|
||||
相关实现:
|
||||
|
||||
```cpp
|
||||
// src/sparkline_plotitem.h
|
||||
@@ -320,16 +329,23 @@ QSGNode* SparklinePlotItem::updatePaintNode(QSGNode* oldNode, UpdatePaintNodeDat
|
||||
}
|
||||
```
|
||||
|
||||
## Export Logic
|
||||
## Export Logic(导出流程)
|
||||
|
||||
Export is driven by the Save dialog and DataBackend::exportHandler():
|
||||
导出由保存对话框驱动,最终落到 `DataBackend::exportHandler()`:
|
||||
|
||||
- QML triggers Backend.data.exportHandler(folder, filename, format, method) from qml/content/LeftPanel.qml.
|
||||
- older is a local QUrl (e.g. ile:///C:/...), converted to a local path on the C++ side.
|
||||
- Supported formats: csv, json.
|
||||
- Supported methods: overwrite (default), ppend.
|
||||
- CSV append: appends all current frames to the file.
|
||||
- JSON append: loads existing array (if any), appends all current frames, and rewrites the file.
|
||||
- xlsx and zip are not implemented yet and will log a warning.
|
||||
- 触发入口:`qml/content/LeftPanel.qml` → `SaveAsExportDialog` → `Backend.data.exportHandler(...)`
|
||||
- `folder` 为本地 `QUrl`(如 `file:///C:/...`),C++ 端会转换为本地路径
|
||||
- `format`:`csv` / `json` / `xlsx`(为空时会根据文件后缀推断)
|
||||
- `method`:`overwrite`(默认) / `append` / `zip`(UI 有入口但后端未实现压缩)
|
||||
|
||||
Relevant code: src/data_backend.cpp (exportHandler, uildCsv_, uildJson_), qml/content/SaveAsExportDialog.qml.
|
||||
追加行为:
|
||||
|
||||
- CSV append:以追加模式写入 `buildCsv_()` 的内容(无表头)
|
||||
- JSON append:读取已有数组(如果存在),追加当前帧后整体重写
|
||||
|
||||
当前限制:
|
||||
|
||||
- `xlsx` 通过QXlsx是实现但是缺少传感器没有测试
|
||||
- `zip` 未实现压缩逻辑(请视为暂不支持)
|
||||
|
||||
相关代码:`src/data_backend.cpp`(`exportHandler` / `buildCsv_` / `buildJson_`),`qml/content/SaveAsExportDialog.qml`
|
||||
|
||||
1
VERSION.txt
Normal file
1
VERSION.txt
Normal file
@@ -0,0 +1 @@
|
||||
1.0.0
|
||||
52
installer/TactileIpc3D.iss
Normal file
52
installer/TactileIpc3D.iss
Normal file
@@ -0,0 +1,52 @@
|
||||
#define MyAppName "TactileIpc3D"
|
||||
#define MyAppVersion "0.2.0"
|
||||
#define MyAppPublisher "TactileIpc3D"
|
||||
#define MyAppExeName "TactileIpc3D.exe"
|
||||
#define SourceDir "..\\build\Desktop_Qt_6_8_3_MinGW_64_bit-Release\\out\Release"
|
||||
#define OutputDir "..\\dist"
|
||||
#define AssetsDir "assets"
|
||||
#define AppIconFile AssetsDir + "\\App.ico"
|
||||
#define WizardSmallFile AssetsDir + "\\WizardSmall.bmp"
|
||||
#define WizardLargeFile AssetsDir + "\\WizardLarge.bmp"
|
||||
|
||||
[Setup]
|
||||
AppId={{1E4C86D4-4E53-4B8A-9F7F-1D56E8E04353}}
|
||||
AppName={#MyAppName}
|
||||
AppVersion={#MyAppVersion}
|
||||
AppPublisher={#MyAppPublisher}
|
||||
DefaultDirName={autopf}\{#MyAppName}
|
||||
DefaultGroupName={#MyAppName}
|
||||
OutputDir={#OutputDir}
|
||||
OutputBaseFilename={#MyAppName}-Setup-{#MyAppVersion}
|
||||
Compression=lzma
|
||||
SolidCompression=yes
|
||||
WizardStyle=modern
|
||||
SetupLogging=yes
|
||||
ArchitecturesAllowed=x64
|
||||
ArchitecturesInstallIn64BitMode=x64
|
||||
#ifexist AppIconFile
|
||||
SetupIconFile={#AppIconFile}
|
||||
#endif
|
||||
#ifexist WizardSmallFile
|
||||
WizardSmallImageFile={#WizardSmallFile}
|
||||
#endif
|
||||
#ifexist WizardLargeFile
|
||||
WizardImageFile={#WizardLargeFile}
|
||||
#endif
|
||||
|
||||
[Languages]
|
||||
Name: "english"; MessagesFile: "compiler:Default.isl"
|
||||
; Name: "chinesesimp"; MessagesFile: "compiler:Languages\\ChineseSimplified.isl"
|
||||
|
||||
[Tasks]
|
||||
Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked
|
||||
|
||||
[Files]
|
||||
Source: "{#SourceDir}\\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs
|
||||
|
||||
[Icons]
|
||||
Name: "{group}\\{#MyAppName}"; Filename: "{app}\\{#MyAppExeName}"
|
||||
Name: "{autodesktop}\\{#MyAppName}"; Filename: "{app}\\{#MyAppExeName}"; Tasks: desktopicon
|
||||
|
||||
[Run]
|
||||
Filename: "{app}\\{#MyAppExeName}"; Description: "{cm:LaunchProgram,{#MyAppName}}"; Flags: postinstall nowait skipifsilent
|
||||
6
installer/assets/README.txt
Normal file
6
installer/assets/README.txt
Normal file
@@ -0,0 +1,6 @@
|
||||
Place installer assets here:
|
||||
- App.ico
|
||||
- WizardSmall.bmp
|
||||
- WizardLarge.bmp
|
||||
|
||||
If these files are missing, the Inno Setup script will skip the custom icon/images.
|
||||
7
main.cpp
7
main.cpp
@@ -125,12 +125,9 @@ int main(int argc, char *argv[]) {
|
||||
return;
|
||||
glw->submitValues(frame.data);
|
||||
}); */
|
||||
backend.data()->setLiveRenderCallback([](const DataFrame &frame) {
|
||||
backend.data()->setLiveRenderCallback([glw](const DataFrame &frame) {
|
||||
if (frame.data.size() != 0) {
|
||||
// AA 55 1A 00 34 00 FB 00 1C 00 00 10 00 00 00 00 00 00 00 00 00 00
|
||||
// 00 00 00 00 00 00 00 00 F1 aa 55 1a 00 34 00 fb 00 1c 00 00 10 00
|
||||
// 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 f1
|
||||
qDebug() << "data size: " << frame.data.size();
|
||||
glw->submitValues(frame.data);
|
||||
}
|
||||
});
|
||||
// TODO:待实现内容(将frame数据分发给右侧曲线/指标的QML接口)
|
||||
|
||||
616
qml/content/ColorPickerDialog.qml
Normal file
616
qml/content/ColorPickerDialog.qml
Normal file
@@ -0,0 +1,616 @@
|
||||
// ColorPickerWindow.qml
|
||||
import QtQuick
|
||||
import QtQuick.Window
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Controls.Material
|
||||
import TactileIPC 1.0
|
||||
|
||||
Window {
|
||||
id: root
|
||||
width: 360
|
||||
height: 650
|
||||
visible: false
|
||||
modality: Qt.ApplicationModal
|
||||
flags: Qt.Dialog
|
||||
title: qsTr("颜色选择")
|
||||
Material.theme: Backend.lightMode ? Material.Light : Material.Dark
|
||||
Material.accent: Material.Green
|
||||
Material.primary: Material.Green
|
||||
|
||||
readonly property bool isDark: !Backend.lightMode
|
||||
readonly property color windowBg: isDark ? "#1F1F1F" : "#F7F7F7"
|
||||
readonly property color panelBorder: isDark ? "#2E2E2E" : "#D9D9D9"
|
||||
readonly property color surfaceBorder: isDark ? "#343434" : "#CFCFCF"
|
||||
readonly property color textPrimary: isDark ? "#EDEDED" : "#1F1F1F"
|
||||
readonly property color textSecondary: isDark ? "#D8D8D8" : "#616161"
|
||||
readonly property color textMuted: isDark ? "#BEBEBE" : "#6E6E6E"
|
||||
readonly property color controlBg: isDark ? "#2A2A2A" : "#FFFFFF"
|
||||
readonly property color controlBorder: isDark ? "#3A3A3A" : "#CFCFCF"
|
||||
readonly property color highlightBg: isDark ? "#55FFFFFF" : "#33000000"
|
||||
readonly property color highlightText: isDark ? "#111111" : "#FFFFFF"
|
||||
readonly property color indicatorBorder: isDark ? "#6A6A6A" : "#9E9E9E"
|
||||
readonly property color accentColor: Material.color(Material.Green)
|
||||
|
||||
// ===== API =====
|
||||
property color color: "#FF7032D2" // AARRGGBB
|
||||
signal accepted(color c)
|
||||
signal rejected()
|
||||
|
||||
function openWith(c) {
|
||||
syncFromColor(c ?? root.color)
|
||||
visible = true
|
||||
requestActivate()
|
||||
}
|
||||
|
||||
Keys.onEscapePressed: {
|
||||
visible = false
|
||||
rejected()
|
||||
}
|
||||
|
||||
// ===== internal HSV(A) =====
|
||||
property real h: 0.75 // 0..1
|
||||
property real s: 0.45
|
||||
property real v: 0.82
|
||||
property real a: 1.0
|
||||
|
||||
property bool _lock: false
|
||||
|
||||
function clamp01(x) { return Math.max(0, Math.min(1, x)) }
|
||||
function clampInt(x, lo, hi) { return Math.max(lo, Math.min(hi, Math.round(x))) }
|
||||
|
||||
function hsvToRgb(hh, ss, vv) {
|
||||
const h6 = (hh % 1) * 6
|
||||
const c = vv * ss
|
||||
const x = c * (1 - Math.abs((h6 % 2) - 1))
|
||||
const m = vv - c
|
||||
let r1=0, g1=0, b1=0
|
||||
if (0 <= h6 && h6 < 1) { r1=c; g1=x; b1=0 }
|
||||
else if (1 <= h6 && h6 < 2) { r1=x; g1=c; b1=0 }
|
||||
else if (2 <= h6 && h6 < 3) { r1=0; g1=c; b1=x }
|
||||
else if (3 <= h6 && h6 < 4) { r1=0; g1=x; b1=c }
|
||||
else if (4 <= h6 && h6 < 5) { r1=x; g1=0; b1=c }
|
||||
else { r1=c; g1=0; b1=x }
|
||||
return { r: r1 + m, g: g1 + m, b: b1 + m }
|
||||
}
|
||||
|
||||
function rgbToHsv(r, g, b) {
|
||||
const max = Math.max(r, g, b)
|
||||
const min = Math.min(r, g, b)
|
||||
const d = max - min
|
||||
let hh = 0
|
||||
if (d !== 0) {
|
||||
if (max === r) hh = ((g - b) / d) % 6
|
||||
else if (max === g) hh = (b - r) / d + 2
|
||||
else hh = (r - g) / d + 4
|
||||
hh /= 6
|
||||
if (hh < 0) hh += 1
|
||||
}
|
||||
const ss = (max === 0) ? 0 : d / max
|
||||
const vv = max
|
||||
return { h: hh, s: ss, v: vv }
|
||||
}
|
||||
|
||||
function toHex2(v01) {
|
||||
const n = clampInt(clamp01(v01) * 255, 0, 255)
|
||||
const s = n.toString(16).toUpperCase()
|
||||
return (s.length === 1) ? ("0" + s) : s
|
||||
}
|
||||
|
||||
function colorToHexAARRGGBB(c) {
|
||||
return "#" + toHex2(c.a) + toHex2(c.r) + toHex2(c.g) + toHex2(c.b)
|
||||
}
|
||||
|
||||
function parseHex(str) {
|
||||
let t = ("" + str).trim()
|
||||
if (t.startsWith("0x") || t.startsWith("0X")) t = t.slice(2)
|
||||
if (t.startsWith("#")) t = t.slice(1)
|
||||
if (t.length === 6) {
|
||||
const rr = parseInt(t.slice(0,2), 16)
|
||||
const gg = parseInt(t.slice(2,4), 16)
|
||||
const bb = parseInt(t.slice(4,6), 16)
|
||||
if ([rr,gg,bb].some(x => isNaN(x))) return null
|
||||
return Qt.rgba(rr/255, gg/255, bb/255, 1)
|
||||
}
|
||||
if (t.length === 8) {
|
||||
const aa = parseInt(t.slice(0,2), 16)
|
||||
const rr = parseInt(t.slice(2,4), 16)
|
||||
const gg = parseInt(t.slice(4,6), 16)
|
||||
const bb = parseInt(t.slice(6,8), 16)
|
||||
if ([aa,rr,gg,bb].some(x => isNaN(x))) return null
|
||||
return Qt.rgba(rr/255, gg/255, bb/255, aa/255)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
function syncFromHSV() {
|
||||
if (_lock) return
|
||||
_lock = true
|
||||
const rgb = hsvToRgb(h, s, v)
|
||||
color = Qt.rgba(rgb.r, rgb.g, rgb.b, a)
|
||||
|
||||
// 更新 UI
|
||||
hexField.text = colorToHexAARRGGBB(color)
|
||||
alphaPercent.text = clampInt(a*100, 0, 100) + "%"
|
||||
|
||||
rField.value = clampInt(color.r * 255, 0, 255)
|
||||
gField.value = clampInt(color.g * 255, 0, 255)
|
||||
bField.value = clampInt(color.b * 255, 0, 255)
|
||||
|
||||
// HSV 显示用度/百分比
|
||||
hField.value = clampInt(h * 360, 0, 360)
|
||||
sField.value = clampInt(s * 100, 0, 100)
|
||||
vField.value = clampInt(v * 100, 0, 100)
|
||||
|
||||
svCanvas.requestPaint()
|
||||
hueCanvas.requestPaint()
|
||||
alphaCanvas.requestPaint()
|
||||
_lock = false
|
||||
}
|
||||
|
||||
function syncFromColor(c) {
|
||||
if (_lock) return
|
||||
_lock = true
|
||||
const hsv = rgbToHsv(c.r, c.g, c.b)
|
||||
h = hsv.h; s = hsv.s; v = hsv.v; a = c.a
|
||||
color = Qt.rgba(c.r, c.g, c.b, a)
|
||||
|
||||
hexField.text = colorToHexAARRGGBB(color)
|
||||
alphaPercent.text = clampInt(a*100, 0, 100) + "%"
|
||||
|
||||
rField.value = clampInt(c.r * 255, 0, 255)
|
||||
gField.value = clampInt(c.g * 255, 0, 255)
|
||||
bField.value = clampInt(c.b * 255, 0, 255)
|
||||
|
||||
hField.value = clampInt(h * 360, 0, 360)
|
||||
sField.value = clampInt(s * 100, 0, 100)
|
||||
vField.value = clampInt(v * 100, 0, 100)
|
||||
|
||||
svCanvas.requestPaint()
|
||||
hueCanvas.requestPaint()
|
||||
alphaCanvas.requestPaint()
|
||||
_lock = false
|
||||
}
|
||||
|
||||
function applyRGB(r255, g255, b255) {
|
||||
if (_lock) return
|
||||
const rr = clampInt(r255, 0, 255) / 255
|
||||
const gg = clampInt(g255, 0, 255) / 255
|
||||
const bb = clampInt(b255, 0, 255) / 255
|
||||
const hsv = rgbToHsv(rr, gg, bb)
|
||||
h = hsv.h; s = hsv.s; v = hsv.v
|
||||
syncFromHSV()
|
||||
}
|
||||
|
||||
function applyHSV(hDeg, sPct, vPct) {
|
||||
if (_lock) return
|
||||
h = clamp01(hDeg / 360)
|
||||
s = clamp01(sPct / 100)
|
||||
v = clamp01(vPct / 100)
|
||||
syncFromHSV()
|
||||
}
|
||||
|
||||
// 初始同步
|
||||
Component.onCompleted: syncFromColor(root.color)
|
||||
|
||||
// ====== UI: 深色窗口面板(不是卡片)======
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
radius: 0
|
||||
color: root.windowBg
|
||||
border.width: 1
|
||||
border.color: root.panelBorder
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 10
|
||||
spacing: 10
|
||||
|
||||
// 顶部:标题 + 勾选(参考图右上角)
|
||||
// RowLayout {
|
||||
// Layout.fillWidth: true
|
||||
// spacing: 8
|
||||
|
||||
// Label {
|
||||
// text: root.title
|
||||
// color: root.textPrimary
|
||||
// font.pixelSize: 14
|
||||
// font.weight: Font.DemiBold
|
||||
// Layout.fillWidth: true
|
||||
// }
|
||||
|
||||
// CheckBox {
|
||||
// id: alphaToggle
|
||||
// checked: true
|
||||
|
||||
// contentItem: Label {
|
||||
// text: qsTr("实时预览")
|
||||
// color: root.textSecondary
|
||||
// verticalAlignment: Text.AlignVCenter
|
||||
// elide: Text.ElideRight
|
||||
// }
|
||||
|
||||
// indicator: Rectangle {
|
||||
// implicitWidth: 16
|
||||
// implicitHeight: 16
|
||||
// radius: 3
|
||||
// border.width: 1
|
||||
// border.color: root.indicatorBorder
|
||||
// color: alphaToggle.checked ? root.accentColor : "transparent"
|
||||
|
||||
// // 选中勾
|
||||
// Canvas {
|
||||
// anchors.fill: parent
|
||||
// onPaint: {
|
||||
// const ctx = getContext("2d")
|
||||
// ctx.clearRect(0,0,width,height)
|
||||
// if (!alphaToggle.checked) return
|
||||
// ctx.strokeStyle = "white"
|
||||
// ctx.lineWidth = 2
|
||||
// ctx.lineCap = "round"
|
||||
// ctx.beginPath()
|
||||
// ctx.moveTo(width*0.25, height*0.55)
|
||||
// ctx.lineTo(width*0.45, height*0.72)
|
||||
// ctx.lineTo(width*0.78, height*0.30)
|
||||
// ctx.stroke()
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
|
||||
// ToolButton {
|
||||
// text: "✕"
|
||||
// onClicked: { root.visible = false; root.rejected() }
|
||||
// }
|
||||
// }
|
||||
|
||||
// HSV 面板
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: 185
|
||||
radius: 8
|
||||
border.width: 1
|
||||
border.color: root.surfaceBorder
|
||||
clip: true
|
||||
|
||||
Canvas {
|
||||
id: svCanvas
|
||||
anchors.fill: parent
|
||||
onPaint: {
|
||||
const ctx = getContext("2d")
|
||||
const w = width, hh = height
|
||||
ctx.clearRect(0,0,w,hh)
|
||||
|
||||
const rgbHue = root.hsvToRgb(root.h, 1, 1)
|
||||
|
||||
// X: white -> hue
|
||||
const gx = ctx.createLinearGradient(0,0,w,0)
|
||||
gx.addColorStop(0, "rgb(255,255,255)")
|
||||
gx.addColorStop(1, "rgb(" + Math.round(rgbHue.r*255) + "," + Math.round(rgbHue.g*255) + "," + Math.round(rgbHue.b*255) + ")")
|
||||
ctx.fillStyle = gx
|
||||
ctx.fillRect(0,0,w,hh)
|
||||
|
||||
// Y: transparent -> black
|
||||
const gy = ctx.createLinearGradient(0,0,0,hh)
|
||||
gy.addColorStop(0, "rgba(0,0,0,0)")
|
||||
gy.addColorStop(1, "rgba(0,0,0,1)")
|
||||
ctx.fillStyle = gy
|
||||
ctx.fillRect(0,0,w,hh)
|
||||
|
||||
// handle
|
||||
const x = root.s * w
|
||||
const y = (1 - root.v) * hh
|
||||
ctx.beginPath()
|
||||
ctx.arc(x, y, 7, 0, Math.PI*2)
|
||||
ctx.lineWidth = 2
|
||||
ctx.strokeStyle = "rgba(255,255,255,0.95)"
|
||||
ctx.stroke()
|
||||
ctx.beginPath()
|
||||
ctx.arc(x, y, 6, 0, Math.PI*2)
|
||||
ctx.lineWidth = 1
|
||||
ctx.strokeStyle = "rgba(0,0,0,0.55)"
|
||||
ctx.stroke()
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
function apply(mx, my) {
|
||||
root.s = root.clamp01(mx / width)
|
||||
root.v = root.clamp01(1 - (my / height))
|
||||
root.syncFromHSV()
|
||||
}
|
||||
onPressed: (e) => apply(e.x, e.y)
|
||||
onPositionChanged: (e) => { if (pressed) apply(e.x, e.y) }
|
||||
}
|
||||
}
|
||||
|
||||
// Hue 彩虹条(参考图那种)
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: 16
|
||||
radius: 8
|
||||
border.width: 1
|
||||
border.color: root.surfaceBorder
|
||||
clip: true
|
||||
|
||||
Canvas {
|
||||
id: hueCanvas
|
||||
anchors.fill: parent
|
||||
onPaint: {
|
||||
const ctx = getContext("2d")
|
||||
const w = width, hh = height
|
||||
ctx.clearRect(0,0,w,hh)
|
||||
const grad = ctx.createLinearGradient(0,0,w,0)
|
||||
for (let i=0; i<=6; i++) {
|
||||
const t = i/6
|
||||
const rgb = root.hsvToRgb(t, 1, 1)
|
||||
grad.addColorStop(t, "rgb(" + Math.round(rgb.r*255) + "," + Math.round(rgb.g*255) + "," + Math.round(rgb.b*255) + ")")
|
||||
}
|
||||
ctx.fillStyle = grad
|
||||
ctx.fillRect(0,0,w,hh)
|
||||
|
||||
const x = root.h * w
|
||||
ctx.beginPath()
|
||||
ctx.arc(x, hh/2, 7, 0, Math.PI*2)
|
||||
ctx.lineWidth = 2
|
||||
ctx.strokeStyle = "rgba(255,255,255,0.95)"
|
||||
ctx.stroke()
|
||||
ctx.beginPath()
|
||||
ctx.arc(x, hh/2, 6, 0, Math.PI*2)
|
||||
ctx.lineWidth = 1
|
||||
ctx.strokeStyle = "rgba(0,0,0,0.55)"
|
||||
ctx.stroke()
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
function apply(mx) {
|
||||
root.h = root.clamp01(mx / width)
|
||||
root.syncFromHSV()
|
||||
}
|
||||
onPressed: (e) => apply(e.x)
|
||||
onPositionChanged: (e) => { if (pressed) apply(e.x) }
|
||||
}
|
||||
}
|
||||
|
||||
// Alpha 条(棋盘 + 渐变 + 右侧圆点)
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: 16
|
||||
radius: 8
|
||||
border.width: 1
|
||||
border.color: root.surfaceBorder
|
||||
clip: true
|
||||
|
||||
// checker
|
||||
Canvas {
|
||||
id: checkerCanvas
|
||||
anchors.fill: parent
|
||||
onPaint: {
|
||||
const ctx = getContext("2d")
|
||||
const w = width, hh = height
|
||||
ctx.clearRect(0,0,w,hh)
|
||||
const s = 8
|
||||
for (let y=0; y<hh; y+=s) {
|
||||
for (let x=0; x<w; x+=s) {
|
||||
const on = ((x/s + y/s) % 2) === 0
|
||||
ctx.fillStyle = on ? "rgba(255,255,255,0.16)" : "rgba(0,0,0,0.0)"
|
||||
ctx.fillRect(x,y,s,s)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Canvas {
|
||||
id: alphaCanvas
|
||||
anchors.fill: parent
|
||||
onPaint: {
|
||||
const ctx = getContext("2d")
|
||||
const w = width, hh = height
|
||||
ctx.clearRect(0,0,w,hh)
|
||||
|
||||
const rgb = root.hsvToRgb(root.h, root.s, root.v)
|
||||
const grad = ctx.createLinearGradient(0,0,w,0)
|
||||
grad.addColorStop(0, "rgba(" + Math.round(rgb.r*255) + "," + Math.round(rgb.g*255) + "," + Math.round(rgb.b*255) + ",0)")
|
||||
grad.addColorStop(1, "rgba(" + Math.round(rgb.r*255) + "," + Math.round(rgb.g*255) + "," + Math.round(rgb.b*255) + ",1)")
|
||||
ctx.fillStyle = grad
|
||||
ctx.fillRect(0,0,w,hh)
|
||||
|
||||
const x = root.a * w
|
||||
ctx.beginPath()
|
||||
ctx.arc(x, hh/2, 7, 0, Math.PI*2)
|
||||
ctx.lineWidth = 2
|
||||
ctx.strokeStyle = "rgba(255,255,255,0.95)"
|
||||
ctx.stroke()
|
||||
ctx.beginPath()
|
||||
ctx.arc(x, hh/2, 6, 0, Math.PI*2)
|
||||
ctx.lineWidth = 1
|
||||
ctx.strokeStyle = "rgba(0,0,0,0.55)"
|
||||
ctx.stroke()
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
function apply(mx) {
|
||||
root.a = root.clamp01(mx / width)
|
||||
root.syncFromHSV()
|
||||
}
|
||||
onPressed: (e) => apply(e.x)
|
||||
onPositionChanged: (e) => { if (pressed) apply(e.x) }
|
||||
}
|
||||
}
|
||||
|
||||
// 下方:色块 + Hex + 透明度百分比
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 10
|
||||
|
||||
// 预览色块(带棋盘底)
|
||||
Rectangle {
|
||||
width: 44
|
||||
height: 44
|
||||
radius: 8
|
||||
border.width: 1
|
||||
border.color: root.surfaceBorder
|
||||
clip: true
|
||||
|
||||
Canvas {
|
||||
anchors.fill: parent
|
||||
onPaint: {
|
||||
const ctx = getContext("2d")
|
||||
const w = width, hh = height
|
||||
ctx.clearRect(0,0,w,hh)
|
||||
const s = 10
|
||||
for (let y=0; y<hh; y+=s) {
|
||||
for (let x=0; x<w; x+=s) {
|
||||
const on = ((x/s + y/s) % 2) === 0
|
||||
ctx.fillStyle = on ? "rgba(255,255,255,0.14)" : "rgba(0,0,0,0.0)"
|
||||
ctx.fillRect(x,y,s,s)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: root.color
|
||||
}
|
||||
}
|
||||
|
||||
TextField {
|
||||
id: hexField
|
||||
Layout.fillWidth: true
|
||||
text: "#FF7032D2"
|
||||
placeholderText: "#AARRGGBB"
|
||||
inputMethodHints: Qt.ImhPreferUppercase | Qt.ImhNoPredictiveText
|
||||
|
||||
// 用 palette,而不是 color/selectionColor/selectedTextColor
|
||||
palette.text: root.textPrimary
|
||||
palette.placeholderText: root.textMuted
|
||||
palette.highlight: root.highlightBg
|
||||
palette.highlightedText: root.highlightText
|
||||
palette.base: root.controlBg // 输入框底色(有的 style 会用它)
|
||||
palette.buttonText: root.textPrimary
|
||||
|
||||
background: Rectangle {
|
||||
radius: 8
|
||||
color: root.controlBg
|
||||
border.width: 1
|
||||
border.color: root.controlBorder
|
||||
}
|
||||
|
||||
onEditingFinished: {
|
||||
const c = root.parseHex(text)
|
||||
if (c) root.syncFromColor(c)
|
||||
else text = root.colorToHexAARRGGBB(root.color)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Rectangle {
|
||||
width: 54
|
||||
height: 44
|
||||
radius: 8
|
||||
color: root.controlBg
|
||||
border.width: 1
|
||||
border.color: root.controlBorder
|
||||
Label {
|
||||
id: alphaPercent
|
||||
anchors.centerIn: parent
|
||||
text: "100%"
|
||||
color: root.textPrimary
|
||||
font.pixelSize: 12
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// RGB / HSV 数值区(像参考图那样两行块)
|
||||
GridLayout {
|
||||
Layout.fillWidth: true
|
||||
columns: 3
|
||||
columnSpacing: 8
|
||||
rowSpacing: 8
|
||||
|
||||
// ---- Row1: RGB ----
|
||||
Label { text: "RGB"; color: root.textMuted; Layout.alignment: Qt.AlignVCenter }
|
||||
SpinBox {
|
||||
id: rField
|
||||
from: 0; to: 255; editable: true
|
||||
value: 112
|
||||
Layout.fillWidth: true
|
||||
onValueModified: root.applyRGB(value, gField.value, bField.value)
|
||||
}
|
||||
SpinBox {
|
||||
id: gField
|
||||
from: 0; to: 255; editable: true
|
||||
value: 50
|
||||
Layout.fillWidth: true
|
||||
onValueModified: root.applyRGB(rField.value, value, bField.value)
|
||||
}
|
||||
Item { width: 1; height: 1 } // 占位,让下一行对齐
|
||||
SpinBox {
|
||||
id: bField
|
||||
from: 0; to: 255; editable: true
|
||||
value: 210
|
||||
Layout.fillWidth: true
|
||||
Layout.columnSpan: 2
|
||||
onValueModified: root.applyRGB(rField.value, gField.value, value)
|
||||
}
|
||||
|
||||
// ---- Row2: HSV ----
|
||||
Label { text: "HSV"; color: root.textMuted; Layout.alignment: Qt.AlignVCenter }
|
||||
SpinBox {
|
||||
id: hField
|
||||
from: 0; to: 360; editable: true
|
||||
value: 263
|
||||
Layout.fillWidth: true
|
||||
onValueModified: root.applyHSV(value, sField.value, vField.value)
|
||||
}
|
||||
SpinBox {
|
||||
id: sField
|
||||
from: 0; to: 100; editable: true
|
||||
value: 64
|
||||
Layout.fillWidth: true
|
||||
onValueModified: root.applyHSV(hField.value, value, vField.value)
|
||||
}
|
||||
Item { width: 1; height: 1 }
|
||||
SpinBox {
|
||||
id: vField
|
||||
from: 0; to: 100; editable: true
|
||||
value: 51
|
||||
Layout.fillWidth: true
|
||||
Layout.columnSpan: 2
|
||||
onValueModified: root.applyHSV(hField.value, sField.value, value)
|
||||
}
|
||||
}
|
||||
|
||||
// 底部按钮(像窗口)
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 10
|
||||
|
||||
Item { Layout.fillWidth: true }
|
||||
|
||||
Button {
|
||||
text: qsTr("Cancel")
|
||||
onClicked: { root.visible = false; root.rejected() }
|
||||
}
|
||||
Button {
|
||||
text: qsTr("OK")
|
||||
highlighted: true
|
||||
onClicked: { root.visible = false; root.accepted(root.color) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 每次 HSV 改变,UI 同步
|
||||
onHChanged: if (!_lock) syncFromHSV()
|
||||
onSChanged: if (!_lock) syncFromHSV()
|
||||
onVChanged: if (!_lock) syncFromHSV()
|
||||
onAChanged: if (!_lock) syncFromHSV()
|
||||
}
|
||||
@@ -104,7 +104,7 @@ Rectangle {
|
||||
|
||||
ComboBox {
|
||||
Layout.fillWidth: true
|
||||
model: ["9600", "57600", "115200", "230400", "912600"]
|
||||
model: ["9600", "57600", "115200", "230400", "921600"]
|
||||
Component.onCompleted: {
|
||||
const idx = model.indexOf(String(Backend.serial.baudRate))
|
||||
if (idx >= 0)
|
||||
@@ -469,8 +469,7 @@ Rectangle {
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onClicked: {
|
||||
lowColorDialog.selectedColor = Backend.colorLow
|
||||
lowColorDialog.open()
|
||||
lowColorDialog.openWith(Backend.colorLow)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -478,8 +477,7 @@ Rectangle {
|
||||
text: root.tr("选择")
|
||||
Layout.fillWidth: true
|
||||
onClicked: {
|
||||
lowColorDialog.selectedColor = Backend.colorLow
|
||||
lowColorDialog.open()
|
||||
lowColorDialog.openWith(Backend.colorLow)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -503,8 +501,7 @@ Rectangle {
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onClicked: {
|
||||
midColorDialog.selectedColor = Backend.colorMid
|
||||
midColorDialog.open()
|
||||
midColorDialog.openWith(Backend.colorMid)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -512,8 +509,7 @@ Rectangle {
|
||||
text: root.tr("选择")
|
||||
Layout.fillWidth: true
|
||||
onClicked: {
|
||||
midColorDialog.selectedColor = Backend.colorMid
|
||||
midColorDialog.open()
|
||||
midColorDialog.openWith(Backend.colorMid)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -537,8 +533,7 @@ Rectangle {
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onClicked: {
|
||||
highColorDialog.selectedColor = Backend.colorHigh
|
||||
highColorDialog.open()
|
||||
highColorDialog.openWith(Backend.colorHigh)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -546,8 +541,7 @@ Rectangle {
|
||||
text: root.tr("选择")
|
||||
Layout.fillWidth: true
|
||||
onClicked: {
|
||||
highColorDialog.selectedColor = Backend.colorHigh
|
||||
highColorDialog.open()
|
||||
highColorDialog.openWith(Backend.colorHigh)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -596,22 +590,22 @@ Rectangle {
|
||||
}
|
||||
}
|
||||
|
||||
ColorDialog {
|
||||
ColorPickerDialog {
|
||||
id: lowColorDialog
|
||||
title: root.tr("选择低色")
|
||||
onAccepted: Backend.colorLow = selectedColor
|
||||
onAccepted: Backend.colorLow = c
|
||||
}
|
||||
|
||||
ColorDialog {
|
||||
ColorPickerDialog {
|
||||
id: midColorDialog
|
||||
title: root.tr("选择中色")
|
||||
onAccepted: Backend.colorMid = selectedColor
|
||||
onAccepted: Backend.colorMid = c
|
||||
}
|
||||
|
||||
ColorDialog {
|
||||
ColorPickerDialog {
|
||||
id: highColorDialog
|
||||
title: root.tr("选择高色")
|
||||
onAccepted: Backend.colorHigh = selectedColor
|
||||
onAccepted: Backend.colorHigh = c
|
||||
}
|
||||
|
||||
SaveAsExportDialog {
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
<file>qml/content/LeftPanel.qml</file>
|
||||
<file>qml/content/RightPanel.qml</file>
|
||||
<file>qml/content/CollapsiblePanel.qml</file>
|
||||
<file>qml/content/ColorPickerDialog.qml</file>
|
||||
<file>shaders/dots.frag</file>
|
||||
<file>shaders/dots.vert</file>
|
||||
<file>shaders/bg.frag</file>
|
||||
|
||||
49
src/globalhelper.h
Normal file
49
src/globalhelper.h
Normal file
@@ -0,0 +1,49 @@
|
||||
//
|
||||
// Created by Lenn on 2026/1/20.
|
||||
//
|
||||
|
||||
#ifndef TACTILEIPC3D_GLOBALHELPER_H
|
||||
#define TACTILEIPC3D_GLOBALHELPER_H
|
||||
|
||||
#include <opencv2/core.hpp>
|
||||
#include <opencv2/imgproc.hpp>
|
||||
|
||||
class GlobalHelper {
|
||||
public:
|
||||
const std::string fuck = "fuck you lsp!!!";
|
||||
static GlobalHelper* Instance() {
|
||||
static GlobalHelper ins;
|
||||
return &ins;
|
||||
}
|
||||
|
||||
static void transToMultiMatrix(const cv::Mat& raw_data, float min_threshold,
|
||||
float max_range, cv::Size display_res, cv::Mat& out_image) {
|
||||
CV_Assert(raw_data.type() == CV_32F);
|
||||
|
||||
cv::Mat raw = raw_data.clone();
|
||||
raw.setTo(0.0f, raw < min_threshold);
|
||||
|
||||
double maxVal = 0.0;
|
||||
cv::minMaxLoc(raw, nullptr, &maxVal);
|
||||
if (maxVal > 0.0) {
|
||||
cv::GaussianBlur(raw, raw, cv::Size(3, 3), 0.8, 0.8, cv::BORDER_DEFAULT);
|
||||
}
|
||||
|
||||
cv::Mat norm_data = raw / max_range;
|
||||
cv::min(norm_data, 1.0f, norm_data);
|
||||
cv::max(norm_data, 0.0f, norm_data);
|
||||
|
||||
cv::pow(norm_data, 0.7, norm_data);
|
||||
|
||||
cv::Mat smoothed;
|
||||
cv::resize(norm_data, smoothed, display_res, 0.0, 0.0, cv::INTER_CUBIC);
|
||||
|
||||
cv::GaussianBlur(smoothed, smoothed, cv::Size(31, 31), 0.0, 0.0, cv::BORDER_DEFAULT);
|
||||
|
||||
cv::transpose(smoothed, out_image);
|
||||
}
|
||||
private:
|
||||
GlobalHelper() {}
|
||||
};
|
||||
|
||||
#endif //TACTILEIPC3D_GLOBALHELPER_H
|
||||
@@ -8,7 +8,6 @@
|
||||
#include <QtMath>
|
||||
#include <QFile>
|
||||
#include <QDebug>
|
||||
#include <GL/gl.h>
|
||||
#include <qevent.h>
|
||||
#include <qlogging.h>
|
||||
#include <qminmax.h>
|
||||
|
||||
@@ -11,11 +11,8 @@
|
||||
#include <vector>
|
||||
|
||||
SerialBackend::SerialBackend(QObject *parent)
|
||||
: QObject(parent)
|
||||
, m_packetQueue(2048)
|
||||
, m_frameQueue(2048)
|
||||
, m_readThread(&m_packetQueue)
|
||||
, m_decodeThread(&m_packetQueue, &m_frameQueue) {
|
||||
: QObject(parent), m_packetQueue(2048), m_frameQueue(2048), m_readThread(&m_packetQueue), m_decodeThread(&m_packetQueue, &m_frameQueue)
|
||||
{
|
||||
m_request.dataLength = 24;
|
||||
m_spec.model = QStringLiteral("PZR-A");
|
||||
|
||||
@@ -27,27 +24,27 @@ SerialBackend::SerialBackend(QObject* parent)
|
||||
|
||||
m_sendWorker = new SerialSendWorker();
|
||||
m_sendWorker->moveToThread(&m_sendThread);
|
||||
connect(m_sendWorker, &SerialSendWorker::bytesReceived, this, [this](const QByteArray& data) {
|
||||
connect(m_sendWorker, &SerialSendWorker::bytesReceived, this, [this](const QByteArray &data)
|
||||
{
|
||||
#if 0
|
||||
if (!data.isEmpty())
|
||||
qDebug().noquote() << "Serial recv bytes:" << QString::fromLatin1(data.toHex(' '));
|
||||
#endif
|
||||
m_readThread.enqueueBytes(data);
|
||||
});
|
||||
m_readThread.enqueueBytes(data); });
|
||||
connect(m_sendWorker, &SerialSendWorker::requestBuilt, this, &SerialBackend::requestBuilt);
|
||||
connect(m_sendWorker, &SerialSendWorker::writeFailed, this, [](const QString& error) {
|
||||
connect(m_sendWorker, &SerialSendWorker::writeFailed, this, [](const QString &error)
|
||||
{
|
||||
if (!error.isEmpty())
|
||||
qWarning().noquote() << "Serial write failed:" << error;
|
||||
});
|
||||
qWarning().noquote() << "Serial write failed:" << error; });
|
||||
|
||||
connect(&m_readThread, &SerialReadThread::parseError, this, [](const QString& error) {
|
||||
connect(&m_readThread, &SerialReadThread::parseError, this, [](const QString &error)
|
||||
{
|
||||
if (!error.isEmpty())
|
||||
qWarning().noquote() << "Serial packet invalid:" << error;
|
||||
});
|
||||
connect(&m_decodeThread, &SerialDecodeThread::decodeError, this, [](const QString& error) {
|
||||
qWarning().noquote() << "Serial packet invalid:" << error; });
|
||||
connect(&m_decodeThread, &SerialDecodeThread::decodeError, this, [](const QString &error)
|
||||
{
|
||||
if (!error.isEmpty())
|
||||
qWarning().noquote() << "Serial decode failed:" << error;
|
||||
});
|
||||
qWarning().noquote() << "Serial decode failed:" << error; });
|
||||
connect(&m_decodeThread, &SerialDecodeThread::frameAvailable, this, &SerialBackend::drainFrames_);
|
||||
|
||||
m_sendThread.start();
|
||||
@@ -59,21 +56,24 @@ SerialBackend::SerialBackend(QObject* parent)
|
||||
refreshPorts();
|
||||
}
|
||||
|
||||
SerialBackend::~SerialBackend() {
|
||||
SerialBackend::~SerialBackend()
|
||||
{
|
||||
close();
|
||||
stopPipeline_();
|
||||
|
||||
if (m_sendWorker && m_sendThread.isRunning()) {
|
||||
QMetaObject::invokeMethod(m_sendWorker, [worker = m_sendWorker]() {
|
||||
worker->closeTransport();
|
||||
}, Qt::BlockingQueuedConnection);
|
||||
if (m_sendWorker && m_sendThread.isRunning())
|
||||
{
|
||||
QMetaObject::invokeMethod(m_sendWorker, [worker = m_sendWorker]()
|
||||
{ worker->closeTransport(); }, Qt::BlockingQueuedConnection);
|
||||
}
|
||||
|
||||
if (m_sendWorker && m_sendThread.isRunning()) {
|
||||
if (m_sendWorker && m_sendThread.isRunning())
|
||||
{
|
||||
QMetaObject::invokeMethod(m_sendWorker, &QObject::deleteLater, Qt::QueuedConnection);
|
||||
}
|
||||
|
||||
if (m_sendThread.isRunning()) {
|
||||
if (m_sendThread.isRunning())
|
||||
{
|
||||
m_sendThread.quit();
|
||||
m_sendThread.wait();
|
||||
}
|
||||
@@ -81,15 +81,18 @@ SerialBackend::~SerialBackend() {
|
||||
m_sendWorker = nullptr;
|
||||
}
|
||||
|
||||
QString SerialBackend::mode() const {
|
||||
QString SerialBackend::mode() const
|
||||
{
|
||||
return (m_config.mode == DeviceMode::Slave) ? QStringLiteral("slave") : QStringLiteral("master");
|
||||
}
|
||||
|
||||
QString SerialBackend::sensorGrid() const {
|
||||
QString SerialBackend::sensorGrid() const
|
||||
{
|
||||
return QStringLiteral("%1x%2").arg(m_spec.rows).arg(m_spec.cols);
|
||||
}
|
||||
|
||||
void SerialBackend::setPortName(const QString& name) {
|
||||
void SerialBackend::setPortName(const QString &name)
|
||||
{
|
||||
if (m_config.portName == name)
|
||||
return;
|
||||
m_config.portName = name;
|
||||
@@ -97,7 +100,8 @@ void SerialBackend::setPortName(const QString& name) {
|
||||
emit portNameChanged();
|
||||
}
|
||||
|
||||
void SerialBackend::setBaudRate(int rate) {
|
||||
void SerialBackend::setBaudRate(int rate)
|
||||
{
|
||||
if (m_config.baudRate == rate)
|
||||
return;
|
||||
m_config.baudRate = rate;
|
||||
@@ -105,7 +109,8 @@ void SerialBackend::setBaudRate(int rate) {
|
||||
emit baudRateChanged();
|
||||
}
|
||||
|
||||
void SerialBackend::setPollIntervalMs(int intervalMs) {
|
||||
void SerialBackend::setPollIntervalMs(int intervalMs)
|
||||
{
|
||||
intervalMs = qMax(1, intervalMs);
|
||||
if (m_config.pollIntervalMs == intervalMs)
|
||||
return;
|
||||
@@ -114,7 +119,8 @@ void SerialBackend::setPollIntervalMs(int intervalMs) {
|
||||
emit pollIntervalMsChanged();
|
||||
}
|
||||
|
||||
void SerialBackend::setDeviceAddress(int address) {
|
||||
void SerialBackend::setDeviceAddress(int address)
|
||||
{
|
||||
const int capped = qBound(0, address, 255);
|
||||
if (m_config.deviceAddress == static_cast<quint8>(capped))
|
||||
return;
|
||||
@@ -123,7 +129,8 @@ void SerialBackend::setDeviceAddress(int address) {
|
||||
emit deviceAddressChanged();
|
||||
}
|
||||
|
||||
void SerialBackend::setMode(const QString& mode) {
|
||||
void SerialBackend::setMode(const QString &mode)
|
||||
{
|
||||
const QString lower = mode.trimmed().toLower();
|
||||
const DeviceMode next = (lower == QStringLiteral("master")) ? DeviceMode::Master : DeviceMode::Slave;
|
||||
if (m_config.mode == next)
|
||||
@@ -133,7 +140,8 @@ void SerialBackend::setMode(const QString& mode) {
|
||||
emit modeChanged();
|
||||
}
|
||||
|
||||
void SerialBackend::setRequestFunction(int func) {
|
||||
void SerialBackend::setRequestFunction(int func)
|
||||
{
|
||||
const int capped = qBound(0, func, 255);
|
||||
if (m_request.functionCode == static_cast<quint8>(capped))
|
||||
return;
|
||||
@@ -142,7 +150,8 @@ void SerialBackend::setRequestFunction(int func) {
|
||||
emit requestFunctionChanged();
|
||||
}
|
||||
|
||||
void SerialBackend::setRequestStartAddress(int addr) {
|
||||
void SerialBackend::setRequestStartAddress(int addr)
|
||||
{
|
||||
const quint32 capped = static_cast<quint32>(qMax(0, addr));
|
||||
if (m_request.startAddress == capped)
|
||||
return;
|
||||
@@ -151,7 +160,8 @@ void SerialBackend::setRequestStartAddress(int addr) {
|
||||
emit requestStartAddressChanged();
|
||||
}
|
||||
|
||||
void SerialBackend::setRequestLength(int len) {
|
||||
void SerialBackend::setRequestLength(int len)
|
||||
{
|
||||
const int capped = qBound(0, len, 65535);
|
||||
if (m_request.dataLength == static_cast<quint16>(capped))
|
||||
return;
|
||||
@@ -160,26 +170,30 @@ void SerialBackend::setRequestLength(int len) {
|
||||
emit requestLengthChanged();
|
||||
}
|
||||
|
||||
void SerialBackend::setProtocol(const QString& name) {
|
||||
void SerialBackend::setProtocol(const QString &name)
|
||||
{
|
||||
if (!m_manager.setActiveProtocol(name))
|
||||
return;
|
||||
updateProtocolBindings_();
|
||||
emit protocolChanged();
|
||||
}
|
||||
|
||||
void SerialBackend::applySensorSpec(const QString& model, int rows, int cols) {
|
||||
void SerialBackend::applySensorSpec(const QString &model, int rows, int cols)
|
||||
{
|
||||
const QString nextModel = model.trimmed();
|
||||
const int nextRows = qMax(0, rows);
|
||||
const int nextCols = qMax(0, cols);
|
||||
bool changed = false;
|
||||
|
||||
if (!nextModel.isEmpty() && m_spec.model != nextModel) {
|
||||
if (!nextModel.isEmpty() && m_spec.model != nextModel)
|
||||
{
|
||||
m_spec.model = nextModel;
|
||||
emit sensorModelChanged();
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (m_spec.rows != nextRows || m_spec.cols != nextCols) {
|
||||
if (m_spec.rows != nextRows || m_spec.cols != nextCols)
|
||||
{
|
||||
m_spec.rows = nextRows;
|
||||
m_spec.cols = nextCols;
|
||||
emit sensorGridChanged();
|
||||
@@ -190,34 +204,38 @@ void SerialBackend::applySensorSpec(const QString& model, int rows, int cols) {
|
||||
return;
|
||||
}
|
||||
|
||||
void SerialBackend::setTransport(std::unique_ptr<ISerialTransport> transport) {
|
||||
void SerialBackend::setTransport(std::unique_ptr<ISerialTransport> transport)
|
||||
{
|
||||
if (!transport || !m_sendWorker)
|
||||
return;
|
||||
if (!m_sendThread.isRunning())
|
||||
m_sendThread.start();
|
||||
if (transport->thread() != &m_sendThread)
|
||||
transport->moveToThread(&m_sendThread);
|
||||
QMetaObject::invokeMethod(m_sendWorker, [worker = m_sendWorker, transport = std::move(transport)]() mutable {
|
||||
worker->setTransport(std::move(transport));
|
||||
}, Qt::BlockingQueuedConnection);
|
||||
QMetaObject::invokeMethod(m_sendWorker, [worker = m_sendWorker, transport = std::move(transport)]() mutable
|
||||
{ worker->setTransport(std::move(transport)); }, Qt::BlockingQueuedConnection);
|
||||
}
|
||||
|
||||
void SerialBackend::refreshPorts() {
|
||||
void SerialBackend::refreshPorts()
|
||||
{
|
||||
// Placeholder for real port discovery (QtSerialPort or third-party transport).
|
||||
|
||||
m_availablePorts.clear();
|
||||
auto device_found = QSerialPortInfo::availablePorts();
|
||||
for (auto item : device_found) {
|
||||
for (auto item : device_found)
|
||||
{
|
||||
m_availablePorts.append(item.portName());
|
||||
}
|
||||
if (m_config.portName.isEmpty() && !m_availablePorts.isEmpty()) {
|
||||
if (m_config.portName.isEmpty() && !m_availablePorts.isEmpty())
|
||||
{
|
||||
m_config.portName = m_availablePorts.first();
|
||||
emit portNameChanged();
|
||||
}
|
||||
emit availablePortsChanged();
|
||||
}
|
||||
|
||||
bool SerialBackend::open() {
|
||||
bool SerialBackend::open()
|
||||
{
|
||||
if (m_connected)
|
||||
return true;
|
||||
|
||||
@@ -225,12 +243,13 @@ bool SerialBackend::open() {
|
||||
|
||||
bool ok = false;
|
||||
QString error;
|
||||
if (m_sendWorker) {
|
||||
QMetaObject::invokeMethod(m_sendWorker, [worker = m_sendWorker, config = m_config, &ok, &error]() {
|
||||
ok = worker->openTransport(config, &error);
|
||||
}, Qt::BlockingQueuedConnection);
|
||||
if (m_sendWorker)
|
||||
{
|
||||
QMetaObject::invokeMethod(m_sendWorker, [worker = m_sendWorker, config = m_config, &ok, &error]()
|
||||
{ ok = worker->openTransport(config, &error); }, Qt::BlockingQueuedConnection);
|
||||
}
|
||||
if (!ok) {
|
||||
if (!ok)
|
||||
{
|
||||
qWarning().noquote() << "Serial open failed:" << error;
|
||||
return false;
|
||||
}
|
||||
@@ -241,44 +260,49 @@ bool SerialBackend::open() {
|
||||
return true;
|
||||
}
|
||||
|
||||
void SerialBackend::close() {
|
||||
void SerialBackend::close()
|
||||
{
|
||||
if (!m_connected)
|
||||
return;
|
||||
if (m_sendWorker && m_sendThread.isRunning()) {
|
||||
QMetaObject::invokeMethod(m_sendWorker, [worker = m_sendWorker]() {
|
||||
worker->closeTransport();
|
||||
}, Qt::BlockingQueuedConnection);
|
||||
if (m_sendWorker && m_sendThread.isRunning())
|
||||
{
|
||||
QMetaObject::invokeMethod(m_sendWorker, [worker = m_sendWorker]()
|
||||
{ worker->closeTransport(); }, Qt::BlockingQueuedConnection);
|
||||
}
|
||||
stopPipeline_();
|
||||
m_connected = false;
|
||||
emit connectedChanged();
|
||||
}
|
||||
|
||||
void SerialBackend::requestOnce() {
|
||||
void SerialBackend::requestOnce()
|
||||
{
|
||||
if (!m_sendWorker)
|
||||
return;
|
||||
QMetaObject::invokeMethod(m_sendWorker, [worker = m_sendWorker]() {
|
||||
worker->requestOnce();
|
||||
}, Qt::QueuedConnection);
|
||||
QMetaObject::invokeMethod(m_sendWorker, [worker = m_sendWorker]()
|
||||
{ worker->requestOnce(); }, Qt::QueuedConnection);
|
||||
}
|
||||
|
||||
void SerialBackend::feedBytes(const QByteArray& data) {
|
||||
void SerialBackend::feedBytes(const QByteArray &data)
|
||||
{
|
||||
if (!m_readThread.isRunning())
|
||||
startPipeline_();
|
||||
m_readThread.enqueueBytes(data);
|
||||
}
|
||||
|
||||
void SerialBackend::setSensorWidth(int w) {
|
||||
void SerialBackend::setSensorWidth(int w)
|
||||
{
|
||||
m_spec.cols = w;
|
||||
syncSendConfig_();
|
||||
}
|
||||
|
||||
void SerialBackend::setSensorHeight(int h) {
|
||||
void SerialBackend::setSensorHeight(int h)
|
||||
{
|
||||
m_spec.rows = h;
|
||||
syncSendConfig_();
|
||||
}
|
||||
|
||||
void SerialBackend::drainFrames_() {
|
||||
void SerialBackend::drainFrames_()
|
||||
{
|
||||
if (!m_frameCallback)
|
||||
return;
|
||||
DataFrame frame;
|
||||
@@ -286,7 +310,8 @@ void SerialBackend::drainFrames_() {
|
||||
m_frameCallback(frame);
|
||||
}
|
||||
|
||||
void SerialBackend::startPipeline_() {
|
||||
void SerialBackend::startPipeline_()
|
||||
{
|
||||
if (m_readThread.isRunning() || m_decodeThread.isRunning())
|
||||
stopPipeline_();
|
||||
m_packetQueue.reset();
|
||||
@@ -300,12 +325,15 @@ void SerialBackend::startPipeline_() {
|
||||
m_decodeThread.start();
|
||||
}
|
||||
|
||||
void SerialBackend::stopPipeline_() {
|
||||
if (m_readThread.isRunning()) {
|
||||
void SerialBackend::stopPipeline_()
|
||||
{
|
||||
if (m_readThread.isRunning())
|
||||
{
|
||||
m_readThread.stop();
|
||||
m_readThread.wait();
|
||||
}
|
||||
if (m_decodeThread.isRunning()) {
|
||||
if (m_decodeThread.isRunning())
|
||||
{
|
||||
m_decodeThread.stop();
|
||||
m_decodeThread.wait();
|
||||
}
|
||||
@@ -314,53 +342,60 @@ void SerialBackend::stopPipeline_() {
|
||||
m_readThread.clear();
|
||||
}
|
||||
|
||||
void SerialBackend::updateProtocolBindings_() {
|
||||
void SerialBackend::updateProtocolBindings_()
|
||||
{
|
||||
const auto bundle = m_manager.activeBundle();
|
||||
|
||||
SerialReadThread::ParseFunc parseFunc;
|
||||
if (bundle.format) {
|
||||
parseFunc = [format = bundle.format](QByteArray* buffer, QByteArray* packet, QString* error) {
|
||||
if (bundle.format)
|
||||
{
|
||||
parseFunc = [format = bundle.format](QByteArray *buffer, QByteArray *packet, QString *error)
|
||||
{
|
||||
return format->tryParse(buffer, packet, error);
|
||||
};
|
||||
}
|
||||
m_readThread.setParseFunc(std::move(parseFunc));
|
||||
|
||||
SerialDecodeThread::DecodeFunc decodeFunc;
|
||||
if (bundle.decoder) {
|
||||
decodeFunc = [decoder = bundle.decoder](const QByteArray& packet, DataFrame* frame, QString* error) {
|
||||
if (bundle.decoder)
|
||||
{
|
||||
decodeFunc = [decoder = bundle.decoder](const QByteArray &packet, DataFrame *frame, QString *error)
|
||||
{
|
||||
return decoder->decodeFrame(packet, frame, error);
|
||||
};
|
||||
}
|
||||
m_decodeThread.setDecodeFunc(std::move(decodeFunc));
|
||||
|
||||
SerialSendWorker::BuildRequestFunc requestFunc;
|
||||
if (bundle.codec) {
|
||||
requestFunc = [codec = bundle.codec](const SerialConfig& config, const SensorRequest& request) {
|
||||
if (bundle.codec)
|
||||
{
|
||||
requestFunc = [codec = bundle.codec](const SerialConfig &config, const SensorRequest &request)
|
||||
{
|
||||
return codec->buildRequest(config, request);
|
||||
};
|
||||
}
|
||||
|
||||
if (m_sendWorker) {
|
||||
QMetaObject::invokeMethod(m_sendWorker, [worker = m_sendWorker, requestFunc]() mutable {
|
||||
worker->setBuildRequestFunc(std::move(requestFunc));
|
||||
}, Qt::QueuedConnection);
|
||||
if (m_sendWorker)
|
||||
{
|
||||
QMetaObject::invokeMethod(m_sendWorker, [worker = m_sendWorker, requestFunc]() mutable
|
||||
{ worker->setBuildRequestFunc(std::move(requestFunc)); }, Qt::QueuedConnection);
|
||||
}
|
||||
}
|
||||
|
||||
void SerialBackend::syncSendConfig_() {
|
||||
void SerialBackend::syncSendConfig_()
|
||||
{
|
||||
if (!m_sendWorker)
|
||||
return;
|
||||
const SerialConfig config = m_config;
|
||||
QMetaObject::invokeMethod(m_sendWorker, [worker = m_sendWorker, config]() {
|
||||
worker->setConfig(config);
|
||||
}, Qt::QueuedConnection);
|
||||
QMetaObject::invokeMethod(m_sendWorker, [worker = m_sendWorker, config]()
|
||||
{ worker->setConfig(config); }, Qt::QueuedConnection);
|
||||
}
|
||||
|
||||
void SerialBackend::syncSendRequest_() {
|
||||
void SerialBackend::syncSendRequest_()
|
||||
{
|
||||
if (!m_sendWorker)
|
||||
return;
|
||||
const SensorRequest request = m_request;
|
||||
QMetaObject::invokeMethod(m_sendWorker, [worker = m_sendWorker, request]() {
|
||||
worker->setRequest(request);
|
||||
}, Qt::QueuedConnection);
|
||||
QMetaObject::invokeMethod(m_sendWorker, [worker = m_sendWorker, request]()
|
||||
{ worker->setRequest(request); }, Qt::QueuedConnection);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user