feat: two-tier status bar, one glance line with the detail behind an info popover
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
f9490061a1
commit
8242812338
4 files changed
+319
-76
No files matched your search
@@ -112,6 +112,7 @@ add_executable(udpscope
|
|||||||
src/source_dialog.cpp
|
src/source_dialog.cpp
|
||||||
src/measure.cpp
|
src/measure.cpp
|
||||||
src/math_channel.cpp
|
src/math_channel.cpp
|
||||||
|
src/status_text.cpp
|
||||||
src/shortcuts.cpp
|
src/shortcuts.cpp
|
||||||
src/help_window.cpp
|
src/help_window.cpp
|
||||||
src/ui.cpp
|
src/ui.cpp
|
||||||
|
|||||||
@@ -0,0 +1,89 @@
|
|||||||
|
// 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.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.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
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstddef>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
namespace udpscope {
|
||||||
|
|
||||||
|
/// Everything either tier of the status bar shows, gathered once per frame by the tick so the
|
||||||
|
/// two cannot disagree. No GTK here on purpose: this is the one part of the status bar that
|
||||||
|
/// can be reasoned about as text.
|
||||||
|
struct StatusSnapshot {
|
||||||
|
/// One word from the fixed set: "live", "paused", "armed", "capturing", "event held",
|
||||||
|
/// "disarmed". It is a word from a *fixed* set because the bar sits directly above the plot
|
||||||
|
/// and a field whose width tracked the stream would relayout it every frame.
|
||||||
|
const char* state_word = "live";
|
||||||
|
/// The long prose the old single-line bar carried, kept for the detail string and the dump.
|
||||||
|
std::string state_detail;
|
||||||
|
|
||||||
|
bool connected = false;
|
||||||
|
bool have_config = false;
|
||||||
|
bool trigger_on = false;
|
||||||
|
|
||||||
|
/// Already collapsed to one figure or a range by the caller, without the "rate " prefix.
|
||||||
|
std::string rate_text;
|
||||||
|
double stored_rate_hz = 0.0;
|
||||||
|
double declared_rate_hz = 0.0;
|
||||||
|
|
||||||
|
/// Pre-formatted by ui.cpp's format_window(), which the window control and the Fit notices
|
||||||
|
/// also use, so it stays there rather than moving here.
|
||||||
|
std::string window_text;
|
||||||
|
|
||||||
|
std::uint64_t packets = 0;
|
||||||
|
std::uint64_t frames = 0;
|
||||||
|
std::uint64_t gaps = 0;
|
||||||
|
std::size_t signals = 0;
|
||||||
|
std::size_t channels = 0;
|
||||||
|
int plot1_traces = 0;
|
||||||
|
std::size_t plot1_samples = 0;
|
||||||
|
double buffer_mb = 0.0;
|
||||||
|
double span_s = 0.0;
|
||||||
|
std::uint64_t time_stride = 1;
|
||||||
|
bool have_event = false;
|
||||||
|
double since_event_s = 0.0;
|
||||||
|
int reconnects = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
/// The bar: "live · 10.0 kS/s · 2 s window", plus " · event 4.2 s ago" in trigger mode and
|
||||||
|
/// nothing else ever. The badge glyph is a separate widget and is not part of this string.
|
||||||
|
std::string format_glance(const StatusSnapshot& s);
|
||||||
|
|
||||||
|
/// The popover's prose and the exit dump's line, field for field as the old single-line bar
|
||||||
|
/// wrote it. CLAUDE.md asserts those fields are read out of the dump, so the field names and
|
||||||
|
/// their order must not change without updating it.
|
||||||
|
std::string format_detail(const StatusSnapshot& s);
|
||||||
|
|
||||||
|
/// A sample rate as "10.0 kS/s". Moved here from ui.cpp, which still calls it.
|
||||||
|
std::string format_rate(double hz);
|
||||||
|
|
||||||
|
/// An elapsed time, stepping to a coarser unit rather than growing digits, so its width
|
||||||
|
/// cannot track the stream. Moved here from ui.cpp.
|
||||||
|
std::string format_elapsed(double seconds);
|
||||||
|
|
||||||
|
} // namespace udpscope
|
||||||
+164
-76
@@ -21,6 +21,7 @@
|
|||||||
#include "help_window.hpp"
|
#include "help_window.hpp"
|
||||||
#include "layout_store.hpp"
|
#include "layout_store.hpp"
|
||||||
#include "shortcuts.hpp"
|
#include "shortcuts.hpp"
|
||||||
|
#include "status_text.hpp"
|
||||||
#include "measure.hpp"
|
#include "measure.hpp"
|
||||||
#include "model.hpp"
|
#include "model.hpp"
|
||||||
#include "plot_host.hpp"
|
#include "plot_host.hpp"
|
||||||
@@ -483,6 +484,24 @@ struct App {
|
|||||||
std::string dbg_mode_live;
|
std::string dbg_mode_live;
|
||||||
int dbg_toggles_visible = 0;
|
int dbg_toggles_visible = 0;
|
||||||
int dbg_trigger_revealed_default = 0;
|
int dbg_trigger_revealed_default = 0;
|
||||||
|
|
||||||
|
/// The detail string the popover and the exit dump read. The dump used to read the label,
|
||||||
|
/// which no longer carries these fields.
|
||||||
|
std::string status_detail;
|
||||||
|
std::size_t status_detail_max = 0;
|
||||||
|
/// The widest glance line this run produced. The bar sits above the plot, so this is the
|
||||||
|
/// "layout must not depend on content" reading for the status row.
|
||||||
|
std::size_t glance_max_chars = 0;
|
||||||
|
/// The last glance line, for the exit dump's `status bar:` line — the dump runs after the
|
||||||
|
/// window is destroyed, so it cannot ask the label.
|
||||||
|
std::string dbg_glance_last;
|
||||||
|
|
||||||
|
GtkWidget* status_info_button = nullptr; ///< the ⓘ
|
||||||
|
GtkWidget* status_popover = nullptr;
|
||||||
|
/// The popover's eleven labels: four row headings and seven value lines. Built once; the
|
||||||
|
/// tick sets their text. A rebuild here would be a per-frame relayout of a visible widget.
|
||||||
|
GtkWidget* popover_label[11] = {};
|
||||||
|
int popover_rebuilds = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
// --- small helpers ----------------------------------------------------------
|
// --- small helpers ----------------------------------------------------------
|
||||||
@@ -810,23 +829,6 @@ const char* badge_markup(BadgePhase phase) {
|
|||||||
return "<span foreground=\"#5A5A5A\">\u25CF</span>";
|
return "<span foreground=\"#5A5A5A\">\u25CF</span>";
|
||||||
}
|
}
|
||||||
|
|
||||||
/// `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 measurement: at most two decimals, scaled by an SI prefix so the number reads in
|
/// A measurement: at most two decimals, scaled by an SI prefix so the number reads in
|
||||||
/// `1..999` (0.01052 V is `10.52 mV`), and the unit appended when the stream declares one.
|
/// `1..999` (0.01052 V is `10.52 mV`), and the unit appended when the stream declares one.
|
||||||
@@ -879,22 +881,6 @@ std::string format_measure(double v, const std::string& unit) {
|
|||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A short-lived line in front of the status text: the outcome of an action that the
|
/// A short-lived line in front of the status text: the outcome of an action that the
|
||||||
/// picture does not show by itself (a drop that changed nothing, an export that wrote a
|
/// picture does not show by itself (a drop that changed nothing, an export that wrote a
|
||||||
@@ -4565,6 +4551,43 @@ void report_event_zoom(App& a, const char* what) {
|
|||||||
std::fflush(stderr);
|
std::fflush(stderr);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- status popover updater -------------------------------------------------
|
||||||
|
|
||||||
|
/// Sets the popover's seven value labels. Never creates a widget: `popover_rebuilds` exists to
|
||||||
|
/// assert that, because the popover is visible when this runs and a rebuild would relayout it.
|
||||||
|
void update_status_popover(App& app, const udpscope::StatusSnapshot& s) {
|
||||||
|
if (app.popover_label[4] == nullptr) return;
|
||||||
|
char buf[256];
|
||||||
|
|
||||||
|
std::snprintf(buf, sizeof(buf), "%s, %s", s.connected ? "connected" : "waiting",
|
||||||
|
s.have_config ? "configured" : "no config");
|
||||||
|
gtk_label_set_text(GTK_LABEL(app.popover_label[4]), buf);
|
||||||
|
|
||||||
|
std::snprintf(buf, sizeof(buf), "packets %llu · frames %llu · gaps %llu",
|
||||||
|
static_cast<unsigned long long>(s.packets),
|
||||||
|
static_cast<unsigned long long>(s.frames),
|
||||||
|
static_cast<unsigned long long>(s.gaps));
|
||||||
|
gtk_label_set_text(GTK_LABEL(app.popover_label[5]), buf);
|
||||||
|
|
||||||
|
std::snprintf(buf, sizeof(buf), "%zu signals · %zu channels", s.signals, s.channels);
|
||||||
|
gtk_label_set_text(GTK_LABEL(app.popover_label[6]), buf);
|
||||||
|
|
||||||
|
std::snprintf(buf, sizeof(buf), "%.1f MB · %.0f s span · time x%llu", s.buffer_mb, s.span_s,
|
||||||
|
static_cast<unsigned long long>(s.time_stride));
|
||||||
|
gtk_label_set_text(GTK_LABEL(app.popover_label[7]), buf);
|
||||||
|
|
||||||
|
const std::string stored = udpscope::format_rate(s.stored_rate_hz);
|
||||||
|
const std::string wire = udpscope::format_rate(s.declared_rate_hz);
|
||||||
|
std::snprintf(buf, sizeof(buf), "stored %s (wire %s)", stored.c_str(), wire.c_str());
|
||||||
|
gtk_label_set_text(GTK_LABEL(app.popover_label[8]), buf);
|
||||||
|
|
||||||
|
std::snprintf(buf, sizeof(buf), "%d traces · %zu samples", s.plot1_traces, s.plot1_samples);
|
||||||
|
gtk_label_set_text(GTK_LABEL(app.popover_label[9]), buf);
|
||||||
|
|
||||||
|
std::snprintf(buf, sizeof(buf), "%d", s.reconnects);
|
||||||
|
gtk_label_set_text(GTK_LABEL(app.popover_label[10]), buf);
|
||||||
|
}
|
||||||
|
|
||||||
// --- the frame tick ---------------------------------------------------------
|
// --- the frame tick ---------------------------------------------------------
|
||||||
|
|
||||||
gboolean tick(gpointer user) {
|
gboolean tick(gpointer user) {
|
||||||
@@ -5063,10 +5086,8 @@ gboolean tick(gpointer user) {
|
|||||||
// line is the receipt for both.
|
// line is the receipt for both.
|
||||||
static gint64 next_channels = 0;
|
static gint64 next_channels = 0;
|
||||||
const gint64 now_c = g_get_monotonic_time();
|
const gint64 now_c = g_get_monotonic_time();
|
||||||
/// Filled only on a measuring tick, and applied to the status label's tooltip after the
|
// Per-channel rate computed only on the measuring tick; the detail tooltip on the label
|
||||||
/// model lock is dropped: the status line can only carry the aggregate rate, so the
|
// now carries the full status string instead of a per-channel breakdown.
|
||||||
/// per-channel breakdown goes where there is room for it.
|
|
||||||
std::string rate_tip;
|
|
||||||
if (app->last_stored.size() != model.channel_count()) {
|
if (app->last_stored.size() != model.channel_count()) {
|
||||||
// Seed new entries with the current count: a fresh entry must not be counted
|
// Seed new entries with the current count: a fresh entry must not be counted
|
||||||
// as a delta from zero.
|
// as a delta from zero.
|
||||||
@@ -5102,8 +5123,6 @@ gboolean tick(gpointer user) {
|
|||||||
const double declared = info.dt > 0.0 ? 1.0 / info.dt : 0.0;
|
const double declared = info.dt > 0.0 ? 1.0 / info.dt : 0.0;
|
||||||
app->stored_rate[c] = per_s;
|
app->stored_rate[c] = per_s;
|
||||||
app->declared_rate[c] = declared;
|
app->declared_rate[c] = declared;
|
||||||
rate_tip += (rate_tip.empty() ? "" : "\n") + info.name + ": " + format_rate(per_s) +
|
|
||||||
" stored (declared " + format_rate(declared) + ")";
|
|
||||||
const ChannelBuffer& buf = model.live().channel(c);
|
const ChannelBuffer& buf = model.live().channel(c);
|
||||||
const plot::Range y = buf.size() > 0 ? buf.y_bounds() : plot::Range{0.0, 0.0};
|
const plot::Range y = buf.size() > 0 ? buf.y_bounds() : plot::Range{0.0, 0.0};
|
||||||
std::fprintf(stderr,
|
std::fprintf(stderr,
|
||||||
@@ -5210,7 +5229,7 @@ gboolean tick(gpointer user) {
|
|||||||
// The rate the buffer is actually storing at. One figure when every channel agrees and
|
// The rate the buffer is actually storing at. One figure when every channel agrees and
|
||||||
// a range when they do not: a per-channel list would make the field's width depend on
|
// a range when they do not: a per-channel list would make the field's width depend on
|
||||||
// the stream, and the breakdown is on the label's tooltip instead.
|
// the stream, and the breakdown is on the label's tooltip instead.
|
||||||
std::string rate = "rate --";
|
std::string rate;
|
||||||
if (!app->stored_rate.empty()) {
|
if (!app->stored_rate.empty()) {
|
||||||
double lo = app->stored_rate.front();
|
double lo = app->stored_rate.front();
|
||||||
double hi = lo;
|
double hi = lo;
|
||||||
@@ -5219,51 +5238,70 @@ gboolean tick(gpointer user) {
|
|||||||
hi = std::max(hi, r);
|
hi = std::max(hi, r);
|
||||||
}
|
}
|
||||||
// Within a percent is the same rate measured over a one-second window, not two rates.
|
// Within a percent is the same rate measured over a one-second window, not two rates.
|
||||||
rate = "rate " + (hi - lo <= 0.01 * hi ? format_rate(hi)
|
rate = hi - lo <= 0.01 * hi ? format_rate(hi) : format_rate(lo) + ".." + format_rate(hi);
|
||||||
: format_rate(lo) + ".." + format_rate(hi));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// In trigger mode, how long ago the event on screen was captured. It is what tells a
|
// Derive the one-word state for the glance line from the same conditions the mode prose
|
||||||
// frozen picture apart from a stale one: the view looks identical whether the last
|
// branches on, so the two cannot disagree.
|
||||||
// event was a second or an hour ago.
|
const char* state_word = "live";
|
||||||
std::string since;
|
if (display == DisplayMode::Capturing) {
|
||||||
if (trigger_on) {
|
state_word = "capturing";
|
||||||
since = app->last_latch_us != 0
|
} else if (display == DisplayMode::Held) {
|
||||||
? " | last event " +
|
state_word = "event held";
|
||||||
format_elapsed(double(g_get_monotonic_time() - app->last_latch_us) / 1.0e6) +
|
} else if (trigger_on && !armed) {
|
||||||
" ago"
|
state_word = "disarmed";
|
||||||
: " | no event yet";
|
} else if (display == DisplayMode::Waiting) {
|
||||||
|
state_word = "armed";
|
||||||
|
} else if (paused) {
|
||||||
|
state_word = "paused";
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only when it has happened: a stream that is fine carries no counter, so the line
|
udpscope::StatusSnapshot snap;
|
||||||
// stays as short as it was, and a client being kicked shows the number it was kicked.
|
snap.state_detail = mode;
|
||||||
char reconnects_note[48];
|
snap.state_word = state_word;
|
||||||
reconnects_note[0] = '\0';
|
snap.connected = st.connected.load();
|
||||||
if (st.reconnects.load(std::memory_order_relaxed) > 0) {
|
snap.have_config = st.have_config.load();
|
||||||
std::snprintf(reconnects_note, sizeof(reconnects_note), " | %llu reconnects",
|
snap.trigger_on = trigger_on;
|
||||||
static_cast<unsigned long long>(st.reconnects.load(std::memory_order_relaxed)));
|
snap.rate_text = rate;
|
||||||
|
snap.stored_rate_hz = app->stored_rate.empty() ? 0.0 : app->stored_rate.front();
|
||||||
|
snap.declared_rate_hz = app->declared_rate.empty() ? 0.0 : app->declared_rate.front();
|
||||||
|
snap.window_text = format_window(app->rows->time_window());
|
||||||
|
snap.packets = st.packets.load();
|
||||||
|
snap.frames = st.frames.load();
|
||||||
|
snap.gaps = st.gaps.load();
|
||||||
|
snap.signals = signal_count;
|
||||||
|
snap.channels = channel_count;
|
||||||
|
snap.plot1_traces = bound;
|
||||||
|
snap.plot1_samples = samples;
|
||||||
|
snap.buffer_mb = double(model.config().buffer_mb);
|
||||||
|
snap.span_s = span_s;
|
||||||
|
snap.time_stride = time_stride;
|
||||||
|
snap.have_event = app->last_latch_us != 0;
|
||||||
|
snap.since_event_s = snap.have_event
|
||||||
|
? double(g_get_monotonic_time() - app->last_latch_us) / 1.0e6
|
||||||
|
: 0.0;
|
||||||
|
snap.reconnects = static_cast<int>(st.reconnects.load(std::memory_order_relaxed));
|
||||||
|
|
||||||
|
const std::string glance = udpscope::format_glance(snap);
|
||||||
|
app->dbg_glance_last = glance;
|
||||||
|
app->status_detail = udpscope::format_detail(snap);
|
||||||
|
if (app->status_detail.size() > app->status_detail_max) {
|
||||||
|
app->status_detail_max = app->status_detail.size();
|
||||||
}
|
}
|
||||||
|
if (glance.size() > app->glance_max_chars) app->glance_max_chars = glance.size();
|
||||||
|
|
||||||
char status[1024];
|
|
||||||
std::snprintf(status, sizeof(status),
|
|
||||||
"[%s] %s %s | %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%s",
|
|
||||||
mode, st.connected.load() ? "connected" : "waiting",
|
|
||||||
st.have_config.load() ? "configured" : "-", rate.c_str(),
|
|
||||||
static_cast<unsigned long long>(st.packets.load()),
|
|
||||||
static_cast<unsigned long long>(st.frames.load()),
|
|
||||||
static_cast<unsigned long long>(st.gaps.load()), signal_count, channel_count, bound,
|
|
||||||
samples, double(model.config().buffer_mb), span_s,
|
|
||||||
static_cast<unsigned long long>(time_stride), since.c_str(), reconnects_note);
|
|
||||||
if (!app->notice.empty() && g_get_monotonic_time() < app->notice_until_us) {
|
if (!app->notice.empty() && g_get_monotonic_time() < app->notice_until_us) {
|
||||||
const std::string with_notice = app->notice + " | " + status;
|
const std::string with_notice = app->notice + " | " + glance;
|
||||||
gtk_label_set_text(GTK_LABEL(app->status_label), with_notice.c_str());
|
gtk_label_set_text(GTK_LABEL(app->status_label), with_notice.c_str());
|
||||||
} else {
|
} else {
|
||||||
app->notice.clear();
|
app->notice.clear();
|
||||||
gtk_label_set_text(GTK_LABEL(app->status_label), status);
|
gtk_label_set_text(GTK_LABEL(app->status_label), glance.c_str());
|
||||||
|
}
|
||||||
|
gtk_widget_set_tooltip_text(app->status_label, app->status_detail.c_str());
|
||||||
|
if (app->status_popover != nullptr &&
|
||||||
|
gtk_widget_get_visible(app->status_popover) == TRUE) {
|
||||||
|
update_status_popover(*app, snap);
|
||||||
}
|
}
|
||||||
if (!rate_tip.empty()) gtk_widget_set_tooltip_text(app->status_label, rate_tip.c_str());
|
|
||||||
const double tick_ms = double(g_get_monotonic_time() - tick_start_us) / 1000.0;
|
const double tick_ms = double(g_get_monotonic_time() - tick_start_us) / 1000.0;
|
||||||
app->dbg_tick_ms_sum += tick_ms;
|
app->dbg_tick_ms_sum += tick_ms;
|
||||||
app->dbg_tick_ms_max = std::max(app->dbg_tick_ms_max, tick_ms);
|
app->dbg_tick_ms_max = std::max(app->dbg_tick_ms_max, tick_ms);
|
||||||
@@ -5760,6 +5798,53 @@ void on_activate(GtkApplication* application, gpointer user) {
|
|||||||
gtk_widget_set_hexpand(app->status_label, true);
|
gtk_widget_set_hexpand(app->status_label, true);
|
||||||
gtk_widget_set_halign(app->status_label, GTK_ALIGN_FILL);
|
gtk_widget_set_halign(app->status_label, GTK_ALIGN_FILL);
|
||||||
gtk_box_append(GTK_BOX(status_row), app->status_label);
|
gtk_box_append(GTK_BOX(status_row), app->status_label);
|
||||||
|
|
||||||
|
// Guard the construction so the counter means something: if status_popover is already
|
||||||
|
// non-null (e.g. run_ui called twice), count the extra build and avoid a leak.
|
||||||
|
if (app->status_popover != nullptr) {
|
||||||
|
++app->popover_rebuilds;
|
||||||
|
} else {
|
||||||
|
app->status_popover = gtk_popover_new();
|
||||||
|
GtkWidget* detail_grid = gtk_grid_new();
|
||||||
|
gtk_grid_set_row_spacing(GTK_GRID(detail_grid), 2);
|
||||||
|
gtk_grid_set_column_spacing(GTK_GRID(detail_grid), 16);
|
||||||
|
gtk_widget_set_margin_start(detail_grid, 10);
|
||||||
|
gtk_widget_set_margin_end(detail_grid, 10);
|
||||||
|
gtk_widget_set_margin_top(detail_grid, 8);
|
||||||
|
gtk_widget_set_margin_bottom(detail_grid, 8);
|
||||||
|
|
||||||
|
// Row headings at indices 0..3, value lines at 4..10, in the order format_detail's fields
|
||||||
|
// appear. Built once: update_status_popover() only ever calls gtk_label_set_text().
|
||||||
|
static const char* const kHeadings[] = {"Stream", "Buffer", "Plot 1", "Reconnects"};
|
||||||
|
const int kHeadingRow[] = {0, 3, 5, 6};
|
||||||
|
for (int i = 0; i < 4; ++i) {
|
||||||
|
GtkWidget* heading = gtk_label_new(nullptr);
|
||||||
|
char markup[64];
|
||||||
|
std::snprintf(markup, sizeof(markup), "<b>%s</b>", kHeadings[i]);
|
||||||
|
gtk_label_set_markup(GTK_LABEL(heading), markup);
|
||||||
|
gtk_label_set_xalign(GTK_LABEL(heading), 0.0f);
|
||||||
|
gtk_label_set_yalign(GTK_LABEL(heading), 0.0f);
|
||||||
|
gtk_grid_attach(GTK_GRID(detail_grid), heading, 0, kHeadingRow[i], 1, 1);
|
||||||
|
app->popover_label[i] = heading;
|
||||||
|
}
|
||||||
|
for (int i = 0; i < 7; ++i) {
|
||||||
|
GtkWidget* value = gtk_label_new("");
|
||||||
|
gtk_label_set_xalign(GTK_LABEL(value), 0.0f);
|
||||||
|
gtk_grid_attach(GTK_GRID(detail_grid), value, 1, i, 1, 1);
|
||||||
|
app->popover_label[4 + i] = value;
|
||||||
|
}
|
||||||
|
gtk_popover_set_child(GTK_POPOVER(app->status_popover), detail_grid);
|
||||||
|
|
||||||
|
app->status_info_button = gtk_menu_button_new();
|
||||||
|
gtk_menu_button_set_icon_name(GTK_MENU_BUTTON(app->status_info_button),
|
||||||
|
"help-about-symbolic");
|
||||||
|
gtk_menu_button_set_popover(GTK_MENU_BUTTON(app->status_info_button),
|
||||||
|
app->status_popover);
|
||||||
|
gtk_widget_set_tooltip_text(app->status_info_button, "Stream and buffer detail");
|
||||||
|
gtk_widget_set_has_tooltip(app->status_info_button, TRUE);
|
||||||
|
gtk_box_append(GTK_BOX(status_row), app->status_info_button);
|
||||||
|
}
|
||||||
|
|
||||||
gtk_widget_set_margin_top(status_row, 2);
|
gtk_widget_set_margin_top(status_row, 2);
|
||||||
gtk_widget_set_margin_bottom(status_row, 2);
|
gtk_widget_set_margin_bottom(status_row, 2);
|
||||||
gtk_widget_set_margin_start(status_row, 6);
|
gtk_widget_set_margin_start(status_row, 6);
|
||||||
@@ -5851,7 +5936,7 @@ void on_activate(GtkApplication* application, gpointer user) {
|
|||||||
/// exist, then close the window. Runs after the reveal animations have settled.
|
/// exist, then close the window. Runs after the reveal animations have settled.
|
||||||
gboolean exit_probe_step3(gpointer user) {
|
gboolean exit_probe_step3(gpointer user) {
|
||||||
App* a = static_cast<App*>(user);
|
App* a = static_cast<App*>(user);
|
||||||
a->dbg_mode_live = gtk_label_get_text(GTK_LABEL(a->status_label));
|
a->dbg_mode_live = a->status_detail;
|
||||||
GtkWidget* window = a->window;
|
GtkWidget* window = a->window;
|
||||||
a->window = nullptr; // stop the tick before the widgets go
|
a->window = nullptr; // stop the tick before the widgets go
|
||||||
a->dbg_side_w = gtk_widget_get_width(a->side_box);
|
a->dbg_side_w = gtk_widget_get_width(a->side_box);
|
||||||
@@ -5896,7 +5981,7 @@ gboolean exit_probe_step3(gpointer user) {
|
|||||||
/// mode the way the "Go live" button does, and stop the tick.
|
/// mode the way the "Go live" button does, and stop the tick.
|
||||||
gboolean exit_probe_step2(gpointer user) {
|
gboolean exit_probe_step2(gpointer user) {
|
||||||
App* a = static_cast<App*>(user);
|
App* a = static_cast<App*>(user);
|
||||||
a->dbg_mode_triggered = gtk_label_get_text(GTK_LABEL(a->status_label));
|
a->dbg_mode_triggered = a->status_detail;
|
||||||
if (a->rows->row(0).plot) {
|
if (a->rows->row(0).plot) {
|
||||||
a->dbg_marker_x = a->rows->row(0).plot->cursor(a->rows->row(0).trigger_marker).x;
|
a->dbg_marker_x = a->rows->row(0).plot->cursor(a->rows->row(0).trigger_marker).x;
|
||||||
a->dbg_marker_max = a->rows->row(0).plot->x_range(0).max;
|
a->dbg_marker_max = a->rows->row(0).plot->x_range(0).max;
|
||||||
@@ -7621,6 +7706,9 @@ int run_ui(int argc, char** argv, const Config& cfg, ScopeModel& model, Acquisit
|
|||||||
app.dbg_marker_x, app.dbg_marker_max, app.dbg_trigger_time,
|
app.dbg_marker_x, app.dbg_marker_max, app.dbg_trigger_time,
|
||||||
app.dbg_trigger_time_valid);
|
app.dbg_trigger_time_valid);
|
||||||
std::printf("[udpscope] mode after go-live: %s\n", app.dbg_mode_live.c_str());
|
std::printf("[udpscope] mode after go-live: %s\n", app.dbg_mode_live.c_str());
|
||||||
|
std::printf("[udpscope] status bar: glance=[● %s] max_glance_chars=%zu detail_fields=11 "
|
||||||
|
"popover_rebuilds=%d\n",
|
||||||
|
app.dbg_glance_last.c_str(), app.glance_max_chars, app.popover_rebuilds);
|
||||||
// The derived channels beside the channel they came from. A math channel defined by the pane
|
// The derived channels beside the channel they came from. A math channel defined by the pane
|
||||||
// reaches the producer by exactly the path the probe uses, so this is the pipeline's own
|
// reaches the producer by exactly the path the probe uses, so this is the pipeline's own
|
||||||
// assertion: `x2` must read twice its source's extremes, a low pass must read less.
|
// assertion: `x2` must read twice its source's extremes, a low pass must read less.
|
||||||
|
|||||||
Reference in new issue
Block a user