@** Script Driven Devices. \noindent There are many data acquisition products that are reasonable to use with \pn which are not natively supported due to lack of available hardware for testing, lack of time or money to develop that support, or lack of documentation. It has also become relatively simple for hardware tinkerers to develop new devices matching this description as well. Vendors in this space tend to give inadequate consideration to interoperability and with some devices the communications protocol used changes significantly between firmware revisions. There are simply far too many devices like this to support everything. At the same time there are people with these devices who are capable of programming the basic communications handling but have difficulty with integrating that with \pn. By providing an in-program environment that handles much of the boilerplate and allowing people to write scripts implementing these protocols without the need to modify core \pn code or recompile the program, these people may find it easier to make their existing hardware work. Such scripts can also serve as prototypes for native support. Configuration widgets for these devices allow key value pairs to be specified both at the device level and on a per-channel basis. This is intentionally kept generic as it is impossible to know what configurable details may be required. Common configurations will have a device node representing a single logical device, usually a single physical device but this is not in any way enforced, and one child node per channel. These details are made available to the device script and are used to integrate with the logging view. Some of the naming conventions used here are legacy of the initial conception of this feature and should be changed before release if there is time to do so. While initial support will be focused on devices that present as a serial port, there is no reason this could not be extended to cover devices that are interfaced through USB HID, Bluetooth, COM, output piped from an external console program, devices interfaced through arbitrary libraries, or any other class of device not directly supported in the core code should there be an interest in any of these. @= class UnsupportedSerialDeviceConfWidget : public BasicDeviceConfigurationWidget { Q_OBJECT public: Q_INVOKABLE UnsupportedSerialDeviceConfWidget(DeviceTreeModel *model, const QModelIndex &index); private slots: void updateConfiguration(); void saveScript(); void addChannel(); private: SaltModel *deviceSettingsModel; QTextEdit *scriptEditor; }; @ The device configuration widget consists of two tabs. One tab provides a button for adding channels and an area for entering device specific configuration details. The other provides an area for entering the device script. This may be extended later to provide better editing, testing, and debugging support, but the initial concern is simply having a working feature. @= UnsupportedSerialDeviceConfWidget::UnsupportedSerialDeviceConfWidget(DeviceTreeModel *model, const QModelIndex &index) : BasicDeviceConfigurationWidget(model, index), deviceSettingsModel(new SaltModel(2)), scriptEditor(new QTextEdit) { scriptEditor->setTabStopWidth(20); QVBoxLayout *dummyLayout = new QVBoxLayout; QTabWidget *central = new QTabWidget; QWidget *deviceConfigurationWidget = new QWidget; QVBoxLayout *deviceConfigurationLayout = new QVBoxLayout; QPushButton *addChannelButton = new QPushButton(tr("Add Channel")); deviceConfigurationLayout->addWidget(addChannelButton); connect(addChannelButton, SIGNAL(clicked()), this, SLOT(addChannel())); QLabel *deviceSettingsLabel = new QLabel(tr("Device Settings:")); deviceConfigurationLayout->addWidget(deviceSettingsLabel); QTableView *deviceSettingsView = new QTableView; deviceSettingsModel->setHeaderData(0, Qt::Horizontal, tr("Key")); deviceSettingsModel->setHeaderData(1, Qt::Horizontal, tr("Value")); deviceSettingsView->setModel(deviceSettingsModel); deviceConfigurationLayout->addWidget(deviceSettingsView); deviceConfigurationWidget->setLayout(deviceConfigurationLayout); central->addTab(deviceConfigurationWidget, tr("Configuration")); central->addTab(scriptEditor, tr("Script")); dummyLayout->addWidget(central); @@; for(int i = 0; i < configData.size(); i++) { node = configData.at(i).toElement(); if(node.attribute("name") == "keys" || node.attribute("name") == "values") { int column = 0; if(node.attribute("name") == "values") { column = 1; } QString data = node.attribute("value"); if(data.length() > 3) { data.chop(2); data = data.remove(0, 2); } QStringList keyList = data.split(", "); for(int j = 0; j < keyList.size(); j++) { deviceSettingsModel->setData(deviceSettingsModel->index(j, column), QVariant(keyList.at(j)), Qt::EditRole); } } else if(node.attribute("name") == "script") { scriptEditor->setPlainText(node.attribute("value")); } } connect(deviceSettingsModel, SIGNAL(dataChanged(QModelIndex, QModelIndex)), this, SLOT(updateConfiguration())); connect(scriptEditor, SIGNAL(textChanged()), this, SLOT(saveScript())); setLayout(dummyLayout); } @ Device configuration data is entered through an ordinary |QTableView| with a |SaltModel| backing. The original use case for that model does not apply here, but that model does ensure that additional blank rows are added as needed so that arbitrarily many key value pairs can be entered. When data changes in the model we write the full content of the model out. Note that commas may not be used in keys or values. For keys in which lists make sense, a different delimiter must be chosen. @= void UnsupportedSerialDeviceConfWidget::updateConfiguration() { updateAttribute("keys", deviceSettingsModel->arrayLiteral(0, Qt::DisplayRole)); updateAttribute("values", deviceSettingsModel->arrayLiteral(1, Qt::DisplayRole)); } @ Every time the script text is changed, the new version of the script is saved. My expectation is that scripts will either be small or that they will be pasted in from outside of \pn so that this decision will not cause usability issues, however if I am wrong about this there may be a need to handle this more intelligently. @= void UnsupportedSerialDeviceConfWidget::saveScript() { updateAttribute("script", scriptEditor->toPlainText()); } @ Typica requires channel nodes to simplify integration with other existing device code. Providing a new node type allows arbitrary attributes to be configured on a per-channel basis without resorting to strange conventions in the device configuration. @= void UnsupportedSerialDeviceConfWidget::addChannel() { insertChildNode(tr("Channel"), "unsupporteddevicechannel"); } @ Channel configuration for unsupported devices is like unsupported device configuration in that arbitrary key value pairs may be entered for use by the device script. Conventions common to all other channel node types are also present here. @= class UnsupportedDeviceChannelConfWidget : public BasicDeviceConfigurationWidget { Q_OBJECT public: Q_INVOKABLE UnsupportedDeviceChannelConfWidget(DeviceTreeModel *model, const QModelIndex &index); private slots: void updateColumnName(const QString &value); void updateHidden(bool hidden); void updateConfiguration(); private: SaltModel *channelSettingsModel; }; @ The constructor is typical for for channel node configuraion widgets. @= UnsupportedDeviceChannelConfWidget::UnsupportedDeviceChannelConfWidget(DeviceTreeModel *model, const QModelIndex &index) : BasicDeviceConfigurationWidget(model, index), channelSettingsModel(new SaltModel(2)) { QFormLayout *layout = new QFormLayout; QLineEdit *columnName = new QLineEdit; layout->addRow(tr("Column Name:"), columnName); QCheckBox *hideSeries = new QCheckBox("Hide this channel"); layout->addRow(hideSeries); QTableView *channelSettings = new QTableView; channelSettingsModel->setHeaderData(0, Qt::Horizontal, "Key"); channelSettingsModel->setHeaderData(1, Qt::Horizontal, "Value"); channelSettings->setModel(channelSettingsModel); layout->addRow(channelSettings); setLayout(layout); @@; for(int i = 0; i < configData.size(); i++) { node = configData.at(i).toElement(); if(node.attribute("name") == "columnname") { columnName->setText(node.attribute("value")); } else if(node.attribute("name") == "hidden") { hideSeries->setChecked(node.attribute("value") == "true"); } else if(node.attribute("name") == "keys" || node.attribute("name") == "values") { int column = 0; if(node.attribute("name") == "values") { column = 1; } QString data = node.attribute("value"); if(data.length() > 3) { data.chop(2); data = data.remove(0, 2); } QStringList keyList = data.split(", "); for(int j = 0; j < keyList.size(); j++) { channelSettingsModel->setData(channelSettingsModel->index(j, column), QVariant(keyList.at(j)), Qt::EditRole); } } } updateColumnName(columnName->text()); updateHidden(hideSeries->isChecked()); updateConfiguration(); connect(columnName, SIGNAL(textEdited(QString)), this, SLOT(updateColumnName(QString))); connect(hideSeries, SIGNAL(toggled(bool)), this, SLOT(updateHidden(bool))); connect(channelSettingsModel, SIGNAL(dataChanged(QModelIndex, QModelIndex)), this, SLOT(updateConfiguration())); } @ Arbitrary channel configuration data is handled in the same way as device level settings while the column name and hidden status is handled in the same way as they are in other channel nodes. @= void UnsupportedDeviceChannelConfWidget::updateColumnName(const QString &value) { updateAttribute("columnname", value); } void UnsupportedDeviceChannelConfWidget::updateHidden(bool hidden) { updateAttribute("hidden", hidden ? "true" : "false"); } void UnsupportedDeviceChannelConfWidget::updateConfiguration() { updateAttribute("keys", channelSettingsModel->arrayLiteral(0, Qt::DisplayRole)); updateAttribute("values", channelSettingsModel->arrayLiteral(1, Qt::DisplayRole)); } @ The configuration widgets need to be registered so they can be instantiated as appropriate. @= app.registerDeviceConfigurationWidget("unsupporteddevicechannel", UnsupportedDeviceChannelConfWidget::staticMetaObject); app.registerDeviceConfigurationWidget("unsupporteddevice", UnsupportedSerialDeviceConfWidget::staticMetaObject); @ A |NodeInserter| for the device node is also provided. @= inserter = new NodeInserter(tr("Other Device"), tr("Other Device"), "unsupporteddevice", NULL); topLevelNodeInserters.append(inserter); @ A device abstraction is not strictly required for this feature, however having one greatly simplifies integrating this feature. At some point I would like to revise other device abstraction classes so that a huge amount of boilerplate associated with these can be removed from configuration documents. This device abstraction includes features in a few particular categories. First there are methods that are required for integrating the device with the logging view. The logging view instantiates the device abstraction, passing in the configuration data required to properly set up the device. It then is able to query information about the measurement channels that have been configured for this device and can set up all of the relevant indicators. Some device classes may be able to produce annotations, so this class can be treated exactly the same as any other annotation source. Another requested feature includes the ability of a device to trigger the start and end of batches, so signals are provided for this capability. Finally, there are methods associated with starting and stopping the device. The |start()| method will be called when the logging view has finished making all of the signal connections. The |stop()| method will be called when the logging view is closed, giving script code the chance to cleanly release any resources that must be held for device communications. @= class JavaScriptDevice : public QObject { Q_OBJECT public: Q_INVOKABLE JavaScriptDevice(const QModelIndex &deviceIndex, QScriptEngine *engine); Q_INVOKABLE int channelCount(); Channel* getChannel(int channel); Q_INVOKABLE bool isChannelHidden(int channel); Q_INVOKABLE Units::Unit expectedChannelUnit(int channel); Q_INVOKABLE QString channelColumnName(int channel); Q_INVOKABLE QString channelIndicatorText(int channel); public slots: void setTemperatureColumn(int tcol); void setAnnotationColumn(int ncol); void start(); void stop(); signals: void annotation(QString note, int tcol, int ncol); void triggerStartBatch(); void triggerStopBatch(); void deviceStopRequested(); private: QVariantMap deviceSettings; QString deviceScript; QList channelList; QList hiddenState; QList channelUnits; QList columnNames; QList indicatorTexts; QList channelSettings; int annotationTemperatureColumn; int annotationNoteColumn; QScriptEngine *scriptengine; }; @ The |JavaScriptDevice| instance provides two interfaces. Its invokable methods provide the information needed to integrate script driven devices with a generic logging view. Additional information is also exposed through the host environment running the device script. This means that the class requires knowledge of the host environment, which it obtains through a script function similar to what is done for window creation. The name of the function is generic so this may be easily extended later to create all device abstraction instances. @= QScriptValue createDevice(QScriptContext *context, QScriptEngine *engine); @ That method is made available to the scripting engine. @= engine->globalObject().setProperty("createDevice", engine->newFunction(createDevice)); @ This function currently creates a |JavaScriptDevice| from a device configuration node which must be passed through as an argument. @= QScriptValue createDevice(QScriptContext *context, QScriptEngine *engine)@/ { QModelIndex deviceIndex = argument(0, context); JavaScriptDevice *device = new JavaScriptDevice(deviceIndex, engine); QScriptValue object = engine->newQObject(device); setQObjectProperties(object, engine); object.setProperty("getChannel", engine->newFunction(JavaScriptDevice_getChannel)); return object; } @ The |start()| method is responsible for preparing the host environment and executing the device script. @= void JavaScriptDevice::start() { QScriptValue object = scriptengine->newQObject(this); @@; @@; QScriptContext *context = scriptengine->currentContext(); QScriptValue oldThis = context->thisObject(); context->setThisObject(object); QScriptValue result = scriptengine->evaluate(deviceScript); QScriptEngine *engine = scriptengine; @@; context->setThisObject(oldThis); } @ Device settings are only needed from the device script itself. As such, these are presented under a settings property available from the |this| object when the script is run. @= QScriptValue settingsObject = scriptengine->newObject(); QVariantMap::const_iterator i = deviceSettings.constBegin(); while(i != deviceSettings.constEnd()) { settingsObject.setProperty(i.key(), i.value().toString()); i++; } object.setProperty("settings", settingsObject); @ While channels are available to the device script through the same |getChannel()| interface used outside of the device script for integration purposes, it is more convenient to have an array of channels with channel specific settings as properties of the channel. @= QScriptValue channelsArray = scriptengine->newArray(channelCount()); for(int i = 0; i < channelCount(); i++) { QScriptValue channelObject = scriptengine->newQObject(getChannel(i)); QScriptValue channelSettingsObject = scriptengine->newObject(); QVariantMap::const_iterator j = channelSettings.at(i).constBegin(); while(j != channelSettings.at(i).constEnd()) { channelSettingsObject.setProperty(j.key(), j.value().toString()); j++; } channelObject.setProperty("settings", channelSettingsObject); channelsArray.setProperty(i, channelObject); } object.setProperty("channels", channelsArray); @ Currently we require wrapper functions to work with channels in the host environment. @= QScriptValue JavaScriptDevice_getChannel(QScriptContext *context, QScriptEngine *engine); @ The implementation is trivial. @= QScriptValue JavaScriptDevice_getChannel(QScriptContext *context, QScriptEngine *engine) { JavaScriptDevice *self = getself(context); QScriptValue object; if(self) { object = engine->newQObject(self->getChannel(argument(0, context))); setChannelProperties(object, engine); } return object; } @ The |stop()| method just fires off a signal that the script can hook into for any required cleanup. @= void JavaScriptDevice::stop() { emit deviceStopRequested(); } @ The constructor is responsible for all boilerplate initialization required for integrating script defined devices with the logging view. Note: At present expected units are assumed to be Fahrenheit. The configuration widget must be updated to allow at least for control measurements and eventually support for runtime defined units should also be added. @= JavaScriptDevice::JavaScriptDevice(const QModelIndex &index, QScriptEngine *engine) : QObject(NULL), scriptengine(engine) { DeviceTreeModel *model = (DeviceTreeModel *)(index.model()); QDomElement deviceReferenceElement = model->referenceElement(model->data(index, Qt::UserRole).toString()); QDomNodeList deviceConfigData = deviceReferenceElement.elementsByTagName("attribute"); QDomElement node; QStringList deviceKeys; QStringList deviceValues; for(int i = 0; i < deviceConfigData.size(); i++) { node = deviceConfigData.at(i).toElement(); if(node.attribute("name") == "keys") { QString data = node.attribute("value"); if(data.length() > 3) { data.chop(2); data = data.remove(0, 2); } deviceKeys = data.split(", "); } else if(node.attribute("name") == "values") { QString data = node.attribute("value"); if(data.length() > 3) { data.chop(2); data = data.remove(0, 2); } deviceValues = data.split(", "); } else if(node.attribute("name") == "script") { deviceScript = node.attribute("value"); } deviceSettings.insert(node.attribute("name"), node.attribute("value")); } for(int i = 0; i < qMin(deviceKeys.length(), deviceValues.length()); i++) { deviceSettings.insert(deviceKeys[i], deviceValues[i]); } if(model->hasChildren(index)) { for(int i = 0; i < model->rowCount(index); i++) { QModelIndex channelIndex = model->index(i, 0, index); QDomElement channelReference = model->referenceElement(model->data(channelIndex, 32).toString()); channelList.append(new Channel); QDomElement channelReferenceElement = model->referenceElement(model->data(channelIndex, Qt::UserRole).toString()); QDomNodeList channelConfigData = channelReferenceElement.elementsByTagName("attribute"); QStringList channelKeys; QStringList channelValues; for(int j = 0; j < channelConfigData.size(); j++) { node = channelConfigData.at(j).toElement(); if(node.attribute("name") == "keys") { QString data = node.attribute("value"); if(data.length() > 3) { data.chop(2); data = data.remove(0, 2); } channelKeys = data.split(", "); } else if(node.attribute("name") == "values") { QString data = node.attribute("value"); if(data.length() > 3) { data.chop(2); data = data.remove(0, 2); } channelValues = data.split(", "); } else if(node.attribute("name") == "hidden") { hiddenState.append(node.attribute("value") == "true"); } else if(node.attribute("name") == "columnname") { columnNames.append(node.attribute("value")); } } QVariantMap cs; for(int j = 0; j < qMin(channelKeys.length(), channelValues.length()); j++) { cs.insert(channelKeys[j], channelValues[j]); } channelSettings.append(cs); indicatorTexts.append(model->data(channelIndex, Qt::DisplayRole).toString()); channelUnits.append(Units::Fahrenheit); } } } @ Several methods are available to query information about the configured channels. @= int JavaScriptDevice::channelCount() { return channelList.length(); } Channel* JavaScriptDevice::getChannel(int channel) { return channelList.at(channel); } bool JavaScriptDevice::isChannelHidden(int channel) { return hiddenState.at(channel); } Units::Unit JavaScriptDevice::expectedChannelUnit(int channel) { return channelUnits.at(channel); } QString JavaScriptDevice::channelColumnName(int channel) { if(channel >= 0 && channel < columnNames.length()) { return columnNames.at(channel); } return QString(); } QString JavaScriptDevice::channelIndicatorText(int channel) { return indicatorTexts.at(channel); } @ Two slots are provided for controlling the placement of annotations. @= void JavaScriptDevice::setTemperatureColumn(int tcol) { annotationTemperatureColumn = tcol; } void JavaScriptDevice::setAnnotationColumn(int ncol) { annotationNoteColumn = ncol; } @ Device scripts must be able to produce measurements on a channel. To do this, a function is provided for obtaining a timestamp. The returned timestamp should not be examined as future changes may break assumptions about the content of the timestamp. @= QScriptValue getMeasurementTimestamp(QScriptContext *context, QScriptEngine *engine); @ That method is made available to the scripting engine. @= engine->globalObject().setProperty("getMeasurementTimestamp", engine->newFunction(getMeasurementTimestamp)); @ At present this simply obtains the current system time. It is planned to switch to a better quality clock in the future, but this should be done for everything that uses |Measurement| objects at once. @= QScriptValue getMeasurementTimestamp(QScriptContext *, QScriptEngine *engine)@/ { return engine->toScriptValue(QTime::currentTime()); } @ At present, implementations are not broken out to a separate file. This should be changed at some point. @= @ @ @* Serial Ports. \noindent The first use case for script driven devices was connecting to devices which present themselves as a serial port. This covers a broad range of data acquisition products. To provide this support, |QextSerialPort|, which was already used to support some other hardware options, is directly exposed to the host environment. @= QScriptValue constructSerialPort(QScriptContext *context, QScriptEngine *engine); void setSerialPortProperties(QScriptValue value, QScriptEngine *engine); QScriptValue SerialPort_flush(QScriptContext *context, QScriptEngine *engine); @ Our constructor is passed to the scripting engine. @= constructor = engine->newFunction(constructSerialPort); value = engine->newQMetaObject(&QextSerialPort::staticMetaObject, constructor); engine->globalObject().setProperty("SerialPort", value); @ At present we only support event driven communications and are not passing any port settings through the constructor. Such functionality may be added in the future, but it is not strictly necessary. @= QScriptValue constructSerialPort(QScriptContext *, QScriptEngine *engine) { QScriptValue object = engine->newQObject(new QextSerialPort()); setSerialPortProperties(object, engine); return object; } @ Some properties of |QIODevice| are brought in as usual for similar subclasses but we also add a wrapper around the |flush()| method. @= void setSerialPortProperties(QScriptValue value, QScriptEngine *engine) { setQIODeviceProperties(value, engine); value.setProperty("flush", engine->newFunction(SerialPort_flush)); } @ The wrapper around |flush()| is trivial. @= QScriptValue SerialPort_flush(QScriptContext *context, QScriptEngine *) { QextSerialPort *self = getself(context); self->flush(); return QScriptValue(); }