<feed xmlns='http://www.w3.org/2005/Atom'>
<title>labUI.git/ui, branch main</title>
<subtitle>Data acquisition program written in Python for use with serial communication devices.
</subtitle>
<id>https://git.kolset.xyz/labUI.git/atom?h=main</id>
<link rel='self' href='https://git.kolset.xyz/labUI.git/atom?h=main'/>
<link rel='alternate' type='text/html' href='https://git.kolset.xyz/labUI.git/'/>
<updated>2026-09-21T06:24:25Z</updated>
<entry>
<title>Fix white native-chrome artifacts on dark theme</title>
<updated>2026-09-21T06:24:25Z</updated>
<author>
<name>Christian Kolset</name>
<email>christian.kolset@gmail.com</email>
</author>
<published>2026-09-21T06:24:25Z</published>
<link rel='alternate' type='text/html' href='https://git.kolset.xyz/labUI.git/commit/?id=be6e8ec356166c2df0fba443a086afc971aaefbd'/>
<id>urn:sha1:be6e8ec356166c2df0fba443a086afc971aaefbd</id>
<content type='text'>
QSS alone doesn't reach everything Qt draws — splitter/dock separator
grip-dot decorations and a checkable QMenu item's tick come from the
app's QPalette, which was never set (stylesheet-only theming), so they
stayed Qt's default light palette under the dark stylesheet:

- MainWindow._apply_theme() now also sets a matching dark QPalette
  (restored to the style's standard palette for light theme).
- QMainWindow::separator and a generic QMenu rule (only #profileMenu was
  styled before — the toolbar's View menu had no objectName and so no
  styling at all) added to style_dark.qss, with QMenu::indicator styled
  as a filled/outline square so checkmarks are visible regardless of
  palette.
- QTabWidget/QTabBar (the bare containers, not ::pane/::tab) had no
  background set, so the strip beside the tab buttons fell back to the
  light palette — fixed globally, covers both the Devices/Channels/Plot
  config tabs and the Settings window since both share the same
  QTabWidget styling.

Co-Authored-By: Claude Sonnet 5 &lt;noreply@anthropic.com&gt;
</content>
</entry>
<entry>
<title>Fix Buffer size setting never being applied; add Safety Switch control type</title>
<updated>2026-09-21T06:16:15Z</updated>
<author>
<name>Christian Kolset</name>
<email>christian.kolset@gmail.com</email>
</author>
<published>2026-09-21T06:16:15Z</published>
<link rel='alternate' type='text/html' href='https://git.kolset.xyz/labUI.git/commit/?id=4ab2eea9af5338485303d421476fa874e5af6841'/>
<id>urn:sha1:4ab2eea9af5338485303d421476fa874e5af6841</id>
<content type='text'>
- Buffer size (samples) in Settings &gt; Acquisition was captured into the
  settings dict but never read anywhere — SignalBuffer always used the
  hardcoded MAX_BUFFER=20_000 constant (which happens to match the
  dialog's default, hence it looking like the value "reverts"). Add
  AcquisitionEngine.set_buffer_size(), wired in on startup and on Settings
  apply; existing buffers are rebuilt at the new size in place, keeping
  their most recent samples.

- New "Safety Switch" control type: an On/Off Switch that's disabled
  outright until acquisition is running, with a lock icon shown while
  disabled. AcquisitionEngine gains a run_state_changed(bool) signal
  (emitted from start()/stop()); ControlWidget gains a
  _check_safety_lock() hook (no-op by default, called uniformly by
  ControlPanel on every widget) that SafetySwitch overrides. engine is
  now threaded through to every ControlWidget for this. Persists via
  ControlSpec.control_type alone — no schema change needed.

Co-Authored-By: Claude Sonnet 5 &lt;noreply@anthropic.com&gt;
</content>
</entry>
<entry>
<title>Persist dock layout to profiles; dedupe port names; Channels-tab Create button</title>
<updated>2026-09-21T05:45:18Z</updated>
<author>
<name>Christian Kolset</name>
<email>christian.kolset@gmail.com</email>
</author>
<published>2026-09-21T05:45:18Z</published>
<link rel='alternate' type='text/html' href='https://git.kolset.xyz/labUI.git/commit/?id=a0473245f1f453bfc87ea1bc9bd479311e8a944b'/>
<id>urn:sha1:a0473245f1f453bfc87ea1bc9bd479311e8a944b</id>
<content type='text'>
- Fix duplicated device names ("FT231X USB UART - FT231X USB UART (...)")
  — pyserial on Linux often reports the same udev model string twice for
  cheap FTDI/CH340-style adapters. Collapse it at the list_ports() source
  so every consumer (device auto-name, port picker) benefits.

- Save Control Panel / Namespace dock area, floating geometry, visibility,
  size and order to .labui profiles via QMainWindow.saveState()/
  restoreState(), round-tripped through the existing Profile.settings
  passthrough (self._settings) — no profile schema change needed.

- Channels tab: replace the per-row Signal ⊕ button (and its pinned/
  frozen-column overlay) with a single "Create Channel" button below the
  Namespace panel. Select one or more Signal/Channel rows (multi-select
  now supported) and click it — selected rows become the new Channel's
  sources, or an empty Channel is created if nothing's selected. Fixes a
  bug in the process: the multi/zero-source path defaulted to
  kind="expression" with no expression text, which fails to compile and
  silently dropped the channel; use kind="none" instead, which the user
  then edits in the Channels pane.

  NamespacePanel itself (ui/namespace_panel.py) goes back to a plain
  read-only reference widget — the create-channel button and its logic
  live entirely in ConfigWindow (config_window.py), so the shared
  dockable Namespace panel on the main window is unaffected.

Co-Authored-By: Claude Sonnet 5 &lt;noreply@anthropic.com&gt;
</content>
</entry>
<entry>
<title>Make Channel the single source of truth for a reading's name</title>
<updated>2026-09-21T04:30:13Z</updated>
<author>
<name>Christian Kolset</name>
<email>christian.kolset@gmail.com</email>
</author>
<published>2026-09-21T04:30:13Z</published>
<link rel='alternate' type='text/html' href='https://git.kolset.xyz/labUI.git/commit/?id=006366ef8099c8313ae596bf6267e9f98dfbeb5e'/>
<id>urn:sha1:006366ef8099c8313ae596bf6267e9f98dfbeb5e</id>
<content type='text'>
Device tab stays hardware-only (pins, wiring, may vary per workstation);
the Channels window owns the profile-facing identity. Concretely:

- Fix a real regression: SerialDevice generic-format Apply &amp; Reconnect
  nulled out custom channel_names/units every time (no fallback, unlike
  every other optional field in reconfigure()), silently reverting to
  sig0/sig1/... ids.
- Plot legend now live-tracks the current Signal/Channel name instead of
  freezing tr.label at add-time, so a rename shows up without re-adding
  the trace.
- Add an inline quick-reconnect (combo + button, pre-selects the actual
  current source) directly on each Channel block in the Channels window,
  for the common single-source case — rewiring hardware no longer means
  touching every Plot/pipeline that referenced it.
- Collapse the Channel ID field behind an "ID ▸" advanced toggle;
  channel_id is already auto-derived from the name (_slug_id) and rarely
  needs to be seen.
- Devices tab's Signal "Name" column relabeled "Label" with a tooltip —
  de-emphasized now that Channel name is the real identity.
- Move "create a Channel from this Signal" from a column on the Devices
  tab's signal table to a ⊕ button on each Signal row in the Channels
  tab's Namespace panel — the action now lives where the identity is
  created, not on the hardware tab. Only that instance opts in
  (on_create_channel); the read-only Namespace dock on the main window
  is unaffected.
- Floor the Namespace panel's width at its columns' natural size so
  dragging the Channels-tab splitter narrower can't push the ⊕ column
  behind the horizontal scrollbar.
- Breadcrumb log when a profile-saved Signal override can't be applied
  because the signal_id no longer exists.

Co-Authored-By: Claude Sonnet 5 &lt;noreply@anthropic.com&gt;
</content>
</entry>
<entry>
<title>Fix Signal name reset and orphaned references on device reconfigure</title>
<updated>2026-09-20T17:31:07Z</updated>
<author>
<name>Christian Kolset</name>
<email>christian.kolset@gmail.com</email>
</author>
<published>2026-09-20T17:31:07Z</published>
<link rel='alternate' type='text/html' href='https://git.kolset.xyz/labUI.git/commit/?id=bb8be051ffffa56a01093c9f3aaa80e84748568f'/>
<id>urn:sha1:bb8be051ffffa56a01093c9f3aaa80e84748568f</id>
<content type='text'>
Apply &amp; Reconnect rebuilt a device's whole Signal list from hardware
defaults on every call, silently discarding user/profile-set names,
units, and colors even when pins hadn't changed. Add
merge_signal_metadata() and call it from ArduinoDevice.remap_pins()
and SerialDevice.reconfigure() to reapply the same fields
ProfileManager already treats as user overrides, keyed by signal_id.

Also close a related gap: removing a pin that a Channel/Plot/Control
still referenced left it silently dangling, and SignalProcessor never
pruned its latest-value cache, so a derived Channel kept computing off
a permanently frozen stale reading. Add SignalProcessor.
on_device_reconfigured() to purge that cache, reference_registry.
find_references() for a console breadcrumb, and live "missing source"
indicators on Plot traces and Channel blocks reusing ControlWidget's
existing stale/QSS pattern. Also fixes ControlWidget._write() still
enqueuing writes to a channel that no longer exists.

Co-Authored-By: Claude Sonnet 5 &lt;noreply@anthropic.com&gt;
</content>
</entry>
<entry>
<title>Redesign Signal/Channel naming and fix pin-remap breaking references</title>
<updated>2026-09-19T23:38:07Z</updated>
<author>
<name>Christian Kolset</name>
<email>christian.kolset@gmail.com</email>
</author>
<published>2026-09-19T23:38:07Z</published>
<link rel='alternate' type='text/html' href='https://git.kolset.xyz/labUI.git/commit/?id=f3327d56d48d63b21ffc03a55fa5e7db753d1707'/>
<id>urn:sha1:f3327d56d48d63b21ffc03a55fa5e7db753d1707</id>
<content type='text'>
Arduino analog Signal ids used to be the pin name itself, so fixing a
wrong pin silently changed the id and orphaned any plot trace, control,
or filter pipeline pointing at it. Analog Signal ids are now positional
(ai0, ai1, ...) and decoupled from the underlying pin, matching how
digital I/O already worked.

More broadly, formalizes a Device -&gt; Signal -&gt; Channel -&gt; Plot naming
model (SignalConfig for physical device channels, Channel for derived/
computed ones) and adds core/reference_registry.py to propagate any
Signal or Channel id rename through plot traces, controls, and filter
pipelines instead of leaving them dangling. Signal ids are now editable
in the Signals tab with the same propagation.

.labui profile schema bumped to v2.0 with a transparent migration for
old files, including remapping old pin-name Signal ids to the new
positional ones. Removed six confirmed-dead files (two legacy device
classes, four superseded top-level ui/ duplicates).
</content>
</entry>
<entry>
<title>Fix control panel freeze and CM1 motor not moving</title>
<updated>2026-08-17T16:02:28Z</updated>
<author>
<name>Christian Kolset</name>
<email>ckolset@colostate.edu</email>
</author>
<published>2026-08-17T16:02:28Z</published>
<link rel='alternate' type='text/html' href='https://git.kolset.xyz/labUI.git/commit/?id=2014b045fc62567aeb06ea610a98aa03a43b7142'/>
<id>urn:sha1:2014b045fc62567aeb06ea610a98aa03a43b7142</id>
<content type='text'>
ControlWidget._write() called dev.write_channel() directly on the GUI
thread from Qt signal handlers — for CML (CoolMuscle), write() does 2-3
blocking serial round-trips, and a slider fires valueChanged on every drag
tick, so each tick froze the whole app until the device responded or timed
out. Replaced with a coalescing background dispatch: writes are keyed by
(device, channel) in a dict instead of a plain FIFO queue, so re-writing
the same channel while a write is still pending updates its value in place
rather than piling up — a strict FIFO was observed backing up 30+ deep
during a single drag, meaning the motor kept "moving" to seconds-old
positions long after the slider stopped, which looked like a freeze even
once off the GUI thread.

Separately, the motor never actually moved because it was never enabled:
per docs/CM1-C_ASCII_Command_Cheatsheet.md, a CM1-C motor stays disabled
("shaft free") until sent "(", and nothing in CMLLayer.write()'s VS/MA
handling ever sent it — enable only existed as a separate M1_ME channel
the generic MotorControl widget has no concept of. VS/MA now send enable
unconditionally before the move sequence (a no-op if already enabled).

Co-Authored-By: Claude Sonnet 5 &lt;noreply@anthropic.com&gt;
</content>
</entry>
<entry>
<title>Fix motion capture UI freezes: throttling, async camera open/scan, stale poll</title>
<updated>2026-08-17T16:02:16Z</updated>
<author>
<name>Christian Kolset</name>
<email>ckolset@colostate.edu</email>
</author>
<published>2026-08-17T16:02:16Z</published>
<link rel='alternate' type='text/html' href='https://git.kolset.xyz/labUI.git/commit/?id=abab8f7938dada9a2abff31e719186b99934601a'/>
<id>urn:sha1:abab8f7938dada9a2abff31e719186b99934601a</id>
<content type='text'>
Several compounding freeze sources in plugins/motion_capture/:
- tracker.py: _run_real had no frame-rate cap independent of the camera's
  configured fps (up to 120fps/2560x1440 was possible), and no bound on
  consecutive failed reads — a camera that "opens" (cv2 isOpened() can lie,
  seen with Windows MSMF/obsensor) but never actually delivers frames would
  get hammered forever. Added a 20Hz processing cap, a real test-read before
  committing to a camera, and a give-up-after-30-failures fallback to
  simulation.
- tracker.py/device.py: cv2.VideoCapture() opening a real camera is a slow,
  blocking call — CameraTracker.start() used to do this synchronously on
  the caller's thread, which froze the whole app when ProfileManager.apply()
  called it during profile load. Now runs on the background thread; status
  reporting (CameraDevice) moved to an on_result callback since the caller
  can no longer learn the outcome synchronously.
- window.py: the Camera View preview ran a 30Hz GUI-thread timer doing a
  full-frame copy + QImage/QPixmap rebuild every tick unconditionally (no
  visibility check) — dropped to 10Hz and skips entirely when hidden.
  Camera enumeration (list_cameras(), up to 8 sequential cv2.VideoCapture
  probes) also ran inline in the window constructor, blocking it open the
  same way camera_panel.py's CameraScanThread already avoided for the Add
  Device dialog — moved to the same async pattern here.

config_window.py: separately, ConfigWindow._dev_poll (device status label
refresh) was stopped in closeEvent and never restarted, since MainWindow
reuses the same ConfigWindow instance across opens — invisible before
because connect() was synchronous, but with the above change a device can
sit in CONNECTING for a while, and a stale poll made that look stuck
forever after reopening the window. Restart it in showEvent.

Co-Authored-By: Claude Sonnet 5 &lt;noreply@anthropic.com&gt;
</content>
</entry>
<entry>
<title>Add .labui file association — open by double-click, cross-platform</title>
<updated>2026-08-17T16:01:59Z</updated>
<author>
<name>Christian Kolset</name>
<email>ckolset@colostate.edu</email>
</author>
<published>2026-08-17T16:01:59Z</published>
<link rel='alternate' type='text/html' href='https://git.kolset.xyz/labUI.git/commit/?id=e749cca30da2d02839aca736873e7e5ad45e191c'/>
<id>urn:sha1:e749cca30da2d02839aca736873e7e5ad45e191c</id>
<content type='text'>
Windows/Linux self-register via core/file_association.py (winreg /
XDG .desktop+mime), triggered from a new Settings button since there's
no installer to do it at install time. macOS gets it at build time via
labui.spec's new BUNDLE() step (CFBundleDocumentTypes in Info.plist) —
that step also makes mac builds a real .app bundle for the first time,
not just a bare onedir folder. main.py gains a LabUIApplication
subclass to catch the macOS QFileOpenEvent (buffered if it arrives
before MainWindow exists) plus argv[1] handling for Windows/Linux.
ProfileButton.load_path() extracted so both paths share the same
load-and-apply logic as the existing file-dialog flow.

Co-Authored-By: Claude Sonnet 5 &lt;noreply@anthropic.com&gt;
</content>
</entry>
<entry>
<title>Channels tab drag-to-reorder, Namespace absolute references, fix stale Channel ID renames</title>
<updated>2026-08-13T19:39:25Z</updated>
<author>
<name>Christian Kolset</name>
<email>ckolset@colostate.edu</email>
</author>
<published>2026-08-13T19:39:25Z</published>
<link rel='alternate' type='text/html' href='https://git.kolset.xyz/labUI.git/commit/?id=34c12b2b5b280206890cd03153b1c95116acc85e'/>
<id>urn:sha1:34c12b2b5b280206890cd03153b1c95116acc85e</id>
<content type='text'>
Generalized the Plot pane drag-to-reorder (_PaneDragHandle/_PlotDropContainer
-&gt; _DragHandle/_DragReorderContainer) and reused it for the Channels tab's
derived-channel list; reordering also updates SignalProcessor's evaluation
order (list order matters for chained Channels) via new reorder_derived().

Namespace panel: added an ID column (bare channel_id, matching the actual
channels.get(...) script convention) and fixed the Source column for
Channels to show resolved absolute references (dev_id/ch_id for a Signal,
bare id for a chained Channel) instead of the operation kind, so a chain
is traceable by eye.

Fixed a real bug this surfaced: renaming a Channel's ID never propagated
to other Channels' sources, leaving dangling ("derived", old_id)
references that silently stopped updating. Added
SignalProcessor.rename_derived() to fix up sources plus carry the ring
buffer/latest-value cache to the new key, wired to DerivedBlock's Apply.

Co-Authored-By: Claude Sonnet 5 &lt;noreply@anthropic.com&gt;
</content>
</entry>
</feed>
