Files
secs-gem/proto/secsgem/v1/equipment.proto
T
raphael 9876dd9b5a
tests / build-and-test (push) Successful in 2m59s
tests / thread-sanitizer (push) Successful in 3m36s
tests / tshark-dissector (push) Successful in 2m25s
tests / secs4j-interop (push) Successful in 59s
tests / python-interop (push) Successful in 3m20s
tests / libfuzzer (push) Successful in 3m40s
feat(daemon): D10 carriers + E16 ops RPCs + stress test + virtual fab
Completes the daemon's GEM300 surface and adds two new test tiers.

D10 — E87 carriers: CarrierStore gains the HandlerSlot observer pattern
(add_id/slot_map/access_handler). The daemon's id-observer forwards host
S3F17 decisions onto the Subscribe stream as CarrierAction (PROCEED on a
Confirmed transition, CANCEL on CancelCarrier); ReportCarrier drives the
flow tool-side: WAITING creates the carrier + records the slot map,
IN_ACCESS/COMPLETE advance the access FSM (INVALID_OBJECT on unknown,
CANNOT_DO_NOW on an illegal transition).

E16 — operations RPCs: Describe (full name inventory: variables/events/
alarms/commands/constants + device header), FlushSpool (purge or drain),
SendTerminalMessage (S10F1 tool->host, honest CANNOT_DO_NOW when no host
and stream 10 isn't spoolable).

Stream responsiveness: Subscribe/WatchHealth poll at 100ms (was 500ms) so a
cancelled stream frees its sync-server worker thread promptly — this was
found by the new stress test, which hung under Subscribe churn at 500ms.

Tests:
- A randomized concurrent RPC stress case: 4 threads x 250 seeded ops
  (set/get/fire/alarm/control-state/describe + Subscribe churn), asserts no
  failed RPC and a still-responsive engine afterward; prints its seed; a
  strong TSan target.
- A virtual fab (interop/virtual_fab.py + the `fab` compose service /
  tools/spawn_fab.sh): N daemons, each with a secsgem-py host AND a
  secsgem_client tool, driven by seeded random traffic with end-to-end
  invariant checks (set/get round-trips, event->S6F11 and alarm->S5F1
  delivery, command->tool->completion). Verified green at N=3 (~150 ops/eq,
  all commands round-tripped, 0 violations). Wired into run_interop.sh
  (now 13 steps).

Also fixes the CI break from the previous commit: the Python-client lane's
test_values.py step lacked PYTHONPATH=clients/python (now step-level env).

Two bugs found and fixed while building this, both mine from this batch:
1. carrier test hung on a CancelCarrier of a still-NotConfirmed carrier — a
   self-transition the FSM doesn't signal, so the observer never fired and
   the stream Read blocked forever. Fixed to cancel a Confirmed carrier;
   the NotConfirmed edge is documented as a known E87 limitation.
2. the 500ms stream poll above.

Daemon suite 7 cases / 214 assertions; core 475 / 3097; virtual fab green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 15:05:13 +02:00

319 lines
12 KiB
Protocol Buffer

syntax = "proto3";
package secsgem.v1;
// =============================================================================
// SECS/GEM Equipment API
//
// The daemon *is* one piece of SEMI equipment: it owns the HSMS link to the
// host (MES) and speaks GEM on your behalf. Your tool software connects as a
// client and only ever does two things:
//
// • tell the equipment about itself — set variables, fire events, raise alarms
// • react to what the host asks for — receive commands/jobs, answer them
//
// Everything SECS lives inside the daemon: message framing, report definitions,
// the GEM state machines, timers, spooling. You need no SEMI knowledge to use
// this API. Items are addressed by the human names from your equipment config
// (e.g. "chamber_pressure"), never by numeric SVID / CEID / ALID.
//
// CAPABILITY TIERS — wire up only what your equipment is:
// • Universal — variables, events, alarms, control state, commands. Every tool.
// • Carriers — E87 carrier/load-port flows. Only carrier-based equipment.
// • Recipes — S7 process-program transfer. Only recipe-driven equipment.
// • Jobs — E40 process jobs. Only job-based process/front-end equipment.
// If a tier doesn't apply, you simply never receive its HostRequest variants and
// never call its report RPCs.
// =============================================================================
service Equipment {
// ---- Universal: report state to the host --------------------------------
// Update one or more status/data variables by name. The daemon remembers the
// values, so the host always sees the latest when it polls (S1F3).
rpc SetVariables(VariableUpdate) returns (Ack);
// Read back what the daemon currently holds (useful on tool restart/reconnect).
rpc GetVariables(VariableQuery) returns (VariableSnapshot);
// Fire a collection event by name. The daemon assembles the configured report
// and sends S6F11. Values in `data` override current values for this one event.
rpc FireEvent(Event) returns (Ack);
// Raise (S5F1 set) or clear an alarm by name.
rpc SetAlarm(Alarm) returns (Ack);
rpc ClearAlarm(Alarm) returns (Ack);
// ---- Universal: control state -------------------------------------------
// Current GEM control state (ONLINE/LOCAL/REMOTE/OFFLINE).
rpc GetControlState(Empty) returns (ControlState);
// Request a transition — e.g. an operator panel taking the tool OFFLINE for
// maintenance, or back ONLINE. The daemon applies E30 rules and may decline.
rpc RequestControlState(ControlStateRequest) returns (Ack);
// ---- Universal: react to the host ---------------------------------------
// Subscribe to everything the host asks of this equipment. The daemon streams
// HostRequest messages for as long as the call stays open.
//
// Delivery contract (v1):
// - firehose: every subscriber receives every host request;
// - NO buffering: a command arriving while no client is subscribed is
// answered with its declarative ack from the equipment config (the
// pre-daemon behaviour) and is NOT replayed on reconnect — the daemon
// never tells the host "will finish later" for work no tool will do;
// - when a Command does arrive here, the host has ALREADY been answered
// with S2F42 HCACK=4; report the real outcome via FireEvent/SetAlarm.
rpc Subscribe(SubscribeRequest) returns (stream HostRequest);
// Report the outcome of a Command delivered on the stream, quoting its `id`.
// NOTE the contract (SEMI-conformant, non-blocking): the daemon has ALREADY
// answered the host with S2F42 HCACK=4 ("accepted, will finish later") when
// it pushed the command onto the stream — the host's transaction is closed.
// CompleteCommand therefore correlates/audits the command lifecycle; the
// host learns the real outcome via the events/alarms you fire (FireEvent /
// SetAlarm), exactly as E30 intends. A synchronous gating mode (tool decides
// the HCACK before S2F42 goes out) is a possible v2 extension.
rpc CompleteCommand(CommandResult) returns (Ack);
// ---- Jobs / Carriers: report progress of work the host asked for --------
// Keyed by the durable id (job_id / carrier_id), not a per-message id — these
// are long-lived objects you report against as the physical work proceeds.
rpc ReportProcessJob(ProcessJobState) returns (Ack); // E40 — job-based tools
// E87 — carrier-based tools. WAITING announces a physically-arrived
// carrier (creates it; idempotent — re-announce updates the slot map);
// IN_ACCESS / COMPLETE drive the access FSM. The host's S3F17 decisions
// (ProceedWithCarrier / CancelCarrier) come back on the Subscribe stream
// as CarrierAction.
rpc ReportCarrier(CarrierState) returns (Ack);
// ---- Diagnostics --------------------------------------------------------
// Live daemon/link status: distinguishes "host went offline" from "cable
// unplugged" from "spool filling up". Streams a snapshot on every change.
rpc WatchHealth(Empty) returns (stream Health);
// Everything this equipment is configured with, by name — for tooling,
// diagnostics, and client-side validation/autocomplete.
rpc Describe(Empty) returns (EquipmentDescription);
// Flush the spool: purge=true discards queued messages, purge=false drains
// them toward the host (requires a SELECTED session).
rpc FlushSpool(SpoolFlushRequest) returns (Ack);
// Equipment-initiated operator message to the host (S10F1). Fails with
// CANNOT_DO_NOW when no host is connected and stream 10 isn't spoolable.
rpc SendTerminalMessage(TerminalMessage) returns (Ack);
}
// ---- Values ----------------------------------------------------------------
// A SECS-II value in plain terms. The daemon converts it to the wire format
// declared for the target item in your config — you never choose U4/F4/ASCII.
message Value {
oneof kind {
string text = 1;
sint64 integer = 2;
double real = 3;
bool boolean = 4;
bytes binary = 5;
List list = 6; // SECS-II nested list
}
}
message List { repeated Value items = 1; }
message Empty {}
// ---- Universal: tool -> equipment ------------------------------------------
message VariableUpdate {
map<string, Value> values = 1; // name -> value
}
message VariableQuery {
repeated string names = 1; // empty = all configured variables
}
message VariableSnapshot {
map<string, Value> values = 1;
}
message Event {
string name = 1; // collection-event name
map<string, Value> data = 2; // optional per-fire variable values
}
message Alarm {
string name = 1; // alarm name
}
message SubscribeRequest {
string client = 1; // optional label for logging/diagnostics
}
// ---- Control state ---------------------------------------------------------
message ControlState {
State state = 1;
enum State {
EQUIPMENT_OFFLINE = 0;
ATTEMPT_ONLINE = 1;
HOST_OFFLINE = 2;
ONLINE_LOCAL = 3;
ONLINE_REMOTE = 4;
}
}
message ControlStateRequest {
ControlState.State desired = 1;
}
// ---- Host -> tool stream ---------------------------------------------------
// Everything the host initiates arrives here. Match on the populated variant;
// ignore the variants for tiers your equipment doesn't implement.
message HostRequest {
oneof request {
Command command = 1; // S2F41/F21/F49 — universal
ControlStateChange control_state = 2; // control state transitioned — universal
ConstantChange constant = 3; // host set an equipment constant — universal
ProcessProgram process_program = 4; // S7 recipe downloaded — recipe tools
CarrierAction carrier = 5; // E87 carrier at a port — carrier tools
ProcessJob process_job = 6; // E40 job to run/stop — job tools
}
}
// A remote command (S2F41 / S2F21 / S2F49). Answer with CompleteCommand.
message Command {
string id = 1; // correlation id — echo in CommandResult
string name = 2; // RCMD, e.g. "START"
map<string, Value> params = 3; // CPNAME -> CPVAL
}
// Control state changed (host went online/offline, operator toggled local/remote).
message ControlStateChange {
ControlState.State state = 1;
}
// The host wrote an equipment constant (S2F15). React if it tunes a process
// parameter you care about; the daemon already stored the new value.
message ConstantChange {
string name = 1;
Value value = 2;
}
// The host downloaded a recipe (S7F3). Load it into your process engine.
message ProcessProgram {
string ppid = 1; // recipe id
bytes body = 2; // recipe contents (opaque to the daemon)
}
// E87: a carrier needs attention at a load port. Reply with ReportCarrier.
message CarrierAction {
string carrier_id = 1; // CARRIERID
uint32 port = 2; // load-port number
Action action = 3;
enum Action {
VERIFY_ID = 0; // read & confirm the carrier's identity
PROCEED = 1; // begin access (open / map slots)
CANCEL = 2;
}
}
// E40: the host wants this process job run (or stopped). Report progress with
// ReportProcessJob. `recipe` + `carriers` tell you what to run and on what.
message ProcessJob {
string job_id = 1; // PRJobID
string recipe = 2; // PPID to run
Action action = 3;
repeated string carriers = 4; // material: carrier ids bound to this job
enum Action { START = 0; STOP = 1; PAUSE = 2; RESUME = 3; ABORT = 4; }
}
// ---- Tool -> equipment: replies & progress reports -------------------------
message CommandResult {
string id = 1; // the Command.id you are answering
Ack ack = 2; // outcome (maps to HCACK on the wire)
}
// Advance an E40 process job as the physical work proceeds; the daemon drives
// the E40 FSM and emits the matching S6F11 / S16F9 to the host.
message ProcessJobState {
string job_id = 1;
State state = 2;
enum State {
SETTING_UP = 0;
PROCESSING = 1;
COMPLETE = 2;
ABORTED = 3;
}
}
// Report a carrier's identity, slot map, and state as the tool reads them.
message CarrierState {
string carrier_id = 1;
uint32 port = 2;
State state = 3;
repeated bool slots = 4; // slot map: true = wafer present
enum State {
WAITING = 0;
IN_ACCESS = 1;
COMPLETE = 2;
}
}
// ---- Diagnostics -----------------------------------------------------------
message Health {
LinkState link = 1;
uint32 spool_depth = 2; // queued messages waiting for the host
ControlState.State control_state = 3;
enum LinkState {
DISCONNECTED = 0; // no TCP
CONNECTED = 1; // TCP up, not yet SELECTED
SELECTED = 2; // HSMS selected — actively talking to the host
}
}
// ---- Diagnostics & operations -----------------------------------------------
message EquipmentDescription {
string model_name = 1;
string software_rev = 2;
repeated string variables = 3; // SVID + DVID names
repeated string events = 4; // collection-event names
repeated string alarms = 5; // alarm names (or stringified ALIDs)
repeated string commands = 6; // RCMDs the host may send
repeated string constants = 7; // equipment-constant names
}
message SpoolFlushRequest {
bool purge = 1; // true = discard, false = drain to host
}
message TerminalMessage {
uint32 tid = 1; // terminal id (0 = main)
string text = 2;
}
// ---- Acknowledgement -------------------------------------------------------
// Mirrors SEMI HCACK exactly. For non-command RPCs, only ACCEPT vs an error
// code matters; `message` carries human detail ("no variable named 'presure'").
message Ack {
Code code = 1;
string message = 2;
enum Code {
ACCEPT = 0; // HCACK 0
INVALID_COMMAND = 1; // HCACK 1
CANNOT_DO_NOW = 2; // HCACK 2
PARAMETER_INVALID = 3; // HCACK 3
ACCEPTED_WILL_FINISH_LATER = 4; // HCACK 4
REJECTED = 5; // HCACK 5
INVALID_OBJECT = 6; // HCACK 6
}
}