#6 SxFy codegen from YAML message catalog

The bulk of the per-SxFy boilerplate — ~90 hand-written builders and parsers
across 30+ message pairs — is now generated at build time from a single YAML
catalog. Adding a new SECS-II message becomes a YAML edit; the C++ code is
generated, not maintained.

What changed
------------

data/messages.yaml
  The catalog. Describes every SxFy currently supported: stream, function,
  W-bit, builder name, optional parser name, and a recursive body shape
  grammar (scalar / list / list_of).  Shapes carry SECS-II item types
  (ASCII, BINARY_BYTE, U4, F8, ITEM, ...) and optional C++ enum types for
  typed ack codes.  Inner-most fields can be marked external_struct: true
  so structs already defined elsewhere (ReportData, CommandParameter) are
  referenced rather than redefined.

tools/gen_messages.py
  Python codegen.  Reads the catalog and emits one inline header.  Handles
  nested shapes via depth-unique variable names in the generated IIFEs, so
  S6F11's three-level nesting compiles without lambda capture conflicts.
  Post-order traversal ensures inner structs are emitted before outer ones
  that reference them.  Generates positional and (where applicable) struct
  builder overloads, plus struct-returning parsers for messages with a
  `parser:` entry.

CMakeLists.txt
  Custom command runs gen_messages.py at configure/build time and emits
  ${CMAKE_BINARY_DIR}/generated/secsgem/gem/messages.hpp.  Added to the
  secsgem target's include path so `#include "secsgem/gem/messages.hpp"`
  resolves to the generated file.  Depends on the YAML + the script, so
  edits trigger regen automatically.

Dockerfile
  Added python3 + python3-yaml to the toolchain image.

include/secsgem/gem/messages_helpers.hpp  (new)
  The small set of hand-written helpers the generated header relies on:
  scalar accessors (as_ascii / as_u4_scalar / ...), parse_u4_list_body,
  u4_list_item, ack_byte, ALED byte constants, and the two special-case
  messages whose shape doesn't fit the codegen schema (S1F4 needs
  per-row std::optional<Item> semantics; S5F6 needs a per-row ALCD
  callback).

include/secsgem/gem/messages.hpp  (deleted)
  The hand-written builder/parser file is gone. Its content now flows
  through the catalog + codegen.

include/secsgem/gem/data_model.hpp
  Moved CommandParameter to namespace scope so it can be shared between
  the data model and the messages.yaml's external_struct entry.  Added
  `using CommandParam = CommandParameter` for back-compat.

apps/secs_server.cpp + apps/secs_client.cpp
  Updated the call sites that the codegen renamed or restructured:
  - parse_terminal_display() split into parse_s10f1 / parse_s10f3.
  - s1f14_establish_comms_ack now takes a McAck struct for the nested
    identity (mdln, softrev) — call site uses brace init.
  - S2F33/S2F35 parsers return strongly-typed entries (DefineReportEntry,
    LinkEventEntry); the server adapts these to the model's pair-based
    API at the call site.
  - S2F15 parser returns vector<EcSet>; iterate by .ecid/.value.
  - S5F3 parser returns EnableAlarmRequest{aled, alid}; bool comes from
    (aled & 0x80) != 0.
  - AlarmReport's is_set()/category() methods removed; callers use the
    raw alcd byte with bit math (alcd & 0x80, alcd & 0x7F).
  - s2f42_host_command_ack and s2f41_host_command always take their
    second list argument explicitly (no defaulted arg from codegen).

tests/test_messages.cpp
  Updated to construct the generated typed structs (EcSet, StatusName,
  EnableAlarmRequest, CommandParameter, CommandParameterAck) and to read
  the new field names (.ecid/.value, .rptid/.vids, .ceid/.rptids,
  .name/.code).

Coverage
--------

Generated by codegen (44 SxFy in catalog):

  S1F1, S1F2, S1F3, S1F11, S1F12, S1F13, S1F14, S1F15, S1F16, S1F17, S1F18
  S2F13, S2F14, S2F15, S2F16, S2F17, S2F18, S2F29, S2F30, S2F31, S2F32
  S2F33, S2F34, S2F35, S2F36, S2F37, S2F38, S2F41, S2F42
  S5F1, S5F2, S5F3, S5F4, S5F5
  S6F11, S6F12
  S7F3, S7F4, S7F5, S7F6, S7F19, S7F20
  S10F1, S10F2, S10F3, S10F4

Hand-written (in messages_helpers.hpp):

  S1F4   list-of-optional-items shape (nullopt -> <L,0>)
  S5F6   per-row ALCD via callback

Adding a new SxFy
-----------------

Append a single entry to data/messages.yaml describing the body shape.
The builder + parser appear in messages.hpp after the next build.  The
host command above for S2F41 (or any other added SxFy) requires no C++
changes if the body fits the recursive scalar/list/list_of grammar.

Tests: 67 cases / 384 assertions still passing.
Demo: byte-for-byte identical behaviour (Select, Establish, Online,
S1F11/F3 namelist+values, S2F29 EC namelist, S2F33/F35/F37 dynamic event
subscription, S2F41 START -> S6F11 emission, S5F5/F3 alarm directory +
enable, S2F41 FAULT -> S5F1 alarm + S6F11, S7F19/F5 recipe ops, S10F1
terminal, S1F15 offline, Separate).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-06-02 09:43:36 +02:00
parent b871cd9da2
commit 29db1caedb
10 changed files with 1151 additions and 810 deletions
+9 -4
View File
@@ -49,6 +49,14 @@ struct CollectionEvent {
std::string name;
};
// One <CPNAME, CPVAL> entry on S2F41. Defined here (not in the generated
// messages.hpp) so the data model can name it in its public API; the codegen
// references it as an external struct.
struct CommandParameter {
std::string name;
s2::Item value;
};
struct Report {
uint32_t id;
std::vector<uint32_t> vids;
@@ -157,10 +165,7 @@ enum class TerminalAck : uint8_t {
// all access happens on the Asio executor.
class EquipmentDataModel {
public:
struct CommandParam {
std::string name;
s2::Item value;
};
using CommandParam = CommandParameter; // back-compat alias
// Declarative host-command effect, loaded from YAML. The server
// dispatches a command by looking up the spec and (optionally) firing
-760
View File
@@ -1,760 +0,0 @@
#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
+130
View File
@@ -0,0 +1,130 @@
#pragma once
#include <cstdint>
#include <functional>
#include <optional>
#include <string>
#include <variant>
#include <vector>
#include "secsgem/gem/data_model.hpp"
#include "secsgem/secs2/item.hpp"
#include "secsgem/secs2/message.hpp"
// Hand-written helpers used by the generated messages.hpp: scalar accessors
// (one per SECS-II type), a few list helpers, and the two special-case
// messages whose shape doesn't fit the codegen schema (S1F4 needs per-row
// optional values; S5F6 needs a callback to compute ALCD per alarm).
namespace secsgem::gem {
namespace s2 = secsgem::secs2;
namespace secs2 = secsgem::secs2; // alias used by generated code
// ---- Scalar accessors ----------------------------------------------------
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 std::optional<std::string> as_binary_string(const s2::Item& item) {
if (item.format() != s2::Format::Binary) return std::nullopt;
const auto& v = item.as_bytes();
return std::string(v.begin(), v.end());
}
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<bool> as_boolean(const s2::Item& item) {
if (item.format() != s2::Format::Boolean) return std::nullopt;
const auto& v = item.as_bytes();
if (v.empty()) return std::nullopt;
return v.front() != 0;
}
// Templated typed-scalar accessor — one specialization per SECS-II numeric.
template <typename Vec>
inline std::optional<typename Vec::value_type> first_or_none(const s2::Item& item,
s2::Format want) {
if (item.format() != want) return std::nullopt;
const auto& v = std::get<Vec>(item.storage());
if (v.empty()) return std::nullopt;
return v.front();
}
inline std::optional<uint8_t> as_u1_scalar(const s2::Item& i) { return first_or_none<std::vector<uint8_t>>(i, s2::Format::U1); }
inline std::optional<uint16_t> as_u2_scalar(const s2::Item& i) { return first_or_none<std::vector<uint16_t>>(i, s2::Format::U2); }
inline std::optional<uint32_t> as_u4_scalar(const s2::Item& i) { return first_or_none<std::vector<uint32_t>>(i, s2::Format::U4); }
inline std::optional<uint64_t> as_u8_scalar(const s2::Item& i) { return first_or_none<std::vector<uint64_t>>(i, s2::Format::U8); }
inline std::optional<int8_t> as_i1_scalar(const s2::Item& i) { return first_or_none<std::vector<int8_t>>(i, s2::Format::I1); }
inline std::optional<int16_t> as_i2_scalar(const s2::Item& i) { return first_or_none<std::vector<int16_t>>(i, s2::Format::I2); }
inline std::optional<int32_t> as_i4_scalar(const s2::Item& i) { return first_or_none<std::vector<int32_t>>(i, s2::Format::I4); }
inline std::optional<int64_t> as_i8_scalar(const s2::Item& i) { return first_or_none<std::vector<int64_t>>(i, s2::Format::I8); }
inline std::optional<float> as_f4_scalar(const s2::Item& i) { return first_or_none<std::vector<float>>(i, s2::Format::F4); }
inline std::optional<double> as_f8_scalar(const s2::Item& i) { return first_or_none<std::vector<double>>(i, s2::Format::F8); }
// ---- List helpers --------------------------------------------------------
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));
}
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;
}
// Generic "body is <B ack>" reader, used by tests + apps as a quick helper.
inline std::optional<uint8_t> ack_byte(const s2::Message& m) {
if (!m.body) return std::nullopt;
return as_binary_first(*m.body);
}
// ---- S1F4: list of values, nullopt -> <L,0> -----------------------------
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({}));
}
return s2::Message(1, 4, false, s2::Item::list(std::move(children)));
}
// ---- S5F6: alarm directory; ALCD bit-7 from per-row callback ------------
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)));
}
// ---- ALED byte constants for S5F3 ---------------------------------------
inline constexpr uint8_t kAlarmEnableByte = 0x80;
inline constexpr uint8_t kAlarmDisableByte = 0x00;
} // namespace secsgem::gem