537 const auto loop_interval =
543 auto loop_start = std::chrono::steady_clock::now();
548 const bool use_test_duty = (td_f > 0.0f || td_o > 0.0f);
566 constexpr double P_D_FROM_U_FRAC = 0.95;
567 bool p_d_fuel_fallback =
false;
568 bool p_d_ox_fallback =
false;
571 p_d_fuel_fallback =
true;
575 p_d_ox_fallback =
true;
577 if ((p_d_fuel_fallback || p_d_ox_fallback) &&
fire_active_.load() && tick % 50 == 0) {
578 std::cerr <<
"[ControllerService] ⚠️ Downstream PT fallback active during FIRE: "
579 << (p_d_fuel_fallback ?
"P_d_fuel " :
"")
580 << (p_d_ox_fallback ?
"P_d_ox " :
"")
581 <<
"— using 0.95 * upstream. Install downstream PTs for accurate control."
588 if (!have_data && !use_test_duty && !run_without_data) {
589 if (tick == 0 || tick % 50 == 0) {
590 std::cout <<
"[ControllerService] ⏳ Waiting for sensor data from Elodin…"
594 auto loop_end = std::chrono::steady_clock::now();
595 auto elapsed = loop_end - loop_start;
597 std::this_thread::sleep_for(loop_interval -
elapsed);
608 actuation.
u_F_on = td_f > 0.0f;
609 actuation.
u_O_on = td_o > 0.0f;
610 actuation.
valid =
true;
614 }
else if (
lut_.is_loaded()) {
626 actuation.
valid =
true;
630 diagnostics.F_estimated = 0.0;
631 diagnostics.MR_estimated = 0.0;
632 diagnostics.P_ch = 0.0;
634 std::map<std::string, double> point;
636 point[
"P_u_ox"] = meas.
P_u_ox;
643 double t_s = std::chrono::duration<double>(t_elapsed).count();
646 point[
"thrust_desired"] = thrust_ref;
647 point[
"MR_ref"] = 2.2;
649 point[
"P_ch"] = P_ch_est;
651 std::map<std::string, double> lut_out;
652 if (
lut_.evaluate(point, lut_out)) {
653 double u_f = lut_out.count(
"u_safe_F") ? lut_out.at(
"u_safe_F")
654 : lut_out.count(
"duty_F") ? lut_out.at(
"duty_F")
656 double u_o = lut_out.count(
"u_safe_O") ? lut_out.at(
"u_safe_O")
657 : lut_out.count(
"duty_O") ? lut_out.at(
"duty_O")
660 actuation.
duty_F = std::clamp(u_f, 0.0, 1.0);
661 actuation.
duty_O = std::clamp(u_o, 0.0, 1.0);
664 actuation.
valid =
true;
666 diagnostics.F_ref = thrust_ref;
667 diagnostics.P_ch = P_ch_est;
668 constexpr double P_MIN_VALID = 1e5;
670 if (lut_out.count(
"F"))
671 diagnostics.F_estimated = lut_out.at(
"F");
672 if (lut_out.count(
"MR"))
673 diagnostics.MR_estimated = lut_out.at(
"MR");
675 diagnostics.F_estimated = 0.0;
676 diagnostics.MR_estimated = 0.0;
685 actuation.
valid =
true;
686 diagnostics.F_ref = thrust_ref;
687 diagnostics.P_ch = P_ch_est;
688 diagnostics.F_estimated = 0.0;
689 diagnostics.MR_estimated = 0.0;
711 if (tick % 10 == 0) {
713 if (
fire_active_.load() && diagnostics.F_estimated == 0.0 && tick % 100 == 0) {
714 std::cerr <<
"[Controller] ⚠️ F_est=0: P_u_fuel=" << meas.
P_u_fuel / 6894.76
715 <<
" psi, P_u_ox=" << meas.
P_u_ox / 6894.76
716 <<
" psi (need both ≥14.5 psi). Check PT ch1/ch5." << std::endl;
718 std::cout <<
"[Controller] tick=" << tick <<
" duty_F=" << actuation.
duty_F
719 <<
" duty_O=" << actuation.
duty_O <<
" F_ref=" << diagnostics.F_ref
720 <<
" F_est=" << diagnostics.F_estimated <<
" MR=" << diagnostics.MR_estimated
721 <<
" P_ch=" << diagnostics.P_ch
722 << (diagnostics.safety_filtered ?
" [FILTERED]" :
"")
723 << (diagnostics.cutoff_active ?
" [CUTOFF]" :
"") << std::endl;
730 if (tick % 10 == 0) {
737 std::array<uint8_t, 4096> drain_buf;
738 while (
elodin_client_->read_data(drain_buf.data(), drain_buf.size()) > 0) {
746 auto loop_end = std::chrono::steady_clock::now();
747 auto elapsed = loop_end - loop_start;
749 std::this_thread::sleep_for(loop_interval -
elapsed);
933 auto actuation_vt = builder::vtable({
935 schema(PrimType::U64(), {}, component(
"CONTROLLER.actuation.timestamp_ns"))),
936 raw_field(8, 4, schema(PrimType::F32(), {}, component(
"CONTROLLER.actuation.duty_F"))),
937 raw_field(12, 4, schema(PrimType::F32(), {}, component(
"CONTROLLER.actuation.duty_O"))),
938 raw_field(16, 1, schema(PrimType::U8(), {}, component(
"CONTROLLER.actuation.u_F_on"))),
939 raw_field(17, 1, schema(PrimType::U8(), {}, component(
"CONTROLLER.actuation.u_O_on"))),
940 raw_field(18, 1, schema(PrimType::U8(), {}, component(
"CONTROLLER.actuation.valid"))),
945 auto diagnostics_vt = builder::vtable({
947 schema(PrimType::U64(), {}, component(
"CONTROLLER.diagnostics.timestamp_ns"))),
948 raw_field(8, 8, schema(PrimType::F64(), {}, component(
"CONTROLLER.diagnostics.F_ref"))),
949 raw_field(16, 8, schema(PrimType::F64(), {}, component(
"CONTROLLER.diagnostics.MR_ref"))),
951 schema(PrimType::F64(), {}, component(
"CONTROLLER.diagnostics.F_estimated"))),
953 schema(PrimType::F64(), {}, component(
"CONTROLLER.diagnostics.MR_estimated"))),
954 raw_field(40, 8, schema(PrimType::F64(), {}, component(
"CONTROLLER.diagnostics.P_ch"))),
955 raw_field(48, 8, schema(PrimType::F64(), {}, component(
"CONTROLLER.diagnostics.cost"))),
957 schema(PrimType::I32(), {}, component(
"CONTROLLER.diagnostics.solver_iters"))),
959 schema(PrimType::U8(), {}, component(
"CONTROLLER.diagnostics.safety_filtered"))),
961 schema(PrimType::U8(), {}, component(
"CONTROLLER.diagnostics.cutoff_active"))),
964 auto measurement_vt = builder::vtable({
966 schema(PrimType::U64(), {}, component(
"CONTROLLER.measurement.timestamp_ns"))),
967 raw_field(8, 8, schema(PrimType::F64(), {}, component(
"CONTROLLER.measurement.P_copv"))),
968 raw_field(16, 8, schema(PrimType::F64(), {}, component(
"CONTROLLER.measurement.P_reg"))),
969 raw_field(24, 8, schema(PrimType::F64(), {}, component(
"CONTROLLER.measurement.P_u_fuel"))),
970 raw_field(32, 8, schema(PrimType::F64(), {}, component(
"CONTROLLER.measurement.P_u_ox"))),
971 raw_field(40, 8, schema(PrimType::F64(), {}, component(
"CONTROLLER.measurement.P_d_fuel"))),
972 raw_field(48, 8, schema(PrimType::F64(), {}, component(
"CONTROLLER.measurement.P_d_ox"))),
973 raw_field(56, 8, schema(PrimType::F64(), {}, component(
"CONTROLLER.measurement.P_ch_mp1"))),
974 raw_field(64, 8, schema(PrimType::F64(), {}, component(
"CONTROLLER.measurement.P_ch_mp2"))),
978 auto state_trans_vt = builder::vtable({
979 raw_field(0, 8, schema(PrimType::U64(), {}, component(
"CONTROLLER.state.timestamp_ns"))),
980 raw_field(8, 1, schema(PrimType::U8(), {}, component(
"CONTROLLER.state.from_state"))),
981 raw_field(9, 1, schema(PrimType::U8(), {}, component(
"CONTROLLER.state.to_state"))),
982 raw_field(10, 1, schema(PrimType::U8(), {}, component(
"CONTROLLER.state.reason"))),
986 auto fire_state_vt = builder::vtable({
987 raw_field(0, 8, schema(PrimType::U64(), {}, component(
"CONTROLLER.fire.timestamp_ns"))),
988 raw_field(8, 1, schema(PrimType::U8(), {}, component(
"CONTROLLER.fire.fire_active"))),
989 raw_field(9, 4, schema(PrimType::F32(), {}, component(
"CONTROLLER.fire.duty_F"))),
990 raw_field(13, 4, schema(PrimType::F32(), {}, component(
"CONTROLLER.fire.duty_O"))),
994 .vtable = actuation_vt});
996 .vtable = diagnostics_vt});
998 .vtable = measurement_vt});
1000 .vtable = state_trans_vt});
1002 .vtable = fire_state_vt});
1005 std::vector<std::string> actuation_fields = {
1006 "CONTROLLER.actuation.timestamp_ns",
"CONTROLLER.actuation.duty_F",
1007 "CONTROLLER.actuation.duty_O",
"CONTROLLER.actuation.u_F_on",
1008 "CONTROLLER.actuation.u_O_on",
"CONTROLLER.actuation.valid"};
1009 std::vector<std::string> diagnostics_fields = {
"CONTROLLER.diagnostics.timestamp_ns",
1010 "CONTROLLER.diagnostics.F_ref",
1011 "CONTROLLER.diagnostics.MR_ref",
1012 "CONTROLLER.diagnostics.F_estimated",
1013 "CONTROLLER.diagnostics.MR_estimated",
1014 "CONTROLLER.diagnostics.P_ch",
1015 "CONTROLLER.diagnostics.cost",
1016 "CONTROLLER.diagnostics.solver_iters",
1017 "CONTROLLER.diagnostics.safety_filtered",
1018 "CONTROLLER.diagnostics.cutoff_active"};
1019 std::vector<std::string> measurement_fields = {
1020 "CONTROLLER.measurement.timestamp_ns",
"CONTROLLER.measurement.P_copv",
1021 "CONTROLLER.measurement.P_reg",
"CONTROLLER.measurement.P_u_fuel",
1022 "CONTROLLER.measurement.P_u_ox",
"CONTROLLER.measurement.P_d_fuel",
1023 "CONTROLLER.measurement.P_d_ox",
"CONTROLLER.measurement.P_ch_mp1",
1024 "CONTROLLER.measurement.P_ch_mp2"};
1025 std::vector<std::string> state_fields = {
1026 "CONTROLLER.state.timestamp_ns",
"CONTROLLER.state.from_state",
"CONTROLLER.state.to_state",
1027 "CONTROLLER.state.reason"};
1028 std::vector<std::string> fire_fields = {
"CONTROLLER.fire.timestamp_ns",
1029 "CONTROLLER.fire.fire_active",
"CONTROLLER.fire.duty_F",
1030 "CONTROLLER.fire.duty_O"};
1032 for (
const auto& f : actuation_fields)
1034 for (
const auto& f : diagnostics_fields)
1036 for (
const auto& f : measurement_fields)
1038 for (
const auto& f : state_fields)
1040 for (
const auto& f : fire_fields)
1049 std::cout <<
"[ControllerService] ✅ Registered controller tables with Elodin DB" << std::endl;