e42: enhanced (formatted) process programs S7F23-F26
E42 was an explicit out-of-scope item in the prior COMPLIANCE.md.
This commit closes it.
Wire messages added via the catalog:
S7F23 Formatted PP Send (H↔E, W=1)
S7F24 Formatted PP Ack (ProcessProgramAck)
S7F25 Formatted PP Request (PPID, W=1)
S7F26 Formatted PP Data (E→H, no reply)
Body shape: <L,4 PPID MDLN SOFTREV <L,n <L,2 CCODE <L,m <L,2
PNAME PVAL>>>>>. PVAL is declared ITEM so any SECS-II Item type
round-trips — proven by a test that mixes ASCII, BOOLEAN, U4, F8,
Binary, and nested List values in one step.
RecipeStore extension:
add_formatted(ppid, FormattedRecipe{mdln, softrev, steps})
get_formatted(ppid) -> optional<FormattedRecipe>
has_formatted(ppid) -> bool
Formatted + opaque views live alongside each other: a PPID can carry
both, size() counts unique PPIDs. remove() kills both views.
Six new tests cover wire round-trip per function, every
ProcessProgramAck code, ITEM passthrough, and the store's dual-view
semantics.
COMPLIANCE.md updated: E30 §6.17 row mentions S7F23-F26, S5 message
table grows two rows, §8 "out of scope" entry for E42 removed.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,167 @@
|
||||
// E42 Enhanced (formatted) Process Programs — S7F23/F24/F25/F26.
|
||||
//
|
||||
// Round-trips the wire shape through the generated builders/parsers,
|
||||
// confirms RecipeStore tracks formatted PPs independently from
|
||||
// opaque ones, and confirms the value parameter can carry any
|
||||
// SECS-II Item type via the ITEM passthrough.
|
||||
|
||||
#include <doctest/doctest.h>
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "secsgem/gem/messages.hpp"
|
||||
#include "secsgem/gem/store/recipes.hpp"
|
||||
#include "secsgem/secs2/item.hpp"
|
||||
|
||||
using namespace secsgem;
|
||||
using namespace secsgem::gem;
|
||||
namespace s2 = secsgem::secs2;
|
||||
|
||||
namespace {
|
||||
|
||||
FormattedStep heat_step() {
|
||||
FormattedStep s;
|
||||
s.ccode = "HEAT";
|
||||
s.params.push_back({"target_c", s2::Item::f4(350.5f)});
|
||||
s.params.push_back({"ramp_rate", s2::Item::f4(2.5f)});
|
||||
return s;
|
||||
}
|
||||
FormattedStep gas_flow_step() {
|
||||
FormattedStep s;
|
||||
s.ccode = "GAS_FLOW";
|
||||
s.params.push_back({"chamber", s2::Item::ascii("A")});
|
||||
s.params.push_back({"sccm", s2::Item::u4(uint32_t{500})});
|
||||
return s;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST_CASE("S7F23 formatted PP send: round-trip preserves ppid/mdln/softrev/steps") {
|
||||
FormattedProcessProgram fpp;
|
||||
fpp.ppid = "RCP-FMT-1";
|
||||
fpp.mdln = "ACME-3000";
|
||||
fpp.softrev = "1.4.2";
|
||||
fpp.steps = {heat_step(), gas_flow_step()};
|
||||
|
||||
auto msg = s7f23_formatted_pp_send(fpp);
|
||||
CHECK(msg.stream == 7);
|
||||
CHECK(msg.function == 23);
|
||||
CHECK(msg.reply_expected);
|
||||
|
||||
auto parsed = parse_s7f23(msg);
|
||||
REQUIRE(parsed.has_value());
|
||||
CHECK(parsed->ppid == "RCP-FMT-1");
|
||||
CHECK(parsed->mdln == "ACME-3000");
|
||||
CHECK(parsed->softrev == "1.4.2");
|
||||
REQUIRE(parsed->steps.size() == 2);
|
||||
CHECK(parsed->steps[0].ccode == "HEAT");
|
||||
REQUIRE(parsed->steps[0].params.size() == 2);
|
||||
CHECK(parsed->steps[0].params[0].name == "target_c");
|
||||
CHECK(parsed->steps[0].params[0].value.format() == s2::Format::F4);
|
||||
CHECK(parsed->steps[1].ccode == "GAS_FLOW");
|
||||
CHECK(parsed->steps[1].params[1].name == "sccm");
|
||||
CHECK(parsed->steps[1].params[1].value.format() == s2::Format::U4);
|
||||
}
|
||||
|
||||
TEST_CASE("S7F24 ack: round-trip every ProcessProgramAck code") {
|
||||
for (auto ack : {ProcessProgramAck::Accept,
|
||||
ProcessProgramAck::PermissionNotGranted,
|
||||
ProcessProgramAck::PpidNotFound,
|
||||
ProcessProgramAck::ModeUnsupported}) {
|
||||
auto msg = s7f24_formatted_pp_ack(ack);
|
||||
CHECK(msg.stream == 7);
|
||||
CHECK(msg.function == 24);
|
||||
REQUIRE(msg.body.has_value());
|
||||
CHECK(msg.body->as_bytes()[0] == static_cast<uint8_t>(ack));
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("S7F25 formatted PP request: round-trip PPID") {
|
||||
auto msg = s7f25_formatted_pp_request("RCP-FMT-1");
|
||||
CHECK(msg.stream == 7);
|
||||
CHECK(msg.function == 25);
|
||||
CHECK(msg.reply_expected);
|
||||
auto parsed = parse_s7f25(msg);
|
||||
REQUIRE(parsed.has_value());
|
||||
CHECK(*parsed == "RCP-FMT-1");
|
||||
}
|
||||
|
||||
TEST_CASE("S7F26 formatted PP data: round-trip same shape as S7F23") {
|
||||
FormattedProcessProgram fpp;
|
||||
fpp.ppid = "RCP-FMT-2";
|
||||
fpp.mdln = "ACME-3000";
|
||||
fpp.softrev = "1.4.2";
|
||||
fpp.steps = {heat_step()};
|
||||
|
||||
auto msg = s7f26_formatted_pp_data(fpp);
|
||||
CHECK(msg.stream == 7);
|
||||
CHECK(msg.function == 26);
|
||||
CHECK_FALSE(msg.reply_expected);
|
||||
auto parsed = parse_s7f26(msg);
|
||||
REQUIRE(parsed.has_value());
|
||||
CHECK(parsed->ppid == "RCP-FMT-2");
|
||||
REQUIRE(parsed->steps.size() == 1);
|
||||
CHECK(parsed->steps[0].ccode == "HEAT");
|
||||
}
|
||||
|
||||
TEST_CASE("RecipeStore: formatted recipes live alongside opaque text recipes") {
|
||||
RecipeStore r;
|
||||
r.add("RCP-OPAQUE", "STEP HEAT 350C\nEND");
|
||||
r.add_formatted("RCP-FMT", {"ACME-3000", "1.4.2",
|
||||
{{"HEAT", {{"target_c", s2::Item::f4(350.0f)}}}}});
|
||||
|
||||
CHECK(r.size() == 2);
|
||||
CHECK(r.get("RCP-OPAQUE").has_value());
|
||||
CHECK_FALSE(r.get("RCP-FMT").has_value()); // not registered as opaque
|
||||
CHECK(r.has_formatted("RCP-FMT"));
|
||||
CHECK_FALSE(r.has_formatted("RCP-OPAQUE"));
|
||||
|
||||
auto fmt = r.get_formatted("RCP-FMT");
|
||||
REQUIRE(fmt.has_value());
|
||||
CHECK(fmt->mdln == "ACME-3000");
|
||||
REQUIRE(fmt->steps.size() == 1);
|
||||
CHECK(fmt->steps[0].ccode == "HEAT");
|
||||
|
||||
// PPID collision: same PPID can have both forms (different views).
|
||||
r.add("RCP-FMT", "OPAQUE_DUMP_OF_RCP_FMT");
|
||||
CHECK(r.size() == 2);
|
||||
CHECK(r.get("RCP-FMT").value() == "OPAQUE_DUMP_OF_RCP_FMT");
|
||||
CHECK(r.has_formatted("RCP-FMT"));
|
||||
|
||||
// Remove kills both views in one call.
|
||||
CHECK(r.remove("RCP-FMT") == ProcessProgramAck::Accept);
|
||||
CHECK_FALSE(r.get("RCP-FMT").has_value());
|
||||
CHECK_FALSE(r.has_formatted("RCP-FMT"));
|
||||
}
|
||||
|
||||
TEST_CASE("S7F23: ITEM passthrough carries any SECS-II type") {
|
||||
// The `value` parameter is declared `ITEM` in the schema — confirms
|
||||
// each major Item kind survives the round-trip.
|
||||
FormattedProcessProgram fpp;
|
||||
fpp.ppid = "RCP-MIXED";
|
||||
fpp.mdln = "M";
|
||||
fpp.softrev = "1";
|
||||
FormattedStep step;
|
||||
step.ccode = "MIXED";
|
||||
step.params = {
|
||||
{"text", s2::Item::ascii("HELLO")},
|
||||
{"flag", s2::Item::boolean(true)},
|
||||
{"counter", s2::Item::u4(uint32_t{42})},
|
||||
{"value", s2::Item::f8(3.14159)},
|
||||
{"binary", s2::Item::binary({0x01, 0xFF})},
|
||||
{"nested", s2::Item::list({s2::Item::ascii("a"),
|
||||
s2::Item::u2(uint16_t{7})})},
|
||||
};
|
||||
fpp.steps = {step};
|
||||
|
||||
auto msg = s7f23_formatted_pp_send(fpp);
|
||||
auto parsed = parse_s7f23(msg);
|
||||
REQUIRE(parsed.has_value());
|
||||
REQUIRE(parsed->steps.size() == 1);
|
||||
REQUIRE(parsed->steps[0].params.size() == 6);
|
||||
CHECK(parsed->steps[0].params[0].value.as_ascii() == "HELLO");
|
||||
CHECK(parsed->steps[0].params[2].value.format() == s2::Format::U4);
|
||||
CHECK(parsed->steps[0].params[3].value.format() == s2::Format::F8);
|
||||
CHECK(parsed->steps[0].params[5].value.is_list());
|
||||
}
|
||||
Reference in New Issue
Block a user