Skip to content

I/O Provider Plugin Specification

Version: 1.0 Interface ID: com.piplc.IIOProviderPlugin/1.0

This document specifies the interface and requirements for developing third-party I/O provider plugins for PiPLC.

Overview

I/O Provider plugins extend PiPLC with support for additional hardware interfaces. Plugins are dynamically loaded shared libraries (.dll on Windows, .so on Linux) that implement the IIOProviderPlugin interface.

Plugin Architecture

┌─────────────────────────────────────────────────────────────┐
│                      PiPLC Application                       │
├─────────────────────────────────────────────────────────────┤
│                    IOProviderRegistry                        │
│  ┌─────────────┐  ┌─────────────┐  ┌─────────────────────┐  │
│  │   Virtual   │  │    GPIO     │  │  Dynamic Plugins    │  │
│  │  (static)   │  │  (static)   │  │  (QPluginLoader)    │  │
│  └─────────────┘  └─────────────┘  └─────────────────────┘  │
└─────────────────────────────────────────────────────────────┘
                              │ loads
                    ┌─────────┴─────────┐
                    │  Your Plugin.dll  │
                    │  IIOProviderPlugin │
                    └───────────────────┘

Required Files

A plugin consists of:

File Description
MyPlugin.h Plugin class header (implements IIOProviderPlugin)
MyPlugin.cpp Plugin class implementation
MyProvider.h Provider class header (implements IIOProvider)
MyProvider.cpp Provider class implementation
myplugin.json Plugin metadata (required by Qt)
CMakeLists.txt Build configuration

Interface: IIOProviderPlugin

Your plugin class must inherit from both QObject and IIOProviderPlugin:

#pragma once

#include "IIOProviderPlugin.h"
#include <QObject>
#include <QtPlugin>

namespace MyCompany {

class MyPlugin : public QObject, public PiPLC::IIOProviderPlugin
{
    Q_OBJECT
    Q_PLUGIN_METADATA(IID IIOProviderPlugin_iid FILE "myplugin.json")
    Q_INTERFACES(PiPLC::IIOProviderPlugin)

public:
    explicit MyPlugin(QObject *parent = nullptr);

    // === Required: Metadata ===

    QString providerId() const override;
    QString displayName() const override;
    QString description() const override;
    QString version() const override;
    QStringList supportedPlatforms() const override;

    // === Required: Configuration ===

    QJsonObject defaultConfig() const override;
    bool validateConfig(const QJsonObject &config,
                        QString *errorMessage = nullptr) const override;

    // === Required: Factory ===

    PiPLC::IIOProvider* createProvider(const QJsonObject &config,
                                        QObject *parent = nullptr) const override;

    // === Optional: UI (return nullptr if not providing custom UI) ===

    // QWidget* createConfigWidget(QWidget *parent = nullptr) const override;
    // void loadConfigToWidget(QWidget *widget, const QJsonObject &config) const override;
    // QJsonObject readConfigFromWidget(QWidget *widget) const override;
};

} // namespace MyCompany

Interface: IIOProvider

Your provider class must inherit from PiPLC::IIOProvider:

#pragma once

#include "IIOProvider.h"

namespace MyCompany {

class MyProvider : public PiPLC::IIOProvider
{
    Q_OBJECT

public:
    explicit MyProvider(QObject *parent = nullptr);
    ~MyProvider() override;

    // === Lifecycle ===

    bool initialize() override;
    void shutdown() override;
    bool isReady() const override;

    // === Digital I/O ===

    const QVector<bool>& readInputs() override;
    void writeOutputs(const QVector<bool> &outputs) override;
    int inputCount() const override;
    int outputCount() const override;

    // === Metadata ===

    QString name() const override;

    // === Optional: Analog I/O ===

    // const QVector<qint32>& readAnalogInputs() override;
    // void writeAnalogOutputs(const QVector<qint32> &outputs) override;
    // int analogInputCount() const override;
    // int analogOutputCount() const override;
    // QVector<int> analogInputMappings() const override;
    // QVector<int> analogOutputMappings() const override;

    // === Optional: Safe State ===

    // void applySafeState() override;
};

} // namespace MyCompany

Method Specifications

Metadata Methods

Method Description Example Return
providerId() Unique identifier (lowercase, hyphenated) "my-custom-io"
displayName() Human-readable name "My Custom I/O"
description() Brief description "Custom I/O for XYZ hardware"
version() Semantic version "1.0.0"
supportedPlatforms() List of platforms {"linux-arm64", "linux-armhf"} or {"*"} for all

Platform Identifiers

Identifier Description
* All platforms
linux-arm64 Linux ARM 64-bit (Raspberry Pi 4/5)
linux-armhf Linux ARM 32-bit (Raspberry Pi 2/3)
linux-x64 Linux x86-64
win32 Windows (any)

Configuration Methods

defaultConfig()

Return a QJsonObject with all configuration keys and their default values:

QJsonObject MyPlugin::defaultConfig() const
{
    return {
        {"devicePath", "/dev/mydevice"},
        {"baudRate", 9600},
        {"inputCount", 8},
        {"outputCount", 8}
    };
}

validateConfig()

Validate the configuration, returning false and setting errorMessage if invalid:

bool MyPlugin::validateConfig(const QJsonObject &config, QString *errorMessage) const
{
    QString path = config.value("devicePath").toString();
    if (path.isEmpty()) {
        if (errorMessage)
            *errorMessage = tr("Device path is required");
        return false;
    }
    return true;
}

Factory Method

createProvider()

Create and return a new provider instance based on the JSON configuration:

IIOProvider* MyPlugin::createProvider(const QJsonObject &config, QObject *parent) const
{
    QString devicePath = config.value("devicePath").toString();
    int baudRate = config.value("baudRate").toInt(9600);

    auto *provider = new MyProvider(parent);
    provider->setDevicePath(devicePath);
    provider->setBaudRate(baudRate);
    return provider;
}

Provider Lifecycle

  1. Construction: Provider is created via createProvider()
  2. Initialization: initialize() is called - open hardware, allocate resources
  3. Operation: readInputs() / writeOutputs() called each scan cycle (~10ms)
  4. Shutdown: shutdown() is called - close hardware, release resources
  5. Destruction: Provider is deleted

Signals (IIOProvider)

Your provider should emit these signals when appropriate:

signals:
    void readyChanged(bool ready);           // Hardware ready state changed
    void errorOccurred(const QString &msg);  // Error occurred
    void inputChanged(int index, bool value);        // Optional: input changed
    void analogInputChanged(int index, int value);   // Optional: analog changed

Plugin Metadata File

Create a JSON file referenced by Q_PLUGIN_METADATA:

{
    "name": "My Custom I/O",
    "version": "1.0.0",
    "author": "My Company",
    "description": "Custom I/O provider for XYZ hardware",
    "platforms": ["linux-arm64", "linux-armhf"],
    "website": "https://mycompany.com/piplc-plugin"
}

CMakeLists.txt Template

cmake_minimum_required(VERSION 3.16)
project(piplc-my-custom-io VERSION 1.0.0 LANGUAGES CXX)

# Find Qt
find_package(Qt6 REQUIRED COMPONENTS Core)

# Plugin sources
set(PLUGIN_SOURCES
    MyPlugin.cpp
    MyProvider.cpp
)

set(PLUGIN_HEADERS
    MyPlugin.h
    MyProvider.h
)

# Create shared library plugin
add_library(piplc-my-custom-io MODULE
    ${PLUGIN_SOURCES}
    ${PLUGIN_HEADERS}
)

# Set plugin properties
set_target_properties(piplc-my-custom-io PROPERTIES
    AUTOMOC ON
    CXX_STANDARD 17
    CXX_STANDARD_REQUIRED ON
    PREFIX ""  # No "lib" prefix
)

# Include PiPLC headers
# Option 1: If building within PiPLC source tree
target_include_directories(piplc-my-custom-io PRIVATE
    ${PIPLC_INCLUDE_DIR}/runtime
)
target_link_libraries(piplc-my-custom-io PRIVATE
    piplc-core
    Qt6::Core
)

# Option 2: If building standalone (requires installed PiPLC SDK)
# find_package(PiPLC REQUIRED)
# target_link_libraries(piplc-my-custom-io PRIVATE PiPLC::core Qt6::Core)

Installation Locations

Plugins are loaded from these directories (in order):

Platform Path Description
All <app>/plugins/io-providers/ Bundled with application
Linux ~/.local/share/piplc/plugins/io-providers/ User-installed
Linux /usr/lib/piplc/plugins/io-providers/ System-wide
Windows %LOCALAPPDATA%/piplc/plugins/io-providers/ User-installed

To install a plugin, copy the .dll or .so file to one of these directories.

Thread Safety

  • readInputs() and writeOutputs() are called from the engine worker thread
  • Use mutexes if your provider has background threads
  • Signals are emitted from the worker thread (Qt handles cross-thread delivery)

Error Handling

  • Return false from initialize() if hardware setup fails
  • Set m_lastError (inherited from IIOProvider) with error details
  • Emit errorOccurred(message) for runtime errors
  • The engine will fall back to Virtual I/O if your provider fails

Example: Complete Minimal Plugin

See the official plugins for complete examples: - modbus-tcp-client - Network-based I/O - explorer-hat-pro - Hardware GPIO + I2C

Testing Your Plugin

  1. Build the plugin to produce .dll or .so
  2. Copy to <PiPLC>/plugins/io-providers/
  3. Launch PiPLC
  4. Go to Tools > I/O Configuration
  5. Your provider should appear in the provider dropdown
  6. Configure and test with the Simulation Panel

Versioning

  • The interface ID com.piplc.IIOProviderPlugin/1.0 defines compatibility
  • Plugins built against version 1.0 will work with all PiPLC versions supporting 1.0
  • Breaking changes will increment the major version (e.g., /2.0)

Future Documentation (TODO)

The following sections are planned for future versions of this specification:

  • [ ] Quick Start Tutorial — Step-by-step guide to create a minimal "hello world" plugin
  • [ ] Debugging Plugins — How to troubleshoot plugin loading failures (Qt debug output, dependency issues)
  • [ ] Analog I/O Example — Full example implementing analog inputs/outputs with scaling
  • [ ] Configuration Widget Example — How to create custom UI for plugin settings in I/O Config panel
  • [ ] Migration Guide — Notes for updating plugins when interface version changes
  • [ ] Troubleshooting FAQ — Common issues like "plugin not appearing in dropdown"

Support

For questions about plugin development: - GitHub Issues: https://github.com/piplc/piplc/issues - Tag issues with plugin-development