feat: the x axis is relative to now, and an export carries both clocks
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>
This commit is contained in:
1 parent
2f4530c128
commit
b313cac46e
18 files changed
+501
-145
No files matched your search
+16
-6
@@ -26,14 +26,24 @@ using plot::u64;
|
||||
/// live ring buffers switched by a pending flag, plus a display-epoch shift)
|
||||
/// could not stop.
|
||||
struct EventSnapshot {
|
||||
/// Seconds, monotonic -- edge-relative for a trigger event, the stream's own for a
|
||||
/// pause view. **f64, like the live ring it is cut from** (see `BufferStore::time()`):
|
||||
/// an event's stamps are small enough that f32 would nearly do, but a *pause* is
|
||||
/// deliberately not rebased, so its stamps carry the full stream time and quantise at
|
||||
/// the same 3.8 us per 32 s of uptime the live ring did. Keeping the two the same type
|
||||
/// also makes the cut in `publish_event()`/`publish_pause_view()` a straight copy.
|
||||
/// Seconds, monotonic, and **in the coordinates the axis reads**: a trigger event's
|
||||
/// stamps are edge-relative and a pause view's are relative to the instant it froze,
|
||||
/// which is what `x_offset` below records. **f64, like the live ring it is cut from**
|
||||
/// (see `BufferStore::time()`), which also makes the cut in `publish_event()`/
|
||||
/// `publish_pause_view()` a straight copy.
|
||||
std::vector<f64> stamps;
|
||||
std::vector<std::vector<f32>> values; ///< one array per channel, same length
|
||||
/// The stream time of this snapshot's x = 0, so `x_offset + stamps[i]` is the clock the
|
||||
/// data arrived on: the edge for a trigger event, the newest sample at the moment of the
|
||||
/// freeze for a pause view.
|
||||
///
|
||||
/// It exists because a snapshot is the one thing on screen whose coordinates outlive the
|
||||
/// `display_epoch()` that produced them, and `ScopeModel::stream_at_x0()` is what reads
|
||||
/// it: an export has to name the stream's own clock whatever is on screen, and the pause
|
||||
/// handler has to put a *display* window back on the ring to cut it. Before it, the
|
||||
/// export added `trigger_time()` to a pause view's stamps -- an unrelated edge, or none
|
||||
/// at all -- for want of anywhere to record which clock a snapshot was on.
|
||||
double x_offset = 0.0;
|
||||
double pre_s = 0.0; ///< seconds of pre-trigger data kept
|
||||
double post_s = 0.0; ///< seconds of post-trigger data kept
|
||||
std::vector<plot::Range> y; ///< per-channel value bounds, precomputed
|
||||
|
||||
+23
-19
@@ -38,7 +38,7 @@ std::string sanitise(const std::string& name) {
|
||||
/// two time columns, with a `_2`, `_3` suffix. Two elements of one vector signal can
|
||||
/// easily sanitise to the same string.
|
||||
std::vector<std::string> column_labels(const ExportTable& table) {
|
||||
std::vector<std::string> taken{"time", "stream_time"};
|
||||
std::vector<std::string> taken{"time", "abs_seconds"};
|
||||
std::vector<std::string> out;
|
||||
out.reserve(table.columns.size());
|
||||
for (const ExportTable::Column& col : table.columns) {
|
||||
@@ -106,6 +106,14 @@ ExportTable collect_export(ScopeModel& model, RowSet& rows) {
|
||||
// What is on screen: a pause's frozen view, or the trigger's held event. Both are immutable
|
||||
// snapshots, so the export is the picture the user is looking at either way.
|
||||
std::shared_ptr<const EventSnapshot> snap = model.display_snapshot();
|
||||
// The absolute clock of display x = 0, so `abs_base + time[i]` is the number the stream
|
||||
// stamped sample i with -- **one expression, correct in all four display modes**, which is the
|
||||
// whole reason `stream_at_x0()` exists. `epoch()` is the stream time of the first sample ever
|
||||
// seen (the rings store stamps relative to it) and `stream_at_x0()` is the offset of the
|
||||
// display's origin within that, whether it comes from a snapshot or from the live epoch. The
|
||||
// snapshot branch used to add `trigger_time()` here, which named an unrelated edge on a
|
||||
// *paused* export, or none at all before the first latch.
|
||||
const f64 abs_base = model.epoch() + model.stream_at_x0();
|
||||
if (snap != nullptr) {
|
||||
// The snapshot is immutable and already index-aligned, so the visible window is two
|
||||
// binary searches over its stamps and the same index range for every channel.
|
||||
@@ -115,15 +123,11 @@ ExportTable collect_export(ScopeModel& model, RowSet& rows) {
|
||||
if (hi <= lo) return table;
|
||||
const std::size_t first = static_cast<std::size_t>(lo - st.begin());
|
||||
const std::size_t n = static_cast<std::size_t>(hi - lo);
|
||||
// The event's x = 0 is the edge, so the stream clock has to be added back for the
|
||||
// second column -- the one case where the visible x is not the stream's own.
|
||||
const f64 edge = model.trigger_time().load(std::memory_order_relaxed);
|
||||
|
||||
table.time.resize(n);
|
||||
table.stream_time.resize(n);
|
||||
table.abs_seconds.resize(n);
|
||||
for (std::size_t i = 0; i < n; ++i) {
|
||||
table.time[i] = st[first + i];
|
||||
table.stream_time[i] = edge + table.time[i];
|
||||
table.abs_seconds[i] = abs_base + table.time[i];
|
||||
}
|
||||
for (const int c : chans) {
|
||||
const std::size_t ch = static_cast<std::size_t>(c);
|
||||
@@ -141,12 +145,12 @@ ExportTable collect_export(ScopeModel& model, RowSet& rows) {
|
||||
// the same window hold the same samples. The lock is against a structural change
|
||||
// dropping the rings under the copy, which is what the UI thread has to fear here.
|
||||
std::lock_guard<std::mutex> lock(model.mutex());
|
||||
// The axis reads `display_epoch + stamp`, so the window has to be put back on the
|
||||
// stream's clock before the ring is searched. It is 0 in live mode, but reading it is
|
||||
// what keeps this correct rather than merely true today.
|
||||
const f64 epoch = model.display_epoch();
|
||||
// The axis reads `stamp - stream_at_x0()`, so the window has to be put back on the stream's
|
||||
// clock before the ring is searched -- and that offset is a whole window while streaming now,
|
||||
// not the 0 it once was: the live axis is `[-window, 0]` about the newest sample.
|
||||
const f64 s0 = model.stream_at_x0();
|
||||
u64 first = 0, last = 0;
|
||||
model.live_window_locked(view.min - epoch, view.max - epoch, first, last);
|
||||
model.live_window_locked(view.min + s0, view.max + s0, first, last);
|
||||
const u64 n = last - first;
|
||||
if (n == 0) return table;
|
||||
|
||||
@@ -154,10 +158,10 @@ ExportTable collect_export(ScopeModel& model, RowSet& rows) {
|
||||
std::vector<f64> stamps(static_cast<std::size_t>(n));
|
||||
store.time().copy_from(first, n, stamps.data());
|
||||
table.time.resize(static_cast<std::size_t>(n));
|
||||
table.stream_time.resize(static_cast<std::size_t>(n));
|
||||
table.abs_seconds.resize(static_cast<std::size_t>(n));
|
||||
for (std::size_t i = 0; i < stamps.size(); ++i) {
|
||||
table.stream_time[i] = stamps[i];
|
||||
table.time[i] = epoch + table.stream_time[i];
|
||||
table.time[i] = stamps[i] - s0;
|
||||
table.abs_seconds[i] = abs_base + table.time[i];
|
||||
}
|
||||
for (const int c : chans) {
|
||||
const std::size_t ch = static_cast<std::size_t>(c);
|
||||
@@ -179,7 +183,7 @@ bool write_npy(const std::string& path, const ExportTable& table, std::string& e
|
||||
// `descr` as a list of (name, format) pairs is a *packed* structured dtype -- NumPy
|
||||
// only inserts alignment padding when asked to (`align=True`) -- so the records below
|
||||
// are field widths back to back with no gaps.
|
||||
std::string descr = "[('time', '<f8'), ('stream_time', '<f8')";
|
||||
std::string descr = "[('time', '<f8'), ('abs_seconds', '<f8')";
|
||||
for (const std::string& label : labels) descr += ", ('" + label + "', '<f4')";
|
||||
descr += "]";
|
||||
std::string header = "{'descr': " + descr + ", 'fortran_order': False, 'shape': (" +
|
||||
@@ -211,7 +215,7 @@ bool write_npy(const std::string& path, const ExportTable& table, std::string& e
|
||||
std::size_t at = 0;
|
||||
std::memcpy(record.data() + at, &table.time[i], sizeof(f64));
|
||||
at += sizeof(f64);
|
||||
std::memcpy(record.data() + at, &table.stream_time[i], sizeof(f64));
|
||||
std::memcpy(record.data() + at, &table.abs_seconds[i], sizeof(f64));
|
||||
at += sizeof(f64);
|
||||
for (const ExportTable::Column& col : table.columns) {
|
||||
const f32 v = i < col.values.size() ? col.values[i] : 0.0f;
|
||||
@@ -239,7 +243,7 @@ bool write_csv(const std::string& path, const ExportTable& table, std::string& e
|
||||
if (!opened(out, path, error)) return false;
|
||||
|
||||
const std::vector<std::string> labels = column_labels(table);
|
||||
std::string line = "time,stream_time";
|
||||
std::string line = "time,abs_seconds";
|
||||
for (const std::string& label : labels) line += "," + label;
|
||||
line += "\n";
|
||||
std::fwrite(line.data(), 1, line.size(), out.f);
|
||||
@@ -252,7 +256,7 @@ bool write_csv(const std::string& path, const ExportTable& table, std::string& e
|
||||
line.clear();
|
||||
std::snprintf(buf, sizeof(buf), "%.17g", table.time[i]);
|
||||
line += buf;
|
||||
std::snprintf(buf, sizeof(buf), "%.17g", table.stream_time[i]);
|
||||
std::snprintf(buf, sizeof(buf), "%.17g", table.abs_seconds[i]);
|
||||
line += ",";
|
||||
line += buf;
|
||||
for (const ExportTable::Column& col : table.columns) {
|
||||
|
||||
+16
-7
@@ -24,13 +24,22 @@ struct ExportTable {
|
||||
std::vector<f32> values; ///< `rows()` long, the ring's own `f32`
|
||||
};
|
||||
|
||||
/// The x the plot shows and the crosshair reads: stream seconds in live mode,
|
||||
/// edge-relative seconds in `[-pre, +post]` while an event is held.
|
||||
/// The x the plot shows and the crosshair reads, which is **relative** in every mode: seconds
|
||||
/// before now while streaming (so the newest sample is 0 and the window is `[-window, 0]`),
|
||||
/// seconds from the edge in `[-pre, +post]` while an event is held, and seconds from the
|
||||
/// instant of the freeze on a paused view.
|
||||
std::vector<f64> time;
|
||||
/// The stream's own clock, so two exports taken in different modes line up. Equal to
|
||||
/// `time` in live mode; `time + trigger_time` while an event is held, which is the one
|
||||
/// case where the visible x is not the stream's.
|
||||
std::vector<f64> stream_time;
|
||||
/// The stream's own absolute clock -- the number the time signal carried, which is what the
|
||||
/// source stamped the samples with -- so two exports taken in different modes, or from
|
||||
/// different runs, line up on one timeline.
|
||||
///
|
||||
/// `f64 seconds` rather than the wire's integer, because the integer does not exist in this
|
||||
/// process: `udps_frame_t::values` is a `const double*`, so the client has already widened
|
||||
/// every value by the time acquisition sees it. At Unix-epoch magnitudes an `f64` resolves
|
||||
/// about 240 ns, which is coarser than a 1 MS/s sample period -- so this column is the
|
||||
/// *provenance* of a row and `time` is the column to difference. Its own `np.diff` is
|
||||
/// legitimately not single-valued for that reason.
|
||||
std::vector<f64> abs_seconds;
|
||||
std::vector<Column> columns;
|
||||
|
||||
std::size_t rows() const noexcept { return time.size(); }
|
||||
@@ -45,7 +54,7 @@ struct ExportTable {
|
||||
/// model access. In trigger mode nothing is locked: the `EventSnapshot` is immutable.
|
||||
ExportTable collect_export(ScopeModel& model, RowSet& rows);
|
||||
|
||||
/// Writes a NumPy `.npy` file holding one structured array: `time` and `stream_time` as
|
||||
/// Writes a NumPy `.npy` file holding one structured array: `time` and `abs_seconds` as
|
||||
/// `<f8`, every signal as `<f4` -- the width the ring actually stores, so the file is
|
||||
/// about half the size of an all-`f8` one and invents no precision.
|
||||
///
|
||||
|
||||
@@ -6,9 +6,15 @@ Acquisition runs on its own thread and writes a ring buffer sized from <tt>--buf
|
||||
the drawing thread reads whatever that ring holds when a frame starts, which is why a trace
|
||||
never makes the window stutter.
|
||||
|
||||
The stream is the clock. Every time you read on screen is a timestamp the source sent, never
|
||||
your machine's, so a stalled source freezes the view instead of scrolling it -- and a source
|
||||
whose clock steps still gets the window you asked for.
|
||||
The stream is the clock. Every interval you read on screen is measured between timestamps the
|
||||
source sent, never your machine's, so a stalled source freezes the view instead of scrolling it
|
||||
-- and a source whose clock steps still gets the window you asked for.
|
||||
|
||||
The time axis is <i>relative to now</i>. While streaming it is a fixed <tt>[-window .. 0]</tt>
|
||||
with the newest sample at 0, so x reads as an age: <tt>-1.5s</tt> is a sample a second and a half
|
||||
old. In trigger mode 0 is the event's edge instead, and a paused view keeps the ages it was
|
||||
frozen with. The source's own clock is still there when you need it -- an export writes it as a
|
||||
second column.
|
||||
|
||||
<b>How to reach it</b>
|
||||
|
||||
|
||||
@@ -24,6 +24,11 @@ The full table is in the shortcut overview (<tt>Ctrl+/</tt>).
|
||||
zoom, so a 2 s window zoomed into reads 2 s again. The window is more than the axis: it is also
|
||||
the trigger's event span and how the samples are stored, so a wheel notch that redefined it
|
||||
would re-plan the buffer and drop your history.
|
||||
• The axis does not scroll — the <i>data</i> does. A live axis is a fixed <tt>[-window .. 0]</tt>
|
||||
with the newest sample at 0, so the labels stand still and the trace rolls under them. The
|
||||
cursors stand still with the labels (they are positions on screen, so a cursor you placed stays
|
||||
where you put it, over newer data each frame), while a measurement's marks travel with the
|
||||
sample they measured and leave the view when it does.
|
||||
• A live view is clamped to the window you asked for, not to what the ring happens to hold.
|
||||
That is why a buffer that has only just started filling leaves its left-hand side empty
|
||||
instead of squeezing the window down to the first few samples.
|
||||
|
||||
@@ -20,6 +20,8 @@ live ring and puts the window back on the newest data.
|
||||
zoom out is clamped to the snapshot's data.
|
||||
• Pausing is the easiest way to measure a layout or read a cursor precisely, because nothing
|
||||
under the pointer moves between frames.
|
||||
• The status bar says how long you have been paused — wall time, since the stream itself never
|
||||
stopped. It is there so a frozen picture cannot be mistaken for a live one that went quiet.
|
||||
|
||||
<b>Example</b>
|
||||
|
||||
|
||||
@@ -37,6 +37,11 @@ owns a <b>Reset</b> button. Drag a marker to move it. The mode dropdown offers f
|
||||
is drawn larger than a trace's sample dots so it does not read as one more sample.
|
||||
• The time row exists only in the modes that show the vertical pair. <tt>horizontal</tt> has no
|
||||
time row, by design.
|
||||
• A cursor <i>stays where you put it</i> while a live view rolls under it, which is what a bench
|
||||
scope does: its <tt>T0</tt> is an age, so a marker left at <tt>-1.5s</tt> keeps reading the
|
||||
sample a second and a half old as the stream advances. A measurement's marks do the opposite —
|
||||
they travel with the sample they measured — because a cursor is a question you asked and a mark
|
||||
is an answer about one particular sample.
|
||||
|
||||
<b>Example</b>
|
||||
|
||||
|
||||
@@ -17,15 +17,17 @@ the format follows the path's extension.
|
||||
• The stored rate is not always the wire rate. A window long enough to need decimation is stored
|
||||
averaged at a lower rate, and the export carries <i>that</i> — read the status bar's ⓘ popover
|
||||
for the figure before trusting a sample count.
|
||||
• Two time columns. <tt>time</tt> is the display timeline (relative to the trigger edge while an
|
||||
event is held) and <tt>stream_time</tt> is the source's own clock. Both are strictly increasing
|
||||
and evenly spaced; duplicate or backwards stamps would mean the file held two clocks.
|
||||
• Two time columns. <tt>time</tt> is the display timeline — negative ages ending at 0 while
|
||||
streaming, offsets from the trigger edge while an event is held — so it is the x you read a
|
||||
value at on screen. <tt>abs_seconds</tt> is the source's own clock for the same sample, which
|
||||
is what lets two exports taken in different modes line up. Both are strictly increasing and
|
||||
evenly spaced; duplicate or backwards stamps would mean the file held two clocks.
|
||||
• An export taken <i>mid-capture</i> writes the partly filled window that is on screen, which is
|
||||
shorter than the event span. Export outside a capture if you want the whole event.
|
||||
|
||||
<b>Example</b>
|
||||
|
||||
A 2 s window at 10 kS/s over 3 traces writes 20001 rows and 5 columns
|
||||
(<tt>time</tt>, <tt>stream_time</tt>, three signals). The <tt>.npy</tt> is
|
||||
(<tt>time</tt>, <tt>abs_seconds</tt>, three signals). The <tt>.npy</tt> is
|
||||
<tt>64·⌈(10+len(header)+1)/64⌉ + rows·(16 + 4·cols)</tt> bytes. Read it back with
|
||||
<tt>pd.DataFrame(np.load(path))</tt>.
|
||||
@@ -12,8 +12,10 @@ The bar is always visible under the plot. Click <b>ⓘ</b> for the detail popove
|
||||
• The badge is one glyph in three colours: lit and dim alternating means data is arriving, and a
|
||||
steady idle colour means it is not. A badge that never blinks is not a working badge.
|
||||
• The state field is one word from a fixed set: <tt>live</tt>, <tt>paused</tt>, <tt>armed</tt>,
|
||||
<tt>capturing</tt>, <tt>event held</tt>, <tt>disarmed</tt>. In trigger mode the bar adds
|
||||
exactly one more field, how long ago the last event was.
|
||||
<tt>capturing</tt>, <tt>event held</tt>, <tt>disarmed</tt>. Two states add one field each: in
|
||||
trigger mode, how long ago the last event was; while paused, how long you have been paused.
|
||||
• The paused figure is <i>wall</i> time, not stream time — the producer never stops while paused,
|
||||
so it answers "how long have I been looking at this picture".
|
||||
• Nothing on the bar may change width with the stream, because the bar sits directly above the
|
||||
plot and a wider label would relayout it. That is why the rate collapses to one figure or a
|
||||
range rather than listing per channel, and why an elapsed time steps to a coarser unit instead
|
||||
|
||||
+10
-4
@@ -515,8 +515,9 @@ void ScopeModel::reset_live_history_locked() {
|
||||
}
|
||||
}
|
||||
|
||||
void ScopeModel::publish_pause_view(double lo_s, double hi_s) {
|
||||
void ScopeModel::publish_pause_view(double lo_s, double hi_s, double x_offset) {
|
||||
auto snap = std::make_shared<EventSnapshot>();
|
||||
snap->x_offset = x_offset;
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
BufferStore& src = live();
|
||||
u64 first = 0;
|
||||
@@ -525,10 +526,14 @@ void ScopeModel::publish_pause_view(double lo_s, double hi_s) {
|
||||
const u64 n = last - first;
|
||||
snap->stamps.resize(static_cast<std::size_t>(n));
|
||||
if (n > 0) {
|
||||
// The stamps go in *as the ring holds them*: the stream's own seconds, which is the
|
||||
// coordinate the axis was already reading. Nothing is rebased, so a pause does not move the
|
||||
// dates under the reader the way a trigger event's edge-relative axis deliberately does.
|
||||
// The stamps come out of the ring on the stream's own clock and are rebased onto
|
||||
// `x_offset`, which is the coordinate the axis was *already* reading: a live view is
|
||||
// newest-relative, so leaving them absolute would throw the frozen picture a whole uptime
|
||||
// to the right of the axis at the instant the user pressed pause. Nothing is lost by the
|
||||
// shift -- `x_offset` is exactly what makes the stream's time recoverable, which is what
|
||||
// `stream_at_x0()` reads it for.
|
||||
src.time().copy_from(first, n, snap->stamps.data());
|
||||
for (f64& t : snap->stamps) t -= x_offset;
|
||||
}
|
||||
snap->values.resize(channels_.size());
|
||||
snap->y.resize(channels_.size(), plot::Range{0.0, 0.0});
|
||||
@@ -551,6 +556,7 @@ void ScopeModel::publish_pause_view(double lo_s, double hi_s) {
|
||||
|
||||
void ScopeModel::publish_event(double pre_s, double post_s, double edge) {
|
||||
auto snap = std::make_shared<EventSnapshot>();
|
||||
snap->x_offset = edge; // the stream time x = 0 means, for everything that has to cross back
|
||||
BufferStore& src = live();
|
||||
plot::data::RingBuffer<f64>& tsrc = src.time();
|
||||
|
||||
|
||||
+29
-10
@@ -290,16 +290,17 @@ class ScopeModel {
|
||||
void live_window_locked(double lo_s, double hi_s, u64& first, u64& last);
|
||||
/// The frozen view taken when the user paused, or null while running. A pause is a
|
||||
/// snapshot of the *visible window* in exactly the sense a trigger event is -- an immutable
|
||||
/// series the plot binds to -- and the difference is the anchoring: an event's stamps are
|
||||
/// edge-relative, a pause's are the stream's own, so the axis does not move when the pause
|
||||
/// begins and the reader keeps the times they were already reading.
|
||||
/// series the plot binds to -- and both are anchored the same way: the stamps are the
|
||||
/// coordinates the axis was already reading, with `x_offset` recording which instant that
|
||||
/// x = 0 was. So the axis does not move when the pause begins.
|
||||
std::shared_ptr<const EventSnapshot> pause_view() const noexcept {
|
||||
return pause_view_.load(std::memory_order_acquire);
|
||||
}
|
||||
/// Snapshots the live ring over `[lo_s, hi_s]` (stream seconds, the display epoch already
|
||||
/// removed) as the pause view. The acquisition thread keeps writing, so this is a copy: what
|
||||
/// is frozen is the picture, not the data.
|
||||
void publish_pause_view(double lo_s, double hi_s);
|
||||
/// Snapshots the live ring over `[lo_s, hi_s]` (stream seconds, i.e. the display window put
|
||||
/// back on the stream's clock) as the pause view, rebased onto `x_offset` so its stamps are
|
||||
/// the x the axis is already showing. The acquisition thread keeps writing, so this is a
|
||||
/// copy: what is frozen is the picture, not the data.
|
||||
void publish_pause_view(double lo_s, double hi_s, double x_offset);
|
||||
void clear_pause_view() noexcept { pause_view_.store(nullptr, std::memory_order_release); }
|
||||
|
||||
/// Whether the user paused acquisition. Lives here rather than on `Acquisition` because the
|
||||
@@ -378,12 +379,30 @@ class ScopeModel {
|
||||
return event() != nullptr ? DisplayMode::Held : DisplayMode::Waiting;
|
||||
}
|
||||
|
||||
/// The x origin the plot uses: 0 for "seconds since the stream started", or minus
|
||||
/// the trigger instant while in trigger mode, so the axis reads seconds from the
|
||||
/// event. Applied to the live store's channels.
|
||||
/// The offset the **live** channels report their x with: `display_x = stream_t + epoch`.
|
||||
/// Minus the newest stored sample's stamp while streaming, so the axis reads
|
||||
/// `[-window, 0]` and the newest data is at 0; minus the trigger instant while a capture
|
||||
/// is filling, so the axis reads seconds from the event. It is 0 while a *snapshot* is on
|
||||
/// screen (a held event, a paused view), because then the live channels are bound to
|
||||
/// nothing and the snapshot's own `x_offset` is what says which clock x = 0 is --
|
||||
/// `stream_at_x0()` is the accessor that does not have to know which of the two it is.
|
||||
f64 display_epoch() const noexcept { return display_epoch_.load(std::memory_order_relaxed); }
|
||||
void set_display_epoch(f64 e) noexcept;
|
||||
|
||||
/// The stream time of the display's x = 0, so `stream_t = display_x + stream_at_x0()` and
|
||||
/// `display_x = stream_t - stream_at_x0()` in **every** display mode.
|
||||
///
|
||||
/// One accessor for the two places the offset can live: a snapshot carries its own
|
||||
/// (`EventSnapshot::x_offset`, frozen with the picture), and a live view carries it as
|
||||
/// minus the display epoch. Everything that has to cross between the two clocks goes
|
||||
/// through here -- the export's absolute column, and the pause handler putting a display
|
||||
/// window back on the ring to cut it -- rather than picking the branch itself and getting
|
||||
/// one of the four modes wrong.
|
||||
f64 stream_at_x0() const noexcept {
|
||||
if (std::shared_ptr<const EventSnapshot> snap = display_snapshot()) return snap->x_offset;
|
||||
return -display_epoch();
|
||||
}
|
||||
|
||||
// --- stream state ---------------------------------------------------------
|
||||
StreamStats& stats() noexcept { return stats_; }
|
||||
const StreamStats& stats() const noexcept { return stats_; }
|
||||
|
||||
+15
-3
@@ -178,6 +178,14 @@ class RowSet {
|
||||
void set_time_window(double seconds);
|
||||
double time_window() const noexcept { return window_s_; }
|
||||
|
||||
/// The newest x any row's live traces hold, which is what the link group follows -- and, since
|
||||
/// it is reported in the display epoch currently in force, also what the frame tick subtracts
|
||||
/// from that epoch to park the newest sample at x = 0. It must be read **after** `bind()` and
|
||||
/// `acquire()`: it asks the bound series, so before them it answers for the previous frame's
|
||||
/// window, and a frame's production at 1 MS/s over a 50 ms view is a third of the screen.
|
||||
/// False when no row holds a streaming trace.
|
||||
bool live_edge(double& newest) const;
|
||||
|
||||
void set_y_auto(int row, bool on);
|
||||
void set_y_range(int row, double lo, double hi);
|
||||
|
||||
@@ -191,7 +199,13 @@ class RowSet {
|
||||
void place_crossings();
|
||||
CursorMode cursor_mode() const noexcept { return cursor_mode_; }
|
||||
|
||||
/// The two vertical cursors' stream times; shared by every row.
|
||||
/// The two vertical cursors' positions, in the **display** x the axis reads -- not stream
|
||||
/// seconds, which they coincided with only while `display_epoch()` was always 0. A live view
|
||||
/// is newest-relative now, so a cursor left at -1.5 s keeps meaning "1.5 s ago" and the data
|
||||
/// flows under it, which is what a bench scope's roll mode does and is the behaviour that was
|
||||
/// asked for. They are read back out of the plots and written to them unchanged, so there is
|
||||
/// nothing to convert; anything that needs the stream's own clock goes through
|
||||
/// `ScopeModel::stream_at_x0()`. Shared by every row.
|
||||
double cursor_a_x() const noexcept { return cursor_a_x_; }
|
||||
double cursor_b_x() const noexcept { return cursor_b_x_; }
|
||||
/// One row's ruler values, or 0 for a row that does not exist.
|
||||
@@ -399,8 +413,6 @@ class RowSet {
|
||||
/// at the end of the gesture (`Controller::end_drag()`), and a value read out of the plot
|
||||
/// mid-drag has already been through that act.
|
||||
void place_cursors(Row& r, plot::CursorPlacement placement = plot::CursorPlacement::Snapped);
|
||||
/// The newest x any row's live traces hold, which is what the group follows.
|
||||
bool live_edge(double& newest) const;
|
||||
|
||||
std::unique_ptr<plot::Figure> figure_;
|
||||
/// The one x link group every visible row belongs to. Groups are numbered from 1.
|
||||
|
||||
@@ -48,6 +48,11 @@ std::string format_glance(const StatusSnapshot& s) {
|
||||
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) {
|
||||
@@ -74,6 +79,7 @@ std::string format_detail(const StatusSnapshot& s) {
|
||||
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";
|
||||
|
||||
+12
-2
@@ -44,10 +44,20 @@ struct StatusSnapshot {
|
||||
bool have_event = false;
|
||||
double since_event_s = 0.0;
|
||||
int reconnects = 0;
|
||||
|
||||
/// How long the view has been frozen, in **wall** seconds, and whether it is. The producer
|
||||
/// never stops while paused -- it keeps filling the live ring, which is what makes the resume
|
||||
/// seamless -- so the stream's own clock says nothing about how long the user has been looking
|
||||
/// at a still picture. Written through `format_elapsed()` like the event age, for the same
|
||||
/// reason: it steps to a coarser unit instead of growing digits, and the bar is directly above
|
||||
/// the plot.
|
||||
bool paused = false;
|
||||
double paused_for_s = 0.0;
|
||||
};
|
||||
|
||||
/// The bar: "live · 10 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.
|
||||
/// The bar: "live · 10 kS/s · 2 s window", plus " · event 4.2 s ago" in trigger mode, " · for
|
||||
/// 12 s" while paused, 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
|
||||
|
||||
+135
-32
@@ -137,6 +137,12 @@ struct App {
|
||||
GtkWidget* trigger_holdoff_spin = nullptr;
|
||||
GtkWidget* signal_list = nullptr;
|
||||
GtkWidget* pause_button = nullptr;
|
||||
/// When the pause began, on the **monotonic wall clock**, or 0 while running. The elapsed time
|
||||
/// the status bar shows has to be wall time rather than stream time: the producer never stops
|
||||
/// while paused (it keeps filling the live ring, which is what makes the resume seamless), so
|
||||
/// the stream's own clock advances whether the view is frozen or not and would say nothing
|
||||
/// about how long the user has been looking at a still picture.
|
||||
gint64 pause_started_us = 0;
|
||||
GtkWidget* menu_button = nullptr;
|
||||
GtkWidget* cursors_toggle = nullptr;
|
||||
/// Where the panel controls ended up, decided once at build time: the dump runs after the window
|
||||
@@ -198,6 +204,12 @@ struct App {
|
||||
std::vector<std::string> measure_text;
|
||||
/// The window the current results were computed over, and whether a capture was held then:
|
||||
/// a recomputation is owed when either moves (see `update_measures`).
|
||||
///
|
||||
/// It is kept in **stream** seconds (through `ScopeModel::stream_at_x0()`), not in the display x
|
||||
/// it was read as, because a live view is newest-relative: the axis there is a fixed
|
||||
/// `[-window, 0]` whatever arrives, so "the window moved" is invisible in display coordinates
|
||||
/// and the pane would compute once and then never again -- the readout frozen on the samples
|
||||
/// that happened to be on screen when the pane opened.
|
||||
plot::Range measure_window{};
|
||||
bool measure_window_valid = false;
|
||||
bool measure_was_held = false;
|
||||
@@ -245,7 +257,17 @@ struct App {
|
||||
std::vector<MeasureResult> measure_last_valid;
|
||||
/// The marks the last computation produced: re-placed every frame, which is what makes the
|
||||
/// measurement cursors read-only.
|
||||
///
|
||||
/// A **time** mark's `value` is held in stream seconds, like `measure_window`, and converted to
|
||||
/// the axis' own x by `place_measure_marks()` on the frame it is placed. That is what keeps a
|
||||
/// mark glued to the sample it measured while a live view rolls the data leftwards under it --
|
||||
/// a mark is a *result*, so it travels with its datum, which is the opposite of a cursor (an
|
||||
/// instrument the user put at a position and that stays there). A level mark's `value` is a y
|
||||
/// and has nothing to do with either clock.
|
||||
std::vector<RowSet::MeasureMark> measure_marks;
|
||||
/// `measure_marks` with the time marks converted to display x, rebuilt in place each frame
|
||||
/// rather than allocated: this is on the frame tick's path.
|
||||
std::vector<RowSet::MeasureMark> measure_marks_display;
|
||||
/// What the dump prints for the header and the panels. Captured while the window is alive:
|
||||
/// the dump runs from the destroy handler, where every widget call is a GTK-CRITICAL and
|
||||
/// reads nothing.
|
||||
@@ -1978,8 +2000,16 @@ void on_pause_toggled(GtkToggleButton* button, gpointer user) {
|
||||
// arrived while paused instead of a hole.
|
||||
if (RowSet::Row& r0 = app->rows->row(0); r0.plot != nullptr) {
|
||||
const plot::Range view = r0.plot->x_range(r0.axis_x);
|
||||
const double epoch = app->model->display_epoch();
|
||||
app->model->publish_pause_view(view.min - epoch, view.max - epoch);
|
||||
// The axis is in display x and the ring is on the stream's clock, so the window has to be
|
||||
// put back on that clock to be cut -- `stream_at_x0()` is the one accessor that is right in
|
||||
// every mode, including a pause taken while an event is *held*, where the axis is
|
||||
// edge-relative and the old `- display_epoch()` form searched the ring with event
|
||||
// coordinates. The same offset is handed to the snapshot as its `x_offset`, so its stamps
|
||||
// come back rebased onto the x the axis is already showing and the frozen picture does not
|
||||
// move at the instant of the pause.
|
||||
const double s0 = app->model->stream_at_x0();
|
||||
app->model->publish_pause_view(view.min + s0, view.max + s0, s0);
|
||||
app->pause_started_us = g_get_monotonic_time();
|
||||
// Pin on what the snapshot *got*: the cut is clamped to the ring, so it can be narrower than
|
||||
// the view, and pinning the request left the shortfall as an empty left margin.
|
||||
plot::Range pin = view;
|
||||
@@ -1994,6 +2024,7 @@ void on_pause_toggled(GtkToggleButton* button, gpointer user) {
|
||||
} else {
|
||||
app->model->clear_pause_view();
|
||||
app->rows->unpin_pause_window();
|
||||
app->pause_started_us = 0;
|
||||
}
|
||||
gtk_button_set_icon_name(GTK_BUTTON(button), paused ? "media-playback-start-symbolic"
|
||||
: "media-playback-pause-symbolic");
|
||||
@@ -2016,7 +2047,12 @@ void on_trigger_toggled(GtkToggleButton* button, gpointer user) {
|
||||
app->capturing = false;
|
||||
if (!on) {
|
||||
app->model->trigger_time_valid().store(false, std::memory_order_relaxed);
|
||||
app->model->set_display_epoch(0.0); // back to seconds since the stream started
|
||||
// The epoch is *not* written here: the frame tick owns it and states the live answer -- minus
|
||||
// the newest acquired sample -- after it has bound and acquired, which is the only moment the
|
||||
// right number is knowable. This used to set it to 0 ("seconds since the stream started"),
|
||||
// which is no longer a display coordinate anything uses. Nothing is left stale in the
|
||||
// meantime: the group follows the live edge again from here, and `Figure::layout()` re-derives
|
||||
// the range from it on every frame.
|
||||
app->rows->set_time_window(app->rows->time_window()); // and follow the live edge
|
||||
}
|
||||
if (on && signal >= 0) {
|
||||
@@ -2876,6 +2912,19 @@ void open_measure_dialog(App& app, int edit_index) {
|
||||
gtk_window_present(GTK_WINDOW(window));
|
||||
}
|
||||
|
||||
/// Puts `App::measure_marks` on the plots, converting each **time** mark from the stream seconds
|
||||
/// it is stored in to the x the axis is reading this frame. Called every frame -- that is what
|
||||
/// makes the marks read-only (a gesture moves one, and this puts it back) and, since a live axis
|
||||
/// is newest-relative, also what rolls each mark leftwards with the sample it measured.
|
||||
void place_measure_marks(App& app) {
|
||||
const double s0 = app.model->stream_at_x0();
|
||||
app.measure_marks_display = app.measure_marks;
|
||||
for (RowSet::MeasureMark& mk : app.measure_marks_display) {
|
||||
if (!mk.level) mk.value -= s0;
|
||||
}
|
||||
app.rows->set_measure_marks(app.measure_marks_display);
|
||||
}
|
||||
|
||||
/// Recomputes every measurement over the visible window and writes the results into the pane,
|
||||
/// collecting the marks the ones asking to be shown need.
|
||||
///
|
||||
@@ -2890,12 +2939,17 @@ void update_measures(App& app, bool force) {
|
||||
if (app.measure_labels.size() != app.measures.size()) return; // mid-rebuild
|
||||
const RowSet::Row& r0 = app.rows->row(0);
|
||||
if (r0.plot == nullptr) return;
|
||||
// The window on the **stream's** clock, which is the only space in which "it moved" is a
|
||||
// question with an answer: a live axis is newest-relative, so `view` itself is a fixed
|
||||
// `[-window, 0]` that never changes while the data under it does.
|
||||
const double s0 = app.model->stream_at_x0();
|
||||
const plot::Range view = r0.plot->x_range(r0.axis_x);
|
||||
const plot::Range swin{view.min + s0, view.max + s0};
|
||||
const bool held = app.model->display_mode() == DisplayMode::Held;
|
||||
const bool moved = app.measure_window_valid && !held &&
|
||||
std::fabs(view.min - app.measure_window.min) +
|
||||
std::fabs(view.max - app.measure_window.max) >
|
||||
0.02 * std::max(1.0e-12, view.span());
|
||||
std::fabs(swin.min - app.measure_window.min) +
|
||||
std::fabs(swin.max - app.measure_window.max) >
|
||||
0.02 * std::max(1.0e-12, swin.span());
|
||||
// A measurement is a statistic of the visible window, and *a live view moves that window every
|
||||
// frame* -- so "recompute when the window moves" is "recompute every frame", at O(visible
|
||||
// samples) per measurement. That is what stalled the frame tick (0.4 ms -> 13.7 ms, a third of
|
||||
@@ -2918,16 +2972,16 @@ void update_measures(App& app, bool force) {
|
||||
// lag on it is not readable.
|
||||
const bool changed = !app.measure_window_valid || held != app.measure_was_held || moved;
|
||||
if (!force && !changed) {
|
||||
app.rows->set_measure_marks(app.measure_marks);
|
||||
place_measure_marks(app);
|
||||
return;
|
||||
}
|
||||
if (!force && moved && held == app.measure_was_held && !due) {
|
||||
// Nothing to recompute -- but the marks are still re-placed below, which is what makes
|
||||
// them read-only: a gesture moves one on the plot, and this is what puts it back.
|
||||
app.rows->set_measure_marks(app.measure_marks);
|
||||
place_measure_marks(app);
|
||||
return;
|
||||
}
|
||||
app.measure_window = view;
|
||||
app.measure_window = swin;
|
||||
app.measure_window_valid = true;
|
||||
app.measure_was_held = held;
|
||||
app.measure_stamp_us = now_us;
|
||||
@@ -3131,11 +3185,15 @@ void update_measures(App& app, bool force) {
|
||||
const plot::u32 color = measure_color(i);
|
||||
for (const MeasureMarkSpec& spec : mk->marks) {
|
||||
if (!spec.level && !r.valid) continue;
|
||||
marks.push_back({row, spec.level, spec.value, color});
|
||||
// A time mark is stored on the **stream's** clock -- the walks report display x, so `s0`
|
||||
// puts it back -- and `place_measure_marks()` converts it again on every frame it draws.
|
||||
// That is what keeps the mark on its own sample as a live view rolls underneath it; a level
|
||||
// mark is a y and is stored as it comes.
|
||||
marks.push_back({row, spec.level, spec.level ? spec.value : spec.value + s0, color});
|
||||
}
|
||||
}
|
||||
app.measure_marks = std::move(marks);
|
||||
app.rows->set_measure_marks(app.measure_marks);
|
||||
place_measure_marks(app);
|
||||
}
|
||||
|
||||
/// The measurements panel: a header with the `[+]`, and one row per measurement.
|
||||
@@ -4865,16 +4923,37 @@ gboolean tick(gpointer user) {
|
||||
// UI's own structural changes: the bind takes shared handles into the live
|
||||
// store, and acquire() reads it.
|
||||
std::lock_guard<std::mutex> lock(model.mutex());
|
||||
// Mid-capture the live ring is drawn on the coming event's clock, so its edge sits at
|
||||
// x = 0 exactly as a snapshot's does and the pinned [-pre, +post] axis means the same
|
||||
// thing in both. Every other mode reads stream seconds. The epoch is a consumer-side
|
||||
// offset on the live channels, so it has to be in place before the bind that hands
|
||||
// them to the traces -- and it is written here, under the lock, because a rebuild can
|
||||
// replace the store.
|
||||
const double want_epoch = display == DisplayMode::Capturing ? -cap_edge : 0.0;
|
||||
if (model.display_epoch() != want_epoch) model.set_display_epoch(want_epoch);
|
||||
app->rows->bind(model, display);
|
||||
app->rows->acquire(model);
|
||||
// What x = 0 means, written *after* the bind and the acquire on purpose: the live answer
|
||||
// is minus the newest sample the traces can now report, and asking before they were bound
|
||||
// would put the newest data a frame's production to the right of the axis -- a third of
|
||||
// the screen at 1 MS/s over a 50 ms window. The epoch is only read on the consumer side
|
||||
// (it offsets the live channels' x), so nothing between here and the next frame's bind
|
||||
// depends on it having been set earlier. It is written under the lock because a rebuild
|
||||
// can replace the store it is pushed into.
|
||||
//
|
||||
// Three cases, one rule -- `display_x = stream_t + epoch`:
|
||||
// * streaming, the newest stored sample sits at x = 0, so the axis is a fixed
|
||||
// [-window, 0] and the data flows leftwards through it. A scope's time base is "how
|
||||
// long ago", and the stream's own clock is an uptime nobody reads off an axis.
|
||||
// * mid-capture, the coming event's edge sits at x = 0 exactly as the published
|
||||
// snapshot's will, so the pinned [-pre, +post] means the same thing in both and the
|
||||
// picture does not jump at the latch.
|
||||
// * with a *snapshot* on screen (a held event, a paused view) the live channels are
|
||||
// bound to nothing and the snapshot carries its own `x_offset`, so the epoch is 0 and
|
||||
// `ScopeModel::stream_at_x0()` is what crosses between the clocks.
|
||||
double want_epoch = 0.0;
|
||||
if (display == DisplayMode::Capturing) {
|
||||
want_epoch = -cap_edge;
|
||||
} else if (model.display_snapshot() == nullptr) {
|
||||
// `live_edge()` reports in the epoch currently in force, so subtracting it is minus the
|
||||
// raw stamp: the fixed point is reached in one frame rather than converged on.
|
||||
double newest = 0.0;
|
||||
want_epoch = app->rows->live_edge(newest) ? model.display_epoch() - newest
|
||||
: model.display_epoch();
|
||||
}
|
||||
if (model.display_epoch() != want_epoch) model.set_display_epoch(want_epoch);
|
||||
}
|
||||
|
||||
// The gesture probe's verdicts, one frame after the ask and after this frame's clamp: a
|
||||
@@ -5468,6 +5547,12 @@ gboolean tick(gpointer user) {
|
||||
? 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));
|
||||
// Paused *and* with a recorded start: the button's handler is what stamps it, so a pause
|
||||
// arriving any other way (there is none today) reports the state without a duration rather
|
||||
// than counting from the epoch.
|
||||
snap.paused = paused && app->pause_started_us != 0;
|
||||
snap.paused_for_s =
|
||||
snap.paused ? double(g_get_monotonic_time() - app->pause_started_us) / 1.0e6 : 0.0;
|
||||
|
||||
const std::string glance = udpscope::format_glance(snap);
|
||||
app->dbg_glance_last = glance;
|
||||
@@ -6625,8 +6710,11 @@ gboolean measure_trace(gpointer user) {
|
||||
line += " " + std::string(measure_type_name(a->measures[i].type)) + "=" +
|
||||
(r.valid ? std::to_string(r.primary) : std::string("--"));
|
||||
}
|
||||
line += " marks=" + std::to_string(a->measure_marks.size());
|
||||
for (const RowSet::MeasureMark& m : a->measure_marks) {
|
||||
// The marks as *placed*, not as stored: a time mark is kept on the stream's clock and converted
|
||||
// for the axis each frame (see `place_measure_marks()`), and it is the converted value that is
|
||||
// comparable with the `view=` printed beside it.
|
||||
line += " marks=" + std::to_string(a->measure_marks_display.size());
|
||||
for (const RowSet::MeasureMark& m : a->measure_marks_display) {
|
||||
line += " " + std::string(m.level ? "level" : "time") + ":" + std::to_string(m.value);
|
||||
}
|
||||
std::fprintf(stderr, "%s\n", line.c_str());
|
||||
@@ -7039,11 +7127,14 @@ void pause_probe_report(App& a, const char* what) {
|
||||
const RowSet::Row& r0 = a.rows->row(0);
|
||||
axis = r0.plot->x_range(r0.axis_x);
|
||||
}
|
||||
// The bar's own string, because "how long have I been paused" is a *status* field and the
|
||||
// elapsed figure is the one part of it that no other line can show: the exit dump's
|
||||
// `status bar:` is read live and the two `mode ...` lines are the live and triggered states.
|
||||
std::fprintf(stderr,
|
||||
"[pause-probe] %-9s paused=%d ring_produced=%zu event_size=%zu "
|
||||
"frozen_x=[%.6f..%.6f] axis=[%.6f..%.6f]\n",
|
||||
"frozen_x=[%.6f..%.6f] axis=[%.6f..%.6f] glance=[%s]\n",
|
||||
what, a.model->paused() ? 1 : 0, ring_produced, event_size, bounds_lo, bounds_hi,
|
||||
double(axis.min), double(axis.max));
|
||||
double(axis.min), double(axis.max), a.dbg_glance_last.c_str());
|
||||
std::fflush(stderr);
|
||||
}
|
||||
|
||||
@@ -7115,13 +7206,13 @@ gboolean export_probe(gpointer user) {
|
||||
const ExportTable table = collect_export(*a->model, *a->rows);
|
||||
std::fprintf(stderr,
|
||||
"[export-probe] mode=%s axis=[%.6f..%.6f] rows=%zu cols=%zu time=[%.6f..%.6f] "
|
||||
"stream=[%.6f..%.6f]\n",
|
||||
"abs=[%.6f..%.6f]\n",
|
||||
a->model->trigger_enabled() && a->model->event() != nullptr ? "event" : "live",
|
||||
axis.min, axis.max, table.rows(), table.columns.size(),
|
||||
table.rows() != 0 ? table.time.front() : 0.0,
|
||||
table.rows() != 0 ? table.time.back() : 0.0,
|
||||
table.rows() != 0 ? table.stream_time.front() : 0.0,
|
||||
table.rows() != 0 ? table.stream_time.back() : 0.0);
|
||||
table.rows() != 0 ? table.abs_seconds.front() : 0.0,
|
||||
table.rows() != 0 ? table.abs_seconds.back() : 0.0);
|
||||
for (const ExportTable::Column& col : table.columns) {
|
||||
std::fprintf(stderr, "[export-probe] column '%s' n=%zu\n", col.name.c_str(), col.values.size());
|
||||
}
|
||||
@@ -7587,10 +7678,13 @@ gboolean measures_probe_step(gpointer user) {
|
||||
// A gesture on a measurement's own cursor, placed through the plot exactly as the
|
||||
// controller would: the next frame has to put it back, which is what "read-only" means
|
||||
// here (the library has no flag the hit test would respect).
|
||||
if (!a->measure_marks.empty()) {
|
||||
// The marks as *placed* throughout: the drag writes a plot cursor and the check below reads
|
||||
// one back, so both sides have to be in the plot's own coordinates -- and a time mark is
|
||||
// stored on the stream's clock (see `place_measure_marks()`).
|
||||
if (!a->measure_marks_display.empty()) {
|
||||
RowSet::Row& r0 = a->rows->row(0);
|
||||
if (r0.plot != nullptr) {
|
||||
const RowSet::MeasureMark& mk = a->measure_marks.front();
|
||||
const RowSet::MeasureMark& mk = a->measure_marks_display.front();
|
||||
if (mk.level) {
|
||||
r0.plot->set_cursor_y(r0.measure_h[0], mk.value + 3.0, plot::CursorPlacement::Free);
|
||||
} else {
|
||||
@@ -7603,9 +7697,13 @@ gboolean measures_probe_step(gpointer user) {
|
||||
default: {
|
||||
double restored = -1.0;
|
||||
double want = 0.0;
|
||||
if (!a->measure_marks.empty()) {
|
||||
// Both read *now*: a time mark rolls leftwards with its sample on a live axis, so the value
|
||||
// placed on the most recent frame is the only one the plot's cursor can be expected to hold
|
||||
// -- comparing against the value the drag was launched from would read 300 ms of scroll as a
|
||||
// failure to restore.
|
||||
if (!a->measure_marks_display.empty()) {
|
||||
const RowSet::Row& r0 = a->rows->row(0);
|
||||
const RowSet::MeasureMark& mk = a->measure_marks.front();
|
||||
const RowSet::MeasureMark& mk = a->measure_marks_display.front();
|
||||
want = mk.value;
|
||||
if (r0.plot != nullptr) {
|
||||
restored = mk.level ? r0.plot->cursor(r0.measure_h[0]).y
|
||||
@@ -7614,7 +7712,7 @@ gboolean measures_probe_step(gpointer user) {
|
||||
}
|
||||
std::fprintf(stderr,
|
||||
"[measures-probe] %-14s marks=%zu first=%.6f plot_now=%.6f restored=%d\n",
|
||||
"after drag", a->measure_marks.size(), want, restored,
|
||||
"after drag", a->measure_marks_display.size(), want, restored,
|
||||
std::fabs(restored - want) < 1.0e-9 ? 1 : 0);
|
||||
std::fflush(stderr);
|
||||
measure_dialog_report(*a, "final");
|
||||
@@ -8384,8 +8482,13 @@ int run_ui(int argc, char** argv, const Config& cfg, ScopeModel& model, Acquisit
|
||||
// `results_for` is the window the numbers describe, `win` the one on screen now: the
|
||||
// drift between them is the assertion that a measurement is a statistic of the *current*
|
||||
// window -- a timer-decoupled recomputation is what used to leave a marker outside it.
|
||||
// Both on the **stream's** clock, which is the space `measure_window` and the time marks
|
||||
// are stored in: a live axis is a fixed `[-window, 0]`, so comparing display x would read
|
||||
// no drift whatever the data did (see `App::measure_window`).
|
||||
const double s0 = model.stream_at_x0();
|
||||
const RowSet::Row& r0 = rows.row(0);
|
||||
const plot::Range win = r0.plot != nullptr ? r0.plot->x_range(r0.axis_x) : plot::Range{};
|
||||
const plot::Range view = r0.plot != nullptr ? r0.plot->x_range(r0.axis_x) : plot::Range{};
|
||||
const plot::Range win{view.min + s0, view.max + s0};
|
||||
const double span = app.measure_window.span();
|
||||
int in_window = 0;
|
||||
int marks = 0;
|
||||
|
||||
Reference in new issue
Block a user