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

949 rivejä
38 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 testUnconditionalCoil()
  235. {
  236. VirtualRegisterRepository repository;
  237. SoftwareLogicExecutor executor;
  238. LadderRung unconditional;
  239. unconditional.id = "unconditional-rung";
  240. unconditional.name = "unconditional-rung";
  241. unconditional.output = coil("unconditional-coil", 10);
  242. const ControlLogic program = logic({unconditional});
  243. LogicTraceSnapshot trace;
  244. require(executor.validate({program}).succeeded,
  245. "an output-only network must pass runtime validation");
  246. require(executor.executeScan({program}, repository, &trace).succeeded,
  247. "an output-only network scan must succeed");
  248. require(readBit(repository, 10),
  249. "an output-only network must energize its coil as a constant-true rung");
  250. require(trace.rungValues.at("unconditional-rung")
  251. && trace.nodePowerValues.at("unconditional-coil"),
  252. "an unconditional rung must report energized power flow");
  253. }
  254. void testWirePassThroughAndPowerTrace()
  255. {
  256. VirtualRegisterRepository repository;
  257. SoftwareLogicExecutor executor;
  258. ConditionExpression root;
  259. root.id = "wire-series";
  260. root.kind = ConditionExpressionKind::Series;
  261. root.children = {
  262. ConditionExpression::fromNode(contact("wire-input", 0)),
  263. ConditionExpression::fromWire("wire-segment", 2),
  264. ConditionExpression::fromNode(contact("wire-output", 1))};
  265. LadderRung wired_rung;
  266. wired_rung.id = "wired-rung";
  267. wired_rung.name = "wired-rung";
  268. wired_rung.condition = root;
  269. wired_rung.output = coil("wired-coil", 10);
  270. const ControlLogic program = logic({wired_rung});
  271. writeBit(repository, 1, true);
  272. LogicTraceSnapshot trace;
  273. require(executor.executeScan({program}, repository, &trace).succeeded,
  274. "wire expression scan must succeed");
  275. require(!readBit(repository, 10),
  276. "a horizontal wire must not bypass a false upstream series contact");
  277. require(trace.expressionValues.at("wire-segment")
  278. && !trace.expressionInputValues.at("wire-segment")
  279. && !trace.expressionPowerValues.at("wire-segment"),
  280. "a wire must remain logically true without showing false upstream power");
  281. writeBit(repository, 0, true);
  282. require(executor.executeScan({program}, repository, &trace).succeeded,
  283. "powered wire expression scan must succeed");
  284. require(readBit(repository, 10)
  285. && trace.expressionInputValues.at("wire-segment")
  286. && trace.expressionPowerValues.at("wire-segment"),
  287. "a powered horizontal wire must pass current to the downstream contact");
  288. }
  289. void testSeriesParallelContactsAndSequentialVisibility()
  290. {
  291. VirtualRegisterRepository repository;
  292. SoftwareLogicExecutor executor;
  293. const ControlLogic program = logic({
  294. rung("rung-1",
  295. {{contact("start", 0), contact("alternate", 1)},
  296. {contact("stop", 2, ContactMode::NormallyClosed)}},
  297. coil("run", 3)),
  298. rung("rung-2", {{contact("run-feedback", 3)}}, coil("downstream", 4))});
  299. writeBit(repository, 1, true);
  300. require(executor.executeScan({program}, repository).succeeded,
  301. "parallel and series scan must succeed");
  302. require(readBit(repository, 3), "parallel OR and series AND must energize output");
  303. require(readBit(repository, 4),
  304. "a later rung must see an earlier rung write in the same scan");
  305. writeBit(repository, 2, true);
  306. require(executor.executeScan({program}, repository).succeeded,
  307. "normally closed scan must succeed");
  308. require(!readBit(repository, 3), "normally closed stop contact must open the rung");
  309. require(!readBit(repository, 4), "downstream normal coil must follow the new value");
  310. }
  311. void testAllComparisons()
  312. {
  313. const std::array<ComparisonOperator, 6> operations{
  314. ComparisonOperator::Equal, ComparisonOperator::NotEqual,
  315. ComparisonOperator::LessThan, ComparisonOperator::LessThanOrEqual,
  316. ComparisonOperator::GreaterThan, ComparisonOperator::GreaterThanOrEqual};
  317. const std::array<std::int16_t, 6> actual_values{10, 9, 9, 10, 11, 10};
  318. for (std::size_t index = 0; index < operations.size(); ++index)
  319. {
  320. VirtualRegisterRepository repository;
  321. SoftwareLogicExecutor executor;
  322. writeWord(repository, 0, actual_values[index]);
  323. const ControlLogic program = logic({
  324. rung("compare-rung", {{comparison("compare", 0, operations[index], 10)},
  325. }, coil("result", 10))});
  326. require(executor.executeScan({program}, repository).succeeded,
  327. "comparison scan must succeed");
  328. require(readBit(repository, 10), "comparison operator must evaluate true");
  329. }
  330. }
  331. void testSetResetAndDisabledLogic()
  332. {
  333. VirtualRegisterRepository repository;
  334. SoftwareLogicExecutor executor;
  335. ControlLogic program = logic({
  336. rung("set-rung", {{contact("set-input", 0)}}, coil("set-output", 5, CoilMode::Set)),
  337. rung("reset-rung", {{contact("reset-input", 1)}},
  338. coil("reset-output", 6, CoilMode::Reset))});
  339. writeBit(repository, 0, true);
  340. require(executor.executeScan({program}, repository).succeeded, "set scan must succeed");
  341. require(readBit(repository, 5), "set coil must latch true");
  342. writeBit(repository, 0, false);
  343. require(executor.executeScan({program}, repository).succeeded,
  344. "inactive set scan must succeed");
  345. require(readBit(repository, 5), "inactive set coil must retain its value");
  346. writeBit(repository, 1, true);
  347. writeBit(repository, 6, true);
  348. require(executor.executeScan({program}, repository).succeeded,
  349. "reset scan must succeed");
  350. require(!readBit(repository, 6), "reset coil must write false");
  351. program.enabled = false;
  352. writeBit(repository, 5, true);
  353. require(executor.executeScan({program}, repository).succeeded,
  354. "disabled logic scan must be ignored successfully");
  355. require(readBit(repository, 5), "disabled logic must not change outputs");
  356. }
  357. void testMultipleLogicScanOrderAndTraceIsolation()
  358. {
  359. VirtualRegisterRepository repository;
  360. SoftwareLogicExecutor executor;
  361. ControlLogic first = logic({
  362. rung("rung-1", {{contact("input", 0)}}, coil("output", 1))});
  363. first.id = "logic-first";
  364. first.name = "First";
  365. ControlLogic second = logic({
  366. rung("rung-1", {{contact("input", 1)}}, coil("output", 2))});
  367. second.id = "logic-second";
  368. second.name = "Second";
  369. writeBit(repository, 0, true);
  370. LogicTraceSnapshot trace;
  371. require(executor.executeScan({first, second}, repository, &trace).succeeded,
  372. "all enabled logic modules must execute in project order");
  373. require(readBit(repository, 1) && readBit(repository, 2),
  374. "a later logic module must observe an earlier module write in one scan");
  375. require(trace.logicValues.size() == 2U
  376. && trace.forLogic(first.id).rungValues.at("rung-1")
  377. && trace.forLogic(second.id).rungValues.at("rung-1"),
  378. "runtime traces must be partitioned by logic id when node ids repeat");
  379. ControlLogic disabled_draft;
  380. disabled_draft.id = "logic-draft";
  381. disabled_draft.name = "Draft";
  382. disabled_draft.enabled = false;
  383. disabled_draft.rungs.push_back(
  384. {"rung-1", "Draft", {}, std::nullopt, std::nullopt});
  385. require(executor.validate({first, disabled_draft}).succeeded,
  386. "a disabled incomplete logic module must not block offline execution");
  387. }
  388. void testEdgeContactsAreOneScanPulsesAndAreLogicScoped()
  389. {
  390. VirtualRegisterRepository repository;
  391. SoftwareLogicExecutor executor;
  392. const ControlLogic program = logic({
  393. rung("rising-rung", {{edgeContact("rising", 0, EdgeMode::Rising)}},
  394. coil("rising-output", 10)),
  395. rung("falling-rung", {{edgeContact("falling", 1, EdgeMode::Falling)}},
  396. coil("falling-output", 11))});
  397. writeBit(repository, 0, true);
  398. require(executor.executeScan({program}, repository).succeeded,
  399. "the first high scan must evaluate rising edge input");
  400. require(readBit(repository, 10), "rising edge must pulse on OFF to ON");
  401. require(!readBit(repository, 11), "falling edge must stay off without ON to OFF");
  402. require(executor.executeScan({program}, repository).succeeded,
  403. "a repeated high scan must succeed");
  404. require(!readBit(repository, 10), "rising edge must clear after one scan");
  405. writeBit(repository, 1, true);
  406. require(executor.executeScan({program}, repository).succeeded,
  407. "the falling edge input must establish its previous high state");
  408. writeBit(repository, 1, false);
  409. require(executor.executeScan({program}, repository).succeeded,
  410. "the first low scan after high must succeed");
  411. require(readBit(repository, 11), "falling edge must pulse on ON to OFF");
  412. require(executor.executeScan({program}, repository).succeeded,
  413. "a repeated low scan must succeed");
  414. require(!readBit(repository, 11), "falling edge must clear after one scan");
  415. ControlLogic first = logic({
  416. rung("shared-rung-first", {{edgeContact("shared-edge", 2, EdgeMode::Rising)}},
  417. coil("shared-output-first", 20))});
  418. first.id = "logic-first";
  419. first.name = "First";
  420. ControlLogic second = logic({
  421. rung("shared-rung-second", {{edgeContact("shared-edge", 3, EdgeMode::Rising)}},
  422. coil("shared-output-second", 21))});
  423. second.id = "logic-second";
  424. second.name = "Second";
  425. writeBit(repository, 2, true);
  426. require(executor.executeScan({first, second}, repository).succeeded,
  427. "logic-scoped edge history must support duplicate node ids");
  428. writeBit(repository, 3, true);
  429. require(executor.executeScan({first, second}, repository).succeeded,
  430. "each duplicate edge node must update its own history");
  431. require(!readBit(repository, 20) && readBit(repository, 21),
  432. "duplicate node ids in different logic modules must not share edge history");
  433. executor.resetRuntime();
  434. require(executor.executeScan({first, second}, repository).succeeded,
  435. "resetting runtime must clear edge history");
  436. require(readBit(repository, 20) && readBit(repository, 21),
  437. "a restarted runtime must treat current ON inputs as fresh rising edges");
  438. }
  439. void testTonUsesElapsedTimeAndResetsAsNonRetentive()
  440. {
  441. VirtualRegisterRepository repository;
  442. SoftwareLogicExecutor executor;
  443. const ControlLogic program = logic({
  444. rung("ton-rung", {{contact("ton-input", 0)}}, ton("ton-0", 0, 100)),
  445. rung("ton-feedback-rung", {{timerContact("timer-done", 0)}},
  446. coil("ton-output", 30))});
  447. const auto start = SoftwareLogicExecutor::TimePoint{};
  448. LogicTraceSnapshot trace;
  449. require(executor.executeScanAt({program}, repository, start, &trace).succeeded,
  450. "an inactive TON must scan successfully");
  451. require(!readBit(repository, 30)
  452. && trace.tonValues.at("ton-0").elapsedMs == 0
  453. && !trace.tonValues.at("ton-0").done,
  454. "an inactive TON must have Q false and ET zero");
  455. writeBit(repository, 0, true);
  456. require(executor.executeScanAt({program}, repository, start, &trace).succeeded,
  457. "the first active TON scan must start timing");
  458. require(!readBit(repository, 30) && trace.tonValues.at("ton-0").elapsedMs == 0,
  459. "TON must not complete on its starting scan");
  460. require(executor.executeScanAt(
  461. {program}, repository,
  462. start + std::chrono::milliseconds{99}, &trace)
  463. .succeeded,
  464. "TON must use elapsed monotonic time before the preset");
  465. require(!readBit(repository, 30)
  466. && trace.tonValues.at("ton-0").elapsedMs == 99,
  467. "TON must remain false before PT");
  468. require(executor.executeScanAt(
  469. {program}, repository,
  470. start + std::chrono::milliseconds{100}, &trace)
  471. .succeeded,
  472. "TON must complete at its preset time");
  473. require(readBit(repository, 30)
  474. && trace.tonValues.at("ton-0").done,
  475. "the T contact must observe TON Q in a later network of the same scan");
  476. writeBit(repository, 0, false);
  477. require(executor.executeScanAt(
  478. {program}, repository,
  479. start + std::chrono::milliseconds{150}, &trace)
  480. .succeeded,
  481. "disconnecting TON input must scan successfully");
  482. require(!readBit(repository, 30)
  483. && trace.tonValues.at("ton-0").elapsedMs == 0
  484. && !trace.tonValues.at("ton-0").done,
  485. "a non-retentive TON must reset ET and Q when input opens");
  486. writeBit(repository, 0, true);
  487. require(executor.executeScanAt(
  488. {program}, repository,
  489. start + std::chrono::milliseconds{200}, &trace)
  490. .succeeded,
  491. "TON must restart timing after an input reset");
  492. require(!readBit(repository, 30), "TON restart must not retain the old done state");
  493. executor.resetRuntime();
  494. require(executor.executeScanAt(
  495. {program}, repository,
  496. start + std::chrono::milliseconds{1000}, &trace)
  497. .succeeded,
  498. "resetRuntime must clear TON state");
  499. require(!readBit(repository, 30),
  500. "a restarted runtime must start a currently active TON from zero");
  501. }
  502. void testMultipleTimersAndNetworkOrder()
  503. {
  504. VirtualRegisterRepository repository;
  505. SoftwareLogicExecutor executor;
  506. const ControlLogic independent = logic({
  507. rung("ton-1-rung", {{contact("input-1", 1)}}, ton("ton-1", 1, 100)),
  508. rung("ton-2-rung", {{contact("input-2", 2)}}, ton("ton-2", 2, 200))});
  509. writeBit(repository, 1, true);
  510. writeBit(repository, 2, true);
  511. const auto start = SoftwareLogicExecutor::TimePoint{};
  512. LogicTraceSnapshot trace;
  513. require(executor.executeScanAt({independent}, repository, start, &trace).succeeded,
  514. "multiple TON resources must start independently");
  515. require(executor.executeScanAt(
  516. {independent}, repository,
  517. start + std::chrono::milliseconds{100}, &trace)
  518. .succeeded,
  519. "the first independent TON must reach PT without completing the second");
  520. require(trace.tonValues.at("ton-1").done
  521. && !trace.tonValues.at("ton-2").done,
  522. "different T resources must retain independent elapsed time");
  523. VirtualRegisterRepository ordered_repository;
  524. SoftwareLogicExecutor ordered_executor;
  525. const ControlLogic ordered = logic({
  526. rung("feedback-before-ton", {{timerContact("early-timer", 4)}},
  527. coil("early-output", 40)),
  528. rung("ton-after-feedback", {{contact("late-input", 4)}}, ton("ton-4", 4, 100))});
  529. writeBit(ordered_repository, 4, true);
  530. require(ordered_executor.executeScanAt(
  531. {ordered}, ordered_repository, start, nullptr)
  532. .succeeded,
  533. "network order test must start TON after its earlier T contact");
  534. require(!readBit(ordered_repository, 40),
  535. "an earlier T contact must see the previous scan state");
  536. require(ordered_executor.executeScanAt(
  537. {ordered}, ordered_repository,
  538. start + std::chrono::milliseconds{100}, nullptr)
  539. .succeeded,
  540. "network order test must complete TON on the next scan");
  541. require(!readBit(ordered_repository, 40),
  542. "the earlier T contact must remain false on the completion scan");
  543. require(ordered_executor.executeScanAt(
  544. {ordered}, ordered_repository,
  545. start + std::chrono::milliseconds{101}, nullptr)
  546. .succeeded,
  547. "network order test must expose TON Q on the following scan");
  548. require(readBit(ordered_repository, 40),
  549. "a later scan must observe the completed TON through T contact");
  550. }
  551. void testCountersUseRisingEdgesAndExternalDValues()
  552. {
  553. VirtualRegisterRepository repository;
  554. SoftwareLogicExecutor executor;
  555. const ControlLogic up_program = logic({
  556. rung(
  557. "ctu-rung",
  558. {{contact("count-input", 0)}},
  559. counter(
  560. "ctu-0",
  561. 0,
  562. CounterMode::Up,
  563. 0,
  564. constantOperand(3),
  565. 1)),
  566. rung(
  567. "ctu-done-rung",
  568. {{counterContact("ctu-done", 0)}},
  569. coil("ctu-done-output", 2))});
  570. LogicTraceSnapshot trace;
  571. require(executor.executeScan({up_program}, repository, &trace).succeeded,
  572. "an inactive CTU must scan successfully");
  573. require(readWord(repository, 0) == 0 && !readBit(repository, 2),
  574. "an inactive CTU must keep CV zero and Q false");
  575. writeBit(repository, 0, true);
  576. require(executor.executeScan({up_program}, repository, &trace).succeeded,
  577. "the first CTU rising edge must scan successfully");
  578. require(readWord(repository, 0) == 1,
  579. "CTU must increment the external D current value on a rising edge");
  580. require(executor.executeScan({up_program}, repository, &trace).succeeded,
  581. "a held CTU input must scan successfully");
  582. require(readWord(repository, 0) == 1,
  583. "a held CTU input must not count every scan");
  584. for (int expected = 2; expected <= 3; ++expected)
  585. {
  586. writeBit(repository, 0, false);
  587. require(executor.executeScan({up_program}, repository).succeeded,
  588. "CTU falling preparation scan must succeed");
  589. writeBit(repository, 0, true);
  590. require(executor.executeScan({up_program}, repository, &trace).succeeded,
  591. "CTU repeated rising edge must succeed");
  592. require(readWord(repository, 0) == expected,
  593. "CTU must increment exactly once for each rising edge");
  594. }
  595. require(readBit(repository, 2)
  596. && trace.counterValues.at("ctu-0").done,
  597. "the C contact must observe CTU completion in a later network");
  598. writeBit(repository, 1, true);
  599. require(executor.executeScan({up_program}, repository, &trace).succeeded,
  600. "CTU reset must scan successfully");
  601. require(readWord(repository, 0) == 0 && !readBit(repository, 2),
  602. "CTU reset must clear CV and the C completion state");
  603. executor.resetRuntime();
  604. repository.clear();
  605. const ControlLogic down_program = logic({
  606. rung(
  607. "ctd-rung",
  608. {{contact("down-input", 3)}},
  609. counter(
  610. "ctd-1",
  611. 1,
  612. CounterMode::Down,
  613. 1,
  614. constantOperand(2),
  615. 4)),
  616. rung(
  617. "ctd-done-rung",
  618. {{counterContact("ctd-done", 1)}},
  619. coil("ctd-done-output", 5))});
  620. writeBit(repository, 4, true);
  621. require(executor.executeScan({down_program}, repository).succeeded,
  622. "CTD load reset must scan successfully");
  623. require(readWord(repository, 1) == 2,
  624. "CTD reset must load PV into the external D current value");
  625. writeBit(repository, 4, false);
  626. for (int expected = 1; expected >= 0; --expected)
  627. {
  628. writeBit(repository, 3, false);
  629. require(executor.executeScan({down_program}, repository).succeeded,
  630. "CTD falling preparation scan must succeed");
  631. writeBit(repository, 3, true);
  632. require(executor.executeScan({down_program}, repository).succeeded,
  633. "CTD rising edge must scan successfully");
  634. require(readWord(repository, 1) == expected,
  635. "CTD must decrement exactly once for each rising edge");
  636. }
  637. require(readBit(repository, 5),
  638. "CTD completion contact must turn on when CV reaches zero");
  639. }
  640. void testMoveAndSaturatingArithmetic()
  641. {
  642. VirtualRegisterRepository repository;
  643. SoftwareLogicExecutor executor;
  644. const ControlLogic program = logic({
  645. rung(
  646. "move-rung",
  647. {{contact("execute", 0)}},
  648. move("move", constantOperand(7), 10)),
  649. rung(
  650. "add-rung",
  651. {{contact("execute-add", 0)}},
  652. arithmetic(
  653. "add",
  654. ArithmeticOperation::Add,
  655. registerOperand(10),
  656. constantOperand(32767),
  657. 11)),
  658. rung(
  659. "sub-rung",
  660. {{contact("execute-sub", 0)}},
  661. arithmetic(
  662. "sub",
  663. ArithmeticOperation::Subtract,
  664. constantOperand(-32768),
  665. constantOperand(1),
  666. 12))});
  667. LogicTraceSnapshot trace;
  668. require(executor.executeScan({program}, repository, &trace).succeeded,
  669. "inactive data instructions must scan successfully");
  670. require(readWord(repository, 10) == 0,
  671. "MOVE must not write while its rung is false");
  672. writeBit(repository, 0, true);
  673. require(executor.executeScan({program}, repository, &trace).succeeded,
  674. "active data instructions must scan successfully");
  675. require(readWord(repository, 10) == 7,
  676. "MOVE must copy a constant into the destination D register");
  677. require(readWord(repository, 11) == 32767
  678. && trace.wordValues.at("add").overflow,
  679. "ADD must saturate positive overflow and expose the overflow trace");
  680. require(readWord(repository, 12) == -32768
  681. && trace.wordValues.at("sub").overflow,
  682. "SUB must saturate negative overflow and expose the overflow trace");
  683. const ControlLogic repeated_add = logic({
  684. rung(
  685. "repeated-add-rung",
  686. {{contact("repeated-add-input", 1)}},
  687. arithmetic(
  688. "repeated-add",
  689. ArithmeticOperation::Add,
  690. registerOperand(20),
  691. constantOperand(1),
  692. 20))});
  693. writeBit(repository, 1, true);
  694. require(executor.executeScan({repeated_add}, repository).succeeded
  695. && executor.executeScan({repeated_add}, repository).succeeded,
  696. "ADD must execute on every scan while its rung remains true");
  697. require(readWord(repository, 20) == 2,
  698. "ADD with the same source and destination must accumulate by scan");
  699. }
  700. void testSetResetPairOnSameAddress()
  701. {
  702. VirtualRegisterRepository repository;
  703. SoftwareLogicExecutor executor;
  704. const ControlLogic program = logic({
  705. rung("low-level-rung",
  706. {{comparison("low-level", 0,
  707. ComparisonOperator::LessThanOrEqual, 30)}},
  708. coil("pump-set", 20, CoilMode::Set)),
  709. rung("high-level-rung",
  710. {{comparison("high-level", 0,
  711. ComparisonOperator::GreaterThanOrEqual, 80)}},
  712. coil("pump-reset", 20, CoilMode::Reset))});
  713. writeWord(repository, 0, 20);
  714. require(executor.executeScan({program}, repository).succeeded,
  715. "set/reset pair scan must succeed at the low limit");
  716. require(readBit(repository, 20), "low level must latch the pump on");
  717. writeWord(repository, 0, 50);
  718. require(executor.executeScan({program}, repository).succeeded,
  719. "set/reset pair scan must succeed inside the deadband");
  720. require(readBit(repository, 20), "deadband must retain the latched on state");
  721. writeWord(repository, 0, 90);
  722. require(executor.executeScan({program}, repository).succeeded,
  723. "set/reset pair scan must succeed at the high limit");
  724. require(!readBit(repository, 20), "high level must reset the pump");
  725. writeWord(repository, 0, 50);
  726. require(executor.executeScan({program}, repository).succeeded,
  727. "set/reset pair rescan must succeed inside the deadband");
  728. require(!readBit(repository, 20), "deadband must retain the reset state");
  729. }
  730. void testConflictingCoilsAreRejected()
  731. {
  732. SoftwareLogicExecutor executor;
  733. const ControlLogic normal_and_set = logic({
  734. rung("normal-rung", {{contact("normal-input", 0)}}, coil("normal", 8)),
  735. rung("set-rung", {{contact("set-input", 1)}}, coil("set", 8, CoilMode::Set))});
  736. const LogicScanResult set_result = executor.validate({normal_and_set});
  737. require(!set_result.succeeded
  738. && set_result.error == LogicScanError::ConflictingOutput,
  739. "normal and set coils for one address must be rejected");
  740. require(set_result.rungId == "set-rung" && set_result.nodeId == "set",
  741. "conflict error must identify the offending rung and node");
  742. const ControlLogic reset_and_normal = logic({
  743. rung("reset-rung", {{contact("reset-input", 0)}},
  744. coil("reset", 9, CoilMode::Reset)),
  745. rung("normal-rung", {{contact("normal-input", 1)}}, coil("normal", 9))});
  746. const LogicScanResult reset_result = executor.validate({reset_and_normal});
  747. require(!reset_result.succeeded
  748. && reset_result.error == LogicScanError::ConflictingOutput,
  749. "reset and normal coils for one address must be rejected");
  750. }
  751. void testHmiSimulationClosedLoop()
  752. {
  753. VirtualRegisterRepository repository;
  754. HmiRuntimeService hmi(repository);
  755. SoftwareLogicExecutor executor;
  756. HmiControl start;
  757. start.id = "start-button";
  758. start.type = HmiControlType::Button;
  759. start.bounds = {0, 0, 80, 30};
  760. start.text = "start";
  761. start.binding = RegisterAddress{RegisterArea::M, 0};
  762. start.buttonOperation = HmiButtonOperation::MomentaryOn;
  763. HmiControl indicator;
  764. indicator.id = "run-indicator";
  765. indicator.type = HmiControlType::Indicator;
  766. indicator.bounds = {0, 40, 80, 30};
  767. indicator.text = "run";
  768. indicator.binding = RegisterAddress{RegisterArea::M, 2};
  769. const ControlLogic program = logic({
  770. rung("hold-rung",
  771. {{contact("stop", 1, ContactMode::NormallyClosed)},
  772. {contact("start", 0), contact("feedback", 2)}},
  773. coil("run", 2))});
  774. require(hmi.operateButton(start, HmiButtonEvent::Pressed).succeeded,
  775. "HMI start button press must write virtual M");
  776. require(executor.executeScan({program}, repository).succeeded,
  777. "closed-loop scan must succeed");
  778. require(hmi.readControl(indicator).bit_value,
  779. "HMI indicator must observe the logic output");
  780. require(hmi.operateButton(start, HmiButtonEvent::Released).succeeded,
  781. "HMI start button release must write virtual M");
  782. require(executor.executeScan({program}, repository).succeeded, "holding scan must succeed");
  783. require(hmi.readControl(indicator).bit_value,
  784. "feedback contact must hold the output after start turns off");
  785. writeBit(repository, 1, true);
  786. require(executor.executeScan({program}, repository).succeeded, "stop scan must succeed");
  787. require(!hmi.readControl(indicator).bit_value,
  788. "stop contact must clear the HMI run indication");
  789. }
  790. void testSimulationLifecycleSnapshotAndFault()
  791. {
  792. VirtualRegisterRepository repository;
  793. OfflineSimulationService simulation(repository);
  794. ControlLogic program = logic({
  795. rung("snapshot-rung", {{contact("input", 0)}}, coil("output", 1))});
  796. writeBit(repository, 0, true);
  797. require(simulation.start({program}).succeeded, "simulation must start");
  798. require(simulation.state() == SimulationState::Running, "simulation must report running");
  799. require(!readBit(repository, 0), "starting a session must clear virtual registers");
  800. require(simulation.scanIntervalMs() == 50, "default scan interval must be 50 ms");
  801. writeBit(repository, 0, true);
  802. program.rungs.front().output = coil("changed-output", 9);
  803. require(simulation.executeOnce().succeeded, "manual lifecycle scan must succeed");
  804. require(readBit(repository, 1), "simulation must use its start-time snapshot");
  805. require(simulation.successfulScanCount() == 1, "successful scans must be counted");
  806. simulation.stop();
  807. require(simulation.state() == SimulationState::Stopped, "simulation must stop");
  808. require(simulation.start({program}).succeeded, "stopped simulation must support restart");
  809. require(!readBit(repository, 1), "new session must clear prior output values");
  810. simulation.stop();
  811. }
  812. class FailingVirtualRegisterRepository final : public VirtualRegisterRepository
  813. {
  814. public:
  815. BitReadResult readBit(const RegisterAddress &) const override
  816. {
  817. return {false, false, RegisterError::Unavailable};
  818. }
  819. };
  820. void testRepositoryFailureEntersFaultState()
  821. {
  822. FailingVirtualRegisterRepository repository;
  823. OfflineSimulationService simulation(repository);
  824. const ControlLogic program = logic({
  825. rung("fault-rung", {{contact("fault-input", 0)}}, coil("fault-output", 1))});
  826. require(simulation.start({program}).succeeded,
  827. "valid logic must pass simulation startup");
  828. const LogicScanResult result = simulation.executeOnce();
  829. require(!result.succeeded && result.error == LogicScanError::RegisterReadFailed,
  830. "repository read failure must fail the scan");
  831. require(simulation.state() == SimulationState::Faulted,
  832. "repository failure must enter the fault state");
  833. require(simulation.lastError().logicId == "logic-1"
  834. && simulation.lastError().rungId == "fault-rung"
  835. && simulation.lastError().nodeId == "fault-input",
  836. "fault feedback must retain logic, rung and node context");
  837. }
  838. } // namespace
  839. int main(int argc, char *argv[])
  840. {
  841. QCoreApplication application(argc, argv);
  842. try
  843. {
  844. testSeriesParallelContactsAndSequentialVisibility();
  845. testNestedSeriesParallelExpression();
  846. testUnconditionalCoil();
  847. testWirePassThroughAndPowerTrace();
  848. testAllComparisons();
  849. testSetResetAndDisabledLogic();
  850. testMultipleLogicScanOrderAndTraceIsolation();
  851. testEdgeContactsAreOneScanPulsesAndAreLogicScoped();
  852. testTonUsesElapsedTimeAndResetsAsNonRetentive();
  853. testMultipleTimersAndNetworkOrder();
  854. testCountersUseRisingEdgesAndExternalDValues();
  855. testMoveAndSaturatingArithmetic();
  856. testSetResetPairOnSameAddress();
  857. testConflictingCoilsAreRejected();
  858. testHmiSimulationClosedLoop();
  859. testSimulationLifecycleSnapshotAndFault();
  860. testRepositoryFailureEntersFaultState();
  861. }
  862. catch (const std::exception &error)
  863. {
  864. std::cerr << "offline simulation service tests failed: " << error.what() << '\n';
  865. return 1;
  866. }
  867. std::cout << "offline simulation service tests passed\n";
  868. return 0;
  869. }