K1: SubstrateHistory ring buffer per substrate
Each Substrate now retains an append-only history of state transitions (both location and processing axes), the triggering event captured as a std::variant<SubstrateEvent, SubstrateProcessingEvent>, 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 <noreply@anthropic.com>
This commit is contained in:
@@ -1,22 +1,38 @@
|
||||
#pragma once
|
||||
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <variant>
|
||||
#include <vector>
|
||||
|
||||
#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<SubstrateEvent, SubstrateProcessingEvent> 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<SubstrateHistoryEntry> history;
|
||||
std::unique_ptr<SubstrateStateMachine> 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<SubstrateHistoryEntry>* history(const std::string& id) const {
|
||||
const auto* s = get(id);
|
||||
return s ? &s->history : nullptr;
|
||||
}
|
||||
|
||||
private:
|
||||
template <typename Event>
|
||||
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<std::string, Substrate> subs_;
|
||||
LocationChangeHandler on_loc_;
|
||||
ProcessingChangeHandler on_proc_;
|
||||
std::size_t history_limit_ = 256;
|
||||
};
|
||||
|
||||
} // namespace secsgem::gem
|
||||
|
||||
@@ -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<std::pair<std::string, SubstrateState>> loc_log;
|
||||
|
||||
Reference in New Issue
Block a user