完成主要交互、高性能组件、国际化和A型传感器数据包接收
This commit is contained in:
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
|
||||
Reference in New Issue
Block a user