综合平台编程器项目的远程存储
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

410 lines
16 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 testSetResetPairOnSameAddress()
  211. {
  212. VirtualRegisterRepository repository;
  213. SoftwareLogicExecutor executor;
  214. const ControlLogic program = logic({
  215. rung("low-level-rung",
  216. {{comparison("low-level", 0,
  217. ComparisonOperator::LessThanOrEqual, 30)}},
  218. coil("pump-set", 20, CoilMode::Set)),
  219. rung("high-level-rung",
  220. {{comparison("high-level", 0,
  221. ComparisonOperator::GreaterThanOrEqual, 80)}},
  222. coil("pump-reset", 20, CoilMode::Reset))});
  223. writeWord(repository, 0, 20);
  224. require(executor.executeScan({program}, repository).succeeded,
  225. "set/reset pair scan must succeed at the low limit");
  226. require(readBit(repository, 20), "low level must latch the pump on");
  227. writeWord(repository, 0, 50);
  228. require(executor.executeScan({program}, repository).succeeded,
  229. "set/reset pair scan must succeed inside the deadband");
  230. require(readBit(repository, 20), "deadband must retain the latched on state");
  231. writeWord(repository, 0, 90);
  232. require(executor.executeScan({program}, repository).succeeded,
  233. "set/reset pair scan must succeed at the high limit");
  234. require(!readBit(repository, 20), "high level must reset the pump");
  235. writeWord(repository, 0, 50);
  236. require(executor.executeScan({program}, repository).succeeded,
  237. "set/reset pair rescan must succeed inside the deadband");
  238. require(!readBit(repository, 20), "deadband must retain the reset state");
  239. }
  240. void testConflictingCoilsAreRejected()
  241. {
  242. SoftwareLogicExecutor executor;
  243. const ControlLogic normal_and_set = logic({
  244. rung("normal-rung", {{contact("normal-input", 0)}}, coil("normal", 8)),
  245. rung("set-rung", {{contact("set-input", 1)}}, coil("set", 8, CoilMode::Set))});
  246. const LogicScanResult set_result = executor.validate({normal_and_set});
  247. require(!set_result.succeeded
  248. && set_result.error == LogicScanError::ConflictingOutput,
  249. "normal and set coils for one address must be rejected");
  250. require(set_result.rungId == "set-rung" && set_result.nodeId == "set",
  251. "conflict error must identify the offending rung and node");
  252. const ControlLogic reset_and_normal = logic({
  253. rung("reset-rung", {{contact("reset-input", 0)}},
  254. coil("reset", 9, CoilMode::Reset)),
  255. rung("normal-rung", {{contact("normal-input", 1)}}, coil("normal", 9))});
  256. const LogicScanResult reset_result = executor.validate({reset_and_normal});
  257. require(!reset_result.succeeded
  258. && reset_result.error == LogicScanError::ConflictingOutput,
  259. "reset and normal coils for one address must be rejected");
  260. }
  261. void testHmiSimulationClosedLoop()
  262. {
  263. VirtualRegisterRepository repository;
  264. HmiRuntimeService hmi(repository);
  265. SoftwareLogicExecutor executor;
  266. HmiControl start;
  267. start.id = "start-button";
  268. start.type = HmiControlType::Button;
  269. start.bounds = {0, 0, 80, 30};
  270. start.text = "start";
  271. start.binding = RegisterAddress{RegisterArea::M, 0};
  272. start.buttonOperation = HmiButtonOperation::MomentaryOn;
  273. HmiControl indicator;
  274. indicator.id = "run-indicator";
  275. indicator.type = HmiControlType::Indicator;
  276. indicator.bounds = {0, 40, 80, 30};
  277. indicator.text = "run";
  278. indicator.binding = RegisterAddress{RegisterArea::M, 2};
  279. const ControlLogic program = logic({
  280. rung("hold-rung",
  281. {{contact("stop", 1, ContactMode::NormallyClosed)},
  282. {contact("start", 0), contact("feedback", 2)}},
  283. coil("run", 2))});
  284. require(hmi.operateButton(start, HmiButtonEvent::Pressed).succeeded,
  285. "HMI start button press must write virtual M");
  286. require(executor.executeScan({program}, repository).succeeded,
  287. "closed-loop scan must succeed");
  288. require(hmi.readControl(indicator).bit_value,
  289. "HMI indicator must observe the logic output");
  290. require(hmi.operateButton(start, HmiButtonEvent::Released).succeeded,
  291. "HMI start button release must write virtual M");
  292. require(executor.executeScan({program}, repository).succeeded, "holding scan must succeed");
  293. require(hmi.readControl(indicator).bit_value,
  294. "feedback contact must hold the output after start turns off");
  295. writeBit(repository, 1, true);
  296. require(executor.executeScan({program}, repository).succeeded, "stop scan must succeed");
  297. require(!hmi.readControl(indicator).bit_value,
  298. "stop contact must clear the HMI run indication");
  299. }
  300. void testSimulationLifecycleSnapshotAndFault()
  301. {
  302. VirtualRegisterRepository repository;
  303. OfflineSimulationService simulation(repository);
  304. ControlLogic program = logic({
  305. rung("snapshot-rung", {{contact("input", 0)}}, coil("output", 1))});
  306. writeBit(repository, 0, true);
  307. require(simulation.start({program}).succeeded, "simulation must start");
  308. require(simulation.state() == SimulationState::Running, "simulation must report running");
  309. require(!readBit(repository, 0), "starting a session must clear virtual registers");
  310. require(simulation.scanIntervalMs() == 50, "default scan interval must be 50 ms");
  311. writeBit(repository, 0, true);
  312. program.rungs.front().output = coil("changed-output", 9);
  313. require(simulation.executeOnce().succeeded, "manual lifecycle scan must succeed");
  314. require(readBit(repository, 1), "simulation must use its start-time snapshot");
  315. require(simulation.successfulScanCount() == 1, "successful scans must be counted");
  316. simulation.stop();
  317. require(simulation.state() == SimulationState::Stopped, "simulation must stop");
  318. require(simulation.start({program}).succeeded, "stopped simulation must support restart");
  319. require(!readBit(repository, 1), "new session must clear prior output values");
  320. simulation.stop();
  321. }
  322. class FailingVirtualRegisterRepository final : public VirtualRegisterRepository
  323. {
  324. public:
  325. BitReadResult readBit(const RegisterAddress &) const override
  326. {
  327. return {false, false, RegisterError::Unavailable};
  328. }
  329. };
  330. void testRepositoryFailureEntersFaultState()
  331. {
  332. FailingVirtualRegisterRepository repository;
  333. OfflineSimulationService simulation(repository);
  334. const ControlLogic program = logic({
  335. rung("fault-rung", {{contact("fault-input", 0)}}, coil("fault-output", 1))});
  336. require(simulation.start({program}).succeeded,
  337. "valid logic must pass simulation startup");
  338. const LogicScanResult result = simulation.executeOnce();
  339. require(!result.succeeded && result.error == LogicScanError::RegisterReadFailed,
  340. "repository read failure must fail the scan");
  341. require(simulation.state() == SimulationState::Faulted,
  342. "repository failure must enter the fault state");
  343. require(simulation.lastError().logicId == "logic-1"
  344. && simulation.lastError().rungId == "fault-rung"
  345. && simulation.lastError().nodeId == "fault-input",
  346. "fault feedback must retain logic, rung and node context");
  347. }
  348. } // namespace
  349. int main(int argc, char *argv[])
  350. {
  351. QCoreApplication application(argc, argv);
  352. try
  353. {
  354. testSeriesParallelContactsAndSequentialVisibility();
  355. testNestedSeriesParallelExpression();
  356. testAllComparisons();
  357. testSetResetAndDisabledLogic();
  358. testSetResetPairOnSameAddress();
  359. testConflictingCoilsAreRejected();
  360. testHmiSimulationClosedLoop();
  361. testSimulationLifecycleSnapshotAndFault();
  362. testRepositoryFailureEntersFaultState();
  363. }
  364. catch (const std::exception &error)
  365. {
  366. std::cerr << "offline simulation service tests failed: " << error.what() << '\n';
  367. return 1;
  368. }
  369. std::cout << "offline simulation service tests passed\n";
  370. return 0;
  371. }