feat(client)+feat(daemon): eq.names, @eq.command, E90/E157 RPCs
Python client:
- eq.names.event.* / .alarm.* / .command.* / .var.* / .constant.* —
autocomplete-able, typo-safe name lookup backed by the Describe RPC
(lazy, cached; AttributeError on bad name with close-match hints)
- @eq.command decorator — binds a handler by function name, validated
against the equipment's real command set at decoration time
- eq.report_substrate() — E90 wafer milestone reporting
- eq.report_module() — E157 module state reporting (auto-create)
Daemon (C++ service):
- ReportSubstrate RPC — drives E90 location + processing FSMs
- ReportModule RPC — drives E157 module FSM (auto-create on first report)
- ack_from_outcome() helper — consistent Ack mapping for read_sync results
Proto: SubstrateReport, ModuleReport, EquipmentDescription,
SpoolFlushRequest, TerminalMessage; Describe, FlushSpool,
SendTerminalMessage RPCs
Tests: C++ FSM test (journey + ghost rejection + E157 illegal jump);
interop coverage for names API and E90/E157 round-trip
Docs: ch42 RPC table + Python example updated; ch16 daemon-path section added
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -527,6 +527,70 @@ class EquipmentService final : public pb::Equipment::Service {
|
||||
return grpc::Status::OK;
|
||||
}
|
||||
|
||||
grpc::Status ReportSubstrate(grpc::ServerContext*, const pb::SubstrateReport* req,
|
||||
pb::Ack* resp) override {
|
||||
const std::string sid = req->substrate_id();
|
||||
const std::string cid = req->carrier_id();
|
||||
const auto slot = static_cast<uint8_t>(req->slot());
|
||||
const auto m = req->milestone();
|
||||
auto outcome = rt_.read_sync([this, sid, cid, slot, m]() -> std::optional<bool> {
|
||||
auto& subs = rt_.model().substrates;
|
||||
if (m == pb::SubstrateReport::ARRIVED) {
|
||||
subs.create(sid, cid, slot); // AtSource / NeedsProcessing
|
||||
return true;
|
||||
}
|
||||
if (!subs.has(sid)) return std::nullopt;
|
||||
switch (m) {
|
||||
case pb::SubstrateReport::AT_WORK:
|
||||
return subs.fire_location_event(sid, gem::SubstrateEvent::Acquire, "");
|
||||
case pb::SubstrateReport::AT_DESTINATION:
|
||||
return subs.fire_location_event(sid, gem::SubstrateEvent::Release, "");
|
||||
case pb::SubstrateReport::PROCESSING:
|
||||
return subs.fire_processing_event(sid, gem::SubstrateProcessingEvent::StartProcessing);
|
||||
case pb::SubstrateReport::PROCESSED:
|
||||
return subs.fire_processing_event(sid, gem::SubstrateProcessingEvent::EndProcessing);
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
});
|
||||
ack_from_outcome(outcome, resp, "substrate '" + sid + "'",
|
||||
"E90 table rejects that transition");
|
||||
return grpc::Status::OK;
|
||||
}
|
||||
|
||||
grpc::Status ReportModule(grpc::ServerContext*, const pb::ModuleReport* req,
|
||||
pb::Ack* resp) override {
|
||||
const std::string mid = req->module_id();
|
||||
const auto st = req->state();
|
||||
auto outcome = rt_.read_sync([this, mid, st]() -> bool {
|
||||
auto& mods = rt_.model().modules;
|
||||
if (!mods.has(mid)) mods.create(mid); // auto-create; starts NotExecuting
|
||||
switch (st) {
|
||||
case pb::ModuleReport::GENERAL_EXECUTING:
|
||||
return mods.fire(mid, gem::ModuleEvent::StartGeneral);
|
||||
case pb::ModuleReport::STEP_EXECUTING:
|
||||
return mods.fire(mid, gem::ModuleEvent::StartStep);
|
||||
case pb::ModuleReport::STEP_COMPLETED:
|
||||
return mods.fire(mid, gem::ModuleEvent::CompleteStep);
|
||||
case pb::ModuleReport::NOT_EXECUTING:
|
||||
return mods.fire(mid, gem::ModuleEvent::Reset);
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
});
|
||||
// Module auto-creates, so "not found" can't happen; only the FSM verdict.
|
||||
if (!outcome) {
|
||||
resp->set_code(pb::Ack::CANNOT_DO_NOW);
|
||||
resp->set_message("engine io thread did not answer");
|
||||
} else if (!*outcome) {
|
||||
resp->set_code(pb::Ack::CANNOT_DO_NOW);
|
||||
resp->set_message("E157 table rejects that transition from the current state");
|
||||
} else {
|
||||
resp->set_code(pb::Ack::ACCEPT);
|
||||
}
|
||||
return grpc::Status::OK;
|
||||
}
|
||||
|
||||
grpc::Status Describe(grpc::ServerContext*, const pb::Empty*,
|
||||
pb::EquipmentDescription* resp) override {
|
||||
resp->set_model_name(descr_model_);
|
||||
@@ -653,6 +717,26 @@ class EquipmentService final : public pb::Equipment::Service {
|
||||
}
|
||||
|
||||
private:
|
||||
// Map a read_sync(std::optional<bool>) result to an Ack: outer nullopt =
|
||||
// io thread didn't answer; inner nullopt = object not found; false = FSM
|
||||
// rejected; true = accepted.
|
||||
void ack_from_outcome(const std::optional<std::optional<bool>>& outcome,
|
||||
pb::Ack* resp, const std::string& what,
|
||||
const std::string& reject_msg) {
|
||||
if (!outcome) {
|
||||
resp->set_code(pb::Ack::CANNOT_DO_NOW);
|
||||
resp->set_message("engine io thread did not answer (not running?)");
|
||||
} else if (!*outcome) {
|
||||
resp->set_code(pb::Ack::INVALID_OBJECT);
|
||||
resp->set_message("no " + what);
|
||||
} else if (!**outcome) {
|
||||
resp->set_code(pb::Ack::CANNOT_DO_NOW);
|
||||
resp->set_message(reject_msg);
|
||||
} else {
|
||||
resp->set_code(pb::Ack::ACCEPT);
|
||||
}
|
||||
}
|
||||
|
||||
// Fan a HostRequest out to every subscriber (fire-and-forget
|
||||
// notifications: jobs, recipes, EC changes). No subscriber = dropped,
|
||||
// matching the no-buffering contract.
|
||||
|
||||
Reference in New Issue
Block a user