23#include <unordered_map>
35 bool isValidLogFileExtension(
const std::string& filePath)
noexcept
37 static const std::vector<std::string> VALID_EXTENSIONS = {
".log",
".txt"};
38 std::string extension = std::filesystem::path(filePath).extension().string();
39 std::ranges::transform(extension, extension.begin(), tolower);
40 return std::ranges::find(VALID_EXTENSIONS, extension) != VALID_EXTENSIONS.end();
49 std::optional<fers_log_level_t> parseLogLevel(
const std::string& level)
noexcept
51 static const std::unordered_map<std::string, fers_log_level_t> LEVEL_MAP = {
55 if (
const auto it = LEVEL_MAP.find(level); it != LEVEL_MAP.end())
62 bool isUnsignedDecimal(
const std::string& value)
noexcept
64 return !value.empty() &&
65 std::ranges::all_of(value, [](
const unsigned char ch) {
return std::isdigit(ch) != 0; });
68 std::expected<std::uint64_t, std::string> parseUnsigned64(
const std::string& value,
69 const std::string& field_name)
noexcept
71 if (!isUnsignedDecimal(value))
73 return std::unexpected(field_name +
" must be an unsigned decimal integer");
77 std::size_t consumed = 0;
78 const auto parsed = std::stoull(value, &consumed, 10);
79 if (consumed != value.size())
81 return std::unexpected(field_name +
" must be an unsigned decimal integer");
83 return static_cast<std::uint64_t
>(parsed);
85 catch (
const std::exception&)
87 return std::unexpected(field_name +
" is out of range");
91 std::expected<double, std::string> parsePositiveReal(
const std::string& value,
92 const std::string& field_name)
noexcept
96 std::size_t consumed = 0;
97 const double parsed = std::stod(value, &consumed);
98 if (consumed != value.size() || !std::isfinite(parsed) || parsed <= 0.0)
100 return std::unexpected(field_name +
" must be a positive real number");
104 catch (
const std::exception&)
106 return std::unexpected(field_name +
" must be a positive real number");
110 std::expected<void, std::string> handleVita49Endpoint(
const std::string& value,
core::Config& config)
noexcept
112 const std::size_t separator = value.rfind(
':');
113 if (separator == std::string::npos || separator == 0 || separator + 1 == value.size())
115 return std::unexpected(
"Invalid VITA49 endpoint: expected host:port");
118 const std::string host = value.substr(0, separator);
119 const std::string port_text = value.substr(separator + 1);
120 if (host.find(
':') != std::string::npos)
122 return std::unexpected(
"Invalid VITA49 endpoint: expected host:port");
125 const auto parsed_port = parseUnsigned64(port_text,
"VITA49 port");
128 return std::unexpected(parsed_port.error());
130 if (*parsed_port == 0 || *parsed_port > std::numeric_limits<std::uint16_t>::max())
132 return std::unexpected(
"VITA49 port must be in the range 1..65535");
135 config.vita49_enabled =
true;
136 config.vita49_host = host;
137 config.vita49_port =
static_cast<std::uint16_t
>(*parsed_port);
141 std::expected<void, std::string> handleVita49Fullscale(
const std::string& value,
core::Config& config)
noexcept
143 const auto parsed = parsePositiveReal(value,
"VITA49 fullscale");
146 return std::unexpected(parsed.error());
148 config.vita49_fullscale = *parsed;
152 std::expected<void, std::string> handleVita49Epoch(
const std::string& value,
core::Config& config)
noexcept
154 constexpr std::uint64_t nanoseconds_per_second = 1'000'000'000ULL;
155 constexpr std::uint64_t max_vrt_utc_epoch_ns =
156 static_cast<std::uint64_t
>(std::numeric_limits<std::uint32_t>::max()) * nanoseconds_per_second +
157 (nanoseconds_per_second - 1ULL);
158 const auto parsed = parseUnsigned64(value,
"VITA49 epoch");
161 return std::unexpected(parsed.error());
163 if (*parsed > max_vrt_utc_epoch_ns)
165 return std::unexpected(
"VITA49 epoch must fit the VRT 32-bit UTC seconds timestamp field");
167 config.vita49_epoch_unix_nanoseconds = *parsed;
171 std::expected<void, std::string> handleVita49MaxUdpPayload(
const std::string& value,
core::Config& config)
noexcept
173 const auto parsed = parseUnsigned64(value,
"VITA49 max UDP payload");
176 return std::unexpected(parsed.error());
178 if (*parsed < 64u || *parsed > 65'507u)
180 return std::unexpected(
"VITA49 max UDP payload must be between 64 and 65507 bytes");
182 config.vita49_max_udp_payload =
static_cast<std::uint16_t
>(*parsed);
186 std::expected<void, std::string> handleVita49QueueDepth(
const std::string& value,
core::Config& config)
noexcept
188 const auto parsed = parseUnsigned64(value,
"VITA49 queue depth");
191 return std::unexpected(parsed.error());
193 if (*parsed == 0u || *parsed > std::numeric_limits<std::uint32_t>::max())
195 return std::unexpected(
"VITA49 queue depth must be in the range 1..4294967295");
197 config.vita49_queue_depth =
static_cast<std::uint32_t
>(*parsed);
201 using ConfigValueHandler = std::expected<void, std::string> (*)(
const std::string&,
core::Config&)
noexcept;
203 std::optional<ConfigValueHandler> valueOptionHandler(
const std::string& arg)
noexcept
205 if (arg ==
"--vita49")
207 return handleVita49Endpoint;
209 if (arg ==
"--vita49-fullscale")
211 return handleVita49Fullscale;
213 if (arg ==
"--vita49-epoch")
215 return handleVita49Epoch;
217 if (arg ==
"--vita49-max-udp-payload")
219 return handleVita49MaxUdpPayload;
221 if (arg ==
"--vita49-queue-depth")
223 return handleVita49QueueDepth;
228 std::expected<void, std::string> validateVita49Options(
const core::Config& config)
noexcept
230 if (!config.vita49_enabled)
232 if (config.vita49_fullscale.has_value())
234 return std::unexpected(
"--vita49-fullscale requires --vita49");
236 if (config.vita49_epoch_unix_nanoseconds.has_value())
238 return std::unexpected(
"--vita49-epoch requires --vita49");
240 if (config.vita49_max_udp_payload.has_value())
242 return std::unexpected(
"--vita49-max-udp-payload requires --vita49");
244 if (config.vita49_queue_depth.has_value())
246 return std::unexpected(
"--vita49-queue-depth requires --vita49");
250 if (!config.vita49_fullscale.has_value())
252 return std::unexpected(
"--vita49-fullscale is required when --vita49 is used");
264 std::expected<void, std::string> handleLogLevel(
const std::string& arg,
core::Config& config)
noexcept
266 const std::string level_str = arg.substr(12);
267 if (
const auto level = parseLogLevel(level_str))
269 config.log_level = *level;
273 std::cerr <<
"[ERROR] Invalid log level '" << level_str <<
"'\n";
274 return std::unexpected(
"Invalid log level: " + level_str);
285 std::expected<void, std::string> handleLogFile(
const std::string& arg,
core::Config& config)
noexcept
287 std::string
const log_file_path = arg.substr(11);
288 if (isValidLogFileExtension(log_file_path))
290 config.log_file = log_file_path;
294 std::cerr <<
"[ERROR] Invalid log file extension. Must be .log or .txt\n";
295 return std::unexpected(
"Invalid log file extension: " + log_file_path);
306 std::expected<void, std::string> handleNumThreads(
const std::string& arg,
core::Config& config)
noexcept
310 const int requested_threads = std::stoi(arg.substr(3));
311 if (requested_threads <= 0)
313 return std::unexpected(
"Number of threads must be greater than 0");
316 config.num_threads =
static_cast<unsigned>(requested_threads);
317 if (
const unsigned max_threads = std::thread::hardware_concurrency();
318 max_threads > 0 && config.num_threads > max_threads)
320 std::cerr <<
"[WARNING] Thread count exceeds available processors. Clamping.\n";
321 config.num_threads = max_threads;
325 catch (
const std::exception&)
327 return std::unexpected(
"Invalid number of threads specified.");
340 std::expected<void, std::string> handleArgument(
const std::string& arg,
core::Config& config,
bool& scriptFileSet,
341 const char* programName)
noexcept
343 if (arg ==
"--help" || arg ==
"-h")
346 return std::unexpected(
"Help requested.");
348 if (arg ==
"--version" || arg ==
"-v")
351 return std::unexpected(
"Version requested.");
353 if (arg.rfind(
"--log-level=", 0) == 0)
355 return handleLogLevel(arg, config);
357 if (arg.rfind(
"--log-file=", 0) == 0)
359 return handleLogFile(arg, config);
361 if (arg.rfind(
"-n=", 0) == 0)
363 return handleNumThreads(arg, config);
365 if (arg.rfind(
"--out-dir=", 0) == 0)
367 config.output_dir = arg.substr(10);
370 if (arg ==
"--no-validate")
372 config.validate =
false;
377 config.generate_kml =
true;
380 if (arg.rfind(
"--kml=", 0) == 0)
382 config.generate_kml =
true;
383 config.kml_file = arg.substr(6);
386 if (arg[0] !=
'-' && !scriptFileSet)
388 config.script_file = arg;
389 scriptFileSet =
true;
393 std::cerr <<
"[ERROR] Unrecognized option: '" << arg <<
"'\n";
394 return std::unexpected(
"Unrecognized argument: " + arg);
404 std::cout <<
"/------------------------------------------------\\\n"
405 <<
"| FERS - The Flexible Extensible Radar Simulator |\n"
406 << std::format(
"| Version {:<40}|\n", version)
407 <<
"\\------------------------------------------------/\n"
408 <<
"Usage: " << programName << R
"( <scriptfile> [options]
411 --help, -h Show this help message and exit
412 --version, -v Show version information and exit
413 --no-validate Disable XML schema validation before running.
414 --kml[=<file>] Generate a KML visualization of the scenario and exit. If a filename
415 is provided, it will be used. Otherwise, it defaults to the scenario
416 name with a .kml extension in the output directory.
417 --out-dir=<dir> Set the output directory for simulation results and default KML output.
418 Defaults to the directory containing the script file.
419 --vita49 host:port Stream receiver output as the FERS VITA 49.2 UDP profile.
420 --vita49-fullscale <x> Set required positive fixed ADC full-scale for VITA int16 IQ output.
421 --vita49-epoch <ns> Set optional deterministic VITA epoch as Unix nanoseconds.
422 --vita49-max-udp-payload <bytes>
423 Set optional VITA UDP payload cap, 64..65507 bytes.
424 --vita49-queue-depth <packets>
425 Set optional VITA sender queue depth, greater than zero.
426 --log-level=<level> Set the logging level (TRACE, DEBUG, INFO, WARNING, ERROR, FATAL)
427 --log-file=<file> Log output to the specified .log or .txt file as well as the console.
428 -n=<threads> Number of threads to use
431 <scriptfile> Path to the simulation script file (XML)
435 << R"( simulation.fersxml --out-dir=./results --log-level=DEBUG -n=4
437This program runs radar simulations based on an XML script file.
438Make sure the script file follows the correct format to avoid errors.
447 <<
"/------------------------------------------------\\\n"
448 <<
"| FERS - The Flexible Extensible Radar Simulator |\n"
449 << std::format(
"| Version {:<40}|\n", version)
450 <<
"| Authors: Marc Brooker, Michael Inggs, |\n"
451 <<
"| and FERS Contributors |\n"
452 <<
"\\------------------------------------------------/\n\n";
455 std::expected<Config, std::string>
parseArguments(
const int argc,
char* argv[])
noexcept
458 bool script_file_set =
false;
463 return std::unexpected(
"No arguments provided.");
466 for (
int i = 1; i < argc; ++i)
468 const std::string arg = argv[i];
469 const auto require_value = [&](
const std::string& option) -> std::expected<std::string, std::string>
473 return std::unexpected(option +
" requires a value");
476 return std::string{argv[i]};
479 if (
const auto handler = valueOptionHandler(arg))
481 const auto value = require_value(arg);
484 return std::unexpected(value.error());
486 if (
const auto result = (*handler)(*value, config); !result)
488 return std::unexpected(result.error());
493 if (
const auto result = handleArgument(arg, config, script_file_set, argv[0]); !result)
495 return std::unexpected(result.error());
499 if (!script_file_set)
501 return std::unexpected(
"No script file provided.");
503 if (
const auto result = validateVita49Options(config); !result)
505 return std::unexpected(result.error());
@ FERS_LOG_FATAL
Fatal logging for unrecoverable failures.
@ FERS_LOG_DEBUG
Debug-level diagnostic logging.
@ FERS_LOG_ERROR
Error logging for failed operations.
@ FERS_LOG_INFO
Informational logging.
@ FERS_LOG_TRACE
Trace-level diagnostic logging.
@ FERS_LOG_WARNING
Warning logging for recoverable issues.
const char * fers_get_version(void)
Returns the library version string.
Command-line argument parsing utilities for the application.
void showVersion() noexcept
Displays the version information.
std::expected< Config, std::string > parseArguments(const int argc, char *argv[]) noexcept
Parses command-line arguments.
void showHelp(const char *programName) noexcept
Displays the help message.
Configuration structure for the application.