Browse Source

Difference and Mean derived series.

Neal Wilson 6 years ago
parent
commit
7681fbe184
3 changed files with 318 additions and 0 deletions
  1. 31
    0
      config/Windows/productionroaster.xml
  2. 285
    0
      src/mergeseries.w
  3. 2
    0
      src/typica.w

+ 31
- 0
config/Windows/productionroaster.xml View File

@@ -697,6 +697,37 @@
697 697
 						}
698 698
 					}
699 699
 				}
700
+				else if(driverReference.driver == "mergeseries")
701
+				{
702
+					var mergeSeries;
703
+					var indicator = new TemperatureDisplay;
704
+					if(driverReference.type == "Difference") {
705
+						mergeSeries = new DifferenceSeries();
706
+						indicator.setRelativeMode(true);
707
+					} else {
708
+						mergeSeries = new MeanSeries();
709
+					}
710
+					var firstColumn = driverReference.column1;
711
+					var secondColumn = driverReference.column2;
712
+					for(var j = 0; j < columnNames.length; j++) {
713
+						if(columnNames[j] == firstColumn) {
714
+							channels[j].newData.connect(mergeSeries.in1);
715
+						} else if (columnNames[j] == secondColumn) {
716
+							channels[j].newData.connect(mergeSeries.in2);
717
+						}
718
+					}
719
+					indicator.display(0);
720
+					indicator.digitCount = 9;
721
+					var decorator = new WidgetDecorator(indicator, configModel.data(driverIndex, 0), 2);
722
+					indicatorPanel.addWidget(decorator);
723
+					mergeSeries.newData.connect(indicator.setValue);
724
+					temperatureDisplays.push(indicator);
725
+					channels.push(mergeSeries);
726
+					// This cannot at present be configured to be hidden.
727
+					channelVisibility.push(true);
728
+					channelType.push("T");
729
+					columnNames.push(driverReference.output);
730
+				}
700 731
 				else if(driverReference.driver == "scale")
701 732
 				{
702 733
 					var scale = new SerialScale(driverReference.port);

+ 285
- 0
src/mergeseries.w View File

@@ -0,0 +1,285 @@
1
+@** Difference and Mean.
2
+
3
+\noindent Some roasters find value in comparing the difference between
4
+measurements on different data series. For example, the difference between
5
+intake and exhaust air or the difference between air and seed temperatures.
6
+These can be expressed as an additional relative temperature series. Similarly,
7
+a roaster might have multiple thermocouples where neither is individually as
8
+reliable as taking the mean between the two. The difference and mean series
9
+allow either of these to be calculated automatically.
10
+
11
+Since these are very similar structurally, a common base class is provided
12
+for performing some calculation based on two inputs. Difference and mean
13
+series then only need to override the |calculate()| method.
14
+
15
+@<Class declarations@>=
16
+class MergeSeries : public QObject
17
+{
18
+	Q_OBJECT
19
+	public:
20
+		MergeSeries();
21
+	public slots:
22
+		void in1(Measurement measure);
23
+		void in2(Measurement measure);
24
+	signals:
25
+		void newData(Measurement measure);
26
+	protected:
27
+		virtual void calculate(Measurement m1, Measurement m2) = 0;
28
+	private:
29
+		Measurement last1;
30
+		Measurement last2;
31
+		bool received1;
32
+		bool received2;
33
+};
34
+
35
+class DifferenceSeries : public MergeSeries
36
+{
37
+	Q_OBJECT
38
+	public:
39
+		DifferenceSeries();
40
+	protected:
41
+		void calculate(Measurement m1, Measurement m2);
42
+};
43
+
44
+class MeanSeries : public MergeSeries
45
+{
46
+	Q_OBJECT
47
+	public:
48
+		MeanSeries();
49
+	protected:
50
+		void calculate(Measurement m1, Measurement m2);
51
+};
52
+
53
+@ Classes derived from |MergeSeries| will wait until there is a measurement
54
+from both series and then call |calculate()| with the most recent measurement
55
+from each. This allows the use of measurements from sources with different
56
+sample rates, though there is no guarantee that measurements with the closest
57
+timestamps will be paired. This is done to minimize latency on the series.
58
+
59
+@<MergeSeries implementation@>=
60
+void MergeSeries::in1(Measurement measure)
61
+{
62
+	last1 = measure;
63
+	received1 = true;
64
+	if(received1 && received2)
65
+	{
66
+		calculate(last1, last2);
67
+		received1 = false;
68
+		received2 = false;
69
+	}
70
+}
71
+
72
+void MergeSeries::in2(Measurement measure)
73
+{
74
+	last2 = measure;
75
+	received2 = true;
76
+	if(received1 && received2)
77
+	{
78
+		calculate(last1, last2);
79
+		received1 = false;
80
+		received2 = false;
81
+	}
82
+}
83
+
84
+@ The constructor just needs to set the initial |bool|s to |false|.
85
+
86
+@<MergeSeries implementation@>=
87
+MergeSeries::MergeSeries() : QObject(NULL), received1(false), received2(false)
88
+{
89
+	/* Nothing needs to be done here. */
90
+}
91
+
92
+@ The calculations will emit a new |Measurement| with the calculated
93
+temperature value. If the measurement times are different, the highest value
94
+is used. A difference needs to be treated as a relative measurement whereas a
95
+mean does not.
96
+
97
+@<MergeSeries implementation@>=
98
+void DifferenceSeries::calculate(Measurement m1, Measurement m2)
99
+{
100
+	Measurement outval(m1.temperature() - m2.temperature(),
101
+	                   (m1.time() > m2.time() ? m1.time() : m2.time()));
102
+	outval.insert("relative", true);
103
+	emit newData(outval);
104
+}
105
+
106
+void MeanSeries::calculate(Measurement m1, Measurement m2)
107
+{
108
+	Measurement outval((m1.temperature() + m2.temperature()) / 2,
109
+                       (m1.time() > m2.time() ? m1.time() : m2.time()));
110
+	emit newData(outval);
111
+}
112
+
113
+@ Nothing special needs to happen in the constructors.
114
+
115
+@<MergeSeries implementation@>=
116
+DifferenceSeries::DifferenceSeries() : MergeSeries()
117
+{
118
+	/* Nothing needs to be done here. */
119
+}
120
+
121
+MeanSeries::MeanSeries() : MergeSeries()
122
+{
123
+	/* Nothing needs to be done here. */
124
+}
125
+
126
+@ The base class does not need to be exposed to the host environment, but the
127
+derived classes do.
128
+
129
+@<Function prototypes for scripting@>=
130
+QScriptValue constructDifferenceSeries(QScriptContext *context,
131
+                                       QScriptEngine *engine);
132
+QScriptValue constructMeanSeries(QScriptContext *context,
133
+                                 QScriptEngine *engine);
134
+
135
+@ The constructors are registered in the usual way.
136
+
137
+@<Set up the scripting engine@>=
138
+constructor = engine->newFunction(constructDifferenceSeries);
139
+value = engine->newQMetaObject(&DifferenceSeries::staticMetaObject,
140
+                               constructor);
141
+engine->globalObject().setProperty("DifferenceSeries", value);
142
+constructor = engine->newFunction(constructMeanSeries);
143
+value = engine->newQMetaObject(&MeanSeries::staticMetaObject, constructor);
144
+engine->globalObject().setProperty("MeanSeries", value);
145
+
146
+@ The constructors are trivial.
147
+
148
+@<Functions for scripting@>=
149
+QScriptValue constructDifferenceSeries(QScriptContext *, QScriptEngine *engine)
150
+{
151
+	QScriptValue object = engine->newQObject(new DifferenceSeries);
152
+	setQObjectProperties(object, engine);
153
+	return object;
154
+}
155
+
156
+QScriptValue constructMeanSeries(QScriptContext *, QScriptEngine *engine)
157
+{
158
+	QScriptValue object = engine->newQObject(new MeanSeries);
159
+	setQObjectProperties(object, engine);
160
+	return object;
161
+}
162
+
163
+@ Both of these require configuration, however since these are structurally
164
+identical, rather than create multiple configuration widgets I have opted to
165
+instead have a selector to choose between the two options.
166
+
167
+@<Class declarations@>=
168
+class MergeSeriesConfWidget : public BasicDeviceConfigurationWidget
169
+{
170
+	Q_OBJECT
171
+	public:
172
+		Q_INVOKABLE MergeSeriesConfWidget(DeviceTreeModel *model,
173
+                                          const QModelIndex &index);
174
+	private slots:
175
+		void updateColumn1(const QString &column);
176
+		void updateColumn2(const QString &column);
177
+		void updateOutput(const QString &column);
178
+		void updateType(int type);
179
+};
180
+
181
+@ The constructor sets up the user interface.
182
+
183
+@<MergeSeriesConfWidget implementation@>=
184
+MergeSeriesConfWidget::MergeSeriesConfWidget(DeviceTreeModel *model,
185
+                                             const QModelIndex &index)
186
+: BasicDeviceConfigurationWidget(model, index)
187
+{
188
+	QFormLayout *layout = new QFormLayout;
189
+	QComboBox *type = new QComboBox;
190
+	type->addItem(tr("Difference"), QVariant("Difference"));
191
+	type->addItem(tr("Mean"), QVariant("Mean"));
192
+	layout->addRow(tr("Series type:"), type);
193
+	QLineEdit *column1 = new QLineEdit;
194
+	layout->addRow(tr("First input column name:"), column1);
195
+	QLineEdit *column2 = new QLineEdit;
196
+	layout->addRow(tr("Second input column name:"), column2);
197
+	QLineEdit *output = new QLineEdit;
198
+	layout->addRow(tr("Output column name:"), output);
199
+	@<Get device configuration data for current node@>@;
200
+	for(int i = 0; i < configData.size(); i++)
201
+	{
202
+		node = configData.at(i).toElement();
203
+		if(node.attribute("name") == "type")
204
+		{
205
+			type->setCurrentIndex(type->findData(node.attribute("value")));
206
+		}
207
+		else if(node.attribute("name") == "column1")
208
+		{
209
+			column1->setText(node.attribute("value"));
210
+		}
211
+		else if(node.attribute("name") == "column2")
212
+		{
213
+			column2->setText(node.attribute("value"));
214
+		}
215
+		else if(node.attribute("name") == "output")
216
+		{
217
+			output->setText(node.attribute("value"));
218
+		}
219
+	}
220
+	updateColumn1(column1->text());
221
+	updateColumn2(column2->text());
222
+	updateOutput(output->text());
223
+	updateType(type->currentIndex());
224
+	connect(column1, SIGNAL(textEdited(QString)), this, SLOT(updateColumn1(QString)));
225
+	connect(column2, SIGNAL(textEdited(QString)), this, SLOT(updateColumn2(QString)));
226
+	connect(output, SIGNAL(textEdited(QString)), this, SLOT(updateOutput(QString)));
227
+	connect(type, SIGNAL(currentIndexChanged(int)), this, SLOT(updateType(int)));
228
+	setLayout(layout);
229
+}
230
+
231
+@ The update methods are trivial.
232
+
233
+@<MergeSeriesConfWidget implementation@>=
234
+void MergeSeriesConfWidget::updateColumn1(const QString &column)
235
+{
236
+	updateAttribute("column1", column);
237
+}
238
+
239
+void MergeSeriesConfWidget::updateColumn2(const QString &column)
240
+{
241
+	updateAttribute("column2", column);
242
+}
243
+
244
+void MergeSeriesConfWidget::updateOutput(const QString &column)
245
+{
246
+	updateAttribute("output", column);
247
+}
248
+
249
+void MergeSeriesConfWidget::updateType(int index)
250
+{
251
+	switch(index)
252
+	{
253
+		case 0:
254
+			updateAttribute("type", "Difference");
255
+			break;
256
+		case 1:
257
+			updateAttribute("type", "Mean");
258
+			break;
259
+		default:
260
+			break;
261
+	}
262
+}
263
+
264
+@ This is registered with the configuration system.
265
+
266
+@<Register device configuration widgets@>=
267
+app.registerDeviceConfigurationWidget("mergeseries",
268
+                                      MergeSeriesConfWidget::staticMetaObject);
269
+
270
+@ This is accessed through the advanced features menu.
271
+
272
+@<Add node inserters to advanced features menu@>=
273
+NodeInserter *mergeSeriesInserter = new NodeInserter(tr("Merge Series"),
274
+                                                     tr("Merge"),
275
+                                                     "mergeseries");
276
+connect(mergeSeriesInserter, SIGNAL(triggered(QString, QString)),
277
+        this, SLOT(insertChildNode(QString, QString)));
278
+advancedMenu->addAction(mergeSeriesInserter);
279
+
280
+@ The class implementations are currently expanded into |"typica.cpp"|.
281
+
282
+@<Class implementations@>=
283
+@<MergeSeriesConfWidget implementation@>
284
+@<MergeSeries implementation@>
285
+

+ 2
- 0
src/typica.w View File

@@ -20236,6 +20236,8 @@ app.registerDeviceConfigurationWidget("translation", TranslationConfWidget::stat
20236 20236
 
20237 20237
 @i rate.w
20238 20238
 
20239
+@i mergeseries.w
20240
+
20239 20241
 @i dataqsdk.w
20240 20242
 
20241 20243
 @i scales.w

Loading…
Cancel
Save