Files
secs-gem/include/secsgem/gem/store/event_reports.hpp
T
raphael 90c177b7ce E40 Process Jobs + E94 Control Jobs + E30 communication state
GEM300 layer: SEMI E40-0705 Process Job and E94-0705 Control Job
state machines, plus the E30 §6.1 communication-state machine that
sits between HSMS SELECT and full GEM communication. Data-driven
via data/process_job_state.yaml and data/control_job_state.yaml,
mirroring the existing control_state.yaml pattern.

Wire coverage:
  S14F9/F10   CreateObject (CJ)              host -> equipment
  S14F11/F12  DeleteObject (CJ)              host -> equipment
  S16F5/F6    PRJobCommand                   host -> equipment
  S16F9       PRJobAlert                     equipment -> host
  S16F11/F12  PRJobCreate (simplified body)  host -> equipment
  S16F13/F14  PRJobDequeue                   host -> equipment
  S16F27/F28  CJobCommand                    host -> equipment

Process Job FSM exposes 8 states matching PRJOBSTATE bytes (E40 §10.3.2);
HOQ is reorder-aware (move-to-head against an insertion-order vector);
Stop/Abort on a Queued PJ routes through ABORTING so the host observes
PRJOBSTATE=7 on the wire (§6.3); alert_enabled is settable per-PJ for
PRALERT control; FSM dispatches through ProcessJobStore::on_change_
dynamically so a late set_state_change_handler() reaches existing PJs.

Hardening: loader rejects NoState (sentinel) as initial/from/to and
rejects `on: created` rows; static_asserts pin enum values to wire
bytes; ProcessJobStore is non-movable to keep the per-PJ this-capture
safe.

Server simulator cascades the full CJ -> PJ lifecycle on CJSTART so
the wire trace exercises every legal state. CEIDs 400/401 fire on CJ
state changes via the existing event-report pipeline.

Tests: 60+ new assertions across test_process_jobs, test_control_jobs,
test_communication_state, test_hsms_connection, plus loader and
messages round-trip coverage.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-07 21:00:32 +02:00

182 lines
5.2 KiB
C++

#pragma once
#include <algorithm>
#include <cstdint>
#include <functional>
#include <map>
#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::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; }
// --- 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