From d159bd39d7cecf9b96e60d5fa60c86a5e7dedc60 Mon Sep 17 00:00:00 2001 From: Raphael Maenle Date: Mon, 8 Jun 2026 03:35:05 +0200 Subject: [PATCH] G: CemObjectStore (E120 Common Equipment Model) Hierarchical object tree for equipment self-description. Each object carries a CemObjectType (Equipment / Subsystem / IODevice / Module / MaterialLocation / Other), an optional parent_objid, and a flat attribute map keyed by name (the wire shape S14F1 / F3 returns). Operations covered: add(CemObject) - dedup'd, validates parent exists get / has - lookup by objid get_attr / set_attr - E14 GetAttr / SetAttr semantics children(parent) - tree traversal; empty parent = roots The flat-map representation matches how E14 ObjectService traffic addresses nodes (by OBJSPEC string). Wiring S14F1/F2 GetAttr and S14F3/F4 SetAttr to this store is a downstream commit; the data model is what was missing. Joins EquipmentDataModel alongside the other top-level stores. Three test cases cover hierarchical add+dedup, children() traversal, and get/set/missing attribute semantics. Co-Authored-By: Claude Opus 4.7 --- CMakeLists.txt | 2 + include/secsgem/gem/data_model.hpp | 2 + include/secsgem/gem/module_state.hpp | 62 +++++++++++++ include/secsgem/gem/store/cem_objects.hpp | 105 ++++++++++++++++++++++ src/gem/cem_objects.cpp | 17 ++++ tests/test_cem_objects.cpp | 46 ++++++++++ 6 files changed, 234 insertions(+) create mode 100644 include/secsgem/gem/module_state.hpp create mode 100644 include/secsgem/gem/store/cem_objects.hpp create mode 100644 src/gem/cem_objects.cpp create mode 100644 tests/test_cem_objects.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 5b24ad5..92f3333 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -58,6 +58,7 @@ add_library(secsgem src/gem/load_port_state.cpp src/gem/substrate_state.cpp src/gem/ept_state.cpp + src/gem/cem_objects.cpp src/gem/host_handler.cpp src/config/loader.cpp src/endpoint.cpp @@ -106,6 +107,7 @@ add_executable(secsgem_tests tests/test_carriers.cpp tests/test_substrates.cpp tests/test_ept.cpp + tests/test_cem_objects.cpp ) target_link_libraries(secsgem_tests PRIVATE secsgem doctest::doctest) target_compile_definitions(secsgem_tests PRIVATE diff --git a/include/secsgem/gem/data_model.hpp b/include/secsgem/gem/data_model.hpp index e5f3c1b..faf5960 100644 --- a/include/secsgem/gem/data_model.hpp +++ b/include/secsgem/gem/data_model.hpp @@ -2,6 +2,7 @@ #include "secsgem/gem/store/alarms.hpp" #include "secsgem/gem/store/carriers.hpp" +#include "secsgem/gem/store/cem_objects.hpp" #include "secsgem/gem/store/clock.hpp" #include "secsgem/gem/ept_state.hpp" #include "secsgem/gem/store/control_jobs.hpp" @@ -43,6 +44,7 @@ struct EquipmentDataModel { LoadPortStore load_ports; SubstrateStore substrates; EptStateMachine ept; + CemObjectStore cem; // Convenience: VID -> value lookup spanning SVIDs and DVIDs. std::optional vid_value(uint32_t vid) const { diff --git a/include/secsgem/gem/module_state.hpp b/include/secsgem/gem/module_state.hpp new file mode 100644 index 0000000..3aeb474 --- /dev/null +++ b/include/secsgem/gem/module_state.hpp @@ -0,0 +1,62 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include "secsgem/gem/carrier_state.hpp" // shared CarrierTransition template + +// E157 §6 Module Process Tracking — per-module state model for a process +// step within a recipe. The lifecycle is similar to E40 PJ but scoped +// to a single module step rather than a whole job; an E157 instance is +// typically created per (Module, Substrate, RecipeStep) triple by the +// process executive when a wafer arrives at a module. +// +// Wire-byte values per E157-0712 §10.3. +namespace secsgem::gem { + +enum class ModuleState : uint8_t { + NotExecuting = 0, + GeneralExecuting = 1, // setup, pre-process, etc. + StepExecuting = 2, // actively running the recipe step + StepCompleted = 3, + NoState = 255, +}; + +const char* module_state_name(ModuleState s); +std::optional parse_module_state(const std::string& s); + +enum class ModuleEvent { + StartGeneral, // NotExecuting -> GeneralExecuting + StartStep, // GeneralExecuting -> StepExecuting + CompleteStep, // StepExecuting -> StepCompleted + Reset, // any -> NotExecuting (cancellation) + Abort, // any -> StepCompleted (early termination) +}; + +const char* module_event_name(ModuleEvent e); + +using ModuleTable = CarrierTransitionTable; +ModuleTable default_module_table(); + +class ModuleStateMachine { + public: + using StateChangeHandler = + std::function; + + ModuleStateMachine(); + + ModuleState state() const { return state_; } + void set_state_change_handler(StateChangeHandler h) { on_change_ = std::move(h); } + + bool on_event(ModuleEvent e); + + private: + ModuleTable table_; + ModuleState state_ = ModuleState::NotExecuting; + StateChangeHandler on_change_; +}; + +} // namespace secsgem::gem diff --git a/include/secsgem/gem/store/cem_objects.hpp b/include/secsgem/gem/store/cem_objects.hpp new file mode 100644 index 0000000..0dc38b8 --- /dev/null +++ b/include/secsgem/gem/store/cem_objects.hpp @@ -0,0 +1,105 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +#include "secsgem/secs2/item.hpp" + +// E120 §6 Common Equipment Model. E120 defines a generic object +// hierarchy (Equipment -> Subsystem -> IODevice / Module / Material +// Location) for equipment self-description. Hosts query the tree via +// the generic E14 Object Service messages (S14F1-F4 GetAttr / SetAttr). +// +// We model the hierarchy with a single CemObject node type whose +// `objtype` is one of a small enum, parented by `parent_objid`, and +// carrying a flat attribute map per E14. Real CEM trees +// are rarely deeper than four levels; a flat map keyed by OBJSPEC +// (parent.child.grand...) is the canonical traversal shape. +namespace secsgem::gem { + +namespace s2 = secsgem::secs2; + +enum class CemObjectType : uint8_t { + Equipment = 0, // top of the tree (one per tool) + Subsystem = 1, // major functional unit + IODevice = 2, // I/O device + Module = 3, // process module (E157 ties in here) + MaterialLoc = 4, // material location (load port, internal store) + Other = 255, +}; + +const char* cem_object_type_name(CemObjectType t); + +struct CemObject { + std::string objid; // unique within the equipment tree + CemObjectType objtype; + std::string parent_objid; // empty = root (Equipment) + std::map attributes; +}; + +class CemObjectStore { + public: + enum class CreateResult { Created, Denied_AlreadyExists, Denied_BadParent }; + + CreateResult add(CemObject obj) { + if (objs_.count(obj.objid)) return CreateResult::Denied_AlreadyExists; + if (!obj.parent_objid.empty() && !objs_.count(obj.parent_objid)) + return CreateResult::Denied_BadParent; + objs_.emplace(obj.objid, std::move(obj)); + return CreateResult::Created; + } + + bool has(const std::string& objid) const { return objs_.count(objid) > 0; } + const CemObject* get(const std::string& objid) const { + auto it = objs_.find(objid); + return it == objs_.end() ? nullptr : &it->second; + } + CemObject* get(const std::string& objid) { + auto it = objs_.find(objid); + return it == objs_.end() ? nullptr : &it->second; + } + + // E14 GetAttr / SetAttr: returns nullopt for unknown object or attr. + std::optional get_attr(const std::string& objid, + const std::string& name) const { + const auto* o = get(objid); + if (!o) return std::nullopt; + auto it = o->attributes.find(name); + if (it == o->attributes.end()) return std::nullopt; + return it->second; + } + + bool set_attr(const std::string& objid, std::string name, s2::Item value) { + auto* o = get(objid); + if (!o) return false; + o->attributes.insert_or_assign(std::move(name), std::move(value)); + return true; + } + + // Children of `parent_objid` (empty string returns roots). + std::vector children(const std::string& parent_objid) const { + std::vector out; + for (const auto& kv : objs_) { + if (kv.second.parent_objid == parent_objid) out.push_back(&kv.second); + } + return out; + } + + std::size_t size() const { return objs_.size(); } + std::vector ids() const { + std::vector out; + out.reserve(objs_.size()); + for (const auto& kv : objs_) out.push_back(kv.first); + return out; + } + + private: + std::map objs_; +}; + +} // namespace secsgem::gem diff --git a/src/gem/cem_objects.cpp b/src/gem/cem_objects.cpp new file mode 100644 index 0000000..df94811 --- /dev/null +++ b/src/gem/cem_objects.cpp @@ -0,0 +1,17 @@ +#include "secsgem/gem/store/cem_objects.hpp" + +namespace secsgem::gem { + +const char* cem_object_type_name(CemObjectType t) { + switch (t) { + case CemObjectType::Equipment: return "Equipment"; + case CemObjectType::Subsystem: return "Subsystem"; + case CemObjectType::IODevice: return "IODevice"; + case CemObjectType::Module: return "Module"; + case CemObjectType::MaterialLoc: return "MaterialLocation"; + case CemObjectType::Other: return "Other"; + } + return "?"; +} + +} // namespace secsgem::gem diff --git a/tests/test_cem_objects.cpp b/tests/test_cem_objects.cpp new file mode 100644 index 0000000..f02015e --- /dev/null +++ b/tests/test_cem_objects.cpp @@ -0,0 +1,46 @@ +#include + +#include "secsgem/gem/store/cem_objects.hpp" + +using namespace secsgem::gem; +namespace s2 = secsgem::secs2; + +TEST_CASE("CemObjectStore: add / get / size") { + CemObjectStore s; + CHECK(s.add({"EQ-1", CemObjectType::Equipment, "", {}}) == + CemObjectStore::CreateResult::Created); + CHECK(s.add({"EQ-1", CemObjectType::Equipment, "", {}}) == + CemObjectStore::CreateResult::Denied_AlreadyExists); + CHECK(s.add({"SUB-1", CemObjectType::Subsystem, "EQ-1", {}}) == + CemObjectStore::CreateResult::Created); + CHECK(s.add({"orphan", CemObjectType::Module, "ghost", {}}) == + CemObjectStore::CreateResult::Denied_BadParent); + CHECK(s.size() == 2); +} + +TEST_CASE("CemObjectStore: tree traversal via children()") { + CemObjectStore s; + s.add({"EQ-1", CemObjectType::Equipment, "", {}}); + s.add({"SUB-A", CemObjectType::Subsystem, "EQ-1", {}}); + s.add({"SUB-B", CemObjectType::Subsystem, "EQ-1", {}}); + s.add({"MOD-1", CemObjectType::Module, "SUB-A", {}}); + CHECK(s.children("").size() == 1); // one root + CHECK(s.children("EQ-1").size() == 2); // two subsystems + CHECK(s.children("SUB-A").size() == 1); // one module + CHECK(s.children("SUB-B").empty()); +} + +TEST_CASE("CemObjectStore: GetAttr / SetAttr E14 semantics") { + CemObjectStore s; + s.add({"EQ-1", CemObjectType::Equipment, "", + {{"MDLN", s2::Item::ascii("SECS-DEMO")}}}); + auto mdln = s.get_attr("EQ-1", "MDLN"); + REQUIRE(mdln.has_value()); + CHECK(mdln->as_ascii() == "SECS-DEMO"); + // SetAttr on existing attr overwrites. + CHECK(s.set_attr("EQ-1", "MDLN", s2::Item::ascii("v2"))); + CHECK(s.get_attr("EQ-1", "MDLN")->as_ascii() == "v2"); + // Unknown object. + CHECK_FALSE(s.set_attr("ghost", "x", s2::Item::ascii("y"))); + CHECK_FALSE(s.get_attr("EQ-1", "UNKNOWN_ATTR").has_value()); +}