Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 16 additions & 9 deletions apps/ied_simulator/test_gui_live_value.py
Original file line number Diff line number Diff line change
Expand Up @@ -263,8 +263,10 @@ def run_direct_normal_control_regression(
f"exit={discovery.returncode} stdout={discovery.stdout!r} stderr={discovery.stderr!r}"
)

# Prove fail-closed Check handling before the accepted command.
rejected = subprocess.run(
# IEDScout accepts ARSAS Check=0xC0 (synchro + interlock requested).
# Keep this compatibility explicit instead of treating valid Check bits as
# an object-value-invalid condition.
checked = subprocess.run(
common
+ [
"--action",
Expand All @@ -273,6 +275,10 @@ def run_direct_normal_control_regression(
"on",
"--value-kind",
"bool",
"--interlock-check",
"on",
"--synchro-check",
"on",
"--arm",
"IEC61850-LAB-CONTROL",
],
Expand All @@ -283,14 +289,15 @@ def run_direct_normal_control_regression(
creationflags=creation_flags(),
)
if (
rejected.returncode != 4
or "accepted=false" not in rejected.stdout
or "mmsFailure=11:object-value-invalid" not in rejected.stdout
or "STATUS_AFTER false" not in rejected.stdout
checked.returncode != 0
or "completion=accepted" not in checked.stdout
or "accepted=true" not in checked.stdout
or "STATUS_AFTER true" not in checked.stdout
or "NO_RETRY_EVIDENCE controlWrites=1" not in checked.stdout
):
raise RuntimeError(
"Direct-Normal fail-closed check-bit regression failed: "
f"exit={rejected.returncode} stdout={rejected.stdout!r} stderr={rejected.stderr!r}"
"Direct-Normal IEDScout Check=0xC0 compatibility regression failed: "
f"exit={checked.returncode} stdout={checked.stdout!r} stderr={checked.stderr!r}"
)

accepted = subprocess.run(
Expand Down Expand Up @@ -333,7 +340,7 @@ def run_direct_normal_control_regression(
"Direct-Normal process status did not persist for a second external association: "
f"exit={status.returncode} stdout={status.stdout!r} stderr={status.stderr!r}"
)
return discovery.stdout.strip() + "\n" + rejected.stdout.strip() + "\n" + accepted.stdout.strip()
return discovery.stdout.strip() + "\n" + checked.stdout.strip() + "\n" + accepted.stdout.strip()


def run_control_action(
Expand Down
11 changes: 11 additions & 0 deletions src/control/mms_control_structure.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,17 @@ using mms::MmsTypeSpecification;
case ControlValueKind::unsigned_integer:
result = std::get<std::uint64_t>(value.value()) != 0U;
break;
case ControlValueKind::double_point: {
const auto point = std::get<DoublePointValue>(value.value());
if (point == DoublePointValue::off) {
result = false;
} else if (point == DoublePointValue::on) {
result = true;
} else {
return failure(MmsControlBuildStatus::value_out_of_range, path);
}
break;
}
default:
return failure(MmsControlBuildStatus::type_mismatch, path);
}
Expand Down
12 changes: 10 additions & 2 deletions src/mms/static_direct_control.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -48,10 +48,18 @@ namespace {
asn1::BerTlvView or_cat;
asn1::BerTlvView or_ident;
if (!read_next(origin.value, offset, or_cat) ||
!context_tag(or_cat, 6, false)) {
!(context_tag(or_cat, 5, false) || context_tag(or_cat, 6, false))) {
return false;
}
const auto category_value = asn1::BerSpanReader::read_unsigned_integer(or_cat);
std::optional<std::uint64_t> category_value;
if (context_tag(or_cat, 5, false)) {
const auto signed_category = asn1::BerSpanReader::read_signed_integer(or_cat);
if (signed_category && *signed_category >= 0) {
category_value = static_cast<std::uint64_t>(*signed_category);
}
} else {
category_value = asn1::BerSpanReader::read_unsigned_integer(or_cat);
}
if (!category_value || *category_value > 0xFFU) {
return false;
}
Expand Down
21 changes: 21 additions & 0 deletions tests/test_control_mms_structure.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,26 @@ void dpc_binding_matches_network_bit_order() {
CHECK(result.value->raw_value()[1] == 0x40U); // DPC Off == binary 01.
}

void dpc_open_close_bind_to_iedscout_boolean_ctlval() {
const auto specification = scalar(MmsTypeKind::boolean, "ctlVal");

auto build = MmsControlStructureBuilder::bind_control_value(
ControlValue::double_point(DoublePointValue::on), specification);
CHECK(build.success());
CHECK(build.value->kind() == MmsDataKind::boolean);
CHECK(std::get<bool>(build.value->value()));

build = MmsControlStructureBuilder::bind_control_value(
ControlValue::double_point(DoublePointValue::off), specification);
CHECK(build.success());
CHECK(build.value->kind() == MmsDataKind::boolean);
CHECK(!std::get<bool>(build.value->value()));

build = MmsControlStructureBuilder::bind_control_value(
ControlValue::double_point(DoublePointValue::intermediate), specification);
CHECK(build.status == MmsControlBuildStatus::value_out_of_range);
}

void operate_matches_csharp_structure_contract_and_golden_wire() {
const auto specification = oper_spec();
auto build = MmsControlStructureBuilder::build_operate(context(), specification);
Expand Down Expand Up @@ -252,6 +272,7 @@ void step_position_and_analogue_structures_follow_live_shape() {
int main() {
try {
dpc_binding_matches_network_bit_order();
dpc_open_close_bind_to_iedscout_boolean_ctlval();
operate_matches_csharp_structure_contract_and_golden_wire();
sbow_uses_same_exact_sequence_contract();
cancel_allows_optional_check_but_not_vendor_guessing();
Expand Down
20 changes: 18 additions & 2 deletions tests/test_mms_static_direct_control.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -41,15 +41,19 @@ struct TestClock final {
const std::uint8_t ctl_num,
const bool test,
const std::uint8_t check_bits = 0U,
const std::uint8_t origin_category = 2U) {
const std::uint8_t origin_category = 2U,
const bool integer_origin_category = false) {
constexpr std::array<std::uint8_t, 3U> origin_id{'H', 'M', 'I'};
constexpr std::array<std::uint8_t, 1U> no_check{0U};
const std::array<std::uint8_t, 1U> check_byte{check_bits};

const auto encoded_origin_category = integer_origin_category
? MmsDataValue::integer(origin_category)
: MmsDataValue::unsigned_integer(origin_category);
auto oper = MmsDataValue::structure({
MmsDataValue::boolean(value),
MmsDataValue::structure({
MmsDataValue::unsigned_integer(origin_category),
encoded_origin_category,
MmsDataValue::octet_string(origin_id),
}),
MmsDataValue::unsigned_integer(ctl_num),
Expand Down Expand Up @@ -136,6 +140,17 @@ void valid_oper_updates_live_state() {
CHECK(state.rejected_operations == 0U);
}

void golden_iedscout_integer_origin_and_checks_decode() {
const auto bytes = make_oper(true, 41U, false, 0xC0U, 2U, true);
MmsStaticDirectBooleanOperate decoded;
CHECK(try_decode_static_direct_boolean_operate(bytes, decoded));
CHECK(decoded.control_value);
CHECK(decoded.origin_category == 2U);
CHECK(decoded.control_number == 41U);
CHECK(decoded.synchro_check);
CHECK(decoded.interlock_check);
}

void test_oper_is_non_mutating() {
MmsStaticDirectBooleanControlState state{};
state.value = 1U;
Expand Down Expand Up @@ -352,6 +367,7 @@ void read_callbacks_match_mms_types() {
int main() {
try {
valid_oper_updates_live_state();
golden_iedscout_integer_origin_and_checks_decode();
test_oper_is_non_mutating();
unsupported_check_bits_fail_closed();
invalid_shape_and_values_are_rejected();
Expand Down
Loading
Loading