diff --git a/CMakeLists.txt b/CMakeLists.txt index e0520e0f3..cf017d302 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -47,14 +47,15 @@ option(ENABLE_STACKTRACE "Enable stack trace when crashing" FALSE) option(ENABLE_DECODE "Build with libsigrokdecode" TRUE) option(ENABLE_FLOW "Build with libsigrokflow" FALSE) option(ENABLE_TESTS "Enable unit tests" FALSE) -option(STATIC_PKGDEPS_LIBS "Statically link to (pkg-config) libraries" FALSE) +if(WIN32) + set(PKGDEPS_STATIC_DEFAULT TRUE) +else() + set(PKGDEPS_STATIC_DEFAULT FALSE) +endif() +option(STATIC_PKGDEPS_LIBS "Statically link to (pkg-config) libraries" ${PKGDEPS_STATIC_DEFAULT}) option(ENABLE_TS_UPDATE "Update .ts source files (Qt l10n)" FALSE) if(WIN32) - # On Windows/MinGW we need to statically link to libraries. - # This option is user configurable, but enable it by default on win32. - set(STATIC_PKGDEPS_LIBS TRUE) - # Windows does not support UNIX signals. set(ENABLE_SIGNALS FALSE) endif() @@ -337,6 +338,7 @@ set(pulseview_SOURCES pv/mainwindow.cpp pv/metadata_obj.cpp pv/session.cpp + pv/triggersettings.cpp pv/storesession.cpp pv/util.cpp pv/binding/binding.cpp @@ -515,7 +517,7 @@ endif() if(WIN32) # Use the sigrok icon for the pulseview.exe executable. - set(CMAKE_RC_COMPILE_OBJECT "${CMAKE_RC_COMPILER} -O coff -I${CMAKE_CURRENT_SOURCE_DIR} ") + set(CMAKE_RC_COMPILE_OBJECT "\"${CMAKE_RC_COMPILER}\" -O coff -I\"${CMAKE_CURRENT_SOURCE_DIR}\" ") enable_language(RC) list(APPEND pulseview_SOURCES pulseviewico.rc) endif() @@ -631,7 +633,12 @@ else() list(APPEND PULSEVIEW_LINK_LIBS ${PKGDEPS_LIBRARIES}) endif() -if(WIN32) +if(Qt5_FOUND) + get_target_property(QT_CORE_LIBRARY_TYPE Qt5::Core TYPE) +else() + get_target_property(QT_CORE_LIBRARY_TYPE Qt6::Core TYPE) +endif() +if(WIN32 AND QT_CORE_LIBRARY_TYPE STREQUAL "STATIC_LIBRARY") # On Windows we need to statically link the libqsvg imageformat # plugin (and the QtSvg component) for SVG graphics/icons to work. # We also need QWindowsIntegrationPlugin, Qt5PlatformSupport (only for diff --git a/main.cpp b/main.cpp index 0867187c5..bccc124de 100644 --- a/main.cpp +++ b/main.cpp @@ -232,7 +232,8 @@ int main(int argc, char *argv[]) qDebug() << "ERROR: invalid log level spec."; break; } - context->set_log_level(sigrok::LogLevel::get(loglevel)); + // No Context exists yet; libsigrok's log level is global. + sr_log_loglevel_set(loglevel); #ifdef ENABLE_DECODE srd_log_loglevel_set(loglevel); @@ -376,5 +377,8 @@ int main(int argc, char *argv[]) } while (false); + // Unregister while the global settings registry is still alive. + // The static logger destructor must not access a destroyed vector. + pv::logging.shutdown(); return ret; } diff --git a/pv/binding/device.cpp b/pv/binding/device.cpp index 855052cdd..15fe3eb8f 100644 --- a/pv/binding/device.cpp +++ b/pv/binding/device.cpp @@ -24,6 +24,7 @@ #include "device.hpp" #include +#include #include #include @@ -44,6 +45,7 @@ using sigrok::ConfigKey; using sigrok::Error; using pv::prop::Bool; +using pv::prop::Double; using pv::prop::Enum; using pv::prop::Int; using pv::prop::Property; @@ -76,7 +78,8 @@ Device::Device(shared_ptr configurable) : if ((key->id() == SR_CONF_CONTINUOUS) || (key->id() == SR_CONF_TRIGGER_MATCH) || (key->id() == SR_CONF_CONN) || (key->id() == SR_CONF_SERIALCOMM) || (key->id() == SR_CONF_NUM_LOGIC_CHANNELS) || (key->id() == SR_CONF_NUM_ANALOG_CHANNELS) || (key->id() == SR_CONF_SESSIONFILE) || (key->id() == SR_CONF_CAPTUREFILE) || - (key->id() == SR_CONF_CAPTURE_UNITSIZE)) + (key->id() == SR_CONF_CAPTURE_UNITSIZE) || (key->id() == SR_CONF_CAPTURE_STATUS) || + (key->id() == SR_CONF_CAPTURE_ERROR)) continue; qDebug() << QString(tr("Note for device developers: Ignoring device configuration capability '%1' " \ @@ -92,14 +95,42 @@ Device::Device(shared_ptr configurable) : }; switch (key->id()) { + case SR_CONF_ENABLED: + properties_.push_back(shared_ptr(new Bool( + descr, "", get, set, true))); + break; + + case SR_CONF_OUTPUT_FREQUENCY: + bind_double_range(descr, " Hz", key, capabilities, get, set); + break; + + case SR_CONF_DUTY_CYCLE: + bind_double_range(descr, " %", key, capabilities, get, set); + break; + case SR_CONF_SAMPLERATE: // Sample rate values are not bound because they are shown // in the MainBar break; - case SR_CONF_CAPTURE_RATIO: - bind_int(descr, "", "%", pair(0, 100), get, set); + case SR_CONF_CAPTURE_RATIO: { + pair limits(0, 100); + if (capabilities.count(Capability::LIST)) { + try { + auto range = configurable_->config_list(key); + if (g_variant_is_of_type(range.gobj(), G_VARIANT_TYPE("(tt)"))) { + uint64_t minimum, maximum; + g_variant_get(range.gobj(), "(tt)", &minimum, &maximum); + if (minimum <= maximum && maximum <= 100) + limits = pair(minimum, maximum); + } + } catch (const Error &e) { + qWarning() << "Cannot query capture ratio range" << e.what(); + } + } + bind_int(descr, "", "%", limits, get, set); break; + } case SR_CONF_LIMIT_FRAMES: // Value 0 means that there is no limit @@ -204,6 +235,30 @@ void Device::bind_int(const QString &name, const QString &desc, QString suffix, range, getter, setter, special_value_text))); } +void Device::bind_double_range(const QString &name, const QString &suffix, + const ConfigKey *key, set capabilities, + Property::Getter getter, Property::Setter setter) +{ + if (!capabilities.count(Capability::LIST)) + return; + try { + auto range = configurable_->config_list(key); + if (!g_variant_is_of_type(range.gobj(), G_VARIANT_TYPE("ad")) || + g_variant_n_children(range.gobj()) != 3) + return; + double minimum, maximum, step; + g_variant_get_child(range.gobj(), 0, "d", &minimum); + g_variant_get_child(range.gobj(), 1, "d", &maximum); + g_variant_get_child(range.gobj(), 2, "d", &step); + if (minimum > maximum || step <= 0) + return; + properties_.push_back(std::make_shared(name, "", step >= 1 ? 0 : 6, + suffix, pair(minimum, maximum), step, getter, setter)); + } catch (const Error &e) { + qWarning() << "Cannot query generator range" << name << e.what(); + } +} + QString Device::print_timebase(Glib::VariantBase gvar) { uint64_t p, q; diff --git a/pv/binding/device.hpp b/pv/binding/device.hpp index 30d89ddd4..c7f864886 100644 --- a/pv/binding/device.hpp +++ b/pv/binding/device.hpp @@ -55,6 +55,9 @@ class Device : public Binding private: void bind_bool(const QString &name, const QString &desc, prop::Property::Getter getter, prop::Property::Setter setter); + void bind_double_range(const QString &name, const QString &suffix, + const sigrok::ConfigKey *key, set capabilities, + prop::Property::Getter getter, prop::Property::Setter setter); void bind_enum(const QString &name, const QString &desc, const sigrok::ConfigKey *key, set capabilities, diff --git a/pv/data/segment.hpp b/pv/data/segment.hpp index f4601d044..5bfa523fc 100644 --- a/pv/data/segment.hpp +++ b/pv/data/segment.hpp @@ -22,6 +22,7 @@ #define PULSEVIEW_PV_DATA_SEGMENT_HPP #include "pv/util.hpp" +#include "segmentmutex.hpp" #include #include @@ -100,7 +101,7 @@ class Segment : public QObject uint64_t get_iterator_valid_length(SegmentDataIterator* it); uint32_t segment_id_; - mutable recursive_mutex mutex_; + mutable SegmentMutex mutex_; deque data_chunks_; uint8_t* current_chunk_; uint64_t used_samples_, unused_samples_; diff --git a/pv/data/segmentmutex.hpp b/pv/data/segmentmutex.hpp new file mode 100644 index 000000000..f9150d0db --- /dev/null +++ b/pv/data/segmentmutex.hpp @@ -0,0 +1,41 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +#ifndef PULSEVIEW_PV_DATA_SEGMENTMUTEX_HPP +#define PULSEVIEW_PV_DATA_SEGMENTMUTEX_HPP + +#include +#include +#include + +namespace pv { +namespace data { + +/* In MinGW's statically initialized libstdc++ mutex branch, the default + * destructor is trivial. Winpthreads lazily allocates an event on first lock, + * which still needs pthread_mutex_destroy. Restrict that extra destruction to + * this implementation; other platforms retain standard destruction. + * Segment lock guards release their locks before member destruction. */ +class SegmentMutex final : public std::recursive_mutex { +public: + SegmentMutex() = default; + ~SegmentMutex() noexcept + { +#if defined(_WIN32) && defined(__GLIBCXX__) && defined(__GTHREAD_RECURSIVE_MUTEX_INIT) + destroy_if_trivial(std::is_trivially_destructible()); +#endif + } + +private: +#if defined(_WIN32) && defined(__GLIBCXX__) && defined(__GTHREAD_RECURSIVE_MUTEX_INIT) + void destroy_if_trivial(std::true_type) noexcept + { + if (__gthread_recursive_mutex_destroy(native_handle()) != 0) + std::terminate(); + } + void destroy_if_trivial(std::false_type) noexcept {} +#endif +}; + +} // namespace data +} // namespace pv + +#endif diff --git a/pv/logging.cpp b/pv/logging.cpp index 7dab545de..ee76c3a0c 100644 --- a/pv/logging.cpp +++ b/pv/logging.cpp @@ -51,6 +51,13 @@ static void *prev_srd_log_cb_data; Logging::~Logging() { + shutdown(); +} + +void Logging::shutdown() +{ + if (!initialized_) + return; qInstallMessageHandler(nullptr); if (prev_sr_log_cb) sr_log_callback_set(prev_sr_log_cb, prev_sr_log_cb_data); @@ -64,6 +71,7 @@ Logging::~Logging() #endif GlobalSettings::remove_change_handler(this); + initialized_ = false; } void Logging::init() @@ -84,6 +92,7 @@ void Logging::init() #endif GlobalSettings::add_change_handler(this); + initialized_ = true; } int Logging::get_log_level() const diff --git a/pv/logging.hpp b/pv/logging.hpp index 28f210743..6d17d1a2a 100644 --- a/pv/logging.hpp +++ b/pv/logging.hpp @@ -50,6 +50,7 @@ class Logging : public QObject, public GlobalSettingsInterface public: ~Logging(); void init(); + void shutdown(); int get_log_level() const; void set_log_level(int level); @@ -73,6 +74,7 @@ class Logging : public QObject, public GlobalSettingsInterface void logged_text(QString s); private: + bool initialized_ = false; int buffer_size_; QStringList buffer_; mutable mutex log_mutex_; diff --git a/pv/mainwindow.cpp b/pv/mainwindow.cpp index 8646175e2..054e60f80 100644 --- a/pv/mainwindow.cpp +++ b/pv/mainwindow.cpp @@ -94,12 +94,13 @@ MainWindow::~MainWindow() sub_windows_.clear(); } -void MainWindow::show_session_error(const QString text, const QString info_text) +void MainWindow::show_session_error(const QString text, const QString info_text, QWidget *parent) { // TODO Emulate noquote() qDebug() << "Notifying user of session error: " << text << "; " << info_text; - QMessageBox msg; + QMessageBox msg(parent ? parent : QApplication::activeWindow()); + msg.setWindowTitle(text); msg.setText(text + "\n\n" + info_text); msg.setStandardButtons(QMessageBox::Ok); msg.setIcon(QMessageBox::Warning); @@ -596,6 +597,13 @@ void MainWindow::update_acq_button(Session *session) run_stop_button_->setIcon(*icons[state]); run_stop_button_->setText((state == pv::Session::Stopped) ? run_caption : tr("Stop")); + const bool reconnect_needed = session && session->device_needs_reconnect(); + const bool stopping = session && session->stop_requested(); + run_stop_button_->setEnabled(!reconnect_needed && !stopping); + run_stop_shortcut_->setEnabled(!reconnect_needed && !stopping); + run_stop_button_->setToolTip(reconnect_needed ? + tr("Device disconnected. Reconnect USB, then use Connect to Device, Scan, and select the analyzer again.") : + (stopping ? tr("Stopping acquisition...") : tr("Start/Stop Acquisition"))); } void MainWindow::save_ui_settings() @@ -670,6 +678,8 @@ bool MainWindow::restoreState(const QByteArray &state, int version) void MainWindow::on_run_stop_clicked() { + if (!run_stop_button_->isEnabled()) + return; GlobalSettings settings; bool all_sessions = settings.value(GlobalSettings::Key_General_StartAllSessions).toBool(); @@ -694,9 +704,10 @@ void MainWindow::on_run_stop_clicked() for (shared_ptr s : hw_sessions) if (any_running) - s->stop_capture(); + s->request_stop_capture(); else s->start_capture([&](QString message) {Q_EMIT session_error_raised("Capture failed", message);}); + update_acq_button(last_focused_session_.get()); } else { shared_ptr session = last_focused_session_; @@ -710,7 +721,12 @@ void MainWindow::on_run_stop_clicked() break; case Session::AwaitingTrigger: case Session::Running: - session->stop_capture(); + try { + session->request_stop_capture(); + } catch (sigrok::Error& error) { + Q_EMIT session_error_raised(tr("Capture failed"), error.what()); + } + update_acq_button(session.get()); break; } } @@ -985,7 +1001,7 @@ void MainWindow::on_close_current_tab() } void MainWindow::on_session_error_raised(const QString text, const QString info_text) { - MainWindow::show_session_error(text, info_text); + MainWindow::show_session_error(text, info_text, this); } } // namespace pv diff --git a/pv/mainwindow.hpp b/pv/mainwindow.hpp index 493e3a55c..76684ad7f 100644 --- a/pv/mainwindow.hpp +++ b/pv/mainwindow.hpp @@ -77,7 +77,7 @@ class MainWindow : public QMainWindow ~MainWindow(); - static void show_session_error(const QString text, const QString info_text); + static void show_session_error(const QString text, const QString info_text, QWidget *parent = nullptr); shared_ptr get_active_view() const; diff --git a/pv/popups/deviceoptions.cpp b/pv/popups/deviceoptions.cpp index bfb702cf0..b885881dd 100644 --- a/pv/popups/deviceoptions.cpp +++ b/pv/popups/deviceoptions.cpp @@ -42,6 +42,10 @@ DeviceOptions::DeviceOptions(shared_ptr device, QWidget *parent) : setLayout(&layout_); layout_.addWidget(binding_.get_property_form(this, true)); + connect(&binding_, &pv::binding::Device::config_changed, this, [this]() { + // A mode change can make an existing setting unavailable. + binding_.update_property_widgets(); + }); } pv::binding::Device& DeviceOptions::binding() diff --git a/pv/prop/bool.cpp b/pv/prop/bool.cpp index 0cd160461..9a7efc8f1 100644 --- a/pv/prop/bool.cpp +++ b/pv/prop/bool.cpp @@ -21,17 +21,21 @@ #include #include +#include #include #include "bool.hpp" +#include "configerror.hpp" namespace pv { namespace prop { -Bool::Bool(QString name, QString desc, Getter getter, Setter setter) : +Bool::Bool(QString name, QString desc, Getter getter, Setter setter, + bool allow_unknown) : Property(name, desc, getter, setter), - check_box_(nullptr) + check_box_(nullptr), + allow_unknown_(allow_unknown) { } @@ -48,8 +52,10 @@ QWidget* Bool::get_widget(QWidget *parent, bool auto_commit) if (!variant.gobj()) return nullptr; } catch (const sigrok::Error &e) { - qWarning() << tr("Querying config key %1 resulted in %2").arg(name_, e.what()); - return nullptr; + if (!allow_unknown_ || e.result != SR_ERR_NA) { + qWarning() << tr("Querying config key %1 resulted in %2").arg(name_, e.what()); + return nullptr; + } } check_box_ = new QCheckBox(name(), parent); @@ -73,31 +79,52 @@ void Bool::update_widget() { if (!check_box_) return; + const QSignalBlocker blocker(check_box_); Glib::VariantBase variant; try { variant = getter_(); } catch (const sigrok::Error &e) { + if (allow_unknown_ && e.result == SR_ERR_NA) { + check_box_->setEnabled(true); + check_box_->setTristate(true); + check_box_->setCheckState(Qt::PartiallyChecked); + check_box_->setToolTip(tr("Output state is unknown. Select enabled or disabled to send an explicit command. Frequency and duty are requested settings until applied.")); + return; + } qWarning() << tr("Querying config key %1 resulted in %2").arg(name_, e.what()); + check_box_->setEnabled(false); return; } assert(variant.gobj()); + check_box_->setEnabled(true); bool value = Glib::VariantBase::cast_dynamic>( variant).get(); + check_box_->setTristate(false); check_box_->setCheckState(value ? Qt::Checked : Qt::Unchecked); + check_box_->setToolTip(desc()); } void Bool::commit() { assert(setter_); - if (!check_box_) + if (!check_box_ || !check_box_->isEnabled()) return; - setter_(Glib::Variant::create(check_box_->checkState() == Qt::Checked)); + if (check_box_->checkState() == Qt::PartiallyChecked) + return; + try { + setter_(Glib::Variant::create(check_box_->checkState() == Qt::Checked)); + update_widget(); + } catch (const sigrok::Error &e) { + update_widget(); + if (e.result == SR_ERR_DEV_CLOSED) check_box_->setEnabled(false); + show_config_error(check_box_, e); + } } void Bool::on_state_changed(int) diff --git a/pv/prop/bool.hpp b/pv/prop/bool.hpp index 310eec6f0..7e9a86d6d 100644 --- a/pv/prop/bool.hpp +++ b/pv/prop/bool.hpp @@ -32,7 +32,8 @@ class Bool : public Property Q_OBJECT public: - Bool(QString name, QString desc, Getter getter, Setter setter); + Bool(QString name, QString desc, Getter getter, Setter setter, + bool allow_unknown = false); virtual ~Bool() = default; @@ -47,6 +48,7 @@ private Q_SLOTS: private: QCheckBox *check_box_; + bool allow_unknown_; }; } // namespace prop diff --git a/pv/prop/configerror.hpp b/pv/prop/configerror.hpp new file mode 100644 index 000000000..6d9b463ff --- /dev/null +++ b/pv/prop/configerror.hpp @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef PULSEVIEW_PROP_CONFIGERROR_HPP +#define PULSEVIEW_PROP_CONFIGERROR_HPP + +#include +#include + +namespace pv { +namespace prop { +inline void show_config_error(QWidget *parent, const sigrok::Error &error) +{ + const bool closed = error.result == SR_ERR_DEV_CLOSED; + QMessageBox::warning(parent, closed ? + QObject::tr("Device unavailable") : QObject::tr("Device configuration failed"), + closed ? QObject::tr("Device unavailable. Reconnect USB, then use Connect to Device, Scan, and select the analyzer again.") : + QString::fromUtf8(error.what())); +} +} // namespace prop +} // namespace pv +#endif diff --git a/pv/prop/double.cpp b/pv/prop/double.cpp index f39ae844f..b8e1ff150 100644 --- a/pv/prop/double.cpp +++ b/pv/prop/double.cpp @@ -21,10 +21,12 @@ #include #include +#include #include #include "double.hpp" +#include "configerror.hpp" using boost::optional; using std::pair; @@ -94,10 +96,13 @@ void Double::update_widget() variant = getter_(); } catch (const sigrok::Error &e) { qWarning() << tr("Querying config key %1 resulted in %2").arg(name_, e.what()); + spin_box_->setEnabled(false); return; } assert(variant.gobj()); + spin_box_->setEnabled(true); + const QSignalBlocker blocker(spin_box_); double value = Glib::VariantBase::cast_dynamic>( variant).get(); @@ -108,10 +113,16 @@ void Double::commit() { assert(setter_); - if (!spin_box_) + if (!spin_box_ || !spin_box_->isEnabled()) return; - setter_(Glib::Variant::create(spin_box_->value())); + try { + setter_(Glib::Variant::create(spin_box_->value())); + } catch (const sigrok::Error &e) { + update_widget(); + if (e.result == SR_ERR_DEV_CLOSED) spin_box_->setEnabled(false); + show_config_error(spin_box_, e); + } } void Double::on_value_changed(double) diff --git a/pv/prop/enum.cpp b/pv/prop/enum.cpp index 8243f32d0..8c04d808c 100644 --- a/pv/prop/enum.cpp +++ b/pv/prop/enum.cpp @@ -28,10 +28,12 @@ #include #include #include +#include #include #include "enum.hpp" +#include "configerror.hpp" using std::abs; // Note that "using std::isnan;" is _not_ put here since that would break @@ -174,10 +176,16 @@ void Enum::update_widget() variant = getter_(); } catch (const sigrok::Error &e) { qWarning() << tr("Querying config key %1 resulted in %2").arg(name_, e.what()); + if (slider_) slider_->setEnabled(false); + if (selector_) selector_->setEnabled(false); return; } assert(variant.gobj()); + if (slider_) slider_->setEnabled(true); + if (selector_) selector_->setEnabled(true); + const QSignalBlocker slider_blocker(slider_); + const QSignalBlocker selector_blocker(selector_); if (is_range_) { @@ -258,22 +266,33 @@ void Enum::commit() if (is_range_) { // Use slider - if (!slider_layout_widget_) + if (!slider_layout_widget_ || !slider_->isEnabled()) return; - setter_(values_.at(slider_->value()).first); - - update_widget(); + try { + setter_(values_.at(slider_->value()).first); + update_widget(); + } catch (const sigrok::Error &e) { + update_widget(); + if (e.result == SR_ERR_DEV_CLOSED) slider_->setEnabled(false); + show_config_error(slider_layout_widget_, e); + } } else { // Use combo box - if (!selector_) + if (!selector_ || !selector_->isEnabled()) return; const int index = selector_->currentIndex(); if (index < 0) return; - setter_(selector_->itemData(index).value()); + try { + setter_(selector_->itemData(index).value()); + } catch (const sigrok::Error &e) { + update_widget(); + if (e.result == SR_ERR_DEV_CLOSED) selector_->setEnabled(false); + show_config_error(selector_, e); + } // The combo box needs no update, it already shows the current value // by definition: the user picked it diff --git a/pv/prop/int.cpp b/pv/prop/int.cpp index c1b2d22a6..eafb375bb 100644 --- a/pv/prop/int.cpp +++ b/pv/prop/int.cpp @@ -22,10 +22,12 @@ #include #include +#include #include #include "int.hpp" +#include "configerror.hpp" using boost::optional; using std::max; @@ -125,9 +127,11 @@ void Int::update_widget() value_ = getter_(); } catch (const sigrok::Error &e) { qWarning() << tr("Querying config key %1 resulted in %2").arg(name_, e.what()); + spin_box_->setEnabled(false); return; } + spin_box_->setEnabled(true); GVariant *value = value_.gobj(); assert(value); @@ -155,6 +159,8 @@ void Int::update_widget() assert(false); } + // Readback must not recursively commit a value to the driver. + const QSignalBlocker blocker(spin_box_); spin_box_->setValue((int)int_val); } @@ -162,7 +168,7 @@ void Int::commit() { assert(setter_); - if (!spin_box_) + if (!spin_box_ || !spin_box_->isEnabled()) return; GVariant *new_value = nullptr; @@ -192,7 +198,13 @@ void Int::commit() value_ = Glib::VariantBase(new_value); - setter_(value_); + try { + setter_(value_); + } catch (const sigrok::Error &e) { + update_widget(); + if (e.result == SR_ERR_DEV_CLOSED) spin_box_->setEnabled(false); + show_config_error(spin_box_, e); + } } void Int::on_value_changed(int) diff --git a/pv/session.cpp b/pv/session.cpp index 037700976..0294affa9 100644 --- a/pv/session.cpp +++ b/pv/session.cpp @@ -31,6 +31,7 @@ #include "devicemanager.hpp" #include "mainwindow.hpp" #include "session.hpp" +#include "triggersettings.hpp" #include "util.hpp" #include "data/analog.hpp" @@ -123,6 +124,7 @@ Session::Session(DeviceManager &device_manager, QString name) : default_name_(name), name_(name), capture_state_(Stopped), + stop_requested_(false), cur_samplerate_(0), data_saved_(true) { @@ -165,6 +167,14 @@ shared_ptr Session::device() const return device_; } +void Session::set_trigger(shared_ptr trigger) +{ + if (!session()) + return; + session()->set_trigger(std::move(trigger)); + trigger_changed(); +} + QString Session::name() const { return name_; @@ -220,6 +230,7 @@ bool Session::data_saved() const void Session::save_setup(QSettings &settings) const { + save_trigger_settings(settings, session()); int i; int decode_signal_count = 0; int gen_signal_count = 0; @@ -382,6 +393,16 @@ void Session::restore_setup(QSettings &settings) settings.endGroup(); } + if (session() && device_ && device_->device()) { + try { + if (restore_trigger_settings(settings, device_manager_.context(), + device_->device(), session())) + trigger_changed(); + } catch (const std::exception &error) { + qWarning() << tr("Failed to restore trigger:") << error.what(); + } + } + // Restore generated signals int gen_signal_count = settings.value("generated_signals").toInt(); @@ -608,6 +629,12 @@ void Session::set_device(shared_ptr device) signals_changed(); device_ = std::move(device); + device_reconnect_needed_ = false; + { + lock_guard lock(data_mutex_); + capture_outcome_.clear(); + capture_error_.clear(); + } try { device_->open(); @@ -797,6 +824,23 @@ Session::capture_state Session::get_capture_state() const return capture_state_; } +QString Session::capture_outcome() const +{ + lock_guard lock(data_mutex_); + return capture_outcome_; +} + +QString Session::capture_error() const +{ + lock_guard lock(data_mutex_); + return capture_error_; +} + +bool Session::device_needs_reconnect() const +{ + return device_reconnect_needed_.load(); +} + void Session::start_capture(function error_handler) { if (!device_) { @@ -807,6 +851,10 @@ void Session::start_capture(function error_handler) stop_capture(); // Check that at least one channel is enabled + if (device_needs_reconnect()) { + error_handler(tr("Device disconnected. Reconnect USB, then use Connect to Device, Scan, and select the analyzer again.")); + return; + } const shared_ptr sr_dev = device_->device(); if (sr_dev) { const auto channels = sr_dev->channels(); @@ -818,6 +866,13 @@ void Session::start_capture(function error_handler) } } + // Outcome and samples describe the same acquisition. A rejected start + // must preserve both until a new acquisition replaces them. + { + lock_guard lock(data_mutex_); + capture_outcome_.clear(); + capture_error_.clear(); + } // Clear signal data for (const shared_ptr& d : all_signal_data_) d->clear(); @@ -841,6 +896,29 @@ void Session::start_capture(function error_handler) sampling_thread_ = std::thread(&Session::sample_thread_proc, this, error_handler); } +bool Session::stop_requested() const +{ + lock_guard lock(sampling_mutex_); + return stop_requested_; +} + +void Session::request_stop_capture() +{ + { + lock_guard lock(sampling_mutex_); + if (capture_state_ == Stopped || stop_requested_) + return; + stop_requested_ = true; + } + try { + device_->stop(); + } catch (...) { + lock_guard lock(sampling_mutex_); + stop_requested_ = false; + throw; + } +} + void Session::stop_capture() { if (get_capture_state() != Stopped) @@ -1071,6 +1149,8 @@ void Session::set_capture_state(capture_state state) { lock_guard lock(sampling_mutex_); capture_state_ = state; + if (state == Stopped) + stop_requested_ = false; } capture_state_changed(state); @@ -1322,6 +1402,10 @@ void Session::sample_thread_proc(function error_handler) if (out_of_memory_) error_handler(tr("Out of memory, acquisition stopped.")); + const QString capture_problem = capture_error(); + if (!capture_problem.isEmpty()) + Q_EMIT session_error_raised(tr("Capture incomplete"), capture_problem + + tr("\nThe received samples can be saved as a partial capture.")); } void Session::free_unused_memory() @@ -1441,6 +1525,21 @@ void Session::feed_in_meta(shared_ptr meta) case SR_CONF_SAMPLERATE: cur_samplerate_ = g_variant_get_uint64(entry.second.gobj()); break; + case SR_CONF_CAPTURE_STATUS: { + const QString outcome = QString::fromUtf8(g_variant_get_string(entry.second.gobj(), nullptr)); + { + lock_guard lock(data_mutex_); + capture_outcome_ = outcome; + } + if (outcome == "disconnected" && dynamic_pointer_cast(device_)) + device_reconnect_needed_ = true; + break; + } + case SR_CONF_CAPTURE_ERROR: { + lock_guard lock(data_mutex_); + capture_error_ = QString::fromUtf8(g_variant_get_string(entry.second.gobj(), nullptr)); + break; + } default: qDebug() << "Received meta data key" << entry.first->id() << ", ignoring."; break; diff --git a/pv/session.hpp b/pv/session.hpp index 94338c20e..77b9b2df4 100644 --- a/pv/session.hpp +++ b/pv/session.hpp @@ -26,6 +26,7 @@ #endif #include +#include #include #include #include @@ -81,6 +82,7 @@ class Option; class OutputFormat; class Packet; class Session; +class Trigger; } // namespace sigrok using sigrok::Option; @@ -137,6 +139,7 @@ class Session : public QObject const DeviceManager& device_manager() const; shared_ptr session() const; + void set_trigger(shared_ptr trigger); shared_ptr device() const; @@ -183,8 +186,13 @@ class Session : public QObject map()); capture_state get_capture_state() const; + QString capture_outcome() const; + QString capture_error() const; + bool device_needs_reconnect() const; void start_capture(function error_handler); void stop_capture(); + void request_stop_capture(); + bool stop_requested() const; double get_samplerate() const; Glib::DateTime get_acquisition_start_time() const; @@ -260,6 +268,7 @@ class Session : public QObject void name_changed(); void trigger_event(int segment_id, util::Timestamp location); + void trigger_changed(); void new_segment(int new_segment_id); void segment_completed(int segment_id); @@ -290,6 +299,9 @@ public Q_SLOTS: mutable mutex sampling_mutex_; //!< Protects access to capture_state_ capture_state capture_state_; + bool stop_requested_; //!< Protected by sampling_mutex_ + QString capture_outcome_, capture_error_; + std::atomic_bool device_reconnect_needed_{false}; vector< shared_ptr > signalbases_; unordered_set< shared_ptr > all_signal_data_; diff --git a/pv/storesession.cpp b/pv/storesession.cpp index 5eb6a8ad2..3f5d533fe 100644 --- a/pv/storesession.cpp +++ b/pv/storesession.cpp @@ -50,7 +50,6 @@ using std::vector; using Glib::VariantBase; using sigrok::ConfigKey; -using sigrok::Error; using sigrok::OutputFormat; using sigrok::OutputFlag; @@ -175,19 +174,28 @@ bool StoreSession::start() map options = options_; - if (!output_format_->test_flag(OutputFlag::INTERNAL_IO_HANDLING)) + if (!output_format_->test_flag(OutputFlag::INTERNAL_IO_HANDLING)) { + output_stream_.exceptions(ios_base::badbit | ios_base::failbit); output_stream_.open(file_name_, ios_base::binary | ios_base::trunc | ios_base::out); + } output_ = output_format_->create_output(file_name_, device, options); auto meta = context->create_meta_packet( {{ConfigKey::SAMPLERATE, Glib::Variant::create( any_segment->samplerate())}}); output_->receive(meta); + // Save the outcome of these samples, not a newly selected device state. + if (!session_.capture_outcome().isEmpty()) { + auto outcome = context->create_meta_packet({ + {ConfigKey::CAPTURE_STATUS, Glib::Variant::create(session_.capture_outcome().toStdString())}, + {ConfigKey::CAPTURE_ERROR, Glib::Variant::create(session_.capture_error().toStdString())}}); + output_->receive(outcome); + } auto header = context->create_header_packet(session_.get_acquisition_start_time()); output_->receive(header); - } catch (Error& error) { + } catch (const std::exception& error) { error_ = tr("Error while saving: ") + error.what(); return false; } @@ -265,12 +273,12 @@ void StoreSession::store_proc(vector< shared_ptr > achannel_li shared_ptr achannel = (achannel_list.at(i))->channel(); shared_ptr asegment = asegment_list.at(i); - float *adata = new float[packet_len]; - asegment->get_samples(start_sample_, start_sample_ + packet_len, adata); + vector adata(packet_len); + asegment->get_samples(start_sample_, start_sample_ + packet_len, adata.data()); auto analog = context->create_analog_packet( vector >{achannel}, - (float *)adata, packet_len, + adata.data(), packet_len, sigrok::Quantity::VOLTAGE, sigrok::Unit::VOLT, vector()); const string adata_str = output_->receive(analog); @@ -278,23 +286,21 @@ void StoreSession::store_proc(vector< shared_ptr > achannel_li if (output_stream_.is_open()) output_stream_ << adata_str; - delete[] adata; } if (lsegment) { const size_t data_size = packet_len * lunit_size; - uint8_t* ldata = new uint8_t[data_size]; - lsegment->get_samples(start_sample_, start_sample_ + packet_len, ldata); + vector ldata(data_size); + lsegment->get_samples(start_sample_, start_sample_ + packet_len, ldata.data()); - auto logic = context->create_logic_packet((void*)ldata, data_size, lunit_size); + auto logic = context->create_logic_packet(ldata.data(), data_size, lunit_size); const string ldata_str = output_->receive(logic); if (output_stream_.is_open()) output_stream_ << ldata_str; - delete[] ldata; } - } catch (Error& error) { + } catch (const std::exception& error) { error_ = tr("Error while saving: ") + error.what(); break; } @@ -309,18 +315,27 @@ void StoreSession::store_proc(vector< shared_ptr > achannel_li const string ldata_str = output_->receive(dfend); if (output_stream_.is_open()) output_stream_ << ldata_str; - } catch (Error& error) { + } catch (const std::exception& error) { error_ = tr("Error while saving: ") + error.what(); } - // Zeroing the progress variables indicates completion - units_stored_ = unit_count_ = 0; + // Finish all output I/O before telling the session that its data is saved. + output_.reset(); + if (output_stream_.is_open()) { + try { + output_stream_.close(); + } catch (const std::exception& error) { + error_ = tr("Error while saving: ") + error.what(); + } + } - store_successful(); - progress_updated(); + if (!interrupt_ && !sample_count_ && error_.isEmpty()) + store_successful(); - output_.reset(); - output_stream_.close(); + // Zeroing the progress variables indicates that the worker has finished, + // including failed/cancelled exports; success is a separate signal. + units_stored_ = unit_count_ = 0; + progress_updated(); } } // namespace pv diff --git a/pv/toolbars/mainbar.cpp b/pv/toolbars/mainbar.cpp index cd46c063b..1b4202c1e 100644 --- a/pv/toolbars/mainbar.cpp +++ b/pv/toolbars/mainbar.cpp @@ -111,6 +111,7 @@ MainBar::MainBar(Session &session, QWidget *parent, pv::views::trace::View *view updating_sample_rate_(false), updating_sample_count_(false), sample_count_supported_(false), + channel_config_timer_(this), #ifdef ENABLE_DECODE add_decoder_button_(new QToolButton()), #endif @@ -291,6 +292,10 @@ MainBar::MainBar(Session &session, QWidget *parent, pv::views::trace::View *view this, SLOT(on_capture_state_changed(int))); connect(&session, SIGNAL(device_changed()), this, SLOT(on_device_changed())); + channel_config_timer_.setSingleShot(true); + channel_config_timer_.setInterval(0); + connect(&channel_config_timer_, SIGNAL(timeout()), + this, SLOT(refresh_channel_config())); update_device_list(); } @@ -315,9 +320,10 @@ void MainBar::set_capture_state(pv::Session::capture_state state) bool ui_enabled = (state == pv::Session::Stopped) ? true : false; device_selector_.setEnabled(ui_enabled); + ui_enabled = ui_enabled && !session_.device_needs_reconnect(); configure_button_.setEnabled(ui_enabled); channels_button_.setEnabled(ui_enabled); - sample_count_.setEnabled(ui_enabled); + sample_count_.setEnabled(ui_enabled && !continuous_sampling_enabled()); sample_rate_.setEnabled(ui_enabled); } @@ -393,6 +399,9 @@ void MainBar::update_sample_rate_selector() gvar_dict = sr_dev->config_list(ConfigKey::SAMPLERATE); } catch (Error& error) { qDebug() << tr("Failed to get sample rate list:") << error.what(); + sample_rate_.show_none(); + updating_sample_rate_ = false; + return; } } else { sample_rate_.show_none(); @@ -459,6 +468,29 @@ void MainBar::update_sample_rate_selector_value() } } +bool MainBar::continuous_sampling_enabled() +{ + const auto device = device_selector_.selected_device(); + if (!device) + return false; + const auto sr_dev = device->device(); + if (!sr_dev) + return false; + // A writable boolean denotes active continuous mode. A read-only + // capability on other drivers is not an active-mode setting. + if (!sr_dev->config_check(ConfigKey::CONTINUOUS, Capability::GET) || + !sr_dev->config_check(ConfigKey::CONTINUOUS, Capability::SET)) + return false; + try { + auto value = sr_dev->config_get(ConfigKey::CONTINUOUS); + return value.gobj() && g_variant_is_of_type(value.gobj(), + G_VARIANT_TYPE_BOOLEAN) && g_variant_get_boolean(value.gobj()); + } catch (const Error &error) { + qWarning() << tr("Failed to query continuous sampling:") << error.what(); + return false; + } +} + void MainBar::update_sample_count_selector() { if (updating_sample_count_) @@ -473,11 +505,13 @@ void MainBar::update_sample_count_selector() assert(!updating_sample_count_); updating_sample_count_ = true; - if (!sample_count_supported_) { + if (!sample_count_supported_ || continuous_sampling_enabled()) { sample_count_.show_none(); + sample_count_.setEnabled(false); updating_sample_count_ = false; return; } + sample_count_.setEnabled(session_.get_capture_state() == Session::Stopped); uint64_t sample_count = sample_count_.value(); uint64_t min_sample_count = 0; @@ -528,6 +562,10 @@ void MainBar::update_sample_count_selector() void MainBar::update_device_config_widgets() { using namespace pv::popups; + channel_config_timer_.stop(); + for (const auto &connection : channel_connections_) + disconnect(connection); + channel_connections_.clear(); const shared_ptr device = device_selector_.selected_device(); @@ -543,6 +581,11 @@ void MainBar::update_device_config_widgets() const shared_ptr sr_dev = device->device(); if (!sr_dev) return; + for (const auto &signal : session_.signalbases()) { + if (signal->channel()) + channel_connections_.push_back(connect(signal.get(), + SIGNAL(enabled_changed(bool)), this, SLOT(on_channels_changed()))); + } // Update the configure popup DeviceOptions *const opts = new DeviceOptions(sr_dev, this); @@ -607,7 +650,7 @@ void MainBar::commit_sample_count() const shared_ptr sr_dev = device->device(); sample_count = sample_count_.value(); - if (sample_count_supported_) { + if (sample_count_supported_ && !continuous_sampling_enabled()) { try { sr_dev->config_set(ConfigKey::LIMIT_SAMPLES, Glib::Variant::create(sample_count)); @@ -618,6 +661,11 @@ void MainBar::commit_sample_count() } } + // Continuous mode owns its zero sample limit. Do not replace it with + // a stale finite toolbar value when a configuration control changes. + if (continuous_sampling_enabled()) + update_sample_count_selector(); + // Devices with built-in memory might impose limits on certain // configurations, so let's check what sample rate the driver // lets us use now. @@ -784,6 +832,8 @@ void MainBar::on_device_changed() { update_device_list(); update_device_config_widgets(); + // A fresh device can clear reconnect without another capture-state transition. + set_capture_state(session_.get_capture_state()); } void MainBar::on_capture_state_changed(int state) @@ -805,6 +855,9 @@ void MainBar::on_sample_rate_changed() void MainBar::on_config_changed() { + // Read back mode/limits before committing the toolbar's finite value. + // Turning continuous mode off must also restore its hidden selector. + update_sample_count_selector(); // We want to also call update_sample_rate_selector() here in case // the user changed the SR_CONF_EXTERNAL_CLOCK option. However, // commit_sample_rate() does this already, so we don't call it here @@ -813,6 +866,40 @@ void MainBar::on_config_changed() commit_sample_rate(); } +void MainBar::on_channels_changed() +{ + // Bulk operations can temporarily disable every channel. Query limits + // once the whole operation has finished, rather than for each checkbox. + if (!channel_config_timer_.isActive()) + channel_config_timer_.start(); +} + +void MainBar::refresh_channel_config() +{ + const auto device = device_selector_.selected_device(); + if (!device || !device->device() || session_.device_needs_reconnect() || + session_.get_capture_state() != Session::Stopped) + return; + bool any_enabled = false; + for (const auto &signal : session_.signalbases()) + any_enabled |= signal->channel() && signal->enabled(); + if (!any_enabled) { + sample_rate_.show_none(); + sample_count_.show_none(); + return; + } + update_sample_rate_selector(); + // set_value() selects the nearest supported entry when the previous + // rate is now too high. Keep the driver and the toolbar in agreement. + if (sample_rate_.value() && device->device()->config_check( + ConfigKey::SAMPLERATE, Capability::SET)) + commit_sample_rate(); + else + update_sample_count_selector(); + if (sample_count_.value()) + commit_sample_count(); +} + void MainBar::on_actionNewView_triggered(QAction* action) { if (action) diff --git a/pv/toolbars/mainbar.hpp b/pv/toolbars/mainbar.hpp index e4aa39b67..0865f12e9 100644 --- a/pv/toolbars/mainbar.hpp +++ b/pv/toolbars/mainbar.hpp @@ -23,6 +23,7 @@ #include #include #include +#include #include @@ -31,6 +32,7 @@ #include #include #include +#include #include #include @@ -113,6 +115,7 @@ class MainBar : public pv::views::trace::StandardBar void update_sample_rate_selector(); void update_sample_rate_selector_value(); void update_sample_count_selector(); + bool continuous_sampling_enabled(); void update_device_config_widgets(); void commit_sample_rate(); void commit_sample_count(); @@ -131,6 +134,8 @@ private Q_SLOTS: void on_sample_rate_changed(); void on_config_changed(); + void on_channels_changed(); + void refresh_channel_config(); void on_actionNewView_triggered(QAction* action = nullptr); @@ -182,6 +187,8 @@ private Q_SLOTS: bool updating_sample_count_; bool sample_count_supported_; + QTimer channel_config_timer_; + std::vector channel_connections_; #ifdef ENABLE_DECODE QToolButton *add_decoder_button_; diff --git a/pv/triggersettings.cpp b/pv/triggersettings.cpp new file mode 100644 index 000000000..3adc99c49 --- /dev/null +++ b/pv/triggersettings.cpp @@ -0,0 +1,108 @@ +/* SPDX-License-Identifier: GPL-3.0-or-later */ +#include "triggersettings.hpp" + +#include +#include +#include +#include + +#include +#include +#include + +namespace pv { + +void save_trigger_settings(QSettings &settings, + const std::shared_ptr &session) +{ + if (!session) + return; + const auto trigger = session->trigger(); + settings.setValue("trigger/version", 1); + settings.setValue("trigger/enabled", !!trigger); + settings.setValue("trigger/name", trigger ? + QString::fromStdString(trigger->name()) : QString()); + QVariantList stages; + if (trigger) { + for (const auto &stage : trigger->stages()) { + QVariantList matches; + for (const auto &match : stage->matches()) { + QVariantMap item; + item["channel"] = match->channel()->index(); + item["channel_type"] = match->channel()->type()->id(); + item["type"] = match->type()->id(); + if (!std::isnan(match->value())) + item["value"] = match->value(); + matches.append(item); + } + QVariantMap item; + item["matches"] = matches; + stages.append(item); + } + } + settings.setValue("trigger/stages", stages); +} + +bool restore_trigger_settings(QSettings &settings, + const std::shared_ptr &context, + const std::shared_ptr &device, + const std::shared_ptr &session) +{ + if (!settings.contains("trigger/version")) + return false; + if (settings.value("trigger/version").toInt() != 1 || + !settings.contains("trigger/enabled") || !context || !device || !session) + throw std::runtime_error("Invalid trigger setup or no capture device."); + const QString enabled = settings.value("trigger/enabled").toString(); + if (enabled != "true" && enabled != "false") + throw std::runtime_error("Invalid trigger enabled state."); + if (enabled == "false") { + session->set_trigger(nullptr); + return true; + } + const auto stages = settings.value("trigger/stages").toList(); + if (stages.empty()) + throw std::runtime_error("Trigger setup has no stages."); + if (!device->config_check(sigrok::ConfigKey::TRIGGER_MATCH, sigrok::Capability::LIST)) + throw std::runtime_error("Device does not list supported trigger conditions."); + auto supported = device->config_list(sigrok::ConfigKey::TRIGGER_MATCH); + gsize size; + const auto types = static_cast( + g_variant_get_fixed_array(supported.gobj(), &size, sizeof(int32_t))); + if (!size) + throw std::runtime_error("Device lists no supported trigger conditions."); + const auto channels = device->channels(); + const auto next = context->create_trigger(settings.value("trigger/name").toString().toStdString()); + for (const auto &stage : stages) { + const auto matches = stage.toMap().value("matches").toList(); + if (matches.empty()) + throw std::runtime_error("Trigger setup has an empty stage."); + const auto next_stage = next->add_stage(); + for (const auto &match : matches) { + const auto item = match.toMap(); + bool channel_ok, type_ok, channel_type_ok; + const auto index = item.value("channel").toUInt(&channel_ok); + const int type = item.value("type").toInt(&type_ok); + const int channel_type = item.value("channel_type").toInt(&channel_type_ok); + const auto channel = std::find_if(channels.begin(), channels.end(), + [index](const std::shared_ptr &c) { return c->index() == index; }); + if (!channel_ok || !type_ok || !channel_type_ok || channel == channels.end() || + (*channel)->type()->id() != channel_type || + std::find(types, types + size, type) == types + size) + throw std::runtime_error("Unsupported trigger channel or condition."); + float value = NAN; + if (item.contains("value")) { + bool value_ok; + value = item.value("value").toFloat(&value_ok); + if (!value_ok || !std::isfinite(value)) + throw std::runtime_error("Invalid trigger threshold value."); + } + next_stage->add_match(*channel, sigrok::TriggerMatchType::get(type), value); + } + } + // All stages have been validated before changing the live session. + session->set_trigger(next); + return true; +} + +} diff --git a/pv/triggersettings.hpp b/pv/triggersettings.hpp new file mode 100644 index 000000000..eaf55cf0a --- /dev/null +++ b/pv/triggersettings.hpp @@ -0,0 +1,28 @@ +/* SPDX-License-Identifier: GPL-3.0-or-later */ +#ifndef PULSEVIEW_PV_TRIGGERSETTINGS_HPP +#define PULSEVIEW_PV_TRIGGERSETTINGS_HPP + +#include + +class QSettings; +namespace sigrok { +class Context; +class Device; +class Session; +} + +namespace pv { + +void save_trigger_settings(QSettings &settings, + const std::shared_ptr &session); + +/* Returns false for legacy setups without trigger settings. Invalid settings + * throw before replacing the current trigger; an explicit disabled trigger + * clears it. */ +bool restore_trigger_settings(QSettings &settings, + const std::shared_ptr &context, + const std::shared_ptr &device, + const std::shared_ptr &session); + +} +#endif diff --git a/pv/views/trace/logicsignal.cpp b/pv/views/trace/logicsignal.cpp index 732d46d2d..51d895f99 100644 --- a/pv/views/trace/logicsignal.cpp +++ b/pv/views/trace/logicsignal.cpp @@ -107,14 +107,8 @@ LogicSignal::LogicSignal(pv::Session &session, shared_ptr base update_logic_level_offsets(); - /* Populate this channel's trigger setting with whatever we - * find in the current session trigger, if anything. */ - trigger_match_ = nullptr; - if (shared_ptr trigger = session_.session()->trigger()) - for (auto stage : trigger->stages()) - for (auto match : stage->matches()) - if (match->channel() == base_->channel()) - trigger_match_ = match->type(); + connect(&session_, SIGNAL(trigger_changed()), this, SLOT(on_session_trigger_changed())); + on_session_trigger_changed(); } std::map LogicSignal::save_settings() const @@ -597,7 +591,7 @@ void LogicSignal::modify_trigger() trigger_match_); } - session_.session()->set_trigger( + session_.set_trigger( new_trigger->stages().empty() ? nullptr : new_trigger); if (owner_) @@ -644,6 +638,25 @@ void LogicSignal::on_setting_changed(const QString &key, const QVariant &value) high_fill_color_ = QColor::fromRgba(value.value()); } +void LogicSignal::on_session_trigger_changed() +{ + trigger_match_ = nullptr; + if (session_.session()) + if (auto trigger = session_.session()->trigger()) + for (const auto &stage : trigger->stages()) + for (const auto &match : stage->matches()) + if (match->channel() == base_->channel()) + trigger_match_ = match->type(); + if (trigger_none_) { + const auto selected = action_from_trigger_type(trigger_match_); + for (auto action : {trigger_none_, trigger_low_, trigger_high_, + trigger_rising_, trigger_falling_, trigger_change_}) + action->setChecked(action == selected); + } + if (owner_) + owner_->row_item_appearance_changed(false, true); +} + void LogicSignal::on_trigger() { QAction *action; diff --git a/pv/views/trace/logicsignal.hpp b/pv/views/trace/logicsignal.hpp index eee6b9c4c..0dc9e39bc 100644 --- a/pv/views/trace/logicsignal.hpp +++ b/pv/views/trace/logicsignal.hpp @@ -131,6 +131,7 @@ protected Q_SLOTS: void on_setting_changed(const QString &key, const QVariant &value); void on_trigger(); + void on_session_trigger_changed(); void on_signal_height_changed(int height); diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index aefe1432b..e7cdcea9c 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -26,6 +26,7 @@ set(pulseview_TEST_SOURCES ${PROJECT_SOURCE_DIR}/pv/mainwindow.cpp ${PROJECT_SOURCE_DIR}/pv/metadata_obj.cpp ${PROJECT_SOURCE_DIR}/pv/session.cpp + ${PROJECT_SOURCE_DIR}/pv/triggersettings.cpp ${PROJECT_SOURCE_DIR}/pv/storesession.cpp ${PROJECT_SOURCE_DIR}/pv/util.cpp ${PROJECT_SOURCE_DIR}/pv/binding/binding.cpp @@ -100,6 +101,7 @@ set(pulseview_TEST_SOURCES data/segment.cpp view/ruler.cpp test.cpp + trigger_settings.cpp util.cpp ) diff --git a/test/trigger_settings.cpp b/test/trigger_settings.cpp new file mode 100644 index 000000000..ea131da78 --- /dev/null +++ b/test/trigger_settings.cpp @@ -0,0 +1,188 @@ +/* SPDX-License-Identifier: GPL-3.0-or-later */ +/* Real QSettings round trips and libsigrok sessions; demo driver only. */ +#include "test/test.hpp" +#include +#include +#include +#include +#include +#include +#include + +namespace { + +struct Fixture { + std::shared_ptr context = sigrok::Context::create(); + std::shared_ptr device; + std::shared_ptr session; + QTemporaryDir directory; + + Fixture() + { + device = context->drivers().at("demo")->scan({ + {sigrok::ConfigKey::NUM_LOGIC_CHANNELS, Glib::Variant::create(32)}, + {sigrok::ConfigKey::NUM_ANALOG_CHANNELS, Glib::Variant::create(1)}, + }).at(0); + device->open(); + session = context->create_session(); + session->add_device(device); + } + + ~Fixture() + { + session->remove_devices(); + session.reset(); + device->close(); + } + + QString filename() const { return directory.filePath("trigger.ini"); } + + std::shared_ptr trigger(unsigned int channel = 0) + { + auto result = context->create_trigger("test"); + result->add_stage()->add_match(device->channels().at(channel), sigrok::TriggerMatchType::RISING); + return result; + } +}; + +std::string snapshot(const std::shared_ptr &trigger) +{ + if (!trigger) + return "none"; + std::string result = trigger->name(); + for (const auto &stage : trigger->stages()) { + result += "/stage"; + for (const auto &match : stage->matches()) { + result += ":" + std::to_string(match->channel()->index()) + + "," + std::to_string(match->type()->id()) + "," + + (std::isnan(match->value()) ? "nan" : std::to_string(match->value())); + } + } + return result; +} + +} + +BOOST_AUTO_TEST_SUITE(TriggerSettings) + +BOOST_AUTO_TEST_CASE(logic_roundtrip) +{ + Fixture f; + for (unsigned int channel : {0, 7, 15, 31}) { + auto trigger = f.trigger(channel); + trigger->stages().at(0)->add_match(f.device->channels().at((channel + 1) % 32), + sigrok::TriggerMatchType::FALLING); + f.session->set_trigger(trigger); + const auto expected = snapshot(trigger); + { + QSettings settings(f.filename(), QSettings::IniFormat); + pv::save_trigger_settings(settings, f.session); + settings.sync(); + BOOST_REQUIRE(settings.status() == QSettings::NoError); + } + f.session->set_trigger(nullptr); + QSettings reopened(f.filename(), QSettings::IniFormat); + BOOST_CHECK(pv::restore_trigger_settings(reopened, f.context, f.device, f.session)); + BOOST_CHECK_EQUAL(snapshot(f.session->trigger()), expected); + } +} + +BOOST_AUTO_TEST_CASE(stages_and_threshold_roundtrip) +{ + Fixture f; + auto trigger = f.trigger(31); + trigger->add_stage()->add_match(f.device->channels().at(32), + sigrok::TriggerMatchType::RISING, 0.75f); + f.session->set_trigger(trigger); + const auto expected = snapshot(trigger); + { + QSettings settings(f.filename(), QSettings::IniFormat); + pv::save_trigger_settings(settings, f.session); + settings.sync(); + } + f.session->set_trigger(nullptr); + QSettings reopened(f.filename(), QSettings::IniFormat); + BOOST_CHECK(pv::restore_trigger_settings(reopened, f.context, f.device, f.session)); + BOOST_CHECK_EQUAL(snapshot(f.session->trigger()), expected); +} + +BOOST_AUTO_TEST_CASE(disabled_trigger_clears_existing) +{ + Fixture f; + QSettings settings(f.filename(), QSettings::IniFormat); + f.session->set_trigger(f.trigger()); + pv::save_trigger_settings(settings, f.session); + f.session->set_trigger(nullptr); + pv::save_trigger_settings(settings, f.session); + f.session->set_trigger(f.trigger(7)); + BOOST_CHECK(pv::restore_trigger_settings(settings, f.context, f.device, f.session)); + BOOST_CHECK(!f.session->trigger()); + BOOST_CHECK(settings.value("trigger/stages").toList().empty()); +} + +BOOST_AUTO_TEST_CASE(legacy_setup_keeps_trigger) +{ + Fixture f; + QSettings settings(f.filename(), QSettings::IniFormat); + auto trigger = f.trigger(); + f.session->set_trigger(trigger); + BOOST_CHECK(!pv::restore_trigger_settings(settings, f.context, f.device, f.session)); + BOOST_CHECK(f.session->trigger() == trigger); +} + +BOOST_AUTO_TEST_CASE(invalid_setup_is_atomic) +{ + Fixture f; + QSettings settings(f.filename(), QSettings::IniFormat); + f.session->set_trigger(f.trigger()); + pv::save_trigger_settings(settings, f.session); + const auto stages = settings.value("trigger/stages").toList(); + auto existing = f.trigger(7); + f.session->set_trigger(existing); + for (const auto &key : {"channel", "channel_type", "type", "value"}) { + auto changed = stages; + auto stage = changed[0].toMap(); + auto matches = stage["matches"].toList(); + auto item = matches[0].toMap(); + item[key] = QString("invalid"); + matches[0] = item; + stage["matches"] = matches; + changed[0] = stage; + settings.setValue("trigger/stages", changed); + BOOST_CHECK_THROW(pv::restore_trigger_settings(settings, f.context, f.device, f.session), std::exception); + BOOST_CHECK(f.session->trigger() == existing); + } + settings.setValue("trigger/stages", QVariantList()); + BOOST_CHECK_THROW(pv::restore_trigger_settings(settings, f.context, f.device, f.session), std::exception); + BOOST_CHECK(f.session->trigger() == existing); +} + +BOOST_AUTO_TEST_CASE(unsupported_condition_keeps_existing) +{ + Fixture f; + QSettings settings(f.filename(), QSettings::IniFormat); + auto analog = f.context->create_trigger("unsupported"); + analog->add_stage()->add_match(f.device->channels().at(32), sigrok::TriggerMatchType::UNDER, 0.5f); + f.session->set_trigger(analog); + pv::save_trigger_settings(settings, f.session); + auto existing = f.trigger(); + f.session->set_trigger(existing); + BOOST_CHECK_THROW(pv::restore_trigger_settings(settings, f.context, f.device, f.session), std::exception); + BOOST_CHECK(f.session->trigger() == existing); +} + +BOOST_AUTO_TEST_CASE(restoring_does_not_enable_channels) +{ + Fixture f; + QSettings settings(f.filename(), QSettings::IniFormat); + f.session->set_trigger(f.trigger(31)); + pv::save_trigger_settings(settings, f.session); + f.device->channels().at(31)->set_enabled(false); + f.session->set_trigger(nullptr); + BOOST_CHECK(pv::restore_trigger_settings(settings, f.context, f.device, f.session)); + BOOST_CHECK(!f.device->channels().at(31)->enabled()); + BOOST_REQUIRE(f.session->trigger()); + BOOST_CHECK_EQUAL(f.session->trigger()->stages().at(0)->matches().at(0)->channel()->index(), 31U); +} + +BOOST_AUTO_TEST_SUITE_END()