From ae9400f1c12d114f9c14bebf9485343b469c84f8 Mon Sep 17 00:00:00 2001 From: suyu <1643689728@qq.com> Date: Wed, 19 Aug 2026 08:45:34 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=94=AF=E6=8C=81=E8=87=AA=E7=94=B1?= =?UTF-8?q?=E7=9B=91=E8=A7=86=E5=99=A8=E5=8D=95=E7=82=B9=E5=86=99=E5=85=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/src/services/register_monitor_service.cpp | 34 ++++++ app/src/services/register_monitor_service.h | 11 ++ app/src/ui/free_monitor_widget.cpp | 108 +++++++++++++++++- app/src/ui/free_monitor_widget.h | 4 + app/src/ui/free_monitor_widget.ui | 14 ++- app/src/ui/runtime_monitor_widget.cpp | 4 +- app/tests/main_window_tests.cpp | 50 ++++++++ app/tests/register_monitor_service_tests.cpp | 30 +++++ 8 files changed, 245 insertions(+), 10 deletions(-) diff --git a/app/src/services/register_monitor_service.cpp b/app/src/services/register_monitor_service.cpp index 5596097..c7678bc 100644 --- a/app/src/services/register_monitor_service.cpp +++ b/app/src/services/register_monitor_service.cpp @@ -112,6 +112,40 @@ RegisterMonitorResult RegisterMonitorService::clear() return {true, RegisterMonitorError::None, {}, removed_count}; } +RegisterMonitorWriteResult RegisterMonitorService::writeBit( + const RegisterAddress &address, bool value) +{ + const RegisterWriteResult result = repository_.writeBit(address, value); + if (result.succeeded) + { + return {true, RegisterError::None, {}}; + } + return {false, result.error, result.error == RegisterError::AreaMismatch + ? "位写入只允许访问 M 区" + : result.error == RegisterError::InvalidAddress + ? "写入地址无效" + : result.error == RegisterError::Unavailable + ? "当前寄存器源不可用" + : "寄存器写入请求被拒绝"}; +} + +RegisterMonitorWriteResult RegisterMonitorService::writeWord( + const RegisterAddress &address, std::int16_t value) +{ + const RegisterWriteResult result = repository_.writeWord(address, value); + if (result.succeeded) + { + return {true, RegisterError::None, {}}; + } + return {false, result.error, result.error == RegisterError::AreaMismatch + ? "字写入只允许访问 D 区" + : result.error == RegisterError::InvalidAddress + ? "写入地址无效" + : result.error == RegisterError::Unavailable + ? "当前寄存器源不可用" + : "寄存器写入请求被拒绝"}; +} + const std::vector &RegisterMonitorService::addresses() const { return model_.addresses(); diff --git a/app/src/services/register_monitor_service.h b/app/src/services/register_monitor_service.h index e394aec..ec1dc40 100644 --- a/app/src/services/register_monitor_service.h +++ b/app/src/services/register_monitor_service.h @@ -3,6 +3,7 @@ #include "domain/register_monitor_model.h" #include "domain/register_repository.h" +#include #include #include #include @@ -24,6 +25,13 @@ struct RegisterMonitorResult int affectedCount = 0; }; +struct RegisterMonitorWriteResult +{ + bool succeeded = false; + RegisterError error = RegisterError::Unavailable; + std::string message; +}; + class RegisterMonitorService { public: @@ -32,6 +40,9 @@ public: RegisterMonitorResult addRange(const std::string &start_address, int count); RegisterMonitorResult remove(const std::vector &addresses); RegisterMonitorResult clear(); + RegisterMonitorWriteResult writeBit(const RegisterAddress &address, bool value); + RegisterMonitorWriteResult writeWord( + const RegisterAddress &address, std::int16_t value); const std::vector &addresses() const; std::vector values(bool communication_fault) const; void setAddressesChangedCallback(std::function callback); diff --git a/app/src/ui/free_monitor_widget.cpp b/app/src/ui/free_monitor_widget.cpp index aa089ce..e3de074 100644 --- a/app/src/ui/free_monitor_widget.cpp +++ b/app/src/ui/free_monitor_widget.cpp @@ -4,10 +4,15 @@ #include "toolbar_icon_factory.h" #include "ui_free_monitor_widget.h" +#include #include +#include +#include #include +#include #include +#include #include namespace { @@ -38,6 +43,8 @@ FreeMonitorWidget::FreeMonitorWidget( ui_->monitorTable->horizontalHeader()->setSectionResizeMode(1, QHeaderView::ResizeToContents); ui_->monitorTable->horizontalHeader()->setSectionResizeMode(2, QHeaderView::Stretch); ui_->monitorTable->horizontalHeader()->setSectionResizeMode(3, QHeaderView::Stretch); + ui_->monitorTable->horizontalHeader()->setSectionResizeMode(4, QHeaderView::ResizeToContents); + ui_->monitorTable->horizontalHeader()->setSectionResizeMode(5, QHeaderView::Stretch); ui_->monitorTable->verticalHeader()->setVisible(false); connect(ui_->addButton, &QPushButton::clicked, this, &FreeMonitorWidget::addAddresses); connect(ui_->addressEdit, &QLineEdit::returnPressed, @@ -51,6 +58,12 @@ FreeMonitorWidget::FreeMonitorWidget( FreeMonitorWidget::~FreeMonitorWidget() = default; +void FreeMonitorWidget::setWriteEnabled(bool enabled) +{ + write_enabled_ = enabled; + updateWriteControls(); +} + void FreeMonitorWidget::refreshValues( ApplicationMode mode, PlcConnectionState plc_state) { @@ -75,7 +88,7 @@ void FreeMonitorWidget::refreshValues( for (int row = 0; row < ui_->monitorTable->rowCount(); ++row) { QTableWidgetItem *value_item = ui_->monitorTable->item(row, 2); - QTableWidgetItem *state_item = ui_->monitorTable->item(row, 3); + QTableWidgetItem *state_item = ui_->monitorTable->item(row, 5); if (!values_available || static_cast(row) >= values.size()) { value_item->setText(QStringLiteral("--")); @@ -97,6 +110,7 @@ void FreeMonitorWidget::refreshValues( state_item->setText(value.state == MonitorValueState::CommunicationFault ? tr("通信故障,最后有效值") : tr("有效")); } + updateWriteControls(); } void FreeMonitorWidget::reloadAddresses() @@ -112,7 +126,97 @@ void FreeMonitorWidget::reloadAddresses() row, 1, new QTableWidgetItem(address.area() == RegisterArea::M ? tr("位") : tr("16 位整数"))); ui_->monitorTable->setItem(row, 2, new QTableWidgetItem(QStringLiteral("--"))); - ui_->monitorTable->setItem(row, 3, new QTableWidgetItem(tr("未运行"))); + if (address.area() == RegisterArea::M) + { + auto *target = new QComboBox(ui_->monitorTable); + target->addItem(tr("OFF"), false); + target->addItem(tr("ON"), true); + ui_->monitorTable->setCellWidget(row, 3, target); + } + else + { + auto *target = new QLineEdit(ui_->monitorTable); + target->setValidator(new QIntValidator(-32768, 32767, target)); + target->setText(QStringLiteral("0")); + target->setAlignment(Qt::AlignRight | Qt::AlignVCenter); + ui_->monitorTable->setCellWidget(row, 3, target); + } + auto *write_button = new QPushButton(tr("写入"), ui_->monitorTable); + write_button->setProperty("monitorRow", row); + connect(write_button, &QPushButton::clicked, this, + [this, write_button] + { + writeRow(write_button->property("monitorRow").toInt()); + }); + ui_->monitorTable->setCellWidget(row, 4, write_button); + ui_->monitorTable->setItem(row, 5, new QTableWidgetItem(tr("未运行"))); + } + updateWriteControls(); +} + +void FreeMonitorWidget::writeRow(int row) +{ + if (!write_enabled_ || row < 0 + || static_cast(row) >= service_.addresses().size()) + { + return; + } + + const RegisterAddress address = service_.addresses().at(static_cast(row)); + RegisterMonitorWriteResult result; + if (address.area() == RegisterArea::M) + { + auto *target = qobject_cast(ui_->monitorTable->cellWidget(row, 3)); + if (target == nullptr) + { + return; + } + result = service_.writeBit(address, target->currentData().toBool()); + } + else + { + auto *target = qobject_cast(ui_->monitorTable->cellWidget(row, 3)); + if (target == nullptr) + { + return; + } + bool converted = false; + const int value = target->text().toInt(&converted); + if (!converted || value < -32768 || value > 32767) + { + ui_->monitorTable->item(row, 5)->setText(tr("目标值无效")); + emit operationMessage(tr("写入 D 区地址失败:请输入 -32768~32767 的整数")); + return; + } + result = service_.writeWord(address, static_cast(value)); + } + + QTableWidgetItem *state_item = ui_->monitorTable->item(row, 5); + if (result.succeeded) + { + state_item->setText(tr("写入请求已发送")); + emit operationMessage(tr("%1 写入请求已发送,等待当前值刷新") + .arg(addressText(address))); + return; + } + state_item->setText(tr("写入失败")); + handleResult(tr("写入 %1").arg(addressText(address)), false, result.message); +} + +void FreeMonitorWidget::updateWriteControls() +{ + for (int row = 0; row < ui_->monitorTable->rowCount(); ++row) + { + QWidget *target = ui_->monitorTable->cellWidget(row, 3); + if (target != nullptr) + { + target->setEnabled(write_enabled_); + } + QWidget *write_button = ui_->monitorTable->cellWidget(row, 4); + if (write_button != nullptr) + { + write_button->setEnabled(write_enabled_); + } } } diff --git a/app/src/ui/free_monitor_widget.h b/app/src/ui/free_monitor_widget.h index 759f340..4729149 100644 --- a/app/src/ui/free_monitor_widget.h +++ b/app/src/ui/free_monitor_widget.h @@ -23,6 +23,7 @@ public: QWidget *parent = nullptr); ~FreeMonitorWidget() override; + void setWriteEnabled(bool enabled); void refreshValues(ApplicationMode mode, PlcConnectionState plc_state); void reloadAddresses(); @@ -34,8 +35,11 @@ private: void addAddresses(); void removeSelectedAddresses(); void clearAddresses(); + void writeRow(int row); + void updateWriteControls(); void handleResult(const QString &action, bool succeeded, const std::string &message); std::unique_ptr ui_; RegisterMonitorService &service_; + bool write_enabled_ = false; }; diff --git a/app/src/ui/free_monitor_widget.ui b/app/src/ui/free_monitor_widget.ui index 3957995..38e2953 100644 --- a/app/src/ui/free_monitor_widget.ui +++ b/app/src/ui/free_monitor_widget.ui @@ -3,7 +3,7 @@ FreeMonitorWidget - 420190 + 620190 6 @@ -31,12 +31,14 @@ QAbstractItemView::SelectRows QAbstractItemView::NoEditTriggers true - 4 + 6 0 - 地址 - 类型 - 当前值 - 状态 + 地址 + 类型 + 当前值 + 目标值 + 写入 + 状态 diff --git a/app/src/ui/runtime_monitor_widget.cpp b/app/src/ui/runtime_monitor_widget.cpp index f5294dc..f00224d 100644 --- a/app/src/ui/runtime_monitor_widget.cpp +++ b/app/src/ui/runtime_monitor_widget.cpp @@ -127,8 +127,7 @@ void RuntimeMonitorWidget::setMode( const bool online = mode == ApplicationMode::OnlineRunning; ui_->logicFrame->setVisible(offline); hmi_view_->setRuntimeActive(offline || online); - hmi_view_->setRuntimeWriteEnabled( - offline || (online && plc_state == PlcConnectionState::Connected)); + setHmiWriteEnabled(offline || (online && plc_state == PlcConnectionState::Connected)); if (!offline) { logic_view_->clearRuntimeTrace(); @@ -145,6 +144,7 @@ void RuntimeMonitorWidget::setMode( void RuntimeMonitorWidget::setHmiWriteEnabled(bool enabled) { hmi_view_->setRuntimeWriteEnabled(enabled); + free_monitor_widget_->setWriteEnabled(enabled); } void RuntimeMonitorWidget::refreshValues( diff --git a/app/tests/main_window_tests.cpp b/app/tests/main_window_tests.cpp index 6ef6201..364db30 100644 --- a/app/tests/main_window_tests.cpp +++ b/app/tests/main_window_tests.cpp @@ -1339,6 +1339,49 @@ void testRepeatedPlcStatusNotificationsAreCoalesced() "repeated PLC status callbacks must append one coalesced output message"); } +void testFreeMonitorWritesSingleValues() +{ + VirtualRegisterRepository repository; + RegisterMonitorService monitor_service(repository); + FreeMonitorWidget widget(monitor_service); + widget.show(); + QApplication::processEvents(); + + QLineEdit *address_edit = widget.findChild("addressEdit"); + QPushButton *add_button = widget.findChild("addButton"); + QTableWidget *table = widget.findChild("monitorTable"); + require(address_edit != nullptr && add_button != nullptr && table != nullptr, + "free monitor UI test requires the address, add and table controls"); + address_edit->setText(QStringLiteral("M0")); + add_button->click(); + require(table->rowCount() == 1, "free monitor UI must add an M row before writing"); + + auto *bit_target = qobject_cast(table->cellWidget(0, 3)); + auto *bit_write = qobject_cast(table->cellWidget(0, 4)); + require(bit_target != nullptr && bit_write != nullptr && !bit_write->isEnabled(), + "free monitor writes must be disabled outside a running mode"); + + widget.setWriteEnabled(true); + bit_target->setCurrentIndex(bit_target->findData(true)); + bit_write->click(); + require(repository.readBit({RegisterArea::M, 0}).succeeded + && repository.readBit({RegisterArea::M, 0}).value, + "free monitor UI must write the selected M value"); + + address_edit->setText(QStringLiteral("D0")); + add_button->click(); + require(table->rowCount() == 2, "free monitor UI must add a D row before writing"); + auto *word_target = qobject_cast(table->cellWidget(1, 3)); + auto *word_write = qobject_cast(table->cellWidget(1, 4)); + require(word_target != nullptr && word_write != nullptr && word_write->isEnabled(), + "D monitor rows must expose an enabled write action when permitted"); + word_target->setText(QStringLiteral("-321")); + word_write->click(); + require(repository.readWord({RegisterArea::D, 0}).succeeded + && repository.readWord({RegisterArea::D, 0}).value == -321, + "free monitor UI must write signed D values"); +} + void testOnlineWorkspaceShowsHmiAndFreeMonitorOnly() { TestProjectStorage storage; @@ -1393,6 +1436,7 @@ void testOnlineWorkspaceShowsHmiAndFreeMonitorOnly() "online running must not show a misleading local ladder runtime trace"); QLineEdit *monitor_address = requiredChild(window, "addressEdit"); QPushButton *monitor_add = requiredChild(window, "addButton"); + QTableWidget *monitor_table = requiredChild(window, "monitorTable"); monitor_address->setText(QStringLiteral("D9")); monitor_add->click(); require(std::find( @@ -1400,6 +1444,9 @@ void testOnlineWorkspaceShowsHmiAndFreeMonitorOnly() gateway.poll_addresses.end(), RegisterAddress{RegisterArea::D, 9}) != gateway.poll_addresses.end(), "online free monitor addresses must join the active PLC poll set immediately"); + auto *monitor_write = qobject_cast(monitor_table->cellWidget(0, 4)); + require(monitor_write != nullptr && monitor_write->isEnabled(), + "online Connected mode must enable free monitor writes"); QAction *configure_action = requiredChild(window, "configurePlcAction"); QAction *disconnect_action = requiredChild(window, "disconnectPlcAction"); @@ -1410,6 +1457,8 @@ void testOnlineWorkspaceShowsHmiAndFreeMonitorOnly() require(mode_service.mode() == ApplicationMode::Editing, "a PLC timeout must return online running to editing"); + require(!monitor_write->isEnabled(), + "a PLC communication fault must disable free monitor writes"); require(disconnect_action->isEnabled(), "a timeout must keep disconnect available while the local port remains open"); require(configure_action->isEnabled() @@ -1599,6 +1648,7 @@ int main(int argc, char *argv[]) testEdgeTimerPropertyEditingAndParallelMenu(); testPlcConfigurationUsesDialog(); testRepeatedPlcStatusNotificationsAreCoalesced(); + testFreeMonitorWritesSingleValues(); testOnlineWorkspaceShowsHmiAndFreeMonitorOnly(); testMultiPageAndLogicMainWindowIntegration(); } diff --git a/app/tests/register_monitor_service_tests.cpp b/app/tests/register_monitor_service_tests.cpp index 7d8934f..f007a1e 100644 --- a/app/tests/register_monitor_service_tests.cpp +++ b/app/tests/register_monitor_service_tests.cpp @@ -76,6 +76,35 @@ void testSharedActiveRepositoryValues() "valid cached values must be marked stale during a communication fault"); } +void testRegisterWrites() +{ + VirtualRegisterRepository repository; + RegisterMonitorService service(repository); + const RegisterAddress m0{RegisterArea::M, 0}; + const RegisterAddress d0{RegisterArea::D, 0}; + + const RegisterMonitorWriteResult bit_write = service.writeBit(m0, true); + require(bit_write.succeeded && repository.readBit(m0).value, + "monitor service must write M values through the active repository"); + + const RegisterMonitorWriteResult word_write = service.writeWord(d0, -123); + require(word_write.succeeded && repository.readWord(d0).value == -123, + "monitor service must write signed D values through the active repository"); + + const RegisterMonitorWriteResult wrong_bit = service.writeBit(d0, true); + require(!wrong_bit.succeeded && wrong_bit.error == RegisterError::AreaMismatch, + "monitor bit writes must reject D addresses"); + const RegisterMonitorWriteResult wrong_word = service.writeWord(m0, 123); + require(!wrong_word.succeeded && wrong_word.error == RegisterError::AreaMismatch, + "monitor word writes must reject M addresses"); + + require(service.writeWord(d0, static_cast(-32768)).succeeded + && repository.readWord(d0).value == -32768 + && service.writeWord(d0, static_cast(32767)).succeeded + && repository.readWord(d0).value == 32767, + "monitor word writes must preserve signed 16-bit boundary values"); +} + } // namespace int main() @@ -84,6 +113,7 @@ int main() { testRangeManagement(); testSharedActiveRepositoryValues(); + testRegisterWrites(); } catch (const std::exception &error) {