B1: HostHandler base class
GEM host-side counterpart to the existing equipment server: wraps an HSMS Connection (Active mode), installs an inbound dispatch table that auto-acks the messages a host is expected to passively accept, and exposes the GEM workflow primitives. Inbound dispatch: S5F1 Alarm Report observe (alarm handler) + S5F2 Accept S6F11 Event Report observe (event handler) + S6F12 Accept S6F25 Spool Data Ready S6F26 Accept (host policy: pull on demand) S10F1 Terminal Display observe + S10F2 Accepted S9F* Equipment errors observe (s9 handler); no ack (one-way) Workflow shortcuts: establish_communication() S1F13 -> S1F14 go_remote() S1F17 -> S1F18 go_offline() S1F15 -> S1F16 Plus a low-level send_request() escape hatch so the senders coming in B2/B3 don't have to friend the connection internals. Drive-by: event_reports.hpp was missing `<optional>` (worked transitively through the equipment-side include chain but not when included from the host-side standalone). secsgem-py has `gem/hosthandler.py`; this mirrors its surface for the inbound-ack and lifecycle parts. Outbound senders land in B2/B3. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -53,6 +53,7 @@ add_library(secsgem
|
|||||||
src/gem/communication_state.cpp
|
src/gem/communication_state.cpp
|
||||||
src/gem/process_job_state.cpp
|
src/gem/process_job_state.cpp
|
||||||
src/gem/control_job_state.cpp
|
src/gem/control_job_state.cpp
|
||||||
|
src/gem/host_handler.cpp
|
||||||
src/config/loader.cpp
|
src/config/loader.cpp
|
||||||
src/endpoint.cpp
|
src/endpoint.cpp
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,94 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <array>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <functional>
|
||||||
|
#include <memory>
|
||||||
|
#include <string>
|
||||||
|
#include <system_error>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#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<HostHandler> {
|
||||||
|
public:
|
||||||
|
using ReplyHandler = std::function<void(std::error_code, const secs2::Message&)>;
|
||||||
|
using EventHandler = std::function<void(uint32_t dataid, uint32_t ceid,
|
||||||
|
const std::vector<ReportData>& reports)>;
|
||||||
|
using AlarmHandler = std::function<void(uint32_t alid, uint8_t alcd,
|
||||||
|
const std::string& altx)>;
|
||||||
|
using TerminalHandler = std::function<void(uint32_t tid, const std::string& text)>;
|
||||||
|
using S9Handler = std::function<void(uint8_t function,
|
||||||
|
const std::array<uint8_t, 10>& mhead)>;
|
||||||
|
using LogHandler = std::function<void(const std::string&)>;
|
||||||
|
|
||||||
|
explicit HostHandler(std::shared_ptr<hsms::Connection> 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<secs2::Message> handle_inbound(const secs2::Message& msg);
|
||||||
|
|
||||||
|
void log(const std::string& msg);
|
||||||
|
|
||||||
|
std::shared_ptr<hsms::Connection> conn_;
|
||||||
|
EventHandler on_event_;
|
||||||
|
AlarmHandler on_alarm_;
|
||||||
|
TerminalHandler on_terminal_;
|
||||||
|
S9Handler on_s9_;
|
||||||
|
LogHandler on_log_;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace secsgem::gem
|
||||||
@@ -4,6 +4,7 @@
|
|||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <functional>
|
#include <functional>
|
||||||
#include <map>
|
#include <map>
|
||||||
|
#include <optional>
|
||||||
#include <set>
|
#include <set>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <utility>
|
#include <utility>
|
||||||
|
|||||||
@@ -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<hsms::Connection> conn)
|
||||||
|
: conn_(std::move(conn)) {}
|
||||||
|
|
||||||
|
void HostHandler::install() {
|
||||||
|
auto self = shared_from_this();
|
||||||
|
conn_->set_message_handler(
|
||||||
|
[self](const s2::Message& msg) -> std::optional<s2::Message> {
|
||||||
|
return self->handle_inbound(msg);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<s2::Message> 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<uint8_t, 10> 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 <MDLN SOFTREV> 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
|
||||||
Reference in New Issue
Block a user