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

445 wiersze
18 KiB

  1. #include "domain/hmi_model.h"
  2. #include "domain/register_repository.h"
  3. #include "services/hmi_runtime_service.h"
  4. #include "services/offline_simulation_service.h"
  5. #include "services/software_logic_executor.h"
  6. #include <QCoreApplication>
  7. #include <array>
  8. #include <iostream>
  9. #include <stdexcept>
  10. #include <string>
  11. #include <vector>
  12. namespace {
  13. void require(bool condition, const std::string &message)
  14. {
  15. if (!condition)
  16. {
  17. throw std::runtime_error(message);
  18. }
  19. }
  20. LogicNode contact(const std::string &id, int address,
  21. ContactMode mode = ContactMode::NormallyOpen)
  22. {
  23. return {id, ContactNodeConfig{RegisterAddress{RegisterArea::M, address}, mode}, true};
  24. }
  25. LogicNode comparison(const std::string &id, int address,
  26. ComparisonOperator operation, std::int16_t value)
  27. {
  28. return {id,
  29. CompareNodeConfig{RegisterAddress{RegisterArea::D, address}, operation, value},
  30. true};
  31. }
  32. LogicNode coil(const std::string &id, int address,
  33. CoilMode mode = CoilMode::Normal)
  34. {
  35. return {id, CoilNodeConfig{RegisterAddress{RegisterArea::M, address}, mode}, true};
  36. }
  37. LadderRung rung(const std::string &id,
  38. const std::vector<std::vector<LogicNode>> &stages,
  39. const LogicNode &output)
  40. {
  41. LadderRung result;
  42. result.id = id;
  43. result.name = id;
  44. std::vector<ConditionExpression> series_children;
  45. for (std::size_t index = 0; index < stages.size(); ++index)
  46. {
  47. std::vector<ConditionExpression> parallel_children;
  48. for (const LogicNode &node : stages[index])
  49. {
  50. parallel_children.push_back(ConditionExpression::fromNode(node));
  51. }
  52. if (parallel_children.size() == 1U)
  53. {
  54. series_children.push_back(std::move(parallel_children.front()));
  55. }
  56. else
  57. {
  58. ConditionExpression parallel;
  59. parallel.id = id + "-parallel-" + std::to_string(index);
  60. parallel.kind = ConditionExpressionKind::Parallel;
  61. parallel.children = std::move(parallel_children);
  62. series_children.push_back(std::move(parallel));
  63. }
  64. }
  65. if (series_children.size() == 1U)
  66. {
  67. result.condition = std::move(series_children.front());
  68. }
  69. else
  70. {
  71. ConditionExpression series;
  72. series.id = id + "-series";
  73. series.kind = ConditionExpressionKind::Series;
  74. series.children = std::move(series_children);
  75. result.condition = std::move(series);
  76. }
  77. result.output = output;
  78. return result;
  79. }
  80. ControlLogic logic(const std::vector<LadderRung> &rungs)
  81. {
  82. return {"logic-1", "logic-1", rungs, true};
  83. }
  84. bool readBit(RegisterRepository &repository, int address)
  85. {
  86. const BitReadResult result = repository.readBit(
  87. RegisterAddress{RegisterArea::M, address});
  88. require(result.succeeded, "test register read must succeed");
  89. return result.value;
  90. }
  91. void writeBit(RegisterRepository &repository, int address, bool value)
  92. {
  93. require(repository.writeBit(RegisterAddress{RegisterArea::M, address}, value).succeeded,
  94. "test bit write must succeed");
  95. }
  96. void writeWord(RegisterRepository &repository, int address, std::int16_t value)
  97. {
  98. require(repository.writeWord(RegisterAddress{RegisterArea::D, address}, value).succeeded,
  99. "test word write must succeed");
  100. }
  101. void testNestedSeriesParallelExpression()
  102. {
  103. VirtualRegisterRepository repository;
  104. SoftwareLogicExecutor executor;
  105. ConditionExpression nested_series;
  106. nested_series.id = "nested-series";
  107. nested_series.kind = ConditionExpressionKind::Series;
  108. nested_series.children = {
  109. ConditionExpression::fromNode(contact("b", 1)),
  110. ConditionExpression::fromNode(contact("c", 2))};
  111. ConditionExpression root;
  112. root.id = "root-parallel";
  113. root.kind = ConditionExpressionKind::Parallel;
  114. root.children = {
  115. ConditionExpression::fromNode(contact("a", 0)),
  116. nested_series};
  117. LadderRung nested_rung;
  118. nested_rung.id = "nested-rung";
  119. nested_rung.name = "nested-rung";
  120. nested_rung.condition = root;
  121. nested_rung.output = coil("nested-output", 10);
  122. const ControlLogic program = logic({nested_rung});
  123. writeBit(repository, 1, true);
  124. writeBit(repository, 2, true);
  125. LogicTraceSnapshot trace;
  126. require(executor.executeScan({program}, repository, &trace).succeeded,
  127. "nested expression scan must succeed");
  128. require(readBit(repository, 10), "B AND C branch must energize A OR (B AND C)");
  129. require(trace.expressionValues.at("nested-series")
  130. && trace.expressionValues.at("root-parallel")
  131. && trace.rungValues.at("nested-rung"),
  132. "scan trace must expose active nested expression and rung values");
  133. writeBit(repository, 2, false);
  134. require(executor.executeScan({program}, repository, &trace).succeeded,
  135. "nested false scan must succeed");
  136. require(!readBit(repository, 10), "incomplete B AND C branch must be false");
  137. writeBit(repository, 0, true);
  138. require(executor.executeScan({program}, repository, &trace).succeeded,
  139. "alternate branch scan must succeed");
  140. require(readBit(repository, 10), "A branch must independently energize output");
  141. }
  142. void testSeriesParallelContactsAndSequentialVisibility()
  143. {
  144. VirtualRegisterRepository repository;
  145. SoftwareLogicExecutor executor;
  146. const ControlLogic program = logic({
  147. rung("rung-1",
  148. {{contact("start", 0), contact("alternate", 1)},
  149. {contact("stop", 2, ContactMode::NormallyClosed)}},
  150. coil("run", 3)),
  151. rung("rung-2", {{contact("run-feedback", 3)}}, coil("downstream", 4))});
  152. writeBit(repository, 1, true);
  153. require(executor.executeScan({program}, repository).succeeded,
  154. "parallel and series scan must succeed");
  155. require(readBit(repository, 3), "parallel OR and series AND must energize output");
  156. require(readBit(repository, 4),
  157. "a later rung must see an earlier rung write in the same scan");
  158. writeBit(repository, 2, true);
  159. require(executor.executeScan({program}, repository).succeeded,
  160. "normally closed scan must succeed");
  161. require(!readBit(repository, 3), "normally closed stop contact must open the rung");
  162. require(!readBit(repository, 4), "downstream normal coil must follow the new value");
  163. }
  164. void testAllComparisons()
  165. {
  166. const std::array<ComparisonOperator, 6> operations{
  167. ComparisonOperator::Equal, ComparisonOperator::NotEqual,
  168. ComparisonOperator::LessThan, ComparisonOperator::LessThanOrEqual,
  169. ComparisonOperator::GreaterThan, ComparisonOperator::GreaterThanOrEqual};
  170. const std::array<std::int16_t, 6> actual_values{10, 9, 9, 10, 11, 10};
  171. for (std::size_t index = 0; index < operations.size(); ++index)
  172. {
  173. VirtualRegisterRepository repository;
  174. SoftwareLogicExecutor executor;
  175. writeWord(repository, 0, actual_values[index]);
  176. const ControlLogic program = logic({
  177. rung("compare-rung", {{comparison("compare", 0, operations[index], 10)},
  178. }, coil("result", 10))});
  179. require(executor.executeScan({program}, repository).succeeded,
  180. "comparison scan must succeed");
  181. require(readBit(repository, 10), "comparison operator must evaluate true");
  182. }
  183. }
  184. void testSetResetAndDisabledLogic()
  185. {
  186. VirtualRegisterRepository repository;
  187. SoftwareLogicExecutor executor;
  188. ControlLogic program = logic({
  189. rung("set-rung", {{contact("set-input", 0)}}, coil("set-output", 5, CoilMode::Set)),
  190. rung("reset-rung", {{contact("reset-input", 1)}},
  191. coil("reset-output", 6, CoilMode::Reset))});
  192. writeBit(repository, 0, true);
  193. require(executor.executeScan({program}, repository).succeeded, "set scan must succeed");
  194. require(readBit(repository, 5), "set coil must latch true");
  195. writeBit(repository, 0, false);
  196. require(executor.executeScan({program}, repository).succeeded,
  197. "inactive set scan must succeed");
  198. require(readBit(repository, 5), "inactive set coil must retain its value");
  199. writeBit(repository, 1, true);
  200. writeBit(repository, 6, true);
  201. require(executor.executeScan({program}, repository).succeeded,
  202. "reset scan must succeed");
  203. require(!readBit(repository, 6), "reset coil must write false");
  204. program.enabled = false;
  205. writeBit(repository, 5, true);
  206. require(executor.executeScan({program}, repository).succeeded,
  207. "disabled logic scan must be ignored successfully");
  208. require(readBit(repository, 5), "disabled logic must not change outputs");
  209. }
  210. void testMultipleLogicScanOrderAndTraceIsolation()
  211. {
  212. VirtualRegisterRepository repository;
  213. SoftwareLogicExecutor executor;
  214. ControlLogic first = logic({
  215. rung("rung-1", {{contact("input", 0)}}, coil("output", 1))});
  216. first.id = "logic-first";
  217. first.name = "First";
  218. ControlLogic second = logic({
  219. rung("rung-1", {{contact("input", 1)}}, coil("output", 2))});
  220. second.id = "logic-second";
  221. second.name = "Second";
  222. writeBit(repository, 0, true);
  223. LogicTraceSnapshot trace;
  224. require(executor.executeScan({first, second}, repository, &trace).succeeded,
  225. "all enabled logic modules must execute in project order");
  226. require(readBit(repository, 1) && readBit(repository, 2),
  227. "a later logic module must observe an earlier module write in one scan");
  228. require(trace.logicValues.size() == 2U
  229. && trace.forLogic(first.id).rungValues.at("rung-1")
  230. && trace.forLogic(second.id).rungValues.at("rung-1"),
  231. "runtime traces must be partitioned by logic id when node ids repeat");
  232. ControlLogic disabled_draft;
  233. disabled_draft.id = "logic-draft";
  234. disabled_draft.name = "Draft";
  235. disabled_draft.enabled = false;
  236. disabled_draft.rungs.push_back(
  237. {"rung-1", "Draft", std::nullopt, std::nullopt});
  238. require(executor.validate({first, disabled_draft}).succeeded,
  239. "a disabled incomplete logic module must not block offline execution");
  240. }
  241. void testSetResetPairOnSameAddress()
  242. {
  243. VirtualRegisterRepository repository;
  244. SoftwareLogicExecutor executor;
  245. const ControlLogic program = logic({
  246. rung("low-level-rung",
  247. {{comparison("low-level", 0,
  248. ComparisonOperator::LessThanOrEqual, 30)}},
  249. coil("pump-set", 20, CoilMode::Set)),
  250. rung("high-level-rung",
  251. {{comparison("high-level", 0,
  252. ComparisonOperator::GreaterThanOrEqual, 80)}},
  253. coil("pump-reset", 20, CoilMode::Reset))});
  254. writeWord(repository, 0, 20);
  255. require(executor.executeScan({program}, repository).succeeded,
  256. "set/reset pair scan must succeed at the low limit");
  257. require(readBit(repository, 20), "low level must latch the pump on");
  258. writeWord(repository, 0, 50);
  259. require(executor.executeScan({program}, repository).succeeded,
  260. "set/reset pair scan must succeed inside the deadband");
  261. require(readBit(repository, 20), "deadband must retain the latched on state");
  262. writeWord(repository, 0, 90);
  263. require(executor.executeScan({program}, repository).succeeded,
  264. "set/reset pair scan must succeed at the high limit");
  265. require(!readBit(repository, 20), "high level must reset the pump");
  266. writeWord(repository, 0, 50);
  267. require(executor.executeScan({program}, repository).succeeded,
  268. "set/reset pair rescan must succeed inside the deadband");
  269. require(!readBit(repository, 20), "deadband must retain the reset state");
  270. }
  271. void testConflictingCoilsAreRejected()
  272. {
  273. SoftwareLogicExecutor executor;
  274. const ControlLogic normal_and_set = logic({
  275. rung("normal-rung", {{contact("normal-input", 0)}}, coil("normal", 8)),
  276. rung("set-rung", {{contact("set-input", 1)}}, coil("set", 8, CoilMode::Set))});
  277. const LogicScanResult set_result = executor.validate({normal_and_set});
  278. require(!set_result.succeeded
  279. && set_result.error == LogicScanError::ConflictingOutput,
  280. "normal and set coils for one address must be rejected");
  281. require(set_result.rungId == "set-rung" && set_result.nodeId == "set",
  282. "conflict error must identify the offending rung and node");
  283. const ControlLogic reset_and_normal = logic({
  284. rung("reset-rung", {{contact("reset-input", 0)}},
  285. coil("reset", 9, CoilMode::Reset)),
  286. rung("normal-rung", {{contact("normal-input", 1)}}, coil("normal", 9))});
  287. const LogicScanResult reset_result = executor.validate({reset_and_normal});
  288. require(!reset_result.succeeded
  289. && reset_result.error == LogicScanError::ConflictingOutput,
  290. "reset and normal coils for one address must be rejected");
  291. }
  292. void testHmiSimulationClosedLoop()
  293. {
  294. VirtualRegisterRepository repository;
  295. HmiRuntimeService hmi(repository);
  296. SoftwareLogicExecutor executor;
  297. HmiControl start;
  298. start.id = "start-button";
  299. start.type = HmiControlType::Button;
  300. start.bounds = {0, 0, 80, 30};
  301. start.text = "start";
  302. start.binding = RegisterAddress{RegisterArea::M, 0};
  303. start.buttonOperation = HmiButtonOperation::MomentaryOn;
  304. HmiControl indicator;
  305. indicator.id = "run-indicator";
  306. indicator.type = HmiControlType::Indicator;
  307. indicator.bounds = {0, 40, 80, 30};
  308. indicator.text = "run";
  309. indicator.binding = RegisterAddress{RegisterArea::M, 2};
  310. const ControlLogic program = logic({
  311. rung("hold-rung",
  312. {{contact("stop", 1, ContactMode::NormallyClosed)},
  313. {contact("start", 0), contact("feedback", 2)}},
  314. coil("run", 2))});
  315. require(hmi.operateButton(start, HmiButtonEvent::Pressed).succeeded,
  316. "HMI start button press must write virtual M");
  317. require(executor.executeScan({program}, repository).succeeded,
  318. "closed-loop scan must succeed");
  319. require(hmi.readControl(indicator).bit_value,
  320. "HMI indicator must observe the logic output");
  321. require(hmi.operateButton(start, HmiButtonEvent::Released).succeeded,
  322. "HMI start button release must write virtual M");
  323. require(executor.executeScan({program}, repository).succeeded, "holding scan must succeed");
  324. require(hmi.readControl(indicator).bit_value,
  325. "feedback contact must hold the output after start turns off");
  326. writeBit(repository, 1, true);
  327. require(executor.executeScan({program}, repository).succeeded, "stop scan must succeed");
  328. require(!hmi.readControl(indicator).bit_value,
  329. "stop contact must clear the HMI run indication");
  330. }
  331. void testSimulationLifecycleSnapshotAndFault()
  332. {
  333. VirtualRegisterRepository repository;
  334. OfflineSimulationService simulation(repository);
  335. ControlLogic program = logic({
  336. rung("snapshot-rung", {{contact("input", 0)}}, coil("output", 1))});
  337. writeBit(repository, 0, true);
  338. require(simulation.start({program}).succeeded, "simulation must start");
  339. require(simulation.state() == SimulationState::Running, "simulation must report running");
  340. require(!readBit(repository, 0), "starting a session must clear virtual registers");
  341. require(simulation.scanIntervalMs() == 50, "default scan interval must be 50 ms");
  342. writeBit(repository, 0, true);
  343. program.rungs.front().output = coil("changed-output", 9);
  344. require(simulation.executeOnce().succeeded, "manual lifecycle scan must succeed");
  345. require(readBit(repository, 1), "simulation must use its start-time snapshot");
  346. require(simulation.successfulScanCount() == 1, "successful scans must be counted");
  347. simulation.stop();
  348. require(simulation.state() == SimulationState::Stopped, "simulation must stop");
  349. require(simulation.start({program}).succeeded, "stopped simulation must support restart");
  350. require(!readBit(repository, 1), "new session must clear prior output values");
  351. simulation.stop();
  352. }
  353. class FailingVirtualRegisterRepository final : public VirtualRegisterRepository
  354. {
  355. public:
  356. BitReadResult readBit(const RegisterAddress &) const override
  357. {
  358. return {false, false, RegisterError::Unavailable};
  359. }
  360. };
  361. void testRepositoryFailureEntersFaultState()
  362. {
  363. FailingVirtualRegisterRepository repository;
  364. OfflineSimulationService simulation(repository);
  365. const ControlLogic program = logic({
  366. rung("fault-rung", {{contact("fault-input", 0)}}, coil("fault-output", 1))});
  367. require(simulation.start({program}).succeeded,
  368. "valid logic must pass simulation startup");
  369. const LogicScanResult result = simulation.executeOnce();
  370. require(!result.succeeded && result.error == LogicScanError::RegisterReadFailed,
  371. "repository read failure must fail the scan");
  372. require(simulation.state() == SimulationState::Faulted,
  373. "repository failure must enter the fault state");
  374. require(simulation.lastError().logicId == "logic-1"
  375. && simulation.lastError().rungId == "fault-rung"
  376. && simulation.lastError().nodeId == "fault-input",
  377. "fault feedback must retain logic, rung and node context");
  378. }
  379. } // namespace
  380. int main(int argc, char *argv[])
  381. {
  382. QCoreApplication application(argc, argv);
  383. try
  384. {
  385. testSeriesParallelContactsAndSequentialVisibility();
  386. testNestedSeriesParallelExpression();
  387. testAllComparisons();
  388. testSetResetAndDisabledLogic();
  389. testMultipleLogicScanOrderAndTraceIsolation();
  390. testSetResetPairOnSameAddress();
  391. testConflictingCoilsAreRejected();
  392. testHmiSimulationClosedLoop();
  393. testSimulationLifecycleSnapshotAndFault();
  394. testRepositoryFailureEntersFaultState();
  395. }
  396. catch (const std::exception &error)
  397. {
  398. std::cerr << "offline simulation service tests failed: " << error.what() << '\n';
  399. return 1;
  400. }
  401. std::cout << "offline simulation service tests passed\n";
  402. return 0;
  403. }