29#include <netinet/in.h>
31#include <sys/socket.h>
56 std::cout <<
"\n[Sequencer] Shutting down..." << std::endl;
60std::string
trim(
const std::string& s) {
61 size_t a = s.find_first_not_of(
" \t\r\n");
62 size_t b = s.find_last_not_of(
" \t\r\n");
63 return (
a == std::string::npos) ?
"" : s.substr(
a,
b -
a + 1);
74 auto sendReply = [&](
const std::string& msg) {
75 send(client_fd, msg.c_str(), msg.size(), MSG_NOSIGNAL);
78 const std::string cmd =
trim(raw);
83 if (cmd.compare(0, 11,
"TRANSITION:") == 0) {
84 const std::string rest =
trim(cmd.substr(11));
85 std::string state_name = rest;
94 const size_t last_colon = rest.rfind(
':');
95 if (last_colon != std::string::npos && last_colon > 0) {
99 state_name =
trim(rest.substr(0, last_colon));
100 }
else if (
trim(rest.substr(last_colon + 1)).find_first_not_of(
"0123456789") ==
104 sendReply(
"ERR:bad hold duration\n");
110 if (state_name.empty()) {
111 sendReply(
"ERR:empty state name\n");
112 }
else if (svc.
transitionTo(state_name, hold_ms, &why)) {
115 sendReply(hold_ms ?
"OK:" + std::to_string(hold_ms) +
"\n" : std::string(
"OK\n"));
117 sendReply(
"ERR:" + (why.empty() ? std::string(
"transition rejected") : why) +
"\n");
121 }
else if (cmd.compare(0, 9,
"ACTUATOR:") == 0) {
122 const std::string rest =
trim(cmd.substr(9));
123 const size_t last_colon = rest.rfind(
':');
124 if (last_colon == std::string::npos || last_colon == 0) {
125 sendReply(
"ERR:bad ACTUATOR format\n");
128 const std::string role_name =
trim(rest.substr(0, last_colon));
129 const std::string val_str =
trim(rest.substr(last_colon + 1));
131 if (val_str ==
"1" || val_str ==
"open")
133 else if (val_str ==
"0" || val_str ==
"closed")
135 if (pos < 0 || role_name.empty()) {
136 sendReply(
"ERR:bad ACTUATOR value\n");
142 sendReply(
"ERR:" + (why.empty() ? std::string(
"actuator command failed") : why) +
147 }
else if (cmd.compare(0, 11,
"DEBUG_MODE:") == 0) {
148 const std::string val =
trim(cmd.substr(11));
149 if (val ==
"1" || val ==
"true") {
152 }
else if (val ==
"0" || val ==
"false") {
156 sendReply(
"ERR:bad DEBUG_MODE value\n");
160 }
else if (cmd ==
"EXTEND_FIRE") {
164 sendReply(
"ERR:not in FIRE state\n");
173 }
else if (cmd ==
"SCRIPTS") {
177 sendReply(
"ERR:unknown command\n");
199 struct timeval tv{.tv_sec = 5, .tv_usec = 0};
200 setsockopt(client_fd, SOL_SOCKET, SO_RCVTIMEO, &tv,
sizeof(tv));
205 ssize_t
n = recv(client_fd, tmp,
sizeof(tmp), 0);
208 buf.append(tmp,
static_cast<size_t>(
n));
211 while ((nl = buf.find(
'\n')) != std::string::npos) {
212 const std::string line = buf.substr(0, nl);
213 buf.erase(0, nl + 1);
217 std::cerr <<
"[Sequencer] oversized command — dropping connection" << std::endl;
225int main(
int argc,
char* argv[]) {
226 std::string config_path =
"config/config.toml";
227 uint16_t listen_port = 9998;
229 for (
int i = 1; i < argc; ++i) {
230 std::string arg = argv[i];
231 if (arg ==
"--config" && i + 1 < argc) {
232 config_path = argv[++i];
233 }
else if (arg ==
"--port" && i + 1 < argc) {
234 listen_port =
static_cast<uint16_t
>(std::atoi(argv[++i]));
235 }
else if (arg ==
"--help" || arg ==
"-h") {
236 std::cout <<
"Usage: " << argv[0] <<
" [--config PATH] [--port PORT]\n";
243 signal(SIGPIPE, SIG_IGN);
246 if (!svc.
init(config_path)) {
247 std::cerr <<
"[Sequencer] Initialization failed" << std::endl;
252 int listen_fd = socket(AF_INET, SOCK_STREAM, 0);
254 std::cerr <<
"[Sequencer] socket() failed" << std::endl;
259 setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &opt,
sizeof(opt));
262 struct sockaddr_in addr{};
263 addr.sin_family = AF_INET;
264 addr.sin_addr.s_addr = INADDR_ANY;
265 addr.sin_port = htons(listen_port);
266 if (bind(listen_fd,
reinterpret_cast<struct sockaddr*
>(&addr),
sizeof(addr)) < 0) {
267 std::cerr <<
"[Sequencer] bind() failed on port " << listen_port << std::endl;
272 if (listen(listen_fd, 10) < 0) {
273 std::cerr <<
"[Sequencer] listen() failed" << std::endl;
278 std::cout <<
"[Sequencer] Listening on port " << listen_port << std::endl;
279 std::cout <<
"[Sequencer] Commands: TRANSITION:<state>[:<ms>] | ACTUATOR:<name>:<0|1> "
280 <<
"| DEBUG_MODE:<0|1> | EXTEND_FIRE" << std::endl;
287 std::atomic<bool> done{
false};
289 std::vector<std::unique_ptr<Conn>> conns;
291 auto reap_finished = [&conns]() {
292 for (
auto it = conns.begin(); it != conns.end();) {
293 if ((*it)->done.load()) {
294 if ((*it)->th.joinable())
296 it = conns.erase(it);
309 FD_SET(listen_fd, &rd);
310 struct timeval tv{.tv_sec = 0, .tv_usec = 10000};
311 if (select(listen_fd + 1, &rd,
nullptr,
nullptr, &tv) <= 0)
314 int client_fd = accept(listen_fd,
nullptr,
nullptr);
325 static const char kBusy[] =
"ERR:too many connections\n";
326 send(client_fd, kBusy,
sizeof(kBusy) - 1, MSG_NOSIGNAL);
327 std::cerr <<
"[Sequencer] client limit (" <<
kMaxClients
328 <<
") reached — refusing connection" << std::endl;
333 auto conn = std::make_unique<Conn>();
334 conn->fd = client_fd;
335 Conn* raw = conn.get();
336 conn->th = std::thread([raw, &svc]() {
340 conns.push_back(std::move(conn));
345 for (
const auto& c : conns)
346 shutdown(c->fd, SHUT_RDWR);
347 for (
const auto& c : conns)
348 if (c->th.joinable())
352 std::cout <<
"[Sequencer] Stopped." << std::endl;
static void signalHandler(int)
Top-level sequencer service.
bool setDebugMode(bool enabled)
Enable or disable debug mode.
bool init(const std::string &config_path="config/config.toml")
Load config, CSVs, connect to Elodin.
bool transitionTo(const std::string &state_name)
Request a state transition by name (e.g.
std::string scriptStatusReport() const
What this process made of its dynamic states at startup, as newline-terminated text.
bool manualActuator(const std::string &name, int pos, std::string *refusal_reason=nullptr)
Debug mode only: manual OPEN/CLOSE for one role.
bool extendFire()
Extend the FIRE window (only valid while in FIRE state).
static std::atomic< bool > g_running
static void send(void *ctx, const uint8_t *bytes, uint16_t len)
*brief create JSON pointer *sa reference_tokens const string_t const string_t & b
*brief create JSON pointer *sa reference_tokens const string_t & a
std::string trim(const std::string &s)
constexpr size_t kMaxClients
void handleClient(int client_fd, sequencer::SequencerService &svc)
One connection, on its own thread: read lines, dispatch, reply, until the peer closes.
std::atomic< bool > g_running
void handleCommandLine(int client_fd, const std::string &raw, sequencer::SequencerService &svc)
Execute one newline-terminated command and write its reply.
constexpr size_t kMaxLineBytes
*brief user defined string literal for JSON values *sa std::size_t n
bool parseHoldMs(const std::string &text, uint32_t &out)
Parse a hold duration off the wire, strictly.