diff --git a/app/src/ui/main_window.cpp b/app/src/ui/main_window.cpp index 73aac9a..234c61a 100644 --- a/app/src/ui/main_window.cpp +++ b/app/src/ui/main_window.cpp @@ -176,13 +176,7 @@ MainWindow::MainWindow( configureHmiEditor(); configureLogicEditor(); runtime_mode_service_.setPlcStatusChangedCallback( - [this] - { - QMetaObject::invokeMethod( - this, - [this] { updateModeUi(plcStatusText(runtime_mode_service_)); }, - Qt::QueuedConnection); - }); + [this] { schedulePlcStatusUpdate(); }); hmi_editor_service_.ensureDefaultPage(); logic_editor_service_.ensureDefaultLogic(); refreshProjectUi(); @@ -581,7 +575,23 @@ void MainWindow::connectPlc() void MainWindow::disconnectPlc() { runtime_mode_service_.disconnectPlc(); - updateModeUi(tr("PLC 已断开")); +} + +void MainWindow::schedulePlcStatusUpdate() +{ + if (plc_status_update_pending_) + { + return; + } + plc_status_update_pending_ = true; + QMetaObject::invokeMethod( + this, + [this] + { + plc_status_update_pending_ = false; + updateModeUi(plcStatusText(runtime_mode_service_)); + }, + Qt::QueuedConnection); } // 根据控件ID加载控件属性到右侧属性面板 @@ -1126,8 +1136,13 @@ void MainWindow::updateModeUi(const QString &message) } updateSimulationUi(false); statusBar()->showMessage(message, 4000); - ui_->outputList->addItem(message); - ui_->outputList->scrollToBottom(); + const bool duplicate = ui_->outputList->count() > 0 + && ui_->outputList->item(ui_->outputList->count() - 1)->text() == message; + if (!duplicate) + { + ui_->outputList->addItem(message); + ui_->outputList->scrollToBottom(); + } } void MainWindow::updateSimulationUi(bool report_fault) diff --git a/app/src/ui/main_window.h b/app/src/ui/main_window.h index 3d86cb5..de994af 100644 --- a/app/src/ui/main_window.h +++ b/app/src/ui/main_window.h @@ -101,6 +101,8 @@ private: void applySelectedLogicNodeProperties(); void connectPlc(); void disconnectPlc(); + // 合并同一事件循环内的 PLC 状态通知,避免重复刷新和重复日志 + void schedulePlcStatusUpdate(); // 创建新的工程并刷新编辑界面 void createNewProject(); // 保存当前工程到已关联的路径 @@ -155,4 +157,5 @@ private: PlcSerialConfiguration plc_configuration_; std::string selected_control_id_; std::string selected_logic_node_id_; + bool plc_status_update_pending_ = false; }; diff --git a/app/tests/main_window_tests.cpp b/app/tests/main_window_tests.cpp index 08aa837..a5b960d 100644 --- a/app/tests/main_window_tests.cpp +++ b/app/tests/main_window_tests.cpp @@ -1,3 +1,4 @@ +#include "domain/active_register_repository.h" #include "domain/project_storage.h" #include "domain/register_repository.h" #include "services/hmi_editor_service.h" @@ -18,6 +19,7 @@ #include #include #include +#include #include #include @@ -42,6 +44,60 @@ public: } }; +class RepeatedStatusGateway final : public PlcCommunicationGateway +{ +public: + PlcCommunicationResult connectDevice( + const PlcSerialConfiguration &) override + { + connection_state = PlcConnectionState::Connecting; + notifyStateChanged(); + connection_state = PlcConnectionState::Connected; + notifyStateChanged(); + notifyStateChanged(); + return {true, {}}; + } + + void disconnectDevice() override + { + connection_state = PlcConnectionState::Disconnected; + notifyStateChanged(); + } + + void setPollAddresses(const std::vector &) override {} + PlcConnectionState state() const override { return connection_state; } + bool initialReadCompleted() const override { return false; } + const std::string &lastError() const override { return last_error; } + + void setCallbacks( + std::function state_callback, + std::function initial_callback, + std::function cache_callback, + std::function error_callback) override + { + state_changed = std::move(state_callback); + initial_read_changed = std::move(initial_callback); + cache_updated = std::move(cache_callback); + error_reported = std::move(error_callback); + } + +private: + void notifyStateChanged() + { + if (state_changed) + { + state_changed(); + } + } + + PlcConnectionState connection_state = PlcConnectionState::Disconnected; + std::string last_error; + std::function state_changed; + std::function initial_read_changed; + std::function cache_updated; + std::function error_reported; +}; + void require(bool condition, const std::string &message) { if (!condition) @@ -273,6 +329,48 @@ void testPlcConfigurationUsesDialog() "PLC configuration action must open the dedicated configuration dialog"); } +void testRepeatedPlcStatusNotificationsAreCoalesced() +{ + TestProjectStorage storage; + ProjectService project_service(storage); + HmiEditorService editor_service(project_service); + LogicEditorService logic_editor_service(project_service); + VirtualRegisterRepository virtual_repository; + VirtualRegisterRepository plc_repository; + ActiveRegisterRepository active_repository(virtual_repository); + HmiRuntimeService runtime_service(active_repository); + OfflineSimulationService simulation_service(virtual_repository); + RuntimeModeService mode_service(project_service, simulation_service); + RepeatedStatusGateway gateway; + mode_service.configurePlc( + gateway, active_repository, virtual_repository, plc_repository); + MainWindow window( + mode_service, + project_service, + editor_service, + logic_editor_service, + runtime_service); + QListWidget *output = requiredChild(window, "outputList"); + const int initial_count = output->count(); + + const PlcCommunicationResult result = mode_service.connectPlc( + {"COM3", 1, 9600, 8, 2, 1, 1000, 2, 200}); + require(result.succeeded, "PLC connection setup must succeed"); + QApplication::processEvents(); + + const QString expected = QStringLiteral("PLC 已连接,正在读取工程使用的 M/D 地址"); + int matching_count = 0; + for (int index = initial_count; index < output->count(); ++index) + { + if (output->item(index)->text() == expected) + { + ++matching_count; + } + } + require(matching_count == 1, + "repeated PLC status callbacks must append one coalesced output message"); +} + } // namespace int main(int argc, char *argv[]) @@ -285,6 +383,7 @@ int main(int argc, char *argv[]) { testModeActionsControlEditingAvailability(); testPlcConfigurationUsesDialog(); + testRepeatedPlcStatusNotificationsAreCoalesced(); } catch (const std::exception &error) {