Просмотр исходного кода

feat: 支持编辑态PLC监控与运行模式切换

main
suyu 3 недель назад
Родитель
Сommit
2ad3c49ccc
7 измененных файлов: 226 добавлений и 41 удалений
  1. +1
    -0
      app/src/main.cpp
  2. +60
    -6
      app/src/services/register_monitor_service.cpp
  3. +16
    -2
      app/src/services/register_monitor_service.h
  4. +17
    -3
      app/src/ui/free_monitor_widget.cpp
  5. +75
    -25
      app/src/ui/main_window.cpp
  6. +7
    -5
      app/src/ui/runtime_monitor_widget.cpp
  7. +50
    -0
      app/tests/register_monitor_service_tests.cpp

+ 1
- 0
app/src/main.cpp Просмотреть файл

@@ -134,6 +134,7 @@ int main(int argc, char *argv[])
RegisterMonitorService register_monitor_service(active_register_repository);
register_monitor_service.setOfflineInitialRepository(offline_initial_repository);
register_monitor_service.setOfflineInitialCaptureEnabled(true);
register_monitor_service.setEditingPlcRepository(plc_register_repository);
// 用虚拟寄存器执行本地梯形图,实现不连接 PLC 的离线仿真
OfflineSimulationService offline_simulation_service(
virtual_register_repository, &offline_initial_repository);


+ 60
- 6
app/src/services/register_monitor_service.cpp Просмотреть файл

@@ -44,6 +44,44 @@ void RegisterMonitorService::setOfflineInitialCaptureEnabled(bool enabled)
capture_offline_initial_values_ = enabled;
}

void RegisterMonitorService::setEditingPlcRepository(
RegisterRepository &repository)
{
editing_plc_repository_ = &repository;
}

void RegisterMonitorService::setEditingPlcState(
bool connected, bool initial_read_completed)
{
editing_plc_connected_ = connected;
editing_plc_ready_ = connected && initial_read_completed;
}

bool RegisterMonitorService::editingPlcConnected() const
{
return usesEditingPlcRepository();
}

bool RegisterMonitorService::editingPlcReady() const
{
return usesEditingPlcRepository() && editing_plc_ready_;
}

bool RegisterMonitorService::usesEditingPlcRepository() const
{
return editing_plc_connected_ && editing_plc_repository_ != nullptr;
}

RegisterRepository &RegisterMonitorService::accessRepository()
{
return usesEditingPlcRepository() ? *editing_plc_repository_ : repository_;
}

const RegisterRepository &RegisterMonitorService::accessRepository() const
{
return usesEditingPlcRepository() ? *editing_plc_repository_ : repository_;
}

RegisterMonitorResult RegisterMonitorService::addRange(
const std::string &start_address, int count)
{
@@ -212,7 +250,11 @@ RegisterMonitorResult RegisterMonitorService::clear()
RegisterMonitorWriteResult RegisterMonitorService::writeBit(
const RegisterAddress &address, bool value)
{
const RegisterWriteResult result = repository_.writeBit(address, value);
if (usesEditingPlcRepository() && !editing_plc_ready_)
{
return {false, RegisterError::Unavailable, "PLC 尚未完成首次读取"};
}
const RegisterWriteResult result = accessRepository().writeBit(address, value);
const RegisterMonitorWriteResult active_result = result.succeeded
? RegisterMonitorWriteResult{true, RegisterError::None, {}}
: RegisterMonitorWriteResult{false, result.error, result.error == RegisterError::AreaMismatch
@@ -228,6 +270,10 @@ RegisterMonitorWriteResult RegisterMonitorService::writeBit(
RegisterMonitorWriteResult RegisterMonitorService::writeNumeric(
const RegisterAddress &address, RegisterDataType data_type, double value)
{
if (usesEditingPlcRepository() && !editing_plc_ready_)
{
return {false, RegisterError::Unavailable, "PLC 尚未完成首次读取"};
}
if (address.isValid() && address.area() != RegisterArea::D)
{
return {false, RegisterError::AreaMismatch,
@@ -247,7 +293,7 @@ RegisterMonitorWriteResult RegisterMonitorService::writeNumeric(
std::string(registerDataTypeDescriptor(data_type).displayName)
+ " 写入值无效"};
}
const RegisterWriteResult result = repository_.writeWords(address, *encoded);
const RegisterWriteResult result = accessRepository().writeWords(address, *encoded);
const RegisterMonitorWriteResult active_result = result.succeeded
? RegisterMonitorWriteResult{true, RegisterError::None, {}}
: RegisterMonitorWriteResult{false, result.error,
@@ -265,7 +311,8 @@ RegisterMonitorWriteResult RegisterMonitorService::captureInitialBit(
const RegisterAddress &address, bool value,
const RegisterMonitorWriteResult &active_result)
{
if (!active_result.succeeded || !capture_offline_initial_values_
if (!active_result.succeeded || usesEditingPlcRepository()
|| !capture_offline_initial_values_
|| offline_initial_repository_ == nullptr)
{
return active_result;
@@ -279,7 +326,8 @@ RegisterMonitorWriteResult RegisterMonitorService::captureInitialWords(
const std::vector<std::int16_t> &values,
const RegisterMonitorWriteResult &active_result)
{
if (!active_result.succeeded || !capture_offline_initial_values_
if (!active_result.succeeded || usesEditingPlcRepository()
|| !capture_offline_initial_values_
|| offline_initial_repository_ == nullptr)
{
return active_result;
@@ -305,9 +353,15 @@ 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_)
{
value.state = MonitorValueState::Unavailable;
result.push_back(value);
continue;
}
if (address.area() == RegisterArea::M)
{
const BitReadResult read = repository_.readBit(address);
const BitReadResult read = accessRepository().readBit(address);
value.bitValue = read.value;
value.state = read.succeeded
? (communication_fault ? MonitorValueState::CommunicationFault
@@ -316,7 +370,7 @@ std::vector<MonitorValue> RegisterMonitorService::values(bool communication_faul
}
else
{
const WordsReadResult read = repository_.readWords(
const WordsReadResult read = accessRepository().readWords(
address, registerDataTypeWordCount(point.dataType));
const std::optional<RegisterNumericValue> decoded = read.succeeded
? decodeRegisterNumericValue(point.dataType, read.values)


+ 16
- 2
app/src/services/register_monitor_service.h Просмотреть файл

@@ -41,7 +41,7 @@ struct RegisterMonitorWriteResult
/**
* @brief 编排自由监控地址列表和寄存器仓库读写
*
* 地址列表和离线初始值只属于当前运行会话,不写入工程文件;仓库由当前运行模式注入
* 地址列表和离线初始值只属于当前运行会话,不写入工程文件;编辑态连接 PLC 后可直接访问 PLC 缓存
*/
class RegisterMonitorService
{
@@ -54,8 +54,16 @@ public:

// 配置编辑态离线初始值仓库;该仓库只保存在当前进程内
void setOfflineInitialRepository(VirtualRegisterRepository &repository);
// 编辑态开启时,监控写入同时更新离线初始值
// 编辑态使用虚拟仓库时,监控写入同时更新离线初始值
void setOfflineInitialCaptureEnabled(bool enabled);
// 配置编辑态可直接访问的 PLC 缓存仓库
void setEditingPlcRepository(RegisterRepository &repository);
// 更新编辑态 PLC 连接和首读状态
void setEditingPlcState(bool connected, bool initial_read_completed);
// 编辑态是否已经连接到 PLC
bool editingPlcConnected() const;
// 编辑态 PLC 是否已完成首轮读取,可执行写入
bool editingPlcReady() const;

/**
* @brief 从起始地址连续添加同一区域的多个地址
@@ -118,6 +126,9 @@ public:
const std::vector<RegisterWordRange> &)> validator);

private:
RegisterRepository &accessRepository();
const RegisterRepository &accessRepository() const;
bool usesEditingPlcRepository() const;
RegisterMonitorWriteResult captureInitialBit(
const RegisterAddress &address, bool value,
const RegisterMonitorWriteResult &active_result);
@@ -131,6 +142,9 @@ private:
const std::vector<MonitorPoint> &points);

RegisterRepository &repository_; // 当前运行模式的寄存器仓库,不由服务拥有
RegisterRepository *editing_plc_repository_ = nullptr;
bool editing_plc_connected_ = false;
bool editing_plc_ready_ = false;
VirtualRegisterRepository *offline_initial_repository_ = nullptr;
bool capture_offline_initial_values_ = false;
RegisterMonitorModel model_; // 当前会话的去重监控地址列表


+ 17
- 3
app/src/ui/free_monitor_widget.cpp Просмотреть файл

@@ -106,8 +106,19 @@ void FreeMonitorWidget::refreshValues(
bool communication_fault = false;
if (mode == ApplicationMode::Editing)
{
source = tr("数据源:离线初始值 · 虚拟 M/D");
if (service_.editingPlcConnected())
{
source = service_.editingPlcReady()
? tr("数据源:PLC 缓存")
: tr("数据源:PLC · 正在首读");
}
else
{
source = tr("数据源:离线初始值 · 虚拟 M/D");
}
values_available = true;
communication_fault = service_.editingPlcConnected()
&& !service_.editingPlcReady();
}
else if (mode == ApplicationMode::OfflineRunning)
{
@@ -138,8 +149,11 @@ void FreeMonitorWidget::refreshValues(
if (value.state == MonitorValueState::Unavailable)
{
value_item->setText(QStringLiteral("--"));
state_item->setText(mode == ApplicationMode::OnlineRunning
? tr("等待读取") : tr("不可用"));
state_item->setText(
mode == ApplicationMode::OnlineRunning
|| (mode == ApplicationMode::Editing
&& service_.editingPlcConnected())
? tr("等待读取") : tr("不可用"));
continue;
}
if (value.address.area() == RegisterArea::M)


+ 75
- 25
app/src/ui/main_window.cpp Просмотреть файл

@@ -1149,6 +1149,14 @@ void MainWindow::configureRuntimeMonitor()
}
});
runtime_panel_controller_->configure();
runtime_panel_controller_->configureUserRuntimeMode(
false,
[this](int mode)
{
requestMode(static_cast<ApplicationMode>(mode));
},
{},
{});
runtime_monitor_widget_ = runtime_panel_controller_->runtimeMonitorWidget();
}

@@ -1214,15 +1222,23 @@ void MainWindow::refreshDataMonitorUi()
return;
}
const ApplicationMode mode = runtime_mode_service_.mode();
const PlcConnectionState plc_state = runtime_mode_service_.plcConnectionState();
const bool editing_plc_connected = mode == ApplicationMode::Editing
&& plc_state != PlcConnectionState::Disconnected
&& plc_state != PlcConnectionState::Faulted;
register_monitor_service_.setEditingPlcState(
editing_plc_connected,
editing_plc_connected && runtime_mode_service_.initialPlcReadCompleted());
const bool online_connected = mode == ApplicationMode::OnlineRunning
&& runtime_mode_service_.plcConnectionState()
== PlcConnectionState::Connected;
&& plc_state == PlcConnectionState::Connected;
data_monitor_widget_->setWriteEnabled(
mode == ApplicationMode::Editing
(mode == ApplicationMode::Editing
&& (!editing_plc_connected
|| register_monitor_service_.editingPlcReady()))
|| mode == ApplicationMode::OfflineRunning
|| online_connected);
data_monitor_widget_->refreshValues(
mode, runtime_mode_service_.plcConnectionState());
mode, plc_state);
}

void MainWindow::configureProjectTree()
@@ -2397,29 +2413,57 @@ bool MainWindow::requestMode(ApplicationMode requested_mode)
// UI 动作只提出目标模式,所有前置条件和仓库切换由 RuntimeModeService 决定
// UI 仅转发模式意图,合法性由服务层和领域状态机决定
ModeTransitionResult result;
switch (requested_mode)
{
case ApplicationMode::Editing:
{
result = runtime_mode_service_.enterEditing();
break;
}
case ApplicationMode::OfflineRunning:
{
result = runtime_mode_service_.enterOfflineRunning();
break;
}
case ApplicationMode::OnlineRunning:
{
result = runtime_mode_service_.enterOnlineRunning();
break;
const ApplicationMode current_mode = runtime_mode_service_.mode();
if ((requested_mode == ApplicationMode::OfflineRunning
|| requested_mode == ApplicationMode::OnlineRunning)
&& current_mode != ApplicationMode::Editing
&& current_mode != requested_mode)
{
// 运行模式切换统一经过编辑态;先检查真机首读,失败时保持原运行态
if (requested_mode == ApplicationMode::OnlineRunning
&& !runtime_mode_service_.initialPlcReadCompleted())
{
result = {false, ModeTransitionError::InitialPlcReadRequired, {}};
}
else
{
const ModeTransitionResult editing = runtime_mode_service_.enterEditing();
if (!editing.succeeded)
{
restoreCurrentModeAction();
return false;
}
result = requested_mode == ApplicationMode::OfflineRunning
? runtime_mode_service_.enterOfflineRunning()
: runtime_mode_service_.enterOnlineRunning();
}
}
default:
else
{
restoreCurrentModeAction();
statusBar()->showMessage(tr("不支持的运行模式"), 5000);
return false;
}
switch (requested_mode)
{
case ApplicationMode::Editing:
{
result = runtime_mode_service_.enterEditing();
break;
}
case ApplicationMode::OfflineRunning:
{
result = runtime_mode_service_.enterOfflineRunning();
break;
}
case ApplicationMode::OnlineRunning:
{
result = runtime_mode_service_.enterOnlineRunning();
break;
}
default:
{
restoreCurrentModeAction();
statusBar()->showMessage(tr("不支持的运行模式"), 5000);
return false;
}
}
}
const bool runtime_request = requested_mode == ApplicationMode::OfflineRunning
|| requested_mode == ApplicationMode::OnlineRunning;
@@ -2437,6 +2481,12 @@ bool MainWindow::requestMode(ApplicationMode requested_mode)
if (!result.succeeded)
{
restoreCurrentModeAction();
if (runtime_monitor_widget_ != nullptr && runtime_request)
{
runtime_monitor_widget_->setMode(
runtime_mode_service_.mode(),
runtime_mode_service_.plcConnectionState());
}
QString message = transitionErrorText(result.error);
if (result.error == ModeTransitionError::InitialPlcReadRequired)
{


+ 7
- 5
app/src/ui/runtime_monitor_widget.cpp Просмотреть файл

@@ -104,7 +104,7 @@ RuntimeMonitorWidget::RuntimeMonitorWidget(
this,
[this](int index)
{
if (user_runtime_mode_ && index >= 0)
if (index >= 0)
{
emit modeChangeRequested(index == 0
? static_cast<int>(ApplicationMode::OfflineRunning)
@@ -177,11 +177,12 @@ void RuntimeMonitorWidget::setMode(
offline ? tr("离线仿真 · 虚拟 M/D")
: online ? tr("真机运行 · PLC 缓存 · 本地推算轨迹")
: tr("当前未运行"));
if (user_runtime_mode_)
{
const QSignalBlocker blocker(ui_->runtimeModeComboBox);
ui_->runtimeModeComboBox->setCurrentIndex(
offline ? 0 : online ? 1 : -1);
ui_->runtimeModeComboBox->setCurrentIndex(offline ? 0 : online ? 1 : -1);
}
if (user_runtime_mode_)
{
const bool plc_configurable = plc_state == PlcConnectionState::Disconnected
|| plc_state == PlcConnectionState::Faulted;
ui_->runtimePlcButton->setEnabled(plc_configurable);
@@ -194,7 +195,8 @@ void RuntimeMonitorWidget::setMode(
void RuntimeMonitorWidget::setUserRuntimeMode(bool enabled)
{
user_runtime_mode_ = enabled;
ui_->runtimeModeComboBox->setVisible(enabled);
// 普通编程器和导出的运行程序都允许在运行窗口切换离线/真机
ui_->runtimeModeComboBox->setVisible(true);
ui_->runtimePlcButton->setVisible(enabled);
ui_->runtimeDisconnectPlcButton->setVisible(enabled);
ui_->exitRuntimeButton->setToolTip(


+ 50
- 0
app/tests/register_monitor_service_tests.cpp Просмотреть файл

@@ -77,6 +77,55 @@ void testSharedActiveRepositoryValues()
"valid cached values must be marked stale during a communication fault");
}

void testEditingPlcMonitorAccess()
{
VirtualRegisterRepository virtual_repository;
VirtualRegisterRepository plc_repository;
ActiveRegisterRepository active_repository(virtual_repository);
VirtualRegisterRepository initial_repository;
RegisterMonitorService service(active_repository);
service.setOfflineInitialRepository(initial_repository);
service.setOfflineInitialCaptureEnabled(true);
service.setEditingPlcRepository(plc_repository);
require(service.addRange("M0", 1).succeeded
&& service.addRange("D0", 1).succeeded,
"editing PLC monitor fixture must accept M and D points");

virtual_repository.writeBit({RegisterArea::M, 0}, false);
virtual_repository.writeWord({RegisterArea::D, 0}, 11);
plc_repository.writeBit({RegisterArea::M, 0}, true);
plc_repository.writeWord({RegisterArea::D, 0}, 22);

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

service.setEditingPlcState(true, true);
values = service.values(false);
require(values[0].state == MonitorValueState::Valid && values[0].bitValue
&& values[1].state == MonitorValueState::Valid
&& std::get<std::int16_t>(values[1].numericValue) == 22,
"editing PLC monitor must display PLC cache values after first read");
require(service.writeBit({RegisterArea::M, 0}, false).succeeded
&& service.writeNumeric(
{RegisterArea::D, 0}, RegisterDataType::Int16, 33).succeeded
&& !plc_repository.readBit({RegisterArea::M, 0}).value
&& plc_repository.readWord({RegisterArea::D, 0}).value == 33
&& !initial_repository.readBit({RegisterArea::M, 0}).value
&& initial_repository.readWord({RegisterArea::D, 0}).value == 0,
"editing PLC monitor writes must target PLC and skip offline capture");

service.setEditingPlcState(false, false);
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");
}

void testRegisterWrites()
{
VirtualRegisterRepository repository;
@@ -264,6 +313,7 @@ int main()
{
testRangeManagement();
testSharedActiveRepositoryValues();
testEditingPlcMonitorAccess();
testRegisterWrites();
testOfflineInitialValueCapture();
testMultiWordMonitoring();


Загрузка…
Отмена
Сохранить