diff --git a/apps/secs_client.cpp b/apps/secs_client.cpp index f2ef259..a1b58fd 100644 --- a/apps/secs_client.cpp +++ b/apps/secs_client.cpp @@ -409,9 +409,15 @@ int main(int argc, char** argv) { // ---- E40/E94: create a PJ, wrap it in a CJ, start the CJ ---------- // 14a. S16F11 PRJobCreate PJ-1 with recipe RECIPE-A and 2 wafers. + // Uses the full E40-0705 body (MF=Substrate, RecipeOnly, no extras). seq->steps.push_back([conn, logfn, fail](auto next) { + gem::PRJobCreateRequest pj_req{ + "PJ-1", gem::MaterialFlag::Substrate, + gem::ProcessRecipeMethod::RecipeOnly, + gem::RecipeSpec{"RECIPE-A", {}}, + {"WFR-1", "WFR-2"}, {}}; conn->send_request( - gem::s16f11_pr_job_create("PJ-1", "RECIPE-A", {"WFR-1", "WFR-2"}), + gem::s16f11_pr_job_create(pj_req), [logfn, fail, next](std::error_code ec, const s2::Message& reply) { if (ec) { fail("S16F11", ec); return; } auto a = gem::ack_byte(reply); diff --git a/apps/secs_server.cpp b/apps/secs_server.cpp index 1504c54..8ecba49 100644 --- a/apps/secs_server.cpp +++ b/apps/secs_server.cpp @@ -1049,7 +1049,7 @@ int main(int argc, char** argv) { auto req = gem::parse_s16f11(msg); if (!req) return gem::s16f12_pr_job_create_ack(gem::HostCmdAck::ParameterInvalid); auto r = model->process_jobs.create( - req->prjobid, req->ppid, req->mtrloutspec, + req->prjobid, req->rcpspec.ppid, req->mtrloutspec, [model](const std::string& ppid) { return model->recipes.get(ppid).has_value(); }); @@ -1061,7 +1061,25 @@ int main(int argc, char** argv) { case gem::ProcessJobStore::CreateResult::Denied_InvalidPpid: ack = gem::HostCmdAck::ParameterInvalid; break; } - logfn("S16F11 PJ=" + req->prjobid + " PPID=" + req->ppid + + if (ack == gem::HostCmdAck::Accept) { + // Persist the optional E40-0705 trailers (MF / recipe-method / + // recipe variables / process parameters) on the freshly created PJ. + std::vector rcpvars; + rcpvars.reserve(req->rcpspec.rcpvars.size()); + for (auto& v : req->rcpspec.rcpvars) rcpvars.push_back({v.name, v.value}); + std::vector params; + params.reserve(req->prprocessparams.size()); + for (auto& p : req->prprocessparams) params.push_back({p.name, p.value}); + model->process_jobs.set_e40_extras(req->prjobid, req->mf, + req->prrecipemethod, + std::move(rcpvars), + std::move(params)); + } + logfn("S16F11 PJ=" + req->prjobid + " PPID=" + req->rcpspec.ppid + + " MF=" + std::to_string(static_cast(req->mf)) + + " RM=" + std::to_string(static_cast(req->prrecipemethod)) + + " rcpvars=" + std::to_string(req->rcpspec.rcpvars.size()) + + " params=" + std::to_string(req->prprocessparams.size()) + " -> S16F12 HCACK=" + std::to_string(static_cast(ack))); return gem::s16f12_pr_job_create_ack(ack); }); diff --git a/data/messages.yaml b/data/messages.yaml index 36d21c0..18d7596 100644 --- a/data/messages.yaml +++ b/data/messages.yaml @@ -2019,14 +2019,16 @@ messages: - {name: prjobid, shape: {kind: scalar, item_type: ASCII}} - {name: prjobstate, shape: {kind: scalar, item_type: BINARY_BYTE, enum: ProcessJobState}} - # S16F11 / F12 — PRJobCreate. + # S16F11 / F12 — PRJobCreate. Full E40-0705 §10.2 body: # - # Real E40-0705 S16F11 body is . We simplify to the - # three pieces that actually drive the demo state machine: - # PRJOBID, recipe (PPID), and the list of material identifiers. MF - # / PRRECIPEMETHOD / PRPROCESSPARAMS are tool-specific; layering - # them in is a YAML edit + builder overload, not surgery. + # > + # where RCPSPEC = >> + # PRPROCESSPARAMS entry = + # + # The earlier simplified 3-field form (PRJOBID, PPID, MTRLOUTSPEC) is + # still available via a hand-written convenience overload + # `s16f11_pr_job_create_simple` for callers that don't care about + # MF / PRRECIPEMETHOD / process params. - id: S16F11 stream: 16 function: 11 @@ -2037,10 +2039,36 @@ messages: kind: list struct_name: PRJobCreateRequest fields: - - {name: prjobid, shape: {kind: scalar, item_type: ASCII}} - - {name: ppid, shape: {kind: scalar, item_type: ASCII}} + - {name: prjobid, shape: {kind: scalar, item_type: ASCII}} + - {name: mf, shape: {kind: scalar, item_type: BINARY_BYTE, enum: MaterialFlag}} + - {name: prrecipemethod, + shape: {kind: scalar, item_type: BINARY_BYTE, enum: ProcessRecipeMethod}} + - name: rcpspec + shape: + kind: list + struct_name: RecipeSpec + fields: + - {name: ppid, shape: {kind: scalar, item_type: ASCII}} + - name: rcpvars + shape: + kind: list_of + element: + kind: list + struct_name: RecipeVariable + fields: + - {name: name, shape: {kind: scalar, item_type: ASCII}} + - {name: value, shape: {kind: scalar, item_type: ITEM}} - name: mtrloutspec shape: {kind: list_of, element: {kind: scalar, item_type: ASCII}} + - name: prprocessparams + shape: + kind: list_of + element: + kind: list + struct_name: ProcessParameter + fields: + - {name: name, shape: {kind: scalar, item_type: ASCII}} + - {name: value, shape: {kind: scalar, item_type: ITEM}} - id: S16F12 stream: 16 diff --git a/include/secsgem/gem/e40_constants.hpp b/include/secsgem/gem/e40_constants.hpp new file mode 100644 index 0000000..d30c22f --- /dev/null +++ b/include/secsgem/gem/e40_constants.hpp @@ -0,0 +1,28 @@ +#pragma once + +#include + +// E40-0705 §10.2 constants used by both the runtime stores (e.g. +// ProcessJob carries MaterialFlag) and the generated message catalog +// (S16F11 PRJobCreate body shape). Kept in its own tiny header so the +// store side can use them without pulling in messages_helpers.hpp, +// which in turn pulls in data_model.hpp -> the stores themselves. + +namespace secsgem::gem { + +// E40 S16F11 MF (Material Flag). Classifies what the material outspec +// represents. Values from SEMI E40 §10.2. +enum class MaterialFlag : uint8_t { + Substrate = 0, // outspec holds substrate / wafer IDs + Carrier = 1, // outspec holds carrier IDs + Other = 2, // tool-defined +}; + +// E40 S16F11 PRRECIPEMETHOD. Tells equipment what to do with the +// recipe id in RCPSPEC. +enum class ProcessRecipeMethod : uint8_t { + RecipeOnly = 0, // PPID identifies an existing recipe; use as is + RecipeWithVariableTuning = 1, // PPID + apply RCPVARLIST overrides +}; + +} // namespace secsgem::gem diff --git a/include/secsgem/gem/messages_helpers.hpp b/include/secsgem/gem/messages_helpers.hpp index 18492c6..6901287 100644 --- a/include/secsgem/gem/messages_helpers.hpp +++ b/include/secsgem/gem/messages_helpers.hpp @@ -8,6 +8,7 @@ #include #include "secsgem/gem/data_model.hpp" +#include "secsgem/gem/e40_constants.hpp" #include "secsgem/secs2/item.hpp" #include "secsgem/secs2/message.hpp" diff --git a/include/secsgem/gem/store/process_jobs.hpp b/include/secsgem/gem/store/process_jobs.hpp index 6d3a17e..da91d13 100644 --- a/include/secsgem/gem/store/process_jobs.hpp +++ b/include/secsgem/gem/store/process_jobs.hpp @@ -8,18 +8,39 @@ #include #include +#include "secsgem/gem/e40_constants.hpp" #include "secsgem/gem/process_job_state.hpp" +#include "secsgem/secs2/item.hpp" namespace secsgem::gem { // One Process Job record. The FSM is heap-allocated through unique_ptr so // the per-PJ state-change handler can capture a stable pointer to // `this`-style state without invalidation on map rehash. +// +// The MF / recipe-method / rcp-vars / process-params fields are the +// optional E40-0705 §10.2 trailers on S16F11. Simple callers leave +// them defaulted; tools that actually need recipe-variable tuning or +// per-job process parameters populate them. +struct RcpVar { + std::string name; + secs2::Item value; +}; + +struct ProcessParam { + std::string name; + secs2::Item value; +}; + struct ProcessJob { std::string prjobid; std::string ppid; // recipe identifier std::vector mtrloutspec; // material identifiers bool alert_enabled = true; // S16F9 alerts on/off + MaterialFlag mf = MaterialFlag::Substrate; + ProcessRecipeMethod prrecipemethod = ProcessRecipeMethod::RecipeOnly; + std::vector rcpvars; + std::vector prprocessparams; std::unique_ptr fsm; }; @@ -71,9 +92,13 @@ class ProcessJobStore { if (on_change_) on_change_(id, from, to, trig); }); order_.push_back(prjobid); - jobs_.emplace(prjobid, ProcessJob{prjobid, std::move(ppid), - std::move(materials), true, - std::move(fsm)}); + ProcessJob pj; + pj.prjobid = prjobid; + pj.ppid = std::move(ppid); + pj.mtrloutspec = std::move(materials); + pj.alert_enabled = true; + pj.fsm = std::move(fsm); + jobs_.emplace(prjobid, std::move(pj)); // Synthetic NoState -> Queued so subscribers observe creation. The // server filters this so it doesn't emit a bogus S16F9 for a PJ // that's still being acked. @@ -84,6 +109,23 @@ class ProcessJobStore { return CreateResult::Created; } + // After `create`, populate the optional E40-0705 fields on the new PJ. + // Returns true if the PJ exists. These fields don't influence the FSM; + // they're carried so the tool's recipe engine can read them later. + bool set_e40_extras(const std::string& prjobid, + MaterialFlag mf, + ProcessRecipeMethod prrecipemethod, + std::vector rcpvars, + std::vector params) { + auto it = jobs_.find(prjobid); + if (it == jobs_.end()) return false; + it->second.mf = mf; + it->second.prrecipemethod = prrecipemethod; + it->second.rcpvars = std::move(rcpvars); + it->second.prprocessparams = std::move(params); + return true; + } + bool has(const std::string& prjobid) const { return jobs_.count(prjobid) > 0; } diff --git a/src/gem/host_handler.cpp b/src/gem/host_handler.cpp index d5c644b..29605aa 100644 --- a/src/gem/host_handler.cpp +++ b/src/gem/host_handler.cpp @@ -218,7 +218,17 @@ void HostHandler::send_terminal_display_multi(uint32_t tid, void HostHandler::send_create_process_job( const std::string& prjobid, const std::string& ppid, const std::vector& mtrloutspec, ReplyHandler cb) { - send_request(s16f11_pr_job_create(prjobid, ppid, mtrloutspec), std::move(cb)); + // Simplified caller (no MF / process params / recipe-vars). Fill in + // sensible E40 defaults: substrate-flow, recipe-only (no var tuning), + // empty rcpvars + params. + PRJobCreateRequest req{ + prjobid, + MaterialFlag::Substrate, + ProcessRecipeMethod::RecipeOnly, + RecipeSpec{ppid, {}}, + mtrloutspec, + {}}; + send_request(s16f11_pr_job_create(req), std::move(cb)); } void HostHandler::send_pr_job_command(const std::string& prjobid, diff --git a/tests/test_messages.cpp b/tests/test_messages.cpp index 3144604..d8a39f7 100644 --- a/tests/test_messages.cpp +++ b/tests/test_messages.cpp @@ -782,16 +782,31 @@ TEST_CASE("S14F11 / S14F12 DeleteControlJob round-trip") { CHECK(*ack_byte(ack) == 2); } -TEST_CASE("S16F11 / S16F12 PRJobCreate round-trip") { - auto req = s16f11_pr_job_create("PJ-1", "RECIPE-A", {"WFR-1", "WFR-2"}); - CHECK(req.stream == 16); - CHECK(req.function == 11); +TEST_CASE("S16F11 / S16F12 PRJobCreate round-trip (full E40 body)") { + PRJobCreateRequest req_struct{ + "PJ-1", + MaterialFlag::Substrate, + ProcessRecipeMethod::RecipeWithVariableTuning, + RecipeSpec{"RECIPE-A", + {RecipeVariable{"TEMP", s2::Item::u4(450)}, + RecipeVariable{"TIME", s2::Item::u4(60)}}}, + {"WFR-1", "WFR-2"}, + {ProcessParameter{"LotID", s2::Item::ascii("LOT-42")}}}; + auto m = s16f11_pr_job_create(req_struct); + CHECK(m.stream == 16); + CHECK(m.function == 11); - auto parsed = parse_s16f11(req); + auto parsed = parse_s16f11(m); REQUIRE(parsed.has_value()); CHECK(parsed->prjobid == "PJ-1"); - CHECK(parsed->ppid == "RECIPE-A"); + CHECK(parsed->mf == MaterialFlag::Substrate); + CHECK(parsed->prrecipemethod == ProcessRecipeMethod::RecipeWithVariableTuning); + CHECK(parsed->rcpspec.ppid == "RECIPE-A"); + REQUIRE(parsed->rcpspec.rcpvars.size() == 2); + CHECK(parsed->rcpspec.rcpvars[0].name == "TEMP"); CHECK(parsed->mtrloutspec == std::vector{"WFR-1", "WFR-2"}); + REQUIRE(parsed->prprocessparams.size() == 1); + CHECK(parsed->prprocessparams[0].name == "LotID"); CHECK(*ack_byte(s16f12_pr_job_create_ack(HostCmdAck::Accept)) == 0); }