Files
HawkeyeVision/docs/PLUGIN_SYSTEM.md
EugeneTes 4bd117af47 plugin system docs
custom buttons for plugins
calibration functions for Leeform
2026-03-30 16:15:47 +02:00

1107 lines
37 KiB
Markdown

# VisionBuilder Plugin System
This document provides in-depth documentation of the VisionBuilder plugin system architecture and step-by-step instructions for implementing new plugins.
## Table of Contents
- [Overview](#overview)
- [Architecture](#architecture)
- [Plugin Lifecycle](#plugin-lifecycle)
- [Dependency Injection Scoping](#dependency-injection-scoping)
- [Module Initialization Order](#module-initialization-order)
- [Core Interfaces](#core-interfaces)
- [IPlugin](#iplugin)
- [IVisionBuilderModule](#ivisionbuildermodule)
- [ISettings](#isettings)
- [IRecognitionControl](#irecognitioncontrol)
- [BaseRecognitionControl](#baserecognitioncontrol)
- [IImageSource](#iimagesource)
- [ILoadingService](#iloadingservice)
- [IRecipeCreationTool](#irecipecreationtool)
- [ILearningTool](#ilearningtool)
- [Plugin Discovery and Loading](#plugin-discovery-and-loading)
- [Directory Structure](#directory-structure)
- [enabled_plugins.txt](#enabled_pluginstxt)
- [PluginLoader](#pluginloader)
- [PluginLoadContext (Assembly Isolation)](#pluginloadcontext-assembly-isolation)
- [Plugin Profiles](#plugin-profiles)
- [Event System](#event-system)
- [Extension Methods Reference](#extension-methods-reference)
- [How to Implement a Plugin](#how-to-implement-a-plugin)
- [Step 1: Create the Project](#step-1-create-the-project)
- [Step 2: Implement IPlugin](#step-2-implement-iplugin)
- [Step 3: Create Modules](#step-3-create-modules)
- [Step 4: Add Settings](#step-4-add-settings)
- [Step 5: Deploy and Enable](#step-5-deploy-and-enable)
- [Common Plugin Patterns](#common-plugin-patterns)
- [Pattern: Add a Background Module](#pattern-add-a-background-module)
- [Pattern: Replace the Recognition Control](#pattern-replace-the-recognition-control)
- [Pattern: React to Image Processing Events](#pattern-react-to-image-processing-events)
- [Pattern: External Hardware Integration (PLC/Serial)](#pattern-external-hardware-integration-plcserial)
- [Pattern: Register Custom Type Converters](#pattern-register-custom-type-converters)
- [Pattern: Override UI Tools](#pattern-override-ui-tools)
- [Pattern: Add Dynamic UI Buttons](#pattern-add-dynamic-ui-buttons)
- [Existing Plugin Examples](#existing-plugin-examples)
- [Key Source File Locations](#key-source-file-locations)
---
## Overview
The plugin system allows extending VisionBuilder with external assemblies that are loaded at runtime. Plugins can:
- Register app-wide (global) services and modules
- Register per-camera services and modules
- Replace core services like `IRecognitionControl`, `ILearningTool`, `IRecipeCreationTool`
- Subscribe to image processing events and camera lifecycle events
- Add configurable settings that persist via `InspectronSettings`
- Register custom type converters for settings serialization
- Integrate with external hardware (PLCs, barcode readers, serial devices)
Plugins are loaded from the `../Data/Plugins/` directory and are enabled/disabled via a text configuration file.
## Architecture
### Plugin Lifecycle
```
Application Startup
|
v
1. Main kernel created (StandardKernel)
|
v
2. kernel.UsePlugins(profile)
|--- PluginLoader registered as IVisionBuilderModule
|--- PluginLoader.InitializeModule(profile) called
|--- Scans ../Data/Plugins/ for enabled plugin folders
|--- Loads assemblies, discovers IPlugin implementations
|--- Stores plugins in PluginLoader.Plugins list
|
v
3. kernel.RegisterGlobalPlugins()
|--- For each IPlugin: calls RegisterGlobalModules(mainKernel)
|--- Plugins register global modules, type converters, rebind global tools
|
v
4. For each camera:
|
|--- Create ChildKernel(mainKernel)
|--- Register camera-specific bindings (CameraSettings, etc.)
|--- kernel.RegisterCameraPlugins(cameraName)
| |--- For each IPlugin: calls RegisterCameraModules(childKernel, cameraName)
| |--- Plugins register camera modules, settings, rebind per-camera services
|
|--- kernel.RegisterSettings()
| |--- Finds all ISettings in kernel, calls RegisterSettings(InspectronSettings)
|
|--- kernel.InitializeModules()
| |--- Finds all IVisionBuilderModule in kernel
| |--- Sorts by [ModulePriority] descending (higher = earlier)
| |--- Calls InitializeModule() on each, skipping already-initialized
|
v
5. Application running — modules active, events flowing
```
### Dependency Injection Scoping
The system uses two levels of Ninject kernels:
**Main Kernel (StandardKernel)** — Global scope:
- Shared across all cameras
- Holds `InspectronSettings`, `UIConfiguration`, `PluginLoader`
- Global modules registered here are singletons for the entire app
- `RegisterGlobalModules()` receives this kernel
**Child Kernels (ChildKernel)** — Per-camera scope:
- One child kernel per camera
- Inherits bindings from the main kernel (can resolve global services)
- Camera-specific bindings: `CameraSettings`, `SingleCameraVM`, `IRecognitionControl`, `IImageSource`
- `RegisterCameraModules()` receives the child kernel
- Services bound here are isolated per camera
```
MainKernel (StandardKernel)
|--- InspectronSettings (singleton)
|--- PluginLoader (singleton)
|--- IRecipeCreationTool (singleton, rebindable)
|--- Global IVisionBuilderModule instances
|
|--- ChildKernel (Camera "Left")
| |--- CameraSettings
| |--- SingleCameraVM (singleton)
| |--- IRecognitionControl (singleton)
| |--- IImageSource
| |--- Camera-specific IVisionBuilderModule instances
| |--- Camera-specific ISettings instances
|
|--- ChildKernel (Camera "Right")
|--- (same structure, independent instances)
```
### Module Initialization Order
Modules are initialized via `kernel.InitializeModules()`, which:
1. Collects all `IVisionBuilderModule` instances from the kernel
2. Filters out already-initialized modules (tracked per kernel)
3. Sorts by `[ModulePriority(n)]` attribute in **descending** order (higher priority = initialized first)
4. Calls `InitializeModule()` on each in order
Default priority is `0` if no attribute is present. The `PluginLoader` itself uses priority `1000` to ensure it runs first.
```csharp
[ModulePriority(100)] // Initialized before modules with lower priority
public class MyCriticalModule : IVisionBuilderModule { ... }
public class MyRegularModule : IVisionBuilderModule { ... } // Priority 0 (default)
```
## Core Interfaces
### IPlugin
**Location:** `VisionBuilder.UI.Common/Plugins/IPlugin.cs`
The entry point for all plugins. Each plugin assembly must contain at least one class implementing this interface.
```csharp
public interface IPlugin
{
void RegisterGlobalModules(IKernel kernel);
void RegisterCameraModules(IKernel kernel, string cameraName);
}
```
- `RegisterGlobalModules(IKernel kernel)` — Called **once** at startup with the main kernel. Register app-wide services, type converters, and rebind global tool interfaces here.
- `RegisterCameraModules(IKernel kernel, string cameraName)` — Called **once per camera** with the camera's child kernel. Register camera-specific modules, settings, and service overrides here.
### IVisionBuilderModule
**Location:** `VisionBuilder.UI.Common/IVisionBuilderModule.cs`
Modules are lifecycle-managed components that perform deferred initialization after the DI container is fully configured.
```csharp
public interface IVisionBuilderModule
{
void InitializeModule();
}
```
Register modules using the extension method:
```csharp
kernel.RegisterModule<MyModule>();
// Equivalent to:
// kernel.Bind<MyModule, IVisionBuilderModule>().To<MyModule>().InSingletonScope();
```
Modules receive their dependencies via **constructor injection** from Ninject. `InitializeModule()` is the place to start background tasks, subscribe to events, open connections, etc.
### ISettings
**Location:** `VisionBuilder.UI.Common/ISettings.cs`
Settings classes register configurable properties with the `InspectronSettings` system.
```csharp
public interface ISettings
{
void RegisterSettings(InspectronSettings settings);
}
```
Settings are bound as constants (since they hold state) and must also be bound to `ISettings` so the framework discovers them:
```csharp
kernel.Bind<MyPluginSettings, ISettings>().ToConstant(new MyPluginSettings(cameraName));
```
Inside `RegisterSettings`, use `InspectronSettings.RegisterSimple()` to expose properties:
```csharp
public class MyPluginSettings : ISettings
{
public string CameraName { get; }
public int PollingInterval { get; set; } = 1000;
public string ServerAddress { get; set; } = "127.0.0.1";
public MyPluginSettings(string cameraName) => CameraName = cameraName;
public void RegisterSettings(InspectronSettings settings)
{
settings.RegisterSimple(this, () => PollingInterval, CameraName + "/MyPlugin", "Polling interval (ms)");
settings.RegisterSimple(this, () => ServerAddress, CameraName + "/MyPlugin", nameof(ServerAddress));
}
}
```
### IRecognitionControl
**Location:** `VisionBuilder.UI.Common/Processing/IRecognitionControl.cs`
The core interface for image processing control. One instance per camera.
```csharp
public interface IRecognitionControl
{
bool IsRunning { get; }
List<RecipeData> GetRecipesData();
void SetRecipe(RecipeData recipe);
void Start();
void Stop();
void Pause();
void Resume();
event Action<ImageProcessedEvent> ImageProcessed;
event Action<SessionStartedEvent> SessionStarted;
event Action<SessionEndedEvent> SessionEnded;
event Action<ErrorsInSequenceEvent> ErrorsInSequenceAlarm;
void HotReload();
event Action RecipeFileChanged;
}
```
Events fired during the processing loop:
- `ImageProcessed` — After each image is processed (contains result, errors, timing)
- `SessionStarted` — When `Start()` is called successfully
- `SessionEnded` — When `Stop()` completes
- `ErrorsInSequenceAlarm` — When consecutive errors exceed the configured threshold
### BaseRecognitionControl
**Location:** `VisionBuilder.UI.Common/Processing/BaseRecognitionControl.cs`
Abstract base class that implements the recognition loop. Extend this when your plugin needs a custom image processing pipeline.
```csharp
public abstract class BaseRecognitionControl : IRecognitionControl
{
// Constructor — requires settings and loading service
public BaseRecognitionControl(
BaseRecognitionControlSettings settings,
ILoadingService loadingService);
// Must implement:
public abstract List<RecipeData> GetRecipesData();
protected abstract void Initialize(RecipeData currentRecipe);
protected abstract void WarmUp();
protected abstract (Mat originalImage, Mat analysisImage,
TimeSpan processingTime, TimeSpan acquisitionTime,
string[] errorNames)? ProcessImage(CancellationToken token);
// Optional override:
protected virtual void Cleanup() { }
public virtual void HotReload() { }
}
```
The base class handles:
- Thread-safe start/stop/pause/resume lifecycle
- The main processing loop (calls `ProcessImage` repeatedly)
- Minimum processing time enforcement (configurable via settings)
- Consecutive error tracking and alarm firing
- Event dispatch (`ImageProcessed`, `SessionStarted`, `SessionEnded`, `ErrorsInSequenceAlarm`)
**RecipeData:**
```csharp
public class RecipeData
{
public Mat? Image { get; set; } // Thumbnail image for UI
public string RecipeName { get; set; } // Recipe identifier
}
```
**BaseRecognitionControlSettings:**
```csharp
public abstract class BaseRecognitionControlSettings(string cameraName) : ISettings
{
public string CameraName { get; }
public int MinimumProcessingTime { get; set; } // ms, enforced between frames
public string RecipeNameFilter { get; set; } = "*"; // Glob filter for recipe listing
public int ErrorsInSequenceAlarm { get; set; } = 5; // Threshold for alarm event
}
```
### IImageSource
**Location:** `VisionBuilder.UI.Common/Processing/IImageSource.cs`
Provides images to the recognition control.
```csharp
public interface IImageSource
{
Task<Mat> GetImage(CancellationToken token);
}
```
### ILoadingService
**Location:** `VisionBuilder.UI.Common/Processing/ILoadingService.cs`
Shows/hides loading indicators in the UI during long operations.
```csharp
public interface ILoadingService
{
void StartLoading(string title);
void StopLoading(string title);
}
```
### IRecipeCreationTool
**Location:** `VisionBuilder.UI.Common/ViewModel/Interfaces/UI/IRecipeCreationTool.cs`
UI tool for creating new recipes. Default implementation (`NoRecipeCreation`) is disabled. Plugins can `Rebind` this to provide custom recipe creation.
```csharp
public interface IRecipeCreationTool
{
bool Enabled { get; set; }
bool CreateRecipe(out string recipeName);
}
```
### ILearningTool
**Location:** `VisionBuilder.UI.Common/ViewModel/Interfaces/UI/ILearningTool.cs`
UI tool for learning/training recipes from images. Default implementation (`NoLearning`) is disabled.
```csharp
public interface ILearningTool
{
bool IsLearningEnabled(string recipeName);
void Learn(string recipeName, Mat image);
}
```
## Plugin Discovery and Loading
### Directory Structure
Plugins are deployed to the `../Data/Plugins/` directory (relative to the application executable). Each plugin resides in its own subfolder:
```
Data/
Plugins/
enabled_plugins.txt # Lists enabled plugins (one per line)
my_custom_plugins.txt # Alternative profile (optional)
B24SiemensPlugin/
B24SiemensPlugin.dll # Plugin assembly (name must match folder)
(dependency DLLs)
CandyboxPlugin/
CandyboxPlugin.dll
(dependency DLLs)
MyNewPlugin/
MyNewPlugin.dll # Your plugin
SomeLibrary.dll # Any additional dependencies
```
**Critical:** The folder name and the main DLL name (without extension) must match exactly. The loader looks for `<FolderName>/<FolderName>.dll`.
### enabled_plugins.txt
A simple text file listing enabled plugin folder names, one per line:
```
B24SiemensPlugin
CandyboxPlugin
MyNewPlugin
```
Plugins not listed here are skipped during loading. If the file doesn't exist, it's created empty (no plugins enabled).
### PluginLoader
**Location:** `VisionBuilder.UI.Common/Plugins/PluginLoader.cs`
The `PluginLoader` is an `IVisionBuilderModule` with `[ModulePriority(1000)]` (highest built-in priority). It:
1. Reads the enabled plugins list from `../Data/Plugins/<profile>.txt`
2. Iterates over subdirectories in `../Data/Plugins/`
3. Skips folders not in the enabled list
4. Loads the main assembly from each enabled folder using `PluginLoadContext`
5. Scans loaded assembly types for `IPlugin` implementations
6. Creates instances via `Activator.CreateInstance()` and stores them in `Plugins` list
### PluginLoadContext (Assembly Isolation)
**Location:** `VisionBuilder.UI.Common/Plugins/PluginLoadContext.cs`
Each plugin is loaded in its own `AssemblyLoadContext` to isolate dependencies:
- Uses `AssemblyDependencyResolver` to resolve managed assemblies from the plugin folder
- Supports native/unmanaged DLL loading for plugins with native dependencies
- Prevents version conflicts between plugins and the host application
**Important project reference configuration:** Plugin projects must reference `VisionBuilder.UI.Common` (and other host assemblies) with `<Private>false</Private>` and `<ExcludeAssets>runtime</ExcludeAssets>` to avoid duplicating host assemblies in the plugin output.
### Plugin Profiles
The application supports multiple plugin configurations via profiles. The `--profile` / `-p` command-line argument selects which `.txt` file to use:
```bash
# Uses ../Data/Plugins/enabled_plugins.txt (default)
VisionBuilder.exe
# Uses ../Data/Plugins/production.txt
VisionBuilder.exe --profile production
# Uses ../Data/Plugins/testing.txt
VisionBuilder.exe -p testing
```
## Event System
The recognition control fires events as `Action<T>` delegates. Modules subscribe in `InitializeModule()`:
### ImageProcessedEvent
Fired after each image is processed.
```csharp
public class ImageProcessedEvent : IEvent
{
public DateTime SessionStart { get; set; }
public string ImageSource { get; set; } // Camera name
public string RecipeName { get; set; }
public Mat ImageOriginal { get; set; } // Raw camera image
public Mat ImageAnalysis { get; set; } // Annotated result image
public bool HasError { get; set; }
public List<string> ErrorNames { get; set; } // Detected defects/errors
public TimeSpan AnalysisTime { get; set; }
public TimeSpan AcquisitionTime { get; set; }
public TimeSpan SleepTime { get; set; }
}
```
### SessionStartedEvent
Fired when recognition starts.
```csharp
public class SessionStartedEvent
{
public string RecipeName { get; set; }
public DateTime SessionStarted { get; set; }
}
```
### SessionEndedEvent
Fired when recognition stops.
```csharp
public class SessionEndedEvent
{
public DateTime SessionEnded { get; set; }
}
```
### ErrorsInSequenceEvent
Fired when consecutive error count reaches the threshold.
```csharp
public class ErrorsInSequenceEvent
{
public int Errors { get; set; }
}
```
## Extension Methods Reference
**Location:** `VisionBuilder.UI.Common/Extensions.cs`
| Method | Description |
|--------|-------------|
| `kernel.RegisterModule<T>()` | Binds `T` as both itself and `IVisionBuilderModule` in singleton scope |
| `kernel.UsePlugins(profile)` | Registers and initializes the `PluginLoader` with the given profile |
| `kernel.RegisterGlobalPlugins()` | Calls `RegisterGlobalModules()` on all loaded plugins |
| `kernel.RegisterCameraPlugins(cameraName)` | Calls `RegisterCameraModules()` on all loaded plugins (child kernel) |
| `kernel.RegisterSettings()` | Finds all `ISettings` instances and calls `RegisterSettings()` |
| `kernel.InitializeModules()` | Initializes all `IVisionBuilderModule` instances in priority order |
## How to Implement a Plugin
### Step 1: Create the Project
Create a new .NET 8.0 class library project. The critical settings are:
```xml
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<!-- Required: enables dynamic loading as a plugin -->
<EnableDynamicLoading>true</EnableDynamicLoading>
<!-- Output directly to the Plugins directory for development -->
<OutDir>..\Data\Plugins\MyPlugin</OutDir>
</PropertyGroup>
<ItemGroup>
<!-- Reference the common library — Private=false prevents copying host DLLs -->
<ProjectReference Include="..\VisionBuilder.UI.Common\VisionBuilder.UI.Common.csproj">
<Private>false</Private>
<ExcludeAssets>runtime</ExcludeAssets>
</ProjectReference>
<!-- Reference settings framework if needed -->
<ProjectReference Include="..\framework\Inspectron.Settings\Inspectron.Settings.csproj">
<Private>false</Private>
<ExcludeAssets>runtime</ExcludeAssets>
</ProjectReference>
<!-- Add any other host references with Private=false -->
</ItemGroup>
<ItemGroup>
<!-- Plugin-specific NuGet packages (these WILL be copied to output) -->
<!-- <PackageReference Include="SomeLibrary" Version="1.0.0" /> -->
</ItemGroup>
</Project>
```
**Key project settings explained:**
- `EnableDynamicLoading` — Tells the build system this assembly will be loaded dynamically. Ensures all plugin-specific dependencies are copied to the output directory.
- `Private=false` / `ExcludeAssets=runtime` on host references — Prevents the plugin from copying VisionBuilder.UI.Common.dll and other host assemblies into the plugin folder. The host application already has these loaded; duplicating them causes type identity conflicts.
- `OutDir` — Set to the Plugins directory for easy development. Adjust the relative path based on your project location.
### Step 2: Implement IPlugin
Create a `Plugin.cs` (the class name can be anything):
```csharp
using Ninject;
using VisionBuilder.UI.Common;
using VisionBuilder.UI.Common.Plugins;
namespace MyPlugin;
public class Plugin : IPlugin
{
public void RegisterGlobalModules(IKernel kernel)
{
// Called once at startup with the main kernel.
// Register app-wide services, type converters, or rebind global tools.
// Example: Register a custom type converter for settings
// TypeConverterRegistry.Register<List<MyMapping>>(new MyMappingConverter());
// Example: Override the recipe creation tool globally
// kernel.Rebind<IRecipeCreationTool>().To<MyRecipeCreationTool>().InSingletonScope();
}
public void RegisterCameraModules(IKernel kernel, string cameraName)
{
// Called once per camera with the camera's child kernel.
// Register camera-specific modules and settings.
// Register a module (will be initialized later via InitializeModules)
kernel.RegisterModule<MyCameraModule>();
// Register settings (bound as constant since they hold state)
kernel.Bind<MyPluginSettings, ISettings>()
.ToConstant(new MyPluginSettings(cameraName));
}
}
```
### Step 3: Create Modules
Modules do the actual work. They receive dependencies via constructor injection and perform initialization in `InitializeModule()`.
```csharp
using Serilog;
using VisionBuilder.UI.Common;
using VisionBuilder.UI.Common.Processing;
namespace MyPlugin;
public class MyCameraModule : IVisionBuilderModule
{
private readonly IRecognitionControl _recognitionControl;
private readonly MyPluginSettings _settings;
// Dependencies are injected by Ninject from the camera's child kernel
public MyCameraModule(
IRecognitionControl recognitionControl,
MyPluginSettings settings)
{
_recognitionControl = recognitionControl;
_settings = settings;
}
public void InitializeModule()
{
// Subscribe to events
_recognitionControl.ImageProcessed += OnImageProcessed;
_recognitionControl.SessionStarted += OnSessionStarted;
_recognitionControl.SessionEnded += OnSessionEnded;
Log.Information("MyPlugin initialized for camera {Camera}", _settings.CameraName);
}
private void OnImageProcessed(ImageProcessedEvent e)
{
if (e.HasError)
{
Log.Warning("Errors detected: {Errors}", string.Join(", ", e.ErrorNames));
// React to errors — send signal, log, notify, etc.
}
}
private void OnSessionStarted(SessionStartedEvent e)
{
Log.Information("Session started: {Recipe}", e.RecipeName);
}
private void OnSessionEnded(SessionEndedEvent e)
{
Log.Information("Session ended");
}
}
```
**Available services for constructor injection in camera modules:**
| Service | Description |
|---------|-------------|
| `IRecognitionControl` | Image processing control for this camera |
| `SingleCameraVM` | ViewModel for this camera (UI state, commands, `DynamicButtons` for adding custom buttons) |
| `CameraSettings` | Camera configuration (source type, label) |
| `IImageSource` | Image provider for this camera |
| `InspectronSettings` | Global settings manager |
| `ILoadingService` | Loading dialog service |
| Any `ISettings` bound in the kernel | Plugin-specific settings |
| Any `IVisionBuilderModule` bound in the kernel | Other modules (be careful of initialization order) |
### Step 4: Add Settings
Create a settings class to expose configurable properties:
```csharp
using Inspectron.Settings;
using VisionBuilder.UI.Common;
namespace MyPlugin;
public class MyPluginSettings : ISettings
{
public string CameraName { get; }
// Configurable properties with defaults
public string ServerAddress { get; set; } = "127.0.0.1";
public int Port { get; set; } = 5000;
public bool Enabled { get; set; } = true;
public MyPluginSettings(string cameraName)
{
CameraName = cameraName;
}
public void RegisterSettings(InspectronSettings settings)
{
// Register each property under a category path
// Format: settings.RegisterSimple(owner, propertyExpression, category, displayName)
settings.RegisterSimple(this, () => ServerAddress, CameraName + "/MyPlugin", "Server address");
settings.RegisterSimple(this, () => Port, CameraName + "/MyPlugin", "Server port");
settings.RegisterSimple(this, () => Enabled, CameraName + "/MyPlugin", "Enable plugin");
}
}
```
Settings are automatically persisted and restored by the `InspectronSettings` infrastructure. The category path (`CameraName + "/MyPlugin"`) determines how settings appear in the UI.
### Step 5: Deploy and Enable
1. **Build the plugin** — output goes to `Data/Plugins/MyPlugin/`
2. **Add to enabled_plugins.txt** — append `MyPlugin` to `../Data/Plugins/enabled_plugins.txt`
3. **Restart the application** — the plugin will be discovered and loaded
```
Data/
Plugins/
enabled_plugins.txt # Add "MyPlugin" line
MyPlugin/
MyPlugin.dll # Folder name must match DLL name
```
## Common Plugin Patterns
### Pattern: Add a Background Module
Register a module that runs background work (e.g., listening on a port, polling a device).
```csharp
// In Plugin.RegisterCameraModules:
kernel.RegisterModule<MyBackgroundModule>();
// Module implementation:
public class MyBackgroundModule : IVisionBuilderModule
{
private readonly MyPluginSettings _settings;
private TcpListener _listener;
public MyBackgroundModule(MyPluginSettings settings)
{
_settings = settings;
}
public void InitializeModule()
{
_listener = new TcpListener(IPAddress.Any, _settings.Port);
_listener.Start();
Task.Run(() => AcceptClients());
}
private async Task AcceptClients() { /* ... */ }
}
```
### Pattern: Replace the Recognition Control
Provide a completely custom image processing pipeline by replacing `IRecognitionControl`.
```csharp
// In Plugin.RegisterCameraModules:
// 1. Remove existing settings binding (if replacing BaseRecognitionControlSettings)
var existingSettings = kernel.GetBindings(typeof(BaseRecognitionControlSettings)).First();
var toRemove = kernel.GetBindings(typeof(ISettings))
.First(x => x.ProviderCallback.Target == existingSettings.ProviderCallback.Target);
kernel.RemoveBinding(toRemove);
// 2. Bind new settings
kernel.Bind<MyRecognitionSettings, BaseRecognitionControlSettings, ISettings>()
.ToConstant(new MyRecognitionSettings(cameraName));
// 3. Rebind the recognition control
kernel.Rebind<IRecognitionControl>().To<MyRecognitionControl>().InSingletonScope();
// Settings class:
public class MyRecognitionSettings : BaseRecognitionControlSettings
{
public MyRecognitionSettings(string cameraName) : base(cameraName) { }
public string CustomProperty { get; set; } = "default";
public override void RegisterSettings(InspectronSettings settings)
{
base.RegisterSettings(settings); // Register base properties
settings.RegisterSimple(this, () => CustomProperty, CameraName + "/MyRecognition", nameof(CustomProperty));
}
}
// Recognition control:
public class MyRecognitionControl : BaseRecognitionControl
{
private readonly IImageSource _imageSource;
public MyRecognitionControl(
MyRecognitionSettings settings,
ILoadingService loadingService,
IImageSource imageSource) : base(settings, loadingService)
{
_imageSource = imageSource;
}
public override List<RecipeData> GetRecipesData()
{
// Return available recipes
return new List<RecipeData>
{
new() { RecipeName = "Recipe1", Image = CreateThumbnail() }
};
}
protected override void Initialize(RecipeData currentRecipe)
{
// Called when Start() is invoked — load recipe data, initialize models, etc.
}
protected override void WarmUp()
{
// Called once after Initialize, before the main loop.
// Use for warm-up runs (e.g., first inference is slow for AI models).
}
protected override (Mat originalImage, Mat analysisImage,
TimeSpan processingTime, TimeSpan acquisitionTime,
string[] errorNames)? ProcessImage(CancellationToken token)
{
var sw = Stopwatch.StartNew();
var image = _imageSource.GetImage(token).Result;
var acquisitionTime = sw.Elapsed;
// Process image...
var errors = AnalyzeImage(image);
sw.Stop();
return (image, image, sw.Elapsed, acquisitionTime, errors);
// Return null to stop the loop
}
protected override void Cleanup()
{
// Called after Stop() — release resources
}
}
```
### Pattern: React to Image Processing Events
Subscribe to events from the recognition control to react to results.
```csharp
public class ResultReporterModule : IVisionBuilderModule
{
private readonly IRecognitionControl _recognitionControl;
public ResultReporterModule(IRecognitionControl recognitionControl)
{
_recognitionControl = recognitionControl;
}
public void InitializeModule()
{
_recognitionControl.ImageProcessed += OnImageProcessed;
_recognitionControl.ErrorsInSequenceAlarm += OnErrorAlarm;
}
private void OnImageProcessed(ImageProcessedEvent e)
{
// e.HasError — true if any errors detected
// e.ErrorNames — list of detected error/defect names
// e.ImageOriginal — raw image (Mat)
// e.ImageAnalysis — annotated image (Mat)
// e.AnalysisTime — processing duration
// e.RecipeName — active recipe
}
private void OnErrorAlarm(ErrorsInSequenceEvent e)
{
// e.Errors — number of consecutive errors
// Trigger alarm, stop line, notify operator, etc.
}
}
```
### Pattern: External Hardware Integration (PLC/Serial)
Integrate with PLC controllers or serial devices.
```csharp
public class PLCIntegrationModule : IVisionBuilderModule
{
private readonly IRecognitionControl _recognitionControl;
private readonly SingleCameraVM _cameraVm;
private readonly MyPLCSettings _settings;
public PLCIntegrationModule(
IRecognitionControl recognitionControl,
SingleCameraVM cameraVm,
MyPLCSettings settings)
{
_recognitionControl = recognitionControl;
_cameraVm = cameraVm;
_settings = settings;
}
public void InitializeModule()
{
// Start PLC communication
var server = new TcpListener(IPAddress.Any, _settings.Port);
server.Start();
// React to processing results
_recognitionControl.ImageProcessed += e =>
{
SendResultToPLC(e.HasError ? "NOK" : "OK");
};
}
// Use SingleCameraVM to control the camera programmatically
private void SelectRecipe(string recipeName)
{
var vm = _cameraVm.GetRecipeSelectionVm();
vm.SelectedRecipe = vm.Recipes.FirstOrDefault(r => r.RecipeName == recipeName);
if (vm.SelectedRecipe != null)
{
// Must post to UI thread for SingleCameraVM operations
_cameraVm.SynchronizationContext!.Post(_ =>
{
_cameraVm.ProcessRecipeSelectionVm(vm);
}, null);
}
}
}
```
**Note:** When interacting with `SingleCameraVM` from a background thread, always use `SynchronizationContext.Post()` to marshal calls to the UI thread.
### Pattern: Register Custom Type Converters
If your settings contain complex types that need custom serialization:
```csharp
// In Plugin.RegisterGlobalModules:
public void RegisterGlobalModules(IKernel kernel)
{
TypeConverterRegistry.Register<List<MyMapping>>(new MyMappingConverter());
}
// Converter implementation:
public class MyMappingConverter : ITypeConverter
{
public object ConvertFrom(object value)
{
// Deserialize from string (e.g., JSON)
return JsonSerializer.Deserialize<List<MyMapping>>((string)value);
}
public object ConvertTo(object value, Type destinationType)
{
// Serialize to string
return JsonSerializer.Serialize((List<MyMapping>)value);
}
}
```
### Pattern: Override UI Tools
Replace default (disabled) UI tools with functional implementations:
```csharp
public void RegisterGlobalModules(IKernel kernel)
{
// Enable recipe creation
kernel.Rebind<IRecipeCreationTool>().To<MyRecipeCreationTool>().InSingletonScope();
}
public void RegisterCameraModules(IKernel kernel, string cameraName)
{
// Enable learning per camera
kernel.Rebind<ILearningTool>().To<MyLearningTool>().InSingletonScope();
}
```
### Pattern: Add Dynamic UI Buttons
Add custom buttons to the camera control panel (right side) or the main window bottom bar at runtime.
**Camera-level buttons** (right-side panel per camera):
```csharp
public class MyCameraModule : IVisionBuilderModule
{
private readonly SingleCameraVM _cameraVm;
public MyCameraModule(SingleCameraVM cameraVm)
{
_cameraVm = cameraVm;
}
public void InitializeModule()
{
// Add a click button
_cameraVm.AddButton(new ButtonDefinition(
"myPlugin.resetCounters", "Reset Counters", () =>
{
// Handle click
}));
// Add a toggle button
_cameraVm.AddButton(new ButtonDefinition(
"myPlugin.autoMode", "Auto Mode", (isToggled) =>
{
// Handle toggle state change
}, initialState: false));
// Control visibility later
_cameraVm.SetButtonVisibility("myPlugin.resetCounters", false);
// Remove a button
_cameraVm.RemoveButton("myPlugin.autoMode");
}
}
```
**App-level buttons** (bottom bar):
```csharp
public void RegisterGlobalModules(IKernel kernel)
{
var mainVm = kernel.Get<MainWindowVM>();
mainVm.AddButton(new ButtonDefinition(
"myPlugin.dashboard", "Dashboard", () =>
{
// Open dashboard window
}));
}
```
**ButtonDefinition properties:**
| Property | Type | Description |
|----------|------|-------------|
| `Key` | `string` | Unique identifier for the button (use plugin prefix, e.g., `"myPlugin.action"`) |
| `Title` | `string` | Display text on the button |
| `Type` | `ButtonType` | `Click` or `Toggle` |
| `IsVisible` | `bool` | Controls button visibility (default: `true`). Observable — UI updates automatically |
| `IsToggled` | `bool` | Current toggle state (only for `Toggle` buttons). Observable |
**Thread safety:** When calling `AddButton`, `RemoveButton`, or `SetButtonVisibility` from a background thread, marshal to the UI thread via `SynchronizationContext.Post`:
```csharp
_cameraVm.SynchronizationContext!.Post(_ =>
{
_cameraVm.AddButton(new ButtonDefinition("key", "Title", () => { }));
}, null);
```
## Existing Plugin Examples
| Plugin | Purpose | Key Patterns Used |
|--------|---------|-------------------|
| **TestPlugin** | Minimal reference implementation | Module registration, custom recognition control |
| **B24SiemensPlugin** | Siemens PLC integration for recipe selection | Type converter, camera module, settings, PLC communication |
| **CandyboxPlugin** | Full-featured candy box inspection | Replace recognition control, barcode reader, recipe creation tool, learning tool |
| **PralinenPLC** | Send pass/fail results to PLC via TCP | Event subscription, network communication |
| **PackstrasseBarcodeReader** | Serial barcode reader for recipe selection | Settings, serial port module, barcode-to-recipe mapping |
## Key Source File Locations
| Component | Path |
|-----------|------|
| `IPlugin` interface | `VisionBuilder.UI.Common/Plugins/IPlugin.cs` |
| `IVisionBuilderModule` interface | `VisionBuilder.UI.Common/IVisionBuilderModule.cs` |
| `ISettings` interface | `VisionBuilder.UI.Common/ISettings.cs` |
| `PluginLoader` | `VisionBuilder.UI.Common/Plugins/PluginLoader.cs` |
| `PluginLoadContext` | `VisionBuilder.UI.Common/Plugins/PluginLoadContext.cs` |
| Extension methods (`RegisterModule`, etc.) | `VisionBuilder.UI.Common/Extensions.cs` |
| `ModulePriorityAttribute` | `VisionBuilder.UI.Common/Attributes/ModulePriorityAttribute.cs` |
| `IRecognitionControl` | `VisionBuilder.UI.Common/Processing/IRecognitionControl.cs` |
| `BaseRecognitionControl` | `VisionBuilder.UI.Common/Processing/BaseRecognitionControl.cs` |
| `BaseRecognitionControlSettings` | `VisionBuilder.UI.Common/Processing/BaseRecognitionControlSettings.cs` |
| `IImageSource` | `VisionBuilder.UI.Common/Processing/IImageSource.cs` |
| `ILoadingService` | `VisionBuilder.UI.Common/Processing/ILoadingService.cs` |
| `IRecipeCreationTool` | `VisionBuilder.UI.Common/ViewModel/Interfaces/UI/IRecipeCreationTool.cs` |
| `ILearningTool` | `VisionBuilder.UI.Common/ViewModel/Interfaces/UI/ILearningTool.cs` |
| `ImageProcessedEvent` | `VisionBuilder.UI.Common/Commands/ImageProcessedEvent.cs` |
| `RecipeData` | `VisionBuilder.UI.Common/ViewModel/Classes/RecipeData.cs` |
| `TypeConverterRegistry` | `framework/Inspectron.Settings/TypeConverterRegistry.cs` |
| `SingleCameraVM` | `VisionBuilder.UI.Common/ViewModel/SingleCameraVM.cs` |
| `ButtonDefinition` | `VisionBuilder.UI.Common/ViewModel/Classes/ButtonDefinition.cs` |
| Existing plugins | `Plugins/` directory |