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

473 wiersze
18 KiB

  1. #include "services/runtime_mode_service.h"
  2. #include "services/logic_editor_service.h"
  3. #include "services/offline_simulation_service.h"
  4. #include "services/project_service.h"
  5. #include "domain/active_register_repository.h"
  6. #include "domain/project_storage.h"
  7. #include "domain/register_repository.h"
  8. #include "domain/virtual_register_repository.h"
  9. #include "support/test_support.h"
  10. #include <functional>
  11. #include <iostream>
  12. #include <stdexcept>
  13. #include <string>
  14. #include <utility>
  15. namespace {
  16. using TestProjectStorage = TestSupport::InMemoryProjectStorage;
  17. class ReadyPlcGateway final : public PlcCommunicationGateway
  18. {
  19. public:
  20. PlcCommunicationResult connectDevice(const PlcSerialConfiguration &) override
  21. {
  22. connection_state = PlcConnectionState::Connected;
  23. if (state_changed)
  24. {
  25. state_changed();
  26. }
  27. return {true, {}};
  28. }
  29. void disconnectDevice() override
  30. {
  31. connection_state = PlcConnectionState::Disconnected;
  32. initial_read = false;
  33. }
  34. PlcCommunicationResult setPollAddresses(
  35. const std::vector<RegisterAddress> &addresses) override
  36. {
  37. return setPollAddresses(addresses, {});
  38. }
  39. PlcCommunicationResult setPollAddresses(
  40. const std::vector<RegisterAddress> &addresses,
  41. const std::vector<RegisterWordRange> &ranges) override
  42. {
  43. if (reject_poll_configuration)
  44. {
  45. return {false, "poll configuration rejected"};
  46. }
  47. poll_addresses = addresses;
  48. poll_ranges = ranges;
  49. return {true, {}};
  50. }
  51. PlcConnectionState state() const override { return connection_state; }
  52. bool initialReadCompleted() const override { return initial_read; }
  53. PlcCommunicationError lastErrorType() const override
  54. {
  55. return PlcCommunicationError::None;
  56. }
  57. const std::string &lastError() const override { return last_error; }
  58. void setCallbacks(
  59. std::function<void()> state_callback,
  60. std::function<void(bool)> initial_callback,
  61. std::function<void()> cache_callback,
  62. std::function<void()> poll_cycle_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. poll_cycle_completed = std::move(poll_cycle_callback);
  69. error_reported = std::move(error_callback);
  70. }
  71. void completeInitialRead()
  72. {
  73. initial_read = true;
  74. if (initial_read_changed)
  75. {
  76. initial_read_changed(true);
  77. }
  78. }
  79. void completePollCycle()
  80. {
  81. if (poll_cycle_completed)
  82. {
  83. poll_cycle_completed();
  84. }
  85. }
  86. const std::vector<RegisterAddress> &pollAddresses() const
  87. {
  88. return poll_addresses;
  89. }
  90. const std::vector<RegisterWordRange> &pollRanges() const
  91. {
  92. return poll_ranges;
  93. }
  94. bool reject_poll_configuration = false;
  95. private:
  96. PlcConnectionState connection_state = PlcConnectionState::Disconnected;
  97. bool initial_read = false;
  98. std::string last_error;
  99. std::function<void()> state_changed;
  100. std::function<void(bool)> initial_read_changed;
  101. std::function<void()> cache_updated;
  102. std::function<void()> poll_cycle_completed;
  103. std::function<void(const std::string &)> error_reported;
  104. std::vector<RegisterAddress> poll_addresses;
  105. std::vector<RegisterWordRange> poll_ranges;
  106. };
  107. using TestSupport::require;
  108. void setConditionPath(LadderRung *rung, LogicNode node)
  109. {
  110. for (int column = 0;
  111. column < ProjectLimits::kMaximumConditionColumns;
  112. ++column)
  113. {
  114. rung->cells.push_back({
  115. rung->id + "-cell-" + std::to_string(column),
  116. column == 0 ? LadderCellKind::Node : LadderCellKind::Wire,
  117. column == 0 ? std::optional<LogicNode>{node} : std::nullopt});
  118. }
  119. }
  120. void testModeTransitions()
  121. {
  122. // 验证服务将 PLC 首读状态与领域模式切换规则正确组合
  123. TestProjectStorage storage;
  124. ProjectService project_service(storage);
  125. VirtualRegisterRepository virtual_repository;
  126. VirtualRegisterRepository plc_repository;
  127. ActiveRegisterRepository active_repository(virtual_repository);
  128. OfflineSimulationService simulation_service(virtual_repository);
  129. OnlineLogicMonitorService online_monitor_service(plc_repository);
  130. LogicEditorService logic_editor_service(project_service);
  131. ReadyPlcGateway gateway;
  132. RuntimeModeService service(
  133. project_service,
  134. logic_editor_service,
  135. simulation_service,
  136. online_monitor_service);
  137. service.configurePlc(
  138. gateway, active_repository, virtual_repository, plc_repository);
  139. Project &project = project_service.editProject();
  140. HmiPage page;
  141. page.id = "runtime-page";
  142. page.name = "Runtime";
  143. HmiControl int32_display;
  144. int32_display.id = "runtime-int32";
  145. int32_display.type = HmiControlType::NumericDisplay;
  146. int32_display.bounds = {0, 0, 100, 40};
  147. int32_display.binding = RegisterAddress{RegisterArea::D, 50};
  148. int32_display.dataType = RegisterDataType::Int32;
  149. page.controls.push_back(int32_display);
  150. HmiControl double_display = int32_display;
  151. double_display.id = "runtime-double";
  152. double_display.bounds = {120, 0, 100, 40};
  153. double_display.binding = RegisterAddress{RegisterArea::D, 60};
  154. double_display.dataType = RegisterDataType::Float64;
  155. page.controls.push_back(double_display);
  156. project.initialHmiPageId = page.id;
  157. project.hmiPages.push_back(page);
  158. project.alarmDefinitions.push_back(
  159. {"alarm-m", {RegisterArea::M, 12}, AlarmCondition::MOn, 0, "M alarm"});
  160. project.alarmDefinitions.push_back(
  161. {"alarm-d", {RegisterArea::D, 34}, AlarmCondition::DHigh, 100, "D alarm"});
  162. project.registerComments = {
  163. {RegisterAddress{RegisterArea::M, 3999}, "只用于说明"},
  164. {RegisterAddress{RegisterArea::D, 3999}, "只用于说明"}};
  165. ControlLogic logic;
  166. logic.id = "poll-logic";
  167. logic.name = "Poll logic";
  168. LogicNode edge;
  169. edge.id = "poll-edge";
  170. edge.config = EdgeContactNodeConfig{
  171. RegisterAddress{RegisterArea::M, 20}, EdgeMode::Rising};
  172. LogicNode edge_coil;
  173. edge_coil.id = "poll-edge-coil";
  174. edge_coil.config = CoilNodeConfig{
  175. RegisterAddress{RegisterArea::M, 21}, CoilMode::Normal};
  176. LadderRung edge_rung;
  177. edge_rung.id = "poll-edge-rung";
  178. edge_rung.name = "Poll edge";
  179. setConditionPath(&edge_rung, edge);
  180. edge_rung.output = edge_coil;
  181. logic.rungs.push_back(edge_rung);
  182. LogicNode comparison;
  183. comparison.id = "poll-comparison";
  184. comparison.config = CompareNodeConfig{
  185. RegisterAddress{RegisterArea::D, 35}, ComparisonOperator::GreaterThan, 0};
  186. LogicNode comparison_coil;
  187. comparison_coil.id = "poll-comparison-coil";
  188. comparison_coil.config = CoilNodeConfig{
  189. RegisterAddress{RegisterArea::M, 22}, CoilMode::Normal};
  190. LadderRung comparison_rung;
  191. comparison_rung.id = "poll-comparison-rung";
  192. comparison_rung.name = "Poll comparison";
  193. setConditionPath(&comparison_rung, comparison);
  194. comparison_rung.output = comparison_coil;
  195. logic.rungs.push_back(comparison_rung);
  196. LogicNode move_input;
  197. move_input.id = "poll-move-input";
  198. move_input.config = ContactNodeConfig{
  199. RegisterAddress{RegisterArea::M, 27}, ContactMode::NormallyOpen};
  200. LogicNode move_output;
  201. move_output.id = "poll-move";
  202. move_output.config = MoveNodeConfig{
  203. WordOperand{
  204. WordOperandKind::Register,
  205. RegisterAddress{RegisterArea::D, 38},
  206. 0},
  207. RegisterAddress{RegisterArea::D, 39}};
  208. LadderRung move_rung;
  209. move_rung.id = "poll-move-rung";
  210. move_rung.name = "Poll MOVE";
  211. setConditionPath(&move_rung, move_input);
  212. move_rung.output = move_output;
  213. logic.rungs.push_back(move_rung);
  214. LogicNode add_input;
  215. add_input.id = "poll-add-input";
  216. add_input.config = ContactNodeConfig{
  217. RegisterAddress{RegisterArea::M, 28}, ContactMode::NormallyOpen};
  218. LogicNode add_output;
  219. add_output.id = "poll-add";
  220. add_output.config = ArithmeticNodeConfig{
  221. ArithmeticOperation::Add,
  222. WordOperand{
  223. WordOperandKind::Register,
  224. RegisterAddress{RegisterArea::D, 40},
  225. 0},
  226. WordOperand{
  227. WordOperandKind::Constant,
  228. RegisterAddress{RegisterArea::D, 0},
  229. 1},
  230. RegisterAddress{RegisterArea::D, 41}};
  231. LadderRung add_rung;
  232. add_rung.id = "poll-add-rung";
  233. add_rung.name = "Poll ADD";
  234. setConditionPath(&add_rung, add_input);
  235. add_rung.output = add_output;
  236. logic.rungs.push_back(add_rung);
  237. project.controlLogics.push_back(logic);
  238. require(service.mode() == ApplicationMode::Editing,
  239. "service must start in editing mode");
  240. require(service.policy().allowsProjectEditing,
  241. "editing mode must allow project editing");
  242. require(service.enterOfflineRunning().succeeded,
  243. "editing mode must enter offline running");
  244. require(service.simulationState() == SimulationState::Running,
  245. "offline mode must start the software executor");
  246. require(service.policy().usesVirtualRegisters,
  247. "offline running must use virtual registers");
  248. require(service.enterOnlineRunning().error
  249. == ModeTransitionError::MustReturnToEditing,
  250. "running modes must not switch directly");
  251. require(service.enterEditing().succeeded,
  252. "offline running must return to editing");
  253. require(service.simulationState() == SimulationState::Stopped,
  254. "returning to editing must stop the software executor first");
  255. require(service.enterOnlineRunning().error
  256. == ModeTransitionError::InitialPlcReadRequired,
  257. "online running must require an initial PLC read");
  258. require(service.connectPlc(
  259. {"COM9", 1, 9600, 8, 2, 1, 1000, 2, 200}).succeeded,
  260. "PLC connection must be established before its initial read can complete");
  261. require(gateway.pollAddresses()
  262. == std::vector<RegisterAddress>({
  263. {RegisterArea::M, 12}, {RegisterArea::M, 20},
  264. {RegisterArea::M, 21}, {RegisterArea::M, 22},
  265. {RegisterArea::M, 27}, {RegisterArea::M, 28},
  266. {RegisterArea::D, 34}, {RegisterArea::D, 35},
  267. {RegisterArea::D, 38}, {RegisterArea::D, 39},
  268. {RegisterArea::D, 40}, {RegisterArea::D, 41},
  269. {RegisterArea::D, 50}, {RegisterArea::D, 51},
  270. {RegisterArea::D, 60}, {RegisterArea::D, 61},
  271. {RegisterArea::D, 62}, {RegisterArea::D, 63}})
  272. && gateway.pollRanges()
  273. == std::vector<RegisterWordRange>({
  274. {{RegisterArea::D, 50}, 2},
  275. {{RegisterArea::D, 60}, 4}}),
  276. "all instruction M/D references must be polled while comments stay metadata-only");
  277. gateway.completeInitialRead();
  278. require(service.initialPlcReadCompleted(),
  279. "service must retain the initial PLC read state");
  280. plc_repository.writeBit({RegisterArea::M, 20}, true);
  281. require(service.enterOnlineRunning().succeeded,
  282. "online running must start after an initial PLC read");
  283. require(service.simulationState() == SimulationState::Stopped,
  284. "online running must keep the offline executor stopped");
  285. require(service.onlineLogicMonitorService().state()
  286. == OnlineLogicMonitorState::Running,
  287. "online running must start the local read-only trace executor");
  288. require(!plc_repository.readBit({RegisterArea::M, 21}).value,
  289. "the local trace output must not change the PLC source repository");
  290. require(service.policy().usesPlcRegisters,
  291. "online running must use PLC registers");
  292. require(service.policy().runsLogicExecutor,
  293. "online running must advertise the local read-only trace executor");
  294. const std::uint64_t scan_count = service.onlineLogicMonitorService()
  295. .successfulScanCount();
  296. gateway.completePollCycle();
  297. require(service.onlineLogicMonitorService().successfulScanCount()
  298. == scan_count + 1U,
  299. "a completed PLC poll cycle must trigger one new local trace scan");
  300. }
  301. void testMonitorPollRangeRollback()
  302. {
  303. TestProjectStorage storage;
  304. ProjectService project_service(storage);
  305. VirtualRegisterRepository virtual_repository;
  306. VirtualRegisterRepository plc_repository;
  307. ActiveRegisterRepository active_repository(virtual_repository);
  308. OfflineSimulationService simulation_service(virtual_repository);
  309. OnlineLogicMonitorService online_monitor_service(plc_repository);
  310. LogicEditorService logic_editor_service(project_service);
  311. ReadyPlcGateway gateway;
  312. RuntimeModeService service(
  313. project_service,
  314. logic_editor_service,
  315. simulation_service,
  316. online_monitor_service);
  317. service.configurePlc(
  318. gateway, active_repository, virtual_repository, plc_repository);
  319. gateway.reject_poll_configuration = true;
  320. const PlcCommunicationResult rejected = service.setMonitorAddresses(
  321. {{RegisterArea::D, 100}, {RegisterArea::D, 101},
  322. {RegisterArea::D, 102}, {RegisterArea::D, 103}},
  323. {{{RegisterArea::D, 100}, 4}});
  324. require(!rejected.succeeded,
  325. "a gateway-rejected Double monitor range must fail atomically");
  326. gateway.reject_poll_configuration = false;
  327. require(service.refreshPlcPollAddresses().succeeded
  328. && gateway.pollAddresses().empty()
  329. && gateway.pollRanges().empty(),
  330. "a rejected monitor candidate must not remain in runtime poll state");
  331. }
  332. void testDisconnectedOutputBlocksOfflineAndOnlineRuntime()
  333. {
  334. TestProjectStorage storage;
  335. ProjectService project_service(storage);
  336. VirtualRegisterRepository virtual_repository;
  337. VirtualRegisterRepository plc_repository;
  338. ActiveRegisterRepository active_repository(virtual_repository);
  339. OfflineSimulationService simulation_service(virtual_repository);
  340. OnlineLogicMonitorService online_monitor_service(plc_repository);
  341. LogicEditorService logic_editor_service(project_service);
  342. ReadyPlcGateway gateway;
  343. RuntimeModeService service(
  344. project_service,
  345. logic_editor_service,
  346. simulation_service,
  347. online_monitor_service);
  348. service.configurePlc(
  349. gateway, active_repository, virtual_repository, plc_repository);
  350. ControlLogic logic;
  351. logic.id = "broken-logic";
  352. logic.name = "断路逻辑";
  353. LadderRung rung;
  354. rung.id = "broken-rung";
  355. rung.name = "行 1";
  356. for (int column = 0;
  357. column < ProjectLimits::kMaximumConditionColumns;
  358. ++column)
  359. {
  360. rung.cells.push_back({
  361. "broken-cell-" + std::to_string(column),
  362. LadderCellKind::Gap,
  363. std::nullopt});
  364. }
  365. rung.output = LogicNode{
  366. "broken-output",
  367. CoilNodeConfig{
  368. RegisterAddress{RegisterArea::M, 60}, CoilMode::Normal},
  369. true};
  370. logic.rungs.push_back(rung);
  371. LadderRung unused;
  372. unused.id = "unused-rung";
  373. unused.name = "行 2";
  374. for (int column = 0;
  375. column < ProjectLimits::kMaximumConditionColumns;
  376. ++column)
  377. {
  378. unused.cells.push_back({
  379. "unused-cell-" + std::to_string(column),
  380. column >= 3 && column <= 5
  381. ? LadderCellKind::Wire : LadderCellKind::Gap,
  382. std::nullopt});
  383. }
  384. logic.rungs.push_back(unused);
  385. project_service.editProject().controlLogics.push_back(logic);
  386. logic_editor_service.clearHistory();
  387. const ModeTransitionResult offline = service.enterOfflineRunning();
  388. require(
  389. !offline.succeeded
  390. && offline.error == ModeTransitionError::ProjectNotReady
  391. && offline.detail.find("断路逻辑") != std::string::npos
  392. && offline.detail.find("第 11 列") != std::string::npos
  393. && offline.detail.find("第 1 列") != std::string::npos
  394. && service.mode() == ApplicationMode::Editing
  395. && simulation_service.state() == SimulationState::Stopped
  396. && service.lastSyntaxCheck().changed
  397. && service.lastSyntaxCheck().removedWireCells == 3U
  398. && logic_editor_service.findCell(
  399. "broken-logic", "unused-rung", 3)->kind
  400. == LadderCellKind::Gap
  401. && logic_editor_service.canUndo(),
  402. "runtime preflight must normalize unused lines before rejecting a broken output");
  403. require(service.connectPlc(
  404. {"COM9", 1, 9600, 8, 2, 1, 1000, 2, 200}).succeeded,
  405. "the online connectivity check requires a ready PLC cache");
  406. gateway.completeInitialRead();
  407. const ModeTransitionResult online = service.enterOnlineRunning();
  408. require(
  409. !online.succeeded
  410. && online.error == ModeTransitionError::ProjectNotReady
  411. && online.detail == offline.detail
  412. && service.mode() == ApplicationMode::Editing
  413. && online_monitor_service.state()
  414. == OnlineLogicMonitorState::Stopped
  415. && !service.lastSyntaxCheck().changed
  416. && logic_editor_service.undo().succeeded
  417. && logic_editor_service.findCell(
  418. "broken-logic", "unused-rung", 3)->kind
  419. == LadderCellKind::Wire,
  420. "the same disconnected output validation must block online runtime");
  421. }
  422. } // namespace
  423. int main()
  424. {
  425. try
  426. {
  427. // 运行模式只有这一组状态机边界测试
  428. testModeTransitions();
  429. testMonitorPollRangeRollback();
  430. testDisconnectedOutputBlocksOfflineAndOnlineRuntime();
  431. }
  432. catch (const std::exception &error)
  433. {
  434. std::cerr << "runtime mode service tests failed: " << error.what() << '\n';
  435. return 1;
  436. }
  437. std::cout << "runtime mode service tests passed\n";
  438. return 0;
  439. }