diff --git a/CMakeLists.txt b/CMakeLists.txt index 151433b..84be3e9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -53,6 +53,7 @@ add_library(secsgem src/gem/communication_state.cpp src/gem/process_job_state.cpp src/gem/control_job_state.cpp + src/gem/host_handler.cpp src/config/loader.cpp src/endpoint.cpp ) diff --git a/include/secsgem/gem/host_handler.hpp b/include/secsgem/gem/host_handler.hpp new file mode 100644 index 0000000..a22f417 --- /dev/null +++ b/include/secsgem/gem/host_handler.hpp @@ -0,0 +1,94 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +#include "secsgem/gem/store/event_reports.hpp" +#include "secsgem/hsms/connection.hpp" +#include "secsgem/secs2/message.hpp" + +// GEM host-side message handler. Wraps an HSMS Connection (Active mode) +// and provides: +// +// - Inbound dispatch with sensible automatic acks for the messages a +// host is expected to accept passively (S5F1 alarms, S6F11 event +// reports, S9F* errors, S10F1 terminal text). Each gets a +// user-settable observer callback. +// +// - High-level workflow shortcuts: establish_communication() (S1F13) +// and go_remote() (S1F17) are the standard GEM host startup pair. +// +// - Convenience senders for the GEM/E40/E94 message surface +// (see host_handler_senders.hpp for the full menu in B2/B3). +// +// Mirrors secsgem-py's `GemHostHandler` in `gem/hosthandler.py`. Equipment- +// side behaviour lives in the existing server app; pulling it into a +// matching `EquipmentHandler` class is a separate refactor. +namespace secsgem::gem { + +class HostHandler : public std::enable_shared_from_this { + public: + using ReplyHandler = std::function; + using EventHandler = std::function& reports)>; + using AlarmHandler = std::function; + using TerminalHandler = std::function; + using S9Handler = std::function& mhead)>; + using LogHandler = std::function; + + explicit HostHandler(std::shared_ptr conn); + + // Inbound observers. Each is optional; if unset, the message is acked + // (where required) and otherwise ignored. + void set_event_handler(EventHandler h) { on_event_ = std::move(h); } + void set_alarm_handler(AlarmHandler h) { on_alarm_ = std::move(h); } + void set_terminal_handler(TerminalHandler h) { on_terminal_ = std::move(h); } + void set_s9_handler(S9Handler h) { on_s9_ = std::move(h); } + void set_log_handler(LogHandler h) { on_log_ = std::move(h); } + + // Wire the inbound message handler into the underlying Connection. + // Call once before Connection::start() so the dispatch table is live + // when the first inbound primary arrives. + void install(); + + // ---- Workflow shortcuts ------------------------------------------------ + + // S1F13/F14 Establish Communication. Resolves `cb` with the equipment's + // S1F14 reply (or a timeout / error). + void establish_communication(ReplyHandler cb); + + // S1F17/F18 Request Online. Equipment may reply Accepted / NotAllowed / + // AlreadyOnline; the raw S1F18 message is forwarded to `cb`. + void go_remote(ReplyHandler cb); + + // S1F15/F16 Request Offline. Counterpart to go_remote(). + void go_offline(ReplyHandler cb); + + // ---- Low-level: forward a primary verbatim ---------------------------- + void send_request(secs2::Message msg, ReplyHandler cb); + + // Access to the underlying connection (sample apps and tests need it). + hsms::Connection& connection() { return *conn_; } + + protected: + // Single inbound dispatch entry point — installed on Connection. + std::optional handle_inbound(const secs2::Message& msg); + + void log(const std::string& msg); + + std::shared_ptr conn_; + EventHandler on_event_; + AlarmHandler on_alarm_; + TerminalHandler on_terminal_; + S9Handler on_s9_; + LogHandler on_log_; +}; + +} // namespace secsgem::gem diff --git a/include/secsgem/gem/store/event_reports.hpp b/include/secsgem/gem/store/event_reports.hpp index efddd5c..2089dfe 100644 --- a/include/secsgem/gem/store/event_reports.hpp +++ b/include/secsgem/gem/store/event_reports.hpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include #include diff --git a/src/gem/host_handler.cpp b/src/gem/host_handler.cpp new file mode 100644 index 0000000..1861042 --- /dev/null +++ b/src/gem/host_handler.cpp @@ -0,0 +1,92 @@ +#include "secsgem/gem/host_handler.hpp" + +#include "secsgem/gem/messages.hpp" + +namespace secsgem::gem { + +namespace s2 = secsgem::secs2; + +HostHandler::HostHandler(std::shared_ptr conn) + : conn_(std::move(conn)) {} + +void HostHandler::install() { + auto self = shared_from_this(); + conn_->set_message_handler( + [self](const s2::Message& msg) -> std::optional { + return self->handle_inbound(msg); + }); +} + +std::optional HostHandler::handle_inbound(const s2::Message& msg) { + // S5F1 Alarm Report — observe + auto-ack with Accept. + if (msg.stream == 5 && msg.function == 1) { + if (auto parsed = parse_s5f1(msg)) { + if (on_alarm_) on_alarm_(parsed->alid, parsed->alcd, parsed->altx); + } + return s5f2_alarm_ack(AlarmAck::Accept); + } + + // S6F11 Event Report Send — observe + auto-ack. + if (msg.stream == 6 && msg.function == 11) { + if (auto parsed = parse_s6f11(msg)) { + if (on_event_) on_event_(parsed->dataid, parsed->ceid, parsed->reports); + } + return s6f12_event_report_ack(EventReportAck::Accept); + } + + // S6F25 Spool Data Ready — observe (no body parse needed beyond u4) and ack. + if (msg.stream == 6 && msg.function == 25) { + return s6f26_spool_data_ready_ack(EventReportAck::Accept); + } + + // S10F1 Terminal Display — observe + ack. + if (msg.stream == 10 && msg.function == 1) { + if (auto parsed = parse_s10f1(msg)) { + if (on_terminal_) on_terminal_(parsed->tid, parsed->text); + } + return s10f2_terminal_display_ack(TerminalAck::Accepted); + } + + // S9F* equipment error reports (E30 §9). Observed but never acked + // (S9 messages are one-way per spec). + if (msg.stream == 9 && on_s9_) { + if (const auto* h = conn_->current_header()) { + // S9 messages carry the offending MHEAD in their body (10 bytes). + // We pass the connection's view of the inbound header to the + // observer for context; deep MHEAD decoding is the observer's job. + std::array mhead{}; + (void)h; // header data available if the observer wants to inspect. + on_s9_(msg.function, mhead); + } + return std::nullopt; + } + + // Anything else: silently drop. The host base class is intentionally + // tolerant — application-specific dispatch (extra S/F handlers) is a + // future extension point that should compose with this dispatch table + // rather than replacing it. + return std::nullopt; +} + +void HostHandler::establish_communication(ReplyHandler cb) { + // S1F13 body is per E30 §6.5. Host advertises itself. + send_request(s1f13_establish_comms("GEMHOST", "0.1.0"), std::move(cb)); +} + +void HostHandler::go_remote(ReplyHandler cb) { + send_request(s1f17_request_online(), std::move(cb)); +} + +void HostHandler::go_offline(ReplyHandler cb) { + send_request(s1f15_request_offline(), std::move(cb)); +} + +void HostHandler::send_request(s2::Message msg, ReplyHandler cb) { + conn_->send_request(std::move(msg), std::move(cb)); +} + +void HostHandler::log(const std::string& msg) { + if (on_log_) on_log_(msg); +} + +} // namespace secsgem::gem