summaryrefslogtreecommitdiff
path: root/core
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 /core
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 'core')
-rw-r--r--core/profile.py66
-rw-r--r--core/signal_processor.py10
2 files changed, 58 insertions, 18 deletions
diff --git a/core/profile.py b/core/profile.py
index d21bf2f..fdd3a48 100644
--- a/core/profile.py
+++ b/core/profile.py
@@ -67,15 +67,19 @@ class ProfileDerived:
@dataclass
class Profile:
- name: str = "Untitled Profile"
- version: str = "1.0"
- devices: List[Dict] = field(default_factory=list)
- controls: List[Dict] = field(default_factory=list)
- channels: List[Dict] = field(default_factory=list)
- pipelines: List[Dict] = field(default_factory=list)
- derived: List[Dict] = field(default_factory=list)
- plot: Optional[Dict] = None # LayoutConfig.to_json()
- settings: Dict[str, Any] = field(default_factory=dict)
+ name: str = "Untitled Profile"
+ version: str = "1.0"
+ devices: List[Dict] = field(default_factory=list)
+ controls: List[Dict] = field(default_factory=list)
+ channels: List[Dict] = field(default_factory=list)
+ pipelines: List[Dict] = field(default_factory=list)
+ derived: List[Dict] = field(default_factory=list)
+ plot: Optional[Dict] = None # LayoutConfig.to_json()
+ settings: Dict[str, Any] = field(default_factory=dict)
+ plugin_state: Dict[str, Any] = field(default_factory=dict)
+ # plugin_state = {plugin_id: plugin.get_save_state()}
+ plugins_enabled: List[str] = field(default_factory=list)
+ # plugins_enabled = [plugin_id, ...] — which plugins were on when saved
def to_json(self) -> str:
return json.dumps(asdict(self), indent=2)
@@ -84,15 +88,17 @@ class Profile:
def from_json(s: str) -> "Profile":
d = json.loads(s)
return Profile(
- name = d.get("name", ""),
- version = d.get("version", "1.0"),
- devices = d.get("devices", []),
- controls = d.get("controls", []),
- channels = d.get("channels", []),
- pipelines = d.get("pipelines", []),
- derived = d.get("derived", []),
- plot = d.get("plot"),
- settings = d.get("settings", {}),
+ name = d.get("name", ""),
+ version = d.get("version", "1.0"),
+ devices = d.get("devices", []),
+ controls = d.get("controls", []),
+ channels = d.get("channels", []),
+ pipelines = d.get("pipelines", []),
+ derived = d.get("derived", []),
+ plot = d.get("plot"),
+ settings = d.get("settings", {}),
+ plugin_state = d.get("plugin_state", {}),
+ plugins_enabled = d.get("plugins_enabled", []),
)
def save(self, path: str):
@@ -135,6 +141,7 @@ class ProfileManager:
control_specs: List,
settings: dict,
profile_name: str = "Profile",
+ plugin_manager=None,
) -> Profile:
p = Profile(name=profile_name)
@@ -193,6 +200,18 @@ class ProfileManager:
pass
p.settings = dict(settings)
+
+ # Plugin state + enabled list
+ if plugin_manager is not None:
+ p.plugins_enabled = plugin_manager.get_enabled_ids()
+ for plugin in plugin_manager.get_loaded():
+ try:
+ state = plugin.get_save_state()
+ if state:
+ p.plugin_state[plugin.plugin_id] = state
+ except Exception:
+ pass
+
return p
# ── Apply ─────────────────────────────────────────────────────────────────
@@ -205,6 +224,7 @@ class ProfileManager:
control_panel,
settings_ref: dict,
engine=None,
+ plugin_manager=None,
):
"""Apply a loaded profile to the live application state."""
from ui.control_editor import ControlSpec
@@ -329,4 +349,14 @@ class ProfileManager:
except Exception as e:
print(f"[Profile] Could not restore plot layout: {e}")
+ # Plugin state
+ if plugin_manager is not None and profile.plugin_state:
+ for plugin_id, state in profile.plugin_state.items():
+ plugin = plugin_manager.get_plugin(plugin_id)
+ if plugin is not None:
+ try:
+ plugin.apply_save_state(state)
+ except Exception as e:
+ print(f"[Profile] Plugin '{plugin_id}' state restore error: {e}")
+
return plot_cfg # caller applies this to the chart
diff --git a/core/signal_processor.py b/core/signal_processor.py
index 417d142..723de66 100644
--- a/core/signal_processor.py
+++ b/core/signal_processor.py
@@ -185,6 +185,16 @@ def filter_from_dict(d: dict) -> FilterBase:
return cls(**params)
+def register_filter_class(name: str, cls) -> None:
+ """Register a plugin-provided filter class by type name."""
+ FILTER_CLASSES[name] = cls
+
+
+def unregister_filter_class(name: str) -> None:
+ """Remove a previously registered plugin filter class."""
+ FILTER_CLASSES.pop(name, None)
+
+
# ══════════════════════════════════════════════════════════════════════════════
# Derived channel definitions
# ══════════════════════════════════════════════════════════════════════════════