| @@ -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); | |||
| @@ -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<std::int16_t> &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<MonitorValue> 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<MonitorValue> RegisterMonitorService::values(bool communication_faul | |||
| } | |||
| else | |||
| { | |||
| const WordsReadResult read = repository_.readWords( | |||
| const WordsReadResult read = accessRepository().readWords( | |||
| address, registerDataTypeWordCount(point.dataType)); | |||
| const std::optional<RegisterNumericValue> decoded = read.succeeded | |||
| ? decodeRegisterNumericValue(point.dataType, read.values) | |||
| @@ -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<RegisterWordRange> &)> 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<MonitorPoint> &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_; // 当前会话的去重监控地址列表 | |||
| @@ -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) | |||
| @@ -1149,6 +1149,14 @@ void MainWindow::configureRuntimeMonitor() | |||
| } | |||
| }); | |||
| runtime_panel_controller_->configure(); | |||
| runtime_panel_controller_->configureUserRuntimeMode( | |||
| false, | |||
| [this](int mode) | |||
| { | |||
| requestMode(static_cast<ApplicationMode>(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) | |||
| { | |||
| @@ -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<int>(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( | |||
| @@ -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<MonitorValue> 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<std::int16_t>(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<std::int16_t>(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(); | |||