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)
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);
160 std::istringstream iss(sec_content);
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)
169 std::string k =
trim(line.substr(0, eq));
170 std::string v =
trim(line.substr(eq + 1));
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;
182 std::string target_ip =
"192.168.2.70";
183 uint16_t target_port = 5006;
184 int force_state = -1;
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";
211 std::string config_content;
213 std::ifstream f(config_path);
215 for (
const auto& fp : {
"config/config.toml",
"../config/config.toml"}) {
224 std::ostringstream ss;
226 config_content = ss.str();
230 if (!config_content.empty()) {
231 auto val =
getTomlValue(config_content,
"stacklight_service",
"interval_ms",
"");
234 interval_ms = std::max(100, std::stoi(val));
239 val =
getTomlValue(config_content,
"stacklight_service",
"target_ip",
"");
243 val =
getTomlValue(config_content,
"stacklight_service",
"target_port",
"");
246 target_port =
static_cast<uint16_t
>(std::stoi(val));
251 val =
getTomlValue(config_content,
"stacklight_service",
"elodin_host",
"");
255 val =
getTomlValue(config_content,
"stacklight_service",
"elodin_port",
"");
258 elodin_port =
static_cast<uint16_t
>(std::stoi(val));
268 int sock = socket(AF_INET, SOCK_DGRAM, 0);
270 std::cerr <<
"[StacklightService] socket() failed" << std::endl;
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;
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;
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"
295 elodin_thread = std::thread(
elodinThread, elodin_host, elodin_port);
298 uint8_t last_sent_state = 0xFF;
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);
307 auto now = std::chrono::steady_clock::now();
308 bool state_changed = (seq_state != last_sent_state);
310 std::chrono::duration_cast<std::chrono::milliseconds>(now - last_send).count() >=
313 if (state_changed || keepalive_due) {
315 auto ts = std::chrono::duration_cast<std::chrono::milliseconds>(
316 std::chrono::system_clock::now().time_since_epoch())
323 ssize_t sent = sendto(sock, buf, len, 0,
reinterpret_cast<struct sockaddr*
>(&dest),
325 if (sent ==
static_cast<ssize_t
>(len)) {
327 last_sent_state = seq_state;
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;
344 if (elodin_thread.joinable())
345 elodin_thread.join();
346 std::cout <<
"[StacklightService] Stopped." << std::endl;
static void signalHandler(int)
static std::atomic< bool > g_running
std::atomic< uint8_t > g_seq_state
daq::StacklightCommandPacket stateToStacklight(uint8_t s)
constexpr int kStacklightRecvTimeoutMs
void elodinThread(std::string host, uint16_t port)
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.
std::string trim(const std::string &s)
std::string getTomlValue(const std::string &content, const std::string §ion, const std::string &key, const std::string &fallback="")