| @@ -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<MonitorValue> 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); | |||
| @@ -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; | |||
| @@ -12,6 +12,7 @@ | |||
| #include <QLocale> | |||
| #include <QMessageBox> | |||
| #include <QPushButton> | |||
| #include <QSignalBlocker> | |||
| #include <QTableWidgetItem> | |||
| #include <cstdint> | |||
| @@ -79,6 +80,10 @@ FreeMonitorWidget::FreeMonitorWidget( | |||
| ui_->dataTypeComboBox->setItemData(1, static_cast<int>(RegisterDataType::Int32)); | |||
| ui_->dataTypeComboBox->setItemData(2, static_cast<int>(RegisterDataType::Float32)); | |||
| ui_->dataTypeComboBox->setItemData(3, static_cast<int>(RegisterDataType::Float64)); | |||
| ui_->editingSourceComboBox->setItemData( | |||
| 0, static_cast<int>(EditingMonitorSource::Offline)); | |||
| ui_->editingSourceComboBox->setItemData( | |||
| 1, static_cast<int>(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<int>::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<EditingMonitorSource>( | |||
| 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) | |||
| { | |||
| @@ -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); | |||
| /** 根据当前权限刷新写入控件状态 */ | |||
| @@ -23,7 +23,9 @@ | |||
| <item row="0" column="7"><spacer name="firstRowSpacer"><property name="orientation"><enum>Qt::Horizontal</enum></property><property name="sizeHint" stdset="0"><size><width>40</width><height>20</height></size></property></spacer></item> | |||
| <item row="1" column="0"><widget class="QToolButton" name="removeButton"><property name="toolTip"><string>删除选中的监控地址</string></property><property name="text"><string>删除</string></property></widget></item> | |||
| <item row="1" column="1"><widget class="QToolButton" name="clearButton"><property name="toolTip"><string>清空监控表</string></property><property name="text"><string>清空</string></property></widget></item> | |||
| <item row="1" column="2" colspan="6"><widget class="QLabel" name="sourceLabel"><property name="text"><string>数据源:未启用</string></property><property name="alignment"><set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set></property></widget></item> | |||
| <item row="1" column="2"><widget class="QLabel" name="editingSourceLabel"><property name="visible"><bool>false</bool></property><property name="text"><string>监控数据</string></property></widget></item> | |||
| <item row="1" column="3"><widget class="QComboBox" name="editingSourceComboBox"><property name="visible"><bool>false</bool></property><property name="minimumSize"><size><width>135</width><height>0</height></size></property><property name="maximumSize"><size><width>180</width><height>16777215</height></size></property><item><property name="text"><string>离线 M/D</string></property></item><item><property name="text"><string>真机 PLC M/D</string></property></item></widget></item> | |||
| <item row="1" column="4" colspan="4"><widget class="QLabel" name="sourceLabel"><property name="text"><string>数据源:未启用</string></property><property name="alignment"><set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set></property></widget></item> | |||
| </layout> | |||
| </item> | |||
| <item> | |||
| @@ -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); | |||
| @@ -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<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"); | |||
| require(values.size() == 2U && !values[0].bitValue | |||
| && std::get<std::int16_t>(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<std::int16_t>(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<std::int16_t>(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<std::int16_t>(values[1].numericValue) == 11, | |||
| "disconnecting PLC must restore the virtual editing source"); | |||
| "explicit offline selection must restore virtual editing values"); | |||
| } | |||
| void testRegisterWrites() | |||
| @@ -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 通信 | |||
| @@ -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` | 完整执行器快照经过控制器和运行窗口后的轨迹投影、排队回调、画布框选、对象/行号剪贴板和连续输入 | | |||
| @@ -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 个字,拆块边界不会切开一个多字值。 | |||