综合平台编程器项目的远程存储
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.
 
 
 
 

336 line
13 KiB

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