diff --git a/app/src/main.cpp b/app/src/main.cpp index 5a6be1e..4db6289 100644 --- a/app/src/main.cpp +++ b/app/src/main.cpp @@ -134,6 +134,7 @@ int main(int argc, char *argv[]) RegisterMonitorService register_monitor_service(active_register_repository); register_monitor_service.setOfflineInitialRepository(offline_initial_repository); register_monitor_service.setOfflineInitialCaptureEnabled(true); + register_monitor_service.setEditingPlcRepository(plc_register_repository); // 用虚拟寄存器执行本地梯形图,实现不连接 PLC 的离线仿真 OfflineSimulationService offline_simulation_service( virtual_register_repository, &offline_initial_repository); diff --git a/app/src/services/register_monitor_service.cpp b/app/src/services/register_monitor_service.cpp index e95840c..6eb9e01 100644 --- a/app/src/services/register_monitor_service.cpp +++ b/app/src/services/register_monitor_service.cpp @@ -44,6 +44,44 @@ void RegisterMonitorService::setOfflineInitialCaptureEnabled(bool enabled) capture_offline_initial_values_ = enabled; } +void RegisterMonitorService::setEditingPlcRepository( + RegisterRepository &repository) +{ + editing_plc_repository_ = &repository; +} + +void RegisterMonitorService::setEditingPlcState( + bool connected, bool initial_read_completed) +{ + editing_plc_connected_ = connected; + editing_plc_ready_ = connected && initial_read_completed; +} + +bool RegisterMonitorService::editingPlcConnected() const +{ + return usesEditingPlcRepository(); +} + +bool RegisterMonitorService::editingPlcReady() const +{ + return usesEditingPlcRepository() && editing_plc_ready_; +} + +bool RegisterMonitorService::usesEditingPlcRepository() const +{ + return editing_plc_connected_ && editing_plc_repository_ != nullptr; +} + +RegisterRepository &RegisterMonitorService::accessRepository() +{ + return usesEditingPlcRepository() ? *editing_plc_repository_ : repository_; +} + +const RegisterRepository &RegisterMonitorService::accessRepository() const +{ + return usesEditingPlcRepository() ? *editing_plc_repository_ : repository_; +} + RegisterMonitorResult RegisterMonitorService::addRange( const std::string &start_address, int count) { @@ -212,7 +250,11 @@ RegisterMonitorResult RegisterMonitorService::clear() RegisterMonitorWriteResult RegisterMonitorService::writeBit( const RegisterAddress &address, bool value) { - const RegisterWriteResult result = repository_.writeBit(address, value); + if (usesEditingPlcRepository() && !editing_plc_ready_) + { + return {false, RegisterError::Unavailable, "PLC 尚未完成首次读取"}; + } + const RegisterWriteResult result = accessRepository().writeBit(address, value); const RegisterMonitorWriteResult active_result = result.succeeded ? RegisterMonitorWriteResult{true, RegisterError::None, {}} : RegisterMonitorWriteResult{false, result.error, result.error == RegisterError::AreaMismatch @@ -228,6 +270,10 @@ RegisterMonitorWriteResult RegisterMonitorService::writeBit( RegisterMonitorWriteResult RegisterMonitorService::writeNumeric( const RegisterAddress &address, RegisterDataType data_type, double value) { + if (usesEditingPlcRepository() && !editing_plc_ready_) + { + return {false, RegisterError::Unavailable, "PLC 尚未完成首次读取"}; + } if (address.isValid() && address.area() != RegisterArea::D) { return {false, RegisterError::AreaMismatch, @@ -247,7 +293,7 @@ RegisterMonitorWriteResult RegisterMonitorService::writeNumeric( std::string(registerDataTypeDescriptor(data_type).displayName) + " 写入值无效"}; } - const RegisterWriteResult result = repository_.writeWords(address, *encoded); + const RegisterWriteResult result = accessRepository().writeWords(address, *encoded); const RegisterMonitorWriteResult active_result = result.succeeded ? RegisterMonitorWriteResult{true, RegisterError::None, {}} : RegisterMonitorWriteResult{false, result.error, @@ -265,7 +311,8 @@ RegisterMonitorWriteResult RegisterMonitorService::captureInitialBit( const RegisterAddress &address, bool value, const RegisterMonitorWriteResult &active_result) { - if (!active_result.succeeded || !capture_offline_initial_values_ + if (!active_result.succeeded || usesEditingPlcRepository() + || !capture_offline_initial_values_ || offline_initial_repository_ == nullptr) { return active_result; @@ -279,7 +326,8 @@ RegisterMonitorWriteResult RegisterMonitorService::captureInitialWords( const std::vector &values, const RegisterMonitorWriteResult &active_result) { - if (!active_result.succeeded || !capture_offline_initial_values_ + if (!active_result.succeeded || usesEditingPlcRepository() + || !capture_offline_initial_values_ || offline_initial_repository_ == nullptr) { return active_result; @@ -305,9 +353,15 @@ std::vector RegisterMonitorService::values(bool communication_faul value.dataType = point.dataType; value.endAddress = address.index() + registerDataTypeWordCount(point.dataType) - 1; + if (usesEditingPlcRepository() && !editing_plc_ready_) + { + value.state = MonitorValueState::Unavailable; + result.push_back(value); + continue; + } if (address.area() == RegisterArea::M) { - const BitReadResult read = repository_.readBit(address); + const BitReadResult read = accessRepository().readBit(address); value.bitValue = read.value; value.state = read.succeeded ? (communication_fault ? MonitorValueState::CommunicationFault @@ -316,7 +370,7 @@ std::vector RegisterMonitorService::values(bool communication_faul } else { - const WordsReadResult read = repository_.readWords( + const WordsReadResult read = accessRepository().readWords( address, registerDataTypeWordCount(point.dataType)); const std::optional decoded = read.succeeded ? decodeRegisterNumericValue(point.dataType, read.values) diff --git a/app/src/services/register_monitor_service.h b/app/src/services/register_monitor_service.h index 13efe12..88bfa0f 100644 --- a/app/src/services/register_monitor_service.h +++ b/app/src/services/register_monitor_service.h @@ -41,7 +41,7 @@ struct RegisterMonitorWriteResult /** * @brief 编排自由监控地址列表和寄存器仓库读写 * - * 地址列表和离线初始值只属于当前运行会话,不写入工程文件;仓库由当前运行模式注入 + * 地址列表和离线初始值只属于当前运行会话,不写入工程文件;编辑态连接 PLC 后可直接访问 PLC 缓存 */ class RegisterMonitorService { @@ -54,8 +54,16 @@ public: // 配置编辑态离线初始值仓库;该仓库只保存在当前进程内 void setOfflineInitialRepository(VirtualRegisterRepository &repository); - // 编辑态开启时,监控写入同时更新离线初始值 + // 编辑态使用虚拟仓库时,监控写入同时更新离线初始值 void setOfflineInitialCaptureEnabled(bool enabled); + // 配置编辑态可直接访问的 PLC 缓存仓库 + void setEditingPlcRepository(RegisterRepository &repository); + // 更新编辑态 PLC 连接和首读状态 + void setEditingPlcState(bool connected, bool initial_read_completed); + // 编辑态是否已经连接到 PLC + bool editingPlcConnected() const; + // 编辑态 PLC 是否已完成首轮读取,可执行写入 + bool editingPlcReady() const; /** * @brief 从起始地址连续添加同一区域的多个地址 @@ -118,6 +126,9 @@ public: const std::vector &)> validator); private: + RegisterRepository &accessRepository(); + const RegisterRepository &accessRepository() const; + bool usesEditingPlcRepository() const; RegisterMonitorWriteResult captureInitialBit( const RegisterAddress &address, bool value, const RegisterMonitorWriteResult &active_result); @@ -131,6 +142,9 @@ private: const std::vector &points); RegisterRepository &repository_; // 当前运行模式的寄存器仓库,不由服务拥有 + RegisterRepository *editing_plc_repository_ = nullptr; + bool editing_plc_connected_ = false; + bool editing_plc_ready_ = false; VirtualRegisterRepository *offline_initial_repository_ = nullptr; bool capture_offline_initial_values_ = false; RegisterMonitorModel model_; // 当前会话的去重监控地址列表 diff --git a/app/src/ui/free_monitor_widget.cpp b/app/src/ui/free_monitor_widget.cpp index 03bb091..0fef1d6 100644 --- a/app/src/ui/free_monitor_widget.cpp +++ b/app/src/ui/free_monitor_widget.cpp @@ -106,8 +106,19 @@ void FreeMonitorWidget::refreshValues( bool communication_fault = false; if (mode == ApplicationMode::Editing) { - source = tr("数据源:离线初始值 · 虚拟 M/D"); + if (service_.editingPlcConnected()) + { + source = service_.editingPlcReady() + ? tr("数据源:PLC 缓存") + : tr("数据源:PLC · 正在首读"); + } + else + { + source = tr("数据源:离线初始值 · 虚拟 M/D"); + } values_available = true; + communication_fault = service_.editingPlcConnected() + && !service_.editingPlcReady(); } else if (mode == ApplicationMode::OfflineRunning) { @@ -138,8 +149,11 @@ void FreeMonitorWidget::refreshValues( if (value.state == MonitorValueState::Unavailable) { value_item->setText(QStringLiteral("--")); - state_item->setText(mode == ApplicationMode::OnlineRunning - ? tr("等待读取") : tr("不可用")); + state_item->setText( + mode == ApplicationMode::OnlineRunning + || (mode == ApplicationMode::Editing + && service_.editingPlcConnected()) + ? tr("等待读取") : tr("不可用")); continue; } if (value.address.area() == RegisterArea::M) diff --git a/app/src/ui/main_window.cpp b/app/src/ui/main_window.cpp index 1014d5b..42c628b 100644 --- a/app/src/ui/main_window.cpp +++ b/app/src/ui/main_window.cpp @@ -1149,6 +1149,14 @@ void MainWindow::configureRuntimeMonitor() } }); runtime_panel_controller_->configure(); + runtime_panel_controller_->configureUserRuntimeMode( + false, + [this](int mode) + { + requestMode(static_cast(mode)); + }, + {}, + {}); runtime_monitor_widget_ = runtime_panel_controller_->runtimeMonitorWidget(); } @@ -1214,15 +1222,23 @@ void MainWindow::refreshDataMonitorUi() return; } const ApplicationMode mode = runtime_mode_service_.mode(); + const PlcConnectionState plc_state = runtime_mode_service_.plcConnectionState(); + const bool editing_plc_connected = mode == ApplicationMode::Editing + && plc_state != PlcConnectionState::Disconnected + && plc_state != PlcConnectionState::Faulted; + register_monitor_service_.setEditingPlcState( + editing_plc_connected, + editing_plc_connected && runtime_mode_service_.initialPlcReadCompleted()); const bool online_connected = mode == ApplicationMode::OnlineRunning - && runtime_mode_service_.plcConnectionState() - == PlcConnectionState::Connected; + && plc_state == PlcConnectionState::Connected; data_monitor_widget_->setWriteEnabled( - mode == ApplicationMode::Editing + (mode == ApplicationMode::Editing + && (!editing_plc_connected + || register_monitor_service_.editingPlcReady())) || mode == ApplicationMode::OfflineRunning || online_connected); data_monitor_widget_->refreshValues( - mode, runtime_mode_service_.plcConnectionState()); + mode, plc_state); } void MainWindow::configureProjectTree() @@ -2397,29 +2413,57 @@ bool MainWindow::requestMode(ApplicationMode requested_mode) // UI 动作只提出目标模式,所有前置条件和仓库切换由 RuntimeModeService 决定 // UI 仅转发模式意图,合法性由服务层和领域状态机决定 ModeTransitionResult result; - switch (requested_mode) - { - case ApplicationMode::Editing: - { - result = runtime_mode_service_.enterEditing(); - break; - } - case ApplicationMode::OfflineRunning: - { - result = runtime_mode_service_.enterOfflineRunning(); - break; - } - case ApplicationMode::OnlineRunning: - { - result = runtime_mode_service_.enterOnlineRunning(); - break; + const ApplicationMode current_mode = runtime_mode_service_.mode(); + if ((requested_mode == ApplicationMode::OfflineRunning + || requested_mode == ApplicationMode::OnlineRunning) + && current_mode != ApplicationMode::Editing + && current_mode != requested_mode) + { + // 运行模式切换统一经过编辑态;先检查真机首读,失败时保持原运行态 + if (requested_mode == ApplicationMode::OnlineRunning + && !runtime_mode_service_.initialPlcReadCompleted()) + { + result = {false, ModeTransitionError::InitialPlcReadRequired, {}}; + } + else + { + const ModeTransitionResult editing = runtime_mode_service_.enterEditing(); + if (!editing.succeeded) + { + restoreCurrentModeAction(); + return false; + } + result = requested_mode == ApplicationMode::OfflineRunning + ? runtime_mode_service_.enterOfflineRunning() + : runtime_mode_service_.enterOnlineRunning(); + } } - default: + else { - restoreCurrentModeAction(); - statusBar()->showMessage(tr("不支持的运行模式"), 5000); - return false; - } + switch (requested_mode) + { + case ApplicationMode::Editing: + { + result = runtime_mode_service_.enterEditing(); + break; + } + case ApplicationMode::OfflineRunning: + { + result = runtime_mode_service_.enterOfflineRunning(); + break; + } + case ApplicationMode::OnlineRunning: + { + result = runtime_mode_service_.enterOnlineRunning(); + break; + } + default: + { + restoreCurrentModeAction(); + statusBar()->showMessage(tr("不支持的运行模式"), 5000); + return false; + } + } } const bool runtime_request = requested_mode == ApplicationMode::OfflineRunning || requested_mode == ApplicationMode::OnlineRunning; @@ -2437,6 +2481,12 @@ bool MainWindow::requestMode(ApplicationMode requested_mode) if (!result.succeeded) { restoreCurrentModeAction(); + if (runtime_monitor_widget_ != nullptr && runtime_request) + { + runtime_monitor_widget_->setMode( + runtime_mode_service_.mode(), + runtime_mode_service_.plcConnectionState()); + } QString message = transitionErrorText(result.error); if (result.error == ModeTransitionError::InitialPlcReadRequired) { diff --git a/app/src/ui/runtime_monitor_widget.cpp b/app/src/ui/runtime_monitor_widget.cpp index de7b591..713efbc 100644 --- a/app/src/ui/runtime_monitor_widget.cpp +++ b/app/src/ui/runtime_monitor_widget.cpp @@ -104,7 +104,7 @@ RuntimeMonitorWidget::RuntimeMonitorWidget( this, [this](int index) { - if (user_runtime_mode_ && index >= 0) + if (index >= 0) { emit modeChangeRequested(index == 0 ? static_cast(ApplicationMode::OfflineRunning) @@ -177,11 +177,12 @@ void RuntimeMonitorWidget::setMode( offline ? tr("离线仿真 · 虚拟 M/D") : online ? tr("真机运行 · PLC 缓存 · 本地推算轨迹") : tr("当前未运行")); - if (user_runtime_mode_) { const QSignalBlocker blocker(ui_->runtimeModeComboBox); - ui_->runtimeModeComboBox->setCurrentIndex( - offline ? 0 : online ? 1 : -1); + ui_->runtimeModeComboBox->setCurrentIndex(offline ? 0 : online ? 1 : -1); + } + if (user_runtime_mode_) + { const bool plc_configurable = plc_state == PlcConnectionState::Disconnected || plc_state == PlcConnectionState::Faulted; ui_->runtimePlcButton->setEnabled(plc_configurable); @@ -194,7 +195,8 @@ void RuntimeMonitorWidget::setMode( void RuntimeMonitorWidget::setUserRuntimeMode(bool enabled) { user_runtime_mode_ = enabled; - ui_->runtimeModeComboBox->setVisible(enabled); + // 普通编程器和导出的运行程序都允许在运行窗口切换离线/真机 + ui_->runtimeModeComboBox->setVisible(true); ui_->runtimePlcButton->setVisible(enabled); ui_->runtimeDisconnectPlcButton->setVisible(enabled); ui_->exitRuntimeButton->setToolTip( diff --git a/app/tests/register_monitor_service_tests.cpp b/app/tests/register_monitor_service_tests.cpp index e457f72..d46b55b 100644 --- a/app/tests/register_monitor_service_tests.cpp +++ b/app/tests/register_monitor_service_tests.cpp @@ -77,6 +77,55 @@ void testSharedActiveRepositoryValues() "valid cached values must be marked stale during a communication fault"); } +void testEditingPlcMonitorAccess() +{ + VirtualRegisterRepository virtual_repository; + VirtualRegisterRepository plc_repository; + ActiveRegisterRepository active_repository(virtual_repository); + VirtualRegisterRepository initial_repository; + RegisterMonitorService service(active_repository); + service.setOfflineInitialRepository(initial_repository); + service.setOfflineInitialCaptureEnabled(true); + service.setEditingPlcRepository(plc_repository); + require(service.addRange("M0", 1).succeeded + && service.addRange("D0", 1).succeeded, + "editing PLC monitor fixture must accept M and D points"); + + virtual_repository.writeBit({RegisterArea::M, 0}, false); + virtual_repository.writeWord({RegisterArea::D, 0}, 11); + plc_repository.writeBit({RegisterArea::M, 0}, true); + plc_repository.writeWord({RegisterArea::D, 0}, 22); + + service.setEditingPlcState(true, false); + std::vector values = service.values(false); + require(values.size() == 2U + && values[0].state == MonitorValueState::Unavailable + && values[1].state == MonitorValueState::Unavailable + && !service.writeBit({RegisterArea::M, 0}, false).succeeded, + "editing PLC monitor must wait for the first read before access"); + + service.setEditingPlcState(true, true); + values = service.values(false); + require(values[0].state == MonitorValueState::Valid && values[0].bitValue + && values[1].state == MonitorValueState::Valid + && std::get(values[1].numericValue) == 22, + "editing PLC monitor must display PLC cache values after first read"); + require(service.writeBit({RegisterArea::M, 0}, false).succeeded + && service.writeNumeric( + {RegisterArea::D, 0}, RegisterDataType::Int16, 33).succeeded + && !plc_repository.readBit({RegisterArea::M, 0}).value + && plc_repository.readWord({RegisterArea::D, 0}).value == 33 + && !initial_repository.readBit({RegisterArea::M, 0}).value + && initial_repository.readWord({RegisterArea::D, 0}).value == 0, + "editing PLC monitor writes must target PLC and skip offline capture"); + + service.setEditingPlcState(false, false); + values = service.values(false); + require(!values[0].bitValue + && std::get(values[1].numericValue) == 11, + "disconnecting PLC must restore the virtual editing source"); +} + void testRegisterWrites() { VirtualRegisterRepository repository; @@ -264,6 +313,7 @@ int main() { testRangeManagement(); testSharedActiveRepositoryValues(); + testEditingPlcMonitorAccess(); testRegisterWrites(); testOfflineInitialValueCapture(); testMultiWordMonitoring();