diff --git a/apps/secs_server.cpp b/apps/secs_server.cpp index e515a31..7541b51 100644 --- a/apps/secs_server.cpp +++ b/apps/secs_server.cpp @@ -824,6 +824,51 @@ int main(int argc, char** argv) { return gem::s7f20_current_eppd_data(list); }); + // ---- E39 generic ObjectService ---------------------------------------- + // S14F1 GetAttr / S14F3 SetAttr against the CemObjectStore. OBJTYPE + // is validated against the stored object's type name (case-sensitive). + router.on(14, 1, [model, logfn](const s2::Message& msg) { + auto req = gem::parse_s14f1(msg); + if (!req) return gem::s14f2_get_attr_data({}, gem::ObjectAck::Error); + auto* obj = model->cem.get(req->objspec); + if (!obj) + return gem::s14f2_get_attr_data({}, gem::ObjectAck::Denied_UnknownObject); + if (gem::cem_object_type_name(obj->objtype) != req->objtype) + return gem::s14f2_get_attr_data({}, gem::ObjectAck::Denied_InvalidAttribute); + std::vector attrs; + attrs.reserve(req->attrids.size()); + for (const auto& id : req->attrids) { + auto v = model->cem.get_attr(req->objspec, id); + attrs.push_back({id, v.value_or(s2::Item::ascii(""))}); + } + logfn("S14F1 " + req->objspec + " (" + + std::to_string(req->attrids.size()) + " attrs) -> S14F2"); + return gem::s14f2_get_attr_data(attrs, gem::ObjectAck::Success); + }); + router.on(14, 3, [model, logfn](const s2::Message& msg) { + auto req = gem::parse_s14f3(msg); + if (!req) + return gem::s14f4_set_attr_ack({}, gem::ObjectAck::Error); + auto* obj = model->cem.get(req->objspec); + if (!obj) + return gem::s14f4_set_attr_ack({}, gem::ObjectAck::Denied_UnknownObject); + if (gem::cem_object_type_name(obj->objtype) != req->objtype) + return gem::s14f4_set_attr_ack({}, gem::ObjectAck::Denied_InvalidAttribute); + for (const auto& a : req->attrs) { + model->cem.set_attr(req->objspec, a.attrid, a.value); + } + // Reply echoes back the now-stored values. + std::vector reply; + reply.reserve(req->attrs.size()); + for (const auto& a : req->attrs) { + auto v = model->cem.get_attr(req->objspec, a.attrid); + reply.push_back({a.attrid, v.value_or(s2::Item::ascii(""))}); + } + logfn("S14F3 " + req->objspec + " (" + + std::to_string(req->attrs.size()) + " attrs) -> S14F4"); + return gem::s14f4_set_attr_ack(reply, gem::ObjectAck::Success); + }); + // ---- E40 / E94 ------------------------------------------------------- router.on(14, 9, [model, logfn, run_cj_lifecycle](const s2::Message& msg) { (void)run_cj_lifecycle; diff --git a/data/messages.yaml b/data/messages.yaml index 84256a0..6124800 100644 --- a/data/messages.yaml +++ b/data/messages.yaml @@ -1407,6 +1407,93 @@ messages: # generic ATTRIBUTES list specialized. # ===================================================================== + # S14F1 / F2 — GetAttr (generic). E39/E14 ObjectService form: host + # requests one or more attribute values from a named object. OBJSPEC + # is the dotted path through the CEM tree, OBJTYPE is the expected + # type (validated by the equipment), ATTRID is a list of names. + # Reply carries the values in the same order, plus an OBJACK code. + - id: S14F1 + stream: 14 + function: 1 + w: true + builder: s14f1_get_attr + parser: parse_s14f1 + body: + kind: list + struct_name: GetAttrRequest + fields: + - {name: objspec, shape: {kind: scalar, item_type: ASCII}} + - {name: objtype, shape: {kind: scalar, item_type: ASCII}} + - name: attrids + shape: {kind: list_of, element: {kind: scalar, item_type: ASCII}} + + - id: S14F2 + stream: 14 + function: 2 + builder: s14f2_get_attr_data + parser: parse_s14f2 + body: + kind: list + struct_name: GetAttrReply + fields: + - name: attrs + shape: + kind: list_of + element: + kind: list + struct_name: AttrValue + fields: + - {name: attrid, shape: {kind: scalar, item_type: ASCII}} + - {name: value, shape: {kind: scalar, item_type: ITEM}} + - {name: ack, shape: {kind: scalar, item_type: BINARY_BYTE, enum: ObjectAck}} + + # S14F3 / F4 — SetAttr (generic). Same OBJSPEC/OBJTYPE addressing as + # GetAttr; body carries (ATTRID, VALUE) pairs the equipment applies + # in order. Reply echoes whatever the equipment now has. + - id: S14F3 + stream: 14 + function: 3 + w: true + builder: s14f3_set_attr + parser: parse_s14f3 + body: + kind: list + struct_name: SetAttrRequest + fields: + - {name: objspec, shape: {kind: scalar, item_type: ASCII}} + - {name: objtype, shape: {kind: scalar, item_type: ASCII}} + - name: attrs + shape: + kind: list_of + element: + kind: list + struct_name: AttrValue + external_struct: true + fields: + - {name: attrid, shape: {kind: scalar, item_type: ASCII}} + - {name: value, shape: {kind: scalar, item_type: ITEM}} + + - id: S14F4 + stream: 14 + function: 4 + builder: s14f4_set_attr_ack + parser: parse_s14f4 + body: + kind: list + struct_name: SetAttrReply + fields: + - name: attrs + shape: + kind: list_of + element: + kind: list + struct_name: AttrValue + external_struct: true + fields: + - {name: attrid, shape: {kind: scalar, item_type: ASCII}} + - {name: value, shape: {kind: scalar, item_type: ITEM}} + - {name: ack, shape: {kind: scalar, item_type: BINARY_BYTE, enum: ObjectAck}} + # S14F9 / F10 — CreateObject (CJ). Host asks the equipment to bring # a new ControlJob into existence and registers the list of PJs it # contains; the CJ starts in CJ-QUEUED. diff --git a/tests/test_messages.cpp b/tests/test_messages.cpp index e7a7bb3..92d8b50 100644 --- a/tests/test_messages.cpp +++ b/tests/test_messages.cpp @@ -584,6 +584,43 @@ TEST_CASE("S7F19 / S7F20 EPPD list") { CHECK(*parsed == std::vector{"RECIPE-A", "RECIPE-B"}); } +TEST_CASE("S14F1 / S14F2 GetAttr round-trip") { + auto m = s14f1_get_attr("EQ-1", "Equipment", {"MDLN", "SOFTREV"}); + CHECK(m.stream == 14); + CHECK(m.function == 1); + auto parsed = parse_s14f1(m); + REQUIRE(parsed.has_value()); + CHECK(parsed->objspec == "EQ-1"); + CHECK(parsed->objtype == "Equipment"); + REQUIRE(parsed->attrids.size() == 2); + + std::vector reply{ + {"MDLN", s2::Item::ascii("DEMO")}, + {"SOFTREV", s2::Item::ascii("1.0")}, + }; + auto r = s14f2_get_attr_data(reply, ObjectAck::Success); + auto pr = parse_s14f2(r); + REQUIRE(pr.has_value()); + CHECK(pr->ack == ObjectAck::Success); + REQUIRE(pr->attrs.size() == 2); + CHECK(pr->attrs[0].attrid == "MDLN"); +} + +TEST_CASE("S14F3 / S14F4 SetAttr round-trip") { + std::vector attrs{{"PORT_STATE", s2::Item::ascii("OPEN")}}; + auto m = s14f3_set_attr("LP-1", "MaterialLocation", attrs); + auto parsed = parse_s14f3(m); + REQUIRE(parsed.has_value()); + CHECK(parsed->objspec == "LP-1"); + REQUIRE(parsed->attrs.size() == 1); + CHECK(parsed->attrs[0].attrid == "PORT_STATE"); + + auto r = s14f4_set_attr_ack(attrs, ObjectAck::Success); + auto pr = parse_s14f4(r); + REQUIRE(pr.has_value()); + CHECK(pr->ack == ObjectAck::Success); +} + TEST_CASE("S14F9 / S14F10 CreateControlJob round-trip") { auto req = s14f9_create_control_job("CJ-1", {"PJ-1", "PJ-2"}); CHECK(req.stream == 14);