Bläddra i källkod

feat: 支持数据监控手动切换数据源

main
suyu 3 veckor sedan
förälder
incheckning
f220912a1e
10 ändrade filer med 241 tillägg och 36 borttagningar
  1. +66
    -8
      app/src/services/register_monitor_service.cpp
  2. +26
    -2
      app/src/services/register_monitor_service.h
  3. +89
    -11
      app/src/ui/free_monitor_widget.cpp
  4. +6
    -0
      app/src/ui/free_monitor_widget.h
  5. +3
    -1
      app/src/ui/free_monitor_widget.ui
  6. +7
    -1
      app/src/ui/main_window.cpp
  7. +35
    -6
      app/tests/register_monitor_service_tests.cpp
  8. +3
    -3
      docs/architecture.md
  9. +1
    -1
      docs/测试约定.md
  10. +5
    -3
      docs/用户使用/数据监控与离线初始值说明.md

+ 66
- 8
app/src/services/register_monitor_service.cpp Visa fil

@@ -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);


+ 26
- 2
app/src/services/register_monitor_service.h Visa fil

@@ -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;


+ 89
- 11
app/src/ui/free_monitor_widget.cpp Visa fil

@@ -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)
{


+ 6
- 0
app/src/ui/free_monitor_widget.h Visa fil

@@ -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);
/** 根据当前权限刷新写入控件状态 */


+ 3
- 1
app/src/ui/free_monitor_widget.ui Visa fil

@@ -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>


+ 7
- 1
app/src/ui/main_window.cpp Visa fil

@@ -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);


+ 35
- 6
app/tests/register_monitor_service_tests.cpp Visa fil

@@ -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()


+ 3
- 3
docs/architecture.md Visa fil

@@ -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 通信



+ 1
- 1
docs/测试约定.md Visa fil

@@ -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` | 完整执行器快照经过控制器和运行窗口后的轨迹投影、排队回调、画布框选、对象/行号剪贴板和连续输入 |


+ 5
- 3
docs/用户使用/数据监控与离线初始值说明.md Visa fil

@@ -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 个字,拆块边界不会切开一个多字值。



Laddar…
Avbryt
Spara