1da56f973f
A2 — alarms: optional 'name:' on alarm config (a LOCAL key — SEMI only
defines numeric ALID + freetext ALTX; field appended last so existing
{id, text, category} brace-inits compile unchanged), parsed by the loader,
checked by the validator, shipped in equipment.yaml. SetAlarm/ClearAlarm
RPCs resolve config name OR stringified ALID via a constructor snapshot.
A3 — control state + health: RequestControlState fires operator events on
the io thread (read_sync) and reports what the E30 table actually did —
ACCEPT iff the equipment landed in the requested state, CANNOT_DO_NOW naming
the actual state otherwise (the shipped table has no operator path to
EquipmentOffline; the test pins that honesty). ATTEMPT_ONLINE is rejected as
transient. WatchHealth streams an immediate snapshot then pushes on link/
control-state changes via service observers (add_link_observer +
add_control_state_observer — the HandlerSlot work paying off), spool depth
sampled at the 500ms poll; ends on cancel or engine stop.
Tests: daemon suite 61 -> 101 assertions (alarm lifecycle by name/id/unknown,
WatchHealth initial + change push, all four RequestControlState semantics);
loader test for the alarm name (present + absent fallback); core 467/3055.
Interop now 15 checks incl. gRPC SetAlarm -> host receives S5F1 ALCD=0x84
ALID=1, and RequestControlState(HOST_OFFLINE) -> GetControlState confirms.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
107 lines
3.4 KiB
C++
107 lines
3.4 KiB
C++
#pragma once
|
|
|
|
#include <cstdint>
|
|
#include <map>
|
|
#include <optional>
|
|
#include <set>
|
|
#include <string>
|
|
#include <utility>
|
|
#include <vector>
|
|
|
|
namespace secsgem::gem {
|
|
|
|
// ALCD bit-flag categories (E5 §10.3 / E30 §6.13). The lower 7 bits
|
|
// of ALCD are a bitmap — an alarm may carry multiple categories at
|
|
// once (e.g. an irrecoverable equipment-safety fault is 0x10 | 0x02).
|
|
// Bit 7 is the alarm SET/CLEAR flag, applied at emit time and not
|
|
// part of the category bitmap.
|
|
enum class AlarmSeverity : uint8_t {
|
|
PersonalSafety = 0x01,
|
|
EquipmentSafety = 0x02,
|
|
ParameterError = 0x04, // parameter control error
|
|
ParameterWarning = 0x08, // parameter control warning
|
|
Irrecoverable = 0x10,
|
|
EquipmentStatus = 0x20, // equipment status warning
|
|
Attention = 0x40, // attention flag (lowest-priority)
|
|
};
|
|
|
|
inline constexpr uint8_t severity_mask = 0x7F;
|
|
|
|
inline bool has_severity(uint8_t alcd, AlarmSeverity bit) {
|
|
return (alcd & static_cast<uint8_t>(bit)) != 0;
|
|
}
|
|
|
|
inline uint8_t severity_bits(uint8_t alcd) { return alcd & severity_mask; }
|
|
|
|
struct Alarm {
|
|
uint32_t id;
|
|
std::string text;
|
|
// Lower 7 bits of ALCD: severity bitmap (see AlarmSeverity). Bit 7
|
|
// marks set vs cleared and is applied at emit time.
|
|
uint8_t severity_category;
|
|
// Optional local key for name-based APIs (the gRPC daemon, future Python
|
|
// client). NOT on the wire — SEMI defines only numeric ALID + freetext
|
|
// ALTX. Last field so existing {id, text, category} brace-inits compile
|
|
// unchanged; empty = unnamed (address it by id).
|
|
std::string name;
|
|
|
|
bool has(AlarmSeverity bit) const { return has_severity(severity_category, bit); }
|
|
bool is_safety() const {
|
|
return has(AlarmSeverity::PersonalSafety) || has(AlarmSeverity::EquipmentSafety);
|
|
}
|
|
};
|
|
|
|
enum class AlarmAck : uint8_t {
|
|
Accept = 0,
|
|
Error = 1,
|
|
};
|
|
|
|
class AlarmRegistry {
|
|
public:
|
|
void add(Alarm a) { by_id_.insert_or_assign(a.id, std::move(a)); }
|
|
std::optional<Alarm> get(uint32_t id) const {
|
|
auto it = by_id_.find(id);
|
|
if (it == by_id_.end()) return std::nullopt;
|
|
return it->second;
|
|
}
|
|
std::vector<Alarm> all() const {
|
|
std::vector<Alarm> out;
|
|
out.reserve(by_id_.size());
|
|
for (const auto& [_, a] : by_id_) out.push_back(a);
|
|
return out;
|
|
}
|
|
bool has(uint32_t id) const { return by_id_.count(id) > 0; }
|
|
|
|
// S5F3 enable / disable.
|
|
AlarmAck set_enabled(uint32_t id, bool enable) {
|
|
if (!by_id_.count(id)) return AlarmAck::Error;
|
|
if (enable) enabled_.insert(id);
|
|
else enabled_.erase(id);
|
|
return AlarmAck::Accept;
|
|
}
|
|
bool enabled(uint32_t id) const { return enabled_.count(id) > 0; }
|
|
|
|
// Trigger set / clear; returns the ALCD byte to put on the wire (bit 7
|
|
// is the set flag, lower 7 carry the category). std::nullopt on unknown.
|
|
std::optional<uint8_t> set_active(uint32_t id) {
|
|
auto it = by_id_.find(id);
|
|
if (it == by_id_.end()) return std::nullopt;
|
|
active_.insert(id);
|
|
return static_cast<uint8_t>((it->second.severity_category & 0x7F) | 0x80);
|
|
}
|
|
std::optional<uint8_t> clear_active(uint32_t id) {
|
|
auto it = by_id_.find(id);
|
|
if (it == by_id_.end()) return std::nullopt;
|
|
active_.erase(id);
|
|
return static_cast<uint8_t>(it->second.severity_category & 0x7F);
|
|
}
|
|
bool active(uint32_t id) const { return active_.count(id) > 0; }
|
|
|
|
private:
|
|
std::map<uint32_t, Alarm> by_id_;
|
|
std::set<uint32_t> enabled_;
|
|
std::set<uint32_t> active_;
|
|
};
|
|
|
|
} // namespace secsgem::gem
|