K2: SubstrateIDStatus (third E90 axis)

Adds the substrate-ID verification FSM that E90 §6.4.6 calls for:

  NotConfirmed   initial; equipment hasn't read the ID yet
  WaitingForHost ID has been read; awaiting host accept/reject
  Confirmed      host confirmed (or force-bound)
  Mismatched     host rejected — recoverable via Bind

Events:
  Read     NotConfirmed -> WaitingForHost
  Confirm  WaitingForHost -> Confirmed
  Mismatch WaitingForHost -> Mismatched
  Bind     any -> Confirmed (force-bind)
  Reset    any -> NotConfirmed

Wire-byte values pinned via static_assert.  The third axis is now
exposed on SubstrateStateMachine alongside location_state() and
processing_state(); set_id_handler() observes transitions.  Existing
two-axis API is unchanged.

4 new test cases cover the happy path, Mismatch+Bind recovery, Reset
from any state, and same-state event handler suppression.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-06-08 08:46:10 +02:00
parent a28a8b5982
commit a52d44ade5
3 changed files with 134 additions and 1 deletions
+58 -1
View File
@@ -13,6 +13,10 @@ static_assert(static_cast<uint8_t>(SubstrateProcessingState::Stopped) ==
static_assert(static_cast<uint8_t>(SubstrateProcessingState::Rejected) == 5);
static_assert(static_cast<uint8_t>(SubstrateProcessingState::Lost) == 6);
static_assert(static_cast<uint8_t>(SubstrateProcessingState::Skipped) == 7);
static_assert(static_cast<uint8_t>(SubstrateIDStatus::NotConfirmed) == 0);
static_assert(static_cast<uint8_t>(SubstrateIDStatus::WaitingForHost) == 1);
static_assert(static_cast<uint8_t>(SubstrateIDStatus::Confirmed) == 2);
static_assert(static_cast<uint8_t>(SubstrateIDStatus::Mismatched) == 3);
const char* substrate_state_name(SubstrateState s) {
switch (s) {
@@ -56,6 +60,28 @@ const char* substrate_event_name(SubstrateEvent e) {
return "?";
}
const char* substrate_id_status_name(SubstrateIDStatus s) {
switch (s) {
case SubstrateIDStatus::NotConfirmed: return "NotConfirmed";
case SubstrateIDStatus::WaitingForHost: return "WaitingForHost";
case SubstrateIDStatus::Confirmed: return "Confirmed";
case SubstrateIDStatus::Mismatched: return "Mismatched";
case SubstrateIDStatus::NoState: return "NoState";
}
return "?";
}
const char* substrate_id_event_name(SubstrateIDEvent e) {
switch (e) {
case SubstrateIDEvent::Read: return "Read";
case SubstrateIDEvent::Confirm: return "Confirm";
case SubstrateIDEvent::Mismatch: return "Mismatch";
case SubstrateIDEvent::Bind: return "Bind";
case SubstrateIDEvent::Reset: return "Reset";
}
return "?";
}
const char* substrate_processing_event_name(SubstrateProcessingEvent e) {
switch (e) {
case SubstrateProcessingEvent::StartProcessing: return "StartProcessing";
@@ -97,9 +123,29 @@ SubstrateProcessingTable default_substrate_processing_table() {
return t;
}
SubstrateIDTable default_substrate_id_table() {
using S = SubstrateIDStatus;
using E = SubstrateIDEvent;
SubstrateIDTable t;
t.add({S::NotConfirmed, E::Read, S::WaitingForHost, std::nullopt});
t.add({S::WaitingForHost, E::Confirm, S::Confirmed, std::nullopt});
t.add({S::WaitingForHost, E::Mismatch, S::Mismatched, std::nullopt});
t.add({S::Mismatched, E::Bind, S::Confirmed, std::nullopt});
// Bind from any state force-confirms.
t.add({S::NotConfirmed, E::Bind, S::Confirmed, std::nullopt});
t.add({S::WaitingForHost, E::Bind, S::Confirmed, std::nullopt});
// Reset returns any state to NotConfirmed.
for (auto src : {S::NotConfirmed, S::WaitingForHost,
S::Confirmed, S::Mismatched}) {
t.add({src, E::Reset, S::NotConfirmed, std::nullopt});
}
return t;
}
SubstrateStateMachine::SubstrateStateMachine()
: loc_table_(default_substrate_table()),
proc_table_(default_substrate_processing_table()) {}
proc_table_(default_substrate_processing_table()),
id_table_(default_substrate_id_table()) {}
bool SubstrateStateMachine::on_location_event(SubstrateEvent e) {
const auto* row = loc_table_.find(loc_, e);
@@ -123,4 +169,15 @@ bool SubstrateStateMachine::on_processing_event(SubstrateProcessingEvent e) {
return true;
}
bool SubstrateStateMachine::on_id_event(SubstrateIDEvent e) {
const auto* row = id_table_.find(id_, e);
if (!row) return false;
if (row->to && *row->to != id_) {
auto prev = id_;
id_ = *row->to;
if (on_id_) on_id_(prev, id_, e);
}
return true;
}
} // namespace secsgem::gem