Files
secs-gem/include/secsgem/gem/messages.hpp
T
raphael 96b02f8b50 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>
2026-06-02 00:21:10 +02:00

761 lines
28 KiB
C++

#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