| Age | Commit message (Collapse) | Author |
|
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>
|
|
|
|
|
|
get_toolbar_actions() returns a checkable 🎥 button. On click:
- 0 camera devices → info message, button unchecks
- 1 camera device → opens MotionCaptureWindow directly
- N camera devices → QInputDialog picker, opens selected device's window
Windows tracked per device_id; closing a window removes it from the
map and unchecks the button when none remain. on_unload closes all
open windows before deregistering the Camera panel.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
Plugin now registers a "Camera" option in the Add Device dialog on load
(removes it on unload) instead of auto-creating a single device.
- camera_panel.py (new): CameraPanel with CameraScanThread that probes
OpenCV indices 0–5, lists found cameras with resolution, shows
"No cameras detected" when none available, simulation checkbox.
build_device() returns CameraDevice for the selected index.
- device.py: rewrite MotionCaptureDevice → CameraDevice. Each instance
owns its own CameraTracker created in connect(). Status is CONNECTED
or SIMULATED based on tracker.simulated. get_config_widget() shows
source/status and an "Open Camera View" button that launches the
existing MotionCaptureWindow for that device's tracker. Backward-compat
alias MotionCaptureDevice = CameraDevice kept for saved profiles.
- plugin.py: simplified to on_load/_PANELS registration + pixel_to_mm
filter. No auto-devices, no toolbar button, no settings widget.
Camera data flows through the standard Device → Signal → Channel → Plot
pipeline like any other device.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
Split zone was unreachable: squeezed between outer squeeze (1/6) and
center (1/3), leaving a 1/6-wide band impossible to target reliably.
Users hitting the tile edge always triggered squeeze instead of split.
- Remove squeeze from inside-tile detection entirely. Split now owns
the outer 1/3 of each tile edge — large, easy to target. Center
keeps the inner 1/3. Squeeze remains gap-detection only (second pass).
- Fix canvas-edge squeeze (no adjacent neighbor): instead of bisecting
the edge tile, wrap the entire remaining tree in a new directional
split with the dragged tile placed at the outer position, then
equalize — all tiles end up equal size.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
- Fix squeeze zone pop-out: _adjacent_tile used self._drag_tile (already
None at drop time) to exclude the dragged tile from neighbor candidates.
Pass dragged_idx explicitly via exclude_idx param instead.
- Swap split/squeeze detection areas: outer 1/6 now triggers squeeze
(insert-between), inner 1/6→1/3 band triggers split (bisect target).
- Extend squeeze detection to gaps between tiles and canvas edges via a
second pass in _zone_at that expands tile rects by _LC_GAP+2 and finds
the closest outside edge. Canvas-edge squeeze falls through to bisect.
- Dynamic canvas height: sync_panes sets height to
max(160, n*(min_tile+gap)+2*gap) so 4+ panes never get crushed below
minimum tile size.
- Remove _tree_grid_size cap of 12: LCM was truncated prematurely,
causing unequal rowspans in the strip chart for 5+ panes.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
Root cause: PlotWindow deepcopied _chart._cfg on init, so refresh_channels()
operated on a stale copy. Also, when _win_plot was None, _on_derived_changed
did nothing to prepare the config for when it opened.
Fix:
- refresh_channels() now owns the derived-pane sync: adds panes for new
virtual channels (updating BSP tree), removes panes for deleted ones
- _open_plot always passes current _chart._cfg reference then calls
refresh_channels() so derived panes sync on every open
- _on_derived_changed syncs cfg reference before calling refresh_channels()
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
|
|
|
|
Replaces layout radio buttons (Stacked/Side-by-Side/Grid) with a
drag-and-drop tiling canvas. Three drop zones per tile edge:
- squeeze (outer 1/6, amber): insert between two tiles
- split (1/6–1/3, blue): bisect tile 50/50
- center (green): swap tile positions
BSP tree stores layout; tree_to_grid() converts to rowspan/colspan for
pyqtgraph. _tree_equalize_ratios ensures all tiles share space equally
after any operation. Backward-compatible with existing .labdaq profiles.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
_zone_at used absolute pixel distance to pick dominant axis, which always
favoured the short (vertical) axis for landscape tiles. Replaced with
normalized fraction (dist/dimension), so left/right zones are accessible
on wide tiles.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
Zone layout (outer to inner per edge):
0–1/6 squeeze (amber): insert dropped tile between two existing tiles
1/6–1/3 split (blue): bisect the target tile 50/50
1/3–2/3 center (green): swap/move
Squeeze behaviour: find the tile adjacent to the target in the drop
direction; insert dragged tile to the opposite side of that neighbor
so it lands visually between them. Falls back to split when no neighbor
exists (edge tile).
- _zone_at: returns "squeeze_*"/"split_*"/"center" with dominant-axis
selection (nearest edge wins)
- _adjacent_tile: finds geometrically adjacent tile by pixel proximity
- _do_drop: routes squeeze→insert-at-neighbor, split→bisect, center→swap
- paintEvent: amber/blue/green per zone type
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
- Edge zones (left/right/top/bottom): split target tile equally
- Center zone: swap dragged and target tile positions in BSP tree
(tree structure preserved, only leaf pane values exchanged)
- Center overlay: green highlight (vs blue for split zones)
- _tree_swap: immutable swap of two leaf pane values
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
After each drag-drop, add, or remove, walk the tree and reset each split's
ratio to left_leaves/(left_leaves+right_leaves). This distributes space
equally per pane regardless of the prior tree structure.
- _tree_equalize_ratios: recomputes all ratios bottom-up by leaf count
- _tree_leaf_count: counts leaves in a subtree
- _tree_grid_size: uses LCM of split denominators (capped at 12) instead
of 2^depth, giving exact integer rowspans for equal distributions
- _tree_default: balanced binary tree + equalize (not uneven chain)
- Applied in _do_drop, _add_pane, _rm
Result: 3 side-by-side panes → equal widths; any mixed layout → equal area.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|