#pragma once #include #include #include #include #include #include #include #include #include "secsgem/config/loader.hpp" #include "secsgem/endpoint.hpp" #include "secsgem/gem/control_state.hpp" #include "secsgem/gem/data_model.hpp" #include "secsgem/gem/router.hpp" #include "secsgem/gem/store/host_commands.hpp" #include "secsgem/secs2/item.hpp" #include "secsgem/secs2/message.hpp" namespace secsgem::gem { // Owns the SECS/GEM engine — io_context, the passive HSMS Server, the data // model, the E30 control-state machine, and the Router — as one reusable // object. It is the foundation both the demo `secs_server` app and the gRPC // daemon build on, replacing a hand-wired main() with a class. // // Threading contract (unchanged from the engine's existing design): a single // io_context thread owns the model and the connection. The outbound methods // (set_variable / emit_event / set_alarm / clear_alarm) are safe to call from // any thread — each posts onto the io_context, exactly like the established // emit_* pattern. Router handlers and the command hook run on the io thread. class EquipmentRuntime { public: struct Config { std::string equipment_yaml; std::string control_state_yaml; std::string process_job_yaml; std::string control_job_yaml; uint16_t port = 5000; std::string spool_dir; // empty = no persistence std::function log; // empty = silent }; explicit EquipmentRuntime(const Config& cfg); ~EquipmentRuntime(); EquipmentRuntime(const EquipmentRuntime&) = delete; EquipmentRuntime& operator=(const EquipmentRuntime&) = delete; // ---- access -------------------------------------------------------------- EquipmentDataModel& model() { return *model_; } const EquipmentDataModel& model() const { return *model_; } Router& router() { return router_; } ControlStateMachine& control() { return *sm_; } asio::io_context& io() { return io_; } uint16_t device_id() const { return descriptor_.device_id; } void log(const std::string& m) const { if (log_) log_(m); } // Shared-pointer / reference accessors used when wiring handlers and // emitters that capture the engine pieces by value (see register handlers). std::shared_ptr model_ptr() { return model_; } std::shared_ptr control_ptr() { return sm_; } const config::EquipmentDescriptor& descriptor() const { return descriptor_; } std::shared_ptr> active_conn() { return active_conn_; } // ---- outbound: report state to the host (thread-safe; each posts) -------- void set_variable(uint32_t vid, secs2::Item value); void emit_event(uint32_t ceid); void set_alarm(uint32_t alid); void clear_alarm(uint32_t alid); // ---- host-command behaviour hook ----------------------------------------- void on_command(std::string rcmd, HostCommandRegistry::Handler h) { model_->commands.set_handler(std::move(rcmd), std::move(h)); } // ---- control state ------------------------------------------------------- ControlState control_state() const { return sm_->state(); } // Deliver a primary to the host, or spool it if there's no SELECTED session. // Call on the io thread (e.g. from a router handler or a posted emitter). // Returns false if dropped (stream not spoolable and no host). bool deliver_or_spool(secs2::Message msg, const std::string& what); // ---- lifecycle ----------------------------------------------------------- void run(); // start accepting + run the io_context (blocks) void run_async(); // run the io_context on a background thread void poll(); // drain ready handlers without blocking (tests) void stop(); private: void wire_connection(); asio::io_context io_; std::shared_ptr model_; std::function log_; std::shared_ptr> active_conn_; std::shared_ptr sm_; config::EquipmentDescriptor descriptor_; std::unique_ptr server_; Router router_; std::optional> work_; std::thread io_thread_; }; } // namespace secsgem::gem