Files
udpscope/src/source/e1608/e1608_panels.cpp
T
2026-11-01 09:46:26 +01:00

346 lines
12 KiB
C++

// 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