e1608: a configurable generator update rate, and the achieved rate in the Analog out panel
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
0c6e69957d
commit
0d8c583bbb
12 files changed
+181
-11
No files matched your search
@@ -77,7 +77,7 @@ bool on_of(GtkWidget* w) { return adw_switch_row_get_active(ADW_SWITCH_ROW(w));
|
||||
/// closure shares; the raw widget pointers are valid while the chooser keeps the rows.
|
||||
struct Form {
|
||||
GtkWidget *address, *mac, *subnet, *port, *code, *timeout;
|
||||
GtkWidget *rate, *ext_clock, *trigger, *auto_restart, *calibrate, *poll_rate;
|
||||
GtkWidget *rate, *ext_clock, *trigger, *auto_restart, *calibrate, *poll_rate, *gen_rate;
|
||||
GtkWidget *mode, *ai[8], *ai_group;
|
||||
GtkWidget *dir[8], *do_init, *plot_counter, *ao_init[2];
|
||||
ChangeCb changed = std::make_shared<std::function<void()>>();
|
||||
@@ -121,6 +121,7 @@ struct Form {
|
||||
c.set("auto_restart", on_of(auto_restart) ? "1" : "0");
|
||||
c.set("calibrate", on_of(calibrate) ? "1" : "0");
|
||||
c.set("poll_rate", num(value_of(poll_rate)));
|
||||
c.set("gen_rate", num(value_of(gen_rate)));
|
||||
c.set("mode", diff() ? "diff" : "se");
|
||||
static const char* const kRange[] = {"", "BIP10V", "BIP5V", "BIP2V", "BIP1V"};
|
||||
for (int i = 0; i < 8; ++i) {
|
||||
@@ -156,6 +157,7 @@ struct Form {
|
||||
adw_switch_row_set_active(ADW_SWITCH_ROW(auto_restart), s.auto_restart);
|
||||
adw_switch_row_set_active(ADW_SWITCH_ROW(calibrate), s.calibrate);
|
||||
adw_spin_row_set_value(ADW_SPIN_ROW(poll_rate), s.poll_rate);
|
||||
adw_spin_row_set_value(ADW_SPIN_ROW(gen_rate), s.gen_rate);
|
||||
adw_combo_row_set_selected(ADW_COMBO_ROW(mode), s.diff ? 1 : 0);
|
||||
for (int i = 0; i < 8; ++i) {
|
||||
adw_combo_row_set_selected(
|
||||
@@ -356,6 +358,7 @@ class E1608Kind final : public SourceKind {
|
||||
f->auto_restart = switch_row("_Restart after an overrun", true);
|
||||
f->calibrate = switch_row("Apply _factory calibration", true);
|
||||
f->poll_rate = spin_row("_Input poll rate (Hz)", 0.1, 100, 1, 10, 1);
|
||||
f->gen_rate = spin_row("_Generator update rate (Hz)", 1, 200, 1, 50, 0);
|
||||
f->mode = combo_row("Input _mode", kMode);
|
||||
for (int i = 0; i < 8; ++i) {
|
||||
const std::string title = "Channel " + std::to_string(i);
|
||||
@@ -384,7 +387,7 @@ class E1608Kind final : public SourceKind {
|
||||
GtkWidget* conn = expander("Connection", {f->scan_row, f->address, f->mac, f->subnet, f->port,
|
||||
f->code, f->timeout});
|
||||
GtkWidget* acq = expander("Acquisition", {f->rate, f->ext_clock, f->trigger, f->auto_restart,
|
||||
f->calibrate, f->poll_rate});
|
||||
f->calibrate, f->poll_rate, f->gen_rate});
|
||||
GtkWidget* ai = expander("Analog inputs", {f->mode, f->ai[0], f->ai[1], f->ai[2], f->ai[3],
|
||||
f->ai[4], f->ai[5], f->ai[6], f->ai[7]});
|
||||
GtkWidget* dio = expander("Digital I/O", {f->dir[0], f->dir[1], f->dir[2], f->dir[3], f->dir[4],
|
||||
@@ -395,7 +398,7 @@ class E1608Kind final : public SourceKind {
|
||||
|
||||
for (GtkWidget* w : {conn, acq, ai, dio}) hook_destroy(w, f);
|
||||
for (GtkWidget* w : {f->address, f->mac, f->subnet}) hook_entry(w, f);
|
||||
for (GtkWidget* w : {f->port, f->code, f->timeout, f->rate, f->poll_rate, f->do_init,
|
||||
for (GtkWidget* w : {f->port, f->code, f->timeout, f->rate, f->poll_rate, f->gen_rate, f->do_init,
|
||||
f->ao_init[0], f->ao_init[1]})
|
||||
hook_notify(w, "notify::value", f);
|
||||
for (GtkWidget* w : {f->ext_clock, f->auto_restart, f->calibrate, f->plot_counter})
|
||||
|
||||
@@ -55,7 +55,7 @@ void set_text(GtkWidget* label, const char* text) {
|
||||
if (std::strcmp(gtk_label_get_text(GTK_LABEL(label)), text) != 0) gtk_label_set_text(GTK_LABEL(label), text);
|
||||
}
|
||||
|
||||
GtkWidget* panel_root(GtkWidget* a, GtkWidget* b = nullptr) {
|
||||
GtkWidget* panel_root(GtkWidget* a, GtkWidget* b = nullptr, GtkWidget* c = nullptr) {
|
||||
GtkWidget* box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 18);
|
||||
gtk_widget_set_margin_top(box, 12);
|
||||
gtk_widget_set_margin_bottom(box, 12);
|
||||
@@ -63,6 +63,7 @@ GtkWidget* panel_root(GtkWidget* a, GtkWidget* b = nullptr) {
|
||||
gtk_widget_set_margin_end(box, 12);
|
||||
gtk_box_append(GTK_BOX(box), a);
|
||||
if (b != nullptr) gtk_box_append(GTK_BOX(box), b);
|
||||
if (c != nullptr) gtk_box_append(GTK_BOX(box), c);
|
||||
return box;
|
||||
}
|
||||
|
||||
@@ -77,6 +78,7 @@ struct AoCtx {
|
||||
E1608Source* src = nullptr;
|
||||
std::shared_ptr<E1608Readouts> ro;
|
||||
AoWidgets w[2];
|
||||
GtkWidget* gen_rate = nullptr; ///< the "Update rate" row: the achieved rate against the requested one
|
||||
Hook hooks[2];
|
||||
bool updating = false; ///< set while the panel writes its own widgets: those writes post nothing
|
||||
unsigned epoch_seen = 0;
|
||||
@@ -148,6 +150,22 @@ void ao_refresh(AoCtx* c) {
|
||||
if (cur == nullptr || std::strcmp(cur, b) != 0)
|
||||
adw_action_row_set_subtitle(ADW_ACTION_ROW(c->w[ch].last), b);
|
||||
}
|
||||
|
||||
// The achieved generator rate: ticks completed per second, measured by the producer thread. Below
|
||||
// the requested rate, the board's command latency is what limits it.
|
||||
char r[64];
|
||||
const double req = c->src->settings().gen_rate;
|
||||
const bool running = c->ro->gen_on[0].load() || c->ro->gen_on[1].load();
|
||||
const double hz = c->ro->gen_hz.load();
|
||||
if (!running)
|
||||
std::snprintf(r, sizeof r, "off (requested %.0f Hz)", req);
|
||||
else if (hz <= 0.0)
|
||||
std::snprintf(r, sizeof r, "measuring... (requested %.0f Hz)", req);
|
||||
else
|
||||
std::snprintf(r, sizeof r, "%.0f Hz (requested %.0f Hz)", hz, req);
|
||||
const char* cur = adw_action_row_get_subtitle(ADW_ACTION_ROW(c->gen_rate));
|
||||
if (cur == nullptr || std::strcmp(cur, r) != 0)
|
||||
adw_action_row_set_subtitle(ADW_ACTION_ROW(c->gen_rate), r);
|
||||
}
|
||||
|
||||
gboolean ao_tick(GtkWidget*, GdkFrameClock*, gpointer user) {
|
||||
@@ -198,7 +216,15 @@ GtkWidget* build_ao(E1608Source* src) {
|
||||
ao_sensitivity(w);
|
||||
}
|
||||
|
||||
GtkWidget* root = panel_root(groups[0], groups[1]);
|
||||
c->gen_rate = adw_action_row_new();
|
||||
gtk_widget_set_name(c->gen_rate, "e1608-gen-rate");
|
||||
adw_preferences_row_set_title(ADW_PREFERENCES_ROW(c->gen_rate), "Update rate");
|
||||
adw_action_row_set_subtitle(ADW_ACTION_ROW(c->gen_rate), "off");
|
||||
GtkWidget* gen_group = adw_preferences_group_new();
|
||||
adw_preferences_group_set_title(ADW_PREFERENCES_GROUP(gen_group), "Generator");
|
||||
adw_preferences_group_add(ADW_PREFERENCES_GROUP(gen_group), c->gen_rate);
|
||||
|
||||
GtkWidget* root = panel_root(groups[0], groups[1], gen_group);
|
||||
g_object_set_data_full(G_OBJECT(root), "e1608-ao-ctx", c,
|
||||
+[](gpointer p) { delete static_cast<AoCtx*>(p); });
|
||||
gtk_widget_add_tick_callback(root, ao_tick, c, nullptr);
|
||||
|
||||
@@ -61,6 +61,7 @@ E1608Settings E1608Settings::from(const SourceConfig& c) {
|
||||
s.auto_restart = c.get_bool("auto_restart", true);
|
||||
s.calibrate = c.get_bool("calibrate", true);
|
||||
s.poll_rate = c.get_double("poll_rate", 10.0);
|
||||
s.gen_rate = c.get_double("gen_rate", 50.0);
|
||||
s.diff = lower(c.get("mode", "se")) == "diff";
|
||||
|
||||
bool any_key = false;
|
||||
@@ -133,6 +134,7 @@ std::string E1608Settings::problem() const {
|
||||
}
|
||||
if (aggregate() < kMinAggregateHz) return "The aggregate rate must be at least 0.02 S/s";
|
||||
if (!(poll_rate >= 0.1 && poll_rate <= 100.0)) return "The input poll rate must be 0.1 to 100 Hz";
|
||||
if (!(gen_rate >= 1.0 && gen_rate <= 200.0)) return "The generator update rate must be 1 to 200 Hz";
|
||||
return {};
|
||||
}
|
||||
|
||||
@@ -230,6 +232,7 @@ void E1608Source::fail(const std::string& msg, bool fatal) {
|
||||
any_gen_ = false;
|
||||
readouts_->gen_on[0].store(false);
|
||||
readouts_->gen_on[1].store(false);
|
||||
gen_rate_reset();
|
||||
if (fatal) {
|
||||
state_ = State::Halted;
|
||||
} else {
|
||||
@@ -327,7 +330,7 @@ void E1608Source::connect_and_start() {
|
||||
const auto now = Clock::now();
|
||||
next_slow_ = now;
|
||||
next_status_ = now + std::chrono::milliseconds(500);
|
||||
next_gen_ = now + std::chrono::milliseconds(20);
|
||||
next_gen_ = now + gen_period();
|
||||
slow_read();
|
||||
|
||||
state_ = State::Scanning;
|
||||
@@ -409,7 +412,7 @@ void E1608Source::run_timers() {
|
||||
check_status();
|
||||
}
|
||||
if (any_gen_ && now >= next_gen_) {
|
||||
next_gen_ = now + std::chrono::milliseconds(20);
|
||||
next_gen_ = now + gen_period(); // from now, not from the old deadline: a slow board gets no catch-up burst
|
||||
generator_tick(now);
|
||||
}
|
||||
}
|
||||
@@ -475,22 +478,46 @@ void E1608Source::write_ao(int ch, double volts) {
|
||||
readouts_->ao[ch].store(dev_.aout(ch, volts, cal_));
|
||||
}
|
||||
|
||||
E1608Source::Clock::duration E1608Source::gen_period() const {
|
||||
return std::chrono::duration_cast<Clock::duration>(std::chrono::duration<double>(1.0 / cfg_.gen_rate));
|
||||
}
|
||||
|
||||
void E1608Source::gen_rate_reset() {
|
||||
gen_win_ticks_ = 0;
|
||||
gen_win_t0_ = Clock::now();
|
||||
readouts_->gen_hz.store(0.0);
|
||||
}
|
||||
|
||||
void E1608Source::update_gen_flags() {
|
||||
const bool was_gen = any_gen_;
|
||||
any_gen_ = false;
|
||||
for (int ch = 0; ch < 2; ++ch) {
|
||||
const bool on = ao_[ch].mode != AoMode::Level && ao_[ch].run;
|
||||
readouts_->gen_on[ch].store(on);
|
||||
any_gen_ = any_gen_ || on;
|
||||
}
|
||||
if (any_gen_ != was_gen) gen_rate_reset(); // a generator started or the last one stopped
|
||||
}
|
||||
|
||||
void E1608Source::generator_tick(Clock::time_point now) {
|
||||
bool wrote = false;
|
||||
for (int ch = 0; ch < 2; ++ch) {
|
||||
const AoSettings& s = ao_[ch];
|
||||
if (s.mode == AoMode::Level || !s.run) continue;
|
||||
const double t = std::chrono::duration<double>(now - gen_t0_[ch]).count();
|
||||
const double phase = std::fmod(t * s.freq_hz, 1.0);
|
||||
write_ao(ch, clamp(s.offset + s.amplitude * wave(s.mode, phase), -10.0, 10.0));
|
||||
wrote = true;
|
||||
}
|
||||
if (!wrote) return;
|
||||
// The achieved rate: ticks that wrote, per second of wall time, published once a window closes.
|
||||
// The DAC writes are synchronous, so a slow board shows up here as a rate below the requested one.
|
||||
++gen_win_ticks_;
|
||||
const double elapsed = std::chrono::duration<double>(Clock::now() - gen_win_t0_).count();
|
||||
if (elapsed >= 1.0) {
|
||||
readouts_->gen_hz.store(gen_win_ticks_ / elapsed);
|
||||
gen_win_ticks_ = 0;
|
||||
gen_win_t0_ = Clock::now();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -36,6 +36,7 @@ struct E1608Settings {
|
||||
bool auto_restart = true;
|
||||
bool calibrate = true;
|
||||
double poll_rate = 10.0; ///< Hz, for the digital inputs and the counter
|
||||
double gen_rate = 50.0; ///< Hz, the waveform generator's DAC update rate
|
||||
bool diff = false;
|
||||
bool ai_on[8] = {};
|
||||
Range ai_range[8] = {};
|
||||
@@ -62,6 +63,7 @@ struct E1608Readouts {
|
||||
std::atomic<std::uint32_t> counter{0};
|
||||
std::atomic<double> ao[2]; ///< the last DAC voltage written; NaN before any
|
||||
std::atomic<bool> gen_on[2];
|
||||
std::atomic<double> gen_hz{0.0}; ///< generator ticks completed per second, over ~1 s; 0 when off
|
||||
std::atomic<unsigned> epoch{0}; ///< bumped at each (re)connect: panels re-sync from it
|
||||
E1608Readouts() {
|
||||
for (int i = 0; i < 2; ++i) {
|
||||
@@ -117,6 +119,8 @@ class E1608Source final : public DataSource {
|
||||
void generator_tick(Clock::time_point now);
|
||||
void write_ao(int ch, double volts);
|
||||
void update_gen_flags();
|
||||
void gen_rate_reset();
|
||||
Clock::duration gen_period() const;
|
||||
void fail(const std::string& msg, bool fatal);
|
||||
void note(const std::string& msg);
|
||||
int wait_ms(int timeout_ms, Clock::time_point now) const;
|
||||
@@ -153,6 +157,8 @@ class E1608Source final : public DataSource {
|
||||
AoSettings ao_[2];
|
||||
Clock::time_point gen_t0_[2];
|
||||
bool any_gen_ = false;
|
||||
Clock::time_point gen_win_t0_; ///< start of the current rate-measurement window
|
||||
unsigned gen_win_ticks_ = 0; ///< ticks that wrote since `gen_win_t0_`
|
||||
|
||||
Clock::time_point retry_at_;
|
||||
Clock::time_point next_slow_, next_status_, next_gen_;
|
||||
|
||||
@@ -6034,6 +6034,9 @@ int run_ui(int argc, char** argv, const Config& cfg, ScopeModel& model, Acquisit
|
||||
if (const char* env = g_getenv("UDPSCOPE_PROBE_E1608")) {
|
||||
g_timeout_add(static_cast<guint>(std::atoi(env)), e1608_probe, &app);
|
||||
}
|
||||
if (const char* env = g_getenv("UDPSCOPE_PROBE_E1608_GEN")) {
|
||||
g_timeout_add(static_cast<guint>(std::atoi(env)), e1608_gen_probe, &app);
|
||||
}
|
||||
if (const char* env = g_getenv("UDPSCOPE_PROBE_E1608_RECONNECT")) {
|
||||
g_timeout_add(static_cast<guint>(std::atoi(env)), e1608_probe_reconnect, &app);
|
||||
}
|
||||
|
||||
@@ -1881,6 +1881,12 @@ gboolean e1608_probe(gpointer user);
|
||||
gboolean e1608_probe_report(gpointer user);
|
||||
gboolean e1608_probe_reconnect(gpointer user);
|
||||
gboolean e1608_probe_resumed(gpointer user);
|
||||
/// The generator's update rate: starts a Sine on DAC 0 through the panel, reads the achieved rate the
|
||||
/// source measured and the Analog out panel's "Update rate" row (`_report`, 2.5 s later), then stops
|
||||
/// it and checks both read "off" (`_stopped`).
|
||||
gboolean e1608_gen_probe(gpointer user);
|
||||
gboolean e1608_gen_probe_report(gpointer user);
|
||||
gboolean e1608_gen_probe_stopped(gpointer user);
|
||||
/// The E-1608 form's "Scan network" row: presses Scan against the mock, then (phases `_fill`,
|
||||
/// `_rescan`) clicks a result to fill the address, "Use MAC" to fill the MAC, scans again and checks
|
||||
/// the results were replaced, not added to.
|
||||
|
||||
@@ -1923,6 +1923,7 @@ gboolean source_kinds_probe(gpointer user) {
|
||||
c.set("trigger", "rising");
|
||||
c.set("dir1", "o");
|
||||
c.set("plot0", "1");
|
||||
c.set("gen_rate", "120");
|
||||
} else if (id == "flatudp") {
|
||||
c.set("port", "44600");
|
||||
c.set("bind", "10.0.0.8");
|
||||
@@ -2428,6 +2429,76 @@ gboolean e1608_probe(gpointer user) {
|
||||
return G_SOURCE_REMOVE;
|
||||
}
|
||||
|
||||
/// Phase 3 of the generator probe: the generator switched off 400 ms ago; the achieved rate and the
|
||||
/// panel must both have gone back to "off".
|
||||
gboolean e1608_gen_probe_stopped(gpointer user) {
|
||||
App* a = static_cast<App*>(user);
|
||||
if (a->window == nullptr) return G_SOURCE_REMOVE;
|
||||
e1608::E1608Source* src = e1608_source(a);
|
||||
if (src == nullptr) return G_SOURCE_REMOVE;
|
||||
GtkWidget* row = find_named(source_panel_content(a, "source-ao"), "e1608-gen-rate");
|
||||
const char* sub = row != nullptr ? adw_action_row_get_subtitle(ADW_ACTION_ROW(row)) : nullptr;
|
||||
const double hz = src->readouts()->gen_hz.load();
|
||||
const bool off = sub != nullptr && std::strncmp(sub, "off", 3) == 0;
|
||||
std::fprintf(stderr, "[e1608] generator stopped: achieved=%.1f panel='%s' ok=%d\n", hz,
|
||||
sub != nullptr ? sub : "", hz == 0.0 && off ? 1 : 0);
|
||||
std::fflush(stderr);
|
||||
return G_SOURCE_REMOVE;
|
||||
}
|
||||
|
||||
/// Phase 2 of the generator probe, 2.5 s after a Sine generator was started on DAC 0: the achieved
|
||||
/// update rate the source measured, and the text the Analog out panel shows for it.
|
||||
gboolean e1608_gen_probe_report(gpointer user) {
|
||||
App* a = static_cast<App*>(user);
|
||||
if (a->window == nullptr) return G_SOURCE_REMOVE;
|
||||
e1608::E1608Source* src = e1608_source(a);
|
||||
if (src == nullptr) return G_SOURCE_REMOVE;
|
||||
GtkWidget* ao = source_panel_content(a, "source-ao");
|
||||
GtkWidget* row = find_named(ao, "e1608-gen-rate");
|
||||
const char* sub = row != nullptr ? adw_action_row_get_subtitle(ADW_ACTION_ROW(row)) : nullptr;
|
||||
const double req = src->settings().gen_rate;
|
||||
const double hz = src->readouts()->gen_hz.load();
|
||||
// The mock answers at once, so the achieved rate sits at the requested one; the band is loose.
|
||||
const bool in_range = hz >= 0.7 * req && hz <= 1.1 * req;
|
||||
char want[32];
|
||||
std::snprintf(want, sizeof want, "(requested %.0f Hz)", req);
|
||||
const bool shown = sub != nullptr && std::strstr(sub, want) != nullptr && std::strstr(sub, "Hz") != nullptr;
|
||||
std::fprintf(stderr, "[e1608] generator: requested=%.1f achieved=%.1f panel='%s' ok=%d\n", req, hz,
|
||||
sub != nullptr ? sub : "", in_range && shown ? 1 : 0);
|
||||
std::fflush(stderr);
|
||||
GtkWidget* run = find_named(ao, "e1608-ao0-run");
|
||||
if (run != nullptr) adw_switch_row_set_active(ADW_SWITCH_ROW(run), FALSE);
|
||||
g_timeout_add(400, e1608_gen_probe_stopped, a);
|
||||
return G_SOURCE_REMOVE;
|
||||
}
|
||||
|
||||
/// Starts a Sine generator on DAC 0 through the panel's own controls, then reads it 2.5 s later.
|
||||
gboolean e1608_gen_probe(gpointer user) {
|
||||
App* a = static_cast<App*>(user);
|
||||
if (a->window == nullptr) return G_SOURCE_REMOVE;
|
||||
e1608::E1608Source* src = e1608_source(a);
|
||||
if (src == nullptr) {
|
||||
std::fprintf(stderr, "[e1608] generator: no e1608 source (run with --source e1608)\n");
|
||||
std::fflush(stderr);
|
||||
return G_SOURCE_REMOVE;
|
||||
}
|
||||
GtkWidget* ao = source_panel_content(a, "source-ao");
|
||||
GtkWidget* mode = find_named(ao, "e1608-ao0-mode");
|
||||
GtkWidget* run = find_named(ao, "e1608-ao0-run");
|
||||
if (mode == nullptr || run == nullptr) {
|
||||
std::fprintf(stderr, "[e1608] generator: controls missing (mode=%d run=%d)\n", mode != nullptr ? 1 : 0,
|
||||
run != nullptr ? 1 : 0);
|
||||
std::fflush(stderr);
|
||||
return G_SOURCE_REMOVE;
|
||||
}
|
||||
// The panel refreshes on its frame clock, which only ticks while the page is on screen.
|
||||
if (a->side_stack != nullptr) adw_view_stack_set_visible_child_name(ADW_VIEW_STACK(a->side_stack), "source-ao");
|
||||
adw_combo_row_set_selected(ADW_COMBO_ROW(mode), 1); // Sine
|
||||
adw_switch_row_set_active(ADW_SWITCH_ROW(run), TRUE);
|
||||
g_timeout_add(2500, e1608_gen_probe_report, a);
|
||||
return G_SOURCE_REMOVE;
|
||||
}
|
||||
|
||||
/// After the mock dropped the link: has the source reconnected, and does the stream move again?
|
||||
gboolean e1608_probe_resumed(gpointer user) {
|
||||
App* a = static_cast<App*>(user);
|
||||
|
||||
Reference in new issue
Block a user