STAR
Cal STAR — firmware, DAQ server, and engine design
Loading...
Searching...
No Matches
ControllerService.hpp
Go to the documentation of this file.
1#ifndef FSW_CONTROL_CONTROLLER_SERVICE_HPP
2#define FSW_CONTROL_CONTROLLER_SERVICE_HPP
3
4#include <atomic>
5#include <chrono>
6#include <cstdint>
7#include <memory>
8#include <mutex>
9#include <string>
10#include <thread>
11#include <vector>
12
13#include "ControllerLUT.hpp"
16
17namespace fsw {
18namespace control {
19
20/**
21 * @brief C++ controller service with UDP PWM output
22 *
23 * Pipeline:
24 * sensor data (pushed via setMeasurement) → RobustDDPController::step()
25 * → PWM command UDP packet (combined_gui.py format) → actuator board
26 *
27 * Optionally writes to Elodin DB if connected.
28 */
30public:
31 // ── PWM / actuator config ──────────────────────────────────────────
32 /** Where one propellant's PWM commands go. Both fields come from config via
33 * [controller].pwm_*_actuator → [actuator_roles] → [boards.*]; there are deliberately no
34 * defaults, because a default is this process inventing a rig layout it cannot know.
35 * channel == 0 means "config did not say" and the target must never be addressed. */
36 struct PWMTarget {
37 std::string board_ip;
38 uint8_t channel = 0;
39 std::string actuator_name; // the [actuator_roles] key, for logs an operator can act on
40
41 bool resolved() const {
42 return channel != 0 && !board_ip.empty();
43 }
44 };
45
46 struct PWMConfig {
49 uint16_t actuator_port = 5005;
50 /** Local address PWM commands leave from; "0.0.0.0" leaves the NIC to the kernel.
51 * Resolved by the caller (see net/DaqInterface.hpp) — this is the fire path, and it must
52 * not reach the actuator board over whichever interface the route table happened to
53 * prefer. */
54 std::string bind_address = "0.0.0.0";
55 float frequency_hz = 10.0f;
56 uint32_t duration_ms = 1000; // PWM burst duration per command
57 };
58
61
62 /**
63 * @brief Initialize the controller service
64 * @param pwm_config PWM output configuration (board IP, channels, …)
65 * @param controller_config Controller algorithm configuration
66 * @param elodin_host Elodin DB host (empty string = skip DB)
67 * @param elodin_port Elodin DB port (default 2240)
68 * @param lut_path Optional path to LUT binary. If non-empty and load succeeds,
69 * bypasses DDP and uses LUT for boolean control (u_safe_F/O > 0.5).
70 * @param thrust_curve_path Optional path to thrust curve CSV (time_s,thrust_N). When set
71 * and fire is active, thrust_desired is interpolated from curve.
72 * @return true if initialization succeeded
73 */
74 bool initialize(const PWMConfig& pwm_config,
75 const RobustDDPController::Config& controller_config,
76 const std::string& elodin_host = "", uint16_t elodin_port = 2240,
77 const std::string& lut_path = "", const std::string& thrust_curve_path = "");
78
79 /** Start the controller loop at the given rate. */
80 bool start(double loop_rate_hz = 10.0);
81
82 /** Stop the controller loop. */
83 void stop();
84
85 bool is_running() const {
86 return running_;
87 }
88
89 // ── Thread-safe data setters (called from external sensor thread) ──
93
94 /**
95 * @brief Enable or disable PWM output (FIRE state gate).
96 * PWM is only sent while fire_active_ is true.
97 * Called by the TCP control thread when FIRE_START / FIRE_STOP is received.
98 */
99 void setFireActive(bool active);
100
101 /**
102 * Sequencer state id that means "firing", for the parity fallback that sniffs the sequencer
103 * state packet when the TCP FIRE_START/FIRE_STOP path misses an edge. Was the literal 16, so
104 * renumbering states silently moved the PWM ignition gate onto whatever state took id 16.
105 * Defaults to 16 so an un-migrated config behaves exactly as before.
106 */
107 void setFireStateId(uint8_t id) {
108 fire_state_id_ = id;
109 }
110
111 /**
112 * Master switch for the PWM ignition gate.
113 *
114 * Disabled means this service never drives a valve, whatever it is told: setFireActive() —
115 * the single funnel BOTH activation paths go through, TCP and the state-packet parity
116 * fallback — returns immediately. Two reasons to turn it off:
117 *
118 * 1. [controller_service].sequencer_owns_valves says the sequencer commands the valves
119 * during the burn, so a second writer on the same channels would fight it.
120 * 2. The PWM roles could not be resolved from [actuator_roles]. This used to fall back to
121 * hardcoded channel numbers, which on a stand that does not have those roles addressed
122 * whatever valve happened to occupy them — on one profile, a vent.
123 */
124 void setFireGateEnabled(bool enabled) {
125 fire_gate_enabled_ = enabled;
126 }
127 bool isFireGateEnabled() const {
128 return fire_gate_enabled_.load();
129 }
130
131 /**
132 * @brief Override controller output with fixed duty cycles for open-loop validation.
133 * When both values are 0 (default), the RobustDDP controller runs normally.
134 * Set non-zero to bypass the controller and send these fixed duties on every FIRE tick.
135 */
136 void setTestDuty(float fuel, float ox);
137
138 /** Get last actuation output (for diagnostics / GUI broadcast). */
141
142private:
143 uint8_t fire_state_id_{16};
144 // ── PWM packet sending ─────────────────────────────────────────────
145 /**
146 * Send a PWM actuator command over UDP.
147 * Packet format matches combined_gui.py exactly:
148 * Header <BBI>: packet_type=10, version=0, timestamp_ms(u32)
149 * Body <B> : num_commands
150 * Per-cmd <BIff>: actuator_id, duration_ms, duty_cycle, frequency
151 */
152 bool sendPWMCommand(const PWMTarget& target, float duty_cycle, float frequency,
153 uint32_t duration_ms);
154
155 /** Single UDP packet carrying both channels. Only valid when both targets are on the same
156 * board — the packet has one destination. sendActuationPWM enforces that. */
157 bool sendPWMCommands(const PWMTarget& t1, float duty1, const PWMTarget& t2, float duty2,
158 float frequency, uint32_t duration_ms);
159
160 /** Send the fuel + ox PWM commands for a given actuation output. One packet when both
161 * actuators are on the same board (preserving the simultaneity the fire path had when
162 * PWMConfig could only express one destination), two when config puts them on different
163 * boards. An unresolved target is never addressed. */
165
166 // ── Elodin DB (optional) ───────────────────────────────────────────
171
172 // ── Controller loop ────────────────────────────────────────────────
173 void controllerLoop();
175
176 /** Connect the subscriber and subscribe to the tables it reads. Both halves run on every
177 * reconnect — a db restart loses the VTables, so re-connecting without re-subscribing comes
178 * back silent. Returns false (having dropped the connection) if either half fails. */
179 bool connectSubscriber();
180
181 /** The read half, one connection's worth. Returns when the connection drops or on shutdown;
182 * elodinSubscriberLoop() decides whether to reconnect. */
183 void subscriberReadLoop(std::vector<uint8_t>& rx_buffer);
184
185 // ── State ──────────────────────────────────────────────────────────
186 std::atomic<bool> running_{false};
187 std::atomic<bool> fire_active_{false}; // PWM only sent when FIRE state is active
188 std::atomic<bool> fire_gate_enabled_{true}; // see setFireGateEnabled
189
190 // Open-loop test duty cycles (0 = use DDP controller; non-zero = bypass DDP)
191 std::atomic<float> test_duty_fuel_{0.0f};
192 std::atomic<float> test_duty_ox_{0.0f};
193
194 // Controller algorithm (DDP when LUT not used)
195 std::unique_ptr<RobustDDPController> controller_;
196
197 // Optional LUT for boolean control (bypasses DDP when loaded)
199
200 // Elodin DB — publisher (write-only; used for publishing actuation/diagnostics)
201 std::unique_ptr<elodin::ElodinClient> elodin_client_;
202 bool elodin_connected_ = false;
203
204 // Elodin DB — subscriber (read-only; dedicated to receiving calibrated PT data)
205 std::unique_ptr<elodin::ElodinClient> elodin_subscriber_;
206 std::string elodin_host_;
207 uint16_t elodin_port_ = 2240;
208
209 // PWM output
212
213 // Thread-safe sensor inputs
214 mutable std::mutex input_mutex_;
218 bool has_measurement_ = false;
219
220 // Last outputs (thread-safe read)
221 mutable std::mutex output_mutex_;
224
225 // Loop timing
228 double loop_rate_hz_ = 10.0;
229 double loop_interval_ms_ = 100.0;
230
231 // Thrust curve (time-varying target from Layer 2 pressure curves)
232 std::vector<double> thrust_curve_times_;
233 std::vector<double> thrust_curve_values_;
234 std::chrono::steady_clock::time_point fire_start_time_;
236
237 bool loadThrustCurve(const std::string& path);
238 double interpolateThrustCurve(double t_elapsed_s) const;
239};
240
241} // namespace control
242} // namespace fsw
243
244#endif // FSW_CONTROL_CONTROLLER_SERVICE_HPP
Controller LUT loader with multilinear interpolation.
std::unique_ptr< elodin::ElodinClient > elodin_subscriber_
std::chrono::steady_clock::time_point fire_start_time_
std::unique_ptr< RobustDDPController > controller_
void stop()
Stop the controller loop.
std::unique_ptr< elodin::ElodinClient > elodin_client_
void writeActuationToDB(const RobustDDPController::ActuationCommand &actuation)
void setNavState(const RobustDDPController::NavState &nav)
std::vector< double > thrust_curve_times_
RobustDDPController::NavState current_nav_
void sendActuationPWM(const RobustDDPController::ActuationCommand &act)
Send the fuel + ox PWM commands for a given actuation output.
RobustDDPController::ActuationCommand last_actuation_
void setMeasurement(const RobustDDPController::Measurement &meas)
void setFireStateId(uint8_t id)
Sequencer state id that means "firing", for the parity fallback that sniffs the sequencer state packe...
void setFireActive(bool active)
Enable or disable PWM output (FIRE state gate).
void setFireGateEnabled(bool enabled)
Master switch for the PWM ignition gate.
double interpolateThrustCurve(double t_elapsed_s) const
bool connectSubscriber()
Connect the subscriber and subscribe to the tables it reads.
RobustDDPController::Measurement current_meas_
void writeMeasurementToDB(const RobustDDPController::Measurement &measurement)
bool start(double loop_rate_hz=10.0)
Start the controller loop at the given rate.
void subscriberReadLoop(std::vector< uint8_t > &rx_buffer)
The read half, one connection's worth.
bool loadThrustCurve(const std::string &path)
RobustDDPController::Diagnostics getLastDiagnostics() const
void writeDiagnosticsToDB(const RobustDDPController::Diagnostics &diagnostics)
void setCommand(const RobustDDPController::Command &cmd)
bool sendPWMCommands(const PWMTarget &t1, float duty1, const PWMTarget &t2, float duty2, float frequency, uint32_t duration_ms)
Single UDP packet carrying both channels.
bool sendPWMCommand(const PWMTarget &target, float duty_cycle, float frequency, uint32_t duration_ms)
Send a PWM actuator command over UDP.
RobustDDPController::Diagnostics last_diagnostics_
void setTestDuty(float fuel, float ox)
Override controller output with fixed duty cycles for open-loop validation.
RobustDDPController::Command current_cmd_
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.
RobustDDPController::ActuationCommand getLastActuation() const
Get last actuation output (for diagnostics / GUI broadcast).
std::vector< double > thrust_curve_values_
long target
Definition main.cpp:38
Motorized ball valve position: raw ADC counts -> % open.
std::string bind_address
Local address PWM commands leave from; "0.0.0.0" leaves the NIC to the kernel.
Where one propellant's PWM commands go.