feat(daemon): FireEvent + event name resolution + in-process gRPC tests

- name_index: add resolve_event(name) -> CEID (unit-tested).
- equipment_service.hpp: extract the gRPC service + value/state conversion
  into a shared header; add FireEvent (optional per-fire variable values,
  then trigger the collection event by name). secs_gemd slims to main().
- test_daemon_service: real in-process gRPC integration test (client stub ->
  service -> EquipmentRuntime) proving SetVariables lands in the model,
  GetControlState reports the state, FireEvent and unknown-name paths behave.
  Separate secs_gemd_tests target (links grpc++/proto), gated on the daemon.

Core suite 459/459 (2799 assertions); daemon gRPC tests 15/15.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-10 18:07:12 +02:00
parent fc898f8410
commit cb85199f49
6 changed files with 230 additions and 64 deletions
+99
View File
@@ -0,0 +1,99 @@
#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.
#include <grpcpp/grpcpp.h>
#include <cstdint>
#include <string>
#include "secsgem/gem/name_index.hpp"
#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. Increment 1 uses a direct mapping; format-aware
// conversion (honouring the variable's declared wire format) is a refinement.
inline s2::Item to_item(const pb::Value& v) {
switch (v.kind_case()) {
case pb::Value::kText: return s2::Item::ascii(v.text());
case pb::Value::kInteger: return s2::Item::i8(static_cast<int64_t>(v.integer()));
case pb::Value::kReal: return s2::Item::f8(v.real());
case pb::Value::kBoolean: return s2::Item::boolean(v.boolean());
default: return s2::Item::ascii("");
}
}
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:
explicit EquipmentService(gem::EquipmentRuntime& rt) : rt_(rt) {}
grpc::Status SetVariables(grpc::ServerContext*, const pb::VariableUpdate* req,
pb::Ack* resp) override {
for (const auto& kv : req->values()) {
auto vid = gem::resolve_variable(rt_.model(), kv.first);
if (!vid) {
resp->set_code(pb::Ack::PARAMETER_INVALID);
resp->set_message("no variable named '" + kv.first + "'");
return grpc::Status::OK;
}
rt_.set_variable(*vid, to_item(kv.second));
}
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 vid = gem::resolve_variable(rt_.model(), kv.first);
if (!vid) {
resp->set_code(pb::Ack::PARAMETER_INVALID);
resp->set_message("no variable named '" + kv.first + "'");
return grpc::Status::OK;
}
rt_.set_variable(*vid, to_item(kv.second));
}
auto ceid = gem::resolve_event(rt_.model(), req->name());
if (!ceid) {
resp->set_code(pb::Ack::PARAMETER_INVALID);
resp->set_message("no event named '" + req->name() + "'");
return grpc::Status::OK;
}
rt_.emit_event(*ceid);
resp->set_code(pb::Ack::ACCEPT);
return grpc::Status::OK;
}
grpc::Status GetControlState(grpc::ServerContext*, const pb::Empty*,
pb::ControlState* resp) override {
resp->set_state(to_proto_state(rt_.control_state()));
return grpc::Status::OK;
}
private:
gem::EquipmentRuntime& rt_;
};
} // namespace secsgem::daemon