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:
@@ -101,6 +101,7 @@ add_executable(secsgem_tests
|
|||||||
tests/test_control_jobs.cpp
|
tests/test_control_jobs.cpp
|
||||||
tests/test_exceptions.cpp
|
tests/test_exceptions.cpp
|
||||||
tests/test_carrier_state.cpp
|
tests/test_carrier_state.cpp
|
||||||
|
tests/test_carriers.cpp
|
||||||
)
|
)
|
||||||
target_link_libraries(secsgem_tests PRIVATE secsgem doctest::doctest)
|
target_link_libraries(secsgem_tests PRIVATE secsgem doctest::doctest)
|
||||||
target_compile_definitions(secsgem_tests PRIVATE
|
target_compile_definitions(secsgem_tests PRIVATE
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "secsgem/gem/store/alarms.hpp"
|
#include "secsgem/gem/store/alarms.hpp"
|
||||||
|
#include "secsgem/gem/store/carriers.hpp"
|
||||||
#include "secsgem/gem/store/clock.hpp"
|
#include "secsgem/gem/store/clock.hpp"
|
||||||
#include "secsgem/gem/store/control_jobs.hpp"
|
#include "secsgem/gem/store/control_jobs.hpp"
|
||||||
#include "secsgem/gem/store/equipment_constants.hpp"
|
#include "secsgem/gem/store/equipment_constants.hpp"
|
||||||
@@ -36,6 +37,8 @@ struct EquipmentDataModel {
|
|||||||
ProcessJobStore process_jobs;
|
ProcessJobStore process_jobs;
|
||||||
ControlJobStore control_jobs;
|
ControlJobStore control_jobs;
|
||||||
ExceptionStore exceptions;
|
ExceptionStore exceptions;
|
||||||
|
CarrierStore carriers;
|
||||||
|
LoadPortStore load_ports;
|
||||||
|
|
||||||
// Convenience: VID -> value lookup spanning SVIDs and DVIDs.
|
// Convenience: VID -> value lookup spanning SVIDs and DVIDs.
|
||||||
std::optional<s2::Item> vid_value(uint32_t vid) const {
|
std::optional<s2::Item> vid_value(uint32_t vid) const {
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
#include <doctest/doctest.h>
|
||||||
|
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#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<std::tuple<std::string, CarrierIDStatus, CarrierIDStatus>> 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);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user