完成主要交互、高性能组件、国际化和A型传感器数据包接收
This commit is contained in:
@@ -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);
|
||||
}
|
||||
@@ -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
338
src/data_backend.cpp
Normal 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
83
src/data_backend.h
Normal 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
19
src/data_frame.h
Normal 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
|
||||
273
src/glwidget.cpp
273
src/glwidget.cpp
@@ -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, ¢er))
|
||||
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();
|
||||
}
|
||||
|
||||
@@ -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
53
src/nice_ticks.h
Normal 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
0
src/ringbuffer.cpp
Normal file
82
src/ringbuffer.h
Normal file
82
src/ringbuffer.h
Normal 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
|
||||
241
src/serial/piezoresistive_a_protocol.cpp
Normal file
241
src/serial/piezoresistive_a_protocol.cpp
Normal 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;
|
||||
}
|
||||
32
src/serial/piezoresistive_a_protocol.h
Normal file
32
src/serial/piezoresistive_a_protocol.h
Normal 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
|
||||
358
src/serial/serial_backend.cpp
Normal file
358
src/serial/serial_backend.cpp
Normal 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
114
src/serial/serial_backend.h
Normal 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
23
src/serial/serial_codec.h
Normal 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
|
||||
22
src/serial/serial_decoder.h
Normal file
22
src/serial/serial_decoder.h
Normal 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
|
||||
19
src/serial/serial_format.h
Normal file
19
src/serial/serial_format.h
Normal 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
|
||||
22
src/serial/serial_manager.cpp
Normal file
22
src/serial/serial_manager.cpp
Normal 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);
|
||||
}
|
||||
31
src/serial/serial_manager.h
Normal file
31
src/serial/serial_manager.h
Normal 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
|
||||
140
src/serial/serial_qt_transport.cpp
Normal file
140
src/serial/serial_qt_transport.cpp
Normal 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;
|
||||
}
|
||||
25
src/serial/serial_qt_transport.h
Normal file
25
src/serial/serial_qt_transport.h
Normal 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
97
src/serial/serial_queue.h
Normal 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
|
||||
216
src/serial/serial_threads.cpp
Normal file
216
src/serial/serial_threads.cpp
Normal 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
112
src/serial/serial_threads.h
Normal 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
|
||||
45
src/serial/serial_transport.h
Normal file
45
src/serial/serial_transport.h
Normal 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
43
src/serial/serial_types.h
Normal 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
114
src/sparkline_plotitem.h
Normal 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
392
src/sparkling_plotitem.cpp
Normal 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;
|
||||
}
|
||||
45
src/translation_manager.cpp
Normal file
45
src/translation_manager.cpp
Normal 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
31
src/translation_manager.h
Normal 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
|
||||
Reference in New Issue
Block a user