Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b505706d92 | ||
|
|
3d7e214295 | ||
|
|
9888fd9977 | ||
|
|
1f6738031a | ||
|
|
d03c9fb443 |
No files matched your search
@@ -449,13 +449,28 @@ Two lines of the self-check output are assertions in disguise:
|
||||
equal `origin + first_rel` as a *tick label* plus its caption, i.e. the labels and the
|
||||
caption must share one anchor. `x_origin_set` is read back out of the plot; it must be 0 in
|
||||
the absolute form.
|
||||
- `[fit-probe] <action> window=.. x=[..] span=.. y=[..]` (stderr, `UDPSCOPE_PROBE_FIT_MS`) —
|
||||
the two fits, through the same `RowSet` calls the right-click menu wires. It first puts
|
||||
the y scale and the window where the data is *not* (a fixed `[-100, 100]`, a 50 ms
|
||||
window), so the assertions have something to be wrong about: **`fit x` must widen the
|
||||
window to the data's span and leave `y` exactly as it was**, and **`fit all` must then
|
||||
move `y` to the data's own range**. A `y` that changed on the `fit x` line is the two
|
||||
paths aliased.
|
||||
- `[fit-probe] <action> pinned=D window=.. event_span=.. x=[..] span=.. y=[..]` (stderr,
|
||||
`UDPSCOPE_PROBE_FIT_MS`) — the two fits, through the **menu handlers** rather than the bare
|
||||
`RowSet` calls, because the notice and the control are the handler's half. A fit is the **undo
|
||||
of an x zoom**, so the probe performs one: it moves the y scale off the data (a fixed
|
||||
`[-100, 100]`), sets a 2 s window through the control's own path, then zooms row 0 to a quarter
|
||||
of it the way a gesture does — on the *member* axis — and fits. Four assertions.
|
||||
**`window` and `event_span` must be unchanged on every line** and equal to the typed 2 s: a fit
|
||||
is a plot interaction and may not restate the time scale (see the invariant below), and they are
|
||||
the only two readings that can see it try. The `zoomed` line's `span` must be a quarter of the
|
||||
window, or the fits have nothing to undo and the lines below mean nothing. **`fit x` must put
|
||||
`span` back to the whole window and leave `y` exactly as it was**, and **`fit all` must do the
|
||||
same and move `y` to the data's own range** — a `y` that changed on the `fit x` line is the two
|
||||
paths aliased. `x`'s *position* advances between lines on a live view and is not an assertion:
|
||||
the follow is re-armed on the newest data, which is what the whole window means there.
|
||||
Run it **inside a pause** as well (`UDPSCOPE_PROBE_PAUSE_MS=2000 UDPSCOPE_PROBE_FIT_MS=2200`),
|
||||
which is the other branch and the one that can silently revert: `pinned=1`, and `fit x` must
|
||||
restore the *pin's* full range — the snapshot's own stamps, legitimately narrower than the
|
||||
requested window (`span=1.179900` against `window=2.000000` is correct, not a finding) — and
|
||||
must still read `pinned=1` afterwards. The pin is restored by writing the **members** as well as
|
||||
the group, because `hold_pinned_window()` prefers a member that has moved since `pinned_stated_`
|
||||
— that is how it tells a gesture from its own echo — so a fit that wrote the group alone reads
|
||||
back as the zoomed range one frame later.
|
||||
- `[layout-probe] <action> rows=N buttons=B done=D done_box=[x,y,WxH] rows_y=[..] close=[..] add=[..] traces=[..]`
|
||||
(stderr, `UDPSCOPE_PROBE_LAYOUT_MS`) — **`buttons` must be `2N+1`** while the editor is
|
||||
up (one `×` per row, one `+` per boundary) and **0 on the `editor off` line**: the
|
||||
@@ -749,6 +764,47 @@ far the largest file.
|
||||
again until the next latch. `PlotArea::create()` is handed `App::placeholder_plot`,
|
||||
never a row: the area keeps a reference to the plot it was created with, and a rebuild
|
||||
destroys every row.
|
||||
- **A readout quotes a stored sample, never a value between two of them.** The hover
|
||||
crosshair and the cursors pane's `mix` values are one call, `Plot::crosshair_value()`, and it
|
||||
reports the **nearer** of the two samples bracketing the x (a tie reads the earlier). Upstream
|
||||
interpolated linearly, and that is wrong twice over here: ingest decimation is a *boxcar
|
||||
average* over `time_stride` samples, so a number between two stored samples is a second
|
||||
estimate laid over the first that nothing on screen or in an export holds; and past the sample
|
||||
spacing the polyline is a line the renderer drew, so a point on it reports the drawing. The
|
||||
change is in `deps/plotter`, which keeps the blend as `CrosshairValue::Interpolated` for a host
|
||||
whose x is a quantity it is willing to interpolate — the nearest sample is the **default** and
|
||||
**udpscope never states the other**, so a `set_crosshair_value_mode()` call appearing here is a
|
||||
regression, not a feature.
|
||||
**Both the mark and the number come from one call, `Plot::crosshair_point()`**, which returns
|
||||
the quoted sample's own **x** as well as its y — so every mark this app draws for a readout is
|
||||
*snapped to the sample it quotes*. The library's `CrosshairRow::marker` and the cursors pane's
|
||||
`mix` crossing annotations (`place_crossings()`, which must use `crosshair_point()` and not
|
||||
`crosshair_value()`) are both placed from it. Two consequences that look like defects and are
|
||||
not: the mark **sits beside the polyline at a deep zoom** (one y feeding both the number and the
|
||||
dot is what keeps them from disagreeing), and a crossing dot **sits a little off its own
|
||||
vertical cursor line** — up to half a sample period, invisible at a whole-window view and the
|
||||
width of the panel at a few samples across, which is the only zoom at which anyone is asking
|
||||
which point it is. The line says where the *marker* is; the dot says which *datum* is quoted.
|
||||
The hover mark is also deliberately *larger* than a trace's own sample dots
|
||||
(`Style::crosshair_marker_px` 9 against `marker_px` 3), because a snapped mark lands among them
|
||||
and at their size reads as one more sample. The guards are the library's
|
||||
`crosshair_value_is_the_nearest_sample`, `the_crosshair_marker_snaps_to_the_sample_it_reports`,
|
||||
`crosshair_interpolated_mode_blends_the_two_samples` and
|
||||
`the_crosshair_mark_is_bigger_than_a_sample_dot`, plus `[cursors-probe]`'s `first mark at x=..
|
||||
y=..` agreeing with the pane's first value.
|
||||
- **`format_measure()` is the one formatter for both panes, and its SI prefix does not depend on
|
||||
there being a unit.** The cursors pane's values and the measurements pane's `text=` go through
|
||||
it, so a change reaches both. The prefix ladder used to be inside `if (!unit.empty())`, which
|
||||
made two decimals of a small unitless value read `0.00` — the reading gone, not merely coarse —
|
||||
and that is a whole class of channels, not a corner: `measure_unit()` returns `""` for any signal
|
||||
whose CONFIG declares no unit, and a **math channel never has one**. The regression net could not
|
||||
see it because `tools/mock_udpstreamer.py` declared `unit="V"` for every data signal; it now takes
|
||||
**`--unit ''`**, and that is the flag to run the measures and cursors probes under when touching
|
||||
this (`text=[636.62m]`, `● sine=585.24m`). With no unit the prefix is written **tight** against
|
||||
the number — `1.34m`, not `1.34 m` — because the space exists to separate the number from its
|
||||
unit and a spaced `m` alone reads as metres; with one it stays spaced and the prefix joins the
|
||||
unit (`636.62 mV`). Below `1e-12` the ladder runs out, and `%.2f` would print `0.00` again, so
|
||||
that range falls back to `%.3g`.
|
||||
- **The cursors are read back from the plots only while a gesture is on that row.** `Cursor::x`
|
||||
is the *free* pointer position while a drag runs and the snapped value otherwise -- which is
|
||||
what the app itself wrote a frame earlier, or the snap of it. A tick that reads a row on
|
||||
@@ -1328,12 +1384,37 @@ far the largest file.
|
||||
that grew from `1 s` to `123.456 ms` would widen the bar and relayout everything under it
|
||||
on a mere change of window. That is also why the window is typed into a popover rather
|
||||
than shown in the bar.
|
||||
- **`apply_time_window()` is the only place the display window changes.** It clamps to
|
||||
`window_limit()` and moves `RowSet::set_time_window()` and `ScopeModel::set_event_span()`
|
||||
together, because the trigger's pre-trigger share is a share of *this* window. Paths that
|
||||
change the span for their own reasons (Fit, an event pin) call `sync_window_display()`
|
||||
instead, which only restates the control and never moves the plot — calling `apply_` there
|
||||
would clamp a legitimately short capture up to the requested window.
|
||||
- **No plot interaction changes the time scale; only the window control does.** The *time scale*
|
||||
is more than the axis: `RowSet::window_s_` also feeds `ScopeModel::set_event_span()` (the
|
||||
trigger's pre-trigger share is a share of *this* window), `ScopeModel::set_target_span()`
|
||||
(which may re-plan the partition, move the decimation and **drop the history**), the window
|
||||
control's readout and the session file. A **zoom or a pan** does not touch it by construction —
|
||||
the controller writes the *member* axis and `clamp_to_data()` caps the span at `window_s_`, so
|
||||
nothing has to be written to keep it that way. **Neither do the two fits**: a fit is the
|
||||
*undo of a zoom*, so `RowSet::fit_x()` restores the window that is already set and reads
|
||||
`window_s_` without ever writing it. Both fits used to frame the *data* instead and move
|
||||
`window_s_` alone, which was wrong twice over — a gesture-triggered re-plan of the rings, and
|
||||
an event span left on the *old* window until the next decade step or unit change applied the
|
||||
fitted value for real (one action, two changes of time scale).
|
||||
- **`apply_time_window()` is the only place the time scale changes**, and the window control is
|
||||
its only caller. It clamps to `window_limit()` and moves `set_target_span()`,
|
||||
`set_event_span()`, `RowSet::set_time_window()`, the control and the session save together —
|
||||
the partition **before** the axis, so the axis is framed over the rings that will exist. A fit
|
||||
must not come here for two reasons: it is not entitled to the scale at all, and
|
||||
`set_time_window()` unpins and re-arms the live follow, which would throw a held event or a
|
||||
paused view onto a live edge it has nothing bound to. Paths that change the span for their own
|
||||
reasons (an event pin) call `sync_window_display()` alone, which only restates the control and
|
||||
never moves the plot — calling `apply_time_window()` there would clamp a legitimately short
|
||||
capture up to the requested window.
|
||||
- **`fit_x()` restores the *whole of what the view may show*, which is not always `window_s_`.**
|
||||
Live, that is the newest `window_s_` at the live edge (`set_time_window(window_s_)`, the same
|
||||
path a resume takes). Pinned — a held event, a filling capture or a paused view — it is the
|
||||
**pin's** full range, which is the snapshot's own stamps and may be legitimately *narrower*
|
||||
than the requested window. It must write the per-row **member** axes as well as the link group:
|
||||
`hold_pinned_window()` prefers a member that differs from `pinned_stated_`, which is how it
|
||||
tells a real gesture from its own echo, so writing the group alone is reverted to the zoomed
|
||||
range on the next frame. `fit_all()` is the y fit plus this, shared rather than duplicated so
|
||||
the two cannot drift apart.
|
||||
- **The header bar's buttons are icon-only**, so headless probes must reach them
|
||||
through the `App` pointers (`trigger_toggle`, `pause_button`, …), not via
|
||||
`find_labelled_button()`. The **pane switches are toolbar buttons** too (`cursors_toggle`,
|
||||
|
||||
@@ -983,9 +983,9 @@ that changes nothing still says why. Each plot's y axis has a right-click menu
|
||||
with auto scale, a fixed range, **Fit X axis**, **Fit all**, and one
|
||||
**Remove '<signal>' from plot N** entry per occupied trace slot (a plot with nothing on
|
||||
it says so instead). The two fits are the same pair the overflow menu and the `f` key
|
||||
offer, split by what they touch: **Fit X axis** frames the window on the data the ring
|
||||
holds and leaves every y scale exactly as the user set it, **Fit all** does that and
|
||||
auto-scales the y axes too.
|
||||
offer, split by what they touch: **Fit X axis** puts the x view back on the whole time
|
||||
window and leaves every y scale exactly as the user set it, **Fit all** does that and
|
||||
auto-scales the y axes too. Neither changes the time window itself — see below.
|
||||
|
||||
**The plot does not twitch.** Two independent causes were fixed, both of which
|
||||
showed up as the x axis moving a few pixels every frame — including while paused,
|
||||
@@ -1139,10 +1139,39 @@ is what made a dragged cursor spring to where it had been. A ruler is per row: e
|
||||
two different values, and the pane names the row.
|
||||
|
||||
`mix` needs no rulers: it reports the value the trace actually has at each marker, read
|
||||
through the library's `Plot::crosshair_value()` — the two bracketing samples,
|
||||
interpolated — so it is the answer to "what is this signal at these two times" rather
|
||||
than "drag a ruler until it crosses the trace". A readout is `--` when the trace has
|
||||
nothing at that time and `partial` when only one of the two markers is over samples.
|
||||
through the library's `Plot::crosshair_value()` — **the nearer of the two bracketing
|
||||
samples**, never a number between them — so it is the answer to "which datum of this
|
||||
signal is at these two times" rather than "drag a ruler until it crosses the trace". A
|
||||
readout is `--` when the trace has nothing at that time and `partial` when only one of
|
||||
the two markers is over samples.
|
||||
|
||||
The value used to be a linear interpolation between the brackets, which is upstream's
|
||||
old convention and was wrong for this app in two ways at once. The samples here are
|
||||
**already averaged** — ingest decimation is a boxcar over `time_stride` samples (see
|
||||
"Decimation at ingest is an average") — so a number computed *between* two of them is a
|
||||
second, undeclared estimate laid over the first, and nothing on screen or in an export
|
||||
holds it. And at a zoom past the sample spacing it is visibly not the datum the pointer
|
||||
is over: the polyline between two samples is a straight line the renderer drew, and
|
||||
quoting a point on it reports the drawing rather than the data. A tie reads the earlier
|
||||
sample. The fix is upstream (`deps/plotter`, `crosshair_span()`'s `t` is a selector rather
|
||||
than a blend factor), so the library's own hover crosshair changed with the pane — which is
|
||||
the point, since both are the same lookup and a user comparing them must not see two
|
||||
numbers. Upstream keeps the blend as `CrosshairValue::Interpolated` for hosts whose x is a
|
||||
quantity they are willing to interpolate; the nearest sample is the default and udpscope
|
||||
never states the other.
|
||||
|
||||
**The crossing dot is on the sample, not on the marker's column.** The dot is placed
|
||||
through `Plot::crosshair_point()` — the same lookup as the value, returning the quoted
|
||||
sample's own x as well as its y — so the mark the eye follows and the number in the pane
|
||||
are the same datum. The two x's differ by up to half a sample period: nothing at a
|
||||
whole-window view, and the whole width of the panel once a few samples span the axes, which
|
||||
is exactly the state in which a reader is asking "which point is that?". It therefore leaves
|
||||
the dot a little off its own vertical line by design — the line says where the *marker* is
|
||||
and the dot says which *datum* is being quoted, and when the marker is between two samples
|
||||
those are two places. The library's hover crosshair snaps the same way and draws its mark
|
||||
`Style::crosshair_marker_px` across (9 logical px against a trace's own 3 px sample dots),
|
||||
since a snapped mark lands among those dots and at their size would read as one more sample
|
||||
instead of as the answer to the question the hover asks.
|
||||
|
||||
The readouts are a **table**: a header naming the columns, then one row per instrument —
|
||||
`| who | A | B | delta |`. The first row is the two times and their difference, and the
|
||||
@@ -1156,7 +1185,16 @@ Every readout is a **measurement**, not a raw number: at most two decimals, scal
|
||||
SI prefix so the digits sit in `1..999`, and the unit the stream declared appended —
|
||||
`10.52 mV`, `-9.8 V`, `dT -398.67 ms`, `1.06 pV`. Two decimals is the resolution the
|
||||
instruments can claim (a marker lands on a sample, and the sample is an `f32`), and a
|
||||
trailing zero is dropped, so `1.50 V` reads `1.5 V`. A row's rulers use its signals' unit
|
||||
trailing zero is dropped, so `1.50 V` reads `1.5 V`. The prefix is applied **whether or not
|
||||
the stream declared a unit**, and that is not a detail: a signal whose CONFIG declares none is
|
||||
ordinary, a *math* channel never has one, and two decimals of an unprefixed small value is
|
||||
`0.00` — the reading was simply gone for an entire class of channels, which went unnoticed
|
||||
because the mock declares `V` for everything (it now takes `--unit ''`, which is the only way
|
||||
a probe can see this). With no unit the prefix is written *tight* against the number —
|
||||
`585.24m`, not `585.24 m` — because the space is there to separate the number from its unit,
|
||||
and a spaced `m` with nothing after it reads as metres. Below `1e-12`, where no prefix can
|
||||
help, the value falls back to three significant digits in whatever form fits rather than to
|
||||
`0.00`. A row's rulers use its signals' unit
|
||||
when they agree on one and print bare digits when they do not: a ruler is on the row's y
|
||||
axis, and two units on one axis make "the" unit a guess. A trace's line carries the
|
||||
signal's **colour chip and its name** — `● sine 616.49 mV -24.57 mV -641.06 mV` — and the
|
||||
@@ -1247,10 +1285,24 @@ logged with both rates and the new anti-alias corner. A window change that leave
|
||||
limitations below); only the shared clock is decimated.
|
||||
|
||||
**Fit to data** (overflow menu, the `f` key, or **Fit all** in a plot's right-click menu)
|
||||
auto-scales every row's y axis and re-frames the x window on the data the ring holds,
|
||||
propagated to every row. **Fit X axis** is the same re-framing without the y scaling, for
|
||||
the common case where the window should be the data's but the levels were placed by hand.
|
||||
The status line acknowledges either (`x fitted to the buffered data (1.25 s)`).
|
||||
auto-scales every row's y axis to the data it holds and puts the x view back on the whole time
|
||||
window, propagated to every row. **Fit X axis** is that x half alone, for the common case where
|
||||
a zoom should be undone but the levels were placed by hand. The status line acknowledges either
|
||||
(`x back to the 2 s window`).
|
||||
|
||||
**Nothing you do on the plot changes the time window** — only the window control does. A fit is
|
||||
the *undo of a zoom*: a 2 s window zoomed into reads 2 s again. That matters because the window
|
||||
is not just the axis. It is also the trigger's event span, how the samples are stored (a window
|
||||
long enough to move the decimation re-plans the rings and **drops the history**), the control's
|
||||
readout and what the session file records — so a wheel notch or a Fit that redefined it would
|
||||
quietly throw the buffer away. A zoom and a pan are view-only by construction; the fits read the
|
||||
window and never write it.
|
||||
|
||||
Which 2 s depends on the state, and the fit restores the whole of whatever the view is allowed to
|
||||
show: live, the newest window at the live edge; and for a held event or a paused view, the pin's
|
||||
full range — the snapshot's own span, which is legitimately *narrower* than the requested window
|
||||
when the ring had not filled it. A fit of a frozen view keeps it frozen rather than throwing it
|
||||
back onto the stream.
|
||||
|
||||
**Shared x axis.** The rows are the rows of **one `plot::Figure`**, drawn by one
|
||||
`plot::gtk4::PlotArea` in figure mode, and they all join a single x link group.
|
||||
|
||||
Vendored
+1
-1
Submodule deps/plotter updated: 26a7c82a80...03c41adbec.
+2
-2
@@ -73,8 +73,8 @@ class ChannelBuffer final : public plot::data::Series {
|
||||
/// datagram leaves a gap, so index times must come from the time ring.
|
||||
f64 x_at(u64 i) const override;
|
||||
/// Bracketing samples by binary search over the time ring, which is monotonic,
|
||||
/// so a crosshair value is interpolated between the two samples actually either
|
||||
/// side of the pointer -- a grid walk would land on the wrong pair after a gap.
|
||||
/// so a crosshair reads whichever of the two samples actually either side of the
|
||||
/// pointer is nearer -- a grid walk would land on the wrong pair after a gap.
|
||||
bool neighbours(f64 x, u64& i0, u64& i1) const override;
|
||||
u64 snap_index(f64 x) const override;
|
||||
plot::Range x_bounds() const override;
|
||||
|
||||
@@ -19,6 +19,9 @@ unit, width-locked so a change of window cannot widen the header bar.
|
||||
value. Stamping at the end of a group would shift every trace half a stored sample late.
|
||||
• Moving the window across a decimation boundary rebuilds the buffer and drops the history.
|
||||
Walking it inside one decade is free.
|
||||
• <b>This control is the only thing that changes the window.</b> A zoom, a pan or a fit moves the
|
||||
view <i>inside</i> it and leaves it alone — which is the point of the previous bullet: a wheel
|
||||
notch that redefined the window could re-plan the buffer and throw your samples away.
|
||||
• The trigger sees the <i>stored</i> (filtered) signal, not the wire signal, because an edge
|
||||
found on a sample that is not in the buffer could not be cut out of it.
|
||||
|
||||
|
||||
@@ -19,6 +19,11 @@ The full table is in the shortcut overview (<tt>Ctrl+/</tt>).
|
||||
• A <i>pinned</i> view is navigable within its own data but cannot leave it. While an event is
|
||||
held, a capture is filling, or the view is paused, a zoom <i>in</i> works normally — "not
|
||||
outside the event" is not "not readable" — but a pan or a zoom out is clamped to the data.
|
||||
• Nothing you do on the plot changes the <i>time window</i> — only the header control does. A
|
||||
zoom or a pan moves the view inside the window; <b>f</b> (and Fit X axis) is the <i>undo</i> of a
|
||||
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.
|
||||
• 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.
|
||||
|
||||
+13
-1
@@ -19,10 +19,22 @@ owns a <b>Reset</b> button. Drag a marker to move it. The mode dropdown offers f
|
||||
• <tt>mix</tt> is the reading mode: one line per <i>bound</i> trace, with the trace's own values
|
||||
at each marker and their difference. A slot with a name but no channel behind it is not a
|
||||
trace and gets no line.
|
||||
• A value is the <i>nearest stored sample's</i>, never a number between two samples. Zoom in past
|
||||
the sample spacing and the line between two samples is something the plot drew, not data —
|
||||
so a reading off it would be the drawing. The hover crosshair reads the same way.
|
||||
• Every reading carries a <i>metric prefix</i> so the digits sit in <tt>1..999</tt>:
|
||||
<tt>10.52 mV</tt>, not <tt>0.01 V</tt>. Two decimals is all an instrument landing on an
|
||||
<tt>f32</tt> sample can claim, so without the prefix a small value would read <tt>0.00</tt>
|
||||
and the reading would be gone. A stream that declares no unit gets the prefix anyway, written
|
||||
tight against the number (<tt>585.24m</tt>) — the space separates the number from its unit,
|
||||
and a spaced <tt>m</tt> with nothing after it would read as metres.
|
||||
• A marker past the newest sample reads <tt>--</tt>. With only one marker over data the row
|
||||
reads <tt>partial</tt>: a difference between a number and nothing is not a number.
|
||||
• In <tt>mix</tt> each marker also places a crossing annotation per trace, which is what lets
|
||||
you see on the plot which sample a number came from.
|
||||
you see on the plot which sample a number came from. It is <i>on that sample</i>, so when the
|
||||
marker falls between two it sits a little off its own line: the line is where you put the
|
||||
marker, the dot is the datum being quoted. The hover crosshair's mark snaps the same way, and
|
||||
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.
|
||||
|
||||
|
||||
@@ -27,7 +27,11 @@ set, with <b>Save</b> and <b>Delete</b>.
|
||||
• A window with too few crossings reads <tt>invalid</tt> / <tt>--</tt> rather than a plausible
|
||||
invented number. Frequency over a window holding one edge is the usual case.
|
||||
• <tt>frequency</tt> is reported in Hz and <tt>period</tt>/<tt>pulse duration</tt> in seconds;
|
||||
every other type carries the channel's own unit.
|
||||
every other type carries the channel's own unit. A value is written with a <i>metric prefix</i>
|
||||
so its digits sit in <tt>1..999</tt> — <tt>636.62 mV</tt>, not <tt>0.64 V</tt> — including when
|
||||
the stream declares no unit at all, which is always the case for a math channel: there the
|
||||
prefix goes tight against the number (<tt>636.62m</tt>), since two decimals of an unprefixed
|
||||
small value is <tt>0.00</tt>.
|
||||
• Cost is bounded by reading the summary pyramid the producer already maintains, not the raw
|
||||
window. The values are still exact: a bucket's sum is a sum.
|
||||
|
||||
|
||||
+38
-34
@@ -396,18 +396,30 @@ void RowSet::place_crossings() {
|
||||
if (ida >= r.plot->annotation_count() || idb >= r.plot->annotation_count()) continue;
|
||||
plot::Annotation a = r.plot->annotation(ida);
|
||||
plot::Annotation b = r.plot->annotation(idb);
|
||||
double xa = 0.0;
|
||||
double ya = 0.0;
|
||||
double xb = 0.0;
|
||||
double yb = 0.0;
|
||||
const bool has_a = mix && r.plot->crosshair_value(static_cast<plot::u32>(s), cursor_a_x_, ya);
|
||||
const bool has_b = mix && r.plot->crosshair_value(static_cast<plot::u32>(s), cursor_b_x_, yb);
|
||||
// `crosshair_point()`, not `crosshair_value()`: the number beside this dot is a *stored
|
||||
// sample's*, so the dot belongs on that sample and not on the marker's own column. The
|
||||
// two differ by up to half a sample period, which is nothing at a whole-window view and
|
||||
// the whole width of the panel once the view is zoomed to a handful of samples -- the
|
||||
// state in which a reader is asking "which point is that?" in the first place. It leaves
|
||||
// the dot a little off its vertical line by design: the line says *where the marker is*
|
||||
// and the dot says *which datum is being quoted*, and when the marker is between two
|
||||
// samples those are not the same place.
|
||||
const bool has_a =
|
||||
mix && r.plot->crosshair_point(static_cast<plot::u32>(s), cursor_a_x_, xa, ya);
|
||||
const bool has_b =
|
||||
mix && r.plot->crosshair_point(static_cast<plot::u32>(s), cursor_b_x_, xb, yb);
|
||||
a.visible = has_a;
|
||||
b.visible = has_b;
|
||||
if (has_a) {
|
||||
a.x = cursor_a_x_;
|
||||
a.x = xa;
|
||||
a.y = ya;
|
||||
}
|
||||
if (has_b) {
|
||||
b.x = cursor_b_x_;
|
||||
b.x = xb;
|
||||
b.y = yb;
|
||||
}
|
||||
r.plot->set_annotation(ida, a);
|
||||
@@ -646,20 +658,26 @@ void RowSet::unpin_pause_window() {
|
||||
}
|
||||
|
||||
void RowSet::fit_x() {
|
||||
Row& first = row(0);
|
||||
if (!first.plot || !figure_) return;
|
||||
first.plot->auto_fit_x();
|
||||
const plot::Range fitted = first.plot->x_range(first.axis_x);
|
||||
const double span = fitted.span();
|
||||
if (!std::isfinite(span) || !(span > 0.0)) return;
|
||||
// The same two paths `fit_all()` takes: a held event owns its window and states it
|
||||
// through the link group, a live view re-arms the follow and frames the newest span.
|
||||
if (pinned_) {
|
||||
window_s_ = span;
|
||||
figure_->set_x_link_range(x_group_, fitted);
|
||||
} else {
|
||||
set_time_window(span);
|
||||
if (!figure_) return;
|
||||
if (!pinned_) {
|
||||
// Live: the same path the window control and a resume take, with the window it already has.
|
||||
// That re-arms the follow and parks the newest `window_s_` at the live edge, which is the
|
||||
// whole of what a live view may show.
|
||||
set_time_window(window_s_);
|
||||
return;
|
||||
}
|
||||
// Pinned (a held event or a paused view): the pin's full range, and the pin stays. The
|
||||
// *members* have to be written as well as the group, because `hold_pinned_window()` prefers a
|
||||
// member that has moved since our own last statement -- that is how it tells a gesture from its
|
||||
// own echo -- so writing the group alone would be reverted to the zoomed-in member next frame.
|
||||
const plot::Range pin{-pinned_pre_, pinned_post_};
|
||||
if (!(pin.span() > 0.0) || !std::isfinite(pin.min) || !std::isfinite(pin.max)) return;
|
||||
for (int i = 0; i < rows_; ++i) {
|
||||
Row& r = row(i);
|
||||
if (r.plot) r.plot->set_x_range(r.axis_x, pin);
|
||||
}
|
||||
pinned_stated_ = pin;
|
||||
figure_->set_x_link_range(x_group_, pin);
|
||||
}
|
||||
|
||||
void RowSet::sync_cursors(int drag_row) {
|
||||
@@ -778,23 +796,9 @@ void RowSet::fit_all() {
|
||||
Row& r = row(i);
|
||||
if (r.plot) r.plot->auto_fit_y(r.axis_y);
|
||||
}
|
||||
Row& first = row(0);
|
||||
if (!first.plot || !figure_) return;
|
||||
// Row 0 fits its own x to the data it holds; that span becomes the shared window, and
|
||||
// stating it through the group is what puts every row on it.
|
||||
first.plot->auto_fit_x();
|
||||
const plot::Range fitted = first.plot->x_range(first.axis_x);
|
||||
const double span = fitted.span();
|
||||
if (!std::isfinite(span) || !(span > 0.0)) return;
|
||||
if (pinned_) {
|
||||
// Fitting a captured event: the window is the snapshot's own `[-pre, +post]` span,
|
||||
// and `set_time_window()` would re-arm the follow and re-frame on a live edge that
|
||||
// trigger mode has nothing bound to.
|
||||
window_s_ = span;
|
||||
figure_->set_x_link_range(x_group_, fitted);
|
||||
} else {
|
||||
set_time_window(span);
|
||||
}
|
||||
// The x half is `fit_x()`'s exactly -- the zoom undone, the time window untouched -- and
|
||||
// sharing it is what keeps the two fits from drifting apart.
|
||||
fit_x();
|
||||
}
|
||||
|
||||
void RowSet::note_y_gutter(double need_px) {
|
||||
|
||||
+20
-7
@@ -298,11 +298,25 @@ class RowSet {
|
||||
void set_data_pin(double lo, double hi);
|
||||
void unpin_pause_window();
|
||||
|
||||
/// Frames the x window on the data row 0 holds, leaving every y axis alone. The x
|
||||
/// counterpart of `fit_all()`, for the one thing the y axes cannot say: how wide the
|
||||
/// window should be.
|
||||
/// Undoes an x zoom: puts the view back on the whole time window, leaving every y axis alone.
|
||||
/// The x counterpart of `fit_all()`.
|
||||
///
|
||||
/// **It does not change the time window** -- `window_s_` is read, never written. A fit is a
|
||||
/// plot interaction, and the window is also the trigger's event span, the buffer partition and
|
||||
/// the control's readout, so a fit that *framed the data* instead would redefine all of that
|
||||
/// from a gesture (and re-plan the rings, dropping the history). What a user asks for here is
|
||||
/// the zoom undone: a 2 s window zoomed into reads 2 s again.
|
||||
///
|
||||
/// Which 2 s depends on the state, and both cases restore the *whole* of what the view is
|
||||
/// allowed to show: live, the newest window at the live edge; pinned (a held event or a paused
|
||||
/// view), the pin's full range, with the pin kept -- `set_time_window()` would clear it and
|
||||
/// re-frame on a live edge that a frozen view has nothing bound to.
|
||||
void fit_x();
|
||||
|
||||
/// Whether a window is pinned: a held event, a filling capture or a paused view. The condition
|
||||
/// `fit_x()` branches on, which is why the fit probe reports it.
|
||||
bool window_pinned() const noexcept { return pinned_; }
|
||||
|
||||
/// Adopts the instruments a gesture is moving, on `drag_row` (the row whose controller holds
|
||||
/// a cursor), and nothing otherwise.
|
||||
///
|
||||
@@ -336,10 +350,9 @@ class RowSet {
|
||||
/// 0, so nothing outside the pre+post window is visible.
|
||||
void pin_event_window(double pre_s, double post_s);
|
||||
|
||||
/// Fits every row's y axis to the data it holds, and re-frames the shared x window
|
||||
/// on what row 0 retains (the same thing the plot's own `f` key does). The window
|
||||
/// the toolbar reports becomes the fitted span, so the dropdown can say what is
|
||||
/// actually on screen.
|
||||
/// Fits every row's y axis to the data it holds, and puts x back on the whole time window --
|
||||
/// `fit_x()`'s x half exactly, so the two cannot drift apart. Like it, **this does not change
|
||||
/// the time window**: the y axes are fitted to their data, x is merely un-zoomed.
|
||||
void fit_all();
|
||||
|
||||
/// Reports the left gutter the rows currently need, and reserves at least the width
|
||||
|
||||
+91
-45
@@ -922,6 +922,13 @@ const char* badge_markup(BadgePhase phase) {
|
||||
/// sample is an `f32` of a value the CONFIG may scale arbitrarily, so more digits would be
|
||||
/// noise dressed as precision. Trailing zeros go, so `1.50 V` reads `1.5 V` -- "at most
|
||||
/// two decimals", not exactly two.
|
||||
///
|
||||
/// The prefix is applied **whether or not a unit is declared**, and that is the point: two
|
||||
/// decimals of an unprefixed small value is `0.00`, i.e. the reading is simply gone. A signal
|
||||
/// whose CONFIG declares no unit is ordinary (and a *math* channel never has one), so the pane
|
||||
/// showed `0.00` for every value under 5 mV of an entire class of channels. A bare prefix is
|
||||
/// written tight against the number -- `1.34m`, not `1.34 m` -- because the space is there to
|
||||
/// separate the number from its unit, and with no unit to separate, a spaced `m` reads as metres.
|
||||
std::string format_measure(double v, const std::string& unit) {
|
||||
if (!std::isfinite(v)) return "--";
|
||||
static const struct {
|
||||
@@ -934,22 +941,22 @@ std::string format_measure(double v, const std::string& unit) {
|
||||
const double mag = std::fabs(v);
|
||||
const char* prefix = "";
|
||||
double scale = 1.0;
|
||||
// No unit, no prefix: `10.52 m` is a prefix with nothing to prefix, which reads as
|
||||
// metres. A bare number stays a bare number and simply keeps its decimals.
|
||||
if (!unit.empty()) {
|
||||
for (const auto& p : kPrefixes) {
|
||||
if (mag >= p.scale) {
|
||||
prefix = p.prefix;
|
||||
scale = p.scale;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (mag > 0.0 && mag < 1.0e-12) { // below the ladder: keep it in plain digits
|
||||
prefix = "";
|
||||
scale = 1.0;
|
||||
for (const auto& p : kPrefixes) {
|
||||
if (mag >= p.scale) {
|
||||
prefix = p.prefix;
|
||||
scale = p.scale;
|
||||
break;
|
||||
}
|
||||
}
|
||||
char buf[64];
|
||||
if (mag > 0.0 && mag < 1.0e-12) {
|
||||
// Below the ladder, where scaling cannot help and `%.2f` would print `0.00` again. Three
|
||||
// significant digits in whatever form fits, so the reading survives as a reading.
|
||||
std::snprintf(buf, sizeof(buf), "%.3g", v);
|
||||
std::string out = buf;
|
||||
if (!unit.empty()) out += " " + unit;
|
||||
return out;
|
||||
}
|
||||
std::snprintf(buf, sizeof(buf), "%.2f", v / scale);
|
||||
std::string num = buf;
|
||||
if (num.find('.') != std::string::npos) { // at most two decimals, not exactly two
|
||||
@@ -958,11 +965,9 @@ std::string format_measure(double v, const std::string& unit) {
|
||||
}
|
||||
if (num == "-0") num = "0";
|
||||
std::string out = num;
|
||||
if (prefix[0] != '\0') out += std::string(" ") + prefix;
|
||||
if (!unit.empty()) {
|
||||
if (prefix[0] == '\0') out += " ";
|
||||
out += unit;
|
||||
}
|
||||
if (!unit.empty()) out += " "; // the space separates the number from its unit, nothing else
|
||||
out += prefix;
|
||||
out += unit;
|
||||
return out;
|
||||
}
|
||||
|
||||
@@ -1865,10 +1870,19 @@ void sync_window_display(App& app, double seconds) {
|
||||
app.updating_panels = false;
|
||||
}
|
||||
|
||||
/// The one place the display window changes. Clamps to what the buffer can hold, moves the
|
||||
/// plot and the trigger's event span together -- its pre-trigger share is a share of *this*
|
||||
/// window -- and then restates the result in the control, so a clamped request reads back as
|
||||
/// the window that was actually taken rather than the one that was typed.
|
||||
/// The one place the time scale changes. Clamps to what the buffer can hold, moves the plot and
|
||||
/// the trigger's event span together -- its pre-trigger share is a share of *this* window -- and
|
||||
/// then restates the result in the control, so a clamped request reads back as the window that
|
||||
/// was actually taken rather than the one that was typed.
|
||||
///
|
||||
/// **Only the window control comes here.** The time scale is more than the axis: it is also the
|
||||
/// trigger's event span, the buffer partition (so a change of decimation drops the history) and
|
||||
/// the session, so a *plot interaction* may not state one. A zoom and a pan never touch it by
|
||||
/// construction -- they move the row's axis, and `clamp_to_data()` caps the span at `window_s_` --
|
||||
/// and the two fits do not either: `RowSet::fit_x()` undoes a zoom by restoring the window that
|
||||
/// is already set. Both fits used to frame the data instead and move `window_s_` alone, which
|
||||
/// left the event span on the *old* window until the next decade step or unit change applied the
|
||||
/// fitted value for real: one action, two changes of time scale.
|
||||
void apply_time_window(App& app, double seconds) {
|
||||
if (!(seconds > 0.0)) return;
|
||||
const double clamped = std::min(std::max(seconds, kWindowMinS), window_limit(app));
|
||||
@@ -1882,8 +1896,9 @@ void apply_time_window(App& app, double seconds) {
|
||||
format_rate(app.model->partition().stored_rate()) + ", history cleared";
|
||||
app.notice_until_us = g_get_monotonic_time() + 4 * G_TIME_SPAN_SECOND;
|
||||
}
|
||||
app.rows->set_time_window(clamped);
|
||||
app.model->set_event_span(clamped);
|
||||
// The partition before the axis, so the axis is framed over the rings that will exist.
|
||||
app.rows->set_time_window(clamped);
|
||||
sync_window_display(app, clamped);
|
||||
schedule_layout_save(app);
|
||||
}
|
||||
@@ -4046,25 +4061,25 @@ void on_signals_toggled(GtkToggleButton* button, gpointer user) {
|
||||
gtk_toggle_button_get_active(button));
|
||||
}
|
||||
|
||||
/// Fit: auto-scale every row's y axis and re-frame the x window on the data the
|
||||
/// ring holds (the plot's own `f` semantics), then tell the toolbar what span that
|
||||
/// is.
|
||||
/// Fit: auto-scale every row's y axis to the data it holds, and put x back on the whole time
|
||||
/// window.
|
||||
///
|
||||
/// No `apply_time_window()` here, deliberately: a fit is a plot interaction and may not restate
|
||||
/// the time scale (see that function). The y axes are fitted to the data; x is only un-zoomed.
|
||||
void on_fit_clicked(GtkButton*, gpointer user) {
|
||||
App* app = static_cast<App*>(user);
|
||||
app->rows->fit_all();
|
||||
sync_window_display(*app, app->rows->time_window());
|
||||
app->notice = "fitted to the buffered data (" + format_window(app->rows->time_window()) + ")";
|
||||
app->notice = "y fitted to the data, x back to the " +
|
||||
format_window(app->rows->time_window()) + " window";
|
||||
app->notice_until_us = g_get_monotonic_time() + 3 * G_TIME_SPAN_SECOND;
|
||||
}
|
||||
|
||||
/// Fit the x window only: the y axes keep the scales the user set. The counterpart of the
|
||||
/// other fit, for the common case where the interesting range is the data's but the levels
|
||||
/// were placed by hand.
|
||||
/// Reset the x zoom only: the y axes keep the scales the user set. The counterpart of the other
|
||||
/// fit, for the common case where a zoom should be undone but the levels were placed by hand.
|
||||
void on_fit_x_clicked(GtkButton*, gpointer user) {
|
||||
App* app = static_cast<App*>(user);
|
||||
app->rows->fit_x();
|
||||
sync_window_display(*app, app->rows->time_window());
|
||||
app->notice = "x fitted to the buffered data (" + format_window(app->rows->time_window()) + ")";
|
||||
app->notice = "x back to the " + format_window(app->rows->time_window()) + " window";
|
||||
app->notice_until_us = g_get_monotonic_time() + 3 * G_TIME_SPAN_SECOND;
|
||||
}
|
||||
|
||||
@@ -7176,15 +7191,21 @@ void cursors_probe_report(App& a, const char* what) {
|
||||
gboolean cursors_probe_step(gpointer user);
|
||||
|
||||
/// The two fits the right-click menu offers, driven through the handlers the menu wires
|
||||
/// (a null button: neither handler looks at it). `fit x` must narrow the window to the
|
||||
/// data's own span and leave the y ranges alone; `fit all` must then move the y ranges too.
|
||||
/// (a null button: neither handler looks at it). Both must put x back on the whole time window
|
||||
/// and **leave the window itself alone**; `fit x` must leave the y ranges alone too, and
|
||||
/// `fit all` must move them to the data's.
|
||||
void fit_probe_report(App& a, const char* what) {
|
||||
const RowSet::Row& r0 = a.rows->row(0);
|
||||
const plot::Range x = r0.plot != nullptr ? r0.plot->x_range(r0.axis_x) : plot::Range{};
|
||||
const plot::Range y = r0.plot != nullptr ? r0.plot->y_range(r0.axis_y) : plot::Range{};
|
||||
// `window` and `event_span` are the assertion that a fit did *not* restate the time scale:
|
||||
// both are written only by `apply_time_window()`, so they must hold the typed value on every
|
||||
// line, with `span` the reading that moves -- down on the zoom, back up on the fit.
|
||||
std::fprintf(stderr,
|
||||
"[fit-probe] %-10s window=%.6f x=[%.6f..%.6f] span=%.6f y=[%.6f..%.6f]\n",
|
||||
what, a.rows->time_window(), x.min, x.max, x.span(), y.min, y.max);
|
||||
"[fit-probe] %-10s pinned=%d window=%.6f event_span=%.6f x=[%.6f..%.6f] span=%.6f "
|
||||
"y=[%.6f..%.6f]\n",
|
||||
what, a.rows->window_pinned() ? 1 : 0, a.rows->time_window(), a.model->event_span(),
|
||||
x.min, x.max, x.span(), y.min, y.max);
|
||||
std::fflush(stderr);
|
||||
}
|
||||
|
||||
@@ -7261,32 +7282,57 @@ gboolean timelabels_probe_step(gpointer user) {
|
||||
return G_SOURCE_REMOVE;
|
||||
}
|
||||
|
||||
/// Zooms row 0 in to a quarter of what it shows, the way a gesture does -- on the *member* axis,
|
||||
/// which `Figure::layout()` adopts into the group. A fit is the undo of this, so the probe has to
|
||||
/// perform it or it has nothing to undo.
|
||||
void fit_probe_zoom_in(App& a) {
|
||||
RowSet::Row& r0 = a.rows->row(0);
|
||||
if (r0.plot == nullptr) return;
|
||||
const plot::Range x = r0.plot->x_range(r0.axis_x);
|
||||
const double mid = x.min + 0.5 * x.span();
|
||||
const double half = 0.125 * x.span();
|
||||
r0.plot->set_x_range(r0.axis_x, {mid - half, mid + half});
|
||||
}
|
||||
|
||||
gboolean fit_probe_step(gpointer user) {
|
||||
App* a = static_cast<App*>(user);
|
||||
if (a->window == nullptr) return G_SOURCE_REMOVE;
|
||||
switch (a->dbg_fit_step) {
|
||||
case 0:
|
||||
// Move the y scale off the data's range first, so "fit x leaves y alone" is a
|
||||
// statement with something to be wrong about.
|
||||
// Move the y scale off the data's range first, so "fit x leaves y alone" is a statement
|
||||
// with something to be wrong about -- and set the window through the control's own path,
|
||||
// since that is the only thing allowed to, and its value is what both fits must preserve.
|
||||
a->rows->set_y_range(0, -100.0, 100.0);
|
||||
a->rows->set_time_window(0.05);
|
||||
// Not while pinned: `apply_time_window()` unpins, and the pinned branch of the fit is the
|
||||
// other half of what this probe is for. Timed inside a pause (`UDPSCOPE_PROBE_PAUSE_MS`)
|
||||
// it takes the window the pause froze and reports `pinned=1`.
|
||||
if (!a->rows->window_pinned()) apply_time_window(*a, 2.0);
|
||||
fit_probe_report(*a, "start");
|
||||
g_timeout_add(400, fit_probe_step, a);
|
||||
a->dbg_fit_step = 1;
|
||||
break;
|
||||
case 1:
|
||||
a->rows->fit_x();
|
||||
sync_window_display(*a, a->rows->time_window());
|
||||
fit_probe_zoom_in(*a);
|
||||
g_timeout_add(400, fit_probe_step, a);
|
||||
a->dbg_fit_step = 2;
|
||||
break;
|
||||
case 2:
|
||||
fit_probe_report(*a, "fit x");
|
||||
a->rows->fit_all();
|
||||
sync_window_display(*a, a->rows->time_window());
|
||||
// A quarter of the window: what the fits have to undo, and the reading that proves the
|
||||
// `span=` on the fit lines is the fit's work rather than a window nothing ever left.
|
||||
fit_probe_report(*a, "zoomed");
|
||||
// The handlers themselves, not `RowSet::fit_x()` alone: the window the fit must *not*
|
||||
// move is the app's, and the notice and the control are the handler's half.
|
||||
on_fit_x_clicked(nullptr, a);
|
||||
g_timeout_add(400, fit_probe_step, a);
|
||||
a->dbg_fit_step = 3;
|
||||
break;
|
||||
case 3:
|
||||
fit_probe_report(*a, "fit x");
|
||||
fit_probe_zoom_in(*a);
|
||||
on_fit_clicked(nullptr, a);
|
||||
g_timeout_add(400, fit_probe_step, a);
|
||||
a->dbg_fit_step = 4;
|
||||
break;
|
||||
default:
|
||||
fit_probe_report(*a, "fit all");
|
||||
break;
|
||||
|
||||
@@ -50,16 +50,16 @@ def signal_desc(name, *, rate, time_mode, time_idx, unit, rows=1, cols=1, dims=0
|
||||
return bytes(d)
|
||||
|
||||
|
||||
def build_config(rate, time_mode=TIME_FULL_ARRAY):
|
||||
def build_config(rate, time_mode=TIME_FULL_ARRAY, unit="V"):
|
||||
sigs = [
|
||||
# The stream's clock: referenced by every data signal, so udpscope treats
|
||||
# it as the time base rather than a plottable channel. Its *own* time_mode
|
||||
# is what udpscope switches on, so `--time-mode first` has to land here.
|
||||
signal_desc("time", rate=0.0, time_mode=time_mode,
|
||||
time_idx=NO_TIME_SIGNAL, unit="s"),
|
||||
signal_desc("sine", rate=rate, time_mode=time_mode, time_idx=0, unit="V"),
|
||||
signal_desc("square", rate=rate, time_mode=time_mode, time_idx=0, unit="V"),
|
||||
signal_desc("ramp", rate=rate, time_mode=time_mode, time_idx=0, unit="V"),
|
||||
signal_desc("sine", rate=rate, time_mode=time_mode, time_idx=0, unit=unit),
|
||||
signal_desc("square", rate=rate, time_mode=time_mode, time_idx=0, unit=unit),
|
||||
signal_desc("ramp", rate=rate, time_mode=time_mode, time_idx=0, unit=unit),
|
||||
]
|
||||
payload = struct.pack("<I", len(sigs)) + b"".join(sigs)
|
||||
payload += bytes([PUBLISH_ACCUMULATE])
|
||||
@@ -101,6 +101,10 @@ def main():
|
||||
"sample (UDPS_TIME_FIRST_SAMPLE) and leaves the viewer to space the "
|
||||
"rest by the declared cadence -- the shape a real MARTe2-style "
|
||||
"producer with a single u64 clock has")
|
||||
ap.add_argument("--unit", default="V",
|
||||
help="the unit the data signals declare in CONFIG; '' declares none, which "
|
||||
"is an ordinary stream (and what every math channel looks like) and is "
|
||||
"the only way a probe can see the unitless readout formatting")
|
||||
ap.add_argument("--clock-jitter", type=float, default=0.0,
|
||||
help="per-packet stamp jitter, as a fraction of one packet's span: a device "
|
||||
"that stamps each batch from its own clock does not land on a perfect "
|
||||
@@ -138,7 +142,7 @@ def main():
|
||||
t_stream = 0.0
|
||||
started = time.monotonic()
|
||||
next_send = started
|
||||
sock.sendto(build_config(declared, time_mode), peer)
|
||||
sock.sendto(build_config(declared, time_mode, args.unit), peer)
|
||||
print("mock udpstreamer: CONNECT from %s:%d, CONFIG sent" % peer, flush=True)
|
||||
elif magic == MAGIC and ptype == PKT_DISCONNECT:
|
||||
print("mock udpstreamer: DISCONNECT", flush=True)
|
||||
|
||||
Reference in new issue
Block a user