Plugin state is saved in the project file (.ssproj) alongside widget layout data. This means different projects can have different plugin configurations. The same plugin shows different gauges or indicators depending on which project is loaded.
Project File (.ssproj)
└── widgetSettings
├── layout:0 ← widget layout data
├── layout:1
├── __plugin__:custom-gauge ← gauge plugin state
└── __plugin__:digital-indicator ← indicator plugin state
success, _ = client.execute("extensions.saveState", {
"pluginId": "my-plugin",
"state": {
"windows": [
{"key": "f0", "label": "Temperature", "color": "#58a6ff"},
{"key": "f1", "label": "Pressure", "color": "#3fb950"}
],
"settings": {"auto_range": True}
}
})success, result = client.execute("extensions.loadState", {
"pluginId": "my-plugin"
})
if success:
state = result # dict with saved state, or {} if nonePlugin starts
│
├─► loadState() → restore windows from saved config
│
├─► User interacts (opens windows, changes settings)
│
├─► Periodic auto-save (optional, e.g. every 30s)
│
└─► Plugin quits → saveState() (final save)
class MyWindow:
def to_dict(self):
return {
"key": self.dataset_key,
"label": self.label,
"settings": {"min": self.min_val, "max": self.max_val}
}class App:
def _save_state(self):
windows = [w.to_dict() for w in self.windows if w.alive]
self.client.execute("extensions.saveState", {
"pluginId": "my-plugin",
"state": {"windows": windows}
})
def _restore_state(self):
def _try():
success, result = self.client.execute(
"extensions.loadState", {"pluginId": "my-plugin"})
if not success or not result:
return
state = result if isinstance(result, dict) else {}
for wc in state.get("windows", []):
# Recreate window from saved config
...
# Run on a background thread to avoid blocking tkinter
threading.Thread(target=_try, daemon=True).start()
def _quit(self):
self._save_state()
# ... cleanupdef main():
store = DataStore()
client = GRPCClient()
client.on_frame = store.ingest
threading.Thread(target=client.run_loop, daemon=True).start()
app = App(store, client)
app._restore_state()
app.run()
client.stop()Plugins that were running when Serial Studio closed are automatically relaunched on the next startup. The ExtensionManager saves the list of running plugin IDs to QSettings and restores them after the extension catalog loads.
No plugin-side code is needed for this. It's handled entirely by the ExtensionManager.
- State is only saved when a project file is loaded (
operationMode == ProjectFile) - The
_restore_stateshould run on a background thread sinceclient.execute()blocks - Never hold the data lock while creating tkinter widgets — this causes deadlocks
- State objects must be JSON-serializable (dicts, lists, strings, numbers, bools)