fixed event/live transition
This commit is contained in:
1 parent
bb99be5889
commit
881b52748e
9 files changed
+314
-2
No files matched your search
@@ -583,6 +583,22 @@ A replay leaves trigger mode, because a file has no edge to wait for -- an armed
|
|||||||
held display draws that snapshot rather than the file that was just loaded. The events pane stays
|
held display draws that snapshot rather than the file that was just loaded. The events pane stays
|
||||||
open through it: it is available while a stored event is on screen.
|
open through it: it is available while a stored event is on screen.
|
||||||
|
|
||||||
|
**Going back to *Live* puts the live view back as it was.** Opening a file changes two things that
|
||||||
|
are not the user's choice -- the time window becomes the event's own span, because that is what
|
||||||
|
framing an event means, and trigger mode goes off for the reason above -- and neither reason survives
|
||||||
|
the way back. So the settings the live view had are taken on the way in and restored on the way out:
|
||||||
|
the time window, trigger mode, the trigger's channel, level, condition, pre-trigger share and
|
||||||
|
hold-off. The trigger's channel is remembered by **name**, since a comparison renames its channels
|
||||||
|
(`square (A)`) and the way back has to find the stream's own. The restore waits for the first CONFIG
|
||||||
|
of the restarted client, which is a frame or two, and is the last word on the window -- it was
|
||||||
|
previously the *saved* session's window that came back, which is to say, somebody else's.
|
||||||
|
|
||||||
|
A related repair: **the session file is no longer written while a stored event is on screen.** A
|
||||||
|
comparison rebinds every plot slot to its own renamed channels, and the automatic save wrote those
|
||||||
|
names into the session -- so the next *Live* re-applied a layout that named no live signal, and the
|
||||||
|
scope came back with an empty plot. Nothing is lost by skipping: a file view configured nothing about
|
||||||
|
the stream. The explicit *Save as* is unaffected; that one is an answer to a question the user asked.
|
||||||
|
|
||||||
Three things worth knowing:
|
Three things worth knowing:
|
||||||
|
|
||||||
- **The samples are the export format**, so an event is readable by everything that reads an
|
- **The samples are the export format**, so an event is readable by everything that reads an
|
||||||
@@ -1181,7 +1197,9 @@ row's `×` forgets a source by deleting its session file, with no confirmation:
|
|||||||
confirmation, and what it destroys is one layout the app rewrites on the next clean exit. A session
|
confirmation, and what it destroys is one layout the app rewrites on the next clean exit. A session
|
||||||
is **not written** when no CONFIG arrived during the run — otherwise a source whose streamer was
|
is **not written** when no CONFIG arrived during the run — otherwise a source whose streamer was
|
||||||
down during one launch would silently vanish from the chooser list because its file was flattened
|
down during one launch would silently vanish from the chooser list because its file was flattened
|
||||||
to empty signal names, which the filter refuses.
|
to empty signal names, which the filter refuses — nor while a **stored event is on screen**, for
|
||||||
|
the same reason stated the other way round: a file view configured nothing about the stream, and a
|
||||||
|
comparison's renamed slots written into the session came back as an empty plot.
|
||||||
|
|
||||||
**Math channels.** A math channel is derived from the others by a graph of operations and is then
|
**Math channels.** A math channel is derived from the others by a graph of operations and is then
|
||||||
an *ordinary channel*: the model synthesises a signal entry for it, so an enabled one is acquired,
|
an *ordinary channel*: the model synthesises a signal entry for it, so an enabled one is acquired,
|
||||||
|
|||||||
@@ -59,6 +59,17 @@ Part of [CLAUDE.md](../../CLAUDE.md). These are rules that cost a defect to lear
|
|||||||
out — a source whose streamer was merely down during one run would silently vanish from the
|
out — a source whose streamer was merely down during one run would silently vanish from the
|
||||||
only list it can be picked from, for ever. Keeping the old file loses nothing: a run that
|
only list it can be picked from, for ever. Keeping the old file loses nothing: a run that
|
||||||
configured nothing had nothing worth persisting.
|
configured nothing had nothing worth persisting.
|
||||||
|
- **It refuses again while `replaying` is set: a file view is not the source's session.** Same
|
||||||
|
argument, different cause — this view configured nothing about the stream. A plain replay leaves
|
||||||
|
the rows where the user put them, so its save would merely be pointless; a **group** rebinds every
|
||||||
|
slot to its own renamed channels and the save wrote `signals = ["square (A)", …]` into the file.
|
||||||
|
Nothing noticed until the way back: *Go live* re-applies the session, those names match no live
|
||||||
|
signal, so nothing was enabled and **the live view came back with no channels at all** — on the
|
||||||
|
window the poisoned file carried, which is also how it swallowed the window restore below it. The
|
||||||
|
reading is `[events] live restored (after a group)`: the symptom was a stuck `pending=1` beside
|
||||||
|
`channels=0`, because the restore waits for a channel list that was never going to arrive. The
|
||||||
|
explicit *Save as* is deliberately **not** guarded: that one is a user's own answer to "save this
|
||||||
|
view", and its dialog reports the outcome.
|
||||||
|
|
||||||
## A configuration is a named session, and a source may hold several
|
## A configuration is a named session, and a source may hold several
|
||||||
|
|
||||||
|
|||||||
@@ -182,6 +182,53 @@ deliberately **not** one of the conditions: a stored event being examined is a f
|
|||||||
trigger, and tying the pane to the trigger would close the list under the click that opened an event.
|
trigger, and tying the pane to the trigger would close the list under the click that opened an event.
|
||||||
The recording switch lives in the trigger panel, not in the pane it empties.
|
The recording switch lives in the trigger panel, not in the pane it empties.
|
||||||
|
|
||||||
|
## Opening a file is a detour, so *Go live* is a restore
|
||||||
|
|
||||||
|
A file view overwrites the live view's settings and every overwrite has a reason: the window becomes
|
||||||
|
the event's own span because that is what *framing* an event means, and the trigger goes off because
|
||||||
|
a file has no edge to wait for (leaving it on kept a frozen capture on screen *instead* of the file,
|
||||||
|
so the click looked like it did nothing at all). None of those reasons survive the way back. What
|
||||||
|
the user had set is theirs, and "going back to live loses the trigger I spent a minute setting up"
|
||||||
|
is the complaint this answers.
|
||||||
|
|
||||||
|
- **`snapshot_live_state()` on the way in, `restore_live_state()` on the way out.** The snapshot is
|
||||||
|
taken by both ways into a file view -- `replay_event()` and `build_event_group()` -- *before* the
|
||||||
|
acquisition is stopped, and is a **no-op while `replaying`**: a second event opened from the first,
|
||||||
|
or a group opened from a replay, would otherwise snapshot a file's settings as "live", which are
|
||||||
|
the very values the restore exists to undo. The group takes it after its argument checks, because a
|
||||||
|
group that is refused leaves the live view alone.
|
||||||
|
- **The trigger channel is stored by *name*.** Same reason `TriggerEngine::configure()` takes one: the
|
||||||
|
index is a position in a channel list that a file view rebuilds, and a group **renames** its
|
||||||
|
channels outright (`square (A)`). Putting an index back would select a different signal.
|
||||||
|
- **`enabled` and `armed` are deliberately not restored from the snapshot.** They belong to the
|
||||||
|
toolbar toggle, which owns an atomic mirror of `enabled` (`set_trigger_enabled()`) that a plain
|
||||||
|
field write under the lock would leave stale. The restore writes the rest of the `TriggerConfig`
|
||||||
|
and then moves the *toggle*, whose handler states both.
|
||||||
|
- **The restore runs from the tick, not from `go_live_again()`, and *after* the deferred layout
|
||||||
|
block.** Two writers would otherwise beat it:
|
||||||
|
- `apply_layout()` ends in `apply_time_window(st.window_s)` -- the window the **session file**
|
||||||
|
holds, not the one the live view had. `go_live_again()` clears `layout_loaded`, so that block
|
||||||
|
fires on the next tick; a restore done inside `go_live_again()` is overwritten by the saved value
|
||||||
|
a frame later. Being the next statement after it in `tick()` is the fix.
|
||||||
|
- `rebuild_trigger_signals()`, finding a `channel` of `-1` after the rebuild renumbered the
|
||||||
|
channels, silently settles on the first signal *and* re-defaults the level -- which is why
|
||||||
|
`trigger_level_touched` is restored with the configuration.
|
||||||
|
- **It waits for the stream's own channel list, gated on `generation()` having moved.** The restored
|
||||||
|
name has to be resolved against the live channels, and those do not exist until the first CONFIG of
|
||||||
|
the restarted client lands on the producer thread. `App::live_restore_generation` is the generation
|
||||||
|
read when *Go live* was pressed; `live_restore_pending` stays set until it differs **and**
|
||||||
|
`channels()` is non-empty. A `pending=1` that never clears is therefore a reading about the
|
||||||
|
*stream*, not about the restore -- see `invariants-session-and-export.md` for the save that made it
|
||||||
|
stick at `channels=0`.
|
||||||
|
- **Only the two controls a file view actually moved are restated** -- the signal dropdown and the
|
||||||
|
level spin. The rest of the trigger panel was never touched, and writing to a control fires its
|
||||||
|
handler, so the ones with nothing to put back are left alone. Both writes are inside
|
||||||
|
`updating_panels`.
|
||||||
|
- **`pinned` is not a pass/fail reading on the way back.** `apply_time_window()` drops the file's pin
|
||||||
|
and re-arms the live follow, but if the restored trigger was *on* it can latch again within the
|
||||||
|
frame or two before the probe reads -- and a held capture is pinned, correctly. That is what
|
||||||
|
`held=` and `mode=` are beside it for.
|
||||||
|
|
||||||
## A comparison is two plots of one model
|
## A comparison is two plots of one model
|
||||||
|
|
||||||
*Compare* and *accumulate* are still one model -- the buffers cannot hold two independent loads -- but a
|
*Compare* and *accumulate* are still one model -- the buffers cannot hold two independent loads -- but a
|
||||||
|
|||||||
@@ -992,6 +992,7 @@ configuration that carries settings, is the defect this exists for — a saved h
|
|||||||
[events] activated row0: replaying=1 pane=1 pane_w=240 pane_box=228 tick=0 window=2.000 axis=[-1.600,0.400] marker=0.000 thumbnail=1 thumb_size=138x46 thumb_visible=1 rows_kept=1 selected=2/2
|
[events] activated row0: replaying=1 pane=1 pane_w=240 pane_box=228 tick=0 window=2.000 axis=[-1.600,0.400] marker=0.000 thumbnail=1 thumb_size=138x46 thumb_visible=1 rows_kept=1 selected=2/2
|
||||||
[events] replayed: channels=3 samples=20001 replaying=1 waveform=1
|
[events] replayed: channels=3 samples=20001 replaying=1 waveform=1
|
||||||
[events] live again: replaying=0 waveform=0
|
[events] live again: replaying=0 waveform=0
|
||||||
|
[events] live restored (after replay): window=0.500000 (want 0.500000) trigger=1 (want 1) channel='square' (want 'square') level=0 (want 0) enabled=1 armed=1 pinned=0 held=0 mode=2 pending=0 gen=10/5
|
||||||
[events] recording off, trigger off: button_visible=1 pane=1 pane_w=240 tick=1 recording=0
|
[events] recording off, trigger off: button_visible=1 pane=1 pane_w=240 tick=1 recording=0
|
||||||
[events] recording off, trigger on: button_visible=1 pane=1 pane_w=240 tick=1 recording=0
|
[events] recording off, trigger on: button_visible=1 pane=1 pane_w=240 tick=1 recording=0
|
||||||
[events] compare buttons: cached=2 selected=2 compare=1 accumulate=1
|
[events] compare buttons: cached=2 selected=2 compare=1 accumulate=1
|
||||||
@@ -1000,6 +1001,7 @@ configuration that carries settings, is the defect this exists for — a saved h
|
|||||||
[events] compare sync: y_a=-3.500 y_b=-3.500 mirrored=1
|
[events] compare sync: y_a=-3.500 y_b=-3.500 mirrored=1
|
||||||
[events] accumulate: kind=2 rows=1 channels=8 samples0=32964 selected=2 traces0=8 window=2.000
|
[events] accumulate: kind=2 rows=1 channels=8 samples0=32964 selected=2 traces0=8 window=2.000
|
||||||
[events] group left: replaying=0 kind=0 waveform=0
|
[events] group left: replaying=0 kind=0 waveform=0
|
||||||
|
[events] live restored (after a group): window=0.500000 (want 0.500000) trigger=1 (want 1) channel='square' (want 'square') level=0 (want 0) enabled=1 armed=1 pinned=0 held=0 mode=2 pending=0 gen=22/16
|
||||||
```
|
```
|
||||||
|
|
||||||
`thumb_size` is the built size of the row's picture: `kEventThumbW` wide, and as tall as the *stored*
|
`thumb_size` is the built size of the row's picture: `kEventThumbW` wide, and as tall as the *stored*
|
||||||
@@ -1016,6 +1018,21 @@ header — `480` wide and `44 * signals` tall — and `row_drawn=1` says the row
|
|||||||
The probe therefore needs a cache a **previous** run populated: it fires ~4.5 s in, before this run's
|
The probe therefore needs a cache a **previous** run populated: it fires ~4.5 s in, before this run's
|
||||||
first latch.
|
first latch.
|
||||||
|
|
||||||
|
`[events] live restored (…)` is the way *back*, read once per way into a file view — after a plain
|
||||||
|
replay and after a group — and four of its fields are a **pair**: the value now, and the value the
|
||||||
|
live view held before the file view opened over it. The probe captures that expectation itself
|
||||||
|
(`capture_live_expectation()`, `App::dbg_live_*`) rather than asking the restore path for it: a
|
||||||
|
reading taken from the mechanism under test asserts nothing. The window is set to 0.5 s just before
|
||||||
|
the event is opened precisely so the pair can disagree — with the event's own span there would be
|
||||||
|
nothing to tell "restored" from "untouched".
|
||||||
|
|
||||||
|
Both lines are read a *later* step, not in the handler that pressed *Live*: the restore waits for the
|
||||||
|
restarted client's first CONFIG, so it is performed by a tick ~900 ms on. `pending=0` is what says it
|
||||||
|
happened; `gen=a/b` is the generation now against the one *Go live* read. A **stuck `pending=1`** is
|
||||||
|
not a reading about the restore but about the stream behind it — the generation never moved, or the
|
||||||
|
channel list came back empty, which is how a session file poisoned by a save taken during a group
|
||||||
|
produced `pending=1 channels=0` and a live view with nothing on it.
|
||||||
|
|
||||||
| field | value it must read | the defect a wrong reading names |
|
| field | value it must read | the defect a wrong reading names |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| `n`, `stem`, `seq` | one entry per latch, named by its time and the run's counter | two events in one second overwriting each other, or a cache that is never written |
|
| `n`, `stem`, `seq` | one entry per latch, named by its time and the run's counter | two events in one second overwriting each other, or a cache that is never written |
|
||||||
@@ -1032,6 +1049,9 @@ first latch.
|
|||||||
| `axis`, `marker`, `pane_box` (in `activated row0`) | `[-pre, +post]` of the event just opened, `marker=0.000`, and a `pane_box` close to `pane_w` | an opened event framed on something other than its own pin; a trigger line left **parked** (a large negative) rather than on the edge, which is a file opened without its marker; or a pane whose *rendered* width is 0 while the divider says 240 -- the position is what was asked for, `pane_box` is what the pane actually got, and a divider at 240 with a zero-width pane is still no pane |
|
| `axis`, `marker`, `pane_box` (in `activated row0`) | `[-pre, +post]` of the event just opened, `marker=0.000`, and a `pane_box` close to `pane_w` | an opened event framed on something other than its own pin; a trigger line left **parked** (a large negative) rather than on the edge, which is a file opened without its marker; or a pane whose *rendered* width is 0 while the divider says 240 -- the position is what was asked for, `pane_box` is what the pane actually got, and a divider at 240 with a zero-width pane is still no pane |
|
||||||
| `rows_kept`, `selected` (in `activated row0`) | `rows_kept=1` and `selected=N/N` -- the counts either side of the activation equal | the list **rebuilt** by the activation. It is the row *object* that is compared, not its index: a rebuilt list has a row 0 too, and only identity tells a list that was left alone from one that was torn down under the user's scroll position and their comparison selection. `mark_events_on_screen()` must rewrite the titles in place |
|
| `rows_kept`, `selected` (in `activated row0`) | `rows_kept=1` and `selected=N/N` -- the counts either side of the activation equal | the list **rebuilt** by the activation. It is the row *object* that is compared, not its index: a rebuilt list has a row 0 too, and only identity tells a list that was left alone from one that was torn down under the user's scroll position and their comparison selection. `mark_events_on_screen()` must rewrite the titles in place |
|
||||||
| `replaying` (last line) | 0 | a replay with no way back: *Live* must start a fresh client and thread |
|
| `replaying` (last line) | 0 | a replay with no way back: *Live* must start a fresh client and thread |
|
||||||
|
| `window`, `trigger`, `channel`, `level` (in `live restored`) | each equal to its `(want …)` | the way back out of a file view discarding what the live view was set to. One wrong half names which writer won: a `window` that reads the **session file's** value is the deferred `apply_layout()` beating the restore; `trigger=0` is the toggle the file view switched off and nobody switched back; an empty or shifted `channel` is an index restored instead of a name, after a rebuild renumbered (or a group renamed) the channels; and a `level` that moved on its own is `rebuild_trigger_signals()` re-defaulting it because `trigger_level_touched` was not restored with the configuration |
|
||||||
|
| `pending`, `gen` (in `live restored`) | `pending=0`, and the two generations different | the restore still waiting. It is gated on the stream's first CONFIG, so `pending=1` says the channel list the stored trigger name needs never arrived -- with `channels=0` behind it, a session file that enabled nothing |
|
||||||
|
| `pinned`, `held`, `mode` (in `live restored`) | `pinned=0`, **or** `pinned=1` with `held=1` | a file's pin surviving the way back, which is a live view that cannot be navigated. `apply_time_window()` drops it; the second reading is the legitimate case -- a restored trigger that was *on* can latch again before the probe reads, and a held capture is pinned, correctly |
|
||||||
| `pane_w` | the divider's position: 0 while the pane is closed, its remembered width (240 by default) while it is open | the pane reserving a column while closed (a paned hands its start child its minimum unless the position says otherwise), or a width that is not remembered |
|
| `pane_w` | the divider's position: 0 while the pane is closed, its remembered width (240 by default) while it is open | the pane reserving a column while closed (a paned hands its start child its minimum unless the position says otherwise), or a width that is not remembered |
|
||||||
| `tick` | 1 in live mode, **0** once a file is on screen | the signal list still offering "acquire this signal" for samples that are already stored |
|
| `tick` | 1 in live mode, **0** once a file is on screen | the signal list still offering "acquire this signal" for samples that are already stored |
|
||||||
| `window` (in `activated row0`) | the **event's** span -- the probe sets 0.5 s just before the click, so reading the event's span says opening it framed it | a replay that leaves the previous window alone, or one that restates it every frame instead of once |
|
| `window` (in `activated row0`) | the **event's** span -- the probe sets 0.5 s just before the click, so reading the event's span says opening it framed it | a replay that leaves the previous window alone, or one that restates it every frame instead of once |
|
||||||
|
|||||||
+10
@@ -3294,6 +3294,16 @@ gboolean tick(gpointer user) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ... and *after* it, which is the point: the block above ends in `apply_time_window()` with the
|
||||||
|
// window the session file holds, so a restore done before it would be overwritten by the saved
|
||||||
|
// value. The generation is the other half of the wait -- the stream's first CONFIG, which is what
|
||||||
|
// gives the stored trigger channel a list to be found in.
|
||||||
|
if (app->live_restore_pending && !app->replaying && !model.channels().empty() &&
|
||||||
|
model.generation() != app->live_restore_generation) {
|
||||||
|
app->live_restore_pending = false;
|
||||||
|
restore_live_state(*app);
|
||||||
|
}
|
||||||
|
|
||||||
// What the display shows, decided once for the whole frame: the producer can open or
|
// What the display shows, decided once for the whole frame: the producer can open or
|
||||||
// close a capture between any two reads of the model, and the bind, the epoch, the pin
|
// close a capture between any two reads of the model, and the bind, the epoch, the pin
|
||||||
// and the status line all have to describe the same picture.
|
// and the status line all have to describe the same picture.
|
||||||
|
|||||||
@@ -452,6 +452,30 @@ struct App {
|
|||||||
std::vector<EventRecord> group;
|
std::vector<EventRecord> group;
|
||||||
/// True while a stored event is on screen instead of the live stream.
|
/// True while a stored event is on screen instead of the live stream.
|
||||||
bool replaying = false;
|
bool replaying = false;
|
||||||
|
/// What the live view was set to when a file view was opened over it, so that *Go live* can put it
|
||||||
|
/// back. A file view overwrites all of it and for good reasons -- the window is pinned to the
|
||||||
|
/// event's own span because that is what framing an event means, and the trigger goes off because a
|
||||||
|
/// file has no edge to wait for -- but none of those reasons survive the return to the stream, and
|
||||||
|
/// what the user set is theirs. Taken on the way *in*, and only when the view being left is the live
|
||||||
|
/// one, so a replay opened from inside a group does not snapshot the group's settings as "live".
|
||||||
|
///
|
||||||
|
/// The trigger's channel is kept by **name**: the index is a position in a channel list that a file
|
||||||
|
/// view rebuilds (a group renames its channels outright), so putting the index back would select a
|
||||||
|
/// different signal, or none.
|
||||||
|
struct LiveState {
|
||||||
|
bool valid = false;
|
||||||
|
double window_s = 0.0;
|
||||||
|
bool trigger_on = false;
|
||||||
|
TriggerConfig trigger;
|
||||||
|
std::string trigger_channel;
|
||||||
|
bool level_touched = false;
|
||||||
|
};
|
||||||
|
LiveState live_state;
|
||||||
|
/// Set by *Go live*, cleared by the tick that performs the restore, with the model generation it
|
||||||
|
/// has to see move first -- the stream's own channel list is what the stored trigger channel is
|
||||||
|
/// resolved against.
|
||||||
|
bool live_restore_pending = false;
|
||||||
|
unsigned live_restore_generation = 0;
|
||||||
/// Set when a latch frames a new snapshot, consumed at the *end* of that tick: the record needs the
|
/// Set when a latch frames a new snapshot, consumed at the *end* of that tick: the record needs the
|
||||||
/// rows bound to the event (for the thumbnail) and the bind happens after the display decision.
|
/// rows bound to the event (for the thumbnail) and the bind happens after the display decision.
|
||||||
bool record_pending = false;
|
bool record_pending = false;
|
||||||
@@ -749,6 +773,16 @@ struct App {
|
|||||||
/// position and their comparison selection.
|
/// position and their comparison selection.
|
||||||
GtkListBoxRow* dbg_events_row0 = nullptr;
|
GtkListBoxRow* dbg_events_row0 = nullptr;
|
||||||
int dbg_events_selected = 0;
|
int dbg_events_selected = 0;
|
||||||
|
/// What the *live* view was set to just before the probe opened an event: the time window, the
|
||||||
|
/// trigger's mode, its channel (by name, the only stable identity) and its level. Read back after
|
||||||
|
/// each way out of a file view, because a file view overwrites all four -- the window is pinned to
|
||||||
|
/// the event's own span and the trigger is switched off, since a file has no edge to wait for --
|
||||||
|
/// and going live has to put them back. The probe keeps its own expectation rather than asking
|
||||||
|
/// `App`: a reading taken from the mechanism under test asserts nothing.
|
||||||
|
double dbg_live_window = 0.0;
|
||||||
|
bool dbg_live_trigger_on = false;
|
||||||
|
std::string dbg_live_trigger_channel;
|
||||||
|
double dbg_live_level = 0.0;
|
||||||
/// Which step of the plot-context-menu probe runs next (`UDPSCOPE_PROBE_ROWMENU_MS`), and the
|
/// Which step of the plot-context-menu probe runs next (`UDPSCOPE_PROBE_ROWMENU_MS`), and the
|
||||||
/// popover it is measuring.
|
/// popover it is measuring.
|
||||||
int dbg_rowmenu_step = 0;
|
int dbg_rowmenu_step = 0;
|
||||||
@@ -1335,6 +1369,8 @@ void update_capture_row(App& app);
|
|||||||
bool record_event(App& app);
|
bool record_event(App& app);
|
||||||
bool replay_event(App& app, const EventRecord& record);
|
bool replay_event(App& app, const EventRecord& record);
|
||||||
bool go_live_again(App& app);
|
bool go_live_again(App& app);
|
||||||
|
void snapshot_live_state(App& app);
|
||||||
|
void restore_live_state(App& app);
|
||||||
bool read_event_samples(const std::string& path, EventSamples* out, std::string* error);
|
bool read_event_samples(const std::string& path, EventSamples* out, std::string* error);
|
||||||
std::vector<EventRecord> selected_events(App& app);
|
std::vector<EventRecord> selected_events(App& app);
|
||||||
void update_group_buttons(App& app);
|
void update_group_buttons(App& app);
|
||||||
|
|||||||
+93
-1
@@ -141,11 +141,94 @@ bool record_event(App& app) {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Remembers the live view's settings before a file view is opened over them.
|
||||||
|
///
|
||||||
|
/// A no-op unless the view being left *is* the live one: replaying a second event from inside the
|
||||||
|
/// first, or a group opened from a replay, must not overwrite the snapshot with a file's settings --
|
||||||
|
/// those are the very values *Go live* exists to undo.
|
||||||
|
void snapshot_live_state(App& app) {
|
||||||
|
if (app.replaying || app.model == nullptr || app.rows == nullptr) return;
|
||||||
|
App::LiveState& s = app.live_state;
|
||||||
|
s.valid = true;
|
||||||
|
s.window_s = app.rows->time_window();
|
||||||
|
s.trigger_on = app.trigger_toggle != nullptr &&
|
||||||
|
gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(app.trigger_toggle)) != FALSE;
|
||||||
|
s.level_touched = app.trigger_level_touched;
|
||||||
|
std::lock_guard<std::mutex> lock(app.model->mutex());
|
||||||
|
s.trigger = app.model->trigger();
|
||||||
|
s.trigger_channel = s.trigger.channel >= 0 &&
|
||||||
|
s.trigger.channel < static_cast<int>(app.model->channels().size())
|
||||||
|
? app.model->channels()[static_cast<std::size_t>(s.trigger.channel)].name
|
||||||
|
: std::string();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Puts those settings back on the way out of a file view.
|
||||||
|
///
|
||||||
|
/// Deferred by `go_live_again()` to the first tick that sees the stream's *own* channel list, which
|
||||||
|
/// is what makes this the last writer over the two things that would otherwise overwrite it: the
|
||||||
|
/// deferred layout re-apply (which ends in `apply_time_window()` with the *saved* window, not the
|
||||||
|
/// live one) and `rebuild_trigger_signals()`, which, finding a `channel` of `-1` after a rebuild
|
||||||
|
/// renumbered or renamed the channels, silently settles on the first signal and re-defaults the level
|
||||||
|
/// with it.
|
||||||
|
///
|
||||||
|
/// Inside, the order is the whole of it:
|
||||||
|
///
|
||||||
|
/// - The configuration goes in under the lock, with the channel resolved from the stored *name*.
|
||||||
|
/// `enabled` and `armed` are deliberately *not* written: they belong to the toolbar toggle, which
|
||||||
|
/// owns an atomic mirror of `enabled` that a plain field write would leave stale.
|
||||||
|
/// - The two controls a file view actually moved are restated from it -- the signal dropdown and the
|
||||||
|
/// level spin. The rest of the panel was never touched, and writing to a control fires its handler,
|
||||||
|
/// so the ones that have nothing to put back are left alone.
|
||||||
|
/// - `apply_time_window()`, the only path allowed to restate the time scale; it also drops the event
|
||||||
|
/// pin the file view left on the axis and re-arms the live follow.
|
||||||
|
/// - The toggle last, because its handler reads the configuration it is turning on.
|
||||||
|
void restore_live_state(App& app) {
|
||||||
|
App::LiveState& s = app.live_state;
|
||||||
|
if (!s.valid) return;
|
||||||
|
s.valid = false; // consumed: the next way in takes its own snapshot
|
||||||
|
int channel = -1;
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(app.model->mutex());
|
||||||
|
const std::vector<ChannelInfo>& channels = app.model->channels();
|
||||||
|
for (std::size_t i = 0; i < channels.size(); ++i) {
|
||||||
|
if (channels[i].name == s.trigger_channel) {
|
||||||
|
channel = static_cast<int>(i);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
TriggerConfig& tc = app.model->trigger();
|
||||||
|
const bool enabled = tc.enabled;
|
||||||
|
const bool armed = tc.armed;
|
||||||
|
tc = s.trigger;
|
||||||
|
tc.enabled = enabled;
|
||||||
|
tc.armed = armed;
|
||||||
|
tc.channel = channel;
|
||||||
|
}
|
||||||
|
app.trigger_level_touched = s.level_touched;
|
||||||
|
app.updating_panels = true;
|
||||||
|
if (channel >= 0 && app.trigger_signal_drop != nullptr) {
|
||||||
|
gtk_drop_down_set_selected(GTK_DROP_DOWN(app.trigger_signal_drop),
|
||||||
|
static_cast<guint>(channel));
|
||||||
|
}
|
||||||
|
if (app.trigger_level_spin != nullptr) {
|
||||||
|
gtk_spin_button_set_value(GTK_SPIN_BUTTON(app.trigger_level_spin), s.trigger.condition.level);
|
||||||
|
}
|
||||||
|
app.updating_panels = false;
|
||||||
|
apply_time_window(app, s.window_s);
|
||||||
|
if (app.trigger_toggle != nullptr &&
|
||||||
|
(gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(app.trigger_toggle)) != FALSE) !=
|
||||||
|
s.trigger_on) {
|
||||||
|
gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(app.trigger_toggle), s.trigger_on ? TRUE : FALSE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Replays a cached event: the same loader `--open` uses, on the event's file. The acquisition is
|
/// Replays a cached event: the same loader `--open` uses, on the event's file. The acquisition is
|
||||||
/// stopped first, because a live producer overwrites the loaded samples within a frame -- so the
|
/// stopped first, because a live producer overwrites the loaded samples within a frame -- so the
|
||||||
/// scope becomes a file viewer until *Go live* starts it again.
|
/// scope becomes a file viewer until *Go live* starts it again.
|
||||||
bool replay_event(App& app, const EventRecord& record) {
|
bool replay_event(App& app, const EventRecord& record) {
|
||||||
if (app.acq == nullptr || app.model == nullptr) return false;
|
if (app.acq == nullptr || app.model == nullptr) return false;
|
||||||
|
// Before the stop, while the live view is still the view: everything below overwrites it.
|
||||||
|
snapshot_live_state(app);
|
||||||
app.acq->stop();
|
app.acq->stop();
|
||||||
WaveformLoadReport report;
|
WaveformLoadReport report;
|
||||||
std::string error;
|
std::string error;
|
||||||
@@ -199,6 +282,13 @@ bool go_live_again(App& app) {
|
|||||||
app.layout_loaded = false; // the session's layout applies to the source again
|
app.layout_loaded = false; // the session's layout applies to the source again
|
||||||
app.event_held = false;
|
app.event_held = false;
|
||||||
app.model->set_display_epoch(0.0);
|
app.model->set_display_epoch(0.0);
|
||||||
|
// What the live view had, put back from the tick rather than here: the stream's own channel list is
|
||||||
|
// what the stored trigger channel has to be resolved against, and it does not exist until the first
|
||||||
|
// CONFIG of the restarted client lands on the producer thread. The generation read here is what the
|
||||||
|
// tick compares against to know that it has (see `restore_live_state()` for what else that ordering
|
||||||
|
// buys).
|
||||||
|
app.live_restore_generation = app.model->generation();
|
||||||
|
app.live_restore_pending = app.live_state.valid;
|
||||||
try {
|
try {
|
||||||
app.acq->start();
|
app.acq->start();
|
||||||
} catch (const std::exception& e) {
|
} catch (const std::exception& e) {
|
||||||
@@ -307,7 +397,9 @@ bool build_event_group(App& app, const std::vector<EventRecord>& records, EventG
|
|||||||
|
|
||||||
// The acquisition first: a live producer keeps pushing into the model it was started with -- and it
|
// The acquisition first: a live producer keeps pushing into the model it was started with -- and it
|
||||||
// pushes *four* stream signals into the group's eight channels, so the group's own samples are the
|
// pushes *four* stream signals into the group's eight channels, so the group's own samples are the
|
||||||
// ones that go missing.
|
// ones that go missing. The live view's settings are taken ahead of it, for `go_live_again()`:
|
||||||
|
// after the checks above, because a group that is refused leaves the live view alone.
|
||||||
|
snapshot_live_state(app);
|
||||||
app.acq->stop();
|
app.acq->stop();
|
||||||
|
|
||||||
std::vector<EventSamples> events(records.size());
|
std::vector<EventSamples> events(records.size());
|
||||||
|
|||||||
@@ -232,6 +232,14 @@ void save_layout_now(App& app) {
|
|||||||
// run would silently vanish from the only place it can be picked. Keeping the old file loses
|
// run would silently vanish from the only place it can be picked. Keeping the old file loses
|
||||||
// nothing: this run configured nothing.
|
// nothing: this run configured nothing.
|
||||||
if (app.model->signals().empty()) return;
|
if (app.model->signals().empty()) return;
|
||||||
|
// A file view is not the source's session, and the same argument applies: this view configured
|
||||||
|
// nothing about the stream. A replay leaves the rows where the user put them, so its save would
|
||||||
|
// merely be pointless -- but a *group* rebinds every slot to its own renamed channels, and a save
|
||||||
|
// taken while one was up wrote `signals = ["square (A)", ...]` into the file. The next load then
|
||||||
|
// matched no live signal, enabled nothing, and the live view came back with no channels at all,
|
||||||
|
// on the window the poisoned file carried. Skipping costs nothing: the live session's own file is
|
||||||
|
// already on disk, and `go_live_again()` puts the rest back.
|
||||||
|
if (app.replaying) return;
|
||||||
if (app.config_name.empty()) app.config_name = active_configuration(app.source_host,
|
if (app.config_name.empty()) app.config_name = active_configuration(app.source_host,
|
||||||
app.source_port);
|
app.source_port);
|
||||||
const std::string path =
|
const std::string path =
|
||||||
|
|||||||
@@ -3107,6 +3107,62 @@ gboolean rowmenu_probe_step(gpointer user) {
|
|||||||
return G_SOURCE_REMOVE;
|
return G_SOURCE_REMOVE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The live view's own settings, taken just before a file view is opened over them, so that every
|
||||||
|
/// way back out can be read against what was there. The trigger is read under the lock and its
|
||||||
|
/// channel by *name*: a file view rebuilds the channel list, so an index means nothing on the way
|
||||||
|
/// back. The probe keeps its own copy rather than asking the restore path for one -- a reading taken
|
||||||
|
/// from the mechanism under test asserts nothing.
|
||||||
|
void capture_live_expectation(App& a) {
|
||||||
|
a.dbg_live_window = a.rows->time_window();
|
||||||
|
a.dbg_live_trigger_on =
|
||||||
|
a.trigger_toggle != nullptr &&
|
||||||
|
gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(a.trigger_toggle)) != FALSE;
|
||||||
|
std::lock_guard<std::mutex> lock(a.model->mutex());
|
||||||
|
const TriggerConfig& tc = a.model->trigger();
|
||||||
|
a.dbg_live_level = tc.condition.level;
|
||||||
|
a.dbg_live_trigger_channel = tc.channel >= 0 && tc.channel < int(a.model->channels().size())
|
||||||
|
? a.model->channels()[std::size_t(tc.channel)].name
|
||||||
|
: std::string();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// What came back when a file view was left: every setting the live view had before it opened, read
|
||||||
|
/// against the expectation the probe captured itself. Four of the five readings are a *pair* on
|
||||||
|
/// purpose -- a file view overwrites the window, the trigger mode, the trigger's channel and its
|
||||||
|
/// level, so a line where the two halves of a pair disagree names the one that was not put back.
|
||||||
|
/// `pinned` is the fifth and has no pair: a file pins the axis to the event's span, and going live
|
||||||
|
/// must leave the axis following again, so the only passing reading is 0.
|
||||||
|
void report_live_restored(App& a, const char* what) {
|
||||||
|
std::string channel;
|
||||||
|
double level = 0.0;
|
||||||
|
bool enabled = false;
|
||||||
|
bool armed = false;
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(a.model->mutex());
|
||||||
|
const TriggerConfig& tc = a.model->trigger();
|
||||||
|
enabled = tc.enabled;
|
||||||
|
armed = tc.armed;
|
||||||
|
level = tc.condition.level;
|
||||||
|
if (tc.channel >= 0 && tc.channel < int(a.model->channels().size())) {
|
||||||
|
channel = a.model->channels()[std::size_t(tc.channel)].name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
std::fprintf(stderr,
|
||||||
|
"[events] live restored (%s): window=%.6f (want %.6f) trigger=%d (want %d) "
|
||||||
|
"channel='%s' (want '%s') level=%.6g (want %.6g) enabled=%d armed=%d pinned=%d "
|
||||||
|
"held=%d mode=%d pending=%d gen=%u/%u\n",
|
||||||
|
what, a.rows->time_window(), a.dbg_live_window,
|
||||||
|
a.trigger_toggle != nullptr &&
|
||||||
|
gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(a.trigger_toggle))
|
||||||
|
? 1
|
||||||
|
: 0,
|
||||||
|
a.dbg_live_trigger_on ? 1 : 0, channel.c_str(), a.dbg_live_trigger_channel.c_str(),
|
||||||
|
level, a.dbg_live_level, enabled ? 1 : 0, armed ? 1 : 0,
|
||||||
|
a.rows->window_pinned() ? 1 : 0, a.event_held ? 1 : 0,
|
||||||
|
static_cast<int>(a.model->display_mode()), a.live_restore_pending ? 1 : 0,
|
||||||
|
a.model->generation(), a.live_restore_generation);
|
||||||
|
std::fflush(stderr);
|
||||||
|
}
|
||||||
|
|
||||||
void events_probe_next(App& a, int step, guint delay_ms) {
|
void events_probe_next(App& a, int step, guint delay_ms) {
|
||||||
a.dbg_events_step = step;
|
a.dbg_events_step = step;
|
||||||
g_timeout_add(delay_ms, events_probe_step, &a);
|
g_timeout_add(delay_ms, events_probe_step, &a);
|
||||||
@@ -3227,6 +3283,7 @@ gboolean events_probe_step(gpointer user) {
|
|||||||
// says whether opening the event framed it: with the same window there would be nothing to tell
|
// says whether opening the event framed it: with the same window there would be nothing to tell
|
||||||
// "framed" from "untouched".
|
// "framed" from "untouched".
|
||||||
apply_time_window(*a, 0.5);
|
apply_time_window(*a, 0.5);
|
||||||
|
capture_live_expectation(*a);
|
||||||
GtkListBoxRow* row = gtk_list_box_get_row_at_index(GTK_LIST_BOX(a->events_list), 0);
|
GtkListBoxRow* row = gtk_list_box_get_row_at_index(GTK_LIST_BOX(a->events_list), 0);
|
||||||
// Selected *and* remembered, because what the activation must not do is rebuild the list:
|
// Selected *and* remembered, because what the activation must not do is rebuild the list:
|
||||||
// a selection the user made for a comparison, and the row object itself, are what a
|
// a selection the user made for a comparison, and the row object itself, are what a
|
||||||
@@ -3312,6 +3369,7 @@ gboolean events_probe_step(gpointer user) {
|
|||||||
|
|
||||||
std::fprintf(stderr, "[events] live again: replaying=%d waveform=%d\n", a->replaying ? 1 : 0,
|
std::fprintf(stderr, "[events] live again: replaying=%d waveform=%d\n", a->replaying ? 1 : 0,
|
||||||
a->waveform_name.empty() ? 0 : 1);
|
a->waveform_name.empty() ? 0 : 1);
|
||||||
|
report_live_restored(*a, "after replay");
|
||||||
// The pane's *other* precondition, through the real switch, in both trigger states: disabling
|
// The pane's *other* precondition, through the real switch, in both trigger states: disabling
|
||||||
// recording has to take the button and the pane with it, and arming the trigger afterwards must not
|
// recording has to take the button and the pane with it, and arming the trigger afterwards must not
|
||||||
// bring back a pane over an empty cache.
|
// bring back a pane over an empty cache.
|
||||||
@@ -3360,6 +3418,10 @@ gboolean events_group_probe_step(gpointer user) {
|
|||||||
a->events_compare != nullptr && gtk_widget_get_sensitive(a->events_compare) ? 1 : 0,
|
a->events_compare != nullptr && gtk_widget_get_sensitive(a->events_compare) ? 1 : 0,
|
||||||
a->events_accumulate != nullptr && gtk_widget_get_sensitive(a->events_accumulate) ? 1
|
a->events_accumulate != nullptr && gtk_widget_get_sensitive(a->events_accumulate) ? 1
|
||||||
: 0);
|
: 0);
|
||||||
|
// The live settings this leg will be read against, taken *here* rather than inherited from the
|
||||||
|
// replay leg: a group is a second, independent way into a file view, and its way back has to put
|
||||||
|
// back what was on screen when the button was pressed.
|
||||||
|
capture_live_expectation(*a);
|
||||||
if (a->events_compare != nullptr) g_signal_emit_by_name(a->events_compare, "clicked");
|
if (a->events_compare != nullptr) g_signal_emit_by_name(a->events_compare, "clicked");
|
||||||
// Read one step later: a rebuilt row's traces are bound by the next *frame*, so a reading taken in
|
// Read one step later: a rebuilt row's traces are bound by the next *frame*, so a reading taken in
|
||||||
// the same handler as the click reads the frame before the bind.
|
// the same handler as the click reads the frame before the bind.
|
||||||
@@ -3502,6 +3564,14 @@ gboolean events_group_probe_step(gpointer user) {
|
|||||||
std::fprintf(stderr, "[events] group left: replaying=%d kind=%d waveform=%d\n", a->replaying ? 1 : 0,
|
std::fprintf(stderr, "[events] group left: replaying=%d kind=%d waveform=%d\n", a->replaying ? 1 : 0,
|
||||||
static_cast<int>(a->group_kind), a->waveform_name.empty() ? 0 : 1);
|
static_cast<int>(a->group_kind), a->waveform_name.empty() ? 0 : 1);
|
||||||
std::fflush(stderr);
|
std::fflush(stderr);
|
||||||
|
// The restore is not this handler's: it waits for the restarted client's first CONFIG, so it is a
|
||||||
|
// *later tick* that performs it. Reading here would read the frame before the stream came back.
|
||||||
|
events_group_probe_next(*a, 61, 900);
|
||||||
|
return G_SOURCE_REMOVE;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (a->dbg_events_step == 61) {
|
||||||
|
report_live_restored(*a, "after a group");
|
||||||
return G_SOURCE_REMOVE;
|
return G_SOURCE_REMOVE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user