| Age | Commit message (Collapse) | Author |
|
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>
|
|
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>
|
|
|
|
|
|
|
|
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>
|
|
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>
|
|
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>
|
|
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>
|
|
|
|
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>
|
|
|
|
|
|
_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>
|
|
Dragging a tile to the edge (left/right/top/bottom) of another tile
splits that tile's space and inserts the dragged tile on that side.
Drop zones occupy the outer third of each tile edge; center has no zone.
Ghost overlay shows the split target during drag.
Data model: LayoutConfig.tree (BSP tree as nested dict). PaneSpec.row/col
removed. strip_chart uses tree_to_grid() → rowspan/colspan for pyqtgraph.
Backward compat: tree=None falls back to layout_mode stacking path.
Example: 3 stacked → drag Tile 3 to left of Tile 1 → Tile 3|Tile 1 on
top (side-by-side), Tile 2 spans full bottom.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
Replace layout radio buttons (Stacked/Side-by-Side/Grid) with a visual
LayoutCanvas widget. Pane tiles snap to grid cells on drag-drop; dropping
on an occupied cell swaps positions. Ghost highlight shows snap target
during drag.
- PaneSpec gains row/col fields (None = unassigned; backward compat preserved)
- LayoutConfig gains "free" mode; strip_chart reads explicit positions
- PaneBlock loses ▲▼ move buttons; reordering is canvas-only
- QSS entries added for layoutTile, layoutTileDragging, layoutCanvasBar
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
Remove Qt.WindowType.Tool flag — it suppresses the maximize button.
Window now has standard chrome with minimize/maximize/close.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
virtual blocks
Physical channels panel: horizontal splitter (left/right), channel picker
dialog replaces dropdown, Filters and Math sections per block, Calibrate
button, pipeline-enabled checkbox removed. Virtual channels: collapsible
blocks, enable checkbox removed, kind dropdown removed (defaults to
expression). Window resized to 1400×820.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
- New core/app_settings.py: load/save settings.json to platform config dir
(Windows: %APPDATA%, Linux: ~/.config, macOS: ~/Library/Application Support)
- MainWindow loads saved settings on startup, saves on every Apply & Close
- Settings survive profile switches and app restarts independently of profiles
- Settings > General: Reset to Defaults button with QMessageBox confirmation
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
- Always insert one source row for derivative/second_derivative on build
- Guard in _on_kind: add row when switching to derivative with no sources
- Code editor: monospace font (IBM Plex Mono/Consolas fallback)
- Code editor: tab stop = 4 character widths
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
Reflects current design: split Physical/Virtual Channels layout,
no tabs, no legacy Signals list as primary UI.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
- Default ID: sig_N → CH_VN
- Default name: Signal N → Virtual Channel N
- Rename "Kind" label to "Operation"
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
- Split PipelineTab into Physical/Virtual sections via QSplitter
- Each section has independent scroll area, header, and add controls
- Add "With Respect To" control for derivative/second_derivative virtual channels: Time (dy/dt) or Channel (dy/dx)
- When Channel selected, x variable picker appears for any physical or derived channel
- signal_processor computes dy/dx using channel inputs when deriv_wrt=channel
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
- Remove redundant Signals tab from Channels window; PipelineTab is now direct content
- PipelineTab removes stale blocks when devices are removed or reconfigured
- Devices>Signals tab refreshes automatically on device add/remove/configure
- Channel name and unit edits in Devices>Signals propagate live to Channels pipeline block headers; missing blocks auto-added if channel is enabled
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
- Add missing QGroupBox import to settings_window (caused silent crash on open)
- Remove Controls tab and _controls_tab() method from settings
- Clamp child window positions to screen bounds so they don't go off-screen on Windows when main window is maximized
- Apply same screen-clamp fix to Motion Capture plugin window
- Delete example plugin; disable it in enabled.json
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
Plugins live in plugins/<name>/ with a manifest.json and a LabPlugin
subclass. Enabled/disabled in Settings > Plugins tab; state persists in
plugins/enabled.json and in .labdaq profiles (loading a profile
enables/disables plugins to match). Plugins can contribute toolbar
buttons, BaseDevice instances (auto-wired into acquisition pipeline),
custom signal filter classes, a settings widget, and profile state via
get_save_state/apply_save_state. Load errors now report the exact
failing stage (file not found, class missing, on_load crash).
Includes example plugin skeleton and docs/plugin-development.md.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
|
|
|
|
Fixed derived signal saving profile. Renamed Signals>Pipeline to Signas>Channels
and adding/removing channels feature.
|
|
|
|
|
|
|
|
Including:
- Control panel
- Change status of simulation after devices creation.
- Arduino serial conflict with multiple channels.
- Claude instructions.
- Update to arduino on board code.
|