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

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