Skip to content
This repository was archived by the owner on Oct 23, 2026. It is now read-only.

Commit d4dfdd5

Browse files
MarkoVcodeclaude
andcommitted
Version and feedback in the app, longer console history with Save, USBTMC stall recovery, Metrics under Instruments, kit styles in drawers
- Top bar shows the running version; Feedback drawer (version, copy, GitHub issues links opened in the system browser via the open_page command) - Instrument console: 50 000 entries / 8 MiB per instrument, first fetch limited, Save writes the full log; GET /devices/{id}/wire takes limit - USBTMC (Linux kernel driver and nusb): a stalled bulk-out is aborted and cleared once and the write retried; recovery steps logged in the console - Metrics moved to an Instruments tab, judged against limits in the bench page style - Drawer pages share .bench-page / .bench-group; e2e edge guard; Instruments form buttons and fields use the kit styles Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
1 parent 2c88370 commit d4dfdd5

39 files changed

Lines changed: 1074 additions & 460 deletions

‎CLAUDE.md‎

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -164,9 +164,10 @@ code; the definitions' `ui_component` is not used by the UI.
164164
| `bench/panels/` | class panels (`PsuPanel`, `DmmPanel`, `EllPanel` - modes without a setpoint, Short (two presses); `AwgChannel` - frequency or period, amplitude as Vpp / Vrms / dBm / high-low levels, phase, ramp symmetry, load, pulse duty/width/edges, mode CW / MOD (AM/FM/PM: modulating wave, frequency, depth or deviation) / SWEEP (start, stop, time, spacing, trigger) / BURST (N cycles or infinite, period, trigger), each control only when the definition binds it: conversions in `bench/awgMath.ts`, values the instrument does not report tagged CALC; `OscPanel` - one screen for all channels, `LcrPanel` - primary/secondary readouts, function keys L/C/R/Z/DCR → secondary → SER/PAR, frequency/level, range/speed, open/short correction with a prompt, bias; `SalPanel` - spectrum screen (`kit/SpectrumScreen`: dB graticule, max-hold ghost, marker diamond, click to place), marker readout, annotation bar, soft-key groups FREQ/BW/AMPT/SWEEP/TRACE/MKR/TG; `GenericPanel`) |
165165
| `bench/layout.ts`, `Rack.tsx` | panel order / width (S, M, L, W) / hidden / channel layout of multi-channel panels (side by side or stacked), persisted in `localStorage` (`benchmesh:bench-layout`); columns follow the window width (300 px units) |
166166
| `bench/BenchLayout.tsx` | shell: top bar, **Arrange** mode (drag, ◀ ▶, width, channels ⇆ / ⇅, hide), drawers (Instruments config, Request log) |
167-
| `drawers/` | instrument configuration + settings (incl. **Detect**: `DetectView.tsx`, `detect.ts`), request log |
167+
| `drawers/` | Instruments drawer tabs: configuration (incl. **Detect**: `DetectView.tsx`, `detect.ts`), Supported, Miscellaneous, **Metrics** (`MetricsView.tsx` + `metrics.ts`: per instrument link busy, exchanges/s, API latency from `/ws/metrics`, judged against limits), Remote access; request log |
168168
| `recording/` | recordings v2 (`ai_context/migration/RECORDINGS.md`): `RecordingsProvider` (REST + `/ws/recordings`, live sample buffers; main and panel windows), recordable readings (`signal.tsx`: `SignalScope` + `Recordable`, kit `ContextMenu` on right-click / context-menu key, red dot when recorded), recorder dock (bottom of the main window, `benchmesh:recorder-dock`), recordings drawer (history, zoomable chart, rename, CSV export, delete). Chart: `chartModel.ts` (one y-axis per series, colours `--kit-series-1..8`), ECharts core loaded lazily in `RecordingChart.tsx` |
169169
| `kit/brand.ts`, `kit/BrandMark.tsx` | the logo ("Readout": seven-segment b, ghost segments, amber decimal point): one geometry, colours on dark / on light; `BrandMark` follows the skin (`--kit-brand-*`) or takes fixed colours (website). `node scripts/logo/build.mjs` writes `docs/static/logo/*.svg`, `frontend/public/favicon.svg` and the app icons (`crates/benchmesh-app/icons`, via Inkscape + `cargo tauri icon`); a test fails when they are stale |
170+
| `about/` | top-bar version (`GET /version`, `version.ts`) and the **Feedback** drawer (`AboutView.tsx`: version, Copy version, GitHub issues links). External pages open by name (`links.tsx`): in the desktop app the system browser via the `open_page` command (fixed list, `crates/benchmesh-app/src/links.rs`, capability `links.json`), in a browser a new tab |
170171
| `automation/` | Automation drawer (top bar **Automation**, contract `ai_context/AUTOMATION.md`): saved flows (new, duplicate, delete, import/export JSON), flow editor (parameter form, Setup / Steps / Cleanup step cards with vocabulary pickers, live validation shown at each step's path, JSON view; pure document model `flowModel.ts`), run panel (arm dialog for energising flows, live log, variables, prompt Continue, Abort / Escape), assistant (user's own LLM key, result loaded as unsaved), settings tab (provider, key - only its hint is ever shown, model, base URL; MCP access + connection snippets). `RunProvider.tsx` polls `/automation/run` for the whole UI: top-bar `▶ flow - state` + red **Stop** while a run is active |
171172

172173
Panel **window controls** (header, top right): fold to the header line (saved with the layout;
@@ -182,16 +183,27 @@ allow keep-on-top; `BENCHMESH_NATIVE_WAYLAND=1` opts out.
182183

183184
**Instrument console** (panel header, console icon): an optional pane at the bottom of the panel
184185
showing what is sent to (→) and received from (←) the instrument, plus port events (•), from
185-
`GET /devices/{id}/wire?after=<seq>` (per-device ring of 2000 entries, `bm_driver::wirelog`;
186-
reply chunks joined in the UI). Auto / Text / Hex view, pause, clear, and **Copy**: paste-ready
187-
evidence with instrument, model, identify reply, link and app version (`bench/wire.ts`,
188-
`bench/WireConsole.tsx`). Open state per panel in `localStorage` `benchmesh:consoles`.
186+
`GET /devices/{id}/wire?after=<seq>&limit=<n>` (per-device ring of 50 000 entries / 8 MiB, about
187+
19 minutes of a polled oscilloscope and longer for meters, `bm_driver::wirelog`; reply chunks
188+
joined in the UI, which shows the last 5000 lines). Auto / Text / Hex view, pause, clear, **Copy**:
189+
paste-ready evidence with instrument, model, identify reply, link and app version, and **Save**:
190+
the same for everything the server kept, as a text file (`bench/wire.ts`, `bench/WireConsole.tsx`).
191+
Transports add their own events as info lines (`Transport::take_notes`), e.g. a USBTMC recovery:
192+
a write that times out or stalls gets abort bulk-out/in, clear halts and a device clear, then one
193+
more try, else an error telling the user to replug (`bm-transport` usbtmc.rs, usbtmc_proto.rs). Open state per panel in `localStorage` `benchmesh:consoles`.
189194

190195
Every control that changes the instrument waits for **confirmation**: after the request is sent it
191196
pulses discreetly until the instrument's reported value (registry status, or a read-back for
192197
setpoints) equals the requested one, and shows a small red dot if that does not happen within 6 s
193198
(`kit/confirm.ts`). Pass the reported value, never a locally assumed one.
194199

200+
Drawer pages of text and controls use the shared page standard in `bench/bench.css`: content in
201+
`.bench-page` (16 px inset, 14 px rhythm, `.bench-hint` text), boxes in `.bench-group`, rows in
202+
`.bench-group__row` (About and feedback, Metrics, Remote access); `e2e/about-feedback.spec.ts` fails when a
203+
drawer's content comes closer than 8 px to the drawer's sides. Buttons are kit buttons (`bench-btn`, `--primary` for
204+
the main action, `--danger` for removal, `--icon` for a glyph), fields the kit field (`kit-field`, `kit-label`,
205+
`kit-input`); the Instruments form styles its fields with the same tokens.
206+
195207
Adding a class panel: write `bench/panels/<Cls>Panel.tsx` from kit components, a `<cls>Profile()`
196208
in `profiles.ts` (with tests against `bench/__tests__/bench-fixtures.json`, captured from the
197209
simulated bench), and dispatch it in `InstrumentPanel.tsx`.

‎ai_context/migration/device-notes/rigol_dho800.md‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,5 +58,19 @@ measurement" (e.g. FREQ on a flat line) → status null.
5858
device clear did not help, and the scope **dropped off USB and came back ≈ 50 s later**
5959
(re-enumerated; settings intact). Operation disabled in the definition. Next step: the userspace
6060
USBTMC transport (`nusb`, same item as the DGE screenshot) with proper multi-transfer reads.
61+
- **USB lockup during normal use** (2026-10-21, kernel usbtmc, app polling the panel): twice in
62+
ten minutes a read request timed out (`usbtmc 1-2:1.0: send_request_dev_dep_msg_in returned
63+
-110`), after which every write timed out (`Unable to send data, error -110`) until the cable
64+
was replugged; the scope did not drop off USB by itself. The trigger is not known yet (the wire
65+
log then kept only 46 s). Since then the transport runs one USB recovery when a write times out
66+
or stalls (abort bulk-out/in, clear halts, device clear; `bm-transport` usbtmc.rs and
67+
usbtmc_proto.rs), the wire log keeps about 19 minutes of this scope, and the console's Save
68+
writes it to a file. Bench result the same night (a third lockup, caused by a development
69+
rebuild killing the app mid-exchange): the recovery made the scope take commands again
70+
(abort bulk-out answered "not in progress", clear halts and device clear ok) but it never
71+
replied; it then dropped off USB and failed to enumerate (-71). After a cable replug it
72+
answered with stale bTags and then not at all - the scope powers its own USB side, so a
73+
replug does not reset its firmware. Only switching the scope off and on brought it back.
74+
The recovery stays (cheap, may help lighter stalls); the cure is not to interrupt sessions.
6175
- Edge trigger only (other trigger types not modelled). Digital channels (D0-D15), math, decoders,
6276
memory depth and RAW/MAXimum waveform reads are not modelled.

‎crates/benchmesh-app/build.rs‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
fn main() {
2-
// The app's own commands (update.rs) get permissions, so a capability can grant them to the
2+
// The app's own commands (update.rs, links.rs) get permissions, so a capability can grant them to the
33
// UI, which is served by the embedded server (a remote origin to Tauri).
4-
let manifest = tauri_build::AppManifest::new().commands(&["update_check", "update_install", "update_open_page"]);
4+
let manifest = tauri_build::AppManifest::new().commands(&["update_check", "update_install", "update_open_page", "open_page"]);
55
tauri_build::try_build(tauri_build::Attributes::new().app_manifest(manifest)).expect("failed to run tauri-build");
66
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"$schema": "../gen/schemas/desktop-schema.json",
3+
"identifier": "links",
4+
"description": "The UI (served by the embedded server on 127.0.0.1) opens the feedback and releases pages in the system browser by name (src/links.rs).",
5+
"windows": ["main", "panel-*"],
6+
"remote": { "urls": ["http://127.0.0.1:*", "http://localhost:*"] },
7+
"permissions": ["allow-open-page"]
8+
}

‎crates/benchmesh-app/src/links.rs‎

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
//! Web pages the UI opens in the system browser (the webview does not follow external links).
2+
//! A fixed list: the UI is served over HTTP, so it names a page and never passes a URL.
3+
4+
pub const RELEASES: &str = "https://github.com/MarkoVcode/BenchMesh/releases/latest";
5+
const PAGES: [(&str, &str); 3] = [
6+
("issues", "https://github.com/MarkoVcode/BenchMesh/issues"),
7+
("new-issue", "https://github.com/MarkoVcode/BenchMesh/issues/new"),
8+
("releases", RELEASES),
9+
];
10+
11+
pub fn url_of(page: &str) -> Option<&'static str> {
12+
PAGES.iter().find(|(name, _)| *name == page).map(|(_, url)| *url)
13+
}
14+
15+
/// Open `url` with the system's default browser.
16+
pub fn open(url: &str) -> std::io::Result<()> {
17+
#[cfg(target_os = "linux")]
18+
let child = std::process::Command::new("xdg-open").arg(url).spawn();
19+
#[cfg(target_os = "macos")]
20+
let child = std::process::Command::new("open").arg(url).spawn();
21+
#[cfg(windows)]
22+
let child = std::process::Command::new("cmd").args(["/C", "start", "", url]).spawn();
23+
child.map(|_| ())
24+
}
25+
26+
/// Open one of the known pages (`issues`, `new-issue`, `releases`).
27+
#[tauri::command]
28+
pub fn open_page(page: String) -> Result<(), String> {
29+
let url = url_of(&page).ok_or_else(|| format!("unknown page {page:?}"))?;
30+
open(url).map_err(|e| format!("could not start the browser: {e}"))
31+
}
32+
33+
#[cfg(test)]
34+
mod tests {
35+
use super::url_of;
36+
37+
#[test]
38+
fn only_known_pages_open() {
39+
assert_eq!(url_of("new-issue"), Some("https://github.com/MarkoVcode/BenchMesh/issues/new"));
40+
assert!(url_of("issues").unwrap().ends_with("/issues"));
41+
assert_eq!(url_of("https://example.com"), None);
42+
assert_eq!(url_of(""), None);
43+
}
44+
}

‎crates/benchmesh-app/src/main.rs‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@
1414
//! by the UI; capability `capabilities/panels.json`). Closing the main window closes them too.
1515
//! - Linux/Wayland: windows cannot ask to stay on top under Wayland, so the app runs through
1616
//! XWayland (`GDK_BACKEND=x11`) unless `BENCHMESH_NATIVE_WAYLAND=1`.
17-
//! - Updates: one check per start, offered by the UI (`update.rs`).
17+
//! - Updates: one check per start, offered by the UI (`update.rs`). Feedback / releases pages
18+
//! open in the system browser by name (`links.rs`, capability `capabilities/links.json`).
1819
//! - Windows: the UI's "Install driver" (USBTMC instruments) starts this executable again
1920
//! elevated with `--install-winusb` (`bm_transport::winusb`); `main` hands that run over first.
2021
//! - `benchmesh --mcp`: no window - a stdio bridge to the running app's MCP endpoint for AI
@@ -29,6 +30,7 @@ use std::time::Duration;
2930
use bm_server::run::{start, RunningServer, ServerOptions};
3031
use tauri::{Manager, RunEvent, WebviewUrl, WebviewWindowBuilder, WindowEvent};
3132

33+
mod links;
3234
mod update;
3335

3436
struct Server(Mutex<Option<RunningServer>>);
@@ -104,7 +106,7 @@ fn main() {
104106
let app = tauri::Builder::default()
105107
.plugin(tauri_plugin_updater::Builder::new().build())
106108
.manage(update::PendingState::default())
107-
.invoke_handler(tauri::generate_handler![update::update_check, update::update_install, update::update_open_page])
109+
.invoke_handler(tauri::generate_handler![update::update_check, update::update_install, update::update_open_page, links::open_page])
108110
// the main window owns the app: closing it closes detached instrument windows too
109111
.on_window_event(|window, event| {
110112
if window.label() == "main" && matches!(event, WindowEvent::Destroyed) {

‎crates/benchmesh-app/src/update.rs‎

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,6 @@ use serde::Serialize;
1414
use tauri::{AppHandle, Emitter, Manager, State};
1515
use tauri_plugin_updater::{Update, UpdaterExt};
1616

17-
const RELEASES_PAGE: &str = "https://github.com/MarkoVcode/BenchMesh/releases/latest";
18-
1917
/// Download progress for the UI (`downloaded` bytes of `total`, absent when unknown).
2018
const PROGRESS_EVENT: &str = "update://progress";
2119

@@ -112,12 +110,7 @@ pub async fn update_install(app: AppHandle, pending: State<'_, PendingState>) ->
112110
/// Open the releases page in the system browser.
113111
#[tauri::command]
114112
pub fn update_open_page() {
115-
#[cfg(target_os = "linux")]
116-
let _ = std::process::Command::new("xdg-open").arg(RELEASES_PAGE).spawn();
117-
#[cfg(target_os = "macos")]
118-
let _ = std::process::Command::new("open").arg(RELEASES_PAGE).spawn();
119-
#[cfg(windows)]
120-
let _ = std::process::Command::new("cmd").args(["/C", "start", "", RELEASES_PAGE]).spawn();
113+
let _ = crate::links::open(crate::links::RELEASES);
121114
}
122115

123116
#[cfg(test)]

‎crates/bm-driver/src/engine.rs‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,11 @@ pub trait Transport: Send {
2626
/// Bytes that arrived within `timeout`; empty = nothing arrived. Message-based
2727
/// transports (USBTMC) return exactly one device message per call.
2828
fn read(&mut self, timeout: Duration) -> std::io::Result<Vec<u8>>;
29+
/// What the transport did on its own since the last call (a USB recovery, say), for the
30+
/// instrument's wire log.
31+
fn take_notes(&mut self) -> Vec<String> {
32+
Vec::new()
33+
}
2934
}
3035

3136
/// Result of an operation.

0 commit comments

Comments
 (0)