| @@ -531,11 +531,6 @@ bool LadderRung::validateForRunning(std::string *error) const | |||
| return true; | |||
| } | |||
| bool ControlLogic::validate(std::string *error) const | |||
| { | |||
| return validate(defaultProjectLimitSettings(), error); | |||
| } | |||
| bool ControlLogic::validate( | |||
| const ProjectLimitSettings &limits, std::string *error) const | |||
| { | |||
| @@ -175,7 +175,6 @@ struct ControlLogic | |||
| bool enabled = true; | |||
| std::vector<VerticalConnection> verticalConnections; | |||
| bool validate(std::string *error = nullptr) const; | |||
| bool validate( | |||
| const ProjectLimitSettings &limits, | |||
| std::string *error = nullptr) const; | |||
| @@ -247,11 +247,6 @@ bool HmiControl::isConfigured() const | |||
| return binding_area.has_value() && binding->area() == *binding_area; | |||
| } | |||
| bool HmiPage::validate(std::string *error) const | |||
| { | |||
| return validate(defaultProjectLimitSettings(), error); | |||
| } | |||
| bool HmiPage::validate( | |||
| const ProjectLimitSettings &limits, std::string *error) const | |||
| { | |||
| @@ -316,11 +311,6 @@ bool HmiPage::validate( | |||
| return true; | |||
| } | |||
| bool HmiPage::validateForRunning(std::string *error) const | |||
| { | |||
| return validateForRunning(defaultProjectLimitSettings(), error); | |||
| } | |||
| bool HmiPage::validateForRunning( | |||
| const ProjectLimitSettings &limits, std::string *error) const | |||
| { | |||
| @@ -121,11 +121,9 @@ struct HmiPage | |||
| * @param error 可选错误输出,失败时写入首个校验原因 | |||
| * @return 页面及其全部控件有效时返回 true | |||
| */ | |||
| bool validate(std::string *error = nullptr) const; | |||
| bool validate( | |||
| const ProjectLimitSettings &limits, | |||
| std::string *error = nullptr) const; | |||
| bool validateForRunning(std::string *error = nullptr) const; | |||
| bool validateForRunning( | |||
| const ProjectLimitSettings &limits, | |||
| std::string *error = nullptr) const; | |||
| @@ -92,10 +92,3 @@ inline const ProjectLimitSettings &defaultProjectLimitSettings() | |||
| static const ProjectLimitSettings settings; | |||
| return settings; | |||
| } | |||
| // 返回长期有效的默认页面尺寸,供没有传入 INI 配置的代码使用 | |||
| inline const HmiDefaultSettings &defaultHmiSettings() | |||
| { | |||
| static const HmiDefaultSettings settings; | |||
| return settings; | |||
| } | |||
| @@ -156,11 +156,6 @@ const RegisterComment *Project::findRegisterComment( | |||
| return comment == registerComments.cend() ? nullptr : &*comment; | |||
| } | |||
| bool Project::validate(std::string *error) const | |||
| { | |||
| return validate(defaultProjectLimitSettings(), error); | |||
| } | |||
| bool Project::validate( | |||
| const ProjectLimitSettings &limits, std::string *error) const | |||
| { | |||
| @@ -456,11 +451,6 @@ bool Project::validate( | |||
| return true; | |||
| } | |||
| bool Project::validateForRunning(std::string *error) const | |||
| { | |||
| return validateForRunning(defaultProjectLimitSettings(), error); | |||
| } | |||
| bool Project::validateForRunning( | |||
| const ProjectLimitSettings &limits, std::string *error) const | |||
| { | |||
| @@ -48,12 +48,10 @@ struct Project | |||
| // 按地址查找工程注释,找不到时返回 nullptr | |||
| const RegisterComment *findRegisterComment(const RegisterAddress &address) const; | |||
| // 编辑态校验:允许尚未绑定完成的草稿 | |||
| bool validate(std::string *error = nullptr) const; | |||
| bool validate( | |||
| const ProjectLimitSettings &limits, | |||
| std::string *error = nullptr) const; | |||
| // 运行态校验:额外要求所有运行所需对象都已配置 | |||
| bool validateForRunning(std::string *error = nullptr) const; | |||
| bool validateForRunning( | |||
| const ProjectLimitSettings &limits, | |||
| std::string *error = nullptr) const; | |||
| @@ -3,7 +3,6 @@ | |||
| #include "register_address.h" | |||
| #include <cstdint> | |||
| #include <array> | |||
| #include <utility> | |||
| #include <vector> | |||
| @@ -40,13 +39,6 @@ struct RegisterWriteResult | |||
| RegisterError error = RegisterError::Unavailable; // 写入失败原因 | |||
| }; | |||
| struct WordPairReadResult | |||
| { | |||
| bool succeeded = false; | |||
| std::array<std::int16_t, 2> values{}; | |||
| RegisterError error = RegisterError::Unavailable; | |||
| }; | |||
| struct WordsReadResult | |||
| { | |||
| bool succeeded = false; | |||
| @@ -118,18 +110,4 @@ public: | |||
| } | |||
| return {true, RegisterError::None}; | |||
| } | |||
| virtual WordPairReadResult readWordPair(const RegisterAddress &address) const | |||
| { | |||
| const WordsReadResult result = readWords(address, 2); | |||
| return result.succeeded | |||
| ? WordPairReadResult{ | |||
| true, {result.values[0], result.values[1]}, RegisterError::None} | |||
| : WordPairReadResult{false, {}, result.error}; | |||
| } | |||
| virtual RegisterWriteResult writeWordPair( | |||
| const RegisterAddress &address, | |||
| const std::array<std::int16_t, 2> &values) | |||
| { | |||
| return writeWords(address, {values[0], values[1]}); | |||
| } | |||
| }; | |||
| @@ -7,8 +7,7 @@ class JsonProjectStorage final : public ProjectStorage | |||
| { | |||
| public: | |||
| explicit JsonProjectStorage( | |||
| const ProjectLimitSettings &project_limits = | |||
| defaultProjectLimitSettings()); | |||
| const ProjectLimitSettings &project_limits); | |||
| ProjectSaveResult save( | |||
| const Project &project, const std::string &file_path) override; | |||
| ProjectLoadResult load(const std::string &file_path) override; | |||
| @@ -401,16 +401,6 @@ const std::string &PlcCommunicationService::lastError() const | |||
| return last_error_; | |||
| } | |||
| const PlcSerialConfiguration &PlcCommunicationService::configuration() const | |||
| { | |||
| return configuration_; | |||
| } | |||
| const std::vector<PlcPollBlock> &PlcCommunicationService::pollBlocks() const | |||
| { | |||
| return poll_blocks_; | |||
| } | |||
| void PlcCommunicationService::setCallbacks( | |||
| std::function<void()> state_changed, | |||
| std::function<void(bool)> initial_read_changed, | |||
| @@ -58,10 +58,6 @@ public: | |||
| PlcCommunicationError lastErrorType() const override; | |||
| // 返回最近一次通信错误的文字 | |||
| const std::string &lastError() const override; | |||
| // 返回当前使用的串口和 Modbus 参数 | |||
| const PlcSerialConfiguration &configuration() const; | |||
| // 返回当前拆分后的只读轮询计划,供诊断和边界测试使用 | |||
| const std::vector<PlcPollBlock> &pollBlocks() const; | |||
| // 注册状态、首读、缓存更新和错误通知回调 | |||
| void setCallbacks( | |||
| std::function<void()> state_changed, | |||
| @@ -136,10 +136,3 @@ void PlcRegisterRepository::invalidate() | |||
| valid_bits_.fill(false); | |||
| valid_words_.fill(false); | |||
| } | |||
| bool PlcRegisterRepository::hasAnyValidValue() const | |||
| { | |||
| return std::any_of(valid_bits_.cbegin(), valid_bits_.cend(), [](bool value) { return value; }) | |||
| || std::any_of( | |||
| valid_words_.cbegin(), valid_words_.cend(), [](bool value) { return value; }); | |||
| } | |||
| @@ -35,8 +35,6 @@ public: | |||
| void updateWord(int address, std::int16_t value); | |||
| // 通信会话失效时清除“已读”标记,但保留数组内的旧值用于诊断 | |||
| void invalidate(); | |||
| // 判断是否至少收到过一个有效 M/D 值 | |||
| bool hasAnyValidValue() const; | |||
| private: | |||
| // M/D 区共用的数组长度,覆盖 0 到最大地址 | |||
| @@ -61,7 +61,7 @@ public: | |||
| */ | |||
| explicit HmiEditorService( | |||
| ProjectService &project_service, | |||
| const HmiDefaultSettings &hmi_defaults = defaultHmiSettings()); | |||
| const HmiDefaultSettings &hmi_defaults); | |||
| /** | |||
| * @brief 按页面标识查找只读页面 | |||
| @@ -1245,46 +1245,6 @@ LogicEditorResult LogicEditorService::updateRungComment( | |||
| return {true, LogicEditorError::None, {}, rung_id}; | |||
| } | |||
| LogicEditorResult LogicEditorService::setConditionAtColumn( | |||
| const std::string &logic_id, | |||
| const std::string &rung_id, | |||
| int column, | |||
| const LogicNodeConfig &config, | |||
| bool configured) | |||
| { | |||
| const LadderCell *cell = findCell(logic_id, rung_id, column); | |||
| if (cell == nullptr) | |||
| { | |||
| return failure(LogicEditorError::CellNotFound, "未找到目标条件网格"); | |||
| } | |||
| if (!isConditionConfig(config)) | |||
| { | |||
| return failure(LogicEditorError::InvalidNode, "条件区只能放置条件指令"); | |||
| } | |||
| LogicNode candidate{"candidate", config, configured}; | |||
| std::string error; | |||
| if (!candidate.validate(&error)) | |||
| { | |||
| return failure(LogicEditorError::InvalidNode, error); | |||
| } | |||
| HistoryState before = captureState(); | |||
| const bool modified_before = project_service_.isModified(); | |||
| Project &project = project_service_.editProject(); | |||
| ControlLogic *logic = editableLogic(&project, logic_id); | |||
| LadderRung *rung = editableRung(logic, rung_id); | |||
| LadderCell &editable_cell = rung->cells[static_cast<std::size_t>(column)]; | |||
| const std::string node_id = makeUniqueId(*logic, nodePrefix(config)); | |||
| editable_cell.kind = LadderCellKind::Node; | |||
| editable_cell.node = LogicNode{node_id, config, configured}; | |||
| if (!logic->validateStructure(project_service_.projectLimits(), &error)) | |||
| { | |||
| rollbackEdit(std::move(before), modified_before); | |||
| return failure(LogicEditorError::InvalidOperation, error); | |||
| } | |||
| recordHistory(std::move(before)); | |||
| return {true, LogicEditorError::None, {}, node_id}; | |||
| } | |||
| LogicEditResult LogicEditorService::applyConditionAndAdvance( | |||
| const std::string &logic_id, | |||
| const LogicEditCursor &cursor, | |||
| @@ -1295,211 +1255,6 @@ LogicEditResult LogicEditorService::applyConditionAndAdvance( | |||
| logic_id, cursor, &config, configured); | |||
| } | |||
| LogicEditorResult LogicEditorService::insertConditionAtColumn( | |||
| const std::string &logic_id, | |||
| const std::string &rung_id, | |||
| int column, | |||
| const LogicNodeConfig &config, | |||
| bool configured) | |||
| { | |||
| const LadderRung *existing = findRung(logic_id, rung_id); | |||
| if (existing == nullptr || column < 0 | |||
| || column >= ProjectLimits::kMaximumConditionColumns) | |||
| { | |||
| return failure(LogicEditorError::CellNotFound, "未找到目标条件网格"); | |||
| } | |||
| if (!isConditionConfig(config)) | |||
| { | |||
| return failure(LogicEditorError::InvalidNode, "条件区只能放置条件指令"); | |||
| } | |||
| if (existing->cells.back().kind != LadderCellKind::Gap) | |||
| { | |||
| return failure( | |||
| LogicEditorError::InvalidOperation, | |||
| "第 10 列已有内容,无法继续向右插入"); | |||
| } | |||
| HistoryState before = captureState(); | |||
| const bool modified_before = project_service_.isModified(); | |||
| Project &project = project_service_.editProject(); | |||
| ControlLogic *logic = editableLogic(&project, logic_id); | |||
| LadderRung *rung = editableRung(logic, rung_id); | |||
| for (int index = ProjectLimits::kMaximumConditionColumns - 1; | |||
| index > column; | |||
| --index) | |||
| { | |||
| rung->cells[static_cast<std::size_t>(index)] = | |||
| std::move(rung->cells[static_cast<std::size_t>(index - 1)]); | |||
| } | |||
| // 插入列会让右侧网格右移,边界 10 是固定输出侧边界,不能继续右移 | |||
| for (VerticalConnection &connection : logic->verticalConnections) | |||
| { | |||
| if ((connection.upperRungId == rung_id | |||
| || connection.lowerRungId == rung_id) | |||
| && connection.columnBoundary >= column | |||
| && connection.columnBoundary < ProjectLimits::kMaximumConditionColumns) | |||
| { | |||
| ++connection.columnBoundary; | |||
| } | |||
| } | |||
| LadderCell inserted; | |||
| inserted.id = makeUniqueId(*logic, "cell"); | |||
| inserted.kind = LadderCellKind::Node; | |||
| const std::string node_id = makeUniqueId(*logic, nodePrefix(config)); | |||
| inserted.node = LogicNode{node_id, config, configured}; | |||
| rung->cells[static_cast<std::size_t>(column)] = std::move(inserted); | |||
| std::string error; | |||
| if (!logic->validateStructure(project_service_.projectLimits(), &error)) | |||
| { | |||
| rollbackEdit(std::move(before), modified_before); | |||
| return failure(LogicEditorError::InvalidOperation, error); | |||
| } | |||
| recordHistory(std::move(before)); | |||
| return {true, LogicEditorError::None, {}, node_id}; | |||
| } | |||
| LogicEditorResult LogicEditorService::appendCondition( | |||
| const std::string &logic_id, | |||
| const std::string &rung_id, | |||
| const LogicNodeConfig &config, | |||
| bool configured) | |||
| { | |||
| const ControlLogic *logic = findLogic(logic_id); | |||
| if (logic == nullptr) | |||
| { | |||
| return failure(LogicEditorError::LogicNotFound, "未找到控制逻辑"); | |||
| } | |||
| if (!isConditionConfig(config)) | |||
| { | |||
| return failure(LogicEditorError::InvalidNode, "条件区只能放置条件指令"); | |||
| } | |||
| std::string target_rung_id = rung_id; | |||
| if (target_rung_id.empty()) | |||
| { | |||
| if (logic->rungs.size() | |||
| >= project_service_.projectLimits().maximumRungsPerLogic | |||
| || totalRungCount(project_service_.project()) | |||
| >= ProjectLimits::kMaximumRungsPerProject) | |||
| { | |||
| return failure( | |||
| LogicEditorError::InvalidOperation, | |||
| "梯形图行数已经达到当前上限"); | |||
| } | |||
| // 新建首行和放置首个节点必须共用一条历史记录 | |||
| HistoryState before = captureState(); | |||
| const bool modified_before = project_service_.isModified(); | |||
| Project &project = project_service_.editProject(); | |||
| ControlLogic *editable = editableLogic(&project, logic_id); | |||
| target_rung_id = insertEmptyRungAt(editable, editable->rungs.size()); | |||
| LadderRung *created = editableRung(editable, target_rung_id); | |||
| const auto empty = std::find_if( | |||
| created->cells.begin(), created->cells.end(), | |||
| [](const LadderCell &cell) | |||
| { | |||
| return cell.kind == LadderCellKind::Gap; | |||
| }); | |||
| if (empty == created->cells.end()) | |||
| { | |||
| rollbackEdit(std::move(before), modified_before); | |||
| return failure( | |||
| LogicEditorError::InvalidOperation, | |||
| "条件区 10 列已经占满"); | |||
| } | |||
| const std::string node_id = makeUniqueId(*editable, nodePrefix(config)); | |||
| LadderCell &cell = created->cells[ | |||
| static_cast<std::size_t>(std::distance(created->cells.begin(), empty))]; | |||
| cell.kind = LadderCellKind::Node; | |||
| cell.node = LogicNode{node_id, config, configured}; | |||
| std::string error; | |||
| if (!editable->validateStructure(project_service_.projectLimits(), &error)) | |||
| { | |||
| rollbackEdit(std::move(before), modified_before); | |||
| return failure(LogicEditorError::InvalidOperation, error); | |||
| } | |||
| recordHistory(std::move(before)); | |||
| return {true, LogicEditorError::None, {}, node_id}; | |||
| } | |||
| const LadderRung *rung = findRung(logic_id, target_rung_id); | |||
| if (rung == nullptr) | |||
| { | |||
| return failure(LogicEditorError::RungNotFound, "未找到梯形图行"); | |||
| } | |||
| const auto empty = std::find_if( | |||
| rung->cells.cbegin(), rung->cells.cend(), | |||
| [](const LadderCell &cell) { return cell.kind == LadderCellKind::Gap; }); | |||
| if (empty == rung->cells.cend()) | |||
| { | |||
| return failure(LogicEditorError::InvalidOperation, "条件区 10 列已经占满"); | |||
| } | |||
| return setConditionAtColumn( | |||
| logic_id, | |||
| target_rung_id, | |||
| static_cast<int>(std::distance(rung->cells.cbegin(), empty)), | |||
| config, | |||
| configured); | |||
| } | |||
| LogicEditorResult LogicEditorService::appendWire( | |||
| const std::string &logic_id, | |||
| const std::string &rung_id, | |||
| int column_span) | |||
| { | |||
| const ControlLogic *logic = findLogic(logic_id); | |||
| if (logic == nullptr) | |||
| { | |||
| return failure(LogicEditorError::LogicNotFound, "未找到控制逻辑"); | |||
| } | |||
| if (column_span <= 0 | |||
| || column_span > ProjectLimits::kMaximumConditionColumns) | |||
| { | |||
| return failure(LogicEditorError::InvalidOperation, "横线参数无效"); | |||
| } | |||
| if (rung_id.empty()) | |||
| { | |||
| if (logic->rungs.size() | |||
| >= project_service_.projectLimits().maximumRungsPerLogic | |||
| || totalRungCount(project_service_.project()) | |||
| >= ProjectLimits::kMaximumRungsPerProject) | |||
| { | |||
| return failure( | |||
| LogicEditorError::InvalidOperation, | |||
| "梯形图行数已经达到当前上限"); | |||
| } | |||
| HistoryState before = captureState(); | |||
| const bool modified_before = project_service_.isModified(); | |||
| Project &project = project_service_.editProject(); | |||
| ControlLogic *editable = editableLogic(&project, logic_id); | |||
| const std::string new_rung_id = insertEmptyRungAt( | |||
| editable, editable->rungs.size()); | |||
| LadderRung *rung = editableRung(editable, new_rung_id); | |||
| const int first = ProjectLimits::kMaximumConditionColumns - column_span; | |||
| for (int column = first; | |||
| column < ProjectLimits::kMaximumConditionColumns; | |||
| ++column) | |||
| { | |||
| rung->cells[static_cast<std::size_t>(column)].kind = | |||
| LadderCellKind::Wire; | |||
| } | |||
| std::string error; | |||
| if (!editable->validateStructure(project_service_.projectLimits(), &error)) | |||
| { | |||
| rollbackEdit(std::move(before), modified_before); | |||
| return failure(LogicEditorError::InvalidOperation, error); | |||
| } | |||
| recordHistory(std::move(before)); | |||
| return {true, LogicEditorError::None, {}, new_rung_id}; | |||
| } | |||
| const LadderRung *rung = findRung(logic_id, rung_id); | |||
| if (rung == nullptr) | |||
| { | |||
| return failure(LogicEditorError::RungNotFound, "未找到梯形图行"); | |||
| } | |||
| const int first = ProjectLimits::kMaximumConditionColumns - column_span; | |||
| return setHorizontalWireRange(logic_id, rung_id, first, | |||
| ProjectLimits::kMaximumConditionColumns - 1, | |||
| true); | |||
| } | |||
| LogicEditResult LogicEditorService::applyWireAndAdvance( | |||
| const std::string &logic_id, | |||
| const LogicEditCursor &cursor) | |||
| @@ -3055,70 +2810,6 @@ bool LogicEditorService::areConditionNodesContiguous( | |||
| == static_cast<int>(columns.size()); | |||
| } | |||
| LogicEditorResult LogicEditorService::pasteRung( | |||
| const std::string &logic_id, | |||
| const LadderRung &source) | |||
| { | |||
| const ControlLogic *logic = findLogic(logic_id); | |||
| std::string error; | |||
| if (logic == nullptr) | |||
| { | |||
| return failure(LogicEditorError::LogicNotFound, "未找到控制逻辑"); | |||
| } | |||
| if (!source.validateStructure(&error)) | |||
| { | |||
| return failure(LogicEditorError::InvalidOperation, error); | |||
| } | |||
| if (logic->rungs.size() | |||
| >= project_service_.projectLimits().maximumRungsPerLogic | |||
| || totalRungCount(project_service_.project()) | |||
| >= ProjectLimits::kMaximumRungsPerProject) | |||
| { | |||
| return failure(LogicEditorError::InvalidOperation, "梯形图行数已经达到上限"); | |||
| } | |||
| HistoryState before = captureState(); | |||
| const bool modified_before = project_service_.isModified(); | |||
| Project &project = project_service_.editProject(); | |||
| ControlLogic *editable = editableLogic(&project, logic_id); | |||
| LadderRung pasted = makeEmptyRung(*editable, editable->rungs.size()); | |||
| pasted.comment = source.comment; | |||
| for (std::size_t column = 0U; column < source.cells.size(); ++column) | |||
| { | |||
| pasted.cells[column].kind = source.cells[column].kind; | |||
| if (source.cells[column].node.has_value()) | |||
| { | |||
| const LogicNode &source_node = *source.cells[column].node; | |||
| pasted.cells[column].node = LogicNode{ | |||
| makeUniqueId(*editable, nodePrefix(source_node.config)), | |||
| source_node.config, | |||
| source_node.configured}; | |||
| } | |||
| } | |||
| if (source.output.has_value()) | |||
| { | |||
| pasted.output = LogicNode{ | |||
| makeUniqueId(*editable, nodePrefix(source.output->config)), | |||
| source.output->config, | |||
| source.output->configured}; | |||
| } | |||
| const std::string pasted_id = pasted.id; | |||
| editable->rungs.push_back(std::move(pasted)); | |||
| refreshRungNames(editable); | |||
| if (!editable->validateStructure(project_service_.projectLimits(), &error)) | |||
| { | |||
| rollbackEdit(std::move(before), modified_before); | |||
| return failure(LogicEditorError::InvalidOperation, error); | |||
| } | |||
| recordHistory(std::move(before)); | |||
| return {true, LogicEditorError::None, {}, pasted_id}; | |||
| } | |||
| LogicEditorResult LogicEditorService::removeNode( | |||
| const std::string &logic_id, const std::string &node_id) | |||
| { | |||
| return removeNodes(logic_id, {node_id}); | |||
| } | |||
| LogicEditorResult LogicEditorService::removeNodes( | |||
| const std::string &logic_id, | |||
| const std::vector<std::string> &node_ids) | |||
| @@ -214,38 +214,17 @@ public: | |||
| const std::string &rung_id, | |||
| const std::string &comment); | |||
| LogicEditorResult setConditionAtColumn( | |||
| const std::string &logic_id, | |||
| const std::string &rung_id, | |||
| int column, | |||
| const LogicNodeConfig &config, | |||
| bool configured = false); | |||
| LogicEditResult applyConditionAndAdvance( | |||
| const std::string &logic_id, | |||
| const LogicEditCursor &cursor, | |||
| const LogicNodeConfig &config, | |||
| bool configured = false); | |||
| LogicEditorResult insertConditionAtColumn( | |||
| const std::string &logic_id, | |||
| const std::string &rung_id, | |||
| int column, | |||
| const LogicNodeConfig &config, | |||
| bool configured = false); | |||
| LogicEditorResult appendCondition( | |||
| const std::string &logic_id, | |||
| const std::string &rung_id, | |||
| const LogicNodeConfig &config, | |||
| bool configured = false); | |||
| LogicEditorResult setHorizontalWireRange( | |||
| const std::string &logic_id, | |||
| const std::string &rung_id, | |||
| int first_column, | |||
| int last_column, | |||
| bool connected); | |||
| LogicEditorResult appendWire( | |||
| const std::string &logic_id, | |||
| const std::string &rung_id, | |||
| int column_span = 1); | |||
| LogicEditResult applyWireAndAdvance( | |||
| const std::string &logic_id, | |||
| const LogicEditCursor &cursor); | |||
| @@ -320,11 +299,6 @@ public: | |||
| const std::string &logic_id, | |||
| const std::string &rung_id, | |||
| const std::vector<std::string> &node_ids) const; | |||
| LogicEditorResult pasteRung( | |||
| const std::string &logic_id, | |||
| const LadderRung &source); | |||
| LogicEditorResult removeNode( | |||
| const std::string &logic_id, const std::string &node_id); | |||
| LogicEditorResult removeNodes( | |||
| const std::string &logic_id, | |||
| const std::vector<std::string> &node_ids); | |||
| @@ -105,11 +105,6 @@ SimulationState OfflineSimulationService::state() const | |||
| return state_; | |||
| } | |||
| int OfflineSimulationService::scanIntervalMs() const | |||
| { | |||
| return timer_.interval(); | |||
| } | |||
| std::uint64_t OfflineSimulationService::successfulScanCount() const | |||
| { | |||
| return successful_scan_count_; | |||
| @@ -64,8 +64,6 @@ public: | |||
| // 返回当前离线仿真生命周期状态 | |||
| SimulationState state() const; | |||
| // 返回当前定时器扫描周期,单位为毫秒 | |||
| int scanIntervalMs() const; | |||
| // 返回本次会话成功完成的扫描轮数 | |||
| std::uint64_t successfulScanCount() const; | |||
| // 返回最近一次扫描错误;无错误时为成功结果 | |||
| @@ -163,11 +163,7 @@ public: | |||
| */ | |||
| virtual PlcCommunicationResult setPollAddresses( | |||
| const std::vector<RegisterAddress> &addresses, | |||
| const std::vector<RegisterWordRange> &multi_word_ranges) | |||
| { | |||
| (void)multi_word_ranges; | |||
| return setPollAddresses(addresses); | |||
| } | |||
| const std::vector<RegisterWordRange> &multi_word_ranges) = 0; | |||
| // 返回当前连接生命周期状态;Connected 不代表首读资格已经完成 | |||
| virtual PlcConnectionState state() const = 0; | |||
| @@ -24,11 +24,6 @@ std::string generateProjectId() | |||
| } // namespace | |||
| ProjectService::ProjectService(ProjectStorage &storage) | |||
| : ProjectService(storage, defaultProjectLimitSettings()) | |||
| { | |||
| } | |||
| ProjectService::ProjectService( | |||
| ProjectStorage &storage, | |||
| const ProjectLimitSettings &project_limits) | |||
| @@ -49,11 +44,6 @@ Project &ProjectService::editProject() | |||
| return project_; | |||
| } | |||
| const std::string &ProjectService::currentFilePath() const | |||
| { | |||
| return current_file_path_; | |||
| } | |||
| bool ProjectService::hasCurrentFile() const | |||
| { | |||
| return !current_file_path_.empty(); | |||
| @@ -27,8 +27,7 @@ struct ProjectOperationResult | |||
| class ProjectService | |||
| { | |||
| public: | |||
| // 使用外部提供的工程存储实现创建服务 | |||
| explicit ProjectService(ProjectStorage &storage); | |||
| // 使用外部提供的工程存储实现和启动配置创建服务 | |||
| ProjectService( | |||
| ProjectStorage &storage, | |||
| const ProjectLimitSettings &project_limits); | |||
| @@ -37,7 +36,6 @@ public: | |||
| const Project &project() const; | |||
| // 获取服务层授权的可编辑引用,调用方必须随后完成校验 | |||
| Project &editProject(); | |||
| const std::string ¤tFilePath() const; | |||
| bool hasCurrentFile() const; | |||
| bool isModified() const; | |||
| const ProjectLimitSettings &projectLimits() const; | |||
| @@ -225,18 +225,6 @@ RegisterMonitorWriteResult RegisterMonitorService::writeBit( | |||
| return captureInitialBit(address, value, active_result); | |||
| } | |||
| RegisterMonitorWriteResult RegisterMonitorService::writeWord( | |||
| const RegisterAddress &address, std::int16_t value) | |||
| { | |||
| return writeNumeric(address, RegisterDataType::Int16, value); | |||
| } | |||
| RegisterMonitorWriteResult RegisterMonitorService::writeFloat( | |||
| const RegisterAddress &address, float value) | |||
| { | |||
| return writeNumeric(address, RegisterDataType::Float32, value); | |||
| } | |||
| RegisterMonitorWriteResult RegisterMonitorService::writeNumeric( | |||
| const RegisterAddress &address, RegisterDataType data_type, double value) | |||
| { | |||
| @@ -91,10 +91,6 @@ public: | |||
| * @param value 要写入的 D 字值 | |||
| * @return 成功或包含底层 RegisterError 和用户可读文本的失败结果 | |||
| */ | |||
| RegisterMonitorWriteResult writeWord( | |||
| const RegisterAddress &address, std::int16_t value); | |||
| RegisterMonitorWriteResult writeFloat( | |||
| const RegisterAddress &address, float value); | |||
| RegisterMonitorWriteResult writeNumeric( | |||
| const RegisterAddress &address, RegisterDataType data_type, double value); | |||
| /** | |||
| @@ -840,11 +840,6 @@ void LogicEditorWidget::clearRuntimeTrace() | |||
| rebuildScene(); | |||
| } | |||
| bool LogicEditorWidget::runtimeTraceEnabled() const | |||
| { | |||
| return runtime_trace_enabled_; | |||
| } | |||
| void LogicEditorWidget::reloadLogic() | |||
| { | |||
| setLogicId(logic_id_); | |||
| @@ -40,7 +40,6 @@ public: | |||
| const LogicTraceSnapshot &trace, | |||
| const std::string &fault_node_id = {}); | |||
| void clearRuntimeTrace(); | |||
| bool runtimeTraceEnabled() const; | |||
| void reloadLogic(); | |||
| void clearSelection(); | |||
| void selectNode(const std::string &node_id); | |||
| @@ -576,16 +576,6 @@ void MainWindow::closeEvent(QCloseEvent *event) | |||
| QMainWindow::closeEvent(event); | |||
| } | |||
| const std::string &MainWindow::currentHmiPageId() const | |||
| { | |||
| return current_hmi_page_id_; | |||
| } | |||
| const std::string &MainWindow::currentLogicId() const | |||
| { | |||
| return current_logic_id_; | |||
| } | |||
| void MainWindow::configureActions() | |||
| { | |||
| mode_action_group_ = new QActionGroup(this); | |||
| @@ -111,11 +111,6 @@ public: | |||
| /** 释放窗口拥有的控制器和可选导航服务 */ | |||
| ~MainWindow() override; | |||
| /** 返回当前选中的 HMI 页面标识 */ | |||
| const std::string ¤tHmiPageId() const; | |||
| /** 返回当前选中的控制逻辑标识 */ | |||
| const std::string ¤tLogicId() const; | |||
| protected: | |||
| /** 处理主窗口及子控件的事件过滤请求 */ | |||
| bool eventFilter(QObject *watched, QEvent *event) override; | |||
| @@ -256,16 +256,6 @@ void ProjectWorkspaceController::refresh() | |||
| updateActions(); | |||
| } | |||
| const std::string &ProjectWorkspaceController::currentHmiPageId() const | |||
| { | |||
| return current_hmi_page_id_; | |||
| } | |||
| const std::string &ProjectWorkspaceController::currentLogicId() const | |||
| { | |||
| return current_logic_id_; | |||
| } | |||
| void ProjectWorkspaceController::handleSelectionChanged() | |||
| { | |||
| QTreeWidgetItem *item = ui_.projectTree->currentItem(); | |||
| @@ -89,11 +89,6 @@ public: | |||
| /** 启用或停用当前选中的控制逻辑 */ | |||
| void toggleSelectedLogicEnabled(); | |||
| /** 返回当前 HMI 页面标识 */ | |||
| const std::string ¤tHmiPageId() const; | |||
| /** 返回当前控制逻辑标识 */ | |||
| const std::string ¤tLogicId() const; | |||
| private: | |||
| /** 工程树项目的三种业务类型 */ | |||
| enum class ItemKind | |||
| @@ -33,7 +33,7 @@ const AlarmRecord *findRecord( | |||
| void testAlarmDefinitionsAndRuntimeLifecycle() | |||
| { | |||
| TestProjectStorage storage; | |||
| ProjectService project_service(storage); | |||
| ProjectService project_service(storage, defaultProjectLimitSettings()); | |||
| AlarmEditorService editor_service(project_service); | |||
| VirtualRegisterRepository repository; | |||
| AlarmService alarm_service(project_service, repository); | |||
| @@ -159,13 +159,15 @@ void testMultiWordCodecsAndBlockAccess() | |||
| VirtualRegisterRepository repository; | |||
| const RegisterAddress d10{RegisterArea::D, 10}; | |||
| require(repository.writeWordPair(d10, Float32Codec::encode(-2.5f)).succeeded, | |||
| const auto float_words = Float32Codec::encode(-2.5f); | |||
| require(repository.writeWords( | |||
| d10, {float_words[0], float_words[1]}).succeeded, | |||
| "virtual repository must write Float32 low and high words"); | |||
| const WordPairReadResult pair = repository.readWordPair(d10); | |||
| const WordsReadResult pair = repository.readWords(d10, 2); | |||
| require(pair.succeeded | |||
| && Float32Codec::decode(pair.values[0], pair.values[1]).value() == -2.5f, | |||
| "virtual repository must read Float32 from two consecutive words"); | |||
| require(!repository.writeWordPair({RegisterArea::D, 4000}, {}).succeeded, | |||
| require(!repository.writeWords({RegisterArea::D, 4000}, {}).succeeded, | |||
| "Float32 write at D4000 must be rejected"); | |||
| require(repository.writeWords( | |||
| {RegisterArea::D, 30}, | |||
| @@ -294,12 +296,12 @@ void testMultiWordHmiBoundaries() | |||
| display.binding = RegisterAddress{RegisterArea::D, 3999}; | |||
| display.dataType = RegisterDataType::Float32; | |||
| project.hmiPages.front().controls.push_back(display); | |||
| require(project.validate(), | |||
| require(project.validate(defaultProjectLimitSettings()), | |||
| "Float32 D3999 must be valid and occupy D3999~D4000"); | |||
| project.hmiPages.front().controls.back().binding = | |||
| RegisterAddress{RegisterArea::D, 4000}; | |||
| require(!project.validate(), "Float32 D4000 must be rejected"); | |||
| require(!project.validate(defaultProjectLimitSettings()), "Float32 D4000 must be rejected"); | |||
| project = makeValidProject(); | |||
| HmiControl int32_display = display; | |||
| @@ -307,10 +309,10 @@ void testMultiWordHmiBoundaries() | |||
| int32_display.dataType = RegisterDataType::Int32; | |||
| int32_display.binding = RegisterAddress{RegisterArea::D, 3999}; | |||
| project.hmiPages.front().controls.push_back(int32_display); | |||
| require(project.validate(), "Int32 D3999 must be valid"); | |||
| require(project.validate(defaultProjectLimitSettings()), "Int32 D3999 must be valid"); | |||
| project.hmiPages.front().controls.back().binding = | |||
| RegisterAddress{RegisterArea::D, 4000}; | |||
| require(!project.validate(), "Int32 D4000 must be rejected"); | |||
| require(!project.validate(defaultProjectLimitSettings()), "Int32 D4000 must be rejected"); | |||
| project = makeValidProject(); | |||
| HmiControl double_display = display; | |||
| @@ -318,13 +320,13 @@ void testMultiWordHmiBoundaries() | |||
| double_display.dataType = RegisterDataType::Float64; | |||
| double_display.binding = RegisterAddress{RegisterArea::D, 3996}; | |||
| project.hmiPages.front().controls.push_back(double_display); | |||
| require(project.validate(), "Double D3996 must be a valid even start address"); | |||
| require(project.validate(defaultProjectLimitSettings()), "Double D3996 must be a valid even start address"); | |||
| project.hmiPages.front().controls.back().binding = | |||
| RegisterAddress{RegisterArea::D, 3997}; | |||
| require(!project.validate(), "Double D3997 must be rejected"); | |||
| require(!project.validate(defaultProjectLimitSettings()), "Double D3997 must be rejected"); | |||
| project.hmiPages.front().controls.back().binding = | |||
| RegisterAddress{RegisterArea::D, 3995}; | |||
| require(!project.validate(), "Double odd start addresses must be rejected"); | |||
| require(!project.validate(defaultProjectLimitSettings()), "Double odd start addresses must be rejected"); | |||
| project = makeValidProject(); | |||
| HmiControl invalid_display = display; | |||
| @@ -332,7 +334,7 @@ void testMultiWordHmiBoundaries() | |||
| invalid_display.binding.reset(); | |||
| invalid_display.dataType = static_cast<RegisterDataType>(99); | |||
| project.hmiPages.front().controls.push_back(invalid_display); | |||
| require(!project.validate(), | |||
| require(!project.validate(defaultProjectLimitSettings()), | |||
| "an unbound HMI draft must still reject an unknown numeric type"); | |||
| project = makeValidProject(); | |||
| @@ -342,12 +344,12 @@ void testMultiWordHmiBoundaries() | |||
| second.id = "float-display-duplicate"; | |||
| project.hmiPages.front().controls.push_back(first); | |||
| project.hmiPages.front().controls.push_back(second); | |||
| require(project.validate(), | |||
| require(project.validate(defaultProjectLimitSettings()), | |||
| "same Float32 start address and type may be bound more than once"); | |||
| second.dataType = RegisterDataType::Int16; | |||
| project.hmiPages.front().controls.back() = second; | |||
| require(!project.validate(), | |||
| require(!project.validate(defaultProjectLimitSettings()), | |||
| "different HMI data types may not partially overlap"); | |||
| for (const RegisterDataType type : { | |||
| @@ -372,7 +374,7 @@ void testMultiWordHmiBoundaries() | |||
| RegisterArea::D, | |||
| 10 + registerDataTypeWordCount(type) - 1}}, | |||
| true}; | |||
| require(!project.validate(), | |||
| require(!project.validate(defaultProjectLimitSettings()), | |||
| "16-bit instructions must not write inside any multi-word HMI range"); | |||
| } | |||
| } | |||
| @@ -386,28 +388,28 @@ void testHmiAppearancePropertyBoundaries() | |||
| button.properties[HmiAppearanceProperty::kFontSize] = "18"; | |||
| button.properties[HmiAppearanceProperty::kFontBold] = "true"; | |||
| button.properties[HmiAppearanceProperty::kFontItalic] = "false"; | |||
| require(project.validate(), "valid HMI appearance properties must pass validation"); | |||
| require(project.validate(defaultProjectLimitSettings()), "valid HMI appearance properties must pass validation"); | |||
| button.properties[HmiAppearanceProperty::kTextColor] = "red"; | |||
| require(!project.validate(), "text colors must use the #RRGGBB format"); | |||
| require(!project.validate(defaultProjectLimitSettings()), "text colors must use the #RRGGBB format"); | |||
| project = makeValidProject(); | |||
| HmiControl &font_control = project.hmiPages.front().controls.front(); | |||
| font_control.properties[HmiAppearanceProperty::kFontSize] = "5"; | |||
| require(!project.validate(), "font sizes below the minimum must be rejected"); | |||
| require(!project.validate(defaultProjectLimitSettings()), "font sizes below the minimum must be rejected"); | |||
| font_control.properties[HmiAppearanceProperty::kFontSize] = "73"; | |||
| require(!project.validate(), "font sizes above the maximum must be rejected"); | |||
| require(!project.validate(defaultProjectLimitSettings()), "font sizes above the maximum must be rejected"); | |||
| font_control.properties[HmiAppearanceProperty::kFontSize] = "large"; | |||
| require(!project.validate(), "non-numeric font sizes must be rejected"); | |||
| require(!project.validate(defaultProjectLimitSettings()), "non-numeric font sizes must be rejected"); | |||
| project = makeValidProject(); | |||
| HmiControl &style_control = project.hmiPages.front().controls.front(); | |||
| style_control.properties[HmiAppearanceProperty::kFontBold] = "yes"; | |||
| require(!project.validate(), "font style flags must be true or false"); | |||
| require(!project.validate(defaultProjectLimitSettings()), "font style flags must be true or false"); | |||
| project = makeValidProject(); | |||
| project.hmiPages.front().controls.front().properties["legacyColor"] = "green"; | |||
| require(project.validate(), "unknown HMI extension properties must remain supported"); | |||
| require(project.validate(defaultProjectLimitSettings()), "unknown HMI extension properties must remain supported"); | |||
| } | |||
| Project makeValidProject() | |||
| @@ -475,11 +477,11 @@ void testMultiPageAndLogicDomainRules() | |||
| label.type = HmiControlType::Label; | |||
| label.text = "Machine"; | |||
| project.hmiPages.front().controls.push_back(label); | |||
| require(project.validate(), "an unbound label must be a valid static control"); | |||
| require(project.validate(defaultProjectLimitSettings()), "an unbound label must be a valid static control"); | |||
| project.hmiPages.front().controls.back().binding = | |||
| RegisterAddress{RegisterArea::M, 10}; | |||
| require(!project.validate(), "labels must reject register bindings"); | |||
| require(!project.validate(defaultProjectLimitSettings()), "labels must reject register bindings"); | |||
| project = makeValidProject(); | |||
| project.hmiPages.push_back(settings); | |||
| @@ -489,21 +491,21 @@ void testMultiPageAndLogicDomainRules() | |||
| jump.text = "Settings"; | |||
| jump.pageJump = HmiPageJumpConfig{settings.id}; | |||
| project.hmiPages.front().controls.push_back(jump); | |||
| require(project.validate() && project.validateForRunning(), | |||
| require(project.validate(defaultProjectLimitSettings()) && project.validateForRunning(defaultProjectLimitSettings()), | |||
| "a page jump must resolve its target by stable page id"); | |||
| project.hmiPages.front().controls.back().pageJump->targetPageId = "missing"; | |||
| require(!project.validate(), "a page jump must reject a missing target page"); | |||
| require(!project.validate(defaultProjectLimitSettings()), "a page jump must reject a missing target page"); | |||
| project = makeValidProject(); | |||
| project.initialHmiPageId = "missing"; | |||
| require(!project.validate(), "the initial HMI page id must resolve to a page"); | |||
| require(!project.validate(defaultProjectLimitSettings()), "the initial HMI page id must resolve to a page"); | |||
| project = makeValidProject(); | |||
| HmiPage duplicate_name = settings; | |||
| duplicate_name.name = project.hmiPages.front().name; | |||
| project.hmiPages.push_back(duplicate_name); | |||
| require(!project.validate(), "HMI page names must be unique"); | |||
| require(!project.validate(defaultProjectLimitSettings()), "HMI page names must be unique"); | |||
| project = makeValidProject(); | |||
| ControlLogic disabled_draft; | |||
| @@ -515,17 +517,17 @@ void testMultiPageAndLogicDomainRules() | |||
| draft_rung.name = "Draft network"; | |||
| disabled_draft.rungs.push_back(std::move(draft_rung)); | |||
| project.controlLogics.push_back(disabled_draft); | |||
| require(project.validateForRunning(), | |||
| require(project.validateForRunning(defaultProjectLimitSettings()), | |||
| "a disabled draft logic must not block offline running"); | |||
| project.controlLogics.back().name = project.controlLogics.front().name; | |||
| require(!project.validate(), "control logic names must be unique"); | |||
| require(!project.validate(defaultProjectLimitSettings()), "control logic names must be unique"); | |||
| project = makeValidProject(); | |||
| project.hmiPages.front().controls.front().type = | |||
| static_cast<HmiControlType>(99); | |||
| project.hmiPages.front().controls.front().binding.reset(); | |||
| require(!project.validate(), "unknown HMI control types must be rejected"); | |||
| require(!project.validate(defaultProjectLimitSettings()), "unknown HMI control types must be rejected"); | |||
| } | |||
| void testQuantityBoundaries() | |||
| @@ -540,9 +542,9 @@ void testQuantityBoundaries() | |||
| 400, | |||
| {}}); | |||
| } | |||
| require(project.validate(), "an HMI page count at the configured limit must be accepted"); | |||
| require(project.validate(defaultProjectLimitSettings()), "an HMI page count at the configured limit must be accepted"); | |||
| project.hmiPages.push_back({"page-over", "Page over", 800, 400, {}}); | |||
| require(!project.validate(), "an HMI page count of 129 must be rejected"); | |||
| require(!project.validate(defaultProjectLimitSettings()), "an HMI page count of 129 must be rejected"); | |||
| project = makeValidProject(); | |||
| project.hmiPages.clear(); | |||
| @@ -569,7 +571,7 @@ void testQuantityBoundaries() | |||
| project.hmiPages.push_back(std::move(page)); | |||
| } | |||
| project.initialHmiPageId = project.hmiPages.front().id; | |||
| require(!project.validate(), | |||
| require(!project.validate(defaultProjectLimitSettings()), | |||
| "an HMI control count over the project limit must be rejected"); | |||
| project = makeValidProject(); | |||
| @@ -585,14 +587,14 @@ void testQuantityBoundaries() | |||
| label.text = "label"; | |||
| project.hmiPages.front().controls.push_back(std::move(label)); | |||
| } | |||
| require(project.validate(), "a page control count at the configured limit must be accepted"); | |||
| require(project.validate(defaultProjectLimitSettings()), "a page control count at the configured limit must be accepted"); | |||
| HmiControl extra_label; | |||
| extra_label.id = "label-over"; | |||
| extra_label.type = HmiControlType::Label; | |||
| extra_label.bounds = {0, 0, 1, 1}; | |||
| extra_label.text = "label"; | |||
| project.hmiPages.front().controls.push_back(std::move(extra_label)); | |||
| require(!project.validate(), "a page control count of 513 must be rejected"); | |||
| require(!project.validate(defaultProjectLimitSettings()), "a page control count of 513 must be rejected"); | |||
| project = makeValidProject(); | |||
| project.controlLogics.clear(); | |||
| @@ -616,24 +618,24 @@ void testQuantityBoundaries() | |||
| } | |||
| project.controlLogics.push_back(std::move(logic)); | |||
| } | |||
| require(!project.validate(), | |||
| require(!project.validate(defaultProjectLimitSettings()), | |||
| "a ladder rung count over the project limit must be rejected"); | |||
| project = makeValidProject(); | |||
| project.hmiPages.front().width = ProjectLimits::kMaximumHmiPageWidth; | |||
| project.hmiPages.front().height = ProjectLimits::kMaximumHmiPageHeight; | |||
| require(project.validate(), "an HMI page size of 1600 by 800 must be accepted"); | |||
| require(project.validate(defaultProjectLimitSettings()), "an HMI page size of 1600 by 800 must be accepted"); | |||
| project.hmiPages.front().width = ProjectLimits::kMaximumHmiPageWidth + 1; | |||
| require(!project.validate(), "an HMI page width of 1601 must be rejected"); | |||
| require(!project.validate(defaultProjectLimitSettings()), "an HMI page width of 1601 must be rejected"); | |||
| project.hmiPages.front().width = ProjectLimits::kMinimumHmiPageWidth - 1; | |||
| require(!project.validate(), "an HMI page width of 319 must be rejected"); | |||
| require(!project.validate(defaultProjectLimitSettings()), "an HMI page width of 319 must be rejected"); | |||
| project.hmiPages.front().width = ProjectLimits::kMaximumHmiPageWidth; | |||
| project.hmiPages.front().height = ProjectLimits::kMinimumHmiPageHeight - 1; | |||
| require(!project.validate(), "an HMI page height of 199 must be rejected"); | |||
| require(!project.validate(defaultProjectLimitSettings()), "an HMI page height of 199 must be rejected"); | |||
| project = makeValidProject(); | |||
| project.controlLogics.front().rungs.front().cells.pop_back(); | |||
| require(!project.validate(), | |||
| require(!project.validate(defaultProjectLimitSettings()), | |||
| "a ladder row with fewer than ten cells must be rejected"); | |||
| } | |||
| @@ -686,19 +688,19 @@ void testEdgeAndCommentBoundaries() | |||
| comment_rung.id = "comment-rung"; | |||
| comment_rung.name = "Comment rung"; | |||
| comment_rung.comment.assign(ProjectLimits::kMaximumRungCommentBytes, 'a'); | |||
| require(comment_rung.validate(), "a rung comment at the byte limit must be valid"); | |||
| require(comment_rung.validateStructure(), "a rung comment at the byte limit must be valid"); | |||
| comment_rung.comment.push_back('a'); | |||
| require(!comment_rung.validate(), "a rung comment above the byte limit must fail"); | |||
| require(!comment_rung.validateStructure(), "a rung comment above the byte limit must fail"); | |||
| comment_rung.comment = "第一行\n第二行"; | |||
| require(!comment_rung.validate(), "a multiline rung comment must be rejected"); | |||
| require(!comment_rung.validateStructure(), "a multiline rung comment must be rejected"); | |||
| comment_rung.comment = "第一行\r第二行"; | |||
| require(!comment_rung.validate(), "a rung comment containing CR must be rejected"); | |||
| require(!comment_rung.validateStructure(), "a rung comment containing CR must be rejected"); | |||
| Project project = makeValidProject(); | |||
| project.registerComments = { | |||
| {RegisterAddress{RegisterArea::M, 0}, "启动按钮"}, | |||
| {RegisterAddress{RegisterArea::M, 0}, "重复地址"}}; | |||
| require(!project.validate(), "duplicate register comments must be rejected"); | |||
| require(!project.validate(defaultProjectLimitSettings()), "duplicate register comments must be rejected"); | |||
| } | |||
| void testDataInstructionBoundaries() | |||
| @@ -772,13 +774,13 @@ void testLadderLogicBoundaries() | |||
| logic.verticalConnections = { | |||
| {"vertical-left", "rung-1", "rung-2", 0}, | |||
| {"vertical-right", "rung-1", "rung-2", 1}}; | |||
| require(logic.validate() && logic.validateForRunning(), | |||
| require(logic.validate(defaultProjectLimitSettings()) && logic.validateForRunning(defaultProjectLimitSettings()), | |||
| "a ten-cell grid with adjacent vertical edges must be valid"); | |||
| logic.rungs.front().cells[5].kind = LadderCellKind::Gap; | |||
| std::string connectivity_error; | |||
| require( | |||
| logic.validate() && !logic.validateForRunning(&connectivity_error) | |||
| logic.validate(defaultProjectLimitSettings()) && !logic.validateForRunning(&connectivity_error) | |||
| && connectivity_error.find("第 1 行") != std::string::npos | |||
| && connectivity_error.find("第 6 列") != std::string::npos, | |||
| "a disconnected output must report its visual row and break column"); | |||
| @@ -794,7 +796,7 @@ void testLadderLogicBoundaries() | |||
| {"vertical-left", "rung-1", "rung-2", 0}, | |||
| {"vertical-bypass", "rung-1", "rung-2", 6}}; | |||
| require( | |||
| logic.validateForRunning(), | |||
| logic.validateForRunning(defaultProjectLimitSettings()), | |||
| "a vertical branch that bypasses a gap must keep the output reachable"); | |||
| logic.rungs = {upper, lower}; | |||
| @@ -806,7 +808,7 @@ void testLadderLogicBoundaries() | |||
| CoilNodeConfig{ | |||
| RegisterAddress{RegisterArea::M, 2}, CoilMode::Normal}, | |||
| true}; | |||
| require(!logic.validate(), "a condition cell must reject output nodes"); | |||
| require(!logic.validate(defaultProjectLimitSettings()), "a condition cell must reject output nodes"); | |||
| logic.rungs.front() = upper; | |||
| logic.rungs.front().output = LogicNode{ | |||
| @@ -814,25 +816,25 @@ void testLadderLogicBoundaries() | |||
| ContactNodeConfig{ | |||
| RegisterAddress{RegisterArea::M, 2}, ContactMode::NormallyOpen}, | |||
| true}; | |||
| require(!logic.validate(), "the output slot must reject condition nodes"); | |||
| require(!logic.validate(defaultProjectLimitSettings()), "the output slot must reject condition nodes"); | |||
| logic.rungs.front() = upper; | |||
| logic.rungs.front().output.reset(); | |||
| require(logic.validate() && logic.validateForRunning(), | |||
| require(logic.validate(defaultProjectLimitSettings()) && logic.validateForRunning(defaultProjectLimitSettings()), | |||
| "a row without an output may act as a connected branch"); | |||
| logic.rungs.front() = upper; | |||
| logic.rungs.front().cells[1].id = logic.rungs.front().cells[0].id; | |||
| require(!logic.validate(), "cell ids must be unique within a logic"); | |||
| require(!logic.validate(defaultProjectLimitSettings()), "cell ids must be unique within a logic"); | |||
| logic.rungs.front() = upper; | |||
| logic.verticalConnections.front().lowerRungId = "missing-rung"; | |||
| require(!logic.validate(), "vertical edges must reference adjacent rows"); | |||
| require(!logic.validate(defaultProjectLimitSettings()), "vertical edges must reference adjacent rows"); | |||
| logic.verticalConnections = { | |||
| {"vertical-left", "rung-1", "rung-2", 0}, | |||
| {"vertical-copy", "rung-1", "rung-2", 0}}; | |||
| require(!logic.validate(), | |||
| require(!logic.validate(defaultProjectLimitSettings()), | |||
| "one row boundary must not contain duplicate vertical edges"); | |||
| } | |||
| @@ -840,38 +842,38 @@ void testModelsValidateBindingsAndIdentifiers() | |||
| { | |||
| // 聚合验证必须拒绝错误绑定、重复标识和越界控件 | |||
| Project project = makeValidProject(); | |||
| require(project.validate(), "valid project model must pass validation"); | |||
| require(project.validate(defaultProjectLimitSettings()), "valid project model must pass validation"); | |||
| project.hmiPages.front().controls.front().binding = | |||
| RegisterAddress{RegisterArea::D, 0}; | |||
| require(!project.validate(), "button bound to D area must be rejected"); | |||
| require(!project.validate(defaultProjectLimitSettings()), "button bound to D area must be rejected"); | |||
| project = makeValidProject(); | |||
| project.hmiPages.push_back(project.hmiPages.front()); | |||
| require(!project.validate(), "duplicate HMI page id must be rejected"); | |||
| require(!project.validate(defaultProjectLimitSettings()), "duplicate HMI page id must be rejected"); | |||
| project = makeValidProject(); | |||
| project.hmiPages.front().controls.front().bounds.x = -1; | |||
| require(!project.validate(), "controls outside the page must be rejected"); | |||
| require(!project.validate(defaultProjectLimitSettings()), "controls outside the page must be rejected"); | |||
| project = makeValidProject(); | |||
| project.hmiPages.front().controls.front().bounds.width = 801; | |||
| require(!project.validate(), "controls wider than the page must be rejected"); | |||
| require(!project.validate(defaultProjectLimitSettings()), "controls wider than the page must be rejected"); | |||
| project = makeValidProject(); | |||
| project.hmiPages.front().controls.front().properties.emplace("", "value"); | |||
| require(!project.validate(), "empty HMI property names must be rejected"); | |||
| require(!project.validate(defaultProjectLimitSettings()), "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(), | |||
| require(project.validate(defaultProjectLimitSettings()), "unbound HMI control must be accepted in a draft"); | |||
| require(!project.validateForRunning(defaultProjectLimitSettings()), | |||
| "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(), | |||
| require(project.validate(defaultProjectLimitSettings()), "unconfigured ladder node must be accepted in a draft"); | |||
| require(!project.validateForRunning(defaultProjectLimitSettings()), | |||
| "unconfigured ladder node must block runtime validation"); | |||
| } | |||
| @@ -24,8 +24,8 @@ void testControlEditing() | |||
| { | |||
| // 覆盖控件创建、移动、绑定校验、重命名冲突和删除流程 | |||
| TestProjectStorage storage; | |||
| ProjectService project_service(storage); | |||
| HmiEditorService service(project_service); | |||
| ProjectService project_service(storage, defaultProjectLimitSettings()); | |||
| HmiEditorService service(project_service, HmiDefaultSettings{}); | |||
| const HmiEditorResult page_result = service.ensureDefaultPage(); | |||
| require(page_result.succeeded, "default HMI page creation must succeed"); | |||
| @@ -190,8 +190,8 @@ void testRuntimeUsesRegisterRepository() | |||
| void testHistoryAndAtomicBatchDelete() | |||
| { | |||
| TestProjectStorage storage; | |||
| ProjectService project_service(storage); | |||
| HmiEditorService service(project_service); | |||
| ProjectService project_service(storage, defaultProjectLimitSettings()); | |||
| HmiEditorService service(project_service, HmiDefaultSettings{}); | |||
| const std::string page_id = service.ensureDefaultPage().id; | |||
| const HmiEditorResult first = service.addControl(page_id, HmiControlType::Label); | |||
| const HmiEditorResult second = service.addControl(page_id, HmiControlType::Label); | |||
| @@ -244,8 +244,8 @@ void testHistoryAndAtomicBatchDelete() | |||
| void testBatchPasteControls() | |||
| { | |||
| TestProjectStorage storage; | |||
| ProjectService project_service(storage); | |||
| HmiEditorService service(project_service); | |||
| ProjectService project_service(storage, defaultProjectLimitSettings()); | |||
| HmiEditorService service(project_service, HmiDefaultSettings{}); | |||
| const std::string page_id = service.ensureDefaultPage().id; | |||
| const HmiEditorResult first = service.addControl( | |||
| page_id, HmiControlType::NumericDisplay); | |||
| @@ -290,8 +290,8 @@ void testBatchPasteControls() | |||
| void testRuntimeValidationAndPasteBoundaries() | |||
| { | |||
| TestProjectStorage storage; | |||
| ProjectService project_service(storage); | |||
| HmiEditorService editor(project_service); | |||
| ProjectService project_service(storage, defaultProjectLimitSettings()); | |||
| HmiEditorService editor(project_service, HmiDefaultSettings{}); | |||
| const std::string page_id = editor.ensureDefaultPage().id; | |||
| HmiControl label; | |||
| @@ -366,8 +366,8 @@ void testRuntimeValidationAndPasteBoundaries() | |||
| void testAppearanceEditing() | |||
| { | |||
| TestProjectStorage storage; | |||
| ProjectService project_service(storage); | |||
| HmiEditorService service(project_service); | |||
| ProjectService project_service(storage, defaultProjectLimitSettings()); | |||
| HmiEditorService service(project_service, HmiDefaultSettings{}); | |||
| const std::string page_id = service.ensureDefaultPage().id; | |||
| const HmiEditorResult label = service.addControl(page_id, HmiControlType::Label); | |||
| require(label.succeeded, "a label must be available for appearance editing"); | |||
| @@ -407,8 +407,8 @@ void testAppearanceEditing() | |||
| void testPageLifecycleAndNavigation() | |||
| { | |||
| TestProjectStorage storage; | |||
| ProjectService project_service(storage); | |||
| HmiEditorService service(project_service); | |||
| ProjectService project_service(storage, defaultProjectLimitSettings()); | |||
| HmiEditorService service(project_service, HmiDefaultSettings{}); | |||
| const std::string main_id = service.ensureDefaultPage().id; | |||
| require(project_service.project().initialHmiPageId == main_id, | |||
| "the first page must become the initial HMI page"); | |||
| @@ -474,8 +474,8 @@ void testPageLifecycleAndNavigation() | |||
| void testPageResizeIsAtomicAndUndoable() | |||
| { | |||
| TestProjectStorage storage; | |||
| ProjectService project_service(storage); | |||
| HmiEditorService service(project_service); | |||
| ProjectService project_service(storage, defaultProjectLimitSettings()); | |||
| HmiEditorService service(project_service, HmiDefaultSettings{}); | |||
| const std::string page_id = service.ensureDefaultPage().id; | |||
| const HmiEditorResult label = service.addControl(page_id, HmiControlType::Label); | |||
| require(label.succeeded, "page resize fixture must add a control"); | |||
| @@ -20,10 +20,22 @@ ContactNodeConfig contact( | |||
| return {RegisterAddress{RegisterArea::M, address}, mode}; | |||
| } | |||
| LogicEditorResult setConditionAtColumn( | |||
| LogicEditorService &editor, | |||
| const std::string &logic_id, | |||
| const std::string &rung_id, | |||
| int column, | |||
| const LogicNodeConfig &config, | |||
| bool configured) | |||
| { | |||
| return editor.applyConditionAndAdvance( | |||
| logic_id, {rung_id, column, false}, config, configured).edit; | |||
| } | |||
| struct Fixture | |||
| { | |||
| TestSupport::InMemoryProjectStorage storage; | |||
| ProjectService projects{storage}; | |||
| ProjectService projects{storage, defaultProjectLimitSettings()}; | |||
| LogicEditorService editor{projects}; | |||
| std::string logicId; | |||
| @@ -109,7 +121,7 @@ void testContinuousGridAndIndependentHorizontalWires() | |||
| && !fixture.editor.canUndo(), | |||
| "repeating an existing horizontal wire range must not dirty the project or history"); | |||
| const LogicEditorResult node = fixture.editor.setConditionAtColumn( | |||
| const LogicEditorResult node = setConditionAtColumn(fixture.editor, | |||
| fixture.logicId, rung_id, 3, contact(3), true); | |||
| require(node.succeeded, "a wire cell must accept a condition node"); | |||
| require( | |||
| @@ -211,39 +223,6 @@ void testInsertRowSplitsVerticalEdges() | |||
| } | |||
| } | |||
| void testInsertConditionMovesOnlyRelatedVerticalBoundaries() | |||
| { | |||
| Fixture fixture; | |||
| const std::string first = fixture.addRung(); | |||
| const std::string second = fixture.addRung(); | |||
| const std::string third = fixture.addRung(); | |||
| const std::string fourth = fixture.addRung(); | |||
| require( | |||
| fixture.editor.setVerticalConnection( | |||
| fixture.logicId, first, second, 2, true).succeeded | |||
| && fixture.editor.setVerticalConnection( | |||
| fixture.logicId, first, second, 10, true).succeeded | |||
| && fixture.editor.setVerticalConnection( | |||
| fixture.logicId, third, fourth, 5, true).succeeded, | |||
| "fixture must create related, output-side, and unrelated edges"); | |||
| const LogicEditorResult inserted = fixture.editor.insertConditionAtColumn( | |||
| fixture.logicId, first, 2, contact(12), true); | |||
| require(inserted.succeeded, | |||
| "inserting a condition into the grid must succeed"); | |||
| const ControlLogic *logic = fixture.editor.findLogic(fixture.logicId); | |||
| require( | |||
| connectionAt(*logic, first, second, 2) == nullptr | |||
| && connectionAt(*logic, first, second, 3) != nullptr, | |||
| "an edge at the insertion point must follow shifted grid content"); | |||
| require( | |||
| connectionAt(*logic, first, second, 10) != nullptr, | |||
| "the fixed output-side boundary must remain at column ten"); | |||
| require( | |||
| connectionAt(*logic, third, fourth, 5) != nullptr, | |||
| "inserting into one row must not move edges in unrelated rows"); | |||
| } | |||
| void testDeleteRowMergesOnlyContinuousEdges() | |||
| { | |||
| Fixture fixture; | |||
| @@ -289,9 +268,9 @@ void testParallelBranchCreatesConnectedVisualRow() | |||
| { | |||
| Fixture fixture; | |||
| const std::string source = fixture.addRung(); | |||
| const LogicEditorResult first = fixture.editor.setConditionAtColumn( | |||
| const LogicEditorResult first = setConditionAtColumn(fixture.editor, | |||
| fixture.logicId, source, 2, contact(1), true); | |||
| const LogicEditorResult second = fixture.editor.setConditionAtColumn( | |||
| const LogicEditorResult second = setConditionAtColumn(fixture.editor, | |||
| fixture.logicId, source, 3, contact(2), true); | |||
| require(first.succeeded && second.succeeded, | |||
| "fixture must place adjacent conditions"); | |||
| @@ -323,9 +302,9 @@ void testParallelBranchReusesExistingEdges() | |||
| Fixture fixture; | |||
| const std::string source = fixture.addRung(); | |||
| const std::string following = fixture.addRung(); | |||
| const LogicEditorResult first = fixture.editor.setConditionAtColumn( | |||
| const LogicEditorResult first = setConditionAtColumn(fixture.editor, | |||
| fixture.logicId, source, 2, contact(4), true); | |||
| const LogicEditorResult second = fixture.editor.setConditionAtColumn( | |||
| const LogicEditorResult second = setConditionAtColumn(fixture.editor, | |||
| fixture.logicId, source, 3, contact(5), true); | |||
| require(first.succeeded && second.succeeded, | |||
| "fixture must place the branch source conditions"); | |||
| @@ -358,7 +337,7 @@ void testNodeDeletionAndHistoryAreAtomic() | |||
| { | |||
| Fixture fixture; | |||
| const std::string rung_id = fixture.addRung(); | |||
| const LogicEditorResult node = fixture.editor.setConditionAtColumn( | |||
| const LogicEditorResult node = setConditionAtColumn(fixture.editor, | |||
| fixture.logicId, rung_id, 0, contact(9), true); | |||
| require(node.succeeded, "fixture must place a condition"); | |||
| fixture.editor.clearHistory(); | |||
| @@ -380,7 +359,7 @@ void testNodeDeletionAndHistoryAreAtomic() | |||
| } | |||
| require(fixture.editor.redo().succeeded, "the drag must redo atomically"); | |||
| require( | |||
| fixture.editor.removeNode(fixture.logicId, node.id).succeeded, | |||
| fixture.editor.removeNodes(fixture.logicId, {node.id}).succeeded, | |||
| "deleting a condition node must succeed"); | |||
| rung = fixture.editor.findRung(fixture.logicId, rung_id); | |||
| require( | |||
| @@ -406,7 +385,7 @@ void testSelectionDeletionIsAtomic() | |||
| const std::string upper = fixture.addRung(); | |||
| const std::string lower = fixture.addRung(); | |||
| require( | |||
| fixture.editor.setConditionAtColumn( | |||
| setConditionAtColumn(fixture.editor, | |||
| fixture.logicId, upper, 0, contact(30), true).succeeded | |||
| && fixture.editor.setHorizontalWireRange( | |||
| fixture.logicId, upper, 1, 1, true).succeeded | |||
| @@ -470,7 +449,7 @@ void testInvalidSelectionDeletionDoesNotMutateOrRecordHistory() | |||
| Fixture fixture; | |||
| const std::string rung_id = fixture.addRung(); | |||
| require( | |||
| fixture.editor.setConditionAtColumn( | |||
| setConditionAtColumn(fixture.editor, | |||
| fixture.logicId, rung_id, 0, contact(40), true).succeeded | |||
| && fixture.editor.setHorizontalWireRange( | |||
| fixture.logicId, rung_id, 1, 1, true).succeeded, | |||
| @@ -564,11 +543,9 @@ void testProjectRungLimitAppliesToBranchAndPaste() | |||
| { | |||
| Fixture fixture; | |||
| const std::string source = fixture.addRung(); | |||
| const LogicEditorResult node = fixture.editor.setConditionAtColumn( | |||
| const LogicEditorResult node = setConditionAtColumn(fixture.editor, | |||
| fixture.logicId, source, 0, contact(20), true); | |||
| require(node.succeeded, "fixture must create a branch source node"); | |||
| const LadderRung source_copy = *fixture.editor.findRung( | |||
| fixture.logicId, source); | |||
| Project &project = fixture.projects.editProject(); | |||
| std::size_t remaining = ProjectLimits::kMaximumRungsPerProject - 1U; | |||
| @@ -601,16 +578,13 @@ void testProjectRungLimitAppliesToBranchAndPaste() | |||
| remaining -= count; | |||
| ++logic_index; | |||
| } | |||
| require(project.validate(), | |||
| require(project.validate(defaultProjectLimitSettings()), | |||
| "the project-wide rung limit fixture must itself be valid"); | |||
| require( | |||
| !fixture.editor.addParallelBranch( | |||
| fixture.logicId, source, {node.id}, contact(21), true).succeeded, | |||
| "parallel insertion must respect the project-wide rung limit"); | |||
| require( | |||
| !fixture.editor.pasteRung(fixture.logicId, source_copy).succeeded, | |||
| "row paste must respect the project-wide rung limit"); | |||
| require( | |||
| fixture.editor.findLogic(fixture.logicId)->rungs.size() == 1U, | |||
| "failed limit checks must not partially add a row"); | |||
| @@ -635,9 +609,9 @@ void testFirstEditOnEmptyLogicIsAtomic() | |||
| Fixture fixture; | |||
| fixture.editor.clearHistory(); | |||
| const LogicEditorResult condition = fixture.editor.appendCondition( | |||
| const LogicEditResult condition = fixture.editor.applyConditionAndAdvance( | |||
| fixture.logicId, {}, contact(11), true); | |||
| require(condition.succeeded, | |||
| require(condition.edit.succeeded, | |||
| "placing the first condition must create the first row"); | |||
| const ControlLogic *logic = fixture.editor.findLogic(fixture.logicId); | |||
| require(logic->rungs.size() == 1U | |||
| @@ -653,10 +627,11 @@ void testFirstEditOnEmptyLogicIsAtomic() | |||
| "redo must restore the atomically created first condition and row"); | |||
| fixture.editor.clearHistory(); | |||
| const LogicEditorResult wire = fixture.editor.appendWire( | |||
| fixture.logicId, {}, 1); | |||
| require(wire.succeeded, | |||
| "appending a wire to an empty logic must create a first row"); | |||
| const std::string wire_rung = fixture.addRung(); | |||
| const LogicEditResult wire = fixture.editor.applyWireAndAdvance( | |||
| fixture.logicId, {wire_rung, 9, false}); | |||
| require(wire.edit.succeeded, | |||
| "a wire edit must succeed on a newly added row"); | |||
| logic = fixture.editor.findLogic(fixture.logicId); | |||
| require(logic->rungs.size() == 2U | |||
| && logic->rungs.back().cells.back().kind == LadderCellKind::Wire, | |||
| @@ -759,9 +734,9 @@ void testOutputAutomaticallyCompletesTrailingWires() | |||
| Fixture trailing; | |||
| const std::string rung_id = trailing.addRung(); | |||
| require( | |||
| trailing.editor.setConditionAtColumn( | |||
| setConditionAtColumn(trailing.editor, | |||
| trailing.logicId, rung_id, 0, contact(55), true).succeeded | |||
| && trailing.editor.setConditionAtColumn( | |||
| && setConditionAtColumn(trailing.editor, | |||
| trailing.logicId, rung_id, 2, contact(56), true).succeeded, | |||
| "the trailing-wire fixture must leave one intentional middle gap"); | |||
| trailing.editor.clearHistory(); | |||
| @@ -888,9 +863,9 @@ void testMixedAndSparseGridClipboardFragments() | |||
| Fixture fixture; | |||
| const std::string source = fixture.addRung(); | |||
| const std::string target = fixture.addRung(); | |||
| const LogicEditorResult first = fixture.editor.setConditionAtColumn( | |||
| const LogicEditorResult first = setConditionAtColumn(fixture.editor, | |||
| fixture.logicId, source, 0, contact(70), true); | |||
| const LogicEditorResult second = fixture.editor.setConditionAtColumn( | |||
| const LogicEditorResult second = setConditionAtColumn(fixture.editor, | |||
| fixture.logicId, source, 2, contact(71), true); | |||
| require( | |||
| first.succeeded && second.succeeded | |||
| @@ -941,7 +916,7 @@ void testMixedAndSparseGridClipboardFragments() | |||
| require( | |||
| sparse.editor.setHorizontalWireRange( | |||
| sparse.logicId, sparse_source, 0, 0, true).succeeded | |||
| && sparse.editor.setConditionAtColumn( | |||
| && setConditionAtColumn(sparse.editor, | |||
| sparse.logicId, sparse_source, 2, contact(72), true).succeeded | |||
| && sparse.editor.setHorizontalWireRange( | |||
| sparse.logicId, sparse_target, 5, 5, true).succeeded, | |||
| @@ -973,7 +948,7 @@ void testGridClipboardFailuresAreAtomic() | |||
| require( | |||
| fixture.editor.setHorizontalWireRange( | |||
| fixture.logicId, source, 0, 1, true).succeeded | |||
| && fixture.editor.setConditionAtColumn( | |||
| && setConditionAtColumn(fixture.editor, | |||
| fixture.logicId, target, 4, contact(73), true).succeeded | |||
| && fixture.editor.setHorizontalWireRange( | |||
| fixture.logicId, target, 0, 0, true).succeeded, | |||
| @@ -1168,7 +1143,7 @@ void testWholeRowClipboardInsertionAndLimit() | |||
| const std::string first = fixture.addRung(); | |||
| const std::string second = fixture.addRung(); | |||
| const std::string target = fixture.addRung(); | |||
| const LogicEditorResult source_node = fixture.editor.setConditionAtColumn( | |||
| const LogicEditorResult source_node = setConditionAtColumn(fixture.editor, | |||
| fixture.logicId, first, 0, contact(90), true); | |||
| require( | |||
| source_node.succeeded | |||
| @@ -1439,7 +1414,6 @@ int main() | |||
| testContinuousGridAndIndependentHorizontalWires(); | |||
| testIndependentVerticalConnectionsAndNetworkSplit(); | |||
| testInsertRowSplitsVerticalEdges(); | |||
| testInsertConditionMovesOnlyRelatedVerticalBoundaries(); | |||
| testDeleteRowMergesOnlyContinuousEdges(); | |||
| testParallelBranchCreatesConnectedVisualRow(); | |||
| testParallelBranchReusesExistingEdges(); | |||
| @@ -747,7 +747,6 @@ void testSimulationLifecycleSnapshotAndFault() | |||
| require(simulation.start({program}).succeeded, "simulation must start"); | |||
| require(simulation.state() == SimulationState::Running, "simulation must report running"); | |||
| require(!readBit(repository, 0), "starting a session must clear virtual registers"); | |||
| require(simulation.scanIntervalMs() == 50, "default scan interval must be 50 ms"); | |||
| writeBit(repository, 0, true); | |||
| program.rungs.front().output = coil("changed-output", 9); | |||
| require(simulation.executeOnce().succeeded, "manual lifecycle scan must succeed"); | |||
| @@ -64,6 +64,15 @@ public: | |||
| return {true, {}}; | |||
| } | |||
| PlcCommunicationResult setPollAddresses( | |||
| const std::vector<RegisterAddress> &addresses, | |||
| const std::vector<RegisterWordRange> &ranges) override | |||
| { | |||
| poll_addresses = addresses; | |||
| poll_ranges = ranges; | |||
| return {true, {}}; | |||
| } | |||
| PlcConnectionState state() const override { return connection_state; } | |||
| bool initialReadCompleted() const override { return initial_read; } | |||
| PlcCommunicationError lastErrorType() const override { return last_error_type; } | |||
| @@ -120,6 +129,7 @@ public: | |||
| std::string last_error; | |||
| PlcSerialConfiguration last_configuration; | |||
| std::vector<RegisterAddress> poll_addresses; | |||
| std::vector<RegisterWordRange> poll_ranges; | |||
| std::function<void()> state_changed; | |||
| std::function<void(bool)> initial_read_changed; | |||
| std::function<void()> cache_updated; | |||
| @@ -211,14 +221,14 @@ void testPlcCacheAndWriteForwarding() | |||
| && multi_word_write_count == 2, | |||
| "Double words must be forwarded in one four-register request"); | |||
| repository.invalidate(); | |||
| require(!repository.hasAnyValidValue(), | |||
| "disconnecting must invalidate all PLC cache validity flags"); | |||
| require(!repository.readWord(d0).succeeded, | |||
| "disconnecting must invalidate cached PLC values"); | |||
| } | |||
| void testRuntimeRepositorySwitchingAndDisconnect() | |||
| { | |||
| TestProjectStorage storage; | |||
| ProjectService project_service(storage); | |||
| ProjectService project_service(storage, defaultProjectLimitSettings()); | |||
| HmiPage page; | |||
| page.id = "main-page"; | |||
| page.name = "Main"; | |||
| @@ -287,7 +297,7 @@ void testRuntimeRepositorySwitchingAndDisconnect() | |||
| void testRuntimeFaultRevokesOnlineReadinessAndAllowsReconnect() | |||
| { | |||
| TestProjectStorage storage; | |||
| ProjectService project_service(storage); | |||
| ProjectService project_service(storage, defaultProjectLimitSettings()); | |||
| VirtualRegisterRepository virtual_repository; | |||
| PlcRegisterRepository plc_repository; | |||
| ActiveRegisterRepository active_repository(virtual_repository); | |||
| @@ -502,13 +512,7 @@ void testPlcPollQuantityBoundaries() | |||
| } | |||
| require(service.setPollAddresses( | |||
| double_boundary_addresses, | |||
| {{RegisterAddress{RegisterArea::D, 118}, 4}}).succeeded | |||
| && service.pollBlocks().size() == 2U | |||
| && service.pollBlocks()[0].area == RegisterArea::D | |||
| && service.pollBlocks()[0].startAddress == 0 | |||
| && service.pollBlocks()[0].count == 118 | |||
| && service.pollBlocks()[1].startAddress == 118 | |||
| && service.pollBlocks()[1].count == 6, | |||
| {{RegisterAddress{RegisterArea::D, 118}, 4}}).succeeded, | |||
| "a Double at the 120-word boundary must stay in one Modbus read block"); | |||
| std::vector<RegisterWordRange> unsplittable_ranges; | |||
| @@ -163,8 +163,8 @@ void testEmptyProjectRoundTrip() | |||
| { | |||
| QTemporaryDir directory; | |||
| require(directory.isValid(), "temporary directory must be valid"); | |||
| JsonProjectStorage storage; | |||
| ProjectService service(storage); | |||
| JsonProjectStorage storage(defaultProjectLimitSettings()); | |||
| ProjectService service(storage, defaultProjectLimitSettings()); | |||
| require(service.createNewProject("Empty project").succeeded, | |||
| "empty project creation must succeed"); | |||
| require(service.project().metadata.formatVersion == "2.0", | |||
| @@ -184,10 +184,10 @@ void testGridProjectRoundTrip() | |||
| { | |||
| QTemporaryDir directory; | |||
| require(directory.isValid(), "temporary directory must be valid"); | |||
| JsonProjectStorage storage; | |||
| JsonProjectStorage storage(defaultProjectLimitSettings()); | |||
| const QString path = directory.filePath("grid.json"); | |||
| const Project original = makeExampleProject(); | |||
| require(original.validate(), "the grid fixture must be structurally valid"); | |||
| require(original.validate(defaultProjectLimitSettings()), "the grid fixture must be structurally valid"); | |||
| require(storage.save(original, path.toStdString()).succeeded, | |||
| "the grid project must save"); | |||
| @@ -225,7 +225,7 @@ void testMOffAlarmRoundTrip() | |||
| { | |||
| QTemporaryDir directory; | |||
| require(directory.isValid(), "temporary directory must be valid"); | |||
| JsonProjectStorage storage; | |||
| JsonProjectStorage storage(defaultProjectLimitSettings()); | |||
| const QString path = directory.filePath("m-off-alarm.json"); | |||
| Project project = makeExampleProject(); | |||
| project.alarmDefinitions.push_back({ | |||
| @@ -254,7 +254,7 @@ void testMultiWordHmiDataTypeRoundTrip() | |||
| { | |||
| QTemporaryDir directory; | |||
| require(directory.isValid(), "temporary directory must be valid"); | |||
| JsonProjectStorage storage; | |||
| JsonProjectStorage storage(defaultProjectLimitSettings()); | |||
| const QString path = directory.filePath("multi-word-hmi.json"); | |||
| Project project = makeExampleProject(); | |||
| @@ -317,7 +317,7 @@ void testStrictVersionAndRequiredFields() | |||
| { | |||
| QTemporaryDir directory; | |||
| require(directory.isValid(), "temporary directory must be valid"); | |||
| JsonProjectStorage storage; | |||
| JsonProjectStorage storage(defaultProjectLimitSettings()); | |||
| const QString fixture_path = directory.filePath("fixture.json"); | |||
| QJsonObject root = savedFixture( | |||
| storage, makeExampleProject(), fixture_path); | |||
| @@ -346,7 +346,7 @@ void testInvalidGridAndConnectionsAreRejected() | |||
| { | |||
| QTemporaryDir directory; | |||
| require(directory.isValid(), "temporary directory must be valid"); | |||
| JsonProjectStorage storage; | |||
| JsonProjectStorage storage(defaultProjectLimitSettings()); | |||
| const QString fixture_path = directory.filePath("fixture.json"); | |||
| const QJsonObject original = savedFixture( | |||
| storage, makeExampleProject(), fixture_path); | |||
| @@ -417,7 +417,7 @@ void testProjectServiceStateAndConfiguredLimits() | |||
| QTemporaryDir directory; | |||
| require(directory.isValid(), "temporary directory must be valid"); | |||
| const QString path = directory.filePath("project.json"); | |||
| JsonProjectStorage storage; | |||
| JsonProjectStorage storage(defaultProjectLimitSettings()); | |||
| require(storage.save(makeExampleProject(), path.toStdString()).succeeded, | |||
| "service fixture must save"); | |||
| @@ -435,7 +435,7 @@ void testProjectServiceStateAndConfiguredLimits() | |||
| && !limited_service.hasCurrentFile(), | |||
| "a failed load must leave the current project untouched"); | |||
| ProjectService service(storage); | |||
| ProjectService service(storage, defaultProjectLimitSettings()); | |||
| require(service.save().error == ProjectServiceError::FilePathRequired, | |||
| "save without a current path must fail explicitly"); | |||
| require(service.createNewProject("State project").succeeded, | |||
| @@ -450,8 +450,8 @@ void testProjectServiceStateAndConfiguredLimits() | |||
| void testRegisterCommentService() | |||
| { | |||
| JsonProjectStorage storage; | |||
| ProjectService projects(storage); | |||
| JsonProjectStorage storage(defaultProjectLimitSettings()); | |||
| ProjectService projects(storage, defaultProjectLimitSettings()); | |||
| RegisterCommentService comments(projects); | |||
| require(comments.setComment( | |||
| RegisterAddress{RegisterArea::D, 3}, " Target value ") | |||
| @@ -88,20 +88,24 @@ void testRegisterWrites() | |||
| 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); | |||
| const RegisterMonitorWriteResult word_write = service.writeNumeric( | |||
| d0, RegisterDataType::Int16, -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); | |||
| const RegisterMonitorWriteResult wrong_word = service.writeNumeric( | |||
| m0, RegisterDataType::Int16, 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 | |||
| require(service.writeNumeric( | |||
| d0, RegisterDataType::Int16, static_cast<std::int16_t>(-32768)).succeeded | |||
| && repository.readWord(d0).value == -32768 | |||
| && service.writeWord(d0, static_cast<std::int16_t>(32767)).succeeded | |||
| && service.writeNumeric( | |||
| d0, RegisterDataType::Int16, static_cast<std::int16_t>(32767)).succeeded | |||
| && repository.readWord(d0).value == 32767, | |||
| "monitor word writes must preserve signed 16-bit boundary values"); | |||
| } | |||
| @@ -116,8 +120,10 @@ void testOfflineInitialValueCapture() | |||
| service.setOfflineInitialCaptureEnabled(true); | |||
| require(service.writeBit({RegisterArea::M, 3}, true).succeeded | |||
| && service.writeWord({RegisterArea::D, 10}, -456).succeeded | |||
| && service.writeFloat({RegisterArea::D, 20}, 1.5f).succeeded | |||
| && service.writeNumeric( | |||
| {RegisterArea::D, 10}, RegisterDataType::Int16, -456).succeeded | |||
| && service.writeNumeric( | |||
| {RegisterArea::D, 20}, RegisterDataType::Float32, 1.5f).succeeded | |||
| && service.writeNumeric( | |||
| {RegisterArea::D, 30}, RegisterDataType::Int32, 305419896) | |||
| .succeeded | |||
| @@ -132,8 +138,8 @@ void testOfflineInitialValueCapture() | |||
| require(initial_repository.readBit({RegisterArea::M, 3}).value | |||
| && initial_repository.readWord({RegisterArea::D, 10}).value == -456 | |||
| && std::fabs(Float32Codec::decode( | |||
| initial_repository.readWordPair({RegisterArea::D, 20}).values[0], | |||
| initial_repository.readWordPair({RegisterArea::D, 20}).values[1]) | |||
| initial_repository.readWords({RegisterArea::D, 20}, 2).values[0], | |||
| initial_repository.readWords({RegisterArea::D, 20}, 2).values[1]) | |||
| .value_or(0.0f) - 1.5f) < 0.000001f, | |||
| "editing monitor writes must be captured as offline initial values"); | |||
| require(captured_int32.succeeded | |||
| @@ -182,7 +188,8 @@ void testMultiWordMonitoring() | |||
| && service.points().size() == 3U, | |||
| "overlapping monitor removal must identify the point type"); | |||
| require(service.writeFloat({RegisterArea::D, 1}, -2.5f).succeeded, | |||
| require(service.writeNumeric( | |||
| {RegisterArea::D, 1}, RegisterDataType::Float32, -2.5f).succeeded, | |||
| "Float32 monitor writes must use the pair write path"); | |||
| std::vector<MonitorValue> values = service.values(false); | |||
| require(values.front().state == MonitorValueState::Valid | |||
| @@ -190,7 +197,8 @@ void testMultiWordMonitoring() | |||
| std::get<float>(values.front().numericValue) - (-2.5f)) | |||
| < 0.000001f, | |||
| "Float32 monitor values must decode the pair correctly"); | |||
| require(!service.writeFloat({RegisterArea::D, 4000}, 1.0f).succeeded, | |||
| require(!service.writeNumeric( | |||
| {RegisterArea::D, 4000}, RegisterDataType::Float32, 1.0f).succeeded, | |||
| "Float32 monitor writes at D4000 must be rejected"); | |||
| const RegisterMonitorResult int32_added = service.addRange( | |||
| @@ -138,7 +138,7 @@ void testModeTransitions() | |||
| { | |||
| // 验证服务将 PLC 首读状态与领域模式切换规则正确组合 | |||
| TestProjectStorage storage; | |||
| ProjectService project_service(storage); | |||
| ProjectService project_service(storage, defaultProjectLimitSettings()); | |||
| VirtualRegisterRepository virtual_repository; | |||
| VirtualRegisterRepository plc_repository; | |||
| ActiveRegisterRepository active_repository(virtual_repository); | |||
| @@ -325,7 +325,7 @@ void testModeTransitions() | |||
| void testMonitorPollRangeRollback() | |||
| { | |||
| TestProjectStorage storage; | |||
| ProjectService project_service(storage); | |||
| ProjectService project_service(storage, defaultProjectLimitSettings()); | |||
| VirtualRegisterRepository virtual_repository; | |||
| VirtualRegisterRepository plc_repository; | |||
| ActiveRegisterRepository active_repository(virtual_repository); | |||
| @@ -359,7 +359,7 @@ void testMonitorPollRangeRollback() | |||
| void testDisconnectedOutputBlocksOfflineAndOnlineRuntime() | |||
| { | |||
| TestProjectStorage storage; | |||
| ProjectService project_service(storage); | |||
| ProjectService project_service(storage, defaultProjectLimitSettings()); | |||
| VirtualRegisterRepository virtual_repository; | |||
| VirtualRegisterRepository plc_repository; | |||
| ActiveRegisterRepository active_repository(virtual_repository); | |||
| @@ -42,6 +42,18 @@ | |||
| namespace { | |||
| LogicEditorResult setConditionAtColumn( | |||
| LogicEditorService &editor, | |||
| const std::string &logic_id, | |||
| const std::string &rung_id, | |||
| int column, | |||
| const LogicNodeConfig &config, | |||
| bool configured) | |||
| { | |||
| return editor.applyConditionAndAdvance( | |||
| logic_id, {rung_id, column, false}, config, configured).edit; | |||
| } | |||
| using TestProjectStorage = TestSupport::InMemoryProjectStorage; | |||
| using TestSupport::require; | |||
| @@ -98,7 +110,7 @@ void testMonitorOffersAllNumericTypes() | |||
| void testAlarmConfigurationOffersMOnAndMOff() | |||
| { | |||
| TestProjectStorage storage; | |||
| ProjectService project_service(storage); | |||
| ProjectService project_service(storage, defaultProjectLimitSettings()); | |||
| AlarmEditorService alarm_editor_service(project_service); | |||
| AlarmConfigurationDialog dialog(alarm_editor_service); | |||
| @@ -123,9 +135,9 @@ void testAlarmConfigurationOffersMOnAndMOff() | |||
| void testAlarmListKeepsFixedGeometryWhileRecordsChange() | |||
| { | |||
| TestProjectStorage storage; | |||
| ProjectService project_service(storage); | |||
| ProjectService project_service(storage, defaultProjectLimitSettings()); | |||
| VirtualRegisterRepository virtual_repository; | |||
| HmiEditorService hmi_editor_service(project_service); | |||
| HmiEditorService hmi_editor_service(project_service, HmiDefaultSettings{}); | |||
| HmiRuntimeService hmi_runtime_service(virtual_repository); | |||
| AlarmService alarm_service(project_service, virtual_repository); | |||
| @@ -195,7 +207,7 @@ void testAlarmListKeepsFixedGeometryWhileRecordsChange() | |||
| void testQueuedOfflineTraceIsIgnoredAfterReturningToEditing() | |||
| { | |||
| TestProjectStorage storage; | |||
| ProjectService project_service(storage); | |||
| ProjectService project_service(storage, defaultProjectLimitSettings()); | |||
| VirtualRegisterRepository virtual_repository; | |||
| OfflineSimulationService simulation_service(virtual_repository); | |||
| OnlineLogicMonitorService online_monitor_service(virtual_repository); | |||
| @@ -205,7 +217,7 @@ void testQueuedOfflineTraceIsIgnoredAfterReturningToEditing() | |||
| logic_editor_service, | |||
| simulation_service, | |||
| online_monitor_service); | |||
| HmiEditorService hmi_editor_service(project_service); | |||
| HmiEditorService hmi_editor_service(project_service, HmiDefaultSettings{}); | |||
| HmiRuntimeService hmi_runtime_service(virtual_repository); | |||
| HmiNavigationService hmi_navigation_service(project_service); | |||
| AlarmService alarm_service(project_service, virtual_repository); | |||
| @@ -301,18 +313,31 @@ void testQueuedOfflineTraceIsIgnoredAfterReturningToEditing() | |||
| controller.leaveRuntime( | |||
| runtime_mode_service.mode(), runtime_mode_service.plcConnectionState()); | |||
| logic_editor_widget.clearRuntimeTrace(); | |||
| require(!logic_editor_widget.runtimeTraceEnabled(), | |||
| const auto runtimeOutputIsActive = [runtime_logic_view] | |||
| { | |||
| for (QGraphicsItem *item : runtime_logic_view->scene()->items()) | |||
| { | |||
| if (item->data(0).toString() == QStringLiteral("output") | |||
| && item->data(1).toString() | |||
| == QStringLiteral("always-on-rung")) | |||
| { | |||
| return item->data(3).toBool() && item->data(4).toBool(); | |||
| } | |||
| } | |||
| return false; | |||
| }; | |||
| require(!runtimeOutputIsActive(), | |||
| "editing transition must initially clear the runtime trace"); | |||
| QCoreApplication::processEvents(QEventLoop::AllEvents); | |||
| require(!logic_editor_widget.runtimeTraceEnabled(), | |||
| require(!runtimeOutputIsActive(), | |||
| "a queued offline scan must not restore the trace after returning to editing"); | |||
| } | |||
| void testLogicEditorGridSelectionAndDeletion() | |||
| { | |||
| TestProjectStorage storage; | |||
| ProjectService project_service(storage); | |||
| ProjectService project_service(storage, defaultProjectLimitSettings()); | |||
| LogicEditorService logic_editor_service(project_service); | |||
| const std::string logic_id = logic_editor_service.ensureDefaultLogic().id; | |||
| @@ -441,7 +466,7 @@ void testLogicEditorGridSelectionAndDeletion() | |||
| == LadderCellKind::Wire, | |||
| "the horizontal-wire command must act on an explicitly selected cell"); | |||
| const LogicEditorResult first_node = logic_editor_service.setConditionAtColumn( | |||
| const LogicEditorResult first_node = setConditionAtColumn(logic_editor_service, | |||
| logic_id, | |||
| rung_id, | |||
| 0, | |||
| @@ -449,7 +474,7 @@ void testLogicEditorGridSelectionAndDeletion() | |||
| RegisterAddress{RegisterArea::M, 0}, | |||
| ContactMode::NormallyOpen}, | |||
| true); | |||
| const LogicEditorResult second_node = logic_editor_service.setConditionAtColumn( | |||
| const LogicEditorResult second_node = setConditionAtColumn(logic_editor_service, | |||
| logic_id, | |||
| rung_id, | |||
| 1, | |||
| @@ -509,13 +534,13 @@ void testLogicEditorGridSelectionAndDeletion() | |||
| void testLadderLayoutAndDragDeletion() | |||
| { | |||
| TestProjectStorage storage; | |||
| ProjectService project_service(storage); | |||
| ProjectService project_service(storage, defaultProjectLimitSettings()); | |||
| LogicEditorService editor(project_service); | |||
| const std::string logic_id = editor.ensureDefaultLogic().id; | |||
| const std::string upper = editor.addRung(logic_id).id; | |||
| const std::string lower = editor.addRung(logic_id).id; | |||
| require( | |||
| editor.setConditionAtColumn( | |||
| setConditionAtColumn(editor, | |||
| logic_id, | |||
| upper, | |||
| 0, | |||
| @@ -688,7 +713,7 @@ void testLadderLayoutAndDragDeletion() | |||
| void testWireGesturePreviewKeepsAtomicCommit() | |||
| { | |||
| TestProjectStorage storage; | |||
| ProjectService project_service(storage); | |||
| ProjectService project_service(storage, defaultProjectLimitSettings()); | |||
| LogicEditorService editor(project_service); | |||
| const std::string logic_id = editor.ensureDefaultLogic().id; | |||
| const std::string first = editor.addRung(logic_id).id; | |||
| @@ -842,7 +867,7 @@ void testWireGesturePreviewKeepsAtomicCommit() | |||
| void testEscapeExitsMouseWireMode() | |||
| { | |||
| TestProjectStorage storage; | |||
| ProjectService project_service(storage); | |||
| ProjectService project_service(storage, defaultProjectLimitSettings()); | |||
| LogicEditorService editor(project_service); | |||
| LogicEditorWidget widget(editor); | |||
| @@ -872,7 +897,7 @@ void testEscapeExitsMouseWireMode() | |||
| void testCursorAdvanceAndInlineCommandInput() | |||
| { | |||
| TestProjectStorage storage; | |||
| ProjectService project_service(storage); | |||
| ProjectService project_service(storage, defaultProjectLimitSettings()); | |||
| LogicEditorService editor(project_service); | |||
| const std::string logic_id = editor.ensureDefaultLogic().id; | |||
| LogicEditorWidget widget(editor); | |||
| @@ -970,7 +995,7 @@ void testCursorAdvanceAndInlineCommandInput() | |||
| void testVerticalWireShortcutAdvancesDownward() | |||
| { | |||
| TestProjectStorage storage; | |||
| ProjectService project_service(storage); | |||
| ProjectService project_service(storage, defaultProjectLimitSettings()); | |||
| LogicEditorService editor(project_service); | |||
| const std::string logic_id = editor.ensureDefaultLogic().id; | |||
| std::vector<std::string> rung_ids; | |||
| @@ -1059,12 +1084,12 @@ void testVerticalWireShortcutAdvancesDownward() | |||
| void testSegmentLevelTraceProjection() | |||
| { | |||
| TestProjectStorage storage; | |||
| ProjectService project_service(storage); | |||
| ProjectService project_service(storage, defaultProjectLimitSettings()); | |||
| LogicEditorService editor(project_service); | |||
| const std::string logic_id = editor.ensureDefaultLogic().id; | |||
| const std::string upper = editor.addRung(logic_id).id; | |||
| const std::string lower = editor.addRung(logic_id).id; | |||
| const LogicEditorResult condition = editor.setConditionAtColumn( | |||
| const LogicEditorResult condition = setConditionAtColumn(editor, | |||
| logic_id, | |||
| upper, | |||
| 0, | |||
| @@ -1157,7 +1182,7 @@ void testSegmentLevelTraceProjection() | |||
| void testLogicClipboardUsesExplicitObjectAndRowSelection() | |||
| { | |||
| TestProjectStorage storage; | |||
| ProjectService project_service(storage); | |||
| ProjectService project_service(storage, defaultProjectLimitSettings()); | |||
| LogicEditorService editor(project_service); | |||
| const std::string logic_id = editor.ensureDefaultLogic().id; | |||
| const std::string source = editor.addRung(logic_id).id; | |||