Writing Emulator Plugins

You can write plugins to extend the emulator with custom views that contain your own controls. To provide new properties, you must also write a target library or an application that receives and handles data from the custom controls. The plugins should reside in the plugins folder below a folder named according to the plugin.

Communication Between Emulator and Target

The main task of a plugin is to communicate property changes to the virtual machine.

One way to communicate property changes is to create a service that enables the use of a custom API by calling slots on either side. Alternatively, for simple access of single values there is also a read-only access mechanism which is provided by the property system by default.

Communication by Full API Service

The QtSimulator module provides functions for communication between the emulator and target. On the emulator side you have to provide a service that the client can connect to. All services are provided through a shared TCP socket. The client has to choose the backend it wants to connect to. After the connection is established both ends can call the slots on the counterpart.

The called function has to be a public slot. The following example illustrates how the function should be called:

class Receiver : public QObject
{
    Q_OBJECT
public:
    Receiver(QObject *parent = nullptr) : QObject(parent) {}
public slots:
    void function(QString param) { qDebug() << "Function called" << param;}
};

The serving component has to be started first on a given TCP port. The emulator takes care of this also for plugins:

    QSimulatorServer server;
    QString errorString;
    server.startServer(8080, &errorString);

The endpoint for the client connections has to be registered with a name and version. The service a client wants to connect to has to use the same name and version.

    Receiver serverReceiver(&server);

    QVariantMap peerInfo({{"examplePeerInfo","myText"}});
    server.registerServer("MyAPI", QVersionNumber(2, 0, 0), peerInfo,
                          [&](QSimulatorConnectionWorker *client) {
                              client->addReceiver(&serverReceiver);
                          });

The peerInfo parameter is used to transfer additional information about the service during the connection handshake in both directions. The map can contain arbitrary information or be empty. It is useful to set a client name and version to make the connection origin obvious in debug output. When a new client connects, all the receiver objects have to be registered with this new connection. This is what the lambda in the snippet above does. It may also be possible to only register specific receivers depending on the version or peer information.

When a call is triggered over a connection, all the matching slots in the receiver objects will be called. In addition to using basic types as parameters, you can register any custom type that was registered with Qt's metatype system, see QMetaType.

The client side setup is similar to the server side setup.

    QSimulatorConnection connection("MyAPI", QVersionNumber(2, 0, 0));
    connection.addPeerInfo("examplePeerInfo", "someText");
    QSimulatorConnectionWorker *worker = connection.connectToHost("192.168.56.1", 0xbeef);

    worker->call("function", QString("blafoo"));

If you want the server to be able to call slots in the client, you have to register a receiver object.

    Receiver clientReceiver;
    worker->addReceiver(&clientReceiver);

Accessing Emulator Properties from the VM

To access single values from the VM, the emulator provides a service that allows a receiver to subscribe to notifications on changes in property values. The notification receiver has to be a QObject with an appropriate slot.

class ReceiverObject : public QObject
{
    Q_OBJECT
public:
    ReceiverObject() {};
public slots:
    void currentFlowChanged(int value) { qDebug() << Q_FUNC_INFO << value; }
};

A subscription can be made using a QSimulatorConnection to the property service.

    QSimulatorConnection connection("emulator.property", QVersionNumber(1, 0, 0));
    QSimulatorConnectionWorker *worker = connection.connectToHost("192.168.56.1", 0xbeef);

    worker->addReceiver(new ReceiverObject);
    worker->call("subscribe", QString("battery.currentFlow"), QByteArray("currentFlowChanged"));

In subscriptions the first parameter is the name of the property. The second parameter is the name of the slot to be called when the value has changed. The slot will also be called with the current value right after the subscription to notify the receiver about the current value.

Creating a View Plugin

A plugin has to implement the plugin interface:

class Plugin : public QObject, public PluginInterface
{
    Q_OBJECT
    Q_PLUGIN_METADATA(IID "io.qt.emulator.PluginInterface")
    Q_INTERFACES(PluginInterface)

public:
    QVector<ToolBoxPage *> pages(QWidget *) override;
    QVector<QToolButton *> menuButtons() override;
    void registerServices(QSimulatorServer *server) override;
};

The emulator will call these functions to get a list of views and quick access buttons to be shown. Each view contains a grouped set of properties or settings and a scripting interface for those.

A basic UI implementation has to cover these functions:

class Ui : public ToolBoxPage
{
    Q_OBJECT
public:
    Ui(QWidget *parent = nullptr);

    QObject* scriptInterface() const override;
    QString scriptInterfaceName() const override;
    QIcon icon() const override;
    void writeSettings(QSettings &settings) const;
    void readSettings(QSettings &settings);

    void addClient(QSimulatorConnectionWorker *client);

private:
    QSpinBox *samplePropertySpinBox;
    UiScriptInterface *mScriptInterface;

The constructor of the ToolBoxPage has to register properties and create control widgets.

The property has to have a unique name and a defined type:

auto p = Property::registerProperty("example.sampleProperty", QVariant::Int);
p->setValue(10);

A QComboBox is created to modify the property value by a user:

QStringList tags(tr("sample properties"));
QList<OptionsItem *> optionsList;

samplePropertySpinBox = new QSpinBox;
samplePropertySpinBox->setRange(1, 10000);
connect(samplePropertySpinBox, &QSpinBox::editingFinished,
        [&]() {
            Property::setValue("example.sampleProperty", samplePropertySpinBox->value());
        });
auto item = new OptionsItem(tr("Sample property"), samplePropertySpinBox);
optionsList << item;

setTitle(tr("Sample Page"));
setOptions(optionsList);

In order for all values to be scriptable the view has to provide a scripting interface via the function scriptInterface. This will be made available in the scripting engine below the name returned from the function scriptInterfanceName.

The script interface itself has to be a QObject providing properties for the previously registered properties. The getter and setter functions for those have to access or modify the global property store when a script uses those values.

Reimplement the following functions to store persistent values in the emulator configuration. Make sure you call the inherited functions from the base class as early as possible, because ToolBoxPage needs to save its window state:

void Ui::writeSettings(QSettings &settings) const
{
    ToolBoxPage::writeSettings(settings);

    settings.beginGroup(title());
    settings.setValue("myValue", 5);
    settings.endGroup();
}

void Ui::readSettings(QSettings &settings)
{
    ToolBoxPage::readSettings(settings);

    settings.beginGroup(title());
    if (settings.contains("myValue"))
        int myValue = settings.value("myValue").toInt();
    settings.endGroup();
}

The emulator displays icons that you can select to open views. To display an icon for your view, implement the function QIcon icon() const and make sure that it returns a valid icon with the size of 32x32 pixels. If you do not implement the function, the name of the page is used instead. You can set the page name using void setName(const QString &). The name is mainly used in the Menu.

If your plugin does provide a service to communicate with clients, the plugin has to handle incoming connections. The plugins API has a registerServices function that is called when the plugin is loaded. This function has to register the provided services with the socket server.

void Plugin::registerServices(QSimulatorServer *server)
{
    QVariantMap peerInfo;
    server->registerServer("ExamplePlugim", QVersionNumber(1, 0, 0), peerInfo,
                           [&](QSimulatorConnectionWorker *client) {
                              ui->addClient(client);
                           });
}

The registration takes a callable that is called when a new client connects. In the example it is a lambda that stores the client to the Ui object. You have to make sure to remove the object from any containers when the client disconnects like this:

void Ui::addClient(QConnectionWorker *client)
{
    mClients += cw;
    connect(cw, &QSimulatorConnectionWorker::disconnected, this, [=](){mClients.removeAll(cw);});

    // transfer initial values (if any)
    cw->call("remoteFunction", QVariant("arg1"));
}

© 2018 The Qt Company Ltd. Documentation contributions included herein are the copyrights of their respective owners. The documentation provided herein is licensed under the terms of the GNU Free Documentation License version 1.3 as published by the Free Software Foundation. Qt and respective logos are trademarks of The Qt Company Ltd. in Finland and/or other countries worldwide. All other trademarks are property of their respective owners.