""" 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 # Optional: main window calls this with the QPushButton after creation. # Lets the plugin store a reference to uncheck it when its window closes. button_ref_callback: Optional[Callable] = None # ── 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."""