#pragma once #include #include #include #include #include #include #include "secsgem/gem/data_model.hpp" #include "secsgem/gem/e40_constants.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()); } // SEMI E5 §13.16 PPBODY (and §13 documents-with-data-format-wildcards // generally) is encoded as "List | Binary | ASCII". We expose a single // helper that accepts either ASCII or Binary and returns a std::string // (Lists end up rejected — callers with list-shaped PPBODY must use the // ITEM passthrough type). inline std::optional as_text_or_binary(const s2::Item& item) { if (item.format() == s2::Format::ASCII || item.format() == s2::Format::JIS8) { return item.as_ascii(); } if (item.format() == s2::Format::Binary) { const auto& v = item.as_bytes(); return std::string(v.begin(), v.end()); } return std::nullopt; } 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(); } // SEMI E5 declares most identifier fields (DATAID, RPTID, CEID, VID, // ALID, EXID, …) with FORMATCODE = "U1 | U2 | U4 | U8" — meaning a peer // is free to encode them as any unsigned width that fits. secsgem-py, // for example, picks the smallest type that holds the value (so an // ALID of 1 goes out as U1). We therefore accept any unsigned width // in the as_uN_scalar helpers, range-checking the downcast. Same logic // for the signed I-types. Strictly typed fields (Binary, Boolean, // ASCII, F4/F8) stay strict. template inline std::optional any_unsigned_first(const s2::Item& item) { auto take = [](auto width) -> std::optional { using W = decltype(width); if constexpr (sizeof(W) == 0) return std::nullopt; // unreachable else { if (static_cast(width) > static_cast(std::numeric_limits::max())) return std::nullopt; return static_cast(width); } }; switch (item.format()) { case s2::Format::U1: { const auto& v = std::get>(item.storage()); if (v.empty()) return std::nullopt; return take(v.front()); } case s2::Format::U2: { const auto& v = std::get>(item.storage()); if (v.empty()) return std::nullopt; return take(v.front()); } case s2::Format::U4: { const auto& v = std::get>(item.storage()); if (v.empty()) return std::nullopt; return take(v.front()); } case s2::Format::U8: { const auto& v = std::get>(item.storage()); if (v.empty()) return std::nullopt; return take(v.front()); } default: return std::nullopt; } } template inline std::optional any_signed_first(const s2::Item& item) { auto take = [](auto width) -> std::optional { using W = decltype(width); if constexpr (sizeof(W) == 0) return std::nullopt; else { const int64_t w = static_cast(width); if (w < static_cast(std::numeric_limits::min()) || w > static_cast(std::numeric_limits::max())) return std::nullopt; return static_cast(w); } }; switch (item.format()) { case s2::Format::I1: { const auto& v = std::get>(item.storage()); if (v.empty()) return std::nullopt; return take(v.front()); } case s2::Format::I2: { const auto& v = std::get>(item.storage()); if (v.empty()) return std::nullopt; return take(v.front()); } case s2::Format::I4: { const auto& v = std::get>(item.storage()); if (v.empty()) return std::nullopt; return take(v.front()); } case s2::Format::I8: { const auto& v = std::get>(item.storage()); if (v.empty()) return std::nullopt; return take(v.front()); } default: return std::nullopt; } } inline std::optional as_u1_scalar(const s2::Item& i) { return any_unsigned_first(i); } inline std::optional as_u2_scalar(const s2::Item& i) { return any_unsigned_first(i); } inline std::optional as_u4_scalar(const s2::Item& i) { return any_unsigned_first(i); } inline std::optional as_u8_scalar(const s2::Item& i) { return any_unsigned_first(i); } inline std::optional as_i1_scalar(const s2::Item& i) { return any_signed_first(i); } inline std::optional as_i2_scalar(const s2::Item& i) { return any_signed_first(i); } inline std::optional as_i4_scalar(const s2::Item& i) { return any_signed_first(i); } inline std::optional as_i8_scalar(const s2::Item& i) { return any_signed_first(i); } 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; // ---- Ack enums that aren't tied to a specific store ------------------- enum class EventReportAck : uint8_t { // S6F12 Accept = 0, Denied = 1, }; // S6F6 GRANT6. Permission byte for equipment-initiated multi-block // (S6F5 inquire). Codes from SEMI E5. enum class MultiBlockGrant : uint8_t { Ok = 0, Busy = 1, NoSpace = 2, DuplicateMsg = 3, BadMsg = 4, }; enum class TerminalAck : uint8_t { // S10F2, S10F4 Accepted = 0, WillNotDisplay = 1, TerminalNotAvailable = 2, }; // E5 §13 S12 (Wafer Maps). Encoded as separate enums even though the // underlying byte semantics differ slightly across F2/F6/F8 — keeping // them distinct preserves call-site readability. // Reference point on a wafer/substrate map (E5 §13.7.4 REFP). Declared // out of line so the messages.yaml catalog can mark it `external_struct` // and reuse the same C++ type across F1 / F4. struct ReferencePoint { int32_t x; int32_t y; }; enum class MapSetupAck : uint8_t { // S12F2 SDACK Accept = 0, Error = 1, }; enum class MapTransmitGrant : uint8_t { // S12F6 GRANT Granted = 0, BusyTryAgain = 1, NoSpaceAvailable = 2, DuplicateMID = 3, MaterialNotRecognized = 4, }; enum class MapDataAck : uint8_t { // S12F8 MAPER Accept = 0, FormatError = 1, UnknownID = 2, AbortNoMap = 3, }; // E87 §10.2 carrier-action ack code (CAACK). Same byte used by S3F18, // S3F26 (carrier transfer), and S3F28 (cancel carrier). enum class CarrierActionAck : uint8_t { Accept = 0, Invalid = 1, ParameterInvalid = 2, CarrierIDUnknown = 3, CarrierActionInvalid = 4, CarrierInaccessible = 5, CarrierActionInProgress = 6, }; // E87 §10.4 port-group change ack code. enum class PortGroupAck : uint8_t { Accept = 0, Error = 1, }; // E87 slot-map verify ack code (SVACK). Same byte used by S3F20 // (verify reply) and S3F22 (host's ack of equipment's slot-map report). enum class SlotMapVerifyAck : uint8_t { Accept = 0, Mismatch = 1, CarrierUnknown = 2, Error = 3, }; } // namespace secsgem::gem