#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:
@@ -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
|
||||
Reference in New Issue
Block a user