文章目录
- 串口线程类
- 初始化串口类
- 打开串口并发送数据
- 析构函数
- 窗口设置
- 窗口函数实现
串口线程类
SlaveThread(从机线程)
目的:等待并响应来自主机的请求,然后发送预设的响应数据。 关键行为: 线程启动后,通过串口监听请求数据。 当检测到有数据可读时,读取请求数据。 读取完毕后,向串口写入预设的响应数据。 如果写入成功,通过信号 request(QString) 发送接收到的请求信息给外部监听者。 如果在读取请求或写入响应时发生超时,会触发 timeout(QString) 信号。 同步机制:使用 QMutex 保证线程安全,检查和更新串口号、等待超时时间以及响应内容,但没有使用 QWaitCondition,因为它是一个被动监听请求的线程,不需要主动唤醒机制。
SlaveThread (从机线程) 示例
假设在一个自动化仓库管理系统中,有多个库存检查站,每个站点装备了一个读取条形码的扫描枪作为从设备(Slave)。这些从设备并不主动发起操作,而是等待仓库管理系统(主设备)的指令来执行任务。
场景描述:任务:当一个物品进入或离开仓库时,仓库管理系统需要记录该物品的条形码信息。 流程:主设备(如中央服务器)通过网络向各个站点的从机发送请求,要求读取条形码。从机(SlaveThread实例)不断监听串口,一旦接收到请求,立刻激活扫描枪读取条形码,然后通过串口将条形码数据作为响应发回给主设备。 特点:在这个场景中,SlaveThread体现为被动响应设备的职责,等待主设备的指令,执行读取任务并反馈结果。
SlaveThread 更适用于那些需要持续监听并响应外部指令的场景,如传感器数据采集、远程控制设备的响应等。(只采集数据,并不需要主动发送请求,而是始终监听并响应数据的到来)
class SlaveThread : public QThread
{
Q_OBJECT
public:
explicit SlaveThread(QObject *parent = nullptr);//
~SlaveThread();
void startSlave(const QString &portName, int waitTimeout, const QString &response);//初始化串口及启动线程
signals:
void request(const QString &s);//请求消息
void error(const QString &s);//串口错误消息
void timeout(const QString &s);//串口连接超时消息
private:
void run() override;//串口通信
QString m_portName;
QString m_response;
int m_waitTimeout = 0;
QMutex m_mutex;
bool m_quit = false;
};
初始化串口类
void SlaveThread::startSlave(const QString &portName, int waitTimeout, const QString &response)
{
const QMutexLocker locker(&m_mutex);//初始化线程锁
m_portName = portName;//设置串口号
m_waitTimeout = waitTimeout;//设置串口的等待时间
m_response = response;//设置串口的相应参数
if (!isRunning())//判断线程是否运行
start();//启动线程
}
打开串口并发送数据
void SlaveThread::run()
{
bool currentPortNameChanged = false;
// 加锁以安全地访问和更新线程间共享的数据
m_mutex.lock();
// 获取并记录当前配置与上次是否有变
QString currentPortName;
if (currentPortName != m_portName) { // 检查端口名是否变更
currentPortName = m_portName;
currentPortNameChanged = true; // 标记端口变更
}
int currentWaitTimeout = m_waitTimeout; // 获取超时时间
QString currentResponse = m_response; // 获取响应信息
m_mutex.unlock(); // 解锁,释放互斥锁
QSerialPort serial; // 创建QSerialPort对象用于串口通信
// 主循环,持续运行直到收到退出信号
while (!m_quit) {
// 如果端口设置发生变更
if (currentPortNameChanged) {
serial.close(); // 关闭旧端口
serial.setPortName(currentPortName); // 设置新端口名
// 尝试打开新的串口,失败则发送错误信号并退出
if (!serial.open(QIODevice::ReadWrite)) {
emit error(tr("无法打开端口 %1, 错误码 %2")
.arg(currentPortName).arg(serial.error()));
return;
}
}
// 等待读取请求数据
if (serial.waitForReadyRead(currentWaitTimeout)) {
// 读取所有可用的请求数据
QByteArray requestData = serial.readAll();
// 处理可能的分包情况,继续读取直到没有更多数据到来
while (serial.waitForReadyRead(10))
requestData += serial.readAll();
// 准备响应数据并发送
QByteArray responseData = currentResponse.toUtf8();
serial.write(responseData);
// 等待响应数据写入完成,成功则发送请求信号,否则发送写入超时信号
if (serial.waitForBytesWritten(m_waitTimeout)) {
QString request = QString::fromUtf8(requestData);
emit request(request);
} else {
emit timeout(tr("等待写入响应超时 %1").arg(QTime::currentTime().toString()));
}
} else {
// 如果等待读取请求超时,发送读取超时信号
emit timeout(tr("等待读取请求超时 %1").arg(QTime::currentTime().toString()));
}
// 再次加锁,检查配置是否更新
m_mutex.lock();
if (currentPortName != m_portName) {
currentPortName = m_portName;
currentPortNameChanged = true;
} else {
currentPortNameChanged = false;
}
currentWaitTimeout = m_waitTimeout; // 更新超时时间
currentResponse = m_response; // 更新响应信息
m_mutex.unlock(); // 解锁
}
}
析构函数
SlaveThread::~SlaveThread()
{
m_mutex.lock();
m_quit = true;
m_mutex.unlock();
wait();
}
窗口设置
#ifndef DIALOG_H
#define DIALOG_H
#include "slavethread.h"
#include <QDialog>
QT_BEGIN_NAMESPACE
class QLabel;
class QLineEdit;
class QComboBox;
class QSpinBox;
class QPushButton;
QT_END_NAMESPACE
class Dialog : public QDialog
{
Q_OBJECT
public:
explicit Dialog(QWidget *parent = nullptr);
private slots:
void startSlave();
void showRequest(const QString &s);
void processError(const QString &s);
void processTimeout(const QString &s);
void activateRunButton();
private:
int m_transactionCount = 0;
QLabel *m_serialPortLabel = nullptr;
QComboBox *m_serialPortComboBox = nullptr;
QLabel *m_waitRequestLabel = nullptr;
QSpinBox *m_waitRequestSpinBox = nullptr;
QLabel *m_responseLabel = nullptr;
QLineEdit *m_responseLineEdit = nullptr;
QLabel *m_trafficLabel = nullptr;
QLabel *m_statusLabel = nullptr;
QPushButton *m_runButton = nullptr;
SlaveThread m_thread;
};
#endif // DIALOG_H
窗口函数实现
#include "dialog.h"
// 引入所需Qt模块的头文件
#include <QComboBox>
#include <QGridLayout>
#include <QLabel>
#include <QLineEdit>
#include <QPushButton>
#include <QSerialPortInfo>
#include <QSpinBox>
// 定义Dialog类,继承自QWidget
Dialog::Dialog(QWidget *parent) :
QDialog(parent),
// 初始化UI组件
m_serialPortLabel(new QLabel(tr("Serial port:"))), // 串口标签
m_serialPortComboBox(new QComboBox), // 串口选择组合框
m_waitRequestLabel(new QLabel(tr("Wait request, msec:"))), // 等待请求时间标签
m_waitRequestSpinBox(new QSpinBox), // 等待请求时间微调框
m_responseLabel(new QLabel(tr("Response:"))), // 响应标签
m_responseLineEdit(new QLineEdit(tr("Hello, I'm Slave."))), // 响应文本框,默认值
m_trafficLabel(new QLabel(tr("No traffic."))), // 流量信息标签
m_statusLabel(new QLabel(tr("Status: Not running."))), // 状态标签
m_runButton(new QPushButton(tr("Start"))) // 开始按钮
{
// 设置等待请求时间的范围和默认值
m_waitRequestSpinBox->setRange(0, 10000);
m_waitRequestSpinBox->setValue(10000);
// 获取并添加所有可用的串口名到组合框中
const auto infos = QSerialPortInfo::availablePorts();
for (const QSerialPortInfo &info : infos)
m_serialPortComboBox->addItem(info.portName());
// 创建主网格布局,并将UI组件添加到布局中
auto mainLayout = new QGridLayout;
mainLayout->addWidget(m_serialPortLabel, 0, 0);
mainLayout->addWidget(m_serialPortComboBox, 0, 1);
mainLayout->addWidget(m_waitRequestLabel, 1, 0);
mainLayout->addWidget(m_waitRequestSpinBox, 1, 1);
mainLayout->addWidget(m_runButton, 0, 2, 2, 1); // 横跨两行
mainLayout->addWidget(m_responseLabel, 2, 0);
mainLayout->addWidget(m_responseLineEdit, 2, 1, 1, 3); // 横跨三列
mainLayout->addWidget(m_trafficLabel, 3, 0, 1, 4); // 横跨四列
mainLayout->addWidget(m_statusLabel, 4, 0, 1, 5); // 横跨五列
setLayout(mainLayout); // 设置对话框的主布局
// 设置窗口标题,并让串口选择框获得焦点
setWindowTitle(tr("Blocking Slave"));
m_serialPortComboBox->setFocus();
// 连接信号与槽函数
// 当按下开始按钮时,启动从机线程
connect(m_runButton, &QPushButton::clicked, this, &Dialog::startSlave);
// 当线程发出请求信号时,显示该请求
connect(&m_thread, &SlaveThread::request, this,&Dialog::showRequest);
// 处理错误信号
connect(&m_thread, &SlaveThread::error, this, &Dialog::processError);
// 处理超时信号
connect(&m_thread, &SlaveThread::timeout, this, &Dialog::processTimeout);
// 当串口选择、等待时间或响应文本改变时,激活或禁用开始按钮
connect(m_serialPortComboBox, QOverload<const QString &>::of(&QComboBox::currentIndexChanged),
this, &Dialog::activateRunButton);
connect(m_waitRequestSpinBox, &QSpinBox::textChanged, this, &Dialog::activateRunButton);
connect(m_responseLineEdit, &QLineEdit::textChanged, this, &Dialog::activateRunButton);
}
// 启动从机线程的槽函数
void Dialog::startSlave()
{
m_runButton->setEnabled(false); // 禁用开始按钮
// 更新状态标签,显示当前连接的端口信息
m_statusLabel->setText(tr("Status: Running, connected to port %1.")
.arg(m_serialPortComboBox->currentText()));
// 启动SlaveThread线程,传递配置参数
m_thread.startSlave(m_serialPortComboBox->currentText(),
m_waitRequestSpinBox->value(),
m_responseLineEdit->text());
}
// 显示请求的槽函数
void Dialog::showRequest(const QString &s)
{
// 更新流量信息,显示当前交易的请求和响应
m_trafficLabel->setText(tr("Traffic, transaction #%1:"
"\n\r-request: %2"
"\n\r-response: %3")
.arg(++m_transactionCount) // 交易计数加一
.arg(s) // 请求内容
.arg(m_responseLineEdit->text())); // 预设响应
}
// 处理错误的槽函数
void Dialog::processError(const QString &s)
{
// 重新激活开始按钮,更新状态标签显示错误信息
activateRunButton();
m_statusLabel->setText(tr("Status: Not running, %1.").arg(s));
m_trafficLabel->setText(tr("No traffic."));
}
// 处理超时的槽函数
void Dialog::processTimeout(const QString &s)
{
// 更新状态标签,显示超时信息
m_statusLabel->setText(tr("Status: Running, %1.").arg(s));
m_trafficLabel->setText(tr("No traffic."));
}
// 激活或禁用开始按钮的槽函数
void Dialog::activateRunButton()
{
m_runButton->setEnabled(true); // 根据输入状态,决定是否启用开始按钮
}