176 lines
8.8 KiB
C++
176 lines
8.8 KiB
C++
// 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 <cstdio>
|
|
#include <cstdlib>
|
|
#include <stdexcept>
|
|
#include <string>
|
|
|
|
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<std::uint16_t>(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<int>(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<int>(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<std::uint32_t>(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
|