cleaned up ui added exporter
This commit is contained in:
1 parent
8059b90de7
commit
ea560ed272
4 files changed
+225
-3
No files matched your search
@@ -45,6 +45,9 @@ xvfb-run -a env UDPSCOPE_EXIT_AFTER_MS=8000 UDPSCOPE_ENABLE_ALL_MS=1000 \
|
|||||||
`UDPSCOPE_TRIG_TRACE_MS` — schedule individual probes.
|
`UDPSCOPE_TRIG_TRACE_MS` — schedule individual probes.
|
||||||
- `UDPSCOPE_PROBE_PAUSE_MS` — pause acquisition mid-run so layout numbers are
|
- `UDPSCOPE_PROBE_PAUSE_MS` — pause acquisition mid-run so layout numbers are
|
||||||
measured on a still picture.
|
measured on a still picture.
|
||||||
|
- `UDPSCOPE_EXPORT_MS` (+ optional `UDPSCOPE_EXPORT_PATH`, default
|
||||||
|
`/tmp/udpscope-export`) — collect the export table and write it both ways, skipping
|
||||||
|
only the file chooser.
|
||||||
|
|
||||||
**Run one self-check at a time.** Two concurrent `xvfb-run -a` instances end up on the
|
**Run one self-check at a time.** Two concurrent `xvfb-run -a` instances end up on the
|
||||||
same display and the second gets no GL context — it dumps `layout over 0 frames` and
|
same display and the second gets no GL context — it dumps `layout over 0 frames` and
|
||||||
@@ -106,6 +109,17 @@ Two lines of the self-check output are assertions in disguise:
|
|||||||
is the same class of bug, not rounding: `publish_event()` clamps the slice to every
|
is the same class of bug, not rounding: `publish_event()` clamps the slice to every
|
||||||
value ring's `produced()`, so publishing before each channel of the latching sample
|
value ring's `produced()`, so publishing before each channel of the latching sample
|
||||||
has been pushed silently drops it.
|
has been pushed silently drops it.
|
||||||
|
- `[export-probe] mode=.. axis=[a..b] rows=N cols=M time=[c..d] stream=[..]` (stderr,
|
||||||
|
`UDPSCOPE_EXPORT_MS`) — **`time=` must equal `axis=`** and **`rows=` must be the
|
||||||
|
span times the stream's real rate, plus one** (2 s at 10 kS/s is 20001). The export
|
||||||
|
is the *visible* range at the *stored* rate, so a count far short of that is a table
|
||||||
|
cut from the LOD pyramid, and a `time=` narrower than `axis=` is a window searched on
|
||||||
|
the wrong clock (the live branch subtracts `display_epoch()` before searching the
|
||||||
|
ring). In `mode=event`, `stream=` must be the edge ± the window; in `mode=live` it
|
||||||
|
equals `time=`. The two `bytes=` lines are the writers: the `.npy` is
|
||||||
|
`64·⌈(10+len(header)+1)/64⌉ + rows·(16 + 4·cols)`, so a size that is not that is a
|
||||||
|
mispacked record. Read it back with `pd.DataFrame(np.load(path))` after changing
|
||||||
|
either writer.
|
||||||
|
|
||||||
The probes are the regression net — when changing UI or plot-host behaviour, extend
|
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.
|
the probe/dump in `src/ui.cpp` rather than adding a separate harness.
|
||||||
@@ -154,6 +168,10 @@ Layers to keep straight:
|
|||||||
the live follow (the *group's* — a member's own axis must be `XFollow::Off`, since
|
the live follow (the *group's* — a member's own axis must be `XFollow::Off`, since
|
||||||
`Figure::link_x()` refuses a `Live` member), the left gutter and the crosshair.
|
`Figure::link_x()` refuses a `Live` member), the left gutter and the crosshair.
|
||||||
|
|
||||||
|
- `ExportTable` (`src/export.hpp`) — the visible window as columns, collected from the
|
||||||
|
snapshot or the live rings and handed to the `.npy`/`.csv` writers. Always the stored
|
||||||
|
samples, never the LOD pyramid's buckets.
|
||||||
|
|
||||||
`src/ui.cpp` is the GTK4 window (toolbar, trigger panel, signal panel, plot box) plus
|
`src/ui.cpp` is the GTK4 window (toolbar, trigger panel, signal panel, plot box) plus
|
||||||
the `tick()` frame function that binds, updates the trigger state and rewrites the
|
the `tick()` frame function that binds, updates the trigger state and rewrites the
|
||||||
status line. One `plot::gtk4::PlotArea` in figure mode draws the whole figure. It is by
|
status line. One `plot::gtk4::PlotArea` in figure mode draws the whole figure. It is by
|
||||||
@@ -239,6 +257,20 @@ far the largest file.
|
|||||||
every value ring.** That is what makes `stamps` and each `values[c]` equal-length
|
every value ring.** That is what makes `stamps` and each `values[c]` equal-length
|
||||||
and index-aligned by construction instead of by two truncations that can disagree —
|
and index-aligned by construction instead of by two truncations that can disagree —
|
||||||
`EventSeries::gather()` indexes both at the same `i`.
|
`EventSeries::gather()` indexes both at the same `i`.
|
||||||
|
- **`live_window_locked()` is the one live cut, shared by `publish_event()` and the
|
||||||
|
export.** Both need the same absolute index range over the time ring and every value
|
||||||
|
ring, or an export and a capture of the same window would hold different samples. Call
|
||||||
|
it under `mutex()`.
|
||||||
|
- **A trace's colour is indexed by *signal*, not by trace slot.** `Row::color_index`
|
||||||
|
stores it and `signal_color_index()` (ui.cpp) computes it from the signal list, so a
|
||||||
|
signal dropped on two rows is one colour and the signal panel's chip names a trace
|
||||||
|
unambiguously. `plot::Plot` fixes a colour at `add_trace()`, so changing one needs a
|
||||||
|
figure rebuild — which `assign()` already forces. A signal on no plot gets
|
||||||
|
`RowSet::unplotted_color()` (grey) rather than no chip, so the panel's rows stay the
|
||||||
|
same height.
|
||||||
|
- **The export is collected at click time, not in the file-dialog callback.** The live
|
||||||
|
view keeps scrolling while the chooser is up, so a late collection would write a
|
||||||
|
window the user never saw. The writer is picked by the chosen path's extension.
|
||||||
- **The header bar's buttons are icon-only**, so headless probes must reach them
|
- **The header bar's buttons are icon-only**, so headless probes must reach them
|
||||||
through the `App` pointers (`trigger_toggle`, `pause_button`, …), not via
|
through the `App` pointers (`trigger_toggle`, `pause_button`, …), not via
|
||||||
`find_labelled_button()`. The Arm/Disarm button in the trigger panel still has text.
|
`find_labelled_button()`. The Arm/Disarm button in the trigger panel still has text.
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ add_executable(udpscope
|
|||||||
src/trigger.cpp
|
src/trigger.cpp
|
||||||
src/acquisition.cpp
|
src/acquisition.cpp
|
||||||
src/plot_host.cpp
|
src/plot_host.cpp
|
||||||
|
src/export.cpp
|
||||||
src/ui.cpp
|
src/ui.cpp
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ src/channel.* ChannelBuffer (a ring pair as a plot::data::Series) and SlotS
|
|||||||
src/acquisition.* the UDPStreamer client, the producer thread, frame decoding
|
src/acquisition.* the UDPStreamer client, the producer thread, frame decoding
|
||||||
src/trigger.* the trigger configuration and the producer-side state machine
|
src/trigger.* the trigger configuration and the producer-side state machine
|
||||||
src/plot_host.* the one plot::Figure, its rows' traces and cursors, the x link group
|
src/plot_host.* the one plot::Figure, its rows' traces and cursors, the x link group
|
||||||
|
src/export.* the visible range as a table, and the .npy / .csv writers
|
||||||
src/ui.* the GTK4 window: toolbar, trigger panel, signal panel, plot box
|
src/ui.* the GTK4 window: toolbar, trigger panel, signal panel, plot box
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -188,8 +189,8 @@ assumed.
|
|||||||
pause/resume, the trigger toggle, the side-panel toggle and an overflow menu. Those
|
pause/resume, the trigger toggle, the side-panel toggle and an overflow menu. Those
|
||||||
four buttons are icons with tooltips; nine text buttons was a wall of words to read
|
four buttons are icons with tooltips; nine text buttons was a wall of words to read
|
||||||
past every time you wanted the one you needed. The overflow menu holds **Fit to
|
past every time you wanted the one you needed. The overflow menu holds **Fit to
|
||||||
data**, **Cursors**, **Reset cursors** and the **Signal list** toggle — the things
|
data**, **Cursors**, **Reset cursors**, **Export visible range…** and the
|
||||||
you set up once. The side column and the two panel toggles stay hidden until a
|
**Signal list** toggle — the things you set up once. The side column and the two panel toggles stay hidden until a
|
||||||
CONFIG arrives — there is nothing to configure before the stream describes itself —
|
CONFIG arrives — there is nothing to configure before the stream describes itself —
|
||||||
and the first CONFIG reveals the signal panel (the trigger panel stays behind its
|
and the first CONFIG reveals the signal panel (the trigger panel stays behind its
|
||||||
own toggle).
|
own toggle).
|
||||||
@@ -239,7 +240,19 @@ latch, which the status line reports.
|
|||||||
|
|
||||||
The signal panel is one line per signal: a colour chip naming the trace it is drawn
|
The signal panel is one line per signal: a colour chip naming the trace it is drawn
|
||||||
as, a checkbox that acquires it, its name, its unit, and a `⋯` menu holding the
|
as, a checkbox that acquires it, its name, its unit, and a `⋯` menu holding the
|
||||||
information popup and the gain/offset calibration popup. A `GtkSwitch` and two
|
information popup and the gain/offset calibration popup.
|
||||||
|
|
||||||
|
**The chip's colour belongs to the signal, not to the slot it landed in.** The
|
||||||
|
palette is indexed by position in the signal list (`signal_color_index()`), so a
|
||||||
|
signal dropped on two rows is the same colour in both and the chip names a trace
|
||||||
|
unambiguously; indexing by trace slot made the same signal blue on row 1 and orange
|
||||||
|
on row 2, and then the chip named neither. A signal that is on no plot — acquired or
|
||||||
|
not — gets a **grey** chip rather than no chip, so every row of the panel is the same
|
||||||
|
shape and the column of chips reads as a legend. Because `plot::Plot` fixes a trace's
|
||||||
|
colour at `add_trace()`, the index is stored on the row (`Row::color_index`) and a
|
||||||
|
colour change goes through a figure rebuild, which a drop already forces.
|
||||||
|
|
||||||
|
A `GtkSwitch` and two
|
||||||
always-visible buttons made a row about twice as tall (46 px against 26 px now),
|
always-visible buttons made a row about twice as tall (46 px against 26 px now),
|
||||||
which is what turned a four-signal stream into a panel you have to scroll; Adwaita's
|
which is what turned a four-signal stream into a panel you have to scroll; Adwaita's
|
||||||
button and list-row minimums, not the widgets themselves, set that height, so the
|
button and list-row minimums, not the widgets themselves, set that height, so the
|
||||||
@@ -342,6 +355,30 @@ the widget keeps its context. A displayed event survives the rebuild because the
|
|||||||
pinned `[-pre, +post]` window is restated rather than re-derived from the live
|
pinned `[-pre, +post]` window is restated rather than re-derived from the live
|
||||||
edge.
|
edge.
|
||||||
|
|
||||||
|
**Export visible range…** (overflow menu) writes every signal bound to a visible row,
|
||||||
|
over the shared x window, at the **stored** sample rate — never the LOD pyramid's
|
||||||
|
buckets, which are the envelope the renderer drew rather than the acquisition. A
|
||||||
|
signal on several rows is one column; the columns come out in channel order, so the
|
||||||
|
file reads in the same order as the signal panel. The table has two time columns:
|
||||||
|
`time` is the axis as displayed (stream seconds live, `[-pre, +post]` while an event
|
||||||
|
is held) and `stream_time` is always the stream's own clock, so two exports taken in
|
||||||
|
different modes line up.
|
||||||
|
|
||||||
|
Two formats, picked by the extension the save dialog ends up with:
|
||||||
|
|
||||||
|
| file | what it is |
|
||||||
|
|---|---|
|
||||||
|
| `.npy` | one NumPy structured array, `time`/`stream_time` as `<f8` and every signal as `<f4` — the width the ring stores, so the file invents no precision and is about half an all-`f8` one. `pd.DataFrame(np.load(path))` is the whole reader. |
|
||||||
|
| `.csv` | the same table as text, `%.17g` for the times and `%.9g` for the values: the shortest widths that round-trip each type exactly. |
|
||||||
|
|
||||||
|
Parquet (the original ask) would have meant linking Arrow — tens of megabytes of
|
||||||
|
dependency for a feature whose output a three-line reader already handles.
|
||||||
|
|
||||||
|
The table is collected when the menu item is clicked, not when the dialog is
|
||||||
|
answered: a live view keeps scrolling while the file chooser is up. The live cut goes
|
||||||
|
through `ScopeModel::live_window_locked()`, the same index range `publish_event()`
|
||||||
|
uses, so an export and a capture of the same window hold the same samples.
|
||||||
|
|
||||||
## Review findings not (yet) implemented
|
## Review findings not (yet) implemented
|
||||||
|
|
||||||
These are known deviations, found by a spec/code review, that are deliberate for
|
These are known deviations, found by a spec/code review, that are deliberate for
|
||||||
|
|||||||
+152
@@ -16,6 +16,7 @@
|
|||||||
|
|
||||||
#include "acquisition.hpp"
|
#include "acquisition.hpp"
|
||||||
#include "config.hpp"
|
#include "config.hpp"
|
||||||
|
#include "export.hpp"
|
||||||
#include "model.hpp"
|
#include "model.hpp"
|
||||||
#include "plot_host.hpp"
|
#include "plot_host.hpp"
|
||||||
|
|
||||||
@@ -26,6 +27,8 @@
|
|||||||
#include <cstdio>
|
#include <cstdio>
|
||||||
#include <cstdlib>
|
#include <cstdlib>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
|
#include <ctime>
|
||||||
|
#include <memory>
|
||||||
#include <mutex>
|
#include <mutex>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
@@ -255,6 +258,14 @@ std::string format_window(double s) {
|
|||||||
return buf;
|
return buf;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A short-lived line in front of the status text: the outcome of an action that the
|
||||||
|
/// picture does not show by itself (a drop that changed nothing, an export that wrote a
|
||||||
|
/// file). `tick()` clears it when it expires.
|
||||||
|
void set_notice(App& app, std::string what, int seconds = 4) {
|
||||||
|
app.notice = std::move(what);
|
||||||
|
app.notice_until_us = g_get_monotonic_time() + seconds * G_TIME_SPAN_SECOND;
|
||||||
|
}
|
||||||
|
|
||||||
// --- plot widgets -----------------------------------------------------------
|
// --- plot widgets -----------------------------------------------------------
|
||||||
|
|
||||||
void on_right_click(GtkGestureClick* gesture, int, double x, double y, gpointer user);
|
void on_right_click(GtkGestureClick* gesture, int, double x, double y, gpointer user);
|
||||||
@@ -864,6 +875,89 @@ void on_cursors_reset(GtkButton*, gpointer user) {
|
|||||||
app->rows->reset_cursors();
|
app->rows->reset_cursors();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- export -----------------------------------------------------------------
|
||||||
|
|
||||||
|
/// The table travels to the async callback. It is collected when the button is clicked,
|
||||||
|
/// not when the dialog is answered: a live view keeps scrolling while the file chooser
|
||||||
|
/// is up, so collecting late would write a window the user never saw.
|
||||||
|
struct ExportContext {
|
||||||
|
App* app = nullptr;
|
||||||
|
ExportTable table;
|
||||||
|
};
|
||||||
|
|
||||||
|
void on_export_response(GObject* source, GAsyncResult* result, gpointer user) {
|
||||||
|
std::unique_ptr<ExportContext> ctx(static_cast<ExportContext*>(user));
|
||||||
|
GError* err = nullptr;
|
||||||
|
GFile* file = gtk_file_dialog_save_finish(GTK_FILE_DIALOG(source), result, &err);
|
||||||
|
if (file == nullptr) {
|
||||||
|
// A cancelled dialog is not a failure and says nothing; anything else does.
|
||||||
|
if (err != nullptr && !g_error_matches(err, GTK_DIALOG_ERROR, GTK_DIALOG_ERROR_DISMISSED)) {
|
||||||
|
set_notice(*ctx->app, std::string("export failed: ") + err->message);
|
||||||
|
}
|
||||||
|
if (err != nullptr) g_error_free(err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
char* raw = g_file_get_path(file);
|
||||||
|
const std::string path = raw != nullptr ? raw : std::string();
|
||||||
|
g_free(raw);
|
||||||
|
g_object_unref(file);
|
||||||
|
if (path.empty()) {
|
||||||
|
set_notice(*ctx->app, "export failed: that location is not a local file");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The extension picks the writer, so the filter the user chose is honoured by way of
|
||||||
|
// the name it put in the entry rather than by a second piece of state.
|
||||||
|
const bool csv = path.size() >= 4 && path.compare(path.size() - 4, 4, ".csv") == 0;
|
||||||
|
std::string error;
|
||||||
|
const bool ok = csv ? write_csv(path, ctx->table, error) : write_npy(path, ctx->table, error);
|
||||||
|
if (!ok) {
|
||||||
|
set_notice(*ctx->app, "export failed: " + error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
set_notice(*ctx->app, "exported " + std::to_string(ctx->table.rows()) + " samples x " +
|
||||||
|
std::to_string(ctx->table.columns.size()) + " signals to " + path);
|
||||||
|
}
|
||||||
|
|
||||||
|
void on_export_clicked(GtkButton*, gpointer user) {
|
||||||
|
App* app = static_cast<App*>(user);
|
||||||
|
auto ctx = std::make_unique<ExportContext>();
|
||||||
|
ctx->app = app;
|
||||||
|
ctx->table = collect_export(*app->model, *app->rows);
|
||||||
|
if (ctx->table.empty()) {
|
||||||
|
set_notice(*app, "nothing to export: no signal is plotted in the visible range");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
GtkFileDialog* dialog = gtk_file_dialog_new();
|
||||||
|
gtk_file_dialog_set_title(dialog, "Export visible range");
|
||||||
|
char stamp[32];
|
||||||
|
const std::time_t now = std::time(nullptr);
|
||||||
|
std::tm tm{};
|
||||||
|
localtime_r(&now, &tm);
|
||||||
|
std::strftime(stamp, sizeof(stamp), "%Y%m%d-%H%M%S", &tm);
|
||||||
|
gtk_file_dialog_set_initial_name(dialog, (std::string("udpscope-") + stamp + ".npy").c_str());
|
||||||
|
|
||||||
|
GListStore* filters = g_list_store_new(GTK_TYPE_FILE_FILTER);
|
||||||
|
GtkFileFilter* npy = gtk_file_filter_new();
|
||||||
|
gtk_file_filter_set_name(npy, "NumPy array (*.npy)");
|
||||||
|
gtk_file_filter_add_pattern(npy, "*.npy");
|
||||||
|
g_list_store_append(filters, npy);
|
||||||
|
GtkFileFilter* csv = gtk_file_filter_new();
|
||||||
|
gtk_file_filter_set_name(csv, "CSV (*.csv)");
|
||||||
|
gtk_file_filter_add_pattern(csv, "*.csv");
|
||||||
|
g_list_store_append(filters, csv);
|
||||||
|
gtk_file_dialog_set_filters(dialog, G_LIST_MODEL(filters));
|
||||||
|
gtk_file_dialog_set_default_filter(dialog, npy);
|
||||||
|
g_object_unref(npy);
|
||||||
|
g_object_unref(csv);
|
||||||
|
g_object_unref(filters);
|
||||||
|
|
||||||
|
gtk_file_dialog_save(dialog, GTK_WINDOW(app->window), nullptr, on_export_response,
|
||||||
|
ctx.release());
|
||||||
|
g_object_unref(dialog);
|
||||||
|
}
|
||||||
|
|
||||||
// --- right-click menu -------------------------------------------------------
|
// --- right-click menu -------------------------------------------------------
|
||||||
|
|
||||||
void on_y_auto_clicked(GtkButton* button, gpointer user) {
|
void on_y_auto_clicked(GtkButton* button, gpointer user) {
|
||||||
@@ -1570,6 +1664,15 @@ GtkWidget* build_toolbar(App& app) {
|
|||||||
|
|
||||||
gtk_box_append(GTK_BOX(menu_box), gtk_separator_new(GTK_ORIENTATION_HORIZONTAL));
|
gtk_box_append(GTK_BOX(menu_box), gtk_separator_new(GTK_ORIENTATION_HORIZONTAL));
|
||||||
|
|
||||||
|
GtkWidget* export_btn = gtk_button_new_with_label("Export visible range...");
|
||||||
|
gtk_widget_set_tooltip_text(export_btn,
|
||||||
|
"write every plotted signal over the visible x range, at the "
|
||||||
|
"stored sample rate, to a .npy or .csv file");
|
||||||
|
g_signal_connect(export_btn, "clicked", G_CALLBACK(on_export_clicked), &app);
|
||||||
|
gtk_box_append(GTK_BOX(menu_box), export_btn);
|
||||||
|
|
||||||
|
gtk_box_append(GTK_BOX(menu_box), gtk_separator_new(GTK_ORIENTATION_HORIZONTAL));
|
||||||
|
|
||||||
app.signal_toggle = gtk_toggle_button_new_with_label("Signal list");
|
app.signal_toggle = gtk_toggle_button_new_with_label("Signal list");
|
||||||
gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(app.signal_toggle), true);
|
gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(app.signal_toggle), true);
|
||||||
g_signal_connect(app.signal_toggle, "toggled", G_CALLBACK(on_signals_toggled), &app);
|
g_signal_connect(app.signal_toggle, "toggled", G_CALLBACK(on_signals_toggled), &app);
|
||||||
@@ -2007,6 +2110,52 @@ gboolean pause_probe(gpointer user) {
|
|||||||
return G_SOURCE_REMOVE;
|
return G_SOURCE_REMOVE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Headless self-check for the export: collect the same table the menu item would and
|
||||||
|
/// write it both ways, skipping only the file chooser. Two numbers are the assertion --
|
||||||
|
/// `time=` must equal `axis=` (the export is the *visible* range, not the whole ring)
|
||||||
|
/// and `rows=` must be that span times the stream's real rate, so a table cut from the
|
||||||
|
/// LOD pyramid or from a stale window shows up as a count far short of it.
|
||||||
|
gboolean export_probe(gpointer user) {
|
||||||
|
App* a = static_cast<App*>(user);
|
||||||
|
const char* env = g_getenv("UDPSCOPE_EXPORT_PATH");
|
||||||
|
const std::string prefix = env != nullptr ? env : "/tmp/udpscope-export";
|
||||||
|
|
||||||
|
const RowSet::Row& r0 = a->rows->row(0);
|
||||||
|
const plot::Range axis = r0.plot != nullptr ? r0.plot->x_range(r0.axis_x) : plot::Range{0.0, 0.0};
|
||||||
|
const ExportTable table = collect_export(*a->model, *a->rows);
|
||||||
|
std::fprintf(stderr,
|
||||||
|
"[export-probe] mode=%s axis=[%.6f..%.6f] rows=%zu cols=%zu time=[%.6f..%.6f] "
|
||||||
|
"stream=[%.6f..%.6f]\n",
|
||||||
|
a->model->trigger_enabled() && a->model->event() != nullptr ? "event" : "live",
|
||||||
|
axis.min, axis.max, table.rows(), table.columns.size(),
|
||||||
|
table.rows() != 0 ? table.time.front() : 0.0,
|
||||||
|
table.rows() != 0 ? table.time.back() : 0.0,
|
||||||
|
table.rows() != 0 ? table.stream_time.front() : 0.0,
|
||||||
|
table.rows() != 0 ? table.stream_time.back() : 0.0);
|
||||||
|
for (const ExportTable::Column& col : table.columns) {
|
||||||
|
std::fprintf(stderr, "[export-probe] column '%s' n=%zu\n", col.name.c_str(), col.values.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const char* ext : {".npy", ".csv"}) {
|
||||||
|
const std::string path = prefix + ext;
|
||||||
|
std::string error;
|
||||||
|
const bool ok = std::strcmp(ext, ".csv") == 0 ? write_csv(path, table, error)
|
||||||
|
: write_npy(path, table, error);
|
||||||
|
long bytes = -1;
|
||||||
|
if (ok) {
|
||||||
|
if (std::FILE* f = std::fopen(path.c_str(), "rb"); f != nullptr) {
|
||||||
|
std::fseek(f, 0, SEEK_END);
|
||||||
|
bytes = std::ftell(f);
|
||||||
|
std::fclose(f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
std::fprintf(stderr, "[export-probe] %s %s bytes=%ld%s%s\n", path.c_str(),
|
||||||
|
ok ? "written" : "FAILED", bytes, ok ? "" : " error=", ok ? "" : error.c_str());
|
||||||
|
}
|
||||||
|
std::fflush(stderr);
|
||||||
|
return G_SOURCE_REMOVE;
|
||||||
|
}
|
||||||
|
|
||||||
gboolean exit_probe_step1(gpointer user) {
|
gboolean exit_probe_step1(gpointer user) {
|
||||||
App* a = static_cast<App*>(user);
|
App* a = static_cast<App*>(user);
|
||||||
// Record the panel states before the toggles are exercised.
|
// Record the panel states before the toggles are exercised.
|
||||||
@@ -2055,6 +2204,9 @@ int run_ui(int argc, char** argv, ScopeModel& model, Acquisition& acq, RowSet& r
|
|||||||
if (const char* env = g_getenv("UDPSCOPE_PROBE_PAUSE_MS")) {
|
if (const char* env = g_getenv("UDPSCOPE_PROBE_PAUSE_MS")) {
|
||||||
g_timeout_add(static_cast<guint>(std::atoi(env)), pause_probe, &app);
|
g_timeout_add(static_cast<guint>(std::atoi(env)), pause_probe, &app);
|
||||||
}
|
}
|
||||||
|
if (const char* env = g_getenv("UDPSCOPE_EXPORT_MS")) {
|
||||||
|
g_timeout_add(static_cast<guint>(std::atoi(env)), export_probe, &app);
|
||||||
|
}
|
||||||
|
|
||||||
// Our own options were consumed by parse_args(); GTK would reject them, so only
|
// Our own options were consumed by parse_args(); GTK would reject them, so only
|
||||||
// the program name is handed to g_application_run().
|
// the program name is handed to g_application_run().
|
||||||
|
|||||||
Reference in new issue
Block a user