STAR
Cal STAR — firmware, DAQ server, and engine design
Loading...
Searching...
No Matches
stacklight_service_main.cpp
Go to the documentation of this file.
1/**
2 * Stacklight Service — C++ STACKLIGHT_COMMAND sender.
3 *
4 * Subscribes to Elodin [0x5000] (SequencerState) for current state, maps it to
5 * a StacklightCommandPacket (red/yellow/green/buzzer), and sends it via UDP
6 * unicast to the stacklight board whenever the state changes (plus a periodic
7 * keepalive resend).
8 */
9
10#include <arpa/inet.h>
11#include <netinet/in.h>
12#include <signal.h>
13#include <sys/socket.h>
14#include <unistd.h>
15
16#include <atomic>
17#include <chrono>
18#include <cstring>
19#include <fstream>
20#include <iostream>
21#include <sstream>
22#include <string>
23#include <thread>
24
25#include "DiabloPacketUtils.h"
26#include "DiabloPackets.h"
28#include "daq-protocol.h"
31
32namespace {
33std::atomic<bool> g_running{true};
34std::atomic<uint8_t> g_seq_state{static_cast<uint8_t>(sequencer::State::UNKNOWN)};
35
36void signalHandler(int /*sig*/) {
37 std::cout << "\n[StacklightService] Shutting down..." << std::endl;
38 g_running = false;
39}
40
41// ── sequencer::State → StacklightCommandPacket ──────────────────────────────
42// Standard industrial convention: red = danger, yellow = caution,
43// green = safe, buzzer = most urgent states (fire, abort).
44daq::StacklightCommandPacket stateToStacklight(uint8_t s) {
45 using sequencer::State;
46 daq::StacklightCommandPacket cmd{0, 0, 0, 0};
47
48 switch (static_cast<State>(s)) {
49 case State::IDLE:
50 cmd.green = 1;
51 break;
52 case State::DEBUG:
53 case State::CALIBRATE:
54 case State::GN2_VENT:
55 case State::FUEL_VENT:
56 case State::OX_VENT:
57 case State::GN2_HIGH_VENT:
58 case State::VENT:
59 cmd.yellow = 1;
60 break;
61 case State::ARMED:
62 case State::READY:
63 case State::PRESS_STANDBY:
64 cmd.red = 1;
65 cmd.yellow = 1;
66 break;
67 case State::FUEL_FILL:
68 case State::OX_FILL:
69 case State::GN2_LOW_PRESS:
70 case State::FUEL_PRESS:
71 case State::OX_PRESS:
72 case State::GN2_HIGH_PRESS:
73 cmd.red = 1;
74 break;
75 case State::FIRE:
76 cmd.red = 1;
77 cmd.buzzer = 1;
78 break;
79 case State::ENGINE_ABORT:
80 case State::GSE_ABORT:
81 case State::EMERGENCY_ABORT:
82 cmd.red = 1;
83 cmd.buzzer = 1;
84 break;
85 case State::UNKNOWN:
86 default:
87 cmd.red = 1;
88 cmd.yellow = 1;
89 cmd.green = 1;
90 cmd.buzzer = 1;
91 break;
92 }
93 return cmd;
94}
95
96// ── Elodin subscriber thread ────────────────────────────────────────────────
97constexpr int kStacklightRecvTimeoutMs = 500;
98
99void elodinThread(std::string host, uint16_t port) {
101
102 while (g_running) {
103 if (!client.is_connected()) {
104 if (!client.connect(host, port)) {
105 std::this_thread::sleep_for(std::chrono::seconds(2));
106 continue;
107 }
108 if (!client.subscribe_tables({fsw::elodin::kTableSequencerState})) {
109 std::cerr << "[StacklightService] Failed to subscribe to SequencerState [0x50,0x00]"
110 << std::endl;
111 client.disconnect();
112 continue;
113 }
114 // Without a receive timeout read_packet() blocks forever and shutdown would hang.
115 client.set_recv_timeout_ms(kStacklightRecvTimeoutMs);
116 std::cout << "[StacklightService] Elodin connected, subscribed to SequencerState"
117 << std::endl;
118 }
119
120 uint8_t buf[4096];
121 ssize_t n = client.read_packet(buf, sizeof(buf));
122 if (n < 0) {
123 client.disconnect(); // read error: drop the socket and reconnect next loop
124 continue;
125 }
126 if (n < 8)
127 continue; // 0 = receive timeout, nothing to do this round
128
129 // SequencerState VTable: [0x50, 0x00]
130 if (buf[5] == fsw::elodin::kTableSequencerState.first &&
131 buf[6] == fsw::elodin::kTableSequencerState.second && n >= 8 + 9) {
132 const uint8_t seq_state = buf[8 + 8];
133 g_seq_state.store(seq_state);
134 }
135 }
136}
137
138} // namespace
139
140// ── Minimal TOML reader (mirrors heartbeat_service_main.cpp) ────────────────
141std::string trim(const std::string& s) {
142 size_t a = s.find_first_not_of(" \t\r\n\"");
143 size_t b = s.find_last_not_of(" \t\r\n\"");
144 return (a == std::string::npos) ? "" : s.substr(a, b - a + 1);
145}
146
147std::string getTomlValue(const std::string& content, const std::string& section,
148 const std::string& key, const std::string& fallback = "") {
149 std::string sec_header = "[" + section + "]";
150 auto sec_pos = content.find(sec_header);
151 if (sec_pos == std::string::npos)
152 return fallback;
153
154 auto search_start = sec_pos + sec_header.size();
155 auto next_sec = content.find("\n[", search_start);
156 std::string sec_content = (next_sec == std::string::npos)
157 ? content.substr(search_start)
158 : content.substr(search_start, next_sec - search_start);
159
160 std::istringstream iss(sec_content);
161 std::string line;
162 while (std::getline(iss, line)) {
163 auto c = line.find('#');
164 if (c != std::string::npos)
165 line = line.substr(0, c);
166 auto eq = line.find('=');
167 if (eq == std::string::npos)
168 continue;
169 std::string k = trim(line.substr(0, eq));
170 std::string v = trim(line.substr(eq + 1));
171 if (k == key)
172 return v;
173 }
174 return fallback;
175}
176
177int main(int argc, char* argv[]) {
178 std::string config_path = "config/config.toml";
179 std::string elodin_host = "127.0.0.1";
180 uint16_t elodin_port = 2240;
181 int interval_ms = 1000; // keepalive resend interval
182 std::string target_ip = "192.168.2.70"; // PLACEHOLDER — update once team assigns a real IP
183 uint16_t target_port = 5006; // PLACEHOLDER — update once team confirms
184 int force_state = -1; // -1 = read from Elodin(normal). 0-255 = test override
185
186 for (int i = 1; i < argc; ++i) {
187 std::string arg = argv[i];
188 if (arg == "--config" && i + 1 < argc) {
189 config_path = argv[++i];
190 } else if (arg == "--elodin-host" && i + 1 < argc) {
191 elodin_host = argv[++i];
192 } else if (arg == "--elodin-port" && i + 1 < argc) {
193 elodin_port = static_cast<uint16_t>(std::atoi(argv[++i]));
194 } else if (arg == "--interval-ms" && i + 1 < argc) {
195 interval_ms = std::max(100, std::atoi(argv[++i]));
196 } else if (arg == "--target-ip" && i + 1 < argc) {
197 target_ip = argv[++i];
198 } else if (arg == "--target-port" && i + 1 < argc) {
199 target_port = static_cast<uint16_t>(std::atoi(argv[++i]));
200 } else if (arg == "--force-state" && i + 1 < argc) {
201 force_state = std::atoi(argv[++i]);
202 } else if (arg == "--help" || arg == "-h") {
203 std::cout << "Usage: " << argv[0]
204 << " [--config PATH] [--elodin-host HOST] [--elodin-port PORT]\n"
205 " [--interval-ms MS] [--target-ip IP] [--target-port PORT]\n";
206 return 0;
207 }
208 }
209
210 // Load config file, falling back to a couple of common relative paths
211 std::string config_content;
212 {
213 std::ifstream f(config_path);
214 if (!f.is_open()) {
215 for (const auto& fp : {"config/config.toml", "../config/config.toml"}) {
216 f.open(fp);
217 if (f.is_open()) {
218 config_path = fp;
219 break;
220 }
221 }
222 }
223 if (f.is_open()) {
224 std::ostringstream ss;
225 ss << f.rdbuf();
226 config_content = ss.str();
227 }
228 }
229
230 if (!config_content.empty()) {
231 auto val = getTomlValue(config_content, "stacklight_service", "interval_ms", "");
232 if (!val.empty()) {
233 try {
234 interval_ms = std::max(100, std::stoi(val));
235 } catch (...) {
236 }
237 }
238
239 val = getTomlValue(config_content, "stacklight_service", "target_ip", "");
240 if (!val.empty())
241 target_ip = val;
242
243 val = getTomlValue(config_content, "stacklight_service", "target_port", "");
244 if (!val.empty()) {
245 try {
246 target_port = static_cast<uint16_t>(std::stoi(val));
247 } catch (...) {
248 }
249 }
250
251 val = getTomlValue(config_content, "stacklight_service", "elodin_host", "");
252 if (!val.empty())
253 elodin_host = val;
254
255 val = getTomlValue(config_content, "stacklight_service", "elodin_port", "");
256 if (!val.empty()) {
257 try {
258 elodin_port = static_cast<uint16_t>(std::stoi(val));
259 } catch (...) {
260 }
261 }
262 }
263
264 signal(SIGINT, signalHandler);
265 signal(SIGTERM, signalHandler);
266
267 // UDP unicast socket
268 int sock = socket(AF_INET, SOCK_DGRAM, 0);
269 if (sock < 0) {
270 std::cerr << "[StacklightService] socket() failed" << std::endl;
271 return 1;
272 }
273
274 struct sockaddr_in dest;
275 memset(&dest, 0, sizeof(dest));
276 dest.sin_family = AF_INET;
277 dest.sin_port = htons(target_port);
278 if (inet_pton(AF_INET, target_ip.c_str(), &dest.sin_addr) != 1) {
279 std::cerr << "[StacklightService] Invalid target IP: " << target_ip << std::endl;
280 close(sock);
281 return 1;
282 }
283
284 std::cout << "[StacklightService] Started — interval=" << interval_ms
285 << "ms target=" << target_ip << ":" << target_port << std::endl;
286 std::cout << "[StacklightService] State from Elodin at " << elodin_host << ":" << elodin_port
287 << " [0x5000]" << std::endl;
288
289 std::thread elodin_thread;
290 if (force_state >= 0 && force_state <= 255) {
291 g_seq_state.store(static_cast<uint8_t>(force_state));
292 std::cout << "[StacklightService] --force-state " << force_state << ": skipping Elodin"
293 << std::endl;
294 } else {
295 elodin_thread = std::thread(elodinThread, elodin_host, elodin_port);
296 }
297
298 uint8_t last_sent_state = 0xFF; // sentinel value, forces the very first send
299 unsigned long count = 0;
300 auto last_log = std::chrono::steady_clock::now();
301 auto last_send = std::chrono::steady_clock::now() - std::chrono::milliseconds(interval_ms);
302
303 uint8_t buf[MAX_PACKET_SIZE];
304
305 while (g_running) {
306 uint8_t seq_state = g_seq_state.load();
307 auto now = std::chrono::steady_clock::now();
308 bool state_changed = (seq_state != last_sent_state);
309 bool keepalive_due =
310 std::chrono::duration_cast<std::chrono::milliseconds>(now - last_send).count() >=
311 interval_ms;
312
313 if (state_changed || keepalive_due) {
314 daq::StacklightCommandPacket cmd = stateToStacklight(seq_state);
315 auto ts = std::chrono::duration_cast<std::chrono::milliseconds>(
316 std::chrono::system_clock::now().time_since_epoch())
317 .count() &
318 0xFFFFFFFFu;
319
320 size_t len = daq::create_stacklight_command_packet(cmd, static_cast<uint32_t>(ts), buf,
321 sizeof(buf));
322 if (len > 0) {
323 ssize_t sent = sendto(sock, buf, len, 0, reinterpret_cast<struct sockaddr*>(&dest),
324 sizeof(dest));
325 if (sent == static_cast<ssize_t>(len)) {
326 count++;
327 last_sent_state = seq_state;
328 last_send = now;
329 }
330 }
331 }
332
333 auto since_log = std::chrono::duration<double>(now - last_log).count();
334 if (since_log >= 10.0) {
335 std::cout << "[StacklightService] Sent " << count
336 << " commands (seq_state=" << (int)seq_state << ")" << std::endl;
337 last_log = now;
338 }
339
340 usleep(100000); // check 10 times per second
341 }
342
343 close(sock);
344 if (elodin_thread.joinable())
345 elodin_thread.join();
346 std::cout << "[StacklightService] Stopped." << std::endl;
347 return 0;
348}
#define MAX_PACKET_SIZE
Definition main.h:6
static void signalHandler(int)
Standalone Elodin database client.
static std::atomic< bool > g_running
static const emmc_port_t port
Definition emmc_stm32.c:210
State
Definition main.cpp:72
*brief create JSON pointer *sa reference_tokens const string_t const string_t & b
Definition json.hpp:13878
*brief create JSON pointer *sa reference_tokens const string_t & a
Definition json.hpp:13877
size_type count(const key_type &key) const
Definition json.hpp:19264
daq::StacklightCommandPacket stateToStacklight(uint8_t s)
size_t create_stacklight_command_packet(const StacklightCommandPacket &data, uint32_t timestamp_ms, uint8_t *buffer, size_t buffer_size)
Creates a complete Stacklight Command packet in the provided buffer.
constexpr std::pair< uint8_t, uint8_t > kTableSequencerState
Non-sensor tables, named rather than spelled as literals at each use site.
*brief user defined string literal for JSON values *sa std::size_t n
Definition json.hpp:24474
State
System state IDs — must match the TypeScript SystemState enum in web-gui/shared/types....
std::string trim(const std::string &s)
std::string getTomlValue(const std::string &content, const std::string &section, const std::string &key, const std::string &fallback="")
int main()