E40 Process Jobs + E94 Control Jobs + E30 communication state

GEM300 layer: SEMI E40-0705 Process Job and E94-0705 Control Job
state machines, plus the E30 §6.1 communication-state machine that
sits between HSMS SELECT and full GEM communication. Data-driven
via data/process_job_state.yaml and data/control_job_state.yaml,
mirroring the existing control_state.yaml pattern.

Wire coverage:
  S14F9/F10   CreateObject (CJ)              host -> equipment
  S14F11/F12  DeleteObject (CJ)              host -> equipment
  S16F5/F6    PRJobCommand                   host -> equipment
  S16F9       PRJobAlert                     equipment -> host
  S16F11/F12  PRJobCreate (simplified body)  host -> equipment
  S16F13/F14  PRJobDequeue                   host -> equipment
  S16F27/F28  CJobCommand                    host -> equipment

Process Job FSM exposes 8 states matching PRJOBSTATE bytes (E40 §10.3.2);
HOQ is reorder-aware (move-to-head against an insertion-order vector);
Stop/Abort on a Queued PJ routes through ABORTING so the host observes
PRJOBSTATE=7 on the wire (§6.3); alert_enabled is settable per-PJ for
PRALERT control; FSM dispatches through ProcessJobStore::on_change_
dynamically so a late set_state_change_handler() reaches existing PJs.

Hardening: loader rejects NoState (sentinel) as initial/from/to and
rejects `on: created` rows; static_asserts pin enum values to wire
bytes; ProcessJobStore is non-movable to keep the per-PJ this-capture
safe.

Server simulator cascades the full CJ -> PJ lifecycle on CJSTART so
the wire trace exercises every legal state. CEIDs 400/401 fire on CJ
state changes via the existing event-report pipeline.

Tests: 60+ new assertions across test_process_jobs, test_control_jobs,
test_communication_state, test_hsms_connection, plus loader and
messages round-trip coverage.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-06-07 21:00:32 +02:00
parent 1f67aad985
commit 90c177b7ce
33 changed files with 3122 additions and 62 deletions
+74 -2
View File
@@ -40,6 +40,8 @@ constexpr uint32_t kDataIdReports = 7;
constexpr uint32_t kRptidStatus = 1000;
constexpr uint32_t kCeidProcessStarted = 300;
constexpr uint32_t kCeidAlarmSetEvent = 200;
constexpr uint32_t kCeidCJExecuting = 400;
constexpr uint32_t kCeidCJCompleted = 401;
constexpr uint32_t kAlarmChiller = 1;
struct Sequence : std::enable_shared_from_this<Sequence> {
@@ -102,6 +104,16 @@ int main(int argc, char** argv) {
logfn("SPOOL READY: " + std::to_string(n.value_or(0)) + " queued messages");
return gem::s6f26_spool_data_ready_ack(gem::EventReportAck::Accept);
}
// S16F9: E40 PRJob alert (one-way, E->H).
if (msg.stream == 16 && msg.function == 9) {
auto a = gem::parse_s16f9(msg);
if (a) {
logfn("PJ ALERT " + a->prjobid + " state=" +
gem::process_job_state_name(a->prjobstate));
}
if (msg.reply_expected) return s2::Message(16, 0, false);
return std::nullopt;
}
// S5F1: alarm send from equipment.
if (msg.stream == 5 && msg.function == 1) {
auto a = gem::parse_s5f1(msg);
@@ -241,7 +253,9 @@ int main(int argc, char** argv) {
conn->send_request(
gem::s2f35_link_event_report(kDataIdReports,
{{kCeidProcessStarted, {kRptidStatus}},
{kCeidAlarmSetEvent, {kRptidStatus}}}),
{kCeidAlarmSetEvent, {kRptidStatus}},
{kCeidCJExecuting, {kRptidStatus}},
{kCeidCJCompleted, {kRptidStatus}}}),
[logfn, fail, next](std::error_code ec, const s2::Message& reply) {
if (ec) { fail("S2F35", ec); return; }
auto a = gem::ack_byte(reply);
@@ -253,7 +267,8 @@ int main(int argc, char** argv) {
// 8. Enable the linked CEIDs.
seq->steps.push_back([conn, logfn, fail](auto next) {
conn->send_request(
gem::s2f37_enable_event(true, {kCeidProcessStarted, kCeidAlarmSetEvent}),
gem::s2f37_enable_event(true, {kCeidProcessStarted, kCeidAlarmSetEvent,
kCeidCJExecuting, kCeidCJCompleted}),
[logfn, fail, next](std::error_code ec, const s2::Message& reply) {
if (ec) { fail("S2F37", ec); return; }
auto a = gem::ack_byte(reply);
@@ -391,6 +406,63 @@ 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.
seq->steps.push_back([conn, logfn, fail](auto next) {
conn->send_request(
gem::s16f11_pr_job_create("PJ-1", "RECIPE-A", {"WFR-1", "WFR-2"}),
[logfn, fail, next](std::error_code ec, const s2::Message& reply) {
if (ec) { fail("S16F11", ec); return; }
auto a = gem::ack_byte(reply);
logfn("S16F12 HCACK=" + (a ? std::to_string(*a) : "?"));
next();
});
});
// 14b. S14F9 CreateControlJob CJ-1 containing [PJ-1].
seq->steps.push_back([conn, logfn, fail](auto next) {
conn->send_request(
gem::s14f9_create_control_job("CJ-1", {"PJ-1"}),
[logfn, fail, next](std::error_code ec, const s2::Message& reply) {
if (ec) { fail("S14F9", ec); return; }
auto r = gem::parse_s14f10(reply);
if (r)
logfn("S14F10 CJ=" + r->ctljobid + " OBJACK=" +
std::to_string(static_cast<int>(r->ack)));
next();
});
});
// 14c. S16F27 CJSTART CJ-1 -> equipment cascades the PJ through every
// state, host sees a burst of S16F9 alerts + S6F11(CEID=400/401).
seq->steps.push_back([conn, logfn, fail](auto next) {
conn->send_request(
gem::s16f27_cj_command("CJ-1", "CJSTART"),
[logfn, fail, next](std::error_code ec, const s2::Message& reply) {
if (ec) { fail("S16F27", ec); return; }
auto a = gem::ack_byte(reply);
logfn("S16F28 HCACK=" + (a ? std::to_string(*a) : "?"));
next();
});
});
// 14d. Pace 200ms so the asynchronous S16F9 / S6F11 alerts arrive
// before we move on to terminal display.
seq->steps.push_back([pause_then](auto next) {
pause_then(std::chrono::milliseconds(200), [next] { next(); });
});
// 14e. Tidy up: delete the CJ via S14F11. The contained PJs are now
// ProcessComplete; the equipment leaves them in the store until
// explicitly dequeued (which can be wired similarly via S16F13).
seq->steps.push_back([conn, logfn, fail](auto next) {
conn->send_request(
gem::s14f11_delete_control_job("CJ-1"),
[logfn, fail, next](std::error_code ec, const s2::Message& reply) {
if (ec) { fail("S14F11", ec); return; }
auto a = gem::ack_byte(reply);
logfn("S14F12 OBJACK=" + (a ? std::to_string(*a) : "?"));
next();
});
});
// 15. Send a terminal display to the equipment.
seq->steps.push_back([conn, logfn, fail](auto next) {
conn->send_request(gem::s10f1_terminal_display_single(0, "Hello equipment!"),