综合平台编程器项目的远程存储
Vous ne pouvez pas sélectionner plus de 25 sujets Les noms de sujets doivent commencer par une lettre ou un nombre, peuvent contenir des tirets ('-') et peuvent comporter jusqu'à 35 caractères.
 
 
 
 

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