1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
|
"""
plugins/base_plugin.py
Abstract base class for all LabUI 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 .labui 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 LabUI 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 .labui profile."""
return {}
def apply_save_state(self, state: Dict[str, Any]) -> None:
"""Restore plugin state when a profile is loaded."""
|