# 14 — E40 + E94: Process jobs and Control jobs ← [13 E30 — GEM](13_e30_gem.md) | [Back to index](00_index.md) | Next: [15 E87 — Carriers and load ports](15_e87_carriers.md) → A modern fab tool doesn't just "process wafers" — it executes **jobs** with explicit lifecycles that the MES can submit, monitor, pause, abort, and audit. Two SEMI standards govern this: - **E40** (1999) — Process Jobs. A PJ describes one *recipe run* on a defined set of material. "Run RECIPE-Cu-A on this list of 25 wafers." - **E94** (2001) — Control Jobs. A CJ wraps a *batch* of PJs with a processing policy. "Run these 4 PJs in order; abort the rest if any one fails." In production the host almost always creates a CJ wrapping its PJs and uses CJ commands (CJSTART, CJPAUSE) to drive the batch. Per-PJ commands (PJSTART) exist but are less common. This chapter walks both lifecycles, the messages that drive them, the FSMs in code, and how they cascade. --- ## E40 — Process Jobs ### The PJ lifecycle Eight states. Values match the **PRJOBSTATE byte** that S16F9 carries on the wire (E40-0705 §10.3.2): | Value | State | Meaning | |-------|--------------------|--------------------------------------------------------| | 0 | `Queued` | Created; awaiting selection by a CJ or by S16F5 SELECT.| | 1 | `SettingUp` | Equipment loading the recipe + verifying material. | | 2 | `WaitingForStart` | Ready; awaiting PJSTART (or auto-start if configured). | | 3 | `Processing` | Recipe running. | | 4 | `ProcessComplete` | Recipe finished; awaiting host dequeue. | | 5 | `Paused` | Mid-process pause; resumable. | | 6 | `Stopping` | Graceful abort in progress. | | 7 | `Aborting` | Forceful abort in progress. | | 255 | `NoState` | Sentinel: "doesn't exist yet / freshly deleted." | ``` Created ───► Queued ──Select──► SettingUp ──SetupComplete──► WaitingForStart │ PJSTART ──────────┤ ▼ Processing ╱ │ ╲ PJPAUSE PJSTOP PJABORT ╱ │ ╲ Paused Stopping Aborting │ │ │ PJRESUME ▼ ▼ ╲ ProcessComplete AbortComplete ╲ │ ╲ ▼ back to Processing ``` Defined in [`include/secsgem/gem/process_job_state.hpp`](../include/secsgem/gem/process_job_state.hpp). Drivers of the FSM (`ProcessJobEvent`): - **`Created`** — synthetic observer signal when the store first records a PJ. Doesn't appear in the transition table. - **`Select`** — `Queued → SettingUp`. Fires when a CJ promotes this PJ for processing, or when S16F5 SELECT arrives. - **`SetupComplete`** — equipment-internal (recipe loaded, material verified). - **`Start` / `Pause` / `Resume` / `Stop` / `Abort`** — host-driven via `S16F5 PRCMD` strings. PRCMD = `"PJSTART"`, `"PJPAUSE"`, `"PJRESUME"`, `"PJSTOP"`, `"PJABORT"`. - **`ProcessComplete` / `AbortComplete`** — equipment-internal, fire when the recipe runner or abort controller finishes. The transition table is loaded from [`data/process_job_state.yaml`](../data/process_job_state.yaml). Same spec-as-data pattern as E30 control state (chapter 13). ### The E40 messages | S/F | Direction | Purpose | |---------|-----------|-----------------------------------------------------------| | S16F11 | H → E | PRJobCreate. Body carries PRJobID, MF, recipe spec, material list, PRProcessStart flag. | | S16F12 | E → H | PRJobAck. PRJobAck byte: 0 = accepted, non-zero = errored. | | S16F13 | H → E | PRJobDequeue. Host clears the PJ from equipment storage after observing ProcessComplete. | | S16F14 | E → H | PRJobDequeueAck. | | S16F5 | H → E | PRJobCommand. Body carries PRJobID + PRCMD string. | | S16F6 | E → H | PRJobCommandAck. | | S16F7 | H → E | PRJobMonitor. Host pulls current state for a PJ. | | S16F8 | E → H | PRJobMonitorAck. Body carries PRJOBSTATE byte. | | S16F9 | E → H | PRJobAlert. Equipment-initiated state-change notification. | `S16F9` is the *interesting* one: it's a W=0 unsolicited message that fires on every state transition (configurable per-PJ via `alert_enabled`). The body carries the new PRJOBSTATE so the host can update its tracking without polling. Tested on the wire by [`tests/test_wire_ceid_emission.cpp`](../tests/test_wire_ceid_emission.cpp) ("PJ Queued→SettingUp fires S16F9 PRJobAlert on the wire"). ### The PJ store [`include/secsgem/gem/store/process_jobs.hpp`](../include/secsgem/gem/store/process_jobs.hpp) houses one entry per PJ — id, MF, recipe spec, current state, material list, alert_enabled bit. Persistent: a per-record file journal lets the store survive equipment restarts (chapter [36](36_persistence_validation_metrics.md)). Tests: [`tests/test_process_jobs.cpp`](../tests/test_process_jobs.cpp) (21 cases — every transition, every wire message round-trip, persistence). --- ## E94 — Control Jobs ### The CJ lifecycle Eight states, similar shape to PJ but distinct values (E94 doesn't pin a wire byte for state; this project picks its own encoding): | Value | State | Meaning | |-------|--------------------|------------------------------------------------------| | 0 | `Queued` | Created; not yet promoted. | | 1 | `Selected` | CJ has selected one of its PJs (the PJ is now `SettingUp`). | | 2 | `WaitingForStart` | All material ready; awaiting CJSTART. | | 3 | `Executing` | At least one PJ in flight. | | 4 | `Paused` | Mid-execution pause. | | 5 | `Completed` | All PJs done; awaiting deletion. | | 6 | `Stopping` | Graceful abort in progress. | | 7 | `Aborting` | Forceful abort in progress. | | 255 | `NoState` | Sentinel. | Drivers: - **`Select`** — Queued → Selected (CJ promotes its first PJ). - **`SetupComplete`** — Selected → WaitingForStart (PJ reached WaitingForStart). - **`Start` / `Pause` / `Resume` / `Stop` / `Abort`** — host-driven via `S16F27 CJCMD` strings. CJCMD = `"CJSTART"`, `"CJPAUSE"`, `"CJRESUME"`, `"CJSTOP"`, `"CJABORT"`. - **`AllJobsComplete`** — internal: every PJ in the CJ reached `ProcessComplete`. - **`AbortComplete`** — internal: every PJ reached an aborted state. Defined in [`include/secsgem/gem/control_job_state.hpp`](../include/secsgem/gem/control_job_state.hpp); transition table in [`data/control_job_state.yaml`](../data/control_job_state.yaml). ### The E94 messages | S/F | Direction | Purpose | |--------|-----------|---------------------------------------------------------------| | S14F9 | H → E | CreateControlJob. Body carries CJobID + ordered PRJobID list.| | S14F10 | E → H | OBJACK reply. | | S14F11 | H → E | DeleteControlJob. | | S14F12 | E → H | OBJACK reply. | | S16F27 | H → E | CJCommand. Body carries CJobID + CJCMD string. | | S16F28 | E → H | HCACK reply. | The wire test in [`apps/secs_conformance.cpp`](../apps/secs_conformance.cpp) drives the full S14F9 → S16F27 (CJSTART) → S14F11 sequence as one conformance check. ### The CJ store [`include/secsgem/gem/store/control_jobs.hpp`](../include/secsgem/gem/store/control_jobs.hpp); tests in [`tests/test_control_jobs.cpp`](../tests/test_control_jobs.cpp) (9 cases). Also persistent. --- ## How a PJ and its CJ cascade The interesting part isn't either FSM in isolation — it's how they **cascade** during a typical batch run. ``` t=0 host S16F11 PRJobCreate (PJ-1) → PJ-1 enters Queued → S16F9 PRJobAlert (PJ-1 NoState → Queued, if alerts enabled) t=1 host S16F11 PRJobCreate (PJ-2) (similar) t=2 host S14F9 CreateControlJob (CJ-1, [PJ-1, PJ-2]) → CJ-1 enters Queued → equipment internally fires Select on PJ-1 (first in CJ list) → PJ-1 enters SettingUp + S16F9 alert → CJ-1 enters Selected t=3 equipment recipe runner: PJ-1 SetupComplete → PJ-1 enters WaitingForStart + S16F9 alert → CJ-1 enters WaitingForStart t=4 host S16F27 CJSTART (CJ-1) → CJ-1 enters Executing → CEID = ControlJobExecuting fires → S6F11 → equipment fires Start on PJ-1 → PJ-1 enters Processing + S16F9 alert → CEID = ProcessStarted fires → S6F11 t=N equipment: PJ-1 recipe done → PJ-1 enters ProcessComplete + S16F9 alert → CEID = ProcessCompleted fires → S6F11 → equipment fires Select on PJ-2 → PJ-2 enters SettingUp ... ... (same dance for PJ-2) t=M all PJs done → CJ-1 fires AllJobsComplete → CJ-1 enters Completed → CEID = ControlJobCompleted fires → S6F11 t=M+1 host S16F13 PRJobDequeue (PJ-1) host S16F13 PRJobDequeue (PJ-2) host S14F11 DeleteControlJob (CJ-1) → all three records removed ``` Notice three things: 1. **CJ events drive PJ events.** CJSTART makes the CJ go to Executing, which causes the equipment to fire Start on the first PJ — the host doesn't send PJSTART explicitly. 2. **Every transition fires S16F9.** Hosts that subscribe to S16F9 don't need to poll with S16F7; they get push notification for every state change. 3. **CEIDs fire alongside FSM transitions.** `ControlJobExecuting`, `ProcessStarted`, `ProcessCompleted`, `ControlJobCompleted` are regular CEIDs (from `data/equipment.yaml`) that fire when the FSMs transition. They drive S6F11 events bundled with whatever reports the host has linked. End-to-end demonstration: [`tests/test_live_gem300.cpp`](../tests/test_live_gem300.cpp) drives the full cascade over a real loopback HSMS connection. --- ## Why CJs exist as a separate layer Could the host just submit PJs one at a time and orchestrate the batch itself? Yes — but: - **Ordering / dependencies** belong on the equipment side. The CJ FSM ensures PJ-2 only starts after PJ-1 finishes, even if the host network drops between them. - **Abort semantics** are sharper. CJSTOP applies to *every PJ in the CJ*, deterministically. Aborting a PJ at a time leaves race windows. - **Reporting** is unified. A CJ-level CEID summarises "this batch is done"; without CJs the host has to track every PJ completion individually. E94 is the layer that makes "run this batch of recipes safely" a single command instead of an orchestration script. --- ## Edge cases worth knowing - **PJ in Paused state when CJ goes Stopping.** The PJ has to resume first (so the recipe runner can reach a safe stopping point), then stop. The transition tables handle this. - **Partial cancel.** Host can S16F5 PJABORT on a single PJ inside a running CJ. The CJ continues with the remaining PJs. - **CJ delete while PJs are still queued.** E94 §6: deleting a CJ that owns Queued PJs cancels them — the equipment fires `AbortComplete` on each. - **PJ status byte on the wire is a raw `uint8_t`.** This is why the enum values match the spec exactly — the encoder just casts to byte. Don't reorder the enum. Every edge case has a test: [`tests/test_process_jobs.cpp`](../tests/test_process_jobs.cpp) + [`tests/test_control_jobs.cpp`](../tests/test_control_jobs.cpp) + the CEID emission and live-scenario tests. --- ## Where to go next You now know how a *job* is created, sequenced, executed, and torn down. But before any of that can happen, the **carrier** holding the material has to arrive at the tool — and the equipment has to know it. Next: [→ 15 E87 — Carriers and load ports](15_e87_carriers.md)