Adds the GEM "Documentation" Fundamental capability: the equipment now
self-reports which GEM capabilities it supports, and the host can
discover the DVID namelist with the same shape used for SVIDs.
Catalog (data/messages.yaml -> generated messages.hpp)
S1F19 W header-only Get GEM Compliance Request
S1F20 <L,3 <A SOFTREV>
<A EQPTYP>
<L,a <L,2 <U1 CCODE> <A CDESC>>>>
Get GEM Compliance Data
S1F21 W <L,n <U4 VID>> DVID Namelist Request (n=0 = all)
S1F22 <L,n <L,3 <U4 VID>
<A VNAME>
<A UNITS>>> DVID Namelist Data
Codegen emits CapabilityEntry and GemCompliance structs. S1F22 reuses
S1F12's StatusName struct (same wire shape; dedup avoids redefinition).
Equipment data dictionary (data/equipment.yaml)
device: Adds `equipment_type: "EQUIPMENT"`
for the S1F20 EQPTYP field.
capabilities: New section. List of
- {code, name} (CCODE, CDESC) pairs honestly
reflecting what the codebase
implements: 1, 2, 3, 5, 6, 7, 8,
9, 11, 12, 14 (partial), 15.
dvids: New section, same schema as
svids:. Demo populates two:
- WaferCounter (U4, units wafer)
- ChamberPressure (F4, units Torr)
Loader (src/config/loader.cpp + include/secsgem/config/loader.hpp)
EquipmentDescriptor gains equipment_type and capabilities (vector of
(uint8_t, string) pairs). load_equipment now reads `capabilities:`
into the descriptor and `dvids:` into model.dvids.
Server (apps/secs_server.cpp)
router.on(1, 19) returns S1F20 with desc.software_rev,
desc.equipment_type, and desc.capabilities converted to
vector<CapabilityEntry>.
router.on(1, 21) returns S1F22 built from model.dvids.all().
Client (apps/secs_client.cpp)
Two new demo steps after Request Online and before SVID discovery:
S1F19 -> S1F20: logs SOFTREV, EQPTYP, and every (CCODE, CDESC)
the equipment claims.
S1F21 -> S1F22: logs each DVID with units.
Tests
tests/test_messages.cpp Round-trip S1F19/F20 with a 3-entry
capability list; round-trip S1F22 with two
DVIDs.
tests/test_loader.cpp Asserts equipment_type, the capabilities
list contains CCODE 14 (Spooling), and the
two DVIDs land in model.dvids.
COMPLIANCE.md
"Documentation" Fundamental moves from ⬜ to ✅.
S1F19/F20 + S1F21/F22 rows in the coverage matrix flip to ✅.
The "what would it take" list drops the documentation-messages bullet.
Verified
- Tests: 77 cases / 444 assertions pass.
- Demo: client logs the full capability list received from the
equipment, including CCODE 14 "Spooling (partial; S2F43/F44 +
S6F23/F24)" — the equipment honestly reports its partial
implementation rather than overclaiming.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
+4
-5
@@ -70,7 +70,7 @@ Legend:
|
||||
| Event Notification | ✅ | E30 §6.6 | S6F11/F12 | Equipment-initiated, host-acknowledged. |
|
||||
| On-Line Identification | ✅ | E30 §6.7 | S1F1/F2 | MDLN + SOFTREV. |
|
||||
| Error Messages | 🟡 | E30 §6.9 | S9F* | HSMS Reject.req covers the transport-level case. S9F1/F3/F5/F7/F9/F11/F13 are now in the message catalog and round-trip-tested. `Connection` automatically emits **S9F7** when a peer's primary or reply body fails SECS-II decode (connection stays up; the caller of `send_request` sees `Error::IllegalData`), and **S9F9** when its outgoing request times out at T3 (alongside the existing `Error::Timeout`). **Not wired**: S9F3 (unknown stream) and S9F5 (unknown function) — these belong in the `Router`'s fallback path; today the fallback returns SxF0 only. |
|
||||
| Documentation | ⬜ | E30 §6.10| S1F19/F20 (GEM compliance), S1F21/F22 (data variable namelist) | Not implemented. |
|
||||
| Documentation | ✅ | E30 §6.10| S1F19/F20, S1F21/F22 | Equipment self-reports its GEM-compliance level via S1F20 (SOFTREV, EQPTYP, list of (CCODE, CDESC) per E30 Appendix A) and its DVID namelist via S1F22. Both populated from `data/equipment.yaml`. |
|
||||
| Control (Operator-Initiated) | ✅ | E30 §6.2 | — | `ControlStateMachine::operator_online/offline/local/remote`. |
|
||||
|
||||
---
|
||||
@@ -107,8 +107,8 @@ Legend:
|
||||
| S1F13 / S1F14 | H↔E | ✅ | `messages.hpp` | ✅ in demo |
|
||||
| S1F15 / S1F16 | H→E | ✅ | `messages.hpp` | ✅ in demo |
|
||||
| S1F17 / S1F18 | H→E | ✅ | `messages.hpp` | ✅ in demo |
|
||||
| S1F19 / S1F20 | H→E | ⬜ | — | — |
|
||||
| S1F21 / S1F22 | H→E | ⬜ | — | — |
|
||||
| S1F19 / S1F20 | H→E | ✅ | catalog | ✅ round-trip + demo |
|
||||
| S1F21 / S1F22 | H→E | ✅ | catalog | ✅ round-trip + demo |
|
||||
| S2F13 / S2F14 | H→E | ✅ | `messages.hpp` | ✅ in demo |
|
||||
| S2F15 / S2F16 | H→E | ✅ | `messages.hpp` | ✅ round-trip |
|
||||
| S2F17 / S2F18 | H→E | ✅ | `messages.hpp` | ✅ round-trip |
|
||||
@@ -196,8 +196,7 @@ The honest list, in priority order:
|
||||
2. **Finish S9 wiring**: route Router-level "unknown stream/function" through
|
||||
S9F3/F5, and emit S9F11 (Data Too Long) with the 4-byte length prefix
|
||||
in place of MHEAD when the incoming frame is oversized.
|
||||
3. **Implement Documentation messages** S1F19/F20 (GEM-compliance) and
|
||||
S1F21/F22 (data variable namelist) — needed for E30 conformance.
|
||||
3. *(done in this revision)*: S1F19/F20 + S1F21/F22 — Documentation.
|
||||
4. **Implement EC range validation** in `set_equipment_constant_value`
|
||||
so out-of-range sets return EAC=4 instead of being silently accepted.
|
||||
5. **Implement Limits Monitoring** (S2F45–F48, S6F45/F46) if the
|
||||
|
||||
@@ -147,6 +147,42 @@ int main(int argc, char** argv) {
|
||||
});
|
||||
});
|
||||
|
||||
// 2a. Ask the equipment to self-report its GEM compliance.
|
||||
seq->steps.push_back([conn, logfn, fail](auto next) {
|
||||
conn->send_request(gem::s1f19_get_gem_compliance_request(),
|
||||
[logfn, fail, next](std::error_code ec, const s2::Message& reply) {
|
||||
if (ec) { fail("S1F19", ec); return; }
|
||||
auto parsed = gem::parse_s1f20(reply);
|
||||
if (parsed) {
|
||||
logfn("S1F20 SOFTREV=" + parsed->softrev +
|
||||
" TYPE=" + parsed->equipment_type +
|
||||
" (" + std::to_string(parsed->capabilities.size()) +
|
||||
" capabilities)");
|
||||
for (const auto& c : parsed->capabilities) {
|
||||
logfn(" CCODE " + std::to_string(c.ccode) + " " + c.cdesc);
|
||||
}
|
||||
}
|
||||
next();
|
||||
});
|
||||
});
|
||||
|
||||
// 2b. Discover DVIDs.
|
||||
seq->steps.push_back([conn, logfn, fail](auto next) {
|
||||
conn->send_request(gem::s1f21_data_variable_namelist_request({}),
|
||||
[logfn, fail, next](std::error_code ec, const s2::Message& reply) {
|
||||
if (ec) { fail("S1F21", ec); return; }
|
||||
auto parsed = gem::parse_s1f22(reply);
|
||||
if (parsed) {
|
||||
logfn("S1F22 (" + std::to_string(parsed->size()) + " DVIDs)");
|
||||
for (const auto& dn : *parsed) {
|
||||
logfn(" DVID " + std::to_string(dn.id) + " " + dn.name +
|
||||
(dn.units.empty() ? "" : " [" + dn.units + "]"));
|
||||
}
|
||||
}
|
||||
next();
|
||||
});
|
||||
});
|
||||
|
||||
// 3. Discover SVIDs.
|
||||
seq->steps.push_back([conn, logfn, fail, svids](auto next) {
|
||||
conn->send_request(gem::s1f11_status_namelist_request({}),
|
||||
|
||||
@@ -178,6 +178,19 @@ int main(int argc, char** argv) {
|
||||
logfn("S1F17 -> S1F18 ONLACK=" + std::to_string(static_cast<int>(ack)));
|
||||
return gem::s1f18_online_ack(ack);
|
||||
});
|
||||
router.on(1, 19, [desc, logfn](const s2::Message&) {
|
||||
std::vector<gem::CapabilityEntry> caps;
|
||||
for (const auto& c : desc.capabilities) caps.push_back({c.first, c.second});
|
||||
logfn("S1F19 -> S1F20 (" + std::to_string(caps.size()) + " capabilities)");
|
||||
return gem::s1f20_get_gem_compliance_data(desc.software_rev, desc.equipment_type, caps);
|
||||
});
|
||||
router.on(1, 21, [model, logfn](const s2::Message&) {
|
||||
std::vector<gem::StatusName> rows;
|
||||
for (const auto& dv : model->dvids.all())
|
||||
rows.push_back({dv.id, dv.name, dv.units});
|
||||
logfn("S1F21 -> S1F22 (" + std::to_string(rows.size()) + " DVIDs)");
|
||||
return gem::s1f22_data_variable_namelist_data(rows);
|
||||
});
|
||||
|
||||
router.on(2, 13, [model, logfn](const s2::Message& msg) -> std::optional<s2::Message> {
|
||||
auto ids = gem::parse_u4_list_body(msg);
|
||||
|
||||
@@ -7,6 +7,25 @@ device:
|
||||
id: 0
|
||||
model_name: "SECSGEM-SIM"
|
||||
software_rev: "0.1.0"
|
||||
equipment_type: "EQUIPMENT" # S1F20 EQPTYP string
|
||||
|
||||
# Reported on S1F19 -> S1F20 (Get GEM Compliance). CCODE values follow
|
||||
# E30 Appendix A; CDESC is implementer-defined. Keep this list honest:
|
||||
# it should reflect what the equipment actually implements (see
|
||||
# COMPLIANCE.md for the audit).
|
||||
capabilities:
|
||||
- {code: 1, name: "Establish Communications"}
|
||||
- {code: 2, name: "Dynamic Event Report Configuration"}
|
||||
- {code: 3, name: "Variable Data Collection"}
|
||||
- {code: 5, name: "Status Data Collection"}
|
||||
- {code: 6, name: "Alarm Management"}
|
||||
- {code: 7, name: "Remote Control"}
|
||||
- {code: 8, name: "Equipment Constants"}
|
||||
- {code: 9, name: "Process Program Management"}
|
||||
- {code: 11, name: "Equipment Terminal Services"}
|
||||
- {code: 12, name: "Clock"}
|
||||
- {code: 14, name: "Spooling (partial; S2F43/F44 + S6F23/F24)"}
|
||||
- {code: 15, name: "Control (operator initiated)"}
|
||||
|
||||
# Reported on S1F3 (values) and S1F11 (namelist). `value` is the initial.
|
||||
# `type` controls the SECS-II Item format.
|
||||
@@ -15,6 +34,11 @@ svids:
|
||||
- {id: 2, name: Clock, units: "", type: ASCII, value: ""}
|
||||
- {id: 3, name: EventsEnabled, units: "", type: BOOLEAN, value: true}
|
||||
|
||||
# Reported on S1F21/F22 (DVID namelist). Same shape as SVIDs.
|
||||
dvids:
|
||||
- {id: 100, name: WaferCounter, units: "wafer", type: U4, value: 0}
|
||||
- {id: 101, name: ChamberPressure, units: "Torr", type: F4, value: 0.000001}
|
||||
|
||||
# Reported on S2F13 (values) / S2F29 (namelist) / S2F15 (set).
|
||||
ecids:
|
||||
- {id: 10, name: TimeFormat, units: "code", type: U4, value: 1, min: "0", max: "1"}
|
||||
|
||||
@@ -130,6 +130,67 @@ messages:
|
||||
builder: s1f18_online_ack
|
||||
body: {kind: scalar, item_type: BINARY_BYTE, enum: OnlineAck, param: ack}
|
||||
|
||||
# S1F19 / S1F20 — Get GEM Compliance. Host asks the equipment which GEM
|
||||
# capabilities it supports; equipment replies with its SOFTREV, an
|
||||
# implementer-defined equipment-type string, and a (CCODE, CDESC) list
|
||||
# per E30 Appendix A. Closes the "Documentation" Fundamental capability
|
||||
# by letting the equipment self-report its own COMPLIANCE.md status.
|
||||
- id: S1F19
|
||||
stream: 1
|
||||
function: 19
|
||||
w: true
|
||||
builder: s1f19_get_gem_compliance_request
|
||||
|
||||
- id: S1F20
|
||||
stream: 1
|
||||
function: 20
|
||||
builder: s1f20_get_gem_compliance_data
|
||||
parser: parse_s1f20
|
||||
body:
|
||||
kind: list
|
||||
struct_name: GemCompliance
|
||||
fields:
|
||||
- {name: softrev, shape: {kind: scalar, item_type: ASCII}}
|
||||
- {name: equipment_type, shape: {kind: scalar, item_type: ASCII}}
|
||||
- name: capabilities
|
||||
shape:
|
||||
kind: list_of
|
||||
element:
|
||||
kind: list
|
||||
struct_name: CapabilityEntry
|
||||
fields:
|
||||
- {name: ccode, shape: {kind: scalar, item_type: U1}}
|
||||
- {name: cdesc, shape: {kind: scalar, item_type: ASCII}}
|
||||
|
||||
# S1F21 / S1F22 — Data Variable Namelist (same wire shape as S1F11/F12
|
||||
# but for DVIDs). Reuses the StatusName struct emitted by S1F12.
|
||||
- id: S1F21
|
||||
stream: 1
|
||||
function: 21
|
||||
w: true
|
||||
builder: s1f21_data_variable_namelist_request
|
||||
parser: parse_s1f21
|
||||
body:
|
||||
kind: list_of
|
||||
element: {kind: scalar, item_type: U4}
|
||||
name: vids
|
||||
|
||||
- id: S1F22
|
||||
stream: 1
|
||||
function: 22
|
||||
builder: s1f22_data_variable_namelist_data
|
||||
parser: parse_s1f22
|
||||
body:
|
||||
kind: list_of
|
||||
name: items
|
||||
element:
|
||||
kind: list
|
||||
struct_name: StatusName
|
||||
fields:
|
||||
- {name: id, shape: {kind: scalar, item_type: U4}}
|
||||
- {name: name, shape: {kind: scalar, item_type: ASCII}}
|
||||
- {name: units, shape: {kind: scalar, item_type: ASCII}}
|
||||
|
||||
# =====================================================================
|
||||
# S2 — Equipment control + reports
|
||||
# =====================================================================
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
#include <optional>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "secsgem/gem/control_state.hpp"
|
||||
#include "secsgem/gem/data_model.hpp"
|
||||
@@ -30,6 +32,8 @@ struct EquipmentDescriptor {
|
||||
uint16_t device_id = 0;
|
||||
std::string model_name;
|
||||
std::string software_rev;
|
||||
std::string equipment_type; // S1F20 EQPTYP
|
||||
std::vector<std::pair<uint8_t, std::string>> capabilities; // (CCODE, CDESC)
|
||||
std::optional<uint32_t> emit_on_control_change;
|
||||
};
|
||||
|
||||
|
||||
@@ -140,11 +140,20 @@ EquipmentDescriptor load_equipment(const std::string& path, gem::EquipmentDataMo
|
||||
desc.device_id = static_cast<uint16_t>(d["id"].as<int>());
|
||||
desc.model_name = d["model_name"].as<std::string>();
|
||||
desc.software_rev = d["software_rev"].as<std::string>();
|
||||
desc.equipment_type =
|
||||
d["equipment_type"] ? d["equipment_type"].as<std::string>() : "EQUIPMENT";
|
||||
}
|
||||
if (auto e = root["emit_on_control_change"]) {
|
||||
if (!e.IsNull()) desc.emit_on_control_change = static_cast<uint32_t>(e.as<int>());
|
||||
}
|
||||
|
||||
if (auto caps = root["capabilities"]) {
|
||||
for (const auto& c : caps) {
|
||||
desc.capabilities.emplace_back(static_cast<uint8_t>(c["code"].as<int>()),
|
||||
c["name"].as<std::string>());
|
||||
}
|
||||
}
|
||||
|
||||
if (auto svids = root["svids"]) {
|
||||
for (const auto& sv : svids) {
|
||||
model.svids.add({
|
||||
@@ -156,6 +165,17 @@ EquipmentDescriptor load_equipment(const std::string& path, gem::EquipmentDataMo
|
||||
}
|
||||
}
|
||||
|
||||
if (auto dvids = root["dvids"]) {
|
||||
for (const auto& dv : dvids) {
|
||||
model.dvids.add({
|
||||
static_cast<uint32_t>(dv["id"].as<int>()),
|
||||
dv["name"].as<std::string>(),
|
||||
dv["units"] ? dv["units"].as<std::string>() : "",
|
||||
make_item(dv["type"], dv["value"], path + " dvid"),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (auto ecids = root["ecids"]) {
|
||||
for (const auto& ec : ecids) {
|
||||
const auto value = make_item(ec["type"], ec["value"], path + " ecid");
|
||||
|
||||
@@ -78,6 +78,20 @@ TEST_CASE("equipment.yaml populates SVIDs, ECIDs, CEIDs, alarms, recipes, comman
|
||||
CHECK(*on.force_spool == true);
|
||||
REQUIRE(off.force_spool.has_value());
|
||||
CHECK(*off.force_spool == false);
|
||||
|
||||
// Capabilities + equipment_type loaded for S1F20.
|
||||
CHECK(desc.equipment_type == "EQUIPMENT");
|
||||
REQUIRE(desc.capabilities.size() >= 5);
|
||||
bool saw_spooling = false;
|
||||
for (const auto& [code, name] : desc.capabilities) {
|
||||
if (code == 14) saw_spooling = true;
|
||||
}
|
||||
CHECK(saw_spooling);
|
||||
|
||||
// DVIDs loaded for S1F22.
|
||||
CHECK(m.dvids.all().size() == 2);
|
||||
CHECK(m.dvids.get(100)->name == "WaferCounter");
|
||||
CHECK(m.dvids.get(101)->units == "Torr");
|
||||
}
|
||||
|
||||
TEST_CASE("loader surfaces YAML errors with file path") {
|
||||
|
||||
@@ -151,6 +151,47 @@ TEST_CASE("S9F13 conversation timeout carries MEXP + EDID") {
|
||||
CHECK(parsed->edid == "EQUIP-01");
|
||||
}
|
||||
|
||||
// ---- S1F19 / S1F20 Get GEM Compliance ----------------------------------
|
||||
|
||||
TEST_CASE("S1F19 is header-only and S1F20 round-trips capability list") {
|
||||
auto req = s1f19_get_gem_compliance_request();
|
||||
CHECK(req.stream == 1);
|
||||
CHECK(req.function == 19);
|
||||
CHECK(req.reply_expected);
|
||||
CHECK_FALSE(req.body.has_value());
|
||||
|
||||
std::vector<CapabilityEntry> caps = {
|
||||
{1, "Establish Communications"},
|
||||
{5, "Status Data Collection"},
|
||||
{14, "Spooling"},
|
||||
};
|
||||
auto m = s1f20_get_gem_compliance_data("0.1.0", "EQUIPMENT", caps);
|
||||
auto parsed = parse_s1f20(m);
|
||||
REQUIRE(parsed.has_value());
|
||||
CHECK(parsed->softrev == "0.1.0");
|
||||
CHECK(parsed->equipment_type == "EQUIPMENT");
|
||||
REQUIRE(parsed->capabilities.size() == 3);
|
||||
CHECK(parsed->capabilities[0].ccode == 1);
|
||||
CHECK(parsed->capabilities[0].cdesc == "Establish Communications");
|
||||
CHECK(parsed->capabilities[2].ccode == 14);
|
||||
CHECK(parsed->capabilities[2].cdesc == "Spooling");
|
||||
}
|
||||
|
||||
// ---- S1F21 / S1F22 DVID Namelist ---------------------------------------
|
||||
|
||||
TEST_CASE("S1F22 round-trip preserves DVIDs (shape matches S1F12)") {
|
||||
std::vector<StatusName> dvs = {{100, "WaferCounter", "wafer"},
|
||||
{101, "ChamberPressure", "Torr"}};
|
||||
auto m = s1f22_data_variable_namelist_data(dvs);
|
||||
auto parsed = parse_s1f22(m);
|
||||
REQUIRE(parsed.has_value());
|
||||
REQUIRE(parsed->size() == 2);
|
||||
CHECK((*parsed)[0].id == 100);
|
||||
CHECK((*parsed)[0].name == "WaferCounter");
|
||||
CHECK((*parsed)[0].units == "wafer");
|
||||
CHECK((*parsed)[1].id == 101);
|
||||
}
|
||||
|
||||
// ---- Extended GEM message round-trips ------------------------------------
|
||||
|
||||
TEST_CASE("S2F33 define-report round-trip") {
|
||||
|
||||
Reference in New Issue
Block a user