summaryrefslogtreecommitdiff
path: root/ui/control_editor.py
diff options
context:
space:
mode:
authorChristian Kolset <christian.kolset@gmail.com>2026-06-09 22:14:20 -0600
committerChristian Kolset <christian.kolset@gmail.com>2026-06-09 22:14:20 -0600
commit7aec470acd69e9c1810b40aa5eea9b96b9eb73ad (patch)
tree9146091077ec64ed1437621ebf2725edf987801f /ui/control_editor.py
parentb138e72d6f81cf58f27c6c1e4ce65cfebbb58624 (diff)
Scripting: named signal vars, shared vars dict, control scripts, code templates
- Named variables in expressions: source channel IDs injected as Python identifiers alongside x[] (e.g. write `A0 * 2` instead of `x[0] * 2`) - Shared `vars` dict on SignalProcessor accessible from all expressions, functions, and control scripts; persisted in .labdaq profiles - Per-derived-channel `state` dict for persistent computation state in custom scripts - Control widgets gain optional on_action_script (Python snippet with value, vars, channels, math in scope); syntax-checked in editor dialog - Built-in derived kinds (derivative, rms, power, etc.) now show their Python implementation as read-only code previews; "Edit as Custom Script" converts to custom_script kind with template pre-filled Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Diffstat (limited to 'ui/control_editor.py')
-rw-r--r--ui/control_editor.py58
1 files changed, 56 insertions, 2 deletions
diff --git a/ui/control_editor.py b/ui/control_editor.py
index c582e6b..48de21d 100644
--- a/ui/control_editor.py
+++ b/ui/control_editor.py
@@ -18,8 +18,10 @@ from PyQt6.QtWidgets import (
QLabel, QLineEdit, QComboBox, QDoubleSpinBox,
QSpinBox, QPushButton, QGroupBox, QWidget,
QStackedWidget, QFrame, QCheckBox, QSizePolicy,
+ QTextEdit, QScrollArea,
)
from PyQt6.QtCore import Qt
+from PyQt6.QtGui import QFont, QFontMetrics
class _DynStack(QStackedWidget):
@@ -68,6 +70,8 @@ class ControlSpec:
max_val: float = 100.0
step: float = 1.0
max_rpm: int = 3000
+ # Optional Python snippet executed on every value change
+ on_action_script: str = ""
def to_dict(self) -> dict:
return asdict(self)
@@ -177,8 +181,8 @@ class ControlEditorDialog(QDialog):
self.result_spec: Optional[ControlSpec] = None
self.setWindowTitle("Edit Control" if spec else "Add Control")
- self.setMinimumSize(440, 460)
- self.resize(460, 500)
+ self.setMinimumSize(440, 520)
+ self.resize(460, 580)
self._build()
self._load_spec()
@@ -244,6 +248,40 @@ class ControlEditorDialog(QDialog):
self._stack.addWidget(panel)
params_lay.addWidget(self._stack)
root.addWidget(self._params_grp)
+
+ # ── On-action script ───────────────────────────────────────────
+ script_grp = QGroupBox("On Action Script (optional)")
+ script_lay = QVBoxLayout(script_grp); script_lay.setContentsMargins(10, 12, 10, 10)
+
+ _help = QLabel(
+ "Runs every time this control changes value.\n"
+ "Available: value, channel_id, device_id, vars (shared dict), "
+ "channels (latest values by channel_id), math"
+ )
+ _help.setObjectName("traceSource"); _help.setWordWrap(True)
+ script_lay.addWidget(_help)
+
+ _mono = QFont("IBM Plex Mono, Consolas, Monospace")
+ _mono.setStyleHint(QFont.StyleHint.Monospace)
+ self._script_edit = QTextEdit()
+ self._script_edit.setFont(_mono)
+ self._script_edit.setTabStopDistance(QFontMetrics(_mono).horizontalAdvance(" ") * 4)
+ self._script_edit.setMinimumHeight(80); self._script_edit.setMaximumHeight(160)
+ self._script_edit.setPlaceholderText(
+ "# Example: zero a position offset when button is pressed\n"
+ "# vars['zero_pos'] = channels.get('encoder_0', 0)"
+ )
+ script_lay.addWidget(self._script_edit)
+
+ self._script_status = QLabel("")
+ self._script_status.setObjectName("traceSource")
+ check_btn = QPushButton("Check Syntax")
+ check_btn.clicked.connect(self._check_script)
+ btn_row = QHBoxLayout()
+ btn_row.addWidget(self._script_status, 1)
+ btn_row.addWidget(check_btn)
+ script_lay.addLayout(btn_row)
+ root.addWidget(script_grp)
root.addStretch()
# ── Buttons ────────────────────────────────────────────────────
@@ -258,6 +296,18 @@ class ControlEditorDialog(QDialog):
btn_row.addWidget(cancel); btn_row.addWidget(ok)
root.addLayout(btn_row)
+ def _check_script(self):
+ src = self._script_edit.toPlainText().strip()
+ if not src:
+ self._script_status.setText(""); return
+ try:
+ compile(src, "<script>", "exec")
+ self._script_status.setText("✓ OK")
+ self._script_status.setStyleSheet("color:#22c55e;")
+ except SyntaxError as e:
+ self._script_status.setText(f"⚠ {e}")
+ self._script_status.setStyleSheet("color:#ef4444;")
+
def _load_spec(self):
"""Populate fields from self.spec."""
# Type
@@ -287,6 +337,9 @@ class ControlEditorDialog(QDialog):
for name, panel in self._param_panels.items():
panel.load(self.spec)
+ # Script
+ self._script_edit.setPlainText(self.spec.on_action_script)
+
def _on_type_changed(self, type_name: str):
idx = list(_PARAM_PANELS.keys()).index(type_name)
self._stack.setCurrentIndex(idx)
@@ -340,6 +393,7 @@ class ControlEditorDialog(QDialog):
device_id=self._dev_cb.currentData() or "",
channel_id=channel_id,
active_low=self._active_low_chk.isChecked(),
+ on_action_script=self._script_edit.toPlainText().strip(),
)
# Save type-specific params
panel = self._param_panels.get(ctype)