N: E39 generic ObjectService — S14F1/F2 GetAttr + S14F3/F4 SetAttr
The catalog had S14F9/F10 / F11/F12 specialized for E94 ControlJob;
this commit adds the generic E14 attribute access pair, the most-
queried half of the E39 surface area, backed by the CemObjectStore.
S14F1 / F2 GetAttr — OBJSPEC + OBJTYPE + ATTRID list ->
(ATTRID, VALUE) pairs + OBJACK
S14F3 / F4 SetAttr — same addressing, applies ATTRID/VALUE pairs,
reply echoes stored values + OBJACK
Server dispatches both into the CemObjectStore added in tranche G.
OBJTYPE validation is case-sensitive against the CemObjectType name
(Equipment / Subsystem / IODevice / Module / MaterialLocation).
Unknown objects return Denied_UnknownObject; type mismatches return
Denied_InvalidAttribute.
The shared AttrValue struct is declared external_struct: true on
F3/F4 so both directions share the same C++ type.
Two round-trip tests cover both pairs.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -824,6 +824,51 @@ int main(int argc, char** argv) {
|
|||||||
return gem::s7f20_current_eppd_data(list);
|
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<gem::AttrValue> 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<gem::AttrValue> 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 -------------------------------------------------------
|
// ---- E40 / E94 -------------------------------------------------------
|
||||||
router.on(14, 9, [model, logfn, run_cj_lifecycle](const s2::Message& msg) {
|
router.on(14, 9, [model, logfn, run_cj_lifecycle](const s2::Message& msg) {
|
||||||
(void)run_cj_lifecycle;
|
(void)run_cj_lifecycle;
|
||||||
|
|||||||
@@ -1407,6 +1407,93 @@ messages:
|
|||||||
# generic ATTRIBUTES list specialized.
|
# 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
|
# S14F9 / F10 — CreateObject (CJ). Host asks the equipment to bring
|
||||||
# a new ControlJob into existence and registers the list of PJs it
|
# a new ControlJob into existence and registers the list of PJs it
|
||||||
# contains; the CJ starts in CJ-QUEUED.
|
# contains; the CJ starts in CJ-QUEUED.
|
||||||
|
|||||||
@@ -584,6 +584,43 @@ TEST_CASE("S7F19 / S7F20 EPPD list") {
|
|||||||
CHECK(*parsed == std::vector<std::string>{"RECIPE-A", "RECIPE-B"});
|
CHECK(*parsed == std::vector<std::string>{"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<AttrValue> 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<AttrValue> 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") {
|
TEST_CASE("S14F9 / S14F10 CreateControlJob round-trip") {
|
||||||
auto req = s14f9_create_control_job("CJ-1", {"PJ-1", "PJ-2"});
|
auto req = s14f9_create_control_job("CJ-1", {"PJ-1", "PJ-2"});
|
||||||
CHECK(req.stream == 14);
|
CHECK(req.stream == 14);
|
||||||
|
|||||||
Reference in New Issue
Block a user