--- name: ui-remote description: > Attach to and drive the running Joulescope UI via its --tcp-server remote-control interface. Use when asked to screenshot the UI, verify a UI change visually, switch views (multimeter/oscilloscope/file), read or change UI settings, inspect widget state, click/drag/type in the UI, watch live statistics, or launch the UI for verification. Not for editing UI source code — only for interacting with a live UI process. --- # Driving the Joulescope UI remotely The UI started with `--tcp-server` exposes remote PubSub + Qt inspection on localhost (default port 21861). Authoritative protocol/API doc: `docs/tcp_server.md`. One-shot helper for everything below: `python ci/uitest/cli.py ` (run from the repo root, in the Python environment where `joulescope_ui` is importable). ## Attach or launch — decision tree 1. Check for a live UI: `python ci/uitest/cli.py ping` - Exit 0 → connected; output includes the active view. Proceed. - Exit 2 → no UI with `--tcp-server`. Credentials live in `%LOCALAPPDATA%\joulescope\server.json` (`{'token','port'}`); the file is deleted on clean exit, so its presence normally means a live UI. 2. `server.json` exists but ping fails → stale file from a hard kill. Tell the user; do not silently delete it. Launching a new UI overwrites it. 3. No UI running and the task needs one → launch it yourself with Bash `run_in_background`: python -m joulescope_ui --tcp-server Wait in two stages: poll for `server.json` to appear (≤30 s), then poll `ping` until it succeeds. The token regenerates on every launch — never cache credentials across launches. 4. **Never use `ci/uitest/harness.py::UiSession` on the user's real UI.** Its `stop()` publishes `!close` with `config_clear`, which **wipes the developer's real UI configuration**. UiSession is for isolated pytest runs only. 5. Closing: only close a UI that you launched yourself, with a plain `cli.py publish "registry/ui/actions/!close" null` (no config clear). Never publish `!close` to a UI the user was already running. ## The verification loop The core workflow for "verify this UI change": 1. Act (publish a setting, click, switch view). 2. Verify state: `query` the topic, or use `publish ... --wait` (read-after-write). `publish` is fire-and-forget with no ack — never sleep-and-hope. 3. Screenshot to the session scratchpad, numbered for before/after pairs: `python ci/uitest/cli.py screenshot /ui_01.png` 4. Read the PNG (the Read tool renders images) and decide. ## CLI cheat sheet All commands print one JSON object. Exit codes: 0 ok, 2 UI not running/stale creds, 3 timeout, 4 server error. Globals: `--timeout` (default 15 s), `--port`, `--token`, `--server-json`. | Command | Example | |---|---| | `ping` | `cli.py ping` — attach check, reports active view | | `screenshot` | `cli.py screenshot out.png --widget WaveformWidget:0` | | `query` | `cli.py query registry/view/settings/active` | | `publish` | `cli.py publish registry/view/settings/active view:multimeter --wait` | | `view` | `cli.py view multimeter` \| `oscilloscope` \| `file` (waits, prints old→new) | | `enum` | `cli.py enum registry` — discover the topic tree | | `inspect` | `cli.py inspect --depth 3` or `cli.py inspect central_widget` | | `find` | `cli.py find --class WaveformWidget` / `--name my_button` / `--text "1.5.1"` — prints usable widget paths | | `action` | `cli.py action click --path my_button`; `cli.py action resize --kwargs '{"width":1280,"height":800}'` | | `devices` | `cli.py devices` — connected Joulescopes (model, serial) | | `stats` | `cli.py stats --duration 2` — sample live statistics safely | PowerShell quoting for JSON args: wrap in single quotes (`--kwargs '{"x":1}'`). Available `action` names: `click`, `drag`, `key`, `cursor`, `resize`, `menu_invoke`, `menu_close`, `menu_items`, `set_property`, `get_property`, `call` (see `docs/tcp_server.md`). ## Direct client use For anything the CLI doesn't cover (persistent subscriptions, numpy signal data), write a script: ```python import sys sys.path.insert(0, 'ci') from uitest.discover import find_credentials from joulescope_ui.tcp_client import Client creds = find_credentials() # {'token','port'} or None with Client(**creds, timeout=15.0) as c: print(c.query('registry/view/settings/active')) ``` Pointers: full client API and wire protocol → `docs/tcp_server.md`. Wait/interaction helpers (`wait_for`, `open_file`, `wait_for_statistics`, `record_start/stop`, `export_range`, `is_waveform_rendered`) → `ci/uitest/harness.py` (pytest only — see warning above). Widget-tree search → `ci/uitest/qt.py`. ## Topics and widget paths - `registry/view/settings/active` — `view:multimeter | view:oscilloscope | view:file` - `registry/view/actions/!widget_open` / `!widget_close` - `registry/+/events/statistics/!data` — live statistics (subscribe) - Discover the rest: `cli.py enum registry`, then drill down. Widget paths are slash-separated `objectName` values with `ClassName:index` fallback (0-based among same-class siblings), e.g. `central_widget/WaveformWidget:0`. **An empty path targets the ACTIVE window — an open dialog steals targeting; dismiss dialogs first** (Escape key action or `menu_close`). ## Pitfalls - Never publish `registry/ui/actions/!close` or anything config-clearing to the user's own UI session. - `publish` has no ack — verify via `query`, `--wait`, or a screenshot. - The library's default client timeout is 5 s; the CLI already uses 15 s. Use more for device operations, file open, or export. - The token changes every launch, and a stale `server.json` survives a hard kill — always `ping` before trusting it. - Streaming signal topics deliver numpy arrays over binary frames — don't try to JSON-print them; use `stats` for quick numbers. - Subscriptions cover the topic's subtree, and a `+` segment matches any one segment (e.g. `registry/+/events/statistics/!data`); callbacks receive the concrete topic. - Offscreen or unpainted OpenGL waveform: hit-test geometry stays empty and waveform mouse interactions **silently no-op**. Waveform interaction needs a real, visible display (`is_waveform_rendered` in the harness checks this). - No CLI flags for port or window size — resize via `action resize`. - Blank screenshot → the window may be minimized; ask the user to restore it. ## Reference map - `docs/tcp_server.md` — protocol, full client API, all `qt_action` kwargs. - `ci/uitest/harness.py` — launch-and-own `UiSession` harness (pytest only). - `ci/uitest/README.md` + `ci/uitest/test_*.py` — worked interaction examples (menus, drag, export, markers).