Implement A2L presets and custom threshold handling
authorSoeren Apel <soeren@apelpie.net>
Mon, 31 Jul 2017 20:31:20 +0000 (22:31 +0200)
committerUwe Hermann <uwe@hermann-uwe.de>
Tue, 1 Aug 2017 12:42:20 +0000 (14:42 +0200)
pv/data/signalbase.cpp
pv/data/signalbase.hpp
pv/views/trace/analogsignal.cpp
pv/views/trace/analogsignal.hpp

index 0e887b4c8bcd5c38ad2d10a09e3db8a2441cab2e..514459d283449b9617fbf0353962ff18cc974de5 100644 (file)
@@ -44,7 +44,9 @@ const uint64_t SignalBase::ConversionBlockSize = 4096;
 SignalBase::SignalBase(shared_ptr<sigrok::Channel> channel, ChannelType channel_type) :
        channel_(channel),
        channel_type_(channel_type),
-       conversion_type_(NoConversion)
+       conversion_type_(NoConversion),
+       min_value_(0),
+       max_value_(0)
 {
        if (channel_)
                internal_name_ = QString::fromStdString(channel_->name());
@@ -203,6 +205,131 @@ void SignalBase::set_conversion_type(ConversionType t)
        conversion_type_changed(t);
 }
 
+map<QString, QVariant> SignalBase::get_conversion_options() const
+{
+       return conversion_options_;
+}
+
+bool SignalBase::set_conversion_option(QString key, QVariant value)
+{
+       QVariant old_value;
+
+       auto key_iter = conversion_options_.find(key);
+       if (key_iter != conversion_options_.end())
+               old_value = key_iter->second;
+
+       conversion_options_[key] = value;
+
+       return (value != old_value);
+}
+
+vector<double> SignalBase::get_conversion_thresholds(const ConversionType t,
+       const bool always_custom) const
+{
+       vector<double> result;
+       ConversionType conv_type = t;
+       int preset;
+
+       // Use currently active conversion if no conversion type was supplied
+       if (conv_type == NoConversion)
+               conv_type = conversion_type_;
+
+       if (always_custom)
+               preset = -1;
+       else
+               preset = get_current_conversion_preset();
+
+       if (conv_type == A2LConversionByTreshold) {
+               double thr = 0;
+
+               if (preset == -1) {
+                       auto thr_iter = conversion_options_.find("threshold_value");
+                       if (thr_iter != conversion_options_.end())
+                               thr = (thr_iter->second).toDouble();
+               }
+
+               if (preset == 0)
+                       thr = (min_value_ + max_value_) * 0.5;  // middle between min and max
+
+               if (preset == 1) thr = 0.9;
+               if (preset == 2) thr = 1.8;
+               if (preset == 3) thr = 2.5;
+               if (preset == 4) thr = 1.5;
+
+               result.push_back(thr);
+       }
+
+       if (conv_type == A2LConversionBySchmittTrigger) {
+               double thr_lo = 0, thr_hi = 0;
+
+               if (preset == -1) {
+                       auto thr_lo_iter = conversion_options_.find("threshold_value_low");
+                       if (thr_lo_iter != conversion_options_.end())
+                               thr_lo = (thr_lo_iter->second).toDouble();
+
+                       auto thr_hi_iter = conversion_options_.find("threshold_value_high");
+                       if (thr_hi_iter != conversion_options_.end())
+                               thr_hi = (thr_hi_iter->second).toDouble();
+               }
+
+               if (preset == 0) {
+                       const double amplitude = max_value_ - min_value_;
+                       const double center = min_value_ + (amplitude / 2);
+                       thr_lo = center - (amplitude * 0.15);  // 15% margin
+                       thr_hi = center + (amplitude * 0.15);  // 15% margin
+               }
+
+               if (preset == 1) { thr_lo = 0.3; thr_hi = 1.2; }
+               if (preset == 2) { thr_lo = 0.7; thr_hi = 2.5; }
+               if (preset == 3) { thr_lo = 1.3; thr_hi = 3.7; }
+               if (preset == 4) { thr_lo = 0.8; thr_hi = 2.0; }
+
+               result.push_back(thr_lo);
+               result.push_back(thr_hi);
+       }
+
+       return result;
+}
+
+vector< pair<QString, int> > SignalBase::get_conversion_presets() const
+{
+       vector< pair<QString, int> > presets;
+
+       if (conversion_type_ == A2LConversionByTreshold) {
+               // Source: http://www.interfacebus.com/voltage_threshold.html
+               presets.emplace_back(tr("Signal average"), 0);
+               presets.emplace_back(tr("0.9V (for 1.8V CMOS)"), 1);
+               presets.emplace_back(tr("1.8V (for 3.3V CMOS)"), 2);
+               presets.emplace_back(tr("2.5V (for 5.0V CMOS)"), 3);
+               presets.emplace_back(tr("1.5V (for TTL)"), 4);
+       }
+
+       if (conversion_type_ == A2LConversionBySchmittTrigger) {
+               // Source: http://www.interfacebus.com/voltage_threshold.html
+               presets.emplace_back(tr("Signal average +/- 15%"), 0);
+               presets.emplace_back(tr("0.3V/1.2V (for 1.8V CMOS)"), 1);
+               presets.emplace_back(tr("0.7V/2.5V (for 3.3V CMOS)"), 2);
+               presets.emplace_back(tr("1.3V/3.7V (for 5.0V CMOS)"), 3);
+               presets.emplace_back(tr("0.8V/2.0V (for TTL)"), 4);
+       }
+
+       return presets;
+}
+
+int SignalBase::get_current_conversion_preset() const
+{
+       auto preset = conversion_options_.find("preset");
+       if (preset != conversion_options_.end())
+               return (preset->second).toInt();
+
+       return -1;
+}
+
+void SignalBase::set_conversion_preset(int id)
+{
+       conversion_options_["preset"] = id;
+}
+
 #ifdef ENABLE_DECODE
 bool SignalBase::is_decode_signal() const
 {
@@ -216,6 +343,14 @@ void SignalBase::save_settings(QSettings &settings) const
        settings.setValue("enabled", enabled());
        settings.setValue("colour", colour());
        settings.setValue("conversion_type", (int)conversion_type_);
+
+       settings.setValue("conv_options", (int)(conversion_options_.size()));
+       int i = 0;
+       for (auto kvp : conversion_options_) {
+               settings.setValue(QString("conv_option%1_key").arg(i), kvp.first);
+               settings.setValue(QString("conv_option%1_value").arg(i), kvp.second);
+               i++;
+       }
 }
 
 void SignalBase::restore_settings(QSettings &settings)
@@ -224,6 +359,15 @@ void SignalBase::restore_settings(QSettings &settings)
        set_enabled(settings.value("enabled").toBool());
        set_colour(settings.value("colour").value<QColor>());
        set_conversion_type((ConversionType)settings.value("conversion_type").toInt());
+
+       int conv_options = settings.value("conv_options").toInt();
+
+       if (conv_options)
+               for (int i = 0; i < conv_options; i++) {
+                       QString key = settings.value(QString("conv_option%1_key").arg(i)).toString();
+                       QVariant value = settings.value(QString("conv_option%1_value").arg(i));
+                       conversion_options_[key] = value;
+               }
 }
 
 bool SignalBase::conversion_is_a2l() const
@@ -260,8 +404,7 @@ void SignalBase::conversion_thread_proc(QObject* segment)
                        end_sample = asegment->get_sample_count();
 
                        if (end_sample > start_sample) {
-                               float min_v, max_v;
-                               tie(min_v, max_v) = asegment->get_min_max();
+                               tie(min_value_, max_value_) = asegment->get_min_max();
 
                                // Create sigrok::Analog instance
                                float *asamples = new float[ConversionBlockSize];
@@ -285,7 +428,7 @@ void SignalBase::conversion_thread_proc(QObject* segment)
                                uint64_t i = start_sample;
 
                                if (conversion_type_ == A2LConversionByTreshold) {
-                                       const float threshold = (min_v + max_v) * 0.5;  // middle between min and max
+                                       const double threshold = get_conversion_thresholds()[0];
 
                                        // Convert as many sample blocks as we can
                                        while ((end_sample - i) > ConversionBlockSize) {
@@ -314,10 +457,10 @@ void SignalBase::conversion_thread_proc(QObject* segment)
                                }
 
                                if (conversion_type_ == A2LConversionBySchmittTrigger) {
-                                       const float amplitude = max_v - min_v;
-                                       const float center = min_v + (amplitude / 2);
-                                       const float lo_thr = center - (amplitude * 0.15); // 15% margin
-                                       const float hi_thr = center + (amplitude * 0.15); // 15% margin
+                                       const vector<double> thresholds = get_conversion_thresholds();
+                                       const double lo_thr = thresholds[0];
+                                       const double hi_thr = thresholds[1];
+
                                        uint8_t state = 0;  // TODO Use value of logic sample n-1 instead of 0
 
                                        // Convert as many sample blocks as we can
@@ -367,6 +510,9 @@ void SignalBase::start_conversion()
 {
        stop_conversion();
 
+       if (converted_data_)
+               converted_data_->clear();
+
        if (conversion_is_a2l()) {
                shared_ptr<Analog> analog_data = dynamic_pointer_cast<Analog>(data_);
 
index a2d23244f0e1d007aad79543f909fce796c8ab0c..aa734165f8d91eb419a47b4b1d56cb880f0df825 100644 (file)
 #include <atomic>
 #include <condition_variable>
 #include <thread>
+#include <vector>
 
 #include <QColor>
 #include <QObject>
 #include <QSettings>
 #include <QString>
+#include <QVariant>
 
 #include <libsigrokcxx/libsigrokcxx.hpp>
 
 using std::atomic;
 using std::condition_variable;
+using std::map;
 using std::mutex;
+using std::pair;
 using std::shared_ptr;
+using std::vector;
 
 namespace sigrok {
 class Channel;
@@ -164,9 +169,75 @@ public:
 
        /**
         * Changes the kind of conversion performed on this channel.
+        *
+        * Restarts the conversion.
         */
        void set_conversion_type(ConversionType t);
 
+       /**
+        * Returns all currently known conversion options
+        */
+       map<QString, QVariant> get_conversion_options() const;
+
+       /**
+        * Sets the value of a particular conversion option
+        * Note: it is not checked whether the option is valid for the
+        * currently conversion. If it's not, it will be silently ignored.
+        *
+        * Does not restart the conversion.
+        *
+        * @return true if the value is different from before, false otherwise
+        */
+       bool set_conversion_option(QString key, QVariant value);
+
+       /**
+        * Returns the threshold(s) used for conversions, if applicable.
+        * The resulting thresholds are given for the chosen conversion, so you
+        * can query thresholds also for conversions which aren't currently active.
+        *
+        * If you want the thresholds for the currently active conversion,
+        * call it either with NoConversion or no parameter.
+        *
+        * @param t the type of conversion to obtain the thresholds for, leave
+        *          empty or use NoConversion if you want to query the currently
+        *          used conversion
+        *
+        * @param always_custom ignore the currently selected preset and always
+        *        return the custom values for this conversion, using 0 if those
+        *        aren't set
+        *
+        * @return a list of threshold(s) used by the chosen conversion
+        */
+       vector<double> get_conversion_thresholds(
+               const ConversionType t = NoConversion, const bool always_custom=false) const;
+
+       /**
+        * Provides all conversion presets available for the currently active
+        * conversion.
+        *
+        * @return a list of description/ID pairs for each preset
+        */
+       vector<pair<QString, int> > get_conversion_presets() const;
+
+       /**
+        * Determines the ID of the currently used conversion preset, which is only
+        * valid for the currently available conversion presets. It is therefore
+        * suggested to call @ref get_conversion_presets right before calling this.
+        *
+        * @return the ID of the currently used conversion preset. -1 if no preset
+        *         is used. In that case, a user setting is used instead.
+        */
+       int get_current_conversion_preset() const;
+
+       /**
+        * Sets the conversion preset to be used.
+        *
+        * Does not restart the conversion.
+        *
+        * @param id the id of the preset to use
+        */
+       void set_conversion_preset(int id);
+
 #ifdef ENABLE_DECODE
        bool is_decode_signal() const;
 #endif
@@ -175,6 +246,8 @@ public:
 
        virtual void restore_settings(QSettings &settings);
 
+       void start_conversion();
+
 private:
        bool conversion_is_a2l() const;
 
@@ -184,7 +257,6 @@ private:
 
        void conversion_thread_proc(QObject* segment);
 
-       void start_conversion();
        void stop_conversion();
 
 Q_SIGNALS:
@@ -215,6 +287,9 @@ protected:
        shared_ptr<pv::data::SignalData> data_;
        shared_ptr<pv::data::SignalData> converted_data_;
        ConversionType conversion_type_;
+       map<QString, QVariant> conversion_options_;
+
+       float min_value_, max_value_;
 
        std::thread conversion_thread_;
        atomic<bool> conversion_interrupt_;
index c346fd5bce4c8d2bc52f5ae3e0981905f6fa3d13..41c1fba4028f34f53df04f5e970cbdd166e467b0 100644 (file)
@@ -101,6 +101,11 @@ AnalogSignal::AnalogSignal(
        connect(analog_data, SIGNAL(samples_added(QObject*, uint64_t, uint64_t)),
                this, SLOT(on_samples_added()));
 
+       connect(&delayed_conversion_starter_, SIGNAL(timeout()),
+               this, SLOT(on_delayed_conversion_starter()));
+       delayed_conversion_starter_.setSingleShot(true);
+       delayed_conversion_starter_.setInterval(1000);  // 1s timeout
+
        GlobalSettings gs;
        div_height_ = gs.value(GlobalSettings::Key_View_DefaultDivHeight).toInt();
 
@@ -590,6 +595,50 @@ void AnalogSignal::update_scale()
        scale_ = div_height_ / resolution_;
 }
 
+void AnalogSignal::update_conversion_widgets()
+{
+       data::SignalBase::ConversionType conv_type = base_->get_conversion_type();
+
+       // Enable or disable widgets depending on conversion state
+       conv_threshold_cb_->setEnabled(conv_type != data::SignalBase::NoConversion);
+       display_type_cb_->setEnabled(conv_type != data::SignalBase::NoConversion);
+
+       conv_threshold_cb_->clear();
+
+       vector < pair<QString, int> > presets = base_->get_conversion_presets();
+
+       // Prevent the combo box from firing the "edit text changed" signal
+       // as that would involuntarily select the first entry
+       conv_threshold_cb_->blockSignals(true);
+
+       // Set available options depending on chosen conversion
+       for (pair<QString, int> preset : presets)
+               conv_threshold_cb_->addItem(preset.first, preset.second);
+
+       map < QString, QVariant > options = base_->get_conversion_options();
+
+       if (conv_type == data::SignalBase::A2LConversionByTreshold) {
+               const vector<double> thresholds = base_->get_conversion_thresholds(
+                               data::SignalBase::A2LConversionByTreshold, true);
+               conv_threshold_cb_->addItem(
+                               QString("%1V").arg(QString::number(thresholds[0], 'f', 1)), -1);
+       }
+
+       if (conv_type == data::SignalBase::A2LConversionBySchmittTrigger) {
+               const vector<double> thresholds = base_->get_conversion_thresholds(
+                               data::SignalBase::A2LConversionBySchmittTrigger, true);
+               conv_threshold_cb_->addItem(QString("%1V/%2V").arg(
+                               QString::number(thresholds[0], 'f', 1),
+                               QString::number(thresholds[1], 'f', 1)), -1);
+       }
+
+       int preset_id = base_->get_current_conversion_preset();
+       conv_threshold_cb_->setCurrentIndex(
+                       conv_threshold_cb_->findData(preset_id));
+
+       conv_threshold_cb_->blockSignals(false);
+}
+
 void AnalogSignal::perform_autoranging(bool keep_divs, bool force_update)
 {
        const deque< shared_ptr<pv::data::AnalogSegment> > &segments =
@@ -736,12 +785,23 @@ void AnalogSignal::populate_popup_form(QWidget *parent, QFormLayout *form)
        connect(conversion_cb_, SIGNAL(currentIndexChanged(int)),
                this, SLOT(on_conversion_changed(int)));
 
+    // Add the conversion threshold settings
+    conv_threshold_cb_ = new QComboBox();
+    conv_threshold_cb_->setEditable(true);
+
+    layout->addRow(tr("Conversion threshold(s)"), conv_threshold_cb_);
+
+    connect(conv_threshold_cb_, SIGNAL(currentIndexChanged(int)),
+            this, SLOT(on_conv_threshold_changed(int)));
+    connect(conv_threshold_cb_, SIGNAL(editTextChanged(const QString)),
+            this, SLOT(on_conv_threshold_changed()));  // index will be -1
+
        // Add the display type dropdown
        display_type_cb_ = new QComboBox();
 
        display_type_cb_->addItem(tr("Analog"), DisplayAnalog);
        display_type_cb_->addItem(tr("Converted"), DisplayConverted);
-       display_type_cb_->addItem(tr("Both"), DisplayBoth);
+       display_type_cb_->addItem(tr("Analog+Converted"), DisplayBoth);
 
        cur_idx = display_type_cb_->findData(QVariant(display_type_));
        display_type_cb_->setCurrentIndex(cur_idx);
@@ -751,6 +811,9 @@ void AnalogSignal::populate_popup_form(QWidget *parent, QFormLayout *form)
        connect(display_type_cb_, SIGNAL(currentIndexChanged(int)),
                this, SLOT(on_display_type_changed(int)));
 
+       // Update the conversion widget contents and states
+       update_conversion_widgets();
+
        form->addRow(layout);
 }
 
@@ -866,12 +929,91 @@ void AnalogSignal::on_conversion_changed(int index)
 
        if (conv_type != old_conv_type) {
                base_->set_conversion_type(conv_type);
+               update_conversion_widgets();
 
                if (owner_)
                        owner_->row_item_appearance_changed(false, true);
        }
 }
 
+void AnalogSignal::on_conv_threshold_changed(int index)
+{
+       data::SignalBase::ConversionType conv_type = base_->get_conversion_type();
+
+       // Note: index is set to -1 if the text in the combo box matches none of
+       // the entries in the combo box
+
+       if ((index == -1) && (conv_threshold_cb_->currentText().length() == 0))
+               return;
+
+       // The combo box entry with the custom value has user_data set to -1
+       const int user_data = conv_threshold_cb_->findText(
+                       conv_threshold_cb_->currentText());
+
+       const bool use_custom_thr = (index == -1) || (user_data == -1);
+
+       if (conv_type == data::SignalBase::A2LConversionByTreshold && use_custom_thr) {
+               // Not one of the preset values, try to parse the combo box text
+               // Note: Regex loosely based on
+               // https://txt2re.com/index-c++.php3?s=0.1V&1&-13
+               QString re1 = "([+-]?\\d*[\\.,]?\\d*)"; // Float value
+               QString re2 = "([a-zA-Z]*)"; // SI unit
+               QRegExp regex(re1 + re2);
+
+               const QString text = conv_threshold_cb_->currentText();
+               if (!regex.exactMatch(text))
+                       return;  // String doesn't match the regex
+
+               QStringList tokens = regex.capturedTexts();
+
+               // For now, we simply assume that the unit is volt without modifiers
+               const double thr = tokens.at(1).toDouble();
+
+               // Only restart the conversion if the threshold was updated
+               if (base_->set_conversion_option("threshold_value", thr))
+                       delayed_conversion_starter_.start();
+       }
+
+       if (conv_type == data::SignalBase::A2LConversionBySchmittTrigger && use_custom_thr) {
+               // Not one of the preset values, try to parse the combo box text
+               // Note: Regex loosely based on
+               // https://txt2re.com/index-c++.php3?s=0.1V/0.2V&2&14&-22&3&15
+               QString re1 = "([+-]?\\d*[\\.,]?\\d*)"; // Float value
+               QString re2 = "([a-zA-Z]*)"; // SI unit
+               QString re3 = "\\/"; // Forward slash, not captured
+               QString re4 = "([+-]?\\d*[\\.,]?\\d*)"; // Float value
+               QString re5 = "([a-zA-Z]*)"; // SI unit
+               QRegExp regex(re1 + re2 + re3 + re4 + re5);
+
+               const QString text = conv_threshold_cb_->currentText();
+               if (!regex.exactMatch(text))
+                       return;  // String doesn't match the regex
+
+               QStringList tokens = regex.capturedTexts();
+
+               // For now, we simply assume that the unit is volt without modifiers
+               const double low_thr = tokens.at(1).toDouble();
+               const double high_thr = tokens.at(3).toDouble();
+
+               // Only restart the conversion if one of the options was updated
+               bool o1 = base_->set_conversion_option("threshold_value_low", low_thr);
+               bool o2 = base_->set_conversion_option("threshold_value_high", high_thr);
+               if (o1 || o2)
+                       delayed_conversion_starter_.start();
+       }
+
+       base_->set_conversion_preset(index);
+
+       // Immediately start the conversion if we're not asking for a delayed reaction
+       if (!delayed_conversion_starter_.isActive())
+               base_->start_conversion();
+}
+
+void AnalogSignal::on_delayed_conversion_starter()
+{
+       base_->start_conversion();
+}
+
 void AnalogSignal::on_display_type_changed(int index)
 {
        display_type_ = (DisplayType)(display_type_cb_->itemData(index).toInt());
index 07b667119b6a7e78a9633971aa2afaa1da9a9075..7dc1e0d9de9917135c7e1dbb60747e1fbdfca11f 100644 (file)
@@ -26,6 +26,7 @@
 
 #include <QComboBox>
 #include <QSpinBox>
+#include <QTimer>
 
 using std::pair;
 using std::shared_ptr;
@@ -144,6 +145,8 @@ private:
 
        void update_scale();
 
+       void update_conversion_widgets();
+
        void perform_autoranging(bool keep_divs, bool force_update);
 
 protected:
@@ -161,13 +164,18 @@ private Q_SLOTS:
        void on_autoranging_changed(int state);
 
        void on_conversion_changed(int index);
+       void on_conv_threshold_changed(int index=-1);
+       void on_delayed_conversion_starter();
 
        void on_display_type_changed(int index);
 
 private:
-       QComboBox *resolution_cb_, *conversion_cb_, *display_type_cb_;
+       QComboBox *resolution_cb_, *conversion_cb_, *conv_threshold_cb_,
+               *display_type_cb_;
        QSpinBox *pvdiv_sb_, *nvdiv_sb_, *div_height_sb_;
 
+       QTimer delayed_conversion_starter_;
+
        float scale_;
        int scale_index_;
        int scale_index_drag_offset_;