# 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; }
    Task<string?> CreateRecipeAsync();
}
```

### 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 |
