summaryrefslogtreecommitdiff
path: root/plugins/base_plugin.py
diff options
context:
space:
mode:
authorChristian Kolset <christian.kolset@gmail.com>2026-05-07 13:02:34 -0600
committerChristian Kolset <christian.kolset@gmail.com>2026-06-03 09:50:17 -0600
commitdeb3ad65d25d4c3167a3cbeca2ac19b5c45b0627 (patch)
tree131eebcabfab2d0a19e52aec21a21ad0a24775fa /plugins/base_plugin.py
parent2b9943ba0d28449a590acfa8a41555b174b6ba84 (diff)
Added plugin/add-on system
Plugins live in plugins/<name>/ with a manifest.json and a LabPlugin subclass. Enabled/disabled in Settings > Plugins tab; state persists in plugins/enabled.json and in .labdaq profiles (loading a profile enables/disables plugins to match). Plugins can contribute toolbar buttons, BaseDevice instances (auto-wired into acquisition pipeline), custom signal filter classes, a settings widget, and profile state via get_save_state/apply_save_state. Load errors now report the exact failing stage (file not found, class missing, on_load crash). Includes example plugin skeleton and docs/plugin-development.md. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Diffstat (limited to 'plugins/base_plugin.py')
-rw-r--r--plugins/base_plugin.py133
1 files changed, 133 insertions, 0 deletions
diff --git a/plugins/base_plugin.py b/plugins/base_plugin.py
new file mode 100644
index 0000000..0eeb25f
--- /dev/null
+++ b/plugins/base_plugin.py
@@ -0,0 +1,133 @@
+"""
+plugins/base_plugin.py
+
+Abstract base class for all LabDAQ plugins.
+
+To create a plugin:
+ 1. Create a directory under plugins/ e.g. plugins/my_plugin/
+ 2. Add manifest.json with plugin metadata
+ 3. Add plugin.py with a class subclassing LabPlugin
+ 4. Enable in Settings → Plugins
+
+manifest.json schema:
+ {
+ "plugin_id": "my_plugin", required — unique snake_case id
+ "name": "My Plugin", required
+ "version": "1.0.0",
+ "description": "What this does.",
+ "author": "Name",
+ "entry_point": "plugin.MyPlugin" module.ClassName relative to plugin dir
+ }
+
+Integration points (all optional — override only what you need):
+
+ get_toolbar_actions() → list of PluginAction toolbar buttons in main window
+ get_devices() → list of BaseDevice auto-added to registry + engine
+ get_filter_classes() → {name: FilterBase cls} custom signal pipeline filters
+ get_settings_widget() → QWidget | None shown in Settings > Plugins
+ get_save_state() → dict persisted in .labdaq profiles
+ apply_save_state(dict) restore from profile
+"""
+
+from __future__ import annotations
+
+from abc import ABC, abstractmethod
+from dataclasses import dataclass, field
+from typing import Any, Callable, Dict, List, Optional, Type
+
+from PyQt6.QtWidgets import QWidget
+
+
+# ── Context ───────────────────────────────────────────────────────────────────
+
+@dataclass
+class PluginContext:
+ """Core app objects handed to each plugin on load."""
+ registry: Any # devices.device_registry.DeviceRegistry
+ engine: Any # core.acquisition.AcquisitionEngine
+ processor: Any # core.signal_processor.SignalProcessor
+ main_window: Any # ui.main_window.MainWindow (QMainWindow)
+
+
+# ── Toolbar action descriptor ─────────────────────────────────────────────────
+
+@dataclass
+class PluginAction:
+ """Describes one toolbar button contributed by a plugin."""
+ label: str
+ callback: Callable[[bool], None] # receives checked state
+ icon: str = ""
+ tooltip: str = ""
+ checkable: bool = False
+
+
+# ── Base class ────────────────────────────────────────────────────────────────
+
+class LabPlugin(ABC):
+ """
+ Abstract base for all LabDAQ plugins.
+
+ Subclass this, set the metadata properties, and override whatever
+ integration hooks your plugin needs.
+ """
+
+ # ── Metadata ──────────────────────────────────────────────────────────
+
+ @property
+ @abstractmethod
+ def plugin_id(self) -> str:
+ """Unique identifier matching manifest plugin_id."""
+
+ @property
+ @abstractmethod
+ def name(self) -> str:
+ """Human-readable display name."""
+
+ @property
+ def version(self) -> str:
+ return "1.0.0"
+
+ @property
+ def description(self) -> str:
+ return ""
+
+ @property
+ def author(self) -> str:
+ return ""
+
+ # ── Lifecycle ─────────────────────────────────────────────────────────
+
+ def on_load(self, context: PluginContext) -> None:
+ """Called when the plugin is enabled. Store context for later use."""
+
+ def on_unload(self) -> None:
+ """Called when the plugin is disabled. Release all resources."""
+
+ # ── Integration hooks ─────────────────────────────────────────────────
+
+ def get_toolbar_actions(self) -> List[PluginAction]:
+ """Toolbar buttons added to the main window when plugin is enabled."""
+ return []
+
+ def get_devices(self) -> list:
+ """BaseDevice instances contributed by this plugin.
+ They are automatically added to the DeviceRegistry and AcquisitionEngine."""
+ return []
+
+ def get_filter_classes(self) -> Dict[str, Type]:
+ """Custom signal filters: {filter_type_name: FilterBase subclass}.
+ Registered in SignalProcessor.FILTER_CLASSES when plugin loads."""
+ return {}
+
+ def get_settings_widget(self) -> Optional[QWidget]:
+ """Plugin-specific settings panel shown in Settings > Plugins."""
+ return None
+
+ # ── Profile persistence ───────────────────────────────────────────────
+
+ def get_save_state(self) -> Dict[str, Any]:
+ """Return a JSON-serialisable dict, saved with the .labdaq profile."""
+ return {}
+
+ def apply_save_state(self, state: Dict[str, Any]) -> None:
+ """Restore plugin state when a profile is loaded."""