综合平台编程器项目的远程存储
Вы не можете выбрать более 25 тем Темы должны начинаться с буквы или цифры, могут содержать дефисы(-) и должны содержать не более 35 символов.
 
 
 
 

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