feat: EquipmentRuntime engine owner + secs_gemd gRPC daemon
Extract the SECS/GEM engine wiring out of the secs_server app into a reusable class, and stand up a language-agnostic gRPC daemon on top so a tool's software (any language) can drive the equipment without linking C++ or knowing SEMI. Foundation for replacing a vendor's SECS/GEM server. Engine reuse: - EquipmentRuntime (include/secsgem/gem/runtime.hpp, src/gem/runtime.cpp): owns io_context, passive Server, model, control-state machine, Router; thread-safe outbound API (set_variable/emit_event/set_alarm/clear_alarm), on_command hook, deliver_or_spool, run()/run_async()/poll()/stop(). - register_default_handlers (src/gem/default_handlers.cpp): the 56 GEM handlers + domain emitters, relocated from secs_server so the app and the daemon speak byte-identical GEM. secs_server.cpp reduced ~1270 -> 113 lines. - name_index.hpp: resolve_variable(name) -> VID (the name->id binding layer). Daemon (apps/secs_gemd.cpp, proto/secsgem/v1/equipment.proto): - runs the engine + HSMS link on a background thread; serves the gRPC Equipment service. Increment 1: SetVariables (name-resolved, plain value->Item) and GetControlState. proto carries the full v1 surface (universal + carrier/recipe/job tiers); remaining RPCs + the Subscribe command stream are next (docs/DAEMON_ROADMAP.md). - CMake: opt-in SECSGEM_DAEMON, protoc/grpc_cpp_plugin codegen, gracefully skipped where protobuf/grpc++ are absent. Dockerfile gains the grpc deps. Tests (proof): test_runtime, test_default_handlers (S1F1->S1F2, S2F41->hook), test_name_index. Full suite 458/458, 2795 assertions; live server<->client GEM300 demo still passes on the refactored server. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,128 @@
|
||||
// secs_gemd — the SECS/GEM daemon.
|
||||
//
|
||||
// Runs the GEM engine (EquipmentRuntime + the default handlers) on a background
|
||||
// thread, owning the HSMS link to the host, and exposes a small name-based gRPC
|
||||
// API (proto/secsgem/v1/equipment.proto) so a tool's software — in any language
|
||||
// — can drive the equipment without linking C++ or knowing SEMI.
|
||||
//
|
||||
// This is increment 1: the universal "report state to the host" RPCs plus
|
||||
// control-state visibility. Events, alarms, and the host->tool Subscribe stream
|
||||
// follow (see docs/DAEMON_ROADMAP.md).
|
||||
|
||||
#include <grpcpp/grpcpp.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <iostream>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
#include "secsgem/gem/default_handlers.hpp"
|
||||
#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 gem = secsgem::gem;
|
||||
namespace s2 = secsgem::secs2;
|
||||
namespace pb = secsgem::v1;
|
||||
|
||||
namespace {
|
||||
|
||||
// proto Value -> SECS-II Item. Increment 1 uses a direct mapping; format-aware
|
||||
// conversion (honouring the variable's declared wire format) is a refinement.
|
||||
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("");
|
||||
}
|
||||
}
|
||||
|
||||
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 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_;
|
||||
};
|
||||
|
||||
std::string arg(int argc, char** argv, const std::string& key, const std::string& def) {
|
||||
for (int i = 1; i + 1 < argc; ++i)
|
||||
if (key == argv[i]) return argv[i + 1];
|
||||
return def;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
const std::string hsms_port = arg(argc, argv, "--port", "5000");
|
||||
const std::string grpc_addr = arg(argc, argv, "--grpc", "0.0.0.0:50051");
|
||||
const std::string cfgdir = arg(argc, argv, "--config-dir", "data");
|
||||
|
||||
gem::EquipmentRuntime::Config cfg;
|
||||
cfg.equipment_yaml = cfgdir + "/equipment.yaml";
|
||||
cfg.control_state_yaml = cfgdir + "/control_state.yaml";
|
||||
cfg.process_job_yaml = cfgdir + "/process_job_state.yaml";
|
||||
cfg.control_job_yaml = cfgdir + "/control_job_state.yaml";
|
||||
cfg.port = static_cast<uint16_t>(std::stoi(hsms_port));
|
||||
cfg.log = [](const std::string& m) { std::cout << "[gemd] " << m << std::endl; };
|
||||
|
||||
std::unique_ptr<gem::EquipmentRuntime> rt;
|
||||
try {
|
||||
rt = std::make_unique<gem::EquipmentRuntime>(cfg);
|
||||
} catch (const std::exception& e) {
|
||||
std::cerr << "[gemd] config error: " << e.what() << std::endl;
|
||||
return 1;
|
||||
}
|
||||
gem::register_default_handlers(*rt);
|
||||
rt->run_async(); // engine + HSMS link on a background thread
|
||||
|
||||
EquipmentService service(*rt);
|
||||
grpc::ServerBuilder builder;
|
||||
builder.AddListeningPort(grpc_addr, grpc::InsecureServerCredentials());
|
||||
builder.RegisterService(&service);
|
||||
std::unique_ptr<grpc::Server> server(builder.BuildAndStart());
|
||||
if (!server) {
|
||||
std::cerr << "[gemd] failed to start gRPC server on " << grpc_addr << std::endl;
|
||||
return 1;
|
||||
}
|
||||
std::cout << "[gemd] gRPC on " << grpc_addr
|
||||
<< "; HSMS equipment on :" << hsms_port << std::endl;
|
||||
server->Wait();
|
||||
return 0;
|
||||
}
|
||||
Reference in New Issue
Block a user