From 811c6d80f01228ae2e6f8a7fa2584682714316f5 Mon Sep 17 00:00:00 2001 From: Martino Ferrari Date: Mon, 5 Oct 2026 22:08:13 +0200 Subject: [PATCH] fix: a source that has sent no CONFIG reads connecting, not connected A stream that answered CONNECT but has sent no CONFIG has no signal list, so nothing can be enabled and no DATA frame is owed. Calling that "connected, but no data is arriving: another reader may hold the source" blamed a second reader that does not exist, and the badge blinked green over it -- a working pipeline is exactly what it did not have. The glance word is `connecting` (ten characters, no wider than `event held`, so the fixed-set layout rule still holds) and the badge takes a fourth, steady yellow phase, both from the first frame rather than after the three-second stall test. The stall notice itself now splits on `have_config`: the unicast contention wording where a signal list exists, "trying to connect" where it does not, with `[udpscope] connecting:` on stderr beside the old `starved:`. --- CLAUDE.md | 49 ++++++++++++++++++------- README.md | 32 +++++++++++++--- src/help/14-status.txt | 24 ++++++++---- src/status_text.hpp | 9 +++-- src/ui.cpp | 83 +++++++++++++++++++++++++++++++----------- 5 files changed, 145 insertions(+), 52 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 5fa7c1b..0eb5e6a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -599,14 +599,24 @@ Two lines of the self-check output are assertions in disguise: first run's values, and the signals it names must come back *acquired* (a `row 0 slot 0 -> channel ... samples=N` line). Delete the file before a run whose layout you did not mean to inherit (`127.0.0.1_9540.toml` and friends accumulate otherwise). -- `[udpscope] starved: connected (config=D), reconnects=N, no new frames in the last +- `[udpscope] starved: connected (config=1), reconnects=N, no new frames in the last second ...` (stderr, once per second while the state holds, plus a status-line notice) — "connected but nothing is arriving after a reconnect", which over unicast is the second - instance taking the source's single session. Two ways to see it without a second client: - point the app at a UDP port where nothing answers (the client CONNECTs, times out, - reconnects; `config=0`), and check it does **not** fire against a live source. The status - line carries ` | N reconnects` only when N > 0, which is the same counter without the - prose. + instance taking the source's single session. Check it does **not** fire against a live + source. The status line carries ` | N reconnects` only when N > 0, which is the same + counter without the prose. + **The same stall test has two outcomes, split on `have_config`, and they are different + diagnoses.** With a signal list in hand, the above. With **no CONFIG at all** it prints + `[udpscope] connecting: connected (config=0), reconnects=N, no configuration from the + stream ...` and the notice reads *trying to connect* — nothing was taken away, because + with no signal list nothing can be enabled and no DATA frame is owed, and the old wording + blamed a second reader that does not exist. That state is also named *without* waiting for + the three-sample stall: the glance word is `connecting` and the badge is **yellow** from the + first frame (see `badge frames:`). The way to see it with no second client is to point the + app at a UDP port where nothing answers (the client CONNECTs, times out, reconnects), which + is now the `config=0` branch and no longer exercises the `starved:` one at all — a check of + the unicast-contention wording needs a real second reader, or a source that configures and + then goes quiet. - **The export's `time`/`abs_seconds` columns must be strictly increasing and evenly spaced.** Check the written file, not just the probe: `np.diff(t)` with no `<= 0` and `rows / (t[-1] - t[0])` equal to the stored rate (the `ch` line's figure). Duplicate or @@ -626,13 +636,19 @@ Two lines of the self-check output are assertions in disguise: (see the `f64` time-ring invariant below). The figure to compute is the ULP of `t[0]` and of `abs_seconds[0]` — the defect that invariant guards is microseconds, nine orders of magnitude above the arithmetic noise, so the two readings cannot be confused. -- `badge frames: lit=L dim=D idle=I` — the activity badge's phase per frame. Over a run - that acquired anything **both `lit` and `dim` must be non-zero**: that is what "it +- `badge frames: lit=L dim=D idle=I connecting=C` — the activity badge's phase per frame. Over + a run that acquired anything **both `lit` and `dim` must be non-zero**: that is what "it blinks" means, and a badge stuck lit is indistinguishable from a working one in a screenshot. One or two `idle` frames at the start are the frames before the socket connected; a large `idle` is correct only when the run paused or disarmed (a - `UDPSCOPE_PROBE_DISARM_MS` run should show hundreds). The `mode ...` lines carry the - rest of the status line, so `rate` and `last event ... ago` are checked there. + `UDPSCOPE_PROBE_DISARM_MS` run should show hundreds). **`connecting` must be 0 on a run + against a live source** — it is the steady *yellow* phase, "the socket answered but the + stream has sent no CONFIG", and a non-zero count against a working mock means a signal list + that arrived late or not at all. It is the *only* non-zero phase on a dead-port run + (measured: `lit=0 dim=0 idle=90 connecting=446`, the `idle` being the frames before the + first CONNECT), which is also the check that a source that has sent nothing never gets the + blinking green. The `mode ...` lines carry the rest of the status line, so `rate` and + `last event ... ago` are checked there. - `frame cost: tick A/B ms over N, draw C/D ms (max at frame F), paint E/G ms over M` and `frame path: raw=R lod=L frames, segments S/T (mean/max), visible max V samples` — the drawing cost, split into the frame tick, `PlotArea`'s own draw (timed from the frame @@ -727,7 +743,7 @@ Two lines of the self-check output are assertions in disguise: it afterwards. - `[keys] a: armed 0->1 | space: paused 0->1 | t: trigger panel: 0->1 | … | editable focused: 'm' refused=1` (stderr, `UDPSCOPE_PROBE_KEYS_MS`) — **every `X->Y` pair must differ** and **`refused` must be 1**. The refusal is the whole reason the controller is in the bubble phase: every bare-letter accel is also a character, so an accel that fires while a `GtkEditable` has focus makes the window entry unusable. A pair reading `0->0` is an action whose dispatch does not reach the handler a click reaches. Note the format is `: : ->` — the name passed to `before_after()` includes the widget's description, so `t: trigger panel: 0->1` is the correct form (not `t: trigger panel 0->1` without the colon before the values). - `[zoomkeys] ctrl+plus x=[..]->[..] y_held=1 | alt+plus y=[..]->[..] x_held=1` (stderr, `UDPSCOPE_PROBE_ZOOMKEYS_MS`) — **both `_held` must be 1 and both zooms must still happen.** Run it inside a pinned state, where a frame cannot re-frame the other axis for its own reasons: `UDPSCOPE_PROBE_PAUSE_MS=1500 UDPSCOPE_PROBE_ZOOMKEYS_MS=2200`. A `_held=0` is the modifier not reaching the plotter's controller; an unchanged range on the *zoomed* axis is the opposite failure, the key doing nothing. Ctrl restricts to x and Alt to y; Ctrl+Alt zooms neither (no special case needed — neither flag alone fires). -- `status bar: glance=[● live · 1.09 MS/s · 2 s window] max_glance_chars=53 detail_fields=11 popover_rebuilds=0` (exit dump) — **`max_glance_chars` must stay small** (under about 60) and **`popover_rebuilds` must be 0**. The status row is directly above the plot, so this is the "layout must not depend on content" rule read from the bar's own side: the state field is one word from a fixed set, the rate field is `%.3g` (so `10 kS/s`, never `10.0 kS/s`), and the two fields a *state* adds are both elapsed times — trigger mode's `last event … ago` and a pause's `for ` — which step units rather than growing digits (`format_elapsed()`). A **paused** glance is not in the dump at all: it is read by `[pause-probe]`'s `glance=` field, since the dump's own line is taken live. `detail_fields` is the popover's label count, a literal, so adding a label without thinking about layout shows up here. The glance rate format matters: `%.3g` drops the trailing zero that a `%.4g` or a fixed-precision format would keep — a rate that reads differently in the dump and in the help page is a format disagreement, and `14-status.txt`'s example must match it. +- `status bar: glance=[● live · 1.09 MS/s · 2 s window] max_glance_chars=53 detail_fields=11 popover_rebuilds=0` (exit dump) — **`max_glance_chars` must stay small** (under about 60) and **`popover_rebuilds` must be 0**. The status row is directly above the plot, so this is the "layout must not depend on content" rule read from the bar's own side: the state field is one word from a fixed set (`connecting`, `live`, `paused`, `armed`, `capturing`, `event held`, `disarmed` — a word added to it must be **no wider than the widest already there**, which is what let `connecting` join at ten characters beside `event held`; the dead-port glance reads `max_glance_chars=51` against a live run's 54), the rate field is `%.3g` (so `10 kS/s`, never `10.0 kS/s`), and the two fields a *state* adds are both elapsed times — trigger mode's `last event … ago` and a pause's `for ` — which step units rather than growing digits (`format_elapsed()`). A **paused** glance is not in the dump at all: it is read by `[pause-probe]`'s `glance=` field, since the dump's own line is taken live. `detail_fields` is the popover's label count, a literal, so adding a label without thinking about layout shows up here. The glance rate format matters: `%.3g` drops the trailing zero that a `%.4g` or a fixed-precision format would keep — a rate that reads differently in the dump and in the help page is a format disagreement, and `14-status.txt`'s example must match it. The probes are the regression net — when changing UI or plot-host behaviour, extend the probe/dump in `src/ui.cpp` rather than adding a separate harness. @@ -1681,9 +1697,14 @@ far the largest file. Those two used to read the *label*, and the label no longer carries `packets`, `gaps`, `plot 1:` or the buffer figures — so a `gtk_label_get_text()` there would silently reduce every documented `mode …:` assertion to four fields. The glance line's state field is **one - word from a fixed set** (`live`, `paused`, `armed`, `capturing`, `event held`, `disarmed`) for - the layout reason above; a seventh word, or a field whose width tracks the stream, breaks the - row the plot is laid out under. + word from a fixed set** (`connecting`, `live`, `paused`, `armed`, `capturing`, `event held`, + `disarmed`) for the layout reason above; a word **wider than the widest already there**, or a + field whose width tracks the stream, breaks the row the plot is laid out under. The set may + grow — `connecting` was added for the no-CONFIG state — but only on that condition, and the + check is the dump's `max_glance_chars`. `connecting` is derived from `have_config` alone and + sits **ahead of every other branch**, including the display mode's: with no signal list, every + other word claims something that does not exist yet. The `mode` prose branches the same way for + the same reason, which is what keeps the two tiers from disagreeing. - **The ⓘ popover's widgets are built once and only their text is set.** It is visible when the tick updates it, so a rebuild is a per-frame relayout of a widget on screen. `update_status_popover()` only ever calls `gtk_label_set_text()`, and `popover_rebuilds` in diff --git a/README.md b/README.md index abfd71c..dd12790 100644 --- a/README.md +++ b/README.md @@ -995,13 +995,21 @@ connection, the measured rate, the packet/frame/gap counters, what plot 1 holds the buffer's size and span. Three of those are worth calling out: - **The badge blinks green at 1 Hz while the pipeline is actually running** — the - stream is connected, acquisition is not paused, and in trigger mode the trigger is - armed. Anything else leaves it a steady grey, with the reason on its tooltip - (`paused`, `trigger disarmed`, `waiting for the stream`). *Blinking* is the signal, - not the colour: a steady badge is a stopped pipeline, and that distinction survives - a screenshot. It is one glyph whose colour is all that changes, so blinking it + stream is connected, a CONFIG has arrived, acquisition is not paused, and in trigger + mode the trigger is armed. Anything else leaves it steady, with the reason on its + tooltip (`paused`, `trigger disarmed`, `waiting for the stream`). *Blinking* is the + signal, not the colour: a steady badge is a stopped pipeline, and that distinction + survives a screenshot. It is one glyph whose colour is all that changes, so blinking it cannot change its width — a badge that grew and shrank twice a second would move the label beside it and re-lay out the plot with it. + There are two steady colours, and the difference is how far the handshake got. + **Grey** is a socket with nothing behind it, or a pipeline the user stopped. + **Yellow** is a source that answered CONNECT but has sent **no CONFIG**: there is no + signal list, so nothing can be enabled and no DATA frame is owed. The glance line reads + `connecting` in both, because neither has the signal list every other state word claims; + the colour is what separates "no socket yet" from "a socket and no stream behind it". A + blinking green over a source that has sent nothing would claim a working pipeline, which + is the one reading the badge exists to prevent. - **`rate 9.99 kS/s` is the rate the buffer actually stored over the last second**, not the rate the CONFIG declared. The two agree unless the requested window is too long to hold at the wire rate, in which case the ingest resamples (see "A window longer @@ -1565,7 +1573,8 @@ fails validation renders a one-line stand-in so the window always opens rather t no `Gtk-CRITICAL`. **The status bar is two tiers.** The bar itself shows one glance line: the activity badge, the -display state (one word from a fixed set), the stored rate and the window. Everything else — +display state (one word from a fixed set — `connecting`, `live`, `paused`, `armed`, `capturing`, +`event held`, `disarmed`, none wider than ten characters), the stored rate and the window. Everything else — packet and frame counts, gaps, buffer size and span, per-plot trace counts — is behind the ⓘ button at the bar's right-hand end, opening a popover. The two moved there because a bar that carried all of them was too wide to read while watching a signal, and because the bar sits @@ -1650,6 +1659,17 @@ status line as `N reconnects` once it is nonzero) it says so in the status line leaving an empty plot to be puzzled over, and prints a `starved:` line to stderr while the state holds. +That notice is **split on whether a CONFIG ever arrived**, because the two states are +different diagnoses and only one of them is about another reader. With a signal list in +hand, a stream that stops is the fight above: something took the session away, and the +notice says so (`starved:` on stderr). With **no CONFIG at all** nothing was ever taken — +there is no signal list, so nothing can be enabled and no DATA frame is owed — and the app +says `trying to connect` instead (`connecting:` on stderr), with the glance line reading +`connecting` and the badge yellow from the first frame rather than after the three-second +stall test. Calling that one "connected, but no data is arriving" blamed a second reader +that does not exist; the usual cause is a streamer that is not running, or one serving a +different port. + ## Limitations - **Multi-element (vector/matrix) signals are acquired with a zero-order hold.** diff --git a/src/help/14-status.txt b/src/help/14-status.txt index 43dc32a..2d13996 100644 --- a/src/help/14-status.txt +++ b/src/help/14-status.txt @@ -9,11 +9,17 @@ The bar is always visible under the plot. Click ⓘ for the detail popove Worth knowing -• 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: live, paused, armed, - capturing, event held, disarmed. 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 badge is one glyph in four colours: green alternating lit and dim means data is + arriving, and a steady colour means it is not. A badge that never blinks is not a working badge. + Of the two steady colours, grey is no stream at all or a pipeline you stopped (the + tooltip says which), and yellow is a source that answered but has sent no configuration. +• The state field is one word from a fixed set: connecting, live, + paused, armed, capturing, event held, disarmed. Two + states add one field each: in trigger mode, how long ago the last event was; while paused, how + long you have been paused. +• connecting means there is no signal list yet — either no session or a session + with no configuration behind it. Every other word claims a signal list, so until one arrives + there is nothing for them to describe: nothing can be enabled and no data is owed. • The paused figure is wall 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 @@ -24,9 +30,11 @@ The bar is always visible under the plot. Click ⓘ for the detail popove non-zero count means samples are absent from the buffer, not merely late. • reconnects counts sessions re-established with the source. Over unicast a climbing count usually means a second client is taking the source's single session. -• The starvation notice — connected, but no new frames in the last second — is that same - situation seen from the other side. Pointing the app at a port where nothing answers produces - it too. +• The starvation notice — connected, but no data is arriving — is that same situation seen + from the other side: the signal list is here, so something took the stream away. A source that + has sent no configuration gets a different notice, trying to connect, because + nothing was taken away — pointing the app at a port where nothing answers is that case, and the + usual cause is a streamer that is not running or is serving another port. • stored versus wire in the popover is the decimation. They differ exactly when the window is too long to store at full rate. diff --git a/src/status_text.hpp b/src/status_text.hpp index cfa31af..04544f5 100644 --- a/src/status_text.hpp +++ b/src/status_text.hpp @@ -11,9 +11,12 @@ namespace udpscope { /// two cannot disagree. No GTK here on purpose: this is the one part of the status bar that /// can be reasoned about as text. struct StatusSnapshot { - /// One word from the fixed set: "live", "paused", "armed", "capturing", "event held", - /// "disarmed". It is a word from a *fixed* set because the bar sits directly above the plot - /// and a field whose width tracked the stream would relayout it every frame. + /// One word from the fixed set: "connecting", "live", "paused", "armed", "capturing", + /// "event held", "disarmed". It is a word from a *fixed* set because the bar sits directly + /// above the plot and a field whose width tracked the stream would relayout it every frame -- + /// so a word added here must be no wider than the widest already in it ("connecting" and + /// "event held" are both ten characters). "connecting" is the state before a CONFIG has + /// arrived, in which there is no signal list and so nothing any other word here can describe. const char* state_word = "live"; /// The long prose the old single-line bar carried, kept for the detail string and the dump. std::string state_detail; diff --git a/src/ui.cpp b/src/ui.cpp index 26679ba..b8673c8 100644 --- a/src/ui.cpp +++ b/src/ui.cpp @@ -452,7 +452,7 @@ struct App { /// Frames the badge spent in each `BadgePhase`. Both `lit` and `dim` have to be non-zero /// over a run that acquired anything -- that is what "it blinks" means, and a badge /// stuck lit looks identical to a working one in a screenshot. - int dbg_badge_frames[3] = {0, 0, 0}; + int dbg_badge_frames[4] = {0, 0, 0, 0}; int dbg_reset_visible = -1; std::vector last_stored; std::uint64_t dbg_latches_at_disarm = 0; @@ -926,10 +926,14 @@ std::string format_window(double s) { /// What the activity badge is saying. `Lit`/`Dim` are the two halves of the blink, so /// the badge is only *blinking* while the pipeline is actually running; a steady badge is -/// a stopped one, which is the state worth noticing on a glance. -enum class BadgePhase { Lit = 0, Dim = 1, Idle = 2 }; +/// a stopped one, which is the state worth noticing on a glance. `Connecting` is the +/// half-way state -- the socket answered but the stream has sent no CONFIG, so there is no +/// signal list and nothing to acquire -- and it is steady yellow rather than green, +/// because a blinking green badge over a source that has sent nothing claims a working +/// pipeline. +enum class BadgePhase { Lit = 0, Dim = 1, Idle = 2, Connecting = 3 }; -/// The badge is one glyph in one of three colours. It must stay the *same glyph*: the +/// The badge is one glyph in one of four colours. It must stay the *same glyph*: the /// status row sits above the plot, and a badge that changed width as it blinked would /// move the label beside it and re-lay out the plot twice a second. const char* badge_markup(BadgePhase phase) { @@ -938,6 +942,8 @@ const char* badge_markup(BadgePhase phase) { return "\u25CF"; case BadgePhase::Dim: return "\u25CF"; + case BadgePhase::Connecting: + return "\u25CF"; case BadgePhase::Idle: break; } @@ -5440,7 +5446,12 @@ gboolean tick(gpointer user) { } StreamStats& st = model.stats(); const char* mode = "live"; - if (trigger_on) { + if (!st.have_config.load()) { + // Ahead of every other branch for the reason the glance word is: with no CONFIG there is no + // signal list, so "live" (or "armed", which cannot happen without one) would name a pipeline + // that has not been described yet. + mode = "trying to connect: no configuration, and so no signal list, from the stream yet"; + } else if (trigger_on) { if (app->capturing) { mode = "trigger: CAPTURING (post-trigger window filling)"; } else if (app->event_held) { @@ -5465,14 +5476,26 @@ gboolean tick(gpointer user) { // green light over a dead socket is the one reading that must not happen. The blink is // a 1 Hz square wave off the monotonic clock rather than its own timer, so it cannot // drift away from the frame it is drawn on. + // A source that answered CONNECT but has sent no CONFIG is *connecting*, not acquiring: + // there is no signal list, so nothing can be enabled and no DATA frame can arrive. It gets + // the steady yellow rather than the blinking green, because the green says "the pipeline is + // running end to end" and here it has not started. const bool paused = app->acq->paused(); - const bool active = st.connected.load() && !paused && (!trigger_on || armed); + const bool connecting = st.connected.load() && !st.have_config.load(); + const bool active = + st.connected.load() && !connecting && !paused && (!trigger_on || armed); const bool lit = ((g_get_monotonic_time() / (G_TIME_SPAN_SECOND / 2)) & 1) == 0; - const BadgePhase phase = - !active ? BadgePhase::Idle : (lit ? BadgePhase::Lit : BadgePhase::Dim); - const int reason = active ? 0 : !st.connected.load() ? 1 : paused ? 2 : 3; + const BadgePhase phase = connecting ? BadgePhase::Connecting + : !active ? BadgePhase::Idle + : (lit ? BadgePhase::Lit : BadgePhase::Dim); + const int reason = active ? 0 + : !st.connected.load() ? 1 + : connecting ? 4 + : paused ? 2 + : 3; static const char* const kWhy[] = {"acquiring", "waiting for the stream", "paused", - "trigger disarmed"}; + "trigger disarmed", + "trying to connect: no configuration from the stream yet"}; // Keyed on the reason as well as the phase: three of the reasons share the Idle phase, // so a pause that becomes a disarm has to reach the tooltip. ++app->dbg_badge_frames[static_cast(phase)]; @@ -5509,8 +5532,16 @@ gboolean tick(gpointer user) { // Derive the one-word state for the glance line from the same conditions the mode prose // branches on, so the two cannot disagree. + // + // `connecting` is first and covers both halves of the handshake -- no socket yet, and a + // socket with no CONFIG behind it -- because in neither is there a signal list, and every + // other word here claims one. The badge is what distinguishes them (grey against yellow): + // the word says what the app is doing, the colour says how far it got. It is no wider than + // `event held`, which is what lets it join a set the plot's layout is measured against. const char* state_word = "live"; - if (display == DisplayMode::Capturing) { + if (!st.have_config.load()) { + state_word = "connecting"; + } else if (display == DisplayMode::Capturing) { state_word = "capturing"; } else if (display == DisplayMode::Held) { state_word = "event held"; @@ -5685,9 +5716,6 @@ void note_starvation(App& app) { const std::uint64_t reconnects = st.reconnects.load(std::memory_order_relaxed); const bool connected = st.connected.load(std::memory_order_relaxed); - // No `have_config` here: the losing client of a single-client source need never see a - // CONFIG at all (the server answered the first one), and "connected, kicked, nothing - // arriving" is the state worth naming either way. // "Connected, but nothing is arriving" is a *rate* fact, not a reconnect one: a source that // answers CONNECT and then goes quiet -- the unicast case where another client took the session -- // never reconnects while it is silent, so requiring a reconnect made the notice invisible in @@ -5706,16 +5734,28 @@ void note_starvation(App& app) { // stream was arriving perfectly well. return; } + // Two diagnoses, split on `have_config`, because they are not the same situation. A source + // that sent a CONFIG and then went quiet is the unicast fight: the signal list is here, so + // something took the stream away. One that has sent no CONFIG at all never had the stream to + // lose -- there is no signal list, so nothing can be enabled and no DATA frame is owed -- and + // naming that "connected, but no data is arriving" blamed a second reader that does not exist. + // It is still the same stall test: a CONFIG that arrives promptly leaves no notice at all. + const bool have_config = st.have_config.load(std::memory_order_relaxed); app.notice = - "connected, but no data is arriving: another reader may hold the source (a unicast " - "source serves one client at a time) -- use --multicast to watch from several UIs"; + have_config + ? "connected, but no data is arriving: another reader may hold the source (a unicast " + "source serves one client at a time) -- use --multicast to watch from several UIs" + : "trying to connect: the source answered, but has sent no configuration, so there is " + "no signal list yet -- check that the streamer is running and serving this port"; app.notice_until_us = g_get_monotonic_time() + 3 * G_TIME_SPAN_SECOND; if (!app.starve_reported) { app.starve_reported = true; std::fprintf(stderr, - "[udpscope] starved: connected (config=%d), reconnects=%llu, no new frames " - "in the last second -- a unicast source serves one client at a time\n", - st.have_config.load(std::memory_order_relaxed) ? 1 : 0, + have_config + ? "[udpscope] starved: connected (config=1), reconnects=%llu, no new frames " + "in the last second -- a unicast source serves one client at a time\n" + : "[udpscope] connecting: connected (config=0), reconnects=%llu, no " + "configuration from the stream -- there is no signal list to acquire\n", static_cast(reconnects)); std::fflush(stderr); } @@ -8342,8 +8382,9 @@ int run_ui(int argc, char** argv, const Config& cfg, ScopeModel& model, Acquisit : 0.0); } } - std::printf("[udpscope] badge frames: lit=%d dim=%d idle=%d\n", app.dbg_badge_frames[0], - app.dbg_badge_frames[1], app.dbg_badge_frames[2]); + std::printf("[udpscope] badge frames: lit=%d dim=%d idle=%d connecting=%d\n", + app.dbg_badge_frames[0], app.dbg_badge_frames[1], app.dbg_badge_frames[2], + app.dbg_badge_frames[3]); std::printf("[udpscope] trigger panel revealed before the probe toggled it: %d; " "signal list items=%d selected=%d; column visible after sidebar " "toggle off=%d\n",