refactor(cli): migrate runtime and simulator parsers to CLI11

Replace hand-rolled argument parsing with CLI11-backed parse paths for streamer runtime config and simulator runtime options.

Simulator help text is now generated from CLI11 app definitions to keep parser/help output in sync, while preserving legacy validation messages and exit semantics used by automation.
This commit is contained in:
2026-03-05 23:52:09 +08:00
parent 5e430ffa83
commit 7413590519
5 changed files with 974 additions and 723 deletions
+71
View File
@@ -0,0 +1,71 @@
# Prerequisites
*.d
# Compiled Object files
*.slo
*.lo
*.o
*.obj
# Precompiled Headers
*.gch
*.pch
# Linker files
*.ilk
# Debugger Files
*.pdb
# Compiled Dynamic libraries
*.so
*.dylib
*.dll
*.so.*
# Fortran module files
*.mod
*.smod
# Compiled Static libraries
*.lai
*.la
*.a
*.lib
# Executables
*.exe
*.out
*.app
# Build directories
build/
Build/
build-*/
# CMake generated files
CMakeFiles/
CMakeCache.txt
cmake_install.cmake
Makefile
install_manifest.txt
compile_commands.json
# Temporary files
*.tmp
*.log
*.bak
*.swp
# vcpkg
vcpkg_installed/
# debug information files
*.dwo
# test output & cache
Testing/
.cache/
# local evidence artifacts generated by standalone scripts
.sisyphus/
+8
View File
@@ -8,6 +8,10 @@ set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
find_package(Threads REQUIRED) find_package(Threads REQUIRED)
if (EXISTS "${CMAKE_CURRENT_LIST_DIR}/lib/CLI11/CMakeLists.txt")
add_subdirectory("${CMAKE_CURRENT_LIST_DIR}/lib/CLI11" "${CMAKE_CURRENT_BINARY_DIR}/vendor/cli11")
endif()
find_package(cppzmq QUIET) find_package(cppzmq QUIET)
find_package(ZeroMQ QUIET) find_package(ZeroMQ QUIET)
find_package(spdlog REQUIRED) find_package(spdlog REQUIRED)
@@ -76,6 +80,10 @@ elseif (TARGET spdlog)
list(APPEND CVMAP_STREAMER_LINK_DEPS spdlog) list(APPEND CVMAP_STREAMER_LINK_DEPS spdlog)
endif() endif()
if (TARGET CLI11::CLI11)
list(APPEND CVMAP_STREAMER_LINK_DEPS CLI11::CLI11)
endif()
target_link_libraries(cvmmap_streamer_common PUBLIC ${CVMAP_STREAMER_LINK_DEPS}) target_link_libraries(cvmmap_streamer_common PUBLIC ${CVMAP_STREAMER_LINK_DEPS})
function(add_cvmmap_binary target source) function(add_cvmmap_binary target source)
+20 -3
View File
@@ -4,6 +4,7 @@
#include <expected> #include <expected>
#include <optional> #include <optional>
#include <string> #include <string>
#include <string_view>
#include "cvmmap_streamer/ipc/contracts.hpp" #include "cvmmap_streamer/ipc/contracts.hpp"
@@ -30,7 +31,23 @@ struct RuntimeConfig {
std::uint32_t payload_size_bytes() const; std::uint32_t payload_size_bytes() const;
}; };
std::expected<RuntimeConfig, std::string> parse_runtime_config(int argc, char **argv); enum class ParseStatus {
void print_help(); Ok,
Help,
Error,
};
} struct ParseResult {
ParseStatus status{ParseStatus::Ok};
RuntimeConfig config{};
std::string message{};
int exit_code{0};
};
std::expected<RuntimeConfig, std::string> parse_runtime_config(int argc,
char **argv);
void print_help();
ParseResult parse_runtime_config_with_cli11(
int argc, char **argv, std::string_view executable_name = "cvmmap_sim");
} // namespace cvmmap_streamer::sim
+249 -214
View File
@@ -1,70 +1,131 @@
#include "cvmmap_streamer/config/runtime_config.hpp" #include "cvmmap_streamer/config/runtime_config.hpp"
#include <CLI/CLI.hpp>
#include <array>
#include <charconv> #include <charconv>
#include <cstdint> #include <cstdint>
#include <limits> #include <limits>
#include <optional>
#include <sstream> #include <sstream>
#include <string>
#include <string_view> #include <string_view>
#include <utility> #include <utility>
#include <vector>
namespace cvmmap_streamer { namespace cvmmap_streamer {
namespace { namespace {
std::expected<std::string_view, std::string> std::string trim_copy(std::string value) {
next_value(int argc, char **argv, int &index, std::string_view flag_name) { auto not_space = [](unsigned char c) {
if (index + 1 >= argc) { return c != ' ' && c != '\t' && c != '\n' && c != '\r';
return std::unexpected("missing value for " + std::string(flag_name)); };
while (!value.empty() &&
!not_space(static_cast<unsigned char>(value.front()))) {
value.erase(value.begin());
} }
++index; while (!value.empty() &&
return std::string_view{argv[index]}; !not_space(static_cast<unsigned char>(value.back()))) {
value.pop_back();
}
return value;
} }
std::expected<std::uint32_t, std::string> parse_u32(std::string_view raw, std::string_view flag_name) { std::string normalize_cli_error(std::string raw_message) {
if (raw_message.find("The following argument was not expected:") !=
std::string::npos) {
const auto pos = raw_message.find(':');
if (pos != std::string::npos && pos + 1 < raw_message.size()) {
return "unknown argument: " + trim_copy(raw_message.substr(pos + 1));
}
return "unknown argument";
}
constexpr std::array<std::string_view, 17> kFlags{"--codec",
"--run-mode",
"--shm-name",
"--zmq-endpoint",
"--rtmp-url",
"--rtmp-mode",
"--rtp-endpoint",
"--rtp-payload-type",
"--rtp-sdp",
"--sdp",
"--queue-size",
"--gop",
"--b-frames",
"--realtime-sync",
"--force-idr-on-reset",
"--ingest-max-frames",
"--ingest-idle-timeout-ms"};
if (raw_message.find("requires at least") != std::string::npos ||
raw_message.find("requires ") != std::string::npos) {
for (const auto flag : kFlags) {
if (raw_message.find(flag) != std::string::npos) {
return "missing value for " + std::string(flag);
}
}
}
return trim_copy(std::move(raw_message));
}
std::expected<std::uint32_t, std::string>
parse_u32(std::string_view raw, std::string_view flag_name) {
std::uint32_t value{0}; std::uint32_t value{0};
const auto *begin = raw.data(); const auto *begin = raw.data();
const auto *end = raw.data() + raw.size(); const auto *end = raw.data() + raw.size();
const auto result = std::from_chars(begin, end, value, 10); const auto result = std::from_chars(begin, end, value, 10);
if (result.ec != std::errc{} || result.ptr != end) { if (result.ec != std::errc{} || result.ptr != end) {
return std::unexpected("invalid value for " + std::string(flag_name) + ": '" + std::string(raw) + "'"); return std::unexpected("invalid value for " + std::string(flag_name) +
": '" + std::string(raw) + "'");
} }
return value; return value;
} }
std::expected<std::uint16_t, std::string> parse_u16(std::string_view raw, std::string_view flag_name) { std::expected<std::uint16_t, std::string>
parse_u16(std::string_view raw, std::string_view flag_name) {
std::uint16_t value{0}; std::uint16_t value{0};
const auto *begin = raw.data(); const auto *begin = raw.data();
const auto *end = raw.data() + raw.size(); const auto *end = raw.data() + raw.size();
const auto result = std::from_chars(begin, end, value, 10); const auto result = std::from_chars(begin, end, value, 10);
if (result.ec != std::errc{} || result.ptr != end) { if (result.ec != std::errc{} || result.ptr != end) {
return std::unexpected("invalid value for " + std::string(flag_name) + ": '" + std::string(raw) + "'"); return std::unexpected("invalid value for " + std::string(flag_name) +
": '" + std::string(raw) + "'");
} }
return value; return value;
} }
std::expected<std::size_t, std::string> parse_size(std::string_view raw, std::string_view flag_name) { std::expected<std::size_t, std::string> parse_size(std::string_view raw,
std::string_view flag_name) {
unsigned long long parsed{0}; unsigned long long parsed{0};
const auto *begin = raw.data(); const auto *begin = raw.data();
const auto *end = raw.data() + raw.size(); const auto *end = raw.data() + raw.size();
const auto result = std::from_chars(begin, end, parsed, 10); const auto result = std::from_chars(begin, end, parsed, 10);
if (result.ec != std::errc{} || result.ptr != end) { if (result.ec != std::errc{} || result.ptr != end) {
return std::unexpected("invalid value for " + std::string(flag_name) + ": '" + std::string(raw) + "'"); return std::unexpected("invalid value for " + std::string(flag_name) +
": '" + std::string(raw) + "'");
} }
if (parsed > static_cast<unsigned long long>(std::numeric_limits<std::size_t>::max())) { if (parsed > static_cast<unsigned long long>(
return std::unexpected("value out of range for " + std::string(flag_name) + ": '" + std::string(raw) + "'"); std::numeric_limits<std::size_t>::max())) {
return std::unexpected("value out of range for " + std::string(flag_name) +
": '" + std::string(raw) + "'");
} }
return static_cast<std::size_t>(parsed); return static_cast<std::size_t>(parsed);
} }
std::expected<bool, std::string> parse_bool(std::string_view raw, std::string_view flag_name) { std::expected<bool, std::string> parse_bool(std::string_view raw,
std::string_view flag_name) {
if (raw == "true" || raw == "1") { if (raw == "true" || raw == "1") {
return true; return true;
} }
if (raw == "false" || raw == "0") { if (raw == "false" || raw == "0") {
return false; return false;
} }
return std::unexpected( return std::unexpected("invalid value for " + std::string(flag_name) + ": '" +
"invalid value for " + std::string(flag_name) + ": '" + std::string(raw) + "' (expected: true|false|1|0)"); std::string(raw) + "' (expected: true|false|1|0)");
} }
std::expected<CodecType, std::string> parse_codec(std::string_view raw) { std::expected<CodecType, std::string> parse_codec(std::string_view raw) {
@@ -74,7 +135,8 @@ namespace {
if (raw == "h265") { if (raw == "h265") {
return CodecType::H265; return CodecType::H265;
} }
return std::unexpected("invalid codec: '" + std::string(raw) + "' (expected: h264|h265)"); return std::unexpected("invalid codec: '" + std::string(raw) +
"' (expected: h264|h265)");
} }
std::expected<RunMode, std::string> parse_run_mode(std::string_view raw) { std::expected<RunMode, std::string> parse_run_mode(std::string_view raw) {
@@ -84,7 +146,8 @@ namespace {
if (raw == "ingest") { if (raw == "ingest") {
return RunMode::Ingest; return RunMode::Ingest;
} }
return std::unexpected("invalid run mode: '" + std::string(raw) + "' (expected: pipeline|ingest)"); return std::unexpected("invalid run mode: '" + std::string(raw) +
"' (expected: pipeline|ingest)");
} }
std::expected<RtmpMode, std::string> parse_rtmp_mode(std::string_view raw) { std::expected<RtmpMode, std::string> parse_rtmp_mode(std::string_view raw) {
@@ -94,23 +157,29 @@ namespace {
if (raw == "domestic") { if (raw == "domestic") {
return RtmpMode::Domestic; return RtmpMode::Domestic;
} }
return std::unexpected("invalid rtmp mode: '" + std::string(raw) + "' (expected: enhanced|domestic)"); return std::unexpected("invalid rtmp mode: '" + std::string(raw) +
"' (expected: enhanced|domestic)");
} }
std::expected<std::pair<std::string, std::uint16_t>, std::string> parse_rtp_endpoint(std::string_view endpoint) { std::expected<std::pair<std::string, std::uint16_t>, std::string>
parse_rtp_endpoint(std::string_view endpoint) {
if (endpoint.empty()) { if (endpoint.empty()) {
return std::unexpected("invalid RTP config: --rtp-endpoint must not be empty"); return std::unexpected(
"invalid RTP config: --rtp-endpoint must not be empty");
} }
const auto colon = endpoint.rfind(':'); const auto colon = endpoint.rfind(':');
if (colon == std::string_view::npos || colon == 0 || colon + 1 >= endpoint.size()) { if (colon == std::string_view::npos || colon == 0 ||
return std::unexpected("invalid RTP config: --rtp-endpoint must be in '<host>:<port>' format"); colon + 1 >= endpoint.size()) {
return std::unexpected(
"invalid RTP config: --rtp-endpoint must be in '<host>:<port>' format");
} }
const auto host = endpoint.substr(0, colon); const auto host = endpoint.substr(0, colon);
const auto port = endpoint.substr(colon + 1); const auto port = endpoint.substr(colon + 1);
if (host.empty()) { if (host.empty()) {
return std::unexpected("invalid RTP config: --rtp-endpoint host must not be empty"); return std::unexpected(
"invalid RTP config: --rtp-endpoint host must not be empty");
} }
auto parsed_port = parse_u16(port, "--rtp-endpoint"); auto parsed_port = parse_u16(port, "--rtp-endpoint");
@@ -118,17 +187,16 @@ namespace {
return std::unexpected(parsed_port.error()); return std::unexpected(parsed_port.error());
} }
if (*parsed_port == 0) { if (*parsed_port == 0) {
return std::unexpected("invalid RTP config: --rtp-endpoint port must be in range [1,65535]"); return std::unexpected(
"invalid RTP config: --rtp-endpoint port must be in range [1,65535]");
} }
return std::pair{std::string(host), *parsed_port}; return std::pair{std::string(host), *parsed_port};
} }
} } // namespace
RuntimeConfig RuntimeConfig::defaults() { RuntimeConfig RuntimeConfig::defaults() { return RuntimeConfig{}; }
return RuntimeConfig{};
}
std::string_view to_string(CodecType codec) { std::string_view to_string(CodecType codec) {
switch (codec) { switch (codec) {
@@ -163,302 +231,256 @@ std::string_view to_string(RtmpMode mode) {
} }
} }
std::expected<RuntimeConfig, std::string> parse_runtime_config(int argc, char **argv) { std::expected<RuntimeConfig, std::string> parse_runtime_config(int argc,
char **argv) {
RuntimeConfig config = RuntimeConfig::defaults(); RuntimeConfig config = RuntimeConfig::defaults();
for (int i = 1; i < argc; ++i) { std::string codec_raw;
const std::string_view arg{argv[i]}; std::string run_mode_raw;
std::string shm_name_raw;
std::string zmq_endpoint_raw;
std::vector<std::string> rtmp_urls_raw;
std::string rtmp_mode_raw;
std::string rtp_endpoint_raw;
std::string rtp_payload_type_raw;
std::string rtp_sdp_raw;
std::string queue_size_raw;
std::string gop_raw;
std::string b_frames_raw;
std::string realtime_sync_raw;
std::string force_idr_on_reset_raw;
std::string ingest_max_frames_raw;
std::string ingest_idle_timeout_raw;
std::string ingest_consumer_delay_raw;
std::string snapshot_copy_delay_raw;
std::string emit_stall_raw;
if (arg == "--codec") { bool rtmp_enabled{false};
auto raw = next_value(argc, argv, i, arg); bool rtp_enabled{false};
if (!raw) { bool version_requested{false};
return std::unexpected(raw.error());
CLI::App app{"cvmmap-streamer runtime options"};
app.allow_extras(false);
app.set_help_flag("--help,-h", "show this message");
app.add_option("--codec", codec_raw);
app.add_option("--run-mode", run_mode_raw);
app.add_option("--shm-name", shm_name_raw);
app.add_option("--zmq-endpoint", zmq_endpoint_raw);
app.add_flag("--rtmp", rtmp_enabled);
app.add_option("--rtmp-url", rtmp_urls_raw);
app.add_option("--rtmp-mode", rtmp_mode_raw);
app.add_flag("--rtp", rtp_enabled);
app.add_option("--rtp-endpoint", rtp_endpoint_raw);
app.add_option("--rtp-payload-type", rtp_payload_type_raw);
auto *rtp_sdp = app.add_option("--rtp-sdp", rtp_sdp_raw);
app.add_option("--sdp", rtp_sdp_raw)->excludes(rtp_sdp);
app.add_option("--queue-size", queue_size_raw);
app.add_option("--gop", gop_raw);
app.add_option("--b-frames", b_frames_raw);
app.add_option("--realtime-sync", realtime_sync_raw);
app.add_option("--force-idr-on-reset", force_idr_on_reset_raw);
app.add_option("--ingest-max-frames", ingest_max_frames_raw);
app.add_option("--ingest-idle-timeout-ms", ingest_idle_timeout_raw);
app.add_option("--ingest-consumer-delay-ms", ingest_consumer_delay_raw);
app.add_option("--snapshot-copy-delay-us", snapshot_copy_delay_raw);
app.add_option("--emit-stall-ms", emit_stall_raw);
app.add_flag("--version", version_requested);
try {
app.parse(argc, argv);
} catch (const CLI::ParseError &e) {
return std::unexpected(normalize_cli_error(e.what()));
} }
auto codec = parse_codec(*raw);
if (!codec_raw.empty()) {
auto codec = parse_codec(codec_raw);
if (!codec) { if (!codec) {
return std::unexpected(codec.error()); return std::unexpected(codec.error());
} }
config.codec = *codec; config.codec = *codec;
continue;
} }
if (arg == "--run-mode") { if (!run_mode_raw.empty()) {
auto raw = next_value(argc, argv, i, arg); auto run_mode = parse_run_mode(run_mode_raw);
if (!raw) { if (!run_mode) {
return std::unexpected(raw.error()); return std::unexpected(run_mode.error());
} }
auto mode = parse_run_mode(*raw); config.run_mode = *run_mode;
if (!mode) {
return std::unexpected(mode.error());
}
config.run_mode = *mode;
continue;
} }
if (arg == "--shm-name") { if (!shm_name_raw.empty()) {
auto value = next_value(argc, argv, i, arg); config.input.shm_name = shm_name_raw;
if (!value) {
return std::unexpected(value.error());
}
config.input.shm_name = std::string(*value);
continue;
} }
if (arg == "--zmq-endpoint") { if (!zmq_endpoint_raw.empty()) {
auto value = next_value(argc, argv, i, arg); config.input.zmq_endpoint = zmq_endpoint_raw;
if (!value) {
return std::unexpected(value.error());
}
config.input.zmq_endpoint = std::string(*value);
continue;
} }
if (arg == "--rtmp") { config.outputs.rtmp.enabled = rtmp_enabled;
if (!rtmp_urls_raw.empty()) {
config.outputs.rtmp.enabled = true; config.outputs.rtmp.enabled = true;
continue; config.outputs.rtmp.urls = std::move(rtmp_urls_raw);
} }
if (arg == "--rtmp-url") { if (!rtmp_mode_raw.empty()) {
auto value = next_value(argc, argv, i, arg); auto mode = parse_rtmp_mode(rtmp_mode_raw);
if (!value) {
return std::unexpected(value.error());
}
config.outputs.rtmp.enabled = true;
config.outputs.rtmp.urls.emplace_back(*value);
continue;
}
if (arg == "--rtmp-mode") {
auto raw = next_value(argc, argv, i, arg);
if (!raw) {
return std::unexpected(raw.error());
}
auto mode = parse_rtmp_mode(*raw);
if (!mode) { if (!mode) {
return std::unexpected(mode.error()); return std::unexpected(mode.error());
} }
config.outputs.rtmp.mode = *mode; config.outputs.rtmp.mode = *mode;
continue;
} }
if (arg == "--rtp") { config.outputs.rtp.enabled = rtp_enabled;
config.outputs.rtp.enabled = true; if (!rtp_endpoint_raw.empty()) {
continue; auto endpoint = parse_rtp_endpoint(rtp_endpoint_raw);
}
if (arg == "--rtp-endpoint") {
auto value = next_value(argc, argv, i, arg);
if (!value) {
return std::unexpected(value.error());
}
auto endpoint = parse_rtp_endpoint(*value);
if (!endpoint) { if (!endpoint) {
return std::unexpected(endpoint.error()); return std::unexpected(endpoint.error());
} }
config.outputs.rtp.enabled = true; config.outputs.rtp.enabled = true;
config.outputs.rtp.endpoint = std::string(*value); config.outputs.rtp.endpoint = rtp_endpoint_raw;
config.outputs.rtp.host = endpoint->first; config.outputs.rtp.host = endpoint->first;
config.outputs.rtp.port = endpoint->second; config.outputs.rtp.port = endpoint->second;
continue;
} }
if (arg == "--rtp-payload-type") { if (!rtp_payload_type_raw.empty()) {
auto raw = next_value(argc, argv, i, arg); auto value = parse_u32(rtp_payload_type_raw, "--rtp-payload-type");
if (!raw) {
return std::unexpected(raw.error());
}
auto value = parse_u32(*raw, arg);
if (!value) { if (!value) {
return std::unexpected(value.error()); return std::unexpected(value.error());
} }
if (*value > std::numeric_limits<std::uint8_t>::max()) { if (*value > std::numeric_limits<std::uint8_t>::max()) {
return std::unexpected("value out of range for --rtp-payload-type: '" + std::string(*raw) + "'"); return std::unexpected("value out of range for --rtp-payload-type: '" +
rtp_payload_type_raw + "'");
} }
config.outputs.rtp.enabled = true; config.outputs.rtp.enabled = true;
config.outputs.rtp.payload_type = static_cast<std::uint8_t>(*value); config.outputs.rtp.payload_type = static_cast<std::uint8_t>(*value);
continue;
} }
if (arg == "--rtp-sdp" || arg == "--sdp") { if (!rtp_sdp_raw.empty()) {
auto value = next_value(argc, argv, i, arg);
if (!value) {
return std::unexpected(value.error());
}
if (value->empty()) {
return std::unexpected("invalid RTP config: " + std::string(arg) + " must not be empty");
}
config.outputs.rtp.enabled = true; config.outputs.rtp.enabled = true;
config.outputs.rtp.sdp_path = std::string(*value); config.outputs.rtp.sdp_path = rtp_sdp_raw;
continue;
} }
if (arg == "--queue-size") { if (!queue_size_raw.empty()) {
auto raw = next_value(argc, argv, i, arg); auto parsed = parse_size(queue_size_raw, "--queue-size");
if (!raw) {
return std::unexpected(raw.error());
}
auto parsed = parse_size(*raw, arg);
if (!parsed) { if (!parsed) {
return std::unexpected(parsed.error()); return std::unexpected(parsed.error());
} }
config.latency.queue_size = *parsed; config.latency.queue_size = *parsed;
continue;
} }
if (arg == "--gop") { if (!gop_raw.empty()) {
auto raw = next_value(argc, argv, i, arg); auto parsed = parse_u32(gop_raw, "--gop");
if (!raw) {
return std::unexpected(raw.error());
}
auto parsed = parse_u32(*raw, arg);
if (!parsed) { if (!parsed) {
return std::unexpected(parsed.error()); return std::unexpected(parsed.error());
} }
config.latency.gop = *parsed; config.latency.gop = *parsed;
continue;
} }
if (arg == "--b-frames") { if (!b_frames_raw.empty()) {
auto raw = next_value(argc, argv, i, arg); auto parsed = parse_u32(b_frames_raw, "--b-frames");
if (!raw) {
return std::unexpected(raw.error());
}
auto parsed = parse_u32(*raw, arg);
if (!parsed) { if (!parsed) {
return std::unexpected(parsed.error()); return std::unexpected(parsed.error());
} }
config.latency.b_frames = *parsed; config.latency.b_frames = *parsed;
continue;
} }
if (arg == "--realtime-sync") { if (!realtime_sync_raw.empty()) {
auto raw = next_value(argc, argv, i, arg); auto parsed = parse_bool(realtime_sync_raw, "--realtime-sync");
if (!raw) {
return std::unexpected(raw.error());
}
auto parsed = parse_bool(*raw, arg);
if (!parsed) { if (!parsed) {
return std::unexpected(parsed.error()); return std::unexpected(parsed.error());
} }
config.latency.realtime_sync = *parsed; config.latency.realtime_sync = *parsed;
continue;
} }
if (arg == "--force-idr-on-reset") { if (!force_idr_on_reset_raw.empty()) {
auto raw = next_value(argc, argv, i, arg); auto parsed = parse_bool(force_idr_on_reset_raw, "--force-idr-on-reset");
if (!raw) {
return std::unexpected(raw.error());
}
auto parsed = parse_bool(*raw, arg);
if (!parsed) { if (!parsed) {
return std::unexpected(parsed.error()); return std::unexpected(parsed.error());
} }
config.latency.force_idr_on_reset = *parsed; config.latency.force_idr_on_reset = *parsed;
continue;
} }
if (arg == "--ingest-max-frames") { if (!ingest_max_frames_raw.empty()) {
auto raw = next_value(argc, argv, i, arg); auto parsed = parse_u32(ingest_max_frames_raw, "--ingest-max-frames");
if (!raw) {
return std::unexpected(raw.error());
}
auto parsed = parse_u32(*raw, arg);
if (!parsed) { if (!parsed) {
return std::unexpected(parsed.error()); return std::unexpected(parsed.error());
} }
config.latency.ingest_max_frames = *parsed; config.latency.ingest_max_frames = *parsed;
continue;
} }
if (arg == "--ingest-idle-timeout-ms") { if (!ingest_idle_timeout_raw.empty()) {
auto raw = next_value(argc, argv, i, arg); auto parsed =
if (!raw) { parse_u32(ingest_idle_timeout_raw, "--ingest-idle-timeout-ms");
return std::unexpected(raw.error());
}
auto parsed = parse_u32(*raw, arg);
if (!parsed) { if (!parsed) {
return std::unexpected(parsed.error()); return std::unexpected(parsed.error());
} }
config.latency.ingest_idle_timeout_ms = *parsed; config.latency.ingest_idle_timeout_ms = *parsed;
continue;
} }
if (arg == "--ingest-consumer-delay-ms") { if (!ingest_consumer_delay_raw.empty()) {
auto raw = next_value(argc, argv, i, arg); auto parsed =
if (!raw) { parse_u32(ingest_consumer_delay_raw, "--ingest-consumer-delay-ms");
return std::unexpected(raw.error());
}
auto parsed = parse_u32(*raw, arg);
if (!parsed) { if (!parsed) {
return std::unexpected(parsed.error()); return std::unexpected(parsed.error());
} }
config.latency.ingest_consumer_delay_ms = *parsed; config.latency.ingest_consumer_delay_ms = *parsed;
continue;
} }
if (arg == "--snapshot-copy-delay-us") { if (!snapshot_copy_delay_raw.empty()) {
auto raw = next_value(argc, argv, i, arg); auto parsed =
if (!raw) { parse_u32(snapshot_copy_delay_raw, "--snapshot-copy-delay-us");
return std::unexpected(raw.error());
}
auto parsed = parse_u32(*raw, arg);
if (!parsed) { if (!parsed) {
return std::unexpected(parsed.error()); return std::unexpected(parsed.error());
} }
config.latency.snapshot_copy_delay_us = *parsed; config.latency.snapshot_copy_delay_us = *parsed;
continue;
} }
if (arg == "--emit-stall-ms") { if (!emit_stall_raw.empty()) {
auto raw = next_value(argc, argv, i, arg); auto parsed = parse_u32(emit_stall_raw, "--emit-stall-ms");
if (!raw) {
return std::unexpected(raw.error());
}
auto parsed = parse_u32(*raw, arg);
if (!parsed) { if (!parsed) {
return std::unexpected(parsed.error()); return std::unexpected(parsed.error());
} }
config.latency.emit_stall_ms = *parsed; config.latency.emit_stall_ms = *parsed;
continue;
}
if (arg == "--help" || arg == "-h" || arg == "--version") {
continue;
}
return std::unexpected("unknown argument: " + std::string(arg));
} }
return config; return config;
} }
std::expected<void, std::string> validate_runtime_config(const RuntimeConfig &config) { std::expected<void, std::string>
validate_runtime_config(const RuntimeConfig &config) {
if (config.input.shm_name.empty()) { if (config.input.shm_name.empty()) {
return std::unexpected("invalid input config: --shm-name must not be empty"); return std::unexpected(
"invalid input config: --shm-name must not be empty");
} }
if (config.input.zmq_endpoint.empty()) { if (config.input.zmq_endpoint.empty()) {
return std::unexpected("invalid input config: --zmq-endpoint must not be empty"); return std::unexpected(
"invalid input config: --zmq-endpoint must not be empty");
} }
if (config.outputs.rtmp.enabled && config.outputs.rtmp.urls.empty()) { if (config.outputs.rtmp.enabled && config.outputs.rtmp.urls.empty()) {
return std::unexpected("invalid RTMP config: --rtmp requires at least one --rtmp-url"); return std::unexpected(
"invalid RTMP config: --rtmp requires at least one --rtmp-url");
} }
for (const auto &url : config.outputs.rtmp.urls) { for (const auto &url : config.outputs.rtmp.urls) {
if (url.empty()) { if (url.empty()) {
return std::unexpected("invalid RTMP config: --rtmp-url must not be empty"); return std::unexpected(
"invalid RTMP config: --rtmp-url must not be empty");
} }
} }
if (config.outputs.rtmp.mode == RtmpMode::Domestic && config.codec != CodecType::H265) { if (config.outputs.rtmp.mode == RtmpMode::Domestic &&
return std::unexpected( config.codec != CodecType::H265) {
"invalid mode matrix: --rtmp-mode domestic requires --codec h265 (h264+domestic is unsupported)"); return std::unexpected("invalid mode matrix: --rtmp-mode domestic requires "
"--codec h265 (h264+domestic is unsupported)");
} }
if (config.outputs.rtp.enabled) { if (config.outputs.rtp.enabled) {
if (!config.outputs.rtp.endpoint || config.outputs.rtp.endpoint->empty()) { if (!config.outputs.rtp.endpoint || config.outputs.rtp.endpoint->empty()) {
return std::unexpected("invalid RTP config: --rtp requires --rtp-endpoint"); return std::unexpected(
"invalid RTP config: --rtp requires --rtp-endpoint");
} }
auto endpoint_validation = parse_rtp_endpoint(*config.outputs.rtp.endpoint); auto endpoint_validation = parse_rtp_endpoint(*config.outputs.rtp.endpoint);
@@ -466,13 +488,15 @@ std::expected<void, std::string> validate_runtime_config(const RuntimeConfig &co
return std::unexpected(endpoint_validation.error()); return std::unexpected(endpoint_validation.error());
} }
if (config.outputs.rtp.payload_type < 96 || config.outputs.rtp.payload_type > 127) { if (config.outputs.rtp.payload_type < 96 ||
return std::unexpected( config.outputs.rtp.payload_type > 127) {
"invalid RTP config: --rtp-payload-type must be in dynamic range [96,127]"); return std::unexpected("invalid RTP config: --rtp-payload-type must be "
"in dynamic range [96,127]");
} }
if (config.outputs.rtp.sdp_path && config.outputs.rtp.sdp_path->empty()) { if (config.outputs.rtp.sdp_path && config.outputs.rtp.sdp_path->empty()) {
return std::unexpected("invalid RTP config: --rtp-sdp/--sdp must not be empty"); return std::unexpected(
"invalid RTP config: --rtp-sdp/--sdp must not be empty");
} }
} }
@@ -485,11 +509,13 @@ std::expected<void, std::string> validate_runtime_config(const RuntimeConfig &co
} }
if (config.latency.b_frames > config.latency.gop) { if (config.latency.b_frames > config.latency.gop) {
return std::unexpected("invalid latency config: --b-frames must be <= --gop"); return std::unexpected(
"invalid latency config: --b-frames must be <= --gop");
} }
if (config.latency.ingest_idle_timeout_ms == 0) { if (config.latency.ingest_idle_timeout_ms == 0) {
return std::unexpected("invalid ingest config: --ingest-idle-timeout-ms must be >= 1"); return std::unexpected(
"invalid ingest config: --ingest-idle-timeout-ms must be >= 1");
} }
return {}; return {};
@@ -505,20 +531,29 @@ std::string summarize_runtime_config(const RuntimeConfig &config) {
ss << ", rtmp.mode=" << to_string(config.outputs.rtmp.mode); ss << ", rtmp.mode=" << to_string(config.outputs.rtmp.mode);
ss << ", rtmp.urls=" << config.outputs.rtmp.urls.size(); ss << ", rtmp.urls=" << config.outputs.rtmp.urls.size();
ss << ", rtp.enabled=" << (config.outputs.rtp.enabled ? "true" : "false"); ss << ", rtp.enabled=" << (config.outputs.rtp.enabled ? "true" : "false");
ss << ", rtp.endpoint=" << (config.outputs.rtp.endpoint ? *config.outputs.rtp.endpoint : "<unset>"); ss << ", rtp.endpoint="
ss << ", rtp.payload_type=" << static_cast<unsigned>(config.outputs.rtp.payload_type); << (config.outputs.rtp.endpoint ? *config.outputs.rtp.endpoint
ss << ", rtp.sdp=" << (config.outputs.rtp.sdp_path ? *config.outputs.rtp.sdp_path : "<auto>"); : "<unset>");
ss << ", rtp.payload_type="
<< static_cast<unsigned>(config.outputs.rtp.payload_type);
ss << ", rtp.sdp="
<< (config.outputs.rtp.sdp_path ? *config.outputs.rtp.sdp_path : "<auto>");
ss << ", latency.queue_size=" << config.latency.queue_size; ss << ", latency.queue_size=" << config.latency.queue_size;
ss << ", latency.gop=" << config.latency.gop; ss << ", latency.gop=" << config.latency.gop;
ss << ", latency.b_frames=" << config.latency.b_frames; ss << ", latency.b_frames=" << config.latency.b_frames;
ss << ", latency.realtime_sync=" << (config.latency.realtime_sync ? "true" : "false"); ss << ", latency.realtime_sync="
ss << ", latency.force_idr_on_reset=" << (config.latency.force_idr_on_reset ? "true" : "false"); << (config.latency.realtime_sync ? "true" : "false");
ss << ", latency.force_idr_on_reset="
<< (config.latency.force_idr_on_reset ? "true" : "false");
ss << ", latency.ingest_max_frames=" << config.latency.ingest_max_frames; ss << ", latency.ingest_max_frames=" << config.latency.ingest_max_frames;
ss << ", latency.ingest_idle_timeout_ms=" << config.latency.ingest_idle_timeout_ms; ss << ", latency.ingest_idle_timeout_ms="
ss << ", latency.ingest_consumer_delay_ms=" << config.latency.ingest_consumer_delay_ms; << config.latency.ingest_idle_timeout_ms;
ss << ", latency.snapshot_copy_delay_us=" << config.latency.snapshot_copy_delay_us; ss << ", latency.ingest_consumer_delay_ms="
<< config.latency.ingest_consumer_delay_ms;
ss << ", latency.snapshot_copy_delay_us="
<< config.latency.snapshot_copy_delay_us;
ss << ", latency.emit_stall_ms=" << config.latency.emit_stall_ms; ss << ", latency.emit_stall_ms=" << config.latency.emit_stall_ms;
return ss.str(); return ss.str();
} }
} } // namespace cvmmap_streamer
+320 -200
View File
@@ -1,8 +1,13 @@
#include "cvmmap_streamer/sim/options.hpp" #include "cvmmap_streamer/sim/options.hpp"
#include <CLI/CLI.hpp>
#include <array> #include <array>
#include <charconv> #include <charconv>
#include <cstdint>
#include <limits> #include <limits>
#include <sstream>
#include <string>
#include <string_view> #include <string_view>
#include <spdlog/spdlog.h> #include <spdlog/spdlog.h>
@@ -11,223 +16,93 @@ namespace cvmmap_streamer::sim {
namespace { namespace {
std::expected<std::string_view, std::string> next_value(int argc, char **argv, int &index, std::string_view flag_name) { std::string trim_copy(std::string value) {
if (index + 1 >= argc) { auto not_space = [](unsigned char c) {
return std::unexpected("missing value for " + std::string(flag_name)); return c != ' ' && c != '\t' && c != '\n' && c != '\r';
};
while (!value.empty() &&
!not_space(static_cast<unsigned char>(value.front()))) {
value.erase(value.begin());
} }
++index; while (!value.empty() &&
return std::string_view{argv[index]}; !not_space(static_cast<unsigned char>(value.back()))) {
} value.pop_back();
std::expected<std::uint32_t, std::string> parse_u32(std::string_view raw, std::string_view flag_name) {
std::uint32_t value{0};
const auto result = std::from_chars(raw.data(), raw.data() + raw.size(), value, 10);
if (result.ec != std::errc{} || result.ptr != raw.data() + raw.size()) {
return std::unexpected("invalid value for " + std::string(flag_name) + ": '" + std::string(raw) + "'");
} }
return value; return value;
} }
std::expected<std::uint16_t, std::string> parse_u16(std::string_view raw, std::string_view flag_name) { std::string normalize_cli_error(std::string raw_message) {
std::uint16_t value{0}; if (raw_message.find("The following argument was not expected:") !=
const auto result = std::from_chars(raw.data(), raw.data() + raw.size(), value, 10); std::string::npos) {
if (result.ec != std::errc{} || result.ptr != raw.data() + raw.size()) { const auto pos = raw_message.find(':');
return std::unexpected("invalid value for " + std::string(flag_name) + ": '" + std::string(raw) + "'"); if (pos != std::string::npos && pos + 1 < raw_message.size()) {
return "unknown argument: '" + trim_copy(raw_message.substr(pos + 1)) +
"'";
} }
return value; return "unknown argument";
} }
constexpr std::array<std::string_view, 12> kFlags{"--frames",
"--fps",
"--width",
"--height",
"--emit-reset-at",
"--emit-reset-every",
"--switch-format-at",
"--switch-width",
"--switch-height",
"--label",
"--shm-name",
"--zmq-endpoint"};
if (raw_message.find("requires at least") != std::string::npos ||
raw_message.find("requires ") != std::string::npos) {
for (const auto flag : kFlags) {
if (raw_message.find(flag) != std::string::npos) {
return "missing value for " + std::string(flag);
} }
std::uint32_t RuntimeConfig::payload_size_bytes() const {
const auto width64 = static_cast<std::uint64_t>(width);
const auto height64 = static_cast<std::uint64_t>(height);
const auto channels64 = static_cast<std::uint64_t>(channels);
return static_cast<std::uint32_t>(width64 * height64 * channels64);
}
void print_help() {
constexpr std::array<std::string_view, 17> kHelpLines{
"cvmmap_sim",
"Usage:",
" cvmmap_sim [options]",
"",
"Required simulation controls:",
" --frames <n> total frames to emit (default: 360)",
" --fps <n> deterministic frame pacing, 0 disables sleep (default: 60)",
" --width <px> frame width (default: 64)",
" --height <px> frame height (default: 48)",
" --emit-reset-at <n> emit MODULE_STATUS_STREAM_RESET at frame_count n",
" --emit-reset-every <n> emit MODULE_STATUS_STREAM_RESET each n frames",
" --switch-format-at <n> switch metadata frame format at frame_count n",
" --switch-width <px> width after format switch (default: --width)",
" --switch-height <px> height after format switch (default: --height)",
"",
"Optional:",
" --label <name> --shm-name <name> --zmq-endpoint <endpoint>"};
for (const auto &line : kHelpLines) {
spdlog::info("{}", line);
} }
} }
std::expected<RuntimeConfig, std::string> parse_runtime_config(int argc, char **argv) { return trim_copy(std::move(raw_message));
RuntimeConfig config{};
for (int i = 1; i < argc; ++i) {
const std::string_view arg{argv[i]};
if (arg == "--frames") {
auto value = next_value(argc, argv, i, arg);
if (!value) {
return std::unexpected(value.error());
}
auto parsed = parse_u32(*value, arg);
if (!parsed) {
return std::unexpected(parsed.error());
}
config.frames = *parsed;
continue;
} }
if (arg == "--fps") { std::expected<std::uint32_t, std::string>
auto value = next_value(argc, argv, i, arg); parse_u32(std::string_view raw, std::string_view flag_name) {
if (!value) { unsigned long parsed{0};
return std::unexpected(value.error()); const auto *begin = raw.data();
const auto *end = raw.data() + raw.size();
const auto result = std::from_chars(begin, end, parsed, 10);
if (result.ec != std::errc{} || result.ptr != end) {
return std::unexpected("invalid value for " + std::string(flag_name) +
": '" + std::string(raw) + "'");
} }
auto parsed = parse_u32(*value, arg); if (parsed > std::numeric_limits<std::uint32_t>::max()) {
if (!parsed) { return std::unexpected("value out of range for " + std::string(flag_name) +
return std::unexpected(parsed.error()); ": '" + std::string(raw) + "'");
} }
config.fps = *parsed; return static_cast<std::uint32_t>(parsed);
continue;
} }
if (arg == "--width") { std::expected<std::uint16_t, std::string>
auto value = next_value(argc, argv, i, arg); parse_u16(std::string_view raw, std::string_view flag_name) {
if (!value) { unsigned int parsed{0};
return std::unexpected(value.error()); const auto *begin = raw.data();
const auto *end = raw.data() + raw.size();
const auto result = std::from_chars(begin, end, parsed, 10);
if (result.ec != std::errc{} || result.ptr != end) {
return std::unexpected("invalid value for " + std::string(flag_name) +
": '" + std::string(raw) + "'");
} }
auto parsed = parse_u16(*value, arg); if (parsed > std::numeric_limits<std::uint16_t>::max()) {
if (!parsed) { return std::unexpected("value out of range for " + std::string(flag_name) +
return std::unexpected(parsed.error()); ": '" + std::string(raw) + "'");
} }
config.width = *parsed; return static_cast<std::uint16_t>(parsed);
continue;
}
if (arg == "--height") {
auto value = next_value(argc, argv, i, arg);
if (!value) {
return std::unexpected(value.error());
}
auto parsed = parse_u16(*value, arg);
if (!parsed) {
return std::unexpected(parsed.error());
}
config.height = *parsed;
continue;
}
if (arg == "--emit-reset-at") {
auto value = next_value(argc, argv, i, arg);
if (!value) {
return std::unexpected(value.error());
}
auto parsed = parse_u32(*value, arg);
if (!parsed) {
return std::unexpected(parsed.error());
}
config.emit_reset_at = *parsed;
continue;
}
if (arg == "--emit-reset-every") {
auto value = next_value(argc, argv, i, arg);
if (!value) {
return std::unexpected(value.error());
}
auto parsed = parse_u32(*value, arg);
if (!parsed) {
return std::unexpected(parsed.error());
}
config.emit_reset_every = *parsed;
continue;
}
if (arg == "--switch-format-at") {
auto value = next_value(argc, argv, i, arg);
if (!value) {
return std::unexpected(value.error());
}
auto parsed = parse_u32(*value, arg);
if (!parsed) {
return std::unexpected(parsed.error());
}
config.switch_format_at = *parsed;
continue;
}
if (arg == "--switch-width") {
auto value = next_value(argc, argv, i, arg);
if (!value) {
return std::unexpected(value.error());
}
auto parsed = parse_u16(*value, arg);
if (!parsed) {
return std::unexpected(parsed.error());
}
config.switch_width = *parsed;
continue;
}
if (arg == "--switch-height") {
auto value = next_value(argc, argv, i, arg);
if (!value) {
return std::unexpected(value.error());
}
auto parsed = parse_u16(*value, arg);
if (!parsed) {
return std::unexpected(parsed.error());
}
config.switch_height = *parsed;
continue;
}
if (arg == "--label") {
auto value = next_value(argc, argv, i, arg);
if (!value) {
return std::unexpected(value.error());
}
config.label = std::string(*value);
continue;
}
if (arg == "--shm-name") {
auto value = next_value(argc, argv, i, arg);
if (!value) {
return std::unexpected(value.error());
}
config.shm_name = std::string(*value);
continue;
}
if (arg == "--zmq-endpoint") {
auto value = next_value(argc, argv, i, arg);
if (!value) {
return std::unexpected(value.error());
}
config.zmq_endpoint = std::string(*value);
continue;
}
if (arg == "--help" || arg == "-h") {
continue;
}
return std::unexpected("unknown argument: '" + std::string(arg) + "'");
} }
std::expected<void, std::string>
validate_runtime_config(const RuntimeConfig &config) {
if (config.frames == 0) { if (config.frames == 0) {
return std::unexpected("--frames must be > 0"); return std::unexpected("--frames must be > 0");
} }
@@ -271,8 +146,10 @@ std::expected<RuntimeConfig, std::string> parse_runtime_config(int argc, char **
const auto payload_size = static_cast<std::uint64_t>(config.width) * const auto payload_size = static_cast<std::uint64_t>(config.width) *
static_cast<std::uint64_t>(config.height) * static_cast<std::uint64_t>(config.height) *
static_cast<std::uint64_t>(config.channels); static_cast<std::uint64_t>(config.channels);
if (payload_size > static_cast<std::uint64_t>(std::numeric_limits<std::uint32_t>::max())) { if (payload_size >
return std::unexpected("computed payload size exceeds cv-mmap frame_info.buffer_size range"); static_cast<std::uint64_t>(std::numeric_limits<std::uint32_t>::max())) {
return std::unexpected(
"computed payload size exceeds cv-mmap frame_info.buffer_size range");
} }
const auto switched_width = config.switch_width.value_or(config.width); const auto switched_width = config.switch_width.value_or(config.width);
@@ -281,11 +158,254 @@ std::expected<RuntimeConfig, std::string> parse_runtime_config(int argc, char **
static_cast<std::uint64_t>(switched_width) * static_cast<std::uint64_t>(switched_width) *
static_cast<std::uint64_t>(switched_height) * static_cast<std::uint64_t>(switched_height) *
static_cast<std::uint64_t>(config.channels); static_cast<std::uint64_t>(config.channels);
if (switched_payload_size > static_cast<std::uint64_t>(std::numeric_limits<std::uint32_t>::max())) { if (switched_payload_size >
return std::unexpected("computed switched payload size exceeds cv-mmap frame_info.buffer_size range"); static_cast<std::uint64_t>(std::numeric_limits<std::uint32_t>::max())) {
return std::unexpected("computed switched payload size exceeds cv-mmap "
"frame_info.buffer_size range");
} }
return config; return {};
} }
std::string build_help_text(std::string_view executable_name = "cvmmap_sim") {
RuntimeConfig defaults{};
std::string frames_raw;
std::string fps_raw;
std::string width_raw;
std::string height_raw;
std::string emit_reset_at_raw;
std::string emit_reset_every_raw;
std::string switch_format_at_raw;
std::string switch_width_raw;
std::string switch_height_raw;
std::string label_raw;
std::string shm_name_raw;
std::string zmq_endpoint_raw;
CLI::App app{"cv-mmap deterministic frame simulator"};
app.name(std::string(executable_name));
app.allow_extras(false);
app.set_help_flag("--help,-h", "show this message");
app.add_option("--frames", frames_raw, "total frames to emit (default: 360)");
app.add_option("--fps", fps_raw,
"deterministic frame pacing, 0 disables sleep (default: 60)");
app.add_option("--width", width_raw, "frame width (default: 64)");
app.add_option("--height", height_raw, "frame height (default: 48)");
app.add_option("--emit-reset-at", emit_reset_at_raw,
"emit MODULE_STATUS_STREAM_RESET at frame_count n");
app.add_option("--emit-reset-every", emit_reset_every_raw,
"emit MODULE_STATUS_STREAM_RESET each n frames");
app.add_option("--switch-format-at", switch_format_at_raw,
"switch metadata frame format at frame_count n");
app.add_option("--switch-width", switch_width_raw,
"width after format switch (default: --width)");
app.add_option("--switch-height", switch_height_raw,
"height after format switch (default: --height)");
app.add_option("--label", label_raw, "stream label (<=24 bytes)")
->default_val(defaults.label);
app.add_option("--shm-name", shm_name_raw, "POSIX shm object name")
->default_val(defaults.shm_name);
app.add_option("--zmq-endpoint", zmq_endpoint_raw, "ZMQ pub endpoint")
->default_val(defaults.zmq_endpoint);
return app.help();
} }
} // namespace
std::uint32_t RuntimeConfig::payload_size_bytes() const {
const auto width64 = static_cast<std::uint64_t>(width);
const auto height64 = static_cast<std::uint64_t>(height);
const auto channels64 = static_cast<std::uint64_t>(channels);
return static_cast<std::uint32_t>(width64 * height64 * channels64);
}
void print_help() {
std::istringstream in(build_help_text());
for (std::string line; std::getline(in, line);) {
spdlog::info("{}", line);
}
}
ParseResult parse_runtime_config_with_cli11(int argc, char **argv,
std::string_view executable_name) {
RuntimeConfig config{};
std::string frames_raw;
std::string fps_raw;
std::string width_raw;
std::string height_raw;
std::string emit_reset_at_raw;
std::string emit_reset_every_raw;
std::string switch_format_at_raw;
std::string switch_width_raw;
std::string switch_height_raw;
std::string label_raw;
std::string shm_name_raw;
std::string zmq_endpoint_raw;
CLI::App app{"cv-mmap deterministic frame simulator"};
app.name(std::string(executable_name));
app.allow_extras(false);
app.set_help_flag("--help,-h", "show this message");
app.add_option("--frames", frames_raw, "total frames to emit (default: 360)");
app.add_option("--fps", fps_raw,
"deterministic frame pacing, 0 disables sleep (default: 60)");
app.add_option("--width", width_raw, "frame width (default: 64)");
app.add_option("--height", height_raw, "frame height (default: 48)");
app.add_option("--emit-reset-at", emit_reset_at_raw,
"emit MODULE_STATUS_STREAM_RESET at frame_count n");
app.add_option("--emit-reset-every", emit_reset_every_raw,
"emit MODULE_STATUS_STREAM_RESET each n frames");
app.add_option("--switch-format-at", switch_format_at_raw,
"switch metadata frame format at frame_count n");
app.add_option("--switch-width", switch_width_raw,
"width after format switch (default: --width)");
app.add_option("--switch-height", switch_height_raw,
"height after format switch (default: --height)");
app.add_option("--label", label_raw, "stream label (<=24 bytes)");
app.add_option("--shm-name", shm_name_raw, "POSIX shm object name");
app.add_option("--zmq-endpoint", zmq_endpoint_raw, "ZMQ pub endpoint");
if (argc <= 1) {
return ParseResult{
.status = ParseStatus::Help, .message = app.help(), .exit_code = 0};
}
try {
app.parse(argc, argv);
} catch (const CLI::CallForHelp &) {
return ParseResult{
.status = ParseStatus::Help, .message = app.help(), .exit_code = 0};
} catch (const CLI::ParseError &e) {
return ParseResult{.status = ParseStatus::Error,
.message = normalize_cli_error(e.what()),
.exit_code = 2};
}
if (!frames_raw.empty()) {
auto parsed = parse_u32(frames_raw, "--frames");
if (!parsed) {
return ParseResult{.status = ParseStatus::Error,
.message = parsed.error(),
.exit_code = 2};
}
config.frames = *parsed;
}
if (!fps_raw.empty()) {
auto parsed = parse_u32(fps_raw, "--fps");
if (!parsed) {
return ParseResult{.status = ParseStatus::Error,
.message = parsed.error(),
.exit_code = 2};
}
config.fps = *parsed;
}
if (!width_raw.empty()) {
auto parsed = parse_u16(width_raw, "--width");
if (!parsed) {
return ParseResult{.status = ParseStatus::Error,
.message = parsed.error(),
.exit_code = 2};
}
config.width = *parsed;
}
if (!height_raw.empty()) {
auto parsed = parse_u16(height_raw, "--height");
if (!parsed) {
return ParseResult{.status = ParseStatus::Error,
.message = parsed.error(),
.exit_code = 2};
}
config.height = *parsed;
}
if (!emit_reset_at_raw.empty()) {
auto parsed = parse_u32(emit_reset_at_raw, "--emit-reset-at");
if (!parsed) {
return ParseResult{.status = ParseStatus::Error,
.message = parsed.error(),
.exit_code = 2};
}
config.emit_reset_at = *parsed;
}
if (!emit_reset_every_raw.empty()) {
auto parsed = parse_u32(emit_reset_every_raw, "--emit-reset-every");
if (!parsed) {
return ParseResult{.status = ParseStatus::Error,
.message = parsed.error(),
.exit_code = 2};
}
config.emit_reset_every = *parsed;
}
if (!switch_format_at_raw.empty()) {
auto parsed = parse_u32(switch_format_at_raw, "--switch-format-at");
if (!parsed) {
return ParseResult{.status = ParseStatus::Error,
.message = parsed.error(),
.exit_code = 2};
}
config.switch_format_at = *parsed;
}
if (!switch_width_raw.empty()) {
auto parsed = parse_u16(switch_width_raw, "--switch-width");
if (!parsed) {
return ParseResult{.status = ParseStatus::Error,
.message = parsed.error(),
.exit_code = 2};
}
config.switch_width = *parsed;
}
if (!switch_height_raw.empty()) {
auto parsed = parse_u16(switch_height_raw, "--switch-height");
if (!parsed) {
return ParseResult{.status = ParseStatus::Error,
.message = parsed.error(),
.exit_code = 2};
}
config.switch_height = *parsed;
}
if (!label_raw.empty()) {
config.label = label_raw;
}
if (!shm_name_raw.empty()) {
config.shm_name = shm_name_raw;
}
if (!zmq_endpoint_raw.empty()) {
config.zmq_endpoint = zmq_endpoint_raw;
}
auto validation = validate_runtime_config(config);
if (!validation) {
return ParseResult{.status = ParseStatus::Error,
.message = validation.error(),
.exit_code = 2};
}
return ParseResult{
.status = ParseStatus::Ok, .config = config, .exit_code = 0};
}
std::expected<RuntimeConfig, std::string> parse_runtime_config(int argc,
char **argv) {
auto parsed = parse_runtime_config_with_cli11(argc, argv);
if (parsed.status == ParseStatus::Ok) {
return parsed.config;
}
if (!parsed.message.empty()) {
return std::unexpected(parsed.message);
}
return std::unexpected("failed to parse runtime options");
}
} // namespace cvmmap_streamer::sim