STAR
Cal STAR — firmware, DAQ server, and engine design
Loading...
Searching...
No Matches
test_controller_pwm_roles.cpp
Go to the documentation of this file.
1/**
2 * Which actuators the controller PWMs, resolved from config alone.
3 *
4 * An [actuator_roles] entry's optional 4th element — "pwm_fuel" or "pwm_ox" — assigns that
5 * actuator to controller_service. It is the single statement of what the controller drives, and
6 * the same fact makes ActuatorCommander stop commanding the actuator during a burn, so the two
7 * cannot disagree about who owns a valve.
8 *
9 * Before this was config-driven, controller_main matched the literal names "Fuel Press"/"LOX Press"
10 * and every step had a fallback: a missing role became CH3/CH8 on board 12, an undeclared board
11 * became "192.168.2.<board_id>", an empty board table became a synthesized 11-14, and two roles on
12 * two boards silently shared the first one's IP. Each drove PWM at whatever hardware happened to
13 * sit on the guessed channel, behind a warning in a log nobody reads.
14 *
15 * So what is pinned here is mostly the *absence* of fallbacks: every unresolvable input must be
16 * reported rather than substituted, because the caller turns a report into a disabled fire gate.
17 *
18 * Pure resolution — no sockets, no files, no dependence on the host's interfaces.
19 */
20#include <iostream>
21#include <map>
22#include <string>
23
24#include "config/Config.hpp"
26
27static int g_failures = 0;
28
29static void check(bool ok, const std::string& what) {
30 std::cout << (ok ? " ok " : " FAIL ") << what << std::endl;
31 if (!ok)
32 g_failures++;
33}
34
35/** `assign` maps an actuator name to its 4th element; a name absent from it is unassigned. */
36static fsw::config::Config makeConfig(const std::map<std::string, std::string>& assign,
37 bool declare_board_14 = true) {
38 std::string toml;
39 toml += "[boards.act]\ntype = \"ACTUATOR\"\nip = \"192.0.2.12\"\nboard_id = 12\n";
40 toml += "enabled = true\n\n";
41 if (declare_board_14) {
42 toml += "[boards.act2]\ntype = \"ACTUATOR\"\nip = \"192.0.2.14\"\nboard_id = 14\n";
43 toml += "enabled = true\n\n";
44 }
45 auto role = [&](const char* name, const char* kind, int ch, int board) {
46 const auto it = assign.find(name);
47 std::string line = std::string("\"") + name + "\" = [\"" + kind + "\", " +
48 std::to_string(ch) + ", " + std::to_string(board);
49 if (it != assign.end())
50 line += ", \"" + it->second + "\"";
51 return line + "]\n";
52 };
53 toml += "[actuator_roles]\n";
54 toml += role("Fuel Press", "NC", 3, 12);
55 toml += role("LOX Press", "NO", 8, 12);
56 toml += role("Far Side Press", "NC", 2, 14);
58}
59
60int main() {
61 std::cout << "=== controller PWM actuator resolution ===" << std::endl;
62
63 // 1. The happy path: one actuator assigned to each output.
64 {
66 makeConfig({{"Fuel Press", "pwm_fuel"}, {"LOX Press", "pwm_ox"}}));
67 check(r.ok(), "both outputs resolve with no issues");
68 check(r.fuel.resolved() && r.fuel.channel == 3 && r.fuel.board_ip == "192.0.2.12",
69 "fuel resolves to the channel and board its actuator declares");
70 check(r.ox.resolved() && r.ox.channel == 8 && r.ox.board_ip == "192.0.2.12",
71 "ox resolves to the channel and board its actuator declares");
72 check(r.fuel.actuator_name == "Fuel Press",
73 "the target carries the actuator name, so logs can say which valve");
74 }
75
76 // 2. The assignment, not the name, is what binds. Renaming an actuator in the editor moves
77 // the assignment with it — the whole point of not matching a literal "Fuel Press".
78 {
80 makeConfig({{"Far Side Press", "pwm_fuel"}, {"LOX Press", "pwm_ox"}}));
81 check(r.ok(), "any actuator can serve an output, whatever it is called");
82 check(r.fuel.channel == 2 && r.fuel.board_ip == "192.0.2.14",
83 "fuel follows the assignment to that actuator's own board and channel");
84 }
85
86 // 3. Nothing assigned to an output. The old code printed a warning and drove CH3 anyway.
87 {
88 const auto r = fsw::control::resolvePWMTargets(makeConfig({{"LOX Press", "pwm_ox"}}));
89 check(!r.ok(), "an unassigned fuel output is reported");
90 check(!r.fuel.resolved(), "an unassigned fuel output resolves to nothing (no CH3 default)");
91 check(r.fuel.channel == 0, "unresolved target keeps channel 0 so it cannot be addressed");
92 check(r.ox.resolved(), "the other output still resolves independently");
93 check(r.issues.size() == 1 && r.issues[0].find("pwm_fuel") != std::string::npos,
94 "the reported issue names the output that has no actuator");
95 }
96
97 // 4. Two actuators claiming one output. Taking the first would put PWM on a valve the operator
98 // did not intend — the same class of failure as the old name-matching fallbacks, so it is
99 // an error rather than a silent pick.
100 {
102 {{"Fuel Press", "pwm_fuel"}, {"Far Side Press", "pwm_fuel"}, {"LOX Press", "pwm_ox"}}));
103 check(!r.ok(), "a duplicate assignment is reported");
104 check(!r.fuel.resolved(),
105 "a duplicated output resolves to nothing rather than picking one");
106 check(r.issues.size() == 1 && r.issues[0].find("Fuel Press") != std::string::npos &&
107 r.issues[0].find("Far Side Press") != std::string::npos,
108 "the reported issue names both claimants");
109 }
110
111 // 5. An actuator on a board no [boards.*] declares an ip for. The old code synthesized
112 // "192.168.2.<board_id>", guessing both the addressing scheme and the subnet — and the
113 // guess is routable, so it can reach a real board that is not the intended one.
114 {
116 makeConfig({{"Fuel Press", "pwm_fuel"}, {"Far Side Press", "pwm_ox"}},
117 /*declare_board_14=*/false));
118 check(!r.ok(), "an actuator on an undeclared board is reported");
119 check(!r.ox.resolved(), "no IP is synthesized from the board_id");
120 check(r.issues.size() == 1 && r.issues[0].find("board_id 14") != std::string::npos,
121 "the reported issue names the board that has no ip");
122 }
123
124 // 6. Nothing assigned at all. The controller reports both and its caller disables the fire
125 // gate — correct for a process whose job is to drive PWM. (The config editor deliberately
126 // treats the same state as valid, since a rig that does not use the controller must still
127 // be saveable; see validateControllerPwmActuators in shared/types.ts.)
128 {
130 check(r.issues.size() == 2, "both unassigned outputs are reported together");
131 check(!r.fuel.resolved() && !r.ox.resolved(), "neither target is invented");
132 }
133
134 // 7. Two actuators on two different boards is a legal rig, not an error: PWMConfig carries an
135 // IP per target, and sendActuationPWM splits the datagram. The old code warned and used the
136 // fuel board's IP for both, so LOX PWM went to the wrong board.
137 {
139 makeConfig({{"Fuel Press", "pwm_fuel"}, {"Far Side Press", "pwm_ox"}}));
140 check(r.ok(), "actuators on different boards are not an error");
141 check(r.fuel.board_ip == "192.0.2.12" && r.ox.board_ip == "192.0.2.14",
142 "each target keeps its own board IP");
143 }
144
145 // 8. No [boards.*] at all. The old code invented boards 11-14 on 192.168.2.x.
146 {
147 const auto cfg = fsw::config::load_from_string(
148 "[actuator_roles]\n\"Fuel Press\" = [\"NC\", 3, 12, \"pwm_fuel\"]\n"
149 "\"LOX Press\" = [\"NO\", 8, 12, \"pwm_ox\"]\n");
150 const auto r = fsw::control::resolvePWMTargets(cfg);
151 check(!r.ok(), "no declared boards is reported, not filled in from a default table");
152 check(!r.fuel.resolved() && !r.ox.resolved(), "neither target is invented");
153 }
154
155 // 9. The assignment slot is independent of polarity — the reason it is a 4th element rather
156 // than a third value of `kind`. "LOX Press" is NO *and* controller-driven; if the two ever
157 // share a slot again, a PWM assignment would silently drop the normally-open inversion.
158 {
159 const auto cfg = makeConfig({{"Fuel Press", "pwm_fuel"}, {"LOX Press", "pwm_ox"}});
160 check(cfg.actuator_roles.at("LOX Press").is_no,
161 "an assigned actuator keeps its NO polarity");
162 check(cfg.actuator_roles.at("LOX Press").controller_role == "pwm_ox",
163 "...and its controller assignment at the same time");
164 check(cfg.actuator_roles.at("Far Side Press").controller_role.empty(),
165 "an unassigned actuator has no controller role");
166 }
167
168 std::cout << (g_failures ? "FAILED" : "PASSED") << ": " << g_failures << " failure(s)"
169 << std::endl;
170 return g_failures ? 1 : 0;
171}
Resolve which hardware the controller PWMs, from config alone.
static bool ok
Definition flight_e2e.c:435
Config load_from_string(const std::string &text)
Parse config from an in-memory TOML string (SequencerService / ActuatorCommander hold it so).
Definition Config.cpp:458
PWMResolution resolvePWMTargets(const fsw::config::Config &cfg)
The whole parsed config.toml, restricted to what C++ consumes.
Definition Config.hpp:264
static int g_failures
static boot_report_t r
static void check(bool ok, const std::string &what)
static fsw::config::Config makeConfig(const std::map< std::string, std::string > &assign, bool declare_board_14=true)
assign maps an actuator name to its 4th element; a name absent from it is unassigned.
std::string board(const std::string &name, const std::string &id, bool enabled=true)
static std::string ch(int ain, const std::string &kind, const std::string &extra="", const std::string &board="lj")
Definition test_t7.cpp:98