D3: CarrierStore + LoadPortStore

Per-CARRIERID and per-PORTID stores wrap the D1 FSMs, mirroring the
ProcessJobStore / ExceptionStore pattern: heap-allocated state machines
keyed in a std::map, non-movable to keep this-capture lambdas safe,
synthetic create() that wires per-row change handlers into the store's
top-level callbacks.

CarrierStore:
  create(carrierid, port_id, capacity)  — default 25-slot map
  fire_id_event / fire_slot_map_event / fire_access_event
  set_id_handler / set_slot_map_handler / set_access_handler

LoadPortStore:
  create(port_id)
  associate(pid, carrierid) / disassociate(pid)
  fire_transfer_event / fire_reservation_event
  set_transfer_handler / set_reservation_handler / set_association_handler

Both join EquipmentDataModel alongside process_jobs / control_jobs /
exceptions.  Six test cases cover create-dedup, ID-status change
observation, slot-map / access independence, port association,
transfer lifecycle, and reservation handler firing.

Server-side dispatch (S3F17 -> CarrierStore::fire_id_event, S3F25 ->
LoadPortStore transfer) lands in D4.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-06-08 00:14:50 +02:00
parent 5f1444c756
commit 7668ceaae4
4 changed files with 306 additions and 0 deletions
+220
View File
@@ -0,0 +1,220 @@
#pragma once
#include <cstdint>
#include <functional>
#include <map>
#include <memory>
#include <optional>
#include <string>
#include <utility>
#include <vector>
#include "secsgem/gem/carrier_state.hpp"
#include "secsgem/gem/load_port_state.hpp"
namespace secsgem::gem {
// One slot in a carrier slot map. E87-0716 slot states: Empty (0),
// NotEmpty (1), Undefined (3) — we keep the wire byte directly rather
// than wrapping it in an enum, since the application may carry tool-
// specific extensions.
struct CarrierSlot {
uint8_t state = 3; // undefined by default
};
struct Carrier {
std::string carrierid;
uint8_t port_id = 0; // 0 = unbound; ports are 1-based
std::vector<CarrierSlot> slots; // length == capacity (typ. 25)
std::unique_ptr<CarrierStateMachine> fsm;
};
struct LoadPort {
uint8_t port_id;
std::string carrierid; // empty when nothing associated
std::unique_ptr<LoadPortStateMachine> fsm;
};
// Both stores are non-movable for the same reason ProcessJobStore is —
// per-row FSMs close over `this` via their change-handler lambdas.
class CarrierStore {
public:
using IDChangeHandler =
std::function<void(const std::string& carrierid, CarrierIDStatus from,
CarrierIDStatus to, CarrierIDEvent)>;
using SlotMapChangeHandler =
std::function<void(const std::string& carrierid, SlotMapStatus from,
SlotMapStatus to, SlotMapEvent)>;
using AccessChangeHandler =
std::function<void(const std::string& carrierid, CarrierAccessStatus from,
CarrierAccessStatus to, CarrierAccessEvent)>;
CarrierStore() = default;
CarrierStore(const CarrierStore&) = delete;
CarrierStore& operator=(const CarrierStore&) = delete;
CarrierStore(CarrierStore&&) = delete;
CarrierStore& operator=(CarrierStore&&) = delete;
void set_id_handler(IDChangeHandler h) { on_id_ = std::move(h); }
void set_slot_map_handler(SlotMapChangeHandler h) { on_sm_ = std::move(h); }
void set_access_handler(AccessChangeHandler h) { on_acc_ = std::move(h); }
enum class CreateResult { Created, Denied_AlreadyExists };
CreateResult create(std::string carrierid, uint8_t port_id = 0,
std::size_t capacity = 25) {
if (carriers_.count(carrierid)) return CreateResult::Denied_AlreadyExists;
auto fsm = std::make_unique<CarrierStateMachine>();
const std::string id = carrierid;
fsm->set_id_handler([this, id](CarrierIDStatus f, CarrierIDStatus t,
CarrierIDEvent e) {
if (on_id_) on_id_(id, f, t, e);
});
fsm->set_slot_map_handler([this, id](SlotMapStatus f, SlotMapStatus t,
SlotMapEvent e) {
if (on_sm_) on_sm_(id, f, t, e);
});
fsm->set_access_handler([this, id](CarrierAccessStatus f, CarrierAccessStatus t,
CarrierAccessEvent e) {
if (on_acc_) on_acc_(id, f, t, e);
});
carriers_.emplace(carrierid, Carrier{carrierid, port_id,
std::vector<CarrierSlot>(capacity),
std::move(fsm)});
return CreateResult::Created;
}
bool has(const std::string& cid) const { return carriers_.count(cid) > 0; }
const Carrier* get(const std::string& cid) const {
auto it = carriers_.find(cid);
return it == carriers_.end() ? nullptr : &it->second;
}
Carrier* get(const std::string& cid) {
auto it = carriers_.find(cid);
return it == carriers_.end() ? nullptr : &it->second;
}
bool fire_id_event(const std::string& cid, CarrierIDEvent e) {
auto* c = get(cid);
return c && c->fsm->on_id_event(e);
}
bool fire_slot_map_event(const std::string& cid, SlotMapEvent e) {
auto* c = get(cid);
return c && c->fsm->on_slot_map_event(e);
}
bool fire_access_event(const std::string& cid, CarrierAccessEvent e) {
auto* c = get(cid);
return c && c->fsm->on_access_event(e);
}
bool remove(const std::string& cid) { return carriers_.erase(cid) > 0; }
std::size_t size() const { return carriers_.size(); }
std::vector<std::string> ids() const {
std::vector<std::string> out;
out.reserve(carriers_.size());
for (const auto& kv : carriers_) out.push_back(kv.first);
return out;
}
private:
std::map<std::string, Carrier> carriers_;
IDChangeHandler on_id_;
SlotMapChangeHandler on_sm_;
AccessChangeHandler on_acc_;
};
class LoadPortStore {
public:
using TransferChangeHandler =
std::function<void(uint8_t portid, LoadPortTransferState from,
LoadPortTransferState to, LoadPortTransferEvent)>;
using ReservationChangeHandler =
std::function<void(uint8_t portid, LoadPortReservationStatus from,
LoadPortReservationStatus to, LoadPortReservationEvent)>;
using AssociationChangeHandler =
std::function<void(uint8_t portid, LoadPortAssociationStatus from,
LoadPortAssociationStatus to, LoadPortAssociationEvent)>;
LoadPortStore() = default;
LoadPortStore(const LoadPortStore&) = delete;
LoadPortStore& operator=(const LoadPortStore&) = delete;
LoadPortStore(LoadPortStore&&) = delete;
LoadPortStore& operator=(LoadPortStore&&) = delete;
void set_transfer_handler(TransferChangeHandler h) { on_tx_ = std::move(h); }
void set_reservation_handler(ReservationChangeHandler h) { on_rs_ = std::move(h); }
void set_association_handler(AssociationChangeHandler h) { on_as_ = std::move(h); }
// Idempotent: re-create a port_id is a no-op.
bool create(uint8_t port_id) {
if (ports_.count(port_id)) return false;
auto fsm = std::make_unique<LoadPortStateMachine>();
const uint8_t pid = port_id;
fsm->set_transfer_handler([this, pid](LoadPortTransferState f,
LoadPortTransferState t,
LoadPortTransferEvent e) {
if (on_tx_) on_tx_(pid, f, t, e);
});
fsm->set_reservation_handler([this, pid](LoadPortReservationStatus f,
LoadPortReservationStatus t,
LoadPortReservationEvent e) {
if (on_rs_) on_rs_(pid, f, t, e);
});
fsm->set_association_handler([this, pid](LoadPortAssociationStatus f,
LoadPortAssociationStatus t,
LoadPortAssociationEvent e) {
if (on_as_) on_as_(pid, f, t, e);
});
ports_.emplace(port_id, LoadPort{port_id, "", std::move(fsm)});
return true;
}
bool has(uint8_t pid) const { return ports_.count(pid) > 0; }
const LoadPort* get(uint8_t pid) const {
auto it = ports_.find(pid);
return it == ports_.end() ? nullptr : &it->second;
}
LoadPort* get(uint8_t pid) {
auto it = ports_.find(pid);
return it == ports_.end() ? nullptr : &it->second;
}
bool associate(uint8_t pid, std::string carrierid) {
auto* p = get(pid);
if (!p) return false;
p->carrierid = std::move(carrierid);
return p->fsm->on_association_event(LoadPortAssociationEvent::Associate);
}
bool disassociate(uint8_t pid) {
auto* p = get(pid);
if (!p) return false;
p->carrierid.clear();
return p->fsm->on_association_event(LoadPortAssociationEvent::Disassociate);
}
bool fire_transfer_event(uint8_t pid, LoadPortTransferEvent e) {
auto* p = get(pid);
return p && p->fsm->on_transfer_event(e);
}
bool fire_reservation_event(uint8_t pid, LoadPortReservationEvent e) {
auto* p = get(pid);
return p && p->fsm->on_reservation_event(e);
}
std::size_t size() const { return ports_.size(); }
std::vector<uint8_t> ids() const {
std::vector<uint8_t> out;
out.reserve(ports_.size());
for (const auto& kv : ports_) out.push_back(kv.first);
return out;
}
private:
std::map<uint8_t, LoadPort> ports_;
TransferChangeHandler on_tx_;
ReservationChangeHandler on_rs_;
AssociationChangeHandler on_as_;
};
} // namespace secsgem::gem