综合平台编程器项目的远程存储
No puede seleccionar más de 25 temas Los temas deben comenzar con una letra o número, pueden incluir guiones ('-') y pueden tener hasta 35 caracteres de largo.
 
 
 
 

361 líneas
14 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 testConflictingCoilsAreRejected()
  211. {
  212. SoftwareLogicExecutor executor;
  213. const ControlLogic program = logic({
  214. rung("normal-rung", {{contact("normal-input", 0)}}, coil("normal", 8)),
  215. rung("set-rung", {{contact("set-input", 1)}}, coil("set", 8, CoilMode::Set))});
  216. const LogicScanResult result = executor.validate({program});
  217. require(!result.succeeded && result.error == LogicScanError::ConflictingOutput,
  218. "mixed output modes for one address must be rejected");
  219. require(result.rungId == "set-rung" && result.nodeId == "set",
  220. "conflict error must identify the offending rung and node");
  221. }
  222. void testHmiSimulationClosedLoop()
  223. {
  224. VirtualRegisterRepository repository;
  225. HmiRuntimeService hmi(repository);
  226. SoftwareLogicExecutor executor;
  227. HmiControl start;
  228. start.id = "start-button";
  229. start.type = HmiControlType::Button;
  230. start.bounds = {0, 0, 80, 30};
  231. start.text = "start";
  232. start.binding = RegisterAddress{RegisterArea::M, 0};
  233. HmiControl indicator;
  234. indicator.id = "run-indicator";
  235. indicator.type = HmiControlType::Indicator;
  236. indicator.bounds = {0, 40, 80, 30};
  237. indicator.text = "run";
  238. indicator.binding = RegisterAddress{RegisterArea::M, 2};
  239. const ControlLogic program = logic({
  240. rung("hold-rung",
  241. {{contact("stop", 1, ContactMode::NormallyClosed)},
  242. {contact("start", 0), contact("feedback", 2)}},
  243. coil("run", 2))});
  244. require(hmi.toggleButton(start).succeeded, "HMI start button must write virtual M");
  245. require(executor.executeScan({program}, repository).succeeded,
  246. "closed-loop scan must succeed");
  247. require(hmi.readControl(indicator).bit_value,
  248. "HMI indicator must observe the logic output");
  249. require(hmi.toggleButton(start).succeeded, "HMI start button must toggle off");
  250. require(executor.executeScan({program}, repository).succeeded, "holding scan must succeed");
  251. require(hmi.readControl(indicator).bit_value,
  252. "feedback contact must hold the output after start turns off");
  253. writeBit(repository, 1, true);
  254. require(executor.executeScan({program}, repository).succeeded, "stop scan must succeed");
  255. require(!hmi.readControl(indicator).bit_value,
  256. "stop contact must clear the HMI run indication");
  257. }
  258. void testSimulationLifecycleSnapshotAndFault()
  259. {
  260. VirtualRegisterRepository repository;
  261. OfflineSimulationService simulation(repository);
  262. ControlLogic program = logic({
  263. rung("snapshot-rung", {{contact("input", 0)}}, coil("output", 1))});
  264. writeBit(repository, 0, true);
  265. require(simulation.start({program}).succeeded, "simulation must start");
  266. require(simulation.state() == SimulationState::Running, "simulation must report running");
  267. require(!readBit(repository, 0), "starting a session must clear virtual registers");
  268. require(simulation.scanIntervalMs() == 50, "default scan interval must be 50 ms");
  269. writeBit(repository, 0, true);
  270. program.rungs.front().output = coil("changed-output", 9);
  271. require(simulation.executeOnce().succeeded, "manual lifecycle scan must succeed");
  272. require(readBit(repository, 1), "simulation must use its start-time snapshot");
  273. require(simulation.successfulScanCount() == 1, "successful scans must be counted");
  274. simulation.stop();
  275. require(simulation.state() == SimulationState::Stopped, "simulation must stop");
  276. require(simulation.start({program}).succeeded, "stopped simulation must support restart");
  277. require(!readBit(repository, 1), "new session must clear prior output values");
  278. simulation.stop();
  279. }
  280. class FailingVirtualRegisterRepository final : public VirtualRegisterRepository
  281. {
  282. public:
  283. BitReadResult readBit(const RegisterAddress &) const override
  284. {
  285. return {false, false, RegisterError::Unavailable};
  286. }
  287. };
  288. void testRepositoryFailureEntersFaultState()
  289. {
  290. FailingVirtualRegisterRepository repository;
  291. OfflineSimulationService simulation(repository);
  292. const ControlLogic program = logic({
  293. rung("fault-rung", {{contact("fault-input", 0)}}, coil("fault-output", 1))});
  294. require(simulation.start({program}).succeeded,
  295. "valid logic must pass simulation startup");
  296. const LogicScanResult result = simulation.executeOnce();
  297. require(!result.succeeded && result.error == LogicScanError::RegisterReadFailed,
  298. "repository read failure must fail the scan");
  299. require(simulation.state() == SimulationState::Faulted,
  300. "repository failure must enter the fault state");
  301. require(simulation.lastError().logicId == "logic-1"
  302. && simulation.lastError().rungId == "fault-rung"
  303. && simulation.lastError().nodeId == "fault-input",
  304. "fault feedback must retain logic, rung and node context");
  305. }
  306. } // namespace
  307. int main(int argc, char *argv[])
  308. {
  309. QCoreApplication application(argc, argv);
  310. try
  311. {
  312. testSeriesParallelContactsAndSequentialVisibility();
  313. testNestedSeriesParallelExpression();
  314. testAllComparisons();
  315. testSetResetAndDisabledLogic();
  316. testConflictingCoilsAreRejected();
  317. testHmiSimulationClosedLoop();
  318. testSimulationLifecycleSnapshotAndFault();
  319. testRepositoryFailureEntersFaultState();
  320. }
  321. catch (const std::exception &error)
  322. {
  323. std::cerr << "offline simulation service tests failed: " << error.what() << '\n';
  324. return 1;
  325. }
  326. std::cout << "offline simulation service tests passed\n";
  327. return 0;
  328. }