From a28a8b59826c9c776e6142739966eb8d9bf67b27 Mon Sep 17 00:00:00 2001 From: Raphael Maenle Date: Mon, 8 Jun 2026 08:43:24 +0200 Subject: [PATCH] K1: SubstrateHistory ring buffer per substrate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Each Substrate now retains an append-only history of state transitions (both location and processing axes), the triggering event captured as a std::variant, the location label at the time, and a steady_clock timestamp. E90 §6.6 requires the equipment to be able to report a wafer's processing history — typically queried via S6F11 batched reports or SVID reads. This commit lays the runtime substrate; wire query plumbing is the natural follow-up. set_history_limit(n) caps per-substrate retention (default 256, 0 = unbounded). Oldest entries are dropped when the cap is reached; vector-erase is fine at this scale (typical wafer lifecycle is a few dozen transitions). Two new test cases cover the recording invariants (every fire results in one history entry on the right axis) and history_limit eviction. Co-Authored-By: Claude Opus 4.7 --- include/secsgem/gem/store/substrates.hpp | 52 +++++++++++++++++++++++- tests/test_substrates.cpp | 36 ++++++++++++++++ 2 files changed, 87 insertions(+), 1 deletion(-) diff --git a/include/secsgem/gem/store/substrates.hpp b/include/secsgem/gem/store/substrates.hpp index dca2d54..8dbb074 100644 --- a/include/secsgem/gem/store/substrates.hpp +++ b/include/secsgem/gem/store/substrates.hpp @@ -1,22 +1,38 @@ #pragma once +#include #include #include #include #include #include #include +#include #include #include "secsgem/gem/substrate_state.hpp" namespace secsgem::gem { +// One entry in a substrate's per-axis transition log. Holds the +// post-transition state on one axis (the other stays at NoState), +// the triggering event encoded back into the SubstrateEvent or +// SubstrateProcessingEvent enum (held discriminated by `is_processing`), +// the location string at the moment, and a steady_clock timestamp. +struct SubstrateHistoryEntry { + std::chrono::steady_clock::time_point at; + SubstrateState location_to{SubstrateState::NoState}; + SubstrateProcessingState processing_to{SubstrateProcessingState::NoState}; + std::variant event; + std::string location_label; // free-form location at the time of the event +}; + struct Substrate { std::string substid; // SUBSTID — E90 wafer identifier std::string carrierid; // CARRIERID where this wafer originated uint8_t slot = 0; // 1-based slot in the source carrier std::string location; // free-form: equipment-defined module name + std::vector history; std::unique_ptr fsm; }; @@ -42,6 +58,12 @@ class SubstrateStore { enum class CreateResult { Created, Denied_AlreadyExists }; + // Cap per-substrate history retention to keep memory bounded. 0 == + // unlimited. Default cap is 256 entries which comfortably covers a + // typical wafer's lifecycle. + void set_history_limit(std::size_t n) { history_limit_ = n; } + std::size_t history_limit() const { return history_limit_; } + CreateResult create(std::string substid, std::string carrierid = "", uint8_t slot = 0) { if (subs_.count(substid)) return CreateResult::Denied_AlreadyExists; @@ -49,15 +71,18 @@ class SubstrateStore { const std::string id = substid; fsm->set_location_handler( [this, id](SubstrateState f, SubstrateState t, SubstrateEvent e) { + record_history(id, t, SubstrateProcessingState::NoState, e); if (on_loc_) on_loc_(id, f, t, e); }); fsm->set_processing_handler( [this, id](SubstrateProcessingState f, SubstrateProcessingState t, SubstrateProcessingEvent e) { + record_history(id, SubstrateState::NoState, t, e); if (on_proc_) on_proc_(id, f, t, e); }); subs_.emplace(substid, - Substrate{substid, std::move(carrierid), slot, "", std::move(fsm)}); + Substrate{substid, std::move(carrierid), slot, "", + {}, std::move(fsm)}); return CreateResult::Created; } @@ -92,10 +117,35 @@ class SubstrateStore { return out; } + const std::vector* history(const std::string& id) const { + const auto* s = get(id); + return s ? &s->history : nullptr; + } + private: + template + void record_history(const std::string& id, SubstrateState loc_to, + SubstrateProcessingState proc_to, Event ev) { + auto* s = get(id); + if (!s) return; + SubstrateHistoryEntry entry; + entry.at = std::chrono::steady_clock::now(); + entry.location_to = loc_to; + entry.processing_to = proc_to; + entry.event = ev; + entry.location_label = s->location; + s->history.push_back(std::move(entry)); + if (history_limit_ != 0 && s->history.size() > history_limit_) { + // Drop the oldest entry; vector shift is fine for the small caps + // we expect (default 256, typically a few dozen per substrate). + s->history.erase(s->history.begin()); + } + } + std::map subs_; LocationChangeHandler on_loc_; ProcessingChangeHandler on_proc_; + std::size_t history_limit_ = 256; }; } // namespace secsgem::gem diff --git a/tests/test_substrates.cpp b/tests/test_substrates.cpp index 2bc37f2..7ad5a0e 100644 --- a/tests/test_substrates.cpp +++ b/tests/test_substrates.cpp @@ -71,6 +71,42 @@ TEST_CASE("SubstrateStore: create + dedup + location update") { CHECK(s.get("W-001")->fsm->location_state() == SubstrateState::AtWork); } +TEST_CASE("SubstrateStore: history records every transition") { + SubstrateStore s; + s.create("W-H", "CAR-X", 3); + s.fire_location_event("W-H", SubstrateEvent::Acquire, "MOD-A"); + s.fire_processing_event("W-H", SubstrateProcessingEvent::StartProcessing); + s.fire_processing_event("W-H", SubstrateProcessingEvent::EndProcessing); + s.fire_location_event("W-H", SubstrateEvent::Release, "DEST"); + const auto* hist = s.history("W-H"); + REQUIRE(hist != nullptr); + CHECK(hist->size() == 4); + // First entry is a location transition to AtWork. + CHECK((*hist)[0].location_to == SubstrateState::AtWork); + CHECK((*hist)[0].processing_to == SubstrateProcessingState::NoState); + CHECK((*hist)[0].location_label == "MOD-A"); + // Third entry is a processing transition to Processed. + CHECK((*hist)[2].processing_to == SubstrateProcessingState::Processed); + CHECK((*hist)[2].location_to == SubstrateState::NoState); +} + +TEST_CASE("SubstrateStore: history_limit caps retention") { + SubstrateStore s; + s.set_history_limit(3); + s.create("W-CAP"); + // Generate more transitions than the cap. + s.fire_location_event("W-CAP", SubstrateEvent::Acquire); + s.fire_location_event("W-CAP", SubstrateEvent::Return); + s.fire_location_event("W-CAP", SubstrateEvent::Acquire); + s.fire_location_event("W-CAP", SubstrateEvent::Release); + const auto* hist = s.history("W-CAP"); + REQUIRE(hist != nullptr); + CHECK(hist->size() == 3); + // Oldest entry (Acquire to AtWork) should be evicted; first remaining + // entry is the Return. + CHECK((*hist)[0].location_to == SubstrateState::AtSource); +} + TEST_CASE("SubstrateStore: change handlers observe both axes independently") { SubstrateStore s; std::vector> loc_log;