综合平台编程器项目的远程存储
Você não pode selecionar mais de 25 tópicos Os tópicos devem começar com uma letra ou um número, podem incluir traços ('-') e podem ter até 35 caracteres.
 
 
 
 

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