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¶
- Construction: Provider is created via
createProvider() - Initialization:
initialize()is called - open hardware, allocate resources - Operation:
readInputs()/writeOutputs()called each scan cycle (~10ms) - Shutdown:
shutdown()is called - close hardware, release resources - 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()andwriteOutputs()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
falsefrominitialize()if hardware setup fails - Set
m_lastError(inherited fromIIOProvider) 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¶
- Build the plugin to produce
.dllor.so - Copy to
<PiPLC>/plugins/io-providers/ - Launch PiPLC
- Go to Tools > I/O Configuration
- Your provider should appear in the provider dropdown
- Configure and test with the Simulation Panel
Versioning¶
- The interface ID
com.piplc.IIOProviderPlugin/1.0defines 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