diff options
| author | Christian Kolset <christian.kolset@gmail.com> | 2026-05-07 13:02:34 -0600 |
|---|---|---|
| committer | Christian Kolset <christian.kolset@gmail.com> | 2026-06-03 09:50:17 -0600 |
| commit | deb3ad65d25d4c3167a3cbeca2ac19b5c45b0627 (patch) | |
| tree | 131eebcabfab2d0a19e52aec21a21ad0a24775fa /plugins/base_plugin.py | |
| parent | 2b9943ba0d28449a590acfa8a41555b174b6ba84 (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.py | 133 |
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.""" |
