77 struct timeval tv{.tv_sec = 5, .tv_usec = 0};
78 setsockopt(client_fd, SOL_SOCKET, SO_RCVTIMEO, &tv,
sizeof(tv));
81 bool got_line =
false;
84 ssize_t
n = ::recv(client_fd, tmp,
sizeof(tmp), 0);
87 buf.append(tmp,
static_cast<size_t>(
n));
88 const size_t nl = buf.find(
'\n');
89 if (nl != std::string::npos) {
96 auto reply = [client_fd](
const char* s) {
97 ::send(client_fd, s, std::strlen(s), MSG_NOSIGNAL);
103 std::cerr <<
"[ControllerService] ⚠️ control connection closed with no command "
104 "(timeout or peer went away)"
106 }
else if (!svc->
isFireGateEnabled() && (buf ==
"FIRE_START" || buf ==
"FIRE_STOP")) {
109 std::cerr <<
"[ControllerService] ⚠️ " << buf
110 <<
" refused — fire gate disabled (sequencer owns the valves)" << std::endl;
111 reply(
"ERR:fire gate disabled (sequencer owns the valves)\n");
112 }
else if (buf ==
"FIRE_START") {
114 std::cout <<
"[ControllerService] 🔥 FIRE_START received — PWM gate open" << std::endl;
116 }
else if (buf ==
"FIRE_STOP") {
118 std::cout <<
"[ControllerService] 🛑 FIRE_STOP received — PWM gate closed" << std::endl;
121 std::cerr <<
"[ControllerService] ⚠️ Unknown control cmd: \"" << buf <<
"\"" << std::endl;
128 int listen_fd = ::socket(AF_INET, SOCK_STREAM, 0);
130 std::cerr <<
"[ControllerService] ❌ Control socket failed: " << strerror(errno)
135 setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &opt,
sizeof(opt));
137 struct sockaddr_in addr{};
138 addr.sin_family = AF_INET;
139 addr.sin_addr.s_addr = INADDR_ANY;
140 addr.sin_port = htons(
port);
141 if (bind(listen_fd,
reinterpret_cast<struct sockaddr*
>(&addr),
sizeof(addr)) < 0) {
142 std::cerr <<
"[ControllerService] ❌ Control bind(" <<
port
143 <<
") failed: " << strerror(errno) << std::endl;
147 ::listen(listen_fd, 4);
148 std::cout <<
"[ControllerService] 🎮 Control server on TCP :" <<
port
149 <<
" (FIRE_START | FIRE_STOP)" << std::endl;
155 std::atomic<bool> done{
false};
157 std::vector<std::unique_ptr<Conn>> conns;
159 auto reap_finished = [&conns]() {
160 for (
auto it = conns.begin(); it != conns.end();) {
161 if ((*it)->done.load()) {
162 if ((*it)->th.joinable())
164 it = conns.erase(it);
174 struct timeval tv{0, 10000};
177 FD_SET(listen_fd, &fds);
178 if (select(listen_fd + 1, &fds,
nullptr,
nullptr, &tv) <= 0)
181 int client = ::accept(listen_fd,
nullptr,
nullptr);
191 static const char kBusy[] =
"ERR:too many connections\n";
192 ::send(client, kBusy,
sizeof(kBusy) - 1, MSG_NOSIGNAL);
194 <<
") reached — refusing connection" << std::endl;
199 auto conn = std::make_unique<Conn>();
201 Conn* raw = conn.get();
202 conn->th = std::thread([raw, svc]() {
206 conns.push_back(std::move(conn));
210 for (
const auto& c : conns)
211 ::shutdown(c->fd, SHUT_RDWR);
212 for (
const auto& c : conns)
213 if (c->th.joinable())
223int main(
int argc,
char* argv[]) {
225 std::string config_path =
"../../config/config.toml";
226 std::string elodin_host =
"";
227 uint16_t elodin_port = 0;
228 uint16_t control_port = 0;
229 double thrust_desired = 1000.0;
230 bool elodin_host_from_cli =
false;
231 bool elodin_port_from_cli =
false;
234 bool use_pressure_control =
false;
235 double p_fuel_target_psi = 0.0;
236 double p_ox_target_psi = 0.0;
237 std::string lut_path_cli;
239 for (
int i = 1; i < argc; ++i) {
240 std::string arg = argv[i];
241 if (arg ==
"--config" && i + 1 < argc) {
242 config_path = argv[++i];
243 }
else if (arg ==
"--elodin-host" && i + 1 < argc) {
244 elodin_host = argv[++i];
245 elodin_host_from_cli =
true;
246 }
else if (arg ==
"--elodin-port" && i + 1 < argc) {
247 elodin_port =
static_cast<uint16_t
>(std::atoi(argv[++i]));
248 elodin_port_from_cli =
true;
249 }
else if (arg ==
"--control-port" && i + 1 < argc) {
250 control_port =
static_cast<uint16_t
>(std::atoi(argv[++i]));
251 }
else if (arg ==
"--thrust" && i + 1 < argc) {
252 thrust_desired = std::atof(argv[++i]);
253 }
else if (arg ==
"--p-fuel" && i + 1 < argc) {
254 p_fuel_target_psi = std::atof(argv[++i]);
255 use_pressure_control =
true;
256 }
else if (arg ==
"--p-ox" && i + 1 < argc) {
257 p_ox_target_psi = std::atof(argv[++i]);
258 use_pressure_control =
true;
259 }
else if (arg ==
"--lut-path" && i + 1 < argc) {
260 lut_path_cli = argv[++i];
261 }
else if (arg ==
"--help" || arg ==
"-h") {
263 <<
"Usage: " << argv[0] <<
" [OPTIONS]\n"
264 <<
" --config PATH config.toml path (default: ../../config/config.toml)\n"
265 <<
" --elodin-host HOST Elodin DB host (empty = no DB)\n"
266 <<
" --elodin-port PORT Elodin DB port (default: 2240)\n"
267 <<
" --thrust N Thrust demand [N] (default: 1000)\n"
268 <<
" --p-fuel PSI Fuel tank pressure target [psi]\n"
269 <<
" --p-ox PSI Ox tank pressure target [psi]\n"
270 <<
" --lut-path PATH LUT binary for boolean control (bypasses DDP)\n";
276 std::string config_content;
278 std::ifstream f(config_path);
280 std::cerr <<
"❌ Cannot open config: " << config_path << std::endl;
282 for (
const auto& fallback :
283 {
"config/config.toml",
"../config/config.toml",
"../../config/config.toml"}) {
286 config_path = fallback;
287 std::cout <<
"ℹ️ Using fallback config: " << fallback << std::endl;
292 std::cerr <<
"❌ No config.toml found. Using defaults." << std::endl;
296 std::ostringstream ss;
298 config_content = ss.str();
299 std::cout <<
"📋 Loaded config: " << config_path << std::endl;
321 const std::vector<std::string>& unresolved = pwm_resolution.issues;
322 pwm.
fuel = pwm_resolution.fuel;
323 pwm.
ox = pwm_resolution.ox;
335 pwm.
frequency_hz =
static_cast<float>(cfg.controller.pwm_frequency_hz);
337 double loop_hz = cfg.controller.controller_loop_hz;
340 if (!elodin_host_from_cli)
341 elodin_host = cfg.database.host;
342 if (!elodin_port_from_cli)
343 elodin_port = cfg.database.port;
344 if (elodin_host.empty())
345 elodin_host =
"127.0.0.1";
346 if (elodin_port == 0)
351 if (control_port == 0)
352 control_port = cfg.controller_service.port;
358 double pmin = cfg.controller.P_copv_min_pa;
361 std::cout <<
" P_copv_min: disabled (0)" << std::endl;
363 std::cout <<
" P_copv_min: " << pmin <<
" Pa (" << (pmin / 6894.76) <<
" psi)"
367 std::cout <<
"\n═══════════════════════════════════════════════════════════" << std::endl;
368 std::cout <<
" Robust DDP Controller Service" << std::endl;
369 std::cout <<
"═══════════════════════════════════════════════════════════" << std::endl;
374 return std::string(
"(unassigned)");
375 return "\"" + t.actuator_name +
"\" → " + t.board_ip +
" CH" +
376 std::to_string((
int)t.channel);
378 std::cout <<
" Actuator port: " << pwm.
actuator_port << std::endl;
379 std::cout <<
" PWM fuel: " << describe_target(pwm.
fuel) << std::endl;
380 std::cout <<
" PWM ox: " << describe_target(pwm.
ox) << std::endl;
381 std::cout <<
" PWM frequency: " << pwm.
frequency_hz <<
" Hz" << std::endl;
382 std::cout <<
" PWM duration: " << pwm.
duration_ms <<
" ms" << std::endl;
383 std::cout <<
" Control loop: " << loop_hz <<
" Hz" << std::endl;
384 std::cout <<
" Thrust demand: " << thrust_desired <<
" N" << std::endl;
385 if (use_pressure_control) {
386 std::cout <<
" Control Mode: PRESSURE TARGET" << std::endl;
387 std::cout <<
" Fuel Target: " << p_fuel_target_psi <<
" psi" << std::endl;
388 std::cout <<
" Ox Target: " << p_ox_target_psi <<
" psi" << std::endl;
390 std::cout <<
" Control Mode: THRUST DESIRED" << std::endl;
392 std::cout <<
" Elodin DB: " << (elodin_host.empty() ?
"(disabled)" : elodin_host)
394 std::cout <<
"═══════════════════════════════════════════════════════════\n" << std::endl;
402 const std::string fire_state = cfg.fire.state;
403 if (fire_state.empty()) {
407 std::cout <<
" Fire state: (none) — PWM fire gate disabled" << std::endl;
412 std::cout <<
" Fire state: " << fire_state <<
" (id " <<
static_cast<int>(id)
419 std::cerr <<
" ⚠️ [fire] state \"" << fire_state
420 <<
"\" is not a declared state — PWM fire gate DISABLED" << std::endl;
430 if (cfg.controller_service.sequencer_owns_valves) {
433 std::cout <<
" Valve owner: sequencer — PWM gate DISABLED, FIRE_START will be refused"
435 }
else if (!unresolved.empty()) {
443 for (
const auto& why : unresolved)
444 std::cerr <<
" ⚠️ [controller] " << why << std::endl;
445 std::cerr <<
" ⚠️ [controller] PWM fire gate DISABLED. Assign exactly one "
446 "[actuator_roles] entry \"pwm_fuel\" and one \"pwm_ox\" via the optional "
447 "4th element, e.g. \"Fuel Press\" = [\"NC\", 3, 12, \"pwm_fuel\"]."
450 std::cout <<
" Valve owner: controller_service (PWM gate armed)" << std::endl;
453 std::string lut_path_raw = !lut_path_cli.empty() ? lut_path_cli : cfg.controller.lut_path;
454 std::string thrust_curve_path_raw = cfg.controller.thrust_curve_path;
456 std::string thrust_curve_path =
resolveConfigPath(config_path, thrust_curve_path_raw);
457 if (!lut_path.empty())
458 std::cout <<
" LUT path: " << lut_path <<
" (boolean control)" << std::endl;
459 if (!thrust_curve_path.empty())
460 std::cout <<
" Thrust curve: " << thrust_curve_path << std::endl;
462 if (!service.
initialize(pwm, ctrl_cfg, elodin_host, elodin_port, lut_path, thrust_curve_path)) {
463 std::cerr <<
"❌ Failed to initialize controller service" << std::endl;
469 if (use_pressure_control) {
483 float td_f =
static_cast<float>(cfg.controller.fallback_fuel_duty_cycle);
484 float td_o =
static_cast<float>(cfg.controller.fallback_ox_duty_cycle);
485 if (td_f > 0.0f || td_o > 0.0f)
494 if (!service.
start(loop_hz)) {
495 std::cerr <<
"❌ Failed to start controller loop" << std::endl;
501 control_thread.detach();
503 std::cout <<
"\n🎯 Controller running. PWM gated to FIRE state (TCP :" << control_port <<
").\n"
508 std::this_thread::sleep_for(std::chrono::milliseconds(100));
512 std::cout <<
"✅ Controller service stopped." << std::endl;
static void signalHandler(int)
bool initialize(const PWMConfig &pwm_config, const RobustDDPController::Config &controller_config, const std::string &elodin_host="", uint16_t elodin_port=2240, const std::string &lut_path="", const std::string &thrust_curve_path="")
Initialize the controller service.
static void handleControlClient(int client_fd, fsw::control::ControllerService *svc)
static void runControlServer(fsw::control::ControllerService *svc, uint16_t port)
static std::atomic< bool > g_running
static std::string resolveConfigPath(const std::string &config_path, const std::string &path)
Resolve path relative to config: paths like output/lut/... are relative to project root.
constexpr size_t kMaxControlClients
static void signalHandler(int)