diff --git a/CMakeLists.txt b/CMakeLists.txt index 01585a0..c5a1590 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -26,6 +26,21 @@ target_compile_definitions(asio INTERFACE ASIO_STANDALONE ASIO_NO_DEPRECATED) target_link_libraries(asio INTERFACE Threads::Threads) # --- 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 src/secs2/codec.cpp src/hsms/header.cpp @@ -35,7 +50,11 @@ add_library(secsgem src/config/loader.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) # --- Demo executables ----------------------------------------------------- diff --git a/Dockerfile b/Dockerfile index 12c6b89..cad4e13 100644 --- a/Dockerfile +++ b/Dockerfile @@ -9,6 +9,8 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ git \ libasio-dev \ libyaml-cpp-dev \ + python3 \ + python3-yaml \ ca-certificates \ bash \ && rm -rf /var/lib/apt/lists/* diff --git a/apps/secs_client.cpp b/apps/secs_client.cpp index 8548225..f88616f 100644 --- a/apps/secs_client.cpp +++ b/apps/secs_client.cpp @@ -78,7 +78,7 @@ int main(int argc, char** argv) { [logfn](const s2::Message& msg) -> std::optional { // S10F3: terminal display from equipment. 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); 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) { auto a = gem::parse_s5f1(msg); 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) + - " 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); } @@ -248,7 +248,7 @@ int main(int argc, char** argv) { // 11. Enable alarm 1 (so the equipment is allowed to send S5F1 for it). 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) { if (ec) { fail("S5F3", ec); return; } 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. 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) { if (ec) { fail("S2F41/FAULT", ec); return; } auto r = gem::parse_s2f42(reply); diff --git a/apps/secs_server.cpp b/apps/secs_server.cpp index 56d97fb..b728bbf 100644 --- a/apps/secs_server.cpp +++ b/apps/secs_server.cpp @@ -139,12 +139,16 @@ int main(int argc, char** argv) { return gem::s1f4_selected_status_data(values); }); router.on(1, 11, [model, logfn](const s2::Message&) { - logfn("S1F11 -> S1F12 (namelist)"); - return gem::s1f12_status_namelist_data(model->all_status_variables()); + std::vector rows; + 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&) { 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&) { auto ack = sm->on_host_request_offline(); @@ -173,8 +177,8 @@ int main(int argc, char** argv) { auto eac = gem::EquipmentAck::Accept; if (!sets) eac = gem::EquipmentAck::Denied_OutOfRange; else - for (const auto& [id, val] : *sets) { - auto r = model->set_equipment_constant_value(id, val); + for (const auto& s : *sets) { + auto r = model->set_equipment_constant_value(s.ecid, s.value); if (r != gem::EquipmentAck::Accept) eac = r; } logfn("S2F15 -> S2F16 EAC=" + std::to_string(static_cast(eac))); @@ -193,8 +197,11 @@ int main(int argc, char** argv) { auto ec = model->equipment_constant(id); if (ec) ecs.push_back(*ec); } - logfn("S2F29 -> S2F30 (" + std::to_string(ecs.size()) + " ECs)"); - return gem::s2f30_ec_namelist_data(ecs); + std::vector rows; + 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) { 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) { auto req = gem::parse_s2f33(msg); - auto ack = req ? model->define_reports(req->reports) - : gem::DefineReportAck::InvalidFormat; + auto ack = gem::DefineReportAck::InvalidFormat; + if (req) { + std::vector>> 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(ack))); return gem::s2f34_define_report_ack(ack); }); router.on(2, 35, [model, logfn](const s2::Message& 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>> 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(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) { 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); logfn("S2F41 RCMD=" + cmd->rcmd + " -> S2F42 HCACK=" + std::to_string(static_cast(result.ack))); @@ -233,12 +251,12 @@ int main(int argc, char** argv) { if (result.emit_ceid) emit_event(*result.emit_ceid); 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) { 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; logfn(std::string("S5F3 -> S5F4 ACKC5=") + std::to_string(static_cast(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) { - auto td = gem::parse_terminal_display(msg); + auto td = gem::parse_s10f1(msg); if (td) logfn("TERMINAL[" + std::to_string(td->tid) + "] " + td->text); return gem::s10f2_terminal_display_ack(gem::TerminalAck::Accepted); }); diff --git a/data/messages.yaml b/data/messages.yaml new file mode 100644 index 0000000..71bfb82 --- /dev/null +++ b/data/messages.yaml @@ -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: - optional +# param: - optional, default "value" +# list: - fixed-arity +# fields: [{name, shape}, ...] +# struct_name: - optional; if set, parser returns the struct +# external_struct: bool - optional; struct is assumed to exist already +# list_of: - variable-arity +# element: +# 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} diff --git a/include/secsgem/gem/data_model.hpp b/include/secsgem/gem/data_model.hpp index 2e458d5..23d8159 100644 --- a/include/secsgem/gem/data_model.hpp +++ b/include/secsgem/gem/data_model.hpp @@ -49,6 +49,14 @@ struct CollectionEvent { std::string name; }; +// One 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 vids; @@ -157,10 +165,7 @@ enum class TerminalAck : uint8_t { // all access happens on the Asio executor. class EquipmentDataModel { public: - struct CommandParam { - std::string name; - s2::Item value; - }; + using CommandParam = CommandParameter; // back-compat alias // Declarative host-command effect, loaded from YAML. The server // dispatches a command by looking up the spec and (optionally) firing diff --git a/include/secsgem/gem/messages.hpp b/include/secsgem/gem/messages.hpp deleted file mode 100644 index 2878b62..0000000 --- a/include/secsgem/gem/messages.hpp +++ /dev/null @@ -1,760 +0,0 @@ -#pragma once - -#include -#include -#include -#include -#include -#include - -#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 as_u4_scalar(const s2::Item& item) { - if (item.format() != s2::Format::U4) return std::nullopt; - const auto& v = std::get>(item.storage()); - if (v.empty()) return std::nullopt; - return v.front(); -} - -inline std::optional 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 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& 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: ... > (n=0 means "all SVIDs") -// S1F4 : ... > (empty list for unknown SVIDs per E5) -// ------------------------------------------------------------------------- - -inline s2::Message s1f3_selected_status_request(const std::vector& 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> parse_s1f3(const s2::Message& m) { - if (!m.body || !m.body->is_list()) return std::nullopt; - std::vector 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>& 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: ... > (n=0 = all) -// S1F12 : > ... > -// ------------------------------------------------------------------------- - -inline s2::Message s1f11_status_namelist_request(const std::vector& 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& 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> parse_s1f12(const s2::Message& m) { - if (!m.body || !m.body->is_list()) return std::nullopt; - std::vector 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(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(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(ack)})); -} - -// Generic helper for messages whose body is a single . -inline std::optional 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& 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& values) { - return s2::Message(2, 14, false, s2::Item::list(values)); -} - -inline s2::Message s2f15_ec_send( - const std::vector>& 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>> parse_s2f15( - const s2::Message& m) { - if (!m.body || !m.body->is_list()) return std::nullopt; - std::vector> 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(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 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 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(ack)})); -} - -// ------------------------------------------------------------------------- -// S2F41 / S2F42 Host Command / Host Command Acknowledge -// -// S2F41 W: >>> -// S2F42 : >>> -// ------------------------------------------------------------------------- - -inline s2::Message s2f41_host_command( - const std::string& rcmd, - const std::vector& 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 params; -}; - -inline std::optional 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>& 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(hcack)}), - s2::Item::list(std::move(cp_rows))})); -} - -struct HostCommandReply { - HostCmdAck hcack; - std::vector> cpacks; -}; - -inline std::optional 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(*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 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(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(ack)})); -} - -// ========================================================================= -// Extended GEM SxFy (events, alarms, recipes) -// ========================================================================= - -// Generic > body reader, used by several messages. -inline std::optional> parse_u4_list_body(const s2::Message& m) { - if (!m.body || !m.body->is_list()) return std::nullopt; - std::vector 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& 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: >> -// ------------------------------------------------------------------------- - -inline s2::Message s2f29_ec_namelist_request(const std::vector& ecids) { - return s2::Message(2, 29, true, u4_list_item(ecids)); -} - -inline s2::Message s2f30_ec_namelist_data(const std::vector& 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: >>>> -// S2F34 : -// ------------------------------------------------------------------------- - -inline s2::Message s2f33_define_report( - uint32_t dataid, - const std::vector>>& 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>> reports; -}; - -inline std::optional 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{}; - 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(ack)})); -} - -// ------------------------------------------------------------------------- -// S2F35 / S2F36 Link Event Report -// -// S2F35 W: >>>> -// S2F36 : -// ------------------------------------------------------------------------- - -inline s2::Message s2f35_link_event_report( - uint32_t dataid, - const std::vector>>& 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>> links; -}; - -inline std::optional 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 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(ack)})); -} - -// ------------------------------------------------------------------------- -// S2F37 / S2F38 Enable / Disable Event Report -// -// S2F37 W: >> -// S2F38 : -// ------------------------------------------------------------------------- - -inline s2::Message s2f37_enable_event(bool enable, const std::vector& ceids) { - return s2::Message(2, 37, true, - s2::Item::list({s2::Item::boolean(enable), u4_list_item(ceids)})); -} - -struct EnableEventRequest { - bool enable; - std::vector ceids; -}; - -inline std::optional 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(ack)})); -} - -// ------------------------------------------------------------------------- -// S5F1 / S5F2 Alarm Report Send / Ack -// -// S5F1 W: > -// S5F2 : -// ------------------------------------------------------------------------- - -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 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(ack)})); -} - -// ------------------------------------------------------------------------- -// S5F3 / S5F4 Enable / Disable Alarm Send -// -// S5F3 W: > (ALED 0x80 = enable, 0x00 = disable) -// S5F4 : -// ------------------------------------------------------------------------- - -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 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(ack)})); -} - -// ------------------------------------------------------------------------- -// S5F5 / S5F6 List Alarms Request / Data -// -// S5F5 W: > (empty = all) -// S5F6 : >> -// ------------------------------------------------------------------------- - -inline s2::Message s5f5_list_alarms_request(const std::vector& alids) { - return s2::Message(5, 5, true, u4_list_item(alids)); -} - -inline s2::Message s5f6_list_alarms_data(const std::vector& alarms, - const std::function& active) { - s2::Item::List rows; - rows.reserve(alarms.size()); - for (const auto& a : alarms) { - const uint8_t alcd = (a.severity_category & 0x7F) | - static_cast(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: >>>> -// S6F12 : -// ------------------------------------------------------------------------- - -inline s2::Message s6f11_event_report(uint32_t dataid, uint32_t ceid, - const std::vector& 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 reports; -}; - -inline std::optional 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(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(ppbody.begin(), ppbody.end()))})); -} - -struct ProcessProgram { - std::string ppid; - std::string ppbody; -}; - -inline std::optional 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(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 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(ppbody.begin(), ppbody.end()))})); -} - -inline std::optional 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& 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> parse_s7f20(const s2::Message& m) { - if (!m.body || !m.body->is_list()) return std::nullopt; - std::vector 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 diff --git a/include/secsgem/gem/messages_helpers.hpp b/include/secsgem/gem/messages_helpers.hpp new file mode 100644 index 0000000..e05a0ae --- /dev/null +++ b/include/secsgem/gem/messages_helpers.hpp @@ -0,0 +1,130 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +#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 as_ascii(const s2::Item& item) { + if (item.format() != s2::Format::ASCII) return std::nullopt; + return item.as_ascii(); +} + +inline std::optional 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 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 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 +inline std::optional first_or_none(const s2::Item& item, + s2::Format want) { + if (item.format() != want) return std::nullopt; + const auto& v = std::get(item.storage()); + if (v.empty()) return std::nullopt; + return v.front(); +} + +inline std::optional as_u1_scalar(const s2::Item& i) { return first_or_none>(i, s2::Format::U1); } +inline std::optional as_u2_scalar(const s2::Item& i) { return first_or_none>(i, s2::Format::U2); } +inline std::optional as_u4_scalar(const s2::Item& i) { return first_or_none>(i, s2::Format::U4); } +inline std::optional as_u8_scalar(const s2::Item& i) { return first_or_none>(i, s2::Format::U8); } +inline std::optional as_i1_scalar(const s2::Item& i) { return first_or_none>(i, s2::Format::I1); } +inline std::optional as_i2_scalar(const s2::Item& i) { return first_or_none>(i, s2::Format::I2); } +inline std::optional as_i4_scalar(const s2::Item& i) { return first_or_none>(i, s2::Format::I4); } +inline std::optional as_i8_scalar(const s2::Item& i) { return first_or_none>(i, s2::Format::I8); } +inline std::optional as_f4_scalar(const s2::Item& i) { return first_or_none>(i, s2::Format::F4); } +inline std::optional as_f8_scalar(const s2::Item& i) { return first_or_none>(i, s2::Format::F8); } + +// ---- List helpers -------------------------------------------------------- + +inline s2::Item u4_list_item(const std::vector& 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> parse_u4_list_body(const s2::Message& m) { + if (!m.body || !m.body->is_list()) return std::nullopt; + std::vector 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 " reader, used by tests + apps as a quick helper. +inline std::optional ack_byte(const s2::Message& m) { + if (!m.body) return std::nullopt; + return as_binary_first(*m.body); +} + +// ---- S1F4: list of values, nullopt -> ----------------------------- + +inline s2::Message s1f4_selected_status_data( + const std::vector>& 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& alarms, + const std::function& active) { + s2::Item::List rows; + rows.reserve(alarms.size()); + for (const auto& a : alarms) { + const uint8_t alcd = (a.severity_category & 0x7F) | + static_cast(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 diff --git a/tests/test_messages.cpp b/tests/test_messages.cpp index a1a9e38..378b6e0 100644 --- a/tests/test_messages.cpp +++ b/tests/test_messages.cpp @@ -27,8 +27,7 @@ TEST_CASE("S1F4 substitutes empty list for unknown SVIDs") { } TEST_CASE("S1F12 round-trip preserves SVID, name, units") { - std::vector svs = {{1, "Clock", "sec", s2::Item::ascii("0")}, - {2, "Power", "W", s2::Item::u4(uint32_t{0})}}; + std::vector svs = {{1, "Clock", "sec"}, {2, "Power", "W"}}; auto m = s1f12_status_namelist_data(svs); auto parsed = parse_s1f12(m); 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") { - std::vector> sets = { + std::vector sets = { {10, s2::Item::u4(uint32_t{50})}, {11, s2::Item::ascii("YYYYMMDDhhmmsscc")}, }; @@ -49,10 +48,10 @@ TEST_CASE("S2F15 round-trip preserves ECID-value pairs") { auto parsed = parse_s2f15(m); REQUIRE(parsed.has_value()); REQUIRE(parsed->size() == 2); - CHECK((*parsed)[0].first == 10); - CHECK((*parsed)[0].second == s2::Item::u4(uint32_t{50})); - CHECK((*parsed)[1].first == 11); - CHECK((*parsed)[1].second == s2::Item::ascii("YYYYMMDDhhmmsscc")); + CHECK((*parsed)[0].ecid == 10); + CHECK((*parsed)[0].value == s2::Item::u4(uint32_t{50})); + CHECK((*parsed)[1].ecid == 11); + CHECK((*parsed)[1].value == s2::Item::ascii("YYYYMMDDhhmmsscc")); } 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") { - std::vector params = { + std::vector params = { {"LOTID", s2::Item::ascii("LOT-42")}, {"PPID", s2::Item::ascii("RECIPE-A")}, }; @@ -91,14 +90,14 @@ TEST_CASE("S2F42 round-trip with HCACK and CPACKs") { REQUIRE(parsed.has_value()); CHECK(parsed->hcack == HostCmdAck::ParameterInvalid); REQUIRE(parsed->cpacks.size() == 2); - CHECK(parsed->cpacks[0].first == "LOTID"); - CHECK(parsed->cpacks[0].second == 0); - CHECK(parsed->cpacks[1].first == "PPID"); - CHECK(parsed->cpacks[1].second == 3); + CHECK(parsed->cpacks[0].name == "LOTID"); + CHECK(parsed->cpacks[0].code == 0); + CHECK(parsed->cpacks[1].name == "PPID"); + CHECK(parsed->cpacks[1].code == 3); } 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); REQUIRE(parsed.has_value()); CHECK(parsed->hcack == HostCmdAck::Accept); @@ -107,7 +106,7 @@ TEST_CASE("S2F42 no-params variant") { TEST_CASE("S10F3 terminal display round-trip") { 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()); CHECK(parsed->tid == 1); CHECK(parsed->text == "ALARM: chiller temperature high"); @@ -121,10 +120,10 @@ TEST_CASE("S2F33 define-report round-trip") { REQUIRE(parsed.has_value()); CHECK(parsed->dataid == 7); REQUIRE(parsed->reports.size() == 2); - CHECK(parsed->reports[0].first == 1000); - CHECK(parsed->reports[0].second == std::vector{1, 2, 3}); - CHECK(parsed->reports[1].first == 1001); - CHECK(parsed->reports[1].second == std::vector{4}); + CHECK(parsed->reports[0].rptid == 1000); + CHECK(parsed->reports[0].vids == std::vector{1, 2, 3}); + CHECK(parsed->reports[1].rptid == 1001); + CHECK(parsed->reports[1].vids == std::vector{4}); } TEST_CASE("S2F35 link-event round-trip") { @@ -133,8 +132,8 @@ TEST_CASE("S2F35 link-event round-trip") { REQUIRE(parsed.has_value()); CHECK(parsed->dataid == 0); REQUIRE(parsed->links.size() == 2); - CHECK(parsed->links[0].first == 100); - CHECK(parsed->links[0].second == std::vector{1000, 1001}); + CHECK(parsed->links[0].ceid == 100); + CHECK(parsed->links[0].rptids == std::vector{1000, 1001}); } TEST_CASE("S2F37 enable-event round-trip") { @@ -157,19 +156,19 @@ TEST_CASE("S5F1 alarm-report round-trip") { CHECK(parsed->alid == 7); CHECK(parsed->altx == "Chiller temp high"); CHECK(parsed->alcd == 0x84); - CHECK(parsed->is_set()); - CHECK(parsed->category() == 4); + CHECK((parsed->alcd & 0x80) != 0); // set bit + CHECK((parsed->alcd & 0x7F) == 4); // category } TEST_CASE("S5F3 enable/disable alarm send round-trip") { - auto on = s5f3_enable_alarm(true, 42); - auto off = s5f3_enable_alarm(false, 42); + auto on = s5f3_enable_alarm(kAlarmEnableByte, 42); + auto off = s5f3_enable_alarm(kAlarmDisableByte, 42); auto pon = parse_s5f3(on); auto poff = parse_s5f3(off); REQUIRE(pon.has_value()); REQUIRE(poff.has_value()); - CHECK(pon->enable); - CHECK_FALSE(poff->enable); + CHECK((pon->aled & 0x80) != 0); + CHECK((poff->aled & 0x80) == 0); CHECK(pon->alid == 42); } diff --git a/tools/gen_messages.py b/tools/gen_messages.py new file mode 100644 index 0000000..f8964e0 --- /dev/null +++ b/tools/gen_messages.py @@ -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: , + enum: ?, # optional C++ enum type + param: ?, # builder parameter name (default 'value') + parser_struct_field: ...} # only valid inside a list's `fields` + + {kind: list, + fields: [{name, shape}, ...], + struct_name: ?, # if set, parser returns std::optional + external_struct: bool?} # if true, the struct is assumed to already exist + + {kind: list_of, + element: , + 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 +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({v}.begin(), {v}.end()))", + "as_binary_string({i})"), + "BOOLEAN": ("bool", "secs2::Item::boolean({v})", + "as_boolean({i})"), + "ITEM": ("secs2::Item", "{v}", + "std::optional({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 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> { " + 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> {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 +#include +#include +#include +#include + +#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()