Browse Source

Add initial support for Phidgets 1048

Neal Wilson 9 years ago
parent
commit
e0aa29a00c
3 changed files with 515 additions and 1 deletions
  1. 28
    0
      config/Windows/productionroaster.xml
  2. 484
    0
      src/phidgets.w
  3. 3
    1
      src/typica.w

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

@@ -70,6 +70,7 @@
70 70
 		var nidevices = new Array();
71 71
 		var dataqsdkdevices = new Array();
72 72
 		var jsdevices = new Array();
73
+		var pdevices = new Array();
73 74
 		var temperatureDisplays = new Array();
74 75
 		var columnNames = new Array();
75 76
 		var modbusdevices = new Array();
@@ -388,6 +389,27 @@
388 389
 					device.start();
389 390
 					jsdevices.push(device);
390 391
 				}
392
+				else if(driverReference.driver == "phidgets1048")
393
+				{
394
+					var device = new PhidgetsTemperatureSensor(driverIndex);
395
+					for(var j = 0; j < device.channelCount(); j++) {
396
+						channels.push(device.getChannel(j));
397
+						columnNames.push(device.channelColumnName(j));
398
+						channelType.push("T");
399
+						if(device.isChannelHidden(j)) {
400
+							channelVisibility.push(false);
401
+						} else {
402
+							channelVisibility.push(true);
403
+							var indicator = new TemperatureDisplay;
404
+							temperatureDisplays.push(indicator);
405
+							var decorator = new WidgetDecorator(indicator, device.channelIndicatorText(j), 2);
406
+							device.getChannel(j).newData.connect(indicator.setValue);
407
+							indicatorPanel.addWidget(decorator);
408
+						}
409
+					}
410
+					device.start();
411
+					pdevices.push(device);
412
+				}
391 413
 				else if(driverReference.driver == "annotationbutton")
392 414
 				{
393 415
 					var button = new AnnotationButton(driverReference.buttontext);
@@ -635,10 +657,16 @@
635 657
 				jsdevices[i].stop();
636 658
 				jsdevices[i].deleteLater();
637 659
 			}
660
+			for(var i = 0; i < pdevices.length; i++)
661
+			{
662
+				pdevices[i].stop();
663
+				pdevices[i].deleteLater();
664
+			}
638 665
 			delete nidevices;
639 666
 			delete modbusdevices;
640 667
 			delete dataqsdkdevices;
641 668
 			delete jsdevices;
669
+			delete pdevices;
642 670
             window.saveSizeAndPosition("window");
643 671
             vsplit.saveState("script/loggingView/" + roasterIndex + "/mainSplitter");
644 672
 			QSettings.setValue("script/loggingView/" + roasterIndex + "/instrumentCount", isplit.count());

+ 484
- 0
src/phidgets.w View File

@@ -0,0 +1,484 @@
1
+@** Phidgets 1048.
2
+
3
+\noindent Phidgets, Inc. has provided one of their four channel temperature
4
+sensor devices so that support could be added to \pn{}. This was originally
5
+planned for version 1.7, however early support was rushed in for the 1.6.3
6
+release. As a result, this support is not full featured, but it should still be
7
+adequate for the most common uses.
8
+
9
+Two configuration widgets are required. The first is for the device as a whole.
10
+
11
+@<Class declarations@>=
12
+class PhidgetsTemperatureSensorConfWidget : public BasicDeviceConfigurationWidget
13
+{
14
+	Q_OBJECT
15
+	public:
16
+		Q_INVOKABLE PhidgetsTemperatureSensorConfWidget(DeviceTreeModel *model,
17
+                                                        const QModelIndex &index);
18
+	private slots:
19
+		void addChannel();
20
+		void updateRate(int ms);
21
+};
22
+
23
+@ This widget allows specification of a device wide sample rate and allows
24
+adding channels for the device to monitor. The device specifications indicate
25
+temperature updates happen up to 25 times per second, but this is generally
26
+excessive for \pn{} so a default rate is set to a multiple of this close to
27
+3 updates per second. There are other options for collecting measurements from
28
+this device and I have not yet had time to experiment with all of the options
29
+to determine the best approach suitable for coffee roasting applications.
30
+
31
+@<Phidgets implementation@>=
32
+PhidgetsTemperatureSensorConfWidget::PhidgetsTemperatureSensorConfWidget(DeviceTreeModel *model,
33
+                                                                         const QModelIndex &index)
34
+	: BasicDeviceConfigurationWidget(model, index)
35
+{
36
+	QFormLayout *layout = new QFormLayout;
37
+	QPushButton *addChannelButton = new QPushButton(tr("Add Channel"));
38
+	QSpinBox *sampleRate = new QSpinBox;
39
+	sampleRate->setMinimum(40);
40
+	sampleRate->setMaximum(600);
41
+	sampleRate->setSingleStep(40);
42
+	sampleRate->setValue(360);
43
+
44
+	@<Get device configuration data for current node@>@;
45
+	for(int i = 0; i < configData.size(); i++)
46
+	{
47
+		node = configData.at(i).toElement();
48
+		if(node.attribute("name") == "sampleRate")
49
+		{
50
+			sampleRate->setValue(node.attribute("value").toInt());
51
+		}
52
+	}
53
+	updateRate(sampleRate->value());
54
+
55
+	connect(sampleRate, SIGNAL(valueChanged(int)), this, SLOT(updateRate(int)));
56
+	connect(addChannelButton, SIGNAL(clicked()), this, SLOT(addChannel()));
57
+
58
+	layout->addRow(addChannelButton);
59
+	layout->addRow(tr("Sample rate:"), sampleRate);
60
+	setLayout(layout);
61
+}
62
+
63
+@ Adding another channel is handled in the usual way, with the channel
64
+configured in a separate widget.
65
+
66
+@<Phidgets implementation@>=
67
+void PhidgetsTemperatureSensorConfWidget::addChannel()
68
+{
69
+	insertChildNode(tr("Channel"), "phidgets1048channel");
70
+}
71
+
72
+@ Changes to the sample rate are saved as an attribute of the node as usual.
73
+
74
+@<Phidgets implementation@>=
75
+void PhidgetsTemperatureSensorConfWidget::updateRate(int ms)
76
+{
77
+	updateAttribute("sampleRate", QString("%1").arg(ms));
78
+}
79
+
80
+@ The other required configuration widget is for a single channel.
81
+
82
+@<Class declarations@>=
83
+class PhidgetTemperatureSensorChannelConfWidget : public BasicDeviceConfigurationWidget
84
+{
85
+	Q_OBJECT
86
+	public:
87
+		Q_INVOKABLE PhidgetTemperatureSensorChannelConfWidget(DeviceTreeModel *model,
88
+                                                              const QModelIndex &index);
89
+	private slots:
90
+		void updateColumnName(const QString &value);
91
+		void updateHidden(bool hidden);
92
+		void updateTC(int index);
93
+		void updateChannel(int channel);
94
+	private:
95
+		QComboBox *tcType;
96
+};
97
+
98
+@ For each channel it is necessary to specify which channel of the device
99
+measurements will come in on. The thermocouple type should be set to match the
100
+type of the thermocouple attached to that channel. The column name and if the
101
+channel is hidden has the same meaning as in channels on other devices.
102
+
103
+@<Phidgets implementation@>=
104
+PhidgetTemperatureSensorChannelConfWidget::PhidgetTemperatureSensorChannelConfWidget(
105
+	DeviceTreeModel *model, const QModelIndex &index)
106
+	: BasicDeviceConfigurationWidget(model, index),
107
+	tcType(new QComboBox)
108
+{
109
+	QFormLayout *layout = new QFormLayout;
110
+	QLineEdit *columnName = new QLineEdit;
111
+	layout->addRow(tr("Column Name:"), columnName);
112
+	QCheckBox *hideSeries = new QCheckBox("Hide this channel");
113
+	layout->addRow(hideSeries);
114
+	layout->addRow(tr("Thermocouple Type:"), tcType);
115
+	tcType->addItem("Type K", "1");
116
+	tcType->addItem("Type J", "2");
117
+	tcType->addItem("Type E", "3");
118
+	tcType->addItem("Type T", "4");
119
+	QSpinBox *channel = new QSpinBox;
120
+	layout->addRow(tr("Channel:"), channel);
121
+	channel->setMinimum(0);
122
+	channel->setMaximum(3);
123
+	setLayout(layout);
124
+	@<Get device configuration data for current node@>@;
125
+	for(int i = 0; i < configData.size(); i++)
126
+	{
127
+		node = configData.at(i).toElement();
128
+		if(node.attribute("name") == "columnname")
129
+		{
130
+			columnName->setText(node.attribute("value"));
131
+		}
132
+		else if(node.attribute("name") == "hidden")
133
+		{
134
+			hideSeries->setChecked(node.attribute("value") == "true");
135
+		}
136
+		else if(node.attribute("name") == "tctype")
137
+		{
138
+			tcType->setCurrentIndex(tcType->findData(node.attribute("value")));
139
+		}
140
+		else if(node.attribute("name") == "channel")
141
+		{
142
+			channel->setValue(node.attribute("value").toInt());
143
+		}
144
+	}
145
+	updateColumnName(columnName->text());
146
+	updateHidden(hideSeries->isChecked());
147
+	updateTC(tcType->currentIndex());
148
+	updateChannel(channel->value());
149
+	connect(columnName, SIGNAL(textEdited(QString)), this, SLOT(updateColumnName(QString)));
150
+	connect(hideSeries, SIGNAL(toggled(bool)), this, SLOT(updateHidden(bool)));
151
+	connect(tcType, SIGNAL(currentIndexChanged(int)), this, SLOT(updateTC(int)));
152
+	connect(channel, SIGNAL(valueChanged(int)), this, SLOT(updateChannel(int)));
153
+}
154
+
155
+@ Channel configuration settings are persisted as they are made.
156
+
157
+@<Phidgets implementation@>=
158
+void PhidgetTemperatureSensorChannelConfWidget::updateColumnName(const QString &value)
159
+{
160
+	updateAttribute("columnname", value);
161
+}
162
+
163
+void PhidgetTemperatureSensorChannelConfWidget::updateHidden(bool hidden)
164
+{
165
+	updateAttribute("hidden", hidden ? "true" : "false");
166
+}
167
+
168
+void PhidgetTemperatureSensorChannelConfWidget::updateTC(int index)
169
+{
170
+	updateAttribute("tctype", tcType->itemData(index).toString());
171
+}
172
+
173
+void PhidgetTemperatureSensorChannelConfWidget::updateChannel(int channel)
174
+{
175
+	updateAttribute("channel", QString("%1").arg(channel));
176
+}
177
+
178
+@ The configuration widgets need to be registered so they can be instantiated as
179
+appropriate.
180
+
181
+@<Register device configuration widgets@>=
182
+app.registerDeviceConfigurationWidget("phidgets1048",
183
+	PhidgetsTemperatureSensorConfWidget::staticMetaObject);
184
+app.registerDeviceConfigurationWidget("phidgets1048channel",
185
+	PhidgetTemperatureSensorChannelConfWidget::staticMetaObject);
186
+
187
+@ A |NodeInserter| for the device node is also required, but this should only
188
+be provided if the required library is installed.
189
+
190
+@<Register top level device configuration nodes@>=
191
+QLibrary phidgetsCheck("phidget21");
192
+if(phidgetsCheck.load())
193
+{
194
+	inserter = new NodeInserter(tr("Phidgets 1048"), tr("Phidgets 1048"),
195
+		"phidgets1048", NULL);
196
+	topLevelNodeInserters.append(inserter);
197
+	phidgetsCheck.unload();
198
+}
199
+else
200
+{
201
+	phidgetsCheck.setFileName("Phidget21.framework/phidget21");
202
+	if(phidgetsCheck.load())
203
+	{
204
+		inserter = new NodeInserter(tr("Phidgets 1048"), tr("Phidgets 1048"),
205
+		"phidgets1048", NULL);
206
+		topLevelNodeInserters.append(inserter);
207
+		phidgetsCheck.unload();
208
+	}
209
+}
210
+
211
+@ As usual, a class representing the device is provided.
212
+
213
+@<Class declarations@>=
214
+class PhidgetsTemperatureSensor : public QObject
215
+{
216
+	Q_OBJECT
217
+	public:
218
+		Q_INVOKABLE PhidgetsTemperatureSensor(const QModelIndex &deviceIndex);
219
+		Q_INVOKABLE int channelCount();
220
+		Channel* getChannel(int channel);
221
+		Q_INVOKABLE bool isChannelHidden(int channel);
222
+		Q_INVOKABLE QString channelColumnName(int channel);
223
+		Q_INVOKABLE QString channelIndicatorText(int channel);
224
+	public slots:
225
+		void start();
226
+		void stop();
227
+	private slots:
228
+		void getMeasurements();
229
+	private:
230
+		QList<int> channelIndices;
231
+		QList<int> tctypes;
232
+		QList<Channel*> channelList;
233
+		QMap<int, Channel*> channelMap;
234
+		QList<bool> hiddenState;
235
+		QList<QString> columnNames;
236
+		QList<QString> indicatorTexts;
237
+		QLibrary driver;
238
+		QTimer sampleTimer;
239
+		void *device;
240
+		@<Phidgets 1048 function pointers@>@;
241
+};
242
+
243
+@ The constructor uses the configuration data to set up the interface used for
244
+integration with the logging view.
245
+
246
+@<Phidgets implementation@>=
247
+PhidgetsTemperatureSensor::PhidgetsTemperatureSensor(const QModelIndex &index)
248
+	: QObject(NULL), driver("phidget21"), device(NULL)
249
+{
250
+	DeviceTreeModel *model = (DeviceTreeModel *)(index.model());
251
+	QDomElement deviceReferenceElement =
252
+		model->referenceElement(model->data(index, Qt::UserRole).toString());
253
+	QDomNodeList deviceConfigData = deviceReferenceElement.elementsByTagName("attribute");
254
+	QDomElement node;
255
+	for(int i = 0; i < deviceConfigData.size(); i++)
256
+	{
257
+		node = deviceConfigData.at(i).toElement();
258
+		if(node.attribute("name") == "sampleRate")
259
+		{
260
+			sampleTimer.setInterval(node.attribute("value").toInt());
261
+		}
262
+	}
263
+	if(model->hasChildren(index))
264
+	{
265
+		for(int i = 0; i < model->rowCount(index); i++)
266
+		{
267
+			QModelIndex channelIndex = model->index(i, 0, index);
268
+			QDomElement channelReference = model->referenceElement(model->data(channelIndex, 32).toString());
269
+			QDomElement channelReferenceElement = model->referenceElement(model->data(channelIndex, Qt::UserRole).toString());
270
+			QDomNodeList channelConfigData = channelReferenceElement.elementsByTagName("attribute");
271
+			for(int j = 0; j < channelConfigData.size(); j++)
272
+			{
273
+				node = channelConfigData.at(j).toElement();
274
+				if(node.attribute("name") == "channel")
275
+				{
276
+					int channelID = node.attribute("value").toInt();
277
+					channelIndices.append(channelID);
278
+					Channel* channel = new Channel;
279
+					channelList.append(channel);
280
+					channelMap.insert(channelID, channel);
281
+				}
282
+				else if(node.attribute("name") == "hidden")
283
+				{
284
+					hiddenState.append(node.attribute("value") == "true");
285
+				}
286
+				else if(node.attribute("name") == "columnname")
287
+				{
288
+					columnNames.append(node.attribute("value"));
289
+				}
290
+				else if(node.attribute("name") == "tctype")
291
+				{
292
+					tctypes.append(node.attribute("value").toInt());
293
+				}
294
+			}
295
+			indicatorTexts.append(model->data(channelIndex, Qt::DisplayRole).toString());
296
+		}
297
+	}
298
+}
299
+
300
+@ There is a distinction between logical and physical channels. Physical
301
+channels are specified as a configuration attribute and are used for
302
+communication with hardware. Logical channels are determined by the order of
303
+nodes in the configuration and are used for integrating device support with the
304
+rest of the program.
305
+
306
+@<Phidgets implementation@>=
307
+int PhidgetsTemperatureSensor::channelCount()
308
+{
309
+	return channelList.length();
310
+}
311
+
312
+Channel* PhidgetsTemperatureSensor::getChannel(int channel)
313
+{
314
+	return channelList.at(channel);
315
+}
316
+
317
+@ Some information is available about each channel.
318
+
319
+@<Phidgets implementation@>=
320
+bool PhidgetsTemperatureSensor::isChannelHidden(int channel)
321
+{
322
+	return hiddenState.at(channel);
323
+}
324
+
325
+QString PhidgetsTemperatureSensor::channelColumnName(int channel)
326
+{
327
+	if(channel >= 0 && channel < columnNames.length())
328
+	{
329
+		return columnNames.at(channel);
330
+	}
331
+	return QString();
332
+}
333
+
334
+QString PhidgetsTemperatureSensor::channelIndicatorText(int channel)
335
+{
336
+	if(channel >= 0 && channel < indicatorTexts.length())
337
+	{
338
+		return indicatorTexts.at(channel);
339
+	}
340
+	return QString();
341
+}
342
+
343
+@ To avoid introducing dependencies on a library that is only needed for
344
+hardware that may not exist, the phidget21 library is only loaded at runtime
345
+if it is needed. Some function pointers and associated types are, therefore,
346
+required. This approach also means the associated header does not need to
347
+exist at compile time.
348
+
349
+@<Phidgets 1048 function pointers@>=
350
+typedef int (*PhidgetHandleOnly)(void *);
351
+typedef int (*PhidgetHandleInt)(void *, int);
352
+typedef int (*PhidgetHandleIntInt)(void *, int, int);
353
+typedef int (*PhidgetHandleIntDoubleOut)(void *, int, double*);
354
+PhidgetHandleOnly createDevice;
355
+PhidgetHandleInt openDevice;
356
+PhidgetHandleInt waitForOpen;
357
+PhidgetHandleIntInt setTCType;
358
+PhidgetHandleIntDoubleOut getTemperature;
359
+PhidgetHandleOnly closeDevice;
360
+PhidgetHandleOnly deleteDevice;
361
+
362
+@ Library loading is deferred until we are ready to open a device.
363
+
364
+@<Phidgets implementation@>=
365
+void PhidgetsTemperatureSensor::start()
366
+{
367
+	if(!driver.load())
368
+	{
369
+		driver.setFileName("Phidget21.framework/phidget21");
370
+		if(!driver.load())
371
+		{
372
+			QMessageBox::critical(NULL, tr("Typica: Driver not found"),
373
+				tr("Failed to find phidget21. Please install it."));
374
+			return;
375
+		}
376
+	}
377
+	if((createDevice = (PhidgetHandleOnly) driver.resolve("CPhidgetTemperatureSensor_create")) == 0 || @|
378
+       (openDevice = (PhidgetHandleInt) driver.resolve("CPhidget_open")) == 0 || @|
379
+       (waitForOpen = (PhidgetHandleInt) driver.resolve("CPhidget_waitForAttachment")) == 0 || @|
380
+       (setTCType = (PhidgetHandleIntInt) driver.resolve("CPhidgetTemperatureSensor_setThermocoupleType")) == 0 || @|
381
+       (getTemperature = (PhidgetHandleIntDoubleOut) driver.resolve("CPhidgetTemperatureSensor_getTemperature")) == 0 || @|
382
+       (closeDevice = (PhidgetHandleOnly) driver.resolve("CPhidget_close")) == 0 || @|
383
+       (deleteDevice = (PhidgetHandleOnly) driver.resolve("CPhidget_delete")) == 0)
384
+	{
385
+		QMessageBox::critical(NULL, tr("Typica: Link error"),
386
+			tr("Failed to link a required symbol in phidget21."));
387
+		return;
388
+	}
389
+	createDevice(&device);
390
+	openDevice(device, -1);
391
+	int error;
392
+	if(error = waitForOpen(device, 10000))
393
+	{
394
+		closeDevice(device);
395
+		deleteDevice(device);
396
+		QMessageBox::critical(NULL, tr("Typica: Failed to Open Device"),
397
+			tr("CPhidget_waitForAttachment returns error %n", 0, error));
398
+		return;
399
+	}
400
+	for(int i = 0; i < channelIndices.length(); i++)
401
+	{
402
+		setTCType(device, channelIndices.at(i), tctypes.at(i));
403
+	}
404
+	connect(&sampleTimer, SIGNAL(timeout()), this, SLOT(getMeasurements()));
405
+	sampleTimer.start();
406
+}
407
+
408
+@ Once the device is started, we periodically request measurements and pass
409
+them to the appropriate |Channel|.
410
+
411
+@<Phidgets implementation@>=
412
+void PhidgetsTemperatureSensor::getMeasurements()
413
+{
414
+	double value = 0.0;
415
+	QTime time = QTime::currentTime();
416
+	foreach(int i, channelIndices)
417
+	{
418
+		getTemperature(device, i, &value);
419
+		Measurement measure(value * 9.0 / 5.0 + 32.0, time);
420
+		channelMap[i]->input(measure);
421
+	}
422
+}
423
+
424
+@ Some clean up is needed in the |stop()| method.
425
+
426
+@<Phidgets implementation@>=
427
+void PhidgetsTemperatureSensor::stop()
428
+{
429
+	sampleTimer.stop();
430
+	closeDevice(device);
431
+	deleteDevice(device);
432
+	driver.unload();
433
+}
434
+
435
+@ The implementation currently goes into typica.cpp.
436
+
437
+@<Class implementations@>=
438
+@<Phidgets implementation@>@;
439
+
440
+@ The |PhidgetsTemperatureSensor| needs to be available from the host
441
+environment. This detail is likely to change in the future.
442
+
443
+@<Set up the scripting engine@>=
444
+constructor = engine->newFunction(constructPhidgetsTemperatureSensor);
445
+value = engine->newQMetaObject(&PhidgetsTemperatureSensor::staticMetaObject, constructor);
446
+engine->globalObject().setProperty("PhidgetsTemperatureSensor", value);
447
+
448
+@ Two function prototypes are needed.
449
+
450
+@<Function prototypes for scripting@>=
451
+QScriptValue constructPhidgetsTemperatureSensor(QScriptContext *context, QScriptEngine *engine);
452
+QScriptValue Phidgets_getChannel(QScriptContext *context, QScriptEngine *engine);
453
+
454
+@ The script constructor is trivial.
455
+
456
+@<Functions for scripting@>=
457
+QScriptValue constructPhidgetsTemperatureSensor(QScriptContext *context, QScriptEngine *engine)
458
+{
459
+	if(context->argumentCount() != 1)
460
+	{
461
+		context->throwError("Incorrect number of arguments passed to "@|
462
+                            "PhidgetsTemperatureSensor constructor. This takes "@|
463
+                            "a QModelIndex.");
464
+	}
465
+	QScriptValue object = engine->newQObject(new PhidgetsTemperatureSensor(argument<QModelIndex>(0, context)), QScriptEngine::ScriptOwnership);
466
+	setQObjectProperties(object, engine);
467
+	object.setProperty("getChannel", engine->newFunction(Phidgets_getChannel));
468
+	return object;
469
+}
470
+
471
+@ As usual, a wrapper is needed for getting channels.
472
+
473
+@<Functions for scripting@>=
474
+QScriptValue Phidgets_getChannel(QScriptContext *context, QScriptEngine *engine)
475
+{
476
+	PhidgetsTemperatureSensor *self = getself<PhidgetsTemperatureSensor *>(context);
477
+	QScriptValue object;
478
+	if(self)
479
+	{
480
+		object = engine->newQObject(self->getChannel(argument<int>(0, context)));
481
+		setChannelProperties(object, engine);
482
+	}
483
+	return object;
484
+}

+ 3
- 1
src/typica.w View File

@@ -23,7 +23,7 @@
23 23
 \def\pn{Typica}
24 24
 \def\filebase{typica}
25 25
 \def\version{1.6.2 \number\year-\number\month-\number\day}
26
-\def\years{2007--2014}
26
+\def\years{2007--2015}
27 27
 \def\title{\pn{} (Version \version)}
28 28
 \newskip\dangerskipb
29 29
 \newskip\dangerskip
@@ -18615,6 +18615,8 @@ topLevelNodeInserters.append(inserter);
18615 18615
 
18616 18616
 @i unsupportedserial.w
18617 18617
 
18618
+@i phidgets.w
18619
+
18618 18620
 @* Configuration widget for a calibrated data series.
18619 18621
 
18620 18622
 \noindent This control is used for adding a |LinearSplineInterpolator| to the

Loading…
Cancel
Save