#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
+20 -1
View File
@@ -26,6 +26,21 @@ target_compile_definitions(asio INTERFACE ASIO_STANDALONE ASIO_NO_DEPRECATED)
target_link_libraries(asio INTERFACE Threads::Threads) target_link_libraries(asio INTERFACE Threads::Threads)
# --- Core library --------------------------------------------------------- # --- Core library ---------------------------------------------------------
# --- Codegen: SECS-II message catalog -> messages.hpp ---------------------
set(MESSAGES_HPP ${CMAKE_BINARY_DIR}/generated/secsgem/gem/messages.hpp)
add_custom_command(
OUTPUT ${MESSAGES_HPP}
COMMAND ${CMAKE_COMMAND} -E make_directory ${CMAKE_BINARY_DIR}/generated/secsgem/gem
COMMAND python3 ${CMAKE_SOURCE_DIR}/tools/gen_messages.py
--catalog ${CMAKE_SOURCE_DIR}/data/messages.yaml
--out ${MESSAGES_HPP}
DEPENDS ${CMAKE_SOURCE_DIR}/tools/gen_messages.py
${CMAKE_SOURCE_DIR}/data/messages.yaml
COMMENT "Generating SECS-II messages.hpp from data/messages.yaml"
VERBATIM
)
add_custom_target(generate_messages DEPENDS ${MESSAGES_HPP})
add_library(secsgem add_library(secsgem
src/secs2/codec.cpp src/secs2/codec.cpp
src/hsms/header.cpp src/hsms/header.cpp
@@ -35,7 +50,11 @@ add_library(secsgem
src/config/loader.cpp src/config/loader.cpp
src/endpoint.cpp src/endpoint.cpp
) )
target_include_directories(secsgem PUBLIC include) add_dependencies(secsgem generate_messages)
target_include_directories(secsgem PUBLIC
include
${CMAKE_BINARY_DIR}/generated
)
target_link_libraries(secsgem PUBLIC asio yaml-cpp) target_link_libraries(secsgem PUBLIC asio yaml-cpp)
# --- Demo executables ----------------------------------------------------- # --- Demo executables -----------------------------------------------------
+2
View File
@@ -9,6 +9,8 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
git \ git \
libasio-dev \ libasio-dev \
libyaml-cpp-dev \ libyaml-cpp-dev \
python3 \
python3-yaml \
ca-certificates \ ca-certificates \
bash \ bash \
&& rm -rf /var/lib/apt/lists/* && rm -rf /var/lib/apt/lists/*
+5 -5
View File
@@ -78,7 +78,7 @@ int main(int argc, char** argv) {
[logfn](const s2::Message& msg) -> std::optional<s2::Message> { [logfn](const s2::Message& msg) -> std::optional<s2::Message> {
// S10F3: terminal display from equipment. // S10F3: terminal display from equipment.
if (msg.stream == 10 && msg.function == 3) { if (msg.stream == 10 && msg.function == 3) {
auto td = gem::parse_terminal_display(msg); auto td = gem::parse_s10f3(msg);
if (td) logfn("TERMINAL[" + std::to_string(td->tid) + "] " + td->text); if (td) logfn("TERMINAL[" + std::to_string(td->tid) + "] " + td->text);
return gem::s10f4_terminal_display_ack(gem::TerminalAck::Accepted); return gem::s10f4_terminal_display_ack(gem::TerminalAck::Accepted);
} }
@@ -100,9 +100,9 @@ int main(int argc, char** argv) {
if (msg.stream == 5 && msg.function == 1) { if (msg.stream == 5 && msg.function == 1) {
auto a = gem::parse_s5f1(msg); auto a = gem::parse_s5f1(msg);
if (a) { if (a) {
logfn(std::string("ALARM ") + (a->is_set() ? "SET" : "CLR") + logfn(std::string("ALARM ") + ((a->alcd & 0x80) ? "SET" : "CLR") +
" ALID=" + std::to_string(a->alid) + " ALID=" + std::to_string(a->alid) +
" cat=" + std::to_string(a->category()) + " \"" + a->altx + "\""); " cat=" + std::to_string(a->alcd & 0x7F) + " \"" + a->altx + "\"");
} }
return gem::s5f2_alarm_ack(gem::AlarmAck::Accept); return gem::s5f2_alarm_ack(gem::AlarmAck::Accept);
} }
@@ -248,7 +248,7 @@ int main(int argc, char** argv) {
// 11. Enable alarm 1 (so the equipment is allowed to send S5F1 for it). // 11. Enable alarm 1 (so the equipment is allowed to send S5F1 for it).
seq->steps.push_back([conn, logfn, fail](auto next) { seq->steps.push_back([conn, logfn, fail](auto next) {
conn->send_request(gem::s5f3_enable_alarm(true, kAlarmChiller), conn->send_request(gem::s5f3_enable_alarm(gem::kAlarmEnableByte, kAlarmChiller),
[logfn, fail, next](std::error_code ec, const s2::Message& reply) { [logfn, fail, next](std::error_code ec, const s2::Message& reply) {
if (ec) { fail("S5F3", ec); return; } if (ec) { fail("S5F3", ec); return; }
auto a = gem::ack_byte(reply); auto a = gem::ack_byte(reply);
@@ -259,7 +259,7 @@ int main(int argc, char** argv) {
// 12. Host command FAULT -> equipment sets alarm 1 and emits S5F1 + S6F11. // 12. Host command FAULT -> equipment sets alarm 1 and emits S5F1 + S6F11.
seq->steps.push_back([conn, logfn, fail, pause_then](auto next) { seq->steps.push_back([conn, logfn, fail, pause_then](auto next) {
conn->send_request(gem::s2f41_host_command("FAULT"), conn->send_request(gem::s2f41_host_command("FAULT", {}),
[logfn, fail, pause_then, next](std::error_code ec, const s2::Message& reply) { [logfn, fail, pause_then, next](std::error_code ec, const s2::Message& reply) {
if (ec) { fail("S2F41/FAULT", ec); return; } if (ec) { fail("S2F41/FAULT", ec); return; }
auto r = gem::parse_s2f42(reply); auto r = gem::parse_s2f42(reply);
+32 -14
View File
@@ -139,12 +139,16 @@ int main(int argc, char** argv) {
return gem::s1f4_selected_status_data(values); return gem::s1f4_selected_status_data(values);
}); });
router.on(1, 11, [model, logfn](const s2::Message&) { router.on(1, 11, [model, logfn](const s2::Message&) {
logfn("S1F11 -> S1F12 (namelist)"); std::vector<gem::StatusName> rows;
return gem::s1f12_status_namelist_data(model->all_status_variables()); for (const auto& sv : model->all_status_variables())
rows.push_back({sv.id, sv.name, sv.units});
logfn("S1F11 -> S1F12 (namelist, " + std::to_string(rows.size()) + ")");
return gem::s1f12_status_namelist_data(rows);
}); });
router.on(1, 13, [desc, logfn](const s2::Message&) { router.on(1, 13, [desc, logfn](const s2::Message&) {
logfn("S1F13 -> S1F14"); logfn("S1F13 -> S1F14");
return gem::s1f14_establish_comms_ack(gem::CommAck::Accept, desc.model_name, desc.software_rev); return gem::s1f14_establish_comms_ack(gem::CommAck::Accept,
{desc.model_name, desc.software_rev});
}); });
router.on(1, 15, [sm, logfn](const s2::Message&) { router.on(1, 15, [sm, logfn](const s2::Message&) {
auto ack = sm->on_host_request_offline(); auto ack = sm->on_host_request_offline();
@@ -173,8 +177,8 @@ int main(int argc, char** argv) {
auto eac = gem::EquipmentAck::Accept; auto eac = gem::EquipmentAck::Accept;
if (!sets) eac = gem::EquipmentAck::Denied_OutOfRange; if (!sets) eac = gem::EquipmentAck::Denied_OutOfRange;
else else
for (const auto& [id, val] : *sets) { for (const auto& s : *sets) {
auto r = model->set_equipment_constant_value(id, val); auto r = model->set_equipment_constant_value(s.ecid, s.value);
if (r != gem::EquipmentAck::Accept) eac = r; if (r != gem::EquipmentAck::Accept) eac = r;
} }
logfn("S2F15 -> S2F16 EAC=" + std::to_string(static_cast<int>(eac))); logfn("S2F15 -> S2F16 EAC=" + std::to_string(static_cast<int>(eac)));
@@ -193,8 +197,11 @@ int main(int argc, char** argv) {
auto ec = model->equipment_constant(id); auto ec = model->equipment_constant(id);
if (ec) ecs.push_back(*ec); if (ec) ecs.push_back(*ec);
} }
logfn("S2F29 -> S2F30 (" + std::to_string(ecs.size()) + " ECs)"); std::vector<gem::EcNameRow> rows;
return gem::s2f30_ec_namelist_data(ecs); for (const auto& ec : ecs)
rows.push_back({ec.id, ec.name, ec.min_str, ec.max_str, "", ec.units});
logfn("S2F29 -> S2F30 (" + std::to_string(rows.size()) + " ECs)");
return gem::s2f30_ec_namelist_data(rows);
}); });
router.on(2, 31, [model, logfn](const s2::Message& msg) { router.on(2, 31, [model, logfn](const s2::Message& msg) {
auto t = gem::parse_s2f31(msg); auto t = gem::parse_s2f31(msg);
@@ -204,14 +211,25 @@ int main(int argc, char** argv) {
}); });
router.on(2, 33, [model, logfn](const s2::Message& msg) { router.on(2, 33, [model, logfn](const s2::Message& msg) {
auto req = gem::parse_s2f33(msg); auto req = gem::parse_s2f33(msg);
auto ack = req ? model->define_reports(req->reports) auto ack = gem::DefineReportAck::InvalidFormat;
: gem::DefineReportAck::InvalidFormat; if (req) {
std::vector<std::pair<uint32_t, std::vector<uint32_t>>> rows;
rows.reserve(req->reports.size());
for (const auto& r : req->reports) rows.emplace_back(r.rptid, r.vids);
ack = model->define_reports(rows);
}
logfn("S2F33 -> S2F34 DRACK=" + std::to_string(static_cast<int>(ack))); logfn("S2F33 -> S2F34 DRACK=" + std::to_string(static_cast<int>(ack)));
return gem::s2f34_define_report_ack(ack); return gem::s2f34_define_report_ack(ack);
}); });
router.on(2, 35, [model, logfn](const s2::Message& msg) { router.on(2, 35, [model, logfn](const s2::Message& msg) {
auto req = gem::parse_s2f35(msg); auto req = gem::parse_s2f35(msg);
auto ack = req ? model->link_event_reports(req->links) : gem::LinkEventAck::InvalidFormat; auto ack = gem::LinkEventAck::InvalidFormat;
if (req) {
std::vector<std::pair<uint32_t, std::vector<uint32_t>>> rows;
rows.reserve(req->links.size());
for (const auto& l : req->links) rows.emplace_back(l.ceid, l.rptids);
ack = model->link_event_reports(rows);
}
logfn("S2F35 -> S2F36 LRACK=" + std::to_string(static_cast<int>(ack))); logfn("S2F35 -> S2F36 LRACK=" + std::to_string(static_cast<int>(ack)));
return gem::s2f36_link_event_report_ack(ack); return gem::s2f36_link_event_report_ack(ack);
}); });
@@ -225,7 +243,7 @@ int main(int argc, char** argv) {
}); });
router.on(2, 41, [model, logfn, emit_event, emit_alarm_set](const s2::Message& msg) { router.on(2, 41, [model, logfn, emit_event, emit_alarm_set](const s2::Message& msg) {
auto cmd = gem::parse_s2f41(msg); auto cmd = gem::parse_s2f41(msg);
if (!cmd) return gem::s2f42_host_command_ack(gem::HostCmdAck::ParameterInvalid); if (!cmd) return gem::s2f42_host_command_ack(gem::HostCmdAck::ParameterInvalid, {});
auto result = model->dispatch_command(cmd->rcmd, cmd->params); auto result = model->dispatch_command(cmd->rcmd, cmd->params);
logfn("S2F41 RCMD=" + cmd->rcmd + " -> S2F42 HCACK=" + logfn("S2F41 RCMD=" + cmd->rcmd + " -> S2F42 HCACK=" +
std::to_string(static_cast<int>(result.ack))); std::to_string(static_cast<int>(result.ack)));
@@ -233,12 +251,12 @@ int main(int argc, char** argv) {
if (result.emit_ceid) emit_event(*result.emit_ceid); if (result.emit_ceid) emit_event(*result.emit_ceid);
if (result.set_alarm) emit_alarm_set(*result.set_alarm); if (result.set_alarm) emit_alarm_set(*result.set_alarm);
} }
return gem::s2f42_host_command_ack(result.ack); return gem::s2f42_host_command_ack(result.ack, {});
}); });
router.on(5, 3, [model, logfn](const s2::Message& msg) { router.on(5, 3, [model, logfn](const s2::Message& msg) {
auto req = gem::parse_s5f3(msg); auto req = gem::parse_s5f3(msg);
auto ack = req ? model->set_alarm_enabled(req->alid, req->enable) auto ack = req ? model->set_alarm_enabled(req->alid, (req->aled & 0x80) != 0)
: gem::AlarmAck::Error; : gem::AlarmAck::Error;
logfn(std::string("S5F3 -> S5F4 ACKC5=") + std::to_string(static_cast<int>(ack))); logfn(std::string("S5F3 -> S5F4 ACKC5=") + std::to_string(static_cast<int>(ack)));
return gem::s5f4_enable_alarm_ack(ack); return gem::s5f4_enable_alarm_ack(ack);
@@ -278,7 +296,7 @@ int main(int argc, char** argv) {
}); });
router.on(10, 1, [logfn](const s2::Message& msg) { router.on(10, 1, [logfn](const s2::Message& msg) {
auto td = gem::parse_terminal_display(msg); auto td = gem::parse_s10f1(msg);
if (td) logfn("TERMINAL[" + std::to_string(td->tid) + "] " + td->text); if (td) logfn("TERMINAL[" + std::to_string(td->tid) + "] " + td->text);
return gem::s10f2_terminal_display_ack(gem::TerminalAck::Accepted); return gem::s10f2_terminal_display_ack(gem::TerminalAck::Accepted);
}); });
+546
View File
@@ -0,0 +1,546 @@
# SECS-II message catalog. Each entry describes one SxFy primary or reply;
# the codegen (tools/gen_messages.py) reads this and emits builders + parsers.
#
# Shape grammar (see tools/gen_messages.py for the full description):
#
# none - header-only (no body)
# scalar: - single Item; cpp_type derived from item_type/enum
# item_type: ASCII | BINARY_BYTE | BOOLEAN | U1..U8 | I1..I8 | F4 | F8 | ITEM
# enum: <C++ enum> - optional
# param: <name> - optional, default "value"
# list: - fixed-arity <L,k>
# fields: [{name, shape}, ...]
# struct_name: <Name> - optional; if set, parser returns the struct
# external_struct: bool - optional; struct is assumed to exist already
# list_of: - variable-arity <L,n>
# element: <shape>
# name: <param-name> - optional, default "values"
#
# Special-case messages whose shape can't be expressed (S1F4's optional-item
# semantics, S5F6's per-row ALCD callback) live in messages_helpers.hpp.
messages:
# =====================================================================
# S1 — Equipment status
# =====================================================================
- id: S1F1
stream: 1
function: 1
w: true
builder: s1f1_are_you_there
- id: S1F2
stream: 1
function: 2
builder: s1f2_on_line_data
body:
kind: list
fields:
- {name: mdln, shape: {kind: scalar, item_type: ASCII}}
- {name: softrev, shape: {kind: scalar, item_type: ASCII}}
- id: S1F3
stream: 1
function: 3
w: true
builder: s1f3_selected_status_request
parser: parse_s1f3
body:
kind: list_of
element: {kind: scalar, item_type: U4}
name: svids
- id: S1F11
stream: 1
function: 11
w: true
builder: s1f11_status_namelist_request
parser: parse_s1f11
body:
kind: list_of
element: {kind: scalar, item_type: U4}
name: svids
- id: S1F12
stream: 1
function: 12
builder: s1f12_status_namelist_data
parser: parse_s1f12
body:
kind: list_of
name: items
element:
kind: list
struct_name: StatusName
fields:
- {name: id, shape: {kind: scalar, item_type: U4}}
- {name: name, shape: {kind: scalar, item_type: ASCII}}
- {name: units, shape: {kind: scalar, item_type: ASCII}}
- id: S1F13
stream: 1
function: 13
w: true
builder: s1f13_establish_comms
body:
kind: list
fields:
- {name: mdln, shape: {kind: scalar, item_type: ASCII}}
- {name: softrev, shape: {kind: scalar, item_type: ASCII}}
- id: S1F14
stream: 1
function: 14
builder: s1f14_establish_comms_ack
body:
kind: list
fields:
- {name: ack, shape: {kind: scalar, item_type: BINARY_BYTE, enum: CommAck}}
- name: identity
shape:
kind: list
struct_name: McAck
fields:
- {name: mdln, shape: {kind: scalar, item_type: ASCII}}
- {name: softrev, shape: {kind: scalar, item_type: ASCII}}
- id: S1F15
stream: 1
function: 15
w: true
builder: s1f15_request_offline
- id: S1F16
stream: 1
function: 16
builder: s1f16_offline_ack
body: {kind: scalar, item_type: BINARY_BYTE, enum: OfflineAck, param: ack}
- id: S1F17
stream: 1
function: 17
w: true
builder: s1f17_request_online
- id: S1F18
stream: 1
function: 18
builder: s1f18_online_ack
body: {kind: scalar, item_type: BINARY_BYTE, enum: OnlineAck, param: ack}
# =====================================================================
# S2 — Equipment control + reports
# =====================================================================
- id: S2F13
stream: 2
function: 13
w: true
builder: s2f13_ec_request
parser: parse_s2f13
body:
kind: list_of
element: {kind: scalar, item_type: U4}
name: ecids
- id: S2F14
stream: 2
function: 14
builder: s2f14_ec_data
parser: parse_s2f14
body:
kind: list_of
element: {kind: scalar, item_type: ITEM}
name: values
- id: S2F15
stream: 2
function: 15
w: true
builder: s2f15_ec_send
parser: parse_s2f15
body:
kind: list_of
name: sets
element:
kind: list
struct_name: EcSet
fields:
- {name: ecid, shape: {kind: scalar, item_type: U4}}
- {name: value, shape: {kind: scalar, item_type: ITEM}}
- id: S2F16
stream: 2
function: 16
builder: s2f16_ec_ack
body: {kind: scalar, item_type: BINARY_BYTE, enum: EquipmentAck, param: eac}
- id: S2F17
stream: 2
function: 17
w: true
builder: s2f17_date_time_request
- id: S2F18
stream: 2
function: 18
builder: s2f18_date_time_data
parser: parse_s2f18
body: {kind: scalar, item_type: ASCII, param: time_str}
- id: S2F29
stream: 2
function: 29
w: true
builder: s2f29_ec_namelist_request
body:
kind: list_of
element: {kind: scalar, item_type: U4}
name: ecids
- id: S2F30
stream: 2
function: 30
builder: s2f30_ec_namelist_data
body:
kind: list_of
name: items
element:
kind: list
struct_name: EcNameRow
fields:
- {name: ecid, shape: {kind: scalar, item_type: U4}}
- {name: name, shape: {kind: scalar, item_type: ASCII}}
- {name: min_str, shape: {kind: scalar, item_type: ASCII}}
- {name: max_str, shape: {kind: scalar, item_type: ASCII}}
- {name: default_str, shape: {kind: scalar, item_type: ASCII}}
- {name: units, shape: {kind: scalar, item_type: ASCII}}
- id: S2F31
stream: 2
function: 31
w: true
builder: s2f31_date_time_set
parser: parse_s2f31
body: {kind: scalar, item_type: ASCII, param: time_str}
- id: S2F32
stream: 2
function: 32
builder: s2f32_date_time_ack
body: {kind: scalar, item_type: BINARY_BYTE, enum: TimeAck, param: ack}
- id: S2F33
stream: 2
function: 33
w: true
builder: s2f33_define_report
parser: parse_s2f33
body:
kind: list
struct_name: DefineReportRequest
fields:
- {name: dataid, shape: {kind: scalar, item_type: U4}}
- name: reports
shape:
kind: list_of
element:
kind: list
struct_name: DefineReportEntry
fields:
- {name: rptid, shape: {kind: scalar, item_type: U4}}
- name: vids
shape:
kind: list_of
element: {kind: scalar, item_type: U4}
- id: S2F34
stream: 2
function: 34
builder: s2f34_define_report_ack
body: {kind: scalar, item_type: BINARY_BYTE, enum: DefineReportAck, param: ack}
- id: S2F35
stream: 2
function: 35
w: true
builder: s2f35_link_event_report
parser: parse_s2f35
body:
kind: list
struct_name: LinkEventReportRequest
fields:
- {name: dataid, shape: {kind: scalar, item_type: U4}}
- name: links
shape:
kind: list_of
element:
kind: list
struct_name: LinkEventEntry
fields:
- {name: ceid, shape: {kind: scalar, item_type: U4}}
- name: rptids
shape:
kind: list_of
element: {kind: scalar, item_type: U4}
- id: S2F36
stream: 2
function: 36
builder: s2f36_link_event_report_ack
body: {kind: scalar, item_type: BINARY_BYTE, enum: LinkEventAck, param: ack}
- id: S2F37
stream: 2
function: 37
w: true
builder: s2f37_enable_event
parser: parse_s2f37
body:
kind: list
struct_name: EnableEventRequest
fields:
- {name: enable, shape: {kind: scalar, item_type: BOOLEAN}}
- name: ceids
shape: {kind: list_of, element: {kind: scalar, item_type: U4}}
- id: S2F38
stream: 2
function: 38
builder: s2f38_enable_event_ack
body: {kind: scalar, item_type: BINARY_BYTE, enum: EnableEventAck, param: ack}
- id: S2F41
stream: 2
function: 41
w: true
builder: s2f41_host_command
parser: parse_s2f41
body:
kind: list
struct_name: HostCommand
fields:
- {name: rcmd, shape: {kind: scalar, item_type: ASCII}}
- name: params
shape:
kind: list_of
element:
kind: list
struct_name: CommandParameter
external_struct: true
fields:
- {name: name, shape: {kind: scalar, item_type: ASCII}}
- {name: value, shape: {kind: scalar, item_type: ITEM}}
- id: S2F42
stream: 2
function: 42
builder: s2f42_host_command_ack
parser: parse_s2f42
body:
kind: list
struct_name: HostCommandReply
fields:
- {name: hcack, shape: {kind: scalar, item_type: BINARY_BYTE, enum: HostCmdAck}}
- name: cpacks
shape:
kind: list_of
element:
kind: list
struct_name: CommandParameterAck
fields:
- {name: name, shape: {kind: scalar, item_type: ASCII}}
- {name: code, shape: {kind: scalar, item_type: BINARY_BYTE}}
# =====================================================================
# S5 — Alarms
# =====================================================================
- id: S5F1
stream: 5
function: 1
w: true
builder: s5f1_alarm_report
parser: parse_s5f1
body:
kind: list
struct_name: AlarmReport
fields:
- {name: alcd, shape: {kind: scalar, item_type: BINARY_BYTE}}
- {name: alid, shape: {kind: scalar, item_type: U4}}
- {name: altx, shape: {kind: scalar, item_type: ASCII}}
- id: S5F2
stream: 5
function: 2
builder: s5f2_alarm_ack
body: {kind: scalar, item_type: BINARY_BYTE, enum: AlarmAck, param: ack}
- id: S5F3
stream: 5
function: 3
w: true
builder: s5f3_enable_alarm
parser: parse_s5f3
body:
kind: list
struct_name: EnableAlarmRequest
fields:
- {name: aled, shape: {kind: scalar, item_type: BINARY_BYTE}}
- {name: alid, shape: {kind: scalar, item_type: U4}}
- id: S5F4
stream: 5
function: 4
builder: s5f4_enable_alarm_ack
body: {kind: scalar, item_type: BINARY_BYTE, enum: AlarmAck, param: ack}
- id: S5F5
stream: 5
function: 5
w: true
builder: s5f5_list_alarms_request
parser: parse_s5f5
body:
kind: list_of
element: {kind: scalar, item_type: U4}
name: alids
# S5F6 is hand-written (per-row ALCD requires an active-callback API).
# =====================================================================
# S6 — Data collection
# =====================================================================
- id: S6F11
stream: 6
function: 11
w: true
builder: s6f11_event_report
parser: parse_s6f11
body:
kind: list
struct_name: EventReportMessage
fields:
- {name: dataid, shape: {kind: scalar, item_type: U4}}
- {name: ceid, shape: {kind: scalar, item_type: U4}}
- name: reports
shape:
kind: list_of
element:
kind: list
struct_name: ReportData
external_struct: true
fields:
- {name: rptid, shape: {kind: scalar, item_type: U4}}
- name: values
shape: {kind: list_of, element: {kind: scalar, item_type: ITEM}}
- id: S6F12
stream: 6
function: 12
builder: s6f12_event_report_ack
body: {kind: scalar, item_type: BINARY_BYTE, enum: EventReportAck, param: ack}
# =====================================================================
# S7 — Process programs
# =====================================================================
- id: S7F3
stream: 7
function: 3
w: true
builder: s7f3_process_program_send
parser: parse_s7f3
body:
kind: list
struct_name: ProcessProgram
fields:
- {name: ppid, shape: {kind: scalar, item_type: ASCII}}
- {name: ppbody, shape: {kind: scalar, item_type: BINARY}}
- id: S7F4
stream: 7
function: 4
builder: s7f4_process_program_ack
body: {kind: scalar, item_type: BINARY_BYTE, enum: ProcessProgramAck, param: ack}
- id: S7F5
stream: 7
function: 5
w: true
builder: s7f5_process_program_request
parser: parse_s7f5
body: {kind: scalar, item_type: ASCII, param: ppid}
- id: S7F6
stream: 7
function: 6
builder: s7f6_process_program_data
parser: parse_s7f6
body:
kind: list
struct_name: ProcessProgram
fields:
- {name: ppid, shape: {kind: scalar, item_type: ASCII}}
- {name: ppbody, shape: {kind: scalar, item_type: BINARY}}
- id: S7F19
stream: 7
function: 19
w: true
builder: s7f19_current_eppd_request
- id: S7F20
stream: 7
function: 20
builder: s7f20_current_eppd_data
parser: parse_s7f20
body:
kind: list_of
element: {kind: scalar, item_type: ASCII}
name: ppids
# =====================================================================
# S10 — Terminal services
# =====================================================================
- id: S10F1
stream: 10
function: 1
w: true
builder: s10f1_terminal_display_single
parser: parse_s10f1
body:
kind: list
struct_name: TerminalDisplay
fields:
- {name: tid, shape: {kind: scalar, item_type: BINARY_BYTE}}
- {name: text, shape: {kind: scalar, item_type: ASCII}}
- id: S10F2
stream: 10
function: 2
builder: s10f2_terminal_display_ack
body: {kind: scalar, item_type: BINARY_BYTE, enum: TerminalAck, param: ack}
- id: S10F3
stream: 10
function: 3
w: true
builder: s10f3_terminal_display_single
parser: parse_s10f3
body:
kind: list
struct_name: TerminalDisplay
fields:
- {name: tid, shape: {kind: scalar, item_type: BINARY_BYTE}}
- {name: text, shape: {kind: scalar, item_type: ASCII}}
- id: S10F4
stream: 10
function: 4
builder: s10f4_terminal_display_ack
body: {kind: scalar, item_type: BINARY_BYTE, enum: TerminalAck, param: ack}
+9 -4
View File
@@ -49,6 +49,14 @@ struct CollectionEvent {
std::string name; 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 { struct Report {
uint32_t id; uint32_t id;
std::vector<uint32_t> vids; std::vector<uint32_t> vids;
@@ -157,10 +165,7 @@ enum class TerminalAck : uint8_t {
// all access happens on the Asio executor. // all access happens on the Asio executor.
class EquipmentDataModel { class EquipmentDataModel {
public: public:
struct CommandParam { using CommandParam = CommandParameter; // back-compat alias
std::string name;
s2::Item value;
};
// Declarative host-command effect, loaded from YAML. The server // Declarative host-command effect, loaded from YAML. The server
// dispatches a command by looking up the spec and (optionally) firing // 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
+25 -26
View File
@@ -27,8 +27,7 @@ TEST_CASE("S1F4 substitutes empty list for unknown SVIDs") {
} }
TEST_CASE("S1F12 round-trip preserves SVID, name, units") { TEST_CASE("S1F12 round-trip preserves SVID, name, units") {
std::vector<StatusVariable> svs = {{1, "Clock", "sec", s2::Item::ascii("0")}, std::vector<StatusName> svs = {{1, "Clock", "sec"}, {2, "Power", "W"}};
{2, "Power", "W", s2::Item::u4(uint32_t{0})}};
auto m = s1f12_status_namelist_data(svs); auto m = s1f12_status_namelist_data(svs);
auto parsed = parse_s1f12(m); auto parsed = parse_s1f12(m);
REQUIRE(parsed.has_value()); REQUIRE(parsed.has_value());
@@ -41,7 +40,7 @@ TEST_CASE("S1F12 round-trip preserves SVID, name, units") {
} }
TEST_CASE("S2F15 round-trip preserves ECID-value pairs") { TEST_CASE("S2F15 round-trip preserves ECID-value pairs") {
std::vector<std::pair<uint32_t, s2::Item>> sets = { std::vector<EcSet> sets = {
{10, s2::Item::u4(uint32_t{50})}, {10, s2::Item::u4(uint32_t{50})},
{11, s2::Item::ascii("YYYYMMDDhhmmsscc")}, {11, s2::Item::ascii("YYYYMMDDhhmmsscc")},
}; };
@@ -49,10 +48,10 @@ TEST_CASE("S2F15 round-trip preserves ECID-value pairs") {
auto parsed = parse_s2f15(m); auto parsed = parse_s2f15(m);
REQUIRE(parsed.has_value()); REQUIRE(parsed.has_value());
REQUIRE(parsed->size() == 2); REQUIRE(parsed->size() == 2);
CHECK((*parsed)[0].first == 10); CHECK((*parsed)[0].ecid == 10);
CHECK((*parsed)[0].second == s2::Item::u4(uint32_t{50})); CHECK((*parsed)[0].value == s2::Item::u4(uint32_t{50}));
CHECK((*parsed)[1].first == 11); CHECK((*parsed)[1].ecid == 11);
CHECK((*parsed)[1].second == s2::Item::ascii("YYYYMMDDhhmmsscc")); CHECK((*parsed)[1].value == s2::Item::ascii("YYYYMMDDhhmmsscc"));
} }
TEST_CASE("S2F16 EAC ack round-trip") { TEST_CASE("S2F16 EAC ack round-trip") {
@@ -70,7 +69,7 @@ TEST_CASE("S2F18 carries 16-char time string") {
} }
TEST_CASE("S2F41 round-trip with parameters") { TEST_CASE("S2F41 round-trip with parameters") {
std::vector<EquipmentDataModel::CommandParam> params = { std::vector<CommandParameter> params = {
{"LOTID", s2::Item::ascii("LOT-42")}, {"LOTID", s2::Item::ascii("LOT-42")},
{"PPID", s2::Item::ascii("RECIPE-A")}, {"PPID", s2::Item::ascii("RECIPE-A")},
}; };
@@ -91,14 +90,14 @@ TEST_CASE("S2F42 round-trip with HCACK and CPACKs") {
REQUIRE(parsed.has_value()); REQUIRE(parsed.has_value());
CHECK(parsed->hcack == HostCmdAck::ParameterInvalid); CHECK(parsed->hcack == HostCmdAck::ParameterInvalid);
REQUIRE(parsed->cpacks.size() == 2); REQUIRE(parsed->cpacks.size() == 2);
CHECK(parsed->cpacks[0].first == "LOTID"); CHECK(parsed->cpacks[0].name == "LOTID");
CHECK(parsed->cpacks[0].second == 0); CHECK(parsed->cpacks[0].code == 0);
CHECK(parsed->cpacks[1].first == "PPID"); CHECK(parsed->cpacks[1].name == "PPID");
CHECK(parsed->cpacks[1].second == 3); CHECK(parsed->cpacks[1].code == 3);
} }
TEST_CASE("S2F42 no-params variant") { TEST_CASE("S2F42 no-params variant") {
auto m = s2f42_host_command_ack(HostCmdAck::Accept); auto m = s2f42_host_command_ack(HostCmdAck::Accept, {});
auto parsed = parse_s2f42(m); auto parsed = parse_s2f42(m);
REQUIRE(parsed.has_value()); REQUIRE(parsed.has_value());
CHECK(parsed->hcack == HostCmdAck::Accept); CHECK(parsed->hcack == HostCmdAck::Accept);
@@ -107,7 +106,7 @@ TEST_CASE("S2F42 no-params variant") {
TEST_CASE("S10F3 terminal display round-trip") { TEST_CASE("S10F3 terminal display round-trip") {
auto m = s10f3_terminal_display_single(1, "ALARM: chiller temperature high"); auto m = s10f3_terminal_display_single(1, "ALARM: chiller temperature high");
auto parsed = parse_terminal_display(m); auto parsed = parse_s10f3(m);
REQUIRE(parsed.has_value()); REQUIRE(parsed.has_value());
CHECK(parsed->tid == 1); CHECK(parsed->tid == 1);
CHECK(parsed->text == "ALARM: chiller temperature high"); CHECK(parsed->text == "ALARM: chiller temperature high");
@@ -121,10 +120,10 @@ TEST_CASE("S2F33 define-report round-trip") {
REQUIRE(parsed.has_value()); REQUIRE(parsed.has_value());
CHECK(parsed->dataid == 7); CHECK(parsed->dataid == 7);
REQUIRE(parsed->reports.size() == 2); REQUIRE(parsed->reports.size() == 2);
CHECK(parsed->reports[0].first == 1000); CHECK(parsed->reports[0].rptid == 1000);
CHECK(parsed->reports[0].second == std::vector<uint32_t>{1, 2, 3}); CHECK(parsed->reports[0].vids == std::vector<uint32_t>{1, 2, 3});
CHECK(parsed->reports[1].first == 1001); CHECK(parsed->reports[1].rptid == 1001);
CHECK(parsed->reports[1].second == std::vector<uint32_t>{4}); CHECK(parsed->reports[1].vids == std::vector<uint32_t>{4});
} }
TEST_CASE("S2F35 link-event round-trip") { TEST_CASE("S2F35 link-event round-trip") {
@@ -133,8 +132,8 @@ TEST_CASE("S2F35 link-event round-trip") {
REQUIRE(parsed.has_value()); REQUIRE(parsed.has_value());
CHECK(parsed->dataid == 0); CHECK(parsed->dataid == 0);
REQUIRE(parsed->links.size() == 2); REQUIRE(parsed->links.size() == 2);
CHECK(parsed->links[0].first == 100); CHECK(parsed->links[0].ceid == 100);
CHECK(parsed->links[0].second == std::vector<uint32_t>{1000, 1001}); CHECK(parsed->links[0].rptids == std::vector<uint32_t>{1000, 1001});
} }
TEST_CASE("S2F37 enable-event round-trip") { TEST_CASE("S2F37 enable-event round-trip") {
@@ -157,19 +156,19 @@ TEST_CASE("S5F1 alarm-report round-trip") {
CHECK(parsed->alid == 7); CHECK(parsed->alid == 7);
CHECK(parsed->altx == "Chiller temp high"); CHECK(parsed->altx == "Chiller temp high");
CHECK(parsed->alcd == 0x84); CHECK(parsed->alcd == 0x84);
CHECK(parsed->is_set()); CHECK((parsed->alcd & 0x80) != 0); // set bit
CHECK(parsed->category() == 4); CHECK((parsed->alcd & 0x7F) == 4); // category
} }
TEST_CASE("S5F3 enable/disable alarm send round-trip") { TEST_CASE("S5F3 enable/disable alarm send round-trip") {
auto on = s5f3_enable_alarm(true, 42); auto on = s5f3_enable_alarm(kAlarmEnableByte, 42);
auto off = s5f3_enable_alarm(false, 42); auto off = s5f3_enable_alarm(kAlarmDisableByte, 42);
auto pon = parse_s5f3(on); auto pon = parse_s5f3(on);
auto poff = parse_s5f3(off); auto poff = parse_s5f3(off);
REQUIRE(pon.has_value()); REQUIRE(pon.has_value());
REQUIRE(poff.has_value()); REQUIRE(poff.has_value());
CHECK(pon->enable); CHECK((pon->aled & 0x80) != 0);
CHECK_FALSE(poff->enable); CHECK((poff->aled & 0x80) == 0);
CHECK(pon->alid == 42); CHECK(pon->alid == 42);
} }
+382
View File
@@ -0,0 +1,382 @@
#!/usr/bin/env python3
"""Generate include/secsgem/gem/messages.hpp from data/messages.yaml.
Reads a SECS-II message catalog and emits an inline-only C++ header with
builders + parsers for each message. The catalog is the single source of
truth for SxFy body shapes.
Shape grammar (used recursively for `body` and `element` and field types):
none -> message has no body (header-only)
{kind: scalar,
item_type: <T>,
enum: <E>?, # optional C++ enum type
param: <name>?, # builder parameter name (default 'value')
parser_struct_field: ...} # only valid inside a list's `fields`
{kind: list,
fields: [{name, shape}, ...],
struct_name: <Name>?, # if set, parser returns std::optional<Name>
external_struct: bool?} # if true, the struct is assumed to already exist
{kind: list_of,
element: <shape>,
name: <param-name>?} # builder param + parser value name
`item_type` is one of:
ASCII, BINARY, BINARY_BYTE, BOOLEAN, ITEM,
U1, U2, U4, U8, I1, I2, I4, I8, F4, F8
The generated file is overwrite-safe and entirely inline (no .cpp).
"""
import argparse
import sys
from pathlib import Path
import yaml
# --- Type table -----------------------------------------------------------
# Maps item_type -> (cpp_param_type, item_factory, parse_helper)
# parse_helper(item_expr) -> C++ expression evaluating to std::optional<T>
SCALAR_TABLE = {
"ASCII": ("std::string", "secs2::Item::ascii({v})",
"as_ascii({i})"),
"BINARY_BYTE": ("uint8_t", "secs2::Item::binary({{ {v} }})",
"as_binary_first({i})"),
"BINARY": ("std::string", "secs2::Item::binary(std::vector<uint8_t>({v}.begin(), {v}.end()))",
"as_binary_string({i})"),
"BOOLEAN": ("bool", "secs2::Item::boolean({v})",
"as_boolean({i})"),
"ITEM": ("secs2::Item", "{v}",
"std::optional<secs2::Item>({i})"),
"U1": ("uint8_t", "secs2::Item::u1({v})",
"as_u1_scalar({i})"),
"U2": ("uint16_t", "secs2::Item::u2({v})",
"as_u2_scalar({i})"),
"U4": ("uint32_t", "secs2::Item::u4({v})",
"as_u4_scalar({i})"),
"U8": ("uint64_t", "secs2::Item::u8({v})",
"as_u8_scalar({i})"),
"I1": ("int8_t", "secs2::Item::i1({v})",
"as_i1_scalar({i})"),
"I2": ("int16_t", "secs2::Item::i2({v})",
"as_i2_scalar({i})"),
"I4": ("int32_t", "secs2::Item::i4({v})",
"as_i4_scalar({i})"),
"I8": ("int64_t", "secs2::Item::i8({v})",
"as_i8_scalar({i})"),
"F4": ("float", "secs2::Item::f4({v})",
"as_f4_scalar({i})"),
"F8": ("double", "secs2::Item::f8({v})",
"as_f8_scalar({i})"),
}
# --- Shape inspection -----------------------------------------------------
def is_scalar(shape): return shape and shape.get("kind") == "scalar"
def is_list(shape): return shape and shape.get("kind") == "list"
def is_list_of(shape): return shape and shape.get("kind") == "list_of"
def cpp_type(shape):
"""C++ type used for a parameter / parser-return / struct field."""
if is_scalar(shape):
base, _, _ = SCALAR_TABLE[shape["item_type"]]
return shape.get("enum", base)
if is_list(shape):
name = shape.get("struct_name")
if not name:
raise ValueError(f"list shape needs `struct_name`: {shape}")
return name
if is_list_of(shape):
return f"std::vector<{cpp_type(shape['element'])}>"
raise ValueError(f"unknown shape: {shape}")
def field_struct_name(field, message_name):
return field.get("struct_name") or message_name
# --- Code emission --------------------------------------------------------
class Emit:
def __init__(self):
self.lines = []
self.indent = 0
self.emitted_structs = set()
def line(self, s=""):
self.lines.append(" " * self.indent + s if s else "")
def block(self, header, body_fn, footer="}"):
self.line(header)
self.indent += 1
body_fn()
self.indent -= 1
self.line(footer)
def collect_structs(shape, out):
"""Post-order traversal of the shape tree, appending any (name, fields) for
list-with-struct_name nodes that aren't marked external. Post-order ensures
deeper structs are emitted before the structs that reference them."""
if not shape:
return
if is_list(shape):
for f in shape["fields"]:
collect_structs(f["shape"], out)
name = shape.get("struct_name")
if name and not shape.get("external_struct"):
out.append((name, shape["fields"]))
elif is_list_of(shape):
collect_structs(shape["element"], out)
def emit_struct(e, name, fields):
if name in e.emitted_structs:
return
e.emitted_structs.add(name)
e.line(f"struct {name} {{")
e.indent += 1
for f in fields:
e.line(f"{cpp_type(f['shape'])} {f['name']};")
e.indent -= 1
e.line("};")
e.line()
def emit_build_expr(shape, var, depth=0):
"""Return a C++ expression that constructs an Item from `var`."""
if is_scalar(shape):
_, factory, _ = SCALAR_TABLE[shape["item_type"]]
if "enum" in shape:
# Cast the typed enum to its underlying byte before constructing.
base, _, _ = SCALAR_TABLE[shape["item_type"]]
return factory.format(v=f"static_cast<{base}>({var})")
return factory.format(v=var)
if is_list(shape):
parts = [emit_build_expr(f["shape"], f"{var}.{f['name']}", depth)
for f in shape["fields"]]
return "secs2::Item::list({" + ", ".join(parts) + "})"
if is_list_of(shape):
c = f"__c_{depth}"
ev = f"__e_{depth}"
elem_expr = emit_build_expr(shape["element"], ev, depth + 1)
return (f"[&]{{ secs2::Item::List {c}; "
f"for (const auto& {ev} : {var}) {c}.push_back({elem_expr}); "
f"return secs2::Item::list(std::move({c})); }}()")
raise ValueError(f"unknown shape: {shape}")
def emit_parse_expr(shape, item_var, depth=0):
"""Return a C++ expression of type std::optional<T> that parses item_var."""
if is_scalar(shape):
_, _, helper = SCALAR_TABLE[shape["item_type"]]
expr = helper.format(i=item_var)
if "enum" in shape:
enum_t = shape["enum"]
t = f"__t_{depth}"
return (f"[&]() -> std::optional<{enum_t}> {{ auto {t} = {expr}; "
f"if (!{t}) return std::nullopt; "
f"return static_cast<{enum_t}>(*{t}); }}()")
return expr
if is_list(shape):
sname = shape["struct_name"]
n = len(shape["fields"])
L = f"__l_{depth}"
R = f"__r_{depth}"
V = f"__v_{depth}"
parts = [
f"if (!{item_var}.is_list() || {item_var}.as_list().size() != {n}) return std::nullopt;",
f"const auto& {L} = {item_var}.as_list();",
f"{sname} {R};",
]
for i, f in enumerate(shape["fields"]):
sub = emit_parse_expr(f["shape"], f"{L}[{i}]", depth + 1)
parts.append(f"{{ auto {V} = {sub}; if (!{V}) return std::nullopt; {R}.{f['name']} = *{V}; }}")
parts.append(f"return std::optional<{sname}>({R});")
return "[&]() -> std::optional<" + sname + "> { " + " ".join(parts) + " }()"
if is_list_of(shape):
elem_t = cpp_type(shape["element"])
c = f"__c_{depth}"
v = f"__v_{depth}"
out = f"__out_{depth}"
sub = emit_parse_expr(shape["element"], c, depth + 1)
return ("[&]() -> std::optional<std::vector<" + elem_t + ">> { "
f"if (!{item_var}.is_list()) return std::nullopt; "
f"std::vector<{elem_t}> {out}; "
f"for (const auto& {c} : {item_var}.as_list()) {{ "
f" auto {v} = {sub}; if (!{v}) return std::nullopt; "
f" {out}.push_back(*{v}); }} "
f"return {out}; }}()")
raise ValueError(f"unknown shape: {shape}")
def emit_message(e, m):
stream, fn = m["stream"], m["function"]
w = "true" if m.get("w") else "false"
builder = m["builder"]
body = m.get("body")
# Emit any structs referenced by the body.
structs = []
if body:
collect_structs(body, structs)
for name, fields in structs:
emit_struct(e, name, fields)
# ---- Builder -------------------------------------------------------
if body is None:
e.line(f"inline secs2::Message {builder}() {{")
e.indent += 1
e.line(f"return secs2::Message({stream}, {fn}, {w});")
e.indent -= 1
e.line("}")
e.line()
elif is_scalar(body):
ptype = cpp_type(body)
pname = body.get("param", "value")
build = emit_build_expr(body, pname)
e.line(f"inline secs2::Message {builder}({ptype} {pname}) {{")
e.indent += 1
e.line(f"return secs2::Message({stream}, {fn}, {w}, {build});")
e.indent -= 1
e.line("}")
e.line()
elif is_list(body):
fields = body["fields"]
sname = body.get("struct_name")
# Always emit a positional-parameter builder (no struct needed).
param_decl = ", ".join(
f"const {cpp_type(f['shape'])}& {f['name']}" for f in fields)
parts = [emit_build_expr(f["shape"], f["name"]) for f in fields]
body_expr = "secs2::Item::list({" + ", ".join(parts) + "})"
e.line(f"inline secs2::Message {builder}({param_decl}) {{")
e.indent += 1
e.line(f"return secs2::Message({stream}, {fn}, {w}, {body_expr});")
e.indent -= 1
e.line("}")
# If a struct exists, also emit an overload that takes it.
if sname:
forward = ", ".join(f"v.{f['name']}" for f in fields)
e.line(f"inline secs2::Message {builder}(const {sname}& v) {{")
e.indent += 1
e.line(f"return {builder}({forward});")
e.indent -= 1
e.line("}")
e.line()
elif is_list_of(body):
elem_t = cpp_type(body["element"])
pname = body.get("name", "values")
e.line(f"inline secs2::Message {builder}(const std::vector<{elem_t}>& {pname}) {{")
e.indent += 1
e.line(f"return secs2::Message({stream}, {fn}, {w}, {emit_build_expr(body, pname)});")
e.indent -= 1
e.line("}")
e.line()
else:
raise ValueError(f"unknown body kind for {m['id']}: {body}")
# ---- Parser --------------------------------------------------------
parser = m.get("parser")
if not parser:
return
if body is None:
# no body to parse
return
if is_scalar(body):
ret_t = cpp_type(body)
e.line(f"inline std::optional<{ret_t}> {parser}(const secs2::Message& m) {{")
e.indent += 1
e.line("if (!m.body) return std::nullopt;")
e.line(f"return {emit_parse_expr(body, '(*m.body)')};")
e.indent -= 1
e.line("}")
e.line()
elif is_list(body):
sname = body["struct_name"]
e.line(f"inline std::optional<{sname}> {parser}(const secs2::Message& m) {{")
e.indent += 1
e.line("if (!m.body) return std::nullopt;")
e.line(f"return {emit_parse_expr(body, '(*m.body)')};")
e.indent -= 1
e.line("}")
e.line()
elif is_list_of(body):
elem_t = cpp_type(body["element"])
e.line(f"inline std::optional<std::vector<{elem_t}>> {parser}(const secs2::Message& m) {{")
e.indent += 1
e.line("if (!m.body) return std::nullopt;")
e.line(f"return {emit_parse_expr(body, '(*m.body)')};")
e.indent -= 1
e.line("}")
e.line()
# --- Top level ------------------------------------------------------------
PREAMBLE = """\
// GENERATED by tools/gen_messages.py from data/messages.yaml.
// DO NOT EDIT — re-run the generator instead.
//
// Provides inline builders + parsers for every SECS-II message in the
// catalog. Hand-written helpers (scalar accessors, list-of-U4 helper,
// special-case messages) live in messages_helpers.hpp.
#pragma once
#include <cstdint>
#include <optional>
#include <string>
#include <utility>
#include <vector>
#include "secsgem/gem/control_state.hpp"
#include "secsgem/gem/data_model.hpp"
#include "secsgem/gem/messages_helpers.hpp"
#include "secsgem/secs2/item.hpp"
#include "secsgem/secs2/message.hpp"
namespace secsgem::gem {
namespace secs2 = secsgem::secs2;
"""
POSTAMBLE = """\
} // namespace secsgem::gem
"""
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--catalog", required=True, help="path to data/messages.yaml")
ap.add_argument("--out", required=True, help="output header path")
args = ap.parse_args()
catalog = yaml.safe_load(Path(args.catalog).read_text())
messages = catalog["messages"]
e = Emit()
e.lines.append(PREAMBLE.rstrip())
e.line()
for m in messages:
e.line(f"// ---- {m['id']} : {m['builder']} ----")
emit_message(e, m)
e.line(POSTAMBLE.rstrip())
Path(args.out).parent.mkdir(parents=True, exist_ok=True)
Path(args.out).write_text("\n".join(e.lines) + "\n")
print(f"generated {len(messages)} messages -> {args.out}", file=sys.stderr)
if __name__ == "__main__":
main()