综合平台编程器项目的远程存储
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

401 lines
16 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 "infrastructure/plc_register_repository.h"
  7. #include "services/offline_simulation_service.h"
  8. #include "services/plc_communication_gateway.h"
  9. #include "services/project_service.h"
  10. #include "services/runtime_mode_service.h"
  11. #include <functional>
  12. #include <iostream>
  13. #include <stdexcept>
  14. #include <utility>
  15. namespace {
  16. class TestProjectStorage final : public ProjectStorage
  17. {
  18. public:
  19. ProjectSaveResult save(const Project &, const std::string &) override
  20. {
  21. return {true, ProjectStorageError::None, {}};
  22. }
  23. ProjectLoadResult load(const std::string &) override
  24. {
  25. return {false, {}, ProjectStorageError::FileReadFailed, {}};
  26. }
  27. };
  28. class FakePlcGateway final : public PlcCommunicationGateway
  29. {
  30. public:
  31. PlcCommunicationResult connectDevice(
  32. const PlcSerialConfiguration &configuration) override
  33. {
  34. last_configuration = configuration;
  35. last_error_type = PlcCommunicationError::None;
  36. last_error.clear();
  37. initial_read = false;
  38. connection_state = PlcConnectionState::Connected;
  39. if (state_changed)
  40. {
  41. state_changed();
  42. }
  43. return {true, {}};
  44. }
  45. void disconnectDevice() override
  46. {
  47. ++disconnect_count;
  48. connection_state = PlcConnectionState::Disconnected;
  49. initial_read = false;
  50. if (initial_read_changed)
  51. {
  52. initial_read_changed(false);
  53. }
  54. if (state_changed)
  55. {
  56. state_changed();
  57. }
  58. }
  59. PlcCommunicationResult setPollAddresses(
  60. const std::vector<RegisterAddress> &addresses) override
  61. {
  62. poll_addresses = addresses;
  63. return {true, {}};
  64. }
  65. PlcConnectionState state() const override { return connection_state; }
  66. bool initialReadCompleted() const override { return initial_read; }
  67. PlcCommunicationError lastErrorType() const override { return last_error_type; }
  68. const std::string &lastError() const override { return last_error; }
  69. void setCallbacks(
  70. std::function<void()> state_callback,
  71. std::function<void(bool)> initial_callback,
  72. std::function<void()> cache_callback,
  73. std::function<void(const std::string &)> error_callback) override
  74. {
  75. state_changed = std::move(state_callback);
  76. initial_read_changed = std::move(initial_callback);
  77. cache_updated = std::move(cache_callback);
  78. error_reported = std::move(error_callback);
  79. }
  80. void completeInitialRead()
  81. {
  82. initial_read = true;
  83. if (initial_read_changed)
  84. {
  85. initial_read_changed(true);
  86. }
  87. }
  88. void failCommunication(
  89. PlcCommunicationError error_type, const std::string &message)
  90. {
  91. initial_read = false;
  92. last_error_type = error_type;
  93. last_error = message;
  94. connection_state = PlcConnectionState::Faulted;
  95. if (initial_read_changed)
  96. {
  97. initial_read_changed(false);
  98. }
  99. if (state_changed)
  100. {
  101. state_changed();
  102. }
  103. if (error_reported)
  104. {
  105. error_reported(last_error);
  106. }
  107. }
  108. PlcConnectionState connection_state = PlcConnectionState::Disconnected;
  109. bool initial_read = false;
  110. int disconnect_count = 0;
  111. PlcCommunicationError last_error_type = PlcCommunicationError::None;
  112. std::string last_error;
  113. PlcSerialConfiguration last_configuration;
  114. std::vector<RegisterAddress> poll_addresses;
  115. std::function<void()> state_changed;
  116. std::function<void(bool)> initial_read_changed;
  117. std::function<void()> cache_updated;
  118. std::function<void(const std::string &)> error_reported;
  119. };
  120. void require(bool condition, const std::string &message)
  121. {
  122. if (!condition)
  123. {
  124. throw std::runtime_error(message);
  125. }
  126. }
  127. void testPlcCacheAndWriteForwarding()
  128. {
  129. PlcRegisterRepository repository;
  130. const RegisterAddress m0{RegisterArea::M, 0};
  131. const RegisterAddress d0{RegisterArea::D, 0};
  132. require(repository.readBit(m0).error == RegisterError::Unavailable,
  133. "uninitialized PLC bit cache must be unavailable");
  134. require(repository.readWord(d0).error == RegisterError::Unavailable,
  135. "uninitialized PLC word cache must be unavailable");
  136. repository.updateBit(0, true);
  137. repository.updateWord(0, -123);
  138. require(repository.readBit(m0).succeeded && repository.readBit(m0).value,
  139. "valid PLC bit cache must be readable");
  140. require(repository.readWord(d0).succeeded
  141. && repository.readWord(d0).value == -123,
  142. "valid PLC word cache must preserve signed values");
  143. RegisterAddress written_address{RegisterArea::M, 1};
  144. bool written_bit = false;
  145. std::int16_t written_word = 0;
  146. repository.setWriteHandlers(
  147. [&](const RegisterAddress &address, bool value)
  148. {
  149. written_address = address;
  150. written_bit = value;
  151. return RegisterWriteResult{true, RegisterError::None};
  152. },
  153. [&](const RegisterAddress &address, std::int16_t value)
  154. {
  155. written_address = address;
  156. written_word = value;
  157. return RegisterWriteResult{true, RegisterError::None};
  158. });
  159. require(repository.writeBit(m0, false).succeeded
  160. && written_address == m0 && !written_bit,
  161. "PLC bit writes must be forwarded without changing the cache");
  162. require(repository.writeWord(d0, 456).succeeded
  163. && written_address == d0 && written_word == 456,
  164. "PLC word writes must be forwarded without changing the cache");
  165. repository.invalidate();
  166. require(!repository.hasAnyValidValue(),
  167. "disconnecting must invalidate all PLC cache validity flags");
  168. }
  169. void testRuntimeRepositorySwitchingAndDisconnect()
  170. {
  171. TestProjectStorage storage;
  172. ProjectService project_service(storage);
  173. HmiPage page;
  174. page.id = "main-page";
  175. page.name = "Main";
  176. HmiControl indicator;
  177. indicator.id = "run-state";
  178. indicator.type = HmiControlType::Indicator;
  179. indicator.bounds = {0, 0, 80, 40};
  180. indicator.text = "Run";
  181. indicator.binding = RegisterAddress{RegisterArea::M, 5};
  182. page.controls.push_back(indicator);
  183. project_service.editProject().hmiPages.push_back(page);
  184. VirtualRegisterRepository virtual_repository;
  185. PlcRegisterRepository plc_repository;
  186. ActiveRegisterRepository active_repository(virtual_repository);
  187. OfflineSimulationService simulation_service(virtual_repository);
  188. FakePlcGateway gateway;
  189. RuntimeModeService service(project_service, simulation_service);
  190. service.configurePlc(
  191. gateway, active_repository, virtual_repository, plc_repository);
  192. const PlcCommunicationResult connected = service.connectPlc(
  193. {"COM9", 2, 19200, 8, 2, 1, 1000, 2, 200});
  194. require(connected.succeeded && gateway.poll_addresses.size() == 1U,
  195. "PLC connection must receive the project address poll set");
  196. service.setMonitorAddresses({
  197. RegisterAddress{RegisterArea::M, 5},
  198. RegisterAddress{RegisterArea::D, 8}});
  199. require(gateway.poll_addresses.size() == 2U
  200. && gateway.poll_addresses.front()
  201. == RegisterAddress{RegisterArea::M, 5}
  202. && gateway.poll_addresses.back()
  203. == RegisterAddress{RegisterArea::D, 8},
  204. "monitor addresses must merge with and deduplicate project poll addresses");
  205. require(service.enterOnlineRunning().error
  206. == ModeTransitionError::InitialPlcReadRequired,
  207. "online mode must wait for the first valid PLC read");
  208. plc_repository.updateBit(5, true);
  209. gateway.completeInitialRead();
  210. require(service.enterOnlineRunning().succeeded,
  211. "online mode must start after the first valid PLC read");
  212. require(active_repository.readBit({RegisterArea::M, 5}).succeeded
  213. && active_repository.readBit({RegisterArea::M, 5}).value,
  214. "online mode must expose the PLC cache through the active repository");
  215. gateway.disconnectDevice();
  216. require(service.mode() == ApplicationMode::Editing,
  217. "an online disconnect must return the application to editing mode");
  218. require(!service.initialPlcReadCompleted(),
  219. "an online disconnect must clear the initial read flag");
  220. require(active_repository.readBit({RegisterArea::M, 5}).succeeded
  221. && !active_repository.readBit({RegisterArea::M, 5}).value,
  222. "editing after disconnect must switch back to the virtual repository");
  223. }
  224. void testRuntimeFaultRevokesOnlineReadinessAndAllowsReconnect()
  225. {
  226. TestProjectStorage storage;
  227. ProjectService project_service(storage);
  228. VirtualRegisterRepository virtual_repository;
  229. PlcRegisterRepository plc_repository;
  230. ActiveRegisterRepository active_repository(virtual_repository);
  231. OfflineSimulationService simulation_service(virtual_repository);
  232. FakePlcGateway gateway;
  233. RuntimeModeService service(project_service, simulation_service);
  234. service.configurePlc(
  235. gateway, active_repository, virtual_repository, plc_repository);
  236. require(service.connectPlc(
  237. {"COM9", 1, 9600, 8, 2, 1, 1000, 2, 200}).succeeded,
  238. "PLC connection must start before testing a communication fault");
  239. gateway.completeInitialRead();
  240. require(service.enterOnlineRunning().succeeded,
  241. "completed initial read must allow online running before a fault");
  242. gateway.failCommunication(
  243. PlcCommunicationError::CommunicationTimeout,
  244. "PLC communication timed out while the serial port remained open");
  245. require(service.mode() == ApplicationMode::Editing,
  246. "a PLC communication fault must return online running to editing");
  247. require(!service.initialPlcReadCompleted(),
  248. "a PLC communication fault must revoke initial read readiness");
  249. require(service.enterOnlineRunning().error
  250. == ModeTransitionError::InitialPlcReadRequired,
  251. "faulted PLC state must not reuse readiness from the previous connection");
  252. const int disconnect_count = gateway.disconnect_count;
  253. require(service.connectPlc(
  254. {"COM9", 1, 9600, 8, 2, 1, 1000, 2, 200}).succeeded,
  255. "faulted PLC state must support direct reconnect");
  256. require(gateway.disconnect_count == disconnect_count + 1,
  257. "reconnecting from a fault must clean the previous serial session first");
  258. }
  259. void testPlcCommunicationErrorClassification()
  260. {
  261. PlcCommunicationErrorContext context;
  262. context.portName = QStringLiteral("COM3");
  263. PlcCommunicationFailure failure = classifyPlcCommunicationError(
  264. QModbusDevice::ConnectionError, context);
  265. require(failure.type == PlcCommunicationError::SerialPortOpenFailed
  266. && failure.message.contains(QStringLiteral("未能打开")),
  267. "connection failure before opening the port must be classified explicitly");
  268. context.serialSessionOpened = true;
  269. failure = classifyPlcCommunicationError(QModbusDevice::TimeoutError, context);
  270. require(failure.type == PlcCommunicationError::PlcNotResponding
  271. && failure.message.contains(QStringLiteral("PLC 未响应")),
  272. "initial timeout with an open serial port must report a non-responsive PLC");
  273. context.receivedValidResponse = true;
  274. failure = classifyPlcCommunicationError(QModbusDevice::TimeoutError, context);
  275. require(failure.type == PlcCommunicationError::CommunicationTimeout
  276. && failure.message.contains(QStringLiteral("仍处于打开状态")),
  277. "timeout after valid traffic must report an open local serial session");
  278. failure = classifyPlcCommunicationError(QModbusDevice::ConnectionError, context);
  279. require(failure.type == PlcCommunicationError::SerialConnectionLost
  280. && failure.message.contains(QStringLiteral("连接已中断")),
  281. "connection errors after opening the port must report a lost serial link");
  282. failure = classifyPlcCommunicationError(QModbusDevice::ProtocolError, context);
  283. require(failure.type == PlcCommunicationError::ProtocolError
  284. && failure.message.contains(QStringLiteral("Modbus 协议异常")),
  285. "protocol errors must remain distinct from timeouts and disconnections");
  286. }
  287. void testPlcConfigurationBoundaries()
  288. {
  289. PlcSerialConfiguration configuration;
  290. configuration.portName = "COM3";
  291. require(validatePlcSerialConfiguration(configuration).succeeded,
  292. "the standard COM3 9600 8E1 configuration must be accepted");
  293. configuration.serverAddress = ProjectLimits::kMaximumPlcServerAddress;
  294. require(validatePlcSerialConfiguration(configuration).succeeded,
  295. "PLC station 247 must be accepted");
  296. configuration.serverAddress = ProjectLimits::kMaximumPlcServerAddress + 1;
  297. require(!validatePlcSerialConfiguration(configuration).succeeded,
  298. "PLC station 248 must be rejected");
  299. configuration.serverAddress = 1;
  300. configuration.retries = ProjectLimits::kMaximumRetries;
  301. require(validatePlcSerialConfiguration(configuration).succeeded,
  302. "five retries must be accepted");
  303. configuration.retries = ProjectLimits::kMaximumRetries + 1;
  304. require(!validatePlcSerialConfiguration(configuration).succeeded,
  305. "six retries must be rejected");
  306. configuration.retries = 0;
  307. configuration.responseTimeoutMs = ProjectLimits::kMinimumResponseTimeoutMs - 1;
  308. require(!validatePlcSerialConfiguration(configuration).succeeded,
  309. "a response timeout below 100 ms must be rejected");
  310. configuration.responseTimeoutMs = ProjectLimits::kMinimumResponseTimeoutMs;
  311. require(validatePlcSerialConfiguration(configuration).succeeded,
  312. "a response timeout of 100 ms must be accepted");
  313. }
  314. void testPlcPollQuantityBoundaries()
  315. {
  316. PlcRegisterRepository repository;
  317. PlcCommunicationService service(repository);
  318. std::vector<RegisterAddress> too_many_addresses;
  319. for (int index = 0; index <= RegisterAddress::kMaximumIndex; ++index)
  320. {
  321. too_many_addresses.push_back({RegisterArea::M, index});
  322. }
  323. too_many_addresses.push_back({RegisterArea::D, 0});
  324. require(!service.setPollAddresses(too_many_addresses).succeeded,
  325. "more than 1024 poll addresses must be rejected");
  326. std::vector<RegisterAddress> too_many_blocks;
  327. for (int index = 0; index < 65; ++index)
  328. {
  329. too_many_blocks.push_back({RegisterArea::M, index * 2});
  330. }
  331. require(!service.setPollAddresses(too_many_blocks).succeeded,
  332. "more than 64 Modbus read blocks must be rejected");
  333. std::vector<RegisterAddress> maximum_contiguous_block;
  334. for (int index = 0; index < ProjectLimits::kMaximumModbusReadCount; ++index)
  335. {
  336. maximum_contiguous_block.push_back({RegisterArea::M, index});
  337. }
  338. require(service.setPollAddresses(maximum_contiguous_block).succeeded,
  339. "a contiguous Modbus read block of 120 values must be accepted");
  340. }
  341. } // namespace
  342. int main()
  343. {
  344. try
  345. {
  346. testPlcCacheAndWriteForwarding();
  347. testRuntimeRepositorySwitchingAndDisconnect();
  348. testRuntimeFaultRevokesOnlineReadinessAndAllowsReconnect();
  349. testPlcCommunicationErrorClassification();
  350. testPlcConfigurationBoundaries();
  351. testPlcPollQuantityBoundaries();
  352. }
  353. catch (const std::exception &error)
  354. {
  355. std::cerr << "PLC runtime tests failed: " << error.what() << '\n';
  356. return 1;
  357. }
  358. std::cout << "PLC runtime tests passed\n";
  359. return 0;
  360. }