综合平台编程器项目的远程存储
選択できるのは25トピックまでです。 トピックは、先頭が英数字で、英数字とダッシュ('-')を使用した35文字以内のものにしてください。
 
 
 
 

322 行
12 KiB

  1. #include "services/runtime_mode_service.h"
  2. #include "services/offline_simulation_service.h"
  3. #include "services/project_service.h"
  4. #include "domain/active_register_repository.h"
  5. #include "domain/project_storage.h"
  6. #include "domain/register_repository.h"
  7. #include <functional>
  8. #include <iostream>
  9. #include <stdexcept>
  10. #include <string>
  11. #include <utility>
  12. namespace {
  13. class TestProjectStorage final : public ProjectStorage
  14. {
  15. public:
  16. ProjectSaveResult save(const Project &, const std::string &) override
  17. {
  18. return {true, ProjectStorageError::None, {}};
  19. }
  20. ProjectLoadResult load(const std::string &) override
  21. {
  22. return {false, {}, ProjectStorageError::FileReadFailed, {}};
  23. }
  24. };
  25. class ReadyPlcGateway final : public PlcCommunicationGateway
  26. {
  27. public:
  28. PlcCommunicationResult connectDevice(const PlcSerialConfiguration &) override
  29. {
  30. connection_state = PlcConnectionState::Connected;
  31. if (state_changed)
  32. {
  33. state_changed();
  34. }
  35. return {true, {}};
  36. }
  37. void disconnectDevice() override
  38. {
  39. connection_state = PlcConnectionState::Disconnected;
  40. initial_read = false;
  41. }
  42. PlcCommunicationResult setPollAddresses(
  43. const std::vector<RegisterAddress> &addresses) override
  44. {
  45. poll_addresses = addresses;
  46. return {true, {}};
  47. }
  48. PlcConnectionState state() const override { return connection_state; }
  49. bool initialReadCompleted() const override { return initial_read; }
  50. PlcCommunicationError lastErrorType() const override
  51. {
  52. return PlcCommunicationError::None;
  53. }
  54. const std::string &lastError() const override { return last_error; }
  55. void setCallbacks(
  56. std::function<void()> state_callback,
  57. std::function<void(bool)> initial_callback,
  58. std::function<void()> cache_callback,
  59. std::function<void(const std::string &)> error_callback) override
  60. {
  61. state_changed = std::move(state_callback);
  62. initial_read_changed = std::move(initial_callback);
  63. cache_updated = std::move(cache_callback);
  64. error_reported = std::move(error_callback);
  65. }
  66. void completeInitialRead()
  67. {
  68. initial_read = true;
  69. if (initial_read_changed)
  70. {
  71. initial_read_changed(true);
  72. }
  73. }
  74. const std::vector<RegisterAddress> &pollAddresses() const
  75. {
  76. return poll_addresses;
  77. }
  78. private:
  79. PlcConnectionState connection_state = PlcConnectionState::Disconnected;
  80. bool initial_read = false;
  81. std::string last_error;
  82. std::function<void()> state_changed;
  83. std::function<void(bool)> initial_read_changed;
  84. std::function<void()> cache_updated;
  85. std::function<void(const std::string &)> error_reported;
  86. std::vector<RegisterAddress> poll_addresses;
  87. };
  88. void require(bool condition, const std::string &message)
  89. {
  90. if (!condition)
  91. {
  92. throw std::runtime_error(message);
  93. }
  94. }
  95. void testModeTransitions()
  96. {
  97. // 验证服务将 PLC 首读状态与领域模式切换规则正确组合
  98. TestProjectStorage storage;
  99. ProjectService project_service(storage);
  100. VirtualRegisterRepository virtual_repository;
  101. VirtualRegisterRepository plc_repository;
  102. ActiveRegisterRepository active_repository(virtual_repository);
  103. OfflineSimulationService simulation_service(virtual_repository);
  104. ReadyPlcGateway gateway;
  105. RuntimeModeService service(project_service, simulation_service);
  106. service.configurePlc(
  107. gateway, active_repository, virtual_repository, plc_repository);
  108. Project &project = project_service.editProject();
  109. project.alarmDefinitions.push_back(
  110. {"alarm-m", {RegisterArea::M, 12}, AlarmCondition::MOn, 0, "M alarm"});
  111. project.alarmDefinitions.push_back(
  112. {"alarm-d", {RegisterArea::D, 34}, AlarmCondition::DHigh, 100, "D alarm"});
  113. project.registerComments = {
  114. {RegisterAddress{RegisterArea::M, 3999}, "只用于说明"},
  115. {RegisterAddress{RegisterArea::D, 3999}, "只用于说明"}};
  116. ControlLogic logic;
  117. logic.id = "poll-logic";
  118. logic.name = "Poll logic";
  119. LogicNode edge;
  120. edge.id = "poll-edge";
  121. edge.config = EdgeContactNodeConfig{
  122. RegisterAddress{RegisterArea::M, 20}, EdgeMode::Rising};
  123. LogicNode edge_coil;
  124. edge_coil.id = "poll-edge-coil";
  125. edge_coil.config = CoilNodeConfig{
  126. RegisterAddress{RegisterArea::M, 21}, CoilMode::Normal};
  127. LadderRung edge_rung;
  128. edge_rung.id = "poll-edge-rung";
  129. edge_rung.name = "Poll edge";
  130. edge_rung.condition = ConditionExpression::fromNode(edge);
  131. edge_rung.output = edge_coil;
  132. logic.rungs.push_back(edge_rung);
  133. LogicNode comparison;
  134. comparison.id = "poll-comparison";
  135. comparison.config = CompareNodeConfig{
  136. RegisterAddress{RegisterArea::D, 35}, ComparisonOperator::GreaterThan, 0};
  137. LogicNode comparison_coil;
  138. comparison_coil.id = "poll-comparison-coil";
  139. comparison_coil.config = CoilNodeConfig{
  140. RegisterAddress{RegisterArea::M, 22}, CoilMode::Normal};
  141. LadderRung comparison_rung;
  142. comparison_rung.id = "poll-comparison-rung";
  143. comparison_rung.name = "Poll comparison";
  144. comparison_rung.condition = ConditionExpression::fromNode(comparison);
  145. comparison_rung.output = comparison_coil;
  146. logic.rungs.push_back(comparison_rung);
  147. LogicNode timer_input;
  148. timer_input.id = "poll-timer-input";
  149. timer_input.config = ContactNodeConfig{
  150. RegisterAddress{RegisterArea::M, 23}, ContactMode::NormallyOpen};
  151. LogicNode ton;
  152. ton.id = "poll-ton";
  153. ton.config = TonNodeConfig{TimerAddress{4}, 100};
  154. LadderRung ton_rung;
  155. ton_rung.id = "poll-ton-rung";
  156. ton_rung.name = "Poll TON";
  157. ton_rung.condition = ConditionExpression::fromNode(timer_input);
  158. ton_rung.output = ton;
  159. logic.rungs.push_back(ton_rung);
  160. LogicNode timer_contact;
  161. timer_contact.id = "poll-timer-contact";
  162. timer_contact.config = TimerContactNodeConfig{
  163. TimerAddress{4}, ContactMode::NormallyOpen};
  164. LogicNode timer_coil;
  165. timer_coil.id = "poll-timer-coil";
  166. timer_coil.config = CoilNodeConfig{
  167. RegisterAddress{RegisterArea::M, 24}, CoilMode::Normal};
  168. LadderRung timer_rung;
  169. timer_rung.id = "poll-timer-rung";
  170. timer_rung.name = "Poll timer contact";
  171. timer_rung.condition = ConditionExpression::fromNode(timer_contact);
  172. timer_rung.output = timer_coil;
  173. logic.rungs.push_back(timer_rung);
  174. LogicNode counter_input;
  175. counter_input.id = "poll-counter-input";
  176. counter_input.config = ContactNodeConfig{
  177. RegisterAddress{RegisterArea::M, 25}, ContactMode::NormallyOpen};
  178. LogicNode counter_output;
  179. counter_output.id = "poll-counter";
  180. counter_output.config = CounterNodeConfig{
  181. CounterAddress{2},
  182. CounterMode::Up,
  183. RegisterAddress{RegisterArea::D, 36},
  184. WordOperand{
  185. WordOperandKind::Register,
  186. RegisterAddress{RegisterArea::D, 37},
  187. 0},
  188. RegisterAddress{RegisterArea::M, 26}};
  189. LadderRung counter_rung;
  190. counter_rung.id = "poll-counter-rung";
  191. counter_rung.name = "Poll counter";
  192. counter_rung.condition = ConditionExpression::fromNode(counter_input);
  193. counter_rung.output = counter_output;
  194. logic.rungs.push_back(counter_rung);
  195. LogicNode move_input;
  196. move_input.id = "poll-move-input";
  197. move_input.config = ContactNodeConfig{
  198. RegisterAddress{RegisterArea::M, 27}, ContactMode::NormallyOpen};
  199. LogicNode move_output;
  200. move_output.id = "poll-move";
  201. move_output.config = MoveNodeConfig{
  202. WordOperand{
  203. WordOperandKind::Register,
  204. RegisterAddress{RegisterArea::D, 38},
  205. 0},
  206. RegisterAddress{RegisterArea::D, 39}};
  207. LadderRung move_rung;
  208. move_rung.id = "poll-move-rung";
  209. move_rung.name = "Poll MOVE";
  210. move_rung.condition = ConditionExpression::fromNode(move_input);
  211. move_rung.output = move_output;
  212. logic.rungs.push_back(move_rung);
  213. LogicNode add_input;
  214. add_input.id = "poll-add-input";
  215. add_input.config = ContactNodeConfig{
  216. RegisterAddress{RegisterArea::M, 28}, ContactMode::NormallyOpen};
  217. LogicNode add_output;
  218. add_output.id = "poll-add";
  219. add_output.config = ArithmeticNodeConfig{
  220. ArithmeticOperation::Add,
  221. WordOperand{
  222. WordOperandKind::Register,
  223. RegisterAddress{RegisterArea::D, 40},
  224. 0},
  225. WordOperand{
  226. WordOperandKind::Constant,
  227. RegisterAddress{RegisterArea::D, 0},
  228. 1},
  229. RegisterAddress{RegisterArea::D, 41}};
  230. LadderRung add_rung;
  231. add_rung.id = "poll-add-rung";
  232. add_rung.name = "Poll ADD";
  233. add_rung.condition = ConditionExpression::fromNode(add_input);
  234. add_rung.output = add_output;
  235. logic.rungs.push_back(add_rung);
  236. project.controlLogics.push_back(logic);
  237. require(service.mode() == ApplicationMode::Editing,
  238. "service must start in editing mode");
  239. require(service.policy().allowsProjectEditing,
  240. "editing mode must allow project editing");
  241. require(service.enterOfflineRunning().succeeded,
  242. "editing mode must enter offline running");
  243. require(service.simulationState() == SimulationState::Running,
  244. "offline mode must start the software executor");
  245. require(service.policy().usesVirtualRegisters,
  246. "offline running must use virtual registers");
  247. require(service.enterOnlineRunning().error
  248. == ModeTransitionError::MustReturnToEditing,
  249. "running modes must not switch directly");
  250. require(service.enterEditing().succeeded,
  251. "offline running must return to editing");
  252. require(service.simulationState() == SimulationState::Stopped,
  253. "returning to editing must stop the software executor first");
  254. require(service.enterOnlineRunning().error
  255. == ModeTransitionError::InitialPlcReadRequired,
  256. "online running must require an initial PLC read");
  257. require(service.connectPlc(
  258. {"COM9", 1, 9600, 8, 2, 1, 1000, 2, 200}).succeeded,
  259. "PLC connection must be established before its initial read can complete");
  260. require(gateway.pollAddresses()
  261. == std::vector<RegisterAddress>({
  262. {RegisterArea::M, 12}, {RegisterArea::M, 20},
  263. {RegisterArea::M, 21}, {RegisterArea::M, 22},
  264. {RegisterArea::M, 23}, {RegisterArea::M, 24},
  265. {RegisterArea::M, 25}, {RegisterArea::M, 26},
  266. {RegisterArea::M, 27}, {RegisterArea::M, 28},
  267. {RegisterArea::D, 34}, {RegisterArea::D, 35},
  268. {RegisterArea::D, 36}, {RegisterArea::D, 37},
  269. {RegisterArea::D, 38}, {RegisterArea::D, 39},
  270. {RegisterArea::D, 40}, {RegisterArea::D, 41}}),
  271. "all instruction M/D references must be polled while T/C resources and comments stay offline-only");
  272. gateway.completeInitialRead();
  273. require(service.initialPlcReadCompleted(),
  274. "service must retain the initial PLC read state");
  275. require(service.enterOnlineRunning().succeeded,
  276. "online running must start after an initial PLC read");
  277. require(service.simulationState() == SimulationState::Stopped,
  278. "online running must never start the software executor");
  279. require(service.policy().usesPlcRegisters,
  280. "online running must use PLC registers");
  281. require(!service.policy().runsLogicExecutor,
  282. "online running must keep the software executor stopped");
  283. }
  284. } // namespace
  285. int main()
  286. {
  287. try
  288. {
  289. // 运行模式只有这一组状态机边界测试
  290. testModeTransitions();
  291. }
  292. catch (const std::exception &error)
  293. {
  294. std::cerr << "runtime mode service tests failed: " << error.what() << '\n';
  295. return 1;
  296. }
  297. std::cout << "runtime mode service tests passed\n";
  298. return 0;
  299. }