diff --git a/app/src/services/register_monitor_service.cpp b/app/src/services/register_monitor_service.cpp index 6eb9e01..1e1bdcc 100644 --- a/app/src/services/register_monitor_service.cpp +++ b/app/src/services/register_monitor_service.cpp @@ -50,6 +50,11 @@ void RegisterMonitorService::setEditingPlcRepository( editing_plc_repository_ = &repository; } +void RegisterMonitorService::setEditingModeActive(bool active) +{ + editing_mode_active_ = active; +} + void RegisterMonitorService::setEditingPlcState( bool connected, bool initial_read_completed) { @@ -57,19 +62,70 @@ void RegisterMonitorService::setEditingPlcState( editing_plc_ready_ = connected && initial_read_completed; } +EditingMonitorSourceResult RegisterMonitorService::selectEditingSource( + EditingMonitorSource source) +{ + if (source == EditingMonitorSource::Offline) + { + editing_source_ = source; + return {true, {}}; + } + if (!editing_mode_active_) + { + return {false, "只有编辑态数据监控可以选择真机 PLC M/D"}; + } + if (editing_plc_repository_ == nullptr) + { + return {false, "PLC 数据源尚未配置,无法切换到真机 M/D"}; + } + if (!editing_plc_connected_) + { + return {false, "PLC 未连接或连接已断开,无法切换到真机 M/D"}; + } + if (!editing_plc_ready_) + { + return {false, "PLC 尚未完成首次读取,请稍后再切换到真机 M/D"}; + } + editing_source_ = source; + return {true, {}}; +} + +EditingMonitorSource RegisterMonitorService::editingSource() const +{ + return editing_source_; +} + bool RegisterMonitorService::editingPlcConnected() const { - return usesEditingPlcRepository(); + return editing_plc_connected_ && editing_plc_repository_ != nullptr; } bool RegisterMonitorService::editingPlcReady() const { - return usesEditingPlcRepository() && editing_plc_ready_; + return editingPlcConnected() && editing_plc_ready_; +} + +bool RegisterMonitorService::editingPlcAccessRequested() const +{ + return editing_mode_active_ && editing_source_ == EditingMonitorSource::Plc; } bool RegisterMonitorService::usesEditingPlcRepository() const { - return editing_plc_connected_ && editing_plc_repository_ != nullptr; + return editingPlcAccessRequested() && editing_plc_repository_ != nullptr; +} + +std::string RegisterMonitorService::editingPlcUnavailableMessage() const +{ + if (editing_plc_repository_ == nullptr) + { + return "PLC 数据源尚未配置"; + } + if (!editing_plc_connected_) + { + return "PLC 未连接或连接已断开"; + } + return "PLC 尚未完成首次读取"; } RegisterRepository &RegisterMonitorService::accessRepository() @@ -250,9 +306,10 @@ RegisterMonitorResult RegisterMonitorService::clear() RegisterMonitorWriteResult RegisterMonitorService::writeBit( const RegisterAddress &address, bool value) { - if (usesEditingPlcRepository() && !editing_plc_ready_) + if (editingPlcAccessRequested() && !editingPlcReady()) { - return {false, RegisterError::Unavailable, "PLC 尚未完成首次读取"}; + return {false, RegisterError::Unavailable, + editingPlcUnavailableMessage()}; } const RegisterWriteResult result = accessRepository().writeBit(address, value); const RegisterMonitorWriteResult active_result = result.succeeded @@ -270,9 +327,10 @@ RegisterMonitorWriteResult RegisterMonitorService::writeBit( RegisterMonitorWriteResult RegisterMonitorService::writeNumeric( const RegisterAddress &address, RegisterDataType data_type, double value) { - if (usesEditingPlcRepository() && !editing_plc_ready_) + if (editingPlcAccessRequested() && !editingPlcReady()) { - return {false, RegisterError::Unavailable, "PLC 尚未完成首次读取"}; + return {false, RegisterError::Unavailable, + editingPlcUnavailableMessage()}; } if (address.isValid() && address.area() != RegisterArea::D) { @@ -353,7 +411,7 @@ std::vector RegisterMonitorService::values(bool communication_faul value.dataType = point.dataType; value.endAddress = address.index() + registerDataTypeWordCount(point.dataType) - 1; - if (usesEditingPlcRepository() && !editing_plc_ready_) + if (editingPlcAccessRequested() && !editingPlcReady()) { value.state = MonitorValueState::Unavailable; result.push_back(value); diff --git a/app/src/services/register_monitor_service.h b/app/src/services/register_monitor_service.h index 88bfa0f..7f84494 100644 --- a/app/src/services/register_monitor_service.h +++ b/app/src/services/register_monitor_service.h @@ -38,10 +38,24 @@ struct RegisterMonitorWriteResult std::string message; // 面向用户的 UTF-8 错误说明 }; +// 编辑态数据监控由用户明确选择离线初始值或真实 PLC +enum class EditingMonitorSource +{ + Offline, + Plc +}; + +// 编辑态数据源切换结果;真机不满足连接和首读条件时拒绝切换 +struct EditingMonitorSourceResult +{ + bool succeeded = false; + std::string message; +}; + /** * @brief 编排自由监控地址列表和寄存器仓库读写 * - * 地址列表和离线初始值只属于当前运行会话,不写入工程文件;编辑态连接 PLC 后可直接访问 PLC 缓存 + * 地址列表和离线初始值只属于当前运行会话,不写入工程文件;编辑态数据源由用户明确选择 */ class RegisterMonitorService { @@ -58,9 +72,15 @@ public: void setOfflineInitialCaptureEnabled(bool enabled); // 配置编辑态可直接访问的 PLC 缓存仓库 void setEditingPlcRepository(RegisterRepository &repository); + // 标记当前是否由编辑态数据监控访问仓库;运行态继续使用活动仓库 + void setEditingModeActive(bool active); // 更新编辑态 PLC 连接和首读状态 void setEditingPlcState(bool connected, bool initial_read_completed); - // 编辑态是否已经连接到 PLC + // 请求切换编辑态数据监控来源;切到真机前必须连接并完成首读 + EditingMonitorSourceResult selectEditingSource(EditingMonitorSource source); + // 返回用户当前选择的数据源;连接故障不会静默改回离线 + EditingMonitorSource editingSource() const; + // 编辑态 PLC 当前是否已经建立连接 bool editingPlcConnected() const; // 编辑态 PLC 是否已完成首轮读取,可执行写入 bool editingPlcReady() const; @@ -128,7 +148,9 @@ public: private: RegisterRepository &accessRepository(); const RegisterRepository &accessRepository() const; + bool editingPlcAccessRequested() const; bool usesEditingPlcRepository() const; + std::string editingPlcUnavailableMessage() const; RegisterMonitorWriteResult captureInitialBit( const RegisterAddress &address, bool value, const RegisterMonitorWriteResult &active_result); @@ -143,6 +165,8 @@ private: RegisterRepository &repository_; // 当前运行模式的寄存器仓库,不由服务拥有 RegisterRepository *editing_plc_repository_ = nullptr; + bool editing_mode_active_ = false; + EditingMonitorSource editing_source_ = EditingMonitorSource::Offline; bool editing_plc_connected_ = false; bool editing_plc_ready_ = false; VirtualRegisterRepository *offline_initial_repository_ = nullptr; diff --git a/app/src/ui/free_monitor_widget.cpp b/app/src/ui/free_monitor_widget.cpp index 0fef1d6..cdf8ba4 100644 --- a/app/src/ui/free_monitor_widget.cpp +++ b/app/src/ui/free_monitor_widget.cpp @@ -12,6 +12,7 @@ #include #include #include +#include #include #include @@ -79,6 +80,10 @@ FreeMonitorWidget::FreeMonitorWidget( ui_->dataTypeComboBox->setItemData(1, static_cast(RegisterDataType::Int32)); ui_->dataTypeComboBox->setItemData(2, static_cast(RegisterDataType::Float32)); ui_->dataTypeComboBox->setItemData(3, static_cast(RegisterDataType::Float64)); + ui_->editingSourceComboBox->setItemData( + 0, static_cast(EditingMonitorSource::Offline)); + ui_->editingSourceComboBox->setItemData( + 1, static_cast(EditingMonitorSource::Plc)); ui_->monitorTable->verticalHeader()->setVisible(false); connect(ui_->addButton, &QPushButton::clicked, this, &FreeMonitorWidget::addAddresses); connect(ui_->addressEdit, &QLineEdit::returnPressed, @@ -87,6 +92,9 @@ FreeMonitorWidget::FreeMonitorWidget( this, &FreeMonitorWidget::removeSelectedAddresses); connect(ui_->clearButton, &QToolButton::clicked, this, &FreeMonitorWidget::clearAddresses); + connect(ui_->editingSourceComboBox, + QOverload::of(&QComboBox::currentIndexChanged), + this, &FreeMonitorWidget::selectEditingSource); reloadAddresses(); } @@ -98,6 +106,12 @@ void FreeMonitorWidget::setWriteEnabled(bool enabled) updateWriteControls(); } +void FreeMonitorWidget::setEditingSourceSelectorVisible(bool visible) +{ + ui_->editingSourceLabel->setVisible(visible); + ui_->editingSourceComboBox->setVisible(visible); +} + void FreeMonitorWidget::refreshValues( ApplicationMode mode, PlcConnectionState plc_state) { @@ -106,19 +120,44 @@ void FreeMonitorWidget::refreshValues( bool communication_fault = false; if (mode == ApplicationMode::Editing) { - if (service_.editingPlcConnected()) + const bool plc_selected = service_.editingSource() + == EditingMonitorSource::Plc; + if (plc_selected) { - source = service_.editingPlcReady() - ? tr("数据源:PLC 缓存") - : tr("数据源:PLC · 正在首读"); + if (service_.editingPlcReady()) + { + source = tr("数据源:真机 PLC M/D"); + } + else if (plc_state == PlcConnectionState::Connecting) + { + source = tr("数据源:真机 PLC M/D · 正在连接"); + } + else if (plc_state == PlcConnectionState::Recovering) + { + source = tr("数据源:真机 PLC M/D · 正在恢复并重新首读"); + } + else if (plc_state == PlcConnectionState::Faulted) + { + source = tr("数据源:真机 PLC M/D · 通信故障"); + } + else if (plc_state == PlcConnectionState::Disconnected) + { + source = tr("数据源:真机 PLC M/D · 已断开"); + } + else + { + source = tr("数据源:真机 PLC M/D · 正在首读"); + } } else { source = tr("数据源:离线初始值 · 虚拟 M/D"); } values_available = true; - communication_fault = service_.editingPlcConnected() - && !service_.editingPlcReady(); + communication_fault = plc_selected + && plc_state == PlcConnectionState::Faulted; + const QSignalBlocker blocker(ui_->editingSourceComboBox); + ui_->editingSourceComboBox->setCurrentIndex(plc_selected ? 1 : 0); } else if (mode == ApplicationMode::OfflineRunning) { @@ -149,11 +188,25 @@ void FreeMonitorWidget::refreshValues( if (value.state == MonitorValueState::Unavailable) { value_item->setText(QStringLiteral("--")); - state_item->setText( - mode == ApplicationMode::OnlineRunning - || (mode == ApplicationMode::Editing - && service_.editingPlcConnected()) - ? tr("等待读取") : tr("不可用")); + if (mode == ApplicationMode::Editing + && service_.editingSource() == EditingMonitorSource::Plc) + { + state_item->setText( + plc_state == PlcConnectionState::Disconnected + ? tr("PLC 已断开") + : plc_state == PlcConnectionState::Faulted + ? tr("PLC 通信故障") + : plc_state == PlcConnectionState::Connecting + ? tr("PLC 正在连接") + : plc_state == PlcConnectionState::Recovering + ? tr("PLC 恢复中") : tr("等待读取")); + } + else + { + state_item->setText( + mode == ApplicationMode::OnlineRunning + ? tr("等待读取") : tr("不可用")); + } continue; } if (value.address.area() == RegisterArea::M) @@ -394,6 +447,31 @@ void FreeMonitorWidget::clearAddresses() handleResult(tr("清空自由监控"), false, result.message); } +void FreeMonitorWidget::selectEditingSource(int index) +{ + if (index < 0) + { + return; + } + const EditingMonitorSource source = static_cast( + ui_->editingSourceComboBox->itemData(index).toInt()); + const EditingMonitorSourceResult result = + service_.selectEditingSource(source); + if (!result.succeeded) + { + const QSignalBlocker blocker(ui_->editingSourceComboBox); + ui_->editingSourceComboBox->setCurrentIndex( + service_.editingSource() == EditingMonitorSource::Plc ? 1 : 0); + emit operationMessage(fromUtf8(result.message)); + return; + } + emit operationMessage( + source == EditingMonitorSource::Plc + ? tr("数据监控已切换到真机 PLC M/D") + : tr("数据监控已切换到离线 M/D")); + emit editingSourceChanged(); +} + void FreeMonitorWidget::handleResult( const QString &action, bool succeeded, const std::string &message) { diff --git a/app/src/ui/free_monitor_widget.h b/app/src/ui/free_monitor_widget.h index fde2eb9..9d3fcaa 100644 --- a/app/src/ui/free_monitor_widget.h +++ b/app/src/ui/free_monitor_widget.h @@ -36,6 +36,8 @@ public: /** 根据当前模式和通信状态开启或关闭写入入口 */ void setWriteEnabled(bool enabled); + /** 仅编辑态数据监控页显示独立的数据源选择控件 */ + void setEditingSourceSelectorVisible(bool visible); /** 根据运行模式和 PLC 状态刷新所有监控值 */ void refreshValues(ApplicationMode mode, PlcConnectionState plc_state); /** 从服务重新加载当前会话中的地址列表 */ @@ -46,6 +48,8 @@ signals: void monitorAddressesChanged(); /** 操作完成后发出可直接显示的消息 */ void operationMessage(const QString &message); + /** 编辑态监控数据源成功切换后发出 */ + void editingSourceChanged(); private: /** 从输入框添加一个或多个监控地址 */ @@ -54,6 +58,8 @@ private: void removeSelectedAddresses(); /** 清空当前会话中的监控地址 */ void clearAddresses(); + /** 处理用户选择的编辑态监控数据源 */ + void selectEditingSource(int index); /** 写入指定行中的寄存器值 */ void writeRow(int row); /** 根据当前权限刷新写入控件状态 */ diff --git a/app/src/ui/free_monitor_widget.ui b/app/src/ui/free_monitor_widget.ui index b4349a0..8244676 100644 --- a/app/src/ui/free_monitor_widget.ui +++ b/app/src/ui/free_monitor_widget.ui @@ -23,7 +23,9 @@ Qt::Horizontal4020 删除选中的监控地址删除 清空监控表清空 - 数据源:未启用Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + false监控数据 + false135018016777215离线 M/D真机 PLC M/D + 数据源:未启用Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter diff --git a/app/src/ui/main_window.cpp b/app/src/ui/main_window.cpp index 83d5dfe..5e48116 100644 --- a/app/src/ui/main_window.cpp +++ b/app/src/ui/main_window.cpp @@ -1235,6 +1235,7 @@ void MainWindow::configureDataMonitor() data_monitor_widget_ = new FreeMonitorWidget( register_monitor_service_, ui_->dataMonitorTab); data_monitor_widget_->setObjectName(QStringLiteral("dataMonitorWidget")); + data_monitor_widget_->setEditingSourceSelectorVisible(true); ui_->dataMonitorLayout->addWidget(data_monitor_widget_); connect(data_monitor_widget_, &FreeMonitorWidget::monitorAddressesChanged, @@ -1253,6 +1254,8 @@ void MainWindow::configureDataMonitor() statusBar()->showMessage(message, 4000); appendOutputMessage(message); }); + connect(data_monitor_widget_, &FreeMonitorWidget::editingSourceChanged, + this, &MainWindow::refreshDataMonitorUi); register_monitor_service_.setAddressesChangedCallback( [this] { @@ -1293,6 +1296,8 @@ void MainWindow::refreshDataMonitorUi() } const ApplicationMode mode = runtime_mode_service_.mode(); const PlcConnectionState plc_state = runtime_mode_service_.plcConnectionState(); + register_monitor_service_.setEditingModeActive( + mode == ApplicationMode::Editing); const bool editing_plc_connected = mode == ApplicationMode::Editing && plc_state != PlcConnectionState::Disconnected && plc_state != PlcConnectionState::Faulted; @@ -1303,7 +1308,8 @@ void MainWindow::refreshDataMonitorUi() && plc_state == PlcConnectionState::Connected; data_monitor_widget_->setWriteEnabled( (mode == ApplicationMode::Editing - && (!editing_plc_connected + && (register_monitor_service_.editingSource() + == EditingMonitorSource::Offline || register_monitor_service_.editingPlcReady())) || mode == ApplicationMode::OfflineRunning || online_connected); diff --git a/app/tests/register_monitor_service_tests.cpp b/app/tests/register_monitor_service_tests.cpp index 4c9cf3c..466147e 100644 --- a/app/tests/register_monitor_service_tests.cpp +++ b/app/tests/register_monitor_service_tests.cpp @@ -87,6 +87,7 @@ void testEditingPlcMonitorAccess() service.setOfflineInitialRepository(initial_repository); service.setOfflineInitialCaptureEnabled(true); service.setEditingPlcRepository(plc_repository); + service.setEditingModeActive(true); require(service.addRange("M0", 1).succeeded && service.addRange("D0", 1).succeeded, "editing PLC monitor fixture must accept M and D points"); @@ -98,13 +99,19 @@ void testEditingPlcMonitorAccess() 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"); + require(values.size() == 2U && !values[0].bitValue + && std::get(values[1].numericValue) == 11, + "connecting a PLC must not automatically replace the offline editing source"); + const EditingMonitorSourceResult first_read_rejected = + service.selectEditingSource(EditingMonitorSource::Plc); + require(!first_read_rejected.succeeded + && service.editingSource() == EditingMonitorSource::Offline, + "editing monitor must reject PLC selection before the first read"); service.setEditingPlcState(true, true); + require(service.editingSource() == EditingMonitorSource::Offline + && service.selectEditingSource(EditingMonitorSource::Plc).succeeded, + "a completed first read must allow an explicit PLC source selection"); values = service.values(false); require(values[0].state == MonitorValueState::Valid && values[0].bitValue && values[1].state == MonitorValueState::Valid @@ -119,11 +126,33 @@ void testEditingPlcMonitorAccess() && initial_repository.readWord({RegisterArea::D, 0}).value == 0, "editing PLC monitor writes must target PLC and skip offline capture"); + service.setEditingModeActive(false); + values = service.values(false); + require(!values[0].bitValue + && std::get(values[1].numericValue) == 11, + "runtime access must keep following the active repository"); + service.setEditingModeActive(true); + + service.setEditingPlcState(false, false); + values = service.values(false); + require(service.editingSource() == EditingMonitorSource::Plc + && values[0].state == MonitorValueState::Unavailable + && values[1].state == MonitorValueState::Unavailable + && !service.writeBit({RegisterArea::M, 0}, true).succeeded, + "a PLC disconnect must preserve the selected source and prohibit access"); + service.setEditingPlcState(true, true); + values = service.values(false); + require(service.editingSource() == EditingMonitorSource::Plc + && !values[0].bitValue + && std::get(values[1].numericValue) == 33, + "reconnection and a new first read must resume the selected PLC source"); service.setEditingPlcState(false, false); + require(service.selectEditingSource(EditingMonitorSource::Offline).succeeded, + "offline source must remain selectable while PLC is disconnected"); values = service.values(false); require(!values[0].bitValue && std::get(values[1].numericValue) == 11, - "disconnecting PLC must restore the virtual editing source"); + "explicit offline selection must restore virtual editing values"); } void testRegisterWrites() diff --git a/docs/architecture.md b/docs/architecture.md index 2049418..32b19fa 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -161,7 +161,7 @@ ApplicationSettings(启动后只读) 编辑:UI -> Editor Services -> ProjectService -> Project └── ProjectStorage -> JSON -编辑态数据监控:UI -> RegisterMonitorService -> 虚拟 M/D 或 PLC 缓存 +编辑态数据监控:UI 显式选择 -> RegisterMonitorService -> 虚拟 M/D 或 PLC 缓存 └── 离线初始值 VirtualRegisterRepository(进程内) 离线:运行监控弹窗 -> ActiveRegisterRepository -> VirtualRegisterRepository @@ -179,7 +179,7 @@ PLC <-> PlcCommunicationService <--------------------┘ | 模式 | 允许工程编辑 | 寄存器来源 | 软件逻辑执行器 | 进入条件 | | --- | --- | --- | --- | --- | -| 编辑态 | 是 | 未连接 PLC 时使用虚拟 M/D;连接并完成首读后数据监控使用 PLC 缓存 | 停止 | 运行态先退出 | +| 编辑态 | 是 | 数据监控由用户选择虚拟 M/D 或 PLC 缓存;连接 PLC 不自动切换 | 停止 | 运行态先退出 | | 离线运行 | 否 | 虚拟 M/D | 运行 | 工程通过运行校验 | | 真机运行 | 否 | HMI 使用 PLC 读回缓存;轨迹使用缓存副本 | 本地只读推算 | PLC 已连接并完成本次首读,工程通过运行校验 | @@ -187,7 +187,7 @@ PLC <-> PlcCommunicationService <--------------------┘ 自由监控是会话级诊断工具,不属于 `Project`,不写入 JSON。M 监控点固定为位值,D 监控点可选 `Int16`、`Int32`、`Float32` 或 `Float64`。批量添加的步长等于类型占用字数:Int16 为 1,Int32/Float32 为 2,Float64 为 4;类型列显示完整占用范围。完全重复点拒绝,部分重叠点允许添加但提示。读写统一使用连续字块接口;真机 Int16 使用单字写入,Int32、Float32 和 Float64 使用一次多保持寄存器写入,并继续受连接与首读门槛约束。 -编辑态数据监控未连接 PLC 时写入虚拟 M/D,并同时更新当前进程内的离线初始值;连接 PLC 并完成首读后,读写直接使用 PLC 缓存,写入等待后续轮询确认且不更新离线初始值。PLC 已连接但尚未完成首读时只显示等待状态并禁止写入。启动离线仿真时,运行仓库先清空,再复制离线初始值;仿真扫描产生的线圈、MOVE、ADD 和 SUB 结果只写入本轮运行仓库。仿真停止或返回编辑态时丢弃本轮输出并恢复初始值。离线初始值和监控地址均不写入工程 JSON。运行态监控继续复用同一 `RegisterMonitorService`,离线写入虚拟仓库,真机写入 PLC 缓存并等待轮询确认。 +编辑态数据监控默认选择离线 M/D,用户可在界面明确切换离线 M/D 和真机 PLC M/D;PLC 连接成功不会自动切换。离线写入虚拟 M/D 并同时更新当前进程内的离线初始值;真机只有在 PLC 已连接并完成本次首读后才允许选择,读写使用 PLC 缓存,写入等待后续轮询确认且不更新离线初始值。真机选择期间断线或通信故障时保留用户选择、显示不可用并禁止写入,不静默回退虚拟数据;重连并重新首读后恢复访问。两套数据切换时互不复制。启动离线仿真时,运行仓库先清空,再复制离线初始值;仿真扫描产生的线圈、MOVE、ADD 和 SUB 结果只写入本轮运行仓库。仿真停止或返回编辑态时丢弃本轮输出并恢复初始值。离线初始值和监控地址均不写入工程 JSON。运行态监控继续复用同一 `RegisterMonitorService`,但忽略编辑态数据源选择:离线写入虚拟仓库,真机写入 PLC 缓存并等待轮询确认。 ## Modbus RTU 通信 diff --git a/docs/测试约定.md b/docs/测试约定.md index 0bea7c6..fd9c14e 100644 --- a/docs/测试约定.md +++ b/docs/测试约定.md @@ -24,7 +24,7 @@ | `logic_editor_service_tests` | 连续网格编辑、网络注释首行归属与合并冲突、输出自动补尾线、网格与整行片段复制粘贴、冲突回滚和历史 | | `offline_simulation_service_tests` | 梯形图扫描语义、离线初始 M/D 快照、真机缓存副本推算、只读隔离、跨扫描状态和故障处理 | | `project_management_tests` | JSON 往返、支路隐藏注释等非法文件和工程保存状态 | -| `register_monitor_service_tests` | 监视地址、四种 D 类型读写、离线初始值捕获、批量步长、重叠提示和活动仓库 | +| `register_monitor_service_tests` | 监视地址、四种 D 类型读写、离线初始值捕获、编辑态数据源显式切换与断线保持、批量步长、重叠提示和运行态活动仓库 | | `runtime_project_bundle_tests` | 用户运行程序封装的写入、读取、校验和损坏拒绝 | | `runtime_mode_service_tests` | 编辑、离线、真机只读轨迹、仓库切换和断路工程启动拦截 | | `runtime_panel_controller_tests` | 完整执行器快照经过控制器和运行窗口后的轨迹投影、排队回调、画布框选、对象/行号剪贴板和连续输入 | diff --git a/docs/用户使用/数据监控与离线初始值说明.md b/docs/用户使用/数据监控与离线初始值说明.md index 49aa354..98c9057 100644 --- a/docs/用户使用/数据监控与离线初始值说明.md +++ b/docs/用户使用/数据监控与离线初始值说明.md @@ -2,9 +2,11 @@ ## 编辑态 -打开主窗口与“HMI 页面”“控制逻辑”并列的“数据监控”编辑页,添加需要观察的 `M` 或 `D` 地址。数据监控的操作方式与运行态自由监控一致:未连接 PLC 时使用虚拟 M/D,连接 PLC 并完成首次读取后自动切换为 PLC 缓存,读取和写入都直接作用于当前数据源。 +打开主窗口与“HMI 页面”“控制逻辑”并列的“数据监控”编辑页,添加需要观察的 `M` 或 `D` 地址。编辑页提供“离线 M/D”和“真机 PLC M/D”数据源选择,连接 PLC 不会自动改变当前选择,读取和写入只作用于界面明确显示的数据源。 -编辑态连接 PLC 后,监控表会显示 PLC 实时读回的数据。PLC 已连接但尚未完成首次读取时显示“正在首读”,暂不允许写入;断开或通信故障后回到虚拟 M/D。编辑态对 PLC 的写入不会修改离线初始值。 +“离线 M/D”始终可以选择,即使 PLC 已连接也继续查看和编辑虚拟 M/D;写入同时更新当前进程内的离线初始值,不向 PLC 发送数据。“真机 PLC M/D”只有在 PLC 已连接并完成本次连接的首次完整读取后才能选择;选择后查看 PLC 轮询缓存,写入请求发送给 PLC,且不会修改离线初始值。两套数据切换时互不复制。 + +未连接、连接已断开、通信故障、正在连接或正在首次读取时,切换到真机数据会被拒绝并显示原因。已经选择真机数据后发生断线或通信故障时,界面保留“真机 PLC M/D”选择,当前值显示不可用并禁止写入,不会静默显示离线数据;用户可以手动切回离线。PLC 重连并重新完成首次读取后,保留的真机选择恢复刷新和写入权限。 进入运行态后,可在运行监控窗口顶部的模式下拉框切换“离线仿真”和“真机运行”。切换过程会自动经过编辑态;切换到真机前仍必须连接 PLC 并完成首次读取。 @@ -29,7 +31,7 @@ Int32、Float32 和 Double 写入时会把占用的 2 个或 4 个 D 一起保 ## 真机运行 -进入真机运行后使用同一个监控地址列表,但数据源切换为 PLC 读回缓存。写入请求必须在 PLC 已连接并完成有效首读后才能提交,写入结果等待后续轮询确认。 +进入真机运行后使用同一个监控地址列表,但运行监控的数据源由运行模式固定为 PLC 读回缓存,不受编辑态数据监控选择影响。写入请求必须在 PLC 已连接并完成有效首读后才能提交,写入结果等待后续轮询确认。 Int16 写一个保持寄存器;Int32、Float32 和 Double 分别把连续 2 个或 4 个保持寄存器放在同一次多寄存器请求中写入,避免通信中途失败留下半个新值。单次轮询最多读取 120 个字,拆块边界不会切开一个多字值。