| Age | Commit message (Collapse) | Author |
|
|
|
|
|
amp+offset spanned the full [0,5] clamp range edge-to-edge, so
per-sample noise pushed past the rails on every peak/trough and got
hard-clamped into a flat plateau. Shrinking amp gives ~8.7 sigma of
headroom so the clamp stays a safety net instead of firing every cycle.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
|
Batches per-tick device readings into one signal emit instead of one
per channel, evaluates derived channels once per tick instead of once
per raw sample, and makes ChannelBuffer.window() cost scale with the
window size instead of total buffer history. Strip chart X-range now
scrolls on an independent ~30fps timer instead of only snapping when
new data lands, which was the main visible cause of the stutter.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
|
- Moved edit and X button to top with header of widget.
- Replaced up/down button with drag-and-drop style headers.
|
|
disappear and no sidebar panel.
Possible future implementation.
|
|
|
|
Debug button, DebugWindow, and the stdout/stderr tee (core/debug_log.py)
weren't wanted. developer_mode toggle stays — still gates device
Simulation Mode checkboxes and verbose terminal logging.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
|
protocol updates
origin/main (23 commits) added a Debug window/log system on top of the old
separate Devices/Channels/Plot windows, plus protocol fixes (cml, mark10,
modbus_rtu, scpi) and device/profile changes. Local main (3 commits)
replaced the separate windows with a unified ConfigWindow + dock panel.
Kept local's ConfigWindow/dock architecture and ported the Debug window
onto it: new toolbar button + _open_debug(), gated by developer_mode same
as origin's version. Deduped the two independent "developer mode" toggles
that had collided in SettingsWindow (origin's General-tab checkbox gating
Debug window + device sim-mode visibility, local's Advanced-tab checkbox
setting verbose logging) into one General-tab checkbox that does both.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
|
Consolidation of Devices/Channels/Plot into ConfigWindow left the
Settings button wired to a method that no longer exists.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
|
Replaces three separate floating windows with single tabbed ConfigWindow:
- Tab 0: Devices (device cards, add/remove/configure)
- Tab 1: Channels (signal table with Dir/IN/OUT badge + pipeline ⚙ per row; derived channels pane)
- Tab 2: Plot (layout canvas + pane config)
Adds is_output field to ChannelConfig for output channel direction badges.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
- Added dedicated firmware file for arduino
- Added View button to consolidate windows
- Control Panel changes:
- Changed name fron "Controls" to "Control Panel"
- Blue side bar when Panel is hidden.
- Supports "popping" out contorl panel
- Settings > Advanced tab added with developer settings
|
|
Branding:
- Rename app, window title, file extension (.labdaq → .labui), user data
dirs (~/.labui/), spec file (labdaq.spec → labui.spec), and APP_NAME
throughout all source, docs, and config files
Plugin system:
- Plugins no longer bundled in the PyInstaller build — installed at
runtime by users via Settings → Plugins → Install Plugin (zip)
- PluginManager now takes user_dir + extra_scan_dirs; user plugins live
in ~/.labui/plugins/, dev scan additionally covers project plugins/
- install_from_zip / uninstall / is_user_installed added to PluginManager
- vendor/ dir inside plugin zips: prepended to sys.path at load time so
plugins can ship their own deps without requiring pip on end-user machine
- source_url field in manifest.json: shown as Download button in the
missing-plugins dialog when a profile requires an absent plugin
- Frozen-app pip install now targets ~/.labui/plugin_packages/ using a
real system Python (sys.executable is the exe in frozen builds)
Profile loading:
- Profile now stores plugins_manifest snapshot (id, name, version,
source_url) alongside plugins_enabled
- On load, missing or dep-broken plugins trigger MissingPluginsDialog
before the rest of the profile is applied; user can install from zip
or download via source_url in-dialog, or cancel the load
- Plugin reconciliation only enables installed plugins — missing ones
are not written to enabled.json
UI:
- Version label added to status bar (bottom-right, muted colour)
- Settings → Plugins tab: Install Plugin… button, per-plugin Remove
button for user-installed plugins, live list refresh after install/remove
Docs:
- New docs/building.md covers the full release pipeline
- docs/plugin-development.md updated for new install flow, vendoring,
source_url, profile behaviour, and distribution instructions
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
Full pipeline, verified end-to-end against the real installed tufup
0.10.0 API (initial docs/summaries turned out inaccurate in places —
e.g. the apply method is download_and_apply_update, not update;
confirmed by inspecting installed package source directly rather than
trusting docs alone):
- core/version.py: single-source app version constant.
- labdaq.spec: PyInstaller onedir build (must be onedir, not onefile —
tufup replaces individual files in the install dir on update).
Bundles ui/*.qss, plugins/ (needed for runtime plugin discovery), and
repository/metadata/root.json once repo_init.py has produced one.
Built and smoke-tested: the frozen exe launches and stays running.
- scripts/release/{repo_init,repo_release}.py: maintainer-run release
tooling using tufup.repo.Repository, manual local signing (keys never
touch CI). Both actually run end-to-end during development of this
feature against a real build, not just written and assumed correct.
Longer expiration_days than tufup-example's CI-oriented defaults
(targets/snapshot/timestamp 90d instead of 7d/7d/1d), since we're
signing manually, not on an automated daily schedule — see
scripts/release/README.md for the re-signing cadence this still
requires even between releases.
- core/updater.py: thin Client wrapper. Refuses to run outside a
frozen build (getattr(sys, "frozen", False)) since there's no
installed bundle for tufup to update in `python main.py` dev mode.
Bootstraps the bundled root.json into the metadata cache dir on
first run — tuf.ngclient.Updater loads root.json from local disk on
construction, it does not fetch it remotely by design (the root of
trust can't come from the same server being verified).
- core/app_settings.py: factored out app_data_dir() (was inline in
_settings_path()) so the updater's metadata/target cache dirs live
in the same per-user location as settings.json, deliberately outside
the install directory an update can replace/move.
- Settings > General: version display + "Check for Updates" button,
manual-only per discussion (no silent background network calls or
surprise restarts for a lab-instrument-control app).
Metadata/targets are hosted on this repo's "updates" GitHub Release —
a fixed tag, not a normal per-version tag, because TUF's top-level
metadata needs a stable URL across app versions. No such GitHub-Releases-
hosting example exists in tufup or tufup-example; verified this by
fetching tufup-example's actual GitHub Actions workflow file directly
after a web search wrongly suggested one existed — the design here is
ours, not copied from upstream.
Not yet done, deliberately left for the user: running repo_init.py for
real (generates production signing keys), and creating the actual
"updates" GitHub Release. Both are irreversible-ish, security-sensitive,
externally-visible actions outside what should happen without the
user directly driving them.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
|
|
|
# Conflicts:
# ui/control_editor.py
|
|
|
|
|
|
|
|
|
|
|
|
New Settings > General > "Developer mode" checkbox (default on, so
this repo's simulate-by-default workflow is unaffected out of the box)
persisted through the existing core/app_settings.py load/save
functions. Exposed to code that can't easily receive the settings dict
via core.app_settings.is_developer_mode()/set_developer_mode(), a
small in-memory cache MainWindow keeps in sync whenever settings are
loaded or applied.
Debug window (ui/windows/debug_window.py): minimal first pass per the
plan — a live console. core/debug_log.py tees stdout/stderr into a
ring buffer + Qt signal (installed once in main.py, before anything
prints), so the window shows everything printed since app start,
including from background poll threads, not just what's printed while
it happens to be open. Its toolbar button is hidden unless developer
mode is on.
Simulate gating: every device's Simulation Mode checkbox/dropdown is
now hidden when developer mode is off — ui/add_device_dialog.py (also
covers the motion_capture camera panel, which shares this same
checkbox rather than having its own), and the per-device config
widgets in analog_input.py, arduino_device.py, digital_io.py,
nidaqmx_device.py, serial_device.py. Hiding rather than force-clearing
an already-simulating device's state — an existing simulated device
keeps working if developer mode is turned off later; the option is
just not offered again until it's back on.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
|
Builds on the previous branch's dependency check: the missing-deps
QMessageBox is now a Yes/No prompt. Yes runs a blocking
`sys.executable -m pip install <reqs>` (subprocess.run, output
captured), shows a result dialog, and on success proceeds straight to
enabling the plugin — no need to click Enable a second time.
Blocking is a deliberate simplification, not an oversight: this
codebase has no worker-thread/progress-dialog pattern for slow
operations anywhere else, so a threaded installer would be
inconsistent with everything else here. pip install is a one-time,
infrequent action, unlike e.g. a serial connect that runs constantly.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
|
manifest.json already had an unused "requires" field (motion_capture's
manifest lists opencv-python>=4.8.0) — PluginManifest never parsed it,
so a missing dependency just crashed the import inside _load_plugin(),
caught by the broad except and only ever printed to the console.
Adds PluginManifest.requires, PluginManager.missing_requirements()/
get_missing_dependencies(), and an early check in _load_plugin() that
skips the risky import entirely when a requirement is missing.
main_window.plugin_enable() now checks this before calling
PluginManager.enable() and shows a QMessageBox with the missing
packages and a pip install command instead of failing silently.
Checks by distribution name via importlib.metadata (what pip installed
it as), not import name — those differ for packages like opencv-python
(imports as cv2) or pyserial (imports as serial), so importlib.util.
find_spec() would give false negatives.
Also fixes a button-state bug this surfaces: SettingsWindow's plugin
toggle optimistically flipped to "Disable" the instant Enable was
clicked, before knowing whether enabling actually succeeds — pre-existing,
but now trivially reproducible (any plugin missing a dependency). Enable
no longer flips the button immediately; main_window calls the new
sync_plugin_button() once the real outcome is known.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
|
Run/Stop previously only paused acquisition — control outputs (motor
speed, switches, PWM duty) kept whatever value was last written, so
stopping the run loop didn't stop a running motor. Adds
ControlWidget.safe_stop() (per-widget override, default zeros the
output) and ControlPanel.safe_stop_all(), called from _toggle_run's
Stop branch before engine.stop().
Per-widget behavior is deliberately not uniform:
- OnOffSwitch/MotorControl/PwmControl have a latched running/enabled
state, so safe_stop() drives their own toggle handler (consistent UI
+ write in one path) and, for Motor/PWM, zeroes the slider too.
- SetpointControl/AnalogOutputControl only write on an explicit user
action and have no universally safe forced value (e.g. 0 isn't
necessarily "off" for an arbitrary process setpoint or analog
output) — Stop leaves them untouched rather than guessing.
Log button: added a :disabled QSS rule so "can't log yet" reads as
clearly inert rather than a duller version of the enabled look, and a
600ms blink (toggling a "recording" dynamic property the QSS keys off)
while a recording is active, so it reads as live/recording rather than
a static pressed button. Master Stop now calls _toggle_log(False)
explicitly when forcing the button off, since QPushButton.setChecked()
doesn't emit clicked — without this the blink would keep running after
a master Stop even though logging itself already halted via
engine.stop()'s internal stop_logging() call.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
|
Mark10Layer._parse() already tracked whichever unit suffix the gauge
last reported (lb/kgF/N/ozF — the gauge's physical unit button cycles
these independently of this app). serial_device.py hardcoded the
"force" channel's unit to "N" and never updated it, so switching units
on the gauge itself was invisible here.
Adds Mark10Layer.current_unit and has SerialDevice.read_channels()
keep the "force" ChannelConfig's unit in sync with it on every poll.
This fixes the unit shown in anything that reads ChannelConfig.unit
live — new plots, channel pickers, CSV log headers — but does not
relabel the Y-axis of an already-open plot pane, since plot axis
labels are baked into PlotConfig.y_label once at plot-build time, not
re-read from the channel live. Making an open pane's axis relabel
itself would need new signal plumbing from the device through
AcquisitionEngine to the strip chart — a bigger, separate change.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
|
Relabel every channel picker/header to "NAME (DEVICE/SIGNAL)" instead of
the previous "DEVICE/SIGNAL (NAME)" ordering, consistently across
channels_window.py (_channel_combo, ChannelPickerDialog,
ChannelPipelineBlock header), plot_builder.py, plot_config.py,
control_editor.py, and plot_window.py.
While touching each of those pickers, added the missing `.enabled`
filter that _make_picker()/_x_cb/_build_channel_picker lacked (the
default-layout builders already filtered disabled channels; these
manual "add channel to pane" pickers didn't).
Devices window Signals tab: removed the separate "Signal ID" column
(folded into the existing non-editable Device column instead of the
editable Name column, to avoid corrupting the in-place channel rename
feature that column already supports).
Channels window Virtual Channels pane: added a "Source" dropdown next
to "+ Add Channel" — picking a source pre-seeds the new derived
channel with it via DerivedBlock._add_src(); leaving it on the default
"none" entry keeps today's behavior of creating an empty channel.
Note: docs/ToDo.md's "Channels window UI" items referred to the *live*
UI (this window's PipelineTab + the Devices window's Signals tab), not
SignalsListTab in this same file, which its own module docstring flags
as dead/unused code kept only for old-profile compatibility — verified
by grepping for any instantiation of it (none exist).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
|
DeviceInfo.name existed but was hardcoded per device type and never
operator-editable. Adds a Display Name field to Add Device and makes
the Info-tab Name field in the device config dialog editable, and
threads the name through get_save_config()/ProfileManager.apply() so
a custom name survives a .labdaq save/reload instead of reverting to
the type default. Name is applied post-construction rather than as a
constructor kwarg, since no device factory declares a "name" param.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
|
Adds ChannelConfig.writable so the Controls editor can only bind to
channels with a real write mapping (VS/MA/ME/MD/ST for CML, do*/DO for
digital I/O, write_cmd-configured SCPI channels, holding-register Modbus
channels) instead of read-only telemetry — this is exactly the class of
mistake hit earlier (binding a motor-speed control to M1_TV instead of
M1_VS). Also carries forward the enabled-channel gating for controls
(hide disabled channels from the picker, block writes to them at
_write() time) done alongside this investigation.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
|
|
|
motion_capture setting to false
|
|
|
|
communication
|
|
|
|
|
|
|
|
|
|
None op
- Variable names derived from signal display name (e.g. "Acceleration X" → acceleration_x)
instead of raw channel ID; all auto-names lowercase with underscores
- Template code auto-updates x[0]/x[1] refs to named vars as sources are added/changed;
stops updating once user edits the code manually
- Derivative wrt-channel mode uses separate template with x[1] substituted to channel var
- Added "None" passthrough operation (result = x[0])
- source_names persisted in .labdaq profiles
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
- custom_script now uses exec-style (assign to `result`) matching the
built-in templates — previously auto-wrapped in def compute() which
had no return statement, causing state and channel vars to silently fail
- function kind unchanged: auto-wraps body as def compute(x,t); use return
- Add SCRIPT_TEMPLATES for expression, function, custom_script kinds;
shown automatically when user switches to a script kind (replacing stale
code from previous kind)
- New channels default to template when no saved code exists
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
- Variable names now appear as a live '# Variables: A0 = x[0] · ...'
comment at the top of the code editor (function, custom_script, and
built-in read-only templates); updates on every source add/remove/change
- Derivative 'With Respect To' channel combo now included in variable list
as x[1] when channel mode is selected
- _apply() strips the auto-generated comment before saving the script so
it doesn't pollute the stored expression
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
When sources are added, removed, or changed in the derived channel editor,
the code group now shows a live "Variables: A0 = x[0] · encoder = x[1]"
hint above the code editor so users know the exact names to use in scripts.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
- 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>
|
|
Naming conventions:
- Devices window: "Channel ID" column → "Signal ID"
- Serial device auto-gen IDs: CH# → sig# (generic, SCPI, Modbus fallbacks)
- Channels window physical section: "Physical Channels" → "Signals"
- Channels window virtual section: "Virtual Channels" → "Channels"
- "Add Virtual" button → "Add Channel"
- New derived channel default name: "Virtual Channel #" → "Channel #"
Signals panel (ChannelPipelineBlock):
- Remove Calibrate header button; add inline Calibration section in body
- Remove Math section (Derivative/Integral) entirely
- scale_offset excluded from filter dropdown; routed to Calibration section
- Existing pipelines: scale_offset loads into Calibration, math filters dropped
- _FILTER_KEYS excludes derivative, integral, scale_offset
Splitter default position:
- Left (Signals) pane initialised to minimum content width via QTimer.singleShot
- setStretchFactor(0,0) / (1,1): left stays narrow, right grows on resize
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
Add Device dialog:
- Replace Available Devices QGroupBox with plain devWindowTitle label
to match Configuration section style — no box/border
- Auto-scan on open via QTimer.singleShot(0)
- Rename Scan All → Refresh
Serial config widget:
- Fix gap between protocol panel and Available Serial Ports: introduce
_AdaptiveStack (QStackedWidget subclass) that reports only the current
panel's sizeHint; set Fixed vertical size policy so the stack never
expands beyond its content regardless of window size
- Stretch added between Available Serial Ports and Apply & Reconnect so
the button stays pinned to the bottom
- Rename Channels → Signals in _GenericPanel and _SCPIPanel (Signal Queries)
Config dialog (device info tab):
- Rename Channels → Signals
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
Profile integration:
- CameraDevice.get_save_config() serialises camera_index, simulate,
resolution, fps — cameras now persist in .labdaq profiles
- ProfileManager.register/unregister_device_factory() lets plugins
register their device classes for profile restoration without
core → plugin dependency
- MotionCapturePlugin registers CameraDevice factory on load/unload
Camera configuration:
- Add Frame Rate combo to CameraPanel (Default/15/24/30/60/120 fps)
defaults to 30 fps; flows through to CAP_PROP_FPS on connect
- tracker.start() accepts fps param alongside existing resolution
Tracking persistence:
- CameraTracker.is_running() — checks thread alive state
- MotionCaptureWindow initialises _connected from tracker.is_running()
so reopening the window resumes the live feed without restarting
the camera or losing the active tracking state
Window state:
- Plugin saves px_per_mm + camera_idx when window closes
- Reopened window restores saved values and syncs mode/shape UI
from tracker state via restore_ui_state() (signals blocked to
avoid resetting tracker)
- Window type changed to Qt.WindowType.Window for independent
close button on all Linux WMs
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
Tracking:
- Add "template" mode (default) using snapshot + shape-masked matchTemplate
- Add "csrt" mode as alternative for texture-rich targets
- set_roi_rect(x, y, w, h, frame_rgb) initialises from exact selection rect
- set_roi() delegates to set_roi_rect (centred square, backwards-compat)
- start() accepts resolution=(w, h) to set camera capture resolution
Selection UX:
- Replace QLabel feed with FrameSelector widget (custom QWidget)
- "Click to Track" freezes frame; user scrolls to zoom (anchored to cursor),
right-drags to pan, left-drags to draw rubber-band selection
- Shape preview: rectangle or inscribed ellipse depending on mode
- "Confirm Selection" extracts template from frozen frame and starts tracking
- "Clear" cancels selection or clears active tracking
Configuration:
- CameraPanel adds Resolution combo (Default / 640×480 / 720p / 1080p / 1440p)
- CameraDevice stores and forwards resolution to tracker.start()
- Template Match listed first in Mode combo; shape combo hidden for CSRT
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
- Unified device scanner at top (serial ports, NI devices, cameras)
replaces per-panel scan groups; clicking pre-fills port/type
- Port, baud rate, NI Device, and simulation toggle moved into
Available Devices section; shown/hidden by device type
- Device type, ID, and Protocol/Format grouped under Configuration
- Panel order: Serial/UART → Arduino → NI-DAQ (Serial first)
- Add button always enabled; validation on press shows error popup
with focus jump to the offending field
- CameraPanel loses sim_chk (sim state set by dialog via set_simulate)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
|