综合平台编程器项目的远程存储
Nie możesz wybrać więcej, niż 25 tematów Tematy muszą się zaczynać od litery lub cyfry, mogą zawierać myślniki ('-') i mogą mieć do 35 znaków.
 
 
 
 

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