fix:TextInput support hot update;feat:add zero color and update
algorithm
This commit is contained in:
@@ -31,7 +31,7 @@ include_directories(${CMAKE_CURRENT_SOURCE_DIR}/src)
|
||||
|
||||
qt_standard_project_setup()
|
||||
|
||||
add_executable(TactileIpc3D
|
||||
add_executable(TactileIpc3D WIN32
|
||||
main.cpp
|
||||
resources.qrc
|
||||
src/translation_manager.h
|
||||
@@ -57,8 +57,6 @@ add_executable(TactileIpc3D
|
||||
src/serial/serial_qt_transport.h
|
||||
src/serial/serial_qt_transport.cpp
|
||||
src/serial/serial_types.h
|
||||
src/serial/piezoresistive_a_protocol.h
|
||||
src/serial/piezoresistive_a_protocol.cpp
|
||||
src/ringbuffer.h
|
||||
src/ringbuffer.cpp
|
||||
src/sparkline_plotitem.h
|
||||
@@ -79,6 +77,19 @@ target_link_libraries(TactileIpc3D PRIVATE
|
||||
Qt6::QuickDialogs2
|
||||
QXlsx::QXlsx
|
||||
)
|
||||
|
||||
add_library(PiezoresistiveAPlugin SHARED
|
||||
plugins/piezoresistive_a/piezoresistive_a_plugin.h
|
||||
plugins/piezoresistive_a/piezoresistive_a_plugin.cpp
|
||||
src/serial/piezoresistive_a_protocol.h
|
||||
src/serial/piezoresistive_a_protocol.cpp
|
||||
)
|
||||
target_include_directories(PiezoresistiveAPlugin PRIVATE
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src
|
||||
)
|
||||
target_link_libraries(PiezoresistiveAPlugin PRIVATE
|
||||
Qt6::Core
|
||||
)
|
||||
target_include_directories(TactileIpc3D PRIVATE
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/3rdpart/OpenCV/include
|
||||
)
|
||||
@@ -132,8 +143,25 @@ set_target_properties(TactileIpc3D PROPERTIES
|
||||
RUNTIME_OUTPUT_DIRECTORY_MINSIZEREL "${runtime_out_dir}/MinSizeRel"
|
||||
)
|
||||
|
||||
set_target_properties(PiezoresistiveAPlugin PROPERTIES
|
||||
RUNTIME_OUTPUT_DIRECTORY "${runtime_out_dir}/plugins/decoders"
|
||||
RUNTIME_OUTPUT_DIRECTORY_DEBUG "${runtime_out_dir}/Debug/plugins/decoders"
|
||||
RUNTIME_OUTPUT_DIRECTORY_RELEASE "${runtime_out_dir}/Release/plugins/decoders"
|
||||
RUNTIME_OUTPUT_DIRECTORY_RELWITHDEBINFO "${runtime_out_dir}/RelWithDebInfo/plugins/decoders"
|
||||
RUNTIME_OUTPUT_DIRECTORY_MINSIZEREL "${runtime_out_dir}/MinSizeRel/plugins/decoders"
|
||||
LIBRARY_OUTPUT_DIRECTORY "${runtime_out_dir}/plugins/decoders"
|
||||
LIBRARY_OUTPUT_DIRECTORY_DEBUG "${runtime_out_dir}/Debug/plugins/decoders"
|
||||
LIBRARY_OUTPUT_DIRECTORY_RELEASE "${runtime_out_dir}/Release/plugins/decoders"
|
||||
LIBRARY_OUTPUT_DIRECTORY_RELWITHDEBINFO "${runtime_out_dir}/RelWithDebInfo/plugins/decoders"
|
||||
LIBRARY_OUTPUT_DIRECTORY_MINSIZEREL "${runtime_out_dir}/MinSizeRel/plugins/decoders"
|
||||
)
|
||||
|
||||
include(GNUInstallDirs)
|
||||
install(TARGETS TactileIpc3D
|
||||
RUNTIME DESTINATION bin
|
||||
)
|
||||
install(TARGETS PiezoresistiveAPlugin
|
||||
RUNTIME DESTINATION bin/plugins/decoders
|
||||
LIBRARY DESTINATION bin/plugins/decoders
|
||||
)
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
1.0.0
|
||||
0.4.0
|
||||
|
||||
@@ -461,6 +461,44 @@ classDiagram
|
||||
- `m_protocols`:协议字典。
|
||||
- `m_activeName`:当前协议名。
|
||||
|
||||
### 协议插件化(`plugins/decoders`)
|
||||
- 目标:每个协议作为独立 DLL 插件,可按需安装与动态加载。
|
||||
- 关键接口:
|
||||
- `IProtocolPlugin`:暴露 `protocolName()` / `apiVersion()` / `createBundle()`。
|
||||
- `ProtocolBundle`:`codec/decoder/format` 三件套。
|
||||
- 加载流程:
|
||||
- `SerialBackend` 启动时调用 `SerialManager::loadPlugins()`。
|
||||
- `SerialManager` 扫描 `appDir/plugins/decoders` 目录,按平台过滤后缀(Windows: `.dll`)。
|
||||
- 校验 `apiVersion`,创建并注册协议 bundle。
|
||||
- 通过 `m_pluginLoaders` 保持插件常驻,避免对象失效。
|
||||
|
||||
#### 插件 UML (Mermaid)
|
||||
```mermaid
|
||||
classDiagram
|
||||
class IProtocolPlugin {
|
||||
+protocolName()
|
||||
+apiVersion()
|
||||
+createBundle()
|
||||
}
|
||||
class ProtocolBundle {
|
||||
+codec
|
||||
+decoder
|
||||
+format
|
||||
}
|
||||
class SerialManager {
|
||||
+loadPlugins(dir)
|
||||
+registerProtocol()
|
||||
+setActiveProtocol()
|
||||
}
|
||||
class SerialBackend {
|
||||
+initPlugins()
|
||||
}
|
||||
|
||||
SerialBackend --> SerialManager
|
||||
SerialManager --> IProtocolPlugin
|
||||
IProtocolPlugin --> ProtocolBundle
|
||||
```
|
||||
|
||||
### ISerialTransport (`src/serial/serial_transport.h`)
|
||||
- 作用:串口传输抽象层,屏蔽不同平台差异。
|
||||
- 接口:
|
||||
@@ -633,6 +671,7 @@ classDiagram
|
||||
|
||||
## 更新记录
|
||||
|
||||
- 2026-01-20:协议解码插件化(DLL),SerialBackend 启动时动态加载 `plugins/decoders`,安装器支持按组件选择协议插件。
|
||||
- 2026-01-11:新增 `QtSerialTransport`(`QSerialPort` 传输实现)并设为默认传输;补齐点选拾取逻辑;新增数据流/时序图并补充可视化 TODO 说明。
|
||||
- 2026-01-12:`LeftPanel` 新增颜色映射参数;`AppBackend`/`GLWidget` 增加颜色与范围接口,shader 使用三色渐变映射数据值。
|
||||
- 2026-01-11:补充串口配置流程图与配置接口说明(协议/参数/解码器绑定/打开流程)。
|
||||
|
||||
@@ -184,6 +184,10 @@
|
||||
<source>显示网络</source>
|
||||
<translation>Show Grid</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>凹陷表面</source>
|
||||
<translation>Dented Surface</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>显示坐标轴</source>
|
||||
<translation>Show Axes</translation>
|
||||
@@ -208,6 +212,14 @@
|
||||
<source>选择高色</source>
|
||||
<translation>Select High Color</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>零色</source>
|
||||
<translation>Zero Color</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>选择零色</source>
|
||||
<translation>Select Zero Color</translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>OpenFileDialog</name>
|
||||
|
||||
@@ -148,6 +148,10 @@
|
||||
<source>显示网络</source>
|
||||
<translation>显示网络</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>凹陷表面</source>
|
||||
<translation>凹陷表面</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>显示坐标轴</source>
|
||||
<translation>显示坐标轴</translation>
|
||||
@@ -160,6 +164,14 @@
|
||||
<source>导出数据</source>
|
||||
<translation>导出数据</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>零色</source>
|
||||
<translation>零色</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>选择零色</source>
|
||||
<translation>选择零色</translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>RightPanel</name>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#define MyAppName "TactileIpc3D"
|
||||
#define MyAppVersion "0.2.0"
|
||||
#define MyAppVersion "0.4.0"
|
||||
#define MyAppPublisher "TactileIpc3D"
|
||||
#define MyAppExeName "TactileIpc3D.exe"
|
||||
#define SourceDir "..\\build\Desktop_Qt_6_8_3_MinGW_64_bit-Release\\out\Release"
|
||||
@@ -41,8 +41,13 @@ Name: "english"; MessagesFile: "compiler:Default.isl"
|
||||
[Tasks]
|
||||
Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked
|
||||
|
||||
[Components]
|
||||
Name: "core"; Description: "Core Application"; Types: full compact custom; Flags: fixed
|
||||
Name: "decoder_pzr_a"; Description: "Piezoresistive A Decoder (PZR-A)"; Types: full
|
||||
|
||||
[Files]
|
||||
Source: "{#SourceDir}\\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs
|
||||
Source: "{#SourceDir}\\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs; Components: core; Excludes: "plugins\\decoders\\*.dll"
|
||||
Source: "{#SourceDir}\\plugins\\decoders\\libPiezoresistiveAPlugin.dll"; DestDir: "{app}\\plugins\\decoders"; Flags: ignoreversion; Components: decoder_pzr_a
|
||||
|
||||
[Icons]
|
||||
Name: "{group}\\{#MyAppName}"; Filename: "{app}\\{#MyAppExeName}"
|
||||
|
||||
6
main.cpp
6
main.cpp
@@ -116,9 +116,11 @@ int main(int argc, char *argv[]) {
|
||||
glw->setSpec(12, 7, 0.1f, 0.03f);
|
||||
glw->setPanelThickness(0.08f);
|
||||
glw->setRange(backend.rangeMin(), backend.rangeMax());
|
||||
glw->setColorZero(backend.colorZero());
|
||||
glw->setColorLow(backend.colorLow());
|
||||
glw->setColorMid(backend.colorMid());
|
||||
glw->setColorHigh(backend.colorHigh());
|
||||
glw->setUseHeatmap(backend.useHeatmap());
|
||||
|
||||
/* backend.data()->setLiveRenderCallback([glw](const DataFrame& frame) {
|
||||
if (frame.data.size() != glw->dotCount())
|
||||
@@ -177,12 +179,16 @@ int main(int argc, char *argv[]) {
|
||||
|
||||
QObject::connect(&backend, &AppBackend::showGridChanged, glw,
|
||||
&GLWidget::setShowGrid);
|
||||
QObject::connect(&backend, &AppBackend::useHeatmapChanged, glw,
|
||||
&GLWidget::setUseHeatmap);
|
||||
QObject::connect(&backend, &AppBackend::sensorRowChanged, glw,
|
||||
&GLWidget::setRow);
|
||||
QObject::connect(&backend, &AppBackend::sensorColChanged, glw,
|
||||
&GLWidget::setCol);
|
||||
QObject::connect(&backend, &AppBackend::rangeChanged, glw,
|
||||
&GLWidget::setRange);
|
||||
QObject::connect(&backend, &AppBackend::colorZeroChanged, glw,
|
||||
&GLWidget::setColorZero);
|
||||
QObject::connect(&backend, &AppBackend::colorLowChanged, glw,
|
||||
&GLWidget::setColorLow);
|
||||
QObject::connect(&backend, &AppBackend::colorMidChanged, glw,
|
||||
|
||||
@@ -503,11 +503,20 @@ Window {
|
||||
border.color: root.controlBorder
|
||||
}
|
||||
|
||||
function applyIfValid() {
|
||||
const c = root.parseHex(text)
|
||||
if (c) root.syncFromColor(c)
|
||||
}
|
||||
onTextEdited: applyIfValid()
|
||||
onEditingFinished: {
|
||||
const c = root.parseHex(text)
|
||||
if (c) root.syncFromColor(c)
|
||||
else text = root.colorToHexAARRGGBB(root.color)
|
||||
}
|
||||
onAccepted: {
|
||||
applyIfValid()
|
||||
focus = false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -63,6 +63,7 @@ Rectangle {
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 4
|
||||
spacing: 8
|
||||
|
||||
Label {
|
||||
@@ -167,6 +168,7 @@ Rectangle {
|
||||
text: root.formatHexByte(Backend.serial.deviceAddress)
|
||||
placeholderText: "0x01"
|
||||
inputMethodHints: Qt.ImhPreferUppercase
|
||||
property bool _internalUpdate: false
|
||||
validator: RegularExpressionValidator {
|
||||
regularExpression: /^(0x|0X)?[0-9a-fA-F]{1,2}$/
|
||||
}
|
||||
@@ -183,7 +185,9 @@ Rectangle {
|
||||
Connections {
|
||||
target: Backend.serial
|
||||
function onDeviceAddressChanged() {
|
||||
addrField._internalUpdate = true
|
||||
addrField.text = root.formatHexByte(Backend.serial.deviceAddress)
|
||||
addrField._internalUpdate = false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -423,12 +427,31 @@ Rectangle {
|
||||
color: root.textColor
|
||||
}
|
||||
SpinBox {
|
||||
id: rangeMinBox
|
||||
Layout.fillWidth: true
|
||||
from: -999999
|
||||
to: 999999
|
||||
editable: true
|
||||
value: Backend.rangeMin
|
||||
function applyTextEdit() {
|
||||
if (!contentItem) return
|
||||
const parsed = valueFromText(contentItem.text, locale)
|
||||
if (isNaN(parsed)) return
|
||||
const clamped = Math.max(from, Math.min(to, parsed))
|
||||
if (Backend.rangeMin !== clamped) {
|
||||
Backend.rangeMin = clamped
|
||||
}
|
||||
}
|
||||
onValueModified: Backend.rangeMin = value
|
||||
Connections {
|
||||
target: rangeMinBox.contentItem
|
||||
function onTextEdited() { rangeMinBox.applyTextEdit() }
|
||||
function onEditingFinished() { rangeMinBox.applyTextEdit() }
|
||||
function onAccepted() {
|
||||
rangeMinBox.applyTextEdit()
|
||||
rangeMinBox.focus = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -441,12 +464,62 @@ Rectangle {
|
||||
color: root.textColor
|
||||
}
|
||||
SpinBox {
|
||||
id: rangeMaxBox
|
||||
Layout.fillWidth: true
|
||||
from: -999999
|
||||
to: 999999
|
||||
editable: true
|
||||
value: Backend.rangeMax
|
||||
function applyTextEdit() {
|
||||
if (!contentItem) return
|
||||
const parsed = valueFromText(contentItem.text, locale)
|
||||
if (isNaN(parsed)) return
|
||||
const clamped = Math.max(from, Math.min(to, parsed))
|
||||
if (Backend.rangeMax !== clamped) {
|
||||
Backend.rangeMax = clamped
|
||||
}
|
||||
}
|
||||
onValueModified: Backend.rangeMax = value
|
||||
Connections {
|
||||
target: rangeMaxBox.contentItem
|
||||
function onTextEdited() { rangeMaxBox.applyTextEdit() }
|
||||
function onEditingFinished() { rangeMaxBox.applyTextEdit() }
|
||||
function onAccepted() {
|
||||
rangeMaxBox.applyTextEdit()
|
||||
rangeMaxBox.focus = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 8
|
||||
Label {
|
||||
text: root.tr("零色")
|
||||
Layout.preferredWidth: 90
|
||||
color: root.textColor
|
||||
}
|
||||
Rectangle {
|
||||
width: 22
|
||||
height: 22
|
||||
radius: 4
|
||||
color: Backend.colorZero
|
||||
border.width: 1
|
||||
border.color: Qt.rgba(0, 0, 0, 0.2)
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onClicked: {
|
||||
zeroColorDialog.openWith(Backend.colorZero)
|
||||
}
|
||||
}
|
||||
}
|
||||
Button {
|
||||
text: root.tr("选择")
|
||||
Layout.fillWidth: true
|
||||
onClicked: {
|
||||
zeroColorDialog.openWith(Backend.colorZero)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -557,6 +630,11 @@ Rectangle {
|
||||
checked: Backend.showGrid
|
||||
onToggled: Backend.showGrid = checked
|
||||
}
|
||||
CheckBox {
|
||||
text: root.tr("凹陷表面")
|
||||
checked: Backend.useHeatmap
|
||||
onToggled: Backend.useHeatmap = checked
|
||||
}
|
||||
CheckBox {
|
||||
text: root.tr("显示坐标轴")
|
||||
checked: false
|
||||
@@ -590,6 +668,13 @@ Rectangle {
|
||||
}
|
||||
}
|
||||
|
||||
ColorPickerDialog {
|
||||
id: zeroColorDialog
|
||||
title: root.tr("选择零色")
|
||||
onAccepted: Backend.colorZero = c
|
||||
}
|
||||
|
||||
|
||||
ColorPickerDialog {
|
||||
id: lowColorDialog
|
||||
title: root.tr("选择低色")
|
||||
|
||||
@@ -2,14 +2,51 @@
|
||||
|
||||
in vec3 vNormal;
|
||||
in vec3 vWorldPos;
|
||||
in vec2 vUV;
|
||||
out vec4 FragColor;
|
||||
|
||||
uniform vec3 uCameraPos;
|
||||
uniform vec3 uLightDir;
|
||||
uniform vec3 uColor;
|
||||
uniform sampler2D uHeightTex;
|
||||
uniform float uMinV;
|
||||
uniform float uMaxV;
|
||||
uniform vec2 uTexelSize;
|
||||
uniform vec3 uColorZero;
|
||||
uniform vec3 uColorLow;
|
||||
uniform vec3 uColorMid;
|
||||
uniform vec3 uColorHigh;
|
||||
|
||||
float saturate(float x) { return clamp(x, 0.0, 1.0); }
|
||||
|
||||
float value01(float v) {
|
||||
return saturate((v - uMinV) / max(uMaxV - uMinV, 1e-6));
|
||||
}
|
||||
|
||||
vec3 colorRamp(float t) {
|
||||
if (t < 0.5) {
|
||||
return mix(uColorLow, uColorMid, t / 0.5);
|
||||
}
|
||||
return mix(uColorMid, uColorHigh, (t - 0.5) / 0.5);
|
||||
}
|
||||
|
||||
float maxNeighborValue(vec2 uv) {
|
||||
vec2 texSizeF = 1.0 / uTexelSize;
|
||||
ivec2 texSizeI = ivec2(max(vec2(1.0), floor(texSizeF + 0.5)));
|
||||
ivec2 maxCoord = texSizeI - ivec2(1);
|
||||
|
||||
vec2 texelF = clamp(uv * texSizeF, vec2(0.0), vec2(maxCoord));
|
||||
ivec2 base = ivec2(floor(texelF));
|
||||
ivec2 base10 = min(base + ivec2(1, 0), maxCoord);
|
||||
ivec2 base01 = min(base + ivec2(0, 1), maxCoord);
|
||||
ivec2 base11 = min(base + ivec2(1, 1), maxCoord);
|
||||
|
||||
float v00 = texelFetch(uHeightTex, base, 0).r;
|
||||
float v10 = texelFetch(uHeightTex, base10, 0).r;
|
||||
float v01 = texelFetch(uHeightTex, base01, 0).r;
|
||||
float v11 = texelFetch(uHeightTex, base11, 0).r;
|
||||
return max(max(v00, v10), max(v01, v11));
|
||||
}
|
||||
|
||||
void main() {
|
||||
vec3 N = normalize(vNormal);
|
||||
vec3 L = normalize(uLightDir);
|
||||
@@ -19,7 +56,16 @@ void main() {
|
||||
float diff = saturate(dot(N, L));
|
||||
float spec = pow(saturate(dot(N, H)), 24.0);
|
||||
|
||||
vec3 col = uColor * (0.35 + 0.65 * diff);
|
||||
float vC = texture(uHeightTex, vUV).r;
|
||||
bool isZero = abs(vC) <= 1e-6;
|
||||
float t = value01(vC);
|
||||
float m = maxNeighborValue(vUV);
|
||||
float eps = max(1e-4, 0.001 * (uMaxV - uMinV));
|
||||
float force = smoothstep(uMaxV - eps, uMaxV, m);
|
||||
t = max(t, force);
|
||||
vec3 base = isZero ? uColorZero : colorRamp(t);
|
||||
|
||||
vec3 col = base * (0.35 + 0.65 * diff);
|
||||
col += vec3(1.0) * spec * 0.10;
|
||||
FragColor = vec4(clamp(col, 0.0, 1.0), 1.0);
|
||||
}
|
||||
|
||||
@@ -2,14 +2,17 @@
|
||||
|
||||
layout(location = 0) in vec3 aPos;
|
||||
layout(location = 1) in vec3 aNormal;
|
||||
layout(location = 2) in vec2 aUV;
|
||||
|
||||
out vec3 vNormal;
|
||||
out vec3 vWorldPos;
|
||||
out vec2 vUV;
|
||||
|
||||
uniform mat4 uMVP;
|
||||
|
||||
void main() {
|
||||
vNormal = aNormal;
|
||||
vWorldPos = aPos;
|
||||
vUV = aUV;
|
||||
gl_Position = uMVP * vec4(aPos, 1.0);
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ out vec4 FragColor;
|
||||
|
||||
uniform float uMinV;
|
||||
uniform float uMaxV;
|
||||
uniform vec3 uColorZero;
|
||||
uniform vec3 uColorLow;
|
||||
uniform vec3 uColorMid;
|
||||
uniform vec3 uColorHigh;
|
||||
@@ -112,7 +113,8 @@ void main() {
|
||||
// data color directly (no remaining gold tint), while preserving depth cues.
|
||||
vec3 metalBase = vec3(0.98, 0.82, 0.30);
|
||||
float value01 = clamp((vValue - uMinV) / max(1e-6, (uMaxV - uMinV)), 0.0, 1.0);
|
||||
vec3 dataCol = dataColorRamp(value01);
|
||||
bool isZero = abs(vValue) <= 1e-6;
|
||||
vec3 dataCol = isZero ? uColorZero : dataColorRamp(value01);
|
||||
|
||||
bool hasData = (uHasData != 0);
|
||||
vec3 baseColor = hasData ? dataCol : metalBase;
|
||||
|
||||
@@ -12,6 +12,7 @@ uniform vec2 uTexelSize;
|
||||
uniform vec2 uPlaneSize;
|
||||
uniform vec3 uCameraPos;
|
||||
uniform vec3 uLightDir;
|
||||
uniform vec3 uColorZero;
|
||||
uniform vec3 uColorLow;
|
||||
uniform vec3 uColorMid;
|
||||
uniform vec3 uColorHigh;
|
||||
@@ -22,6 +23,24 @@ float value01(float v) {
|
||||
return saturate((v - uMinV) / max(uMaxV - uMinV, 1e-6));
|
||||
}
|
||||
|
||||
float maxNeighborValue(vec2 uv) {
|
||||
vec2 texSizeF = 1.0 / uTexelSize;
|
||||
ivec2 texSizeI = ivec2(max(vec2(1.0), floor(texSizeF + 0.5)));
|
||||
ivec2 maxCoord = texSizeI - ivec2(1);
|
||||
|
||||
vec2 texelF = clamp(uv * texSizeF, vec2(0.0), vec2(maxCoord));
|
||||
ivec2 base = ivec2(floor(texelF));
|
||||
ivec2 base10 = min(base + ivec2(1, 0), maxCoord);
|
||||
ivec2 base01 = min(base + ivec2(0, 1), maxCoord);
|
||||
ivec2 base11 = min(base + ivec2(1, 1), maxCoord);
|
||||
|
||||
float v00 = texelFetch(uHeightTex, base, 0).r;
|
||||
float v10 = texelFetch(uHeightTex, base10, 0).r;
|
||||
float v01 = texelFetch(uHeightTex, base01, 0).r;
|
||||
float v11 = texelFetch(uHeightTex, base11, 0).r;
|
||||
return max(max(v00, v10), max(v01, v11));
|
||||
}
|
||||
|
||||
vec3 colorRamp(float t) {
|
||||
if (t < 0.5) {
|
||||
return mix(uColorLow, uColorMid, t / 0.5);
|
||||
@@ -31,8 +50,13 @@ vec3 colorRamp(float t) {
|
||||
|
||||
void main() {
|
||||
float vC = texture(uHeightTex, vUV).r;
|
||||
bool isZero = abs(vC) <= 1e-6;
|
||||
float t = value01(vC);
|
||||
vec3 base = colorRamp(t);
|
||||
float m = maxNeighborValue(vUV);
|
||||
float eps = max(1e-4, 0.001 * (uMaxV - uMinV));
|
||||
float force = smoothstep(uMaxV - eps, uMaxV, m);
|
||||
t = max(t, force);
|
||||
vec3 base = isZero ? uColorZero : colorRamp(t);
|
||||
|
||||
float vL = texture(uHeightTex, vUV - vec2(uTexelSize.x, 0.0)).r;
|
||||
float vR = texture(uHeightTex, vUV + vec2(uTexelSize.x, 0.0)).r;
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include "backend.h"
|
||||
#include "data_backend.h"
|
||||
#include "serial/serial_backend.h"
|
||||
#include <qcolor.h>
|
||||
#include <qnumeric.h>
|
||||
|
||||
AppBackend::AppBackend(QObject* parent)
|
||||
@@ -51,6 +52,13 @@ void AppBackend::setShowGrid(bool on) {
|
||||
emit showGridChanged(on);
|
||||
}
|
||||
|
||||
void AppBackend::setUseHeatmap(bool on) {
|
||||
if (m_useHeatmap == on)
|
||||
return;
|
||||
m_useHeatmap = on;
|
||||
emit useHeatmapChanged(on);
|
||||
}
|
||||
|
||||
void AppBackend::setSensorCol(int c) {
|
||||
if (m_serial->connected()) {
|
||||
return;
|
||||
@@ -93,6 +101,13 @@ void AppBackend::setRangeMax(int v) {
|
||||
emit rangeChanged(m_rangeMin, m_rangeMax);
|
||||
}
|
||||
|
||||
void AppBackend::setColorZero(const QColor& color) {
|
||||
if (m_colorZero == color)
|
||||
return;
|
||||
m_colorZero = color;
|
||||
emit colorZeroChanged(m_colorZero);
|
||||
}
|
||||
|
||||
void AppBackend::setColorLow(const QColor& color) {
|
||||
if (m_colorLow == color)
|
||||
return;
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
#include <QObject>
|
||||
#include <QString>
|
||||
#include <QColor>
|
||||
#include <qcolor.h>
|
||||
#include <qtmetamacros.h>
|
||||
|
||||
#include "data_backend.h"
|
||||
@@ -18,12 +19,14 @@ class AppBackend : public QObject {
|
||||
Q_PROPERTY(QString language READ language WRITE setLanguage NOTIFY languageChanged)
|
||||
Q_PROPERTY(bool connected READ connected NOTIFY connectedChanged)
|
||||
Q_PROPERTY(bool showGrid READ showGrid WRITE setShowGrid NOTIFY showGridChanged);
|
||||
Q_PROPERTY(bool useHeatmap READ useHeatmap WRITE setUseHeatmap NOTIFY useHeatmapChanged);
|
||||
Q_PROPERTY(SerialBackend* serial READ serial CONSTANT)
|
||||
Q_PROPERTY(DataBackend* data READ data CONSTANT)
|
||||
Q_PROPERTY(int sensorCol READ sensorCol WRITE setSensorCol NOTIFY sensorColChanged);
|
||||
Q_PROPERTY(int sensorRow READ sensorRow WRITE setSensorRow NOTIFY sensorRowChanged);
|
||||
Q_PROPERTY(int rangeMin READ rangeMin WRITE setRangeMin NOTIFY rangeMinChanged);
|
||||
Q_PROPERTY(int rangeMax READ rangeMax WRITE setRangeMax NOTIFY rangeMaxChanged);
|
||||
Q_PROPERTY(QColor colorZero READ colorZero WRITE setColorZero NOTIFY colorZeroChanged);
|
||||
Q_PROPERTY(QColor colorLow READ colorLow WRITE setColorLow NOTIFY colorLowChanged);
|
||||
Q_PROPERTY(QColor colorMid READ colorMid WRITE setColorMid NOTIFY colorMidChanged);
|
||||
Q_PROPERTY(QColor colorHigh READ colorHigh WRITE setColorHigh NOTIFY colorHighChanged);
|
||||
@@ -43,6 +46,8 @@ public:
|
||||
|
||||
bool showGrid() const { return m_showGrid; }
|
||||
void setShowGrid(bool on);
|
||||
bool useHeatmap() const { return m_useHeatmap; }
|
||||
void setUseHeatmap(bool on);
|
||||
int sensorCol() const { qInfo() << "col:" << m_sensorCol; return m_sensorCol; }
|
||||
int sensorRow() const { qInfo() << "row:" << m_sensorRow; return m_sensorRow; }
|
||||
void setSensorRow(int r);
|
||||
@@ -51,9 +56,11 @@ public:
|
||||
int rangeMax() const { return m_rangeMax; }
|
||||
void setRangeMin(int v);
|
||||
void setRangeMax(int v);
|
||||
QColor colorZero() const { return m_colorZero; }
|
||||
QColor colorLow() const { return m_colorLow; }
|
||||
QColor colorMid() const { return m_colorMid; }
|
||||
QColor colorHigh() const { return m_colorHigh; }
|
||||
void setColorZero(const QColor& color);
|
||||
void setColorLow(const QColor& color);
|
||||
void setColorMid(const QColor& color);
|
||||
void setColorHigh(const QColor& color);
|
||||
@@ -63,11 +70,13 @@ signals:
|
||||
void languageChanged();
|
||||
void connectedChanged();
|
||||
void showGridChanged(bool on);
|
||||
void useHeatmapChanged(bool on);
|
||||
void sensorColChanged(int c);
|
||||
void sensorRowChanged(int r);
|
||||
void rangeMinChanged(int v);
|
||||
void rangeMaxChanged(int v);
|
||||
void rangeChanged(int minV, int maxV);
|
||||
void colorZeroChanged(const QColor& color);
|
||||
void colorLowChanged(const QColor& color);
|
||||
void colorMidChanged(const QColor& color);
|
||||
void colorHighChanged(const QColor& color);
|
||||
@@ -78,10 +87,12 @@ private:
|
||||
QString m_language = QStringLiteral("zh_CN");
|
||||
|
||||
bool m_showGrid = true;
|
||||
bool m_useHeatmap = false;
|
||||
int m_sensorRow = 12;
|
||||
int m_sensorCol = 7;
|
||||
int m_rangeMin = 0;
|
||||
int m_rangeMax = 1000;
|
||||
QColor m_colorZero = QColor::fromRgbF(0.10, 0.75, 1.00);
|
||||
QColor m_colorLow = QColor::fromRgbF(0.10, 0.75, 1.00);
|
||||
QColor m_colorMid = QColor::fromRgbF(0.10, 0.95, 0.35);
|
||||
QColor m_colorHigh = QColor::fromRgbF(1.00, 0.22, 0.10);
|
||||
|
||||
@@ -127,6 +127,8 @@ bool DataBackend::importXlsx(const QString& path) {
|
||||
cells[r][c] = v;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void DataBackend::startPlayback(int intervalMs) {
|
||||
|
||||
@@ -5,48 +5,217 @@
|
||||
#ifndef TACTILEIPC3D_GLOBALHELPER_H
|
||||
#define TACTILEIPC3D_GLOBALHELPER_H
|
||||
|
||||
#include <algorithm>
|
||||
#include <opencv2/core.hpp>
|
||||
#include <opencv2/imgproc.hpp>
|
||||
#include <algorithm>
|
||||
#include <QCoreApplication>
|
||||
#include <QDir>
|
||||
#include <qset.h>
|
||||
#include <qsettings.h>
|
||||
|
||||
const QString APP_VERSION = "0.4.0";
|
||||
|
||||
class GlobalHelper {
|
||||
public:
|
||||
const std::string fuck = "fuck you lsp!!!";
|
||||
|
||||
public :
|
||||
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_matrix) {
|
||||
float max_range, cv::Size display_res,
|
||||
cv::Mat &out_matrix) {
|
||||
CV_Assert(raw_data.type() == CV_32F);
|
||||
|
||||
const float safe_max = std::max(max_range, 1e-6f);
|
||||
const float safe_span = std::max(safe_max - min_threshold, 1e-6f);
|
||||
|
||||
cv::Mat saturate_mask_u8;
|
||||
cv::compare(raw_data, max_range, saturate_mask_u8, cv::CMP_GE);
|
||||
cv::Mat zero_mask_u8;
|
||||
cv::compare(raw_data, 0.0f, zero_mask_u8, cv::CMP_LE);
|
||||
|
||||
cv::Mat raw = raw_data.clone();
|
||||
raw.setTo(0.0f, raw < min_threshold);
|
||||
cv::min(raw, safe_max, raw);
|
||||
|
||||
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 peak_norm = (raw - min_threshold) / safe_span;
|
||||
cv::max(peak_norm, 0.0f, peak_norm);
|
||||
cv::min(peak_norm, 1.0f, peak_norm);
|
||||
|
||||
const float safe_range = std::max(max_range, 1e-6f);
|
||||
cv::Mat norm_data = raw / safe_range;
|
||||
cv::min(norm_data, 1.0f, norm_data);
|
||||
cv::medianBlur(raw, raw, 3);
|
||||
cv::GaussianBlur(raw, raw, cv::Size(5, 5), 1.2, 1.2, cv::BORDER_DEFAULT);
|
||||
|
||||
cv::Mat norm_data = (raw - min_threshold) / safe_span;
|
||||
cv::max(norm_data, 0.0f, norm_data);
|
||||
cv::min(norm_data, 1.0f, norm_data);
|
||||
|
||||
cv::pow(norm_data, 0.7, norm_data);
|
||||
cv::pow(norm_data, 1.4, norm_data);
|
||||
|
||||
cv::Mat smoothed;
|
||||
cv::resize(norm_data, smoothed, display_res, 0.0, 0.0, cv::INTER_CUBIC);
|
||||
cv::max(smoothed, 0.0f, smoothed);
|
||||
cv::min(smoothed, 1.0f, smoothed);
|
||||
|
||||
cv::GaussianBlur(smoothed, smoothed, cv::Size(31, 31), 0.0, 0.0, cv::BORDER_DEFAULT);
|
||||
const int min_dim =
|
||||
std::max(1, std::min(display_res.width, display_res.height));
|
||||
const double sigma = std::max(1.0, 0.02 * min_dim);
|
||||
int ksize = std::max(3, (static_cast<int>(std::ceil(sigma * 6)) | 1));
|
||||
cv::GaussianBlur(smoothed, smoothed, cv::Size(ksize, ksize), sigma, sigma,
|
||||
cv::BORDER_DEFAULT);
|
||||
cv::max(smoothed, 0.0f, smoothed);
|
||||
cv::min(smoothed, 1.0f, smoothed);
|
||||
|
||||
smoothed *= safe_range;
|
||||
cv::Mat peak_resized;
|
||||
cv::resize(peak_norm, peak_resized, display_res, 0.0, 0.0,
|
||||
cv::INTER_CUBIC);
|
||||
cv::max(smoothed, peak_resized, smoothed);
|
||||
|
||||
if (!saturate_mask_u8.empty()) {
|
||||
cv::Mat mask_f;
|
||||
saturate_mask_u8.convertTo(mask_f, CV_32F, 1.0 / 255.0);
|
||||
cv::Mat mask_resized;
|
||||
cv::resize(mask_f, mask_resized, display_res, 0.0, 0.0,
|
||||
cv::INTER_CUBIC);
|
||||
// Soften the threshold to avoid blocky plateaus.
|
||||
const double mask_sigma = std::max(0.5, 0.008 * min_dim);
|
||||
int mask_ksize =
|
||||
std::max(3, (static_cast<int>(std::ceil(mask_sigma * 6)) | 1));
|
||||
cv::GaussianBlur(mask_resized, mask_resized,
|
||||
cv::Size(mask_ksize, mask_ksize), mask_sigma,
|
||||
mask_sigma, cv::BORDER_DEFAULT);
|
||||
cv::max(mask_resized, 0.0f, mask_resized);
|
||||
cv::min(mask_resized, 1.0f, mask_resized);
|
||||
smoothed = cv::max(smoothed, mask_resized);
|
||||
}
|
||||
|
||||
if (!zero_mask_u8.empty()) {
|
||||
cv::Mat zero_f;
|
||||
zero_mask_u8.convertTo(zero_f, CV_32F, 1.0 / 255.0);
|
||||
cv::Mat zero_resized;
|
||||
cv::resize(zero_f, zero_resized, display_res, 0.0, 0.0,
|
||||
cv::INTER_CUBIC);
|
||||
const double zero_sigma = std::max(0.5, 0.006 * min_dim);
|
||||
int zero_ksize =
|
||||
std::max(3, (static_cast<int>(std::ceil(zero_sigma * 6)) | 1));
|
||||
cv::GaussianBlur(zero_resized, zero_resized,
|
||||
cv::Size(zero_ksize, zero_ksize), zero_sigma,
|
||||
zero_sigma, cv::BORDER_DEFAULT);
|
||||
cv::max(zero_resized, 0.0f, zero_resized);
|
||||
cv::min(zero_resized, 1.0f, zero_resized);
|
||||
cv::Mat zero_bin;
|
||||
cv::threshold(zero_resized, zero_bin, 0.5, 255.0,
|
||||
cv::THRESH_BINARY);
|
||||
zero_bin.convertTo(zero_bin, CV_8U);
|
||||
smoothed.setTo(0.0f, zero_bin);
|
||||
}
|
||||
|
||||
smoothed *= safe_max;
|
||||
out_matrix = smoothed;
|
||||
}
|
||||
|
||||
static void SaveGridConfig(bool b) {
|
||||
QSettings settings(GlobalHelper::GetConfigFilePath(), QSettings::IniFormat);
|
||||
settings.setValue("display/grid", b);
|
||||
}
|
||||
|
||||
static void SaveAxisConfig(bool b) {
|
||||
QSettings settings(GlobalHelper::GetConfigFilePath(), QSettings::IniFormat);
|
||||
settings.setValue("display/axis", b);
|
||||
}
|
||||
|
||||
static void SaveSurfaceConfig(bool b) {
|
||||
QSettings settings(GlobalHelper::GetConfigFilePath(), QSettings::IniFormat);
|
||||
settings.setValue("display/surface", b);
|
||||
}
|
||||
|
||||
static void GetGridConfig(bool& b) {
|
||||
QSettings settings(GlobalHelper::GetConfigFilePath(), QSettings::IniFormat);
|
||||
b = settings.value("display/grid", b).toBool();
|
||||
}
|
||||
|
||||
static void GetAxisConfig(bool& b) {
|
||||
QSettings settings(GlobalHelper::GetConfigFilePath(), QSettings::IniFormat);
|
||||
b = settings.value("display/axis", b).toBool();
|
||||
}
|
||||
|
||||
static void GetSurfaceConfig(bool b) {
|
||||
QSettings settings(GlobalHelper::GetConfigFilePath(), QSettings::IniFormat);
|
||||
b = settings.value("display/surface", b).toBool();
|
||||
}
|
||||
|
||||
static void SaveLastRow(int value) {
|
||||
QSettings settings(GlobalHelper::GetConfigFilePath(), QSettings::IniFormat);
|
||||
settings.setValue("spec/row", value);
|
||||
}
|
||||
|
||||
static void GetLastRow(int& value) {
|
||||
QSettings settings(GlobalHelper::GetConfigFilePath(), QSettings::IniFormat);
|
||||
value = settings.value("spec/row", value).toInt();
|
||||
}
|
||||
|
||||
static void GetLastCol(int value) {
|
||||
QSettings settings(GlobalHelper::GetConfigFilePath(), QSettings::IniFormat);
|
||||
settings.setValue("spec/col", value);
|
||||
}
|
||||
|
||||
static void GetLastCol(int& value) {
|
||||
QSettings settings(GlobalHelper::GetConfigFilePath(), QSettings::IniFormat);
|
||||
value = settings.value("spec/col", value).toInt();
|
||||
}
|
||||
|
||||
static void SaveZeroColor(QString color) {
|
||||
QSettings settings(GlobalHelper::GetConfigFilePath(), QSettings::IniFormat);
|
||||
settings.setValue("color/zero", color);
|
||||
}
|
||||
|
||||
static void GetZeroColor(QString& color) {
|
||||
QSettings settings(GlobalHelper::GetConfigFilePath(), QSettings::IniFormat);
|
||||
color = settings.value("color/zero", color).toString();
|
||||
}
|
||||
|
||||
static void SaveLowColor(QString color) {
|
||||
QSettings settings(GlobalHelper::GetConfigFilePath(), QSettings::IniFormat);
|
||||
settings.setValue("color/low", color);
|
||||
}
|
||||
|
||||
static void GetLowColor(QString& color) {
|
||||
QSettings settings(GlobalHelper::GetConfigFilePath(), QSettings::IniFormat);
|
||||
color = settings.value("color/low", color).toString();
|
||||
}
|
||||
|
||||
static void SaveMidColor(QString color) {
|
||||
QSettings settings(GlobalHelper::GetConfigFilePath(), QSettings::IniFormat);
|
||||
settings.setValue("color/mid", color);
|
||||
}
|
||||
|
||||
static void GetMidColor(QString& color) {
|
||||
QSettings settings(GlobalHelper::GetConfigFilePath(), QSettings::IniFormat);
|
||||
color = settings.value("color/mid", color).toString();
|
||||
}
|
||||
|
||||
static void SaveHighColor(QString color) {
|
||||
QSettings settings(GlobalHelper::GetConfigFilePath(), QSettings::IniFormat);
|
||||
settings.setValue("color/high", color);
|
||||
}
|
||||
|
||||
static void GetHighColor(QString& color) {
|
||||
QSettings settings(GlobalHelper::GetConfigFilePath(), QSettings::IniFormat);
|
||||
color = settings.value("color/high", color).toString();
|
||||
}
|
||||
|
||||
static QString GetConfigFilePath() {
|
||||
return QCoreApplication::applicationDirPath() + QDir::separator() + "conf.ini";
|
||||
}
|
||||
|
||||
static QString GetAppVersion() {
|
||||
return APP_VERSION;
|
||||
}
|
||||
|
||||
private:
|
||||
GlobalHelper() {}
|
||||
GlobalHelper() {
|
||||
}
|
||||
};
|
||||
|
||||
#endif // TACTILEIPC3D_GLOBALHELPER_H
|
||||
|
||||
543
src/glwidget.cpp
543
src/glwidget.cpp
File diff suppressed because it is too large
Load Diff
@@ -1,9 +1,11 @@
|
||||
#include "serial_backend.h"
|
||||
|
||||
#include "piezoresistive_a_protocol.h"
|
||||
#include "serial_qt_transport.h"
|
||||
|
||||
#include <QDebug>
|
||||
#include <QCoreApplication>
|
||||
#include <QDir>
|
||||
#include <QStringList>
|
||||
#include <QMetaObject>
|
||||
#include <QtGlobal>
|
||||
#include <qcontainerfwd.h>
|
||||
@@ -14,13 +16,20 @@ SerialBackend::SerialBackend(QObject *parent)
|
||||
: 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");
|
||||
|
||||
auto codec = std::make_shared<PiezoresistiveACodec>();
|
||||
auto decoder = std::make_shared<PiezoresistiveADecoder>();
|
||||
auto format = std::make_shared<PiezoresistiveAFormat>();
|
||||
m_manager.registerProtocol(codec->name(), {codec, decoder, format});
|
||||
m_manager.setActiveProtocol(codec->name());
|
||||
const QString pluginDir = QDir(QCoreApplication::applicationDirPath())
|
||||
.filePath(QStringLiteral("plugins/decoders"));
|
||||
QStringList errors;
|
||||
const QStringList loaded = m_manager.loadPlugins(pluginDir, &errors);
|
||||
for (const QString& err : errors) {
|
||||
qWarning().noquote() << err;
|
||||
}
|
||||
if (!loaded.isEmpty()) {
|
||||
m_spec.model = m_manager.activeProtocol();
|
||||
} else {
|
||||
qWarning() << "No protocol plugins loaded.";
|
||||
m_spec.model = QStringLiteral("UNKNOWN");
|
||||
}
|
||||
|
||||
m_sendWorker = new SerialSendWorker();
|
||||
m_sendWorker->moveToThread(&m_sendThread);
|
||||
@@ -175,6 +184,10 @@ void SerialBackend::setProtocol(const QString &name)
|
||||
if (!m_manager.setActiveProtocol(name))
|
||||
return;
|
||||
updateProtocolBindings_();
|
||||
if (m_spec.model != name) {
|
||||
m_spec.model = name;
|
||||
emit sensorModelChanged();
|
||||
}
|
||||
emit protocolChanged();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
#include "serial_manager.h"
|
||||
#include "protocol_plugin.h"
|
||||
|
||||
#include <QDir>
|
||||
#include <QPluginLoader>
|
||||
|
||||
void SerialManager::registerProtocol(const QString& name, const ProtocolBundle& bundle) {
|
||||
if (name.isEmpty())
|
||||
@@ -15,8 +19,76 @@ bool SerialManager::setActiveProtocol(const QString& name) {
|
||||
return true;
|
||||
}
|
||||
|
||||
SerialManager::ProtocolBundle SerialManager::activeBundle() const {
|
||||
ProtocolBundle SerialManager::activeBundle() const {
|
||||
if (!m_protocols.contains(m_activeName))
|
||||
return {};
|
||||
return m_protocols.value(m_activeName);
|
||||
}
|
||||
|
||||
QStringList SerialManager::loadPlugins(const QString& dirPath, QStringList* errors) {
|
||||
QStringList loaded;
|
||||
if (!m_pluginLoaders.empty()) {
|
||||
return loaded;
|
||||
}
|
||||
|
||||
QDir dir(dirPath);
|
||||
if (!dir.exists()) {
|
||||
if (errors)
|
||||
errors->append(QStringLiteral("Plugin dir not found: %1").arg(dirPath));
|
||||
return loaded;
|
||||
}
|
||||
|
||||
QStringList filters;
|
||||
#if defined(Q_OS_WIN)
|
||||
filters << QStringLiteral("*.dll");
|
||||
#elif defined(Q_OS_MAC)
|
||||
filters << QStringLiteral("*.dylib");
|
||||
#else
|
||||
filters << QStringLiteral("*.so");
|
||||
#endif
|
||||
|
||||
const QStringList entries = dir.entryList(filters, QDir::Files);
|
||||
for (const QString& fileName : entries) {
|
||||
const QString path = dir.absoluteFilePath(fileName);
|
||||
auto loader = std::make_unique<QPluginLoader>(path);
|
||||
QObject* instance = loader->instance();
|
||||
if (!instance) {
|
||||
if (errors)
|
||||
errors->append(QStringLiteral("Load failed: %1 (%2)").arg(path, loader->errorString()));
|
||||
continue;
|
||||
}
|
||||
|
||||
auto* plugin = qobject_cast<IProtocolPlugin*>(instance);
|
||||
if (!plugin) {
|
||||
if (errors)
|
||||
errors->append(QStringLiteral("Invalid plugin: %1").arg(path));
|
||||
loader->unload();
|
||||
continue;
|
||||
}
|
||||
if (plugin->apiVersion() != kProtocolPluginApiVersion) {
|
||||
if (errors)
|
||||
errors->append(QStringLiteral("API mismatch: %1").arg(path));
|
||||
loader->unload();
|
||||
continue;
|
||||
}
|
||||
|
||||
const ProtocolBundle bundle = plugin->createBundle();
|
||||
if (!bundle.codec || !bundle.decoder || !bundle.format) {
|
||||
if (errors)
|
||||
errors->append(QStringLiteral("Missing components: %1").arg(path));
|
||||
loader->unload();
|
||||
continue;
|
||||
}
|
||||
|
||||
const QString name = plugin->protocolName();
|
||||
if (!m_protocols.contains(name)) {
|
||||
registerProtocol(name, bundle);
|
||||
loaded.append(name);
|
||||
m_pluginLoaders.push_back(std::move(loader));
|
||||
} else {
|
||||
loader->unload();
|
||||
}
|
||||
}
|
||||
|
||||
return loaded;
|
||||
}
|
||||
|
||||
@@ -3,22 +3,18 @@
|
||||
|
||||
#include <QHash>
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
#include <QPluginLoader>
|
||||
#include <vector>
|
||||
#include <memory>
|
||||
|
||||
#include "serial_codec.h"
|
||||
#include "serial_decoder.h"
|
||||
#include "serial_format.h"
|
||||
#include "protocol_bundle.h"
|
||||
|
||||
class SerialManager {
|
||||
public:
|
||||
struct ProtocolBundle {
|
||||
std::shared_ptr<ISerialCodec> codec;
|
||||
std::shared_ptr<ISerialDecoder> decoder;
|
||||
std::shared_ptr<ISerialFormat> format;
|
||||
};
|
||||
|
||||
void registerProtocol(const QString& name, const ProtocolBundle& bundle);
|
||||
bool setActiveProtocol(const QString& name);
|
||||
QStringList loadPlugins(const QString& dirPath, QStringList* errors = nullptr);
|
||||
|
||||
QString activeProtocol() const { return m_activeName; }
|
||||
ProtocolBundle activeBundle() const;
|
||||
@@ -26,6 +22,7 @@ public:
|
||||
private:
|
||||
QHash<QString, ProtocolBundle> m_protocols;
|
||||
QString m_activeName;
|
||||
std::vector<std::unique_ptr<QPluginLoader>> m_pluginLoaders;
|
||||
};
|
||||
|
||||
#endif // TACTILEIPC3D_SERIAL_MANAGER_H
|
||||
|
||||
Reference in New Issue
Block a user