Преглед изворни кода

feat: 支持自由监视器单点写入

main
suyu пре 1 месец
родитељ
комит
ae9400f1c1
8 измењених фајлова са 245 додато и 10 уклоњено
  1. +34
    -0
      app/src/services/register_monitor_service.cpp
  2. +11
    -0
      app/src/services/register_monitor_service.h
  3. +106
    -2
      app/src/ui/free_monitor_widget.cpp
  4. +4
    -0
      app/src/ui/free_monitor_widget.h
  5. +8
    -6
      app/src/ui/free_monitor_widget.ui
  6. +2
    -2
      app/src/ui/runtime_monitor_widget.cpp
  7. +50
    -0
      app/tests/main_window_tests.cpp
  8. +30
    -0
      app/tests/register_monitor_service_tests.cpp

+ 34
- 0
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<RegisterAddress> &RegisterMonitorService::addresses() const
{
return model_.addresses();


+ 11
- 0
app/src/services/register_monitor_service.h Прегледај датотеку

@@ -3,6 +3,7 @@
#include "domain/register_monitor_model.h"
#include "domain/register_repository.h"

#include <cstdint>
#include <functional>
#include <string>
#include <vector>
@@ -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<RegisterAddress> &addresses);
RegisterMonitorResult clear();
RegisterMonitorWriteResult writeBit(const RegisterAddress &address, bool value);
RegisterMonitorWriteResult writeWord(
const RegisterAddress &address, std::int16_t value);
const std::vector<RegisterAddress> &addresses() const;
std::vector<MonitorValue> values(bool communication_fault) const;
void setAddressesChangedCallback(std::function<void()> callback);


+ 106
- 2
app/src/ui/free_monitor_widget.cpp Прегледај датотеку

@@ -4,10 +4,15 @@
#include "toolbar_icon_factory.h"
#include "ui_free_monitor_widget.h"

#include <QComboBox>
#include <QHeaderView>
#include <QIntValidator>
#include <QLineEdit>
#include <QMessageBox>
#include <QPushButton>
#include <QTableWidgetItem>

#include <cstdint>
#include <set>

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<std::size_t>(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<std::size_t>(row) >= service_.addresses().size())
{
return;
}

const RegisterAddress address = service_.addresses().at(static_cast<std::size_t>(row));
RegisterMonitorWriteResult result;
if (address.area() == RegisterArea::M)
{
auto *target = qobject_cast<QComboBox *>(ui_->monitorTable->cellWidget(row, 3));
if (target == nullptr)
{
return;
}
result = service_.writeBit(address, target->currentData().toBool());
}
else
{
auto *target = qobject_cast<QLineEdit *>(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<std::int16_t>(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_);
}
}
}



+ 4
- 0
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::FreeMonitorWidget> ui_;
RegisterMonitorService &service_;
bool write_enabled_ = false;
};

+ 8
- 6
app/src/ui/free_monitor_widget.ui Прегледај датотеку

@@ -3,7 +3,7 @@
<class>FreeMonitorWidget</class>
<widget class="QWidget" name="FreeMonitorWidget">
<property name="minimumSize">
<size><width>420</width><height>190</height></size>
<size><width>620</width><height>190</height></size>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<property name="spacing"><number>6</number></property>
@@ -31,12 +31,14 @@
<property name="selectionBehavior"><enum>QAbstractItemView::SelectRows</enum></property>
<property name="editTriggers"><set>QAbstractItemView::NoEditTriggers</set></property>
<property name="showGrid"><bool>true</bool></property>
<property name="columnCount"><number>4</number></property>
<property name="columnCount"><number>6</number></property>
<property name="rowCount"><number>0</number></property>
<column><property name="text"><string>地址</string></property></column>
<column><property name="text"><string>类型</string></property></column>
<column><property name="text"><string>当前值</string></property></column>
<column><property name="text"><string>状态</string></property></column>
<column><property name="text"><string>地址</string></property></column>
<column><property name="text"><string>类型</string></property></column>
<column><property name="text"><string>当前值</string></property></column>
<column><property name="text"><string>目标值</string></property></column>
<column><property name="text"><string>写入</string></property></column>
<column><property name="text"><string>状态</string></property></column>
</widget>
</item>
</layout>


+ 2
- 2
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(


+ 50
- 0
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<QLineEdit *>("addressEdit");
QPushButton *add_button = widget.findChild<QPushButton *>("addButton");
QTableWidget *table = widget.findChild<QTableWidget *>("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<QComboBox *>(table->cellWidget(0, 3));
auto *bit_write = qobject_cast<QPushButton *>(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<QLineEdit *>(table->cellWidget(1, 3));
auto *word_write = qobject_cast<QPushButton *>(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<QLineEdit>(window, "addressEdit");
QPushButton *monitor_add = requiredChild<QPushButton>(window, "addButton");
QTableWidget *monitor_table = requiredChild<QTableWidget>(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<QPushButton *>(monitor_table->cellWidget(0, 4));
require(monitor_write != nullptr && monitor_write->isEnabled(),
"online Connected mode must enable free monitor writes");

QAction *configure_action = requiredChild<QAction>(window, "configurePlcAction");
QAction *disconnect_action = requiredChild<QAction>(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();
}


+ 30
- 0
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<std::int16_t>(-32768)).succeeded
&& repository.readWord(d0).value == -32768
&& service.writeWord(d0, static_cast<std::int16_t>(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)
{


Loading…
Откажи
Сачувај