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

645 строки
27 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 ton(const std::string &id, int timer_index, int preset_ms)
  40. {
  41. return {id, TonNodeConfig{TimerAddress{timer_index}, preset_ms}, true};
  42. }
  43. LogicNode comparison(const std::string &id, int address,
  44. ComparisonOperator operation, std::int16_t value)
  45. {
  46. return {id,
  47. CompareNodeConfig{RegisterAddress{RegisterArea::D, address}, operation, value},
  48. true};
  49. }
  50. LogicNode coil(const std::string &id, int address,
  51. CoilMode mode = CoilMode::Normal)
  52. {
  53. return {id, CoilNodeConfig{RegisterAddress{RegisterArea::M, address}, mode}, true};
  54. }
  55. LadderRung rung(const std::string &id,
  56. const std::vector<std::vector<LogicNode>> &stages,
  57. const LogicNode &output)
  58. {
  59. LadderRung result;
  60. result.id = id;
  61. result.name = id;
  62. std::vector<ConditionExpression> series_children;
  63. for (std::size_t index = 0; index < stages.size(); ++index)
  64. {
  65. std::vector<ConditionExpression> parallel_children;
  66. for (const LogicNode &node : stages[index])
  67. {
  68. parallel_children.push_back(ConditionExpression::fromNode(node));
  69. }
  70. if (parallel_children.size() == 1U)
  71. {
  72. series_children.push_back(std::move(parallel_children.front()));
  73. }
  74. else
  75. {
  76. ConditionExpression parallel;
  77. parallel.id = id + "-parallel-" + std::to_string(index);
  78. parallel.kind = ConditionExpressionKind::Parallel;
  79. parallel.children = std::move(parallel_children);
  80. series_children.push_back(std::move(parallel));
  81. }
  82. }
  83. if (series_children.size() == 1U)
  84. {
  85. result.condition = std::move(series_children.front());
  86. }
  87. else
  88. {
  89. ConditionExpression series;
  90. series.id = id + "-series";
  91. series.kind = ConditionExpressionKind::Series;
  92. series.children = std::move(series_children);
  93. result.condition = std::move(series);
  94. }
  95. result.output = output;
  96. return result;
  97. }
  98. ControlLogic logic(const std::vector<LadderRung> &rungs)
  99. {
  100. return {"logic-1", "logic-1", rungs, true};
  101. }
  102. bool readBit(RegisterRepository &repository, int address)
  103. {
  104. const BitReadResult result = repository.readBit(
  105. RegisterAddress{RegisterArea::M, address});
  106. require(result.succeeded, "test register read must succeed");
  107. return result.value;
  108. }
  109. void writeBit(RegisterRepository &repository, int address, bool value)
  110. {
  111. require(repository.writeBit(RegisterAddress{RegisterArea::M, address}, value).succeeded,
  112. "test bit write must succeed");
  113. }
  114. void writeWord(RegisterRepository &repository, int address, std::int16_t value)
  115. {
  116. require(repository.writeWord(RegisterAddress{RegisterArea::D, address}, value).succeeded,
  117. "test word write must succeed");
  118. }
  119. void testNestedSeriesParallelExpression()
  120. {
  121. VirtualRegisterRepository repository;
  122. SoftwareLogicExecutor executor;
  123. ConditionExpression nested_series;
  124. nested_series.id = "nested-series";
  125. nested_series.kind = ConditionExpressionKind::Series;
  126. nested_series.children = {
  127. ConditionExpression::fromNode(contact("b", 1)),
  128. ConditionExpression::fromNode(contact("c", 2))};
  129. ConditionExpression root;
  130. root.id = "root-parallel";
  131. root.kind = ConditionExpressionKind::Parallel;
  132. root.children = {
  133. ConditionExpression::fromNode(contact("a", 0)),
  134. nested_series};
  135. LadderRung nested_rung;
  136. nested_rung.id = "nested-rung";
  137. nested_rung.name = "nested-rung";
  138. nested_rung.condition = root;
  139. nested_rung.output = coil("nested-output", 10);
  140. const ControlLogic program = logic({nested_rung});
  141. writeBit(repository, 1, true);
  142. writeBit(repository, 2, true);
  143. LogicTraceSnapshot trace;
  144. require(executor.executeScan({program}, repository, &trace).succeeded,
  145. "nested expression scan must succeed");
  146. require(readBit(repository, 10), "B AND C branch must energize A OR (B AND C)");
  147. require(trace.expressionValues.at("nested-series")
  148. && trace.expressionValues.at("root-parallel")
  149. && trace.rungValues.at("nested-rung"),
  150. "scan trace must expose active nested expression and rung values");
  151. writeBit(repository, 2, false);
  152. require(executor.executeScan({program}, repository, &trace).succeeded,
  153. "nested false scan must succeed");
  154. require(!readBit(repository, 10), "incomplete B AND C branch must be false");
  155. writeBit(repository, 0, true);
  156. require(executor.executeScan({program}, repository, &trace).succeeded,
  157. "alternate branch scan must succeed");
  158. require(readBit(repository, 10), "A branch must independently energize output");
  159. }
  160. void testSeriesParallelContactsAndSequentialVisibility()
  161. {
  162. VirtualRegisterRepository repository;
  163. SoftwareLogicExecutor executor;
  164. const ControlLogic program = logic({
  165. rung("rung-1",
  166. {{contact("start", 0), contact("alternate", 1)},
  167. {contact("stop", 2, ContactMode::NormallyClosed)}},
  168. coil("run", 3)),
  169. rung("rung-2", {{contact("run-feedback", 3)}}, coil("downstream", 4))});
  170. writeBit(repository, 1, true);
  171. require(executor.executeScan({program}, repository).succeeded,
  172. "parallel and series scan must succeed");
  173. require(readBit(repository, 3), "parallel OR and series AND must energize output");
  174. require(readBit(repository, 4),
  175. "a later rung must see an earlier rung write in the same scan");
  176. writeBit(repository, 2, true);
  177. require(executor.executeScan({program}, repository).succeeded,
  178. "normally closed scan must succeed");
  179. require(!readBit(repository, 3), "normally closed stop contact must open the rung");
  180. require(!readBit(repository, 4), "downstream normal coil must follow the new value");
  181. }
  182. void testAllComparisons()
  183. {
  184. const std::array<ComparisonOperator, 6> operations{
  185. ComparisonOperator::Equal, ComparisonOperator::NotEqual,
  186. ComparisonOperator::LessThan, ComparisonOperator::LessThanOrEqual,
  187. ComparisonOperator::GreaterThan, ComparisonOperator::GreaterThanOrEqual};
  188. const std::array<std::int16_t, 6> actual_values{10, 9, 9, 10, 11, 10};
  189. for (std::size_t index = 0; index < operations.size(); ++index)
  190. {
  191. VirtualRegisterRepository repository;
  192. SoftwareLogicExecutor executor;
  193. writeWord(repository, 0, actual_values[index]);
  194. const ControlLogic program = logic({
  195. rung("compare-rung", {{comparison("compare", 0, operations[index], 10)},
  196. }, coil("result", 10))});
  197. require(executor.executeScan({program}, repository).succeeded,
  198. "comparison scan must succeed");
  199. require(readBit(repository, 10), "comparison operator must evaluate true");
  200. }
  201. }
  202. void testSetResetAndDisabledLogic()
  203. {
  204. VirtualRegisterRepository repository;
  205. SoftwareLogicExecutor executor;
  206. ControlLogic program = logic({
  207. rung("set-rung", {{contact("set-input", 0)}}, coil("set-output", 5, CoilMode::Set)),
  208. rung("reset-rung", {{contact("reset-input", 1)}},
  209. coil("reset-output", 6, CoilMode::Reset))});
  210. writeBit(repository, 0, true);
  211. require(executor.executeScan({program}, repository).succeeded, "set scan must succeed");
  212. require(readBit(repository, 5), "set coil must latch true");
  213. writeBit(repository, 0, false);
  214. require(executor.executeScan({program}, repository).succeeded,
  215. "inactive set scan must succeed");
  216. require(readBit(repository, 5), "inactive set coil must retain its value");
  217. writeBit(repository, 1, true);
  218. writeBit(repository, 6, true);
  219. require(executor.executeScan({program}, repository).succeeded,
  220. "reset scan must succeed");
  221. require(!readBit(repository, 6), "reset coil must write false");
  222. program.enabled = false;
  223. writeBit(repository, 5, true);
  224. require(executor.executeScan({program}, repository).succeeded,
  225. "disabled logic scan must be ignored successfully");
  226. require(readBit(repository, 5), "disabled logic must not change outputs");
  227. }
  228. void testMultipleLogicScanOrderAndTraceIsolation()
  229. {
  230. VirtualRegisterRepository repository;
  231. SoftwareLogicExecutor executor;
  232. ControlLogic first = logic({
  233. rung("rung-1", {{contact("input", 0)}}, coil("output", 1))});
  234. first.id = "logic-first";
  235. first.name = "First";
  236. ControlLogic second = logic({
  237. rung("rung-1", {{contact("input", 1)}}, coil("output", 2))});
  238. second.id = "logic-second";
  239. second.name = "Second";
  240. writeBit(repository, 0, true);
  241. LogicTraceSnapshot trace;
  242. require(executor.executeScan({first, second}, repository, &trace).succeeded,
  243. "all enabled logic modules must execute in project order");
  244. require(readBit(repository, 1) && readBit(repository, 2),
  245. "a later logic module must observe an earlier module write in one scan");
  246. require(trace.logicValues.size() == 2U
  247. && trace.forLogic(first.id).rungValues.at("rung-1")
  248. && trace.forLogic(second.id).rungValues.at("rung-1"),
  249. "runtime traces must be partitioned by logic id when node ids repeat");
  250. ControlLogic disabled_draft;
  251. disabled_draft.id = "logic-draft";
  252. disabled_draft.name = "Draft";
  253. disabled_draft.enabled = false;
  254. disabled_draft.rungs.push_back(
  255. {"rung-1", "Draft", {}, std::nullopt, std::nullopt});
  256. require(executor.validate({first, disabled_draft}).succeeded,
  257. "a disabled incomplete logic module must not block offline execution");
  258. }
  259. void testEdgeContactsAreOneScanPulsesAndAreLogicScoped()
  260. {
  261. VirtualRegisterRepository repository;
  262. SoftwareLogicExecutor executor;
  263. const ControlLogic program = logic({
  264. rung("rising-rung", {{edgeContact("rising", 0, EdgeMode::Rising)}},
  265. coil("rising-output", 10)),
  266. rung("falling-rung", {{edgeContact("falling", 1, EdgeMode::Falling)}},
  267. coil("falling-output", 11))});
  268. writeBit(repository, 0, true);
  269. require(executor.executeScan({program}, repository).succeeded,
  270. "the first high scan must evaluate rising edge input");
  271. require(readBit(repository, 10), "rising edge must pulse on OFF to ON");
  272. require(!readBit(repository, 11), "falling edge must stay off without ON to OFF");
  273. require(executor.executeScan({program}, repository).succeeded,
  274. "a repeated high scan must succeed");
  275. require(!readBit(repository, 10), "rising edge must clear after one scan");
  276. writeBit(repository, 1, true);
  277. require(executor.executeScan({program}, repository).succeeded,
  278. "the falling edge input must establish its previous high state");
  279. writeBit(repository, 1, false);
  280. require(executor.executeScan({program}, repository).succeeded,
  281. "the first low scan after high must succeed");
  282. require(readBit(repository, 11), "falling edge must pulse on ON to OFF");
  283. require(executor.executeScan({program}, repository).succeeded,
  284. "a repeated low scan must succeed");
  285. require(!readBit(repository, 11), "falling edge must clear after one scan");
  286. ControlLogic first = logic({
  287. rung("shared-rung-first", {{edgeContact("shared-edge", 2, EdgeMode::Rising)}},
  288. coil("shared-output-first", 20))});
  289. first.id = "logic-first";
  290. first.name = "First";
  291. ControlLogic second = logic({
  292. rung("shared-rung-second", {{edgeContact("shared-edge", 3, EdgeMode::Rising)}},
  293. coil("shared-output-second", 21))});
  294. second.id = "logic-second";
  295. second.name = "Second";
  296. writeBit(repository, 2, true);
  297. require(executor.executeScan({first, second}, repository).succeeded,
  298. "logic-scoped edge history must support duplicate node ids");
  299. writeBit(repository, 3, true);
  300. require(executor.executeScan({first, second}, repository).succeeded,
  301. "each duplicate edge node must update its own history");
  302. require(!readBit(repository, 20) && readBit(repository, 21),
  303. "duplicate node ids in different logic modules must not share edge history");
  304. executor.resetRuntime();
  305. require(executor.executeScan({first, second}, repository).succeeded,
  306. "resetting runtime must clear edge history");
  307. require(readBit(repository, 20) && readBit(repository, 21),
  308. "a restarted runtime must treat current ON inputs as fresh rising edges");
  309. }
  310. void testTonUsesElapsedTimeAndResetsAsNonRetentive()
  311. {
  312. VirtualRegisterRepository repository;
  313. SoftwareLogicExecutor executor;
  314. const ControlLogic program = logic({
  315. rung("ton-rung", {{contact("ton-input", 0)}}, ton("ton-0", 0, 100)),
  316. rung("ton-feedback-rung", {{timerContact("timer-done", 0)}},
  317. coil("ton-output", 30))});
  318. const auto start = SoftwareLogicExecutor::TimePoint{};
  319. LogicTraceSnapshot trace;
  320. require(executor.executeScanAt({program}, repository, start, &trace).succeeded,
  321. "an inactive TON must scan successfully");
  322. require(!readBit(repository, 30)
  323. && trace.tonValues.at("ton-0").elapsedMs == 0
  324. && !trace.tonValues.at("ton-0").done,
  325. "an inactive TON must have Q false and ET zero");
  326. writeBit(repository, 0, true);
  327. require(executor.executeScanAt({program}, repository, start, &trace).succeeded,
  328. "the first active TON scan must start timing");
  329. require(!readBit(repository, 30) && trace.tonValues.at("ton-0").elapsedMs == 0,
  330. "TON must not complete on its starting scan");
  331. require(executor.executeScanAt(
  332. {program}, repository,
  333. start + std::chrono::milliseconds{99}, &trace)
  334. .succeeded,
  335. "TON must use elapsed monotonic time before the preset");
  336. require(!readBit(repository, 30)
  337. && trace.tonValues.at("ton-0").elapsedMs == 99,
  338. "TON must remain false before PT");
  339. require(executor.executeScanAt(
  340. {program}, repository,
  341. start + std::chrono::milliseconds{100}, &trace)
  342. .succeeded,
  343. "TON must complete at its preset time");
  344. require(readBit(repository, 30)
  345. && trace.tonValues.at("ton-0").done,
  346. "the T contact must observe TON Q in a later network of the same scan");
  347. writeBit(repository, 0, false);
  348. require(executor.executeScanAt(
  349. {program}, repository,
  350. start + std::chrono::milliseconds{150}, &trace)
  351. .succeeded,
  352. "disconnecting TON input must scan successfully");
  353. require(!readBit(repository, 30)
  354. && trace.tonValues.at("ton-0").elapsedMs == 0
  355. && !trace.tonValues.at("ton-0").done,
  356. "a non-retentive TON must reset ET and Q when input opens");
  357. writeBit(repository, 0, true);
  358. require(executor.executeScanAt(
  359. {program}, repository,
  360. start + std::chrono::milliseconds{200}, &trace)
  361. .succeeded,
  362. "TON must restart timing after an input reset");
  363. require(!readBit(repository, 30), "TON restart must not retain the old done state");
  364. executor.resetRuntime();
  365. require(executor.executeScanAt(
  366. {program}, repository,
  367. start + std::chrono::milliseconds{1000}, &trace)
  368. .succeeded,
  369. "resetRuntime must clear TON state");
  370. require(!readBit(repository, 30),
  371. "a restarted runtime must start a currently active TON from zero");
  372. }
  373. void testMultipleTimersAndNetworkOrder()
  374. {
  375. VirtualRegisterRepository repository;
  376. SoftwareLogicExecutor executor;
  377. const ControlLogic independent = logic({
  378. rung("ton-1-rung", {{contact("input-1", 1)}}, ton("ton-1", 1, 100)),
  379. rung("ton-2-rung", {{contact("input-2", 2)}}, ton("ton-2", 2, 200))});
  380. writeBit(repository, 1, true);
  381. writeBit(repository, 2, true);
  382. const auto start = SoftwareLogicExecutor::TimePoint{};
  383. LogicTraceSnapshot trace;
  384. require(executor.executeScanAt({independent}, repository, start, &trace).succeeded,
  385. "multiple TON resources must start independently");
  386. require(executor.executeScanAt(
  387. {independent}, repository,
  388. start + std::chrono::milliseconds{100}, &trace)
  389. .succeeded,
  390. "the first independent TON must reach PT without completing the second");
  391. require(trace.tonValues.at("ton-1").done
  392. && !trace.tonValues.at("ton-2").done,
  393. "different T resources must retain independent elapsed time");
  394. VirtualRegisterRepository ordered_repository;
  395. SoftwareLogicExecutor ordered_executor;
  396. const ControlLogic ordered = logic({
  397. rung("feedback-before-ton", {{timerContact("early-timer", 4)}},
  398. coil("early-output", 40)),
  399. rung("ton-after-feedback", {{contact("late-input", 4)}}, ton("ton-4", 4, 100))});
  400. writeBit(ordered_repository, 4, true);
  401. require(ordered_executor.executeScanAt(
  402. {ordered}, ordered_repository, start, nullptr)
  403. .succeeded,
  404. "network order test must start TON after its earlier T contact");
  405. require(!readBit(ordered_repository, 40),
  406. "an earlier T contact must see the previous scan state");
  407. require(ordered_executor.executeScanAt(
  408. {ordered}, ordered_repository,
  409. start + std::chrono::milliseconds{100}, nullptr)
  410. .succeeded,
  411. "network order test must complete TON on the next scan");
  412. require(!readBit(ordered_repository, 40),
  413. "the earlier T contact must remain false on the completion scan");
  414. require(ordered_executor.executeScanAt(
  415. {ordered}, ordered_repository,
  416. start + std::chrono::milliseconds{101}, nullptr)
  417. .succeeded,
  418. "network order test must expose TON Q on the following scan");
  419. require(readBit(ordered_repository, 40),
  420. "a later scan must observe the completed TON through T contact");
  421. }
  422. void testSetResetPairOnSameAddress()
  423. {
  424. VirtualRegisterRepository repository;
  425. SoftwareLogicExecutor executor;
  426. const ControlLogic program = logic({
  427. rung("low-level-rung",
  428. {{comparison("low-level", 0,
  429. ComparisonOperator::LessThanOrEqual, 30)}},
  430. coil("pump-set", 20, CoilMode::Set)),
  431. rung("high-level-rung",
  432. {{comparison("high-level", 0,
  433. ComparisonOperator::GreaterThanOrEqual, 80)}},
  434. coil("pump-reset", 20, CoilMode::Reset))});
  435. writeWord(repository, 0, 20);
  436. require(executor.executeScan({program}, repository).succeeded,
  437. "set/reset pair scan must succeed at the low limit");
  438. require(readBit(repository, 20), "low level must latch the pump on");
  439. writeWord(repository, 0, 50);
  440. require(executor.executeScan({program}, repository).succeeded,
  441. "set/reset pair scan must succeed inside the deadband");
  442. require(readBit(repository, 20), "deadband must retain the latched on state");
  443. writeWord(repository, 0, 90);
  444. require(executor.executeScan({program}, repository).succeeded,
  445. "set/reset pair scan must succeed at the high limit");
  446. require(!readBit(repository, 20), "high level must reset the pump");
  447. writeWord(repository, 0, 50);
  448. require(executor.executeScan({program}, repository).succeeded,
  449. "set/reset pair rescan must succeed inside the deadband");
  450. require(!readBit(repository, 20), "deadband must retain the reset state");
  451. }
  452. void testConflictingCoilsAreRejected()
  453. {
  454. SoftwareLogicExecutor executor;
  455. const ControlLogic normal_and_set = logic({
  456. rung("normal-rung", {{contact("normal-input", 0)}}, coil("normal", 8)),
  457. rung("set-rung", {{contact("set-input", 1)}}, coil("set", 8, CoilMode::Set))});
  458. const LogicScanResult set_result = executor.validate({normal_and_set});
  459. require(!set_result.succeeded
  460. && set_result.error == LogicScanError::ConflictingOutput,
  461. "normal and set coils for one address must be rejected");
  462. require(set_result.rungId == "set-rung" && set_result.nodeId == "set",
  463. "conflict error must identify the offending rung and node");
  464. const ControlLogic reset_and_normal = logic({
  465. rung("reset-rung", {{contact("reset-input", 0)}},
  466. coil("reset", 9, CoilMode::Reset)),
  467. rung("normal-rung", {{contact("normal-input", 1)}}, coil("normal", 9))});
  468. const LogicScanResult reset_result = executor.validate({reset_and_normal});
  469. require(!reset_result.succeeded
  470. && reset_result.error == LogicScanError::ConflictingOutput,
  471. "reset and normal coils for one address must be rejected");
  472. }
  473. void testHmiSimulationClosedLoop()
  474. {
  475. VirtualRegisterRepository repository;
  476. HmiRuntimeService hmi(repository);
  477. SoftwareLogicExecutor executor;
  478. HmiControl start;
  479. start.id = "start-button";
  480. start.type = HmiControlType::Button;
  481. start.bounds = {0, 0, 80, 30};
  482. start.text = "start";
  483. start.binding = RegisterAddress{RegisterArea::M, 0};
  484. start.buttonOperation = HmiButtonOperation::MomentaryOn;
  485. HmiControl indicator;
  486. indicator.id = "run-indicator";
  487. indicator.type = HmiControlType::Indicator;
  488. indicator.bounds = {0, 40, 80, 30};
  489. indicator.text = "run";
  490. indicator.binding = RegisterAddress{RegisterArea::M, 2};
  491. const ControlLogic program = logic({
  492. rung("hold-rung",
  493. {{contact("stop", 1, ContactMode::NormallyClosed)},
  494. {contact("start", 0), contact("feedback", 2)}},
  495. coil("run", 2))});
  496. require(hmi.operateButton(start, HmiButtonEvent::Pressed).succeeded,
  497. "HMI start button press must write virtual M");
  498. require(executor.executeScan({program}, repository).succeeded,
  499. "closed-loop scan must succeed");
  500. require(hmi.readControl(indicator).bit_value,
  501. "HMI indicator must observe the logic output");
  502. require(hmi.operateButton(start, HmiButtonEvent::Released).succeeded,
  503. "HMI start button release must write virtual M");
  504. require(executor.executeScan({program}, repository).succeeded, "holding scan must succeed");
  505. require(hmi.readControl(indicator).bit_value,
  506. "feedback contact must hold the output after start turns off");
  507. writeBit(repository, 1, true);
  508. require(executor.executeScan({program}, repository).succeeded, "stop scan must succeed");
  509. require(!hmi.readControl(indicator).bit_value,
  510. "stop contact must clear the HMI run indication");
  511. }
  512. void testSimulationLifecycleSnapshotAndFault()
  513. {
  514. VirtualRegisterRepository repository;
  515. OfflineSimulationService simulation(repository);
  516. ControlLogic program = logic({
  517. rung("snapshot-rung", {{contact("input", 0)}}, coil("output", 1))});
  518. writeBit(repository, 0, true);
  519. require(simulation.start({program}).succeeded, "simulation must start");
  520. require(simulation.state() == SimulationState::Running, "simulation must report running");
  521. require(!readBit(repository, 0), "starting a session must clear virtual registers");
  522. require(simulation.scanIntervalMs() == 50, "default scan interval must be 50 ms");
  523. writeBit(repository, 0, true);
  524. program.rungs.front().output = coil("changed-output", 9);
  525. require(simulation.executeOnce().succeeded, "manual lifecycle scan must succeed");
  526. require(readBit(repository, 1), "simulation must use its start-time snapshot");
  527. require(simulation.successfulScanCount() == 1, "successful scans must be counted");
  528. simulation.stop();
  529. require(simulation.state() == SimulationState::Stopped, "simulation must stop");
  530. require(simulation.start({program}).succeeded, "stopped simulation must support restart");
  531. require(!readBit(repository, 1), "new session must clear prior output values");
  532. simulation.stop();
  533. }
  534. class FailingVirtualRegisterRepository final : public VirtualRegisterRepository
  535. {
  536. public:
  537. BitReadResult readBit(const RegisterAddress &) const override
  538. {
  539. return {false, false, RegisterError::Unavailable};
  540. }
  541. };
  542. void testRepositoryFailureEntersFaultState()
  543. {
  544. FailingVirtualRegisterRepository repository;
  545. OfflineSimulationService simulation(repository);
  546. const ControlLogic program = logic({
  547. rung("fault-rung", {{contact("fault-input", 0)}}, coil("fault-output", 1))});
  548. require(simulation.start({program}).succeeded,
  549. "valid logic must pass simulation startup");
  550. const LogicScanResult result = simulation.executeOnce();
  551. require(!result.succeeded && result.error == LogicScanError::RegisterReadFailed,
  552. "repository read failure must fail the scan");
  553. require(simulation.state() == SimulationState::Faulted,
  554. "repository failure must enter the fault state");
  555. require(simulation.lastError().logicId == "logic-1"
  556. && simulation.lastError().rungId == "fault-rung"
  557. && simulation.lastError().nodeId == "fault-input",
  558. "fault feedback must retain logic, rung and node context");
  559. }
  560. } // namespace
  561. int main(int argc, char *argv[])
  562. {
  563. QCoreApplication application(argc, argv);
  564. try
  565. {
  566. testSeriesParallelContactsAndSequentialVisibility();
  567. testNestedSeriesParallelExpression();
  568. testAllComparisons();
  569. testSetResetAndDisabledLogic();
  570. testMultipleLogicScanOrderAndTraceIsolation();
  571. testEdgeContactsAreOneScanPulsesAndAreLogicScoped();
  572. testTonUsesElapsedTimeAndResetsAsNonRetentive();
  573. testMultipleTimersAndNetworkOrder();
  574. testSetResetPairOnSameAddress();
  575. testConflictingCoilsAreRejected();
  576. testHmiSimulationClosedLoop();
  577. testSimulationLifecycleSnapshotAndFault();
  578. testRepositoryFailureEntersFaultState();
  579. }
  580. catch (const std::exception &error)
  581. {
  582. std::cerr << "offline simulation service tests failed: " << error.what() << '\n';
  583. return 1;
  584. }
  585. std::cout << "offline simulation service tests passed\n";
  586. return 0;
  587. }