]> sigrok.org Git - pulseview.git/blame - pv/views/trace/mathsignal.cpp
Session: Fix issue #67 by improving error handling
[pulseview.git] / pv / views / trace / mathsignal.cpp
CommitLineData
4640a84e
SA
1/*
2 * This file is part of the PulseView project.
3 *
4 * Copyright (C) 2020 Soeren Apel <soeren@apelpie.net>
5 *
6 * This program is free software; you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License as published by
8 * the Free Software Foundation; either version 2 of the License, or
9 * (at your option) any later version.
10 *
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU General Public License for more details.
15 *
16 * You should have received a copy of the GNU General Public License
17 * along with this program; if not, see <http://www.gnu.org/licenses/>.
18 */
19
20#include <extdef.h>
21
22#include <QComboBox>
23#include <QDebug>
86eeec3b 24#include <QDialogButtonBox>
4640a84e
SA
25#include <QFormLayout>
26#include <QGridLayout>
2823de2c 27#include <QLabel>
4640a84e 28#include <QLineEdit>
2823de2c 29#include <QPushButton>
4640a84e 30#include <QString>
2823de2c
SA
31#include <QTabWidget>
32#include <QVBoxLayout>
4640a84e
SA
33
34#include "mathsignal.hpp"
35
36#include "pv/data/signalbase.hpp"
37
38using pv::data::SignalBase;
39
40namespace pv {
41namespace views {
42namespace trace {
43
44#define MATHSIGNAL_INPUT_TIMEOUT (2000)
45
2823de2c
SA
46const vector< pair<string, string> > MathEditDialog::Examples = {
47 {"Product of signals named A1 and A2:",
48 "A1 * A2"},
49 {"Create a sine wave with f=1MHz:",
50 "sin(1e6 * 2 * pi * t)"},
51 {"Average the past 4 samples of A2:",
52 "var v := 0;\n" \
53 "var n := 4;\n\n" \
54 "for (var i := 0; i < n; i += 1) {\n" \
55 "\tv += sample('A2', s - i) / n;\n" \
56 "}"},
57 {"Create a <a href=https://en.wikipedia.org/wiki/Chirp#Linear>frequency sweep</a> from 1Hz to 1MHz over 10 seconds:",
58 "var f_min := 1;\n" \
59 "var f_max := 1e6;\n" \
60 "var duration := 10;\n" \
61 "var gradient := (f_max - f_min) / duration;\n" \
62 "// phase is the antiderivative of (gradient * t + f_min) dt\n" \
63 "var phase := gradient * (t ^ 2 / 2) + f_min * t;\n" \
64 "sin(2 * pi * phase)"},
65 {"Single-pole low-pass IIR filter, see " \
66 "<a href=https://tomroelandts.com/articles/low-pass-single-pole-iir-filter>this</a> " \
67 "and <a href=https://dsp.stackexchange.com/questions/34969/cutoff-frequency-of-a-first-order-recursive-filter>this</a> article:",
68 "// -3db point is set to 10% of the sampling frequency\n" \
69 "var f_c := 0.1;\n" \
70 "var omega_c := 2 * pi * f_c;\n" \
71 "var b := 2 - cos(omega_c) - sqrt(((2 - cos(omega_c)) ^ 2) - 1);\n" \
72 "var a := 1 - b;\n\n" \
73 "// formula is y[n] = ax[n] + (1 - a) * y[n - 1]\n" \
74 "// x[n] becomes the input signal, here A4\n" \
75 "// y[n - 1] becomes sample('Math1', s - 1)\n" \
76 "a * A4 + (1 - a) * sample('Math1', s - 1)"}
77};
78
86eeec3b
SA
79
80MathEditDialog::MathEditDialog(pv::Session &session,
81 shared_ptr<pv::data::MathSignal> math_signal, QWidget *parent) :
82 QDialog(parent),
83 session_(session),
84 math_signal_(math_signal),
2823de2c
SA
85 old_expression_(math_signal->get_expression()),
86 expr_edit_(new QPlainTextEdit())
86eeec3b
SA
87{
88 setWindowTitle(tr("Math Expression Editor"));
89
2823de2c
SA
90 // Create the pages
91 QWidget *basics_page = new QWidget();
92 QVBoxLayout *basics_layout = new QVBoxLayout(basics_page);
93 basics_layout->addWidget(new QLabel("<b>" + tr("Inputs:") + "</b>"));
94 basics_layout->addWidget(new QLabel("signal_name\tCurrent value of signal signal_name (e.g. A0 or CH1)"));
95 basics_layout->addWidget(new QLabel("T\t\tSampling interval (= 1 / sample rate)"));
96 basics_layout->addWidget(new QLabel("t\t\tCurrent time in seconds"));
97 basics_layout->addWidget(new QLabel("s\t\tCurrent sample number"));
98 basics_layout->addWidget(new QLabel("sample('s', n)\tValue of sample #n of the signal named s"));
99 basics_layout->addWidget(new QLabel("<b>" + tr("Variables:") + "</b>"));
100 basics_layout->addWidget(new QLabel("var x;\t\tCreate variable\nvar x := 5;\tCreate variable with initial value"));
101 basics_layout->addWidget(new QLabel("<b>" + tr("Basic operators:") + "</b>"));
102 basics_layout->addWidget(new QLabel("x + y\t\tAddition of x and y"));
103 basics_layout->addWidget(new QLabel("x - y\t\tSubtraction of x and y"));
104 basics_layout->addWidget(new QLabel("x * y\t\tMultiplication of x and y"));
105 basics_layout->addWidget(new QLabel("x / y\t\tDivision of x and y"));
106 basics_layout->addWidget(new QLabel("x % y\t\tRemainder of division x / y"));
107 basics_layout->addWidget(new QLabel("x ^ y\t\tx to the power of y"));
108 basics_layout->addWidget(new QLabel("<b>" + tr("Assignments:") + "</b>"));
109 basics_layout->addWidget(new QLabel("x := y\t\tAssign the value of y to x"));
110 basics_layout->addWidget(new QLabel("x += y\t\tIncrement x by y"));
111 basics_layout->addWidget(new QLabel("x -= y\t\tDecrement x by y"));
112 basics_layout->addWidget(new QLabel("x *= y\t\tMultiply x by y"));
113 basics_layout->addWidget(new QLabel("x /= y\t\tDivide x by y"));
114 basics_layout->addWidget(new QLabel("x %= y\t\tSet x to the value of x % y"));
115
116 QWidget *func1_page = new QWidget();
117 QVBoxLayout *func1_layout = new QVBoxLayout(func1_page);
118 func1_layout->addWidget(new QLabel("<b>" + tr("General purpose functions:") + "</b>"));
119 func1_layout->addWidget(new QLabel(tr("abs(x)\t\tAbsolute value of x")));
120 func1_layout->addWidget(new QLabel(tr("avg(x, y, ...)\tAverage of all input values")));
121 func1_layout->addWidget(new QLabel(tr("ceil(x)\t\tSmallest integer that is greater than or equal to x")));
122 func1_layout->addWidget(new QLabel(tr("clamp(lb, x, ub)\tClamp x in range between lb and ub, where lb < ub")));
123 func1_layout->addWidget(new QLabel(tr("equal(x, y)\tEquality test between x and y using normalised epsilon")));
124 func1_layout->addWidget(new QLabel(tr("erf(x)\t\tError function of x")));
125 func1_layout->addWidget(new QLabel(tr("erfc(x)\t\tComplimentary error function of x")));
126 func1_layout->addWidget(new QLabel(tr("exp(x)\t\te to the power of x")));
127 func1_layout->addWidget(new QLabel(tr("expm1(x)\te to the power of x minus 1, where x is very small.")));
128 func1_layout->addWidget(new QLabel(tr("floor(x)\t\tLargest integer that is less than or equal to x")));
129 func1_layout->addWidget(new QLabel(tr("frac(x)\t\tFractional portion of x")));
130 func1_layout->addWidget(new QLabel(tr("hypot(x)\t\tHypotenuse of x and y (i.e. sqrt(x*x + y*y))")));
131 func1_layout->addWidget(new QLabel(tr("iclamp(lb, x, ub)\tInverse-clamp x outside of the range lb and ub, where lb < ub.\n\t\tIf x is within the range it will snap to the closest bound")));
132 func1_layout->addWidget(new QLabel(tr("inrange(lb, x, ub)\tIn-range returns true when x is within the range lb and ub, where lb < ub.")));
133 func1_layout->addWidget(new QLabel(tr("log(x)\t\tNatural logarithm of x")));
134 func1_layout->addWidget(new QLabel(tr("log10(x)\t\tBase 10 logarithm of x")));
135
136 QWidget *func2_page = new QWidget();
137 QVBoxLayout *func2_layout = new QVBoxLayout(func2_page);
138 func2_layout->addWidget(new QLabel(tr("log1p(x)\t\tNatural logarithm of 1 + x, where x is very small")));
139 func2_layout->addWidget(new QLabel(tr("log2(x)\t\tBase 2 logarithm of x")));
140 func2_layout->addWidget(new QLabel(tr("logn(x)\t\tBase N logarithm of x, where n is a positive integer")));
141 func2_layout->addWidget(new QLabel(tr("max(x, y, ...)\tLargest value of all the inputs")));
142 func2_layout->addWidget(new QLabel(tr("min(x, y, ...)\tSmallest value of all the inputs")));
143 func2_layout->addWidget(new QLabel(tr("mul(x, y, ...)\tProduct of all the inputs")));
144 func2_layout->addWidget(new QLabel(tr("ncdf(x)\t\tNormal cumulative distribution function")));
145 func2_layout->addWidget(new QLabel(tr("nequal(x, y)\tNot-equal test between x and y using normalised epsilon")));
146 func2_layout->addWidget(new QLabel(tr("pow(x, y)\tx to the power of y")));
147 func2_layout->addWidget(new QLabel(tr("root(x, n)\tNth-Root of x, where n is a positive integer")));
148 func2_layout->addWidget(new QLabel(tr("round(x)\t\tRound x to the nearest integer")));
149 func2_layout->addWidget(new QLabel(tr("roundn(x, n)\tRound x to n decimal places, where n > 0 and is an integer")));
150 func2_layout->addWidget(new QLabel(tr("sgn(x)\t\tSign of x; -1 if x < 0, +1 if x > 0, else zero")));
151 func2_layout->addWidget(new QLabel(tr("sqrt(x)\t\tSquare root of x, where x >= 0")));
152 func2_layout->addWidget(new QLabel(tr("sum(x, y, ..,)\tSum of all the inputs")));
153 func2_layout->addWidget(new QLabel(tr("swap(x, y)\tSwap the values of the variables x and y and return the current value of y")));
154 func2_layout->addWidget(new QLabel(tr("trunc(x)\t\tInteger portion of x")));
155
156 QWidget *trig_page = new QWidget();
157 QVBoxLayout *trig_layout = new QVBoxLayout(trig_page);
158 trig_layout->addWidget(new QLabel("<b>" + tr("Trigonometry functions:") + "</b>"));
159 trig_layout->addWidget(new QLabel(tr("acos(x)\t\tArc cosine of x expressed in radians. Interval [-1,+1]")));
160 trig_layout->addWidget(new QLabel(tr("acosh(x)\t\tInverse hyperbolic cosine of x expressed in radians")));
161 trig_layout->addWidget(new QLabel(tr("asin(x)\t\tArc sine of x expressed in radians. Interval [-1,+1]")));
162 trig_layout->addWidget(new QLabel(tr("asinh(x)\t\tInverse hyperbolic sine of x expressed in radians")));
163 trig_layout->addWidget(new QLabel(tr("atan(x)\t\tArc tangent of x expressed in radians. Interval [-1,+1]")));
164 trig_layout->addWidget(new QLabel(tr("atan2(x, y)\tArc tangent of (x / y) expressed in radians. [-pi,+pi] ")));
165 trig_layout->addWidget(new QLabel(tr("atanh(x)\t\tInverse hyperbolic tangent of x expressed in radians")));
166 trig_layout->addWidget(new QLabel(tr("cos(x)\t\tCosine of x")));
167 trig_layout->addWidget(new QLabel(tr("cosh(x)\t\tHyperbolic cosine of x")));
168 trig_layout->addWidget(new QLabel(tr("cot(x)\t\tCotangent of x")));
169 trig_layout->addWidget(new QLabel(tr("csc(x)\t\tCosectant of x")));
170 trig_layout->addWidget(new QLabel(tr("sec(x)\t\tSecant of x")));
171 trig_layout->addWidget(new QLabel(tr("sin(x)\t\tSine of x")));
172 trig_layout->addWidget(new QLabel(tr("sinc(x)\t\tSine cardinal of x")));
173 trig_layout->addWidget(new QLabel(tr("sinh(x)\t\tHyperbolic sine of x")));
174 trig_layout->addWidget(new QLabel(tr("tan(x)\t\tTangent of x")));
175 trig_layout->addWidget(new QLabel(tr("tanh(x)\t\tHyperbolic tangent of x")));
176 trig_layout->addWidget(new QLabel(tr("deg2rad(x)\tConvert x from degrees to radians")));
177 trig_layout->addWidget(new QLabel(tr("deg2grad(x)\tConvert x from degrees to gradians")));
178 trig_layout->addWidget(new QLabel(tr("rad2deg(x)\tConvert x from radians to degrees")));
179 trig_layout->addWidget(new QLabel(tr("grad2deg(x)\tConvert x from gradians to degrees")));
180
181 QWidget *logic_page = new QWidget();
182 QVBoxLayout *logic_layout = new QVBoxLayout(logic_page);
183 logic_layout->addWidget(new QLabel("<b>" + tr("Logic operators:") + "</b>"));
184 logic_layout->addWidget(new QLabel("true\t\tTrue state or any value other than zero (typically 1)"));
185 logic_layout->addWidget(new QLabel("false\t\tFalse state, value of exactly zero"));
186 logic_layout->addWidget(new QLabel("x and y\t\tLogical AND, true only if x and y are both true"));
187 logic_layout->addWidget(new QLabel("mand(x, y, z, ...)\tMulti-input logical AND, true only if all inputs are true"));
188 logic_layout->addWidget(new QLabel("x or y\t\tLogical OR, true if either x or y is true"));
189 logic_layout->addWidget(new QLabel("mor(x, y, z, ...)\tMulti-input logical OR, true if at least one of the inputs is true"));
190 logic_layout->addWidget(new QLabel("x nand y\t\tLogical NAND, true only if either x or y is false"));
191 logic_layout->addWidget(new QLabel("x nor y\t\tLogical NOR, true only if the result of x or y is false"));
192 logic_layout->addWidget(new QLabel("not(x)\t\tLogical NOT, negate the logical sense of the input"));
193 logic_layout->addWidget(new QLabel("x xor y\t\tLogical NOR, true only if the logical states of x and y differ"));
194 logic_layout->addWidget(new QLabel("x xnor y\t\tLogical NOR, true only if x and y have the same state"));
195 logic_layout->addWidget(new QLabel("x & y\t\tSame as AND but with left to right expression short circuiting optimisation"));
196 logic_layout->addWidget(new QLabel("x | y\t\tSame as OR but with left to right expression short circuiting optimisation"));
197
198 QWidget *control1_page = new QWidget();
199 QVBoxLayout *control1_layout = new QVBoxLayout(control1_page);
200 control1_layout->addWidget(new QLabel("<b>" + tr("Comparisons:") + "</b>"));
201 control1_layout->addWidget(new QLabel(tr("x = y or x == y\tTrue only if x is strictly equal to y")));
202 control1_layout->addWidget(new QLabel(tr("x <> y or x != y\tTrue only if x does not equal y")));
203 control1_layout->addWidget(new QLabel(tr("x < y\t\tTrue only if x is less than y")));
204 control1_layout->addWidget(new QLabel(tr("x <= y\t\tTrue only if x is less than or equal to y")));
205 control1_layout->addWidget(new QLabel(tr("x > y\t\tTrue only if x is greater than y")));
206 control1_layout->addWidget(new QLabel(tr("x >= y\t\tTrue only if x is greater than or equal to y")));
207 control1_layout->addWidget(new QLabel("<b>" + tr("Flow control:") + "</b>"));
208 control1_layout->addWidget(new QLabel(tr("{ ... }\t\tBeginning and end of instruction block")));
209 control1_layout->addWidget(new QLabel(tr("if (x, y, z)\tIf x is true then return y else return z\nif (x) y;\t\t\tvariant without implied else\nif (x) { y };\t\tvariant with an instruction block\nif (x) y; else z;\t\tvariant with explicit else\nif (x) { y } else { z };\tvariant with instruction blocks")));
210 control1_layout->addWidget(new QLabel(tr("x ? y : z\tTernary operator, equivalent to 'if (x, y, z)'")));
211 control1_layout->addWidget(new QLabel(tr("switch {\t\tThe first true case condition that is encountered will\n case x > 1: a;\tdetermine the result of the switch. If none of the case\n case x < 1: b;\tconditions hold true, the default action is used\n default: c;\tto determine the return value\n}")));
212
213 QWidget *control2_page = new QWidget();
214 QVBoxLayout *control2_layout = new QVBoxLayout(control2_page);
215 control2_layout->addWidget(new QLabel(tr("while (conditon) {\tEvaluates expression repeatedly as long as condition is true,\n expression;\treturning the last value of expression\n}")));
216 control2_layout->addWidget(new QLabel(tr("repeat\t\tEvalues expression repeatedly as long as condition is false,\n expression;\treturning the last value of expression\nuntil (condition)\n")));
217 control2_layout->addWidget(new QLabel(tr("for (var x := 0; condition; x += 1) {\tRepeatedly evaluates expression while the condition is true,\n expression\t\t\twhile evaluating the 'increment' expression on each loop\n}")));
218 control2_layout->addWidget(new QLabel(tr("break\t\tTerminates the execution of the nearest enclosed loop, returning NaN")));
219 control2_layout->addWidget(new QLabel(tr("break[x]\t\tTerminates the execution of the nearest enclosed loop, returning x")));
220 control2_layout->addWidget(new QLabel(tr("continue\t\tInterrupts loop execution and resumes with the next loop iteration")));
221 control2_layout->addWidget(new QLabel(tr("return[x]\t\tReturns immediately from within the current expression, returning x")));
222 control2_layout->addWidget(new QLabel(tr("~(expr; expr; ...)\tEvaluates each sub-expression and returns the value of the last one\n~{expr; expr; ...}")));
223
224 QWidget *example_page = new QWidget();
225 QVBoxLayout *example_layout = new QVBoxLayout(example_page);
226 for (const pair<string, string> &entry : Examples) {
227 const string &desc = entry.first;
228 const string &example = entry.second;
229
230 QLabel *desc_label = new QLabel("<b>" + tr(desc.c_str()) + "</b>");
231 desc_label->setTextInteractionFlags(Qt::TextSelectableByMouse | Qt::LinksAccessibleByMouse | Qt::LinksAccessibleByKeyboard);
232 desc_label->setOpenExternalLinks(true);
233 example_layout->addWidget(desc_label);
234 QPushButton *btn = new QPushButton(tr("Copy to expression"));
235 connect(btn, &QPushButton::clicked, this, [&]() { set_expr(QString::fromStdString(example)); });
236 QGridLayout *gridlayout = new QGridLayout();
237 gridlayout->addWidget(new QLabel(QString::fromStdString(example)), 0, 0, Qt::AlignLeft);
238 gridlayout->addWidget(btn, 0, 1, Qt::AlignRight);
239 example_layout->addLayout(gridlayout);
240 }
241
86eeec3b
SA
242 // Create the rest of the dialog
243 QDialogButtonBox *button_box = new QDialogButtonBox(
244 QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
245
2823de2c
SA
246 QTabWidget *tabs = new QTabWidget();
247 tabs->addTab(basics_page, tr("Basics"));
248 tabs->addTab(func1_page, tr("Functions 1"));
249 tabs->addTab(func2_page, tr("Functions 2"));
250 tabs->addTab(trig_page, tr("Trigonometry"));
251 tabs->addTab(logic_page, tr("Logic"));
252 tabs->addTab(control1_page, tr("Flow Control 1"));
253 tabs->addTab(control2_page, tr("Flow Control 2"));
254 tabs->addTab(example_page, tr("Examples"));
255
256 QVBoxLayout *root_layout = new QVBoxLayout(this);
257 root_layout->addWidget(tabs);
258 root_layout->addWidget(expr_edit_);
86eeec3b
SA
259 root_layout->addWidget(button_box);
260
2823de2c 261 // Set tab width to 4 characters
ffad6cd6
SA
262#if QT_VERSION >= QT_VERSION_CHECK(5, 10, 0)
263 expr_edit_->setTabStopDistance(util::text_width(QFontMetrics(font()), "XXXX"));
264#else
2823de2c 265 expr_edit_->setTabStopWidth(util::text_width(QFontMetrics(font()), "XXXX"));
ffad6cd6 266#endif
2823de2c 267
86eeec3b
SA
268 connect(button_box, SIGNAL(accepted()), this, SLOT(accept()));
269 connect(button_box, SIGNAL(rejected()), this, SLOT(reject()));
270}
271
2823de2c
SA
272void MathEditDialog::set_expr(const QString &expr)
273{
274 expr_edit_->document()->setPlainText(expr);
275}
276
86eeec3b
SA
277void MathEditDialog::accept()
278{
2823de2c 279 math_signal_->set_expression(expr_edit_->document()->toPlainText());
86eeec3b
SA
280 QDialog::accept();
281}
282
283void MathEditDialog::reject()
284{
285 math_signal_->set_expression(old_expression_);
286 QDialog::reject();
287}
288
289
4640a84e
SA
290MathSignal::MathSignal(
291 pv::Session &session,
292 shared_ptr<data::SignalBase> base) :
293 AnalogSignal(session, base),
294 math_signal_(dynamic_pointer_cast<pv::data::MathSignal>(base))
295{
296 delayed_expr_updater_.setSingleShot(true);
297 delayed_expr_updater_.setInterval(MATHSIGNAL_INPUT_TIMEOUT);
298 connect(&delayed_expr_updater_, &QTimer::timeout,
299 this, [this]() { math_signal_->set_expression(expression_edit_->text()); });
300}
301
302void MathSignal::populate_popup_form(QWidget *parent, QFormLayout *form)
303{
304 AnalogSignal::populate_popup_form(parent, form);
305
306 expression_edit_ = new QLineEdit();
307 expression_edit_->setText(math_signal_->get_expression());
86eeec3b
SA
308
309 const QIcon edit_icon(QIcon::fromTheme("edit", QIcon(":/icons/math.svg")));
310 QAction *edit_action =
311 expression_edit_->addAction(edit_icon, QLineEdit::TrailingPosition);
312
4640a84e
SA
313 connect(expression_edit_, SIGNAL(textEdited(QString)),
314 this, SLOT(on_expression_changed(QString)));
86eeec3b
SA
315 connect(edit_action, SIGNAL(triggered(bool)),
316 this, SLOT(on_edit_clicked()));
4640a84e
SA
317 form->addRow(tr("Expression"), expression_edit_);
318
319 sample_count_cb_ = new QComboBox();
320 sample_count_cb_->setEditable(true);
321 sample_count_cb_->addItem(tr("same as session"));
322 sample_count_cb_->addItem(tr("100"));
323 sample_count_cb_->addItem(tr("10000"));
324 sample_count_cb_->addItem(tr("1000000"));
325 connect(sample_count_cb_, SIGNAL(editTextChanged(QString)),
326 this, SLOT(on_sample_count_changed(QString)));
327 form->addRow(tr("Number of Samples"), sample_count_cb_);
328
329 sample_rate_cb_ = new QComboBox();
330 sample_rate_cb_->setEditable(true);
331 sample_rate_cb_->addItem(tr("same as session"));
332 sample_rate_cb_->addItem(tr("100"));
333 sample_rate_cb_->addItem(tr("10000"));
334 sample_rate_cb_->addItem(tr("1000000"));
335 form->addRow(tr("Sample rate"), sample_rate_cb_);
336}
337
338void MathSignal::on_expression_changed(const QString &text)
339{
340 (void)text;
341
342 // Restart update timer
343 delayed_expr_updater_.start();
344}
345
346void MathSignal::on_sample_count_changed(const QString &text)
347{
348 (void)text;
349}
350
86eeec3b
SA
351void MathSignal::on_edit_clicked()
352{
353 MathEditDialog dlg(session_, math_signal_);
2823de2c 354 dlg.set_expr(expression_edit_->text());
86eeec3b
SA
355
356 dlg.exec();
357}
358
4640a84e
SA
359} // namespace trace
360} // namespace views
361} // namespace pv