summaryrefslogtreecommitdiff
path: root/ui/control_editor.py
diff options
context:
space:
mode:
authorChristian Kolset <ckolset@colostate.edu>2026-08-10 16:12:05 -0600
committerChristian Kolset <ckolset@colostate.edu>2026-08-10 16:12:05 -0600
commit5d5bcaed8639a162fda85c8cd0d340c296a054b8 (patch)
treea7a1a62fb607c9c85127dab4aab17ccf8e22c906 /ui/control_editor.py
parent46231c00994cd681d598961f2056dbda31c3e483 (diff)
Unify Signals/Channels around auto-naming, plot-by-channel, and shared vars
Devices: auto-name from OS-reported port description (Device Manager / udev string) instead of a generic label; default Serial #Channels 1; consistent simulate-mode checkbox across backends; inline device rename. Channels: calibration/filters move out of Signals into a Channel "Operation" (adds Scale/Offset alongside the existing math kinds); single-physical-source Channels get an embedded Filters block; new Channels auto-name/auto-ID from their source and reject duplicate names/IDs on apply; "Create Channel" button on each Signal row; renamed "Sources" -> "Signals" in the Channels UI; Signals' READ/WRITE dir badge (was silently-broken IN/OUT) now reflects the real writable flag. Plot: default layout is one pane per Channel (not per device, not raw Signals) titled by channel name; "+ Add Channel" is a checkbox dialog instead of an inline dropdown; traces get up/down reorder; live unit changes (e.g. a Mark-10 gauge switching lb/kgF/N/ozF) propagate to an open pane's axis label without clobbering a manually-typed one. Control Panel: hardware assignment now picks from the same writable- Signals list instead of a separate device/channel combo with a hardcoded Arduino pin list; stale visual indicator when a control's target device/channel disconnects or disappears; documented the `vars` bridge (Control Panel On Action Script <-> Channel scripts) in both directions' help text and script templates. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Diffstat (limited to 'ui/control_editor.py')
-rw-r--r--ui/control_editor.py96
1 files changed, 31 insertions, 65 deletions
diff --git a/ui/control_editor.py b/ui/control_editor.py
index 111a71f..d167517 100644
--- a/ui/control_editor.py
+++ b/ui/control_editor.py
@@ -204,31 +204,26 @@ class ControlEditorDialog(QDialog):
root.addWidget(id_grp)
- # ── Device & channel ────────────────────────────────────────────
+ # ── Target signal ────────────────────────────────────────────────
+ # Sourced from the same writable-channel set Configuration > Channels >
+ # Signals shows as "OUT" — the Signals tab is the central place hardware
+ # endpoints (including button targets) are registered.
hw_grp = QGroupBox("Hardware Assignment")
hw_form = QFormLayout(hw_grp); hw_form.setContentsMargins(10, 16, 10, 10)
- self._dev_cb = QComboBox()
- self._dev_cb.addItem("— none —", userData="")
+ self._target_cb = QComboBox()
+ self._target_cb.addItem("— none —", userData=("", ""))
for dev in self.registry.all_instances():
- label = f"{dev.info.device_id} ({dev.info.name})"
- self._dev_cb.addItem(label, userData=dev.info.device_id)
- self._dev_cb.currentIndexChanged.connect(self._on_dev_changed)
- hw_form.addRow("Device:", self._dev_cb)
-
- self._ch_cb = QComboBox()
- self._ch_cb.setEditable(True) # allow typing custom pin like "D7"
- self._ch_cb.setInsertPolicy(QComboBox.InsertPolicy.NoInsert)
- hw_form.addRow("Channel / Pin:", self._ch_cb)
+ for ch in dev.info.channels:
+ if not ch.enabled or not ch.writable:
+ continue
+ label = f"{dev.info.device_id} / {ch.name} ({ch.channel_id})"
+ self._target_cb.addItem(label, userData=(dev.info.device_id, ch.channel_id))
+ hw_form.addRow("Target Signal:", self._target_cb)
self._active_low_chk = QCheckBox("Active-low output (ON sends LOW)")
hw_form.addRow("Polarity:", self._active_low_chk)
- self._ch_hint = QLabel("")
- self._ch_hint.setObjectName("traceSource")
- self._ch_hint.setWordWrap(True)
- hw_form.addRow(self._ch_hint)
-
root.addWidget(hw_grp)
# ── Type-specific params ────────────────────────────────────────
@@ -256,7 +251,10 @@ class ControlEditorDialog(QDialog):
_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"
+ "channels (latest values by channel_id), math\n"
+ "vars set here are readable from any Channel's script "
+ "(Configuration > Channels) via vars.get('key', default) — "
+ "this is how a button feeds a value into Channel math."
)
_help.setObjectName("traceSource"); _help.setWordWrap(True)
script_lay.addWidget(_help)
@@ -316,21 +314,22 @@ class ControlEditorDialog(QDialog):
self._type_cb.setCurrentIndex(idx)
self._title_edit.setText(self.spec.title)
- # Device
- for i in range(self._dev_cb.count()):
- if self._dev_cb.itemData(i) == self.spec.device_id:
- self._dev_cb.setCurrentIndex(i); break
-
- # Channel (populated after device selection)
- self._on_dev_changed()
+ # Target signal
found = False
- for i in range(self._ch_cb.count()):
- if self._ch_cb.itemData(i) == self.spec.channel_id:
- self._ch_cb.setCurrentIndex(i)
+ target = (self.spec.device_id, self.spec.channel_id)
+ for i in range(self._target_cb.count()):
+ if self._target_cb.itemData(i) == target:
+ self._target_cb.setCurrentIndex(i)
found = True
break
- if not found:
- self._ch_cb.setCurrentText(self.spec.channel_id)
+ if not found and self.spec.device_id:
+ # Not in the current writable-signal list (e.g. a legacy raw pin
+ # target, or the channel/device is now disabled/removed) — keep it
+ # as a synthetic entry so editing this control doesn't silently
+ # clear its saved hardware assignment.
+ label = f"{self.spec.device_id} / {self.spec.channel_id} (not currently available)"
+ self._target_cb.addItem(label, userData=target)
+ self._target_cb.setCurrentIndex(self._target_cb.count() - 1)
self._active_low_chk.setChecked(bool(self.spec.active_low))
# Params
@@ -351,50 +350,17 @@ class ControlEditorDialog(QDialog):
self._title_edit.text() in CONTROL_TYPES:
self._title_edit.setText(type_name)
- def _on_dev_changed(self):
- self._ch_cb.clear()
- dev_id = self._dev_cb.currentData()
- if not dev_id:
- self._ch_hint.setText("No device selected — type a channel manually")
- return
- dev = self.registry.get_instance(dev_id)
- if not dev:
- return
- # Add actual channels (skip disabled — can't be driven while switched
- # off — and skip read-only channels — a control writes, so a channel
- # with no write mapping should never be offered as a target)
- for ch in dev.info.channels:
- if not ch.enabled or not ch.writable:
- continue
- self._ch_cb.addItem(f"{ch.name} ({ch.channel_id})",
- userData=ch.channel_id)
- # For Arduino backends also suggest digital pins for output
- if hasattr(dev, "backend") and dev.backend == "arduino":
- self._ch_cb.insertSeparator(self._ch_cb.count())
- for pin in ["D5", "D6", "D7", "D8", "D9", "D10", "D11", "D13"]:
- self._ch_cb.addItem(f"{pin} (digital out)", userData=pin)
- self._ch_hint.setText(
- "Analog pins (A0…) for reading.\n"
- "Digital pins (D5…) for on/off output.\n"
- "D9, D10, D11 support PWM."
- )
- else:
- self._ch_hint.setText("")
-
def _on_ok(self):
ctype = self._type_cb.currentText()
title = self._title_edit.text().strip() or ctype
- # Resolve channel_id — prefer userData if it's a real combo item
- ch_text = self._ch_cb.currentText().strip()
- ch_data = self._ch_cb.currentData()
- channel_id = ch_data if ch_data else ch_text.split()[0] # strip "(name)" part
+ dev_id, channel_id = self._target_cb.currentData() or ("", "")
spec = ControlSpec(
control_type=ctype,
title=title,
icon=CONTROL_ICONS.get(ctype, "⚙"),
- device_id=self._dev_cb.currentData() or "",
+ device_id=dev_id,
channel_id=channel_id,
active_low=self._active_low_chk.isChecked(),
on_action_script=self._script_edit.toPlainText().strip(),