#include "plc_communication_service.h" #include "plc_communication_error_classifier.h" #include "plc_register_repository.h" #include #include #include #include #include #include #include #include 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(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 &addresses, std::vector *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 &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 &addresses, const std::vector &multi_word_ranges, std::vector *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()) { // 仓库只负责缓存;写入请求通过这里注入的回调转给通信服务 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 &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(configuration.dataBits)); master_->setConnectionParameter( QModbusDevice::SerialParityParameter, static_cast(configuration.parity)); master_->setConnectionParameter( QModbusDevice::SerialStopBitsParameter, static_cast(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 &addresses) { return setPollAddresses(addresses, {}); } // 校验并应用地址及多字范围,必要时等待当前异步读请求结束 PlcCommunicationResult PlcCommunicationService::setPollAddresses( const std::vector &addresses, const std::vector &multi_word_ranges) { std::vector normalized; std::string error; if (!normalizePollAddresses(addresses, &normalized, &error)) { return {false, error}; } std::vector 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 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 state_changed, std::function initial_read_changed, std::function cache_updated, std::function poll_cycle_completed, std::function 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 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 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 &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(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 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 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(index); if (block.area == RegisterArea::M) { // M 地址只用成功读回的线圈值刷新缓存 repository_.updateBit(address, result.value(index) != 0U); } else { // D 地址只用成功读回的保持寄存器值刷新缓存 repository_.updateWord(address, static_cast(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(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 &values) { const int count = static_cast(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(values[static_cast(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); } }