综合平台编程器项目的远程存储
Non puoi selezionare più di 25 argomenti Gli argomenti devono iniziare con una lettera o un numero, possono includere trattini ('-') e possono essere lunghi fino a 35 caratteri.
 
 
 
 

887 righe
35 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 edgeContact(const std::string &id, int address, EdgeMode mode)
  26. {
  27. return {id,
  28. EdgeContactNodeConfig{
  29. RegisterAddress{RegisterArea::M, address}, mode},
  30. true};
  31. }
  32. LogicNode timerContact(
  33. const std::string &id,
  34. int timer_index,
  35. ContactMode mode = ContactMode::NormallyOpen)
  36. {
  37. return {id, TimerContactNodeConfig{TimerAddress{timer_index}, mode}, true};
  38. }
  39. LogicNode counterContact(
  40. const std::string &id,
  41. int counter_index,
  42. ContactMode mode = ContactMode::NormallyOpen)
  43. {
  44. return {id, CounterContactNodeConfig{CounterAddress{counter_index}, mode}, true};
  45. }
  46. LogicNode ton(const std::string &id, int timer_index, int preset_ms)
  47. {
  48. return {id, TonNodeConfig{TimerAddress{timer_index}, preset_ms}, true};
  49. }
  50. WordOperand constantOperand(std::int16_t value)
  51. {
  52. return {
  53. WordOperandKind::Constant,
  54. RegisterAddress{RegisterArea::D, 0},
  55. value};
  56. }
  57. WordOperand registerOperand(int address)
  58. {
  59. return {
  60. WordOperandKind::Register,
  61. RegisterAddress{RegisterArea::D, address},
  62. 0};
  63. }
  64. LogicNode counter(
  65. const std::string &id,
  66. int counter_index,
  67. CounterMode mode,
  68. int current_address,
  69. WordOperand preset,
  70. int reset_address)
  71. {
  72. return {
  73. id,
  74. CounterNodeConfig{
  75. CounterAddress{counter_index},
  76. mode,
  77. RegisterAddress{RegisterArea::D, current_address},
  78. preset,
  79. RegisterAddress{RegisterArea::M, reset_address}},
  80. true};
  81. }
  82. LogicNode move(
  83. const std::string &id,
  84. WordOperand source,
  85. int destination)
  86. {
  87. return {
  88. id,
  89. MoveNodeConfig{
  90. source,
  91. RegisterAddress{RegisterArea::D, destination}},
  92. true};
  93. }
  94. LogicNode arithmetic(
  95. const std::string &id,
  96. ArithmeticOperation operation,
  97. WordOperand left,
  98. WordOperand right,
  99. int destination)
  100. {
  101. return {
  102. id,
  103. ArithmeticNodeConfig{
  104. operation,
  105. left,
  106. right,
  107. RegisterAddress{RegisterArea::D, destination}},
  108. true};
  109. }
  110. LogicNode comparison(const std::string &id, int address,
  111. ComparisonOperator operation, std::int16_t value)
  112. {
  113. return {id,
  114. CompareNodeConfig{RegisterAddress{RegisterArea::D, address}, operation, value},
  115. true};
  116. }
  117. LogicNode coil(const std::string &id, int address,
  118. CoilMode mode = CoilMode::Normal)
  119. {
  120. return {id, CoilNodeConfig{RegisterAddress{RegisterArea::M, address}, mode}, true};
  121. }
  122. LadderRung rung(const std::string &id,
  123. const std::vector<std::vector<LogicNode>> &stages,
  124. const LogicNode &output)
  125. {
  126. LadderRung result;
  127. result.id = id;
  128. result.name = id;
  129. std::vector<ConditionExpression> series_children;
  130. for (std::size_t index = 0; index < stages.size(); ++index)
  131. {
  132. std::vector<ConditionExpression> parallel_children;
  133. for (const LogicNode &node : stages[index])
  134. {
  135. parallel_children.push_back(ConditionExpression::fromNode(node));
  136. }
  137. if (parallel_children.size() == 1U)
  138. {
  139. series_children.push_back(std::move(parallel_children.front()));
  140. }
  141. else
  142. {
  143. ConditionExpression parallel;
  144. parallel.id = id + "-parallel-" + std::to_string(index);
  145. parallel.kind = ConditionExpressionKind::Parallel;
  146. parallel.children = std::move(parallel_children);
  147. series_children.push_back(std::move(parallel));
  148. }
  149. }
  150. if (series_children.size() == 1U)
  151. {
  152. result.condition = std::move(series_children.front());
  153. }
  154. else
  155. {
  156. ConditionExpression series;
  157. series.id = id + "-series";
  158. series.kind = ConditionExpressionKind::Series;
  159. series.children = std::move(series_children);
  160. result.condition = std::move(series);
  161. }
  162. result.output = output;
  163. return result;
  164. }
  165. ControlLogic logic(const std::vector<LadderRung> &rungs)
  166. {
  167. return {"logic-1", "logic-1", rungs, true};
  168. }
  169. bool readBit(RegisterRepository &repository, int address)
  170. {
  171. const BitReadResult result = repository.readBit(
  172. RegisterAddress{RegisterArea::M, address});
  173. require(result.succeeded, "test register read must succeed");
  174. return result.value;
  175. }
  176. void writeBit(RegisterRepository &repository, int address, bool value)
  177. {
  178. require(repository.writeBit(RegisterAddress{RegisterArea::M, address}, value).succeeded,
  179. "test bit write must succeed");
  180. }
  181. void writeWord(RegisterRepository &repository, int address, std::int16_t value)
  182. {
  183. require(repository.writeWord(RegisterAddress{RegisterArea::D, address}, value).succeeded,
  184. "test word write must succeed");
  185. }
  186. std::int16_t readWord(RegisterRepository &repository, int address)
  187. {
  188. const WordReadResult result = repository.readWord(
  189. RegisterAddress{RegisterArea::D, address});
  190. require(result.succeeded, "test word read must succeed");
  191. return result.value;
  192. }
  193. void testNestedSeriesParallelExpression()
  194. {
  195. VirtualRegisterRepository repository;
  196. SoftwareLogicExecutor executor;
  197. ConditionExpression nested_series;
  198. nested_series.id = "nested-series";
  199. nested_series.kind = ConditionExpressionKind::Series;
  200. nested_series.children = {
  201. ConditionExpression::fromNode(contact("b", 1)),
  202. ConditionExpression::fromNode(contact("c", 2))};
  203. ConditionExpression root;
  204. root.id = "root-parallel";
  205. root.kind = ConditionExpressionKind::Parallel;
  206. root.children = {
  207. ConditionExpression::fromNode(contact("a", 0)),
  208. nested_series};
  209. LadderRung nested_rung;
  210. nested_rung.id = "nested-rung";
  211. nested_rung.name = "nested-rung";
  212. nested_rung.condition = root;
  213. nested_rung.output = coil("nested-output", 10);
  214. const ControlLogic program = logic({nested_rung});
  215. writeBit(repository, 1, true);
  216. writeBit(repository, 2, true);
  217. LogicTraceSnapshot trace;
  218. require(executor.executeScan({program}, repository, &trace).succeeded,
  219. "nested expression scan must succeed");
  220. require(readBit(repository, 10), "B AND C branch must energize A OR (B AND C)");
  221. require(trace.expressionValues.at("nested-series")
  222. && trace.expressionValues.at("root-parallel")
  223. && trace.rungValues.at("nested-rung"),
  224. "scan trace must expose active nested expression and rung values");
  225. writeBit(repository, 2, false);
  226. require(executor.executeScan({program}, repository, &trace).succeeded,
  227. "nested false scan must succeed");
  228. require(!readBit(repository, 10), "incomplete B AND C branch must be false");
  229. writeBit(repository, 0, true);
  230. require(executor.executeScan({program}, repository, &trace).succeeded,
  231. "alternate branch scan must succeed");
  232. require(readBit(repository, 10), "A branch must independently energize output");
  233. }
  234. void testSeriesParallelContactsAndSequentialVisibility()
  235. {
  236. VirtualRegisterRepository repository;
  237. SoftwareLogicExecutor executor;
  238. const ControlLogic program = logic({
  239. rung("rung-1",
  240. {{contact("start", 0), contact("alternate", 1)},
  241. {contact("stop", 2, ContactMode::NormallyClosed)}},
  242. coil("run", 3)),
  243. rung("rung-2", {{contact("run-feedback", 3)}}, coil("downstream", 4))});
  244. writeBit(repository, 1, true);
  245. require(executor.executeScan({program}, repository).succeeded,
  246. "parallel and series scan must succeed");
  247. require(readBit(repository, 3), "parallel OR and series AND must energize output");
  248. require(readBit(repository, 4),
  249. "a later rung must see an earlier rung write in the same scan");
  250. writeBit(repository, 2, true);
  251. require(executor.executeScan({program}, repository).succeeded,
  252. "normally closed scan must succeed");
  253. require(!readBit(repository, 3), "normally closed stop contact must open the rung");
  254. require(!readBit(repository, 4), "downstream normal coil must follow the new value");
  255. }
  256. void testAllComparisons()
  257. {
  258. const std::array<ComparisonOperator, 6> operations{
  259. ComparisonOperator::Equal, ComparisonOperator::NotEqual,
  260. ComparisonOperator::LessThan, ComparisonOperator::LessThanOrEqual,
  261. ComparisonOperator::GreaterThan, ComparisonOperator::GreaterThanOrEqual};
  262. const std::array<std::int16_t, 6> actual_values{10, 9, 9, 10, 11, 10};
  263. for (std::size_t index = 0; index < operations.size(); ++index)
  264. {
  265. VirtualRegisterRepository repository;
  266. SoftwareLogicExecutor executor;
  267. writeWord(repository, 0, actual_values[index]);
  268. const ControlLogic program = logic({
  269. rung("compare-rung", {{comparison("compare", 0, operations[index], 10)},
  270. }, coil("result", 10))});
  271. require(executor.executeScan({program}, repository).succeeded,
  272. "comparison scan must succeed");
  273. require(readBit(repository, 10), "comparison operator must evaluate true");
  274. }
  275. }
  276. void testSetResetAndDisabledLogic()
  277. {
  278. VirtualRegisterRepository repository;
  279. SoftwareLogicExecutor executor;
  280. ControlLogic program = logic({
  281. rung("set-rung", {{contact("set-input", 0)}}, coil("set-output", 5, CoilMode::Set)),
  282. rung("reset-rung", {{contact("reset-input", 1)}},
  283. coil("reset-output", 6, CoilMode::Reset))});
  284. writeBit(repository, 0, true);
  285. require(executor.executeScan({program}, repository).succeeded, "set scan must succeed");
  286. require(readBit(repository, 5), "set coil must latch true");
  287. writeBit(repository, 0, false);
  288. require(executor.executeScan({program}, repository).succeeded,
  289. "inactive set scan must succeed");
  290. require(readBit(repository, 5), "inactive set coil must retain its value");
  291. writeBit(repository, 1, true);
  292. writeBit(repository, 6, true);
  293. require(executor.executeScan({program}, repository).succeeded,
  294. "reset scan must succeed");
  295. require(!readBit(repository, 6), "reset coil must write false");
  296. program.enabled = false;
  297. writeBit(repository, 5, true);
  298. require(executor.executeScan({program}, repository).succeeded,
  299. "disabled logic scan must be ignored successfully");
  300. require(readBit(repository, 5), "disabled logic must not change outputs");
  301. }
  302. void testMultipleLogicScanOrderAndTraceIsolation()
  303. {
  304. VirtualRegisterRepository repository;
  305. SoftwareLogicExecutor executor;
  306. ControlLogic first = logic({
  307. rung("rung-1", {{contact("input", 0)}}, coil("output", 1))});
  308. first.id = "logic-first";
  309. first.name = "First";
  310. ControlLogic second = logic({
  311. rung("rung-1", {{contact("input", 1)}}, coil("output", 2))});
  312. second.id = "logic-second";
  313. second.name = "Second";
  314. writeBit(repository, 0, true);
  315. LogicTraceSnapshot trace;
  316. require(executor.executeScan({first, second}, repository, &trace).succeeded,
  317. "all enabled logic modules must execute in project order");
  318. require(readBit(repository, 1) && readBit(repository, 2),
  319. "a later logic module must observe an earlier module write in one scan");
  320. require(trace.logicValues.size() == 2U
  321. && trace.forLogic(first.id).rungValues.at("rung-1")
  322. && trace.forLogic(second.id).rungValues.at("rung-1"),
  323. "runtime traces must be partitioned by logic id when node ids repeat");
  324. ControlLogic disabled_draft;
  325. disabled_draft.id = "logic-draft";
  326. disabled_draft.name = "Draft";
  327. disabled_draft.enabled = false;
  328. disabled_draft.rungs.push_back(
  329. {"rung-1", "Draft", {}, std::nullopt, std::nullopt});
  330. require(executor.validate({first, disabled_draft}).succeeded,
  331. "a disabled incomplete logic module must not block offline execution");
  332. }
  333. void testEdgeContactsAreOneScanPulsesAndAreLogicScoped()
  334. {
  335. VirtualRegisterRepository repository;
  336. SoftwareLogicExecutor executor;
  337. const ControlLogic program = logic({
  338. rung("rising-rung", {{edgeContact("rising", 0, EdgeMode::Rising)}},
  339. coil("rising-output", 10)),
  340. rung("falling-rung", {{edgeContact("falling", 1, EdgeMode::Falling)}},
  341. coil("falling-output", 11))});
  342. writeBit(repository, 0, true);
  343. require(executor.executeScan({program}, repository).succeeded,
  344. "the first high scan must evaluate rising edge input");
  345. require(readBit(repository, 10), "rising edge must pulse on OFF to ON");
  346. require(!readBit(repository, 11), "falling edge must stay off without ON to OFF");
  347. require(executor.executeScan({program}, repository).succeeded,
  348. "a repeated high scan must succeed");
  349. require(!readBit(repository, 10), "rising edge must clear after one scan");
  350. writeBit(repository, 1, true);
  351. require(executor.executeScan({program}, repository).succeeded,
  352. "the falling edge input must establish its previous high state");
  353. writeBit(repository, 1, false);
  354. require(executor.executeScan({program}, repository).succeeded,
  355. "the first low scan after high must succeed");
  356. require(readBit(repository, 11), "falling edge must pulse on ON to OFF");
  357. require(executor.executeScan({program}, repository).succeeded,
  358. "a repeated low scan must succeed");
  359. require(!readBit(repository, 11), "falling edge must clear after one scan");
  360. ControlLogic first = logic({
  361. rung("shared-rung-first", {{edgeContact("shared-edge", 2, EdgeMode::Rising)}},
  362. coil("shared-output-first", 20))});
  363. first.id = "logic-first";
  364. first.name = "First";
  365. ControlLogic second = logic({
  366. rung("shared-rung-second", {{edgeContact("shared-edge", 3, EdgeMode::Rising)}},
  367. coil("shared-output-second", 21))});
  368. second.id = "logic-second";
  369. second.name = "Second";
  370. writeBit(repository, 2, true);
  371. require(executor.executeScan({first, second}, repository).succeeded,
  372. "logic-scoped edge history must support duplicate node ids");
  373. writeBit(repository, 3, true);
  374. require(executor.executeScan({first, second}, repository).succeeded,
  375. "each duplicate edge node must update its own history");
  376. require(!readBit(repository, 20) && readBit(repository, 21),
  377. "duplicate node ids in different logic modules must not share edge history");
  378. executor.resetRuntime();
  379. require(executor.executeScan({first, second}, repository).succeeded,
  380. "resetting runtime must clear edge history");
  381. require(readBit(repository, 20) && readBit(repository, 21),
  382. "a restarted runtime must treat current ON inputs as fresh rising edges");
  383. }
  384. void testTonUsesElapsedTimeAndResetsAsNonRetentive()
  385. {
  386. VirtualRegisterRepository repository;
  387. SoftwareLogicExecutor executor;
  388. const ControlLogic program = logic({
  389. rung("ton-rung", {{contact("ton-input", 0)}}, ton("ton-0", 0, 100)),
  390. rung("ton-feedback-rung", {{timerContact("timer-done", 0)}},
  391. coil("ton-output", 30))});
  392. const auto start = SoftwareLogicExecutor::TimePoint{};
  393. LogicTraceSnapshot trace;
  394. require(executor.executeScanAt({program}, repository, start, &trace).succeeded,
  395. "an inactive TON must scan successfully");
  396. require(!readBit(repository, 30)
  397. && trace.tonValues.at("ton-0").elapsedMs == 0
  398. && !trace.tonValues.at("ton-0").done,
  399. "an inactive TON must have Q false and ET zero");
  400. writeBit(repository, 0, true);
  401. require(executor.executeScanAt({program}, repository, start, &trace).succeeded,
  402. "the first active TON scan must start timing");
  403. require(!readBit(repository, 30) && trace.tonValues.at("ton-0").elapsedMs == 0,
  404. "TON must not complete on its starting scan");
  405. require(executor.executeScanAt(
  406. {program}, repository,
  407. start + std::chrono::milliseconds{99}, &trace)
  408. .succeeded,
  409. "TON must use elapsed monotonic time before the preset");
  410. require(!readBit(repository, 30)
  411. && trace.tonValues.at("ton-0").elapsedMs == 99,
  412. "TON must remain false before PT");
  413. require(executor.executeScanAt(
  414. {program}, repository,
  415. start + std::chrono::milliseconds{100}, &trace)
  416. .succeeded,
  417. "TON must complete at its preset time");
  418. require(readBit(repository, 30)
  419. && trace.tonValues.at("ton-0").done,
  420. "the T contact must observe TON Q in a later network of the same scan");
  421. writeBit(repository, 0, false);
  422. require(executor.executeScanAt(
  423. {program}, repository,
  424. start + std::chrono::milliseconds{150}, &trace)
  425. .succeeded,
  426. "disconnecting TON input must scan successfully");
  427. require(!readBit(repository, 30)
  428. && trace.tonValues.at("ton-0").elapsedMs == 0
  429. && !trace.tonValues.at("ton-0").done,
  430. "a non-retentive TON must reset ET and Q when input opens");
  431. writeBit(repository, 0, true);
  432. require(executor.executeScanAt(
  433. {program}, repository,
  434. start + std::chrono::milliseconds{200}, &trace)
  435. .succeeded,
  436. "TON must restart timing after an input reset");
  437. require(!readBit(repository, 30), "TON restart must not retain the old done state");
  438. executor.resetRuntime();
  439. require(executor.executeScanAt(
  440. {program}, repository,
  441. start + std::chrono::milliseconds{1000}, &trace)
  442. .succeeded,
  443. "resetRuntime must clear TON state");
  444. require(!readBit(repository, 30),
  445. "a restarted runtime must start a currently active TON from zero");
  446. }
  447. void testMultipleTimersAndNetworkOrder()
  448. {
  449. VirtualRegisterRepository repository;
  450. SoftwareLogicExecutor executor;
  451. const ControlLogic independent = logic({
  452. rung("ton-1-rung", {{contact("input-1", 1)}}, ton("ton-1", 1, 100)),
  453. rung("ton-2-rung", {{contact("input-2", 2)}}, ton("ton-2", 2, 200))});
  454. writeBit(repository, 1, true);
  455. writeBit(repository, 2, true);
  456. const auto start = SoftwareLogicExecutor::TimePoint{};
  457. LogicTraceSnapshot trace;
  458. require(executor.executeScanAt({independent}, repository, start, &trace).succeeded,
  459. "multiple TON resources must start independently");
  460. require(executor.executeScanAt(
  461. {independent}, repository,
  462. start + std::chrono::milliseconds{100}, &trace)
  463. .succeeded,
  464. "the first independent TON must reach PT without completing the second");
  465. require(trace.tonValues.at("ton-1").done
  466. && !trace.tonValues.at("ton-2").done,
  467. "different T resources must retain independent elapsed time");
  468. VirtualRegisterRepository ordered_repository;
  469. SoftwareLogicExecutor ordered_executor;
  470. const ControlLogic ordered = logic({
  471. rung("feedback-before-ton", {{timerContact("early-timer", 4)}},
  472. coil("early-output", 40)),
  473. rung("ton-after-feedback", {{contact("late-input", 4)}}, ton("ton-4", 4, 100))});
  474. writeBit(ordered_repository, 4, true);
  475. require(ordered_executor.executeScanAt(
  476. {ordered}, ordered_repository, start, nullptr)
  477. .succeeded,
  478. "network order test must start TON after its earlier T contact");
  479. require(!readBit(ordered_repository, 40),
  480. "an earlier T contact must see the previous scan state");
  481. require(ordered_executor.executeScanAt(
  482. {ordered}, ordered_repository,
  483. start + std::chrono::milliseconds{100}, nullptr)
  484. .succeeded,
  485. "network order test must complete TON on the next scan");
  486. require(!readBit(ordered_repository, 40),
  487. "the earlier T contact must remain false on the completion scan");
  488. require(ordered_executor.executeScanAt(
  489. {ordered}, ordered_repository,
  490. start + std::chrono::milliseconds{101}, nullptr)
  491. .succeeded,
  492. "network order test must expose TON Q on the following scan");
  493. require(readBit(ordered_repository, 40),
  494. "a later scan must observe the completed TON through T contact");
  495. }
  496. void testCountersUseRisingEdgesAndExternalDValues()
  497. {
  498. VirtualRegisterRepository repository;
  499. SoftwareLogicExecutor executor;
  500. const ControlLogic up_program = logic({
  501. rung(
  502. "ctu-rung",
  503. {{contact("count-input", 0)}},
  504. counter(
  505. "ctu-0",
  506. 0,
  507. CounterMode::Up,
  508. 0,
  509. constantOperand(3),
  510. 1)),
  511. rung(
  512. "ctu-done-rung",
  513. {{counterContact("ctu-done", 0)}},
  514. coil("ctu-done-output", 2))});
  515. LogicTraceSnapshot trace;
  516. require(executor.executeScan({up_program}, repository, &trace).succeeded,
  517. "an inactive CTU must scan successfully");
  518. require(readWord(repository, 0) == 0 && !readBit(repository, 2),
  519. "an inactive CTU must keep CV zero and Q false");
  520. writeBit(repository, 0, true);
  521. require(executor.executeScan({up_program}, repository, &trace).succeeded,
  522. "the first CTU rising edge must scan successfully");
  523. require(readWord(repository, 0) == 1,
  524. "CTU must increment the external D current value on a rising edge");
  525. require(executor.executeScan({up_program}, repository, &trace).succeeded,
  526. "a held CTU input must scan successfully");
  527. require(readWord(repository, 0) == 1,
  528. "a held CTU input must not count every scan");
  529. for (int expected = 2; expected <= 3; ++expected)
  530. {
  531. writeBit(repository, 0, false);
  532. require(executor.executeScan({up_program}, repository).succeeded,
  533. "CTU falling preparation scan must succeed");
  534. writeBit(repository, 0, true);
  535. require(executor.executeScan({up_program}, repository, &trace).succeeded,
  536. "CTU repeated rising edge must succeed");
  537. require(readWord(repository, 0) == expected,
  538. "CTU must increment exactly once for each rising edge");
  539. }
  540. require(readBit(repository, 2)
  541. && trace.counterValues.at("ctu-0").done,
  542. "the C contact must observe CTU completion in a later network");
  543. writeBit(repository, 1, true);
  544. require(executor.executeScan({up_program}, repository, &trace).succeeded,
  545. "CTU reset must scan successfully");
  546. require(readWord(repository, 0) == 0 && !readBit(repository, 2),
  547. "CTU reset must clear CV and the C completion state");
  548. executor.resetRuntime();
  549. repository.clear();
  550. const ControlLogic down_program = logic({
  551. rung(
  552. "ctd-rung",
  553. {{contact("down-input", 3)}},
  554. counter(
  555. "ctd-1",
  556. 1,
  557. CounterMode::Down,
  558. 1,
  559. constantOperand(2),
  560. 4)),
  561. rung(
  562. "ctd-done-rung",
  563. {{counterContact("ctd-done", 1)}},
  564. coil("ctd-done-output", 5))});
  565. writeBit(repository, 4, true);
  566. require(executor.executeScan({down_program}, repository).succeeded,
  567. "CTD load reset must scan successfully");
  568. require(readWord(repository, 1) == 2,
  569. "CTD reset must load PV into the external D current value");
  570. writeBit(repository, 4, false);
  571. for (int expected = 1; expected >= 0; --expected)
  572. {
  573. writeBit(repository, 3, false);
  574. require(executor.executeScan({down_program}, repository).succeeded,
  575. "CTD falling preparation scan must succeed");
  576. writeBit(repository, 3, true);
  577. require(executor.executeScan({down_program}, repository).succeeded,
  578. "CTD rising edge must scan successfully");
  579. require(readWord(repository, 1) == expected,
  580. "CTD must decrement exactly once for each rising edge");
  581. }
  582. require(readBit(repository, 5),
  583. "CTD completion contact must turn on when CV reaches zero");
  584. }
  585. void testMoveAndSaturatingArithmetic()
  586. {
  587. VirtualRegisterRepository repository;
  588. SoftwareLogicExecutor executor;
  589. const ControlLogic program = logic({
  590. rung(
  591. "move-rung",
  592. {{contact("execute", 0)}},
  593. move("move", constantOperand(7), 10)),
  594. rung(
  595. "add-rung",
  596. {{contact("execute-add", 0)}},
  597. arithmetic(
  598. "add",
  599. ArithmeticOperation::Add,
  600. registerOperand(10),
  601. constantOperand(32767),
  602. 11)),
  603. rung(
  604. "sub-rung",
  605. {{contact("execute-sub", 0)}},
  606. arithmetic(
  607. "sub",
  608. ArithmeticOperation::Subtract,
  609. constantOperand(-32768),
  610. constantOperand(1),
  611. 12))});
  612. LogicTraceSnapshot trace;
  613. require(executor.executeScan({program}, repository, &trace).succeeded,
  614. "inactive data instructions must scan successfully");
  615. require(readWord(repository, 10) == 0,
  616. "MOVE must not write while its rung is false");
  617. writeBit(repository, 0, true);
  618. require(executor.executeScan({program}, repository, &trace).succeeded,
  619. "active data instructions must scan successfully");
  620. require(readWord(repository, 10) == 7,
  621. "MOVE must copy a constant into the destination D register");
  622. require(readWord(repository, 11) == 32767
  623. && trace.wordValues.at("add").overflow,
  624. "ADD must saturate positive overflow and expose the overflow trace");
  625. require(readWord(repository, 12) == -32768
  626. && trace.wordValues.at("sub").overflow,
  627. "SUB must saturate negative overflow and expose the overflow trace");
  628. const ControlLogic repeated_add = logic({
  629. rung(
  630. "repeated-add-rung",
  631. {{contact("repeated-add-input", 1)}},
  632. arithmetic(
  633. "repeated-add",
  634. ArithmeticOperation::Add,
  635. registerOperand(20),
  636. constantOperand(1),
  637. 20))});
  638. writeBit(repository, 1, true);
  639. require(executor.executeScan({repeated_add}, repository).succeeded
  640. && executor.executeScan({repeated_add}, repository).succeeded,
  641. "ADD must execute on every scan while its rung remains true");
  642. require(readWord(repository, 20) == 2,
  643. "ADD with the same source and destination must accumulate by scan");
  644. }
  645. void testSetResetPairOnSameAddress()
  646. {
  647. VirtualRegisterRepository repository;
  648. SoftwareLogicExecutor executor;
  649. const ControlLogic program = logic({
  650. rung("low-level-rung",
  651. {{comparison("low-level", 0,
  652. ComparisonOperator::LessThanOrEqual, 30)}},
  653. coil("pump-set", 20, CoilMode::Set)),
  654. rung("high-level-rung",
  655. {{comparison("high-level", 0,
  656. ComparisonOperator::GreaterThanOrEqual, 80)}},
  657. coil("pump-reset", 20, CoilMode::Reset))});
  658. writeWord(repository, 0, 20);
  659. require(executor.executeScan({program}, repository).succeeded,
  660. "set/reset pair scan must succeed at the low limit");
  661. require(readBit(repository, 20), "low level must latch the pump on");
  662. writeWord(repository, 0, 50);
  663. require(executor.executeScan({program}, repository).succeeded,
  664. "set/reset pair scan must succeed inside the deadband");
  665. require(readBit(repository, 20), "deadband must retain the latched on state");
  666. writeWord(repository, 0, 90);
  667. require(executor.executeScan({program}, repository).succeeded,
  668. "set/reset pair scan must succeed at the high limit");
  669. require(!readBit(repository, 20), "high level must reset the pump");
  670. writeWord(repository, 0, 50);
  671. require(executor.executeScan({program}, repository).succeeded,
  672. "set/reset pair rescan must succeed inside the deadband");
  673. require(!readBit(repository, 20), "deadband must retain the reset state");
  674. }
  675. void testConflictingCoilsAreRejected()
  676. {
  677. SoftwareLogicExecutor executor;
  678. const ControlLogic normal_and_set = logic({
  679. rung("normal-rung", {{contact("normal-input", 0)}}, coil("normal", 8)),
  680. rung("set-rung", {{contact("set-input", 1)}}, coil("set", 8, CoilMode::Set))});
  681. const LogicScanResult set_result = executor.validate({normal_and_set});
  682. require(!set_result.succeeded
  683. && set_result.error == LogicScanError::ConflictingOutput,
  684. "normal and set coils for one address must be rejected");
  685. require(set_result.rungId == "set-rung" && set_result.nodeId == "set",
  686. "conflict error must identify the offending rung and node");
  687. const ControlLogic reset_and_normal = logic({
  688. rung("reset-rung", {{contact("reset-input", 0)}},
  689. coil("reset", 9, CoilMode::Reset)),
  690. rung("normal-rung", {{contact("normal-input", 1)}}, coil("normal", 9))});
  691. const LogicScanResult reset_result = executor.validate({reset_and_normal});
  692. require(!reset_result.succeeded
  693. && reset_result.error == LogicScanError::ConflictingOutput,
  694. "reset and normal coils for one address must be rejected");
  695. }
  696. void testHmiSimulationClosedLoop()
  697. {
  698. VirtualRegisterRepository repository;
  699. HmiRuntimeService hmi(repository);
  700. SoftwareLogicExecutor executor;
  701. HmiControl start;
  702. start.id = "start-button";
  703. start.type = HmiControlType::Button;
  704. start.bounds = {0, 0, 80, 30};
  705. start.text = "start";
  706. start.binding = RegisterAddress{RegisterArea::M, 0};
  707. start.buttonOperation = HmiButtonOperation::MomentaryOn;
  708. HmiControl indicator;
  709. indicator.id = "run-indicator";
  710. indicator.type = HmiControlType::Indicator;
  711. indicator.bounds = {0, 40, 80, 30};
  712. indicator.text = "run";
  713. indicator.binding = RegisterAddress{RegisterArea::M, 2};
  714. const ControlLogic program = logic({
  715. rung("hold-rung",
  716. {{contact("stop", 1, ContactMode::NormallyClosed)},
  717. {contact("start", 0), contact("feedback", 2)}},
  718. coil("run", 2))});
  719. require(hmi.operateButton(start, HmiButtonEvent::Pressed).succeeded,
  720. "HMI start button press must write virtual M");
  721. require(executor.executeScan({program}, repository).succeeded,
  722. "closed-loop scan must succeed");
  723. require(hmi.readControl(indicator).bit_value,
  724. "HMI indicator must observe the logic output");
  725. require(hmi.operateButton(start, HmiButtonEvent::Released).succeeded,
  726. "HMI start button release must write virtual M");
  727. require(executor.executeScan({program}, repository).succeeded, "holding scan must succeed");
  728. require(hmi.readControl(indicator).bit_value,
  729. "feedback contact must hold the output after start turns off");
  730. writeBit(repository, 1, true);
  731. require(executor.executeScan({program}, repository).succeeded, "stop scan must succeed");
  732. require(!hmi.readControl(indicator).bit_value,
  733. "stop contact must clear the HMI run indication");
  734. }
  735. void testSimulationLifecycleSnapshotAndFault()
  736. {
  737. VirtualRegisterRepository repository;
  738. OfflineSimulationService simulation(repository);
  739. ControlLogic program = logic({
  740. rung("snapshot-rung", {{contact("input", 0)}}, coil("output", 1))});
  741. writeBit(repository, 0, true);
  742. require(simulation.start({program}).succeeded, "simulation must start");
  743. require(simulation.state() == SimulationState::Running, "simulation must report running");
  744. require(!readBit(repository, 0), "starting a session must clear virtual registers");
  745. require(simulation.scanIntervalMs() == 50, "default scan interval must be 50 ms");
  746. writeBit(repository, 0, true);
  747. program.rungs.front().output = coil("changed-output", 9);
  748. require(simulation.executeOnce().succeeded, "manual lifecycle scan must succeed");
  749. require(readBit(repository, 1), "simulation must use its start-time snapshot");
  750. require(simulation.successfulScanCount() == 1, "successful scans must be counted");
  751. simulation.stop();
  752. require(simulation.state() == SimulationState::Stopped, "simulation must stop");
  753. require(simulation.start({program}).succeeded, "stopped simulation must support restart");
  754. require(!readBit(repository, 1), "new session must clear prior output values");
  755. simulation.stop();
  756. }
  757. class FailingVirtualRegisterRepository final : public VirtualRegisterRepository
  758. {
  759. public:
  760. BitReadResult readBit(const RegisterAddress &) const override
  761. {
  762. return {false, false, RegisterError::Unavailable};
  763. }
  764. };
  765. void testRepositoryFailureEntersFaultState()
  766. {
  767. FailingVirtualRegisterRepository repository;
  768. OfflineSimulationService simulation(repository);
  769. const ControlLogic program = logic({
  770. rung("fault-rung", {{contact("fault-input", 0)}}, coil("fault-output", 1))});
  771. require(simulation.start({program}).succeeded,
  772. "valid logic must pass simulation startup");
  773. const LogicScanResult result = simulation.executeOnce();
  774. require(!result.succeeded && result.error == LogicScanError::RegisterReadFailed,
  775. "repository read failure must fail the scan");
  776. require(simulation.state() == SimulationState::Faulted,
  777. "repository failure must enter the fault state");
  778. require(simulation.lastError().logicId == "logic-1"
  779. && simulation.lastError().rungId == "fault-rung"
  780. && simulation.lastError().nodeId == "fault-input",
  781. "fault feedback must retain logic, rung and node context");
  782. }
  783. } // namespace
  784. int main(int argc, char *argv[])
  785. {
  786. QCoreApplication application(argc, argv);
  787. try
  788. {
  789. testSeriesParallelContactsAndSequentialVisibility();
  790. testNestedSeriesParallelExpression();
  791. testAllComparisons();
  792. testSetResetAndDisabledLogic();
  793. testMultipleLogicScanOrderAndTraceIsolation();
  794. testEdgeContactsAreOneScanPulsesAndAreLogicScoped();
  795. testTonUsesElapsedTimeAndResetsAsNonRetentive();
  796. testMultipleTimersAndNetworkOrder();
  797. testCountersUseRisingEdgesAndExternalDValues();
  798. testMoveAndSaturatingArithmetic();
  799. testSetResetPairOnSameAddress();
  800. testConflictingCoilsAreRejected();
  801. testHmiSimulationClosedLoop();
  802. testSimulationLifecycleSnapshotAndFault();
  803. testRepositoryFailureEntersFaultState();
  804. }
  805. catch (const std::exception &error)
  806. {
  807. std::cerr << "offline simulation service tests failed: " << error.what() << '\n';
  808. return 1;
  809. }
  810. std::cout << "offline simulation service tests passed\n";
  811. return 0;
  812. }