// SPDX-License-Identifier: MIT // SPDX-FileCopyrightText: 2026 Martino Ferrari #include "config.hpp" // For `RowSet::kMaxRows`: `--plots` names a row count, and one constant for the ceiling // is what stops the message and the clamp from drifting apart. #include "plot_host.hpp" #include "source/registry.hpp" #include #include #include #include namespace udpscope { namespace { [[noreturn]] void fail(const std::string& what) { throw std::runtime_error(what); } /// `--name=value` / `--name value`; returns the value and advances `i` past both /// spellings. `argv[i]` must start with `--name`. std::string value_of(int argc, char** argv, int& i, const char* name) { const std::string arg = argv[i]; const std::string prefix = std::string(name) + "="; if (arg.rfind(prefix, 0) == 0) return arg.substr(prefix.size()); if (i + 1 >= argc) fail(std::string("missing value for ") + name); return argv[++i]; } double parse_double(const char* name, const std::string& v) { try { std::size_t used = 0; const double d = std::stod(v, &used); if (used != v.size()) fail(std::string("not a number: ") + name + " " + v); return d; } catch (const std::exception&) { fail(std::string("not a number: ") + name + " " + v); } } } // namespace SourceConfig Config::source_config() const { SourceConfig out; if (source_kind == "udps") { out.set("server", server); out.set("port", std::to_string(port)); if (!multicast.empty()) out.set("multicast", multicast); if (!interface_addr.empty()) out.set("interface", interface_addr); out.set("recv_buffer", std::to_string(recv_buffer_bytes)); } for (const auto& kv : source_opts.items()) out.set(kv.first, kv.second); return out; } std::string Config::session_host() const { return udpscope::session_host(source_kind, source_config()); } std::uint16_t Config::session_port() const { return udpscope::session_port(source_kind, source_config()); } void print_usage(std::FILE* out, const char* argv0) { std::string names; for (const SourceKind* k : kinds()) names += (names.empty() ? "" : ", ") + k->id(); std::fprintf(out, "usage: %s --server ADDR --port PORT [--source NAME] [options]\n" "\n" " --server ADDR stream source IPv4 address (default 127.0.0.1)\n" " --port PORT UDP port (unicast) or TCP port (multicast) (default 8000)\n" " --multicast GROUP join GROUP for DATA instead of unicast\n" " --interface ADDR local IPv4 of the interface to join on\n" " --source NAME source kind (%s; default udps). --server/--port/\n" " --multicast/--interface configure udps\n" " --source-opt K=V a setting for the source kind; repeatable\n" " --buffer-mb MB size of each of the two sample buffers (default 200)\n" " --max-window SEC largest time window the toolbar offers (default 600)\n" " --holdoff SEC default trigger hold-off (default 0.010)\n" " --recv-buffer BYTES SO_RCVBUF for the receiver socket (default 8388608)\n" " --plots N stacked plots to start with, 1..8 (default 1); rows are\n" " added and removed from the layout editor\n" " --capture-packets N store the last N raw datagrams on disk for forensics, in a\n" " ring under the cache directory (1000 slots is ~62.6 MiB);\n" " off unless given, and toggleable from the status bar\n" " --relative-time take the plot's time from the packet counter and the\n" " declared rate, not the stream's time signal; absolute\n" " time is then kept for export only\n" " --open FILE open an exported waveform (.npy) instead of a live source:\n" " signals from its columns, no socket, no acquisition\n" " --renderer NAME render with cairo (software), gl or vulkan. cairo also\n" " exports LIBGL_ALWAYS_SOFTWARE=1, because the plot is a\n" " GtkGLArea and still needs a GL context. Over SSH/X11\n" " forwarding use cairo: Vulkan cannot make a swapchain there\n" " and the GL area has no hardware context, so the window\n" " never paints or allocates\n" " -h, --help this text\n", argv0, names.c_str()); } Config parse_args(int argc, char** argv) { Config c; for (int i = 1; i < argc; ++i) { const std::string a = argv[i]; if (a == "-h" || a == "--help") { print_usage(stdout, argv[0]); std::exit(0); } else if (a == "--server" || a.rfind("--server=", 0) == 0) { c.server = value_of(argc, argv, i, "--server"); c.server_specified = true; } else if (a == "--port" || a.rfind("--port=", 0) == 0) { const std::string v = value_of(argc, argv, i, "--port"); const long p = std::strtol(v.c_str(), nullptr, 10); if (p <= 0 || p > 65535) fail("port out of range: " + v); c.port = static_cast(p); } else if (a == "--multicast" || a.rfind("--multicast=", 0) == 0) { c.multicast = value_of(argc, argv, i, "--multicast"); } else if (a == "--interface" || a.rfind("--interface=", 0) == 0) { c.interface_addr = value_of(argc, argv, i, "--interface"); } else if (a == "--source" || a.rfind("--source=", 0) == 0) { const std::string v = value_of(argc, argv, i, "--source"); if (find_kind(v) == nullptr) { std::string names; for (const SourceKind* k : kinds()) names += (names.empty() ? "" : ", ") + k->id(); fail("unknown source '" + v + "' (available: " + names + ")"); } c.source_kind = v; c.source_specified = true; } else if (a == "--source-opt" || a.rfind("--source-opt=", 0) == 0) { const std::string v = value_of(argc, argv, i, "--source-opt"); if (!c.source_opts.parse_assignment(v)) fail("--source-opt must be key=value: " + v); } else if (a == "--buffer-mb" || a.rfind("--buffer-mb=", 0) == 0) { c.buffer_mb = parse_double("--buffer-mb", value_of(argc, argv, i, "--buffer-mb")); c.buffer_mb_specified = true; if (c.buffer_mb < 1.0) fail("--buffer-mb must be at least 1"); } else if (a == "--max-window" || a.rfind("--max-window=", 0) == 0) { c.max_window_s = parse_double("--max-window", value_of(argc, argv, i, "--max-window")); if (c.max_window_s <= 0.0) fail("--max-window must be positive"); } else if (a == "--holdoff" || a.rfind("--holdoff=", 0) == 0) { c.trigger_holdoff_s = parse_double("--holdoff", value_of(argc, argv, i, "--holdoff")); if (c.trigger_holdoff_s < 0.0) fail("--holdoff must not be negative"); } else if (a == "--recv-buffer" || a.rfind("--recv-buffer=", 0) == 0) { const std::string v = value_of(argc, argv, i, "--recv-buffer"); const long n = std::strtol(v.c_str(), nullptr, 10); if (n <= 0) fail("--recv-buffer must be positive: " + v); c.recv_buffer_bytes = static_cast(n); } else if (a == "--plots" || a.rfind("--plots=", 0) == 0) { const std::string v = value_of(argc, argv, i, "--plots"); const long n = std::strtol(v.c_str(), nullptr, 10); if (n < 1 || n > RowSet::kMaxRows) { fail("--plots must be 1.." + std::to_string(RowSet::kMaxRows) + ": " + v); } c.plots = static_cast(n); } else if (a == "--capture-packets" || a.rfind("--capture-packets=", 0) == 0) { const long n = std::strtol(value_of(argc, argv, i, "--capture-packets").c_str(), nullptr, 10); if (n < 1 || n > 100000) fail("--capture-packets must be 1..100000"); c.capture_packets = static_cast(n); } else if (a == "--open" || a.rfind("--open=", 0) == 0) { c.open_waveform = value_of(argc, argv, i, "--open"); } else if (a == "--renderer" || a.rfind("--renderer=", 0) == 0) { const std::string v = value_of(argc, argv, i, "--renderer"); // Only the three a source or a forwarded display actually chooses between. Anything // GTK does not know would fall back with a warning and leave the user reading a // renderer failure that is really a typo, so it is refused here instead. if (v != "cairo" && v != "gl" && v != "vulkan") { fail("--renderer must be cairo, gl or vulkan: " + v); } c.renderer = v; } else if (a == "--relative-time") { c.relative_time = true; c.relative_time_specified = true; } else { fail("unknown argument: " + a); } } return c; } } // namespace udpscope