综合平台编程器项目的远程存储
您最多选择25个主题 主题必须以字母或数字开头,可以包含连字符 (-),并且长度不得超过35个字符
 
 
 
 

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