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