2d60571a9c
Adds a Docker-based interop harness that drives the C++ server with
secsgem-py 0.3.0 as the active host and probes a secsgem-py-passive
equipment from a minimal C++ active client. Surfaces and fixes four
interoperability bugs uncovered by cross-testing:
* SEMI E5 identifier formatcodes are a U1|U2|U4|U8 wildcard;
secsgem-py picks the narrowest fitting width while our parsers
only accepted U4. `as_uN_scalar` / `as_iN_scalar` now accept
any unsigned/signed width and range-check the downcast.
* PPBODY (S7F3/F6) is "ASCII | Binary | List" per the spec;
secsgem-py defaults to ASCII. Added BINARY_OR_ASCII codegen
item type with `as_text_or_binary` accessor.
* S1F23/F24 Collection Event Namelist was unimplemented; added
schema + `vids_for(ceid)` accessor on EventReportSubscriptions
plus the dispatch handler.
* S10F1 was registered as a host->equipment handler, but per
SEMI E5 §12 S10F1 is equipment->host; S10F3 is the actual
host->equipment Terminal Display Single. Added an S10F3
handler alongside (we keep S10F1 too for backward compat).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
210 lines
6.3 KiB
C++
210 lines
6.3 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;
|
|
|
|
struct CollectionEvent {
|
|
uint32_t id;
|
|
std::string name;
|
|
};
|
|
|
|
struct Report {
|
|
uint32_t id;
|
|
std::vector<uint32_t> vids;
|
|
};
|
|
|
|
struct ReportData {
|
|
uint32_t rptid;
|
|
std::vector<s2::Item> values;
|
|
};
|
|
|
|
// S2F34 DRACK per SEMI E5/E30: 0=accept, 1=insufficient space,
|
|
// 2=invalid format, 3=at least one RPTID already defined,
|
|
// 4=at least one VID does not exist.
|
|
enum class DefineReportAck : uint8_t {
|
|
Accept = 0,
|
|
InsufficientSpace = 1,
|
|
InvalidFormat = 2,
|
|
RptidAlreadyDefined = 3,
|
|
InvalidVid = 4,
|
|
};
|
|
|
|
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,
|
|
};
|
|
|
|
// VID resolver: a callable that maps a VID (which may be an SVID or DVID) to
|
|
// its current value, or std::nullopt if unknown. Injected at S2F33 validation
|
|
// and again at compose_reports_for emission time; that way the subscription
|
|
// service stays pure data (no back-reference to the SVID / DVID stores).
|
|
using VidLookup = std::function<std::optional<s2::Item>(uint32_t)>;
|
|
using VidExists = std::function<bool(uint32_t)>;
|
|
|
|
// E30 §6.6 dynamic event reporting state:
|
|
// * the catalog of registered CEIDs (defined by the equipment),
|
|
// * the host-defined reports (RPTID -> VID list),
|
|
// * the CEID -> RPTID links,
|
|
// * and the set of CEIDs the host has enabled.
|
|
//
|
|
// Pure data; no IO.
|
|
class EventReportSubscriptions {
|
|
public:
|
|
// --- CEID catalog -----------------------------------------------------
|
|
void register_event(CollectionEvent ce) {
|
|
by_ceid_.insert_or_assign(ce.id, std::move(ce));
|
|
}
|
|
bool has_event(uint32_t ceid) const { return by_ceid_.count(ceid) > 0; }
|
|
std::optional<CollectionEvent> event_info(uint32_t ceid) const {
|
|
auto it = by_ceid_.find(ceid);
|
|
if (it == by_ceid_.end()) return std::nullopt;
|
|
return it->second;
|
|
}
|
|
std::vector<CollectionEvent> all_events() const {
|
|
std::vector<CollectionEvent> out;
|
|
out.reserve(by_ceid_.size());
|
|
for (const auto& [_, e] : by_ceid_) out.push_back(e);
|
|
return out;
|
|
}
|
|
|
|
// --- S2F33 define reports --------------------------------------------
|
|
DefineReportAck define_reports(
|
|
const std::vector<std::pair<uint32_t, std::vector<uint32_t>>>& rows,
|
|
const VidExists& vid_exists) {
|
|
if (rows.empty()) {
|
|
reports_.clear();
|
|
links_.clear();
|
|
return DefineReportAck::Accept;
|
|
}
|
|
for (const auto& [rptid, vids] : rows) {
|
|
if (vids.empty()) continue;
|
|
for (auto v : vids)
|
|
if (!vid_exists(v)) return DefineReportAck::InvalidVid;
|
|
}
|
|
for (const auto& [rptid, vids] : rows) {
|
|
if (vids.empty()) {
|
|
reports_.erase(rptid);
|
|
for (auto& [_, rpts] : links_)
|
|
rpts.erase(std::remove(rpts.begin(), rpts.end(), rptid), rpts.end());
|
|
} else {
|
|
reports_.insert_or_assign(rptid, Report{rptid, vids});
|
|
}
|
|
}
|
|
return DefineReportAck::Accept;
|
|
}
|
|
|
|
std::vector<Report> all_reports() const {
|
|
std::vector<Report> out;
|
|
out.reserve(reports_.size());
|
|
for (const auto& [_, r] : reports_) out.push_back(r);
|
|
return out;
|
|
}
|
|
|
|
// --- S2F35 link event report -----------------------------------------
|
|
LinkEventAck link_event_reports(
|
|
const std::vector<std::pair<uint32_t, std::vector<uint32_t>>>& rows) {
|
|
if (rows.empty()) {
|
|
links_.clear();
|
|
return LinkEventAck::Accept;
|
|
}
|
|
for (const auto& [ceid, rpts] : rows) {
|
|
if (!has_event(ceid)) return LinkEventAck::UnknownCeid;
|
|
for (auto r : rpts)
|
|
if (!reports_.count(r)) return LinkEventAck::UnknownRptid;
|
|
}
|
|
for (const auto& [ceid, rpts] : rows) {
|
|
if (rpts.empty()) links_.erase(ceid);
|
|
else links_[ceid] = rpts;
|
|
}
|
|
return LinkEventAck::Accept;
|
|
}
|
|
|
|
// --- S2F37 enable / disable event -----------------------------------
|
|
EnableEventAck enable_events(bool enable, const std::vector<uint32_t>& ceids) {
|
|
if (ceids.empty()) {
|
|
if (enable) for (const auto& [id, _] : by_ceid_) enabled_.insert(id);
|
|
else enabled_.clear();
|
|
return EnableEventAck::Accept;
|
|
}
|
|
for (auto id : ceids)
|
|
if (!has_event(id)) return EnableEventAck::UnknownCeid;
|
|
for (auto id : ceids) {
|
|
if (enable) enabled_.insert(id);
|
|
else enabled_.erase(id);
|
|
}
|
|
return EnableEventAck::Accept;
|
|
}
|
|
|
|
bool is_enabled(uint32_t ceid) const { return enabled_.count(ceid) > 0; }
|
|
|
|
// --- S1F24 directory --------------------------------------------------
|
|
// Return the deduplicated set of VIDs referenced by every report linked
|
|
// to this CEID — i.e. the data the equipment would emit in S6F11 for
|
|
// the CEID, projected onto its variable identifiers. Used to answer
|
|
// S1F23 Collection-Event-Namelist-Request. Returns an empty list if
|
|
// no reports are linked (the CEID is known but no host has configured
|
|
// a report for it yet).
|
|
std::vector<uint32_t> vids_for(uint32_t ceid) const {
|
|
std::vector<uint32_t> out;
|
|
auto it = links_.find(ceid);
|
|
if (it == links_.end()) return out;
|
|
std::set<uint32_t> seen;
|
|
for (auto rptid : it->second) {
|
|
auto rit = reports_.find(rptid);
|
|
if (rit == reports_.end()) continue;
|
|
for (auto v : rit->second.vids) {
|
|
if (seen.insert(v).second) out.push_back(v);
|
|
}
|
|
}
|
|
return out;
|
|
}
|
|
|
|
// --- S6F11 emission --------------------------------------------------
|
|
std::vector<ReportData> compose_for(uint32_t ceid, const VidLookup& lookup) const {
|
|
std::vector<ReportData> out;
|
|
auto it = links_.find(ceid);
|
|
if (it == links_.end()) return out;
|
|
for (auto rptid : it->second) {
|
|
auto rit = reports_.find(rptid);
|
|
if (rit == reports_.end()) continue;
|
|
ReportData rd{rptid, {}};
|
|
for (auto vid : rit->second.vids) {
|
|
auto v = lookup(vid);
|
|
rd.values.push_back(v ? *v : s2::Item::list({}));
|
|
}
|
|
out.push_back(std::move(rd));
|
|
}
|
|
return out;
|
|
}
|
|
|
|
private:
|
|
std::map<uint32_t, CollectionEvent> by_ceid_;
|
|
std::map<uint32_t, Report> reports_;
|
|
std::map<uint32_t, std::vector<uint32_t>> links_;
|
|
std::set<uint32_t> enabled_;
|
|
};
|
|
|
|
} // namespace secsgem::gem
|