diff --git a/ScopeOneCore/README.md b/ScopeOneCore/README.md index 8186681..5153ca4 100644 --- a/ScopeOneCore/README.md +++ b/ScopeOneCore/README.md @@ -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. diff --git a/ScopeOneCore/include/scopeone/PluginManifest.h b/ScopeOneCore/include/scopeone/PluginManifest.h index b7dd545..589a770 100644 --- a/ScopeOneCore/include/scopeone/PluginManifest.h +++ b/ScopeOneCore/include/scopeone/PluginManifest.h @@ -3,7 +3,9 @@ #include "scopeone/scopeone_sdk_export.h" #include +#include #include +#include namespace scopeone::core { @@ -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 discoverPlugins( + PluginKind kind, + const QStringList& directories = {}); SCOPEONE_SDK_EXPORT bool parsePluginManifest(const QJsonObject& metadata, PluginKind expectedKind, PluginManifest& manifest, diff --git a/ScopeOneCore/include/scopeone/ProcessingPlugin.h b/ScopeOneCore/include/scopeone/ProcessingPlugin.h index eaba54a..6561fa7 100644 --- a/ScopeOneCore/include/scopeone/ProcessingPlugin.h +++ b/ScopeOneCore/include/scopeone/ProcessingPlugin.h @@ -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) diff --git a/ScopeOneCore/include/scopeone/ToolPlugin.h b/ScopeOneCore/include/scopeone/ToolPlugin.h index 71dd013..9b8423a 100644 --- a/ScopeOneCore/include/scopeone/ToolPlugin.h +++ b/ScopeOneCore/include/scopeone/ToolPlugin.h @@ -3,7 +3,6 @@ #include "scopeone/ScopeOneCore.h" #include -#include #include #include #include @@ -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 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( @@ -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) diff --git a/ScopeOneCore/src/DaqDeviceManager.cpp b/ScopeOneCore/src/DaqDeviceManager.cpp index 763173f..b8f916e 100644 --- a/ScopeOneCore/src/DaqDeviceManager.cpp +++ b/ScopeOneCore/src/DaqDeviceManager.cpp @@ -1,13 +1,9 @@ #include "internal/DaqDeviceManager.h" #include "scopeone/PluginManifest.h" -#include #include -#include #include -#include #include -#include #include #include @@ -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(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(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(discovered.path); + auto* plugin = qobject_cast(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)); } } diff --git a/ScopeOneCore/src/PluginManifest.cpp b/ScopeOneCore/src/PluginManifest.cpp index 6005aa5..8f668ad 100644 --- a/ScopeOneCore/src/PluginManifest.cpp +++ b/ScopeOneCore/src/PluginManifest.cpp @@ -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 +#include +#include +#include +#include #include +#include +#include namespace scopeone::core { @@ -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 discoverPlugins(PluginKind kind, + const QStringList& directories) + { + QList 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, @@ -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); } diff --git a/ScopeOneCore/src/ProcessingModuleRegistry.cpp b/ScopeOneCore/src/ProcessingModuleRegistry.cpp index 18a61bb..e5a54f4 100644 --- a/ScopeOneCore/src/ProcessingModuleRegistry.cpp +++ b/ScopeOneCore/src/ProcessingModuleRegistry.cpp @@ -10,16 +10,10 @@ #include "internal/SpatiotemporalBinningModule.h" #include "scopeone/PluginManifest.h" -#include #include -#include #include #include -#ifdef Q_OS_WIN -# include -#endif - namespace scopeone::core::internal { namespace @@ -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(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(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(discovered.path); QObject* instance = loader->instance(); auto* plugin = qobject_cast(instance); if (!plugin) diff --git a/ScopeOneCore/src/ScopeOneCore.cpp b/ScopeOneCore/src/ScopeOneCore.cpp index cd761d6..6b5cd4f 100644 --- a/ScopeOneCore/src/ScopeOneCore.cpp +++ b/ScopeOneCore/src/ScopeOneCore.cpp @@ -1,5 +1,6 @@ #include "scopeone/ScopeOneCore.h" #include "scopeone/ImageSceneModel.h" +#include "scopeone/PluginManifest.h" #include "scopeone/ScanImageAssembler.h" #include "internal/AcquisitionEngine.h" @@ -28,7 +29,6 @@ #include #include #include -#include #include #include #include @@ -998,7 +998,6 @@ namespace scopeone::core CameraManager* cameraManager{nullptr}; RecordingManager* recordingManager{nullptr}; ImageProcessingManager* imageProcessingManager{nullptr}; - std::vector> processingExtensionLibraries; std::unique_ptr processingModuleRegistry; StageMosaicManager* stageMosaicManager{nullptr}; internal::DaqDeviceManager* daqDeviceManager{nullptr}; @@ -1255,36 +1254,13 @@ namespace scopeone::core "scopeone::core::ScanImageConfig"); m_managers->processingModuleRegistry = std::make_unique(); - const QStringList processingExtensionNames{ - QStringLiteral("ScopeOneCuda"), - QStringLiteral("ScopeOneInference")}; - for (const QString& extensionName : processingExtensionNames) - { - auto library = std::make_unique( - QDir(QCoreApplication::applicationDirPath()).filePath(extensionName)); - if (library->load()) - { - using RegisterProcessingModules = void (*)(ScopeOneCore*); - const auto registerProcessingModules = - reinterpret_cast( - library->resolve("scopeone_register_processing_modules")); - if (registerProcessingModules) - { - registerProcessingModules(this); - m_managers->processingExtensionLibraries.push_back(std::move(library)); - } - else - { - library->unload(); - } - } + QStringList allProcessingPluginErrors; + for (const QString& directory : + scopeone::core::pluginDirectories(scopeone::core::PluginKind::Processing)) + { + allProcessingPluginErrors.append( + m_managers->processingModuleRegistry->loadPlugins(directory)); } - const QStringList processingPluginErrors = m_managers->processingModuleRegistry->loadPlugins( - QDir(QCoreApplication::applicationDirPath()).filePath(QStringLiteral("plugins/processing"))); - QStringList allProcessingPluginErrors = processingPluginErrors; - allProcessingPluginErrors.append(m_managers->processingModuleRegistry->loadPlugins( - QDir(QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation)) - .filePath(QStringLiteral("plugins/processing")))); for (const QString& error : allProcessingPluginErrors) { qWarning().noquote() << QStringLiteral("Failed to load processing plugin %1").arg(error); diff --git a/ScopeOneCore/src/SignalSourceManager.cpp b/ScopeOneCore/src/SignalSourceManager.cpp index cd3220f..8d42eb3 100644 --- a/ScopeOneCore/src/SignalSourceManager.cpp +++ b/ScopeOneCore/src/SignalSourceManager.cpp @@ -1,13 +1,9 @@ #include "internal/SignalSourceManager.h" #include "scopeone/PluginManifest.h" -#include #include -#include #include -#include #include -#include #include #include @@ -229,58 +225,47 @@ namespace scopeone::core::internal void SignalSourceManager::loadPlugins() { - 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_SIGNAL_SOURCE_PLUGIN_IID)) { - if (!QLibrary::isLibrary(file.absoluteFilePath())) - { - continue; - } - auto loader = std::make_unique(file.absoluteFilePath()); - PluginManifest manifest; - QString manifestError; - if (!parsePluginManifest( - loader->metaData().value(QStringLiteral("MetaData")).toObject(), - PluginKind::Hardware, - manifest, - &manifestError)) - { - qWarning().noquote() - << QStringLiteral("Failed to load signal source plugin %1: %2") - .arg(file.fileName(), manifestError); - continue; - } - auto* plugin = qobject_cast(loader->instance()); - if (!plugin) - { - qWarning().noquote() - << QStringLiteral("Failed to load signal source plugin %1: %2") - .arg(file.fileName(), loader->errorString()); - continue; - } + continue; + } + const QFileInfo file(discovered.path); + if (!discovered.error.isEmpty()) + { + qWarning().noquote() + << QStringLiteral("Failed to load signal source plugin %1: %2") + .arg(file.fileName(), discovered.error); + continue; + } + if (!pluginEnabled(discovered.manifest)) + { + continue; + } + auto loader = std::make_unique(discovered.path); + auto* plugin = qobject_cast(loader->instance()); + if (!plugin) + { + qWarning().noquote() + << QStringLiteral("Failed to load signal source plugin %1: %2") + .arg(file.fileName(), loader->errorString()); + continue; + } - for (const SignalSourceDescriptor& descriptor : plugin->signalSources()) + for (const SignalSourceDescriptor& descriptor : plugin->signalSources()) + { + const QString sourceId = descriptor.id.trimmed(); + if (sourceId.isEmpty() || m_descriptors.contains(sourceId)) { - const QString sourceId = descriptor.id.trimmed(); - if (sourceId.isEmpty() || m_descriptors.contains(sourceId)) - { - continue; - } - m_descriptors.insert(sourceId, descriptor); - m_plugins.insert(sourceId, plugin); - m_states.insert(sourceId, SignalSourceState::Idle); - m_messages.insert(sourceId, QStringLiteral("Signal source is idle")); + continue; } - m_loaders.push_back(std::move(loader)); + m_descriptors.insert(sourceId, descriptor); + m_plugins.insert(sourceId, plugin); + m_states.insert(sourceId, SignalSourceState::Idle); + m_messages.insert(sourceId, QStringLiteral("Signal source is idle")); } + m_loaders.push_back(std::move(loader)); } } diff --git a/ScopeOneCuda/CMakeLists.txt b/ScopeOneCuda/CMakeLists.txt index 434bd2b..c6094ca 100644 --- a/ScopeOneCuda/CMakeLists.txt +++ b/ScopeOneCuda/CMakeLists.txt @@ -2,6 +2,7 @@ cmake_minimum_required(VERSION 3.23) include(GNUInstallDirs) include(CMakePackageConfigHelpers) +set(CMAKE_AUTOMOC ON) set(SCOPEONE_CUDA_VERSION 1.0.0) @@ -13,6 +14,7 @@ add_library(ScopeOneCuda SHARED src/CudaGaussianBlurModule.cpp src/CudaGaussianBlurModule.h src/CudaProcessingRegistration.cpp + plugin.json src/CudaRealImageModule.cpp src/GpuComplexFrame.cpp src/GpuRealFrame.cpp @@ -29,10 +31,11 @@ set(SCOPEONE_CUDA_ARCHITECTURES "50;75;80;86;89" CACHE STRING "Target CUDA architectures") set_target_properties(ScopeOneCuda PROPERTIES EXPORT_NAME Cuda + PREFIX "" CUDA_SEPARABLE_COMPILATION OFF CUDA_ARCHITECTURES "${SCOPEONE_CUDA_ARCHITECTURES}" - RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}" - LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}" + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/plugins/processing" + LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/plugins/processing" ) target_compile_features(ScopeOneCuda PUBLIC cxx_std_20) @@ -58,20 +61,20 @@ if (WIN32) add_custom_command(TARGET ScopeOneCuda POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy_if_different "${_dll}" - "$" + "${CMAKE_BINARY_DIR}/plugins/processing" ) endforeach () endif () install(TARGETS ScopeOneCuda EXPORT ScopeOneCudaTargets - RUNTIME DESTINATION "." - LIBRARY DESTINATION "." + RUNTIME DESTINATION "plugins/processing" + LIBRARY DESTINATION "plugins/processing" ARCHIVE DESTINATION "${CMAKE_INSTALL_LIBDIR}" ) install(DIRECTORY include/ DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}") if (SCOPEONE_CUDA_RUNTIME_DLLS) - install(FILES ${SCOPEONE_CUDA_RUNTIME_DLLS} DESTINATION ".") + install(FILES ${SCOPEONE_CUDA_RUNTIME_DLLS} DESTINATION "plugins/processing") endif () install(EXPORT ScopeOneCudaTargets FILE ScopeOneCudaTargets.cmake diff --git a/ScopeOneCuda/plugin.json b/ScopeOneCuda/plugin.json new file mode 100644 index 0000000..100a469 --- /dev/null +++ b/ScopeOneCuda/plugin.json @@ -0,0 +1,7 @@ +{ + "id": "scopeone.cuda", + "name": "ScopeOne CUDA Processing", + "version": "1.0.0", + "scopeOneApi": 2, + "kind": "processing" +} diff --git a/ScopeOneCuda/src/CudaProcessingRegistration.cpp b/ScopeOneCuda/src/CudaProcessingRegistration.cpp index 1c4a242..df30f96 100644 --- a/ScopeOneCuda/src/CudaProcessingRegistration.cpp +++ b/ScopeOneCuda/src/CudaProcessingRegistration.cpp @@ -1,13 +1,23 @@ #include "CudaFrequencyFilterModule.h" #include "CudaGaussianBlurModule.h" -#include "scopeone/ScopeOneCore.h" +#include "scopeone/ProcessingPlugin.h" -extern "C" SCOPEONE_CUDA_EXPORT void scopeone_register_processing_modules( - scopeone::core::ScopeOneCore* core) +#include + +namespace scopeone::cuda_plugin { - core->registerProcessingModule( - {QStringLiteral("cuda.gaussian_blur"), + class CudaProcessingPlugin final : public QObject, + public core::ProcessingPlugin + { + Q_OBJECT + Q_PLUGIN_METADATA(IID ScopeOneProcessingPlugin_iid FILE "../plugin.json") + Q_INTERFACES(scopeone::core::ProcessingPlugin) + + public: + QList processingModules() const override + { + return {{QStringLiteral("cuda.gaussian_blur"), QStringLiteral("CUDA Gaussian Blur"), 1, {{QStringLiteral("kernel_size"), @@ -26,53 +36,62 @@ extern "C" SCOPEONE_CUDA_EXPORT void scopeone_register_processing_modules( 100.0, 0.1, 2}}}, - []() - { - return std::make_unique(); - }); + {QStringLiteral("cuda.frequency_filter"), + QStringLiteral("CUDA Frequency Filter"), + 1, + {{QStringLiteral("output_mode"), + QStringLiteral("Output"), + core::ProcessingParameterType::Choice, + 2, + 0, + 2, + 1, + 0, + {{QStringLiteral("Spectrum"), 0}, + {QStringLiteral("Filtered spectrum"), 1}, + {QStringLiteral("Filtered image"), 2}}}, + {QStringLiteral("min_feature_size"), + QStringLiteral("Min feature size"), + core::ProcessingParameterType::Real, + 2.0, + 0.0, + 1000.0, + 0.1, + 2}, + {QStringLiteral("max_feature_size"), + QStringLiteral("Max feature size"), + core::ProcessingParameterType::Real, + 10.0, + 0.0, + 1000.0, + 0.1, + 2}, + {QStringLiteral("filter_kind"), + QStringLiteral("Filter kind"), + core::ProcessingParameterType::Choice, + 0, + 0, + 1, + 1, + 0, + {{QStringLiteral("Smooth"), 0}, + {QStringLiteral("Hard"), 1}}}}}}; + } - core->registerProcessingModule( - {QStringLiteral("cuda.frequency_filter"), - QStringLiteral("CUDA Frequency Filter"), - 1, - {{QStringLiteral("output_mode"), - QStringLiteral("Output"), - scopeone::core::ProcessingParameterType::Choice, - 2, - 0, - 2, - 1, - 0, - {{QStringLiteral("Spectrum"), 0}, - {QStringLiteral("Filtered spectrum"), 1}, - {QStringLiteral("Filtered image"), 2}}}, - {QStringLiteral("min_feature_size"), - QStringLiteral("Min feature size"), - scopeone::core::ProcessingParameterType::Real, - 2.0, - 0.0, - 1000.0, - 0.1, - 2}, - {QStringLiteral("max_feature_size"), - QStringLiteral("Max feature size"), - scopeone::core::ProcessingParameterType::Real, - 10.0, - 0.0, - 1000.0, - 0.1, - 2}, - {QStringLiteral("filter_kind"), - QStringLiteral("Filter kind"), - scopeone::core::ProcessingParameterType::Choice, - 0, - 0, - 1, - 1, - 0, - {{QStringLiteral("Smooth"), 0}, {QStringLiteral("Hard"), 1}}}}}, - []() + std::unique_ptr createProcessingModule( + const QString& moduleId) override { - return std::make_unique(); - }); + if (moduleId == QStringLiteral("cuda.gaussian_blur")) + { + return std::make_unique(); + } + if (moduleId == QStringLiteral("cuda.frequency_filter")) + { + return std::make_unique(); + } + return {}; + } + }; } + +#include "CudaProcessingRegistration.moc" diff --git a/ScopeOneInference/CMakeLists.txt b/ScopeOneInference/CMakeLists.txt index 5d920e2..33b0b07 100644 --- a/ScopeOneInference/CMakeLists.txt +++ b/ScopeOneInference/CMakeLists.txt @@ -8,6 +8,7 @@ endif () include(GNUInstallDirs) include(CMakePackageConfigHelpers) +set(CMAKE_AUTOMOC ON) set(ONNXRUNTIME_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/../ScopeOneCore/external/onnxruntime-win-x64-gpu_cuda12-1.30.0" @@ -34,6 +35,7 @@ add_library(ScopeOneInference SHARED src/OnnxInferenceModule.cpp src/OnnxInferenceModule.h src/InferenceProcessingRegistration.cpp + plugin.json include/scopeone/inference/ExecutionProvider.h include/scopeone/inference/InferenceExport.h include/scopeone/inference/OnnxRuntime.h @@ -44,8 +46,9 @@ add_library(scopeone::Inference ALIAS ScopeOneInference) set_target_properties(ScopeOneInference PROPERTIES EXPORT_NAME Inference - RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}" - LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}" + PREFIX "" + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/plugins/processing" + LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/plugins/processing" ) target_compile_features(ScopeOneInference PUBLIC cxx_std_20) target_compile_definitions(ScopeOneInference PRIVATE SCOPEONE_INFERENCE_EXPORTS) @@ -73,20 +76,20 @@ if (WIN32) add_custom_command(TARGET ScopeOneInference POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy_if_different ${ONNXRUNTIME_DLLS} - "$" + "${CMAKE_BINARY_DIR}/plugins/processing" COMMAND_EXPAND_LISTS ) endif () install(TARGETS ScopeOneInference EXPORT ScopeOneInferenceTargets - RUNTIME DESTINATION "." - LIBRARY DESTINATION "." + RUNTIME DESTINATION "plugins/processing" + LIBRARY DESTINATION "plugins/processing" ARCHIVE DESTINATION "${CMAKE_INSTALL_LIBDIR}" ) install(DIRECTORY include/ DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}") if (ONNXRUNTIME_DLLS) - install(FILES ${ONNXRUNTIME_DLLS} DESTINATION ".") + install(FILES ${ONNXRUNTIME_DLLS} DESTINATION "plugins/processing") endif () install(EXPORT ScopeOneInferenceTargets FILE ScopeOneInferenceTargets.cmake diff --git a/ScopeOneInference/plugin.json b/ScopeOneInference/plugin.json new file mode 100644 index 0000000..1b689ae --- /dev/null +++ b/ScopeOneInference/plugin.json @@ -0,0 +1,7 @@ +{ + "id": "scopeone.inference", + "name": "ScopeOne Inference Processing", + "version": "1.0.0", + "scopeOneApi": 2, + "kind": "processing" +} diff --git a/ScopeOneInference/src/InferenceProcessingRegistration.cpp b/ScopeOneInference/src/InferenceProcessingRegistration.cpp index 3da7548..1ce3084 100644 --- a/ScopeOneInference/src/InferenceProcessingRegistration.cpp +++ b/ScopeOneInference/src/InferenceProcessingRegistration.cpp @@ -1,20 +1,29 @@ #include "OnnxInferenceModule.h" -#include "scopeone/ScopeOneCore.h" -#include "scopeone/inference/InferenceExport.h" +#include "scopeone/ProcessingPlugin.h" -extern "C" SCOPEONE_INFERENCE_EXPORT void scopeone_register_processing_modules( - scopeone::core::ScopeOneCore* core) +#include + +namespace scopeone::inference { - scopeone::core::ProcessingParameterDescriptor modelPath{ - QStringLiteral("model_path"), - QStringLiteral("Model"), - scopeone::core::ProcessingParameterType::FilePath, - QString{}}; - modelPath.fileFilter = QStringLiteral("ONNX models (*.onnx)"); + class InferenceProcessingPlugin final : public QObject, + public core::ProcessingPlugin + { + Q_OBJECT + Q_PLUGIN_METADATA(IID ScopeOneProcessingPlugin_iid FILE "../plugin.json") + Q_INTERFACES(scopeone::core::ProcessingPlugin) + + public: + QList processingModules() const override + { + core::ProcessingParameterDescriptor modelPath{ + QStringLiteral("model_path"), + QStringLiteral("Model"), + core::ProcessingParameterType::FilePath, + QString{}}; + modelPath.fileFilter = QStringLiteral("ONNX models (*.onnx)"); - core->registerProcessingModule( - {QStringLiteral("onnx_inference"), + return {{QStringLiteral("onnx_inference"), QStringLiteral("ONNX Inference"), 2, {modelPath, @@ -71,9 +80,19 @@ extern "C" SCOPEONE_INFERENCE_EXPORT void scopeone_register_processing_modules( {}, 0, {{QStringLiteral("Image"), 0}, - {QStringLiteral("Input - Output"), 1}}}}}, - []() + {QStringLiteral("Input - Output"), 1}}}}}}; + } + + std::unique_ptr createProcessingModule( + const QString& moduleId) override { - return std::make_unique(); - }); + if (moduleId == QStringLiteral("onnx_inference")) + { + return std::make_unique(); + } + return {}; + } + }; } + +#include "InferenceProcessingRegistration.moc" diff --git a/ScopeOneInference/src/OnnxInferenceModule.cpp b/ScopeOneInference/src/OnnxInferenceModule.cpp index 5d708e2..73e0b13 100644 --- a/ScopeOneInference/src/OnnxInferenceModule.cpp +++ b/ScopeOneInference/src/OnnxInferenceModule.cpp @@ -156,7 +156,7 @@ namespace scopeone::inference void OnnxInferenceModule::createSession() { - m_runtime = std::make_unique(); + auto runtime = std::make_unique(); SessionOptions options; options.provider = m_provider == 0 ? ExecutionProvider::Cpu @@ -166,10 +166,10 @@ namespace scopeone::inference #else const std::filesystem::path path(m_modelPath.toStdString()); #endif - m_session = std::make_unique(*m_runtime, path, options); + auto session = std::make_unique(*runtime, path, options); - const auto& inputs = m_session->inputs(); - const auto& outputs = m_session->outputs(); + const auto& inputs = session->inputs(); + const auto& outputs = session->outputs(); if (inputs.size() != 1) { throw std::runtime_error("ONNX image model must have exactly one input"); @@ -194,6 +194,8 @@ namespace scopeone::inference { throw std::runtime_error("ONNX image model output must have four dimensions"); } + m_runtime = std::move(runtime); + m_session = std::move(session); } core::ProcessingResult OnnxInferenceModule::process(const core::ImageFrame& frame, int) diff --git a/plugins/hardware/NIDaqmx/plugin.json b/plugins/hardware/NIDaqmx/plugin.json index 35e6102..21abc3f 100644 --- a/plugins/hardware/NIDaqmx/plugin.json +++ b/plugins/hardware/NIDaqmx/plugin.json @@ -3,5 +3,6 @@ "name": "NI-DAQmx Device Plugin", "version": "1.0.0", "scopeOneApi": 1, - "kind": "hardware" + "kind": "hardware", + "autoLoad": true } diff --git a/plugins/hardware/PtuFile/plugin.json b/plugins/hardware/PtuFile/plugin.json index bf13605..69533aa 100644 --- a/plugins/hardware/PtuFile/plugin.json +++ b/plugins/hardware/PtuFile/plugin.json @@ -3,5 +3,6 @@ "name": "PicoQuant PTU Source", "version": "1.0.0", "scopeOneApi": 1, - "kind": "hardware" + "kind": "hardware", + "autoLoad": true } diff --git a/plugins/hardware/SimulatedPmt/plugin.json b/plugins/hardware/SimulatedPmt/plugin.json index 546f876..a397c73 100644 --- a/plugins/hardware/SimulatedPmt/plugin.json +++ b/plugins/hardware/SimulatedPmt/plugin.json @@ -4,5 +4,6 @@ "version": "1.0.0", "scopeOneApi": 1, "kind": "hardware", - "providerId": "pmt.simulator" + "providerId": "pmt.simulator", + "autoLoad": true } diff --git a/plugins/processing/ExampleCudaProcessing/plugin.json b/plugins/processing/ExampleCudaProcessing/plugin.json index 8322427..b317dd2 100644 --- a/plugins/processing/ExampleCudaProcessing/plugin.json +++ b/plugins/processing/ExampleCudaProcessing/plugin.json @@ -2,6 +2,6 @@ "id": "example.cuda_processing", "name": "Example CUDA Processing", "version": "1.0.0", - "scopeOneApi": 1, + "scopeOneApi": 2, "kind": "processing" } diff --git a/plugins/processing/ExampleProcessing/plugin.json b/plugins/processing/ExampleProcessing/plugin.json index 5be3b4f..2401d28 100644 --- a/plugins/processing/ExampleProcessing/plugin.json +++ b/plugins/processing/ExampleProcessing/plugin.json @@ -2,6 +2,6 @@ "id": "example.processing", "name": "Example Processing", "version": "1.0.0", - "scopeOneApi": 1, + "scopeOneApi": 2, "kind": "processing" } diff --git a/plugins/processing/IscatProcessing/plugin.json b/plugins/processing/IscatProcessing/plugin.json index 6aa1c76..f947c38 100644 --- a/plugins/processing/IscatProcessing/plugin.json +++ b/plugins/processing/IscatProcessing/plugin.json @@ -2,6 +2,6 @@ "id": "iscat.processing", "name": "iSCAT Processing", "version": "1.0.0", - "scopeOneApi": 1, + "scopeOneApi": 2, "kind": "processing" } diff --git a/plugins/tools/DhmTool/DhmToolPlugin.cpp b/plugins/tools/DhmTool/DhmToolPlugin.cpp index 1fcd0f6..24c805f 100644 --- a/plugins/tools/DhmTool/DhmToolPlugin.cpp +++ b/plugins/tools/DhmTool/DhmToolPlugin.cpp @@ -297,7 +297,8 @@ namespace const QString layerKey = m_layerComboBox->currentData().toString(); const ImageFrame frame = !layerKey.isEmpty() ? m_context.core().graphFrame(layerKey) - : m_context.currentFrame(); + : m_context.core().graphFrame( + m_context.currentLayerKey()); if (frame.isValid()) { m_sourceId = scopeone::core::ScopeOneCore::sourceIdFromLayerKey(layerKey); @@ -316,7 +317,7 @@ namespace } }); - const ImageFrame frame = m_context.currentFrame(); + const ImageFrame frame = m_context.core().graphFrame(m_context.currentLayerKey()); if (frame.isValid()) { m_sourceId = frame.cameraId; @@ -501,7 +502,8 @@ namespace const QString layerKey = m_layerComboBox->currentData().toString(); const ImageFrame input = !layerKey.isEmpty() ? m_context.core().graphFrame(layerKey) - : m_context.currentFrame(); + : m_context.core().graphFrame( + m_context.currentLayerKey()); if (!input.isValid()) { m_status->setText(QStringLiteral("No hologram layer available")); @@ -588,7 +590,7 @@ namespace const QString sourceId = result.outputFrame.cameraId; const QString displayName = outputName(); - const ImageFrame stored = m_context.publishToolStreamFrame( + const ImageFrame stored = m_context.core().publishToolStreamFrame( sourceId, result.outputFrame, displayName); diff --git a/plugins/tools/DhmTool/plugin.json b/plugins/tools/DhmTool/plugin.json index 966d58e..3b9a6d3 100644 --- a/plugins/tools/DhmTool/plugin.json +++ b/plugins/tools/DhmTool/plugin.json @@ -2,6 +2,6 @@ "id": "scopeone.dhm.reconstruction", "name": "DHM Reconstruction", "version": "1.0.0", - "scopeOneApi": 1, + "scopeOneApi": 2, "kind": "tool" } diff --git a/plugins/tools/ExampleTool/plugin.json b/plugins/tools/ExampleTool/plugin.json index b9e207b..d655f94 100644 --- a/plugins/tools/ExampleTool/plugin.json +++ b/plugins/tools/ExampleTool/plugin.json @@ -2,6 +2,6 @@ "id": "example.tool", "name": "Example Tool", "version": "1.0.0", - "scopeOneApi": 1, + "scopeOneApi": 2, "kind": "tool" } diff --git a/plugins/tools/ScanningDaqTool/plugin.json b/plugins/tools/ScanningDaqTool/plugin.json index 3a17f0f..97deb8f 100644 --- a/plugins/tools/ScanningDaqTool/plugin.json +++ b/plugins/tools/ScanningDaqTool/plugin.json @@ -2,6 +2,6 @@ "id": "scopeone.scanning-daq-tool", "name": "Scanning and DAQ Tools", "version": "1.0.0", - "scopeOneApi": 1, + "scopeOneApi": 2, "kind": "tool" } diff --git a/src/MainWindow.cpp b/src/MainWindow.cpp index 7f1cd09..ef82f14 100644 --- a/src/MainWindow.cpp +++ b/src/MainWindow.cpp @@ -1,6 +1,7 @@ #include "MainWindow.h" #include "scopeone/ScopeOneCore.h" +#include "scopeone/PluginManifest.h" #include "AboutDialog.h" #include "InspectWidget.h" #include "ConsoleWidget.h" @@ -997,18 +998,13 @@ namespace scopeone::ui return new ParticleDetectionDialog(context, parent); }); - const QString pluginDirectory = QDir(QCoreApplication::applicationDirPath()) - .filePath(QStringLiteral("plugins/tools")); - for (const QString& error : m_toolRegistry->loadPlugins(pluginDirectory)) + for (const QString& directory : scopeone::core::pluginDirectories( + scopeone::core::PluginKind::Tool)) { - qWarning().noquote() << QStringLiteral("Failed to load tool plugin %1").arg(error); - } - const QString userPluginDirectory = - QDir(QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation)) - .filePath(QStringLiteral("plugins/tools")); - for (const QString& error : m_toolRegistry->loadPlugins(userPluginDirectory)) - { - qWarning().noquote() << QStringLiteral("Failed to load tool plugin %1").arg(error); + for (const QString& error : m_toolRegistry->loadPlugins(directory)) + { + qWarning().noquote() << QStringLiteral("Failed to load tool plugin %1").arg(error); + } } } @@ -1022,29 +1018,6 @@ namespace scopeone::ui return m_imageWorkspace->activeLayerKey(); } - scopeone::core::ImageFrame MainWindow::currentFrame() const - { - return m_scopeonecore->graphFrame(currentLayerKey()); - } - - double MainWindow::layerFrameRate(const QString& layerKey) const - { - return m_scopeonecore->layerFrameRate(layerKey); - } - - QMap MainWindow::layerFrameRates() const - { - return m_scopeonecore->layerFrameRates(); - } - - scopeone::core::ImageFrame MainWindow::publishToolStreamFrame( - const QString& sourceId, - const scopeone::core::ImageFrame& frame, - const QString& displayName) - { - return m_scopeonecore->publishToolStreamFrame(sourceId, frame, displayName); - } - void MainWindow::showLayers(const QStringList& layerKeys, bool sideBySide) { m_imageWorkspace->setVisibleLayers(layerKeys, sideBySide); diff --git a/src/MainWindow.h b/src/MainWindow.h index 91bda37..36252ac 100644 --- a/src/MainWindow.h +++ b/src/MainWindow.h @@ -48,13 +48,6 @@ namespace scopeone::ui scopeone::core::ScopeOneCore& core() const override; QString currentLayerKey() const override; - scopeone::core::ImageFrame currentFrame() const override; - double layerFrameRate(const QString& layerKey) const override; - QMap layerFrameRates() const override; - scopeone::core::ImageFrame publishToolStreamFrame( - const QString& sourceId, - const scopeone::core::ImageFrame& frame, - const QString& displayName = QString()) override; void showLayers(const QStringList& layerKeys, bool sideBySide = false) override; void showToolStatus(const QString& message, int timeoutMs = 5000) override; void presentSession( diff --git a/src/PluginManagerDialog.cpp b/src/PluginManagerDialog.cpp index 5525a3d..3f4a342 100644 --- a/src/PluginManagerDialog.cpp +++ b/src/PluginManagerDialog.cpp @@ -8,7 +8,6 @@ #include "scopeone/ScopeOneCore.h" #include "scopeone/ToolPlugin.h" -#include #include #include #include @@ -18,10 +17,8 @@ #include #include #include -#include #include #include -#include #include #include #include @@ -38,100 +35,19 @@ namespace scopeone::ui constexpr int kStatusRole = Qt::UserRole + 2; constexpr int kMetadataRole = Qt::UserRole + 3; - struct DiscoveredPlugin - { - scopeone::core::PluginManifest manifest; - scopeone::core::PluginKind expectedKind{scopeone::core::PluginKind::Processing}; - QString path; - QString interfaceId; - QJsonObject metadata; - QString error; - }; - - QStringList pluginInterfaceIds(scopeone::core::PluginKind kind); + using scopeone::core::DiscoveredPlugin; QList discoverPlugins() { - using scopeone::core::PluginKind; - const QList> directories{ - {QStringLiteral("processing"), PluginKind::Processing}, - {QStringLiteral("tools"), PluginKind::Tool}, - {QStringLiteral("hardware"), PluginKind::Hardware} - }; - QList plugins; - const QStringList roots{ - QDir(QCoreApplication::applicationDirPath()).filePath(QStringLiteral("plugins")), - QDir(QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation)) - .filePath(QStringLiteral("plugins")) - }; - for (const QString& rootPath : roots) + QList plugins = scopeone::core::discoverPlugins( + scopeone::core::PluginKind::Processing); + for (const scopeone::core::PluginKind kind : + {scopeone::core::PluginKind::Tool, scopeone::core::PluginKind::Hardware}) { - const QDir root(rootPath); - for (const auto& [directoryName, kind] : directories) - { - const QDir directory(root.filePath(directoryName)); - for (const QFileInfo& file : directory.entryInfoList(QDir::Files, QDir::Name)) - { - if (!QLibrary::isLibrary(file.absoluteFilePath())) - { - continue; - } - QPluginLoader loader(file.absoluteFilePath()); - DiscoveredPlugin plugin; - plugin.expectedKind = kind; - plugin.path = file.absoluteFilePath(); - const QJsonObject loaderMetadata = loader.metaData(); - plugin.interfaceId = loaderMetadata.value(QStringLiteral("IID")).toString(); - if (plugin.interfaceId.isEmpty()) - { - continue; - } - plugin.metadata = loaderMetadata.value(QStringLiteral("MetaData")).toObject(); - scopeone::core::parsePluginManifest( - plugin.metadata, - kind, - plugin.manifest, - &plugin.error); - if (plugin.error.isEmpty() - && !pluginInterfaceIds(kind).contains(plugin.interfaceId)) - { - plugin.error = QStringLiteral("plugin interface does not match its kind"); - } - plugins.append(std::move(plugin)); - } - } + plugins.append(scopeone::core::discoverPlugins(kind)); } return plugins; } - - QString settingsKey(const QString& pluginId, const QString& name) - { - return QStringLiteral("Plugins/%1/%2").arg(pluginId, name); - } - - QString pluginDirectoryName(scopeone::core::PluginKind kind) - { - return kind == scopeone::core::PluginKind::Tool - ? QStringLiteral("tools") - : scopeone::core::pluginKindName(kind); - } - - QStringList pluginInterfaceIds(scopeone::core::PluginKind kind) - { - switch (kind) - { - case scopeone::core::PluginKind::Processing: - return {QStringLiteral(ScopeOneProcessingPlugin_iid)}; - case scopeone::core::PluginKind::Tool: - return {QStringLiteral(ScopeOneToolPlugin_iid)}; - case scopeone::core::PluginKind::Hardware: - return { - QStringLiteral(ScopeOneDriverHostProviderPlugin_iid), - QStringLiteral(SCOPEONE_DAQ_DEVICE_PLUGIN_IID), - QStringLiteral(SCOPEONE_SIGNAL_SOURCE_PLUGIN_IID)}; - } - return {}; - } } QStringList loadConfiguredHardwarePlugins(scopeone::core::ScopeOneCore& core) @@ -156,11 +72,7 @@ namespace scopeone::ui } const QString id = plugin.manifest.id; - const QString enabledKey = settingsKey(id, QStringLiteral("enabled")); - const bool enabled = settings.contains(enabledKey) - ? settings.value(enabledKey).toBool() - : plugin.manifest.autoLoad; - if (!enabled) + if (!scopeone::core::pluginEnabled(plugin.manifest)) { continue; } @@ -175,7 +87,7 @@ namespace scopeone::ui continue; } const QVariantMap options = settings - .value(settingsKey(id, QStringLiteral("options"))) + .value(scopeone::core::pluginSettingsKey(id, QStringLiteral("options"))) .toMap(); QString error; if (!core.registerDriverHostProvider(providerId, plugin.path, options, &error)) @@ -252,7 +164,8 @@ namespace scopeone::ui if (hardware && plugin.error.isEmpty()) { enabled->setFlags(enabled->flags() | Qt::ItemIsUserCheckable); - const QString key = settingsKey(plugin.manifest.id, QStringLiteral("enabled")); + const QString key = scopeone::core::pluginSettingsKey( + plugin.manifest.id, QStringLiteral("enabled")); const bool checked = settings.contains(key) ? settings.value(key).toBool() : plugin.manifest.autoLoad; @@ -304,73 +217,68 @@ namespace scopeone::ui } QSettings settings(QStringLiteral("ScopeOne"), QStringLiteral("ScopeOne")); const QJsonObject options = QJsonObject::fromVariantMap( - settings.value(settingsKey(id, QStringLiteral("options"))).toMap()); + settings.value(scopeone::core::pluginSettingsKey( + id, QStringLiteral("options"))).toMap()); m_optionsEdit->setPlainText(QString::fromUtf8( QJsonDocument(options).toJson(QJsonDocument::Indented))); } void PluginManagerDialog::installPlugin() { - const QString sourcePath = QFileDialog::getOpenFileName( - this, tr("Install Plugin"), {}, tr("Plugin libraries (*.dll *.so *.dylib)")); - if (sourcePath.isEmpty()) + const QString sourceDirectory = QFileDialog::getExistingDirectory( + this, tr("Install Plugin Package")); + if (sourceDirectory.isEmpty()) { return; } - QPluginLoader loader(sourcePath); - const QJsonObject loaderMetadata = loader.metaData(); - const QJsonObject metadata = loaderMetadata.value(QStringLiteral("MetaData")).toObject(); - scopeone::core::PluginManifest manifest; - QString error; - bool valid = false; + QList candidates; scopeone::core::PluginKind kind = scopeone::core::PluginKind::Processing; - for (const auto candidate : {scopeone::core::PluginKind::Processing, - scopeone::core::PluginKind::Tool, - scopeone::core::PluginKind::Hardware}) + for (const auto candidateKind : {scopeone::core::PluginKind::Processing, + scopeone::core::PluginKind::Tool, + scopeone::core::PluginKind::Hardware}) { - if (scopeone::core::parsePluginManifest(metadata, candidate, manifest, &error)) + for (const auto& candidate : + scopeone::core::discoverPlugins(candidateKind, {sourceDirectory})) { - kind = candidate; - valid = true; - break; + if (candidate.error.isEmpty()) + { + candidates.append(candidate); + kind = candidateKind; + } } } - if (!valid) - { - QMessageBox::warning(this, tr("Plugin Manager"), error); - return; - } - if (!pluginInterfaceIds(kind).contains(loaderMetadata.value(QStringLiteral("IID")).toString())) + if (candidates.size() != 1) { QMessageBox::warning(this, tr("Plugin Manager"), - tr("The plugin interface does not match its declared type.")); + tr("A plugin package must contain one plugin library.")); return; } QDir userRoot(QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation)); const QString pluginDirectory = userRoot.filePath( - QStringLiteral("plugins/%1").arg(pluginDirectoryName(kind))); + QStringLiteral("plugins/%1").arg(scopeone::core::pluginDirectoryName(kind))); if (!QDir().mkpath(pluginDirectory)) { QMessageBox::warning(this, tr("Plugin Manager"), tr("The plugin directory could not be created.")); return; } - const QString destination = QDir(pluginDirectory).filePath(QFileInfo(sourcePath).fileName()); - if (QFileInfo(sourcePath).canonicalFilePath() == QFileInfo(destination).canonicalFilePath()) + if (QFileInfo(sourceDirectory).canonicalFilePath() + == QFileInfo(pluginDirectory).canonicalFilePath()) { QMessageBox::information(this, tr("Plugin Manager"), tr("This plugin is already installed.")); return; } - if (QFileInfo::exists(destination) && !QFile::remove(destination)) + for (const QFileInfo& source : QDir(sourceDirectory).entryInfoList(QDir::Files)) { - QMessageBox::warning(this, tr("Plugin Manager"), tr("The existing plugin could not be replaced.")); - return; - } - if (!QFile::copy(sourcePath, destination)) - { - QMessageBox::warning(this, tr("Plugin Manager"), tr("The plugin could not be installed.")); - return; + const QString destination = QDir(pluginDirectory).filePath(source.fileName()); + QFile::remove(destination); + if (!QFile::copy(source.absoluteFilePath(), destination)) + { + QMessageBox::warning(this, tr("Plugin Manager"), + tr("The plugin package could not be installed.")); + return; + } } refreshPlugins(); QMessageBox::information(this, tr("Plugin Manager"), @@ -414,13 +322,15 @@ namespace scopeone::ui const QString pluginId = pluginItem->data(kIdRole).toString(); if (pluginKind == scopeone::core::PluginKind::Hardware && !pluginId.isEmpty()) { - settings.setValue(settingsKey(pluginId, QStringLiteral("enabled")), + settings.setValue(scopeone::core::pluginSettingsKey( + pluginId, QStringLiteral("enabled")), pluginItem->checkState() == Qt::Checked); } } if (kind == scopeone::core::PluginKind::Hardware && !id.isEmpty()) { - settings.setValue(settingsKey(id, QStringLiteral("options")), selectedOptions); + settings.setValue(scopeone::core::pluginSettingsKey( + id, QStringLiteral("options")), selectedOptions); } QMessageBox::information(this, tr("Plugin Manager"), tr("Plugin settings will take effect after ScopeOne restarts.")); diff --git a/src/ScopeOneToolPlugin.cpp b/src/ScopeOneToolPlugin.cpp index e78940c..bfd603d 100644 --- a/src/ScopeOneToolPlugin.cpp +++ b/src/ScopeOneToolPlugin.cpp @@ -3,9 +3,7 @@ #include #include -#include #include -#include #include #include #include @@ -61,25 +59,17 @@ namespace scopeone::ui QStringList ToolRegistry::loadPlugins(const QString& directoryPath) { QStringList errors; - const QDir directory(directoryPath); - for (const QFileInfo& file : directory.entryInfoList(QDir::Files, QDir::Name)) + for (const scopeone::core::DiscoveredPlugin& discovered : + scopeone::core::discoverPlugins(scopeone::core::PluginKind::Tool, + {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(file.absoluteFilePath()); - scopeone::core::PluginManifest manifest; - QString manifestError; - if (!scopeone::core::parsePluginManifest( - loader->metaData().value(QStringLiteral("MetaData")).toObject(), - scopeone::core::PluginKind::Tool, - manifest, - &manifestError)) - { - errors.append(QStringLiteral("%1: %2").arg(file.fileName(), manifestError)); - continue; - } + auto loader = std::make_unique(discovered.path); auto* plugin = qobject_cast(loader->instance()); if (!plugin) {