#pragma once #include #include #include #include #include #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; std::filesystem::path journal_path; // empty when persistence disabled }; class SubstrateStore { public: using LocationChangeHandler = std::function; using ProcessingChangeHandler = std::function; SubstrateStore() = default; SubstrateStore(const SubstrateStore&) = delete; SubstrateStore& operator=(const SubstrateStore&) = delete; SubstrateStore(SubstrateStore&&) = delete; SubstrateStore& operator=(SubstrateStore&&) = delete; void set_location_handler(LocationChangeHandler h) { on_loc_ = std::move(h); } void set_processing_handler(ProcessingChangeHandler h) { on_proc_ = std::move(h); } 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; install_(substid, carrierid, slot, "", std::nullopt, std::nullopt, std::nullopt); if (persistent_) write_record_(substid); return CreateResult::Created; } bool has(const std::string& id) const { return subs_.count(id) > 0; } const Substrate* get(const std::string& id) const { auto it = subs_.find(id); return it == subs_.end() ? nullptr : &it->second; } Substrate* get(const std::string& id) { auto it = subs_.find(id); return it == subs_.end() ? nullptr : &it->second; } bool fire_location_event(const std::string& id, SubstrateEvent e, std::string new_location = "") { auto* s = get(id); if (!s) return false; if (!new_location.empty()) s->location = std::move(new_location); bool ok = s->fsm->on_location_event(e); if (persistent_) write_record_(id); return ok; } bool fire_processing_event(const std::string& id, SubstrateProcessingEvent e) { auto* s = get(id); if (!(s && s->fsm->on_processing_event(e))) return false; if (persistent_) write_record_(id); return true; } bool remove(const std::string& id) { auto it = subs_.find(id); if (it == subs_.end()) return false; if (persistent_ && !it->second.journal_path.empty()) { std::error_code ec; std::filesystem::remove(it->second.journal_path, ec); } subs_.erase(it); return true; } std::size_t size() const { return subs_.size(); } std::vector ids() const { std::vector out; out.reserve(subs_.size()); for (const auto& kv : subs_) out.push_back(kv.first); return out; } const std::vector* history(const std::string& id) const { const auto* s = get(id); return s ? &s->history : nullptr; } // ---- Persistence ---------------------------------------------------- // v2 records also journal the per-substrate transition history. // Each entry's `at` field is written as a milliseconds-since-UNIX // epoch wall-clock timestamp; on replay the steady_clock time_point // is reconstructed relative to the *current* steady_now, so the // delta between two replayed entries matches their original spacing. // Absolute wall-clock accuracy depends on system_clock not having // been NTP-stepped between write and read. void enable_persistence(std::filesystem::path dir) { namespace fs = std::filesystem; journal_dir_ = std::move(dir); persistent_ = true; std::error_code ec; fs::create_directories(journal_dir_, ec); std::vector> existing; for (auto& entry : fs::directory_iterator(journal_dir_, ec)) { if (!entry.is_regular_file()) continue; const auto& p = entry.path(); if (p.extension() != ".sub") continue; uint64_t seq = 0; try { seq = std::stoull(p.stem().string()); } catch (...) { continue; } existing.emplace_back(seq, p); } std::sort(existing.begin(), existing.end(), [](const auto& a, const auto& b) { return a.first < b.first; }); for (auto& [seq, path] : existing) { auto rec = load_record_(path); if (!rec) { fs::remove(path, ec); continue; } install_(rec->substid, rec->carrierid, rec->slot, rec->location, rec->loc_state, rec->proc_state, rec->id_state); auto* s = get(rec->substid); if (s) { s->journal_path = path; s->history = restore_history_(rec->history); } if (seq >= next_seq_) next_seq_ = seq + 1; } } bool persistence_enabled() const { return persistent_; } std::filesystem::path journal_dir() const { return journal_dir_; } private: // Persisted history entry — system_clock millis at write time, plus // axis bytes and a small variant discriminator. struct PersistedHistoryEntry { int64_t system_ms_at_write; SubstrateState location_to; SubstrateProcessingState processing_to; uint8_t event_kind; // 0 = SubstrateEvent, 1 = SubstrateProcessingEvent uint8_t event_value; // raw enum value std::string location_label; }; struct Record { std::string substid; std::string carrierid; uint8_t slot; std::string location; SubstrateState loc_state; SubstrateProcessingState proc_state; SubstrateIDStatus id_state; std::vector history; }; // Substrate record (v2): // v1 header (kept) + v2 trailer: // [u8 magic = 0xC6][u8 version] // [u8 loc_state][u8 proc_state][u8 id_state][u8 slot] // [u16 substid_len][substid] // [u16 carrierid_len][carrierid] // [u16 location_len][location] // --- v2 trailer --- // [u32 history_count] // [repeat: i64 system_ms, u8 loc_to, u8 proc_to, u8 kind, u8 event, // u16 label_len, label] // v1 records accepted; history comes back empty. static constexpr uint8_t kMagic = 0xC6; static constexpr uint8_t kVersion = 0x02; void install_(const std::string& substid, const std::string& carrierid, uint8_t slot, const std::string& location, std::optional loc, std::optional proc, std::optional id) { auto fsm = std::make_unique(); if (loc || proc || id) { fsm->restore_state(loc.value_or(SubstrateState::AtSource), proc.value_or(SubstrateProcessingState::NeedsProcessing), id.value_or(SubstrateIDStatus::NotConfirmed)); } const std::string keyed = substid; fsm->set_location_handler( [this, keyed](SubstrateState f, SubstrateState t, SubstrateEvent e) { record_history(keyed, t, SubstrateProcessingState::NoState, e); if (on_loc_) on_loc_(keyed, f, t, e); }); fsm->set_processing_handler( [this, keyed](SubstrateProcessingState f, SubstrateProcessingState t, SubstrateProcessingEvent e) { record_history(keyed, SubstrateState::NoState, t, e); if (on_proc_) on_proc_(keyed, f, t, e); }); subs_.emplace(substid, Substrate{substid, carrierid, slot, location, {}, std::move(fsm), {}}); } void write_record_(const std::string& substid) { namespace fs = std::filesystem; auto* s = get(substid); if (!s) return; if (s->journal_path.empty()) { char namebuf[32]; std::snprintf(namebuf, sizeof(namebuf), "%010llu.sub", static_cast(next_seq_++)); s->journal_path = journal_dir_ / namebuf; } fs::path tmp = s->journal_path; tmp += ".tmp"; std::ofstream out(tmp, std::ios::binary | std::ios::trunc); if (!out) return; uint8_t header[6] = { kMagic, kVersion, static_cast(s->fsm->location_state()), static_cast(s->fsm->processing_state()), static_cast(s->fsm->id_state()), s->slot}; out.write(reinterpret_cast(header), sizeof(header)); auto write_str = [&](const std::string& str) { uint16_t n = static_cast(str.size()); uint8_t lb[2] = {static_cast(n >> 8), static_cast(n)}; out.write(reinterpret_cast(lb), 2); out.write(str.data(), static_cast(n)); }; write_str(s->substid); write_str(s->carrierid); write_str(s->location); // --- v2 history trailer --- auto write_be32 = [&](uint32_t v) { uint8_t b[4] = {static_cast((v >> 24) & 0xFF), static_cast((v >> 16) & 0xFF), static_cast((v >> 8) & 0xFF), static_cast(v & 0xFF)}; out.write(reinterpret_cast(b), 4); }; auto write_be64 = [&](int64_t v) { uint64_t u = static_cast(v); for (int i = 7; i >= 0; --i) { uint8_t byte = static_cast((u >> (i * 8)) & 0xFF); out.put(static_cast(byte)); } }; using clk = std::chrono::steady_clock; using sclk = std::chrono::system_clock; const auto steady_now = clk::now(); const auto system_now_ms = std::chrono::duration_cast( sclk::now().time_since_epoch()).count(); write_be32(static_cast(s->history.size())); for (const auto& h : s->history) { const auto elapsed_ms = std::chrono::duration_cast( h.at - steady_now).count(); const int64_t system_ms_for_entry = system_now_ms + elapsed_ms; write_be64(system_ms_for_entry); out.put(static_cast(h.location_to)); out.put(static_cast(h.processing_to)); const uint8_t kind = std::holds_alternative(h.event) ? 0 : 1; out.put(static_cast(kind)); uint8_t ev_val = 0; if (kind == 0) { ev_val = static_cast(std::get(h.event)); } else { ev_val = static_cast(std::get(h.event)); } out.put(static_cast(ev_val)); uint16_t lab_len = static_cast(h.location_label.size()); uint8_t lb[2] = {static_cast(lab_len >> 8), static_cast(lab_len)}; out.write(reinterpret_cast(lb), 2); out.write(h.location_label.data(), static_cast(lab_len)); } out.flush(); out.close(); std::error_code ec; fs::rename(tmp, s->journal_path, ec); if (ec) fs::remove(tmp, ec); } // Reconstruct steady_clock::time_point from persisted system_clock millis // by anchoring to the current (now_steady, now_system) pair. Wall-clock // jumps between write and read will skew absolute values; deltas stay // accurate. static std::vector restore_history_( const std::vector& src) { std::vector out; out.reserve(src.size()); using clk = std::chrono::steady_clock; using sclk = std::chrono::system_clock; const auto steady_now = clk::now(); const auto system_now_ms = std::chrono::duration_cast( sclk::now().time_since_epoch()).count(); for (const auto& p : src) { SubstrateHistoryEntry e; const int64_t delta_ms = p.system_ms_at_write - system_now_ms; e.at = steady_now + std::chrono::milliseconds(delta_ms); e.location_to = p.location_to; e.processing_to = p.processing_to; e.location_label = p.location_label; if (p.event_kind == 0) { e.event = static_cast(p.event_value); } else { e.event = static_cast(p.event_value); } out.push_back(std::move(e)); } return out; } static std::optional load_record_(const std::filesystem::path& p) { std::ifstream in(p, std::ios::binary); if (!in) return std::nullopt; uint8_t header[6]; in.read(reinterpret_cast(header), sizeof(header)); if (!in || header[0] != kMagic) return std::nullopt; if (header[1] != 0x01 && header[1] != 0x02) return std::nullopt; const uint8_t version = header[1]; Record r; r.loc_state = static_cast(header[2]); r.proc_state = static_cast(header[3]); r.id_state = static_cast(header[4]); r.slot = header[5]; auto read_str = [&](std::string& out) { uint8_t lb[2]; in.read(reinterpret_cast(lb), 2); if (!in) return false; uint16_t n = static_cast((uint16_t(lb[0]) << 8) | lb[1]); out.resize(n); in.read(out.data(), n); return static_cast(in) || n == 0; }; if (!read_str(r.substid)) return std::nullopt; if (!read_str(r.carrierid)) return std::nullopt; if (!read_str(r.location)) return std::nullopt; if (version >= 2) { auto read_be32 = [&](uint32_t& v) { uint8_t b[4]; in.read(reinterpret_cast(b), 4); if (!in) return false; v = (uint32_t(b[0]) << 24) | (uint32_t(b[1]) << 16) | (uint32_t(b[2]) << 8) | uint32_t(b[3]); return true; }; auto read_be64 = [&](int64_t& v) { uint8_t b[8]; in.read(reinterpret_cast(b), 8); if (!in) return false; uint64_t u = 0; for (int i = 0; i < 8; ++i) u = (u << 8) | b[i]; v = static_cast(u); return true; }; uint32_t count = 0; if (!read_be32(count)) return std::nullopt; r.history.resize(count); for (auto& h : r.history) { if (!read_be64(h.system_ms_at_write)) return std::nullopt; int b0 = in.get(); if (b0 == EOF) return std::nullopt; int b1 = in.get(); if (b1 == EOF) return std::nullopt; int b2 = in.get(); if (b2 == EOF) return std::nullopt; int b3 = in.get(); if (b3 == EOF) return std::nullopt; h.location_to = static_cast(b0); h.processing_to = static_cast(b1); h.event_kind = static_cast(b2); h.event_value = static_cast(b3); if (!read_str(h.location_label)) return std::nullopt; } } return r; } 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; bool persistent_ = false; std::filesystem::path journal_dir_; uint64_t next_seq_ = 0; }; } // namespace secsgem::gem