Initial commit: C++20 SECS-II / HSMS / GEM client + server
A fully containerised SECS/GEM toolchain. Single docker compose project,
no host build tools. 63 unit-test cases / 278 assertions, two demo
executables, end-to-end two-container demo exercising every implemented
capability.
Architecture (bottom-up):
secs2/ E5 SECS-II codec
Item variant over L/A/B/BOOLEAN/I1-8/U1-8/F4/F8
encode/decode big-endian, 1/2/3-byte length encoding
Message SxFy + W-bit + optional root item
to_sml human-readable text rendering
hsms/ E37 HSMS transport (TCP)
Header 10-byte header + SType enum (Data/Select/Deselect/
Linktest/Reject/Separate)
Frame 4-byte length prefix + payload encode/decode
Connection async Asio TCP, NOT-SELECTED -> SELECTED state machine,
T3/T5/T6/T7/T8 timers, system-bytes reply correlation,
graceful close-after-flush separation
endpoint active Client (connect with T5 retry) and passive Server
(accept loop) wrappers over Connection
gem/ E30 GEM logic
ControlStateMachine 5-state E30 control model with operator
actions, host requests, SEMI-mandated ack
codes (OnlineAck, OfflineAck, CommAck), and
a state-change handler
EquipmentDataModel in-memory dictionary: SVIDs, DVIDs, ECIDs
(with EAC), CEIDs, report defs, CEID->report
links, enabled-events set, alarm table
(ALCD, enabled, active), process programs,
host command registry, clock (16-char
YYYYMMDDhhmmsscc with offset)
messages.hpp builders + parsers for every SxFy below
GEM message coverage (full list):
S1F1/F2 Are You There / On Line Data
S1F3/F4 Selected Equipment Status Request / Data
S1F11/F12 Status Variable Namelist Request / Data
S1F13/F14 Establish Communications (+ CommAck)
S1F15/F16 Request OFFLINE (+ OfflineAck)
S1F17/F18 Request ONLINE (+ OnlineAck)
S2F13/F14 Equipment Constant Request / Data
S2F15/F16 EC Send + EquipmentAck (Accept/UnknownEcid/Busy/OutOfRange)
S2F17/F18 Date and Time Request / Data
S2F29/F30 Equipment Constant Namelist Request / Data
S2F31/F32 Date and Time Set Request / TimeAck
S2F33/F34 Define Report + DefineReportAck (5 enum values)
S2F35/F36 Link Event Report + LinkEventAck
S2F37/F38 Enable / Disable Event Report + EnableEventAck
S2F41/F42 Host Command + HostCmdAck (7 values) + per-param CPACKs
S5F1/F2 Alarm Report Send + AlarmAck (ALCD bit-7 set/cleared
+ lower-7 category)
S5F3/F4 Enable/Disable Alarm Send + AlarmAck
S5F5/F6 List Alarms Request / Data (active alarms tagged in ALCD)
S6F11/F12 Event Report Send (equipment-initiated CEID emission
with full report data) + EventReportAck
S7F3/F4 Process Program Send + ProcessProgramAck (7 values)
S7F5/F6 Process Program Request / Data
S7F19/F20 Current EPPD List Request / Data
S10F1/F2 Terminal Display Single (host->equipment) + TerminalAck
S10F3/F4 Terminal Display Single (equipment->host)
Demo apps:
apps/secs_server.cpp passive equipment. Populates the data model
with 3 SVIDs (ControlState, Clock,
EventsEnabled), 2 ECIDs, 3 CEIDs
(ControlStateChanged, AlarmSetEvent,
ProcessStarted), 2 alarms (Chiller Temp High
cat 4, Door Open cat 1), 2 recipes
(RECIPE-A, RECIPE-B), and 4 host commands
(START, STOP, PAUSE, FAULT). Emits S6F11 on
every control state transition + on START;
emits S5F1 + the AlarmSetEvent CEID on FAULT.
Pushes an S10F3 welcome message when the host
comes online.
apps/secs_client.cpp active host. Walks 17 steps: Establish ->
Online -> S1F11 SVID namelist -> S1F3 read ->
S2F29 EC namelist -> S2F13 read ->
S2F17 clock -> S2F33/S2F35/S2F37 dynamic
event subscription -> S2F41 START
(-> receives S6F11) -> S5F5 alarm list ->
S5F3 enable alarm 1 -> S2F41 FAULT
(-> receives S5F1 + S6F11) -> S7F19/S7F5
recipe list + body -> S10F1 terminal ->
S1F15 Offline -> Separate. Handles inbound
S6F11, S5F1, S10F3 primaries.
Testing:
tests/test_secs2.cpp codec round-trip for every format,
byte-layout assertions for known values,
truncation/trailing-byte rejection,
nested list round-trip, SML rendering
tests/test_hsms.cpp header byte layout, data + control
header round-trip, full frame round-
trip with length prefix, short-payload
rejection
tests/test_control_state.cpp every (state, event) pair in the E30
control state machine, including
AlreadyOnline / NotAccept rejections
and idempotent offline-while-offline
tests/test_data_model.cpp SVID/ECID/Alarm/Recipe CRUD, clock
format + parse, host command registry,
full event-report pipeline (define ->
link -> enable -> compose) with
every error path (InvalidVid,
UnknownCeid, UnknownRptid), alarm
set/clear with ALCD bit-7 semantics
tests/test_messages.cpp round-trip + byte-layout for every
builder/parser pair, including S6F11
event reports with mixed item types
Toolchain:
Dockerfile Ubuntu 24.04, g++-13, CMake, Ninja, libasio-dev
docker-compose.yml builder / tests / server / client services,
source bind-mounted, build artifacts in a
named volume so the host tree stays clean
CMakeLists.txt C++20, -Wall -Wextra -Wpedantic, standalone
Asio (ASIO_STANDALONE), doctest via FetchContent
Documentation:
README.md architecture, quick start, demo log
COMPLIANCE.md honest per-capability E5/E30/E37 audit with
spec section refs. Calls out what's implemented,
what's partial (Reject.req, Alarms missing F7/F8,
EC range validation, PP without verify, terminal
single-line only), and what's intentionally not
yet implemented (spooling, S9 error stream,
Documentation S1F19/F20+F21/F22, limits monitoring,
trace data collection, multi-block, material
movement). Does NOT claim "100% GEM-compliant" and
lists the work required to honestly make that claim.
This is Layer 0 + the start of Layer 1 from implementation_plan.md.
The transition-table-driven "spec-as-data" architecture (Layer 1
proper) is not yet implemented; the current code uses imperative
state machines that are structurally ready to be refactored onto
tables.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,85 @@
|
||||
#pragma once
|
||||
|
||||
#include <asio.hpp>
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
#include "secsgem/hsms/connection.hpp"
|
||||
#include "secsgem/hsms/types.hpp"
|
||||
|
||||
namespace secsgem {
|
||||
|
||||
using hsms::Connection;
|
||||
using hsms::Timers;
|
||||
|
||||
// Passive (equipment) endpoint: listens for inbound TCP connections and turns
|
||||
// each into a passive HSMS Connection. The connection handler is invoked once
|
||||
// per accepted session so the application can install message/selected handlers
|
||||
// before the session starts.
|
||||
class Server {
|
||||
public:
|
||||
struct Config {
|
||||
uint16_t port = 5000;
|
||||
uint16_t device_id = 0;
|
||||
Timers timers{};
|
||||
};
|
||||
|
||||
using ConnectionHandler = std::function<void(std::shared_ptr<Connection>)>;
|
||||
using LogHandler = std::function<void(const std::string&)>;
|
||||
|
||||
Server(asio::io_context& io, Config cfg);
|
||||
|
||||
void on_connection(ConnectionHandler h) { on_connection_ = std::move(h); }
|
||||
void on_log(LogHandler h) { on_log_ = std::move(h); }
|
||||
|
||||
void start();
|
||||
|
||||
private:
|
||||
void do_accept();
|
||||
void log(const std::string& msg);
|
||||
|
||||
asio::io_context& io_;
|
||||
asio::ip::tcp::acceptor acceptor_;
|
||||
Config cfg_;
|
||||
ConnectionHandler on_connection_;
|
||||
LogHandler on_log_;
|
||||
};
|
||||
|
||||
// Active (host) endpoint: connects out to a passive peer, retrying every T5 on
|
||||
// failure, and turns the socket into an active HSMS Connection (which initiates
|
||||
// SELECT). The connection handler is invoked once the TCP connection is up.
|
||||
class Client {
|
||||
public:
|
||||
struct Config {
|
||||
std::string host = "127.0.0.1";
|
||||
uint16_t port = 5000;
|
||||
uint16_t device_id = 0;
|
||||
Timers timers{};
|
||||
};
|
||||
|
||||
using ConnectionHandler = std::function<void(std::shared_ptr<Connection>)>;
|
||||
using LogHandler = std::function<void(const std::string&)>;
|
||||
|
||||
Client(asio::io_context& io, Config cfg);
|
||||
|
||||
void on_connection(ConnectionHandler h) { on_connection_ = std::move(h); }
|
||||
void on_log(LogHandler h) { on_log_ = std::move(h); }
|
||||
|
||||
void start();
|
||||
|
||||
private:
|
||||
void do_connect();
|
||||
void schedule_retry();
|
||||
void log(const std::string& msg);
|
||||
|
||||
asio::io_context& io_;
|
||||
asio::ip::tcp::resolver resolver_;
|
||||
asio::steady_timer retry_timer_;
|
||||
Config cfg_;
|
||||
ConnectionHandler on_connection_;
|
||||
LogHandler on_log_;
|
||||
};
|
||||
|
||||
} // namespace secsgem
|
||||
@@ -0,0 +1,101 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <string>
|
||||
|
||||
namespace secsgem::gem {
|
||||
|
||||
// E30 Control State Model (§6.2). Drives whether the equipment is
|
||||
// communicating with a host and, if so, who is in control.
|
||||
enum class ControlState {
|
||||
EquipmentOffline, // equipment is offline; no host comms attempted
|
||||
AttemptOnline, // transient: equipment trying to come online
|
||||
HostOffline, // HSMS up but host has not established control
|
||||
OnlineLocal, // online, operator in control; host observes only
|
||||
OnlineRemote, // online, host in control
|
||||
};
|
||||
|
||||
const char* control_state_name(ControlState s);
|
||||
bool is_online(ControlState s);
|
||||
|
||||
// What triggered a state change — surfaced to the on_change handler so the UI
|
||||
// or logs can show *why* the state moved.
|
||||
enum class ControlEvent {
|
||||
OperatorSwitchOnline,
|
||||
OperatorSwitchOffline,
|
||||
OperatorSwitchLocal,
|
||||
OperatorSwitchRemote,
|
||||
AttemptComplete,
|
||||
AttemptFailed,
|
||||
HostRequestOnline, // S1F17
|
||||
HostRequestOffline, // S1F15
|
||||
};
|
||||
|
||||
const char* control_event_name(ControlEvent e);
|
||||
|
||||
// S1F18 ONLACK codes.
|
||||
enum class OnlineAck : uint8_t {
|
||||
Accept = 0,
|
||||
NotAccept = 1,
|
||||
AlreadyOnline = 2,
|
||||
};
|
||||
|
||||
// S1F16 OFLACK codes.
|
||||
enum class OfflineAck : uint8_t {
|
||||
Accept = 0,
|
||||
};
|
||||
|
||||
// S1F14 COMMACK codes.
|
||||
enum class CommAck : uint8_t {
|
||||
Accept = 0,
|
||||
Denied = 1,
|
||||
};
|
||||
|
||||
// Drives the E30 control state. Pure state machine — no IO. The Server layer
|
||||
// owns one of these per equipment and dispatches host-initiated events and
|
||||
// operator actions into it.
|
||||
class ControlStateMachine {
|
||||
public:
|
||||
struct Config {
|
||||
ControlState initial = ControlState::HostOffline;
|
||||
// When ATTEMPT_ONLINE completes via a host request, do we land in REMOTE
|
||||
// (host in control) or LOCAL (host observes only)? For host-initiated
|
||||
// online this defaults to REMOTE; for operator-initiated online it follows
|
||||
// `operator_default_remote`.
|
||||
bool host_request_grants_remote = true;
|
||||
bool operator_default_remote = false;
|
||||
};
|
||||
|
||||
using StateChangeHandler =
|
||||
std::function<void(ControlState from, ControlState to, ControlEvent trigger)>;
|
||||
|
||||
ControlStateMachine();
|
||||
explicit ControlStateMachine(Config cfg);
|
||||
|
||||
ControlState state() const { return state_; }
|
||||
bool online() const { return is_online(state_); }
|
||||
|
||||
void set_state_change_handler(StateChangeHandler h) { on_change_ = std::move(h); }
|
||||
|
||||
// Operator actions. Each returns true if a transition occurred, false if the
|
||||
// current state didn't permit it.
|
||||
bool operator_online();
|
||||
bool operator_offline();
|
||||
bool operator_local();
|
||||
bool operator_remote();
|
||||
|
||||
// Host-initiated requests. The SM responds with the SEMI-mandated ack code
|
||||
// and performs any transition.
|
||||
OnlineAck on_host_request_online();
|
||||
OfflineAck on_host_request_offline();
|
||||
|
||||
private:
|
||||
void transition(ControlState next, ControlEvent trigger);
|
||||
|
||||
Config cfg_;
|
||||
ControlState state_;
|
||||
StateChangeHandler on_change_;
|
||||
};
|
||||
|
||||
} // namespace secsgem::gem
|
||||
@@ -0,0 +1,264 @@
|
||||
#pragma once
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <map>
|
||||
#include <optional>
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "secsgem/secs2/item.hpp"
|
||||
|
||||
namespace secsgem::gem {
|
||||
|
||||
namespace s2 = secsgem::secs2;
|
||||
|
||||
// ---- Status / data / equipment-constant variables ------------------------
|
||||
|
||||
struct StatusVariable {
|
||||
uint32_t id;
|
||||
std::string name;
|
||||
std::string units;
|
||||
s2::Item value;
|
||||
};
|
||||
|
||||
struct DataVariable {
|
||||
uint32_t id;
|
||||
std::string name;
|
||||
std::string units;
|
||||
s2::Item value;
|
||||
};
|
||||
|
||||
struct EquipmentConstant {
|
||||
uint32_t id;
|
||||
std::string name;
|
||||
std::string units;
|
||||
s2::Item value;
|
||||
s2::Item def_value;
|
||||
std::string min_str; // optional, ASCII for S2F30 ECMIN/ECMAX
|
||||
std::string max_str;
|
||||
};
|
||||
|
||||
// ---- Event reports -------------------------------------------------------
|
||||
|
||||
struct CollectionEvent {
|
||||
uint32_t id;
|
||||
std::string name;
|
||||
};
|
||||
|
||||
struct Report {
|
||||
uint32_t id;
|
||||
std::vector<uint32_t> vids;
|
||||
};
|
||||
|
||||
// One report's worth of data at emission time: the RPTID and the values for
|
||||
// each VID, in declaration order.
|
||||
struct ReportData {
|
||||
uint32_t rptid;
|
||||
std::vector<s2::Item> values;
|
||||
};
|
||||
|
||||
// ---- Alarms --------------------------------------------------------------
|
||||
|
||||
struct Alarm {
|
||||
uint32_t id;
|
||||
std::string text;
|
||||
// Lower 7 bits of ALCD: alarm severity category (1=personal safety,
|
||||
// 2=equipment safety, 4=parameter control error, ...). Bit 7 is the
|
||||
// set/cleared flag, applied at emit time.
|
||||
uint8_t severity_category;
|
||||
};
|
||||
|
||||
// ---- Ack codes for the new SxFy ------------------------------------------
|
||||
|
||||
// S2F30 carries no ack; S2F34 / S2F36 / S2F38 do.
|
||||
enum class DefineReportAck : uint8_t {
|
||||
Accept = 0,
|
||||
InsufficientSpace = 1,
|
||||
InvalidFormat = 2,
|
||||
RptidAlreadyDefined = 3,
|
||||
InvalidVid = 5,
|
||||
};
|
||||
|
||||
enum class LinkEventAck : uint8_t {
|
||||
Accept = 0,
|
||||
InsufficientSpace = 1,
|
||||
InvalidFormat = 2,
|
||||
UnknownCeid = 3,
|
||||
UnknownRptid = 4,
|
||||
CeidAlreadyLinked = 5,
|
||||
};
|
||||
|
||||
enum class EnableEventAck : uint8_t {
|
||||
Accept = 0,
|
||||
UnknownCeid = 1,
|
||||
};
|
||||
|
||||
// S5F2 / S5F4 alarm ack.
|
||||
enum class AlarmAck : uint8_t {
|
||||
Accept = 0,
|
||||
Error = 1,
|
||||
};
|
||||
|
||||
// S6F12 event-report ack.
|
||||
enum class EventReportAck : uint8_t {
|
||||
Accept = 0,
|
||||
Denied = 1,
|
||||
};
|
||||
|
||||
// S7F4 process-program ack.
|
||||
enum class ProcessProgramAck : uint8_t {
|
||||
Accept = 0,
|
||||
PermissionNotGranted = 1,
|
||||
LengthError = 2,
|
||||
MatrixOverflow = 3,
|
||||
PpidNotFound = 4,
|
||||
ModeUnsupported = 5,
|
||||
PerformanceError = 6,
|
||||
};
|
||||
|
||||
// ---- Existing ack enums from earlier batches -----------------------------
|
||||
|
||||
enum class EquipmentAck : uint8_t {
|
||||
Accept = 0,
|
||||
Denied_UnknownEcid = 1,
|
||||
Denied_Busy = 3,
|
||||
Denied_OutOfRange = 4,
|
||||
};
|
||||
|
||||
enum class TimeAck : uint8_t {
|
||||
Accept = 0,
|
||||
Error = 1,
|
||||
NotDoneNotEmpty = 2,
|
||||
};
|
||||
|
||||
enum class HostCmdAck : uint8_t {
|
||||
Accept = 0,
|
||||
InvalidCommand = 1,
|
||||
CannotDoNow = 2,
|
||||
ParameterInvalid = 3,
|
||||
AcceptedWillFinishLater = 4,
|
||||
Rejected = 5,
|
||||
InvalidObject = 6,
|
||||
};
|
||||
|
||||
enum class TerminalAck : uint8_t {
|
||||
Accepted = 0,
|
||||
WillNotDisplay = 1,
|
||||
TerminalNotAvailable = 2,
|
||||
};
|
||||
|
||||
// The in-memory equipment data dictionary. Owns SVIDs, DVIDs, ECIDs, the
|
||||
// dynamic event-report subscription state (reports + links + enabled set),
|
||||
// the alarm table, and a small process-program registry. Single-threaded;
|
||||
// all access happens on the Asio executor.
|
||||
class EquipmentDataModel {
|
||||
public:
|
||||
struct CommandParam {
|
||||
std::string name;
|
||||
s2::Item value;
|
||||
};
|
||||
using HostCommandHandler = std::function<HostCmdAck(const std::vector<CommandParam>&)>;
|
||||
|
||||
// --- SVID ---------------------------------------------------------------
|
||||
void add_status_variable(StatusVariable sv);
|
||||
std::optional<StatusVariable> status_variable(uint32_t id) const;
|
||||
std::vector<StatusVariable> all_status_variables() const;
|
||||
void set_status_value(uint32_t id, s2::Item value);
|
||||
|
||||
// --- DVID ---------------------------------------------------------------
|
||||
void add_data_variable(DataVariable dv);
|
||||
std::optional<DataVariable> data_variable(uint32_t id) const;
|
||||
std::vector<DataVariable> all_data_variables() const;
|
||||
void set_data_value(uint32_t id, s2::Item value);
|
||||
|
||||
// VID lookup that searches SVIDs then DVIDs (E30 §6.5 — VIDs share a
|
||||
// namespace from the host's point of view).
|
||||
std::optional<s2::Item> vid_value(uint32_t vid) const;
|
||||
bool vid_exists(uint32_t vid) const;
|
||||
|
||||
// --- ECID ---------------------------------------------------------------
|
||||
void add_equipment_constant(EquipmentConstant ec);
|
||||
std::optional<EquipmentConstant> equipment_constant(uint32_t id) const;
|
||||
std::vector<EquipmentConstant> all_equipment_constants() const;
|
||||
EquipmentAck set_equipment_constant_value(uint32_t id, s2::Item value);
|
||||
|
||||
// --- Clock --------------------------------------------------------------
|
||||
std::string current_time_string() const;
|
||||
TimeAck set_time_string(const std::string& time_str);
|
||||
|
||||
// --- Host commands ------------------------------------------------------
|
||||
void register_command(const std::string& rcmd, HostCommandHandler handler);
|
||||
HostCmdAck dispatch_command(const std::string& rcmd,
|
||||
const std::vector<CommandParam>& params) const;
|
||||
bool has_command(const std::string& rcmd) const;
|
||||
|
||||
// --- Collection events --------------------------------------------------
|
||||
void register_event(CollectionEvent ce);
|
||||
bool has_event(uint32_t ceid) const;
|
||||
std::vector<CollectionEvent> all_events() const;
|
||||
|
||||
// S2F33: define reports. `reports` maps RPTID -> VID list. An empty
|
||||
// vector deletes all reports (and clears all links). An empty VID list
|
||||
// for a given RPTID deletes that report.
|
||||
DefineReportAck define_reports(
|
||||
const std::vector<std::pair<uint32_t, std::vector<uint32_t>>>& reports);
|
||||
|
||||
// S2F35: link CEIDs to RPTID lists. `links` maps CEID -> RPTID list.
|
||||
// An empty RPTID list for a CEID clears its links. An empty outer list
|
||||
// clears all links.
|
||||
LinkEventAck link_event_reports(
|
||||
const std::vector<std::pair<uint32_t, std::vector<uint32_t>>>& links);
|
||||
|
||||
// S2F37: enable/disable events. Empty CEID list applies to *all*
|
||||
// registered events.
|
||||
EnableEventAck enable_events(bool enable, const std::vector<uint32_t>& ceids);
|
||||
|
||||
bool is_event_enabled(uint32_t ceid) const;
|
||||
|
||||
// Compose the report data for a CEID emission (S6F11 body's report list).
|
||||
std::vector<ReportData> compose_reports_for(uint32_t ceid) const;
|
||||
|
||||
std::vector<Report> all_reports() const;
|
||||
|
||||
// --- Alarms -------------------------------------------------------------
|
||||
void add_alarm(Alarm a);
|
||||
std::optional<Alarm> alarm(uint32_t alid) const;
|
||||
std::vector<Alarm> all_alarms() const;
|
||||
AlarmAck set_alarm_enabled(uint32_t alid, bool enable);
|
||||
bool alarm_enabled(uint32_t alid) const;
|
||||
// Returns the ALCD byte to put on the wire (bit 7 indicates set/cleared).
|
||||
// Returns nullopt for unknown alarms.
|
||||
std::optional<uint8_t> alarm_set(uint32_t alid);
|
||||
std::optional<uint8_t> alarm_clear(uint32_t alid);
|
||||
bool alarm_active(uint32_t alid) const;
|
||||
|
||||
// --- Process programs ---------------------------------------------------
|
||||
void add_process_program(std::string ppid, std::string ppbody);
|
||||
std::optional<std::string> process_program(const std::string& ppid) const;
|
||||
std::vector<std::string> process_program_list() const;
|
||||
ProcessProgramAck delete_process_program(const std::string& ppid);
|
||||
|
||||
private:
|
||||
std::map<uint32_t, StatusVariable> svids_;
|
||||
std::map<uint32_t, DataVariable> dvids_;
|
||||
std::map<uint32_t, EquipmentConstant> ecids_;
|
||||
std::int64_t time_offset_seconds_ = 0;
|
||||
std::map<std::string, HostCommandHandler> commands_;
|
||||
|
||||
std::map<uint32_t, CollectionEvent> ceids_;
|
||||
std::map<uint32_t, Report> reports_;
|
||||
std::map<uint32_t, std::vector<uint32_t>> ce_links_; // CEID -> RPTID list
|
||||
std::set<uint32_t> events_enabled_;
|
||||
|
||||
std::map<uint32_t, Alarm> alarms_;
|
||||
std::set<uint32_t> alarms_enabled_;
|
||||
std::set<uint32_t> alarms_active_;
|
||||
|
||||
std::map<std::string, std::string> process_programs_;
|
||||
};
|
||||
|
||||
} // namespace secsgem::gem
|
||||
@@ -0,0 +1,760 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <variant>
|
||||
#include <vector>
|
||||
|
||||
#include "secsgem/gem/control_state.hpp"
|
||||
#include "secsgem/gem/data_model.hpp"
|
||||
#include "secsgem/secs2/item.hpp"
|
||||
#include "secsgem/secs2/message.hpp"
|
||||
|
||||
// GEM (E30 / E5) message builders for the SxFy primaries and replies the demo
|
||||
// and tests exercise. Builders construct a `secs2::Message`; parsers do the
|
||||
// inverse and return `std::optional<...>` on shape mismatch.
|
||||
namespace secsgem::gem {
|
||||
|
||||
namespace s2 = secsgem::secs2;
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// small Item helpers
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
inline std::optional<uint32_t> as_u4_scalar(const s2::Item& item) {
|
||||
if (item.format() != s2::Format::U4) return std::nullopt;
|
||||
const auto& v = std::get<std::vector<uint32_t>>(item.storage());
|
||||
if (v.empty()) return std::nullopt;
|
||||
return v.front();
|
||||
}
|
||||
|
||||
inline std::optional<uint8_t> as_binary_first(const s2::Item& item) {
|
||||
if (item.format() != s2::Format::Binary) return std::nullopt;
|
||||
const auto& v = item.as_bytes();
|
||||
if (v.empty()) return std::nullopt;
|
||||
return v.front();
|
||||
}
|
||||
|
||||
inline std::optional<std::string> as_ascii(const s2::Item& item) {
|
||||
if (item.format() != s2::Format::ASCII) return std::nullopt;
|
||||
return item.as_ascii();
|
||||
}
|
||||
|
||||
inline s2::Item u4_list(const std::vector<uint32_t>& ids) {
|
||||
return s2::Item::u4(ids); // a single U4 array — SECS-II elements, not a list of items
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// S1F1 / S1F2 Are You There / On Line Data
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
inline s2::Message s1f1_are_you_there() { return s2::Message(1, 1, true); }
|
||||
|
||||
inline s2::Message s1f2_on_line_data(const std::string& mdln, const std::string& softrev) {
|
||||
return s2::Message(1, 2, false,
|
||||
s2::Item::list({s2::Item::ascii(mdln), s2::Item::ascii(softrev)}));
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// S1F3 / S1F4 Selected Equipment Status Request / Data
|
||||
//
|
||||
// S1F3 W: <L,n <U4 SVID> ... > (n=0 means "all SVIDs")
|
||||
// S1F4 : <L,n <Item> ... > (empty list for unknown SVIDs per E5)
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
inline s2::Message s1f3_selected_status_request(const std::vector<uint32_t>& svids) {
|
||||
s2::Item::List children;
|
||||
children.reserve(svids.size());
|
||||
for (auto id : svids) children.push_back(s2::Item::u4(id));
|
||||
return s2::Message(1, 3, true, s2::Item::list(std::move(children)));
|
||||
}
|
||||
|
||||
inline std::optional<std::vector<uint32_t>> parse_s1f3(const s2::Message& m) {
|
||||
if (!m.body || !m.body->is_list()) return std::nullopt;
|
||||
std::vector<uint32_t> out;
|
||||
for (const auto& child : m.body->as_list()) {
|
||||
auto v = as_u4_scalar(child);
|
||||
if (!v) return std::nullopt;
|
||||
out.push_back(*v);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
inline s2::Message s1f4_selected_status_data(const std::vector<std::optional<s2::Item>>& values) {
|
||||
s2::Item::List children;
|
||||
children.reserve(values.size());
|
||||
for (const auto& v : values) {
|
||||
children.push_back(v ? *v : s2::Item::list({})); // empty L for unknown
|
||||
}
|
||||
return s2::Message(1, 4, false, s2::Item::list(std::move(children)));
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// S1F11 / S1F12 Status Variable Namelist Request / Data
|
||||
//
|
||||
// S1F11 W: <L,n <U4 SVID> ... > (n=0 = all)
|
||||
// S1F12 : <L,n <L,3 <U4 SVID> <A SVNAME> <A SVUNITS>> ... >
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
inline s2::Message s1f11_status_namelist_request(const std::vector<uint32_t>& svids) {
|
||||
s2::Item::List children;
|
||||
children.reserve(svids.size());
|
||||
for (auto id : svids) children.push_back(s2::Item::u4(id));
|
||||
return s2::Message(1, 11, true, s2::Item::list(std::move(children)));
|
||||
}
|
||||
|
||||
inline s2::Message s1f12_status_namelist_data(const std::vector<StatusVariable>& items) {
|
||||
s2::Item::List children;
|
||||
children.reserve(items.size());
|
||||
for (const auto& sv : items) {
|
||||
children.push_back(s2::Item::list(
|
||||
{s2::Item::u4(sv.id), s2::Item::ascii(sv.name), s2::Item::ascii(sv.units)}));
|
||||
}
|
||||
return s2::Message(1, 12, false, s2::Item::list(std::move(children)));
|
||||
}
|
||||
|
||||
struct StatusName {
|
||||
uint32_t id;
|
||||
std::string name;
|
||||
std::string units;
|
||||
};
|
||||
|
||||
inline std::optional<std::vector<StatusName>> parse_s1f12(const s2::Message& m) {
|
||||
if (!m.body || !m.body->is_list()) return std::nullopt;
|
||||
std::vector<StatusName> out;
|
||||
for (const auto& row : m.body->as_list()) {
|
||||
if (!row.is_list() || row.as_list().size() != 3) return std::nullopt;
|
||||
auto id = as_u4_scalar(row.as_list()[0]);
|
||||
auto name = as_ascii(row.as_list()[1]);
|
||||
auto units = as_ascii(row.as_list()[2]);
|
||||
if (!id || !name || !units) return std::nullopt;
|
||||
out.push_back({*id, *name, *units});
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// S1F13 / S1F14 Establish Communications
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
inline s2::Message s1f13_establish_comms(const std::string& mdln, const std::string& softrev) {
|
||||
return s2::Message(1, 13, true,
|
||||
s2::Item::list({s2::Item::ascii(mdln), s2::Item::ascii(softrev)}));
|
||||
}
|
||||
|
||||
inline s2::Message s1f14_establish_comms_ack(CommAck ack, const std::string& mdln,
|
||||
const std::string& softrev) {
|
||||
return s2::Message(
|
||||
1, 14, false,
|
||||
s2::Item::list({s2::Item::binary({static_cast<uint8_t>(ack)}),
|
||||
s2::Item::list({s2::Item::ascii(mdln), s2::Item::ascii(softrev)})}));
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// S1F15 / S1F16 Request OFFLINE / OFFLINE Acknowledge
|
||||
// S1F17 / S1F18 Request ONLINE / ONLINE Acknowledge
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
inline s2::Message s1f15_request_offline() { return s2::Message(1, 15, true); }
|
||||
inline s2::Message s1f16_offline_ack(OfflineAck ack) {
|
||||
return s2::Message(1, 16, false, s2::Item::binary({static_cast<uint8_t>(ack)}));
|
||||
}
|
||||
inline s2::Message s1f17_request_online() { return s2::Message(1, 17, true); }
|
||||
inline s2::Message s1f18_online_ack(OnlineAck ack) {
|
||||
return s2::Message(1, 18, false, s2::Item::binary({static_cast<uint8_t>(ack)}));
|
||||
}
|
||||
|
||||
// Generic helper for messages whose body is a single <B ack>.
|
||||
inline std::optional<uint8_t> ack_byte(const s2::Message& msg) {
|
||||
if (!msg.body) return std::nullopt;
|
||||
return as_binary_first(*msg.body);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// S2F13 / S2F14 Equipment Constant Request / Data
|
||||
// S2F15 / S2F16 New Equipment Constant Send / Acknowledge
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
inline s2::Message s2f13_ec_request(const std::vector<uint32_t>& ecids) {
|
||||
s2::Item::List children;
|
||||
children.reserve(ecids.size());
|
||||
for (auto id : ecids) children.push_back(s2::Item::u4(id));
|
||||
return s2::Message(2, 13, true, s2::Item::list(std::move(children)));
|
||||
}
|
||||
|
||||
inline s2::Message s2f14_ec_data(const std::vector<s2::Item>& values) {
|
||||
return s2::Message(2, 14, false, s2::Item::list(values));
|
||||
}
|
||||
|
||||
inline s2::Message s2f15_ec_send(
|
||||
const std::vector<std::pair<uint32_t, s2::Item>>& sets) {
|
||||
s2::Item::List rows;
|
||||
rows.reserve(sets.size());
|
||||
for (const auto& [id, val] : sets) {
|
||||
rows.push_back(s2::Item::list({s2::Item::u4(id), val}));
|
||||
}
|
||||
return s2::Message(2, 15, true, s2::Item::list(std::move(rows)));
|
||||
}
|
||||
|
||||
inline std::optional<std::vector<std::pair<uint32_t, s2::Item>>> parse_s2f15(
|
||||
const s2::Message& m) {
|
||||
if (!m.body || !m.body->is_list()) return std::nullopt;
|
||||
std::vector<std::pair<uint32_t, s2::Item>> out;
|
||||
for (const auto& row : m.body->as_list()) {
|
||||
if (!row.is_list() || row.as_list().size() != 2) return std::nullopt;
|
||||
auto id = as_u4_scalar(row.as_list()[0]);
|
||||
if (!id) return std::nullopt;
|
||||
out.emplace_back(*id, row.as_list()[1]);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
inline s2::Message s2f16_ec_ack(EquipmentAck eac) {
|
||||
return s2::Message(2, 16, false, s2::Item::binary({static_cast<uint8_t>(eac)}));
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// S2F17 / S2F18 Date and Time Request / Data
|
||||
// S2F31 / S2F32 Date and Time Set Request / Acknowledge
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
inline s2::Message s2f17_date_time_request() { return s2::Message(2, 17, true); }
|
||||
inline s2::Message s2f18_date_time_data(const std::string& time_str) {
|
||||
return s2::Message(2, 18, false, s2::Item::ascii(time_str));
|
||||
}
|
||||
inline std::optional<std::string> parse_s2f18(const s2::Message& m) {
|
||||
if (!m.body) return std::nullopt;
|
||||
return as_ascii(*m.body);
|
||||
}
|
||||
|
||||
inline s2::Message s2f31_date_time_set(const std::string& time_str) {
|
||||
return s2::Message(2, 31, true, s2::Item::ascii(time_str));
|
||||
}
|
||||
inline std::optional<std::string> parse_s2f31(const s2::Message& m) {
|
||||
if (!m.body) return std::nullopt;
|
||||
return as_ascii(*m.body);
|
||||
}
|
||||
inline s2::Message s2f32_date_time_ack(TimeAck ack) {
|
||||
return s2::Message(2, 32, false, s2::Item::binary({static_cast<uint8_t>(ack)}));
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// S2F41 / S2F42 Host Command / Host Command Acknowledge
|
||||
//
|
||||
// S2F41 W: <L,2 <A RCMD> <L,n <L,2 <A CPNAME> <Item CPVAL>>>>
|
||||
// S2F42 : <L,2 <B HCACK> <L,n <L,2 <A CPNAME> <B CPACK>>>>
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
inline s2::Message s2f41_host_command(
|
||||
const std::string& rcmd,
|
||||
const std::vector<EquipmentDataModel::CommandParam>& params = {}) {
|
||||
s2::Item::List param_rows;
|
||||
param_rows.reserve(params.size());
|
||||
for (const auto& p : params) {
|
||||
param_rows.push_back(s2::Item::list({s2::Item::ascii(p.name), p.value}));
|
||||
}
|
||||
return s2::Message(
|
||||
2, 41, true,
|
||||
s2::Item::list({s2::Item::ascii(rcmd), s2::Item::list(std::move(param_rows))}));
|
||||
}
|
||||
|
||||
struct HostCommand {
|
||||
std::string rcmd;
|
||||
std::vector<EquipmentDataModel::CommandParam> params;
|
||||
};
|
||||
|
||||
inline std::optional<HostCommand> parse_s2f41(const s2::Message& m) {
|
||||
if (!m.body || !m.body->is_list() || m.body->as_list().size() != 2) return std::nullopt;
|
||||
const auto& outer = m.body->as_list();
|
||||
auto rcmd = as_ascii(outer[0]);
|
||||
if (!rcmd) return std::nullopt;
|
||||
HostCommand cmd{*rcmd, {}};
|
||||
if (!outer[1].is_list()) return std::nullopt;
|
||||
for (const auto& row : outer[1].as_list()) {
|
||||
if (!row.is_list() || row.as_list().size() != 2) return std::nullopt;
|
||||
auto name = as_ascii(row.as_list()[0]);
|
||||
if (!name) return std::nullopt;
|
||||
cmd.params.push_back({*name, row.as_list()[1]});
|
||||
}
|
||||
return cmd;
|
||||
}
|
||||
|
||||
inline s2::Message s2f42_host_command_ack(
|
||||
HostCmdAck hcack,
|
||||
const std::vector<std::pair<std::string, uint8_t>>& cpacks = {}) {
|
||||
s2::Item::List cp_rows;
|
||||
cp_rows.reserve(cpacks.size());
|
||||
for (const auto& [name, code] : cpacks) {
|
||||
cp_rows.push_back(s2::Item::list({s2::Item::ascii(name), s2::Item::binary({code})}));
|
||||
}
|
||||
return s2::Message(
|
||||
2, 42, false,
|
||||
s2::Item::list({s2::Item::binary({static_cast<uint8_t>(hcack)}),
|
||||
s2::Item::list(std::move(cp_rows))}));
|
||||
}
|
||||
|
||||
struct HostCommandReply {
|
||||
HostCmdAck hcack;
|
||||
std::vector<std::pair<std::string, uint8_t>> cpacks;
|
||||
};
|
||||
|
||||
inline std::optional<HostCommandReply> parse_s2f42(const s2::Message& m) {
|
||||
if (!m.body || !m.body->is_list() || m.body->as_list().size() != 2) return std::nullopt;
|
||||
const auto& outer = m.body->as_list();
|
||||
auto hcack = as_binary_first(outer[0]);
|
||||
if (!hcack) return std::nullopt;
|
||||
HostCommandReply rep{static_cast<HostCmdAck>(*hcack), {}};
|
||||
if (!outer[1].is_list()) return std::nullopt;
|
||||
for (const auto& row : outer[1].as_list()) {
|
||||
if (!row.is_list() || row.as_list().size() != 2) return std::nullopt;
|
||||
auto name = as_ascii(row.as_list()[0]);
|
||||
auto code = as_binary_first(row.as_list()[1]);
|
||||
if (!name || !code) return std::nullopt;
|
||||
rep.cpacks.emplace_back(*name, *code);
|
||||
}
|
||||
return rep;
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// S10F1 / S10F2 Terminal Display, Single (host -> equipment)
|
||||
// S10F3 / S10F4 Terminal Display, Single (equipment -> host)
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
inline s2::Message s10f1_terminal_display_single(uint8_t tid, const std::string& text) {
|
||||
return s2::Message(10, 1, true,
|
||||
s2::Item::list({s2::Item::binary({tid}), s2::Item::ascii(text)}));
|
||||
}
|
||||
|
||||
struct TerminalDisplay {
|
||||
uint8_t tid;
|
||||
std::string text;
|
||||
};
|
||||
|
||||
inline std::optional<TerminalDisplay> parse_terminal_display(const s2::Message& m) {
|
||||
if (!m.body || !m.body->is_list() || m.body->as_list().size() != 2) return std::nullopt;
|
||||
const auto& l = m.body->as_list();
|
||||
auto tid = as_binary_first(l[0]);
|
||||
auto text = as_ascii(l[1]);
|
||||
if (!tid || !text) return std::nullopt;
|
||||
return TerminalDisplay{*tid, *text};
|
||||
}
|
||||
|
||||
inline s2::Message s10f2_terminal_display_ack(TerminalAck ack) {
|
||||
return s2::Message(10, 2, false, s2::Item::binary({static_cast<uint8_t>(ack)}));
|
||||
}
|
||||
|
||||
inline s2::Message s10f3_terminal_display_single(uint8_t tid, const std::string& text) {
|
||||
return s2::Message(10, 3, true,
|
||||
s2::Item::list({s2::Item::binary({tid}), s2::Item::ascii(text)}));
|
||||
}
|
||||
|
||||
inline s2::Message s10f4_terminal_display_ack(TerminalAck ack) {
|
||||
return s2::Message(10, 4, false, s2::Item::binary({static_cast<uint8_t>(ack)}));
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Extended GEM SxFy (events, alarms, recipes)
|
||||
// =========================================================================
|
||||
|
||||
// Generic <L,n <U4 id>> body reader, used by several messages.
|
||||
inline std::optional<std::vector<uint32_t>> parse_u4_list_body(const s2::Message& m) {
|
||||
if (!m.body || !m.body->is_list()) return std::nullopt;
|
||||
std::vector<uint32_t> out;
|
||||
for (const auto& c : m.body->as_list()) {
|
||||
auto v = as_u4_scalar(c);
|
||||
if (!v) return std::nullopt;
|
||||
out.push_back(*v);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
inline s2::Item u4_list_item(const std::vector<uint32_t>& ids) {
|
||||
s2::Item::List children;
|
||||
children.reserve(ids.size());
|
||||
for (auto id : ids) children.push_back(s2::Item::u4(id));
|
||||
return s2::Item::list(std::move(children));
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// S2F29 / S2F30 Equipment Constant Namelist Request / Data
|
||||
// S2F30: <L,n <L,6 <U4 ECID> <A ECNAME> <A ECMIN> <A ECMAX> <A ECDEF> <A ECUNITS>>>
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
inline s2::Message s2f29_ec_namelist_request(const std::vector<uint32_t>& ecids) {
|
||||
return s2::Message(2, 29, true, u4_list_item(ecids));
|
||||
}
|
||||
|
||||
inline s2::Message s2f30_ec_namelist_data(const std::vector<EquipmentConstant>& ecs) {
|
||||
s2::Item::List rows;
|
||||
rows.reserve(ecs.size());
|
||||
for (const auto& ec : ecs) {
|
||||
rows.push_back(s2::Item::list({
|
||||
s2::Item::u4(ec.id),
|
||||
s2::Item::ascii(ec.name),
|
||||
s2::Item::ascii(ec.min_str),
|
||||
s2::Item::ascii(ec.max_str),
|
||||
s2::Item::ascii(""), // ECDEF rendering left simple
|
||||
s2::Item::ascii(ec.units),
|
||||
}));
|
||||
}
|
||||
return s2::Message(2, 30, false, s2::Item::list(std::move(rows)));
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// S2F33 / S2F34 Define Report
|
||||
//
|
||||
// S2F33 W: <L,2 <U4 DATAID> <L,a <L,2 <U4 RPTID> <L,n <U4 VID>>>>>
|
||||
// S2F34 : <B DRACK>
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
inline s2::Message s2f33_define_report(
|
||||
uint32_t dataid,
|
||||
const std::vector<std::pair<uint32_t, std::vector<uint32_t>>>& reports) {
|
||||
s2::Item::List rows;
|
||||
rows.reserve(reports.size());
|
||||
for (const auto& [rptid, vids] : reports) {
|
||||
rows.push_back(s2::Item::list({s2::Item::u4(rptid), u4_list_item(vids)}));
|
||||
}
|
||||
return s2::Message(2, 33, true,
|
||||
s2::Item::list({s2::Item::u4(dataid), s2::Item::list(std::move(rows))}));
|
||||
}
|
||||
|
||||
struct DefineReportRequest {
|
||||
uint32_t dataid;
|
||||
std::vector<std::pair<uint32_t, std::vector<uint32_t>>> reports;
|
||||
};
|
||||
|
||||
inline std::optional<DefineReportRequest> parse_s2f33(const s2::Message& m) {
|
||||
if (!m.body || !m.body->is_list() || m.body->as_list().size() != 2) return std::nullopt;
|
||||
const auto& outer = m.body->as_list();
|
||||
auto dataid = as_u4_scalar(outer[0]);
|
||||
if (!dataid) return std::nullopt;
|
||||
DefineReportRequest req{*dataid, {}};
|
||||
if (!outer[1].is_list()) return std::nullopt;
|
||||
for (const auto& row : outer[1].as_list()) {
|
||||
if (!row.is_list() || row.as_list().size() != 2) return std::nullopt;
|
||||
auto rptid = as_u4_scalar(row.as_list()[0]);
|
||||
if (!rptid) return std::nullopt;
|
||||
auto vids = std::vector<uint32_t>{};
|
||||
if (!row.as_list()[1].is_list()) return std::nullopt;
|
||||
for (const auto& v : row.as_list()[1].as_list()) {
|
||||
auto u = as_u4_scalar(v);
|
||||
if (!u) return std::nullopt;
|
||||
vids.push_back(*u);
|
||||
}
|
||||
req.reports.emplace_back(*rptid, std::move(vids));
|
||||
}
|
||||
return req;
|
||||
}
|
||||
|
||||
inline s2::Message s2f34_define_report_ack(DefineReportAck ack) {
|
||||
return s2::Message(2, 34, false, s2::Item::binary({static_cast<uint8_t>(ack)}));
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// S2F35 / S2F36 Link Event Report
|
||||
//
|
||||
// S2F35 W: <L,2 <U4 DATAID> <L,a <L,2 <U4 CEID> <L,n <U4 RPTID>>>>>
|
||||
// S2F36 : <B LRACK>
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
inline s2::Message s2f35_link_event_report(
|
||||
uint32_t dataid,
|
||||
const std::vector<std::pair<uint32_t, std::vector<uint32_t>>>& links) {
|
||||
s2::Item::List rows;
|
||||
rows.reserve(links.size());
|
||||
for (const auto& [ceid, rpts] : links) {
|
||||
rows.push_back(s2::Item::list({s2::Item::u4(ceid), u4_list_item(rpts)}));
|
||||
}
|
||||
return s2::Message(2, 35, true,
|
||||
s2::Item::list({s2::Item::u4(dataid), s2::Item::list(std::move(rows))}));
|
||||
}
|
||||
|
||||
struct LinkEventReportRequest {
|
||||
uint32_t dataid;
|
||||
std::vector<std::pair<uint32_t, std::vector<uint32_t>>> links;
|
||||
};
|
||||
|
||||
inline std::optional<LinkEventReportRequest> parse_s2f35(const s2::Message& m) {
|
||||
if (!m.body || !m.body->is_list() || m.body->as_list().size() != 2) return std::nullopt;
|
||||
const auto& outer = m.body->as_list();
|
||||
auto dataid = as_u4_scalar(outer[0]);
|
||||
if (!dataid) return std::nullopt;
|
||||
LinkEventReportRequest req{*dataid, {}};
|
||||
if (!outer[1].is_list()) return std::nullopt;
|
||||
for (const auto& row : outer[1].as_list()) {
|
||||
if (!row.is_list() || row.as_list().size() != 2) return std::nullopt;
|
||||
auto ceid = as_u4_scalar(row.as_list()[0]);
|
||||
if (!ceid) return std::nullopt;
|
||||
std::vector<uint32_t> rpts;
|
||||
if (!row.as_list()[1].is_list()) return std::nullopt;
|
||||
for (const auto& v : row.as_list()[1].as_list()) {
|
||||
auto u = as_u4_scalar(v);
|
||||
if (!u) return std::nullopt;
|
||||
rpts.push_back(*u);
|
||||
}
|
||||
req.links.emplace_back(*ceid, std::move(rpts));
|
||||
}
|
||||
return req;
|
||||
}
|
||||
|
||||
inline s2::Message s2f36_link_event_report_ack(LinkEventAck ack) {
|
||||
return s2::Message(2, 36, false, s2::Item::binary({static_cast<uint8_t>(ack)}));
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// S2F37 / S2F38 Enable / Disable Event Report
|
||||
//
|
||||
// S2F37 W: <L,2 <BOOLEAN CEED> <L,n <U4 CEID>>>
|
||||
// S2F38 : <B ERACK>
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
inline s2::Message s2f37_enable_event(bool enable, const std::vector<uint32_t>& ceids) {
|
||||
return s2::Message(2, 37, true,
|
||||
s2::Item::list({s2::Item::boolean(enable), u4_list_item(ceids)}));
|
||||
}
|
||||
|
||||
struct EnableEventRequest {
|
||||
bool enable;
|
||||
std::vector<uint32_t> ceids;
|
||||
};
|
||||
|
||||
inline std::optional<EnableEventRequest> parse_s2f37(const s2::Message& m) {
|
||||
if (!m.body || !m.body->is_list() || m.body->as_list().size() != 2) return std::nullopt;
|
||||
const auto& outer = m.body->as_list();
|
||||
if (outer[0].format() != s2::Format::Boolean) return std::nullopt;
|
||||
const auto& bytes = outer[0].as_bytes();
|
||||
if (bytes.empty()) return std::nullopt;
|
||||
EnableEventRequest req{bytes.front() != 0, {}};
|
||||
if (!outer[1].is_list()) return std::nullopt;
|
||||
for (const auto& v : outer[1].as_list()) {
|
||||
auto u = as_u4_scalar(v);
|
||||
if (!u) return std::nullopt;
|
||||
req.ceids.push_back(*u);
|
||||
}
|
||||
return req;
|
||||
}
|
||||
|
||||
inline s2::Message s2f38_enable_event_ack(EnableEventAck ack) {
|
||||
return s2::Message(2, 38, false, s2::Item::binary({static_cast<uint8_t>(ack)}));
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// S5F1 / S5F2 Alarm Report Send / Ack
|
||||
//
|
||||
// S5F1 W: <L,3 <B ALCD> <U4 ALID> <A ALTX>>
|
||||
// S5F2 : <B ACKC5>
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
inline s2::Message s5f1_alarm_report(uint8_t alcd, uint32_t alid, const std::string& altx) {
|
||||
return s2::Message(5, 1, true,
|
||||
s2::Item::list({s2::Item::binary({alcd}), s2::Item::u4(alid),
|
||||
s2::Item::ascii(altx)}));
|
||||
}
|
||||
|
||||
struct AlarmReport {
|
||||
uint8_t alcd;
|
||||
uint32_t alid;
|
||||
std::string altx;
|
||||
bool is_set() const { return (alcd & 0x80) != 0; }
|
||||
uint8_t category() const { return alcd & 0x7F; }
|
||||
};
|
||||
|
||||
inline std::optional<AlarmReport> parse_s5f1(const s2::Message& m) {
|
||||
if (!m.body || !m.body->is_list() || m.body->as_list().size() != 3) return std::nullopt;
|
||||
const auto& l = m.body->as_list();
|
||||
auto alcd = as_binary_first(l[0]);
|
||||
auto alid = as_u4_scalar(l[1]);
|
||||
auto altx = as_ascii(l[2]);
|
||||
if (!alcd || !alid || !altx) return std::nullopt;
|
||||
return AlarmReport{*alcd, *alid, *altx};
|
||||
}
|
||||
|
||||
inline s2::Message s5f2_alarm_ack(AlarmAck ack) {
|
||||
return s2::Message(5, 2, false, s2::Item::binary({static_cast<uint8_t>(ack)}));
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// S5F3 / S5F4 Enable / Disable Alarm Send
|
||||
//
|
||||
// S5F3 W: <L,2 <B ALED> <U4 ALID>> (ALED 0x80 = enable, 0x00 = disable)
|
||||
// S5F4 : <B ACKC5>
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
inline constexpr uint8_t kAlarmEnableByte = 0x80;
|
||||
inline constexpr uint8_t kAlarmDisableByte = 0x00;
|
||||
|
||||
inline s2::Message s5f3_enable_alarm(bool enable, uint32_t alid) {
|
||||
return s2::Message(
|
||||
5, 3, true,
|
||||
s2::Item::list({s2::Item::binary({enable ? kAlarmEnableByte : kAlarmDisableByte}),
|
||||
s2::Item::u4(alid)}));
|
||||
}
|
||||
|
||||
struct EnableAlarmRequest {
|
||||
bool enable;
|
||||
uint32_t alid;
|
||||
};
|
||||
|
||||
inline std::optional<EnableAlarmRequest> parse_s5f3(const s2::Message& m) {
|
||||
if (!m.body || !m.body->is_list() || m.body->as_list().size() != 2) return std::nullopt;
|
||||
const auto& l = m.body->as_list();
|
||||
auto aled = as_binary_first(l[0]);
|
||||
auto alid = as_u4_scalar(l[1]);
|
||||
if (!aled || !alid) return std::nullopt;
|
||||
return EnableAlarmRequest{(*aled & 0x80) != 0, *alid};
|
||||
}
|
||||
|
||||
inline s2::Message s5f4_enable_alarm_ack(AlarmAck ack) {
|
||||
return s2::Message(5, 4, false, s2::Item::binary({static_cast<uint8_t>(ack)}));
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// S5F5 / S5F6 List Alarms Request / Data
|
||||
//
|
||||
// S5F5 W: <L,n <U4 ALID>> (empty = all)
|
||||
// S5F6 : <L,n <L,3 <B ALCD> <U4 ALID> <A ALTX>>>
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
inline s2::Message s5f5_list_alarms_request(const std::vector<uint32_t>& alids) {
|
||||
return s2::Message(5, 5, true, u4_list_item(alids));
|
||||
}
|
||||
|
||||
inline s2::Message s5f6_list_alarms_data(const std::vector<Alarm>& alarms,
|
||||
const std::function<bool(uint32_t)>& active) {
|
||||
s2::Item::List rows;
|
||||
rows.reserve(alarms.size());
|
||||
for (const auto& a : alarms) {
|
||||
const uint8_t alcd = (a.severity_category & 0x7F) |
|
||||
static_cast<uint8_t>(active(a.id) ? 0x80 : 0x00);
|
||||
rows.push_back(s2::Item::list(
|
||||
{s2::Item::binary({alcd}), s2::Item::u4(a.id), s2::Item::ascii(a.text)}));
|
||||
}
|
||||
return s2::Message(5, 6, false, s2::Item::list(std::move(rows)));
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// S6F11 / S6F12 Event Report Send
|
||||
//
|
||||
// S6F11 W: <L,3 <U4 DATAID> <U4 CEID> <L,a <L,2 <U4 RPTID> <L,n <Item>>>>>
|
||||
// S6F12 : <B ACKC6>
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
inline s2::Message s6f11_event_report(uint32_t dataid, uint32_t ceid,
|
||||
const std::vector<ReportData>& reports) {
|
||||
s2::Item::List rows;
|
||||
rows.reserve(reports.size());
|
||||
for (const auto& r : reports) {
|
||||
s2::Item::List values(r.values.begin(), r.values.end());
|
||||
rows.push_back(s2::Item::list({s2::Item::u4(r.rptid), s2::Item::list(std::move(values))}));
|
||||
}
|
||||
return s2::Message(6, 11, true,
|
||||
s2::Item::list({s2::Item::u4(dataid), s2::Item::u4(ceid),
|
||||
s2::Item::list(std::move(rows))}));
|
||||
}
|
||||
|
||||
struct EventReportMessage {
|
||||
uint32_t dataid;
|
||||
uint32_t ceid;
|
||||
std::vector<ReportData> reports;
|
||||
};
|
||||
|
||||
inline std::optional<EventReportMessage> parse_s6f11(const s2::Message& m) {
|
||||
if (!m.body || !m.body->is_list() || m.body->as_list().size() != 3) return std::nullopt;
|
||||
const auto& outer = m.body->as_list();
|
||||
auto dataid = as_u4_scalar(outer[0]);
|
||||
auto ceid = as_u4_scalar(outer[1]);
|
||||
if (!dataid || !ceid) return std::nullopt;
|
||||
EventReportMessage out{*dataid, *ceid, {}};
|
||||
if (!outer[2].is_list()) return std::nullopt;
|
||||
for (const auto& row : outer[2].as_list()) {
|
||||
if (!row.is_list() || row.as_list().size() != 2) return std::nullopt;
|
||||
auto rptid = as_u4_scalar(row.as_list()[0]);
|
||||
if (!rptid) return std::nullopt;
|
||||
if (!row.as_list()[1].is_list()) return std::nullopt;
|
||||
ReportData rd{*rptid, {}};
|
||||
for (const auto& v : row.as_list()[1].as_list()) rd.values.push_back(v);
|
||||
out.reports.push_back(std::move(rd));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
inline s2::Message s6f12_event_report_ack(EventReportAck ack) {
|
||||
return s2::Message(6, 12, false, s2::Item::binary({static_cast<uint8_t>(ack)}));
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// S7F3 / S7F4 Process Program Send / Ack
|
||||
// S7F5 / S7F6 Process Program Request / Data
|
||||
// S7F19/ S7F20 Current EPPD List Request / Data
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
inline s2::Message s7f3_process_program_send(const std::string& ppid, const std::string& ppbody) {
|
||||
return s2::Message(
|
||||
7, 3, true,
|
||||
s2::Item::list({s2::Item::ascii(ppid),
|
||||
s2::Item::binary(std::vector<uint8_t>(ppbody.begin(), ppbody.end()))}));
|
||||
}
|
||||
|
||||
struct ProcessProgram {
|
||||
std::string ppid;
|
||||
std::string ppbody;
|
||||
};
|
||||
|
||||
inline std::optional<ProcessProgram> parse_s7f3(const s2::Message& m) {
|
||||
if (!m.body || !m.body->is_list() || m.body->as_list().size() != 2) return std::nullopt;
|
||||
const auto& l = m.body->as_list();
|
||||
auto ppid = as_ascii(l[0]);
|
||||
if (!ppid) return std::nullopt;
|
||||
if (l[1].format() != s2::Format::Binary) return std::nullopt;
|
||||
const auto& bytes = l[1].as_bytes();
|
||||
return ProcessProgram{*ppid, std::string(bytes.begin(), bytes.end())};
|
||||
}
|
||||
|
||||
inline s2::Message s7f4_process_program_ack(ProcessProgramAck ack) {
|
||||
return s2::Message(7, 4, false, s2::Item::binary({static_cast<uint8_t>(ack)}));
|
||||
}
|
||||
|
||||
inline s2::Message s7f5_process_program_request(const std::string& ppid) {
|
||||
return s2::Message(7, 5, true, s2::Item::ascii(ppid));
|
||||
}
|
||||
|
||||
inline std::optional<std::string> parse_s7f5(const s2::Message& m) {
|
||||
if (!m.body) return std::nullopt;
|
||||
return as_ascii(*m.body);
|
||||
}
|
||||
|
||||
inline s2::Message s7f6_process_program_data(const std::string& ppid, const std::string& ppbody) {
|
||||
return s2::Message(
|
||||
7, 6, false,
|
||||
s2::Item::list({s2::Item::ascii(ppid),
|
||||
s2::Item::binary(std::vector<uint8_t>(ppbody.begin(), ppbody.end()))}));
|
||||
}
|
||||
|
||||
inline std::optional<ProcessProgram> parse_s7f6(const s2::Message& m) {
|
||||
return parse_s7f3(m); // identical body shape
|
||||
}
|
||||
|
||||
inline s2::Message s7f19_current_eppd_request() { return s2::Message(7, 19, true); }
|
||||
|
||||
inline s2::Message s7f20_current_eppd_data(const std::vector<std::string>& ppids) {
|
||||
s2::Item::List rows;
|
||||
rows.reserve(ppids.size());
|
||||
for (const auto& p : ppids) rows.push_back(s2::Item::ascii(p));
|
||||
return s2::Message(7, 20, false, s2::Item::list(std::move(rows)));
|
||||
}
|
||||
|
||||
inline std::optional<std::vector<std::string>> parse_s7f20(const s2::Message& m) {
|
||||
if (!m.body || !m.body->is_list()) return std::nullopt;
|
||||
std::vector<std::string> out;
|
||||
for (const auto& c : m.body->as_list()) {
|
||||
auto s = as_ascii(c);
|
||||
if (!s) return std::nullopt;
|
||||
out.push_back(*s);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
} // namespace secsgem::gem
|
||||
@@ -0,0 +1,139 @@
|
||||
#pragma once
|
||||
|
||||
#include <asio.hpp>
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <deque>
|
||||
#include <functional>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <system_error>
|
||||
|
||||
#include "secsgem/hsms/frame.hpp"
|
||||
#include "secsgem/hsms/types.hpp"
|
||||
#include "secsgem/secs2/message.hpp"
|
||||
|
||||
namespace secsgem::hsms {
|
||||
|
||||
// One HSMS session over a connected TCP socket. Drives the connection state
|
||||
// machine (NOT_SELECTED -> SELECTED), runs the SELECT/LINKTEST/SEPARATE control
|
||||
// handshakes and the T3/T6/T7/T8 timers, and correlates data replies by system
|
||||
// bytes. Single-threaded: all work runs on the socket's executor.
|
||||
class Connection : public std::enable_shared_from_this<Connection> {
|
||||
public:
|
||||
enum class Mode { Active, Passive };
|
||||
enum class State { NotSelected, Selected };
|
||||
|
||||
// Handler for an inbound primary (request) message. Return a reply Message to
|
||||
// send it back (system bytes are filled in automatically), or nullopt for none.
|
||||
using MessageHandler = std::function<std::optional<secs2::Message>(const secs2::Message&)>;
|
||||
using SelectedHandler = std::function<void()>;
|
||||
using ClosedHandler = std::function<void(const std::string& reason)>;
|
||||
using ReplyHandler = std::function<void(std::error_code, const secs2::Message&)>;
|
||||
using LogHandler = std::function<void(const std::string&)>;
|
||||
|
||||
Connection(asio::ip::tcp::socket socket, Mode mode, uint16_t device_id, Timers timers);
|
||||
|
||||
void set_message_handler(MessageHandler h) { on_message_ = std::move(h); }
|
||||
void set_selected_handler(SelectedHandler h) { on_selected_ = std::move(h); }
|
||||
void set_closed_handler(ClosedHandler h) { on_closed_ = std::move(h); }
|
||||
void set_log_handler(LogHandler h) { on_log_ = std::move(h); }
|
||||
|
||||
// Begin the read loop. Active mode also initiates the SELECT handshake;
|
||||
// Passive mode arms the T7 not-selected timer and waits for Select.req.
|
||||
void start();
|
||||
|
||||
// Send a primary data message (W-bit set) and invoke `cb` with the reply or a
|
||||
// timeout/error. Must be SELECTED.
|
||||
void send_request(secs2::Message msg, ReplyHandler cb);
|
||||
|
||||
// Send a data message with no reply expected.
|
||||
void send_data(secs2::Message msg);
|
||||
|
||||
// Graceful teardown: send Separate.req and close.
|
||||
void separate();
|
||||
|
||||
// Hard close.
|
||||
void close(const std::string& reason);
|
||||
|
||||
State state() const { return state_; }
|
||||
bool selected() const { return state_ == State::Selected; }
|
||||
|
||||
private:
|
||||
// --- read path ---
|
||||
void read_length();
|
||||
void on_length(std::error_code ec, std::size_t n);
|
||||
void on_payload(std::error_code ec, std::size_t n);
|
||||
void handle_frame(Frame frame);
|
||||
void handle_data(const Frame& frame);
|
||||
void handle_control(const Frame& frame);
|
||||
|
||||
// --- write path ---
|
||||
void send_frame(Frame frame);
|
||||
void write_next();
|
||||
|
||||
// --- handshakes & timers ---
|
||||
void send_select_req();
|
||||
void send_linktest_req();
|
||||
void enter_selected();
|
||||
void arm_t7();
|
||||
void arm_linktest();
|
||||
void start_control_transaction(SType expected_response, uint32_t system_bytes,
|
||||
const char* what);
|
||||
void clear_control_transaction();
|
||||
|
||||
uint32_t next_system_bytes();
|
||||
void log(const std::string& msg);
|
||||
|
||||
asio::ip::tcp::socket socket_;
|
||||
asio::steady_timer t6_timer_; // control transaction timeout
|
||||
asio::steady_timer t7_timer_; // not-selected timeout (passive)
|
||||
asio::steady_timer t8_timer_; // intercharacter timeout (during a read)
|
||||
asio::steady_timer linktest_timer_; // periodic linktest interval
|
||||
|
||||
Mode mode_;
|
||||
uint16_t device_id_;
|
||||
Timers timers_;
|
||||
State state_ = State::NotSelected;
|
||||
bool closed_ = false;
|
||||
|
||||
std::array<uint8_t, kLengthPrefixSize> len_buf_{};
|
||||
std::vector<uint8_t> payload_;
|
||||
|
||||
std::deque<std::vector<uint8_t>> write_queue_;
|
||||
bool writing_ = false;
|
||||
bool close_after_flush_ = false;
|
||||
std::string close_reason_;
|
||||
|
||||
uint32_t next_system_bytes_ = 1;
|
||||
|
||||
// outstanding data request transactions, keyed by system bytes
|
||||
struct PendingRequest {
|
||||
ReplyHandler cb;
|
||||
std::shared_ptr<asio::steady_timer> t3;
|
||||
};
|
||||
std::map<uint32_t, PendingRequest> pending_requests_;
|
||||
|
||||
// single outstanding control transaction (select / linktest)
|
||||
struct PendingControl {
|
||||
SType expected_response;
|
||||
uint32_t system_bytes;
|
||||
};
|
||||
std::optional<PendingControl> pending_control_;
|
||||
|
||||
MessageHandler on_message_;
|
||||
SelectedHandler on_selected_;
|
||||
ClosedHandler on_closed_;
|
||||
LogHandler on_log_;
|
||||
};
|
||||
|
||||
// Error category for HSMS protocol-level failures surfaced through ReplyHandler.
|
||||
enum class Error {
|
||||
Timeout = 1,
|
||||
Closed = 2,
|
||||
};
|
||||
std::error_code make_error(Error e);
|
||||
|
||||
} // namespace secsgem::hsms
|
||||
@@ -0,0 +1,40 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <stdexcept>
|
||||
#include <vector>
|
||||
|
||||
#include "secsgem/hsms/header.hpp"
|
||||
|
||||
namespace secsgem::hsms {
|
||||
|
||||
class FrameError : public std::runtime_error {
|
||||
public:
|
||||
using std::runtime_error::runtime_error;
|
||||
};
|
||||
|
||||
// One HSMS message: a header plus an optional SECS-II body (control messages
|
||||
// have no body). On the wire it is prefixed by a 4-byte big-endian length that
|
||||
// counts the header (10) plus the body.
|
||||
struct Frame {
|
||||
Header header;
|
||||
std::vector<uint8_t> body;
|
||||
|
||||
Frame() = default;
|
||||
explicit Frame(Header h, std::vector<uint8_t> b = {})
|
||||
: header(h), body(std::move(b)) {}
|
||||
|
||||
// Full wire bytes including the 4-byte length prefix.
|
||||
std::vector<uint8_t> encode() const;
|
||||
|
||||
// Decode a message from its payload (header + body, i.e. the bytes that
|
||||
// follow the length prefix). `len` must be >= 10.
|
||||
static Frame decode(const uint8_t* payload, std::size_t len);
|
||||
};
|
||||
|
||||
// HSMS message length prefix is 4 bytes; payload must be at least the 10-byte
|
||||
// header.
|
||||
inline constexpr std::size_t kLengthPrefixSize = 4;
|
||||
inline constexpr std::size_t kHeaderSize = 10;
|
||||
|
||||
} // namespace secsgem::hsms
|
||||
@@ -0,0 +1,60 @@
|
||||
#pragma once
|
||||
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
|
||||
#include "secsgem/hsms/types.hpp"
|
||||
|
||||
namespace secsgem::hsms {
|
||||
|
||||
// The fixed 10-byte HSMS message header (SEMI E37). The interpretation of
|
||||
// byte2/byte3 depends on `stype`: for data messages byte2 = (W<<7)|stream and
|
||||
// byte3 = function; for control messages they carry status / reason codes.
|
||||
struct Header {
|
||||
uint16_t session_id = kControlSessionId;
|
||||
uint8_t byte2 = 0;
|
||||
uint8_t byte3 = 0;
|
||||
uint8_t ptype = kPTypeSecsII;
|
||||
SType stype = SType::Data;
|
||||
uint32_t system_bytes = 0;
|
||||
|
||||
// Data-message field views.
|
||||
bool w_bit() const { return (byte2 & 0x80) != 0; }
|
||||
uint8_t stream() const { return byte2 & 0x7F; }
|
||||
uint8_t function() const { return byte3; }
|
||||
|
||||
static Header data_message(uint16_t session_id, uint8_t stream, uint8_t function,
|
||||
bool reply_expected, uint32_t system_bytes) {
|
||||
Header h;
|
||||
h.session_id = session_id;
|
||||
h.byte2 = static_cast<uint8_t>((reply_expected ? 0x80 : 0x00) | (stream & 0x7F));
|
||||
h.byte3 = function;
|
||||
h.ptype = kPTypeSecsII;
|
||||
h.stype = SType::Data;
|
||||
h.system_bytes = system_bytes;
|
||||
return h;
|
||||
}
|
||||
|
||||
static Header control(SType stype, uint32_t system_bytes,
|
||||
uint16_t session_id = kControlSessionId, uint8_t byte2 = 0,
|
||||
uint8_t byte3 = 0) {
|
||||
Header h;
|
||||
h.session_id = session_id;
|
||||
h.byte2 = byte2;
|
||||
h.byte3 = byte3;
|
||||
h.ptype = kPTypeSecsII;
|
||||
h.stype = stype;
|
||||
h.system_bytes = system_bytes;
|
||||
return h;
|
||||
}
|
||||
|
||||
std::array<uint8_t, 10> encode() const;
|
||||
static Header decode(const uint8_t* data); // reads exactly 10 bytes
|
||||
|
||||
std::string describe() const;
|
||||
|
||||
bool operator==(const Header&) const = default;
|
||||
};
|
||||
|
||||
} // namespace secsgem::hsms
|
||||
@@ -0,0 +1,62 @@
|
||||
#pragma once
|
||||
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
|
||||
namespace secsgem::hsms {
|
||||
|
||||
// HSMS session type (byte 5 of the message header) — SEMI E37.
|
||||
enum class SType : uint8_t {
|
||||
Data = 0,
|
||||
SelectReq = 1,
|
||||
SelectRsp = 2,
|
||||
DeselectReq = 3,
|
||||
DeselectRsp = 4,
|
||||
LinktestReq = 5,
|
||||
LinktestRsp = 6,
|
||||
RejectReq = 7,
|
||||
SeparateReq = 9,
|
||||
};
|
||||
|
||||
const char* stype_name(SType s);
|
||||
|
||||
// Select.rsp status (header byte 3).
|
||||
enum class SelectStatus : uint8_t {
|
||||
Ok = 0,
|
||||
AlreadyActive = 1,
|
||||
NotReady = 2,
|
||||
ConnectExhaust = 3,
|
||||
};
|
||||
|
||||
// Deselect.rsp status (header byte 3).
|
||||
enum class DeselectStatus : uint8_t {
|
||||
Ok = 0,
|
||||
NotEstablished = 1,
|
||||
Busy = 2,
|
||||
};
|
||||
|
||||
// Reject.req reason code (header byte 3).
|
||||
enum class RejectReason : uint8_t {
|
||||
StypeNotSupported = 1,
|
||||
PtypeNotSupported = 2,
|
||||
TransactionNotOpen = 3,
|
||||
EntityNotSelected = 4,
|
||||
};
|
||||
|
||||
// Presentation type 0 == SECS-II message encoding.
|
||||
inline constexpr uint8_t kPTypeSecsII = 0;
|
||||
|
||||
// Control messages carry no device id; E37 recommends 0xFFFF.
|
||||
inline constexpr uint16_t kControlSessionId = 0xFFFF;
|
||||
|
||||
// HSMS protocol timers (SEMI E37 defaults).
|
||||
struct Timers {
|
||||
std::chrono::milliseconds t3{45000}; // reply timeout
|
||||
std::chrono::milliseconds t5{10000}; // connect separation timeout
|
||||
std::chrono::milliseconds t6{5000}; // control transaction timeout
|
||||
std::chrono::milliseconds t7{10000}; // not-selected timeout
|
||||
std::chrono::milliseconds t8{5000}; // network intercharacter timeout
|
||||
std::chrono::milliseconds linktest{0}; // linktest interval; 0 disables
|
||||
};
|
||||
|
||||
} // namespace secsgem::hsms
|
||||
@@ -0,0 +1,30 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "secsgem/secs2/item.hpp"
|
||||
|
||||
namespace secsgem::secs2 {
|
||||
|
||||
class CodecError : public std::runtime_error {
|
||||
public:
|
||||
using std::runtime_error::runtime_error;
|
||||
};
|
||||
|
||||
// Encode a single item (with all nested children) to its SECS-II byte form.
|
||||
std::vector<uint8_t> encode(const Item& item);
|
||||
void encode_into(const Item& item, std::vector<uint8_t>& out);
|
||||
|
||||
// Decode exactly one item starting at `pos`, advancing `pos` past it.
|
||||
Item decode_at(const uint8_t* data, std::size_t len, std::size_t& pos);
|
||||
|
||||
// Decode one item that occupies the entire buffer; throws on trailing bytes.
|
||||
Item decode(const std::vector<uint8_t>& bytes);
|
||||
|
||||
// Human-readable SML (SECS Message Language) rendering, single line.
|
||||
std::string to_sml(const Item& item);
|
||||
|
||||
} // namespace secsgem::secs2
|
||||
@@ -0,0 +1,119 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <variant>
|
||||
#include <vector>
|
||||
|
||||
namespace secsgem::secs2 {
|
||||
|
||||
// SECS-II data item format codes (the 6-bit value, before being shifted left
|
||||
// two bits to make the format byte). Values are the canonical octal codes from
|
||||
// SEMI E5.
|
||||
enum class Format : uint8_t {
|
||||
List = 000, // 0
|
||||
Binary = 010, // 8
|
||||
Boolean = 011, // 9
|
||||
ASCII = 020, // 16
|
||||
I8 = 030, // 24
|
||||
I1 = 031, // 25
|
||||
I2 = 032, // 26
|
||||
I4 = 034, // 28
|
||||
F8 = 040, // 32
|
||||
F4 = 044, // 36
|
||||
U8 = 050, // 40
|
||||
U1 = 051, // 41
|
||||
U2 = 052, // 42
|
||||
U4 = 054, // 44
|
||||
};
|
||||
|
||||
const char* format_name(Format f);
|
||||
|
||||
// Number of bytes one element of the given format occupies on the wire.
|
||||
// Lists are special (their length is an element count, not a byte count) and
|
||||
// return 0 here.
|
||||
std::size_t element_size(Format f);
|
||||
|
||||
// A SECS-II data item: a typed, possibly nested value. Lists hold child items;
|
||||
// every other format holds a homogeneous array of scalars (a single scalar is
|
||||
// just an array of length one). The active variant alternative is kept in sync
|
||||
// with `format_`; several formats (Binary, Boolean, U1) share the same C++
|
||||
// storage type and are disambiguated by `format_`.
|
||||
class Item {
|
||||
public:
|
||||
using List = std::vector<Item>;
|
||||
using Storage = std::variant<
|
||||
List, // List
|
||||
std::string, // ASCII
|
||||
std::vector<uint8_t>, // Binary, Boolean, U1
|
||||
std::vector<int8_t>, // I1
|
||||
std::vector<int16_t>, // I2
|
||||
std::vector<int32_t>, // I4
|
||||
std::vector<int64_t>, // I8
|
||||
std::vector<uint16_t>, // U2
|
||||
std::vector<uint32_t>, // U4
|
||||
std::vector<uint64_t>, // U8
|
||||
std::vector<float>, // F4
|
||||
std::vector<double>>; // F8
|
||||
|
||||
Item() : format_(Format::List), data_(List{}) {}
|
||||
|
||||
Format format() const { return format_; }
|
||||
bool is_list() const { return format_ == Format::List; }
|
||||
|
||||
// Number of elements: child count for lists, character count for ASCII,
|
||||
// array length for numeric/binary formats.
|
||||
std::size_t size() const;
|
||||
|
||||
// --- Factory functions -------------------------------------------------
|
||||
static Item list(List items) { return Item(Format::List, std::move(items)); }
|
||||
static Item ascii(std::string s) { return Item(Format::ASCII, std::move(s)); }
|
||||
static Item binary(std::vector<uint8_t> b) { return Item(Format::Binary, std::move(b)); }
|
||||
static Item boolean(std::vector<uint8_t> b) { return Item(Format::Boolean, std::move(b)); }
|
||||
static Item boolean(bool b) { return Item(Format::Boolean, std::vector<uint8_t>{static_cast<uint8_t>(b ? 1 : 0)}); }
|
||||
|
||||
static Item u1(std::vector<uint8_t> v) { return Item(Format::U1, std::move(v)); }
|
||||
static Item u2(std::vector<uint16_t> v) { return Item(Format::U2, std::move(v)); }
|
||||
static Item u4(std::vector<uint32_t> v) { return Item(Format::U4, std::move(v)); }
|
||||
static Item u8(std::vector<uint64_t> v) { return Item(Format::U8, std::move(v)); }
|
||||
static Item i1(std::vector<int8_t> v) { return Item(Format::I1, std::move(v)); }
|
||||
static Item i2(std::vector<int16_t> v) { return Item(Format::I2, std::move(v)); }
|
||||
static Item i4(std::vector<int32_t> v) { return Item(Format::I4, std::move(v)); }
|
||||
static Item i8(std::vector<int64_t> v) { return Item(Format::I8, std::move(v)); }
|
||||
static Item f4(std::vector<float> v) { return Item(Format::F4, std::move(v)); }
|
||||
static Item f8(std::vector<double> v) { return Item(Format::F8, std::move(v)); }
|
||||
|
||||
// Scalar convenience overloads.
|
||||
static Item u1(uint8_t v) { return u1(std::vector<uint8_t>{v}); }
|
||||
static Item u2(uint16_t v) { return u2(std::vector<uint16_t>{v}); }
|
||||
static Item u4(uint32_t v) { return u4(std::vector<uint32_t>{v}); }
|
||||
static Item u8(uint64_t v) { return u8(std::vector<uint64_t>{v}); }
|
||||
static Item i1(int8_t v) { return i1(std::vector<int8_t>{v}); }
|
||||
static Item i2(int16_t v) { return i2(std::vector<int16_t>{v}); }
|
||||
static Item i4(int32_t v) { return i4(std::vector<int32_t>{v}); }
|
||||
static Item i8(int64_t v) { return i8(std::vector<int64_t>{v}); }
|
||||
static Item f4(float v) { return f4(std::vector<float>{v}); }
|
||||
static Item f8(double v) { return f8(std::vector<double>{v}); }
|
||||
|
||||
// Construct directly from a format and matching storage (used by the decoder).
|
||||
static Item raw(Format f, Storage s) { return Item(f, std::move(s)); }
|
||||
|
||||
// --- Typed accessors (throw std::bad_variant_access on mismatch) --------
|
||||
const List& as_list() const { return std::get<List>(data_); }
|
||||
List& as_list() { return std::get<List>(data_); }
|
||||
const std::string& as_ascii() const { return std::get<std::string>(data_); }
|
||||
const std::vector<uint8_t>& as_bytes() const { return std::get<std::vector<uint8_t>>(data_); }
|
||||
|
||||
const Storage& storage() const { return data_; }
|
||||
|
||||
bool operator==(const Item&) const = default;
|
||||
|
||||
private:
|
||||
Item(Format f, Storage s) : format_(f), data_(std::move(s)) {}
|
||||
|
||||
Format format_;
|
||||
Storage data_;
|
||||
};
|
||||
|
||||
} // namespace secsgem::secs2
|
||||
@@ -0,0 +1,37 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "secsgem/secs2/item.hpp"
|
||||
|
||||
namespace secsgem::secs2 {
|
||||
|
||||
// A logical SECS-II message: stream/function, the W-bit (reply expected), and
|
||||
// an optional root data item as the body. The transport (HSMS) header carries
|
||||
// stream/function/W on the wire; the body bytes are the encoded root item.
|
||||
struct Message {
|
||||
uint8_t stream = 0;
|
||||
uint8_t function = 0;
|
||||
bool reply_expected = false; // W-bit
|
||||
std::optional<Item> body;
|
||||
|
||||
Message() = default;
|
||||
Message(uint8_t s, uint8_t f, bool w, std::optional<Item> b = std::nullopt)
|
||||
: stream(s), function(f), reply_expected(w), body(std::move(b)) {}
|
||||
|
||||
// Encode the body item to bytes (empty if there is no body).
|
||||
std::vector<uint8_t> encode_body() const;
|
||||
|
||||
// Build a Message from stream/function/W and raw body bytes (empty -> no body).
|
||||
static Message from_body(uint8_t stream, uint8_t function, bool reply_expected,
|
||||
const std::vector<uint8_t>& body_bytes);
|
||||
|
||||
// e.g. S1F2 W
|
||||
// <L [2] <A "MDLN"> <A "1.0"> >
|
||||
std::string sml() const;
|
||||
};
|
||||
|
||||
} // namespace secsgem::secs2
|
||||
Reference in New Issue
Block a user