综合平台编程器项目的远程存储
Du kannst nicht mehr als 25 Themen auswählen Themen müssen entweder mit einem Buchstaben oder einer Ziffer beginnen. Sie können Bindestriche („-“) enthalten und bis zu 35 Zeichen lang sein.
 
 
 
 

397 Zeilen
16 KiB

  1. #include "domain/active_register_repository.h"
  2. #include "domain/project_storage.h"
  3. #include "domain/register_repository.h"
  4. #include "services/hmi_editor_service.h"
  5. #include "services/hmi_runtime_service.h"
  6. #include "services/logic_editor_service.h"
  7. #include "services/offline_simulation_service.h"
  8. #include "services/project_service.h"
  9. #include "services/runtime_mode_service.h"
  10. #include "ui/hmi_editor_widget.h"
  11. #include "ui/logic_editor_widget.h"
  12. #include "ui/main_window.h"
  13. #include "ui/plc_connection_dialog.h"
  14. #include <QAction>
  15. #include <QApplication>
  16. #include <QComboBox>
  17. #include <QDialog>
  18. #include <QDockWidget>
  19. #include <QLabel>
  20. #include <QLineEdit>
  21. #include <QListWidget>
  22. #include <QSpinBox>
  23. #include <QTimer>
  24. #include <iostream>
  25. #include <stdexcept>
  26. #include <string>
  27. namespace {
  28. class TestProjectStorage final : public ProjectStorage
  29. {
  30. public:
  31. // 主窗口测试使用无副作用存储实现,隔离文件对 UI 行为的影响
  32. ProjectSaveResult save(const Project &, const std::string &) override
  33. {
  34. return {true, ProjectStorageError::None, {}};
  35. }
  36. ProjectLoadResult load(const std::string &) override
  37. {
  38. return {false, {}, ProjectStorageError::FileReadFailed, {}};
  39. }
  40. };
  41. class RepeatedStatusGateway final : public PlcCommunicationGateway
  42. {
  43. public:
  44. PlcCommunicationResult connectDevice(
  45. const PlcSerialConfiguration &) override
  46. {
  47. connection_state = PlcConnectionState::Connecting;
  48. notifyStateChanged();
  49. connection_state = PlcConnectionState::Connected;
  50. notifyStateChanged();
  51. notifyStateChanged();
  52. return {true, {}};
  53. }
  54. void disconnectDevice() override
  55. {
  56. connection_state = PlcConnectionState::Disconnected;
  57. notifyStateChanged();
  58. }
  59. void setPollAddresses(const std::vector<RegisterAddress> &) override {}
  60. PlcConnectionState state() const override { return connection_state; }
  61. bool initialReadCompleted() const override { return false; }
  62. const std::string &lastError() const override { return last_error; }
  63. void setCallbacks(
  64. std::function<void()> state_callback,
  65. std::function<void(bool)> initial_callback,
  66. std::function<void()> cache_callback,
  67. std::function<void(const std::string &)> error_callback) override
  68. {
  69. state_changed = std::move(state_callback);
  70. initial_read_changed = std::move(initial_callback);
  71. cache_updated = std::move(cache_callback);
  72. error_reported = std::move(error_callback);
  73. }
  74. private:
  75. void notifyStateChanged()
  76. {
  77. if (state_changed)
  78. {
  79. state_changed();
  80. }
  81. }
  82. PlcConnectionState connection_state = PlcConnectionState::Disconnected;
  83. std::string last_error;
  84. std::function<void()> state_changed;
  85. std::function<void(bool)> initial_read_changed;
  86. std::function<void()> cache_updated;
  87. std::function<void(const std::string &)> error_reported;
  88. };
  89. void require(bool condition, const std::string &message)
  90. {
  91. if (!condition)
  92. {
  93. throw std::runtime_error(message);
  94. }
  95. }
  96. template<typename ObjectType>
  97. ObjectType *requiredChild(MainWindow &window, const char *name)
  98. {
  99. // 通过 objectName 取得 Designer 组件,缺失时给出明确测试失败信息
  100. ObjectType *child = window.findChild<ObjectType *>(QString::fromLatin1(name));
  101. require(child != nullptr, std::string("missing UI object ") + name);
  102. return child;
  103. }
  104. void testModeActionsControlEditingAvailability()
  105. {
  106. // 验证模式动作会同步禁用编辑入口,并在失败时恢复当前选择
  107. TestProjectStorage storage;
  108. ProjectService project_service(storage);
  109. HmiEditorService editor_service(project_service);
  110. LogicEditorService logic_editor_service(project_service);
  111. VirtualRegisterRepository repository;
  112. HmiRuntimeService runtime_service(repository);
  113. OfflineSimulationService simulation_service(repository);
  114. RuntimeModeService mode_service(project_service, simulation_service);
  115. MainWindow window(
  116. mode_service,
  117. project_service,
  118. editor_service,
  119. logic_editor_service,
  120. runtime_service);
  121. window.resize(1000, 640);
  122. window.show();
  123. QApplication::processEvents();
  124. require(window.findChild<QWidget *>(QStringLiteral("dataPointDock")) == nullptr,
  125. "the removed data point dock must not remain in the main window");
  126. require(window.findChild<QWidget *>(QStringLiteral("hmiDataPointComboBox")) == nullptr
  127. && window.findChild<QWidget *>(QStringLiteral("logicDataPointComboBox")) == nullptr,
  128. "HMI and ladder properties must use direct M/D address inputs");
  129. QAction *editing_action = requiredChild<QAction>(window, "editingModeAction");
  130. QAction *offline_action = requiredChild<QAction>(window, "offlineModeAction");
  131. QAction *online_action = requiredChild<QAction>(window, "onlineModeAction");
  132. QAction *add_button_action = requiredChild<QAction>(window, "addButtonAction");
  133. QAction *delete_control_action = requiredChild<QAction>(window, "deleteControlAction");
  134. QAction *add_normally_open_action = requiredChild<QAction>(
  135. window, "addNormallyOpenAction");
  136. QAction *add_normal_coil_action = requiredChild<QAction>(
  137. window, "addNormalCoilAction");
  138. QAction *parallel_insert_action = requiredChild<QAction>(
  139. window, "parallelInsertAction");
  140. QDockWidget *project_dock = requiredChild<QDockWidget>(window, "projectDock");
  141. QDockWidget *properties_dock = requiredChild<QDockWidget>(window, "propertiesDock");
  142. QLabel *selection = requiredChild<QLabel>(window, "selectionValueLabel");
  143. QLineEdit *text_edit = requiredChild<QLineEdit>(window, "controlTextEdit");
  144. HmiEditorWidget *hmi_editor = requiredChild<HmiEditorWidget>(
  145. window, "hmiEditorWidget");
  146. QLabel *executor_status = requiredChild<QLabel>(window, "executorStatusLabel");
  147. const qreal compact_scale = hmi_editor->transform().m11();
  148. window.resize(1600, 900);
  149. QApplication::processEvents();
  150. require(hmi_editor->transform().m11() > compact_scale,
  151. "HMI page must refit when the window becomes larger");
  152. require(editing_action->isChecked(), "editing action must be selected initially");
  153. require(project_dock->isEnabled(), "project dock must be enabled while editing");
  154. require(properties_dock->isEnabled(), "properties dock must be enabled while editing");
  155. add_button_action->trigger();
  156. const std::string page_id = editor_service.firstPageId();
  157. require(editor_service.findPage(page_id)->controls.size() == 1,
  158. "adding a control must update the HMI page model");
  159. require(selection->text() == QStringLiteral("button-1(未绑定)"),
  160. "an unbound added control must be marked in the property panel");
  161. require(text_edit->text() == QStringLiteral("按钮"),
  162. "property panel must show the control text");
  163. delete_control_action->trigger();
  164. require(editor_service.findPage(page_id)->controls.empty(),
  165. "deleting a selected control must update the HMI page model");
  166. add_normally_open_action->trigger();
  167. parallel_insert_action->trigger();
  168. add_normal_coil_action->trigger();
  169. const ControlLogic *logic = logic_editor_service.findLogic(
  170. logic_editor_service.firstLogicId());
  171. require(logic != nullptr && logic->rungs.size() == 1,
  172. "logic actions must edit the default ladder rung");
  173. require(logic->rungs.front().condition.has_value()
  174. && logic->rungs.front().condition->kind
  175. == ConditionExpressionKind::Parallel
  176. && logic->rungs.front().condition->children.size() == 2U,
  177. "parallel branch action must create a parallel expression");
  178. require(logic->rungs.front().output.has_value(),
  179. "coil action must set the fixed ladder output");
  180. offline_action->trigger();
  181. require(mode_service.mode() == ApplicationMode::Editing,
  182. "unconfigured ladder nodes must block offline running");
  183. const LadderRung &rung = logic->rungs.front();
  184. std::vector<const LogicNode *> condition_nodes;
  185. collectConditionNodes(*rung.condition, &condition_nodes);
  186. for (const LogicNode *node : condition_nodes)
  187. {
  188. require(logic_editor_service.updateNodeConfig(
  189. logic_editor_service.firstLogicId(),
  190. node->id,
  191. ContactNodeConfig{
  192. RegisterAddress{RegisterArea::M, 0},
  193. ContactMode::NormallyOpen})
  194. .succeeded,
  195. "applying a contact configuration must complete the ladder node");
  196. }
  197. require(logic_editor_service.updateNodeConfig(
  198. logic_editor_service.firstLogicId(),
  199. rung.output->id,
  200. CoilNodeConfig{
  201. RegisterAddress{RegisterArea::M, 1},
  202. CoilMode::Normal})
  203. .succeeded,
  204. "applying a coil configuration must complete the ladder node");
  205. offline_action->trigger();
  206. require(mode_service.mode() == ApplicationMode::OfflineRunning,
  207. "offline action must enter offline running");
  208. require(simulation_service.state() == SimulationState::Running,
  209. "offline action must start the actual simulation service");
  210. require(executor_status->text().contains(QStringLiteral("运行")),
  211. "executor status label must report the actual running state");
  212. require(!project_dock->isEnabled(),
  213. "project dock must be disabled while running");
  214. require(!properties_dock->isEnabled(),
  215. "properties dock must be disabled while running");
  216. require(!add_button_action->isEnabled(),
  217. "HMI add controls must be disabled while running");
  218. require(!add_normally_open_action->isEnabled(),
  219. "logic add nodes must be disabled while running");
  220. online_action->trigger();
  221. require(mode_service.mode() == ApplicationMode::OfflineRunning,
  222. "running modes must not switch directly through the UI");
  223. require(offline_action->isChecked(),
  224. "failed mode changes must restore the active action");
  225. editing_action->trigger();
  226. require(mode_service.mode() == ApplicationMode::Editing,
  227. "editing action must return to editing");
  228. require(simulation_service.state() == SimulationState::Stopped,
  229. "editing action must stop the simulation service");
  230. require(executor_status->text() == QStringLiteral("逻辑执行器:停止"),
  231. "executor status label must report the actual stopped state");
  232. require(project_dock->isEnabled(),
  233. "project dock must be restored after returning to editing");
  234. require(properties_dock->isEnabled(),
  235. "properties dock must be restored after returning to editing");
  236. require(add_button_action->isEnabled(),
  237. "HMI add controls must be restored after returning to editing");
  238. require(add_normally_open_action->isEnabled(),
  239. "logic add nodes must be restored after returning to editing");
  240. online_action->trigger();
  241. require(mode_service.mode() == ApplicationMode::Editing,
  242. "online action must require an initial PLC read");
  243. require(editing_action->isChecked(),
  244. "rejected online running must restore the editing action");
  245. }
  246. void testPlcConfigurationUsesDialog()
  247. {
  248. PlcSerialConfiguration initial;
  249. initial.portName = "COM17";
  250. initial.serverAddress = 12;
  251. initial.baudRate = 38400;
  252. initial.dataBits = 7;
  253. initial.parity = 3;
  254. initial.stopBits = 2;
  255. initial.responseTimeoutMs = 2500;
  256. initial.retries = 4;
  257. initial.pollIntervalMs = 350;
  258. PlcConnectionDialog configuration_dialog(initial);
  259. const PlcSerialConfiguration actual = configuration_dialog.configuration();
  260. require(actual.portName == initial.portName,
  261. "PLC dialog must preserve a manually entered serial port");
  262. require(actual.serverAddress == initial.serverAddress
  263. && actual.baudRate == initial.baudRate
  264. && actual.dataBits == initial.dataBits
  265. && actual.parity == initial.parity
  266. && actual.stopBits == initial.stopBits,
  267. "PLC dialog must preserve Modbus RTU serial parameters");
  268. require(actual.responseTimeoutMs == initial.responseTimeoutMs
  269. && actual.retries == initial.retries
  270. && actual.pollIntervalMs == initial.pollIntervalMs,
  271. "PLC dialog must preserve communication timing parameters");
  272. TestProjectStorage storage;
  273. ProjectService project_service(storage);
  274. HmiEditorService editor_service(project_service);
  275. LogicEditorService logic_editor_service(project_service);
  276. VirtualRegisterRepository repository;
  277. HmiRuntimeService runtime_service(repository);
  278. OfflineSimulationService simulation_service(repository);
  279. RuntimeModeService mode_service(project_service, simulation_service);
  280. MainWindow window(
  281. mode_service,
  282. project_service,
  283. editor_service,
  284. logic_editor_service,
  285. runtime_service);
  286. require(window.findChild<QComboBox *>(QStringLiteral("serialPortComboBox")) == nullptr,
  287. "serial configuration controls must not be embedded in the main window");
  288. QAction *configure_action = requiredChild<QAction>(window, "configurePlcAction");
  289. bool dialog_opened = false;
  290. QTimer::singleShot(
  291. 0,
  292. [&dialog_opened]
  293. {
  294. auto *dialog = qobject_cast<PlcConnectionDialog *>(
  295. QApplication::activeModalWidget());
  296. dialog_opened = dialog != nullptr;
  297. if (dialog != nullptr)
  298. {
  299. dialog->reject();
  300. }
  301. });
  302. configure_action->trigger();
  303. require(dialog_opened,
  304. "PLC configuration action must open the dedicated configuration dialog");
  305. }
  306. void testRepeatedPlcStatusNotificationsAreCoalesced()
  307. {
  308. TestProjectStorage storage;
  309. ProjectService project_service(storage);
  310. HmiEditorService editor_service(project_service);
  311. LogicEditorService logic_editor_service(project_service);
  312. VirtualRegisterRepository virtual_repository;
  313. VirtualRegisterRepository plc_repository;
  314. ActiveRegisterRepository active_repository(virtual_repository);
  315. HmiRuntimeService runtime_service(active_repository);
  316. OfflineSimulationService simulation_service(virtual_repository);
  317. RuntimeModeService mode_service(project_service, simulation_service);
  318. RepeatedStatusGateway gateway;
  319. mode_service.configurePlc(
  320. gateway, active_repository, virtual_repository, plc_repository);
  321. MainWindow window(
  322. mode_service,
  323. project_service,
  324. editor_service,
  325. logic_editor_service,
  326. runtime_service);
  327. QListWidget *output = requiredChild<QListWidget>(window, "outputList");
  328. const int initial_count = output->count();
  329. const PlcCommunicationResult result = mode_service.connectPlc(
  330. {"COM3", 1, 9600, 8, 2, 1, 1000, 2, 200});
  331. require(result.succeeded, "PLC connection setup must succeed");
  332. QApplication::processEvents();
  333. const QString expected = QStringLiteral("PLC 已连接,正在读取工程使用的 M/D 地址");
  334. int matching_count = 0;
  335. for (int index = initial_count; index < output->count(); ++index)
  336. {
  337. if (output->item(index)->text() == expected)
  338. {
  339. ++matching_count;
  340. }
  341. }
  342. require(matching_count == 1,
  343. "repeated PLC status callbacks must append one coalesced output message");
  344. }
  345. } // namespace
  346. int main(int argc, char *argv[])
  347. {
  348. // 无窗口平台使 Qt Widgets 测试可在自动化环境稳定运行
  349. qputenv("QT_QPA_PLATFORM", QByteArrayLiteral("offscreen"));
  350. QApplication application(argc, argv);
  351. try
  352. {
  353. testModeActionsControlEditingAvailability();
  354. testPlcConfigurationUsesDialog();
  355. testRepeatedPlcStatusNotificationsAreCoalesced();
  356. }
  357. catch (const std::exception &error)
  358. {
  359. std::cerr << "main window tests failed: " << error.what() << '\n';
  360. return 1;
  361. }
  362. std::cout << "main window tests passed\n";
  363. return 0;
  364. }