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:
@@ -735,3 +735,71 @@ TEST_CASE("randomized concurrent RPC stress (seeded)") {
|
||||
server->Shutdown();
|
||||
rt.stop();
|
||||
}
|
||||
|
||||
// Phase 16 tail: E90 substrate + E157 module reporting (observe-and-report).
|
||||
TEST_CASE("ReportSubstrate (E90) and ReportModule (E157) drive the FSMs") {
|
||||
gem::EquipmentRuntime rt(test_config());
|
||||
gem::register_default_handlers(rt);
|
||||
dmn::EquipmentService svc(rt);
|
||||
rt.run_async();
|
||||
|
||||
grpc::ServerBuilder builder;
|
||||
builder.RegisterService(&svc);
|
||||
std::unique_ptr<grpc::Server> server(builder.BuildAndStart());
|
||||
REQUIRE(server);
|
||||
auto stub = pb::Equipment::NewStub(server->InProcessChannel(grpc::ChannelArguments{}));
|
||||
|
||||
auto sub = [&](const std::string& sid, pb::SubstrateReport::Milestone m) {
|
||||
grpc::ClientContext ctx;
|
||||
pb::SubstrateReport req;
|
||||
pb::Ack ack;
|
||||
req.set_substrate_id(sid);
|
||||
req.set_milestone(m);
|
||||
REQUIRE(stub->ReportSubstrate(&ctx, req, &ack).ok());
|
||||
return ack.code();
|
||||
};
|
||||
|
||||
// A wafer's journey: arrive -> picked up -> process -> done -> deposited.
|
||||
CHECK(sub("WFR-1", pb::SubstrateReport::ARRIVED) == pb::Ack::ACCEPT);
|
||||
CHECK(sub("WFR-1", pb::SubstrateReport::AT_WORK) == pb::Ack::ACCEPT);
|
||||
CHECK(sub("WFR-1", pb::SubstrateReport::PROCESSING) == pb::Ack::ACCEPT);
|
||||
CHECK(sub("WFR-1", pb::SubstrateReport::PROCESSED) == pb::Ack::ACCEPT);
|
||||
CHECK(sub("WFR-1", pb::SubstrateReport::AT_DESTINATION) == pb::Ack::ACCEPT);
|
||||
auto loc = rt.read_sync([&rt]() {
|
||||
const auto* s = rt.model().substrates.get("WFR-1");
|
||||
return s ? s->fsm->location_state() : gem::SubstrateState::NoState;
|
||||
});
|
||||
REQUIRE(loc.has_value());
|
||||
CHECK(*loc == gem::SubstrateState::AtDestination);
|
||||
|
||||
// Reporting on a substrate that never ARRIVED is rejected.
|
||||
CHECK(sub("WFR-GHOST", pb::SubstrateReport::AT_WORK) == pb::Ack::INVALID_OBJECT);
|
||||
|
||||
auto mod = [&](const std::string& mid, pb::ModuleReport::State st) {
|
||||
grpc::ClientContext ctx;
|
||||
pb::ModuleReport req;
|
||||
pb::Ack ack;
|
||||
req.set_module_id(mid);
|
||||
req.set_state(st);
|
||||
REQUIRE(stub->ReportModule(&ctx, req, &ack).ok());
|
||||
return ack.code();
|
||||
};
|
||||
|
||||
// Module: auto-created, then walked General -> Step -> StepCompleted.
|
||||
CHECK(mod("MOD-1", pb::ModuleReport::GENERAL_EXECUTING) == pb::Ack::ACCEPT);
|
||||
CHECK(mod("MOD-1", pb::ModuleReport::STEP_EXECUTING) == pb::Ack::ACCEPT);
|
||||
CHECK(mod("MOD-1", pb::ModuleReport::STEP_COMPLETED) == pb::Ack::ACCEPT);
|
||||
auto mstate = rt.read_sync([&rt]() {
|
||||
const auto* m = rt.model().modules.get("MOD-1");
|
||||
return m ? m->fsm->state() : gem::ModuleState::NotExecuting;
|
||||
});
|
||||
REQUIRE(mstate.has_value());
|
||||
CHECK(*mstate == gem::ModuleState::StepCompleted);
|
||||
|
||||
// An illegal jump (StepExecuting straight from a fresh NotExecuting module)
|
||||
// is rejected by the E157 table.
|
||||
CHECK(mod("MOD-2", pb::ModuleReport::STEP_EXECUTING) == pb::Ack::CANNOT_DO_NOW);
|
||||
|
||||
server->Shutdown();
|
||||
rt.stop();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user