综合平台编程器项目的远程存储
Вы не можете выбрать более 25 тем Темы должны начинаться с буквы или цифры, могут содержать дефисы(-) и должны содержать не более 35 символов.
 
 
 
 

546 строки
23 KiB

  1. #include "domain/active_register_repository.h"
  2. #include "domain/project_limits.h"
  3. #include "domain/project_storage.h"
  4. #include "infrastructure/plc_communication_error_classifier.h"
  5. #include "infrastructure/plc_communication_service.h"
  6. #include "support/test_support.h"
  7. #include "infrastructure/plc_register_repository.h"
  8. #include "domain/virtual_register_repository.h"
  9. #include "services/offline_simulation_service.h"
  10. #include "services/plc_communication_gateway.h"
  11. #include "services/plc_discovery_gateway.h"
  12. #include "services/project_service.h"
  13. #include "services/runtime_mode_service.h"
  14. #include "services/logic_editor_service.h"
  15. #include <functional>
  16. #include <iostream>
  17. #include <set>
  18. #include <stdexcept>
  19. #include <tuple>
  20. #include <utility>
  21. namespace {
  22. using TestProjectStorage = TestSupport::InMemoryProjectStorage;
  23. class FakePlcGateway final : public PlcCommunicationGateway
  24. {
  25. public:
  26. PlcCommunicationResult connectDevice(
  27. const PlcSerialConfiguration &configuration) override
  28. {
  29. last_configuration = configuration;
  30. last_error_type = PlcCommunicationError::None;
  31. last_error.clear();
  32. initial_read = false;
  33. connection_state = PlcConnectionState::Connected;
  34. if (state_changed)
  35. {
  36. state_changed();
  37. }
  38. return {true, {}};
  39. }
  40. void disconnectDevice() override
  41. {
  42. ++disconnect_count;
  43. connection_state = PlcConnectionState::Disconnected;
  44. initial_read = false;
  45. if (initial_read_changed)
  46. {
  47. initial_read_changed(false);
  48. }
  49. if (state_changed)
  50. {
  51. state_changed();
  52. }
  53. }
  54. PlcCommunicationResult setPollAddresses(
  55. const std::vector<RegisterAddress> &addresses) override
  56. {
  57. poll_addresses = addresses;
  58. return {true, {}};
  59. }
  60. PlcConnectionState state() const override { return connection_state; }
  61. bool initialReadCompleted() const override { return initial_read; }
  62. PlcCommunicationError lastErrorType() const override { return last_error_type; }
  63. const std::string &lastError() const override { return last_error; }
  64. void setCallbacks(
  65. std::function<void()> state_callback,
  66. std::function<void(bool)> initial_callback,
  67. std::function<void()> cache_callback,
  68. std::function<void()> poll_cycle_callback,
  69. std::function<void(const std::string &)> error_callback) override
  70. {
  71. state_changed = std::move(state_callback);
  72. initial_read_changed = std::move(initial_callback);
  73. cache_updated = std::move(cache_callback);
  74. poll_cycle_completed = std::move(poll_cycle_callback);
  75. error_reported = std::move(error_callback);
  76. }
  77. void completeInitialRead()
  78. {
  79. initial_read = true;
  80. if (initial_read_changed)
  81. {
  82. initial_read_changed(true);
  83. }
  84. }
  85. void failCommunication(
  86. PlcCommunicationError error_type, const std::string &message)
  87. {
  88. initial_read = false;
  89. last_error_type = error_type;
  90. last_error = message;
  91. connection_state = PlcConnectionState::Faulted;
  92. if (initial_read_changed)
  93. {
  94. initial_read_changed(false);
  95. }
  96. if (state_changed)
  97. {
  98. state_changed();
  99. }
  100. if (error_reported)
  101. {
  102. error_reported(last_error);
  103. }
  104. }
  105. PlcConnectionState connection_state = PlcConnectionState::Disconnected;
  106. bool initial_read = false;
  107. int disconnect_count = 0;
  108. PlcCommunicationError last_error_type = PlcCommunicationError::None;
  109. std::string last_error;
  110. PlcSerialConfiguration last_configuration;
  111. std::vector<RegisterAddress> poll_addresses;
  112. std::function<void()> state_changed;
  113. std::function<void(bool)> initial_read_changed;
  114. std::function<void()> cache_updated;
  115. std::function<void()> poll_cycle_completed;
  116. std::function<void(const std::string &)> error_reported;
  117. };
  118. using TestSupport::require;
  119. void testPlcCacheAndWriteForwarding()
  120. {
  121. PlcRegisterRepository repository;
  122. const RegisterAddress m0{RegisterArea::M, 0};
  123. const RegisterAddress d0{RegisterArea::D, 0};
  124. require(repository.readBit(m0).error == RegisterError::Unavailable,
  125. "uninitialized PLC bit cache must be unavailable");
  126. require(repository.readWord(d0).error == RegisterError::Unavailable,
  127. "uninitialized PLC word cache must be unavailable");
  128. repository.updateBit(0, true);
  129. repository.updateWord(0, -123);
  130. require(repository.readBit(m0).succeeded && repository.readBit(m0).value,
  131. "valid PLC bit cache must be readable");
  132. require(repository.readWord(d0).succeeded
  133. && repository.readWord(d0).value == -123,
  134. "valid PLC word cache must preserve signed values");
  135. RegisterAddress written_address{RegisterArea::M, 1};
  136. bool written_bit = false;
  137. std::int16_t written_word = 0;
  138. std::vector<std::int16_t> written_words;
  139. int single_word_write_count = 0;
  140. int multi_word_write_count = 0;
  141. repository.setWriteHandlers(
  142. [&](const RegisterAddress &address, bool value)
  143. {
  144. written_address = address;
  145. written_bit = value;
  146. return RegisterWriteResult{true, RegisterError::None};
  147. },
  148. [&](const RegisterAddress &address, std::int16_t value)
  149. {
  150. written_address = address;
  151. written_word = value;
  152. ++single_word_write_count;
  153. return RegisterWriteResult{true, RegisterError::None};
  154. },
  155. [&](const RegisterAddress &address,
  156. const std::vector<std::int16_t> &values)
  157. {
  158. written_address = address;
  159. written_words = values;
  160. ++multi_word_write_count;
  161. return RegisterWriteResult{true, RegisterError::None};
  162. });
  163. require(repository.writeBit(m0, false).succeeded
  164. && written_address == m0 && !written_bit,
  165. "PLC bit writes must be forwarded without changing the cache");
  166. require(repository.writeWord(d0, 456).succeeded
  167. && written_address == d0 && written_word == 456
  168. && single_word_write_count == 1,
  169. "PLC word writes must be forwarded without changing the cache");
  170. require(repository.writeWords(d0, {789}).succeeded
  171. && written_word == 789
  172. && single_word_write_count == 2
  173. && multi_word_write_count == 0,
  174. "one-word generic writes must keep using the PLC single-word request");
  175. const RegisterAddress d10{RegisterArea::D, 10};
  176. require(repository.writeWords(
  177. d10,
  178. {static_cast<std::int16_t>(0x5678),
  179. static_cast<std::int16_t>(0x1234)}).succeeded
  180. && written_address == d10
  181. && written_words.size() == 2U
  182. && static_cast<std::uint16_t>(written_words[0]) == 0x5678U
  183. && static_cast<std::uint16_t>(written_words[1]) == 0x1234U
  184. && multi_word_write_count == 1,
  185. "Int32 words must be forwarded in one low-word-first request");
  186. const RegisterAddress d20{RegisterArea::D, 20};
  187. require(repository.writeWords(
  188. d20,
  189. {static_cast<std::int16_t>(0x0000),
  190. static_cast<std::int16_t>(0x0000),
  191. static_cast<std::int16_t>(0x0000),
  192. static_cast<std::int16_t>(0x3ff0)}).succeeded
  193. && written_address == d20
  194. && written_words.size() == 4U
  195. && static_cast<std::uint16_t>(written_words[3]) == 0x3ff0U
  196. && multi_word_write_count == 2,
  197. "Double words must be forwarded in one four-register request");
  198. repository.invalidate();
  199. require(!repository.hasAnyValidValue(),
  200. "disconnecting must invalidate all PLC cache validity flags");
  201. }
  202. void testRuntimeRepositorySwitchingAndDisconnect()
  203. {
  204. TestProjectStorage storage;
  205. ProjectService project_service(storage);
  206. HmiPage page;
  207. page.id = "main-page";
  208. page.name = "Main";
  209. HmiControl indicator;
  210. indicator.id = "run-state";
  211. indicator.type = HmiControlType::Indicator;
  212. indicator.bounds = {0, 0, 80, 40};
  213. indicator.text = "Run";
  214. indicator.binding = RegisterAddress{RegisterArea::M, 5};
  215. page.controls.push_back(indicator);
  216. project_service.editProject().initialHmiPageId = page.id;
  217. project_service.editProject().hmiPages.push_back(page);
  218. VirtualRegisterRepository virtual_repository;
  219. PlcRegisterRepository plc_repository;
  220. ActiveRegisterRepository active_repository(virtual_repository);
  221. OfflineSimulationService simulation_service(virtual_repository);
  222. OnlineLogicMonitorService online_monitor_service(plc_repository);
  223. LogicEditorService logic_editor_service(project_service);
  224. FakePlcGateway gateway;
  225. RuntimeModeService service(
  226. project_service,
  227. logic_editor_service,
  228. simulation_service,
  229. online_monitor_service);
  230. service.configurePlc(
  231. gateway, active_repository, virtual_repository, plc_repository);
  232. const PlcCommunicationResult connected = service.connectPlc(
  233. {"COM9", 2, 19200, 8, 2, 1, 1000, 2, 200});
  234. require(connected.succeeded && gateway.poll_addresses.size() == 1U,
  235. "PLC connection must receive the project address poll set");
  236. service.setMonitorAddresses({
  237. RegisterAddress{RegisterArea::M, 5},
  238. RegisterAddress{RegisterArea::D, 8}});
  239. require(gateway.poll_addresses.size() == 2U
  240. && gateway.poll_addresses.front()
  241. == RegisterAddress{RegisterArea::M, 5}
  242. && gateway.poll_addresses.back()
  243. == RegisterAddress{RegisterArea::D, 8},
  244. "monitor addresses must merge with and deduplicate project poll addresses");
  245. require(service.enterOnlineRunning().error
  246. == ModeTransitionError::InitialPlcReadRequired,
  247. "online mode must wait for the first valid PLC read");
  248. plc_repository.updateBit(5, true);
  249. gateway.completeInitialRead();
  250. require(service.enterOnlineRunning().succeeded,
  251. "online mode must start after the first valid PLC read");
  252. require(active_repository.readBit({RegisterArea::M, 5}).succeeded
  253. && active_repository.readBit({RegisterArea::M, 5}).value,
  254. "online mode must expose the PLC cache through the active repository");
  255. gateway.disconnectDevice();
  256. require(service.mode() == ApplicationMode::Editing,
  257. "an online disconnect must return the application to editing mode");
  258. require(service.onlineLogicMonitorService().state()
  259. == OnlineLogicMonitorState::Stopped,
  260. "an online disconnect must stop the local read-only trace executor");
  261. require(!service.initialPlcReadCompleted(),
  262. "an online disconnect must clear the initial read flag");
  263. require(active_repository.readBit({RegisterArea::M, 5}).succeeded
  264. && !active_repository.readBit({RegisterArea::M, 5}).value,
  265. "editing after disconnect must switch back to the virtual repository");
  266. }
  267. void testRuntimeFaultRevokesOnlineReadinessAndAllowsReconnect()
  268. {
  269. TestProjectStorage storage;
  270. ProjectService project_service(storage);
  271. VirtualRegisterRepository virtual_repository;
  272. PlcRegisterRepository plc_repository;
  273. ActiveRegisterRepository active_repository(virtual_repository);
  274. OfflineSimulationService simulation_service(virtual_repository);
  275. OnlineLogicMonitorService online_monitor_service(plc_repository);
  276. LogicEditorService logic_editor_service(project_service);
  277. FakePlcGateway gateway;
  278. RuntimeModeService service(
  279. project_service,
  280. logic_editor_service,
  281. simulation_service,
  282. online_monitor_service);
  283. service.configurePlc(
  284. gateway, active_repository, virtual_repository, plc_repository);
  285. require(service.connectPlc(
  286. {"COM9", 1, 9600, 8, 2, 1, 1000, 2, 200}).succeeded,
  287. "PLC connection must start before testing a communication fault");
  288. gateway.completeInitialRead();
  289. require(service.enterOnlineRunning().succeeded,
  290. "completed initial read must allow online running before a fault");
  291. gateway.failCommunication(
  292. PlcCommunicationError::CommunicationTimeout,
  293. "PLC communication timed out while the serial port remained open");
  294. require(service.mode() == ApplicationMode::Editing,
  295. "a PLC communication fault must return online running to editing");
  296. require(service.onlineLogicMonitorService().state()
  297. == OnlineLogicMonitorState::Stopped,
  298. "a PLC communication fault must stop the local trace executor");
  299. require(!service.initialPlcReadCompleted(),
  300. "a PLC communication fault must revoke initial read readiness");
  301. require(service.enterOnlineRunning().error
  302. == ModeTransitionError::InitialPlcReadRequired,
  303. "faulted PLC state must not reuse readiness from the previous connection");
  304. const int disconnect_count = gateway.disconnect_count;
  305. require(service.connectPlc(
  306. {"COM9", 1, 9600, 8, 2, 1, 1000, 2, 200}).succeeded,
  307. "faulted PLC state must support direct reconnect");
  308. require(gateway.disconnect_count == disconnect_count + 1,
  309. "reconnecting from a fault must clean the previous serial session first");
  310. }
  311. void testPlcCommunicationErrorClassification()
  312. {
  313. PlcCommunicationErrorContext context;
  314. context.portName = QStringLiteral("COM3");
  315. PlcCommunicationFailure failure = classifyPlcCommunicationError(
  316. QModbusDevice::ConnectionError, context);
  317. require(failure.type == PlcCommunicationError::SerialPortOpenFailed
  318. && failure.message.contains(QStringLiteral("未能打开")),
  319. "connection failure before opening the port must be classified explicitly");
  320. context.serialSessionOpened = true;
  321. failure = classifyPlcCommunicationError(QModbusDevice::TimeoutError, context);
  322. require(failure.type == PlcCommunicationError::PlcNotResponding
  323. && failure.message.contains(QStringLiteral("PLC 未响应")),
  324. "initial timeout with an open serial port must report a non-responsive PLC");
  325. context.receivedValidResponse = true;
  326. failure = classifyPlcCommunicationError(QModbusDevice::TimeoutError, context);
  327. require(failure.type == PlcCommunicationError::CommunicationTimeout
  328. && failure.message.contains(QStringLiteral("仍处于打开状态")),
  329. "timeout after valid traffic must report an open local serial session");
  330. failure = classifyPlcCommunicationError(QModbusDevice::ConnectionError, context);
  331. require(failure.type == PlcCommunicationError::SerialConnectionLost
  332. && failure.message.contains(QStringLiteral("连接已中断")),
  333. "connection errors after opening the port must report a lost serial link");
  334. failure = classifyPlcCommunicationError(QModbusDevice::ProtocolError, context);
  335. require(failure.type == PlcCommunicationError::ProtocolError
  336. && failure.message.contains(QStringLiteral("Modbus 协议异常")),
  337. "protocol errors must remain distinct from timeouts and disconnections");
  338. }
  339. void testPlcConfigurationBoundaries()
  340. {
  341. PlcSerialConfiguration configuration;
  342. configuration.portName = "COM3";
  343. require(validatePlcSerialConfiguration(configuration).succeeded,
  344. "the standard COM3 9600 8E1 configuration must be accepted");
  345. configuration.serverAddress = ProjectLimits::kMaximumPlcServerAddress;
  346. require(validatePlcSerialConfiguration(configuration).succeeded,
  347. "PLC station 247 must be accepted");
  348. configuration.serverAddress = ProjectLimits::kMaximumPlcServerAddress + 1;
  349. require(!validatePlcSerialConfiguration(configuration).succeeded,
  350. "PLC station 248 must be rejected");
  351. configuration.serverAddress = 1;
  352. configuration.retries = ProjectLimits::kMaximumRetries;
  353. require(validatePlcSerialConfiguration(configuration).succeeded,
  354. "five retries must be accepted");
  355. configuration.retries = ProjectLimits::kMaximumRetries + 1;
  356. require(!validatePlcSerialConfiguration(configuration).succeeded,
  357. "six retries must be rejected");
  358. configuration.retries = 0;
  359. configuration.responseTimeoutMs = ProjectLimits::kMinimumResponseTimeoutMs - 1;
  360. require(!validatePlcSerialConfiguration(configuration).succeeded,
  361. "a response timeout below 100 ms must be rejected");
  362. configuration.responseTimeoutMs = ProjectLimits::kMinimumResponseTimeoutMs;
  363. require(validatePlcSerialConfiguration(configuration).succeeded,
  364. "a response timeout of 100 ms must be accepted");
  365. }
  366. void testPlcDiscoveryCandidatePriorityAndCoverage()
  367. {
  368. PlcSerialConfiguration preferred;
  369. preferred.portName = "COM3";
  370. preferred.serverAddress = 17;
  371. preferred.baudRate = 38400;
  372. preferred.dataBits = 7;
  373. preferred.parity = 3;
  374. preferred.stopBits = 2;
  375. preferred.responseTimeoutMs = 1500;
  376. preferred.retries = 4;
  377. preferred.pollIntervalMs = 350;
  378. const std::vector<PlcSerialConfiguration> candidates =
  379. buildPlcDiscoveryCandidates(
  380. preferred, {"COM1", "COM2", "COM3", "COM2", ""});
  381. require(candidates.size() == 180U,
  382. "discovery must cover 60 serial settings on every unique available port");
  383. require(candidates.front().portName == "COM3"
  384. && candidates.front().serverAddress == 17
  385. && candidates.front().baudRate == 38400
  386. && candidates.front().dataBits == 7
  387. && candidates.front().parity == 3
  388. && candidates.front().stopBits == 2,
  389. "discovery must try the complete current configuration first");
  390. require(candidates.at(1).portName == "COM1"
  391. && candidates.at(2).portName == "COM2",
  392. "the preferred port must be first without dropping other ports");
  393. std::set<std::tuple<std::string, int, int, int, int>> unique_candidates;
  394. bool contains_standard_8e1 = false;
  395. bool contains_full_supported_edge = false;
  396. for (const PlcSerialConfiguration &candidate : candidates)
  397. {
  398. require(candidate.serverAddress == preferred.serverAddress
  399. && candidate.responseTimeoutMs == preferred.responseTimeoutMs
  400. && candidate.retries == preferred.retries
  401. && candidate.pollIntervalMs == preferred.pollIntervalMs,
  402. "discovery must preserve the selected station and normal timing settings");
  403. unique_candidates.emplace(
  404. candidate.portName,
  405. candidate.baudRate,
  406. candidate.dataBits,
  407. candidate.parity,
  408. candidate.stopBits);
  409. contains_standard_8e1 = contains_standard_8e1
  410. || (candidate.portName == "COM2"
  411. && candidate.baudRate == 9600
  412. && candidate.dataBits == 8
  413. && candidate.parity == 2
  414. && candidate.stopBits == 1);
  415. contains_full_supported_edge = contains_full_supported_edge
  416. || (candidate.portName == "COM1"
  417. && candidate.baudRate == 115200
  418. && candidate.dataBits == 7
  419. && candidate.parity == 0
  420. && candidate.stopBits == 2);
  421. }
  422. require(unique_candidates.size() == candidates.size(),
  423. "discovery candidates must not contain duplicate serial settings");
  424. require(contains_standard_8e1 && contains_full_supported_edge,
  425. "discovery must cover common settings and every supported boundary");
  426. require(buildPlcDiscoveryCandidates(preferred, {}).empty(),
  427. "discovery must stop immediately when no serial port is available");
  428. }
  429. void testPlcPollQuantityBoundaries()
  430. {
  431. PlcRegisterRepository repository;
  432. PlcCommunicationService service(repository);
  433. std::vector<RegisterAddress> too_many_addresses;
  434. for (int index = 0; index <= RegisterAddress::kMaximumIndex; ++index)
  435. {
  436. too_many_addresses.push_back({RegisterArea::M, index});
  437. }
  438. too_many_addresses.push_back({RegisterArea::D, 0});
  439. require(!service.setPollAddresses(too_many_addresses).succeeded,
  440. "more than the configured poll address limit must be rejected");
  441. std::vector<RegisterAddress> too_many_blocks;
  442. for (std::size_t index = 0;
  443. index < ProjectLimits::kMaximumPollBlocks + 1U;
  444. ++index)
  445. {
  446. too_many_blocks.push_back({RegisterArea::M, static_cast<int>(index * 2U)});
  447. }
  448. require(!service.setPollAddresses(too_many_blocks).succeeded,
  449. "more than the configured Modbus read block limit must be rejected");
  450. std::vector<RegisterAddress> maximum_contiguous_block;
  451. for (int index = 0; index < ProjectLimits::kMaximumModbusReadCount; ++index)
  452. {
  453. maximum_contiguous_block.push_back({RegisterArea::M, index});
  454. }
  455. require(service.setPollAddresses(maximum_contiguous_block).succeeded,
  456. "a contiguous Modbus read block of 120 values must be accepted");
  457. std::vector<RegisterAddress> double_boundary_addresses;
  458. for (int index = 0; index <= 123; ++index)
  459. {
  460. double_boundary_addresses.push_back({RegisterArea::D, index});
  461. }
  462. require(service.setPollAddresses(
  463. double_boundary_addresses,
  464. {{RegisterAddress{RegisterArea::D, 118}, 4}}).succeeded
  465. && service.pollBlocks().size() == 2U
  466. && service.pollBlocks()[0].area == RegisterArea::D
  467. && service.pollBlocks()[0].startAddress == 0
  468. && service.pollBlocks()[0].count == 118
  469. && service.pollBlocks()[1].startAddress == 118
  470. && service.pollBlocks()[1].count == 6,
  471. "a Double at the 120-word boundary must stay in one Modbus read block");
  472. std::vector<RegisterWordRange> unsplittable_ranges;
  473. for (int start = 0; start <= 120; start += 3)
  474. {
  475. unsplittable_ranges.push_back({
  476. RegisterAddress{RegisterArea::D, start}, 4});
  477. }
  478. require(!service.setPollAddresses({}, unsplittable_ranges).succeeded,
  479. "an overlapping multi-word chain without a 120-word split point must fail");
  480. }
  481. } // namespace
  482. int main()
  483. {
  484. try
  485. {
  486. testPlcCacheAndWriteForwarding();
  487. testRuntimeRepositorySwitchingAndDisconnect();
  488. testRuntimeFaultRevokesOnlineReadinessAndAllowsReconnect();
  489. testPlcCommunicationErrorClassification();
  490. testPlcConfigurationBoundaries();
  491. testPlcDiscoveryCandidatePriorityAndCoverage();
  492. testPlcPollQuantityBoundaries();
  493. }
  494. catch (const std::exception &error)
  495. {
  496. std::cerr << "PLC runtime tests failed: " << error.what() << '\n';
  497. return 1;
  498. }
  499. std::cout << "PLC runtime tests passed\n";
  500. return 0;
  501. }