FERS 0.1.0
The Flexible Extensible Radar Simulator
Loading...
Searching...
No Matches
cli_runner.cpp
Go to the documentation of this file.
1// SPDX-License-Identifier: GPL-2.0-only
2// Copyright (c) 2026-present FERS Contributors (see AUTHORS.md).
3
4/**
5 * @file cli_runner.cpp
6 * @brief Testable implementation of the FERS command-line interface.
7 */
8
9#include "cli_runner.h"
10
11#include <filesystem>
12#include <format>
13#include <iostream>
14#include <libfers/api.h>
15#include <string>
16#include <utility>
17
18#include "arg_parser.h"
19#include "cli_paths.h"
20
21namespace
22{
23 std::string getLevelString(fers_log_level_t level)
24 {
25 switch (level)
26 {
27 case FERS_LOG_TRACE:
28 return "TRACE";
29 case FERS_LOG_DEBUG:
30 return "DEBUG";
31 case FERS_LOG_INFO:
32 return "INFO";
34 return "WARNING";
35 case FERS_LOG_ERROR:
36 return "ERROR";
37 case FERS_LOG_FATAL:
38 return "FATAL";
39 default:
40 return "UNKNOWN";
41 }
42 }
43
44 template <typename... Args>
45 void log(fers_log_level_t level, std::format_string<Args...> fmt, Args&&... args)
46 {
47 const std::string message = std::format(fmt, std::forward<Args>(args)...);
48 fers_log(level, message.c_str());
49 }
50
51 class ContextHandle
52 {
53 public:
54 explicit ContextHandle(fers_context_t* context) noexcept : _context(context) {}
55 ContextHandle(const ContextHandle&) = delete;
56 ContextHandle& operator=(const ContextHandle&) = delete;
57 ContextHandle(ContextHandle&&) = delete;
58 ContextHandle& operator=(ContextHandle&&) = delete;
59 ~ContextHandle()
60 {
61 if (_context != nullptr)
62 {
63 fers_context_destroy(_context);
64 }
65 }
66
67 [[nodiscard]] fers_context_t* get() const noexcept { return _context; }
68
69 private:
70 fers_context_t* _context;
71 };
72
73 bool isParseExit(const std::string& error)
74 {
75 return error == "Help requested." || error == "Version requested." || error == "No arguments provided.";
76 }
77
78 int logApiFailure(const fers_log_level_t level, const char* message, const int result)
79 {
80 char* err = fers_get_last_error_message();
81 log(level, "{}: {}", message, (err != nullptr) ? err : "Unknown error");
83 return result;
84 }
85
86 int configureLogging(const core::Config& config)
87 {
88 const char* log_file_ptr = config.log_file ? config.log_file->c_str() : nullptr;
89 if (fers_configure_logging(config.log_level, log_file_ptr) == 0)
90 {
91 return 0;
92 }
93
94 char* err = fers_get_last_error_message();
95 std::cerr << "[ERROR] Failed to configure logging: " << ((err != nullptr) ? err : "Unknown error") << '\n';
97 return 1;
98 }
99
100 int loadScenario(fers_context_t* context, const core::Config& config)
101 {
102 log(FERS_LOG_INFO, "Loading scenario from '{}'...", config.script_file);
103 if (fers_load_scenario_from_xml_file(context, config.script_file.c_str(), config.validate ? 1 : 0) == 0)
104 {
105 return 0;
106 }
107 return logApiFailure(FERS_LOG_FATAL, "Failed to load scenario", 1);
108 }
109
110 int configureVita49Output(fers_context_t* context, const core::Config& config)
111 {
112 if (!config.vita49_enabled)
113 {
114 return 0;
115 }
116 if (!config.vita49_fullscale.has_value())
117 {
118 log(FERS_LOG_FATAL, "Failed to configure VITA49 fullscale: missing required fullscale.");
119 return 1;
120 }
121 if (fers_enable_vita49_udp_output(context, config.vita49_host.c_str(), config.vita49_port) != 0)
122 {
123 return logApiFailure(FERS_LOG_FATAL, "Failed to configure VITA49 endpoint", 1);
124 }
125 if (fers_set_vita49_fullscale(context, config.vita49_fullscale.value_or(0.0)) != 0)
126 {
127 return logApiFailure(FERS_LOG_FATAL, "Failed to configure VITA49 fullscale", 1);
128 }
129 if (config.vita49_epoch_unix_nanoseconds.has_value() &&
131 {
132 return logApiFailure(FERS_LOG_FATAL, "Failed to configure VITA49 epoch", 1);
133 }
134 if (config.vita49_max_udp_payload.has_value() &&
136 {
137 return logApiFailure(FERS_LOG_FATAL, "Failed to configure VITA49 max UDP payload", 1);
138 }
139 if (config.vita49_queue_depth.has_value() &&
140 fers_set_vita49_queue_depth(context, *config.vita49_queue_depth) != 0)
141 {
142 return logApiFailure(FERS_LOG_FATAL, "Failed to configure VITA49 queue depth", 1);
143 }
144 return 0;
145 }
146
147 int setOutputDirectory(fers_context_t* context, const std::filesystem::path& final_out_dir)
148 {
149 if (fers_set_output_directory(context, final_out_dir.string().c_str()) == 0)
150 {
151 return 0;
152 }
153 return logApiFailure(FERS_LOG_FATAL, "Failed to set output directory", 1);
154 }
155
156 int generateKml(fers_context_t* context, const core::Config& config, const std::filesystem::path& final_out_dir)
157 {
158 const std::filesystem::path kml_output_path =
159 core::resolveKmlOutputPath(config.script_file, final_out_dir, config.kml_file);
160 const std::string kml_output_file = kml_output_path.string();
161
162 log(FERS_LOG_INFO, "Generating KML file for scenario: {}", kml_output_file);
163 if (fers_generate_kml(context, kml_output_file.c_str()) == 0)
164 {
165 log(FERS_LOG_INFO, "KML file generated successfully: {}", kml_output_file);
166 return 0;
167 }
168 return logApiFailure(FERS_LOG_FATAL, "Failed to generate KML file", 1);
169 }
170
171 void configureThreadCount(const core::Config& config)
172 {
173 if (fers_set_thread_count(config.num_threads) == 0)
174 {
175 return;
176 }
177 char* err = fers_get_last_error_message();
178 log(FERS_LOG_ERROR, "Failed to set number of threads: {}", (err != nullptr) ? err : "Unknown error");
179 fers_free_string(err);
180 }
181
182 int runSimulation(fers_context_t* context)
183 {
184 log(FERS_LOG_INFO, "Starting simulation...");
185 if (fers_run_simulation(context, nullptr, nullptr) != 0)
186 {
187 return logApiFailure(FERS_LOG_FATAL, "Simulation run failed", 1);
188 }
189 log(FERS_LOG_INFO, "Simulation completed successfully.");
190 return 0;
191 }
192}
193
194namespace core
195{
196 int runCli(const int argc, char* argv[])
197 {
198 const auto config_result = parseArguments(argc, argv);
199 if (!config_result)
200 {
201 if (!isParseExit(config_result.error()))
202 {
203 std::cerr << "[ERROR] Argument parsing error: " << config_result.error() << '\n';
204 return 1;
205 }
206 return 0;
207 }
208
209 const auto& config = config_result.value();
210
211 if (configureLogging(config) != 0)
212 {
213 return 1;
214 }
215
216 log(FERS_LOG_INFO, "FERS CLI started. Using libfers backend.");
217
219 "Running FERS with arguments: script_file={}, log_level={}, num_threads={}, validate={}, log_file={}",
220 config.script_file, getLevelString(config.log_level), config.num_threads, config.validate,
221 config.log_file.value_or("None"));
222
223 const std::filesystem::path final_out_dir = resolveOutputDir(config.script_file, config.output_dir);
224
225 ContextHandle context(fers_context_create());
226 if (context.get() == nullptr)
227 {
228 log(FERS_LOG_FATAL, "Failed to create FERS simulation context.");
229 return 1;
230 }
231
232 if (loadScenario(context.get(), config) != 0)
233 {
234 return 1;
235 }
236
237 if (configureVita49Output(context.get(), config) != 0)
238 {
239 return 1;
240 }
241
242 if (setOutputDirectory(context.get(), final_out_dir) != 0)
243 {
244 return 1;
245 }
246
247 if (config.generate_kml)
248 {
249 return generateKml(context.get(), config, final_out_dir);
250 }
251
252 configureThreadCount(config);
253
254 return runSimulation(context.get());
255 }
256}
int fers_set_vita49_queue_depth(fers_context_t *context, uint32_t queue_depth)
Sets the bounded FERS VITA 49.2 sender queue depth in packets.
void fers_log(fers_log_level_t level, const char *message)
Submits a log message to the library's unified logging system.
Definition api.cpp:392
int fers_load_scenario_from_xml_file(fers_context_t *context, const char *xml_filepath, int validate)
Loads a scenario into the context from a FERS XML file.
Definition api.cpp:605
char * fers_get_last_error_message()
Retrieves the last error message that occurred on the current thread.
Definition api.cpp:1084
int fers_enable_vita49_udp_output(fers_context_t *context, const char *host, uint16_t port)
Enables the FERS VITA 49.2 UDP streaming profile for a context.
int fers_generate_kml(const fers_context_t *context, const char *output_kml_filepath)
Generates a KML file for visualizing the scenario in the context.
Definition api.cpp:1241
fers_log_level_t
Log levels for the FERS library.
Definition api.h:193
@ FERS_LOG_FATAL
Fatal logging for unrecoverable failures.
Definition api.h:199
@ FERS_LOG_DEBUG
Debug-level diagnostic logging.
Definition api.h:195
@ FERS_LOG_ERROR
Error logging for failed operations.
Definition api.h:198
@ FERS_LOG_INFO
Informational logging.
Definition api.h:196
@ FERS_LOG_TRACE
Trace-level diagnostic logging.
Definition api.h:194
@ FERS_LOG_WARNING
Warning logging for recoverable issues.
Definition api.h:197
int fers_set_output_directory(fers_context_t *context, const char *out_dir)
Sets the output directory for simulation results.
Definition api.cpp:419
int fers_set_vita49_epoch_unix_nanoseconds(fers_context_t *context, uint64_t epoch_unix_nanoseconds)
Sets a deterministic FERS VITA 49.2 stream epoch as Unix nanoseconds.
void fers_context_destroy(fers_context_t *context)
Destroys a FERS simulation context and releases all associated memory.
Definition api.cpp:118
fers_context_t * fers_context_create()
Creates a new FERS simulation context.
Definition api.cpp:97
int fers_run_simulation(fers_context_t *context, fers_progress_callback_t callback, void *user_data)
Runs the simulation defined in the provided context.
Definition api.cpp:1211
void fers_free_string(char *str)
Frees a string that was allocated and returned by the libfers API.
Definition api.cpp:1109
int fers_configure_logging(fers_log_level_t level, const char *log_file_path)
Configures the internal logger.
Definition api.cpp:353
int fers_set_vita49_fullscale(fers_context_t *context, double fullscale)
Sets the fixed ADC full-scale value used by the FERS VITA 49.2 int16 IQ profile.
Definition api.cpp:505
int fers_set_vita49_max_udp_payload(fers_context_t *context, uint16_t max_udp_payload)
Sets the FERS VITA 49.2 UDP profile maximum payload size in bytes.
int fers_set_thread_count(unsigned num_threads)
Sets the number of worker threads for the simulation.
Definition api.cpp:400
Command-line argument parsing utilities for the application.
std::filesystem::path resolveKmlOutputPath(const std::string &script_file, const std::filesystem::path &final_output_dir, const std::optional< std::string > &kml_file) noexcept
Resolves the KML output file path from CLI arguments and output directory.
Definition cli_paths.cpp:25
std::filesystem::path resolveOutputDir(const std::string &script_file, const std::optional< std::string > &output_dir) noexcept
Resolves the final simulation output directory from CLI arguments.
Definition cli_paths.cpp:8
std::expected< Config, std::string > parseArguments(const int argc, char *argv[]) noexcept
Parses command-line arguments.
void log(Level level, const std::source_location &location, const std::string &formatStr, Args &&... args) noexcept
Logs a formatted message with a specific log level and source location.
Definition logging.h:177
std::string getLevelString(const Level level) noexcept
Converts a log level enum value to its string representation.
Definition logging.h:144
Configuration structure for the application.
Definition arg_parser.h:31
std::string script_file
Path to the script file.
Definition arg_parser.h:32
fers_log_level_t log_level
Logging level.
Definition arg_parser.h:33
std::optional< std::string > output_dir
Optional output directory for simulation results.
Definition arg_parser.h:39
bool validate
Validate the input .fersxml file by default.
Definition arg_parser.h:35
std::optional< uint64_t > vita49_epoch_unix_nanoseconds
Optional deterministic VITA epoch.
Definition arg_parser.h:44
std::optional< double > vita49_fullscale
Fixed ADC full-scale for VITA int16 IQ output.
Definition arg_parser.h:43
std::optional< uint32_t > vita49_queue_depth
Optional VITA sender queue depth in packets.
Definition arg_parser.h:46
std::optional< std::string > kml_file
Optional specific file path for KML output.
Definition arg_parser.h:38
bool generate_kml
Optional flag to generate KML visualization output.
Definition arg_parser.h:37
std::string vita49_host
VITA 49.2 UDP destination host.
Definition arg_parser.h:41
bool vita49_enabled
True when VITA 49.2 UDP output is selected.
Definition arg_parser.h:40
unsigned num_threads
Number of threads to use.
Definition arg_parser.h:34
std::optional< uint16_t > vita49_max_udp_payload
Optional VITA UDP payload cap in bytes.
Definition arg_parser.h:45
uint16_t vita49_port
VITA 49.2 UDP destination port.
Definition arg_parser.h:42
std::optional< std::string > log_file
Optional log file path for logging output.
Definition arg_parser.h:36