扩展前文的 Qt GUI 调试工具,添加 QTreeView 控件以显示设备数据的层次结构,支持更多 Modbus 功能码(新增 0x05 写单个线圈),并实现详细的 CSV 日志保存功能。代码将基于 Qt 网络编程(QtNetwork),集成 ReferenceClass、PointerClass、SerialPort、ModbusTCPDebugger 和 ModbusConverter,使用 std::string、std::vector、C 风格数组和迭代器,保持异步 I/O 和线程池优化。GUI 将显示串口和 Modbus 数据(功能码 0x01、0x03、0x05、0x06),支持 TCP-to-RTU 转换。代码基于 Windows,提供 Linux 适配说明,Demo 通过 main 函数启动 GUI。
1. 设计目标
QTreeView 控件:
显示设备数据层次结构(如设备 > 寄存器/线圈 > 值)。
支持动态更新,反映串口和 Modbus 数据。
Modbus 功能码:
新增 0x05:写单个线圈。
保持 0x01(读线圈)、0x03(读寄存器)、0x06(写寄存器)。
CSV 日志保存:
将串口和 Modbus 数据(时间戳、HEX、解析值)保存为 CSV 文件。
支持手动触发保存或自动记录。
集成:
ReferenceClass:处理串口数据。
PointerClass:处理 Modbus TCP 数据。
ModbusTCPDebugger:支持新功能码。
ModbusConverter:TCP-to-RTU 转换。
场景:调试工具监控设备状态,发送控制命令,保存日志。
优化:异步 I/O(QTcpSocket)、线程池(QThreadPool)、GUI 实时更新。
2. 前置类调整
以下调整 SerialPort(保持不变)、ModbusConverter(不变)、ReferenceClass、PointerClass 和 ModbusTCPDebugger,支持新功能。
ReferenceClass(添加 CSV 日志)
cpp
#include
#include
#include
#include
#include
#include
#include
#include "SerialPort.h"
class ReferenceClass : public QObject {
Q_OBJECT
private:
std::vector& data;
std::string& buffer;
SerialPort& serial;
QFile logFile;
public:
ReferenceClass(std::vector& d, std::string& b, SerialPort& s, QObject* parent = nullptr)
: QObject(parent), data(d), buffer(b), serial(s), logFile("serial_log.csv") {
if (data.empty()) {
throw std::runtime_error("ReferenceClass: Data vector cannot be empty");
}
if (!logFile.open(QIODevice::WriteOnly | QIODevice::Append)) {
throw std::runtime_error("Failed to open serial log file");
}
if (logFile.size() == 0) {
QTextStream out(&logFile);
out << "Timestamp,Type,Data\n";
}
}
void asyncReadFromSerial(std::function&)> callback) {
serial.asyncRead(data, [this, callback](const std::vector& serialData) {
std::string str(serialData.begin(), serialData.end());
buffer += str;
std::string num;
for (char c : str) {
if (c == ' ') {
if (!num.empty()) data.push_back(std::stoi(num));
num.clear();
} else {
num += c;
}
}
if (!num.empty()) data.push_back(std::stoi(num));
logToCSV("Received", serialData);
callback(serialData);
emit dataUpdated();
});
}
void writeToSerial(const std::string& command) {
std::vector data(command.begin(), command.end());
serial.write(data);
buffer += "Sent: " + command;
logToCSV("Sent", data);
emit dataUpdated();
}
const std::vector& getData() const { return data; }
const std::string& getBuffer() const { return buffer; }
signals:
void dataUpdated();
private:
void logToCSV(const std::string& type, const std::vector& data) {
QTextStream out(&logFile);
QString hexData;
for (auto byte : data) {
hexData += QString("%1 ").arg(byte, 2, 16, QChar('0')).toUpper();
}
out << QDateTime::currentDateTime().toString("yyyy-MM-dd hh:mm:ss") << ","
<< QString::fromStdString(type) << ","
<< hexData << "\n";
logFile.flush();
}
};
调整:
添加 CSV 日志文件(serial_log.csv)。
logToCSV 记录时间戳、类型(发送/接收)、HEX 数据。
PointerClass(支持 0x05)
cpp
#include
#include
#include
#include
#include
#include
#include
#include
#include
class PointerClass : public QObject {
Q_OBJECT
private:
std::vector* data;
std::string* buffer;
std::unique_ptr dynamicArray;
size_t arraySize;
QTcpSocket* tcpSocket;
uint16_t transactionId;
QFile logFile;
public:
PointerClass(std::vector* d, std::string* b, size_t size, const std::string& host, int port, QObject* parent = nullptr)
: QObject(parent), data(d), buffer(b), arraySize(size), tcpSocket(new QTcpSocket(this)), transactionId(0), logFile("modbus_log.csv") {
if (!data || !buffer) {
throw std::runtime_error("PointerClass: Null pointer provided");
}
dynamicArray = std::unique_ptr(new int[size]);
for (size_t i = 0; i < size; ++i) {
dynamicArray[i] = static_cast(i);
}
if (!logFile.open(QIODevice::WriteOnly | QIODevice::Append)) {
throw std::runtime_error("Failed to open modbus log file");
}
if (logFile.size() == 0) {
QTextStream out(&logFile);
out << "Timestamp,Type,FunctionCode,Data\n";
}
tcpSocket->connectToHost(QString::fromStdString(host), port);
connect(tcpSocket, &QTcpSocket::connected, this, &PointerClass::onConnected);
connect(tcpSocket, &QTcpSocket::readyRead, this, &PointerClass::onReadyRead);
connect(tcpSocket, &QTcpSocket::disconnected, this, &PointerClass::onDisconnected);
}
void asyncModbusRequest(uint16_t functionCode, uint16_t startAddr, uint16_t countOrValue,
std::function&)> callback) {
if (tcpSocket->state() != QAbstractSocket::ConnectedState) {
emit errorOccurred("Not connected");
return;
}
std::vector frame = createModbusFrame(functionCode, startAddr, countOrValue);
tcpSocket->write(QByteArray(reinterpret_cast(frame.data()), frame.size()));
*buffer += "Sent Modbus: ";
for (auto it = frame.begin(); it != frame.end(); ++it) {
*buffer += std::to_string(static_cast(*it) & 0xFF) + " ";
}
logToCSV("Sent", functionCode, frame);
connect(this, &PointerClass::responseReceived, this, [=](const std::vector& data) {
logToCSV("Received", functionCode, data);
callback(data);
emit dataUpdated();
}, Qt::SingleShotConnection);
}
void updateDynamicArray(size_t index, int value) {
if (index >= arraySize) {
throw std::out_of_range("Dynamic array index out of range");
}
dynamicArray[index] = value;
}
std::vector parseModbusResponse(const std::vector& response) {
std::vector values;
if (response.size() >= 9) {
uint8_t functionCode = response[7];
if (functionCode == 0x03) {
for (size_t i = 0; i < response[8] / 2; ++i) {
values.push_back((response[9 + 2 * i] << 8) | response[9 + 2 * i + 1]);
data->push_back(values.back());
}
} else if (functionCode == 0x01) {
for (size_t i = 0; i < response[8]; ++i) {
uint8_t byte = response[9 + i];
for (size_t j = 0; j < 8; ++j) {
values.push_back((byte >> j) & 1);
data->push_back(values.back());
}
}
} else if (functionCode == 0x05 || functionCode == 0x06) {
values.push_back((response[10] << 8) | response[11]);
data->push_back(values.back());
}
}
return values;
}
const std::vector& getData() const { return *data; }
const std::string& getBuffer() const { return *buffer; }
signals:
void dataUpdated();
void responseReceived(const std::vector& data);
void errorOccurred(const QString& error);
void connectionStateChanged(bool connected);
private slots:
void onConnected() { emit connectionStateChanged(true); }
void onDisconnected() { emit connectionStateChanged(false); }
void onReadyRead() {
QByteArray response = tcpSocket->readAll();
std::vector data(response.begin(), response.end());
emit responseReceived(data);
}
private:
std::vector createModbusFrame(uint16_t functionCode, uint16_t startAddr, uint16_t countOrValue) {
std::vector frame(12);
transactionId++;
frame[0] = (transactionId >> 8) & 0xFF;
frame[1] = transactionId & 0xFF;
frame[2] = 0x00;
frame[3] = 0x00;
frame[4] = 0x00;
frame[5] = 0x06;
frame[6] = 0x01;
frame[7] = functionCode;
frame[8] = (startAddr >> 8) & 0xFF;
frame[9] = startAddr & 0xFF;
frame[10] = (countOrValue >> 8) & 0xFF;
frame[11] = countOrValue & 0xFF;
return frame;
}
void logToCSV(const std::string& type, uint16_t functionCode, const std::vector& data) {
QTextStream out(&logFile);
QString hexData;
for (auto byte : data) {
hexData += QString("%1 ").arg(byte, 2, 16, QChar('0')).toUpper();
}
out << QDateTime::currentDateTime().toString("yyyy-MM-dd hh:mm:ss") << ","
<< QString::fromStdString(type) << ","
<< QString("0x%1").arg(functionCode, 2, 16, QChar('0')).toUpper() << ","
<< hexData << "\n";
logFile.flush();
}
};
调整:
支持 0x05 响应解析。
添加 CSV 日志(modbus_log.csv),记录功能码和数据。
ModbusTCPDebugger(支持 0x05)
cpp
#include
#include
#include
#include
#include
#include
#include
#include
#include
class ModbusTCPDebugger : public QObject {
Q_OBJECT
private:
QTcpServer* server;
std::vector coils = std::vector(100, false);
std::vector registers = {0x1234, 0x5678, 0x9ABC, 0xDEF0};
std::mutex dataMutex;
std::ofstream logFile;
QThreadPool* threadPool;
public:
ModbusTCPDebugger(const std::string& host, int port, QObject* parent = nullptr)
: QObject(parent), server(new QTcpServer(this)), logFile("modbus_server_log.txt", std::ios::app), threadPool(new QThreadPool(this)) {
threadPool->setMaxThreadCount(4);
if (!server->listen(QHostAddress(QString::fromStdString(host)), port)) {
throw std::runtime_error("Server failed to listen");
}
connect(server, &QTcpServer::newConnection, this, &ModbusTCPDebugger::handleNewConnection);
}
private slots:
void handleNewConnection() {
QTcpSocket* clientSocket = server->nextPendingConnection();
connect(clientSocket, &QTcpSocket::readyRead, this, [=]() {
class ClientTask : public QRunnable {
QTcpSocket* socket;
ModbusTCPDebugger* debugger;
public:
ClientTask(QTcpSocket* s, ModbusTCPDebugger* d) : socket(s), debugger(d) {}
void run() override {
QByteArray data = socket->readAll();
std::vector buffer(data.begin(), data.end());
debugger->logData("Received: ", buffer);
std::vector response;
{
std::lock_guard lock(debugger->dataMutex);
response = debugger->processModbusFrame(buffer);
}
if (!response.empty()) {
socket->write(QByteArray(reinterpret_cast(response.data()), response.size()));
debugger->logData("Sent: ", response);
}
}
};
threadPool->start(new ClientTask(clientSocket, this));
});
connect(clientSocket, &QTcpSocket::disconnected, clientSocket, &QTcpSocket::deleteLater);
}
private:
std::vector processModbusFrame(const std::vector& frame) {
if (frame.size() < 8) return {};
uint16_t transactionId = (frame[0] << 8) | frame[1];
uint16_t protocolId = (frame[2] << 8) | frame[3];
uint16_t length = (frame[4] << 8) | frame[5];
uint8_t unitId = frame[6];
uint8_t functionCode = frame[7];
if (protocolId != 0x0000 || length != frame.size() - 6) return {};
if (functionCode == 0x01 && frame.size() >= 12) { // 读线圈
uint16_t startAddr = (frame[8] << 8) | frame[9];
uint16_t count = (frame[10] << 8) | frame[11];
if (startAddr + count <= coils.size()) {
size_t byteCount = (count + 7) / 8;
std::vector response(9 + byteCount);
response[0] = frame[0]; response[1] = frame[1];
response[2] = 0x00; response[3] = 0x00;
response[4] = 0x00; response[5] = 3 + byteCount;
response[6] = unitId; response[7] = functionCode;
response[8] = byteCount;
for (size_t i = 0; i < byteCount; ++i) {
uint8_t byte = 0;
for (size_t j = 0; j < 8 && (i * 8 + j) < count; ++j) {
byte |= (coils[startAddr + i * 8 + j] ? 1 : 0) << j;
}
response[9 + i] = byte;
}
return response;
}
} else if (functionCode == 0x03 && frame.size() >= 12) { // 读寄存器
uint16_t startAddr = (frame[8] << 8) | frame[9];
uint16_t count = (frame[10] << 8) | frame[11];
if (startAddr + count <= registers.size()) {
std::vector response(9 + 2 * count);
response[0] = frame[0]; response[1] = frame[1];
response[2] = 0x00; response[3] = 0x00;
response[4] = 0x00; response[5] = 3 + 2 * count;
response[6] = unitId; response[7] = functionCode;
response[8] = 2 * count;
for (size_t i = 0; i < count; ++i) {
response[9 + 2 * i] = (registers[startAddr + i] >> 8) & 0xFF;
response[9 + 2 * i + 1] = registers[startAddr + i] & 0xFF;
}
return response;
}
} else if (functionCode == 0x05 && frame.size() >= 12) { // 写单个线圈
uint16_t addr = (frame[8] << 8) | frame[9];
uint16_t value = (frame[10] << 8) | frame[11];
if (addr < coils.size() && (value == 0x0000 || value == 0xFF00)) {
coils[addr] = (value == 0xFF00);
return frame;
}
} else if (functionCode == 0x06 && frame.size() >= 12) { // 写寄存器
uint16_t addr = (frame[8] << 8) | frame[9];
uint16_t value = (frame[10] << 8) | frame[11];
if (addr < registers.size()) {
registers[addr] = value;
return frame;
}
}
std::vector response(9);
response[0] = frame[0]; response[1] = frame[1];
response[2] = 0x00; response[3] = 0x00;
response[4] = 0x00; response[5] = 0x03;
response[6] = unitId; response[7] = functionCode | 0x80;
response[8] = 0x02;
return response;
}
void logData(const std::string& prefix, const std::vector& data) {
std::lock_guard lock(dataMutex);
logFile << prefix;
for (auto it = data.begin(); it != data.end(); ++it) {
logFile << std::hex << std::setw(2) << std::setfill('0') << (static_cast(*it) & 0xFF) << " ";
}
logFile << std::dec << "\n";
logFile.flush();
}
};
调整:
支持 0x05:写单个线圈,设置 coils[addr] 为 true(0xFF00)或 false(0x0000)。
3. 扩展的 Qt GUI(添加 QTreeView)
cpp
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include "ReferenceClass.h"
#include "PointerClass.h"
#include "ModbusTCPDebugger.h"
#include "ModbusConverter.h"
class DebugWindow : public QMainWindow {
Q_OBJECT
private:
ReferenceClass* refClass;
PointerClass* ptrClass;
QTextEdit* logText;
QLineEdit* commandInput;
QComboBox* commandType;
QTableWidget* dataTable;
QTreeView* deviceTree;
QStandardItemModel* treeModel;
QChart* chart;
QChartView* chartView;
QLineSeries* regSeries;
QTimer* updateTimer;
QLabel* statusLabel;
std::vector lastRegValues;
public:
DebugWindow(ReferenceClass* ref, PointerClass* ptr, QWidget* parent = nullptr)
: QMainWindow(parent), refClass(ref), ptrClass(ptr) {
setWindowTitle("Qt Network Debug Tool");
resize(1000, 800);
QWidget* centralWidget = new QWidget(this);
setCentralWidget(centralWidget);
QVBoxLayout* mainLayout = new QVBoxLayout(centralWidget);
// 状态标签
statusLabel = new QLabel("Disconnected", this);
mainLayout->addWidget(statusLabel);
// 日志区域
logText = new QTextEdit(this);
logText->setReadOnly(true);
mainLayout->addWidget(logText);
// 设备树和表格布局
QHBoxLayout* dataLayout = new QHBoxLayout;
deviceTree = new QTreeView(this);
treeModel = new QStandardItemModel(this);
treeModel->setHorizontalHeaderLabels({"Device Data"});
deviceTree->setModel(treeModel);
dataLayout->addWidget(deviceTree);
dataTable = new QTableWidget(10, 3, this);
dataTable->setHorizontalHeaderLabels({"Address", "Register", "Coil"});
dataLayout->addWidget(dataTable);
mainLayout->addLayout(dataLayout);
// 图表
chart = new QtCharts::QChart;
regSeries = new QtCharts::QLineSeries;
chart->addSeries(regSeries);
chart->createDefaultAxes();
chart->setTitle("Register Values");
chartView = new QtCharts::QChartView(chart);
chartView->setRenderHint(QPainter::Antialiasing);
mainLayout->addWidget(chartView);
// 命令输入
QHBoxLayout* inputLayout = new QHBoxLayout;
commandType = new QComboBox(this);
commandType->addItems({"Serial", "Modbus Read Coils (0x01)", "Modbus Read Registers (0x03)", "Modbus Write Coil (0x05)", "Modbus Write Register (0x06)"});
commandInput = new QLineEdit(this);
QPushButton* sendButton = new QPushButton("Send", this);
inputLayout->addWidget(commandType);
inputLayout->addWidget(commandInput);
inputLayout->addWidget(sendButton);
mainLayout->addLayout(inputLayout);
// 定时器
updateTimer = new QTimer(this);
updateTimer->start(500);
// 信号与槽
connect(sendButton, &QPushButton::clicked, this, &DebugWindow::sendCommand);
connect(updateTimer, &QTimer::timeout, this, &DebugWindow::updateUI);
connect(refClass, &ReferenceClass::dataUpdated, this, &DebugWindow::updateUI);
connect(ptrClass, &PointerClass::dataUpdated, this, &DebugWindow::updateUI);
connect(ptrClass, &PointerClass::connectionStateChanged, this, &DebugWindow::updateStatus);
connect(ptrClass, &PointerClass::errorOccurred, this, &DebugWindow::showError);
// 初始请求
refClass->asyncReadFromSerial([this](const std::vector& data) {
logText->append("Serial: " + QString::fromStdString(formatData(data)));
});
ptrClass->asyncModbusRequest(0x03, 0, 4, [this](const std::vector& data) {
logText->append("Modbus: " + QString::fromStdString(formatData(data)));
});
updateTree();
}
private slots:
void sendCommand() {
std::string cmd = commandInput->text().toStdString();
std::stringstream ss(cmd);
std::string type = commandType->currentText().toStdString();
if (type == "Serial") {
refClass->writeToSerial(cmd + "\n");
logText->append("Sent Serial: " + QString::fromStdString(cmd));
} else if (type == "Modbus Read Coils (0x01)") {
int start, count;
ss >> start >> count;
ptrClass->asyncModbusRequest(0x01, start, count, [this](const std::vector& data) {
logText->append("Modbus Coils: " + QString::fromStdString(formatData(data)));
});
} else if (type == "Modbus Read Registers (0x03)") {
int start, count;
ss >> start >> count;
ptrClass->asyncModbusRequest(0x03, start, count, [this](const std::vector& data) {
logText->append("Modbus Registers: " + QString::fromStdString(formatData(data)));
});
} else if (type == "Modbus Write Coil (0x05)") {
int addr, value;
ss >> addr >> value;
ptrClass->asyncModbusRequest(0x05, addr, value ? 0xFF00 : 0x0000, [this](const std::vector& data) {
logText->append("Modbus Write Coil: " + QString::fromStdString(formatData(data)));
});
// TCP-to-RTU
std::vector tcpFrame = ptrClass->createModbusFrame(0x05, addr, value ? 0xFF00 : 0x0000);
std::vector rtuFrame = ModbusConverter::tcpToRtu(tcpFrame);
refClass->writeToSerial(std::string(rtuFrame.begin(), rtuFrame.end()));
} else if (type == "Modbus Write Register (0x06)") {
int addr, value;
ss >> addr >> value;
ptrClass->asyncModbusRequest(0x06, addr, value, [this](const std::vector& data) {
logText->append("Modbus Write Register: " + QString::fromStdString(formatData(data)));
});
// TCP-to-RTU
std::vector tcpFrame = ptrClass->createModbusFrame(0x06, addr, value);
std::vector rtuFrame = ModbusConverter::tcpToRtu(tcpFrame);
refClass->writeToSerial(std::string(rtuFrame.begin(), rtuFrame.end()));
}
commandInput->clear();
updateTree();
}
void updateUI() {
updateTable();
updateChart(ptrClass->getData());
updateTree();
}
void updateStatus(bool connected) {
statusLabel->setText(connected ? "Connected" : "Disconnected");
statusLabel->setStyleSheet(connected ? "color: green" : "color: red");
}
void showError(const QString& error) {
logText->append("Error: " + error);
}
private:
void updateTable() {
const auto& data = ptrClass->getData();
for (size_t i = 0; i < std::min(data.size(), 10); ++i) {
dataTable->setItem(i, 0, new QTableWidgetItem(QString::number(i)));
dataTable->setItem(i, 1, new QTableWidgetItem(QString::number(data[i])));
ptrClass->asyncModbusRequest(0x01, i, 1, [this, i](const std::vector& data) {
auto values = ptrClass->parseModbusResponse(data);
if (!values.empty()) {
dataTable->setItem(i, 2, new QTableWidgetItem(QString::number(values[0])));
}
});
}
}
void updateChart(const std::vector& values) {
if (values == lastRegValues) return;
lastRegValues = values;
regSeries->clear();
for (size_t i = 0; i < values.size(); ++i) {
regSeries->append(i, values[i]);
}
chart->axes(Qt::Horizontal)->first()->setRange(0, values.size());
chart->axes(Qt::Vertical)->first()->setRange(0, *std::max_element(values.begin(), values.end()) + 100);
}
void updateTree() {
treeModel->clear();
treeModel->setHorizontalHeaderLabels({"Device Data"});
QStandardItem* root = new QStandardItem("Device");
QStandardItem* serialNode = new QStandardItem("Serial Data");
QStandardItem* modbusNode = new QStandardItem("Modbus Data");
root->appendRow(serialNode);
root->appendRow(modbusNode);
// 串口数据
const auto& serialData = refClass->getData();
for (size_t i = 0; i < serialData.size(); ++i) {
serialNode->appendRow(new QStandardItem(QString("Value %1: %2").arg(i).arg(serialData[i])));
}
// Modbus 数据
QStandardItem* regNode = new QStandardItem("Registers");
QStandardItem* coilNode = new QStandardItem("Coils");
modbusNode->appendRow(regNode);
modbusNode->appendRow(coilNode);
const auto& modbusData = ptrClass->getData();
for (size_t i = 0; i < std::min(modbusData.size(), 4); ++i) {
regNode->appendRow(new QStandardItem(QString("Reg %1: %2").arg(i).arg(modbusData[i])));
}
ptrClass->asyncModbusRequest(0x01, 0, 8, [this](const std::vector& data) {
auto values = ptrClass->parseModbusResponse(data);
QStandardItem* coilNode = treeModel->item(0)->child(1)->child(1);
for (size_t i = 0; i < values.size(); ++i) {
coilNode->appendRow(new QStandardItem(QString("Coil %1: %2").arg(i).arg(values[i])));
}
});
treeModel->appendRow(root);
deviceTree->expandAll();
}
std::string formatData(const std::vector& data) {
std::stringstream ss;
for (auto byte : data) {
ss << std::hex << std::setw(2) << std::setfill('0') << (static_cast(byte) & 0xFF) << " ";
}
return ss.str();
}
};
4. Main 函数
cpp
#include
#include "ReferenceClass.h"
#include "PointerClass.h"
#include "ModbusTCPDebugger.h"
#include "ModbusConverter.h"
int main(int argc, char* argv[]) {
QApplication app(argc, argv);
try {
std::vector data = {10, 20, 30};
std::string buffer = "Initial";
SerialPort serial("COM3", CBR_9600);
ModbusTCPDebugger server("0.0.0.0", 502);
ReferenceClass refClass(data, buffer, serial);
PointerClass ptrClass(&data, &buffer, 3, "127.0.0.1", 502);
DebugWindow window(&refClass, &ptrClass);
window.show();
return app.exec();
} catch (const std::exception& e) {
std::cerr << "Error: " << e.what() << std::endl;
return 1;
}
}
5. 功能说明
QTreeView:
结构:Device > Serial Data > Value X 和 Device > Modbus Data > Registers/Coils > Reg X/Coil X。
动态更新:通过 updateTree 反映串口和 Modbus 数据。
使用 QStandardItemModel 管理树形数据。
Modbus 功能码:
0x05:输入 addr value(如 0 1 置线圈为 true),生成 0xFF00 或 0x0000。
支持 TCP-to-RTU 转换,RTU 帧通过串口发送。
0x01、0x03、0x06 保持不变。
CSV 日志:
serial_log.csv:记录串口数据(时间戳、类型、HEX)。
Timestamp,Type,Data
2025-06-03 19:19:00,Received,01 02 03
2025-06-03 19:19:01,Sent,53 54 41 52 54 0A
modbus_log.csv:记录 Modbus 数据(时间戳、类型、功能码、HEX)。
Timestamp,Type,FunctionCode,Data
2025-06-03 19:19:00,Sent,0x03,00 01 00 00 00 06 01 03 00 00 00 04
2025-06-03 19:19:00,Received,0x03,00 01 00 00 00 0B 01 03 08 12 34 56 78 9A BC DE F0
GUI:
布局:状态标签、日志区域、树形视图/表格并列、图表、命令输入。
交互:选择命令类型,输入参数(如 0 4 读 4 个寄存器),点击发送。
实时更新:QTimer 和信号触发 updateUI。
6. 输出示例
假设串口返回 "1 2 3",Modbus 返回寄存器 {0x1234, 0x5678, 0x9ABC, 0xDEF0},线圈 {0, 1, 0, 0}:
日志区域:
Serial: 01 02 03
Modbus Registers: 00 01 00 00 00 0B 01 03 08 12 34 56 78 9A BC DE F0
Modbus Coils: 00 02 00 00 00 04 01 01 01 02
树形视图:
Device
├── Serial Data
│ ├── Value 0: 10
│ ├── Value 1: 20
│ ├── Value 2: 30
│ ├── Value 3: 1
│ ├── Value 4: 2
│ └── Value 5: 3
└── Modbus Data
├── Registers
│ ├── Reg 0: 4660
│ ├── Reg 1: 22136
│ ├── Reg 2: 39612
│ └── Reg 3: 57088
└── Coils
├── Coil 0: 0
├── Coil 1: 1
├── Coil 2: 0
└── Coil 3: 0
表格:
Address | Register | Coil
0 | 4660 | 0
1 | 22136 | 1
2 | 39612 | 0
3 | 57088 | 0
图表:显示寄存器曲线(4660, 22136, 39612, 57088)。
7. Linux 适配
Qt:确保安装 libqt5widgets5-dev, libqt5charts5-dev, libqt5network5-dev.
SerialPort:替换 Windows API 为 POSIX(open, read, write)。
QtNetwork:跨平台,无需修改。
编译:
g++ -o app main.cpp ReferenceClass.cpp PointerClass.cpp SerialPort.cpp ModbusTCPDebugger.cpp ModbusConverter.cpp -std=c++11 -pthread $(pkg-config --cflags --libs Qt5Widgets Qt5Charts Qt5Network)
桥接:使用 socat:
socat TCP-LISTEN:12345,reuseaddr /dev/ttyUSB0,raw,echo=0,b9600
8. 扩展建议
QTreeView 增强:
支持双击编辑寄存器/线圈值,触发 0x05/0x06。
添加上下文菜单(右键保存节点数据)。
更多功能码:
0x0F:写多个线圈。
0x10:写多个寄存器。
CSV 增强:
支持按时间范围导出(使用 QDateTimeEdit)。
添加解析值列(如寄存器值)。
其他控件:
QProgressBar:显示数据传输进度。
QTabWidget:分页显示串口/Modbus 数据。
9. 注意事项
线程安全:Qt 信号与槽确保 UI 主线程更新,std::mutex 保护服务器数据。
性能:QTreeView 大数据时可使用自定义模型优化。
CSV 文件:定期关闭/重新打开避免占用。
如果你需要 QTreeView 编辑功能、更多功能码(如 0x0F)、CSV 按时间导出,或其他控件(如 QProgressBar),请提供细节,我可以进一步定制!