diff --git a/CMakeLists.txt b/CMakeLists.txt index 04fee28..274c7e3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -101,6 +101,7 @@ add_executable(secsgem_tests tests/test_control_jobs.cpp tests/test_exceptions.cpp tests/test_carrier_state.cpp + tests/test_carriers.cpp ) target_link_libraries(secsgem_tests PRIVATE secsgem doctest::doctest) target_compile_definitions(secsgem_tests PRIVATE diff --git a/include/secsgem/gem/data_model.hpp b/include/secsgem/gem/data_model.hpp index d5b5f87..1833b8b 100644 --- a/include/secsgem/gem/data_model.hpp +++ b/include/secsgem/gem/data_model.hpp @@ -1,6 +1,7 @@ #pragma once #include "secsgem/gem/store/alarms.hpp" +#include "secsgem/gem/store/carriers.hpp" #include "secsgem/gem/store/clock.hpp" #include "secsgem/gem/store/control_jobs.hpp" #include "secsgem/gem/store/equipment_constants.hpp" @@ -36,6 +37,8 @@ struct EquipmentDataModel { ProcessJobStore process_jobs; ControlJobStore control_jobs; ExceptionStore exceptions; + CarrierStore carriers; + LoadPortStore load_ports; // Convenience: VID -> value lookup spanning SVIDs and DVIDs. std::optional vid_value(uint32_t vid) const { diff --git a/include/secsgem/gem/store/carriers.hpp b/include/secsgem/gem/store/carriers.hpp new file mode 100644 index 0000000..071b7ea --- /dev/null +++ b/include/secsgem/gem/store/carriers.hpp @@ -0,0 +1,220 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include + +#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 slots; // length == capacity (typ. 25) + std::unique_ptr fsm; +}; + +struct LoadPort { + uint8_t port_id; + std::string carrierid; // empty when nothing associated + std::unique_ptr 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; + using SlotMapChangeHandler = + std::function; + using AccessChangeHandler = + std::function; + + 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(); + 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(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 ids() const { + std::vector out; + out.reserve(carriers_.size()); + for (const auto& kv : carriers_) out.push_back(kv.first); + return out; + } + + private: + std::map carriers_; + IDChangeHandler on_id_; + SlotMapChangeHandler on_sm_; + AccessChangeHandler on_acc_; +}; + +class LoadPortStore { + public: + using TransferChangeHandler = + std::function; + using ReservationChangeHandler = + std::function; + using AssociationChangeHandler = + std::function; + + 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(); + 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 ids() const { + std::vector out; + out.reserve(ports_.size()); + for (const auto& kv : ports_) out.push_back(kv.first); + return out; + } + + private: + std::map ports_; + TransferChangeHandler on_tx_; + ReservationChangeHandler on_rs_; + AssociationChangeHandler on_as_; +}; + +} // namespace secsgem::gem diff --git a/tests/test_carriers.cpp b/tests/test_carriers.cpp new file mode 100644 index 0000000..878b7bc --- /dev/null +++ b/tests/test_carriers.cpp @@ -0,0 +1,82 @@ +#include + +#include + +#include "secsgem/gem/store/carriers.hpp" + +using namespace secsgem::gem; + +TEST_CASE("CarrierStore: create / get / remove") { + CarrierStore s; + CHECK(s.create("CAR-1", /*port=*/1) == CarrierStore::CreateResult::Created); + CHECK(s.create("CAR-1") == CarrierStore::CreateResult::Denied_AlreadyExists); + REQUIRE(s.has("CAR-1")); + CHECK(s.get("CAR-1")->port_id == 1); + CHECK(s.get("CAR-1")->slots.size() == 25); + CHECK(s.remove("CAR-1")); + CHECK_FALSE(s.has("CAR-1")); +} + +TEST_CASE("CarrierStore: id-status handler observes transitions") { + CarrierStore s; + std::vector> log; + s.set_id_handler([&](const std::string& id, CarrierIDStatus f, + CarrierIDStatus t, CarrierIDEvent) { + log.emplace_back(id, f, t); + }); + s.create("CAR-A"); + CHECK(s.fire_id_event("CAR-A", CarrierIDEvent::ProceedWithCarrier)); + REQUIRE(log.size() == 1); + CHECK(std::get<0>(log[0]) == "CAR-A"); + CHECK(std::get<1>(log[0]) == CarrierIDStatus::NotConfirmed); + CHECK(std::get<2>(log[0]) == CarrierIDStatus::Confirmed); +} + +TEST_CASE("CarrierStore: slot-map and access events are independent") { + CarrierStore s; + s.create("CAR-B"); + CHECK(s.fire_slot_map_event("CAR-B", SlotMapEvent::Read)); + CHECK(s.get("CAR-B")->fsm->slot_map_status() == SlotMapStatus::Read); + CHECK(s.fire_access_event("CAR-B", CarrierAccessEvent::BeginAccess)); + CHECK(s.get("CAR-B")->fsm->access_status() == CarrierAccessStatus::InAccess); + // ID state untouched. + CHECK(s.get("CAR-B")->fsm->id_status() == CarrierIDStatus::NotConfirmed); +} + +// ---- LoadPortStore ----------------------------------------------------- + +TEST_CASE("LoadPortStore: create + associate/disassociate") { + LoadPortStore p; + CHECK(p.create(1)); + CHECK_FALSE(p.create(1)); // already exists + CHECK(p.associate(1, "CAR-X")); + CHECK(p.get(1)->carrierid == "CAR-X"); + CHECK(p.get(1)->fsm->association_state() == + LoadPortAssociationStatus::Associated); + CHECK(p.disassociate(1)); + CHECK(p.get(1)->carrierid.empty()); + CHECK(p.get(1)->fsm->association_state() == + LoadPortAssociationStatus::NotAssociated); +} + +TEST_CASE("LoadPortStore: transfer events progress through Loading/Unloading") { + LoadPortStore p; + p.create(2); + CHECK(p.fire_transfer_event(2, LoadPortTransferEvent::StartLoading)); + CHECK(p.get(2)->fsm->transfer_state() == LoadPortTransferState::Loading); + CHECK(p.fire_transfer_event(2, LoadPortTransferEvent::CompleteLoading)); + CHECK(p.get(2)->fsm->transfer_state() == LoadPortTransferState::InService); +} + +TEST_CASE("LoadPortStore: reservation toggle observed by handler") { + LoadPortStore p; + int calls = 0; + p.set_reservation_handler([&](uint8_t, LoadPortReservationStatus, + LoadPortReservationStatus, + LoadPortReservationEvent) { ++calls; }); + p.create(3); + p.fire_reservation_event(3, LoadPortReservationEvent::Reserve); + CHECK(calls == 1); + p.fire_reservation_event(3, LoadPortReservationEvent::Release); + CHECK(calls == 2); +}