benchmesh
Python client for BenchMesh, one cockpit for lab instruments: power supplies, multimeters, electronic loads, signal generators, oscilloscopes, LCR meters and spectrum analyzers. Control them, read them and record them from scripts and Jupyter notebooks.
- No runtime dependencies (standard library only). Python 3.9 or newer.
- One API for every instrument of a class: the limits, ranges and modes come from the server.
- Live readings, recordings into pandas, automation flows, remote access with a pinned certificate.
Install
pip install benchmesh # the client
pip install "benchmesh[pandas]" # plus pandas for DataFrames
The BenchMesh app (or benchmesh-server) must be running. The client talks to its API at
http://127.0.0.1:57666 on the same computer.
Quick start
import benchmesh
bench = benchmesh.connect() # $BENCHMESH_URL or http://127.0.0.1:57666
for inst in bench.instruments():
print(inst.id, inst.classes, inst.model, "online" if inst.online else "offline")
psu = bench["tenma-72-2540"] # the id from the Instruments drawer
psu.set("voltage", 5.0) # setter, checked by the server against 0-60 V
psu.set("current", 0.5)
psu.set("output", "ON") # switches the output ON at once
print(psu.read("VOUT")) # latest polled value: 4.94 (float, volts)
print(psu.status()) # {"VOUT": 4.94, "IOUT": 0.012, "OUTPUT": "ON", ...}
print(psu.query("voltage")) # asks the instrument now
psu.set("output", "OFF")
An instrument with several classes (the OWON SPM is a PSU and a DMM) needs the class:
spm = bench["owon-spm3103"]
spm.set("voltage", 3.3, cls="PSU")
spm.read("MEAS", cls="DMM")
Multi-channel instruments take ch= (1-9, default 1): scope.set("scale", 0.5, ch=2).
Values
- Measured values (SI values on the server) become
floatin base units (volts, amperes, ohms). An instrument that reports something that is not a number (for exampleOLon an overloaded meter) gives that text back. - Modes and switches stay strings (
"ON","VOLT"); flags staybool; missing values areNone. - Binary results (screenshots) are
bytes. raw=Trueonquery,readandstatusreturns the server's JSON unchanged.- Setter values are sent as given: floats with
repr, integers as integers, strings unchanged. The server validates them against the definition (range, allowed values) and refuses invalid ones withBadRequest.inst.operations()lists every query and setter with its value spec.
Examples
Read a meter, adjust a load
The classic loop: hold 5 V on a source by raising or lowering the load current (full script: examples/hold_voltage.py).
import benchmesh
bench = benchmesh.connect()
load = bench["owon-oel1515"]
current = 0.0
load.set("mode", "CURR")
load.set("curr", current)
load.set("input", "ON") # the load starts drawing current
try:
for t, values in bench.watch({"v": ("owon-xdm1241", "DMM", "MEAS")}, interval=0.5, duration=60):
v = values["v"]
if v is None:
continue # meter offline
current = min(current + 0.01, 1.0) if v > 5.0 else max(current - 0.01, 0.0)
load.set("curr", round(current, 3))
finally:
load.set("input", "OFF")
bench.watch(signals) polls the latest status (nothing extra is sent to the instruments) and
yields (time_s, {label: value}) only when a reading was updated. A signal is
(device_id, class, field) or (device_id, class, field, channel).
Sweep into pandas
The server records on whole seconds; the script only steps the voltage (full script: examples/sweep_to_pandas.py).
import time
import benchmesh
bench = benchmesh.connect()
psu = bench["tenma-72-2540"]
psu.set("voltage", 1.0)
psu.set("output", "ON")
try:
with bench.record(
"sweep", {"vout": (psu.id, "PSU", "VOUT"), "meter": ("owon-xdm1241", "DMM", "MEAS")}
) as rec:
for v in (1.0, 2.0, 3.0, 4.0, 5.0):
psu.set("voltage", v)
time.sleep(2)
finally:
psu.set("output", "OFF")
df = rec.to_pandas() # index: time (UTC), one column per label
df.plot()
Older recordings: bench.recordings() (newest first), rec.data() for plain lists
{label: (times_s, values)}, rec.export_csv() for the CSV the app exports.
Jupyter
import benchmesh
bench = benchmesh.connect()
rec = bench.recordings()[0]
df = rec.to_pandas()
df.resample("10s").mean().plot(subplots=True)
Remote access
On the bench computer open the BenchMesh app, Instruments - Remote access: set a password, switch it on and note the address and the certificate fingerprint. On the other computer:
bench = benchmesh.connect(
"https://192.168.1.20:57667",
password="the password",
fingerprint="AB:CD:...:EF", # as shown in the app; case and colons do not matter
)
The certificate is self-signed, so the client pins it: right after the TLS handshake it compares
the certificate's SHA-256 fingerprint with the one you give, and only then sends the password.
A different certificate raises FingerprintMismatch. Without a fingerprint the system's
certificate authorities must trust it; otherwise TLSError tells you which fingerprint the
server presented, so you can compare it with the app. An expired session signs in again by
itself. bench.close() (or with benchmesh.connect(...) as bench:) signs out.
BENCHMESH_URL, BENCHMESH_PASSWORD and BENCHMESH_FINGERPRINT can replace the arguments
(see examples/remote_watch.py).
Automation flows
for f in bench.flows():
print(f.id, f.name)
status = bench.run_flow("hold-5v", params={"target": 4.8}, arm=True, on_event=lambda e: print(e["msg"]))
print(status.state, status.vars) # "finished" / "failed" / "aborted"
A flow that can switch outputs on needs arm=True, otherwise the server refuses it with
Refused (error.body["energizes"] lists the operations). run_flow waits until the run ends;
timeout= and Ctrl-C abort it (its cleanup still runs). A flow document (dict) can be run
directly instead of a saved id.
API overview
benchmesh.connect(url=None, *, password=None, fingerprint=None, timeout=30.0) |
Bench; checks the server answers |
bench.instruments(cls=None) / bench.instrument(id) / bench[id] |
Instrument |
bench.watch(signals, interval=0.5, duration=None) |
iterator of (time_s, {label: value}) |
bench.record(name, signals, interval_s=1, description=None) |
Recording (context manager stops it) |
bench.recordings() / bench.recording(id) |
Recording |
bench.flows() / bench.flow(id) / bench.run_flow(flow, params=None, *, arm=False, wait=True, timeout=None, on_event=None) |
Flow, flow document, RunStatus |
bench.run_status() / bench.abort_run() / bench.continue_run() |
current run |
bench.version() / bench.close() |
|
inst.id, .name, .model, .idn, .classes, .channels, .online, .parked, .health |
facts from the server |
inst.set(op, value, *, cls=None, ch=1) |
run a setter |
inst.query(op, *, cls=None, ch=1, raw=False) |
run a query on the instrument |
inst.read(field, *, cls=None, ch=1, raw=False) / inst.status(...) |
latest polled status |
inst.operations(cls=None) / inst.features(cls=None) / inst.refresh() |
description |
rec.data(start=None, end=None, max_points=None) / rec.to_pandas(...) / rec.export_csv() |
samples |
rec.add(label, signal) / rec.pause() / rec.resume() / rec.stop() / rec.delete() / rec.refresh() |
control |
cls is omitted when the instrument has one class. Class names are three letters (PSU, DMM,
ELL, AWG, OSC, LCR, SAL), any case.
Errors
Every server error is a BenchMeshError with .status, .detail and .body:
| Exception | When |
|---|---|
ServerNotRunning |
nothing answers at the address: start the BenchMesh app |
BadRequest (400, 422) |
value outside the setter's range, invalid flow, ... |
AuthenticationError (401, 403, sign-in) |
wrong password, remote session not allowed |
NotFound (404) |
unknown instrument, class, operation, field, recording, flow |
Refused (409) |
parked instrument, a run is busy, flow needs arm, name taken |
InstrumentError (502) |
the instrument answered something unexpected |
InstrumentUnavailable (503, 504) |
not connected, parked or no answer in time |
TLSError, FingerprintMismatch |
remote certificate not trusted / not the pinned one |
Wrong arguments (class not on the instrument, channel 0, an empty name) raise ValueError or
TypeError before anything is sent.
Safety
The API acts at once. set("output", "ON") switches a supply output on, set("input", "ON") a
load input; there is no second confirmation as in the app. Check wiring and limits before you
run a script, keep try / finally blocks that switch outputs off, and test new scripts with
the simulated bench (benchmesh-server --simulate). Automation flows are safer for unattended
runs: they need arm, confirm every setting and switch off what they switched on when they fail.
Development
uv sync # or: python -m venv .venv && pip install -e . pytest cryptography pandas
uv run pytest # unit tests (a fake server; no hardware)
uvx ruff check . && uvx ruff format --check .
Integration tests run against a real server and are skipped unless BENCHMESH_INTEGRATION_URL
is set; use a simulated server:
# in the BenchMesh repository
cargo build -p bm-server
BENCHMESH_ONLINE=off BENCHMESH_DATA_DIR=$(mktemp -d) ./target/debug/benchmesh-server \
--config captures/bench-config.yaml --simulate captures/20261016-203514 --port 57690
# here
BENCHMESH_INTEGRATION_URL=http://127.0.0.1:57690 uv run pytest tests/integration
# also remote access over HTTPS (simulation only; switches it on at that port and off again)
BENCHMESH_INTEGRATION_URL=http://127.0.0.1:57690 BENCHMESH_INTEGRATION_REMOTE_PORT=57697 uv run pytest tests/integration
Releasing
Bump src/benchmesh/_version.py, commit, tag vX.Y.Z and push the tag. The release
workflow builds and publishes to PyPI with trusted publishing. Once, on PyPI: add a trusted
publisher for project benchmesh with this repository, workflow release.yml and
environment pypi (Account settings - Publishing - Add a new pending publisher).
Licence
MIT, see LICENSE. BenchMesh: https://benchmesh.online
Metadata
Release files for benchmesh 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| benchmesh-0.1.0.tar.gz | 39.4 kB | With provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| benchmesh-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 68.4 kB
Release files / benchmesh-0.1.0.tar.gz
| Download URL | benchmesh-0.1.0.tar.gz |
|---|---|
| Size | 39.4 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
54e61ca102aa56d0004059e1890c88bd3d56e6becc88b333f416f1383739649c
|
|
BLAKE2b-256 checksum How to use checksums |
144083ffdb20cf8b532b13d63c55356a9b481c27b9842036497c3f7dcb3f7633
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 23, 2026.
Transparency logRelease files / benchmesh-0.1.0-py3-none-any.whl
| Download URL | benchmesh-0.1.0-py3-none-any.whl |
|---|---|
| Size | 28.9 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
3220e683bd31f62eead1c7f992f642a97a0d2783bc340599c40a9199852240d8
|
|
BLAKE2b-256 checksum How to use checksums |
3781acacb8dbf071a64f971c38c907653925acaf62c210f2482317202556471f
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 23, 2026.
Transparency logSecurity for benchmesh 0.1.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
Attestation for benchmesh-0.1.0.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 23, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.