Selaa lähdekoodia

refactor: 移除定时器和计数器指令

main
suyu 3 viikkoa sitten
vanhempi
commit
54476bd248
22 muutettua tiedostoa jossa 56 lisäystä ja 1989 poistoa
  1. +0
    -307
      app/src/domain/control_logic_model.cpp
  2. +1
    -102
      app/src/domain/control_logic_model.h
  3. +0
    -4
      app/src/domain/project_model.cpp
  4. +9
    -217
      app/src/infrastructure/json_project_storage.cpp
  5. +0
    -45
      app/src/services/logic_editor_service.cpp
  6. +1
    -1
      app/src/services/offline_simulation_service.cpp
  7. +3
    -176
      app/src/services/software_logic_executor.cpp
  8. +4
    -66
      app/src/services/software_logic_executor.h
  9. +1
    -130
      app/src/ui/logic_editor_widget.cpp
  10. +2
    -44
      app/src/ui/logic_instruction_dialog.cpp
  11. +0
    -2
      app/src/ui/logic_instruction_dialog.h
  12. +0
    -21
      app/src/ui/logic_instruction_dialog.ui
  13. +1
    -83
      app/src/ui/main_window.cpp
  14. +1
    -49
      app/src/ui/main_window.ui
  15. +0
    -69
      app/src/ui/property_panel_controller.cpp
  16. +0
    -25
      app/src/ui/toolbar_icon_factory.cpp
  17. +0
    -5
      app/src/ui/toolbar_icon_factory.h
  18. +4
    -181
      app/tests/domain_tests.cpp
  19. +7
    -34
      app/tests/logic_editor_service_tests.cpp
  20. +0
    -258
      app/tests/offline_simulation_service_tests.cpp
  21. +21
    -118
      app/tests/project_management_tests.cpp
  22. +1
    -52
      app/tests/runtime_mode_service_tests.cpp

+ 0
- 307
app/src/domain/control_logic_model.cpp Näytä tiedosto

@@ -3,7 +3,6 @@
#include "project_limits.h"

#include <algorithm>
#include <map>
#include <type_traits>
#include <utility>

@@ -52,22 +51,6 @@ bool validateWordAddress(
return true;
}

bool validateConfig(const CounterContactNodeConfig &config, std::string *error)
{
if (!config.address.isValid())
{
setError(error, "计数器触点必须使用有效的 C 地址");
return false;
}
if (config.mode != ContactMode::NormallyOpen
&& config.mode != ContactMode::NormallyClosed)
{
setError(error, "计数器触点使用了不支持的模式");
return false;
}
return true;
}

bool validateConfig(const EdgeContactNodeConfig &config, std::string *error)
{
if (!config.address.isValid() || config.address.area() != RegisterArea::M)
@@ -120,70 +103,6 @@ bool validateConfig(const CompareNodeConfig &config, std::string *error)
return true;
}

bool validateConfig(const TimerContactNodeConfig &config, std::string *error)
{
if (!config.address.isValid())
{
setError(error, "定时器触点必须使用有效的 T 地址");
return false;
}
if (config.mode != ContactMode::NormallyOpen
&& config.mode != ContactMode::NormallyClosed)
{
setError(error, "定时器触点使用了不支持的模式");
return false;
}
return true;
}

bool validateConfig(const TonNodeConfig &config, std::string *error)
{
if (!config.address.isValid())
{
setError(error, "TON 必须使用有效的 T 地址");
return false;
}
if (config.presetMs < TonNodeConfig::kMinimumPresetMs
|| config.presetMs > TonNodeConfig::kMaximumPresetMs)
{
setError(error, "TON 预设时间必须在 1~86400000 ms 范围内");
return false;
}
return true;
}

bool validateConfig(const CounterNodeConfig &config, std::string *error)
{
if (!config.address.isValid())
{
setError(error, "计数器指令必须使用有效的 C 地址");
return false;
}
if (config.mode != CounterMode::Up && config.mode != CounterMode::Down)
{
setError(error, "计数器指令使用了不支持的方向");
return false;
}
if (!validateWordAddress(config.currentValueAddress, "计数当前值", error)
|| !config.preset.validate(error))
{
return false;
}
if (config.preset.kind == WordOperandKind::Constant
&& config.preset.constant < 0)
{
setError(error, "计数器常量预设值必须在 0~32767 范围内");
return false;
}
if (!config.resetAddress.isValid()
|| config.resetAddress.area() != RegisterArea::M)
{
setError(error, "计数器复位输入必须使用有效的 M 区地址");
return false;
}
return true;
}

bool validateConfig(const MoveNodeConfig &config, std::string *error)
{
if (!config.source.validate(error))
@@ -419,11 +338,6 @@ bool validateConditionExpression(

} // namespace

TimerAddress::TimerAddress(int index)
: index_(index)
{
}

bool WordOperand::validate(std::string *error) const
{
if (kind == WordOperandKind::Constant)
@@ -443,61 +357,6 @@ bool WordOperand::validate(std::string *error) const
return true;
}

CounterAddress::CounterAddress(int index)
: index_(index)
{
}

int CounterAddress::index() const
{
return index_;
}

bool CounterAddress::isValid() const
{
return index_ >= kMinimumIndex && index_ <= kMaximumIndex;
}

std::string CounterAddress::toString() const
{
return isValid() ? "C" + std::to_string(index_) : "InvalidCounterAddress";
}

bool CounterAddress::operator==(const CounterAddress &other) const
{
return index_ == other.index_;
}

bool CounterAddress::operator!=(const CounterAddress &other) const
{
return !(*this == other);
}

int TimerAddress::index() const
{
return index_;
}

bool TimerAddress::isValid() const
{
return index_ >= kMinimumIndex && index_ <= kMaximumIndex;
}

std::string TimerAddress::toString() const
{
return isValid() ? "T" + std::to_string(index_) : "InvalidTimerAddress";
}

bool TimerAddress::operator==(const TimerAddress &other) const
{
return index_ == other.index_;
}

bool TimerAddress::operator!=(const TimerAddress &other) const
{
return !(*this == other);
}

std::optional<RegisterAddress> registerAddressForLogicNode(
const LogicNodeConfig &config)
{
@@ -512,10 +371,6 @@ std::optional<RegisterAddress> registerAddressForLogicNode(
{
return value.address;
}
else if constexpr (std::is_same_v<Config, CounterNodeConfig>)
{
return value.currentValueAddress;
}
else if constexpr (std::is_same_v<Config, MoveNodeConfig>
|| std::is_same_v<Config, ArithmeticNodeConfig>)
{
@@ -545,15 +400,6 @@ void collectRegisterAddressesForLogicNode(
{
appendValidAddress(addresses, value.address);
}
else if constexpr (std::is_same_v<Config, CounterNodeConfig>)
{
appendValidAddress(addresses, value.currentValueAddress);
appendValidAddress(addresses, value.resetAddress);
if (value.preset.kind == WordOperandKind::Register)
{
appendValidAddress(addresses, value.preset.address);
}
}
else if constexpr (std::is_same_v<Config, MoveNodeConfig>)
{
appendValidAddress(addresses, value.destination);
@@ -574,10 +420,6 @@ void collectRegisterAddressesForLogicNode(
appendValidAddress(addresses, value.right.address);
}
}
else if constexpr (std::is_same_v<Config, CounterContactNodeConfig>)
{
return;
}
},
config);
}
@@ -609,8 +451,6 @@ bool LogicNode::isCondition() const
{
return std::holds_alternative<ContactNodeConfig>(config)
|| std::holds_alternative<EdgeContactNodeConfig>(config)
|| std::holds_alternative<TimerContactNodeConfig>(config)
|| std::holds_alternative<CounterContactNodeConfig>(config)
|| std::holds_alternative<CompareNodeConfig>(config);
}

@@ -618,8 +458,6 @@ bool LogicNode::isCondition() const
bool LogicNode::isOutput() const
{
return std::holds_alternative<CoilNodeConfig>(config)
|| std::holds_alternative<TonNodeConfig>(config)
|| std::holds_alternative<CounterNodeConfig>(config)
|| std::holds_alternative<MoveNodeConfig>(config)
|| std::holds_alternative<ArithmeticNodeConfig>(config);
}
@@ -984,148 +822,3 @@ bool ControlLogic::validateForRunning(std::string *error) const
}
return true;
}

bool validateTimerReferencesForRunning(
const std::vector<ControlLogic> &logics,
std::string *error)
{
std::map<int, std::string> timer_outputs;
for (const ControlLogic &logic : logics)
{
if (!logic.enabled)
{
continue;
}
for (const LadderRung &rung : logic.rungs)
{
if (!rung.output.has_value())
{
continue;
}
const auto *ton = std::get_if<TonNodeConfig>(&rung.output->config);
if (ton == nullptr)
{
continue;
}
const auto inserted = timer_outputs.emplace(
ton->address.index(), rung.output->id);
if (!inserted.second)
{
setError(
error,
"定时器 " + ton->address.toString()
+ " 只能由一个已启用 TON 指令驱动");
return false;
}
}
}

for (const ControlLogic &logic : logics)
{
if (!logic.enabled)
{
continue;
}
for (const LadderRung &rung : logic.rungs)
{
if (!rung.condition.has_value())
{
continue;
}
std::vector<const LogicNode *> nodes;
collectConditionNodes(*rung.condition, &nodes);
for (const LogicNode *node : nodes)
{
const auto *contact = std::get_if<TimerContactNodeConfig>(
&node->config);
if (contact != nullptr
&& timer_outputs.count(contact->address.index()) == 0U)
{
setError(
error,
"定时器触点 " + contact->address.toString()
+ " 没有对应的已启用 TON 指令");
return false;
}
}
}
}
return true;
}

bool validateCounterReferencesForRunning(
const std::vector<ControlLogic> &logics,
std::string *error)
{
std::map<int, std::string> counter_outputs;
for (const ControlLogic &logic : logics)
{
if (!logic.enabled)
{
continue;
}
for (const LadderRung &rung : logic.rungs)
{
if (!rung.output.has_value())
{
continue;
}
const auto *counter = std::get_if<CounterNodeConfig>(
&rung.output->config);
if (counter == nullptr)
{
continue;
}
const auto inserted = counter_outputs.emplace(
counter->address.index(), rung.output->id);
if (!inserted.second)
{
setError(
error,
"计数器 " + counter->address.toString()
+ " 只能由一个已启用计数指令驱动");
return false;
}
}
}

for (const ControlLogic &logic : logics)
{
if (!logic.enabled)
{
continue;
}
for (const LadderRung &rung : logic.rungs)
{
if (!rung.condition.has_value())
{
continue;
}
std::vector<const LogicNode *> nodes;
collectConditionNodes(*rung.condition, &nodes);
for (const LogicNode *node : nodes)
{
const auto *contact = std::get_if<CounterContactNodeConfig>(
&node->config);
if (contact != nullptr
&& counter_outputs.count(contact->address.index()) == 0U)
{
setError(
error,
"计数器触点 " + contact->address.toString()
+ " 没有对应的已启用计数指令");
return false;
}
}
}
}
return true;
}

bool validateLogicResourceReferencesForRunning(
const std::vector<ControlLogic> &logics,
std::string *error)
{
return validateTimerReferencesForRunning(logics, error)
&& validateCounterReferencesForRunning(logics, error);
}

+ 1
- 102
app/src/domain/control_logic_model.h Näytä tiedosto

@@ -49,7 +49,7 @@ enum class WordOperandKind
Register // 使用 D 寄存器中的数值
};

// MOVE、ADD/SUB、计数器预置值共用的字操作数
// MOVE、ADD/SUB 共用的字操作数
struct WordOperand
{
WordOperandKind kind = WordOperandKind::Constant; // 操作数来源
@@ -94,90 +94,6 @@ struct CompareNodeConfig
std::int16_t value = 0;
};

// 定时器 T 编号,内部使用,不映射 M/D 寄存器;限制编号范围,判断是否合法,可转字符串
class TimerAddress
{
public:
static constexpr int kMinimumIndex = 0; // 允许使用的最小 T 编号
static constexpr int kMaximumIndex = 4000; // 允许使用的最大 T 编号

explicit TimerAddress(int index = 0); // 使用编号创建 T 地址

int index() const; // 返回地址编号
bool isValid() const; // 判断地址编号是否在有效范围内
std::string toString() const; // 转成 T0 这样的显示文本

bool operator==(const TimerAddress &other) const; // 判断两个 T 地址是否相同
bool operator!=(const TimerAddress &other) const; // 判断两个 T 地址是否不同

private:
int index_ = 0; // 保存 T 地址编号
};

// 读取 TON 完成状态的条件触点
struct TimerContactNodeConfig
{
// 要读取完成状态的定时器地址
TimerAddress address;
// 按常开或常闭方式判断完成状态
ContactMode mode = ContactMode::NormallyOpen;
};

// 离线执行器内部使用的 C 区地址,不会直接映射 PLC C 区
class CounterAddress
{
public:
static constexpr int kMinimumIndex = 0; // 允许使用的最小 C 编号
static constexpr int kMaximumIndex = 4000; // 允许使用的最大 C 编号

explicit CounterAddress(int index = 0); // 使用编号创建 C 地址

int index() const; // 返回地址编号
bool isValid() const; // 判断地址编号是否在有效范围内
std::string toString() const; // 转成 C0 这样的显示文本

bool operator==(const CounterAddress &other) const; // 判断两个 C 地址是否相同
bool operator!=(const CounterAddress &other) const; // 判断两个 C 地址是否不同

private:
int index_ = 0; // 保存 C 地址编号
};

enum class CounterMode
{
Up, // 加计数
Down // 减计数
};

// 读取计数器完成状态的条件触点
struct CounterContactNodeConfig
{
// 要读取完成状态的计数器地址
CounterAddress address;
// 按常开或常闭方式判断完成状态
ContactMode mode = ContactMode::NormallyOpen;
};

// TON 输出的定时器和预置时间,单位为毫秒
struct TonNodeConfig
{
static constexpr int kMinimumPresetMs = 1;
static constexpr int kMaximumPresetMs = 86400000;

TimerAddress address; // 定时器地址
int presetMs = 1000; // 延时时间,单位为毫秒
};

// CTU/CTD 输出配置;当前值、预置值和复位信号都可引用 M/D
struct CounterNodeConfig
{
CounterAddress address; // 计数器地址
CounterMode mode = CounterMode::Up; // 加计数或减计数
RegisterAddress currentValueAddress{RegisterArea::D, 0}; // 当前值保存到的 D 地址
WordOperand preset; // 计数预置值
RegisterAddress resetAddress{RegisterArea::M, 0}; // 复位输入使用的 M 地址
};

// MOVE 输出:把一个字操作数写入目标 D 地址
struct MoveNodeConfig
{
@@ -204,12 +120,8 @@ struct ArithmeticNodeConfig
using LogicNodeConfig = std::variant<
ContactNodeConfig, // 普通常开或常闭触点
EdgeContactNodeConfig, // 上升沿或下降沿触点
TimerContactNodeConfig, // 定时器完成触点
CounterContactNodeConfig, // 计数器完成触点
CoilNodeConfig, // 普通、置位或复位线圈
CompareNodeConfig, // D 寄存器比较节点
TonNodeConfig, // TON 定时器输出
CounterNodeConfig, // CTU 或 CTD 计数器输出
MoveNodeConfig, // MOVE 数据传送输出
ArithmeticNodeConfig>; // ADD 或 SUB 算术输出

@@ -333,16 +245,3 @@ struct ControlLogic
// 校验运行所需资源和网络配置
bool validateForRunning(std::string *error = nullptr) const;
};

// 检查同一 T 地址是否被多个逻辑节点以冲突方式使用
bool validateTimerReferencesForRunning(
const std::vector<ControlLogic> &logics,
std::string *error = nullptr);
// 检查同一 C 地址是否被多个计数器配置重复占用
bool validateCounterReferencesForRunning(
const std::vector<ControlLogic> &logics,
std::string *error = nullptr);
// 一次性执行 T/C 资源引用冲突检查
bool validateLogicResourceReferencesForRunning(
const std::vector<ControlLogic> &logics,
std::string *error = nullptr);

+ 0
- 4
app/src/domain/project_model.cpp Näytä tiedosto

@@ -334,9 +334,5 @@ bool Project::validateForRunning(std::string *error) const
return false;
}
}
if (!validateLogicResourceReferencesForRunning(controlLogics, error))
{
return false;
}
return true;
}

+ 9
- 217
app/src/infrastructure/json_project_storage.cpp Näytä tiedosto

@@ -270,64 +270,6 @@ bool parseAddress(
return true;
}

QJsonObject serializeTimerAddress(const TimerAddress &address)
{
QJsonObject object;
object.insert(QStringLiteral("index"), address.index());
return object;
}

bool parseTimerAddress(
const QJsonObject &object,
const std::string &context,
TimerAddress *address,
ParseState *state)
{
int index = 0;
if (!readInt(
object,
"index",
context,
TimerAddress::kMinimumIndex,
TimerAddress::kMaximumIndex,
&index,
state))
{
return false;
}
*address = TimerAddress{index};
return true;
}

QJsonObject serializeCounterAddress(const CounterAddress &address)
{
QJsonObject object;
object.insert(QStringLiteral("index"), address.index());
return object;
}

bool parseCounterAddress(
const QJsonObject &object,
const std::string &context,
CounterAddress *address,
ParseState *state)
{
int index = 0;
if (!readInt(
object,
"index",
context,
CounterAddress::kMinimumIndex,
CounterAddress::kMaximumIndex,
&index,
state))
{
return false;
}
*address = CounterAddress{index};
return true;
}

QString wordOperandKindName(WordOperandKind kind)
{
return kind == WordOperandKind::Constant
@@ -1113,24 +1055,6 @@ QJsonObject serializeNodeConfig(const EdgeContactNodeConfig &config)
return object;
}

QJsonObject serializeNodeConfig(const TimerContactNodeConfig &config)
{
QJsonObject object;
object.insert(QStringLiteral("type"), QStringLiteral("timerContact"));
object.insert(QStringLiteral("timer"), serializeTimerAddress(config.address));
object.insert(QStringLiteral("mode"), contactModeName(config.mode));
return object;
}

QJsonObject serializeNodeConfig(const CounterContactNodeConfig &config)
{
QJsonObject object;
object.insert(QStringLiteral("type"), QStringLiteral("counterContact"));
object.insert(QStringLiteral("counter"), serializeCounterAddress(config.address));
object.insert(QStringLiteral("mode"), contactModeName(config.mode));
return object;
}

// 将线圈节点配置序列化,并写入用于反序列化分派的类型标记
QJsonObject serializeNodeConfig(const CoilNodeConfig &config)
{
@@ -1152,35 +1076,6 @@ QJsonObject serializeNodeConfig(const CompareNodeConfig &config)
return object;
}

QJsonObject serializeNodeConfig(const TonNodeConfig &config)
{
QJsonObject object;
object.insert(QStringLiteral("type"), QStringLiteral("ton"));
object.insert(QStringLiteral("timer"), serializeTimerAddress(config.address));
object.insert(QStringLiteral("presetMs"), config.presetMs);
return object;
}

QString counterModeName(CounterMode mode)
{
return mode == CounterMode::Up
? QStringLiteral("up") : QStringLiteral("down");
}

QJsonObject serializeNodeConfig(const CounterNodeConfig &config)
{
QJsonObject object;
object.insert(QStringLiteral("type"), QStringLiteral("counter"));
object.insert(QStringLiteral("counter"), serializeCounterAddress(config.address));
object.insert(QStringLiteral("mode"), counterModeName(config.mode));
object.insert(
QStringLiteral("currentValueAddress"),
serializeAddress(config.currentValueAddress));
object.insert(QStringLiteral("preset"), serializeWordOperand(config.preset));
object.insert(QStringLiteral("resetAddress"), serializeAddress(config.resetAddress));
return object;
}

QJsonObject serializeNodeConfig(const MoveNodeConfig &config)
{
QJsonObject object;
@@ -1238,113 +1133,6 @@ bool parseNodeConfig(
return false;
}

if (type == "timerContact" || type == "ton"
|| type == "counterContact" || type == "counter")
{
const char *resource_field =
type == "timerContact" || type == "ton" ? "timer" : "counter";
QJsonObject resource_object;
if (!readObject(object, resource_field, context, &resource_object, state))
{
return false;
}
if (type == "counterContact" || type == "counter")
{
CounterAddress address;
if (!parseCounterAddress(
resource_object, context + ".counter", &address, state))
{
return false;
}
if (type == "counterContact")
{
std::string mode_text;
ContactMode mode = ContactMode::NormallyOpen;
if (!readString(object, "mode", context, &mode_text, state)
|| !parseContactMode(mode_text, &mode, state))
{
return false;
}
*config = CounterContactNodeConfig{address, mode};
return true;
}
std::string mode_text;
CounterMode mode = CounterMode::Up;
if (!readString(object, "mode", context, &mode_text, state))
{
return false;
}
if (mode_text == "up")
{
mode = CounterMode::Up;
}
else if (mode_text == "down")
{
mode = CounterMode::Down;
}
else
{
return state->fail(
ProjectStorageError::InvalidField,
"不支持的计数器方向:" + mode_text);
}
QJsonObject current_object;
QJsonObject preset_object;
QJsonObject reset_object;
RegisterAddress current{RegisterArea::D, 0};
RegisterAddress reset{RegisterArea::M, 0};
WordOperand preset;
if (!readObject(
object, "currentValueAddress", context, &current_object, state)
|| !parseAddress(
current_object,
context + ".currentValueAddress",
&current,
state)
|| !readObject(object, "preset", context, &preset_object, state)
|| !parseWordOperand(preset_object, context + ".preset", &preset, state)
|| !readObject(object, "resetAddress", context, &reset_object, state)
|| !parseAddress(
reset_object, context + ".resetAddress", &reset, state))
{
return false;
}
*config = CounterNodeConfig{address, mode, current, preset, reset};
return true;
}
TimerAddress address;
if (!parseTimerAddress(resource_object, context + ".timer", &address, state))
{
return false;
}
if (type == "timerContact")
{
std::string mode_text;
ContactMode mode = ContactMode::NormallyOpen;
if (!readString(object, "mode", context, &mode_text, state)
|| !parseContactMode(mode_text, &mode, state))
{
return false;
}
*config = TimerContactNodeConfig{address, mode};
return true;
}
int preset_ms = 0;
if (!readInt(
object,
"presetMs",
context,
TonNodeConfig::kMinimumPresetMs,
TonNodeConfig::kMaximumPresetMs,
&preset_ms,
state))
{
return false;
}
*config = TonNodeConfig{address, preset_ms};
return true;
}

if (type == "move")
{
QJsonObject source_object;
@@ -1410,6 +1198,14 @@ bool parseNodeConfig(
return true;
}

if (type != "contact" && type != "edgeContact"
&& type != "coil" && type != "compare")
{
return state->fail(
ProjectStorageError::InvalidField,
"不支持的逻辑节点类型:" + type);
}

QJsonObject address_object;
if (!readObject(object, "address", context, &address_object, state))
{
@@ -1481,11 +1277,7 @@ bool parseNodeConfig(
address, comparison, static_cast<std::int16_t>(value)};
return true;
}


return state->fail(
ProjectStorageError::InvalidField,
"不支持的逻辑节点类型:" + type);
return state->fail(ProjectStorageError::InvalidField, "逻辑节点类型解析失败");
}

// 解析逻辑节点标识及其多态配置


+ 0
- 45
app/src/services/logic_editor_service.cpp Näytä tiedosto

@@ -699,16 +699,6 @@ bool LogicEditorService::configsEqual(
return left_config.address == right_config.address
&& left_config.mode == right_config.mode;
}
else if constexpr (std::is_same_v<Left, TimerContactNodeConfig>)
{
return left_config.address == right_config.address
&& left_config.mode == right_config.mode;
}
else if constexpr (std::is_same_v<Left, CounterContactNodeConfig>)
{
return left_config.address == right_config.address
&& left_config.mode == right_config.mode;
}
else if constexpr (std::is_same_v<Left, CoilNodeConfig>)
{
return left_config.address == right_config.address
@@ -720,21 +710,6 @@ bool LogicEditorService::configsEqual(
&& left_config.comparison == right_config.comparison
&& left_config.value == right_config.value;
}
else if constexpr (std::is_same_v<Left, TonNodeConfig>)
{
return left_config.address == right_config.address
&& left_config.presetMs == right_config.presetMs;
}
else if constexpr (std::is_same_v<Left, CounterNodeConfig>)
{
return left_config.address == right_config.address
&& left_config.mode == right_config.mode
&& left_config.currentValueAddress == right_config.currentValueAddress
&& left_config.preset.kind == right_config.preset.kind
&& left_config.preset.address == right_config.preset.address
&& left_config.preset.constant == right_config.preset.constant
&& left_config.resetAddress == right_config.resetAddress;
}
else if constexpr (std::is_same_v<Left, MoveNodeConfig>)
{
return left_config.source.kind == right_config.source.kind
@@ -2434,16 +2409,12 @@ bool LogicEditorService::isConditionConfig(const LogicNodeConfig &config)
{
return std::holds_alternative<ContactNodeConfig>(config)
|| std::holds_alternative<EdgeContactNodeConfig>(config)
|| std::holds_alternative<TimerContactNodeConfig>(config)
|| std::holds_alternative<CounterContactNodeConfig>(config)
|| std::holds_alternative<CompareNodeConfig>(config);
}

bool LogicEditorService::isOutputConfig(const LogicNodeConfig &config)
{
return std::holds_alternative<CoilNodeConfig>(config)
|| std::holds_alternative<TonNodeConfig>(config)
|| std::holds_alternative<CounterNodeConfig>(config)
|| std::holds_alternative<MoveNodeConfig>(config)
|| std::holds_alternative<ArithmeticNodeConfig>(config);
}
@@ -2462,26 +2433,10 @@ std::string LogicEditorService::nodePrefix(const LogicNodeConfig &config)
{
return "edge";
}
else if constexpr (std::is_same_v<Config, TimerContactNodeConfig>)
{
return "timer-contact";
}
else if constexpr (std::is_same_v<Config, CounterContactNodeConfig>)
{
return "counter-contact";
}
else if constexpr (std::is_same_v<Config, CoilNodeConfig>)
{
return "coil";
}
else if constexpr (std::is_same_v<Config, TonNodeConfig>)
{
return "ton";
}
else if constexpr (std::is_same_v<Config, CounterNodeConfig>)
{
return "counter";
}
else if constexpr (std::is_same_v<Config, MoveNodeConfig>)
{
return "move";


+ 1
- 1
app/src/services/offline_simulation_service.cpp Näytä tiedosto

@@ -31,7 +31,7 @@ SimulationStartResult OfflineSimulationService::start(
return {false, SimulationStartError::InvalidLogic, validation};
}

// 重启会话时清空 T/C/沿触发状态和虚拟 M/D,避免继承上次运行现场
// 重启会话时清空沿触发状态和虚拟 M/D,避免继承上次运行现场
timer_.stop();
executor_.resetRuntime();
repository_.clear();


+ 3
- 176
app/src/services/software_logic_executor.cpp Näytä tiedosto

@@ -79,8 +79,6 @@ void LogicTraceValues::clear()
expressionInputValues.clear();
expressionPowerValues.clear();
rungValues.clear();
tonValues.clear();
counterValues.clear();
wordValues.clear();
}

@@ -103,8 +101,6 @@ LogicTraceSnapshot LogicTraceSnapshot::forLogic(
projection.expressionInputValues = values->second.expressionInputValues;
projection.expressionPowerValues = values->second.expressionPowerValues;
projection.rungValues = values->second.rungValues;
projection.tonValues = values->second.tonValues;
projection.counterValues = values->second.counterValues;
projection.wordValues = values->second.wordValues;
}
return projection;
@@ -113,7 +109,7 @@ LogicTraceSnapshot LogicTraceSnapshot::forLogic(
LogicScanResult SoftwareLogicExecutor::validate(
const std::vector<ControlLogic> &logics) const
{
// 先检查每个逻辑自身,再检查跨网络共享线圈和 T/C 资源的冲突
// 先检查每个逻辑自身,再检查跨网络共享线圈的冲突
std::map<int, CoilMode> output_modes;
for (const ControlLogic &logic : logics)
{
@@ -145,9 +141,7 @@ LogicScanResult SoftwareLogicExecutor::validate(
const auto *output = std::get_if<CoilNodeConfig>(&rung.output->config);
if (output == nullptr)
{
if (std::holds_alternative<TonNodeConfig>(rung.output->config)
|| std::holds_alternative<CounterNodeConfig>(rung.output->config)
|| std::holds_alternative<MoveNodeConfig>(rung.output->config)
if (std::holds_alternative<MoveNodeConfig>(rung.output->config)
|| std::holds_alternative<ArithmeticNodeConfig>(
rung.output->config))
{
@@ -174,35 +168,18 @@ LogicScanResult SoftwareLogicExecutor::validate(
output_modes[output->address.index()] = output->mode;
}
}
std::string resource_error;
if (!validateLogicResourceReferencesForRunning(logics, &resource_error))
{
return failure(LogicScanError::InvalidLogic, resource_error);
}
return success();
}

void SoftwareLogicExecutor::resetRuntime()
{
previous_edge_inputs_.clear();
previous_counter_inputs_.clear();
ton_states_.clear();
counter_states_.clear();
}

LogicScanResult SoftwareLogicExecutor::executeScan(
const std::vector<ControlLogic> &logics,
RegisterRepository &repository,
LogicTraceSnapshot *trace)
{
return executeScanAt(logics, repository, Clock::now(), trace);
}

LogicScanResult SoftwareLogicExecutor::executeScanAt(
const std::vector<ControlLogic> &logics,
RegisterRepository &repository,
TimePoint now,
LogicTraceSnapshot *trace)
{
const LogicScanResult validation = validate(logics);
if (!validation.succeeded)
@@ -258,11 +235,9 @@ LogicScanResult SoftwareLogicExecutor::executeScanAt(
}
bool output_value = false;
result = executeOutput(
logic.id,
*rung.output,
rung_value,
repository,
now,
logic_trace,
&output_value);
if (!result.succeeded)
@@ -294,8 +269,6 @@ LogicScanResult SoftwareLogicExecutor::executeScanAt(
trace->expressionInputValues = values->second.expressionInputValues;
trace->expressionPowerValues = values->second.expressionPowerValues;
trace->rungValues = values->second.rungValues;
trace->tonValues = values->second.tonValues;
trace->counterValues = values->second.counterValues;
trace->wordValues = values->second.wordValues;
}
}
@@ -383,7 +356,7 @@ LogicScanResult SoftwareLogicExecutor::evaluateCondition(
RegisterRepository &repository,
bool *value)
{
// 根据条件节点的具体配置读取 M/D 或内部 T/C 状态,并计算触点结果
// 根据条件节点的具体配置读取 M/D 状态,并计算触点结果
// 边沿触点使用 logic_id 和 node.id 组成运行时状态键,避免同名节点互相影响
if (value == nullptr)
{
@@ -437,20 +410,6 @@ LogicScanResult SoftwareLogicExecutor::evaluateCondition(
previous_edge_inputs_[runtime_key] = read.value;
return success();
}
else if constexpr (std::is_same_v<Config, TimerContactNodeConfig>)
{
// 定时器触点不读 PLC 地址,而是读取本地 TON 的完成状态
const bool done = ton_states_[config.address.index()].done;
*value = config.mode == ContactMode::NormallyOpen ? done : !done;
return success();
}
else if constexpr (std::is_same_v<Config, CounterContactNodeConfig>)
{
// 计数器触点读取本地 CTU/CTD 的完成状态
const bool done = counter_states_[config.address.index()].done;
*value = config.mode == ContactMode::NormallyOpen ? done : !done;
return success();
}
else if constexpr (std::is_same_v<Config, CompareNodeConfig>)
{
// 字比较触点读取 D 字,再按配置的比较运算符与目标值比较
@@ -525,11 +484,9 @@ LogicScanResult SoftwareLogicExecutor::readWordOperand(
}

LogicScanResult SoftwareLogicExecutor::executeOutput(
const std::string &logic_id,
const LogicNode &node,
bool rung_value,
RegisterRepository &repository,
TimePoint now,
LogicTraceValues *trace,
bool *output_value)
{
@@ -542,136 +499,6 @@ LogicScanResult SoftwareLogicExecutor::executeOutput(
{},
node.id);
}
if (const auto *ton = std::get_if<TonNodeConfig>(&node.config))
{
TonRuntimeState &state = ton_states_[ton->address.index()];
if (!rung_value)
{
state = TonRuntimeState{};
}
else if (!state.timing)
{
state.timing = true;
state.startedAt = now;
state.elapsed = std::chrono::milliseconds{0};
state.done = false;
}
else
{
state.elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
now >= state.startedAt ? now - state.startedAt : TimePoint::duration::zero());
state.done = state.elapsed.count() >= ton->presetMs;
}
*output_value = state.done;
if (trace != nullptr)
{
trace->tonValues[node.id] = {
rung_value,
state.done,
state.elapsed.count(),
ton->presetMs};
}
return success();
}

if (const auto *counter = std::get_if<CounterNodeConfig>(&node.config))
{
const BitReadResult reset_read = repository.readBit(counter->resetAddress);
if (!reset_read.succeeded)
{
return failure(
LogicScanError::RegisterReadFailed,
"读取计数器复位输入失败:" + counter->resetAddress.toString(),
{},
{},
node.id);
}
std::int16_t preset = 0;
LogicScanResult result = readWordOperand(
counter->preset, repository, &preset, node.id);
if (!result.succeeded)
{
return result;
}
const std::int32_t bounded_preset = std::max<std::int32_t>(0, preset);
preset = static_cast<std::int16_t>(
std::min<std::int32_t>(bounded_preset, std::numeric_limits<std::int16_t>::max()));
const WordReadResult current_read = repository.readWord(
counter->currentValueAddress);
if (!current_read.succeeded)
{
return failure(
LogicScanError::RegisterReadFailed,
"读取计数当前值失败:" + counter->currentValueAddress.toString(),
{},
{},
node.id);
}
std::int32_t current = std::max<std::int32_t>(0, current_read.value);
current = std::min<std::int32_t>(current, std::numeric_limits<std::int16_t>::max());
const auto runtime_key = std::make_pair(logic_id, node.id);
const bool previous = previous_counter_inputs_[runtime_key];
const bool rising = rung_value && !previous;
previous_counter_inputs_[runtime_key] = rung_value;
bool done = counter_states_[counter->address.index()].done;
if (reset_read.value)
{
current = counter->mode == CounterMode::Up ? 0 : preset;
done = counter->mode == CounterMode::Down && current == 0;
}
else if (rising)
{
if (counter->mode == CounterMode::Up)
{
if (current < preset)
{
++current;
}
done = current >= preset;
}
else
{
if (current > 0)
{
--current;
}
done = current <= 0;
}
}
if (counter->mode == CounterMode::Up && current >= preset)
{
done = true;
}
if (counter->mode == CounterMode::Down && current <= 0)
{
done = true;
}
const std::int16_t current_value = static_cast<std::int16_t>(current);
const RegisterWriteResult write = repository.writeWord(
counter->currentValueAddress, current_value);
if (!write.succeeded)
{
return failure(
LogicScanError::RegisterWriteFailed,
"写入计数当前值失败:" + counter->currentValueAddress.toString(),
{},
{},
node.id);
}
counter_states_[counter->address.index()].done = done;
*output_value = done;
if (trace != nullptr)
{
trace->counterValues[node.id] = {
rung_value,
reset_read.value,
done,
current_value,
preset};
}
return success();
}

if (const auto *move = std::get_if<MoveNodeConfig>(&node.config))
{
if (!rung_value)


+ 4
- 66
app/src/services/software_logic_executor.h Näytä tiedosto

@@ -3,7 +3,6 @@
#include "domain/control_logic_model.h"
#include "domain/register_repository.h"

#include <chrono>
#include <cstdint>
#include <map>
#include <string>
@@ -32,25 +31,6 @@ struct LogicScanResult
std::string nodeId; // 失败关联的节点 ID
};

// TON 节点在最近一轮扫描中的可视化状态
struct TonTraceValue
{
bool input = false; // 本轮 TON 输入状态
bool done = false; // 当前是否已经到达预置时间
std::int64_t elapsedMs = 0; // 已累计时间,单位为毫秒
int presetMs = 0; // 预置时间,单位为毫秒
};

// CTU/CTD 节点在最近一轮扫描中的可视化状态
struct CounterTraceValue
{
bool input = false; // 本轮计数输入状态
bool reset = false; // 本轮复位输入状态
bool done = false; // 当前计数值是否达到预置值
std::int16_t value = 0; // 当前计数值
std::int16_t preset = 0; // 计数预置值
};

// MOVE/ADD/SUB 节点的结果值和溢出标记
struct WordTraceValue
{
@@ -67,8 +47,6 @@ struct LogicTraceValues
std::unordered_map<std::string, bool> expressionInputValues; // 表达式输入状态
std::unordered_map<std::string, bool> expressionPowerValues; // 表达式输出电源状态
std::unordered_map<std::string, bool> rungValues; // 网络最终逻辑值
std::unordered_map<std::string, TonTraceValue> tonValues; // TON 节点状态
std::unordered_map<std::string, CounterTraceValue> counterValues; // CTU/CTD 节点状态
std::unordered_map<std::string, WordTraceValue> wordValues; // MOVE/ADD/SUB 节点状态

// 清除当前逻辑或网络的全部轨迹
@@ -90,22 +68,19 @@ struct LogicTraceSnapshot : LogicTraceValues
class SoftwareLogicExecutor
{
public:
using Clock = std::chrono::steady_clock;
using TimePoint = Clock::time_point;

/**
* @brief 检查逻辑是否满足软件扫描的运行要求
* @param logics 待检查的控制逻辑集合
* @return 成功或包含逻辑、网络、节点定位信息的失败结果
*
* 检查逻辑结构、启用逻辑的运行配置、共享输出冲突以及 T/C 资源引用
* 检查逻辑结构、启用逻辑的运行配置和共享输出冲突
*/
LogicScanResult validate(const std::vector<ControlLogic> &logics) const;

/**
* @brief 清除所有跨扫描运行状态
*
* 重置边沿触发、TON 定时器和 CTU/CTD 计数器;不修改寄存器仓库和轨迹对象
* 重置边沿触发状态;不修改寄存器仓库和轨迹对象
*/
void resetRuntime();

@@ -120,35 +95,12 @@ public:
const std::vector<ControlLogic> &logics,
RegisterRepository &repository,
LogicTraceSnapshot *trace = nullptr);
/**
* @brief 使用指定时间执行一轮扫描
* @param logics 按工程顺序扫描的控制逻辑集合
* @param repository 扫描读取和写入的寄存器仓库
* @param now 本轮扫描使用的单调时钟时间点
* @param trace 可选的轨迹输出;适合测试 TON 等时间相关逻辑
* @return 扫描结果
*/
LogicScanResult executeScanAt(
const std::vector<ControlLogic> &logics,
RegisterRepository &repository,
TimePoint now,
LogicTraceSnapshot *trace = nullptr);

private:
// 单个 TON 定时器跨扫描保存的计时状态
struct TonRuntimeState
{
bool timing = false; // 是否正在累计输入有效时间
bool done = false; // 是否已经完成预置时间
TimePoint startedAt{}; // 本次计时开始时间
std::chrono::milliseconds elapsed{0}; // 最近一次计算的累计时间
};

/**
* @brief 计算单个条件节点的导通结果
* @param logic_id 所属控制逻辑 ID,用于隔离边沿触点的跨扫描状态
* @param node 待读取和计算的条件节点
* @param repository 提供 M/D 值的寄存器仓库;T/C 条件读取执行器内部状态
* @param repository 提供 M/D 值的寄存器仓库
* @param value 输出节点导通结果,不能为空
* @return 成功结果,或包含节点 ID 的逻辑/寄存器读取失败结果
*/
@@ -176,21 +128,17 @@ private:
bool *value);
/**
* @brief 在网络结果驱动下执行单个输出节点
* @param logic_id 所属控制逻辑 ID,用于隔离计数器等跨扫描状态
* @param node 待读取配置并执行的输出节点
* @param rung_value 当前网络的逻辑结果,决定输出是否动作
* @param repository 提供输出读写所需的 M/D 寄存器仓库
* @param now 本轮扫描的单调时钟时间点,TON 使用它计算累计时间
* @param trace 可选的轨迹输出,用于记录 TON、计数器和字操作结果
* @param trace 可选的轨迹输出,用于记录字操作结果
* @param output_value 输出节点最终逻辑值,不能为空
* @return 成功结果,或包含节点 ID 的逻辑/寄存器读写失败结果
*/
LogicScanResult executeOutput(
const std::string &logic_id,
const LogicNode &node,
bool rung_value,
RegisterRepository &repository,
TimePoint now,
LogicTraceValues *trace,
bool *output_value);
/**
@@ -209,14 +157,4 @@ private:

// 按逻辑 ID 和节点 ID 保存上一轮边沿输入
std::map<std::pair<std::string, std::string>, bool> previous_edge_inputs_;
// 按逻辑 ID 和节点 ID 保存上一轮计数输入
std::map<std::pair<std::string, std::string>, bool> previous_counter_inputs_;
// 按定时器编号保存跨扫描状态
std::unordered_map<int, TonRuntimeState> ton_states_;
// 单个计数器跨扫描保存的完成状态
struct CounterRuntimeState
{
bool done = false; // 是否已经达到预置值
};
std::unordered_map<int, CounterRuntimeState> counter_states_; // 按计数器编号保存状态
};

+ 1
- 130
app/src/ui/logic_editor_widget.cpp Näytä tiedosto

@@ -48,16 +48,6 @@ QString registerAddressText(const RegisterAddress &address)
return QString::fromStdString(address.toString());
}

QString timerAddressText(const TimerAddress &address)
{
return QString::fromStdString(address.toString());
}

QString counterAddressText(const CounterAddress &address)
{
return QString::fromStdString(address.toString());
}

QString wordOperandText(const WordOperand &operand)
{
return operand.kind == WordOperandKind::Register
@@ -119,16 +109,6 @@ QString nodeToolTip(const LogicNodeConfig &config)
: LogicEditorWidget::tr("下降"))
.arg(registerAddressText(value.address));
}
else if constexpr (std::is_same_v<Config, TimerContactNodeConfig>)
{
return LogicEditorWidget::tr("T 触点:%1")
.arg(timerAddressText(value.address));
}
else if constexpr (std::is_same_v<Config, CounterContactNodeConfig>)
{
return LogicEditorWidget::tr("C 触点:%1")
.arg(counterAddressText(value.address));
}
else if constexpr (std::is_same_v<Config, CoilNodeConfig>)
{
return LogicEditorWidget::tr("输出线圈:%1")
@@ -141,22 +121,6 @@ QString nodeToolTip(const LogicNodeConfig &config)
.arg(comparisonText(value.comparison))
.arg(value.value);
}
else if constexpr (std::is_same_v<Config, TonNodeConfig>)
{
return LogicEditorWidget::tr("TON:%1,预设 %2 ms")
.arg(timerAddressText(value.address))
.arg(value.presetMs);
}
else if constexpr (std::is_same_v<Config, CounterNodeConfig>)
{
return LogicEditorWidget::tr("%1:%2,CV %3,PV %4,复位 %5")
.arg(value.mode == CounterMode::Up
? QStringLiteral("CTU") : QStringLiteral("CTD"))
.arg(counterAddressText(value.address))
.arg(registerAddressText(value.currentValueAddress))
.arg(wordOperandText(value.preset))
.arg(registerAddressText(value.resetAddress));
}
else if constexpr (std::is_same_v<Config, MoveNodeConfig>)
{
return LogicEditorWidget::tr("MOVE:%1 -> %2")
@@ -358,41 +322,6 @@ public:
configured_ ? registerAddressText(edge->address) : tr("< M 地址 >"));
drawRegisterComment(painter, register_comment_);
}
else if (const auto *timer_contact =
std::get_if<TimerContactNodeConfig>(&config_))
{
painter->fillRect(QRectF(-25, -22, 50, 44), Qt::white);
painter->drawLine(QPointF(-kNodeTerminalX, 0), QPointF(-18, 0));
painter->drawLine(QPointF(18, 0), QPointF(kNodeTerminalX, 0));
painter->drawLine(QPointF(-18, -15), QPointF(-18, 15));
painter->drawLine(QPointF(18, -15), QPointF(18, 15));
if (timer_contact->mode == ContactMode::NormallyClosed)
{
painter->drawLine(QPointF(-23, 18), QPointF(23, -18));
}
painter->drawText(
QRectF(-kCellWidth / 2.0, -40, kCellWidth, 18),
Qt::AlignCenter,
configured_ ? timerAddressText(timer_contact->address) : tr("< T 地址 >"));
}
else if (const auto *counter_contact =
std::get_if<CounterContactNodeConfig>(&config_))
{
painter->fillRect(QRectF(-25, -22, 50, 44), Qt::white);
painter->drawLine(QPointF(-kNodeTerminalX, 0), QPointF(-18, 0));
painter->drawLine(QPointF(18, 0), QPointF(kNodeTerminalX, 0));
painter->drawLine(QPointF(-18, -15), QPointF(-18, 15));
painter->drawLine(QPointF(18, -15), QPointF(18, 15));
if (counter_contact->mode == ContactMode::NormallyClosed)
{
painter->drawLine(QPointF(-23, 18), QPointF(23, -18));
}
painter->drawText(
QRectF(-kCellWidth / 2.0, -40, kCellWidth, 18),
Qt::AlignCenter,
configured_ ? counterAddressText(counter_contact->address)
: tr("< C 地址 >"));
}
else if (const auto *coil = std::get_if<CoilNodeConfig>(&config_))
{
painter->fillRect(QRectF(-35, -23, 70, 46), Qt::white);
@@ -439,54 +368,6 @@ public:
configured_ ? QString::number(comparison->value) : tr("< 常量 >"));
drawRegisterComment(painter, register_comment_, 40.0);
}
else if (const auto *ton = std::get_if<TonNodeConfig>(&config_))
{
const QRectF box(-47, -18, 94, 36);
painter->fillRect(box.adjusted(-1, -1, 1, 1), Qt::white);
painter->drawLine(QPointF(-kNodeTerminalX, 0), QPointF(-47, 0));
painter->drawLine(QPointF(47, 0), QPointF(kNodeTerminalX, 0));
painter->drawRect(box);
painter->drawText(box, Qt::AlignCenter, QStringLiteral("TON"));
painter->drawText(
QRectF(-kCellWidth / 2.0, -40, kCellWidth, 18),
Qt::AlignCenter,
configured_ ? timerAddressText(ton->address) : tr("< T 地址 >"));
painter->drawText(
QRectF(-kCellWidth / 2.0, 22, kCellWidth, 18),
Qt::AlignCenter,
configured_ ? QStringLiteral("PT %1 ms").arg(ton->presetMs)
: tr("< 预设时间 >"));
}
else if (const auto *counter = std::get_if<CounterNodeConfig>(&config_))
{
const QRectF box(-47, -18, 94, 36);
painter->fillRect(box.adjusted(-1, -1, 1, 1), Qt::white);
painter->drawLine(QPointF(-kNodeTerminalX, 0), QPointF(-47, 0));
painter->drawLine(QPointF(47, 0), QPointF(kNodeTerminalX, 0));
painter->drawRect(box);
painter->drawText(
box,
Qt::AlignCenter,
counter->mode == CounterMode::Up
? QStringLiteral("CTU") : QStringLiteral("CTD"));
painter->drawText(
QRectF(-kCellWidth / 2.0, -40, kCellWidth, 18),
Qt::AlignCenter,
configured_ ? counterAddressText(counter->address)
: tr("< C 地址 >"));
painter->drawText(
QRectF(-kCellWidth / 2.0 + 4.0, 20, kCellWidth - 8.0, 18),
Qt::AlignCenter | Qt::TextSingleLine,
configured_ ? QStringLiteral("CV %1")
.arg(registerAddressText(counter->currentValueAddress))
: tr("< CV >"));
painter->drawText(
QRectF(-kCellWidth / 2.0 + 4.0, 38, kCellWidth - 8.0, 18),
Qt::AlignCenter | Qt::TextSingleLine,
configured_ ? QStringLiteral("PV %1")
.arg(wordOperandText(counter->preset))
: tr("< PV >"));
}
else if (const auto *move = std::get_if<MoveNodeConfig>(&config_))
{
const QRectF box(-47, -18, 94, 36);
@@ -1228,17 +1109,7 @@ void LogicEditorWidget::reloadLogic()
editor_service_.registerCommentFor(*address))
: QString{};
}(),
[&rung, this, rung_active]
{
return (std::holds_alternative<TonNodeConfig>(rung.output->config)
|| std::holds_alternative<CounterNodeConfig>(
rung.output->config))
? traceValue(
trace_,
&LogicTraceSnapshot::nodeValues,
rung.output->id)
: rung_active;
}(),
rung_active,
rung.output->id == fault_node_id_,
false));
}


+ 2
- 44
app/src/ui/logic_instruction_dialog.cpp Näytä tiedosto

@@ -31,14 +31,12 @@ LogicInstructionDialog::LogicInstructionDialog(
original_config_(config)
{
ui_->setupUi(this);
configureOperandCombo(ui_->counterPresetKindComboBox);
configureOperandCombo(ui_->leftOperandKindComboBox);
configureOperandCombo(ui_->rightOperandKindComboBox);

connect(ui_->buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept);
connect(ui_->buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject);
for (QComboBox *combo : {
ui_->counterPresetKindComboBox,
ui_->leftOperandKindComboBox,
ui_->rightOperandKindComboBox})
{
@@ -49,11 +47,7 @@ LogicInstructionDialog::LogicInstructionDialog(
&LogicInstructionDialog::updateOperandRanges);
}

if (const auto *counter = std::get_if<CounterNodeConfig>(&config))
{
configureCounter(*counter);
}
else if (const auto *move = std::get_if<MoveNodeConfig>(&config))
if (const auto *move = std::get_if<MoveNodeConfig>(&config))
{
configureMove(*move);
}
@@ -68,21 +62,6 @@ LogicInstructionDialog::~LogicInstructionDialog() = default;

LogicNodeConfig LogicInstructionDialog::config() const
{
if (std::holds_alternative<CounterNodeConfig>(original_config_))
{
return CounterNodeConfig{
CounterAddress{ui_->counterAddressSpinBox->value()},
static_cast<CounterMode>(ui_->counterModeComboBox->currentData().toInt()),
RegisterAddress{
RegisterArea::D,
ui_->counterCurrentAddressSpinBox->value()},
operandFrom(
ui_->counterPresetKindComboBox,
ui_->counterPresetValueSpinBox),
RegisterAddress{
RegisterArea::M,
ui_->counterResetAddressSpinBox->value()}};
}
if (std::holds_alternative<MoveNodeConfig>(original_config_))
{
return MoveNodeConfig{
@@ -101,25 +80,6 @@ LogicNodeConfig LogicInstructionDialog::config() const
ui_->destinationAddressSpinBox->value()}};
}

void LogicInstructionDialog::configureCounter(const CounterNodeConfig &config)
{
setWindowTitle(tr("配置计数器"));
ui_->instructionStackedWidget->setCurrentWidget(ui_->counterPage);
ui_->counterModeComboBox->clear();
ui_->counterModeComboBox->addItem(tr("CTU 加计数"), static_cast<int>(CounterMode::Up));
ui_->counterModeComboBox->addItem(
tr("CTD 减计数"), static_cast<int>(CounterMode::Down));
ui_->counterModeComboBox->setCurrentIndex(
ui_->counterModeComboBox->findData(static_cast<int>(config.mode)));
ui_->counterAddressSpinBox->setValue(config.address.index());
ui_->counterCurrentAddressSpinBox->setValue(config.currentValueAddress.index());
ui_->counterResetAddressSpinBox->setValue(config.resetAddress.index());
setOperand(
ui_->counterPresetKindComboBox,
ui_->counterPresetValueSpinBox,
config.preset);
}

void LogicInstructionDialog::configureMove(const MoveNodeConfig &config)
{
setWindowTitle(tr("配置 MOVE"));
@@ -165,13 +125,11 @@ void LogicInstructionDialog::updateOperandRanges()
spin->setPrefix(register_operand ? QStringLiteral("D") : QString{});
spin->setMinimum(register_operand
? RegisterAddress::kMinimumIndex
: (combo->objectName() == QStringLiteral("counterPresetKindComboBox")
? 0 : std::numeric_limits<std::int16_t>::min()));
: std::numeric_limits<std::int16_t>::min());
spin->setMaximum(register_operand
? RegisterAddress::kMaximumIndex
: std::numeric_limits<std::int16_t>::max());
};
update(ui_->counterPresetKindComboBox, ui_->counterPresetValueSpinBox);
update(ui_->leftOperandKindComboBox, ui_->leftOperandValueSpinBox);
update(ui_->rightOperandKindComboBox, ui_->rightOperandValueSpinBox);
}


+ 0
- 2
app/src/ui/logic_instruction_dialog.h Näytä tiedosto

@@ -38,8 +38,6 @@ public:
LogicNodeConfig config() const;

private:
/** 根据计数器配置初始化计数器输入项 */
void configureCounter(const CounterNodeConfig &config);
/** 根据传送配置初始化传送输入项 */
void configureMove(const MoveNodeConfig &config);
/** 根据算术配置初始化运算输入项 */


+ 0
- 21
app/src/ui/logic_instruction_dialog.ui Näytä tiedosto

@@ -9,27 +9,6 @@
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QStackedWidget" name="instructionStackedWidget">
<widget class="QWidget" name="counterPage">
<layout class="QFormLayout" name="counterFormLayout">
<item row="0" column="0"><widget class="QLabel" name="counterModeLabel"><property name="text"><string>计数方式</string></property></widget></item>
<item row="0" column="1"><widget class="QComboBox" name="counterModeComboBox"/></item>
<item row="1" column="0"><widget class="QLabel" name="counterAddressLabel"><property name="text"><string>计数器资源</string></property></widget></item>
<item row="1" column="1"><widget class="QSpinBox" name="counterAddressSpinBox"><property name="prefix"><string>C</string></property><property name="maximum"><number>4000</number></property></widget></item>
<item row="2" column="0"><widget class="QLabel" name="counterCurrentAddressLabel"><property name="text"><string>当前值 CV</string></property></widget></item>
<item row="2" column="1"><widget class="QSpinBox" name="counterCurrentAddressSpinBox"><property name="prefix"><string>D</string></property><property name="maximum"><number>4000</number></property></widget></item>
<item row="3" column="0"><widget class="QLabel" name="counterPresetLabel"><property name="text"><string>预设值 PV</string></property></widget></item>
<item row="3" column="1">
<widget class="QWidget" name="counterPresetEditor" native="true">
<layout class="QHBoxLayout" name="counterPresetLayout"><property name="leftMargin"><number>0</number></property><property name="topMargin"><number>0</number></property><property name="rightMargin"><number>0</number></property><property name="bottomMargin"><number>0</number></property>
<item><widget class="QComboBox" name="counterPresetKindComboBox"/></item>
<item><widget class="QSpinBox" name="counterPresetValueSpinBox"><property name="maximum"><number>32767</number></property><property name="value"><number>10</number></property></widget></item>
</layout>
</widget>
</item>
<item row="4" column="0"><widget class="QLabel" name="counterResetAddressLabel"><property name="text"><string>复位输入</string></property></widget></item>
<item row="4" column="1"><widget class="QSpinBox" name="counterResetAddressSpinBox"><property name="prefix"><string>M</string></property><property name="maximum"><number>4000</number></property></widget></item>
</layout>
</widget>
<widget class="QWidget" name="dataInstructionPage">
<layout class="QFormLayout" name="dataInstructionFormLayout">
<item row="0" column="0"><widget class="QLabel" name="instructionCaptionLabel"><property name="text"><string>指令</string></property></widget></item>


+ 1
- 83
app/src/ui/main_window.cpp Näytä tiedosto

@@ -423,8 +423,6 @@ void MainWindow::configureActions()
QAction *parallel_closed = parallel_menu->addAction(tr("并联常闭触点"));
QAction *parallel_rising = parallel_menu->addAction(tr("并联上升沿触点"));
QAction *parallel_falling = parallel_menu->addAction(tr("并联下降沿触点"));
QAction *parallel_timer = parallel_menu->addAction(tr("并联 T 触点"));
QAction *parallel_counter = parallel_menu->addAction(tr("并联 C 触点"));
QAction *parallel_compare = parallel_menu->addAction(tr("并联比较条件"));
connect(parallel_open, &QAction::triggered,
this,
@@ -456,20 +454,6 @@ void MainWindow::configureActions()
addLogicParallelBranch(EdgeContactNodeConfig{
RegisterAddress{RegisterArea::M, 0}, EdgeMode::Falling});
});
connect(parallel_timer, &QAction::triggered,
this,
[this]
{
addLogicParallelBranch(TimerContactNodeConfig{
TimerAddress{0}, ContactMode::NormallyOpen});
});
connect(parallel_counter, &QAction::triggered,
this,
[this]
{
addLogicParallelBranch(CounterContactNodeConfig{
CounterAddress{0}, ContactMode::NormallyOpen});
});
connect(parallel_compare, &QAction::triggered,
this,
[this]
@@ -515,20 +499,6 @@ void MainWindow::configureActions()
addLogicCondition(EdgeContactNodeConfig{
RegisterAddress{RegisterArea::M, 0}, EdgeMode::Falling});
});
connect(ui_->addTimerContactAction, &QAction::triggered,
this,
[this]
{
addLogicCondition(TimerContactNodeConfig{
TimerAddress{0}, ContactMode::NormallyOpen});
});
connect(ui_->addCounterContactAction, &QAction::triggered,
this,
[this]
{
addLogicCondition(CounterContactNodeConfig{
CounterAddress{0}, ContactMode::NormallyOpen});
});
connect(ui_->addNormalCoilAction, &QAction::triggered,
this,
[this]
@@ -553,40 +523,6 @@ void MainWindow::configureActions()
RegisterAddress{RegisterArea::M, 0},
CoilMode::Reset});
});
connect(ui_->addTonAction, &QAction::triggered,
this,
[this]
{
setLogicOutput(TonNodeConfig{TimerAddress{0}, 1000});
});
connect(ui_->addCtuAction, &QAction::triggered,
this,
[this]
{
configureAndSetLogicOutput(CounterNodeConfig{
CounterAddress{0},
CounterMode::Up,
RegisterAddress{RegisterArea::D, 0},
WordOperand{
WordOperandKind::Constant,
RegisterAddress{RegisterArea::D, 0},
10},
RegisterAddress{RegisterArea::M, 0}});
});
connect(ui_->addCtdAction, &QAction::triggered,
this,
[this]
{
configureAndSetLogicOutput(CounterNodeConfig{
CounterAddress{0},
CounterMode::Down,
RegisterAddress{RegisterArea::D, 0},
WordOperand{
WordOperandKind::Constant,
RegisterAddress{RegisterArea::D, 0},
10},
RegisterAddress{RegisterArea::M, 0}});
});
connect(ui_->addMoveAction, &QAction::triggered,
this,
[this]
@@ -650,21 +586,13 @@ void MainWindow::configureActions()
makeUiIcon(UiIcon::NormallyOpenContact),
QList<QAction *>{
ui_->addRisingEdgeAction,
ui_->addFallingEdgeAction,
ui_->addTimerContactAction,
ui_->addCounterContactAction});
ui_->addFallingEdgeAction});
addToolbarMenu(
ui_->logicToolBar,
tr("更多输出"),
QStringLiteral("logicOutputMenuButton"),
makeUiIcon(UiIcon::Coil),
QList<QAction *>{ui_->addSetCoilAction, ui_->addResetCoilAction});
addToolbarMenu(
ui_->logicToolBar,
tr("定时/计数"),
QStringLiteral("logicTimerCounterMenuButton"),
makeUiIcon(UiIcon::Ton),
QList<QAction *>{ui_->addTonAction, ui_->addCtuAction, ui_->addCtdAction});
addToolbarMenu(
ui_->logicToolBar,
tr("数据运算"),
@@ -749,14 +677,9 @@ void MainWindow::configureAppearance()
ui_->addNormallyClosedAction->setIcon(makeUiIcon(UiIcon::NormallyClosedContact));
ui_->addRisingEdgeAction->setIcon(makeUiIcon(UiIcon::RisingEdgeContact));
ui_->addFallingEdgeAction->setIcon(makeUiIcon(UiIcon::FallingEdgeContact));
ui_->addTimerContactAction->setIcon(makeUiIcon(UiIcon::TimerContact));
ui_->addCounterContactAction->setIcon(makeUiIcon(UiIcon::CounterContact));
ui_->addNormalCoilAction->setIcon(makeUiIcon(UiIcon::Coil));
ui_->addSetCoilAction->setIcon(makeUiIcon(UiIcon::SetCoil));
ui_->addResetCoilAction->setIcon(makeUiIcon(UiIcon::ResetCoil));
ui_->addTonAction->setIcon(makeUiIcon(UiIcon::Ton));
ui_->addCtuAction->setIcon(makeUiIcon(UiIcon::Ctu));
ui_->addCtdAction->setIcon(makeUiIcon(UiIcon::Ctd));
ui_->addMoveAction->setIcon(makeUiIcon(UiIcon::Move));
ui_->addAddAction->setIcon(makeUiIcon(UiIcon::Add));
ui_->addSubAction->setIcon(makeUiIcon(UiIcon::Subtract));
@@ -1601,14 +1524,9 @@ void MainWindow::updateModeUi(const QString &message)
ui_->addNormallyClosedAction->setEnabled(policy.allowsProjectEditing);
ui_->addRisingEdgeAction->setEnabled(policy.allowsProjectEditing);
ui_->addFallingEdgeAction->setEnabled(policy.allowsProjectEditing);
ui_->addTimerContactAction->setEnabled(policy.allowsProjectEditing);
ui_->addCounterContactAction->setEnabled(policy.allowsProjectEditing);
ui_->addNormalCoilAction->setEnabled(policy.allowsProjectEditing);
ui_->addSetCoilAction->setEnabled(policy.allowsProjectEditing);
ui_->addResetCoilAction->setEnabled(policy.allowsProjectEditing);
ui_->addTonAction->setEnabled(policy.allowsProjectEditing);
ui_->addCtuAction->setEnabled(policy.allowsProjectEditing);
ui_->addCtdAction->setEnabled(policy.allowsProjectEditing);
ui_->addMoveAction->setEnabled(policy.allowsProjectEditing);
ui_->addAddAction->setEnabled(policy.allowsProjectEditing);
ui_->addSubAction->setEnabled(policy.allowsProjectEditing);


+ 1
- 49
app/src/ui/main_window.ui Näytä tiedosto

@@ -922,27 +922,7 @@
</property>
</widget>
</item>
<item row="7" column="0">
<widget class="QLabel" name="logicPresetLabel">
<property name="text">
<string>预设时间(ms)</string>
</property>
</widget>
</item>
<item row="7" column="1">
<widget class="QSpinBox" name="logicPresetSpinBox">
<property name="minimum">
<number>1</number>
</property>
<property name="maximum">
<number>86400000</number>
</property>
<property name="value">
<number>1000</number>
</property>
</widget>
</item>
<item row="8" column="0" colspan="2">
<item row="7" column="0" colspan="2">
<widget class="QPushButton" name="applyLogicPropertiesButton">
<property name="text">
<string>应用属性</string>
@@ -1252,18 +1232,6 @@
<string>在选中的空网格或横线处添加一个扫描周期的下降沿触点</string>
</property>
</action>
<action name="addTimerContactAction">
<property name="text">
<string>T 触点</string>
</property>
<property name="toolTip">
<string>在选中的空网格或横线处添加离线仿真定时器触点</string>
</property>
</action>
<action name="addCounterContactAction">
<property name="text"><string>C 触点</string></property>
<property name="toolTip"><string>在选中的空网格或横线处添加离线仿真计数器完成触点</string></property>
</action>
<action name="addNormalCoilAction">
<property name="text">
<string>线圈</string>
@@ -1288,22 +1256,6 @@
<string>设置当前网络的复位线圈</string>
</property>
</action>
<action name="addTonAction">
<property name="text">
<string>TON</string>
</property>
<property name="toolTip">
<string>设置非保持接通延时定时器输出</string>
</property>
</action>
<action name="addCtuAction">
<property name="text"><string>CTU</string></property>
<property name="toolTip"><string>设置按上升沿执行的加计数器</string></property>
</action>
<action name="addCtdAction">
<property name="text"><string>CTD</string></property>
<property name="toolTip"><string>设置按上升沿执行的减计数器</string></property>
</action>
<action name="addMoveAction">
<property name="text"><string>MOVE</string></property>
<property name="toolTip"><string>设置 D 数据传送指令</string></property>


+ 0
- 69
app/src/ui/property_panel_controller.cpp Näytä tiedosto

@@ -378,7 +378,6 @@ void PropertyPanelController::showLogicNodeProperties(const std::string &node_id
static_cast<QWidget *>(ui_.logicModeComboBox),
static_cast<QWidget *>(ui_.logicComparisonComboBox),
static_cast<QWidget *>(ui_.logicValueSpinBox),
static_cast<QWidget *>(ui_.logicPresetSpinBox),
static_cast<QWidget *>(ui_.applyLogicPropertiesButton)})
{
widget->setEnabled(has_node);
@@ -391,7 +390,6 @@ void PropertyPanelController::showLogicNodeProperties(const std::string &node_id
ui_.logicNodeTypeLabel->clear();
ui_.logicAddressAreaLabel->clear();
ui_.logicModeComboBox->clear();
ui_.logicPresetSpinBox->setValue(TonNodeConfig::kMinimumPresetMs);
return;
}

@@ -399,7 +397,6 @@ void PropertyPanelController::showLogicNodeProperties(const std::string &node_id
ui_.logicModeComboBox->clear();
ui_.logicComparisonComboBox->setEnabled(false);
ui_.logicValueSpinBox->setEnabled(false);
ui_.logicPresetSpinBox->setEnabled(false);
if (const auto *contact = std::get_if<ContactNodeConfig>(&node->config))
{
ui_.logicNodeTypeLabel->setText(QObject::tr("触点"));
@@ -426,33 +423,6 @@ void PropertyPanelController::showLogicNodeProperties(const std::string &node_id
ui_.logicModeComboBox->findData(static_cast<int>(edge->mode)));
ui_.logicModeComboBox->setEnabled(true);
}
else if (const auto *timer = std::get_if<TimerContactNodeConfig>(&node->config))
{
ui_.logicNodeTypeLabel->setText(QObject::tr("T 触点"));
ui_.logicAddressAreaLabel->setText(QStringLiteral("T"));
ui_.logicAddressSpinBox->setValue(timer->address.index());
ui_.logicModeComboBox->addItem(
QObject::tr("常开"), static_cast<int>(ContactMode::NormallyOpen));
ui_.logicModeComboBox->addItem(
QObject::tr("常闭"), static_cast<int>(ContactMode::NormallyClosed));
ui_.logicModeComboBox->setCurrentIndex(
ui_.logicModeComboBox->findData(static_cast<int>(timer->mode)));
ui_.logicModeComboBox->setEnabled(true);
}
else if (const auto *counter_contact =
std::get_if<CounterContactNodeConfig>(&node->config))
{
ui_.logicNodeTypeLabel->setText(QObject::tr("C 触点"));
ui_.logicAddressAreaLabel->setText(QStringLiteral("C"));
ui_.logicAddressSpinBox->setValue(counter_contact->address.index());
ui_.logicModeComboBox->addItem(
QObject::tr("常开"), static_cast<int>(ContactMode::NormallyOpen));
ui_.logicModeComboBox->addItem(
QObject::tr("常闭"), static_cast<int>(ContactMode::NormallyClosed));
ui_.logicModeComboBox->setCurrentIndex(ui_.logicModeComboBox->findData(
static_cast<int>(counter_contact->mode)));
ui_.logicModeComboBox->setEnabled(true);
}
else if (const auto *coil = std::get_if<CoilNodeConfig>(&node->config))
{
ui_.logicNodeTypeLabel->setText(QObject::tr("线圈"));
@@ -482,27 +452,6 @@ void PropertyPanelController::showLogicNodeProperties(const std::string &node_id
ui_.logicValueSpinBox->setEnabled(true);
ui_.logicValueSpinBox->setValue(compare->value);
}
else if (const auto *ton = std::get_if<TonNodeConfig>(&node->config))
{
ui_.logicNodeTypeLabel->setText(QObject::tr("TON"));
ui_.logicAddressAreaLabel->setText(QStringLiteral("T"));
ui_.logicAddressSpinBox->setValue(ton->address.index());
ui_.logicModeComboBox->addItem(QObject::tr("非保持接通延时"));
ui_.logicModeComboBox->setEnabled(false);
ui_.logicPresetSpinBox->setEnabled(true);
ui_.logicPresetSpinBox->setValue(ton->presetMs);
}
else if (const auto *counter = std::get_if<CounterNodeConfig>(&node->config))
{
ui_.logicNodeTypeLabel->setText(
counter->mode == CounterMode::Up ? QStringLiteral("CTU")
: QStringLiteral("CTD"));
ui_.logicAddressAreaLabel->setText(QStringLiteral("C"));
ui_.logicAddressSpinBox->setValue(counter->address.index());
ui_.logicAddressSpinBox->setEnabled(false);
ui_.logicModeComboBox->addItem(QObject::tr("使用配置对话框编辑"));
ui_.logicModeComboBox->setEnabled(false);
}
else if (const auto *move = std::get_if<MoveNodeConfig>(&node->config))
{
ui_.logicNodeTypeLabel->setText(QStringLiteral("MOVE"));
@@ -726,18 +675,6 @@ void PropertyPanelController::applySelectedLogicNodeProperties()
RegisterAddress{RegisterArea::M, ui_.logicAddressSpinBox->value()},
static_cast<EdgeMode>(ui_.logicModeComboBox->currentData().toInt())};
}
else if (std::holds_alternative<TimerContactNodeConfig>(config))
{
config = TimerContactNodeConfig{
TimerAddress{ui_.logicAddressSpinBox->value()},
static_cast<ContactMode>(ui_.logicModeComboBox->currentData().toInt())};
}
else if (std::holds_alternative<CounterContactNodeConfig>(config))
{
config = CounterContactNodeConfig{
CounterAddress{ui_.logicAddressSpinBox->value()},
static_cast<ContactMode>(ui_.logicModeComboBox->currentData().toInt())};
}
else if (std::holds_alternative<CoilNodeConfig>(config))
{
config = CoilNodeConfig{
@@ -752,12 +689,6 @@ void PropertyPanelController::applySelectedLogicNodeProperties()
ui_.logicComparisonComboBox->currentData().toInt()),
static_cast<std::int16_t>(ui_.logicValueSpinBox->value())};
}
else if (std::holds_alternative<TonNodeConfig>(config))
{
config = TonNodeConfig{
TimerAddress{ui_.logicAddressSpinBox->value()},
ui_.logicPresetSpinBox->value()};
}
else
{
LogicInstructionDialog dialog(node->config, &parent_);


+ 0
- 25
app/src/ui/toolbar_icon_factory.cpp Näytä tiedosto

@@ -410,16 +410,6 @@ QPixmap renderIcon(UiIcon icon, int size)
drawContact(painter, QStringLiteral("N"), false);
break;
}
case UiIcon::TimerContact:
{
drawContact(painter, QStringLiteral("T"), false);
break;
}
case UiIcon::CounterContact:
{
drawContact(painter, QStringLiteral("C"), false);
break;
}
case UiIcon::Coil:
{
drawCoil(painter, {});
@@ -435,21 +425,6 @@ QPixmap renderIcon(UiIcon icon, int size)
drawCoil(painter, QStringLiteral("R"));
break;
}
case UiIcon::Ton:
{
drawInstructionBlock(painter, QStringLiteral("TON"));
break;
}
case UiIcon::Ctu:
{
drawInstructionBlock(painter, QStringLiteral("CTU"));
break;
}
case UiIcon::Ctd:
{
drawInstructionBlock(painter, QStringLiteral("CTD"));
break;
}
case UiIcon::Move:
{
painter.drawRect(QRectF(2, 6, 6, 12));


+ 0
- 5
app/src/ui/toolbar_icon_factory.h Näytä tiedosto

@@ -44,14 +44,9 @@ enum class UiIcon
NormallyClosedContact, // 常闭触点
RisingEdgeContact, // 上升沿触点
FallingEdgeContact, // 下降沿触点
TimerContact, // 定时器触点
CounterContact, // 计数器触点
Coil, // 普通线圈
SetCoil, // 置位线圈
ResetCoil, // 复位线圈
Ton, // TON 定时器
Ctu, // CTU 加计数器
Ctd, // CTD 减计数器
Move, // MOVE 指令
Add, // ADD 指令
Subtract, // SUB 指令


+ 4
- 181
app/tests/domain_tests.cpp Näytä tiedosto

@@ -434,14 +434,6 @@ void testQuantityBoundaries()
require(!excessive_depth.validate(),
"an expression depth of 21 must be rejected");

CounterNodeConfig counter_config{
CounterAddress{0},
CounterMode::Up,
RegisterAddress{RegisterArea::D, 0},
WordOperand{WordOperandKind::Constant, RegisterAddress{RegisterArea::D, 0}, -1},
RegisterAddress{RegisterArea::M, 0}};
LogicNode counter{"counter-negative-preset", counter_config, true};
require(!counter.validate(), "a negative constant counter preset must be rejected");
(void)leaf;
}

@@ -469,7 +461,7 @@ void testLogicNodeConfigurationBoundaries()
require(comparison.validate(), "comparison node bound to D address must be valid");
}

void testTimerAndCommentBoundaries()
void testEdgeAndCommentBoundaries()
{
LogicNode edge;
edge.id = "edge";
@@ -477,25 +469,6 @@ void testTimerAndCommentBoundaries()
RegisterAddress{RegisterArea::M, 0}, EdgeMode::Rising};
require(edge.validate(), "a valid rising edge contact must pass validation");

LogicNode timer_contact;
timer_contact.id = "timer-contact";
timer_contact.config = TimerContactNodeConfig{
TimerAddress{4000}, ContactMode::NormallyOpen};
require(timer_contact.validate(), "T4000 must be a valid timer contact");
timer_contact.config = TimerContactNodeConfig{
TimerAddress{-1}, ContactMode::NormallyOpen};
require(!timer_contact.validate(), "a negative T address must be rejected");

LogicNode ton;
ton.id = "ton";
ton.config = TonNodeConfig{TimerAddress{0}, TonNodeConfig::kMinimumPresetMs};
require(ton.validate(), "the minimum TON preset must be valid");
ton.config = TonNodeConfig{TimerAddress{0}, 0};
require(!ton.validate(), "a zero TON preset must be rejected");
ton.config = TonNodeConfig{
TimerAddress{0}, TonNodeConfig::kMaximumPresetMs + 1};
require(!ton.validate(), "an oversized TON preset must be rejected");

RegisterComment comment{RegisterAddress{RegisterArea::M, 0}, "启动按钮"};
require(comment.validate(), "a nonblank register comment must be valid");
comment.text.assign(ProjectLimits::kMaximumRegisterCommentBytes, 'a');
@@ -528,124 +501,8 @@ void testTimerAndCommentBoundaries()
require(!project.validate(), "duplicate register comments must be rejected");
}

LadderRung makeTimerRung(
const std::string &rung_id,
const std::string &condition_id,
const std::string &output_id,
int timer_index,
int preset_ms)
{
LogicNode condition;
condition.id = condition_id;
condition.config = ContactNodeConfig{
RegisterAddress{RegisterArea::M, timer_index}, ContactMode::NormallyOpen};
LogicNode output;
output.id = output_id;
output.config = TonNodeConfig{TimerAddress{timer_index}, preset_ms};
LadderRung rung;
rung.id = rung_id;
rung.name = rung_id;
rung.condition = ConditionExpression::fromNode(condition);
rung.output = output;
return rung;
}

void testTimerReferencesForRunning()
void testDataInstructionBoundaries()
{
ControlLogic valid;
valid.id = "timer-valid";
valid.name = "Timer valid";
valid.rungs.push_back(makeTimerRung("rung-0", "input-0", "ton-0", 0, 100));
require(validateTimerReferencesForRunning({valid}),
"a timer contact-free TON network must pass timer reference validation");

ControlLogic duplicate = valid;
duplicate.id = "timer-duplicate";
duplicate.name = "Timer duplicate";
duplicate.rungs.front().output->id = "ton-duplicate";
require(!validateTimerReferencesForRunning({valid, duplicate}),
"the same T must not have two enabled TON drivers");

ControlLogic missing;
missing.id = "timer-missing";
missing.name = "Timer missing";
LogicNode contact;
contact.id = "missing-contact";
contact.config = TimerContactNodeConfig{
TimerAddress{7}, ContactMode::NormallyOpen};
LogicNode output;
output.id = "missing-coil";
output.config = CoilNodeConfig{
RegisterAddress{RegisterArea::M, 7}, CoilMode::Normal};
LadderRung missing_rung;
missing_rung.id = "missing-rung";
missing_rung.name = "Missing rung";
missing_rung.condition = ConditionExpression::fromNode(contact);
missing_rung.output = output;
missing.rungs.push_back(missing_rung);
require(!validateTimerReferencesForRunning({missing}),
"a T contact without an enabled TON driver must be rejected");

missing.enabled = false;
require(validateTimerReferencesForRunning({missing}),
"disabled timer drafts must not block runtime timer validation");
}

LadderRung makeCounterRung(
const std::string &rung_id,
const std::string &output_id,
int counter_index)
{
LogicNode condition;
condition.id = rung_id + "-input";
condition.config = ContactNodeConfig{
RegisterAddress{RegisterArea::M, counter_index},
ContactMode::NormallyOpen};
LogicNode output;
output.id = output_id;
output.config = CounterNodeConfig{
CounterAddress{counter_index},
CounterMode::Up,
RegisterAddress{RegisterArea::D, counter_index},
WordOperand{
WordOperandKind::Constant,
RegisterAddress{RegisterArea::D, 0},
10},
RegisterAddress{RegisterArea::M, counter_index + 1}};
LadderRung rung;
rung.id = rung_id;
rung.name = rung_id;
rung.condition = ConditionExpression::fromNode(condition);
rung.output = output;
return rung;
}

void testCounterAndDataInstructionBoundaries()
{
require(CounterAddress{0}.isValid() && CounterAddress{4000}.isValid(),
"C0 and C4000 must be valid counter resources");
require(!CounterAddress{-1}.isValid() && !CounterAddress{4001}.isValid(),
"counter resources outside 0 through 4000 must be rejected");

LogicNode counter;
counter.id = "counter";
counter.config = CounterNodeConfig{
CounterAddress{0},
CounterMode::Up,
RegisterAddress{RegisterArea::D, 10},
WordOperand{
WordOperandKind::Register,
RegisterAddress{RegisterArea::D, 11},
0},
RegisterAddress{RegisterArea::M, 12}};
require(counter.validate(),
"a counter with C identity and external M/D addresses must be valid");

CounterNodeConfig invalid_counter = std::get<CounterNodeConfig>(counter.config);
invalid_counter.currentValueAddress = RegisterAddress{RegisterArea::M, 10};
counter.config = invalid_counter;
require(!counter.validate(), "counter CV must reject M addresses");

LogicNode move;
move.id = "move";
move.config = MoveNodeConfig{
@@ -673,39 +530,6 @@ void testCounterAndDataInstructionBoundaries()
require(add.validate() && add.isOutput(),
"ADD must allow the same D register as source and destination");

ControlLogic valid;
valid.id = "counter-valid";
valid.name = "Counter valid";
valid.rungs.push_back(makeCounterRung("counter-rung", "ctu-0", 0));
require(validateCounterReferencesForRunning({valid}),
"a counter output without contacts must pass reference validation");

ControlLogic duplicate = valid;
duplicate.id = "counter-duplicate";
duplicate.name = "Counter duplicate";
duplicate.rungs.front().output->id = "ctu-duplicate";
require(!validateCounterReferencesForRunning({valid, duplicate}),
"the same C resource must not have multiple enabled drivers");

ControlLogic missing;
missing.id = "counter-missing";
missing.name = "Counter missing";
LogicNode missing_contact;
missing_contact.id = "missing-counter-contact";
missing_contact.config = CounterContactNodeConfig{
CounterAddress{7}, ContactMode::NormallyOpen};
LogicNode output;
output.id = "missing-counter-coil";
output.config = CoilNodeConfig{
RegisterAddress{RegisterArea::M, 7}, CoilMode::Normal};
LadderRung missing_rung;
missing_rung.id = "missing-counter-rung";
missing_rung.name = "Missing counter rung";
missing_rung.condition = ConditionExpression::fromNode(missing_contact);
missing_rung.output = output;
missing.rungs.push_back(missing_rung);
require(!validateCounterReferencesForRunning({missing}),
"a C contact without an enabled counter driver must be rejected");
}

void testLadderLogicBoundaries()
@@ -914,9 +738,8 @@ int main()
testProgressBarConfigurationBoundaries();
testHmiAppearancePropertyBoundaries();
testLogicNodeConfigurationBoundaries();
testTimerAndCommentBoundaries();
testTimerReferencesForRunning();
testCounterAndDataInstructionBoundaries();
testEdgeAndCommentBoundaries();
testDataInstructionBoundaries();
testLadderLogicBoundaries();
testModelsValidateBindingsAndIdentifiers();
testMultiPageAndLogicDomainRules();


+ 7
- 34
app/tests/logic_editor_service_tests.cpp Näytä tiedosto

@@ -883,7 +883,7 @@ void testLogicLifecycleAndOrdering()
"the project must retain at least one control logic module");
}

void testEdgeTimerNodesAndRungComments()
void testEdgeNodesAndRungComments()
{
TestProjectStorage storage;
ProjectService project_service(storage);
@@ -898,18 +898,6 @@ void testEdgeTimerNodesAndRungComments()
RegisterAddress{RegisterArea::M, 3}, EdgeMode::Rising});
require(rising.succeeded && rising.id == "edge-1",
"the editor must create rising edge nodes with a stable prefix");
const LogicEditorResult timer = service.addParallelBranch(
logic_id,
rung_id,
{rising.id},
TimerContactNodeConfig{TimerAddress{2}, ContactMode::NormallyOpen});
require(timer.succeeded && timer.id == "timer-contact-1",
"the editor must insert T contacts as conditions");
const LogicEditorResult ton = service.setOutput(
logic_id, rung_id, TonNodeConfig{TimerAddress{2}, 500});
require(ton.succeeded && ton.id == "ton-1",
"the editor must create TON outputs with a stable prefix");

require(service.updateNodeConfig(
logic_id,
rising.id,
@@ -917,18 +905,6 @@ void testEdgeTimerNodesAndRungComments()
RegisterAddress{RegisterArea::M, 4}, EdgeMode::Falling})
.succeeded,
"the editor must apply edge mode and M address properties");
require(service.updateNodeConfig(
logic_id,
timer.id,
TimerContactNodeConfig{TimerAddress{4}, ContactMode::NormallyClosed})
.succeeded,
"the editor must apply T contact mode and address properties");
require(service.updateNodeConfig(
logic_id,
ton.id,
TonNodeConfig{TimerAddress{4}, 750})
.succeeded,
"the editor must apply TON preset properties");
require(service.updateRungComment(logic_id, rung_id, "延时启动网络").succeeded,
"the editor must update a network comment by stable rung id");
require(!service.updateRungComment(
@@ -942,15 +918,12 @@ void testEdgeTimerNodesAndRungComments()
"the editor must reject oversized network comments");

const LadderRung *rung = service.findRung(logic_id, rung_id);
const LogicNode *edge = service.findNode(logic_id, rising.id);
require(rung != nullptr && rung->comment == "延时启动网络"
&& std::get<EdgeContactNodeConfig>(
rung->condition->children.front().node->config).mode
== EdgeMode::Falling
&& std::get<TimerContactNodeConfig>(
rung->condition->children.at(1).node->config).address
== TimerAddress{4}
&& std::get<TonNodeConfig>(rung->output->config).presetMs == 750,
"edge, T contact, TON and rung comment updates must remain in the model");
&& edge != nullptr
&& std::get<EdgeContactNodeConfig>(edge->config).mode
== EdgeMode::Falling,
"edge and rung comment updates must remain in the model");
}

void testHistoryAndAtomicBatchDelete()
@@ -1045,7 +1018,7 @@ int main()
testWireColumnReplacement();
testSequentialConditionInsertionConsumesFollowingWire();
testLogicLifecycleAndOrdering();
testEdgeTimerNodesAndRungComments();
testEdgeNodesAndRungComments();
testHistoryAndAtomicBatchDelete();
}
catch (const std::exception &error)


+ 0
- 258
app/tests/offline_simulation_service_tests.cpp Näytä tiedosto

@@ -32,27 +32,6 @@ LogicNode edgeContact(const std::string &id, int address, EdgeMode mode)
true};
}

LogicNode timerContact(
const std::string &id,
int timer_index,
ContactMode mode = ContactMode::NormallyOpen)
{
return {id, TimerContactNodeConfig{TimerAddress{timer_index}, mode}, true};
}

LogicNode counterContact(
const std::string &id,
int counter_index,
ContactMode mode = ContactMode::NormallyOpen)
{
return {id, CounterContactNodeConfig{CounterAddress{counter_index}, mode}, true};
}

LogicNode ton(const std::string &id, int timer_index, int preset_ms)
{
return {id, TonNodeConfig{TimerAddress{timer_index}, preset_ms}, true};
}

WordOperand constantOperand(std::int16_t value)
{
return {
@@ -69,25 +48,6 @@ WordOperand registerOperand(int address)
0};
}

LogicNode counter(
const std::string &id,
int counter_index,
CounterMode mode,
int current_address,
WordOperand preset,
int reset_address)
{
return {
id,
CounterNodeConfig{
CounterAddress{counter_index},
mode,
RegisterAddress{RegisterArea::D, current_address},
preset,
RegisterAddress{RegisterArea::M, reset_address}},
true};
}

LogicNode move(
const std::string &id,
WordOperand source,
@@ -479,221 +439,6 @@ void testEdgeContactsAreOneScanPulsesAndAreLogicScoped()
"a restarted runtime must treat current ON inputs as fresh rising edges");
}

void testTonUsesElapsedTimeAndResetsAsNonRetentive()
{
VirtualRegisterRepository repository;
SoftwareLogicExecutor executor;
const ControlLogic program = logic({
rung("ton-rung", {{contact("ton-input", 0)}}, ton("ton-0", 0, 100)),
rung("ton-feedback-rung", {{timerContact("timer-done", 0)}},
coil("ton-output", 30))});
const auto start = SoftwareLogicExecutor::TimePoint{};
LogicTraceSnapshot trace;

require(executor.executeScanAt({program}, repository, start, &trace).succeeded,
"an inactive TON must scan successfully");
require(!readBit(repository, 30)
&& trace.tonValues.at("ton-0").elapsedMs == 0
&& !trace.tonValues.at("ton-0").done,
"an inactive TON must have Q false and ET zero");

writeBit(repository, 0, true);
require(executor.executeScanAt({program}, repository, start, &trace).succeeded,
"the first active TON scan must start timing");
require(!readBit(repository, 30) && trace.tonValues.at("ton-0").elapsedMs == 0,
"TON must not complete on its starting scan");
require(executor.executeScanAt(
{program}, repository,
start + std::chrono::milliseconds{99}, &trace)
.succeeded,
"TON must use elapsed monotonic time before the preset");
require(!readBit(repository, 30)
&& trace.tonValues.at("ton-0").elapsedMs == 99,
"TON must remain false before PT");
require(executor.executeScanAt(
{program}, repository,
start + std::chrono::milliseconds{100}, &trace)
.succeeded,
"TON must complete at its preset time");
require(readBit(repository, 30)
&& trace.tonValues.at("ton-0").done,
"the T contact must observe TON Q in a later network of the same scan");

writeBit(repository, 0, false);
require(executor.executeScanAt(
{program}, repository,
start + std::chrono::milliseconds{150}, &trace)
.succeeded,
"disconnecting TON input must scan successfully");
require(!readBit(repository, 30)
&& trace.tonValues.at("ton-0").elapsedMs == 0
&& !trace.tonValues.at("ton-0").done,
"a non-retentive TON must reset ET and Q when input opens");

writeBit(repository, 0, true);
require(executor.executeScanAt(
{program}, repository,
start + std::chrono::milliseconds{200}, &trace)
.succeeded,
"TON must restart timing after an input reset");
require(!readBit(repository, 30), "TON restart must not retain the old done state");

executor.resetRuntime();
require(executor.executeScanAt(
{program}, repository,
start + std::chrono::milliseconds{1000}, &trace)
.succeeded,
"resetRuntime must clear TON state");
require(!readBit(repository, 30),
"a restarted runtime must start a currently active TON from zero");
}

void testMultipleTimersAndNetworkOrder()
{
VirtualRegisterRepository repository;
SoftwareLogicExecutor executor;
const ControlLogic independent = logic({
rung("ton-1-rung", {{contact("input-1", 1)}}, ton("ton-1", 1, 100)),
rung("ton-2-rung", {{contact("input-2", 2)}}, ton("ton-2", 2, 200))});
writeBit(repository, 1, true);
writeBit(repository, 2, true);
const auto start = SoftwareLogicExecutor::TimePoint{};
LogicTraceSnapshot trace;
require(executor.executeScanAt({independent}, repository, start, &trace).succeeded,
"multiple TON resources must start independently");
require(executor.executeScanAt(
{independent}, repository,
start + std::chrono::milliseconds{100}, &trace)
.succeeded,
"the first independent TON must reach PT without completing the second");
require(trace.tonValues.at("ton-1").done
&& !trace.tonValues.at("ton-2").done,
"different T resources must retain independent elapsed time");

VirtualRegisterRepository ordered_repository;
SoftwareLogicExecutor ordered_executor;
const ControlLogic ordered = logic({
rung("feedback-before-ton", {{timerContact("early-timer", 4)}},
coil("early-output", 40)),
rung("ton-after-feedback", {{contact("late-input", 4)}}, ton("ton-4", 4, 100))});
writeBit(ordered_repository, 4, true);
require(ordered_executor.executeScanAt(
{ordered}, ordered_repository, start, nullptr)
.succeeded,
"network order test must start TON after its earlier T contact");
require(!readBit(ordered_repository, 40),
"an earlier T contact must see the previous scan state");
require(ordered_executor.executeScanAt(
{ordered}, ordered_repository,
start + std::chrono::milliseconds{100}, nullptr)
.succeeded,
"network order test must complete TON on the next scan");
require(!readBit(ordered_repository, 40),
"the earlier T contact must remain false on the completion scan");
require(ordered_executor.executeScanAt(
{ordered}, ordered_repository,
start + std::chrono::milliseconds{101}, nullptr)
.succeeded,
"network order test must expose TON Q on the following scan");
require(readBit(ordered_repository, 40),
"a later scan must observe the completed TON through T contact");
}

void testCountersUseRisingEdgesAndExternalDValues()
{
VirtualRegisterRepository repository;
SoftwareLogicExecutor executor;
const ControlLogic up_program = logic({
rung(
"ctu-rung",
{{contact("count-input", 0)}},
counter(
"ctu-0",
0,
CounterMode::Up,
0,
constantOperand(3),
1)),
rung(
"ctu-done-rung",
{{counterContact("ctu-done", 0)}},
coil("ctu-done-output", 2))});
LogicTraceSnapshot trace;

require(executor.executeScan({up_program}, repository, &trace).succeeded,
"an inactive CTU must scan successfully");
require(readWord(repository, 0) == 0 && !readBit(repository, 2),
"an inactive CTU must keep CV zero and Q false");

writeBit(repository, 0, true);
require(executor.executeScan({up_program}, repository, &trace).succeeded,
"the first CTU rising edge must scan successfully");
require(readWord(repository, 0) == 1,
"CTU must increment the external D current value on a rising edge");
require(executor.executeScan({up_program}, repository, &trace).succeeded,
"a held CTU input must scan successfully");
require(readWord(repository, 0) == 1,
"a held CTU input must not count every scan");

for (int expected = 2; expected <= 3; ++expected)
{
writeBit(repository, 0, false);
require(executor.executeScan({up_program}, repository).succeeded,
"CTU falling preparation scan must succeed");
writeBit(repository, 0, true);
require(executor.executeScan({up_program}, repository, &trace).succeeded,
"CTU repeated rising edge must succeed");
require(readWord(repository, 0) == expected,
"CTU must increment exactly once for each rising edge");
}
require(readBit(repository, 2)
&& trace.counterValues.at("ctu-0").done,
"the C contact must observe CTU completion in a later network");

writeBit(repository, 1, true);
require(executor.executeScan({up_program}, repository, &trace).succeeded,
"CTU reset must scan successfully");
require(readWord(repository, 0) == 0 && !readBit(repository, 2),
"CTU reset must clear CV and the C completion state");

executor.resetRuntime();
repository.clear();
const ControlLogic down_program = logic({
rung(
"ctd-rung",
{{contact("down-input", 3)}},
counter(
"ctd-1",
1,
CounterMode::Down,
1,
constantOperand(2),
4)),
rung(
"ctd-done-rung",
{{counterContact("ctd-done", 1)}},
coil("ctd-done-output", 5))});
writeBit(repository, 4, true);
require(executor.executeScan({down_program}, repository).succeeded,
"CTD load reset must scan successfully");
require(readWord(repository, 1) == 2,
"CTD reset must load PV into the external D current value");
writeBit(repository, 4, false);
for (int expected = 1; expected >= 0; --expected)
{
writeBit(repository, 3, false);
require(executor.executeScan({down_program}, repository).succeeded,
"CTD falling preparation scan must succeed");
writeBit(repository, 3, true);
require(executor.executeScan({down_program}, repository).succeeded,
"CTD rising edge must scan successfully");
require(readWord(repository, 1) == expected,
"CTD must decrement exactly once for each rising edge");
}
require(readBit(repository, 5),
"CTD completion contact must turn on when CV reaches zero");
}

void testMoveAndSaturatingArithmetic()
{
VirtualRegisterRepository repository;
@@ -924,9 +669,6 @@ int main(int argc, char *argv[])
testSetResetAndDisabledLogic();
testMultipleLogicScanOrderAndTraceIsolation();
testEdgeContactsAreOneScanPulsesAndAreLogicScoped();
testTonUsesElapsedTimeAndResetsAsNonRetentive();
testMultipleTimersAndNetworkOrder();
testCountersUseRisingEdgesAndExternalDValues();
testMoveAndSaturatingArithmetic();
testSetResetPairOnSameAddress();
testConflictingCoilsAreRejected();


+ 21
- 118
app/tests/project_management_tests.cpp Näytä tiedosto

@@ -159,80 +159,17 @@ Project makeExampleProject()
rising_edge.id = "rising-edge";
rising_edge.config = EdgeContactNodeConfig{
RegisterAddress{RegisterArea::M, 4}, EdgeMode::Rising};
LogicNode ton_output;
ton_output.id = "ton-output";
ton_output.config = TonNodeConfig{TimerAddress{7}, 2500};
LadderRung ton_rung;
ton_rung.id = "ton-rung";
ton_rung.name = "TON network";
ton_rung.comment = "延时启动";
ton_rung.condition = ConditionExpression::fromNode(rising_edge);
ton_rung.output = ton_output;
logic.rungs.push_back(ton_rung);

LogicNode falling_edge;
falling_edge.id = "falling-edge";
falling_edge.config = EdgeContactNodeConfig{
RegisterAddress{RegisterArea::M, 5}, EdgeMode::Falling};
LogicNode timer_contact;
timer_contact.id = "timer-contact";
timer_contact.config = TimerContactNodeConfig{
TimerAddress{7}, ContactMode::NormallyOpen};
LogicNode timer_coil;
timer_coil.id = "timer-coil";
timer_coil.config = CoilNodeConfig{
LogicNode edge_output;
edge_output.id = "edge-output";
edge_output.config = CoilNodeConfig{
RegisterAddress{RegisterArea::M, 6}, CoilMode::Normal};
LadderRung timer_rung;
timer_rung.id = "timer-rung";
timer_rung.name = "Timer feedback";
timer_rung.comment = "定时器完成反馈";
ConditionExpression timer_series;
timer_series.id = "timer-series";
timer_series.kind = ConditionExpressionKind::Series;
timer_series.children = {
ConditionExpression::fromNode(falling_edge),
ConditionExpression::fromNode(timer_contact)};
timer_rung.condition = std::move(timer_series);
timer_rung.output = timer_coil;
logic.rungs.push_back(timer_rung);

LogicNode counter_input;
counter_input.id = "counter-input";
counter_input.config = ContactNodeConfig{
RegisterAddress{RegisterArea::M, 7}, ContactMode::NormallyOpen};
LogicNode counter_output;
counter_output.id = "counter-output";
counter_output.config = CounterNodeConfig{
CounterAddress{3},
CounterMode::Up,
RegisterAddress{RegisterArea::D, 10},
WordOperand{
WordOperandKind::Register,
RegisterAddress{RegisterArea::D, 11},
0},
RegisterAddress{RegisterArea::M, 8}};
LadderRung counter_rung;
counter_rung.id = "counter-rung";
counter_rung.name = "Counter network";
counter_rung.comment = "计数器上升沿计数";
counter_rung.condition = ConditionExpression::fromNode(counter_input);
counter_rung.output = counter_output;
logic.rungs.push_back(counter_rung);

LogicNode counter_contact;
counter_contact.id = "counter-contact";
counter_contact.config = CounterContactNodeConfig{
CounterAddress{3}, ContactMode::NormallyOpen};
LogicNode counter_coil;
counter_coil.id = "counter-coil";
counter_coil.config = CoilNodeConfig{
RegisterAddress{RegisterArea::M, 9}, CoilMode::Normal};
LadderRung counter_feedback_rung;
counter_feedback_rung.id = "counter-feedback-rung";
counter_feedback_rung.name = "Counter feedback";
counter_feedback_rung.condition = ConditionExpression::fromNode(counter_contact);
counter_feedback_rung.output = counter_coil;
logic.rungs.push_back(counter_feedback_rung);
LadderRung edge_rung;
edge_rung.id = "edge-rung";
edge_rung.name = "Edge network";
edge_rung.comment = "上升沿输出";
edge_rung.condition = ConditionExpression::fromNode(rising_edge);
edge_rung.output = edge_output;
logic.rungs.push_back(edge_rung);

const auto addDataRung = [&logic](
const std::string &rung_id,
@@ -452,10 +389,6 @@ void testExampleProjectRoundTrip()
&& saved_json.contains("\"registerComments\"")
&& saved_json.contains("\"text\": \"启动按钮\"")
&& saved_json.contains("\"type\": \"edgeContact\"")
&& saved_json.contains("\"type\": \"timerContact\"")
&& saved_json.contains("\"type\": \"ton\"")
&& saved_json.contains("\"type\": \"counterContact\"")
&& saved_json.contains("\"type\": \"counter\"")
&& saved_json.contains("\"type\": \"move\"")
&& saved_json.contains("\"type\": \"arithmetic\"")
&& saved_json.contains("\"operation\": \"add\"")
@@ -550,56 +483,26 @@ void testExampleProjectRoundTrip()
require(rung.output.has_value(),
"ladder output must survive round trip");

const LadderRung &ton_rung = project.controlLogics.front().rungs.at(1);
require(ton_rung.output.has_value()
&& std::holds_alternative<TonNodeConfig>(ton_rung.output->config)
&& std::get<TonNodeConfig>(ton_rung.output->config).address
== TimerAddress{7}
&& std::get<TonNodeConfig>(ton_rung.output->config).presetMs == 2500,
"TON timer address and preset must survive round trip");
const LadderRung &edge_rung = project.controlLogics.front().rungs.at(1);
require(edge_rung.output.has_value()
&& std::holds_alternative<CoilNodeConfig>(edge_rung.output->config)
&& std::get<CoilNodeConfig>(edge_rung.output->config).address
== RegisterAddress{RegisterArea::M, 6},
"edge network output must survive round trip");
require(std::holds_alternative<EdgeContactNodeConfig>(
ton_rung.condition->node->config)
&& std::get<EdgeContactNodeConfig>(ton_rung.condition->node->config).mode
edge_rung.condition->node->config)
&& std::get<EdgeContactNodeConfig>(edge_rung.condition->node->config).mode
== EdgeMode::Rising,
"rising edge configuration must survive round trip");
const LadderRung &timer_rung = project.controlLogics.front().rungs.at(2);
require(timer_rung.comment == "定时器完成反馈"
&& std::holds_alternative<TimerContactNodeConfig>(
timer_rung.condition->children.at(1).node->config)
&& std::get<TimerContactNodeConfig>(
timer_rung.condition->children.at(1).node->config).address
== TimerAddress{7}
&& std::holds_alternative<EdgeContactNodeConfig>(
timer_rung.condition->children.front().node->config)
&& std::get<EdgeContactNodeConfig>(
timer_rung.condition->children.front().node->config).mode
== EdgeMode::Falling,
"falling edge and T contact configurations must survive round trip");

const LadderRung &counter_rung = project.controlLogics.front().rungs.at(3);
const auto &counter = std::get<CounterNodeConfig>(counter_rung.output->config);
require(counter.address == CounterAddress{3}
&& counter.currentValueAddress == RegisterAddress{RegisterArea::D, 10}
&& counter.preset.kind == WordOperandKind::Register
&& counter.preset.address == RegisterAddress{RegisterArea::D, 11}
&& counter.resetAddress == RegisterAddress{RegisterArea::M, 8},
"counter resource and external M/D addresses must survive round trip");
const LadderRung &counter_feedback = project.controlLogics.front().rungs.at(4);
require(std::holds_alternative<CounterContactNodeConfig>(
counter_feedback.condition->node->config)
&& std::get<CounterContactNodeConfig>(
counter_feedback.condition->node->config).address
== CounterAddress{3},
"counter contacts must survive round trip");
require(std::get<MoveNodeConfig>(
project.controlLogics.front().rungs.at(5).output->config)
project.controlLogics.front().rungs.at(2).output->config)
.source.constant == 25,
"MOVE operands must survive round trip");
require(std::get<ArithmeticNodeConfig>(
project.controlLogics.front().rungs.at(6).output->config)
project.controlLogics.front().rungs.at(3).output->config)
.operation == ArithmeticOperation::Add
&& std::get<ArithmeticNodeConfig>(
project.controlLogics.front().rungs.at(7).output->config)
project.controlLogics.front().rungs.at(4).output->config)
.operation == ArithmeticOperation::Subtract,
"ADD and SUB operations must survive round trip");



+ 1
- 52
app/tests/runtime_mode_service_tests.cpp Näytä tiedosto

@@ -143,54 +143,6 @@ void testModeTransitions()
comparison_rung.condition = ConditionExpression::fromNode(comparison);
comparison_rung.output = comparison_coil;
logic.rungs.push_back(comparison_rung);
LogicNode timer_input;
timer_input.id = "poll-timer-input";
timer_input.config = ContactNodeConfig{
RegisterAddress{RegisterArea::M, 23}, ContactMode::NormallyOpen};
LogicNode ton;
ton.id = "poll-ton";
ton.config = TonNodeConfig{TimerAddress{4}, 100};
LadderRung ton_rung;
ton_rung.id = "poll-ton-rung";
ton_rung.name = "Poll TON";
ton_rung.condition = ConditionExpression::fromNode(timer_input);
ton_rung.output = ton;
logic.rungs.push_back(ton_rung);
LogicNode timer_contact;
timer_contact.id = "poll-timer-contact";
timer_contact.config = TimerContactNodeConfig{
TimerAddress{4}, ContactMode::NormallyOpen};
LogicNode timer_coil;
timer_coil.id = "poll-timer-coil";
timer_coil.config = CoilNodeConfig{
RegisterAddress{RegisterArea::M, 24}, CoilMode::Normal};
LadderRung timer_rung;
timer_rung.id = "poll-timer-rung";
timer_rung.name = "Poll timer contact";
timer_rung.condition = ConditionExpression::fromNode(timer_contact);
timer_rung.output = timer_coil;
logic.rungs.push_back(timer_rung);
LogicNode counter_input;
counter_input.id = "poll-counter-input";
counter_input.config = ContactNodeConfig{
RegisterAddress{RegisterArea::M, 25}, ContactMode::NormallyOpen};
LogicNode counter_output;
counter_output.id = "poll-counter";
counter_output.config = CounterNodeConfig{
CounterAddress{2},
CounterMode::Up,
RegisterAddress{RegisterArea::D, 36},
WordOperand{
WordOperandKind::Register,
RegisterAddress{RegisterArea::D, 37},
0},
RegisterAddress{RegisterArea::M, 26}};
LadderRung counter_rung;
counter_rung.id = "poll-counter-rung";
counter_rung.name = "Poll counter";
counter_rung.condition = ConditionExpression::fromNode(counter_input);
counter_rung.output = counter_output;
logic.rungs.push_back(counter_rung);
LogicNode move_input;
move_input.id = "poll-move-input";
move_input.config = ContactNodeConfig{
@@ -264,14 +216,11 @@ void testModeTransitions()
== std::vector<RegisterAddress>({
{RegisterArea::M, 12}, {RegisterArea::M, 20},
{RegisterArea::M, 21}, {RegisterArea::M, 22},
{RegisterArea::M, 23}, {RegisterArea::M, 24},
{RegisterArea::M, 25}, {RegisterArea::M, 26},
{RegisterArea::M, 27}, {RegisterArea::M, 28},
{RegisterArea::D, 34}, {RegisterArea::D, 35},
{RegisterArea::D, 36}, {RegisterArea::D, 37},
{RegisterArea::D, 38}, {RegisterArea::D, 39},
{RegisterArea::D, 40}, {RegisterArea::D, 41}}),
"all instruction M/D references must be polled while T/C resources and comments stay offline-only");
"all instruction M/D references must be polled while comments stay metadata-only");
gateway.completeInitialRead();
require(service.initialPlcReadCompleted(),
"service must retain the initial PLC read state");


Ladataan…
Peruuta
Tallenna