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

486 рядки
19 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, defaultProjectLimitSettings());
  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. HmiControl double_status = int32_display;
  157. double_status.id = "runtime-double-status";
  158. double_status.type = HmiControlType::StatusText;
  159. double_status.bounds = {240, 0, 140, 40};
  160. double_status.binding = RegisterAddress{RegisterArea::D, 70};
  161. double_status.dataType = RegisterDataType::Float64;
  162. double_status.statusText = HmiStatusWordTextConfig{{
  163. {std::nullopt, 0.0, "Low"},
  164. {0.0, std::nullopt, "High"}}};
  165. page.controls.push_back(double_status);
  166. project.initialHmiPageId = page.id;
  167. project.hmiPages.push_back(page);
  168. project.alarmDefinitions.push_back(
  169. {"alarm-m", {RegisterArea::M, 12}, AlarmCondition::MOn, 0, "M alarm"});
  170. project.alarmDefinitions.push_back(
  171. {"alarm-d", {RegisterArea::D, 34}, AlarmCondition::DHigh, 100, "D alarm"});
  172. project.registerComments = {
  173. {RegisterAddress{RegisterArea::M, 3999}, "只用于说明"},
  174. {RegisterAddress{RegisterArea::D, 3999}, "只用于说明"}};
  175. ControlLogic logic;
  176. logic.id = "poll-logic";
  177. logic.name = "Poll logic";
  178. LogicNode edge;
  179. edge.id = "poll-edge";
  180. edge.config = EdgeContactNodeConfig{
  181. RegisterAddress{RegisterArea::M, 20}, EdgeMode::Rising};
  182. LogicNode edge_coil;
  183. edge_coil.id = "poll-edge-coil";
  184. edge_coil.config = CoilNodeConfig{
  185. RegisterAddress{RegisterArea::M, 21}, CoilMode::Normal};
  186. LadderRung edge_rung;
  187. edge_rung.id = "poll-edge-rung";
  188. edge_rung.name = "Poll edge";
  189. setConditionPath(&edge_rung, edge);
  190. edge_rung.output = edge_coil;
  191. logic.rungs.push_back(edge_rung);
  192. LogicNode comparison;
  193. comparison.id = "poll-comparison";
  194. comparison.config = CompareNodeConfig{
  195. RegisterAddress{RegisterArea::D, 35}, ComparisonOperator::GreaterThan, 0};
  196. LogicNode comparison_coil;
  197. comparison_coil.id = "poll-comparison-coil";
  198. comparison_coil.config = CoilNodeConfig{
  199. RegisterAddress{RegisterArea::M, 22}, CoilMode::Normal};
  200. LadderRung comparison_rung;
  201. comparison_rung.id = "poll-comparison-rung";
  202. comparison_rung.name = "Poll comparison";
  203. setConditionPath(&comparison_rung, comparison);
  204. comparison_rung.output = comparison_coil;
  205. logic.rungs.push_back(comparison_rung);
  206. LogicNode move_input;
  207. move_input.id = "poll-move-input";
  208. move_input.config = ContactNodeConfig{
  209. RegisterAddress{RegisterArea::M, 27}, ContactMode::NormallyOpen};
  210. LogicNode move_output;
  211. move_output.id = "poll-move";
  212. move_output.config = MoveNodeConfig{
  213. WordOperand{
  214. WordOperandKind::Register,
  215. RegisterAddress{RegisterArea::D, 38},
  216. 0},
  217. RegisterAddress{RegisterArea::D, 39}};
  218. LadderRung move_rung;
  219. move_rung.id = "poll-move-rung";
  220. move_rung.name = "Poll MOVE";
  221. setConditionPath(&move_rung, move_input);
  222. move_rung.output = move_output;
  223. logic.rungs.push_back(move_rung);
  224. LogicNode add_input;
  225. add_input.id = "poll-add-input";
  226. add_input.config = ContactNodeConfig{
  227. RegisterAddress{RegisterArea::M, 28}, ContactMode::NormallyOpen};
  228. LogicNode add_output;
  229. add_output.id = "poll-add";
  230. add_output.config = ArithmeticNodeConfig{
  231. ArithmeticOperation::Add,
  232. WordOperand{
  233. WordOperandKind::Register,
  234. RegisterAddress{RegisterArea::D, 40},
  235. 0},
  236. WordOperand{
  237. WordOperandKind::Constant,
  238. RegisterAddress{RegisterArea::D, 0},
  239. 1},
  240. RegisterAddress{RegisterArea::D, 41}};
  241. LadderRung add_rung;
  242. add_rung.id = "poll-add-rung";
  243. add_rung.name = "Poll ADD";
  244. setConditionPath(&add_rung, add_input);
  245. add_rung.output = add_output;
  246. logic.rungs.push_back(add_rung);
  247. project.controlLogics.push_back(logic);
  248. require(service.mode() == ApplicationMode::Editing,
  249. "service must start in editing mode");
  250. require(service.policy().allowsProjectEditing,
  251. "editing mode must allow project editing");
  252. require(service.enterOfflineRunning().succeeded,
  253. "editing mode must enter offline running");
  254. require(service.simulationState() == SimulationState::Running,
  255. "offline mode must start the software executor");
  256. require(service.policy().usesVirtualRegisters,
  257. "offline running must use virtual registers");
  258. require(service.enterOnlineRunning().error
  259. == ModeTransitionError::MustReturnToEditing,
  260. "running modes must not switch directly");
  261. require(service.enterEditing().succeeded,
  262. "offline running must return to editing");
  263. require(service.simulationState() == SimulationState::Stopped,
  264. "returning to editing must stop the software executor first");
  265. require(service.enterOnlineRunning().error
  266. == ModeTransitionError::InitialPlcReadRequired,
  267. "online running must require an initial PLC read");
  268. require(service.connectPlc(
  269. {"COM9", 1, 9600, 8, 2, 1, 1000, 2, 200}).succeeded,
  270. "PLC connection must be established before its initial read can complete");
  271. require(gateway.pollAddresses()
  272. == std::vector<RegisterAddress>({
  273. {RegisterArea::M, 12}, {RegisterArea::M, 20},
  274. {RegisterArea::M, 21}, {RegisterArea::M, 22},
  275. {RegisterArea::M, 27}, {RegisterArea::M, 28},
  276. {RegisterArea::D, 34}, {RegisterArea::D, 35},
  277. {RegisterArea::D, 38}, {RegisterArea::D, 39},
  278. {RegisterArea::D, 40}, {RegisterArea::D, 41},
  279. {RegisterArea::D, 50}, {RegisterArea::D, 51},
  280. {RegisterArea::D, 60}, {RegisterArea::D, 61},
  281. {RegisterArea::D, 62}, {RegisterArea::D, 63},
  282. {RegisterArea::D, 70}, {RegisterArea::D, 71},
  283. {RegisterArea::D, 72}, {RegisterArea::D, 73}})
  284. && gateway.pollRanges()
  285. == std::vector<RegisterWordRange>({
  286. {{RegisterArea::D, 50}, 2},
  287. {{RegisterArea::D, 60}, 4},
  288. {{RegisterArea::D, 70}, 4}}),
  289. "instructions and multi-word status text must be fully polled while comments stay metadata-only");
  290. gateway.completeInitialRead();
  291. require(service.initialPlcReadCompleted(),
  292. "service must retain the initial PLC read state");
  293. plc_repository.writeBit({RegisterArea::M, 20}, true);
  294. require(service.enterOnlineRunning().succeeded,
  295. "online running must start after an initial PLC read");
  296. require(service.simulationState() == SimulationState::Stopped,
  297. "online running must keep the offline executor stopped");
  298. require(service.onlineLogicMonitorService().state()
  299. == OnlineLogicMonitorState::Running,
  300. "online running must start the local read-only trace executor");
  301. require(!plc_repository.readBit({RegisterArea::M, 21}).value,
  302. "the local trace output must not change the PLC source repository");
  303. require(service.policy().usesPlcRegisters,
  304. "online running must use PLC registers");
  305. require(service.policy().runsLogicExecutor,
  306. "online running must advertise the local read-only trace executor");
  307. const std::uint64_t scan_count = service.onlineLogicMonitorService()
  308. .successfulScanCount();
  309. gateway.completePollCycle();
  310. require(service.onlineLogicMonitorService().successfulScanCount()
  311. == scan_count + 1U,
  312. "a completed PLC poll cycle must trigger one new local trace scan");
  313. }
  314. void testMonitorPollRangeRollback()
  315. {
  316. TestProjectStorage storage;
  317. ProjectService project_service(storage, defaultProjectLimitSettings());
  318. VirtualRegisterRepository virtual_repository;
  319. VirtualRegisterRepository plc_repository;
  320. ActiveRegisterRepository active_repository(virtual_repository);
  321. OfflineSimulationService simulation_service(virtual_repository);
  322. OnlineLogicMonitorService online_monitor_service(plc_repository);
  323. LogicEditorService logic_editor_service(project_service);
  324. ReadyPlcGateway gateway;
  325. RuntimeModeService service(
  326. project_service,
  327. logic_editor_service,
  328. simulation_service,
  329. online_monitor_service);
  330. service.configurePlc(
  331. gateway, active_repository, virtual_repository, plc_repository);
  332. gateway.reject_poll_configuration = true;
  333. const PlcCommunicationResult rejected = service.setMonitorAddresses(
  334. {{RegisterArea::D, 100}, {RegisterArea::D, 101},
  335. {RegisterArea::D, 102}, {RegisterArea::D, 103}},
  336. {{{RegisterArea::D, 100}, 4}});
  337. require(!rejected.succeeded,
  338. "a gateway-rejected Double monitor range must fail atomically");
  339. gateway.reject_poll_configuration = false;
  340. require(service.refreshPlcPollAddresses().succeeded
  341. && gateway.pollAddresses().empty()
  342. && gateway.pollRanges().empty(),
  343. "a rejected monitor candidate must not remain in runtime poll state");
  344. }
  345. void testDisconnectedOutputBlocksOfflineAndOnlineRuntime()
  346. {
  347. TestProjectStorage storage;
  348. ProjectService project_service(storage, defaultProjectLimitSettings());
  349. VirtualRegisterRepository virtual_repository;
  350. VirtualRegisterRepository plc_repository;
  351. ActiveRegisterRepository active_repository(virtual_repository);
  352. OfflineSimulationService simulation_service(virtual_repository);
  353. OnlineLogicMonitorService online_monitor_service(plc_repository);
  354. LogicEditorService logic_editor_service(project_service);
  355. ReadyPlcGateway gateway;
  356. RuntimeModeService service(
  357. project_service,
  358. logic_editor_service,
  359. simulation_service,
  360. online_monitor_service);
  361. service.configurePlc(
  362. gateway, active_repository, virtual_repository, plc_repository);
  363. ControlLogic logic;
  364. logic.id = "broken-logic";
  365. logic.name = "断路逻辑";
  366. LadderRung rung;
  367. rung.id = "broken-rung";
  368. rung.name = "行 1";
  369. for (int column = 0;
  370. column < ProjectLimits::kMaximumConditionColumns;
  371. ++column)
  372. {
  373. rung.cells.push_back({
  374. "broken-cell-" + std::to_string(column),
  375. LadderCellKind::Gap,
  376. std::nullopt});
  377. }
  378. rung.output = LogicNode{
  379. "broken-output",
  380. CoilNodeConfig{
  381. RegisterAddress{RegisterArea::M, 60}, CoilMode::Normal},
  382. true};
  383. logic.rungs.push_back(rung);
  384. LadderRung unused;
  385. unused.id = "unused-rung";
  386. unused.name = "行 2";
  387. for (int column = 0;
  388. column < ProjectLimits::kMaximumConditionColumns;
  389. ++column)
  390. {
  391. unused.cells.push_back({
  392. "unused-cell-" + std::to_string(column),
  393. column >= 3 && column <= 5
  394. ? LadderCellKind::Wire : LadderCellKind::Gap,
  395. std::nullopt});
  396. }
  397. logic.rungs.push_back(unused);
  398. project_service.editProject().controlLogics.push_back(logic);
  399. logic_editor_service.clearHistory();
  400. const ModeTransitionResult offline = service.enterOfflineRunning();
  401. require(
  402. !offline.succeeded
  403. && offline.error == ModeTransitionError::ProjectNotReady
  404. && offline.detail.find("断路逻辑") != std::string::npos
  405. && offline.detail.find("第 11 列") != std::string::npos
  406. && offline.detail.find("第 1 列") != std::string::npos
  407. && service.mode() == ApplicationMode::Editing
  408. && simulation_service.state() == SimulationState::Stopped
  409. && service.lastSyntaxCheck().changed
  410. && service.lastSyntaxCheck().removedWireCells == 3U
  411. && logic_editor_service.findCell(
  412. "broken-logic", "unused-rung", 3)->kind
  413. == LadderCellKind::Gap
  414. && logic_editor_service.canUndo(),
  415. "runtime preflight must normalize unused lines before rejecting a broken output");
  416. require(service.connectPlc(
  417. {"COM9", 1, 9600, 8, 2, 1, 1000, 2, 200}).succeeded,
  418. "the online connectivity check requires a ready PLC cache");
  419. gateway.completeInitialRead();
  420. const ModeTransitionResult online = service.enterOnlineRunning();
  421. require(
  422. !online.succeeded
  423. && online.error == ModeTransitionError::ProjectNotReady
  424. && online.detail == offline.detail
  425. && service.mode() == ApplicationMode::Editing
  426. && online_monitor_service.state()
  427. == OnlineLogicMonitorState::Stopped
  428. && !service.lastSyntaxCheck().changed
  429. && logic_editor_service.undo().succeeded
  430. && logic_editor_service.findCell(
  431. "broken-logic", "unused-rung", 3)->kind
  432. == LadderCellKind::Wire,
  433. "the same disconnected output validation must block online runtime");
  434. }
  435. } // namespace
  436. int main()
  437. {
  438. try
  439. {
  440. // 运行模式只有这一组状态机边界测试
  441. testModeTransitions();
  442. testMonitorPollRangeRollback();
  443. testDisconnectedOutputBlocksOfflineAndOnlineRuntime();
  444. }
  445. catch (const std::exception &error)
  446. {
  447. std::cerr << "runtime mode service tests failed: " << error.what() << '\n';
  448. return 1;
  449. }
  450. std::cout << "runtime mode service tests passed\n";
  451. return 0;
  452. }