完成主要交互、高性能组件、国际化和A型传感器数据包接收

This commit is contained in:
2026-01-13 16:34:28 +08:00
parent 47e6dc7244
commit 1960e6a5b9
84 changed files with 7752 additions and 332 deletions

View File

@@ -3,62 +3,48 @@
//
#include "backend.h"
#include "data_backend.h"
#include "serial/serial_backend.h"
#include <qnumeric.h>
static QString normalizeRenderMode_(const QString& mode) {
if (mode.compare(QStringLiteral("realistic"), Qt::CaseInsensitive) == 0)
return QStringLiteral("realistic");
return QStringLiteral("dataViz");
AppBackend::AppBackend(QObject* parent)
: QObject(parent)
, m_serial(new SerialBackend(this))
, m_data(new DataBackend(this)) {
m_serial->setFrameCallback([this](const DataFrame& frame) {
m_data->ingestFrame(frame);
});
connect(m_serial, &SerialBackend::connectedChanged, this, [this]() {
if (m_serial->connected())
m_data->clear();
emit connectedChanged();
});
}
static QString normalizeLabelMode_(const QString& mode) {
if (mode.compare(QStringLiteral("hover"), Qt::CaseInsensitive) == 0)
return QStringLiteral("hover");
if (mode.compare(QStringLiteral("always"), Qt::CaseInsensitive) == 0)
return QStringLiteral("always");
return QStringLiteral("off");
bool AppBackend::connected() const {
return m_serial && m_serial->connected();
}
Backend::Backend(QObject *parent)
: QObject(parent) {
}
void Backend::setMinValue(int v) {
if (m_min == v)
void AppBackend::setLanguage(const QString& lang) {
if (m_language == lang)
return;
m_min = v;
emit minValueChanged();
emit rangeChanged(m_min, m_max);
m_language = lang;
emit languageChanged();
}
void Backend::setMaxValue(int v) {
if (m_max == v)
void AppBackend::setLightMode(bool on) {
if (m_lightMode == on)
return;
m_max = v;
emit maxValueChanged();
emit rangeChanged(m_min, m_max);
m_lightMode = on;
emit lightModeChanged();
}
void Backend::setRenderMode(const QString &mode) {
const QString norm = normalizeRenderMode_(mode);
if (m_renderMode == norm)
void AppBackend::setShowGrid(bool on) {
qInfo() << "setShowGrid:" << on;
if (m_showGrid == on)
return;
m_renderMode = norm;
emit renderModeChanged();
emit renderModeValueChanged(m_renderMode);
}
void Backend::setShowLegend(bool show) {
if (m_showLegend == show)
return;
m_showLegend = show;
emit showLegendChanged();
}
void Backend::setLabelMode(const QString &mode) {
const QString norm = normalizeLabelMode_(mode);
if (m_labelMode == norm)
return;
m_labelMode = norm;
emit labelModeChanged();
emit labelModeValueChanged(m_labelMode);
}
m_showGrid = on;
emit showGridChanged(on);
}

View File

@@ -4,51 +4,50 @@
#ifndef TACTILEIPC3D_BACKEND_H
#define TACTILEIPC3D_BACKEND_H
#include <qobject.h>
#include <QObject>
#include <QString>
#include <qtmetamacros.h>
#include "data_backend.h"
#include "serial/serial_backend.h"
class Backend : public QObject {
class AppBackend : public QObject {
Q_OBJECT
Q_PROPERTY(int minValue READ minValue WRITE setMinValue NOTIFY minValueChanged)
Q_PROPERTY(int maxValue READ maxValue WRITE setMaxValue NOTIFY maxValueChanged)
Q_PROPERTY(QString renderMode READ renderMode WRITE setRenderMode NOTIFY renderModeChanged)
Q_PROPERTY(bool showLegend READ showLegend WRITE setShowLegend NOTIFY showLegendChanged)
Q_PROPERTY(QString labelMode READ labelMode WRITE setLabelMode NOTIFY labelModeChanged)
Q_PROPERTY(bool lightMode READ lightMode WRITE setLightMode NOTIFY lightModeChanged)
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(SerialBackend* serial READ serial CONSTANT)
Q_PROPERTY(DataBackend* data READ data CONSTANT)
public:
explicit Backend(QObject* parent = nullptr);
explicit AppBackend(QObject* parent=nullptr);
int minValue() const { return m_min; }
int maxValue() const { return m_max; }
QString renderMode() const { return m_renderMode; }
bool showLegend() const { return m_showLegend; }
QString labelMode() const { return m_labelMode; }
bool lightMode() const { return m_lightMode; }
QString language() const { return m_language; }
bool connected() const;
public slots:
void setMinValue(int v);
void setMaxValue(int v);
void setRenderMode(const QString& mode);
void setShowLegend(bool show);
void setLabelMode(const QString& mode);
SerialBackend* serial() const { return m_serial; }
DataBackend* data() const { return m_data; }
void setLanguage(const QString& lang);
void setLightMode(bool on);
bool showGrid() const { return m_showGrid; }
void setShowGrid(bool on);
signals:
void minValueChanged();
void maxValueChanged();
void renderModeChanged();
void showLegendChanged();
void labelModeChanged();
void rangeChanged(int minV, int maxV);
void renderModeValueChanged(const QString& mode);
void labelModeValueChanged(const QString& mode);
void lightModeChanged();
void languageChanged();
void connectedChanged();
void showGridChanged(bool on);
private:
int m_min = 100;
int m_max = 2000;
QString m_renderMode = QStringLiteral("dataViz");
bool m_showLegend = true;
QString m_labelMode = QStringLiteral("off");
SerialBackend* m_serial = nullptr;
DataBackend* m_data = nullptr;
bool m_lightMode = true;
QString m_language = QStringLiteral("zh_CN");
bool m_showGrid = true;
};
#endif //TACTILEIPC3D_BACKEND_H

338
src/data_backend.cpp Normal file
View File

@@ -0,0 +1,338 @@
#include "data_backend.h"
#include <QFile>
#include <QIODevice>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QTextStream>
#include <QDir>
#include <cmath>
#include <qtpreprocessorsupport.h>
#include <qurl.h>
DataBackend::DataBackend(QObject* parent)
: QObject(parent) {
m_playbackTimer.setTimerType(Qt::PreciseTimer);
connect(&m_playbackTimer, &QTimer::timeout, this, [this]() {
if (m_playbackIndex >= m_frames.size()) {
stopPlayback();
return;
}
const RenderCallback& cb = m_playbackCallback ? m_playbackCallback : m_liveCallback;
emitFrame_(m_frames[m_playbackIndex], cb);
m_playbackIndex++;
});
seedDebugFrames_();
}
void DataBackend::ingestFrame(const DataFrame& frame) {
m_frames.push_back(frame);
emit frameCountChanged();
emitFrame_(frame, m_liveCallback);
}
void DataBackend::clear() {
stopPlayback();
m_frames.clear();
m_playbackIndex = 0;
updateMetrics_(DataFrame());
emit frameCountChanged();
}
bool DataBackend::exportJson(const QString& path) const {
QFile file(path);
if (!file.open(QIODevice::WriteOnly))
return false;
file.write(buildJson_());
return true;
}
bool DataBackend::exportCsv(const QString& path) const {
QFile file(path);
if (!file.open(QIODevice::WriteOnly | QIODevice::Text))
return false;
file.write(buildCsv_());
return true;
}
bool DataBackend::importJson(const QString& path) {
QFile file(path);
if (!file.open(QIODevice::ReadOnly))
return false;
return loadJson_(file.readAll());
}
bool DataBackend::importCsv(const QString& path) {
QFile file(path);
if (!file.open(QIODevice::ReadOnly | QIODevice::Text))
return false;
return loadCsv_(file.readAll());
}
void DataBackend::startPlayback(int intervalMs) {
if (m_frames.isEmpty())
return;
if (intervalMs < 1)
intervalMs = 1;
m_playbackIndex = 0;
m_playbackRunning = true;
emit playbackRunningChanged();
m_playbackTimer.start(intervalMs);
}
void DataBackend::stopPlayback() {
if (!m_playbackRunning)
return;
m_playbackRunning = false;
emit playbackRunningChanged();
m_playbackTimer.stop();
}
void DataBackend::exportHandler(const QUrl& folder, const QString& filename,
const QString& format, const QString& method) {
const QString trimmedName = filename.trimmed();
if (trimmedName.isEmpty()) {
qWarning().noquote() << "Export failed: filename is empty.";
return;
}
QString folderPath = folder.isLocalFile() ? folder.toLocalFile() : folder.toString();
if (folderPath.startsWith(QStringLiteral("file://")))
folderPath = QUrl(folderPath).toLocalFile();
if (folderPath.isEmpty()) {
qWarning().noquote() << "Export failed: folder is empty.";
return;
}
const QDir dir(folderPath);
if (!dir.exists()) {
qWarning().noquote() << "Export failed: folder does not exist:" << folderPath;
return;
}
const QString filePath = dir.filePath(trimmedName);
QString fmt = format.trimmed().toLower();
if (fmt.isEmpty()) {
if (trimmedName.endsWith(QStringLiteral(".csv"), Qt::CaseInsensitive))
fmt = QStringLiteral("csv");
else if (trimmedName.endsWith(QStringLiteral(".json"), Qt::CaseInsensitive))
fmt = QStringLiteral("json");
else if (trimmedName.endsWith(QStringLiteral(".xlsx"), Qt::CaseInsensitive))
fmt = QStringLiteral("xlsx");
}
QString mode = method.trimmed().toLower();
if (mode.isEmpty())
mode = QStringLiteral("overwrite");
if (fmt == QStringLiteral("csv")) {
if (mode == QStringLiteral("append")) {
QFile file(filePath);
if (!file.open(QIODevice::WriteOnly | QIODevice::Append | QIODevice::Text)) {
qWarning().noquote() << "Export CSV append failed:" << file.errorString();
return;
}
file.write(buildCsv_());
return;
}
if (!exportCsv(filePath))
qWarning().noquote() << "Export CSV failed:" << filePath;
return;
}
if (fmt == QStringLiteral("json")) {
if (mode == QStringLiteral("append")) {
QJsonArray arr;
QFile file(filePath);
if (file.exists() && file.open(QIODevice::ReadOnly)) {
const QJsonDocument existing = QJsonDocument::fromJson(file.readAll());
if (existing.isArray())
arr = existing.array();
}
for (const DataFrame& frame : m_frames) {
QJsonObject obj;
obj.insert(QStringLiteral("pts"), frame.pts);
obj.insert(QStringLiteral("func"), frame.functionCode);
QJsonArray dataArr;
for (float v : frame.data)
dataArr.append(static_cast<double>(v));
obj.insert(QStringLiteral("data"), dataArr);
arr.append(obj);
}
QFile out(filePath);
if (!out.open(QIODevice::WriteOnly)) {
qWarning().noquote() << "Export JSON append failed:" << out.errorString();
return;
}
const QJsonDocument doc(arr);
out.write(doc.toJson(QJsonDocument::Indented));
return;
}
if (!exportJson(filePath))
qWarning().noquote() << "Export JSON failed:" << filePath;
return;
}
if (fmt == QStringLiteral("xlsx")) {
qWarning().noquote() << "Export XLSX not supported yet:" << filePath;
return;
}
if (mode == QStringLiteral("zip")) {
qWarning().noquote() << "Export ZIP not supported yet:" << filePath;
return;
}
qWarning().noquote() << "Export failed: unsupported format:" << format;
}
bool DataBackend::loadJson_(const QByteArray& data) {
const QJsonDocument doc = QJsonDocument::fromJson(data);
if (doc.isNull() || !doc.isArray())
return false;
stopPlayback();
clear();
const QJsonArray arr = doc.array();
for (const QJsonValue& val : arr) {
if (!val.isObject())
continue;
const QJsonObject obj = val.toObject();
DataFrame frame;
frame.pts = obj.value(QStringLiteral("pts")).toString();
frame.functionCode = static_cast<quint8>(obj.value(QStringLiteral("func")).toInt());
const QJsonArray dataArr = obj.value(QStringLiteral("data")).toArray();
frame.data.reserve(dataArr.size());
for (const QJsonValue& d : dataArr)
frame.data.push_back(static_cast<float>(d.toDouble()));
m_frames.push_back(frame);
}
emit frameCountChanged();
return true;
}
bool DataBackend::loadCsv_(const QByteArray& data) {
stopPlayback();
clear();
QString text = QString::fromUtf8(data);
QTextStream stream(&text);
while (!stream.atEnd()) {
const QString line = stream.readLine().trimmed();
if (line.isEmpty())
continue;
const QStringList parts = line.split(',', Qt::KeepEmptyParts);
if (parts.size() < 2)
continue;
if (parts[0].trimmed().toLower() == QStringLiteral("pts"))
continue;
DataFrame frame;
frame.pts = parts[0];
frame.functionCode = 0;
for (int i = 1; i < parts.size(); ++i)
frame.data.push_back(parts[i].toFloat());
m_frames.push_back(frame);
}
emit frameCountChanged();
return true;
}
QByteArray DataBackend::buildJson_() const {
QJsonArray arr;
for (const DataFrame& frame : m_frames) {
QJsonObject obj;
obj.insert(QStringLiteral("pts"), frame.pts);
obj.insert(QStringLiteral("func"), frame.functionCode);
QJsonArray dataArr;
for (float v : frame.data)
dataArr.append(static_cast<double>(v));
obj.insert(QStringLiteral("data"), dataArr);
arr.append(obj);
}
const QJsonDocument doc(arr);
return doc.toJson(QJsonDocument::Indented);
}
QByteArray DataBackend::buildCsv_() const {
QByteArray out;
QTextStream stream(&out, QIODevice::WriteOnly);
for (const DataFrame& frame : m_frames) {
stream << frame.pts;
for (float v : frame.data)
stream << ',' << v;
stream << '\n';
}
return out;
}
void DataBackend::seedDebugFrames_() {
if (!m_frames.isEmpty())
return;
constexpr int kFrameCount = 3;
constexpr int kSampleCount = 12;
constexpr int kBase = 100;
const QDateTime start = QDateTime::currentDateTime();
m_frames.reserve(kFrameCount);
for (int f = 0; f < kFrameCount; ++f) {
DataFrame frame;
frame.pts = DataFrame::makePts(start.addMSecs(f * 100));
frame.functionCode = 1;
frame.data.reserve(kSampleCount);
for (int i = 0; i < kSampleCount; ++i)
frame.data.push_back(static_cast<float>(kBase + f * 2 + i * 5));
m_frames.push_back(frame);
}
emit frameCountChanged();
}
void DataBackend::emitFrame_(const DataFrame& frame, const RenderCallback& cb) {
updateMetrics_(frame);
if (cb)
cb(frame);
}
void DataBackend::updateMetrics_(const DataFrame& frame) {
double peak = 0.0;
double rms = 0.0;
double avg = 0.0;
double delta = 0.0;
double sum = 0.0;
if (!frame.data.isEmpty()) {
double sumSq = 0.0;
float minV = frame.data.front();
float maxV = frame.data.front();
for (float v : frame.data) {
sum += v;
sumSq += static_cast<double>(v) * static_cast<double>(v);
if (v < minV)
minV = v;
if (v > maxV)
maxV = v;
}
const double count = static_cast<double>(frame.data.size());
avg = sum / count;
rms = std::sqrt(sumSq / count);
peak = maxV;
delta = maxV - minV;
}
m_metricPeak = peak;
m_metricRms = rms;
m_metricAvg = avg;
m_metricDelta = delta;
m_metricSum = sum;
emit metricsChanged();
}

83
src/data_backend.h Normal file
View File

@@ -0,0 +1,83 @@
#ifndef TACTILEIPC3D_DATA_BACKEND_H
#define TACTILEIPC3D_DATA_BACKEND_H
#include <QObject>
#include <QVector>
#include <QTimer>
#include <QString>
#include <functional>
#include <QSerialPort>
#include <QSerialPortInfo>
#include <qtmetamacros.h>
#include "data_frame.h"
class DataBackend : public QObject {
Q_OBJECT
Q_PROPERTY(int frameCount READ frameCount NOTIFY frameCountChanged)
Q_PROPERTY(bool playbackRunning READ playbackRunning NOTIFY playbackRunningChanged)
Q_PROPERTY(double metricPeak READ metricPeak NOTIFY metricsChanged)
Q_PROPERTY(double metricRms READ metricRms NOTIFY metricsChanged)
Q_PROPERTY(double metricAvg READ metricAvg NOTIFY metricsChanged)
Q_PROPERTY(double metricDelta READ metricDelta NOTIFY metricsChanged)
Q_PROPERTY(double metricSum READ metricSum NOTIFY metricsChanged)
public:
using RenderCallback = std::function<void(const DataFrame&)>;
explicit DataBackend(QObject* parent = nullptr);
int frameCount() const { return m_frames.size(); }
bool playbackRunning() const { return m_playbackRunning; }
double metricPeak() const { return m_metricPeak; }
double metricRms() const { return m_metricRms; }
double metricAvg() const { return m_metricAvg; }
double metricDelta() const { return m_metricDelta; }
double metricSum() const { return m_metricSum; }
void setLiveRenderCallback(RenderCallback cb) { m_liveCallback = std::move(cb); }
void setPlaybackRenderCallback(RenderCallback cb) { m_playbackCallback = std::move(cb); }
void ingestFrame(const DataFrame& frame);
Q_INVOKABLE void clear();
Q_INVOKABLE bool exportJson(const QString& path) const;
Q_INVOKABLE bool exportCsv(const QString& path) const;
Q_INVOKABLE bool importJson(const QString& path);
Q_INVOKABLE bool importCsv(const QString& path);
Q_INVOKABLE void startPlayback(int intervalMs);
Q_INVOKABLE void stopPlayback();
Q_INVOKABLE void exportHandler(const QUrl& folder, const QString& filename,
const QString& format, const QString& method);
signals:
void frameCountChanged();
void playbackRunningChanged();
void metricsChanged();
private:
bool loadJson_(const QByteArray& data);
bool loadCsv_(const QByteArray& data);
QByteArray buildJson_() const;
QByteArray buildCsv_() const;
void seedDebugFrames_();
void emitFrame_(const DataFrame& frame, const RenderCallback& cb);
void updateMetrics_(const DataFrame& frame);
QVector<DataFrame> m_frames;
QTimer m_playbackTimer;
int m_playbackIndex = 0;
bool m_playbackRunning = false;
double m_metricPeak = 0.0;
double m_metricRms = 0.0;
double m_metricAvg = 0.0;
double m_metricDelta = 0.0;
double m_metricSum = 0.0;
RenderCallback m_liveCallback;
RenderCallback m_playbackCallback;
};
#endif // TACTILEIPC3D_DATA_BACKEND_H

19
src/data_frame.h Normal file
View File

@@ -0,0 +1,19 @@
#ifndef TACTILEIPC3D_DATA_FRAME_H
#define TACTILEIPC3D_DATA_FRAME_H
#include <QVector>
#include <QString>
#include <QDateTime>
#include <QtGlobal>
struct DataFrame {
QString pts;
quint8 functionCode = 0;
QVector<float> data;
static QString makePts(const QDateTime& dt) {
return dt.toString(QStringLiteral("yyyyMMddhhmmsszzz"));
}
};
#endif // TACTILEIPC3D_DATA_FRAME_H

View File

@@ -8,13 +8,21 @@
#include <QtMath>
#include <QFile>
#include <QDebug>
#include <GL/gl.h>
#include <qevent.h>
#include <qlogging.h>
#include <qminmax.h>
#include <qopenglext.h>
#include <qopenglshaderprogram.h>
#include <qstringliteral.h>
#include <qvectornd.h>
#include <QVector3D>
#include <QVector2D>
#include <QVector4D>
#include <QImage>
#include <QPainter>
#include <QFontMetrics>
#include <algorithm>
#include <limits>
// 读取文本文件内容(这里主要用来从 Qt Resource `:/shaders/...` 读取 shader 源码)
@@ -77,7 +85,7 @@ void GLWidget::setPanelSize(float w, float h, float d) {
void GLWidget::setPanelThickness(float h) {
if (qFuzzyCompare(m_panelH, h))
return;
m_panelH = h;
m_panelD = h;
m_panelGeometryDirty = true;
update();
}
@@ -91,9 +99,9 @@ void GLWidget::setSpec(int rows, int cols, float pitch, float dotRaius) {
// 自动根据点阵尺寸调整 panel 的尺寸(保证圆点不会越过顶面边界)。
// 约定:点中心覆盖的范围是 (cols-1)*pitch / (rows-1)*pitch面板需要额外留出 dotRadius 的边缘空间。
const float gridW = float(qMax(0, m_cols - 1)) * m_pitch;
const float gridD = float(qMax(0, m_rows - 1)) * m_pitch;
const float gridH = float(qMax(0, m_rows - 1)) * m_pitch;
m_panelW = gridW + 2.0f * m_dotRadius;
m_panelD = gridD + 2.0f * m_dotRadius;
m_panelH = gridH + 2.0f * m_dotRadius;
m_panelGeometryDirty = true;
m_dotsGeometryDirty = true;
@@ -166,6 +174,23 @@ void GLWidget::setLabelModeString(const QString &mode) {
update();
}
void GLWidget::setLightMode(bool on = true) {
if (on == m_lightMode) {
return;
}
m_lightMode = on;
update();
}
void GLWidget::setShowBg(bool on = true) {
if (on == m_showBg) {
return;
}
m_showBg = on;
update();
}
void GLWidget::initializeGL() {
initializeOpenGLFunctions();
@@ -177,6 +202,7 @@ void GLWidget::initializeGL() {
initBackgroundGeometry_();
initPanelGeometry_();
initDotGeometry_();
initRoomGeometry_();
m_panelGeometryDirty = false;
m_dotsGeometryDirty = false;
@@ -205,7 +231,7 @@ void GLWidget::paintGL() {
}
// --- 背景:屏幕空间网格(不随相机旋转)---
if (m_bgProg && m_bgVao) {
if (m_bgProg && m_bgVao && m_showBg) {
const float dpr = devicePixelRatioF();
const QVector2D viewport(float(width()) * dpr, float(height()) * dpr);
@@ -229,6 +255,7 @@ void GLWidget::paintGL() {
// 1) 更新相机/投影矩阵MVP决定如何把 3D 世界投影到屏幕
updateMatrices_();
updateRoom_();
// 2) 如果外部提交了新数据,就把 CPU 生成的 instance 数据更新到 GPU
updateInstanceBufferIfNeeded_();
@@ -245,7 +272,8 @@ void GLWidget::paintGL() {
m_panelProg->setUniformValue("uCols", m_cols);
m_panelProg->setUniformValue("uPitch", m_pitch);
m_panelProg->setUniformValue("uDotRadius", m_dotRadius);
m_panelProg->setUniformValue("uRenderMode", int(m_renderMode));
m_panelProg->setUniformValue("uRenderMode", 1);
m_panelProg->setUniformValue("uLightMode", m_lightMode);
glBindVertexArray(m_panelVao);
glDrawElements(GL_TRIANGLES, m_panelIndexCount, GL_UNSIGNED_INT, nullptr);
glBindVertexArray(0);
@@ -257,16 +285,16 @@ void GLWidget::paintGL() {
// uniforms每次 draw 前设置的一组“常量参数”(对当前 draw call 的所有 instance 都一致)
// uMVP: 同上;用于把每个 dot 的世界坐标变换到屏幕
m_dotsProg->setUniformValue("uMVP", m_mvp);
m_dotsProg->setUniformValue("uRenderMode", int(m_renderMode));
m_dotsProg->setUniformValue("uRenderMode", 1);
// uDotRadius: dot 的半径(世界坐标单位)
m_dotsProg->setUniformValue("uDotRadius", m_dotRadius);
// uBaseY: dot 的高度(放在 panel 顶面上方一点点,避免 z-fighting/闪烁)
m_dotsProg->setUniformValue("uBaseY", (m_panelH * 0.5f) + 0.001f);
m_dotsProg->setUniformValue("uBaseZ", -(m_panelD * 0.5f) - 0.001f);
// uMinV/uMaxV: 传感值范围,用于 fragment shader 把 value 映射成颜色
m_dotsProg->setUniformValue("uMinV", float(m_min));
m_dotsProg->setUniformValue("uMaxV", float(m_max));
const int hasData = (m_hasData.load() || m_dotTex == 0) ? 1 : 0;
m_dotsProg->setUniformValue("uHasData", hasData);
m_dotsProg->setUniformValue("uHasData", 0);
m_dotsProg->setUniformValue("uCameraPos", m_cameraPos);
m_dotsProg->setUniformValue("uDotTex", 0);
if (m_dotTex) {
@@ -282,7 +310,7 @@ void GLWidget::paintGL() {
glBindTexture(GL_TEXTURE_2D, 0);
}
m_dotsProg->release();
// m_dotsProg->release();
}
if (m_labelMode != LabelsOff && dotCount() > 0) {
@@ -396,6 +424,25 @@ void GLWidget::mousePressEvent(QMouseEvent *event) {
event->accept();
return;
}
if (event->button() == Qt::LeftButton) {
QVector3D world;
const int index = pickDotIndex_(event->pos(), &world);
if (index >= 0) {
float value = 0.0f;
int row = 0;
int col = 0;
if (m_cols > 0) {
row = index / m_cols;
col = index % m_cols;
}
{
QMutexLocker lk(&m_dataMutex);
if (index < m_latestValues.size())
value = m_latestValues[index];
}
emit dotClicked(index, row, col, value);
}
}
QOpenGLWidget::mousePressEvent(event);
}
@@ -483,11 +530,75 @@ void GLWidget::mouseReleaseEvent(QMouseEvent *event) {
void GLWidget::wheelEvent(QWheelEvent *event) {
const float steps = event->angleDelta().y() / 120.0f;
const float factor = std::pow(0.9f, steps);
m_zoom_ = qBound(0.2f, m_zoom_ * factor, 45.0f);
m_zoom_ = qBound(0.2f, m_zoom_ * factor, 90.0f);
update();
event->accept();
}
bool GLWidget::projectToScreen_(const QVector3D& world, QPointF* out) const {
if (!out)
return false;
const QVector4D clip = m_mvp * QVector4D(world, 1.0f);
if (clip.w() <= 1e-6f)
return false;
const QVector3D ndc = clip.toVector3D() / clip.w();
if (ndc.z() < -1.2f || ndc.z() > 1.2f)
return false;
out->setX((ndc.x() * 0.5f + 0.5f) * float(width()));
out->setY((1.0f - (ndc.y() * 0.5f + 0.5f)) * float(height()));
return true;
}
int GLWidget::pickDotIndex_(const QPoint& pos, QVector3D* worldOut) const {
if (dotCount() <= 0)
return -1;
const float baseY = (m_panelH * 0.5f) + 0.001f;
const float w = (m_cols - 1) * m_pitch;
const float h = (m_rows - 1) * m_pitch;
int best = -1;
float bestDist2 = std::numeric_limits<float>::infinity();
QVector3D bestWorld;
for (int i = 0; i < dotCount(); ++i) {
const int rr = (m_cols > 0) ? (i / m_cols) : 0;
const int cc = (m_cols > 0) ? (i % m_cols) : 0;
const QVector3D worldCenter(
(cc * m_pitch) - w * 0.5f,
baseY,
(rr * m_pitch) - h * 0.5f
);
QPointF center;
if (!projectToScreen_(worldCenter, &center))
continue;
QPointF edge;
if (!projectToScreen_(worldCenter + QVector3D(m_dotRadius, 0.0f, 0.0f), &edge))
continue;
const float radDx = float(edge.x() - center.x());
const float radDy = float(edge.y() - center.y());
const float radPx = std::sqrt(radDx * radDx + radDy * radDy);
const float threshold = radPx + 6.0f;
const float dx = float(pos.x()) - float(center.x());
const float dy = float(pos.y()) - float(center.y());
const float dist2 = dx * dx + dy * dy;
if (dist2 <= threshold * threshold && dist2 < bestDist2) {
best = i;
bestDist2 = dist2;
bestWorld = worldCenter;
}
}
if (best >= 0 && worldOut)
*worldOut = bestWorld;
return best;
}
void GLWidget::initBackgroundGeometry_() {
if (m_bgVbo) {
glDeleteBuffers(1, &m_bgVbo);
@@ -675,7 +786,6 @@ void GLWidget::initDotGeometry_() {
glBindVertexArray(0);
}
void GLWidget::initDotTexture_() {
if (m_dotTex) {
glDeleteTextures(1, &m_dotTex);
@@ -712,6 +822,91 @@ void GLWidget::initDotTexture_() {
glBindTexture(GL_TEXTURE_2D, 0);
}
void GLWidget::initRoomGeometry_() {
qInfo() << "initRoomGeometry_()";
if (room_vao) {
glDeleteVertexArrays(1, &room_vao);
room_vao = 0;
}
if (room_vbo) {
glDeleteBuffers(1, &room_vbo);
room_vbo = 0;
}
if (room_ibo) {
glDeleteBuffers(1, &room_ibo);
room_ibo = 0;
}
using V = struct {
float x, y, z;
float nx, ny, nz;
};
V verts[24] = {
// floor (y = -1), normal +Y
{-1, -1, -1, 0, +1, 0},
{+1, -1, -1, 0, +1, 0},
{+1, -1, +1, 0, +1, 0},
{-1, -1, +1, 0, +1, 0},
// ceiling (y = +1), normal -Y
{-1, +1, +1, 0, -1, 0},
{+1, +1, +1, 0, -1, 0},
{+1, +1, -1, 0, -1, 0},
{-1, +1, -1, 0, -1, 0},
// back wall (z = -1), normal +Z
{-1, +1, -1, 0, 0, +1},
{+1, +1, -1, 0, 0, +1},
{+1, -1, -1, 0, 0, +1},
{-1, -1, -1, 0, 0, +1},
// front wall (z = +1), normal -Z
{+1, +1, +1, 0, 0, -1},
{-1, +1, +1, 0, 0, -1},
{-1, -1, +1, 0, 0, -1},
{+1, -1, +1, 0, 0, -1},
// left wall (x = -1), normal +X
{-1, +1, +1, +1, 0, 0},
{-1, +1, -1, +1, 0, 0},
{-1, -1, -1, +1, 0, 0},
{-1, -1, +1, +1, 0, 0},
// right wall (x = +1), normal -X
{+1, +1, -1, -1, 0, 0},
{+1, +1, +1, -1, 0, 0},
{+1, -1, +1, -1, 0, 0},
{+1, -1, -1, -1, 0, 0},
};
unsigned int idx[36] = {
0, 1, 2, 0, 2, 3,
4, 5, 6, 4, 6, 7,
8, 9, 10, 8, 10, 11,
12, 13, 14, 12, 14, 15,
16, 17, 18, 16, 18, 19,
20, 21, 22, 20, 22, 23
};
room_index_count = 36;
glGenVertexArrays(1, &room_vao);
glBindVertexArray(room_vao);
glGenBuffers(1, &room_vbo);
glBindBuffer(GL_ARRAY_BUFFER, room_vbo);
glBufferData(GL_ARRAY_BUFFER, sizeof(verts), verts, GL_STATIC_DRAW);
glGenBuffers(1, &room_ibo);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, room_ibo);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(idx), idx, GL_STATIC_DRAW);
glEnableVertexAttribArray(0);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(V), (void*)0);
glEnableVertexAttribArray(1);
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, sizeof(V), (void*)(3 * sizeof(float)));
glBindVertexArray(0);
}
void GLWidget::initPrograms_() {
// Qt Resource 里打包的 shader 文件路径:使用 `:/` 前缀(不是文件系统路径)
const QString vsd_path = QStringLiteral(":/shaders/dots.vert");
@@ -720,6 +915,8 @@ void GLWidget::initPrograms_() {
const QString fsb_path = QStringLiteral(":/shaders/bg.frag");
const QString vsp_path = QStringLiteral(":/shaders/panel.vert");
const QString fsp_path = QStringLiteral(":/shaders/panel.frag");
const QString vsr_path = QStringLiteral(":/shaders/room.vert");
const QString fsr_path = QStringLiteral(":/shaders/room.frag");
{
auto *p = new QOpenGLShaderProgram;
@@ -757,6 +954,18 @@ void GLWidget::initPrograms_() {
}
m_dotsProg = p;
}
{
auto *p = new QOpenGLShaderProgram;
const bool okV = p->addShaderFromSourceCode(QOpenGLShader::Vertex, readFile(vsr_path));
const bool okF = p->addShaderFromSourceCode(QOpenGLShader::Fragment, readFile(fsr_path));
const bool okL = okV && okF && p->link();
if (!okL) {
qWarning() << "dots program build failed:" << vsr_path << fsr_path << p->log();
delete p;
p = nullptr;
}
m_roomProg = p;
}
}
void GLWidget::updateInstanceBufferIfNeeded_() {
@@ -825,7 +1034,7 @@ void GLWidget::updateMatrices_() {
const float distance = qMax(0.5f, radius * 2.5f);
// 让相机看向 panel 的中心点
const QVector3D center(0.0f, m_panelH * 0.5f, 0.0f);
const QVector3D center(0.0f, 0.0f, 0.0f);
// yaw/pitch 控制相机绕目标点“环绕”orbit camera
const float yawRad = qDegreesToRadians(m_camYawDeg);
@@ -849,3 +1058,43 @@ void GLWidget::updateMatrices_() {
m_mvp = proj * view;
}
void GLWidget::updateRoom_(){
if (!m_roomProg || !room_vao || room_index_count <= 0) {
return;
}
m_roomProg->bind();
const float base = std::max({ m_panelW, m_panelH, m_panelD });
const QVector3D roomHalfSize(
std::max(1.0f, base * 1.5f),
std::max(1.0f, base * 1.5f),
std::max(1.0f, base * 1.5f)
);
const float minorStep = std::max(0.05f, base * 0.25f);
const float majorStep = minorStep * 5.0f;
m_roomProg->setUniformValue("uMVP", m_mvp);
m_roomProg->setUniformValue("uCameraPos", m_cameraPos);
m_roomProg->setUniformValue("uRoomHalfSize", roomHalfSize);
m_roomProg->setUniformValue("uMinorStep", minorStep);
m_roomProg->setUniformValue("uMajorStep", majorStep);
m_roomProg->setUniformValue("uRenderMode", 1);
m_roomProg->setUniformValue("uLightMode", m_lightMode);
m_roomProg->setUniformValue("uShowGrid", m_showGrid);
glBindVertexArray(room_vao);
glDrawElements(GL_TRIANGLES, room_index_count, GL_UNSIGNED_INT, nullptr);
glBindVertexArray(0);
m_roomProg->release();
}
void GLWidget::setShowGrid(bool on) {
if (m_showGrid == on)
return;
m_showGrid = on;
update();
}

View File

@@ -1,10 +1,11 @@
//
// Created by Lenn on 2025/12/16.
//
#ifndef TACTILEIPC3D_GLWIDGET_H
#define TACTILEIPC3D_GLWIDGET_H
#include <qopenglshaderprogram.h>
#include <QOpenGLWidget>
#include <QOpenGLFunctions>
#include <QMutex>
@@ -14,10 +15,18 @@
#include <QVector3D>
#include <QString>
#include <atomic>
#include <qtmetamacros.h>
#include <qvectornd.h>
struct Ray {
QVector3D origin;
QVector3D dir;
};
class GLWidget : public QOpenGLWidget, protected QOpenGLFunctions_3_3_Core {
Q_OBJECT
Q_PROPERTY(float yaw READ yaw WRITE setYaw NOTIFY yawChanged)
// Q_PROPERTY(bool showGrid READ showGrid WRITE setShowGrid NOTIFY showGridChanged)
public:
enum RenderMode {
Realistic = 0,
@@ -48,15 +57,23 @@ public:
float yaw() const { return m_camYawDeg; }
bool showGrid() const { return m_showGrid; }
public slots:
// 值域范围,用于 shader 里把 value 映射到颜色(绿->红)
void setRange(int minV, int maxV);
void setYaw(float yawDeg);
void setRenderModeString(const QString& mode);
void setLabelModeString(const QString& mode);
void setLightMode(bool on);
void setShowBg(bool on);
void setShowGrid(bool on);
signals:
void yawChanged();
void dotClicked(int index, int row, int col, float value);
protected:
void initializeGL() override;
@@ -73,9 +90,12 @@ private:
void initBackgroundGeometry_();
void initPrograms_();
void initDotTexture_();
void initRoomGeometry_();
void updateInstanceBufferIfNeeded_();
void updateMatrices_();
void updateRoom_();
bool projectToScreen_(const QVector3D& world, QPointF* out) const;
int pickDotIndex_(const QPoint& pos, QVector3D* worldOut) const;
private:
// 传感值范围(用于颜色映射)
int m_min = 0;
@@ -86,9 +106,9 @@ private:
int m_cols = 4;
// panel: 一个长方体/板子(当前只画顶面矩形)
float m_panelW = 1.2f;
float m_panelH = 0.08f;
float m_panelD = 0.08f;
float m_panelW = 0.25f;
float m_panelH = 0.35f;
float m_panelD = 0.05f;
// 点阵布局参数
float m_pitch = 0.1f;
@@ -103,6 +123,7 @@ private:
// shader program编译/链接后的可执行 GPU 程序)
QOpenGLShaderProgram* m_bgProg = nullptr;
QOpenGLShaderProgram* m_roomProg = nullptr;
QOpenGLShaderProgram* m_panelProg = nullptr;
QOpenGLShaderProgram* m_dotsProg = nullptr;
@@ -119,11 +140,20 @@ private:
unsigned int m_dotsVao = 0;
unsigned int m_dotsVbo = 0;
unsigned int m_instanceVbo = 0;
unsigned int room_vao = 0;
unsigned int room_vbo = 0;
unsigned int room_ibo = 0;
int room_index_count = 0;
bool m_showGrid = true;
int m_instanceCount = 0;
bool m_dotsGeometryDirty = false;
unsigned int m_bgVao = 0;
unsigned int m_bgVbo = 0;
bool m_lightMode = true;
bool m_showBg = true;
// MVP = Projection * View * Model。
// 这里 panel/dots 顶点基本已经是“世界坐标”,所以我们用 proj*view 即可model 先省略)。
@@ -140,11 +170,12 @@ private:
float m_modelPanel[16]{};
float m_zoom_ = 45.0;
float m_camYawDeg = 45.0f;
float m_camPitchDeg = 35.0f;
float m_camYawDeg = -90.0f;
float m_camPitchDeg = 0.0f;
std::atomic<bool> m_rightDown{false};
QPoint m_lastPos;
};

53
src/nice_ticks.h Normal file
View File

@@ -0,0 +1,53 @@
#ifndef NICE_TICKS
#define NICE_TICKS
#include <cmath>
#include <vector>
inline double niceNumber(double x, bool round) {
const double expv = std::floor(std::log10(x));
const double f = x / std::pow(10.0, expv);
double nf;
if (round) {
if (f < 1.5) nf = 1.0;
else if (f < 3.0) nf = 2.0;
else if (f < 7.0) nf = 5.0;
else nf = 10.0;
} else {
if (f <= 1.0) nf = 1.0;
else if (f <= 2.0) nf = 2.0;
else if (f <= 5.0) nf = 5.0;
else nf = 10.0;
}
return nf * std::pow(10.0, expv);
}
struct NiceTicksResult {
double niceMin = 0;
double niceMax = 1;
double step = 0.2;
std::vector<double> ticks;
};
inline NiceTicksResult niceTicks(double minv, double maxv, int maxTicks = 5) {
NiceTicksResult r;
if (minv == maxv) {
// 给一个小范围
minv -= 1.0;
maxv += 1.0;
}
const double range = niceNumber(maxv - minv, false);
const double step = niceNumber(range / (maxTicks - 1), true);
const double niceMin = std::floor(minv / step) * step;
const double niceMax = std::ceil(maxv / step) * step;
r.niceMin = niceMin;
r.niceMax = niceMax;
r.step = step;
for (double v = niceMin; v <= niceMax + 0.5 * step; v += step) {
r.ticks.push_back(v);
}
return r;
}
#endif

0
src/ringbuffer.cpp Normal file
View File

82
src/ringbuffer.h Normal file
View File

@@ -0,0 +1,82 @@
#ifndef RINGBUFFER_H
#define RINGBUFFER_H
#include <cstddef>
#include <iostream>
#include <vector>
#include <atomic>
#include <algorithm>
#include <cstdint>
#include <type_traits>
template<typename T>
class RingBuffer {
public:
explicit RingBuffer(size_t capacity, T initValue = T{})
: m_capacity(capacity)
, m_data(capacity, initValue) {}
inline void push(const T& v) {
const uint64_t w = m_write.fetch_add(1, std::memory_order_relaxed);
m_data[w % m_capacity] = v;
const uint64_t newSize = std::min<uint64_t>(w + 1, m_capacity);
m_size.store(newSize, std::memory_order_release);
}
inline uint64_t size() const {
return m_size.load(std::memory_order_acquire);
}
inline uint64_t capacity() const {
return m_capacity;
}
inline uint64_t oldestGlobalIndex() const {
const uint64_t w = m_write.load(std::memory_order_acquire);
const uint64_t s = size();
return (w >= s) ? (w - s) : 0;
}
inline uint64_t newestGlobalIndex() const {
const uint64_t w = m_write.load(std::memory_order_acquire);
return (w > 0) ? (w - 1) : 0;
}
inline bool readByGlobalIndex(uint64_t gidx, T& out) const {
const uint64_t oldest = oldestGlobalIndex();
const uint64_t newest = newestGlobalIndex();
if (gidx < oldest || gidx > newest)
return false;
out = m_data[gidx % m_capacity];
return true;
}
inline void reset() {
m_write.store(0, std::memory_order_release);
m_size.store(0, std::memory_order_release);
}
inline void readRange(uint64_t gidx, uint64_t count, std::vector<T>& out) const {
out.clear();
out.reserve(count);
for (size_t i = 0; i < count; i++) {
T v{};
if (readByGlobalIndex(gidx, v)) {
out.push_back(v);
}
else {
}
}
}
private:
uint64_t m_capacity;
std::vector<T> m_data;
std::atomic<uint64_t> m_write{0};
std::atomic<uint64_t> m_size{0};
};
#endif

View File

@@ -0,0 +1,241 @@
#include "piezoresistive_a_protocol.h"
#include <QDateTime>
#include <qcontainerfwd.h>
#include <qtypes.h>
namespace {
constexpr quint8 kReplyStart0 = 0x55; // 0x55AA little-endian
constexpr quint8 kReplyStart1 = 0xAA;
constexpr quint8 kReplyStartAlt0 = 0xAA;
constexpr quint8 kReplyStartAlt1 = 0x55;
constexpr quint8 kRequestStart0 = 0x55; // 0x55AA little-endian
constexpr quint8 kRequestStart1 = 0xAA;
quint16 readLe16(const QByteArray& data, int offset) {
const quint8 b0 = static_cast<quint8>(data[offset]);
const quint8 b1 = static_cast<quint8>(data[offset + 1]);
return static_cast<quint16>(b0 | (b1 << 8));
}
quint32 readLe32(const QByteArray& data, int offset) {
const quint8 b0 = static_cast<quint8>(data[offset]);
const quint8 b1 = static_cast<quint8>(data[offset + 1]);
const quint8 b2 = static_cast<quint8>(data[offset + 2]);
const quint8 b3 = static_cast<quint8>(data[offset + 3]);
return static_cast<quint32>(b0 | (b1 << 8) | (b2 << 16) | (b3 << 24));
}
void appendLe16(QByteArray& data, quint16 v) {
data.append(static_cast<char>(v & 0xFF));
data.append(static_cast<char>((v >> 8) & 0xFF));
}
void appendLe32(QByteArray& data, quint32 v) {
data.append(static_cast<char>(v & 0xFF));
data.append(static_cast<char>((v >> 8) & 0xFF));
data.append(static_cast<char>((v >> 16) & 0xFF));
data.append(static_cast<char>((v >> 24) & 0xFF));
}
}
quint8 PiezoresistiveAFormat::crc8ITU(const QByteArray& data, int length) {
quint8 crc = 0x00;
const int limit = qMin(length, data.size());
for (int i = 0; i < limit; ++i) {
crc ^= static_cast<quint8>(data[i]);
for (int bit = 0; bit < 8; ++bit) {
if (crc & 0x80)
crc = static_cast<quint8>((crc << 1) ^ 0x07);
else
crc = static_cast<quint8>(crc << 1);
}
}
return static_cast<quint8>(crc ^ 0x55);
}
ISerialFormat::ParseResult PiezoresistiveAFormat::tryParse(QByteArray* buffer, QByteArray* packet, QString* error) {
if (!buffer || buffer->isEmpty())
return ParseResult::NeedMore;
int startIndex = -1;
for (int i = 0; i + 1 < buffer->size(); ++i) {
if (static_cast<quint8>((*buffer)[i]) == kReplyStart0 &&
static_cast<quint8>((*buffer)[i + 1]) == kReplyStart1) {
startIndex = i;
break;
}
if (static_cast<quint8>((*buffer)[i]) == kReplyStartAlt0 &&
static_cast<quint8>((*buffer)[i + 1]) == kReplyStartAlt1) {
startIndex = i;
break;
}
}
if (startIndex < 0) {
const quint8 tail = static_cast<quint8>(buffer->back());
buffer->clear();
if (tail == kReplyStart0 || tail == kReplyStart1)
buffer->append(static_cast<char>(tail));
return ParseResult::NeedMore;
}
if (startIndex > 0)
buffer->remove(0, startIndex);
if (buffer->size() < 5)
return ParseResult::NeedMore;
const quint16 payloadLen = readLe16(*buffer, 2);
const int totalLen = 4 + payloadLen + 1;
if (buffer->size() < totalLen)
return ParseResult::NeedMore;
QByteArray candidate = buffer->left(totalLen);
buffer->remove(0, totalLen);
const quint8 crc = static_cast<quint8>(candidate.at(candidate.size() - 1));
const quint8 calc = crc8ITU(candidate, candidate.size() - 1);
if (crc != calc) {
if (error)
*error = QStringLiteral("CRC mismatch");
return ParseResult::Invalid;
}
if (packet)
*packet = candidate;
if (error)
error->clear();
return ParseResult::Ok;
}
QByteArray PiezoresistiveACodec::buildRequest(const SerialConfig& config, const SensorRequest& request) {
QByteArray packet;
packet.reserve(15);
packet.append(static_cast<char>(kRequestStart0));
packet.append(static_cast<char>(kRequestStart1));
const quint16 payloadLen = 9;
appendLe16(packet, payloadLen);
packet.append(static_cast<char>(config.deviceAddress));
packet.append(static_cast<char>(0x00));
packet.append(static_cast<char>(0x80 | request.functionCode));
appendLe32(packet, request.startAddress);
appendLe16(packet, request.dataLength);
const quint8 crc = PiezoresistiveAFormat::crc8ITU(packet, packet.size());
packet.append(static_cast<char>(crc));
return packet;
}
QByteArray PiezoresistiveACodec::buildGetVersionRequest(const SerialConfig& config) {
// TODO:待实现内容(压阻 A 型版本号查询请求帧)
Q_UNUSED(config)
return QByteArray();
}
QByteArray PiezoresistiveACodec::buildGetSpecRequest(const SerialConfig& config) {
// TODO:待实现内容(压阻 A 型规格查询请求帧)
Q_UNUSED(config)
return QByteArray();
}
bool PiezoresistiveADecoder::decodeFrame(const QByteArray& packet, DataFrame* frame, QString* error) {
if (!frame) {
if (error)
*error = QStringLiteral("Null frame output");
return false;
}
if (packet.size() < 15) {
if (error)
*error = QStringLiteral("Packet too short");
return false;
}
const quint8 start0 = static_cast<quint8>(packet[0]);
const quint8 start1 = static_cast<quint8>(packet[1]);
const bool startOk = (start0 == kReplyStart0 && start1 == kReplyStart1) ||
(start0 == kReplyStartAlt0 && start1 == kReplyStartAlt1);
if (!startOk) {
if (error)
*error = QStringLiteral("Bad start bytes");
return false;
}
const quint16 payloadLen = readLe16(packet, 2);
const int totalLen = 4 + payloadLen + 1;
if (packet.size() != totalLen) {
if (error)
*error = QStringLiteral("Length mismatch");
return false;
}
const quint8 crc = static_cast<quint8>(packet.at(packet.size() - 1));
const quint8 calc = PiezoresistiveAFormat::crc8ITU(packet, packet.size() - 1);
if (crc != calc) {
if (error)
*error = QStringLiteral("CRC mismatch");
return false;
}
const quint8 funcRaw = static_cast<quint8>(packet[6]);
const quint16 dataLen = readLe16(packet, 11);
const quint8 status = static_cast<quint8>(packet[13]);
if (payloadLen != static_cast<quint16>(10 + dataLen)) {
if (error)
*error = QStringLiteral("Payload length mismatch");
return false;
}
if (status != 0) {
if (error)
*error = QStringLiteral("Device status error");
return false;
}
if (packet.size() < 15 + dataLen) {
if (error)
*error = QStringLiteral("Data length mismatch");
return false;
}
if ((dataLen % 2) != 0) {
if (error)
*error = QStringLiteral("Odd data length");
return false;
}
const int sampleCount = dataLen / 2;
QVector<float> values;
values.reserve(sampleCount);
const int dataOffset = 14;
for (int i = 0; i < sampleCount; ++i) {
const int offset = dataOffset + i * 2;
const quint16 raw = readLe16(packet, offset);
values.push_back(static_cast<float>(raw));
}
frame->pts = DataFrame::makePts(QDateTime::currentDateTime());
frame->functionCode = (funcRaw >= 0x80) ? static_cast<quint8>(funcRaw - 0x80) : funcRaw;
frame->data = values;
if (error)
error->clear();
return true;
}
bool PiezoresistiveADecoder::decodeSpec(const QByteArray& packet, SensorSpec* spec, QString* error) {
// TODO:待实现内容(解析压阻 A 型规格回复并写入 SensorSpec)
Q_UNUSED(packet)
Q_UNUSED(spec)
if (error)
*error = QStringLiteral("Not implemented");
return false;
}

View File

@@ -0,0 +1,32 @@
#ifndef TACTILEIPC3D_PIEZORESISTIVE_A_PROTOCOL_H
#define TACTILEIPC3D_PIEZORESISTIVE_A_PROTOCOL_H
#include "serial_codec.h"
#include "serial_decoder.h"
#include "serial_format.h"
class PiezoresistiveAFormat : public ISerialFormat {
public:
ParseResult tryParse(QByteArray* buffer, QByteArray* packet, QString* error) override;
static quint8 crc8ITU(const QByteArray& data, int length);
};
class PiezoresistiveACodec : public ISerialCodec {
public:
QString name() const override { return QStringLiteral("piezoresistive_a"); }
QByteArray buildRequest(const SerialConfig& config, const SensorRequest& request) override;
QByteArray buildGetVersionRequest(const SerialConfig& config) override;
QByteArray buildGetSpecRequest(const SerialConfig& config) override;
};
class PiezoresistiveADecoder : public ISerialDecoder {
public:
QString name() const override { return QStringLiteral("piezoresistive_a"); }
bool decodeFrame(const QByteArray& packet, DataFrame* frame, QString* error) override;
bool decodeSpec(const QByteArray& packet, SensorSpec* spec, QString* error) override;
};
#endif // TACTILEIPC3D_PIEZORESISTIVE_A_PROTOCOL_H

View File

@@ -0,0 +1,358 @@
#include "serial_backend.h"
#include "piezoresistive_a_protocol.h"
#include "serial_qt_transport.h"
#include <QDebug>
#include <QMetaObject>
#include <QtGlobal>
#include <qcontainerfwd.h>
#include <qserialportinfo.h>
#include <vector>
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");
m_spec.rows = 3;
m_spec.cols = 4;
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());
m_sendWorker = new SerialSendWorker();
m_sendWorker->moveToThread(&m_sendThread);
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);
});
connect(m_sendWorker, &SerialSendWorker::requestBuilt, this, &SerialBackend::requestBuilt);
connect(m_sendWorker, &SerialSendWorker::writeFailed, this, [](const QString& error) {
if (!error.isEmpty())
qWarning().noquote() << "Serial write failed:" << 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) {
if (!error.isEmpty())
qWarning().noquote() << "Serial decode failed:" << error;
});
connect(&m_decodeThread, &SerialDecodeThread::frameAvailable, this, &SerialBackend::drainFrames_);
m_sendThread.start();
setTransport(std::make_unique<QtSerialTransport>());
updateProtocolBindings_();
syncSendConfig_();
syncSendRequest_();
refreshPorts();
}
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, &QObject::deleteLater, Qt::QueuedConnection);
}
if (m_sendThread.isRunning()) {
m_sendThread.quit();
m_sendThread.wait();
}
m_sendWorker = nullptr;
}
QString SerialBackend::mode() const {
return (m_config.mode == DeviceMode::Slave) ? QStringLiteral("slave") : QStringLiteral("master");
}
QString SerialBackend::sensorGrid() const {
return QStringLiteral("%1x%2").arg(m_spec.rows).arg(m_spec.cols);
}
void SerialBackend::setPortName(const QString& name) {
if (m_config.portName == name)
return;
m_config.portName = name;
syncSendConfig_();
emit portNameChanged();
}
void SerialBackend::setBaudRate(int rate) {
if (m_config.baudRate == rate)
return;
m_config.baudRate = rate;
syncSendConfig_();
emit baudRateChanged();
}
void SerialBackend::setPollIntervalMs(int intervalMs) {
intervalMs = qMax(1, intervalMs);
if (m_config.pollIntervalMs == intervalMs)
return;
m_config.pollIntervalMs = intervalMs;
syncSendConfig_();
emit pollIntervalMsChanged();
}
void SerialBackend::setDeviceAddress(int address) {
const int capped = qBound(0, address, 255);
if (m_config.deviceAddress == static_cast<quint8>(capped))
return;
m_config.deviceAddress = static_cast<quint8>(capped);
syncSendConfig_();
emit deviceAddressChanged();
}
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)
return;
m_config.mode = next;
syncSendConfig_();
emit modeChanged();
}
void SerialBackend::setRequestFunction(int func) {
const int capped = qBound(0, func, 255);
if (m_request.functionCode == static_cast<quint8>(capped))
return;
m_request.functionCode = static_cast<quint8>(capped);
syncSendRequest_();
emit requestFunctionChanged();
}
void SerialBackend::setRequestStartAddress(int addr) {
const quint32 capped = static_cast<quint32>(qMax(0, addr));
if (m_request.startAddress == capped)
return;
m_request.startAddress = capped;
syncSendRequest_();
emit requestStartAddressChanged();
}
void SerialBackend::setRequestLength(int len) {
const int capped = qBound(0, len, 65535);
if (m_request.dataLength == static_cast<quint16>(capped))
return;
m_request.dataLength = static_cast<quint16>(capped);
syncSendRequest_();
emit requestLengthChanged();
}
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) {
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) {
m_spec.model = nextModel;
emit sensorModelChanged();
changed = true;
}
if (m_spec.rows != nextRows || m_spec.cols != nextCols) {
m_spec.rows = nextRows;
m_spec.cols = nextCols;
emit sensorGridChanged();
changed = true;
}
if (!changed)
return;
}
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);
}
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) {
m_availablePorts.append(item.portName());
}
if (m_config.portName.isEmpty() && !m_availablePorts.isEmpty()) {
m_config.portName = m_availablePorts.first();
emit portNameChanged();
}
emit availablePortsChanged();
}
bool SerialBackend::open() {
if (m_connected)
return true;
startPipeline_();
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 (!ok) {
qWarning().noquote() << "Serial open failed:" << error;
return false;
}
m_connected = true;
emit connectedChanged();
return true;
}
void SerialBackend::close() {
if (!m_connected)
return;
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() {
if (!m_sendWorker)
return;
QMetaObject::invokeMethod(m_sendWorker, [worker = m_sendWorker]() {
worker->requestOnce();
}, Qt::QueuedConnection);
}
void SerialBackend::feedBytes(const QByteArray& data) {
if (!m_readThread.isRunning())
startPipeline_();
m_readThread.enqueueBytes(data);
}
void SerialBackend::drainFrames_() {
if (!m_frameCallback)
return;
DataFrame frame;
while (m_frameQueue.tryPop(&frame))
m_frameCallback(frame);
}
void SerialBackend::startPipeline_() {
if (m_readThread.isRunning() || m_decodeThread.isRunning())
stopPipeline_();
m_packetQueue.reset();
m_frameQueue.reset();
m_readThread.clear();
updateProtocolBindings_();
if (!m_readThread.isRunning())
m_readThread.start();
if (!m_decodeThread.isRunning())
m_decodeThread.start();
}
void SerialBackend::stopPipeline_() {
if (m_readThread.isRunning()) {
m_readThread.stop();
m_readThread.wait();
}
if (m_decodeThread.isRunning()) {
m_decodeThread.stop();
m_decodeThread.wait();
}
m_packetQueue.reset();
m_frameQueue.reset();
m_readThread.clear();
}
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) {
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) {
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) {
return codec->buildRequest(config, request);
};
}
if (m_sendWorker) {
QMetaObject::invokeMethod(m_sendWorker, [worker = m_sendWorker, requestFunc]() mutable {
worker->setBuildRequestFunc(std::move(requestFunc));
}, Qt::QueuedConnection);
}
}
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);
}
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);
}

114
src/serial/serial_backend.h Normal file
View File

@@ -0,0 +1,114 @@
#ifndef TACTILEIPC3D_SERIAL_BACKEND_H
#define TACTILEIPC3D_SERIAL_BACKEND_H
#include <QObject>
#include <QStringList>
#include <QString>
#include <functional>
#include <memory>
#include "serial_manager.h"
#include "serial_threads.h"
#include "../data_frame.h"
class SerialBackend : public QObject {
Q_OBJECT
Q_PROPERTY(QString portName READ portName WRITE setPortName NOTIFY portNameChanged)
Q_PROPERTY(int baudRate READ baudRate WRITE setBaudRate NOTIFY baudRateChanged)
Q_PROPERTY(int pollIntervalMs READ pollIntervalMs WRITE setPollIntervalMs NOTIFY pollIntervalMsChanged)
Q_PROPERTY(int deviceAddress READ deviceAddress WRITE setDeviceAddress NOTIFY deviceAddressChanged)
Q_PROPERTY(QString mode READ mode WRITE setMode NOTIFY modeChanged)
Q_PROPERTY(int requestFunction READ requestFunction WRITE setRequestFunction NOTIFY requestFunctionChanged)
Q_PROPERTY(int requestStartAddress READ requestStartAddress WRITE setRequestStartAddress NOTIFY requestStartAddressChanged)
Q_PROPERTY(int requestLength READ requestLength WRITE setRequestLength NOTIFY requestLengthChanged)
Q_PROPERTY(QString protocol READ protocol WRITE setProtocol NOTIFY protocolChanged)
Q_PROPERTY(bool connected READ connected NOTIFY connectedChanged)
Q_PROPERTY(QStringList availablePorts READ availablePorts NOTIFY availablePortsChanged)
Q_PROPERTY(QString sensorModel READ sensorModel NOTIFY sensorModelChanged)
Q_PROPERTY(QString sensorGrid READ sensorGrid NOTIFY sensorGridChanged)
public:
using FrameCallback = std::function<void(const DataFrame&)>;
explicit SerialBackend(QObject* parent = nullptr);
~SerialBackend() override;
QString portName() const { return m_config.portName; }
int baudRate() const { return m_config.baudRate; }
int pollIntervalMs() const { return m_config.pollIntervalMs; }
int deviceAddress() const { return static_cast<int>(m_config.deviceAddress); }
QString mode() const;
int requestFunction() const { return m_request.functionCode; }
int requestStartAddress() const { return static_cast<int>(m_request.startAddress); }
int requestLength() const { return m_request.dataLength; }
QString protocol() const { return m_manager.activeProtocol(); }
bool connected() const { return m_connected; }
QStringList availablePorts() const { return m_availablePorts; }
QString sensorModel() const { return m_spec.model; }
QString sensorGrid() const;
void setPortName(const QString& name);
void setBaudRate(int rate);
void setPollIntervalMs(int intervalMs);
void setDeviceAddress(int address);
void setMode(const QString& mode);
void setRequestFunction(int func);
void setRequestStartAddress(int addr);
void setRequestLength(int len);
void setProtocol(const QString& name);
Q_INVOKABLE void applySensorSpec(const QString& model, int rows, int cols);
void setFrameCallback(FrameCallback cb) { m_frameCallback = std::move(cb); }
void setTransport(std::unique_ptr<ISerialTransport> transport);
Q_INVOKABLE void refreshPorts();
Q_INVOKABLE bool open();
Q_INVOKABLE void close();
Q_INVOKABLE void requestOnce();
Q_INVOKABLE void feedBytes(const QByteArray& data);
signals:
void portNameChanged();
void baudRateChanged();
void pollIntervalMsChanged();
void deviceAddressChanged();
void modeChanged();
void requestFunctionChanged();
void requestStartAddressChanged();
void requestLengthChanged();
void protocolChanged();
void connectedChanged();
void availablePortsChanged();
void sensorModelChanged();
void sensorGridChanged();
void requestBuilt(const QByteArray& data);
private:
void drainFrames_();
void startPipeline_();
void stopPipeline_();
void updateProtocolBindings_();
void syncSendConfig_();
void syncSendRequest_();
SerialConfig m_config;
SensorRequest m_request;
SensorSpec m_spec;
SerialManager m_manager;
PacketQueue m_packetQueue;
FrameQueue m_frameQueue;
SerialReadThread m_readThread;
SerialDecodeThread m_decodeThread;
QThread m_sendThread;
SerialSendWorker* m_sendWorker = nullptr;
FrameCallback m_frameCallback;
bool m_connected = false;
QStringList m_availablePorts;
};
#endif // TACTILEIPC3D_SERIAL_BACKEND_H

23
src/serial/serial_codec.h Normal file
View File

@@ -0,0 +1,23 @@
#ifndef TACTILEIPC3D_SERIAL_CODEC_H
#define TACTILEIPC3D_SERIAL_CODEC_H
#include <QByteArray>
#include <QString>
#include "serial_types.h"
class ISerialCodec {
public:
virtual ~ISerialCodec() = default;
virtual QString name() const = 0;
virtual QByteArray buildRequest(const SerialConfig& config, const SensorRequest& request) = 0;
// Reserved hooks for later expansion
// TODO:待实现内容(构建获取版本号的请求帧)
virtual QByteArray buildGetVersionRequest(const SerialConfig& config) = 0;
// TODO:待实现内容(构建获取传感器规格的请求帧)
virtual QByteArray buildGetSpecRequest(const SerialConfig& config) = 0;
};
#endif // TACTILEIPC3D_SERIAL_CODEC_H

View File

@@ -0,0 +1,22 @@
#ifndef TACTILEIPC3D_SERIAL_DECODER_H
#define TACTILEIPC3D_SERIAL_DECODER_H
#include <QByteArray>
#include <QString>
#include "serial_types.h"
#include "../data_frame.h"
class ISerialDecoder {
public:
virtual ~ISerialDecoder() = default;
virtual QString name() const = 0;
virtual bool decodeFrame(const QByteArray& packet, DataFrame* frame, QString* error) = 0;
// Reserved hooks for later expansion
// TODO:待实现内容(解析规格回复帧并填充 SensorSpec)
virtual bool decodeSpec(const QByteArray& packet, SensorSpec* spec, QString* error) = 0;
};
#endif // TACTILEIPC3D_SERIAL_DECODER_H

View File

@@ -0,0 +1,19 @@
#ifndef TACTILEIPC3D_SERIAL_FORMAT_H
#define TACTILEIPC3D_SERIAL_FORMAT_H
#include <QByteArray>
#include <QString>
class ISerialFormat {
public:
enum class ParseResult {
NeedMore = 0,
Ok = 1,
Invalid = 2,
};
virtual ~ISerialFormat() = default;
virtual ParseResult tryParse(QByteArray* buffer, QByteArray* packet, QString* error) = 0;
};
#endif // TACTILEIPC3D_SERIAL_FORMAT_H

View File

@@ -0,0 +1,22 @@
#include "serial_manager.h"
void SerialManager::registerProtocol(const QString& name, const ProtocolBundle& bundle) {
if (name.isEmpty())
return;
m_protocols.insert(name, bundle);
if (m_activeName.isEmpty())
m_activeName = name;
}
bool SerialManager::setActiveProtocol(const QString& name) {
if (!m_protocols.contains(name))
return false;
m_activeName = name;
return true;
}
SerialManager::ProtocolBundle SerialManager::activeBundle() const {
if (!m_protocols.contains(m_activeName))
return {};
return m_protocols.value(m_activeName);
}

View File

@@ -0,0 +1,31 @@
#ifndef TACTILEIPC3D_SERIAL_MANAGER_H
#define TACTILEIPC3D_SERIAL_MANAGER_H
#include <QHash>
#include <QString>
#include <memory>
#include "serial_codec.h"
#include "serial_decoder.h"
#include "serial_format.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);
QString activeProtocol() const { return m_activeName; }
ProtocolBundle activeBundle() const;
private:
QHash<QString, ProtocolBundle> m_protocols;
QString m_activeName;
};
#endif // TACTILEIPC3D_SERIAL_MANAGER_H

View File

@@ -0,0 +1,140 @@
#include "serial_qt_transport.h"
#include "serial/serial_types.h"
#include <QSerialPort>
#include <QtGlobal>
#include <qcontainerfwd.h>
namespace {
QSerialPort::DataBits mapDataBits(int bits) {
switch (bits) {
case 5:
return QSerialPort::Data5;
case 6:
return QSerialPort::Data6;
case 7:
return QSerialPort::Data7;
case 8:
default:
return QSerialPort::Data8;
}
}
QSerialPort::StopBits mapStopBits(int bits) {
switch (bits) {
case 2:
return QSerialPort::TwoStop;
case 1:
default:
return QSerialPort::OneStop;
}
}
QSerialPort::Parity mapParity(const QString& parity) {
const QString key = parity.trimmed().toUpper();
if (key == QStringLiteral("E") || key == QStringLiteral("EVEN"))
return QSerialPort::EvenParity;
if (key == QStringLiteral("O") || key == QStringLiteral("ODD"))
return QSerialPort::OddParity;
if (key == QStringLiteral("M") || key == QStringLiteral("MARK"))
return QSerialPort::MarkParity;
if (key == QStringLiteral("S") || key == QStringLiteral("SPACE"))
return QSerialPort::SpaceParity;
return QSerialPort::NoParity;
}
} // namespace
QtSerialTransport::QtSerialTransport(QObject* parent)
: ISerialTransport(parent) {}
QtSerialTransport::~QtSerialTransport() {
close();
}
void QtSerialTransport::ensurePort_() {
if (m_port)
return;
m_port = new QSerialPort(this);
connect(m_port, &QSerialPort::readyRead, this, [this]() {
const QByteArray data = m_port->readAll();
if (!data.isEmpty())
emit bytesReceived(data);
});
}
void applyConfigDebug(const SerialConfig& config) {
#if DEBUG_MODE
QString strd = "";
strd += "baud:" + QString::number(config.baudRate);
#endif
}
void QtSerialTransport::applyConfig_(const SerialConfig& config) {
if (!m_port)
return;
applyConfigDebug(config);
m_port->setBaudRate(config.baudRate);
m_port->setDataBits(mapDataBits(config.dataBits));
m_port->setStopBits(mapStopBits(config.stopBits));
m_port->setParity(mapParity(config.parity));
// TODO:待实现内容(根据SerialConfig扩展软件/硬件流控配置)
m_port->setFlowControl(QSerialPort::NoFlowControl);
}
bool QtSerialTransport::open(const SerialConfig& config, QString* error) {
ensurePort_();
if (config.portName.trimmed().isEmpty()) {
if (error)
*error = QStringLiteral("Port name is empty");
return false;
}
if (m_port->isOpen())
m_port->close();
m_port->setPortName(config.portName);
applyConfig_(config);
const bool ok = m_port->open(QIODevice::ReadWrite);
if (!ok) {
if (error)
*error = m_port->errorString();
return false;
}
if (error)
error->clear();
return true;
}
void QtSerialTransport::close() {
if (m_port && m_port->isOpen())
m_port->close();
}
bool QtSerialTransport::writeBytes(const QByteArray& data, QString* error) {
if (!m_port || !m_port->isOpen()) {
if (error)
*error = QStringLiteral("Serial port not open");
return false;
}
if (data.isEmpty()) {
if (error)
error->clear();
return true;
}
const qint64 written = m_port->write(data);
if (written < 0) {
if (error)
*error = m_port->errorString();
return false;
}
if (error)
error->clear();
return true;
}

View File

@@ -0,0 +1,25 @@
#ifndef TACTILEIPC3D_SERIAL_QT_TRANSPORT_H
#define TACTILEIPC3D_SERIAL_QT_TRANSPORT_H
#include "serial_transport.h"
class QSerialPort;
class QtSerialTransport : public ISerialTransport {
Q_OBJECT
public:
explicit QtSerialTransport(QObject* parent = nullptr);
~QtSerialTransport() override;
bool open(const SerialConfig& config, QString* error) override;
void close() override;
bool writeBytes(const QByteArray& data, QString* error) override;
private:
void ensurePort_();
void applyConfig_(const SerialConfig& config);
QSerialPort* m_port = nullptr;
};
#endif // TACTILEIPC3D_SERIAL_QT_TRANSPORT_H

97
src/serial/serial_queue.h Normal file
View File

@@ -0,0 +1,97 @@
#ifndef TACTILEIPC3D_SERIAL_QUEUE_H
#define TACTILEIPC3D_SERIAL_QUEUE_H
#include <QQueue>
#include <QMutex>
#include <QWaitCondition>
#include <QByteArray>
#include <QtGlobal>
#include "../data_frame.h"
template <typename T>
class SerialQueue {
public:
explicit SerialQueue(int maxSize = 2048)
: m_maxSize(maxSize) {}
void setMaxSize(int maxSize) {
QMutexLocker locker(&m_mutex);
m_maxSize = qMax(1, maxSize);
}
void push(const T& item) {
QMutexLocker locker(&m_mutex);
if (m_stopped)
return;
if (m_maxSize > 0 && m_queue.size() >= m_maxSize) {
// TODO:待实现内容(指定队列溢出策略,例如丢弃最新/丢弃最旧/阻塞等待)
m_queue.dequeue();
}
m_queue.enqueue(item);
m_hasData.wakeOne();
}
bool pop(T* out, int timeoutMs = -1) {
QMutexLocker locker(&m_mutex);
if (timeoutMs < 0) {
while (m_queue.isEmpty() && !m_stopped)
m_hasData.wait(&m_mutex);
} else {
if (m_queue.isEmpty() && !m_stopped)
m_hasData.wait(&m_mutex, timeoutMs);
}
if (m_queue.isEmpty() || m_stopped)
return false;
if (out)
*out = m_queue.dequeue();
else
m_queue.dequeue();
return true;
}
bool tryPop(T* out) {
QMutexLocker locker(&m_mutex);
if (m_queue.isEmpty() || m_stopped)
return false;
if (out)
*out = m_queue.dequeue();
else
m_queue.dequeue();
return true;
}
void clear() {
QMutexLocker locker(&m_mutex);
m_queue.clear();
}
void stop() {
QMutexLocker locker(&m_mutex);
m_stopped = true;
m_hasData.wakeAll();
}
void reset() {
QMutexLocker locker(&m_mutex);
m_stopped = false;
m_queue.clear();
}
int size() const {
QMutexLocker locker(&m_mutex);
return m_queue.size();
}
private:
mutable QMutex m_mutex;
QWaitCondition m_hasData;
QQueue<T> m_queue;
int m_maxSize = 0;
bool m_stopped = false;
};
using PacketQueue = SerialQueue<QByteArray>;
using FrameQueue = SerialQueue<DataFrame>;
#endif // TACTILEIPC3D_SERIAL_QUEUE_H

View File

@@ -0,0 +1,216 @@
#include "serial_threads.h"
#include <QDebug>
#include <QtGlobal>
SerialReadThread::SerialReadThread(PacketQueue* packetQueue, QObject* parent)
: QThread(parent)
, m_packetQueue(packetQueue) {}
void SerialReadThread::setParseFunc(ParseFunc func) {
QMutexLocker locker(&m_funcMutex);
m_parseFunc = std::move(func);
}
void SerialReadThread::enqueueBytes(const QByteArray& data) {
if (data.isEmpty())
return;
QMutexLocker locker(&m_queueMutex);
m_byteQueue.enqueue(data);
m_dataReady.wakeOne();
}
void SerialReadThread::clear() {
QMutexLocker locker(&m_queueMutex);
m_byteQueue.clear();
m_buffer.clear();
}
void SerialReadThread::stop() {
m_running = false;
m_dataReady.wakeAll();
}
void SerialReadThread::run() {
m_running = true;
while (m_running) {
QByteArray chunk;
{
QMutexLocker locker(&m_queueMutex);
while (m_byteQueue.isEmpty() && m_running)
m_dataReady.wait(&m_queueMutex);
if (!m_running)
break;
if (!m_byteQueue.isEmpty())
chunk = m_byteQueue.dequeue();
}
if (chunk.isEmpty())
continue;
m_buffer.append(chunk);
ParseFunc parseFunc;
{
QMutexLocker locker(&m_funcMutex);
parseFunc = m_parseFunc;
}
if (!parseFunc)
continue;
while (m_running) {
QByteArray packet;
QString error;
const ISerialFormat::ParseResult result = parseFunc(&m_buffer, &packet, &error);
if (result == ISerialFormat::ParseResult::NeedMore)
break;
if (result == ISerialFormat::ParseResult::Invalid) {
emit parseError(error);
continue;
}
if (!packet.isEmpty())
qDebug().noquote() << "Serial packet rawdata:" << QString::fromLatin1(packet.toHex(' '));
if (m_packetQueue)
m_packetQueue->push(packet);
}
}
}
SerialDecodeThread::SerialDecodeThread(PacketQueue* packetQueue, FrameQueue* frameQueue, QObject* parent)
: QThread(parent)
, m_packetQueue(packetQueue)
, m_frameQueue(frameQueue) {}
void SerialDecodeThread::setDecodeFunc(DecodeFunc func) {
QMutexLocker locker(&m_funcMutex);
m_decodeFunc = std::move(func);
}
void SerialDecodeThread::stop() {
m_running = false;
if (m_packetQueue)
m_packetQueue->stop();
}
void SerialDecodeThread::run() {
m_running = true;
while (m_running) {
QByteArray packet;
if (!m_packetQueue || !m_packetQueue->pop(&packet))
break;
DecodeFunc decodeFunc;
{
QMutexLocker locker(&m_funcMutex);
decodeFunc = m_decodeFunc;
}
if (!decodeFunc)
continue;
DataFrame frame;
QString error;
if (!decodeFunc(packet, &frame, &error)) {
emit decodeError(error);
continue;
}
if (m_frameQueue)
m_frameQueue->push(frame);
emit frameAvailable();
}
}
SerialSendWorker::SerialSendWorker(QObject* parent)
: QObject(parent) {
m_pollTimer.setParent(this);
m_pollTimer.setTimerType(Qt::PreciseTimer);
connect(&m_pollTimer, &QTimer::timeout, this, [this]() {
sendRequest_();
});
}
void SerialSendWorker::setTransport(std::unique_ptr<ISerialTransport> transport) {
if (m_transport)
m_transport->disconnect(this);
m_transport = std::move(transport);
if (m_transport) {
connect(m_transport.get(), &ISerialTransport::bytesReceived,
this, &SerialSendWorker::bytesReceived);
}
}
void SerialSendWorker::setBuildRequestFunc(BuildRequestFunc func) {
m_buildRequest = std::move(func);
}
void SerialSendWorker::setConfig(const SerialConfig& config) {
m_config = config;
updatePolling_();
}
void SerialSendWorker::setRequest(const SensorRequest& request) {
m_request = request;
}
bool SerialSendWorker::openTransport(const SerialConfig& config, QString* error) {
m_config = config;
if (!m_transport) {
if (error)
*error = QStringLiteral("Transport missing");
return false;
}
if (m_connected) {
if (error)
error->clear();
updatePolling_();
return true;
}
QString localError;
const bool ok = m_transport->open(m_config, &localError);
m_connected = ok;
updatePolling_();
if (error)
*error = localError;
return ok;
}
void SerialSendWorker::closeTransport() {
if (!m_connected)
return;
m_pollTimer.stop();
if (m_transport)
m_transport->close();
m_connected = false;
}
void SerialSendWorker::requestOnce() {
sendRequest_();
}
void SerialSendWorker::updatePolling_() {
if (!m_connected || m_config.mode != DeviceMode::Slave) {
m_pollTimer.stop();
return;
}
const int interval = qMax(1, m_config.pollIntervalMs);
if (m_pollTimer.isActive())
m_pollTimer.setInterval(interval);
else
m_pollTimer.start(interval);
}
void SerialSendWorker::sendRequest_() {
if (!m_connected || !m_transport || !m_buildRequest)
return;
const QByteArray request = m_buildRequest(m_config, m_request);
if (request.isEmpty())
return;
emit requestBuilt(request);
QString error;
if (!m_transport->writeBytes(request, &error))
emit writeFailed(error);
}

112
src/serial/serial_threads.h Normal file
View File

@@ -0,0 +1,112 @@
#ifndef TACTILEIPC3D_SERIAL_THREADS_H
#define TACTILEIPC3D_SERIAL_THREADS_H
#include <QThread>
#include <QMutex>
#include <QWaitCondition>
#include <QQueue>
#include <QTimer>
#include <QByteArray>
#include <QString>
#include <atomic>
#include <functional>
#include <memory>
#include "serial_format.h"
#include "serial_transport.h"
#include "serial_queue.h"
#include "serial_types.h"
class SerialReadThread : public QThread {
Q_OBJECT
public:
using ParseFunc = std::function<ISerialFormat::ParseResult(QByteArray*, QByteArray*, QString*)>;
explicit SerialReadThread(PacketQueue* packetQueue, QObject* parent = nullptr);
void setParseFunc(ParseFunc func);
void enqueueBytes(const QByteArray& data);
void clear();
void stop();
signals:
void parseError(const QString& error);
protected:
void run() override;
private:
PacketQueue* m_packetQueue = nullptr;
QMutex m_queueMutex;
QWaitCondition m_dataReady;
QQueue<QByteArray> m_byteQueue;
QMutex m_funcMutex;
ParseFunc m_parseFunc;
QByteArray m_buffer;
std::atomic_bool m_running{false};
};
class SerialDecodeThread : public QThread {
Q_OBJECT
public:
using DecodeFunc = std::function<bool(const QByteArray&, DataFrame*, QString*)>;
explicit SerialDecodeThread(PacketQueue* packetQueue, FrameQueue* frameQueue, QObject* parent = nullptr);
void setDecodeFunc(DecodeFunc func);
void stop();
signals:
void frameAvailable();
void decodeError(const QString& error);
protected:
void run() override;
private:
PacketQueue* m_packetQueue = nullptr;
FrameQueue* m_frameQueue = nullptr;
QMutex m_funcMutex;
DecodeFunc m_decodeFunc;
std::atomic_bool m_running{false};
};
class SerialSendWorker : public QObject {
Q_OBJECT
public:
using BuildRequestFunc = std::function<QByteArray(const SerialConfig&, const SensorRequest&)>;
explicit SerialSendWorker(QObject* parent = nullptr);
void setTransport(std::unique_ptr<ISerialTransport> transport);
void setBuildRequestFunc(BuildRequestFunc func);
void setConfig(const SerialConfig& config);
void setRequest(const SensorRequest& request);
bool openTransport(const SerialConfig& config, QString* error);
void closeTransport();
void requestOnce();
signals:
void bytesReceived(const QByteArray& data);
void requestBuilt(const QByteArray& data);
void writeFailed(const QString& error);
private:
void updatePolling_();
void sendRequest_();
SerialConfig m_config;
SensorRequest m_request;
BuildRequestFunc m_buildRequest;
std::unique_ptr<ISerialTransport> m_transport;
QTimer m_pollTimer;
bool m_connected = false;
};
#endif // TACTILEIPC3D_SERIAL_THREADS_H

View File

@@ -0,0 +1,45 @@
#ifndef TACTILEIPC3D_SERIAL_TRANSPORT_H
#define TACTILEIPC3D_SERIAL_TRANSPORT_H
#include <QObject>
#include <QByteArray>
#include "serial_types.h"
class ISerialTransport : public QObject {
Q_OBJECT
public:
explicit ISerialTransport(QObject* parent = nullptr) : QObject(parent) {}
~ISerialTransport() override = default;
virtual bool open(const SerialConfig& config, QString* error) = 0;
virtual void close() = 0;
virtual bool writeBytes(const QByteArray& data, QString* error) = 0;
signals:
void bytesReceived(const QByteArray& data);
};
class NullSerialTransport : public ISerialTransport {
Q_OBJECT
public:
explicit NullSerialTransport(QObject* parent = nullptr) : ISerialTransport(parent) {}
bool open(const SerialConfig& config, QString* error) override {
Q_UNUSED(config)
if (error)
error->clear();
return true;
}
void close() override {}
bool writeBytes(const QByteArray& data, QString* error) override {
Q_UNUSED(data)
if (error)
error->clear();
return true;
}
};
#endif // TACTILEIPC3D_SERIAL_TRANSPORT_H

43
src/serial/serial_types.h Normal file
View File

@@ -0,0 +1,43 @@
#ifndef TACTILEIPC3D_SERIAL_TYPES_H
#define TACTILEIPC3D_SERIAL_TYPES_H
#include <QString>
#include <QtGlobal>
#define DEBUG_MODE 1
enum class DeviceMode {
Master = 0,
Slave = 1,
};
struct SerialConfig {
QString portName;
int baudRate = 115200;
int dataBits = 8;
int stopBits = 1;
QString parity = QStringLiteral("N");
quint8 deviceAddress = 0x01;
DeviceMode mode = DeviceMode::Slave;
int pollIntervalMs = 50;
};
struct SensorRequest {
quint8 functionCode = 0x00;
quint32 startAddress = 0;
quint16 dataLength = 0;
};
struct SensorSpec {
QString model;
QString version;
int rows = 0;
int cols = 0;
float pitch = 0.0f;
float dotRadius = 0.0f;
float rangeMin = 0.0f;
float rangeMax = 0.0f;
};
#endif // TACTILEIPC3D_SERIAL_TYPES_H

114
src/sparkline_plotitem.h Normal file
View File

@@ -0,0 +1,114 @@
#pragma once
#include <QQuickItem>
#include <QColor>
#include <algorithm>
#include <memory>
#include <qcolor.h>
#include <qcontainerfwd.h>
#include <qcoreapplication.h>
#include <qquickitem.h>
#include <qquickwindow.h>
#include <qsgnode.h>
#include <qsize.h>
#include <qtmetamacros.h>
#include <qtypes.h>
#include <unordered_map>
#include "ringbuffer.h"
#include "nice_ticks.h"
class SparklinePlotItem : public QQuickItem {
Q_OBJECT
Q_PROPERTY(int viewCount READ viewCount WRITE setViewCount NOTIFY viewCountChanged)
Q_PROPERTY(qulonglong viewStart READ viewStart WRITE setViewStart NOTIFY viewStartChanged)
Q_PROPERTY(bool follow READ follow WRITE setFollow NOTIFY followChanged)
Q_PROPERTY(QColor lineColor READ lineColor WRITE setLineColor NOTIFY lineColorChanged)
Q_PROPERTY(QColor gridColor READ gridColor WRITE setGridColor NOTIFY gridColorChanged)
Q_PROPERTY(QColor textColor READ textColor WRITE setTextColor NOTIFY textColorChanged)
Q_PROPERTY(int yTickCount READ yTickCount WRITE setYTickCount NOTIFY yTickCountChanged)
Q_PROPERTY(int leftPadding READ leftPadding WRITE setLeftPadding NOTIFY leftPaddingChanged)
Q_PROPERTY(int rightPadding READ rightPadding WRITE setRightPadding NOTIFY rightPaddingChanged)
Q_PROPERTY(int topPadding READ topPadding WRITE setTopPadding NOTIFY topPaddingChanged)
Q_PROPERTY(int bottomPadding READ bottomPadding WRITE setBottomPadding NOTIFY bottomPaddingChanged)
public:
explicit SparklinePlotItem(QQuickItem* parent=nullptr);
Q_INVOKABLE void append(float y);
Q_INVOKABLE void clear();
int viewCount() const { return m_viewCount; }
void setViewCount(int c);
qulonglong viewStart() const { return m_viewStart; }
void setViewStart(qulonglong s);
bool follow() const { return m_follow; }
void setFollow(bool f);
QColor lineColor() const { return m_lineColor; }
void setLineColor(const QColor& c);
QColor gridColor() const { return m_gridColor; }
void setGridColor(const QColor& c);
QColor textColor() const { return m_textColor; }
void setTextColor(const QColor& c);
int yTickCount() const { return m_yTickCount; }
void setYTickCount(int c);
int leftPadding() const { return m_leftPad; }
void setLeftPadding(int v);
int rightPadding() const { return m_rightPad; }
void setRightPadding(int v);
int topPadding() const { return m_topPad; }
void setTopPadding(int v);
int bottomPadding() const { return m_topPad; }
void setBottomPadding(int v);
signals:
void viewCountChanged();
void viewStartChanged();
void followChanged();
void lineColorChanged();
void gridColorChanged();
void textColorChanged();
void yTickCountChanged();
void leftPaddingChanged();
void rightPaddingChanged();
void topPaddingChanged();
void bottomPaddingChanged();
protected:
QSGNode* updatePaintNode(QSGNode* oldNode, UpdatePaintNodeData*) override;
private:
QSGTexture* getTextTexture(const QString& text, const QFont& font, QSize* outSize = nullptr);
private:
std::unique_ptr<RingBuffer<double>> m_buf;
int m_viewCount = 300;
qulonglong m_viewStart = 0;
bool m_follow = true;
QColor m_lineColor = QColor("#39D535");
QColor m_gridColor = QColor(255, 255, 255, 35);
QColor m_textColor = QColor(255, 255, 255, 200);
int m_yTickCount = 5;
int m_leftPad = 40;
int m_rightPad = 0;
int m_topPad = 10;
int m_bottomPad = 18;
struct TexEntry {
QSGTexture* tex = nullptr;
QSize size;
};
std::unordered_map<QString, TexEntry> m_textCache;
};

392
src/sparkling_plotitem.cpp Normal file
View File

@@ -0,0 +1,392 @@
#include "ringbuffer.h"
#include "sparkline_plotitem.h"
#include <QSGGeometryNode>
#include <QSGFlatColorMaterial>
#include <QSGSimpleTextureNode>
#include <QSGTexture>
#include <QPainter>
#include <QImage>
#include <algorithm>
#include <cmath>
#include <cstdint>
#include <iostream>
#include <memory>
#include <qcolor.h>
#include <qcontainerfwd.h>
#include <qfontmetrics.h>
#include <qimage.h>
#include <qnamespace.h>
#include <qpoint.h>
#include <qquickitem.h>
#include <qsggeometry.h>
#include <qsgnode.h>
#include <qsgtexture.h>
#include <qsize.h>
#include <qtypes.h>
#include <qvectornd.h>
#include <vector>
SparklinePlotItem::SparklinePlotItem(QQuickItem* parent)
: QQuickItem(parent) {
setFlag(ItemHasContents, true);
m_buf = std::make_unique<RingBuffer<double>>(10'000'000);
}
void SparklinePlotItem::append(float y) {
m_buf->push(y);
if (m_follow) {
const auto newest = m_buf->newestGlobalIndex();
if (newest + 1 >= (uint64_t)m_viewCount) {
m_viewStart = newest + 1 - (uint64_t)m_viewCount;
}
else {
m_viewStart = 0;
}
emit viewStartChanged();
}
}
void SparklinePlotItem::clear() {
m_textCache.clear();
m_buf = std::make_unique<RingBuffer<double>>(m_buf->capacity());
m_viewStart = 0;
emit viewStartChanged();
update();
}
void SparklinePlotItem::setViewCount(int c) {
c = std::max(10, c);
if (m_viewCount == c)
return;
m_viewCount = c;
emit viewCountChanged();
update();
}
void SparklinePlotItem::setViewStart(qulonglong s) {
if (m_viewStart == s)
return;
m_viewStart = s;
emit viewStartChanged();
update();
}
void SparklinePlotItem::setFollow(bool f) {
if (m_follow == f)
return;
m_follow = f;
emit followChanged();
}
void SparklinePlotItem::setLineColor(const QColor& c) {
if (m_lineColor == c)
return;
m_lineColor = c;
emit lineColorChanged();
update();
}
void SparklinePlotItem::setGridColor(const QColor& c) {
if (m_gridColor == c) {
return;
}
m_gridColor = c;
emit gridColorChanged();
update();
}
void SparklinePlotItem::setTextColor(const QColor& c) {
if (m_textColor == c) {
return;
}
m_textColor = c;
emit textColorChanged();
update();
}
void SparklinePlotItem::setYTickCount(int c) {
c = std::max(3, std::min(8, c));
if (m_yTickCount == c)
return;
m_yTickCount = c;
emit yTickCountChanged();
update();
}
void SparklinePlotItem::setLeftPadding(int v) {
if(m_leftPad==v)
return; m_leftPad=v;
emit leftPaddingChanged();
update();
}
void SparklinePlotItem::setRightPadding(int v){ if(m_rightPad==v) return; m_rightPad=v; emit rightPaddingChanged(); update(); }
void SparklinePlotItem::setTopPadding(int v){ if(m_topPad==v) return; m_topPad=v; emit topPaddingChanged(); update(); }
void SparklinePlotItem::setBottomPadding(int v){ if(m_bottomPad==v) return; m_bottomPad=v; emit bottomPaddingChanged(); update(); }
QSGTexture* SparklinePlotItem::getTextTexture(const QString& text, const QFont& font, QSize* outSize) {
const qreal dpr = window() ? window()->devicePixelRatio() : 1.0;
const QString key = text + "|" + font.family() + "|" + QString::number(font.pixelSize()) +
"|" + QString::number(dpr, 'f', 2);
auto it = m_textCache.find(key);
if (it != m_textCache.end()) {
if (outSize)
*outSize = it->second.size;
return it->second.tex;
}
QFont f = font;
if (f.pixelSize() < 0) {
f.setPixelSize(10);
}
QFontMetrics fm(f);
QSize sz = fm.size(Qt::TextSingleLine, text) + QSize(6, 4);
const QSize pixelSize(qMax(1, qRound(sz.width() * dpr)), qMax(1, qRound(sz.height() * dpr)));
QImage img(pixelSize, QImage::Format_ARGB32_Premultiplied);
img.setDevicePixelRatio(dpr);
img.fill(Qt::transparent);
QPainter p(&img);
p.setRenderHint(QPainter::Antialiasing, true);
p.setFont(f);
p.setPen(m_textColor);
p.drawText(QRect(QPoint(0, 0), sz), Qt::AlignCenter, text);
p.end();
QSGTexture* tex = window()->createTextureFromImage(img);
m_textCache[key] = { tex, sz };
if (outSize)
*outSize = sz;
return tex;
}
static void downsampleMinMax(const RingBuffer<double>& buf, uint64_t startGidx, uint64_t count, int widthPx,
std::vector<QPointF>& out, double& outMinY, double& outMaxY) {
out.clear();
outMinY = 1e30;
outMaxY = -1e30;
if (count < 2 || widthPx <= 2)
return;
const int buckets = std::max(2, widthPx);
const double samplesPerBucket = double(count) / double(buckets);
out.reserve(buckets * 2);
for (int b = 0; b < buckets; ++b) {
uint64_t s0 = startGidx + uint64_t(std::floor(b * samplesPerBucket));
uint64_t s1 = startGidx + uint64_t(std::floor((b + 1) * samplesPerBucket));
if (s1 <= s0) {
s1 = s0 + 1;
}
if (s1 > startGidx + count) {
s1 = startGidx + count;
}
double v;
bool ok = false;
double mn = 1e30;
double mx = -1e30;
for (uint64_t g = s0; g < s1; ++g) {
if (buf.readByGlobalIndex(g, v)) {
ok = true;
mn = std::min(mn, (double)v);
mx = std::max(mx, (double)v);
}
}
if (!ok) {
continue;
}
outMinY = std::min(outMinY, mn);
outMaxY = std::max(outMaxY, mx);
out.emplace_back(double(b), mn);
out.emplace_back(double(b), mx);
}
}
QSGNode* SparklinePlotItem::updatePaintNode(QSGNode* oldNode, UpdatePaintNodeData*) {
if (!window()) {
return oldNode;
}
const qreal dpr = window()->devicePixelRatio();
auto alignToPixel = [dpr](float v) -> float {
return (dpr > 0.0) ? (std::round(v * dpr) / dpr) : v;
};
QSGNode* root = oldNode;
if (!root) {
root = new QSGNode();
}
QSGGeometryNode* gridNode = nullptr;
QSGGeometryNode* lineNode = nullptr;
if (root->childCount() >= 2) {
gridNode = static_cast<QSGGeometryNode*>(root->childAtIndex(0));
lineNode = static_cast<QSGGeometryNode*>(root->childAtIndex(1));
}
else {
gridNode = new QSGGeometryNode();
auto* gridGeom = new QSGGeometry(QSGGeometry::defaultAttributes_Point2D(), 0);
gridGeom->setDrawingMode(QSGGeometry::DrawLines);
gridNode->setGeometry(gridGeom);
gridNode->setFlag(QSGNode::OwnsGeometry);
auto* gridMat = new QSGFlatColorMaterial();
gridMat->setColor(m_gridColor);
gridNode->setMaterial(gridMat);
gridNode->setFlag(QSGNode::OwnsMaterial);
// Line node
lineNode = new QSGGeometryNode();
auto* lineGeom = new QSGGeometry(QSGGeometry::defaultAttributes_Point2D(), 0);
lineGeom->setDrawingMode(QSGGeometry::DrawLineStrip);
lineNode->setGeometry(lineGeom);
lineNode->setFlag(QSGNode::OwnsGeometry);
auto* lineMat = new QSGFlatColorMaterial();
lineMat->setColor(m_lineColor);
lineNode->setMaterial(lineMat);
lineNode->setFlag(QSGNode::OwnsMaterial);
root->appendChildNode(gridNode);
root->appendChildNode(lineNode);
}
static_cast<QSGFlatColorMaterial*>(gridNode->material())->setColor(m_gridColor);
static_cast<QSGFlatColorMaterial*>(lineNode->material())->setColor(m_lineColor);
// 计算绘图区
const float W = float(width());
const float H = float(height());
const float left = float(m_leftPad);
const float right = W - float(m_rightPad);
const float top = float(m_topPad);
const float bottom = H - float(m_bottomPad);
const float plotW = std::max(1.0f, right - left);
const float plotH = std::max(1.0f, bottom - top);
const uint64_t oldest = m_buf->oldestGlobalIndex();
const uint64_t newest = m_buf->newestGlobalIndex();
const uint64_t sz = m_buf->size();
if (sz < 2) {
gridNode->geometry()->allocate(0);
lineNode->geometry()->allocate(0);
return root;
}
// clamp viewStart/viewCount
uint64_t startG = std::max<uint64_t>(oldest, (uint64_t)m_viewStart);
uint64_t count = (uint64_t)m_viewCount;
if (startG + count > newest + 1) {
if (newest + 1 >= count) startG = newest + 1 - count;
else startG = oldest;
}
startG = std::max<uint64_t>(oldest, startG);
// 降采样
std::vector<QPointF> ds;
ds.reserve(int(plotW) * 2);
double rawMinY, rawMaxY;
downsampleMinMax(*m_buf, startG, count, int(plotW), ds, rawMinY, rawMaxY);
if (ds.size() < 2) {
gridNode->geometry()->allocate(0);
lineNode->geometry()->allocate(0);
return root;
}
// 自动 nice ticks
// 增加一点 padding避免贴边
const double pad = (rawMaxY - rawMinY) * 0.08 + 1e-9;
auto ticks = niceTicks(rawMinY - pad, rawMaxY + pad, m_yTickCount);
const double yMin = ticks.niceMin;
const double yMax = ticks.niceMax;
const double yRange = (yMax - yMin != 0) ? (yMax - yMin) : 1.0;
// --- 1) 网格线(水平线) ---
{
auto* geom = gridNode->geometry();
// 每个 tick 画一条水平线 = 2 points
const int n = int(ticks.ticks.size());
geom->allocate(n * 2);
auto* v = geom->vertexDataAsPoint2D();
for (int i = 0; i < n; ++i) {
double tv = ticks.ticks[i];
float yn = float((tv - yMin) / yRange);
float py = alignToPixel(bottom - yn * plotH);
v[i*2 + 0].set(left, py);
v[i*2 + 1].set(right, py);
}
gridNode->markDirty(QSGNode::DirtyGeometry);
}
// --- 2) 折线(注意 min/max per pixel 输出是 [x, min][x,max],可直接 line strip ---
{
auto* geom = lineNode->geometry();
const int vCount = int(ds.size());
geom->allocate(vCount);
auto* v = geom->vertexDataAsPoint2D();
const double xMaxBucket = ds.back().x();
const double xDen = (xMaxBucket > 0) ? xMaxBucket : 1.0;
for (int i = 0; i < vCount; ++i) {
const double bx = ds[i].x();
const double yv = ds[i].y();
float xn = float(bx / xDen);
float yn = float((yv - yMin) / yRange);
float px = left + xn * plotW;
float py = bottom - yn * plotH;
v[i].set(px, py);
}
lineNode->markDirty(QSGNode::DirtyGeometry);
}
// --- 3) Tick 标签 ---
// 先删除旧 label nodes保留 root 的前两个 children
while (root->childCount() > 2) {
delete root->childAtIndex(2);
}
QFont font;
font.setPixelSize(10);
const int nTicks = int(ticks.ticks.size());
for (int i = 0; i < nTicks; ++i) {
const double tv = ticks.ticks[i];
QString label = QString::number(tv, 'g', 4);
QSize logicalSize;
QSGTexture* tex = getTextTexture(label, font, &logicalSize);
if (!tex) continue;
float yn = float((tv - yMin) / yRange);
float py = bottom - yn * plotH;
// 创建 texture node
auto* tnode = new QSGSimpleTextureNode();
tnode->setTexture(tex);
// label 放到左侧 padding 区
float tx = alignToPixel(2.0f);
float ty = alignToPixel(py - logicalSize.height() / 2.0f);
tnode->setRect(QRectF(tx, ty, logicalSize.width(), logicalSize.height()));
root->appendChildNode(tnode);
}
return root;
}

View File

@@ -0,0 +1,45 @@
#include "translation_manager.h"
#include <QCoreApplication>
#include <QDebug>
TranslationManager::TranslationManager(QObject* parent)
: QObject(parent) {
}
bool TranslationManager::setLanguage(const QString& language) {
if (language == m_language) {
return true;
}
if (language.isEmpty()) {
if (m_translator) {
QCoreApplication::removeTranslator(m_translator.get());
m_translator.reset();
}
m_language.clear();
++m_retranslateToken;
emit retranslateTokenChanged();
emit languageChanged();
return true;
}
auto translator = std::make_unique<QTranslator>();
const QString qmPath = QStringLiteral(":/i18n/app_%1.qm").arg(language);
if (!translator->load(qmPath)) {
qWarning() << "Failed to load translation:" << language;
return false;
}
if (m_translator) {
QCoreApplication::removeTranslator(m_translator.get());
}
m_translator = std::move(translator);
QCoreApplication::installTranslator(m_translator.get());
m_language = language;
++m_retranslateToken;
emit retranslateTokenChanged();
emit languageChanged();
return true;
}

31
src/translation_manager.h Normal file
View File

@@ -0,0 +1,31 @@
#ifndef TRANSLATION_MANAGER_H
#define TRANSLATION_MANAGER_H
#include <QObject>
#include <QTranslator>
#include <QString>
#include <memory>
class TranslationManager : public QObject {
Q_OBJECT
Q_PROPERTY(int retranslateToken READ retranslateToken NOTIFY retranslateTokenChanged)
Q_PROPERTY(QString language READ language NOTIFY languageChanged)
public:
explicit TranslationManager(QObject* parent = nullptr);
Q_INVOKABLE bool setLanguage(const QString& language);
int retranslateToken() const { return m_retranslateToken; }
QString language() const { return m_language; }
signals:
void retranslateTokenChanged();
void languageChanged();
private:
std::unique_ptr<QTranslator> m_translator;
QString m_language;
int m_retranslateToken = 0;
};
#endif