综合平台编程器项目的远程存储
Du kan inte välja fler än 25 ämnen Ämnen måste starta med en bokstav eller siffra, kan innehålla bindestreck ('-') och vara max 35 tecken långa.
 
 
 
 

945 rader
38 KiB

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