| @@ -50,6 +50,11 @@ void RegisterMonitorService::setEditingPlcRepository( | |||||
| editing_plc_repository_ = &repository; | editing_plc_repository_ = &repository; | ||||
| } | } | ||||
| void RegisterMonitorService::setEditingModeActive(bool active) | |||||
| { | |||||
| editing_mode_active_ = active; | |||||
| } | |||||
| void RegisterMonitorService::setEditingPlcState( | void RegisterMonitorService::setEditingPlcState( | ||||
| bool connected, bool initial_read_completed) | bool connected, bool initial_read_completed) | ||||
| { | { | ||||
| @@ -57,19 +62,70 @@ void RegisterMonitorService::setEditingPlcState( | |||||
| editing_plc_ready_ = connected && initial_read_completed; | 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 | bool RegisterMonitorService::editingPlcConnected() const | ||||
| { | { | ||||
| return usesEditingPlcRepository(); | |||||
| return editing_plc_connected_ && editing_plc_repository_ != nullptr; | |||||
| } | } | ||||
| bool RegisterMonitorService::editingPlcReady() const | 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 | 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() | RegisterRepository &RegisterMonitorService::accessRepository() | ||||
| @@ -250,9 +306,10 @@ RegisterMonitorResult RegisterMonitorService::clear() | |||||
| RegisterMonitorWriteResult RegisterMonitorService::writeBit( | RegisterMonitorWriteResult RegisterMonitorService::writeBit( | ||||
| const RegisterAddress &address, bool value) | 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 RegisterWriteResult result = accessRepository().writeBit(address, value); | ||||
| const RegisterMonitorWriteResult active_result = result.succeeded | const RegisterMonitorWriteResult active_result = result.succeeded | ||||
| @@ -270,9 +327,10 @@ RegisterMonitorWriteResult RegisterMonitorService::writeBit( | |||||
| RegisterMonitorWriteResult RegisterMonitorService::writeNumeric( | RegisterMonitorWriteResult RegisterMonitorService::writeNumeric( | ||||
| const RegisterAddress &address, RegisterDataType data_type, double value) | 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) | if (address.isValid() && address.area() != RegisterArea::D) | ||||
| { | { | ||||
| @@ -353,7 +411,7 @@ std::vector<MonitorValue> RegisterMonitorService::values(bool communication_faul | |||||
| value.dataType = point.dataType; | value.dataType = point.dataType; | ||||
| value.endAddress = address.index() | value.endAddress = address.index() | ||||
| + registerDataTypeWordCount(point.dataType) - 1; | + registerDataTypeWordCount(point.dataType) - 1; | ||||
| if (usesEditingPlcRepository() && !editing_plc_ready_) | |||||
| if (editingPlcAccessRequested() && !editingPlcReady()) | |||||
| { | { | ||||
| value.state = MonitorValueState::Unavailable; | value.state = MonitorValueState::Unavailable; | ||||
| result.push_back(value); | result.push_back(value); | ||||
| @@ -38,10 +38,24 @@ struct RegisterMonitorWriteResult | |||||
| std::string message; // 面向用户的 UTF-8 错误说明 | std::string message; // 面向用户的 UTF-8 错误说明 | ||||
| }; | }; | ||||
| // 编辑态数据监控由用户明确选择离线初始值或真实 PLC | |||||
| enum class EditingMonitorSource | |||||
| { | |||||
| Offline, | |||||
| Plc | |||||
| }; | |||||
| // 编辑态数据源切换结果;真机不满足连接和首读条件时拒绝切换 | |||||
| struct EditingMonitorSourceResult | |||||
| { | |||||
| bool succeeded = false; | |||||
| std::string message; | |||||
| }; | |||||
| /** | /** | ||||
| * @brief 编排自由监控地址列表和寄存器仓库读写 | * @brief 编排自由监控地址列表和寄存器仓库读写 | ||||
| * | * | ||||
| * 地址列表和离线初始值只属于当前运行会话,不写入工程文件;编辑态连接 PLC 后可直接访问 PLC 缓存 | |||||
| * 地址列表和离线初始值只属于当前运行会话,不写入工程文件;编辑态数据源由用户明确选择 | |||||
| */ | */ | ||||
| class RegisterMonitorService | class RegisterMonitorService | ||||
| { | { | ||||
| @@ -58,9 +72,15 @@ public: | |||||
| void setOfflineInitialCaptureEnabled(bool enabled); | void setOfflineInitialCaptureEnabled(bool enabled); | ||||
| // 配置编辑态可直接访问的 PLC 缓存仓库 | // 配置编辑态可直接访问的 PLC 缓存仓库 | ||||
| void setEditingPlcRepository(RegisterRepository &repository); | void setEditingPlcRepository(RegisterRepository &repository); | ||||
| // 标记当前是否由编辑态数据监控访问仓库;运行态继续使用活动仓库 | |||||
| void setEditingModeActive(bool active); | |||||
| // 更新编辑态 PLC 连接和首读状态 | // 更新编辑态 PLC 连接和首读状态 | ||||
| void setEditingPlcState(bool connected, bool initial_read_completed); | void setEditingPlcState(bool connected, bool initial_read_completed); | ||||
| // 编辑态是否已经连接到 PLC | |||||
| // 请求切换编辑态数据监控来源;切到真机前必须连接并完成首读 | |||||
| EditingMonitorSourceResult selectEditingSource(EditingMonitorSource source); | |||||
| // 返回用户当前选择的数据源;连接故障不会静默改回离线 | |||||
| EditingMonitorSource editingSource() const; | |||||
| // 编辑态 PLC 当前是否已经建立连接 | |||||
| bool editingPlcConnected() const; | bool editingPlcConnected() const; | ||||
| // 编辑态 PLC 是否已完成首轮读取,可执行写入 | // 编辑态 PLC 是否已完成首轮读取,可执行写入 | ||||
| bool editingPlcReady() const; | bool editingPlcReady() const; | ||||
| @@ -128,7 +148,9 @@ public: | |||||
| private: | private: | ||||
| RegisterRepository &accessRepository(); | RegisterRepository &accessRepository(); | ||||
| const RegisterRepository &accessRepository() const; | const RegisterRepository &accessRepository() const; | ||||
| bool editingPlcAccessRequested() const; | |||||
| bool usesEditingPlcRepository() const; | bool usesEditingPlcRepository() const; | ||||
| std::string editingPlcUnavailableMessage() const; | |||||
| RegisterMonitorWriteResult captureInitialBit( | RegisterMonitorWriteResult captureInitialBit( | ||||
| const RegisterAddress &address, bool value, | const RegisterAddress &address, bool value, | ||||
| const RegisterMonitorWriteResult &active_result); | const RegisterMonitorWriteResult &active_result); | ||||
| @@ -143,6 +165,8 @@ private: | |||||
| RegisterRepository &repository_; // 当前运行模式的寄存器仓库,不由服务拥有 | RegisterRepository &repository_; // 当前运行模式的寄存器仓库,不由服务拥有 | ||||
| RegisterRepository *editing_plc_repository_ = nullptr; | RegisterRepository *editing_plc_repository_ = nullptr; | ||||
| bool editing_mode_active_ = false; | |||||
| EditingMonitorSource editing_source_ = EditingMonitorSource::Offline; | |||||
| bool editing_plc_connected_ = false; | bool editing_plc_connected_ = false; | ||||
| bool editing_plc_ready_ = false; | bool editing_plc_ready_ = false; | ||||
| VirtualRegisterRepository *offline_initial_repository_ = nullptr; | VirtualRegisterRepository *offline_initial_repository_ = nullptr; | ||||
| @@ -12,6 +12,7 @@ | |||||
| #include <QLocale> | #include <QLocale> | ||||
| #include <QMessageBox> | #include <QMessageBox> | ||||
| #include <QPushButton> | #include <QPushButton> | ||||
| #include <QSignalBlocker> | |||||
| #include <QTableWidgetItem> | #include <QTableWidgetItem> | ||||
| #include <cstdint> | #include <cstdint> | ||||
| @@ -79,6 +80,10 @@ FreeMonitorWidget::FreeMonitorWidget( | |||||
| ui_->dataTypeComboBox->setItemData(1, static_cast<int>(RegisterDataType::Int32)); | ui_->dataTypeComboBox->setItemData(1, static_cast<int>(RegisterDataType::Int32)); | ||||
| ui_->dataTypeComboBox->setItemData(2, static_cast<int>(RegisterDataType::Float32)); | ui_->dataTypeComboBox->setItemData(2, static_cast<int>(RegisterDataType::Float32)); | ||||
| ui_->dataTypeComboBox->setItemData(3, static_cast<int>(RegisterDataType::Float64)); | 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); | ui_->monitorTable->verticalHeader()->setVisible(false); | ||||
| connect(ui_->addButton, &QPushButton::clicked, this, &FreeMonitorWidget::addAddresses); | connect(ui_->addButton, &QPushButton::clicked, this, &FreeMonitorWidget::addAddresses); | ||||
| connect(ui_->addressEdit, &QLineEdit::returnPressed, | connect(ui_->addressEdit, &QLineEdit::returnPressed, | ||||
| @@ -87,6 +92,9 @@ FreeMonitorWidget::FreeMonitorWidget( | |||||
| this, &FreeMonitorWidget::removeSelectedAddresses); | this, &FreeMonitorWidget::removeSelectedAddresses); | ||||
| connect(ui_->clearButton, &QToolButton::clicked, | connect(ui_->clearButton, &QToolButton::clicked, | ||||
| this, &FreeMonitorWidget::clearAddresses); | this, &FreeMonitorWidget::clearAddresses); | ||||
| connect(ui_->editingSourceComboBox, | |||||
| QOverload<int>::of(&QComboBox::currentIndexChanged), | |||||
| this, &FreeMonitorWidget::selectEditingSource); | |||||
| reloadAddresses(); | reloadAddresses(); | ||||
| } | } | ||||
| @@ -98,6 +106,12 @@ void FreeMonitorWidget::setWriteEnabled(bool enabled) | |||||
| updateWriteControls(); | updateWriteControls(); | ||||
| } | } | ||||
| void FreeMonitorWidget::setEditingSourceSelectorVisible(bool visible) | |||||
| { | |||||
| ui_->editingSourceLabel->setVisible(visible); | |||||
| ui_->editingSourceComboBox->setVisible(visible); | |||||
| } | |||||
| void FreeMonitorWidget::refreshValues( | void FreeMonitorWidget::refreshValues( | ||||
| ApplicationMode mode, PlcConnectionState plc_state) | ApplicationMode mode, PlcConnectionState plc_state) | ||||
| { | { | ||||
| @@ -106,19 +120,44 @@ void FreeMonitorWidget::refreshValues( | |||||
| bool communication_fault = false; | bool communication_fault = false; | ||||
| if (mode == ApplicationMode::Editing) | 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 | else | ||||
| { | { | ||||
| source = tr("数据源:离线初始值 · 虚拟 M/D"); | source = tr("数据源:离线初始值 · 虚拟 M/D"); | ||||
| } | } | ||||
| values_available = true; | 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) | else if (mode == ApplicationMode::OfflineRunning) | ||||
| { | { | ||||
| @@ -149,11 +188,25 @@ void FreeMonitorWidget::refreshValues( | |||||
| if (value.state == MonitorValueState::Unavailable) | if (value.state == MonitorValueState::Unavailable) | ||||
| { | { | ||||
| value_item->setText(QStringLiteral("--")); | 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; | continue; | ||||
| } | } | ||||
| if (value.address.area() == RegisterArea::M) | if (value.address.area() == RegisterArea::M) | ||||
| @@ -394,6 +447,31 @@ void FreeMonitorWidget::clearAddresses() | |||||
| handleResult(tr("清空自由监控"), false, result.message); | 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( | void FreeMonitorWidget::handleResult( | ||||
| const QString &action, bool succeeded, const std::string &message) | const QString &action, bool succeeded, const std::string &message) | ||||
| { | { | ||||
| @@ -36,6 +36,8 @@ public: | |||||
| /** 根据当前模式和通信状态开启或关闭写入入口 */ | /** 根据当前模式和通信状态开启或关闭写入入口 */ | ||||
| void setWriteEnabled(bool enabled); | void setWriteEnabled(bool enabled); | ||||
| /** 仅编辑态数据监控页显示独立的数据源选择控件 */ | |||||
| void setEditingSourceSelectorVisible(bool visible); | |||||
| /** 根据运行模式和 PLC 状态刷新所有监控值 */ | /** 根据运行模式和 PLC 状态刷新所有监控值 */ | ||||
| void refreshValues(ApplicationMode mode, PlcConnectionState plc_state); | void refreshValues(ApplicationMode mode, PlcConnectionState plc_state); | ||||
| /** 从服务重新加载当前会话中的地址列表 */ | /** 从服务重新加载当前会话中的地址列表 */ | ||||
| @@ -46,6 +48,8 @@ signals: | |||||
| void monitorAddressesChanged(); | void monitorAddressesChanged(); | ||||
| /** 操作完成后发出可直接显示的消息 */ | /** 操作完成后发出可直接显示的消息 */ | ||||
| void operationMessage(const QString &message); | void operationMessage(const QString &message); | ||||
| /** 编辑态监控数据源成功切换后发出 */ | |||||
| void editingSourceChanged(); | |||||
| private: | private: | ||||
| /** 从输入框添加一个或多个监控地址 */ | /** 从输入框添加一个或多个监控地址 */ | ||||
| @@ -54,6 +58,8 @@ private: | |||||
| void removeSelectedAddresses(); | void removeSelectedAddresses(); | ||||
| /** 清空当前会话中的监控地址 */ | /** 清空当前会话中的监控地址 */ | ||||
| void clearAddresses(); | void clearAddresses(); | ||||
| /** 处理用户选择的编辑态监控数据源 */ | |||||
| void selectEditingSource(int index); | |||||
| /** 写入指定行中的寄存器值 */ | /** 写入指定行中的寄存器值 */ | ||||
| void writeRow(int row); | 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="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="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="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> | </layout> | ||||
| </item> | </item> | ||||
| <item> | <item> | ||||
| @@ -1235,6 +1235,7 @@ void MainWindow::configureDataMonitor() | |||||
| data_monitor_widget_ = new FreeMonitorWidget( | data_monitor_widget_ = new FreeMonitorWidget( | ||||
| register_monitor_service_, ui_->dataMonitorTab); | register_monitor_service_, ui_->dataMonitorTab); | ||||
| data_monitor_widget_->setObjectName(QStringLiteral("dataMonitorWidget")); | data_monitor_widget_->setObjectName(QStringLiteral("dataMonitorWidget")); | ||||
| data_monitor_widget_->setEditingSourceSelectorVisible(true); | |||||
| ui_->dataMonitorLayout->addWidget(data_monitor_widget_); | ui_->dataMonitorLayout->addWidget(data_monitor_widget_); | ||||
| connect(data_monitor_widget_, &FreeMonitorWidget::monitorAddressesChanged, | connect(data_monitor_widget_, &FreeMonitorWidget::monitorAddressesChanged, | ||||
| @@ -1253,6 +1254,8 @@ void MainWindow::configureDataMonitor() | |||||
| statusBar()->showMessage(message, 4000); | statusBar()->showMessage(message, 4000); | ||||
| appendOutputMessage(message); | appendOutputMessage(message); | ||||
| }); | }); | ||||
| connect(data_monitor_widget_, &FreeMonitorWidget::editingSourceChanged, | |||||
| this, &MainWindow::refreshDataMonitorUi); | |||||
| register_monitor_service_.setAddressesChangedCallback( | register_monitor_service_.setAddressesChangedCallback( | ||||
| [this] | [this] | ||||
| { | { | ||||
| @@ -1293,6 +1296,8 @@ void MainWindow::refreshDataMonitorUi() | |||||
| } | } | ||||
| const ApplicationMode mode = runtime_mode_service_.mode(); | const ApplicationMode mode = runtime_mode_service_.mode(); | ||||
| const PlcConnectionState plc_state = runtime_mode_service_.plcConnectionState(); | const PlcConnectionState plc_state = runtime_mode_service_.plcConnectionState(); | ||||
| register_monitor_service_.setEditingModeActive( | |||||
| mode == ApplicationMode::Editing); | |||||
| const bool editing_plc_connected = mode == ApplicationMode::Editing | const bool editing_plc_connected = mode == ApplicationMode::Editing | ||||
| && plc_state != PlcConnectionState::Disconnected | && plc_state != PlcConnectionState::Disconnected | ||||
| && plc_state != PlcConnectionState::Faulted; | && plc_state != PlcConnectionState::Faulted; | ||||
| @@ -1303,7 +1308,8 @@ void MainWindow::refreshDataMonitorUi() | |||||
| && plc_state == PlcConnectionState::Connected; | && plc_state == PlcConnectionState::Connected; | ||||
| data_monitor_widget_->setWriteEnabled( | data_monitor_widget_->setWriteEnabled( | ||||
| (mode == ApplicationMode::Editing | (mode == ApplicationMode::Editing | ||||
| && (!editing_plc_connected | |||||
| && (register_monitor_service_.editingSource() | |||||
| == EditingMonitorSource::Offline | |||||
| || register_monitor_service_.editingPlcReady())) | || register_monitor_service_.editingPlcReady())) | ||||
| || mode == ApplicationMode::OfflineRunning | || mode == ApplicationMode::OfflineRunning | ||||
| || online_connected); | || online_connected); | ||||
| @@ -87,6 +87,7 @@ void testEditingPlcMonitorAccess() | |||||
| service.setOfflineInitialRepository(initial_repository); | service.setOfflineInitialRepository(initial_repository); | ||||
| service.setOfflineInitialCaptureEnabled(true); | service.setOfflineInitialCaptureEnabled(true); | ||||
| service.setEditingPlcRepository(plc_repository); | service.setEditingPlcRepository(plc_repository); | ||||
| service.setEditingModeActive(true); | |||||
| require(service.addRange("M0", 1).succeeded | require(service.addRange("M0", 1).succeeded | ||||
| && service.addRange("D0", 1).succeeded, | && service.addRange("D0", 1).succeeded, | ||||
| "editing PLC monitor fixture must accept M and D points"); | "editing PLC monitor fixture must accept M and D points"); | ||||
| @@ -98,13 +99,19 @@ void testEditingPlcMonitorAccess() | |||||
| service.setEditingPlcState(true, false); | service.setEditingPlcState(true, false); | ||||
| std::vector<MonitorValue> values = service.values(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); | 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); | values = service.values(false); | ||||
| require(values[0].state == MonitorValueState::Valid && values[0].bitValue | require(values[0].state == MonitorValueState::Valid && values[0].bitValue | ||||
| && values[1].state == MonitorValueState::Valid | && values[1].state == MonitorValueState::Valid | ||||
| @@ -119,11 +126,33 @@ void testEditingPlcMonitorAccess() | |||||
| && initial_repository.readWord({RegisterArea::D, 0}).value == 0, | && initial_repository.readWord({RegisterArea::D, 0}).value == 0, | ||||
| "editing PLC monitor writes must target PLC and skip offline capture"); | "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); | service.setEditingPlcState(false, false); | ||||
| require(service.selectEditingSource(EditingMonitorSource::Offline).succeeded, | |||||
| "offline source must remain selectable while PLC is disconnected"); | |||||
| values = service.values(false); | values = service.values(false); | ||||
| require(!values[0].bitValue | require(!values[0].bitValue | ||||
| && std::get<std::int16_t>(values[1].numericValue) == 11, | && 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() | void testRegisterWrites() | ||||
| @@ -161,7 +161,7 @@ ApplicationSettings(启动后只读) | |||||
| 编辑:UI -> Editor Services -> ProjectService -> Project | 编辑:UI -> Editor Services -> ProjectService -> Project | ||||
| └── ProjectStorage -> JSON | └── ProjectStorage -> JSON | ||||
| 编辑态数据监控:UI -> RegisterMonitorService -> 虚拟 M/D 或 PLC 缓存 | |||||
| 编辑态数据监控:UI 显式选择 -> RegisterMonitorService -> 虚拟 M/D 或 PLC 缓存 | |||||
| └── 离线初始值 VirtualRegisterRepository(进程内) | └── 离线初始值 VirtualRegisterRepository(进程内) | ||||
| 离线:运行监控弹窗 -> ActiveRegisterRepository -> VirtualRegisterRepository | 离线:运行监控弹窗 -> ActiveRegisterRepository -> VirtualRegisterRepository | ||||
| @@ -179,7 +179,7 @@ PLC <-> PlcCommunicationService <--------------------┘ | |||||
| | 模式 | 允许工程编辑 | 寄存器来源 | 软件逻辑执行器 | 进入条件 | | | 模式 | 允许工程编辑 | 寄存器来源 | 软件逻辑执行器 | 进入条件 | | ||||
| | --- | --- | --- | --- | --- | | | --- | --- | --- | --- | --- | | ||||
| | 编辑态 | 是 | 未连接 PLC 时使用虚拟 M/D;连接并完成首读后数据监控使用 PLC 缓存 | 停止 | 运行态先退出 | | |||||
| | 编辑态 | 是 | 数据监控由用户选择虚拟 M/D 或 PLC 缓存;连接 PLC 不自动切换 | 停止 | 运行态先退出 | | |||||
| | 离线运行 | 否 | 虚拟 M/D | 运行 | 工程通过运行校验 | | | 离线运行 | 否 | 虚拟 M/D | 运行 | 工程通过运行校验 | | ||||
| | 真机运行 | 否 | HMI 使用 PLC 读回缓存;轨迹使用缓存副本 | 本地只读推算 | PLC 已连接并完成本次首读,工程通过运行校验 | | | 真机运行 | 否 | 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 使用一次多保持寄存器写入,并继续受连接与首读门槛约束。 | 自由监控是会话级诊断工具,不属于 `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 通信 | ## Modbus RTU 通信 | ||||
| @@ -24,7 +24,7 @@ | |||||
| | `logic_editor_service_tests` | 连续网格编辑、网络注释首行归属与合并冲突、输出自动补尾线、网格与整行片段复制粘贴、冲突回滚和历史 | | | `logic_editor_service_tests` | 连续网格编辑、网络注释首行归属与合并冲突、输出自动补尾线、网格与整行片段复制粘贴、冲突回滚和历史 | | ||||
| | `offline_simulation_service_tests` | 梯形图扫描语义、离线初始 M/D 快照、真机缓存副本推算、只读隔离、跨扫描状态和故障处理 | | | `offline_simulation_service_tests` | 梯形图扫描语义、离线初始 M/D 快照、真机缓存副本推算、只读隔离、跨扫描状态和故障处理 | | ||||
| | `project_management_tests` | JSON 往返、支路隐藏注释等非法文件和工程保存状态 | | | `project_management_tests` | JSON 往返、支路隐藏注释等非法文件和工程保存状态 | | ||||
| | `register_monitor_service_tests` | 监视地址、四种 D 类型读写、离线初始值捕获、批量步长、重叠提示和活动仓库 | | |||||
| | `register_monitor_service_tests` | 监视地址、四种 D 类型读写、离线初始值捕获、编辑态数据源显式切换与断线保持、批量步长、重叠提示和运行态活动仓库 | | |||||
| | `runtime_project_bundle_tests` | 用户运行程序封装的写入、读取、校验和损坏拒绝 | | | `runtime_project_bundle_tests` | 用户运行程序封装的写入、读取、校验和损坏拒绝 | | ||||
| | `runtime_mode_service_tests` | 编辑、离线、真机只读轨迹、仓库切换和断路工程启动拦截 | | | `runtime_mode_service_tests` | 编辑、离线、真机只读轨迹、仓库切换和断路工程启动拦截 | | ||||
| | `runtime_panel_controller_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 并完成首次读取。 | 进入运行态后,可在运行监控窗口顶部的模式下拉框切换“离线仿真”和“真机运行”。切换过程会自动经过编辑态;切换到真机前仍必须连接 PLC 并完成首次读取。 | ||||
| @@ -29,7 +31,7 @@ Int32、Float32 和 Double 写入时会把占用的 2 个或 4 个 D 一起保 | |||||
| ## 真机运行 | ## 真机运行 | ||||
| 进入真机运行后使用同一个监控地址列表,但数据源切换为 PLC 读回缓存。写入请求必须在 PLC 已连接并完成有效首读后才能提交,写入结果等待后续轮询确认。 | |||||
| 进入真机运行后使用同一个监控地址列表,但运行监控的数据源由运行模式固定为 PLC 读回缓存,不受编辑态数据监控选择影响。写入请求必须在 PLC 已连接并完成有效首读后才能提交,写入结果等待后续轮询确认。 | |||||
| Int16 写一个保持寄存器;Int32、Float32 和 Double 分别把连续 2 个或 4 个保持寄存器放在同一次多寄存器请求中写入,避免通信中途失败留下半个新值。单次轮询最多读取 120 个字,拆块边界不会切开一个多字值。 | Int16 写一个保持寄存器;Int32、Float32 和 Double 分别把连续 2 个或 4 个保持寄存器放在同一次多寄存器请求中写入,避免通信中途失败留下半个新值。单次轮询最多读取 120 个字,拆块边界不会切开一个多字值。 | ||||