// 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 #include #include #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(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()); }