综合平台编程器项目的远程存储
Nelze vybrat více než 25 témat Téma musí začínat písmenem nebo číslem, může obsahovat pomlčky („-“) a může být dlouhé až 35 znaků.
 
 
 
 

1004 řádky
37 KiB

  1. #include "plc_communication_service.h"
  2. #include "plc_communication_error_classifier.h"
  3. #include "plc_register_repository.h"
  4. #include <QModbusDataUnit>
  5. #include <QModbusDevice>
  6. #include <QModbusReply>
  7. #include <QModbusRtuSerialMaster>
  8. #include <QSerialPort>
  9. #include <QVariant>
  10. #include <algorithm>
  11. #include <utility>
  12. namespace {
  13. // 故障恢复时,两次轻量探测之间等待的时间
  14. constexpr int kRecoveryProbeIntervalMs = 2000;
  15. // Qt 使用 QString,项目内部错误信息统一使用 UTF-8 std::string
  16. std::string toUtf8(const QString &value)
  17. {
  18. const QByteArray bytes = value.toUtf8();
  19. return std::string(bytes.constData(), static_cast<std::size_t>(bytes.size()));
  20. }
  21. // M 区在 Modbus 中按线圈读取,D 区按保持寄存器读取
  22. QModbusDataUnit::RegisterType registerType(RegisterArea area)
  23. {
  24. return area == RegisterArea::M
  25. ? QModbusDataUnit::Coils : QModbusDataUnit::HoldingRegisters;
  26. }
  27. // 只有超时和 PLC 不响应属于可以自动探测恢复的故障
  28. bool isRecoverableTimeout(PlcCommunicationError error)
  29. {
  30. return error == PlcCommunicationError::PlcNotResponding
  31. || error == PlcCommunicationError::CommunicationTimeout;
  32. }
  33. // Connected 正常轮询,Recovering 正在恢复后的重新首读
  34. bool isReadingState(PlcConnectionState state)
  35. {
  36. return state == PlcConnectionState::Connected
  37. || state == PlcConnectionState::Recovering;
  38. }
  39. // 排序并去重轮询地址,同时检查地址总量上限
  40. bool normalizePollAddresses(
  41. const std::vector<RegisterAddress> &addresses,
  42. std::vector<RegisterAddress> *normalized,
  43. std::string *error)
  44. {
  45. // 先复制,后续排序和去重不能改变调用方传入的地址列表
  46. *normalized = addresses;
  47. if (std::any_of(
  48. normalized->cbegin(), normalized->cend(),
  49. [](const RegisterAddress &address) { return !address.isValid(); }))
  50. {
  51. *error = "PLC 轮询地址中包含无效的 M/D 地址";
  52. return false;
  53. }
  54. std::sort(
  55. normalized->begin(), normalized->end(),
  56. [](const RegisterAddress &left, const RegisterAddress &right)
  57. {
  58. return left.area() == right.area()
  59. ? left.index() < right.index()
  60. : left.area() == RegisterArea::M;
  61. });
  62. normalized->erase(
  63. std::unique(normalized->begin(), normalized->end()), normalized->end());
  64. if (normalized->size() > ProjectLimits::kMaximumPollAddresses)
  65. {
  66. *error = "PLC 轮询的去重 M/D 地址最多为 256 个";
  67. return false;
  68. }
  69. return true;
  70. }
  71. // 判断读块边界是否落在多字范围内部,避免拆开一次多字读取
  72. bool boundaryBelongsToMultiWordRange(
  73. RegisterArea area,
  74. int boundary,
  75. const std::vector<RegisterWordRange> &ranges)
  76. {
  77. return std::any_of(
  78. ranges.cbegin(), ranges.cend(),
  79. [area, boundary](const RegisterWordRange &range)
  80. {
  81. return range.start.area() == area
  82. && range.start.index() <= boundary
  83. && boundary < range.start.index() + range.wordCount - 1;
  84. });
  85. }
  86. // 将地址集合拆成符合 Modbus 单次读取上限的连续读块
  87. bool calculatePollBlocks(
  88. const std::vector<RegisterAddress> &addresses,
  89. const std::vector<RegisterWordRange> &multi_word_ranges,
  90. std::vector<PlcPollBlock> *blocks)
  91. {
  92. blocks->clear();
  93. if (addresses.empty())
  94. {
  95. blocks->push_back({RegisterArea::M, 0, 1});
  96. blocks->push_back({RegisterArea::D, 0, 1});
  97. return true;
  98. }
  99. // 先处理同一区域内连续地址,再按单次读取上限切分
  100. std::size_t run_start = 0U;
  101. while (run_start < addresses.size())
  102. {
  103. std::size_t run_end = run_start;
  104. while (run_end + 1U < addresses.size()
  105. && addresses[run_end + 1U].area() == addresses[run_start].area()
  106. && addresses[run_end + 1U].index() == addresses[run_end].index() + 1)
  107. {
  108. ++run_end;
  109. }
  110. const RegisterArea area = addresses[run_start].area();
  111. int cursor = addresses[run_start].index();
  112. const int last = addresses[run_end].index();
  113. while (cursor <= last)
  114. {
  115. int block_end = std::min(
  116. cursor + ProjectLimits::kMaximumModbusReadCount - 1, last);
  117. while (block_end < last
  118. && boundaryBelongsToMultiWordRange(
  119. area, block_end, multi_word_ranges))
  120. {
  121. --block_end;
  122. }
  123. if (block_end < cursor)
  124. {
  125. blocks->clear();
  126. return false;
  127. }
  128. blocks->push_back({area, cursor, block_end - cursor + 1});
  129. cursor = block_end + 1;
  130. }
  131. run_start = run_end + 1U;
  132. }
  133. return true;
  134. }
  135. } // namespace
  136. // 创建 Qt Modbus 主站,绑定异步信号和 PLC 缓存写入回调
  137. PlcCommunicationService::PlcCommunicationService(
  138. PlcRegisterRepository &repository,
  139. QObject *parent)
  140. : QObject(parent),
  141. repository_(repository),
  142. master_(std::make_unique<QModbusRtuSerialMaster>())
  143. {
  144. // 仓库只负责缓存;写入请求通过这里注入的回调转给通信服务
  145. repository_.setWriteHandlers(
  146. [this](const RegisterAddress &address, bool value)
  147. {
  148. // PLC 仓库收到 M 写入时转成 Modbus 线圈单写请求
  149. return sendBitWrite(address, value);
  150. },
  151. [this](const RegisterAddress &address, std::int16_t value)
  152. {
  153. // PLC 仓库收到单个 D 写入时转成保持寄存器单写请求
  154. return sendWordWrite(address, value);
  155. },
  156. [this](const RegisterAddress &address,
  157. const std::vector<std::int16_t> &values)
  158. {
  159. // PLC 仓库收到连续 D 写入时转成保持寄存器多写请求
  160. return sendWordsWrite(address, values);
  161. });
  162. // 轮询定时器每次只推动一个读块,避免一次压入大量异步请求
  163. connect(&poll_timer_, &QTimer::timeout, this, &PlcCommunicationService::pollNextBlock);
  164. recovery_timer_.setSingleShot(true);
  165. // PLC 超时进入 Faulted 后,由恢复定时器调用轻量探测入口
  166. connect(
  167. &recovery_timer_,
  168. &QTimer::timeout,
  169. this,
  170. &PlcCommunicationService::probeRecovery);
  171. // Qt 串口状态变化是异步通知,所有连接状态都从这里统一处理
  172. connect(master_.get(), &QModbusClient::stateChanged,
  173. this,
  174. [this](QModbusDevice::State device_state)
  175. {
  176. if (device_state == QModbusDevice::ConnectedState)
  177. {
  178. // 串口连接成功后立即开始轮询,并不代表首读已经完成
  179. serial_session_opened_ = true;
  180. setState(PlcConnectionState::Connected);
  181. poll_timer_.start(configuration_.pollIntervalMs);
  182. // 串口连上后立即异步读取第一个 M/D 地址块
  183. pollNextBlock();
  184. }
  185. else if (device_state == QModbusDevice::ConnectingState)
  186. {
  187. serial_session_opened_ = false;
  188. setState(PlcConnectionState::Connecting);
  189. }
  190. else if (device_state == QModbusDevice::UnconnectedState)
  191. {
  192. // 主动断开和意外断线要区分,后者需要报告故障
  193. if (disconnecting_)
  194. {
  195. serial_session_opened_ = false;
  196. }
  197. else if (serial_session_opened_)
  198. {
  199. handleUnexpectedDisconnect();
  200. }
  201. else if (state_ != PlcConnectionState::Faulted)
  202. {
  203. setState(PlcConnectionState::Disconnected);
  204. }
  205. }
  206. });
  207. // Qt 报告底层错误时,转换为项目自己的错误和状态
  208. connect(master_.get(), &QModbusClient::errorOccurred,
  209. this,
  210. [this](QModbusDevice::Error error)
  211. {
  212. if (error != QModbusDevice::NoError)
  213. {
  214. // 底层 Modbus 错误统一交给项目错误分类和恢复流程
  215. handleModbusError(error);
  216. }
  217. });
  218. }
  219. // 释放通信服务持有的 Qt 资源
  220. PlcCommunicationService::~PlcCommunicationService() = default;
  221. // 校验串口配置并启动一次新的 PLC 异步连接
  222. PlcCommunicationResult PlcCommunicationService::connectDevice(
  223. const PlcSerialConfiguration &configuration)
  224. {
  225. const PlcCommunicationResult validation =
  226. validatePlcSerialConfiguration(configuration);
  227. if (!validation.succeeded)
  228. {
  229. return validation;
  230. }
  231. if (state_ == PlcConnectionState::Disconnected
  232. && master_->state() != QModbusDevice::UnconnectedState)
  233. {
  234. closeSerialSession();
  235. }
  236. if (master_->state() != QModbusDevice::UnconnectedState)
  237. {
  238. return {false, "PLC 连接已经启动,请先断开当前连接"};
  239. }
  240. // 每次连接都递增代次;旧连接的异步回复即使晚到也不能污染新缓存
  241. configuration_ = configuration;
  242. ++connection_generation_;
  243. disconnecting_ = false;
  244. serial_session_opened_ = false;
  245. received_valid_response_ = false;
  246. last_error_type_ = PlcCommunicationError::None;
  247. last_error_.clear();
  248. // 把项目配置转换成 Qt 串口参数
  249. master_->setConnectionParameter(
  250. QModbusDevice::SerialPortNameParameter,
  251. QString::fromStdString(configuration.portName));
  252. master_->setConnectionParameter(
  253. QModbusDevice::SerialBaudRateParameter, configuration.baudRate);
  254. master_->setConnectionParameter(
  255. QModbusDevice::SerialDataBitsParameter,
  256. static_cast<QSerialPort::DataBits>(configuration.dataBits));
  257. master_->setConnectionParameter(
  258. QModbusDevice::SerialParityParameter,
  259. static_cast<QSerialPort::Parity>(configuration.parity));
  260. master_->setConnectionParameter(
  261. QModbusDevice::SerialStopBitsParameter,
  262. static_cast<QSerialPort::StopBits>(configuration.stopBits));
  263. master_->setTimeout(configuration.responseTimeoutMs);
  264. master_->setNumberOfRetries(configuration.retries);
  265. // 新连接必须重新完成完整首读,不能沿用上一条连接的运行资格
  266. repository_.invalidate();
  267. updateInitialReadCompleted(false, true);
  268. rebuildPollBlocks();
  269. setState(PlcConnectionState::Connecting);
  270. // 只发起异步串口连接,连接结果由 Qt 状态回调继续推进
  271. if (!master_->connectDevice())
  272. {
  273. if (last_error_.empty())
  274. {
  275. const QModbusDevice::Error error = master_->error() == QModbusDevice::NoError
  276. ? QModbusDevice::ConnectionError : master_->error();
  277. // 串口连接请求启动失败也进入统一错误分类和状态更新
  278. handleModbusError(error);
  279. }
  280. return {false, last_error_};
  281. }
  282. return {true, {}};
  283. }
  284. // 停止定时器、关闭串口并清除本次连接的缓存有效状态
  285. void PlcCommunicationService::disconnectDevice()
  286. {
  287. // 关闭串口、清除缓存有效标记,防止断开后继续使用旧值
  288. closeSerialSession();
  289. repository_.invalidate();
  290. updateInitialReadCompleted(false, true);
  291. last_error_type_ = PlcCommunicationError::None;
  292. last_error_.clear();
  293. setState(PlcConnectionState::Disconnected);
  294. }
  295. // 使用默认的单字范围设置轮询地址
  296. PlcCommunicationResult PlcCommunicationService::setPollAddresses(
  297. const std::vector<RegisterAddress> &addresses)
  298. {
  299. return setPollAddresses(addresses, {});
  300. }
  301. // 校验并应用地址及多字范围,必要时等待当前异步读请求结束
  302. PlcCommunicationResult PlcCommunicationService::setPollAddresses(
  303. const std::vector<RegisterAddress> &addresses,
  304. const std::vector<RegisterWordRange> &multi_word_ranges)
  305. {
  306. std::vector<RegisterAddress> normalized;
  307. std::string error;
  308. if (!normalizePollAddresses(addresses, &normalized, &error))
  309. {
  310. return {false, error};
  311. }
  312. std::vector<RegisterWordRange> normalized_ranges = multi_word_ranges;
  313. std::sort(
  314. normalized_ranges.begin(), normalized_ranges.end(),
  315. [](const RegisterWordRange &left, const RegisterWordRange &right)
  316. {
  317. return left.start.index() == right.start.index()
  318. ? left.wordCount < right.wordCount
  319. : left.start.index() < right.start.index();
  320. });
  321. normalized_ranges.erase(
  322. std::unique(
  323. normalized_ranges.begin(), normalized_ranges.end()),
  324. normalized_ranges.end());
  325. if (std::any_of(
  326. normalized_ranges.cbegin(), normalized_ranges.cend(),
  327. [](const RegisterWordRange &range)
  328. {
  329. return !range.start.isValid()
  330. || range.start.area() != RegisterArea::D
  331. || range.wordCount < 2 || range.wordCount > 4
  332. || range.start.index()
  333. > RegisterAddress::kMaximumIndex - range.wordCount + 1;
  334. }))
  335. {
  336. return {false, "多字轮询范围必须是 D 区内连续的 2~4 个字"};
  337. }
  338. for (const RegisterWordRange &range : normalized_ranges)
  339. {
  340. for (int offset = 0; offset < range.wordCount; ++offset)
  341. {
  342. normalized.push_back(RegisterAddress{
  343. RegisterArea::D, range.start.index() + offset});
  344. }
  345. }
  346. std::sort(
  347. normalized.begin(), normalized.end(),
  348. [](const RegisterAddress &left, const RegisterAddress &right)
  349. {
  350. return left.area() == right.area()
  351. ? left.index() < right.index()
  352. : left.area() == RegisterArea::M;
  353. });
  354. normalized.erase(std::unique(normalized.begin(), normalized.end()), normalized.end());
  355. if (normalized.size() > ProjectLimits::kMaximumPollAddresses)
  356. {
  357. return {false, "PLC 轮询的去重 M/D 地址最多为 256 个"};
  358. }
  359. std::vector<PlcPollBlock> calculated_blocks;
  360. if (!calculatePollBlocks(normalized, normalized_ranges, &calculated_blocks))
  361. {
  362. return {false, "重叠的多字范围无法在单次 120 字读取边界内完整轮询"};
  363. }
  364. if (calculated_blocks.size() > ProjectLimits::kMaximumPollBlocks)
  365. {
  366. return {false, "PLC 轮询地址拆分后最多允许 8 个读块"};
  367. }
  368. // 当前读请求完成前暂存新集合,避免按半旧半新的地址解析回复
  369. if (pending_reply_ != nullptr)
  370. {
  371. // 当前回复仍在解析旧集合,等它结束后再切换到新集合
  372. pending_poll_addresses_ = std::move(normalized);
  373. pending_poll_multi_word_ranges_ = std::move(normalized_ranges);
  374. poll_update_pending_ = true;
  375. return {true, {}};
  376. }
  377. poll_multi_word_ranges_ = std::move(normalized_ranges);
  378. applyPollAddresses(normalized);
  379. return {true, {}};
  380. }
  381. // 返回当前 PLC 连接状态
  382. PlcConnectionState PlcCommunicationService::state() const
  383. {
  384. return state_;
  385. }
  386. // 返回当前连接是否已完成全部轮询块的首次读取
  387. bool PlcCommunicationService::initialReadCompleted() const
  388. {
  389. return initial_read_completed_;
  390. }
  391. // 返回最近一次通信错误的统一类型
  392. PlcCommunicationError PlcCommunicationService::lastErrorType() const
  393. {
  394. return last_error_type_;
  395. }
  396. // 返回最近一次通信错误的可读文字
  397. const std::string &PlcCommunicationService::lastError() const
  398. {
  399. return last_error_;
  400. }
  401. // 保存外部回调,在对应通信事件发生时通知调用方
  402. void PlcCommunicationService::setCallbacks(
  403. std::function<void()> state_changed,
  404. std::function<void(bool)> initial_read_changed,
  405. std::function<void()> cache_updated,
  406. std::function<void()> poll_cycle_completed,
  407. std::function<void(const std::string &)> error_reported)
  408. {
  409. // 保存外部通知函数,通信事件发生时再调用对应函数
  410. state_changed_callback_ = std::move(state_changed); // 通信状态变化时通知外部
  411. initial_read_changed_callback_ = std::move(initial_read_changed); // 首读资格变化时通知外部
  412. cache_updated_callback_ = std::move(cache_updated); // PLC 缓存更新后通知外部
  413. poll_cycle_completed_callback_ = std::move(poll_cycle_completed);
  414. error_reported_callback_ = std::move(error_reported); // 发生通信错误时通知外部
  415. }
  416. // 根据当前地址集合重新生成连续轮询读块
  417. void PlcCommunicationService::rebuildPollBlocks()
  418. {
  419. std::vector<RegisterAddress> addresses = poll_addresses_;
  420. if (addresses.empty())
  421. {
  422. // 没有指定地址时保留 M0/D0,保证连接后仍能验证设备是否响应
  423. addresses = {
  424. RegisterAddress{RegisterArea::M, 0},
  425. RegisterAddress{RegisterArea::D, 0}};
  426. }
  427. std::sort(
  428. addresses.begin(), addresses.end(),
  429. [](const RegisterAddress &left, const RegisterAddress &right)
  430. {
  431. if (left.area() != right.area())
  432. {
  433. return left.area() == RegisterArea::M;
  434. }
  435. return left.index() < right.index();
  436. });
  437. addresses.erase(std::unique(addresses.begin(), addresses.end()), addresses.end());
  438. std::vector<PlcPollBlock> calculated_blocks;
  439. calculatePollBlocks(addresses, poll_multi_word_ranges_, &calculated_blocks);
  440. poll_blocks_.clear();
  441. poll_blocks_.reserve(calculated_blocks.size());
  442. for (const PlcPollBlock &block : calculated_blocks)
  443. {
  444. poll_blocks_.push_back(block);
  445. }
  446. next_poll_block_ = 0;
  447. if (!initial_read_completed_)
  448. {
  449. // 地址集合变化或重新连接后,首读资格从头统计
  450. initial_blocks_read_.assign(poll_blocks_.size(), false);
  451. }
  452. else
  453. {
  454. initial_blocks_read_.clear();
  455. }
  456. }
  457. // 在没有读请求占用时正式切换到新的轮询地址集合
  458. void PlcCommunicationService::applyPollAddresses(
  459. const std::vector<RegisterAddress> &addresses)
  460. {
  461. // 只有在没有读请求占用时,新的地址集合才会真正生效
  462. poll_addresses_ = addresses;
  463. rebuildPollBlocks();
  464. poll_update_pending_ = false;
  465. pending_poll_addresses_.clear();
  466. pending_poll_multi_word_ranges_.clear();
  467. if (isReadingState(state_) && pending_reply_ == nullptr)
  468. {
  469. // 新轮询地址生效后立即从第一个读块继续异步轮询
  470. pollNextBlock();
  471. }
  472. }
  473. // 发送一个异步轮询读请求,并由完成回调推进后续读块
  474. void PlcCommunicationService::pollNextBlock()
  475. {
  476. if (!isReadingState(state_)
  477. || pending_reply_ != nullptr || poll_blocks_.empty())
  478. {
  479. return;
  480. }
  481. // 每次只发一个异步请求,完成回调中再推进到下一个块
  482. const std::size_t block_index = next_poll_block_;
  483. const PlcPollBlock block = poll_blocks_.at(block_index);
  484. next_poll_block_ = (next_poll_block_ + 1U) % poll_blocks_.size();
  485. QModbusDataUnit request(
  486. registerType(block.area), block.startAddress, static_cast<quint16>(block.count));
  487. // 发出当前 M 或 D 地址块的异步 Modbus 读取请求
  488. QModbusReply *reply = master_->sendReadRequest(request, configuration_.serverAddress);
  489. if (reply == nullptr)
  490. {
  491. // 读请求未能创建时按通信故障撤销首读资格
  492. handleModbusError(master_->error());
  493. return;
  494. }
  495. pending_reply_ = reply;
  496. // 把当前连接代次带进回调,防止断线重连后旧回复误更新新连接
  497. const std::uint64_t generation = connection_generation_;
  498. connect(reply, &QModbusReply::finished,
  499. this,
  500. [this, reply, block, block_index, generation]
  501. {
  502. // 连接已经重建时,直接丢弃旧回复
  503. if (generation != connection_generation_)
  504. {
  505. reply->deleteLater();
  506. return;
  507. }
  508. // 读取完成后解析回复并只用成功读回值更新 PLC 缓存
  509. handleReadFinished(reply, block);
  510. if (isReadingState(state_)
  511. && reply->error() == QModbusDevice::NoError
  512. && !poll_update_pending_
  513. && block_index < initial_blocks_read_.size())
  514. {
  515. initial_blocks_read_[block_index] = true;
  516. const bool completed = std::all_of(
  517. initial_blocks_read_.cbegin(),
  518. initial_blocks_read_.cend(),
  519. [](bool read) { return read; });
  520. // 所有轮询块都成功读过,才授予真机运行资格
  521. if (completed && !initial_read_completed_)
  522. {
  523. updateInitialReadCompleted(true);
  524. if (state_ == PlcConnectionState::Recovering)
  525. {
  526. setState(PlcConnectionState::Connected);
  527. }
  528. }
  529. }
  530. const bool poll_cycle_completed = isReadingState(state_)
  531. && reply->error() == QModbusDevice::NoError
  532. && !poll_update_pending_
  533. && next_poll_block_ == 0U;
  534. if (pending_reply_ == reply)
  535. {
  536. // 只有当前指针仍指向本次回复时才清空,避免误清新请求
  537. pending_reply_ = nullptr;
  538. }
  539. reply->deleteLater();
  540. if (poll_update_pending_)
  541. {
  542. const std::vector<RegisterAddress> addresses = pending_poll_addresses_;
  543. poll_multi_word_ranges_ = pending_poll_multi_word_ranges_;
  544. applyPollAddresses(addresses);
  545. }
  546. if (poll_cycle_completed)
  547. {
  548. emit pollCycleCompleted();
  549. if (poll_cycle_completed_callback_)
  550. {
  551. poll_cycle_completed_callback_();
  552. }
  553. }
  554. });
  555. }
  556. // 对可恢复通信故障发送一个轻量读取请求探测链路
  557. void PlcCommunicationService::probeRecovery()
  558. {
  559. // 只在可恢复的超时故障中探测;串口拔出等故障不会反复探测
  560. if (state_ != PlcConnectionState::Faulted
  561. || !isRecoverableTimeout(last_error_type_))
  562. {
  563. return;
  564. }
  565. if (master_->state() != QModbusDevice::ConnectedState)
  566. {
  567. return;
  568. }
  569. if (pending_reply_ != nullptr)
  570. {
  571. // 已有读请求在途,稍后再探测,保证同一时间只有一个读回复
  572. recovery_timer_.start(kRecoveryProbeIntervalMs);
  573. return;
  574. }
  575. if (poll_blocks_.empty())
  576. {
  577. return;
  578. }
  579. // 恢复探测只读取一个地址,确认链路恢复后再进行完整首读
  580. const PlcPollBlock block = poll_blocks_.front();
  581. const QModbusDataUnit request(
  582. registerType(block.area), block.startAddress, 1);
  583. QModbusReply *reply = master_->sendReadRequest(request, configuration_.serverAddress);
  584. if (reply == nullptr)
  585. {
  586. handleRecoveryProbeFailure(master_->error());
  587. return;
  588. }
  589. pending_reply_ = reply;
  590. const std::uint64_t generation = connection_generation_;
  591. connect(reply, &QModbusReply::finished,
  592. this,
  593. [this, reply, generation]
  594. {
  595. if (generation != connection_generation_)
  596. {
  597. reply->deleteLater();
  598. return;
  599. }
  600. if (pending_reply_ == reply)
  601. {
  602. pending_reply_ = nullptr;
  603. }
  604. const QModbusDevice::Error error = reply->error();
  605. reply->deleteLater();
  606. if (poll_update_pending_)
  607. {
  608. const std::vector<RegisterAddress> addresses = pending_poll_addresses_;
  609. poll_multi_word_ranges_ = pending_poll_multi_word_ranges_;
  610. applyPollAddresses(addresses);
  611. }
  612. if (state_ != PlcConnectionState::Faulted
  613. || !isRecoverableTimeout(last_error_type_))
  614. {
  615. return;
  616. }
  617. if (error != QModbusDevice::NoError)
  618. {
  619. handleRecoveryProbeFailure(error);
  620. return;
  621. }
  622. // 单地址探测成功后恢复正常轮询,并重新开始完整首读
  623. restoreCommunication();
  624. });
  625. }
  626. // 处理恢复探测错误,并决定立即上报或继续等待探测
  627. void PlcCommunicationService::handleRecoveryProbeFailure(QModbusDevice::Error error)
  628. {
  629. if (state_ != PlcConnectionState::Faulted
  630. || !isRecoverableTimeout(last_error_type_))
  631. {
  632. return;
  633. }
  634. if (error == QModbusDevice::ConnectionError)
  635. {
  636. const PlcCommunicationErrorContext context{
  637. QString::fromStdString(configuration_.portName),
  638. serial_session_opened_,
  639. received_valid_response_};
  640. // 探测阶段发现串口连接已经失效时,转入统一故障处理
  641. setError(classifyPlcCommunicationError(error, context));
  642. return;
  643. }
  644. recovery_timer_.start(kRecoveryProbeIntervalMs);
  645. }
  646. // 探测成功后恢复轮询,并重新开始完整首读
  647. void PlcCommunicationService::restoreCommunication()
  648. {
  649. // 探测成功只说明链路恢复,仍要重新读取全部读块才能恢复真机资格
  650. received_valid_response_ = true;
  651. last_error_type_ = PlcCommunicationError::None;
  652. last_error_.clear();
  653. rebuildPollBlocks();
  654. setState(PlcConnectionState::Recovering);
  655. poll_timer_.start(configuration_.pollIntervalMs);
  656. // 恢复后从第一个地址块重新开始完整首读
  657. pollNextBlock();
  658. }
  659. // 处理轮询读回复并更新 PLC 缓存
  660. void PlcCommunicationService::handleReadFinished(
  661. QModbusReply *reply, PlcPollBlock block)
  662. {
  663. if (!isReadingState(state_))
  664. {
  665. return;
  666. }
  667. if (reply->error() != QModbusDevice::NoError)
  668. {
  669. // 轮询回复失败时统一记录故障并启动对应恢复路径
  670. handleModbusError(reply->error());
  671. return;
  672. }
  673. // 只有成功回复才能更新 PLC 缓存;写请求不会直接改缓存
  674. const QModbusDataUnit result = reply->result();
  675. for (uint index = 0; index < result.valueCount(); ++index)
  676. {
  677. const int address = block.startAddress + static_cast<int>(index);
  678. if (block.area == RegisterArea::M)
  679. {
  680. // M 地址只用成功读回的线圈值刷新缓存
  681. repository_.updateBit(address, result.value(index) != 0U);
  682. }
  683. else
  684. {
  685. // D 地址只用成功读回的保持寄存器值刷新缓存
  686. repository_.updateWord(address, static_cast<std::int16_t>(result.value(index)));
  687. }
  688. }
  689. received_valid_response_ = true;
  690. last_error_type_ = PlcCommunicationError::None;
  691. last_error_.clear();
  692. emit cacheUpdated();
  693. if (cache_updated_callback_)
  694. {
  695. cache_updated_callback_();
  696. }
  697. }
  698. // 发送一个 M 位异步写请求,不直接修改缓存
  699. RegisterWriteResult PlcCommunicationService::sendBitWrite(
  700. const RegisterAddress &address, bool value)
  701. {
  702. // 写入只允许在正常 Connected 状态进行,Recovering/Faulted 都拒绝
  703. if (state_ != PlcConnectionState::Connected)
  704. {
  705. return {false, RegisterError::Unavailable};
  706. }
  707. if (pending_write_reply_ != nullptr)
  708. {
  709. return {false, RegisterError::WriteRejected};
  710. }
  711. // M 区对应 Modbus Coils,单次只写一个地址
  712. QModbusDataUnit unit(QModbusDataUnit::Coils, address.index(), 1);
  713. unit.setValue(0, value ? 1U : 0U);
  714. // 异步发送 Modbus 线圈单写,成功后仍等待轮询读回确认
  715. QModbusReply *reply = master_->sendWriteRequest(unit, configuration_.serverAddress);
  716. if (reply == nullptr)
  717. {
  718. // M 写请求未能创建时进入统一通信故障处理
  719. handleModbusError(master_->error());
  720. return {false, RegisterError::WriteRejected};
  721. }
  722. pending_write_reply_ = reply;
  723. // 写回复完成后只处理成功或错误,不直接改缓存;后续轮询负责确认真实值
  724. connect(reply, &QModbusReply::finished,
  725. this,
  726. [this, reply, generation = connection_generation_]
  727. {
  728. if (pending_write_reply_ == reply)
  729. {
  730. pending_write_reply_ = nullptr;
  731. }
  732. if (generation != connection_generation_)
  733. {
  734. reply->deleteLater();
  735. return;
  736. }
  737. if (reply->error() != QModbusDevice::NoError)
  738. {
  739. // M 异步写回复失败时进入统一通信故障处理
  740. handleModbusError(reply->error());
  741. }
  742. reply->deleteLater();
  743. });
  744. return {true, RegisterError::None};
  745. }
  746. // 发送一个 D 字异步写请求,不直接修改缓存
  747. RegisterWriteResult PlcCommunicationService::sendWordWrite(
  748. const RegisterAddress &address, std::int16_t value)
  749. {
  750. // D 区写入流程与 M 区相同,只是 Modbus 类型不同
  751. if (state_ != PlcConnectionState::Connected)
  752. {
  753. return {false, RegisterError::Unavailable};
  754. }
  755. if (pending_write_reply_ != nullptr)
  756. {
  757. return {false, RegisterError::WriteRejected};
  758. }
  759. // D 区对应 Modbus HoldingRegisters,单次只写一个字
  760. QModbusDataUnit unit(QModbusDataUnit::HoldingRegisters, address.index(), 1);
  761. unit.setValue(0, static_cast<quint16>(value));
  762. // 异步发送保持寄存器单写,成功后不直接修改本地缓存
  763. QModbusReply *reply = master_->sendWriteRequest(unit, configuration_.serverAddress);
  764. if (reply == nullptr)
  765. {
  766. // 单个 D 写请求未能创建时进入统一通信故障处理
  767. handleModbusError(master_->error());
  768. return {false, RegisterError::WriteRejected};
  769. }
  770. pending_write_reply_ = reply;
  771. connect(reply, &QModbusReply::finished,
  772. this,
  773. [this, reply, generation = connection_generation_]
  774. {
  775. if (pending_write_reply_ == reply)
  776. {
  777. pending_write_reply_ = nullptr;
  778. }
  779. if (generation != connection_generation_)
  780. {
  781. reply->deleteLater();
  782. return;
  783. }
  784. if (reply->error() != QModbusDevice::NoError)
  785. {
  786. // 单个 D 异步写回复失败时进入统一通信故障处理
  787. handleModbusError(reply->error());
  788. }
  789. reply->deleteLater();
  790. });
  791. return {true, RegisterError::None};
  792. }
  793. // 发送一组连续 D 字异步写请求,不直接修改缓存
  794. RegisterWriteResult PlcCommunicationService::sendWordsWrite(
  795. const RegisterAddress &address,
  796. const std::vector<std::int16_t> &values)
  797. {
  798. const int count = static_cast<int>(values.size());
  799. if (state_ != PlcConnectionState::Connected)
  800. {
  801. return {false, RegisterError::Unavailable};
  802. }
  803. if (pending_write_reply_ != nullptr
  804. || !address.isValid() || address.area() != RegisterArea::D
  805. || count < 2 || count > 4
  806. || address.index() > RegisterAddress::kMaximumIndex - count + 1)
  807. {
  808. return {false, pending_write_reply_ != nullptr
  809. ? RegisterError::WriteRejected : RegisterError::InvalidAddress};
  810. }
  811. QModbusDataUnit unit(QModbusDataUnit::HoldingRegisters, address.index(), count);
  812. for (int offset = 0; offset < count; ++offset)
  813. {
  814. unit.setValue(
  815. offset, static_cast<quint16>(values[static_cast<std::size_t>(offset)]));
  816. }
  817. // 异步发送连续保持寄存器多写,供 32 位和 64 位数值使用
  818. QModbusReply *reply = master_->sendWriteRequest(unit, configuration_.serverAddress);
  819. if (reply == nullptr)
  820. {
  821. // 连续 D 写请求未能创建时进入统一通信故障处理
  822. handleModbusError(master_->error());
  823. return {false, RegisterError::WriteRejected};
  824. }
  825. pending_write_reply_ = reply;
  826. connect(reply, &QModbusReply::finished,
  827. this,
  828. [this, reply, generation = connection_generation_]
  829. {
  830. if (pending_write_reply_ == reply)
  831. {
  832. pending_write_reply_ = nullptr;
  833. }
  834. if (generation != connection_generation_)
  835. {
  836. reply->deleteLater();
  837. return;
  838. }
  839. if (reply->error() != QModbusDevice::NoError)
  840. {
  841. // 连续 D 异步写回复失败时进入统一通信故障处理
  842. handleModbusError(reply->error());
  843. }
  844. reply->deleteLater();
  845. });
  846. return {true, RegisterError::None};
  847. }
  848. // 更新首读完成标记,并在需要时通知外部观察者
  849. void PlcCommunicationService::updateInitialReadCompleted(
  850. bool completed, bool force_notification)
  851. {
  852. if (initial_read_completed_ == completed && !force_notification)
  853. {
  854. return;
  855. }
  856. // 只有状态真的变化,或调用方明确要求通知时才发出信号
  857. initial_read_completed_ = completed;
  858. emit initialReadCompletedChanged(completed);
  859. if (initial_read_changed_callback_)
  860. {
  861. initial_read_changed_callback_(completed);
  862. }
  863. }
  864. // 处理未主动断开时发生的串口连接丢失
  865. void PlcCommunicationService::handleUnexpectedDisconnect()
  866. {
  867. // 这里表示设备原本连上过,后来串口意外断开
  868. serial_session_opened_ = false;
  869. const QString port_name = QString::fromStdString(configuration_.portName).trimmed();
  870. // 将 USB 串口拔出等异常记录为 Disconnected,交给运行版定时重连
  871. setError({
  872. PlcCommunicationError::SerialConnectionLost,
  873. QStringLiteral(
  874. "PLC 串口 %1 连接已中断;请检查 USB 转串口是否被拔出或已经失效")
  875. .arg(port_name)});
  876. }
  877. // 过滤重复错误后,分类并保存 Qt Modbus 错误
  878. void PlcCommunicationService::handleModbusError(QModbusDevice::Error error)
  879. {
  880. // 主动断开、已处理的故障和旧回复错误都不重复上报
  881. if (disconnecting_
  882. || (error == QModbusDevice::ReplyAbortedError
  883. && state_ == PlcConnectionState::Disconnected)
  884. || (state_ == PlcConnectionState::Disconnected
  885. && last_error_type_ == PlcCommunicationError::SerialConnectionLost)
  886. || state_ == PlcConnectionState::Faulted)
  887. {
  888. return;
  889. }
  890. const PlcCommunicationErrorContext context{
  891. QString::fromStdString(configuration_.portName),
  892. serial_session_opened_,
  893. received_valid_response_};
  894. // 将 PLC 无响应等错误记录为 Faulted,并启动通信恢复探测
  895. setError(classifyPlcCommunicationError(error, context));
  896. }
  897. // 使旧异步请求失效并关闭当前串口会话
  898. void PlcCommunicationService::closeSerialSession()
  899. {
  900. // 先递增代次并停止定时器,再断开串口;旧异步回调会因此失效
  901. ++connection_generation_;
  902. disconnecting_ = true;
  903. poll_timer_.stop();
  904. recovery_timer_.stop();
  905. pending_reply_ = nullptr;
  906. pending_write_reply_ = nullptr;
  907. poll_update_pending_ = false;
  908. pending_poll_addresses_.clear();
  909. pending_poll_multi_word_ranges_.clear();
  910. if (master_->state() != QModbusDevice::UnconnectedState)
  911. {
  912. master_->disconnectDevice();
  913. }
  914. serial_session_opened_ = false;
  915. received_valid_response_ = false;
  916. disconnecting_ = false;
  917. }
  918. // 修改连接状态并同步发出 Qt 信号和外部回调
  919. void PlcCommunicationService::setState(PlcConnectionState state)
  920. {
  921. if (state_ == state)
  922. {
  923. return;
  924. }
  925. // 状态集中从这里修改,确保 Qt 信号和 std::function 回调同步触发
  926. state_ = state;
  927. emit stateChanged();
  928. if (state_changed_callback_)
  929. {
  930. state_changed_callback_();
  931. }
  932. }
  933. // 保存通信故障、撤销首读资格并通知外部观察者
  934. void PlcCommunicationService::setError(const PlcCommunicationFailure &failure)
  935. {
  936. // 统一保存错误、停止正常轮询、撤销首读资格并通知 UI
  937. last_error_type_ = failure.type;
  938. last_error_ = toUtf8(failure.message);
  939. poll_timer_.stop();
  940. recovery_timer_.stop();
  941. // 任意通信故障都会撤销首读资格;恢复后必须重新完整读取
  942. updateInitialReadCompleted(false);
  943. const bool disconnected = failure.type == PlcCommunicationError::SerialPortOpenFailed
  944. || failure.type == PlcCommunicationError::SerialConnectionLost
  945. || failure.type == PlcCommunicationError::UsbSerialAdapterRemoved;
  946. if (disconnected)
  947. {
  948. closeSerialSession();
  949. }
  950. setState(disconnected
  951. ? PlcConnectionState::Disconnected : PlcConnectionState::Faulted);
  952. emit communicationError(failure.message);
  953. if (error_reported_callback_)
  954. {
  955. error_reported_callback_(last_error_);
  956. }
  957. if (isRecoverableTimeout(failure.type)
  958. && master_->state() == QModbusDevice::ConnectedState)
  959. {
  960. recovery_timer_.start(kRecoveryProbeIntervalMs);
  961. }
  962. }