综合平台编程器项目的远程存储
您最多选择25个主题 主题必须以字母或数字开头,可以包含连字符 (-),并且长度不得超过35个字符
 
 
 
 

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