From deb3ad65d25d4c3167a3cbeca2ac19b5c45b0627 Mon Sep 17 00:00:00 2001 From: Christian Kolset Date: Thu, 7 May 2026 13:02:34 -0600 Subject: Added plugin/add-on system Plugins live in plugins// 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 --- core/profile.py | 66 +++++++++++++++++++++++++++++++++++------------- core/signal_processor.py | 10 ++++++++ 2 files changed, 58 insertions(+), 18 deletions(-) (limited to 'core') 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 # ══════════════════════════════════════════════════════════════════════════════ -- cgit v1.2.3