#pragma once // The gRPC Equipment service: translates proto/secsgem/v1 RPCs onto an // EquipmentRuntime. Header-only so both secs_gemd and the daemon tests share // one definition. // // Threading: gRPC handlers run on gRPC's own threads while the engine's // io thread owns the data model, so handlers must not read the live stores. // The constructor therefore snapshots the name->id/format maps (immutable // after config load) — construct the service BEFORE run_async(). All writes // go through the runtime's posting API. #include #include #include #include #include "secsgem/gem/runtime.hpp" #include "secsgem/secs2/item.hpp" #include "secsgem/v1/equipment.grpc.pb.h" #include "secsgem/v1/equipment.pb.h" namespace secsgem::daemon { namespace gem = secsgem::gem; namespace s2 = secsgem::secs2; namespace pb = secsgem::v1; // proto Value -> SECS-II Item, honouring the variable's declared wire format // (from equipment.yaml) so the host sees the same format in S1F11 namelists // and in reported values. E.g. real 2.5 -> F4 if the variable is F4. inline s2::Item to_item(const pb::Value& v, s2::Format want) { using F = s2::Format; switch (v.kind_case()) { case pb::Value::kText: return s2::Item::ascii(v.text()); case pb::Value::kBoolean: return s2::Item::boolean(v.boolean()); case pb::Value::kBinary: return s2::Item::binary({v.binary().begin(), v.binary().end()}); case pb::Value::kReal: return want == F::F4 ? s2::Item::f4(static_cast(v.real())) : s2::Item::f8(v.real()); case pb::Value::kInteger: { const int64_t n = v.integer(); switch (want) { case F::U1: return s2::Item::u1(static_cast(n)); case F::U2: return s2::Item::u2(static_cast(n)); case F::U4: return s2::Item::u4(static_cast(n)); case F::U8: return s2::Item::u8(static_cast(n)); case F::I1: return s2::Item::i1(static_cast(n)); case F::I2: return s2::Item::i2(static_cast(n)); case F::I4: return s2::Item::i4(static_cast(n)); case F::F4: return s2::Item::f4(static_cast(n)); case F::F8: return s2::Item::f8(static_cast(n)); case F::Boolean: return s2::Item::boolean(n != 0); default: return s2::Item::i8(n); } } case pb::Value::kList: { // TODO(daemon): list elements inherit the variable's scalar format; // honouring per-element formats needs the declared Item's nested shape. s2::Item::List items; for (const auto& e : v.list().items()) items.push_back(to_item(e, want)); return s2::Item::list(std::move(items)); } default: // Unreachable: callers reject KIND_NOT_SET before converting (see // value_is_set). Kept as a safe fallback for future oneof additions. return s2::Item::ascii(""); } } // Validate a client-supplied Value before conversion. An unset oneof would // otherwise silently become ASCII "" — reject it at the API edge instead. inline bool value_is_set(const pb::Value& v) { return v.kind_case() != pb::Value::KIND_NOT_SET; } inline pb::ControlState::State to_proto_state(gem::ControlState s) { switch (s) { case gem::ControlState::EquipmentOffline: return pb::ControlState::EQUIPMENT_OFFLINE; case gem::ControlState::AttemptOnline: return pb::ControlState::ATTEMPT_ONLINE; case gem::ControlState::HostOffline: return pb::ControlState::HOST_OFFLINE; case gem::ControlState::OnlineLocal: return pb::ControlState::ONLINE_LOCAL; case gem::ControlState::OnlineRemote: return pb::ControlState::ONLINE_REMOTE; } return pb::ControlState::EQUIPMENT_OFFLINE; } class EquipmentService final : public pb::Equipment::Service { public: // Snapshots the (immutable) name->id/format dictionaries. Construct before // run_async() so the model is read while the io thread isn't running yet. explicit EquipmentService(gem::EquipmentRuntime& rt) : rt_(rt) { for (const auto& sv : rt.model().svids.all()) vars_.insert({sv.name, {sv.id, sv.value.format()}}); for (const auto& dv : rt.model().dvids.all()) vars_.insert({dv.name, {dv.id, dv.value.format()}}); for (const auto& ev : rt.model().events.all_events()) events_.insert({ev.name, ev.id}); } grpc::Status SetVariables(grpc::ServerContext*, const pb::VariableUpdate* req, pb::Ack* resp) override { for (const auto& kv : req->values()) { auto it = vars_.find(kv.first); if (it == vars_.end()) { resp->set_code(pb::Ack::PARAMETER_INVALID); resp->set_message("no variable named '" + kv.first + "'"); return grpc::Status::OK; } if (!value_is_set(kv.second)) { resp->set_code(pb::Ack::PARAMETER_INVALID); resp->set_message("value for '" + kv.first + "' has no kind set"); return grpc::Status::OK; } rt_.set_variable(it->second.vid, to_item(kv.second, it->second.format)); } resp->set_code(pb::Ack::ACCEPT); return grpc::Status::OK; } grpc::Status FireEvent(grpc::ServerContext*, const pb::Event* req, pb::Ack* resp) override { // Optional per-fire variable values, then trigger the collection event. for (const auto& kv : req->data()) { auto it = vars_.find(kv.first); if (it == vars_.end()) { resp->set_code(pb::Ack::PARAMETER_INVALID); resp->set_message("no variable named '" + kv.first + "'"); return grpc::Status::OK; } if (!value_is_set(kv.second)) { resp->set_code(pb::Ack::PARAMETER_INVALID); resp->set_message("value for '" + kv.first + "' has no kind set"); return grpc::Status::OK; } rt_.set_variable(it->second.vid, to_item(kv.second, it->second.format)); } auto ev = events_.find(req->name()); if (ev == events_.end()) { resp->set_code(pb::Ack::PARAMETER_INVALID); resp->set_message("no event named '" + req->name() + "'"); return grpc::Status::OK; } rt_.emit_event(ev->second); resp->set_code(pb::Ack::ACCEPT); return grpc::Status::OK; } grpc::Status GetControlState(grpc::ServerContext*, const pb::Empty*, pb::ControlState* resp) override { // Thread-safe: control_state() reads the runtime's atomic mirror. resp->set_state(to_proto_state(rt_.control_state())); return grpc::Status::OK; } private: struct VarRef { uint32_t vid; s2::Format format; // declared wire format from equipment.yaml }; gem::EquipmentRuntime& rt_; std::map vars_; // SVIDs + DVIDs (SVIDs win on clash) std::map events_; // CEID by name }; } // namespace secsgem::daemon