Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions ScopeOneCore/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,9 +116,9 @@ Providers use ScopeOne logical device IDs and publish `ImageFrame` objects throu
- `plugins/hardware` contains DAQ and signal source plugins alongside native `HardwareProvider` modules. The Core selects each plugin by its interface without linking DAQ vendor libraries into the application.
- `ScanImageAssembler` is implemented in Core as a provider-independent 1D-to-2D reconstruction algorithm, while its public contract is owned by the SDK. The Core publishes reconstructed frames through the shared frame graph and Gallery session path.
- `plugins/processing` contains `ProcessingPlugin` modules loaded by ScopeOneCore. A plugin publishes stable module IDs, parameter descriptors and factories. Built-in processing methods use the same registry.
- `plugins/tools` contains optional desktop `ScopeOneToolPlugin` modules. These receive a restricted UI context rather than direct access to `MainWindow` or `PreviewWidget`. Built-in Scale, Stage Mosaic and Particle Detection tools use the same registry.
- `plugins/tools` contains optional desktop `ScopeOneToolPlugin` modules. They use the public `ScopeOneCore` service for data and hardware operations, while `ScopeOneToolContext` supplies desktop-only layer presentation and status operations. Built-in Scale, Stage Mosaic and Particle Detection tools use the same registry.

External projects consume the exported `scopeone::PluginSDK` CMake target. Every plugin manifest declares `id`, `name`, `version`, `scopeOneApi`, and `kind`; incompatible manifests are rejected before the plugin instance is created.
External projects consume the exported `scopeone::PluginSDK` CMake target. Every plugin manifest declares `id`, `name`, `version`, `scopeOneApi`, and `kind`; processing, tool and hardware contracts are versioned independently and incompatible manifests are rejected before the plugin instance is created.

Hardware, processing, DAQ, signal-source and tool contracts are installed SDK APIs. Desktop tool plugins target the ScopeOne application UI contract exposed by the SDK.

Expand Down
26 changes: 25 additions & 1 deletion ScopeOneCore/include/scopeone/PluginManifest.h
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@
#include "scopeone/scopeone_sdk_export.h"

#include <QJsonObject>
#include <QList>
#include <QString>
#include <QStringList>

namespace scopeone::core
{
Expand All @@ -24,9 +26,31 @@ namespace scopeone::core
QJsonObject metadata;
};

inline constexpr int ScopeOnePluginApiVersion = 1;
struct DiscoveredPlugin
{
PluginManifest manifest;
PluginKind expectedKind{PluginKind::Processing};
QString path;
QString interfaceId;
QJsonObject metadata;
QString error;
};

inline constexpr int ScopeOneProcessingPluginApiVersion = 2;
inline constexpr int ScopeOneToolPluginApiVersion = 2;
inline constexpr int ScopeOneHardwarePluginApiVersion = 1;

SCOPEONE_SDK_EXPORT QString pluginKindName(PluginKind kind);
SCOPEONE_SDK_EXPORT int pluginApiVersion(PluginKind kind);
SCOPEONE_SDK_EXPORT QString pluginDirectoryName(PluginKind kind);
SCOPEONE_SDK_EXPORT QStringList pluginInterfaceIds(PluginKind kind);
SCOPEONE_SDK_EXPORT QStringList pluginDirectories(PluginKind kind);
SCOPEONE_SDK_EXPORT QString pluginSettingsKey(const QString& pluginId,
const QString& name);
SCOPEONE_SDK_EXPORT bool pluginEnabled(const PluginManifest& manifest);
SCOPEONE_SDK_EXPORT QList<DiscoveredPlugin> discoverPlugins(
PluginKind kind,
const QStringList& directories = {});
SCOPEONE_SDK_EXPORT bool parsePluginManifest(const QJsonObject& metadata,
PluginKind expectedKind,
PluginManifest& manifest,
Expand Down
2 changes: 1 addition & 1 deletion ScopeOneCore/include/scopeone/ProcessingPlugin.h
Original file line number Diff line number Diff line change
Expand Up @@ -156,5 +156,5 @@ namespace scopeone::core
};
}

#define ScopeOneProcessingPlugin_iid "org.scopeone.ProcessingPlugin/1.0"
#define ScopeOneProcessingPlugin_iid "org.scopeone.ProcessingPlugin/2.0"
Q_DECLARE_INTERFACE(scopeone::core::ProcessingPlugin, ScopeOneProcessingPlugin_iid)
10 changes: 1 addition & 9 deletions ScopeOneCore/include/scopeone/ToolPlugin.h
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
#include "scopeone/ScopeOneCore.h"

#include <QList>
#include <QMap>
#include <QString>
#include <QStringList>
#include <QtPlugin>
Expand Down Expand Up @@ -35,13 +34,6 @@ namespace scopeone::ui

virtual scopeone::core::ScopeOneCore& core() const = 0;
virtual QString currentLayerKey() const = 0;
virtual scopeone::core::ImageFrame currentFrame() const = 0;
virtual double layerFrameRate(const QString& layerKey) const = 0;
virtual QMap<QString, double> layerFrameRates() const = 0;
virtual scopeone::core::ImageFrame publishToolStreamFrame(
const QString& sourceId,
const scopeone::core::ImageFrame& frame,
const QString& displayName = QString()) = 0;
virtual void showLayers(const QStringList& layerKeys, bool sideBySide = false) = 0;
virtual void showToolStatus(const QString& message, int timeoutMs = 5000) = 0;
virtual void presentSession(
Expand All @@ -61,5 +53,5 @@ namespace scopeone::ui
};
}

#define ScopeOneToolPlugin_iid "org.scopeone.ToolPlugin/1.0"
#define ScopeOneToolPlugin_iid "org.scopeone.ToolPlugin/2.0"
Q_DECLARE_INTERFACE(scopeone::ui::ScopeOneToolPlugin, ScopeOneToolPlugin_iid)
81 changes: 32 additions & 49 deletions ScopeOneCore/src/DaqDeviceManager.cpp
Original file line number Diff line number Diff line change
@@ -1,13 +1,9 @@
#include "internal/DaqDeviceManager.h"
#include "scopeone/PluginManifest.h"

#include <QCoreApplication>
#include <QDebug>
#include <QDir>
#include <QFileInfo>
#include <QLibrary>
#include <QPluginLoader>
#include <QStandardPaths>

#include <algorithm>
#include <cmath>
Expand Down Expand Up @@ -170,57 +166,44 @@ namespace scopeone::core::internal
void DaqDeviceManager::loadPlugins()
{
m_pluginsLoaded = true;
const QStringList directories = {
QDir(QCoreApplication::applicationDirPath())
.filePath(QStringLiteral("plugins/hardware")),
QDir(QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation))
.filePath(QStringLiteral("plugins/hardware"))};

for (const QString& path : directories)
for (const DiscoveredPlugin& discovered : discoverPlugins(PluginKind::Hardware))
{
const QDir directory(path);
for (const QFileInfo& file : directory.entryInfoList(QDir::Files, QDir::Name))
if (discovered.interfaceId != QStringLiteral(SCOPEONE_DAQ_DEVICE_PLUGIN_IID))
{
if (!QLibrary::isLibrary(file.absoluteFilePath()))
{
continue;
}
auto loader = std::make_unique<QPluginLoader>(file.absoluteFilePath());
PluginManifest manifest;
QString manifestError;
if (!parsePluginManifest(
loader->metaData().value(QStringLiteral("MetaData")).toObject(),
PluginKind::Hardware,
manifest,
&manifestError))
{
qWarning().noquote()
<< QStringLiteral("Failed to load DAQ plugin %1: %2")
.arg(file.fileName(), manifestError);
continue;
}
auto* plugin = qobject_cast<DaqDevicePlugin*>(loader->instance());
if (!plugin)
continue;
}
const QFileInfo file(discovered.path);
if (!discovered.error.isEmpty())
{
qWarning().noquote() << QStringLiteral("Failed to load DAQ plugin %1: %2")
.arg(file.fileName(), discovered.error);
continue;
}
if (!pluginEnabled(discovered.manifest))
{
continue;
}
auto loader = std::make_unique<QPluginLoader>(discovered.path);
auto* plugin = qobject_cast<DaqDevicePlugin*>(loader->instance());
if (!plugin)
{
qWarning().noquote() << QStringLiteral("Failed to load DAQ plugin %1: %2")
.arg(file.fileName(), loader->errorString());
continue;
}
for (const DaqDeviceDescriptor& descriptor : plugin->devices())
{
const QString deviceId = descriptor.id.trimmed();
if (deviceId.isEmpty() || m_descriptors.contains(deviceId))
{
qWarning().noquote()
<< QStringLiteral("Failed to load DAQ plugin %1: %2")
.arg(file.fileName(), loader->errorString());
continue;
}
for (const DaqDeviceDescriptor& descriptor : plugin->devices())
{
const QString deviceId = descriptor.id.trimmed();
if (deviceId.isEmpty() || m_descriptors.contains(deviceId))
{
continue;
}
m_descriptors.insert(deviceId, descriptor);
m_plugins.insert(deviceId, plugin);
m_states.insert(deviceId, DaqState::Idle);
m_messages.insert(deviceId, QStringLiteral("DAQ device is idle"));
}
m_loaders.push_back(std::move(loader));
m_descriptors.insert(deviceId, descriptor);
m_plugins.insert(deviceId, plugin);
m_states.insert(deviceId, DaqState::Idle);
m_messages.insert(deviceId, QStringLiteral("DAQ device is idle"));
}
m_loaders.push_back(std::move(loader));
}
}

Expand Down
112 changes: 111 additions & 1 deletion ScopeOneCore/src/PluginManifest.cpp
Original file line number Diff line number Diff line change
@@ -1,6 +1,19 @@
#include "scopeone/PluginManifest.h"

#include "scopeone/DaqDevice.h"
#include "scopeone/DriverHostProviderPlugin.h"
#include "scopeone/ProcessingPlugin.h"
#include "scopeone/SignalSource.h"
#include "scopeone/ToolPlugin.h"

#include <QCoreApplication>
#include <QDir>
#include <QFileInfo>
#include <QLibrary>
#include <QPluginLoader>
#include <QRegularExpression>
#include <QSettings>
#include <QStandardPaths>

namespace scopeone::core
{
Expand All @@ -18,6 +31,103 @@ namespace scopeone::core
return {};
}

int pluginApiVersion(PluginKind kind)
{
switch (kind)
{
case PluginKind::Processing:
return ScopeOneProcessingPluginApiVersion;
case PluginKind::Tool:
return ScopeOneToolPluginApiVersion;
case PluginKind::Hardware:
return ScopeOneHardwarePluginApiVersion;
}
return 0;
}

QString pluginDirectoryName(PluginKind kind)
{
return kind == PluginKind::Tool ? QStringLiteral("tools") : pluginKindName(kind);
}

QStringList pluginInterfaceIds(PluginKind kind)
{
switch (kind)
{
case PluginKind::Processing:
return {QStringLiteral(ScopeOneProcessingPlugin_iid)};
case PluginKind::Tool:
return {QStringLiteral(ScopeOneToolPlugin_iid)};
case PluginKind::Hardware:
return {QStringLiteral(ScopeOneDriverHostProviderPlugin_iid),
QStringLiteral(SCOPEONE_DAQ_DEVICE_PLUGIN_IID),
QStringLiteral(SCOPEONE_SIGNAL_SOURCE_PLUGIN_IID)};
}
return {};
}

QStringList pluginDirectories(PluginKind kind)
{
const QString directoryName = pluginDirectoryName(kind);
return {
QDir(QCoreApplication::applicationDirPath())
.filePath(QStringLiteral("plugins/%1").arg(directoryName)),
QDir(QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation))
.filePath(QStringLiteral("plugins/%1").arg(directoryName))};
}

QString pluginSettingsKey(const QString& pluginId, const QString& name)
{
return QStringLiteral("Plugins/%1/%2").arg(pluginId, name);
}

bool pluginEnabled(const PluginManifest& manifest)
{
QSettings settings(QStringLiteral("ScopeOne"), QStringLiteral("ScopeOne"));
const QString key = pluginSettingsKey(manifest.id, QStringLiteral("enabled"));
return settings.contains(key) ? settings.value(key).toBool() : manifest.autoLoad;
}

QList<DiscoveredPlugin> discoverPlugins(PluginKind kind,
const QStringList& directories)
{
QList<DiscoveredPlugin> plugins;
const QStringList searchDirectories = directories.isEmpty()
? pluginDirectories(kind)
: directories;
for (const QString& directoryPath : searchDirectories)
{
const QDir directory(directoryPath);
for (const QFileInfo& file : directory.entryInfoList(QDir::Files, QDir::Name))
{
if (!QLibrary::isLibrary(file.absoluteFilePath()))
{
continue;
}
QPluginLoader loader(file.absoluteFilePath());
const QJsonObject loaderMetadata = loader.metaData();
const QString interfaceId = loaderMetadata.value(QStringLiteral("IID")).toString();
if (interfaceId.isEmpty())
{
continue;
}

DiscoveredPlugin plugin;
plugin.expectedKind = kind;
plugin.path = file.absoluteFilePath();
plugin.interfaceId = interfaceId;
plugin.metadata = loaderMetadata.value(QStringLiteral("MetaData")).toObject();
parsePluginManifest(plugin.metadata, kind, plugin.manifest, &plugin.error);
if (plugin.error.isEmpty() && !pluginInterfaceIds(kind).contains(interfaceId))
{
plugin.error = QStringLiteral("plugin interface does not match its kind");
}
plugins.append(std::move(plugin));
}
}
return plugins;
}

bool parsePluginManifest(const QJsonObject& metadata,
PluginKind expectedKind,
PluginManifest& manifest,
Expand Down Expand Up @@ -45,7 +155,7 @@ namespace scopeone::core
{
error = QStringLiteral("plugin kind must be '%1'").arg(expectedKindName);
}
else if (apiVersion != ScopeOnePluginApiVersion)
else if (apiVersion != pluginApiVersion(expectedKind))
{
error = QStringLiteral("unsupported ScopeOne plugin API %1").arg(apiVersion);
}
Expand Down
36 changes: 6 additions & 30 deletions ScopeOneCore/src/ProcessingModuleRegistry.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,10 @@
#include "internal/SpatiotemporalBinningModule.h"
#include "scopeone/PluginManifest.h"

#include <QDir>
#include <QFileInfo>
#include <QLibrary>
#include <QPluginLoader>
#include <QSet>

#ifdef Q_OS_WIN
# include <windows.h>
#endif

namespace scopeone::core::internal
{
namespace
Expand Down Expand Up @@ -255,35 +249,17 @@ namespace scopeone::core::internal
QStringList ProcessingModuleRegistry::loadPlugins(const QString& directoryPath)
{
QStringList errors;
const QDir directory(directoryPath);
#ifdef Q_OS_WIN
SetDllDirectoryW(reinterpret_cast<LPCWSTR>(directoryPath.utf16()));
#endif
for (const QFileInfo& file : directory.entryInfoList(QDir::Files, QDir::Name))
for (const DiscoveredPlugin& discovered :
discoverPlugins(PluginKind::Processing, {directoryPath}))
{
if (!QLibrary::isLibrary(file.absoluteFilePath()))
const QFileInfo file(discovered.path);
if (!discovered.error.isEmpty())
{
errors.append(QStringLiteral("%1: %2").arg(file.fileName(), discovered.error));
continue;
}

auto loader = std::make_unique<QPluginLoader>(file.absoluteFilePath());
const QJsonObject metadata = loader->metaData();
if (metadata.value(QStringLiteral("IID")).toString()
!= QStringLiteral(ScopeOneProcessingPlugin_iid))
{
continue;
}
PluginManifest manifest;
QString manifestError;
if (!parsePluginManifest(
metadata.value(QStringLiteral("MetaData")).toObject(),
PluginKind::Processing,
manifest,
&manifestError))
{
errors.append(QStringLiteral("%1: %2").arg(file.fileName(), manifestError));
continue;
}
auto loader = std::make_unique<QPluginLoader>(discovered.path);
QObject* instance = loader->instance();
auto* plugin = qobject_cast<ProcessingPlugin*>(instance);
if (!plugin)
Expand Down
Loading
Loading