fc898f8410
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>
114 lines
4.4 KiB
C++
114 lines
4.4 KiB
C++
// Passive SECS/GEM equipment. Capabilities (SVIDs, ECIDs, CEIDs, alarms,
|
|
// recipes, host commands) come from data/equipment.yaml; the E30 control
|
|
// state machine comes from data/control_state.yaml. Dispatch is a Router
|
|
// table. No imperative if-ladder; no in-code device data dictionary.
|
|
|
|
#include <asio.hpp>
|
|
#include <chrono>
|
|
#include <cstdint>
|
|
#include <iostream>
|
|
#include <memory>
|
|
#include <optional>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
#include "secsgem/config/loader.hpp"
|
|
#include "secsgem/config/validate.hpp"
|
|
#include "secsgem/endpoint.hpp"
|
|
#include "secsgem/gem/control_state.hpp"
|
|
#include "secsgem/gem/data_model.hpp"
|
|
#include "secsgem/gem/e116_constants.hpp"
|
|
#include "secsgem/gem/e157_constants.hpp"
|
|
#include "secsgem/gem/e90_constants.hpp"
|
|
#include "secsgem/gem/messages.hpp"
|
|
#include "secsgem/gem/router.hpp"
|
|
#include "secsgem/gem/runtime.hpp"
|
|
#include "secsgem/gem/default_handlers.hpp"
|
|
#include "secsgem/secs2/message.hpp"
|
|
|
|
using namespace secsgem;
|
|
namespace s2 = secsgem::secs2;
|
|
namespace gem = secsgem::gem;
|
|
|
|
namespace {
|
|
|
|
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;
|
|
}
|
|
|
|
bool has_flag(int argc, char** argv, const std::string& key) {
|
|
for (int i = 1; i < argc; ++i)
|
|
if (key == argv[i]) return true;
|
|
return false;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
int main(int argc, char** argv) {
|
|
const auto port = static_cast<uint16_t>(std::stoi(arg(argc, argv, "--port", "5000")));
|
|
// Defaults are relative to the current working directory so the
|
|
// binary works both inside the docker image (WORKDIR=/app) and on a
|
|
// host build run from the repo root. Pass explicit --config etc.
|
|
// when running from anywhere else.
|
|
const auto equipment_yaml = arg(argc, argv, "--config", "data/equipment.yaml");
|
|
const auto state_yaml = arg(argc, argv, "--state-table", "data/control_state.yaml");
|
|
const auto pj_state_yaml = arg(argc, argv, "--pj-state-table",
|
|
"data/process_job_state.yaml");
|
|
const auto cj_state_yaml = arg(argc, argv, "--cj-state-table",
|
|
"data/control_job_state.yaml");
|
|
const auto spool_dir = arg(argc, argv, "--spool-dir", "");
|
|
const bool validate_only = has_flag(argc, argv, "--validate-config");
|
|
|
|
auto logfn = [](const std::string& m) { std::cout << "[equip] " << m << std::endl; };
|
|
|
|
// --validate-config: read every YAML, accumulate every issue we can
|
|
// find, print to stderr, and exit 0/1. Does NOT bind the port — this
|
|
// is the day-1 friction killer the README points customers at.
|
|
if (validate_only) {
|
|
config::ConfigValidator v;
|
|
v.validate_equipment(equipment_yaml);
|
|
v.validate_control_state(state_yaml);
|
|
v.validate_process_job_state(pj_state_yaml);
|
|
v.validate_control_job_state(cj_state_yaml);
|
|
config::format_issues_to(std::cerr, v.issues());
|
|
std::cerr << v.error_count() << " error(s), "
|
|
<< v.warning_count() << " warning(s) across 4 files\n";
|
|
return v.has_errors() ? 1 : 0;
|
|
}
|
|
|
|
// The engine — io_context, passive Server, model, control-state machine,
|
|
// Router, emit/spool plumbing — now lives in EquipmentRuntime. Construct it
|
|
// from the YAML config; the handler-registration block below wires GEM
|
|
// behaviour onto it through the aliases that follow.
|
|
gem::EquipmentRuntime::Config rt_cfg;
|
|
rt_cfg.equipment_yaml = equipment_yaml;
|
|
rt_cfg.control_state_yaml = state_yaml;
|
|
rt_cfg.process_job_yaml = pj_state_yaml;
|
|
rt_cfg.control_job_yaml = cj_state_yaml;
|
|
rt_cfg.port = port;
|
|
rt_cfg.spool_dir = spool_dir;
|
|
rt_cfg.log = logfn;
|
|
|
|
std::unique_ptr<gem::EquipmentRuntime> runtime;
|
|
try {
|
|
runtime = std::make_unique<gem::EquipmentRuntime>(rt_cfg);
|
|
} catch (const std::exception& e) {
|
|
std::cerr << "[equip] config error: " << e.what() << std::endl;
|
|
return 1;
|
|
}
|
|
auto& R = *runtime;
|
|
logfn("loaded " + std::to_string(R.model().svids.size()) + " SVIDs, " +
|
|
std::to_string(R.model().ecids.all().size()) + " ECIDs, " +
|
|
std::to_string(R.model().events.all_events().size()) + " CEIDs, " +
|
|
std::to_string(R.model().alarms.all().size()) + " alarms");
|
|
|
|
gem::register_default_handlers(R);
|
|
|
|
// Accepting connections, the SELECTED spool-notify handler, and the
|
|
// S9-on-unhandled message dispatch are all wired by the runtime; run it.
|
|
R.run();
|
|
return 0;
|
|
}
|