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

feat: 增加离线运行前工程配置校验

main
suyu пре 1 месец
родитељ
комит
bf417e8dc6
21 измењених фајлова са 300 додато и 40 уклоњено
  1. +56
    -1
      app/src/domain/control_logic_model.cpp
  2. +5
    -0
      app/src/domain/control_logic_model.h
  3. +36
    -2
      app/src/domain/hmi_model.cpp
  4. +2
    -0
      app/src/domain/hmi_model.h
  5. +23
    -0
      app/src/domain/project_model.cpp
  6. +1
    -0
      app/src/domain/project_model.h
  7. +2
    -1
      app/src/domain/runtime_state.h
  8. +8
    -0
      app/src/infrastructure/json_project_storage.cpp
  9. +1
    -1
      app/src/main.cpp
  10. +4
    -0
      app/src/services/logic_editor_service.cpp
  11. +12
    -0
      app/src/services/runtime_mode_service.cpp
  12. +5
    -4
      app/src/services/runtime_mode_service.h
  13. +8
    -0
      app/src/ui/hmi_editor_widget.cpp
  14. +18
    -6
      app/src/ui/logic_editor_widget.cpp
  15. +27
    -19
      app/src/ui/main_window.cpp
  16. +18
    -2
      app/tests/domain_tests.cpp
  17. +13
    -0
      app/tests/logic_editor_service_tests.cpp
  18. +26
    -3
      app/tests/main_window_tests.cpp
  19. +19
    -1
      app/tests/runtime_mode_service_tests.cpp
  20. +11
    -0
      app/tests/runtime_mode_service_tests.pro
  21. +5
    -0
      docs/architecture.md

+ 56
- 1
app/src/domain/control_logic_model.cpp Прегледај датотеку

@@ -101,6 +101,11 @@ bool LogicNode::validate(std::string *error) const
config);
}

bool LogicNode::isConfigured() const
{
return configured;
}

bool LogicNode::isCondition() const
{
return !std::holds_alternative<CoilNodeConfig>(config);
@@ -138,7 +143,29 @@ bool LadderStage::validate(std::string *error) const
return true;
}

bool LadderStage::validateForRunning(std::string *error) const
{
if (!validate(error))
{
return false;
}
for (const LogicNode &node : branches)
{
if (!node.isConfigured())
{
setError(error, "ladder condition " + node.id + " is not configured");
return false;
}
}
return true;
}

bool LadderRung::validate(std::string *error) const
{
return validateStructure(error);
}

bool LadderRung::validateForRunning(std::string *error) const
{
if (!validateStructure(error))
{
@@ -150,7 +177,19 @@ bool LadderRung::validate(std::string *error) const
}
if (stages.empty() || !output.has_value())
{
setError(error, "non-empty ladder rung requires conditions and an output coil");
setError(error, "incomplete ladder network requires conditions and an output coil");
return false;
}
for (const LadderStage &stage : stages)
{
if (!stage.validateForRunning(error))
{
return false;
}
}
if (!output->isConfigured())
{
setError(error, "output coil " + output->id + " is not configured");
return false;
}
return true;
@@ -257,3 +296,19 @@ bool ControlLogic::validateStructure(std::string *error) const
}
return true;
}

bool ControlLogic::validateForRunning(std::string *error) const
{
if (!validateStructure(error))
{
return false;
}
for (const LadderRung &rung : rungs)
{
if (!rung.validateForRunning(error))
{
return false;
}
}
return true;
}

+ 5
- 0
app/src/domain/control_logic_model.h Прегледај датотеку

@@ -56,8 +56,10 @@ struct LogicNode
{
std::string id;
LogicNodeConfig config;
bool configured = true;

bool validate(std::string *error = nullptr) const;
bool isConfigured() const;
bool isCondition() const;
bool isOutput() const;
};
@@ -69,6 +71,7 @@ struct LadderStage
std::vector<LogicNode> branches;

bool validate(std::string *error = nullptr) const;
bool validateForRunning(std::string *error = nullptr) const;
};

// 梯级中的各级按顺序串联,输出线圈固定在最右侧
@@ -81,6 +84,7 @@ struct LadderRung

bool validate(std::string *error = nullptr) const;
bool validateStructure(std::string *error = nullptr) const;
bool validateForRunning(std::string *error = nullptr) const;
};

struct ControlLogic
@@ -92,4 +96,5 @@ struct ControlLogic

bool validate(std::string *error = nullptr) const;
bool validateStructure(std::string *error = nullptr) const;
bool validateForRunning(std::string *error = nullptr) const;
};

+ 36
- 2
app/src/domain/hmi_model.cpp Прегледај датотеку

@@ -49,7 +49,7 @@ bool HmiControl::validate(std::string *error) const
}
if (requiresBitBinding(type))
{
if (!binding.has_value() || binding->area() != RegisterArea::M)
if (binding.has_value() && binding->area() != RegisterArea::M)
{
setError(error, "button and indicator controls require an M address");
return false;
@@ -57,7 +57,7 @@ bool HmiControl::validate(std::string *error) const
}
if (requiresWordBinding(type))
{
if (!binding.has_value() || binding->area() != RegisterArea::D)
if (binding.has_value() && binding->area() != RegisterArea::D)
{
setError(error, "numeric controls require a D address");
return false;
@@ -71,6 +71,23 @@ bool HmiControl::validate(std::string *error) const
return true;
}

bool HmiControl::isConfigured() const
{
if (type == HmiControlType::Label)
{
return true;
}
if (!binding.has_value() || !binding->isValid())
{
return false;
}
if (requiresBitBinding(type))
{
return binding->area() == RegisterArea::M;
}
return !requiresWordBinding(type) || binding->area() == RegisterArea::D;
}

bool HmiPage::validate(std::string *error) const
{
if (id.empty() || name.empty())
@@ -109,3 +126,20 @@ bool HmiPage::validate(std::string *error) const
}
return true;
}

bool HmiPage::validateForRunning(std::string *error) const
{
if (!validate(error))
{
return false;
}
for (const HmiControl &control : controls)
{
if (!control.isConfigured())
{
setError(error, "HMI control " + control.id + " is not bound to a register");
return false;
}
}
return true;
}

+ 2
- 0
app/src/domain/hmi_model.h Прегледај датотеку

@@ -66,6 +66,7 @@ struct HmiControl
* 按钮和指示灯必须绑定有效 M 地址,数值显示和数值输入必须绑定有效 D 地址
*/
bool validate(std::string *error = nullptr) const;
bool isConfigured() const;
};

/**
@@ -87,4 +88,5 @@ struct HmiPage
* @return 页面及其全部控件有效时返回 true
*/
bool validate(std::string *error = nullptr) const;
bool validateForRunning(std::string *error = nullptr) const;
};

+ 23
- 0
app/src/domain/project_model.cpp Прегледај датотеку

@@ -69,3 +69,26 @@ bool Project::validate(std::string *error) const
}
return true;
}

bool Project::validateForRunning(std::string *error) const
{
if (!validate(error))
{
return false;
}
for (const HmiPage &page : hmiPages)
{
if (!page.validateForRunning(error))
{
return false;
}
}
for (const ControlLogic &logic : controlLogics)
{
if (!logic.validateForRunning(error))
{
return false;
}
}
return true;
}

+ 1
- 0
app/src/domain/project_model.h Прегледај датотеку

@@ -29,4 +29,5 @@ struct Project

// 校验工程配置并通过 error 返回失败原因
bool validate(std::string *error = nullptr) const;
bool validateForRunning(std::string *error = nullptr) const;
};

+ 2
- 1
app/src/domain/runtime_state.h Прегледај датотеку

@@ -74,7 +74,8 @@ enum class ModeTransitionError
None, // 切换成功
AlreadyInRequestedMode, // 已经处于目标模式
MustReturnToEditing, // 必须先返回编辑态
InitialPlcReadRequired // 进入真机态前必须完成 PLC 初次读取
InitialPlcReadRequired, // 进入真机态前必须完成 PLC 初次读取
ProjectNotReady // 工程存在未配置或未完成的运行项
};

// 模式切换操作结果


+ 8
- 0
app/src/infrastructure/json_project_storage.cpp Прегледај датотеку

@@ -719,6 +719,7 @@ QJsonObject serializeLogicNode(const LogicNode &node)
{
QJsonObject object;
object.insert(QStringLiteral("id"), fromUtf8(node.id));
object.insert(QStringLiteral("configured"), node.configured);
// std::visit 将不同节点配置统一转换为 config JSON 对象
object.insert(
QStringLiteral("config"),
@@ -820,6 +821,13 @@ bool parseLogicNode(
{
return false;
}
// 1.0 旧工程没有 configured 字段,按历史默认地址视为已配置
node->configured = true;
if (object.contains(QStringLiteral("configured"))
&& !readBool(object, "configured", context, &node->configured, state))
{
return false;
}
return true;
}



+ 1
- 1
app/src/main.cpp Прегледај датотеку

@@ -30,7 +30,7 @@ int main(int argc, char *argv[])
// 当前离线模式使用内存仓库,后续真机模式替换为 PLC 缓存实现
VirtualRegisterRepository virtual_register_repository;
HmiRuntimeService hmi_runtime_service(virtual_register_repository);
RuntimeModeService runtime_mode_service;
RuntimeModeService runtime_mode_service(project_service);
MainWindow main_window(
runtime_mode_service,
project_service,


+ 4
- 0
app/src/services/logic_editor_service.cpp Прегледај датотеку

@@ -14,6 +14,7 @@ LogicNode makeNode(const std::string &id, const LogicNodeConfig &config)
LogicNode node;
node.id = id;
node.config = config;
node.configured = false;
return node;
}

@@ -367,6 +368,7 @@ LogicEditorResult LogicEditorService::updateNodeConfig(
"node category cannot be changed after creation");
}
LogicNode candidate = makeNode(node_id, config);
candidate.configured = true;
std::string error;
if (!candidate.validate(&error))
{
@@ -388,6 +390,7 @@ LogicEditorResult LogicEditorService::updateNodeConfig(
if (editable.id == node_id)
{
editable.config = config;
editable.configured = true;
return {true, LogicEditorError::None, {}, node_id};
}
}
@@ -395,6 +398,7 @@ LogicEditorResult LogicEditorService::updateNodeConfig(
if (rung.output.has_value() && rung.output->id == node_id)
{
rung.output->config = config;
rung.output->configured = true;
return {true, LogicEditorError::None, {}, node_id};
}
}


+ 12
- 0
app/src/services/runtime_mode_service.cpp Прегледај датотеку

@@ -8,6 +8,13 @@

#include "runtime_mode_service.h"

#include "project_service.h"

RuntimeModeService::RuntimeModeService(const ProjectService &project_service)
: project_service_(project_service)
{
}

ApplicationMode RuntimeModeService::mode() const
{
return state_.mode();
@@ -25,6 +32,11 @@ ModeTransitionResult RuntimeModeService::enterEditing()

ModeTransitionResult RuntimeModeService::enterOfflineRunning()
{
std::string error;
if (!project_service_.project().validateForRunning(&error))
{
return {false, ModeTransitionError::ProjectNotReady};
}
return state_.enterOfflineRunning();
}



+ 5
- 4
app/src/services/runtime_mode_service.h Прегледај датотеку

@@ -10,14 +10,14 @@

#include "domain/runtime_state.h"

class ProjectService;

// 隔离 UI 与领域状态机并编排进入真机运行态的前置条件
class RuntimeModeService
{
public:
/**
* @brief 获取当前应用运行模式
* @return 当前的编辑、离线运行或真机运行模式
*/
explicit RuntimeModeService(const ProjectService &project_service);

ApplicationMode mode() const;
/**
* @brief 获取当前模式下允许的工程编辑和寄存器使用策略
@@ -55,6 +55,7 @@ public:
bool initialPlcReadCompleted() const;

private:
const ProjectService &project_service_;
RuntimeState state_;
// 表示 PLC 缓存是否已通过至少一次有效读取建立
bool initial_plc_read_completed_ = false;


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

@@ -138,6 +138,14 @@ public:
painter->setPen(QPen(QColor(QStringLiteral("#1677a8")), 2));
painter->drawRect(boundingRect().adjusted(0, 0, -1, -1));
}
if (!control_.isConfigured())
{
painter->setPen(QColor(QStringLiteral("#b54708")));
painter->drawText(
rect.adjusted(2, rect.height() - 15, -2, 0),
Qt::AlignCenter,
QStringLiteral("未绑定"));
}
}

// 向场景和外部控件返回该图元对应的领域控件标识


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

@@ -93,6 +93,12 @@ QString nodeToolTip(const LogicNodeConfig &config)
config);
}

QString nodeAddressText(const RegisterAddress &address, bool configured)
{
return configured ? registerAddressText(address)
: LogicEditorWidget::tr("< M 地址 >");
}

qreal rungHeight(const LadderRung &rung)
{
std::size_t maximum_branches = 1U;
@@ -139,12 +145,15 @@ public:
: node_id_(node.id),
rung_id_(rung_id),
stage_id_(stage_id),
config_(node.config)
config_(node.config),
configured_(node.configured)
{
setPos(center);
setFlag(ItemIsSelectable, true);
setZValue(1.0);
setToolTip(nodeToolTip(config_));
setToolTip(configured_ ? nodeToolTip(config_)
: LogicEditorWidget::tr(
"待配置:请在属性区设置地址和参数"));
}

QRectF boundingRect() const override
@@ -185,7 +194,7 @@ public:
painter->drawText(
QRectF(-kNodeWidth / 2.0, -44, kNodeWidth, 18),
Qt::AlignCenter,
registerAddressText(contact->address));
nodeAddressText(contact->address, configured_));
}
else if (const auto *coil = std::get_if<CoilNodeConfig>(&config_))
{
@@ -212,7 +221,7 @@ public:
painter->drawText(
QRectF(-kNodeWidth / 2.0, -44, kNodeWidth, 18),
Qt::AlignCenter,
registerAddressText(coil->address));
nodeAddressText(coil->address, configured_));
}
else if (const auto *comparison = std::get_if<CompareNodeConfig>(&config_))
{
@@ -232,11 +241,13 @@ public:
painter->drawText(
QRectF(-kNodeWidth / 2.0, -44, kNodeWidth, 18),
Qt::AlignCenter,
registerAddressText(comparison->address));
configured_ ? registerAddressText(comparison->address)
: tr("< D 地址 >"));
painter->drawText(
QRectF(-kNodeWidth / 2.0, 22, kNodeWidth, 18),
Qt::AlignCenter,
QString::number(comparison->value));
configured_ ? QString::number(comparison->value)
: LogicEditorWidget::tr("< 常量 >"));
}
}

@@ -249,6 +260,7 @@ private:
std::string rung_id_;
std::string stage_id_;
LogicNodeConfig config_;
bool configured_ = true;
};

class LogicEditorWidget::RungItem final : public QGraphicsItem


+ 27
- 19
app/src/ui/main_window.cpp Прегледај датотеку

@@ -74,6 +74,10 @@ QString transitionErrorText(ModeTransitionError error)
{
return MainWindow::tr("当前已处于所选模式");
}
case ModeTransitionError::ProjectNotReady:
{
return MainWindow::tr("工程存在未配置或未完成的 HMI/梯形图节点,暂不能进入离线运行态");
}
case ModeTransitionError::None:
default:
{
@@ -121,11 +125,6 @@ QString controlTypeText(HmiControlType type)
}
}

bool usesMAddress(HmiControlType type)
{
return type == HmiControlType::Button || type == HmiControlType::Indicator;
}

} // namespace

MainWindow::MainWindow(
@@ -481,6 +480,7 @@ void MainWindow::configurePropertyEditor()
}
binding_area_combo_box_ = new QComboBox(ui_->propertiesPage);
binding_area_combo_box_->setObjectName(QStringLiteral("bindingAreaComboBox"));
binding_area_combo_box_->addItem(tr("未绑定"), QVariant::fromValue(-1));
binding_area_combo_box_->addItem(QStringLiteral("M"), QVariant::fromValue(0));
binding_area_combo_box_->addItem(QStringLiteral("D"), QVariant::fromValue(1));
binding_index_spin_box_ = new QSpinBox(ui_->propertiesPage);
@@ -613,7 +613,9 @@ void MainWindow::showControlProperties(const std::string &control_id)

// 更新属性面板顶部状态标签,显示选中控件ID或提示未选择
ui_->selectionValueLabel->setText(
has_control ? fromUtf8(control->id) : tr("未选择"));
has_control ? fromUtf8(control->id)
+ (control->isConfigured() ? QString{} : tr("(未绑定)"))
: tr("未选择"));

// 批量控制所有属性编辑组件可用性:无选中控件时整体置灰,禁止编辑
for (QWidget *widget : {static_cast<QWidget *>(control_id_edit_),
@@ -646,15 +648,9 @@ void MainWindow::showControlProperties(const std::string &control_id)
control_height_spin_box_->setValue(control->bounds.height);

// 根据控件类型获取默认寄存器区域:按钮/指示灯默认M区;数值类控件默认D区
const RegisterArea default_area = usesMAddress(control->type)
? RegisterArea::M : RegisterArea::D;
// 优先使用控件已绑定的寄存器区域;无绑定则使用类型对应的默认区域
const RegisterArea area = control->binding.has_value()
? control->binding->area() : default_area;

// 同步寄存器区域下拉框选中项(0=M,1=D,与combo初始化顺序严格对应)
binding_area_combo_box_->setCurrentIndex(area == RegisterArea::M ? 0 : 1);
// 填充寄存器地址索引;无绑定时默认地址为0
// 未绑定必须保留明确状态,避免属性编辑把显示默认值误提交为 M0/D0
binding_area_combo_box_->setCurrentIndex(!control->binding.has_value() ? 0
: control->binding->area() == RegisterArea::M ? 1 : 2);
binding_index_spin_box_->setValue(
control->binding.has_value() ? control->binding->index() : 0);
}
@@ -737,6 +733,11 @@ void MainWindow::showLogicNodeProperties(const std::string &node_id)
logic_value_spin_box_->setEnabled(true);
logic_value_spin_box_->setValue(compare.value);
}
if (!node->configured)
{
logic_node_type_label_->setText(tr("待配置:%1").arg(
logic_node_type_label_->text()));
}
}

void MainWindow::addHmiControl(HmiControlType type)
@@ -790,9 +791,16 @@ void MainWindow::applySelectedControlProperties()
control.bounds.y = control_y_spin_box_->value();
control.bounds.width = control_width_spin_box_->value();
control.bounds.height = control_height_spin_box_->value();
const RegisterArea area = binding_area_combo_box_->currentIndex() == 0
? RegisterArea::M : RegisterArea::D;
control.binding = RegisterAddress{area, binding_index_spin_box_->value()};
const int binding_area = binding_area_combo_box_->currentData().toInt();
if (binding_area < 0)
{
control.binding.reset();
}
else
{
const RegisterArea area = binding_area == 0 ? RegisterArea::M : RegisterArea::D;
control.binding = RegisterAddress{area, binding_index_spin_box_->value()};
}
const HmiEditorResult result = hmi_editor_service_.updateControl(
hmi_editor_service_.firstPageId(), selected_control_id_, control);
if (!result.succeeded)
@@ -907,7 +915,7 @@ void MainWindow::applySelectedLogicNodeProperties()
logic_editor_widget_->selectNode(selected_logic_node_id_);
showLogicNodeProperties(selected_logic_node_id_);
refreshProjectUi();
statusBar()->showMessage(tr("逻辑节点属性已更新"), 3000);
statusBar()->showMessage(tr("逻辑节点属性已更新,节点已配置"), 3000);
}

void MainWindow::createNewProject()


+ 18
- 2
app/tests/domain_tests.cpp Прегледај датотеку

@@ -170,13 +170,17 @@ void testLadderLogicBoundaries()
require(!logic.validate(), "a ladder output must be a coil");

logic.rungs.front().output.reset();
require(!logic.validate(), "conditions without an output must fail full validation");
require(logic.validate(), "incomplete ladder may remain in an editable draft");
require(!logic.validateForRunning(),
"conditions without an output must block runtime validation");

LadderRung empty_rung{"rung-empty", "Empty network", {}, std::nullopt};
require(empty_rung.validate(), "an empty editing network must be valid");

empty_rung.output = coil;
require(!empty_rung.validate(), "an output without conditions must be rejected");
require(empty_rung.validate(), "output-only network may remain in an editable draft");
require(!empty_rung.validateForRunning(),
"an output without conditions must block runtime validation");

logic.rungs.front().output = coil;
logic.rungs.front().stages.at(1).branches.at(1).id = start.id;
@@ -208,6 +212,18 @@ void testModelsValidateBindingsAndIdentifiers()
project = makeValidProject();
project.hmiPages.front().controls.front().properties.emplace("", "value");
require(!project.validate(), "empty HMI property names must be rejected");

project = makeValidProject();
project.hmiPages.front().controls.front().binding.reset();
require(project.validate(), "unbound HMI control must be accepted in a draft");
require(!project.validateForRunning(),
"unbound HMI control must block runtime validation");

project = makeValidProject();
project.controlLogics.front().rungs.front().output->configured = false;
require(project.validate(), "unconfigured ladder node must be accepted in a draft");
require(!project.validateForRunning(),
"unconfigured ladder node must block runtime validation");
}

void testRuntimeStateBoundaries()


+ 13
- 0
app/tests/logic_editor_service_tests.cpp Прегледај датотеку

@@ -75,6 +75,19 @@ void testEditorOperations()
require(hold_result.succeeded && coil_result.succeeded,
"parallel hold contact and output coil must be added");

require(!service.findNode(logic_id, stop_result.id)->configured,
"new logic nodes must remain unconfigured until properties are applied");
require(service.updateNodeConfig(
logic_id,
stop_result.id,
ContactNodeConfig{
RegisterAddress{RegisterArea::M, 1},
ContactMode::NormallyClosed})
.succeeded,
"applying logic node properties must succeed");
require(service.findNode(logic_id, stop_result.id)->configured,
"applying logic node properties must mark the node configured");

const LadderRung *rung = service.findRung(logic_id, rung_id);
require(rung != nullptr && rung->stages.size() == 2,
"series conditions must occupy ordered stages");


+ 26
- 3
app/tests/main_window_tests.cpp Прегледај датотеку

@@ -62,7 +62,7 @@ void testModeActionsControlEditingAvailability()
LogicEditorService logic_editor_service(project_service);
VirtualRegisterRepository repository;
HmiRuntimeService runtime_service(repository);
RuntimeModeService mode_service;
RuntimeModeService mode_service(project_service);
MainWindow window(
mode_service,
project_service,
@@ -105,8 +105,8 @@ void testModeActionsControlEditingAvailability()
const std::string page_id = editor_service.firstPageId();
require(editor_service.findPage(page_id)->controls.size() == 1,
"adding a control must update the HMI page model");
require(selection->text() == QStringLiteral("button-1"),
"selecting an added control must update the property panel");
require(selection->text() == QStringLiteral("button-1(未绑定)"),
"an unbound added control must be marked in the property panel");
require(text_edit->text() == QStringLiteral("按钮"),
"property panel must show the control text");
delete_control_action->trigger();
@@ -128,6 +128,29 @@ void testModeActionsControlEditingAvailability()
"coil action must set the fixed ladder output");

offline_action->trigger();
require(mode_service.mode() == ApplicationMode::Editing,
"unconfigured ladder nodes must block offline running");
const LadderRung &rung = logic->rungs.front();
for (const LogicNode &node : rung.stages.front().branches)
{
require(logic_editor_service.updateNodeConfig(
logic_editor_service.firstLogicId(),
node.id,
ContactNodeConfig{
RegisterAddress{RegisterArea::M, 0},
ContactMode::NormallyOpen})
.succeeded,
"applying a contact configuration must complete the ladder node");
}
require(logic_editor_service.updateNodeConfig(
logic_editor_service.firstLogicId(),
rung.output->id,
CoilNodeConfig{
RegisterAddress{RegisterArea::M, 1},
CoilMode::Normal})
.succeeded,
"applying a coil configuration must complete the ladder node");
offline_action->trigger();
require(mode_service.mode() == ApplicationMode::OfflineRunning,
"offline action must enter offline running");
require(!project_dock->isEnabled(),


+ 19
- 1
app/tests/runtime_mode_service_tests.cpp Прегледај датотеку

@@ -1,4 +1,6 @@
#include "services/runtime_mode_service.h"
#include "services/project_service.h"
#include "domain/project_storage.h"

#include <iostream>
#include <stdexcept>
@@ -6,6 +8,20 @@

namespace {

class TestProjectStorage final : public ProjectStorage
{
public:
ProjectSaveResult save(const Project &, const std::string &) override
{
return {true, ProjectStorageError::None, {}};
}

ProjectLoadResult load(const std::string &) override
{
return {false, {}, ProjectStorageError::FileReadFailed, {}};
}
};

void require(bool condition, const std::string &message)
{
if (!condition)
@@ -17,7 +33,9 @@ void require(bool condition, const std::string &message)
void testModeTransitions()
{
// 验证服务将 PLC 首读状态与领域模式切换规则正确组合
RuntimeModeService service;
TestProjectStorage storage;
ProjectService project_service(storage);
RuntimeModeService service(project_service);

require(service.mode() == ApplicationMode::Editing,
"service must start in editing mode");


+ 11
- 0
app/tests/runtime_mode_service_tests.pro Прегледај датотеку

@@ -10,9 +10,20 @@ INCLUDEPATH += ../src

SOURCES += \
runtime_mode_service_tests.cpp \
../src/domain/register_address.cpp \
../src/domain/hmi_model.cpp \
../src/domain/control_logic_model.cpp \
../src/domain/project_model.cpp \
../src/domain/runtime_state.cpp \
../src/services/project_service.cpp \
../src/services/runtime_mode_service.cpp

HEADERS += \
../src/domain/register_address.h \
../src/domain/hmi_model.h \
../src/domain/control_logic_model.h \
../src/domain/project_model.h \
../src/domain/project_storage.h \
../src/domain/runtime_state.h \
../src/services/project_service.h \
../src/services/runtime_mode_service.h

+ 5
- 0
docs/architecture.md Прегледај датотеку

@@ -68,6 +68,11 @@ main.cpp -> UI + Services + Infrastructure
由 `infrastructure/JsonProjectStorage` 实现。加载必须先完成文件解析、版本检查和领域校验,成功
后才能替换当前工程;保存失败或加载失败不得修改当前工程路径和内存内容。

工程编辑支持保留草稿:HMI 非标签控件可以暂时没有寄存器绑定,梯形图节点可以处于待配置状态,
这些状态仍通过 `Project::validate()` 保存和加载。进入离线运行前由
`Project::validateForRunning()` 额外拒绝未绑定控件、待配置节点和缺少条件/输出的网络;旧版 1.0
工程缺少节点 `configured` 字段时按历史完整节点兼容读取。

`services/HmiEditorService` 负责 HMI 页面的控件添加、删除、移动和属性更新,不依赖 Qt 图元。
`ui/HmiEditorWidget` 使用 `QGraphicsScene` 将模型投影为画布项,画布项只保存控件标识,移动结束后
才通过编辑服务提交新位置。保存时由 `Project::validate()` 拒绝缺失必需绑定或绑定区域错误的控件。


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