Files
secs-gem/include/secsgem/gem/data_model.hpp
T
raphael 29db1caedb #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>
2026-06-02 09:43:36 +02:00

284 lines
8.4 KiB
C++

#pragma once
#include <algorithm>
#include <cstdint>
#include <functional>
#include <map>
#include <optional>
#include <set>
#include <string>
#include <utility>
#include <vector>
#include "secsgem/secs2/item.hpp"
namespace secsgem::gem {
namespace s2 = secsgem::secs2;
// ---- Status / data / equipment-constant variables ------------------------
struct StatusVariable {
uint32_t id;
std::string name;
std::string units;
s2::Item value;
};
struct DataVariable {
uint32_t id;
std::string name;
std::string units;
s2::Item value;
};
struct EquipmentConstant {
uint32_t id;
std::string name;
std::string units;
s2::Item value;
s2::Item def_value;
std::string min_str; // optional, ASCII for S2F30 ECMIN/ECMAX
std::string max_str;
};
// ---- Event reports -------------------------------------------------------
struct CollectionEvent {
uint32_t id;
std::string name;
};
// 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;
};
// One report's worth of data at emission time: the RPTID and the values for
// each VID, in declaration order.
struct ReportData {
uint32_t rptid;
std::vector<s2::Item> values;
};
// ---- Alarms --------------------------------------------------------------
struct Alarm {
uint32_t id;
std::string text;
// Lower 7 bits of ALCD: alarm severity category (1=personal safety,
// 2=equipment safety, 4=parameter control error, ...). Bit 7 is the
// set/cleared flag, applied at emit time.
uint8_t severity_category;
};
// ---- Ack codes for the new SxFy ------------------------------------------
// S2F30 carries no ack; S2F34 / S2F36 / S2F38 do.
enum class DefineReportAck : uint8_t {
Accept = 0,
InsufficientSpace = 1,
InvalidFormat = 2,
RptidAlreadyDefined = 3,
InvalidVid = 5,
};
enum class LinkEventAck : uint8_t {
Accept = 0,
InsufficientSpace = 1,
InvalidFormat = 2,
UnknownCeid = 3,
UnknownRptid = 4,
CeidAlreadyLinked = 5,
};
enum class EnableEventAck : uint8_t {
Accept = 0,
UnknownCeid = 1,
};
// S5F2 / S5F4 alarm ack.
enum class AlarmAck : uint8_t {
Accept = 0,
Error = 1,
};
// S6F12 event-report ack.
enum class EventReportAck : uint8_t {
Accept = 0,
Denied = 1,
};
// S7F4 process-program ack.
enum class ProcessProgramAck : uint8_t {
Accept = 0,
PermissionNotGranted = 1,
LengthError = 2,
MatrixOverflow = 3,
PpidNotFound = 4,
ModeUnsupported = 5,
PerformanceError = 6,
};
// ---- Existing ack enums from earlier batches -----------------------------
enum class EquipmentAck : uint8_t {
Accept = 0,
Denied_UnknownEcid = 1,
Denied_Busy = 3,
Denied_OutOfRange = 4,
};
enum class TimeAck : uint8_t {
Accept = 0,
Error = 1,
NotDoneNotEmpty = 2,
};
enum class HostCmdAck : uint8_t {
Accept = 0,
InvalidCommand = 1,
CannotDoNow = 2,
ParameterInvalid = 3,
AcceptedWillFinishLater = 4,
Rejected = 5,
InvalidObject = 6,
};
enum class TerminalAck : uint8_t {
Accepted = 0,
WillNotDisplay = 1,
TerminalNotAvailable = 2,
};
// The in-memory equipment data dictionary. Owns SVIDs, DVIDs, ECIDs, the
// dynamic event-report subscription state (reports + links + enabled set),
// the alarm table, and a small process-program registry. Single-threaded;
// all access happens on the Asio executor.
class EquipmentDataModel {
public:
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
// a CEID emit / setting an alarm after the S2F42 reply is sent.
struct CommandSpec {
HostCmdAck ack = HostCmdAck::Accept;
std::optional<uint32_t> emit_ceid;
std::optional<uint32_t> set_alarm;
};
struct CommandResult {
HostCmdAck ack = HostCmdAck::InvalidCommand;
std::optional<uint32_t> emit_ceid;
std::optional<uint32_t> set_alarm;
};
// --- SVID ---------------------------------------------------------------
void add_status_variable(StatusVariable sv);
std::optional<StatusVariable> status_variable(uint32_t id) const;
std::vector<StatusVariable> all_status_variables() const;
void set_status_value(uint32_t id, s2::Item value);
// --- DVID ---------------------------------------------------------------
void add_data_variable(DataVariable dv);
std::optional<DataVariable> data_variable(uint32_t id) const;
std::vector<DataVariable> all_data_variables() const;
void set_data_value(uint32_t id, s2::Item value);
// VID lookup that searches SVIDs then DVIDs (E30 §6.5 — VIDs share a
// namespace from the host's point of view).
std::optional<s2::Item> vid_value(uint32_t vid) const;
bool vid_exists(uint32_t vid) const;
// --- ECID ---------------------------------------------------------------
void add_equipment_constant(EquipmentConstant ec);
std::optional<EquipmentConstant> equipment_constant(uint32_t id) const;
std::vector<EquipmentConstant> all_equipment_constants() const;
EquipmentAck set_equipment_constant_value(uint32_t id, s2::Item value);
// --- Clock --------------------------------------------------------------
std::string current_time_string() const;
TimeAck set_time_string(const std::string& time_str);
// --- Host commands ------------------------------------------------------
void register_command(const std::string& rcmd, CommandSpec spec);
CommandResult dispatch_command(const std::string& rcmd,
const std::vector<CommandParam>& params) const;
bool has_command(const std::string& rcmd) const;
// --- Collection events --------------------------------------------------
void register_event(CollectionEvent ce);
bool has_event(uint32_t ceid) const;
std::vector<CollectionEvent> all_events() const;
// S2F33: define reports. `reports` maps RPTID -> VID list. An empty
// vector deletes all reports (and clears all links). An empty VID list
// for a given RPTID deletes that report.
DefineReportAck define_reports(
const std::vector<std::pair<uint32_t, std::vector<uint32_t>>>& reports);
// S2F35: link CEIDs to RPTID lists. `links` maps CEID -> RPTID list.
// An empty RPTID list for a CEID clears its links. An empty outer list
// clears all links.
LinkEventAck link_event_reports(
const std::vector<std::pair<uint32_t, std::vector<uint32_t>>>& links);
// S2F37: enable/disable events. Empty CEID list applies to *all*
// registered events.
EnableEventAck enable_events(bool enable, const std::vector<uint32_t>& ceids);
bool is_event_enabled(uint32_t ceid) const;
// Compose the report data for a CEID emission (S6F11 body's report list).
std::vector<ReportData> compose_reports_for(uint32_t ceid) const;
std::vector<Report> all_reports() const;
// --- Alarms -------------------------------------------------------------
void add_alarm(Alarm a);
std::optional<Alarm> alarm(uint32_t alid) const;
std::vector<Alarm> all_alarms() const;
AlarmAck set_alarm_enabled(uint32_t alid, bool enable);
bool alarm_enabled(uint32_t alid) const;
// Returns the ALCD byte to put on the wire (bit 7 indicates set/cleared).
// Returns nullopt for unknown alarms.
std::optional<uint8_t> alarm_set(uint32_t alid);
std::optional<uint8_t> alarm_clear(uint32_t alid);
bool alarm_active(uint32_t alid) const;
// --- Process programs ---------------------------------------------------
void add_process_program(std::string ppid, std::string ppbody);
std::optional<std::string> process_program(const std::string& ppid) const;
std::vector<std::string> process_program_list() const;
ProcessProgramAck delete_process_program(const std::string& ppid);
private:
std::map<uint32_t, StatusVariable> svids_;
std::map<uint32_t, DataVariable> dvids_;
std::map<uint32_t, EquipmentConstant> ecids_;
std::int64_t time_offset_seconds_ = 0;
std::map<std::string, CommandSpec> commands_;
std::map<uint32_t, CollectionEvent> ceids_;
std::map<uint32_t, Report> reports_;
std::map<uint32_t, std::vector<uint32_t>> ce_links_; // CEID -> RPTID list
std::set<uint32_t> events_enabled_;
std::map<uint32_t, Alarm> alarms_;
std::set<uint32_t> alarms_enabled_;
std::set<uint32_t> alarms_active_;
std::map<std::string, std::string> process_programs_;
};
} // namespace secsgem::gem