e1608: the Analog out and Digital I/O panels
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
bacc960776
commit
407051959a
4 files changed
+367
No files matched your search
@@ -246,6 +246,7 @@ add_executable(udpscope
|
|||||||
src/source/e1608/e1608_device.cpp
|
src/source/e1608/e1608_device.cpp
|
||||||
src/source/e1608/e1608_source.cpp
|
src/source/e1608/e1608_source.cpp
|
||||||
src/source/e1608/e1608_kind.cpp
|
src/source/e1608/e1608_kind.cpp
|
||||||
|
src/source/e1608/e1608_panels.cpp
|
||||||
src/plot_host.cpp
|
src/plot_host.cpp
|
||||||
src/export.cpp
|
src/export.cpp
|
||||||
src/search.cpp
|
src/search.cpp
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
#include "source/e1608/e1608_source.hpp"
|
#include "source/e1608/e1608_source.hpp"
|
||||||
#include "source/registry.hpp"
|
#include "source/registry.hpp"
|
||||||
|
#include "source/e1608/e1608_panels.hpp"
|
||||||
|
|
||||||
#include <adwaita.h>
|
#include <adwaita.h>
|
||||||
|
|
||||||
@@ -279,6 +280,10 @@ class E1608Kind final : public SourceKind {
|
|||||||
out.on_change = [cb](std::function<void()> fn) { *cb = std::move(fn); };
|
out.on_change = [cb](std::function<void()> fn) { *cb = std::move(fn); };
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::vector<SourcePanel> make_panels(DataSource& source) const override {
|
||||||
|
return make_e1608_panels(source);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|||||||
@@ -0,0 +1,345 @@
|
|||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
// SPDX-FileCopyrightText: 2026 Martino Ferrari
|
||||||
|
|
||||||
|
#include "source/e1608/e1608_panels.hpp"
|
||||||
|
|
||||||
|
#include "source/e1608/e1608_source.hpp"
|
||||||
|
|
||||||
|
#include <adwaita.h>
|
||||||
|
|
||||||
|
#include <cmath>
|
||||||
|
#include <cstdio>
|
||||||
|
#include <cstring>
|
||||||
|
#include <memory>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
namespace udpscope {
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
using namespace e1608;
|
||||||
|
|
||||||
|
constexpr gint64 kRefreshUs = 100000; // the panels redraw their readouts at 10 Hz
|
||||||
|
|
||||||
|
/// A signal closure's argument: the panel's context and the index (DAC or bit) it concerns.
|
||||||
|
struct Hook {
|
||||||
|
void* ctx = nullptr;
|
||||||
|
int idx = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
GtkWidget* spin(const std::string& name, const char* title, double lo, double hi, double step,
|
||||||
|
double def, guint digits) {
|
||||||
|
GtkWidget* r = GTK_WIDGET(adw_spin_row_new_with_range(lo, hi, step));
|
||||||
|
gtk_widget_set_name(r, name.c_str());
|
||||||
|
adw_preferences_row_set_title(ADW_PREFERENCES_ROW(r), title);
|
||||||
|
adw_spin_row_set_digits(ADW_SPIN_ROW(r), digits);
|
||||||
|
adw_spin_row_set_value(ADW_SPIN_ROW(r), def);
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
GtkWidget* switch_row(const std::string& name, const char* title, const char* subtitle) {
|
||||||
|
GtkWidget* r = adw_switch_row_new();
|
||||||
|
gtk_widget_set_name(r, name.c_str());
|
||||||
|
adw_preferences_row_set_title(ADW_PREFERENCES_ROW(r), title);
|
||||||
|
if (subtitle != nullptr) adw_action_row_set_subtitle(ADW_ACTION_ROW(r), subtitle);
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool due(gint64& last_us) {
|
||||||
|
const gint64 now = g_get_monotonic_time();
|
||||||
|
if (now - last_us < kRefreshUs) return false;
|
||||||
|
last_us = now;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
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* box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 18);
|
||||||
|
gtk_widget_set_margin_top(box, 12);
|
||||||
|
gtk_widget_set_margin_bottom(box, 12);
|
||||||
|
gtk_widget_set_margin_start(box, 12);
|
||||||
|
gtk_widget_set_margin_end(box, 12);
|
||||||
|
gtk_box_append(GTK_BOX(box), a);
|
||||||
|
if (b != nullptr) gtk_box_append(GTK_BOX(box), b);
|
||||||
|
return box;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Analog out -------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
struct AoWidgets {
|
||||||
|
GtkWidget *mode = nullptr, *level = nullptr, *amp = nullptr, *offset = nullptr, *freq = nullptr,
|
||||||
|
*run = nullptr, *last = nullptr;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct AoCtx {
|
||||||
|
E1608Source* src = nullptr;
|
||||||
|
std::shared_ptr<E1608Readouts> ro;
|
||||||
|
AoWidgets w[2];
|
||||||
|
Hook hooks[2];
|
||||||
|
bool updating = false; ///< set while the panel writes its own widgets: those writes post nothing
|
||||||
|
unsigned epoch_seen = 0;
|
||||||
|
gint64 last_us = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
void ao_sensitivity(const AoWidgets& w) {
|
||||||
|
const bool level = adw_combo_row_get_selected(ADW_COMBO_ROW(w.mode)) == 0;
|
||||||
|
gtk_widget_set_sensitive(w.level, level);
|
||||||
|
for (GtkWidget* x : {w.amp, w.offset, w.freq, w.run}) gtk_widget_set_sensitive(x, !level);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Back to what the source holds after a (re)connect.
|
||||||
|
void ao_reset(AoCtx* c) {
|
||||||
|
c->updating = true;
|
||||||
|
for (int ch = 0; ch < 2; ++ch) {
|
||||||
|
const AoWidgets& w = c->w[ch];
|
||||||
|
adw_combo_row_set_selected(ADW_COMBO_ROW(w.mode), 0);
|
||||||
|
adw_spin_row_set_value(ADW_SPIN_ROW(w.level), c->src->settings().ao_init[ch]);
|
||||||
|
adw_spin_row_set_value(ADW_SPIN_ROW(w.amp), 1.0);
|
||||||
|
adw_spin_row_set_value(ADW_SPIN_ROW(w.offset), 0.0);
|
||||||
|
adw_spin_row_set_value(ADW_SPIN_ROW(w.freq), 1.0);
|
||||||
|
adw_switch_row_set_active(ADW_SWITCH_ROW(w.run), FALSE);
|
||||||
|
ao_sensitivity(w);
|
||||||
|
}
|
||||||
|
c->updating = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ao_push(AoCtx* c, int ch) {
|
||||||
|
if (c->updating) return;
|
||||||
|
const AoWidgets& w = c->w[ch];
|
||||||
|
const guint mode = adw_combo_row_get_selected(ADW_COMBO_ROW(w.mode));
|
||||||
|
if (mode == 0 && adw_switch_row_get_active(ADW_SWITCH_ROW(w.run))) { // Level has no generator
|
||||||
|
c->updating = true;
|
||||||
|
adw_switch_row_set_active(ADW_SWITCH_ROW(w.run), FALSE);
|
||||||
|
c->updating = false;
|
||||||
|
}
|
||||||
|
ao_sensitivity(w);
|
||||||
|
AoSettings s;
|
||||||
|
s.mode = static_cast<AoMode>(mode);
|
||||||
|
s.level = adw_spin_row_get_value(ADW_SPIN_ROW(w.level));
|
||||||
|
s.amplitude = adw_spin_row_get_value(ADW_SPIN_ROW(w.amp));
|
||||||
|
s.offset = adw_spin_row_get_value(ADW_SPIN_ROW(w.offset));
|
||||||
|
s.freq_hz = adw_spin_row_get_value(ADW_SPIN_ROW(w.freq));
|
||||||
|
s.run = adw_switch_row_get_active(ADW_SWITCH_ROW(w.run));
|
||||||
|
E1608Source* src = c->src;
|
||||||
|
src->post([src, ch, s] { src->set_analog_out(ch, s); });
|
||||||
|
}
|
||||||
|
|
||||||
|
void on_ao_changed(GObject*, GParamSpec*, gpointer user) {
|
||||||
|
const Hook* h = static_cast<const Hook*>(user);
|
||||||
|
ao_push(static_cast<AoCtx*>(h->ctx), h->idx);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ao_refresh(AoCtx* c) {
|
||||||
|
const unsigned ep = c->ro->epoch.load();
|
||||||
|
if (ep != c->epoch_seen) {
|
||||||
|
c->epoch_seen = ep;
|
||||||
|
ao_reset(c);
|
||||||
|
}
|
||||||
|
for (int ch = 0; ch < 2; ++ch) {
|
||||||
|
const double v = c->ro->ao[ch].load();
|
||||||
|
char b[48];
|
||||||
|
if (std::isnan(v))
|
||||||
|
std::snprintf(b, sizeof b, "not written");
|
||||||
|
else
|
||||||
|
std::snprintf(b, sizeof b, "%+.4f V", v);
|
||||||
|
const char* cur = adw_action_row_get_subtitle(ADW_ACTION_ROW(c->w[ch].last));
|
||||||
|
if (cur == nullptr || std::strcmp(cur, b) != 0)
|
||||||
|
adw_action_row_set_subtitle(ADW_ACTION_ROW(c->w[ch].last), b);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
gboolean ao_tick(GtkWidget*, GdkFrameClock*, gpointer user) {
|
||||||
|
AoCtx* c = static_cast<AoCtx*>(user);
|
||||||
|
if (due(c->last_us)) ao_refresh(c);
|
||||||
|
return G_SOURCE_CONTINUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
GtkWidget* build_ao(E1608Source* src) {
|
||||||
|
auto* c = new AoCtx;
|
||||||
|
c->src = src;
|
||||||
|
c->ro = src->readouts();
|
||||||
|
|
||||||
|
GtkWidget* groups[2];
|
||||||
|
static const char* const kModes[] = {"Level", "Sine", "Square", "Triangle", "Ramp", nullptr};
|
||||||
|
for (int ch = 0; ch < 2; ++ch) {
|
||||||
|
const std::string p = "e1608-ao" + std::to_string(ch);
|
||||||
|
AoWidgets& w = c->w[ch];
|
||||||
|
w.mode = adw_combo_row_new();
|
||||||
|
gtk_widget_set_name(w.mode, (p + "-mode").c_str());
|
||||||
|
adw_preferences_row_set_title(ADW_PREFERENCES_ROW(w.mode), "Mode");
|
||||||
|
adw_combo_row_set_model(ADW_COMBO_ROW(w.mode), G_LIST_MODEL(gtk_string_list_new(kModes)));
|
||||||
|
w.level = spin(p + "-level", "Level (V)", -10, 10, 0.1, src->settings().ao_init[ch], 3);
|
||||||
|
w.amp = spin(p + "-amp", "Amplitude (V)", 0, 10, 0.1, 1.0, 3);
|
||||||
|
w.offset = spin(p + "-offset", "Offset (V)", -10, 10, 0.1, 0.0, 3);
|
||||||
|
w.freq = spin(p + "-freq", "Frequency (Hz)", 0.1, 10, 0.1, 1.0, 1);
|
||||||
|
w.run = switch_row(p + "-run", "Run", nullptr);
|
||||||
|
w.last = adw_action_row_new();
|
||||||
|
gtk_widget_set_name(w.last, (p + "-last").c_str());
|
||||||
|
adw_preferences_row_set_title(ADW_PREFERENCES_ROW(w.last), "Last written");
|
||||||
|
adw_action_row_set_subtitle(ADW_ACTION_ROW(w.last), "not written");
|
||||||
|
|
||||||
|
GtkWidget* g = adw_preferences_group_new();
|
||||||
|
const std::string title = "DAC " + std::to_string(ch);
|
||||||
|
adw_preferences_group_set_title(ADW_PREFERENCES_GROUP(g), title.c_str());
|
||||||
|
for (GtkWidget* r : {w.mode, w.level, w.amp, w.offset, w.freq, w.run, w.last})
|
||||||
|
adw_preferences_group_add(ADW_PREFERENCES_GROUP(g), r);
|
||||||
|
groups[ch] = g;
|
||||||
|
|
||||||
|
c->hooks[ch] = {c, ch};
|
||||||
|
g_signal_connect_data(w.mode, "notify::selected", G_CALLBACK(on_ao_changed), &c->hooks[ch],
|
||||||
|
nullptr, static_cast<GConnectFlags>(0));
|
||||||
|
for (GtkWidget* x : {w.level, w.amp, w.offset, w.freq})
|
||||||
|
g_signal_connect_data(x, "notify::value", G_CALLBACK(on_ao_changed), &c->hooks[ch], nullptr,
|
||||||
|
static_cast<GConnectFlags>(0));
|
||||||
|
g_signal_connect_data(w.run, "notify::active", G_CALLBACK(on_ao_changed), &c->hooks[ch],
|
||||||
|
nullptr, static_cast<GConnectFlags>(0));
|
||||||
|
ao_sensitivity(w);
|
||||||
|
}
|
||||||
|
|
||||||
|
GtkWidget* root = panel_root(groups[0], groups[1]);
|
||||||
|
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);
|
||||||
|
return root;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Digital I/O ------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
struct DioCtx {
|
||||||
|
E1608Source* src = nullptr;
|
||||||
|
std::shared_ptr<E1608Readouts> ro;
|
||||||
|
GtkWidget* led[8] = {}; ///< input bits
|
||||||
|
GtkWidget* out[8] = {}; ///< output bits (switch rows)
|
||||||
|
GtkWidget* counter = nullptr;
|
||||||
|
Hook hooks[8];
|
||||||
|
bool updating = false;
|
||||||
|
unsigned epoch_seen = 0;
|
||||||
|
gint64 last_us = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
void set_led(GtkWidget* l, bool on) {
|
||||||
|
if (gtk_widget_has_css_class(l, "success") == on) return;
|
||||||
|
if (on) {
|
||||||
|
gtk_widget_remove_css_class(l, "dim-label");
|
||||||
|
gtk_widget_add_css_class(l, "success");
|
||||||
|
} else {
|
||||||
|
gtk_widget_remove_css_class(l, "success");
|
||||||
|
gtk_widget_add_css_class(l, "dim-label");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void dio_refresh(DioCtx* c) {
|
||||||
|
const unsigned ep = c->ro->epoch.load();
|
||||||
|
if (ep != c->epoch_seen) { // a (re)connect re-latched the outputs from the form
|
||||||
|
c->epoch_seen = ep;
|
||||||
|
const unsigned latch = c->ro->do_latch.load();
|
||||||
|
c->updating = true;
|
||||||
|
for (int b = 0; b < 8; ++b) {
|
||||||
|
if (c->out[b] != nullptr)
|
||||||
|
adw_switch_row_set_active(ADW_SWITCH_ROW(c->out[b]), ((latch >> b) & 1u) != 0);
|
||||||
|
}
|
||||||
|
c->updating = false;
|
||||||
|
}
|
||||||
|
const unsigned di = c->ro->di.load();
|
||||||
|
for (int b = 0; b < 8; ++b) {
|
||||||
|
if (c->led[b] != nullptr) set_led(c->led[b], ((di >> b) & 1u) != 0);
|
||||||
|
}
|
||||||
|
char buf[24];
|
||||||
|
std::snprintf(buf, sizeof buf, "%u", static_cast<unsigned>(c->ro->counter.load()));
|
||||||
|
set_text(c->counter, buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
gboolean dio_tick(GtkWidget*, GdkFrameClock*, gpointer user) {
|
||||||
|
DioCtx* c = static_cast<DioCtx*>(user);
|
||||||
|
if (due(c->last_us)) dio_refresh(c);
|
||||||
|
return G_SOURCE_CONTINUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
void on_do_changed(GObject* obj, GParamSpec*, gpointer user) {
|
||||||
|
const Hook* h = static_cast<const Hook*>(user);
|
||||||
|
DioCtx* c = static_cast<DioCtx*>(h->ctx);
|
||||||
|
if (c->updating) return;
|
||||||
|
const bool on = adw_switch_row_get_active(ADW_SWITCH_ROW(obj));
|
||||||
|
E1608Source* src = c->src;
|
||||||
|
const int bit = h->idx;
|
||||||
|
src->post([src, bit, on] { src->set_output_bit(bit, on); });
|
||||||
|
}
|
||||||
|
|
||||||
|
void on_counter_reset(GtkButton*, gpointer user) {
|
||||||
|
DioCtx* c = static_cast<DioCtx*>(user);
|
||||||
|
E1608Source* src = c->src;
|
||||||
|
src->post([src] { src->reset_counter(); });
|
||||||
|
}
|
||||||
|
|
||||||
|
GtkWidget* build_dio(E1608Source* src) {
|
||||||
|
auto* c = new DioCtx;
|
||||||
|
c->src = src;
|
||||||
|
c->ro = src->readouts();
|
||||||
|
|
||||||
|
GtkWidget* lines = adw_preferences_group_new();
|
||||||
|
adw_preferences_group_set_title(ADW_PREFERENCES_GROUP(lines), "Digital lines");
|
||||||
|
for (int b = 0; b < 8; ++b) {
|
||||||
|
const std::string title = "Bit " + std::to_string(b);
|
||||||
|
if (src->settings().dir_out[b]) {
|
||||||
|
GtkWidget* row = switch_row("e1608-do" + std::to_string(b), title.c_str(), "Output");
|
||||||
|
c->out[b] = row;
|
||||||
|
c->hooks[b] = {c, b};
|
||||||
|
g_signal_connect_data(row, "notify::active", G_CALLBACK(on_do_changed), &c->hooks[b], nullptr,
|
||||||
|
static_cast<GConnectFlags>(0));
|
||||||
|
adw_preferences_group_add(ADW_PREFERENCES_GROUP(lines), row);
|
||||||
|
} else {
|
||||||
|
GtkWidget* row = adw_action_row_new();
|
||||||
|
adw_preferences_row_set_title(ADW_PREFERENCES_ROW(row), title.c_str());
|
||||||
|
adw_action_row_set_subtitle(ADW_ACTION_ROW(row), "Input");
|
||||||
|
GtkWidget* led = gtk_label_new("\u25cf");
|
||||||
|
gtk_widget_set_name(led, ("e1608-di" + std::to_string(b)).c_str());
|
||||||
|
gtk_widget_add_css_class(led, "dim-label");
|
||||||
|
adw_action_row_add_suffix(ADW_ACTION_ROW(row), led);
|
||||||
|
c->led[b] = led;
|
||||||
|
adw_preferences_group_add(ADW_PREFERENCES_GROUP(lines), row);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
GtkWidget* counter_group = adw_preferences_group_new();
|
||||||
|
adw_preferences_group_set_title(ADW_PREFERENCES_GROUP(counter_group), "Counter");
|
||||||
|
GtkWidget* crow = adw_action_row_new();
|
||||||
|
adw_preferences_row_set_title(ADW_PREFERENCES_ROW(crow), "Counts");
|
||||||
|
c->counter = gtk_label_new("0");
|
||||||
|
gtk_widget_set_name(c->counter, "e1608-counter");
|
||||||
|
gtk_widget_add_css_class(c->counter, "numeric");
|
||||||
|
adw_action_row_add_suffix(ADW_ACTION_ROW(crow), c->counter);
|
||||||
|
GtkWidget* reset = gtk_button_new_with_label("Reset");
|
||||||
|
gtk_widget_set_name(reset, "e1608-counter-reset");
|
||||||
|
gtk_widget_set_valign(reset, GTK_ALIGN_CENTER);
|
||||||
|
g_signal_connect_data(reset, "clicked", G_CALLBACK(on_counter_reset), c, nullptr,
|
||||||
|
static_cast<GConnectFlags>(0));
|
||||||
|
adw_action_row_add_suffix(ADW_ACTION_ROW(crow), reset);
|
||||||
|
adw_preferences_group_add(ADW_PREFERENCES_GROUP(counter_group), crow);
|
||||||
|
|
||||||
|
GtkWidget* root = panel_root(lines, counter_group);
|
||||||
|
g_object_set_data_full(G_OBJECT(root), "e1608-dio-ctx", c,
|
||||||
|
+[](gpointer p) { delete static_cast<DioCtx*>(p); });
|
||||||
|
gtk_widget_add_tick_callback(root, dio_tick, c, nullptr);
|
||||||
|
return root;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
std::vector<SourcePanel> make_e1608_panels(DataSource& source) {
|
||||||
|
E1608Source* src = static_cast<E1608Source*>(&source);
|
||||||
|
SourcePanel ao;
|
||||||
|
ao.id = "ao";
|
||||||
|
ao.title = "Analog out";
|
||||||
|
ao.icon = "audio-volume-high-symbolic";
|
||||||
|
ao.widget = build_ao(src);
|
||||||
|
SourcePanel dio;
|
||||||
|
dio.id = "dio";
|
||||||
|
dio.title = "Digital I/O";
|
||||||
|
dio.icon = "input-dialpad-symbolic";
|
||||||
|
dio.widget = build_dio(src);
|
||||||
|
return {ao, dio};
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace udpscope
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
// SPDX-FileCopyrightText: 2026 Martino Ferrari
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "source/source_kind.hpp"
|
||||||
|
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace udpscope {
|
||||||
|
|
||||||
|
/// The E-1608's two capability panels, "Analog out" and "Digital I/O". `source` must be an
|
||||||
|
/// `e1608::E1608Source`. The widgets reach it only through `source.post()`.
|
||||||
|
std::vector<SourcePanel> make_e1608_panels(DataSource& source);
|
||||||
|
|
||||||
|
} // namespace udpscope
|
||||||
Reference in new issue
Block a user