Three asked-for changes, one commit because they are one coordinate system. The live axis is a fixed [-window, 0] with the newest sample at 0, so x reads as an age and the labels stand still while the trace rolls under them. That makes the display clock distinct from the stream clock for the first time, so `ScopeModel::stream_at_x0()` becomes the single conversion between them (`display_x = stream_t - stream_at_x0()` in all four modes) and a snapshot carries its own origin in `EventSnapshot::x_offset`. The pause view is now rebased like an event's, reversing an earlier deliberate choice: a frozen absolute-stamped picture would sit a whole uptime right of a newest-relative axis. That also fixed a latent bug, the export adding `trigger_time()` to a *paused* view's stamps. The status bar says how long you have been paused -- wall time, since the producer never stops, through `format_elapsed()` so the field cannot widen. The export writes two time columns: `time`, the axis as displayed, and `abs_seconds`, the stream's own clock for the same sample. f64 seconds and not the wire's integer, because `udps_frame_t::values` is a `const double*` and the integer never exists in this process. Cursors and measurement marks move in opposite directions under the rolling axis, deliberately: a cursor is a position on screen and stays put, a mark names a sample and travels with it. `App::measure_window` is stream-clocked for a sharper reason -- a display-coordinate live window never moves, so the "the window moved" recompute would never fire again. Moves the deps/plotter pin to 58473db, which stops a time label's mantissa reaching its own radix: a one-second tick differenced off a live axis read `1e+3ms`. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
96 lines
3.4 KiB
C++
96 lines
3.4 KiB
C++
// SPDX-License-Identifier: MIT
|
|
#include "status_text.hpp"
|
|
|
|
#include <cmath>
|
|
#include <cstdio>
|
|
|
|
namespace udpscope {
|
|
|
|
/// `12 s`, `3m 07s`, `2h 14m` -- one unit coarser as soon as the finer one would need
|
|
/// three digits, so the field never grows past six characters and the status line's shape
|
|
/// does not depend on how long the wait has been.
|
|
std::string format_elapsed(double seconds) {
|
|
if (!(seconds >= 0.0)) return "--";
|
|
char buf[32];
|
|
if (seconds < 60.0) {
|
|
std::snprintf(buf, sizeof(buf), "%.0f s", std::floor(seconds));
|
|
} else if (seconds < 3600.0) {
|
|
const int m = static_cast<int>(seconds) / 60;
|
|
std::snprintf(buf, sizeof(buf), "%dm %02ds", m, static_cast<int>(seconds) % 60);
|
|
} else {
|
|
const int h = static_cast<int>(seconds) / 3600;
|
|
std::snprintf(buf, sizeof(buf), "%dh %02dm", h, (static_cast<int>(seconds) / 60) % 60);
|
|
}
|
|
return buf;
|
|
}
|
|
|
|
/// A sample rate in the unit it reads shortest in. Rates here span six decades (a 1 Hz
|
|
/// housekeeping signal next to a 1 MS/s acquisition), so a fixed unit makes one of the
|
|
/// two unreadable.
|
|
std::string format_rate(double per_s) {
|
|
char buf[32];
|
|
if (!(per_s > 0.0)) {
|
|
return "0 S/s";
|
|
} else if (per_s >= 1.0e6) {
|
|
std::snprintf(buf, sizeof(buf), "%.3g MS/s", per_s / 1.0e6);
|
|
} else if (per_s >= 1.0e3) {
|
|
std::snprintf(buf, sizeof(buf), "%.3g kS/s", per_s / 1.0e3);
|
|
} else {
|
|
std::snprintf(buf, sizeof(buf), "%.3g S/s", per_s);
|
|
}
|
|
return buf;
|
|
}
|
|
|
|
std::string format_glance(const StatusSnapshot& s) {
|
|
std::string out = s.state_word;
|
|
out += " · ";
|
|
out += s.rate_text.empty() ? "no data" : s.rate_text;
|
|
out += " · ";
|
|
out += s.window_text;
|
|
out += " window";
|
|
if (s.paused) {
|
|
// Directly after the state word's own field rather than at the end, because it qualifies
|
|
// *that* word: "paused · 10 kS/s · 2 s window · for 12 s" reads as one statement.
|
|
out += " · for " + format_elapsed(s.paused_for_s);
|
|
}
|
|
if (s.trigger_on) {
|
|
out += " · ";
|
|
if (s.have_event) {
|
|
out += "event " + format_elapsed(s.since_event_s) + " ago";
|
|
} else {
|
|
out += "no event yet";
|
|
}
|
|
}
|
|
return out;
|
|
}
|
|
|
|
std::string format_detail(const StatusSnapshot& s) {
|
|
char buf[1024];
|
|
std::snprintf(buf, sizeof(buf),
|
|
"[%s] %s %s | rate %s | packets %llu frames %llu gaps %llu | %zu signals, "
|
|
"%zu channels | plot 1: %d trace(s), %zu samples | buffer %.1f MB, %.2f s "
|
|
"span, time x%llu",
|
|
s.state_detail.c_str(), s.connected ? "connected" : "waiting",
|
|
s.have_config ? "configured" : "-",
|
|
s.rate_text.empty() ? "--" : s.rate_text.c_str(),
|
|
static_cast<unsigned long long>(s.packets),
|
|
static_cast<unsigned long long>(s.frames),
|
|
static_cast<unsigned long long>(s.gaps), s.signals, s.channels,
|
|
s.plot1_traces, s.plot1_samples, s.buffer_mb, s.span_s,
|
|
static_cast<unsigned long long>(s.time_stride));
|
|
std::string out = buf;
|
|
if (s.paused) out += " | paused for " + format_elapsed(s.paused_for_s);
|
|
if (s.trigger_on) {
|
|
out += s.have_event ? " | last event " + format_elapsed(s.since_event_s) + " ago"
|
|
: " | no event yet";
|
|
}
|
|
if (s.reconnects > 0) {
|
|
char note[48];
|
|
std::snprintf(note, sizeof(note), " | %d reconnects", s.reconnects);
|
|
out += note;
|
|
}
|
|
return out;
|
|
}
|
|
|
|
} // namespace udpscope
|