|
- #include "plc_communication_service.h"
-
- #include "plc_communication_error_classifier.h"
- #include "plc_register_repository.h"
-
- #include <QModbusDataUnit>
- #include <QModbusDevice>
- #include <QModbusReply>
- #include <QModbusRtuSerialMaster>
- #include <QSerialPort>
- #include <QVariant>
-
- #include <algorithm>
- #include <utility>
-
- namespace {
-
- // 故障恢复时,两次轻量探测之间等待的时间
- constexpr int kRecoveryProbeIntervalMs = 2000;
-
- // Qt 使用 QString,项目内部错误信息统一使用 UTF-8 std::string
- std::string toUtf8(const QString &value)
- {
- const QByteArray bytes = value.toUtf8();
- return std::string(bytes.constData(), static_cast<std::size_t>(bytes.size()));
- }
-
- // M 区在 Modbus 中按线圈读取,D 区按保持寄存器读取
- QModbusDataUnit::RegisterType registerType(RegisterArea area)
- {
- return area == RegisterArea::M
- ? QModbusDataUnit::Coils : QModbusDataUnit::HoldingRegisters;
- }
-
- // 只有超时和 PLC 不响应属于可以自动探测恢复的故障
- bool isRecoverableTimeout(PlcCommunicationError error)
- {
- return error == PlcCommunicationError::PlcNotResponding
- || error == PlcCommunicationError::CommunicationTimeout;
- }
-
- // Connected 正常轮询,Recovering 正在恢复后的重新首读
- bool isReadingState(PlcConnectionState state)
- {
- return state == PlcConnectionState::Connected
- || state == PlcConnectionState::Recovering;
- }
-
- // 排序并去重轮询地址,同时检查地址总量上限
- bool normalizePollAddresses(
- const std::vector<RegisterAddress> &addresses,
- std::vector<RegisterAddress> *normalized,
- std::string *error)
- {
- // 先复制,后续排序和去重不能改变调用方传入的地址列表
- *normalized = addresses;
- if (std::any_of(
- normalized->cbegin(), normalized->cend(),
- [](const RegisterAddress &address) { return !address.isValid(); }))
- {
- *error = "PLC 轮询地址中包含无效的 M/D 地址";
- return false;
- }
- std::sort(
- normalized->begin(), normalized->end(),
- [](const RegisterAddress &left, const RegisterAddress &right)
- {
- return left.area() == right.area()
- ? left.index() < right.index()
- : left.area() == RegisterArea::M;
- });
- normalized->erase(
- std::unique(normalized->begin(), normalized->end()), normalized->end());
- if (normalized->size() > ProjectLimits::kMaximumPollAddresses)
- {
- *error = "PLC 轮询的去重 M/D 地址最多为 256 个";
- return false;
- }
- return true;
- }
-
- // 判断读块边界是否落在多字范围内部,避免拆开一次多字读取
- bool boundaryBelongsToMultiWordRange(
- RegisterArea area,
- int boundary,
- const std::vector<RegisterWordRange> &ranges)
- {
- return std::any_of(
- ranges.cbegin(), ranges.cend(),
- [area, boundary](const RegisterWordRange &range)
- {
- return range.start.area() == area
- && range.start.index() <= boundary
- && boundary < range.start.index() + range.wordCount - 1;
- });
- }
-
- // 将地址集合拆成符合 Modbus 单次读取上限的连续读块
- bool calculatePollBlocks(
- const std::vector<RegisterAddress> &addresses,
- const std::vector<RegisterWordRange> &multi_word_ranges,
- std::vector<PlcPollBlock> *blocks)
- {
- blocks->clear();
- if (addresses.empty())
- {
- blocks->push_back({RegisterArea::M, 0, 1});
- blocks->push_back({RegisterArea::D, 0, 1});
- return true;
- }
-
- // 先处理同一区域内连续地址,再按单次读取上限切分
- std::size_t run_start = 0U;
- while (run_start < addresses.size())
- {
- std::size_t run_end = run_start;
- while (run_end + 1U < addresses.size()
- && addresses[run_end + 1U].area() == addresses[run_start].area()
- && addresses[run_end + 1U].index() == addresses[run_end].index() + 1)
- {
- ++run_end;
- }
- const RegisterArea area = addresses[run_start].area();
- int cursor = addresses[run_start].index();
- const int last = addresses[run_end].index();
- while (cursor <= last)
- {
- int block_end = std::min(
- cursor + ProjectLimits::kMaximumModbusReadCount - 1, last);
- while (block_end < last
- && boundaryBelongsToMultiWordRange(
- area, block_end, multi_word_ranges))
- {
- --block_end;
- }
- if (block_end < cursor)
- {
- blocks->clear();
- return false;
- }
- blocks->push_back({area, cursor, block_end - cursor + 1});
- cursor = block_end + 1;
- }
- run_start = run_end + 1U;
- }
- return true;
- }
-
- } // namespace
-
- // 创建 Qt Modbus 主站,绑定异步信号和 PLC 缓存写入回调
- PlcCommunicationService::PlcCommunicationService(
- PlcRegisterRepository &repository,
- QObject *parent)
- : QObject(parent),
- repository_(repository),
- master_(std::make_unique<QModbusRtuSerialMaster>())
- {
- // 仓库只负责缓存;写入请求通过这里注入的回调转给通信服务
- repository_.setWriteHandlers(
- [this](const RegisterAddress &address, bool value)
- {
- // PLC 仓库收到 M 写入时转成 Modbus 线圈单写请求
- return sendBitWrite(address, value);
- },
- [this](const RegisterAddress &address, std::int16_t value)
- {
- // PLC 仓库收到单个 D 写入时转成保持寄存器单写请求
- return sendWordWrite(address, value);
- },
- [this](const RegisterAddress &address,
- const std::vector<std::int16_t> &values)
- {
- // PLC 仓库收到连续 D 写入时转成保持寄存器多写请求
- return sendWordsWrite(address, values);
- });
- // 轮询定时器每次只推动一个读块,避免一次压入大量异步请求
- connect(&poll_timer_, &QTimer::timeout, this, &PlcCommunicationService::pollNextBlock);
- recovery_timer_.setSingleShot(true);
- // PLC 超时进入 Faulted 后,由恢复定时器调用轻量探测入口
- connect(
- &recovery_timer_,
- &QTimer::timeout,
- this,
- &PlcCommunicationService::probeRecovery);
- // Qt 串口状态变化是异步通知,所有连接状态都从这里统一处理
- connect(master_.get(), &QModbusClient::stateChanged,
- this,
- [this](QModbusDevice::State device_state)
- {
- if (device_state == QModbusDevice::ConnectedState)
- {
- // 串口连接成功后立即开始轮询,并不代表首读已经完成
- serial_session_opened_ = true;
- setState(PlcConnectionState::Connected);
- poll_timer_.start(configuration_.pollIntervalMs);
- // 串口连上后立即异步读取第一个 M/D 地址块
- pollNextBlock();
- }
- else if (device_state == QModbusDevice::ConnectingState)
- {
- serial_session_opened_ = false;
- setState(PlcConnectionState::Connecting);
- }
- else if (device_state == QModbusDevice::UnconnectedState)
- {
- // 主动断开和意外断线要区分,后者需要报告故障
- if (disconnecting_)
- {
- serial_session_opened_ = false;
- }
- else if (serial_session_opened_)
- {
- handleUnexpectedDisconnect();
- }
- else if (state_ != PlcConnectionState::Faulted)
- {
- setState(PlcConnectionState::Disconnected);
- }
- }
- });
- // Qt 报告底层错误时,转换为项目自己的错误和状态
- connect(master_.get(), &QModbusClient::errorOccurred,
- this,
- [this](QModbusDevice::Error error)
- {
- if (error != QModbusDevice::NoError)
- {
- // 底层 Modbus 错误统一交给项目错误分类和恢复流程
- handleModbusError(error);
- }
- });
- }
-
- // 释放通信服务持有的 Qt 资源
- PlcCommunicationService::~PlcCommunicationService() = default;
-
- // 校验串口配置并启动一次新的 PLC 异步连接
- PlcCommunicationResult PlcCommunicationService::connectDevice(
- const PlcSerialConfiguration &configuration)
- {
- const PlcCommunicationResult validation =
- validatePlcSerialConfiguration(configuration);
- if (!validation.succeeded)
- {
- return validation;
- }
- if (state_ == PlcConnectionState::Disconnected
- && master_->state() != QModbusDevice::UnconnectedState)
- {
- closeSerialSession();
- }
- if (master_->state() != QModbusDevice::UnconnectedState)
- {
- return {false, "PLC 连接已经启动,请先断开当前连接"};
- }
- // 每次连接都递增代次;旧连接的异步回复即使晚到也不能污染新缓存
- configuration_ = configuration;
- ++connection_generation_;
- disconnecting_ = false;
- serial_session_opened_ = false;
- received_valid_response_ = false;
- last_error_type_ = PlcCommunicationError::None;
- last_error_.clear();
- // 把项目配置转换成 Qt 串口参数
- master_->setConnectionParameter(
- QModbusDevice::SerialPortNameParameter,
- QString::fromStdString(configuration.portName));
- master_->setConnectionParameter(
- QModbusDevice::SerialBaudRateParameter, configuration.baudRate);
- master_->setConnectionParameter(
- QModbusDevice::SerialDataBitsParameter,
- static_cast<QSerialPort::DataBits>(configuration.dataBits));
- master_->setConnectionParameter(
- QModbusDevice::SerialParityParameter,
- static_cast<QSerialPort::Parity>(configuration.parity));
- master_->setConnectionParameter(
- QModbusDevice::SerialStopBitsParameter,
- static_cast<QSerialPort::StopBits>(configuration.stopBits));
- master_->setTimeout(configuration.responseTimeoutMs);
- master_->setNumberOfRetries(configuration.retries);
- // 新连接必须重新完成完整首读,不能沿用上一条连接的运行资格
- repository_.invalidate();
- updateInitialReadCompleted(false, true);
- rebuildPollBlocks();
- setState(PlcConnectionState::Connecting);
- // 只发起异步串口连接,连接结果由 Qt 状态回调继续推进
- if (!master_->connectDevice())
- {
- if (last_error_.empty())
- {
- const QModbusDevice::Error error = master_->error() == QModbusDevice::NoError
- ? QModbusDevice::ConnectionError : master_->error();
- // 串口连接请求启动失败也进入统一错误分类和状态更新
- handleModbusError(error);
- }
- return {false, last_error_};
- }
- return {true, {}};
- }
-
- // 停止定时器、关闭串口并清除本次连接的缓存有效状态
- void PlcCommunicationService::disconnectDevice()
- {
- // 关闭串口、清除缓存有效标记,防止断开后继续使用旧值
- closeSerialSession();
- repository_.invalidate();
- updateInitialReadCompleted(false, true);
- last_error_type_ = PlcCommunicationError::None;
- last_error_.clear();
- setState(PlcConnectionState::Disconnected);
- }
-
- // 使用默认的单字范围设置轮询地址
- PlcCommunicationResult PlcCommunicationService::setPollAddresses(
- const std::vector<RegisterAddress> &addresses)
- {
- return setPollAddresses(addresses, {});
- }
-
- // 校验并应用地址及多字范围,必要时等待当前异步读请求结束
- PlcCommunicationResult PlcCommunicationService::setPollAddresses(
- const std::vector<RegisterAddress> &addresses,
- const std::vector<RegisterWordRange> &multi_word_ranges)
- {
- std::vector<RegisterAddress> normalized;
- std::string error;
- if (!normalizePollAddresses(addresses, &normalized, &error))
- {
- return {false, error};
- }
- std::vector<RegisterWordRange> normalized_ranges = multi_word_ranges;
- std::sort(
- normalized_ranges.begin(), normalized_ranges.end(),
- [](const RegisterWordRange &left, const RegisterWordRange &right)
- {
- return left.start.index() == right.start.index()
- ? left.wordCount < right.wordCount
- : left.start.index() < right.start.index();
- });
- normalized_ranges.erase(
- std::unique(
- normalized_ranges.begin(), normalized_ranges.end()),
- normalized_ranges.end());
- if (std::any_of(
- normalized_ranges.cbegin(), normalized_ranges.cend(),
- [](const RegisterWordRange &range)
- {
- return !range.start.isValid()
- || range.start.area() != RegisterArea::D
- || range.wordCount < 2 || range.wordCount > 4
- || range.start.index()
- > RegisterAddress::kMaximumIndex - range.wordCount + 1;
- }))
- {
- return {false, "多字轮询范围必须是 D 区内连续的 2~4 个字"};
- }
- for (const RegisterWordRange &range : normalized_ranges)
- {
- for (int offset = 0; offset < range.wordCount; ++offset)
- {
- normalized.push_back(RegisterAddress{
- RegisterArea::D, range.start.index() + offset});
- }
- }
- std::sort(
- normalized.begin(), normalized.end(),
- [](const RegisterAddress &left, const RegisterAddress &right)
- {
- return left.area() == right.area()
- ? left.index() < right.index()
- : left.area() == RegisterArea::M;
- });
- normalized.erase(std::unique(normalized.begin(), normalized.end()), normalized.end());
- if (normalized.size() > ProjectLimits::kMaximumPollAddresses)
- {
- return {false, "PLC 轮询的去重 M/D 地址最多为 256 个"};
- }
- std::vector<PlcPollBlock> calculated_blocks;
- if (!calculatePollBlocks(normalized, normalized_ranges, &calculated_blocks))
- {
- return {false, "重叠的多字范围无法在单次 120 字读取边界内完整轮询"};
- }
- if (calculated_blocks.size() > ProjectLimits::kMaximumPollBlocks)
- {
- return {false, "PLC 轮询地址拆分后最多允许 8 个读块"};
- }
- // 当前读请求完成前暂存新集合,避免按半旧半新的地址解析回复
- if (pending_reply_ != nullptr)
- {
- // 当前回复仍在解析旧集合,等它结束后再切换到新集合
- pending_poll_addresses_ = std::move(normalized);
- pending_poll_multi_word_ranges_ = std::move(normalized_ranges);
- poll_update_pending_ = true;
- return {true, {}};
- }
- poll_multi_word_ranges_ = std::move(normalized_ranges);
- applyPollAddresses(normalized);
- return {true, {}};
- }
-
- // 返回当前 PLC 连接状态
- PlcConnectionState PlcCommunicationService::state() const
- {
- return state_;
- }
-
- // 返回当前连接是否已完成全部轮询块的首次读取
- bool PlcCommunicationService::initialReadCompleted() const
- {
- return initial_read_completed_;
- }
-
- // 返回最近一次通信错误的统一类型
- PlcCommunicationError PlcCommunicationService::lastErrorType() const
- {
- return last_error_type_;
- }
-
- // 返回最近一次通信错误的可读文字
- const std::string &PlcCommunicationService::lastError() const
- {
- return last_error_;
- }
-
- // 保存外部回调,在对应通信事件发生时通知调用方
- void PlcCommunicationService::setCallbacks(
- std::function<void()> state_changed,
- std::function<void(bool)> initial_read_changed,
- std::function<void()> cache_updated,
- std::function<void()> poll_cycle_completed,
- std::function<void(const std::string &)> error_reported)
- {
- // 保存外部通知函数,通信事件发生时再调用对应函数
- state_changed_callback_ = std::move(state_changed); // 通信状态变化时通知外部
- initial_read_changed_callback_ = std::move(initial_read_changed); // 首读资格变化时通知外部
- cache_updated_callback_ = std::move(cache_updated); // PLC 缓存更新后通知外部
- poll_cycle_completed_callback_ = std::move(poll_cycle_completed);
- error_reported_callback_ = std::move(error_reported); // 发生通信错误时通知外部
- }
-
- // 根据当前地址集合重新生成连续轮询读块
- void PlcCommunicationService::rebuildPollBlocks()
- {
- std::vector<RegisterAddress> addresses = poll_addresses_;
- if (addresses.empty())
- {
- // 没有指定地址时保留 M0/D0,保证连接后仍能验证设备是否响应
- addresses = {
- RegisterAddress{RegisterArea::M, 0},
- RegisterAddress{RegisterArea::D, 0}};
- }
- std::sort(
- addresses.begin(), addresses.end(),
- [](const RegisterAddress &left, const RegisterAddress &right)
- {
- if (left.area() != right.area())
- {
- return left.area() == RegisterArea::M;
- }
- return left.index() < right.index();
- });
- addresses.erase(std::unique(addresses.begin(), addresses.end()), addresses.end());
- std::vector<PlcPollBlock> calculated_blocks;
- calculatePollBlocks(addresses, poll_multi_word_ranges_, &calculated_blocks);
- poll_blocks_.clear();
- poll_blocks_.reserve(calculated_blocks.size());
- for (const PlcPollBlock &block : calculated_blocks)
- {
- poll_blocks_.push_back(block);
- }
- next_poll_block_ = 0;
- if (!initial_read_completed_)
- {
- // 地址集合变化或重新连接后,首读资格从头统计
- initial_blocks_read_.assign(poll_blocks_.size(), false);
- }
- else
- {
- initial_blocks_read_.clear();
- }
- }
-
- // 在没有读请求占用时正式切换到新的轮询地址集合
- void PlcCommunicationService::applyPollAddresses(
- const std::vector<RegisterAddress> &addresses)
- {
- // 只有在没有读请求占用时,新的地址集合才会真正生效
- poll_addresses_ = addresses;
- rebuildPollBlocks();
- poll_update_pending_ = false;
- pending_poll_addresses_.clear();
- pending_poll_multi_word_ranges_.clear();
- if (isReadingState(state_) && pending_reply_ == nullptr)
- {
- // 新轮询地址生效后立即从第一个读块继续异步轮询
- pollNextBlock();
- }
- }
-
- // 发送一个异步轮询读请求,并由完成回调推进后续读块
- void PlcCommunicationService::pollNextBlock()
- {
- if (!isReadingState(state_)
- || pending_reply_ != nullptr || poll_blocks_.empty())
- {
- return;
- }
- // 每次只发一个异步请求,完成回调中再推进到下一个块
- const std::size_t block_index = next_poll_block_;
- const PlcPollBlock block = poll_blocks_.at(block_index);
- next_poll_block_ = (next_poll_block_ + 1U) % poll_blocks_.size();
- QModbusDataUnit request(
- registerType(block.area), block.startAddress, static_cast<quint16>(block.count));
- // 发出当前 M 或 D 地址块的异步 Modbus 读取请求
- QModbusReply *reply = master_->sendReadRequest(request, configuration_.serverAddress);
- if (reply == nullptr)
- {
- // 读请求未能创建时按通信故障撤销首读资格
- handleModbusError(master_->error());
- return;
- }
- pending_reply_ = reply;
- // 把当前连接代次带进回调,防止断线重连后旧回复误更新新连接
- const std::uint64_t generation = connection_generation_;
- connect(reply, &QModbusReply::finished,
- this,
- [this, reply, block, block_index, generation]
- {
- // 连接已经重建时,直接丢弃旧回复
- if (generation != connection_generation_)
- {
- reply->deleteLater();
- return;
- }
- // 读取完成后解析回复并只用成功读回值更新 PLC 缓存
- handleReadFinished(reply, block);
- if (isReadingState(state_)
- && reply->error() == QModbusDevice::NoError
- && !poll_update_pending_
- && block_index < initial_blocks_read_.size())
- {
- initial_blocks_read_[block_index] = true;
- const bool completed = std::all_of(
- initial_blocks_read_.cbegin(),
- initial_blocks_read_.cend(),
- [](bool read) { return read; });
- // 所有轮询块都成功读过,才授予真机运行资格
- if (completed && !initial_read_completed_)
- {
- updateInitialReadCompleted(true);
- if (state_ == PlcConnectionState::Recovering)
- {
- setState(PlcConnectionState::Connected);
- }
- }
- }
- const bool poll_cycle_completed = isReadingState(state_)
- && reply->error() == QModbusDevice::NoError
- && !poll_update_pending_
- && next_poll_block_ == 0U;
- if (pending_reply_ == reply)
- {
- // 只有当前指针仍指向本次回复时才清空,避免误清新请求
- pending_reply_ = nullptr;
- }
- reply->deleteLater();
- if (poll_update_pending_)
- {
- const std::vector<RegisterAddress> addresses = pending_poll_addresses_;
- poll_multi_word_ranges_ = pending_poll_multi_word_ranges_;
- applyPollAddresses(addresses);
- }
- if (poll_cycle_completed)
- {
- emit pollCycleCompleted();
- if (poll_cycle_completed_callback_)
- {
- poll_cycle_completed_callback_();
- }
- }
- });
- }
-
- // 对可恢复通信故障发送一个轻量读取请求探测链路
- void PlcCommunicationService::probeRecovery()
- {
- // 只在可恢复的超时故障中探测;串口拔出等故障不会反复探测
- if (state_ != PlcConnectionState::Faulted
- || !isRecoverableTimeout(last_error_type_))
- {
- return;
- }
- if (master_->state() != QModbusDevice::ConnectedState)
- {
- return;
- }
- if (pending_reply_ != nullptr)
- {
- // 已有读请求在途,稍后再探测,保证同一时间只有一个读回复
- recovery_timer_.start(kRecoveryProbeIntervalMs);
- return;
- }
- if (poll_blocks_.empty())
- {
- return;
- }
- // 恢复探测只读取一个地址,确认链路恢复后再进行完整首读
- const PlcPollBlock block = poll_blocks_.front();
- const QModbusDataUnit request(
- registerType(block.area), block.startAddress, 1);
- QModbusReply *reply = master_->sendReadRequest(request, configuration_.serverAddress);
- if (reply == nullptr)
- {
- handleRecoveryProbeFailure(master_->error());
- return;
- }
- pending_reply_ = reply;
- const std::uint64_t generation = connection_generation_;
- connect(reply, &QModbusReply::finished,
- this,
- [this, reply, generation]
- {
- if (generation != connection_generation_)
- {
- reply->deleteLater();
- return;
- }
- if (pending_reply_ == reply)
- {
- pending_reply_ = nullptr;
- }
- const QModbusDevice::Error error = reply->error();
- reply->deleteLater();
- if (poll_update_pending_)
- {
- const std::vector<RegisterAddress> addresses = pending_poll_addresses_;
- poll_multi_word_ranges_ = pending_poll_multi_word_ranges_;
- applyPollAddresses(addresses);
- }
- if (state_ != PlcConnectionState::Faulted
- || !isRecoverableTimeout(last_error_type_))
- {
- return;
- }
- if (error != QModbusDevice::NoError)
- {
- handleRecoveryProbeFailure(error);
- return;
- }
- // 单地址探测成功后恢复正常轮询,并重新开始完整首读
- restoreCommunication();
- });
- }
-
- // 处理恢复探测错误,并决定立即上报或继续等待探测
- void PlcCommunicationService::handleRecoveryProbeFailure(QModbusDevice::Error error)
- {
- if (state_ != PlcConnectionState::Faulted
- || !isRecoverableTimeout(last_error_type_))
- {
- return;
- }
- if (error == QModbusDevice::ConnectionError)
- {
- const PlcCommunicationErrorContext context{
- QString::fromStdString(configuration_.portName),
- serial_session_opened_,
- received_valid_response_};
- // 探测阶段发现串口连接已经失效时,转入统一故障处理
- setError(classifyPlcCommunicationError(error, context));
- return;
- }
- recovery_timer_.start(kRecoveryProbeIntervalMs);
- }
-
- // 探测成功后恢复轮询,并重新开始完整首读
- void PlcCommunicationService::restoreCommunication()
- {
- // 探测成功只说明链路恢复,仍要重新读取全部读块才能恢复真机资格
- received_valid_response_ = true;
- last_error_type_ = PlcCommunicationError::None;
- last_error_.clear();
- rebuildPollBlocks();
- setState(PlcConnectionState::Recovering);
- poll_timer_.start(configuration_.pollIntervalMs);
- // 恢复后从第一个地址块重新开始完整首读
- pollNextBlock();
- }
-
- // 处理轮询读回复并更新 PLC 缓存
- void PlcCommunicationService::handleReadFinished(
- QModbusReply *reply, PlcPollBlock block)
- {
- if (!isReadingState(state_))
- {
- return;
- }
- if (reply->error() != QModbusDevice::NoError)
- {
- // 轮询回复失败时统一记录故障并启动对应恢复路径
- handleModbusError(reply->error());
- return;
- }
- // 只有成功回复才能更新 PLC 缓存;写请求不会直接改缓存
- const QModbusDataUnit result = reply->result();
- for (uint index = 0; index < result.valueCount(); ++index)
- {
- const int address = block.startAddress + static_cast<int>(index);
- if (block.area == RegisterArea::M)
- {
- // M 地址只用成功读回的线圈值刷新缓存
- repository_.updateBit(address, result.value(index) != 0U);
- }
- else
- {
- // D 地址只用成功读回的保持寄存器值刷新缓存
- repository_.updateWord(address, static_cast<std::int16_t>(result.value(index)));
- }
- }
- received_valid_response_ = true;
- last_error_type_ = PlcCommunicationError::None;
- last_error_.clear();
- emit cacheUpdated();
- if (cache_updated_callback_)
- {
- cache_updated_callback_();
- }
- }
-
- // 发送一个 M 位异步写请求,不直接修改缓存
- RegisterWriteResult PlcCommunicationService::sendBitWrite(
- const RegisterAddress &address, bool value)
- {
- // 写入只允许在正常 Connected 状态进行,Recovering/Faulted 都拒绝
- if (state_ != PlcConnectionState::Connected)
- {
- return {false, RegisterError::Unavailable};
- }
- if (pending_write_reply_ != nullptr)
- {
- return {false, RegisterError::WriteRejected};
- }
- // M 区对应 Modbus Coils,单次只写一个地址
- QModbusDataUnit unit(QModbusDataUnit::Coils, address.index(), 1);
- unit.setValue(0, value ? 1U : 0U);
- // 异步发送 Modbus 线圈单写,成功后仍等待轮询读回确认
- QModbusReply *reply = master_->sendWriteRequest(unit, configuration_.serverAddress);
- if (reply == nullptr)
- {
- // M 写请求未能创建时进入统一通信故障处理
- handleModbusError(master_->error());
- return {false, RegisterError::WriteRejected};
- }
- pending_write_reply_ = reply;
- // 写回复完成后只处理成功或错误,不直接改缓存;后续轮询负责确认真实值
- connect(reply, &QModbusReply::finished,
- this,
- [this, reply, generation = connection_generation_]
- {
- if (pending_write_reply_ == reply)
- {
- pending_write_reply_ = nullptr;
- }
- if (generation != connection_generation_)
- {
- reply->deleteLater();
- return;
- }
- if (reply->error() != QModbusDevice::NoError)
- {
- // M 异步写回复失败时进入统一通信故障处理
- handleModbusError(reply->error());
- }
- reply->deleteLater();
- });
- return {true, RegisterError::None};
- }
-
- // 发送一个 D 字异步写请求,不直接修改缓存
- RegisterWriteResult PlcCommunicationService::sendWordWrite(
- const RegisterAddress &address, std::int16_t value)
- {
- // D 区写入流程与 M 区相同,只是 Modbus 类型不同
- if (state_ != PlcConnectionState::Connected)
- {
- return {false, RegisterError::Unavailable};
- }
- if (pending_write_reply_ != nullptr)
- {
- return {false, RegisterError::WriteRejected};
- }
- // D 区对应 Modbus HoldingRegisters,单次只写一个字
- QModbusDataUnit unit(QModbusDataUnit::HoldingRegisters, address.index(), 1);
- unit.setValue(0, static_cast<quint16>(value));
- // 异步发送保持寄存器单写,成功后不直接修改本地缓存
- QModbusReply *reply = master_->sendWriteRequest(unit, configuration_.serverAddress);
- if (reply == nullptr)
- {
- // 单个 D 写请求未能创建时进入统一通信故障处理
- handleModbusError(master_->error());
- return {false, RegisterError::WriteRejected};
- }
- pending_write_reply_ = reply;
- connect(reply, &QModbusReply::finished,
- this,
- [this, reply, generation = connection_generation_]
- {
- if (pending_write_reply_ == reply)
- {
- pending_write_reply_ = nullptr;
- }
- if (generation != connection_generation_)
- {
- reply->deleteLater();
- return;
- }
- if (reply->error() != QModbusDevice::NoError)
- {
- // 单个 D 异步写回复失败时进入统一通信故障处理
- handleModbusError(reply->error());
- }
- reply->deleteLater();
- });
- return {true, RegisterError::None};
- }
-
- // 发送一组连续 D 字异步写请求,不直接修改缓存
- RegisterWriteResult PlcCommunicationService::sendWordsWrite(
- const RegisterAddress &address,
- const std::vector<std::int16_t> &values)
- {
- const int count = static_cast<int>(values.size());
- if (state_ != PlcConnectionState::Connected)
- {
- return {false, RegisterError::Unavailable};
- }
- if (pending_write_reply_ != nullptr
- || !address.isValid() || address.area() != RegisterArea::D
- || count < 2 || count > 4
- || address.index() > RegisterAddress::kMaximumIndex - count + 1)
- {
- return {false, pending_write_reply_ != nullptr
- ? RegisterError::WriteRejected : RegisterError::InvalidAddress};
- }
- QModbusDataUnit unit(QModbusDataUnit::HoldingRegisters, address.index(), count);
- for (int offset = 0; offset < count; ++offset)
- {
- unit.setValue(
- offset, static_cast<quint16>(values[static_cast<std::size_t>(offset)]));
- }
- // 异步发送连续保持寄存器多写,供 32 位和 64 位数值使用
- QModbusReply *reply = master_->sendWriteRequest(unit, configuration_.serverAddress);
- if (reply == nullptr)
- {
- // 连续 D 写请求未能创建时进入统一通信故障处理
- handleModbusError(master_->error());
- return {false, RegisterError::WriteRejected};
- }
- pending_write_reply_ = reply;
- connect(reply, &QModbusReply::finished,
- this,
- [this, reply, generation = connection_generation_]
- {
- if (pending_write_reply_ == reply)
- {
- pending_write_reply_ = nullptr;
- }
- if (generation != connection_generation_)
- {
- reply->deleteLater();
- return;
- }
- if (reply->error() != QModbusDevice::NoError)
- {
- // 连续 D 异步写回复失败时进入统一通信故障处理
- handleModbusError(reply->error());
- }
- reply->deleteLater();
- });
- return {true, RegisterError::None};
- }
-
- // 更新首读完成标记,并在需要时通知外部观察者
- void PlcCommunicationService::updateInitialReadCompleted(
- bool completed, bool force_notification)
- {
- if (initial_read_completed_ == completed && !force_notification)
- {
- return;
- }
- // 只有状态真的变化,或调用方明确要求通知时才发出信号
- initial_read_completed_ = completed;
- emit initialReadCompletedChanged(completed);
- if (initial_read_changed_callback_)
- {
- initial_read_changed_callback_(completed);
- }
- }
-
- // 处理未主动断开时发生的串口连接丢失
- void PlcCommunicationService::handleUnexpectedDisconnect()
- {
- // 这里表示设备原本连上过,后来串口意外断开
- serial_session_opened_ = false;
- const QString port_name = QString::fromStdString(configuration_.portName).trimmed();
- // 将 USB 串口拔出等异常记录为 Disconnected,交给运行版定时重连
- setError({
- PlcCommunicationError::SerialConnectionLost,
- QStringLiteral(
- "PLC 串口 %1 连接已中断;请检查 USB 转串口是否被拔出或已经失效")
- .arg(port_name)});
- }
-
- // 过滤重复错误后,分类并保存 Qt Modbus 错误
- void PlcCommunicationService::handleModbusError(QModbusDevice::Error error)
- {
- // 主动断开、已处理的故障和旧回复错误都不重复上报
- if (disconnecting_
- || (error == QModbusDevice::ReplyAbortedError
- && state_ == PlcConnectionState::Disconnected)
- || (state_ == PlcConnectionState::Disconnected
- && last_error_type_ == PlcCommunicationError::SerialConnectionLost)
- || state_ == PlcConnectionState::Faulted)
- {
- return;
- }
- const PlcCommunicationErrorContext context{
- QString::fromStdString(configuration_.portName),
- serial_session_opened_,
- received_valid_response_};
- // 将 PLC 无响应等错误记录为 Faulted,并启动通信恢复探测
- setError(classifyPlcCommunicationError(error, context));
- }
-
- // 使旧异步请求失效并关闭当前串口会话
- void PlcCommunicationService::closeSerialSession()
- {
- // 先递增代次并停止定时器,再断开串口;旧异步回调会因此失效
- ++connection_generation_;
- disconnecting_ = true;
- poll_timer_.stop();
- recovery_timer_.stop();
- pending_reply_ = nullptr;
- pending_write_reply_ = nullptr;
- poll_update_pending_ = false;
- pending_poll_addresses_.clear();
- pending_poll_multi_word_ranges_.clear();
- if (master_->state() != QModbusDevice::UnconnectedState)
- {
- master_->disconnectDevice();
- }
- serial_session_opened_ = false;
- received_valid_response_ = false;
- disconnecting_ = false;
- }
-
- // 修改连接状态并同步发出 Qt 信号和外部回调
- void PlcCommunicationService::setState(PlcConnectionState state)
- {
- if (state_ == state)
- {
- return;
- }
- // 状态集中从这里修改,确保 Qt 信号和 std::function 回调同步触发
- state_ = state;
- emit stateChanged();
- if (state_changed_callback_)
- {
- state_changed_callback_();
- }
- }
-
- // 保存通信故障、撤销首读资格并通知外部观察者
- void PlcCommunicationService::setError(const PlcCommunicationFailure &failure)
- {
- // 统一保存错误、停止正常轮询、撤销首读资格并通知 UI
- last_error_type_ = failure.type;
- last_error_ = toUtf8(failure.message);
- poll_timer_.stop();
- recovery_timer_.stop();
- // 任意通信故障都会撤销首读资格;恢复后必须重新完整读取
- updateInitialReadCompleted(false);
- const bool disconnected = failure.type == PlcCommunicationError::SerialPortOpenFailed
- || failure.type == PlcCommunicationError::SerialConnectionLost
- || failure.type == PlcCommunicationError::UsbSerialAdapterRemoved;
- if (disconnected)
- {
- closeSerialSession();
- }
- setState(disconnected
- ? PlcConnectionState::Disconnected : PlcConnectionState::Faulted);
- emit communicationError(failure.message);
- if (error_reported_callback_)
- {
- error_reported_callback_(last_error_);
- }
- if (isRecoverableTimeout(failure.type)
- && master_->state() == QModbusDevice::ConnectedState)
- {
- recovery_timer_.start(kRecoveryProbeIntervalMs);
- }
- }
|