open289-mcp
Provides tools for reading a Fluke 287/289 multimeter through an IR3000 FC Bluetooth adapter: live readings (~4/s), full display snapshots (function, range, HOLD/MIN MAX/REL modes, MIN/MAX/AVERAGE), listing saved measurements and sessions, downloading trend recordings, logging to CSV/JSONL with auto-reconnect, and pressing the meter's front-panel keys.
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@open289-mcpread the live measurement from my Fluke 289"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
open289
English
An open Python package, command-line interface and MCP server for the Fluke 287/289 multimeter, read over a Fluke IR3000 FC infrared-to-Bluetooth-LE adapter, written so that an AI agent (Claude Code, or any MCP client) can read, log and download real measurements by itself, without Fluke Connect. It reads the live value (about 4 readings/s), the full display state (function, range, HOLD / MIN MAX / REL modes, MIN / MAX / AVERAGE), everything saved in the meter (measurements, MIN MAX and peak sessions, trend recordings), logs to CSV or JSONL for hours with automatic reconnect, downloads trend recordings, and can press the meter's front-panel keys.
The meter is reached through the adapter's pass-through to the meter's own serial command set (ID, QM, QDDB,
QSRR, ...); the protocol was worked out on real hardware, see docs/PROTOCOL.md.
Status and tested hardware
Version 0.1.0 (beta). What was verified on real hardware on 2026-11-06: an IR3000 FC with radio firmware 01.00.01 and MSP firmware 01.00.15, a Fluke 289 with firmware V1.71, a Windows 11 PC with Intel Bluetooth (Python and bleak over WinRT).
Only a 289 was tested. The 287 speaks the same protocol and should work; that is untested.
Linux and macOS: bleak supports them and the package has no Windows-only code in the Bluetooth path, but real hardware was never tried there. CI runs the whole test suite on Linux and Windows against the virtual meter only (see Virtual meter).
Measured speeds: live readings arrive about 4 times per second; one pass-through command takes about 1.3 s (QDDB display snapshots ran about 1.2 to 1.8 s apart); a trend recording downloads at 0.44 s per record (up to 8 records per round-trip, as many as fit in 87 bytes).
Related MCP server: ScopeProbe
Install
Requirements: Python 3.10+, a Bluetooth LE radio, a Fluke 287/289 and a Fluke IR3000 FC.
pip install open289This installs the commands open289 and open289-mcp (python -m open289 ... is the same as open289 ...) and the
single dependency bleak.
From a clone, with uv or a plain venv:
uv venv --python 3.12 .venv
uv pip install --python .venv\Scripts\python.exe -e .python -m venv .venv
.venv\Scripts\pip install -e ".[test]" # Linux / macOS: .venv/bin/pipWindows notes:
Switch Bluetooth on (Settings, Bluetooth and devices). No pairing or bonding is needed.
The console may use a legacy code page (GBK, CP1252); measurement strings contain
µ,Ωand°. The CLI reconfigures its own stdout and stderr to UTF-8, and the launchers in a clone start Python with-X utf8for everything else:open289.cmd live -n 10(Windows) and./open289.sh live -n 10(POSIX shell, Git Bash). With a pip install,python -X utf8 -m open289 ...does the same.If pip's
Scriptsfolder is not onPATH, callpython -m open289or give the full path toopen289.exe.
Hardware setup
Clip the IR3000 FC onto the meter's infrared port on the back of the 287/289 (the meter must be switched on).
Wake the adapter: press its button for more than 1 s until the LED flashes green. It advertises over Bluetooth only while it is awake and not connected.
Close Fluke Connect on any phone or tablet. The adapter accepts exactly one Bluetooth connection. While another program holds it,
open289reportsNOT_FOUND(the adapter does not advertise) orBUSY. open289 itself takes a lock, so two open289 processes never fight over the adapter: the second getsBUSY.Check:
open289 scanlists the adapter;open289 infoshows the meter.inforeports"meter": {"attached": false}when the adapter is awake but not on a powered meter.
Adapter and meter both go to sleep or off by themselves and cannot be switched on remotely; wake the adapter with its button and the meter with its dial.
Quick start
open289 scan # find the adapter
open289 info # adapter and meter identity, batteries, clock, saved counts
open289 read # the whole display now: function, range, modes, every reading
open289 live -n 20 # 20 real-time readings, one JSON line each
open289 log --duration 10m --out volts.csv # log to a file; ends by itself, or: open289 stop / Ctrl+C
open289 stop # end a running live / read --every / log cleanlyEvery command prints JSON (one object per line) unless --text is given; add --sim to try everything without
hardware. Output of the virtual meter (never invented; hardware values differ, the fields are the same):
$ open289 --sim scan
{"ok":true,"cmd":"scan","devices":[{"address":"FA:28:90:00:00:01","name":"IR 3000 FC","rssi":-60}]}
$ open289 --sim info
{"ok":true,"cmd":"info","adapter":{"address":"FA:28:90:00:00:01","name":"IR 3000 FC","radio_fw":"01.00.01","msp_fw":"01.00.15","battery_pct":100},"meter":{"attached":true,"model":"FLUKE 289","firmware":"V1.71","serial":"12345678","battery":"FULL","memory_level":"OK","clock_epoch":1794013461,"clock":"2026-11-07 01:04:21.000"},"saved":{"recordings":1,"minmax":0,"peak":1,"measurements":2}}
$ open289 --sim --text info
adapter : FA:28:90:00:00:01 radio fw 01.00.01 MSP fw 01.00.15 battery 100%
meter : FLUKE 289 firmware V1.71 serial 12345678
battery : FULL memory: OK clock: 2026-11-07 01:04:31.000 (meter local time)
saved : {'recordings': 1, 'minmax': 0, 'peak': 1, 'measurements': 2}info.meter.clock is the meter's own clock (local time, as set on the meter), not the PC's.
Real-time acquisition
Command | Source | Rate | Carries |
| the adapter's BLE notifications | ~4/s, one JSON line per notification | the primary reading and, when the meter shows one, the secondary reading |
| one | one round-trip, ~1.3 s | the whole display: function, secondary function, range, autorange, modes, every reading |
| the same, repeated | about one per interval (at least ~1.3 s) | one snapshot per line; ends with a |
Use live for fast values and logging, read when you need to know what the meter is doing (HOLD? MIN MAX? REL?
which range?), and read --every when you need modes or MIN / MAX / AVERAGE over time. live and read can be
combined by log --snapshot-every (next section) on one connection.
$ open289 --sim live -n 3
{"type":"reading","t":1794027861.982,"value":5.0625,"unit":"V DC","display":"5.0625 V DC","state":"NORMAL","function":"VDC"}
{"type":"reading","t":1794027862.227,"value":5.0625,"unit":"V DC","display":"5.0625 V DC","state":"NORMAL","function":"VDC"}
{"type":"reading","t":1794027862.481,"value":5.0625,"unit":"V DC","display":"5.0625 V DC","state":"NORMAL","function":"VDC"}
{"ok":true,"cmd":"live","event":"done","readings":3,"duration_s":0.748,"rate_hz":4.01,"stopped_by":"count"}A live reading line
Field | Meaning |
|
|
| PC time of the notification, Unix seconds (3 decimals) |
| the number in SI base units (V, A, ohm, F, Hz, s, S; degrees C / F and percent as they are): 1.5 mV is |
| the base unit (without the SI prefix; |
| the text the meter shows, with the SI prefix and decimals of the display ( |
|
|
| the live function name ( |
| present when the function has one: positive edge for Hz, continuity open / short / glitch, a good diode |
|
|
| present when the meter shows a secondary value: an object with the same |
| with |
The primary and secondary readings are what the meter's main and secondary display show. With Hz on a voltage function the Hz value becomes primary and the volts the secondary, as on the meter:
$ open289 --sim live -n 1 # V AC with the Hz secondary function
{"type":"reading","t":1794027906.375,"value":60.002,"unit":"Hz","display":"60.002 Hz [positive edge]","state":"NORMAL","function":"VAC","attribute":"positive edge","secondary":{"value":null,"unit":"V AC","display":"OL V AC","state":"OL_RANGE","function":"VAC"}}What live does not carry: no mode flags (HOLD, MIN MAX, REL, RECORD), no MIN / MAX / AVERAGE values, no range,
no function name in the display-snapshot vocabulary. In HOLD the primary is the held value, in REL the relative value.
Use read (or log --snapshot-every) for those. A live run ends at -n, --for or open289 stop and
prints a done line (readings, duration_s, rate_hz, stopped_by: count, duration, stop); Ctrl+C ends it without a summary (exit 130). --timeout
(default 10 s) turns a silent link into an error instead of waiting forever.
A display snapshot (read)
$ open289 --sim read
{"ok":true,"cmd":"read","t":1794027861.456,"function":"V_DC","secondary_function":"NONE","autorange":true,"range":5.0,"unit":"VDC","modes":["HOLD"],"readings":[{"id":"LIVE","value":5.0613,"unit":"VDC","display":"5.0613 V DC","state":"NORMAL","attribute":"NONE","decimals":4,"time":"2026-11-07 01:04:21.456","ts":1794013461.4560547},{"id":"PRIMARY","value":5.0625,"unit":"VDC","display":"5.0625 V DC","state":"NORMAL","attribute":"NONE","decimals":4,"time":"2026-11-07 01:03:58.325","ts":1794013438.3251953}],"hazard":false,"primary":{"id":"PRIMARY",...}}Field | Meaning |
| PC time of the query, Unix seconds |
| the primary function: |
|
|
| autorange on / off and the range's full-scale value (in base units; |
| the primary unit code ( |
| active modes, e.g. |
| every number on the screen, in display order (below) |
| a copy of the |
|
|
| when MIN MAX was started, local meter time (only when MIN MAX is active) |
Each readings entry has: id (LIVE, PRIMARY, SECONDARY, MINIMUM, MAXIMUM, AVERAGE, REL_REFERENCE,
TEMP_OFFSET, ...), value (SI base units, null for OL), unit, display, state, attribute, decimals,
time (meter local time) and ts (the same meter clock value as a number: local wall-clock time encoded as epoch seconds, no timezone; not comparable with t). LIVE is the instantaneous input; PRIMARY is
the main reading, which differs from LIVE in HOLD or REL. With MIN MAX active the snapshot also has MINIMUM,
MAXIMUM and AVERAGE:
$ open289 --sim press MINMAX ; open289 --sim --text read
00:05:09.566 V_DC range AUTO 5 modes MIN_MAX_AVG
LIVE 5.0626 V DC 2026-11-07 01:05:09.567
PRIMARY 5.0626 V DC 2026-11-07 01:05:09.567
MINIMUM 5.0612 V DC 2026-11-07 01:05:09.099
MAXIMUM 5.0631 V DC 2026-11-07 01:05:07.500
AVERAGE 5.0621 V DC 2026-11-07 01:05:09.567read --every 1s [-n N] [--for 5m] prints one snapshot per line (ok, cmd, t, ...), an {"ok":false,...} line when
one query fails (for example METER_BUSY) without ending the run, and a final {"event":"done","snapshots":N, "stopped_by":...}.
Logging
log writes live readings to a file, in the foreground, until --duration, open289 stop or Ctrl+C. It never
stops because of a lost link as long as --reconnect-for allows retries.
open289 log --duration 10m --out volts.csv
open289 log --duration forever --out run.jsonl --snapshot-every 10s # until: open289 stop
open289 log --every 1s --duration 8h --out night.csv # one reading per secondOption | Default | Meaning |
|
| output file ( |
| from the | file format |
|
| logging time, counted from the first connection: |
|
| one reading per interval (the most recent reading at each tick); |
|
| also record the full display (modes, MIN / MAX / AVG) at this interval, e.g. |
|
| status line on stdout; |
|
| keep retrying this long after the link is lost; |
CSV columns: pc_time (Unix seconds), local_time (PC local time with milliseconds), value, unit, display,
state, function, attribute, secondary_value, secondary_unit, secondary_display:
pc_time,local_time,value,unit,display,state,function,attribute,secondary_value,secondary_unit,secondary_display
1794027865.921,2026-11-07 00:04:25.921,5.062,V DC,5.0620 V DC,NORMAL,VDC,,,,
1794027866.168,2026-11-07 00:04:26.167,5.062,V DC,5.0620 V DC,NORMAL,VDC,,,,value is in SI base units and empty for an overload. With --snapshot-every, a CSV log puts the snapshots in a
sidecar file <file>.display.jsonl, one JSON object per line: t (Unix seconds), ack, hex (the raw QDDB
answer) and display (the same object as a read snapshot). A failed snapshot query is written as
{"t":..., "ack":"ERROR", "error":{...}} and the log goes on.
JSONL has one object per line, with a type:
| Content |
| a reading: the fields of a |
| a display snapshot ( |
|
|
{"t": 1794027869.307, "value": 5.0612, "unit": "V DC", "display": "5.0612 V DC", "state": "NORMAL", "function": "VDC", "type": "ble", "hex": "b4c50008020c00000000000000000000"}Decimation. --every 1s writes one row per second: the latest reading at that moment. The statistics in the
summary then cover only the written rows. Use --every 0 when you need every notification (or to see
transients), --every for long runs.
Status lines on stdout while it runs (a started line first, then connected once the adapter is found and
armed, then a status line every --status-every):
{"ok":true,"cmd":"log","event":"started","file":"...x.csv","format":"csv","duration_s":3.0,"every_s":0.0,"snapshot_every_s":1.0}
{"ok":true,"cmd":"log","event":"connected","connect_s":2.41,"model":"FLUKE 289","adapter":"FA:28:90:00:00:01"}
{"ok":true,"cmd":"log","event":"status","elapsed_s":3.5,"records":4,"last":"5.0627 V DC"}Connecting takes time. Every command first has to see the adapter advertise. The IR3000 FC advertises only every
few seconds, and right after a previous connection ends it can take about 20 s before it advertises again (measured on
hardware). --duration therefore counts from the first connection (connect_s says how long finding the adapter
took), so a --duration 30s log really holds about 30 s of readings.
Reconnect and gaps. When the Bluetooth link drops (meter off, adapter asleep, out of range, "no live reading for 10
s") the log prints link_lost (with retry_for_s), writes a link_lost event into a JSONL file, retries every 3 s,
and on success prints reconnected (with gap_s) and carries on appending to the same file. If the link does not
come back within --reconnect-for, the log ends with stopped_by: "link_lost" and the data written so far is kept.
It fails with LINK_LOST only when not one reading was logged; a log that never connected at all fails at once with
the real error (NOT_FOUND, DETACHED, ...).
Stopping. open289 stop (from another terminal or process) ends it within about a second; so do Ctrl+C, the
duration, and an unrecoverable link loss. The file is flushed after every row, so a killed process loses at most the
last row.
Summary (the last line, "event":"done"):
{"ok":true,"cmd":"log","event":"done","file":"...x.csv","format":"csv","records":12,"packets":12,"snapshots":2,"started":1794027863.2,"ended":1794027868.7,"duration_s":5.535,"connect_s":2.41,"logging_s":3.125,"stopped_by":"duration","gaps":[],"stats":{"V DC":{"n":12,"min":5.0614,"max":5.0629,"mean":5.0622}},"meter":{"model":"FLUKE 289","adapter":"FA:28:90:00:00:01"},"display_file":"...x.csv.display.jsonl"}records rows written, packets notifications received, snapshots display snapshots taken, duration_s the
whole command, connect_s the time until the first connection, logging_s the time from then on, stopped_by
(duration, stop, link_lost, interrupted), gaps (a list of {from, to, reason}, Unix seconds), stats
(n, min, max, mean of the primary value per unit; overloads are left out) and display_file (CSV with
snapshots only). --text prints a two-line version with the same numbers.
Saved data
open289 memory # everything saved in the meter (--text --detail lists every reading)
open289 recording 0 --out rec0.csv # download trend recording 0
open289 recording 0 --out rec0.csv --start 40 # resume an interrupted download (appends)memory reads the counts and then every saved item, and prints one JSON object: counts (measurements, minmax,
peak, recordings) and items. Each item has kind (measurement, minmax, peak, recording), index (the
index within its kind: this is what recording INDEX takes), name, seq, the same fields as a display snapshot
(function, modes, readings, primary, ...), start / end for sessions, and for recordings interval_s,
event_threshold, records, reading_index and duration_s. If some items cannot be read the result has
"ok":false and an errors list, and the exit code is 6 (the readable items are still listed). Every saved item is
one query, so memory has a 600 s limit instead of 120 s.
$ open289 --sim --text memory
saved items: {'recordings': 1, 'minmax': 0, 'peak': 1, 'measurements': 2}
measurement #0 'Save 1' 2026-09-29 03:15:52.785 TEMPERATURE 34.7 °C
measurement #1 'Save 2' 2026-11-01 01:56:42.182 CONTINUITY OL Ω [OPEN_CIRCUIT]
peak #0 'Save 3' 2026-11-06 21:29:01.471 V_DC 0.005 V DC
recording #0 'Save 4' 2026-11-06 21:40:19.248 V_DC 1.9874 V DC 72 records, interval 1 s, 1.0 minRecording download. recording INDEX fetches the trend records of one recording (the meter's RECORD mode) and
writes one row per record. It batches up to 8 queries per round-trip and takes 0.44 s per record on the hardware
(--no-batch: one per round-trip, about 1.4 s). A record that fails is retried (up to 3 more times). If the meter
shows a menu, the download stops with METER_BUSY and a hint with the record to resume from; so does a failing
record (PROTOCOL_ERROR). Resume with the same --out and --start K: the file is appended to, without a second
header. --limit K stops before record K (an end index, not a count). Events: started, progress every 10 s
(done, of), and done (records, duration_s, s_per_record, retries, file).
CSV columns (JSONL has the same keys): index, start, end (meter local time), unit, primary,
primary_state, minimum, minimum_state, average, average_state, maximum, maximum_state, samples,
record_type (INTERVAL, ...), stable, transient. Values are in SI base units; an _state of OL means the
value is empty.
index,start,end,unit,primary,primary_state,minimum,minimum_state,average,average_state,maximum,maximum_state,samples,record_type,stable,transient
0,2026-11-06 21:40:19.248,2026-11-06 21:40:19.752,VDC,1.4904,NORMAL,1.4903,NORMAL,1.49032,NORMAL,1.4904,NORMAL,5,INTERVAL,STABLE,NON_TFront-panel keys
press KEY [KEY ...] simulates the keys in order and returns the display state after each one. Keys: F1 F2 F3 F4 UP DOWN LEFT RIGHT HOLD MINMAX RANGE INFO BACKLIGHT ONOFF.
open289 press HOLD # toggle HOLD
open289 press RANGE # step the range (manual)
open289 press RANGE --held # back to autorange
open289 press MINMAX # start MIN MAX
open289 press MINMAX F4 # stop it (opens the result page), then close the page
open289 press ONOFF F1 --allow-off # switch the meter off for goodOptions: --held sends a long press (HELD_DOWN) for every key; --settle S (default 1.0) waits that long before the
state is read back; --allow-off permits ONOFF. Each entry of the result has key, held, ack (OK) and state
(a display snapshot; or a text when the meter shows a page, see below). press stops at the first key the meter does
not acknowledge. press is the only command that changes the meter's state (apart from raw --unsafe).
$ open289 --sim press HOLD
{"ok":true,"cmd":"press","presses":[{"key":"HOLD","held":false,"ack":"OK","state":{"function":"V_DC","secondary_function":"NONE","autorange":true,"range":5.0,"unit":"VDC","modes":[],...}}]}What was observed on the hardware:
HELD_DOWNis a long press.RANGEheld = back to autorange. HeldMINMAXbehaves like a press.MIN MAX has pages. The first
MINMAXstarts MIN MAX (modeshasMIN_MAX_AVG). The second stops it and opens the result page (F1 restart, F2 save,MINMAXor F4 close). F2 on the result page opens the save page (F1 adds to the name, F2 saves, F4 closes). Closing a page returns to the measurement screen.Why queries return
METER_BUSYthere. While a menu, result page, save page or setup screen is open, the meter answers every measurement query (QDDB,QSLS, ...) with an execution error. open289 names itMETER_BUSY(exit 5).pressthen shows"state": "menu / result page open (measurement queries blocked)". Close the page with the keys (F4) or on the meter, then retry.BACKLIGHTcycles off, bright, dim, also on the pages.ONOFFis refused (UNSAFE_OPERATION, exit 7) unless--allow-off. With it, the meter asks for confirmation (turning off meter will break communication):F1confirms,F4cancels. After F1 the meter is off, the adapter switches itself off a moment later, and neither can be switched on remotely. This cannot be undone from the PC.
Raw commands
raw COMMAND [COMMAND ...] sends meter commands through the pass-through and returns each answer (ack, and text
or, for binary replies, binary_hex). Query-only by default: ID, IM and Q* commands (QM, QDDB, QDDA,
QBL, QSLS, QMP clock, ...) pass; anything else (MP, CSD = clear memory, RI = reset, OFF, PRESS, ...)
is refused with UNSAFE_OPERATION before it is sent. --unsafe lifts the filter; use it only when you know exactly
what the command does, because some commands erase saved data or switch the meter off.
$ open289 --sim raw ID QM
{"ok":true,"cmd":"raw","results":[{"command":"ID","ack":"OK","text":"FLUKE 289,V1.71,12345678"},{"command":"QM","ack":"OK","text":"5.0628E0,VDC,NORMAL,NONE"}]}
$ open289 --sim raw MP
{"ok":false,"cmd":"raw","results":[{"command":"MP","error":{"code":"UNSAFE_OPERATION","message":"meter command b'MP' is not a read-only query","hint":"only ID, IM and Q* queries pass by default; add --unsafe for other commands"}}]}Each command is sent and answered in turn; a refused or failed one is reported in its own result and the exit code is that of the first error (7 for a refusal). A command over 87 bytes is refused before it is sent (see Safety).
Self-test
open289 selftest is a read-only health check of the adapter, the link and the meter (it never changes the meter):
Check | What |
S01 | adapter advertising |
S02 | connect and arm the adapter control point |
S03 | meter attached ( |
S04 | adapter firmware answers |
S05 | pass-through |
S06 | display snapshot ( |
S07 | a live reading arrives within 5 s |
S08 | adapter battery >= 20 % |
S09 | meter battery not EMPTY / ALMOST_EMPTY |
The result lists every check with pass, measured and expected, plus passed and failed counts; the exit code
is 1 when a check failed. A failed S01, S02 or S03 ends the run early (the later checks need that link).
$ open289 --sim selftest
{"ok":true,"cmd":"selftest","passed":9,"failed":0,"items":[{"id":"S01","name":"adapter advertising","pass":true,"measured":"FA:28:90:00:00:01","expected":null},...]}Virtual meter
The package contains a model of an IR3000 FC with a Fluke 289 attached (open289/sim.py), so the whole tool, the MCP
server and the test suite run with no Bluetooth and no hardware. Select it with --sim or OPEN289_SIM=1.
It simulates what was measured on the real hardware: the GATT table and the CNX ID write that arms the adapter, the
18-byte control-point write limit, the pass-through framing, the out-buffer overflow (no reply above 128 bytes, a modelled guess; 181 measured) and the adapter hang at 373 bytes and more (open289 itself refuses anything over 87 bytes) when it
is overrun, the binary QDDB / QSMR / QMMSI / QPSI / QRSI / QSRR layouts, the 16-byte live notifications at 4/s,
the single connection (a second process is refused and the adapter stops advertising), the front-panel keys with
their pages (MIN MAX result and save pages, power-off confirmation, backlight cycle), a meter that is switched off and
an adapter that goes to sleep, saved memory with two measurements, a peak session and a 72-record recording. Behaviour
that was never observed on hardware is marked GUESS in the code. Identity: address FA:28:90:00:00:01, serial
12345678. It is a stand-in for tests and demos, not a measurement instrument: the "readings" are synthetic.
The state of the virtual device is one JSON file shared by every process (state/sim_state.json in the
data folder), so open289 press HOLD in one command is seen by the next.
python -m open289.sim show # the state as JSON
python -m open289.sim reset # factory state
python -m open289.sim power-cycle # switch adapter and meter on again (clears an adapter hang)
python -m open289.sim set function=V_AC input=open meter_on=false
python -m open289.sim audit [--clear] # every GATT write the tool made (state/sim_writes.jsonl)set keys: function (V_DC V_AC MV_DC MV_AC OHMS CONTINUITY DIODE_TEST TEMPERATURE MA_DC, and V_AC_HZ / V_DC_HZ for
the Hz secondary function), secondary (NONE / HERTZ), input (the input of the current function; open for
OL), inputs.<FUNCTION>, hz, meter_on, adapter_on, adapter_hung, autorange, range_index, screen
(normal, minmax_result, save_page, power_off_confirm), backlight, modes, seed, clock_offset.
Environment: OPEN289_SIM_RATE (live notifications per second, default 4), OPEN289_SIM_DELAY (latency of a
pass-through send, default 0.05 s; the hardware takes about 0.6 s), OPEN289_SIM_STATE, OPEN289_SIM_AUDIT
(file overrides). OPEN289_NO_BLE=1 makes the real Bluetooth backend refuse to start (tests set it, so a test can
never reach hardware).
CLI reference
Global options go before the command:
Option | Meaning |
| adapter BLE address (default: the first IR3000 FC found) |
| use the virtual IR3000 + Fluke 289 (no hardware) |
| human-readable output instead of JSON |
| log adapter control-point traffic to stderr |
| time limit in seconds for one-shot commands (default 120; |
| print the version |
open289 <command> --help lists the options of one command.
Command | Purpose | Options (defaults) |
| find IR3000 FC adapters |
|
| adapter and meter identity, battery, memory level, clock, saved counts | |
| full display state now (function, range, modes, all readings) |
|
| real-time readings from BLE notifications (~4/s), one JSON line each |
|
| log readings to a CSV / JSONL file |
|
| saved measurements, MIN MAX, peak sessions and recordings |
|
| download trend recording INDEX to CSV / JSONL |
|
| simulate front-panel keys |
|
| raw meter commands ( |
|
| read-only health check of adapter, link and meter | |
| end a running |
Durations (--every, --for, --duration, --snapshot-every, --status-every, --reconnect-for) take ms, s,
m (or min), h, d; a bare number is seconds: 500ms, 30, 90s, 5m, 1.5h, 2d. forever (also inf,
until-stop) means no limit (for --duration).
stop writes a request file that a running live, read --every or log (in any process) sees within about a
second; it does not need the adapter. recording and the one-shot commands do not react to it.
Output format
Every command prints JSON on stdout, one object per line, compact. One-shot commands print one object. live, read --every, log and recording print event lines and end with an "event": "done" summary. Event lines carry
"ok": true, "cmd": ..., "event": ... (started, connected, status, progress, link_lost, reconnected, done); live
readings are {"type":"reading", ...}. The first field of a result is ok. Warnings and --verbose traffic go to
stderr. Values are in SI base units; display is the text from the meter. Times: t, pc_time, started, ended are Unix seconds on the PC; time, start, end, info.meter.clock, and the numeric ts / clock_epoch, are the meter's local clock (no timezone).
Errors are one object and a non-zero exit code:
{"ok":false,"cmd":"read","error":{"code":"METER_BUSY","message":"'QDDB' -> EXECUTION_ERROR","hint":"the meter is on a menu, result page or setup screen: close it on the meter (or with open289 press), then retry"}}--text prints a human-readable form of the same data: one line per reading for live (HH:MM:SS.mmm value unit
and the secondary after |), a short table for read, memory, press and selftest, one line per record for recording, and a short summary for
log. Errors in --text are error CODE: message plus a hint: line. Scripts and agents should use
the JSON.
Errors and exit codes
Every error has a code, a message and (mostly) a hint; the exit code follows the code.
Code | Exit | Meaning and typical fix |
| 2 | an argument cannot be parsed or is out of range (duration, key, index): fix the command |
| 3 | no IR3000 FC is advertising: wake the adapter (button until the LED flashes green), come closer, close Fluke Connect on phones |
| 4 | another open289 process holds the adapter (wait, or |
| 5 | the meter answers EXECUTION_ERROR: a menu, result page, save page or setup screen is open; close it on the meter or with |
| 5 | the adapter reports no powered meter (per the Fluke app it then reports |
| 5 | the meter has no data for the request, e.g. no saved item with that index: check |
| 6 | a one-shot command passed |
| 6 | scan or connect failed: is Bluetooth switched on? (also: |
| 6 | the Bluetooth link dropped (a |
| 6 | an unexpected or undecodable answer; a recording download names the record to resume from |
| 6 | the meter did not accept the command (syntax error) or did not answer |
| 6 | reserved (not emitted yet; a failed Bluetooth write currently surfaces as |
| 7 | refused by the safety rules: a non-query |
| 0 | reserved; a stopped run ends normally with |
| 1 | unexpected exception (the result carries the last trace lines); anything else maps to exit 1 |
Exit 0 is success; Ctrl+C gives 130. A failed selftest (a check failed, it is not an error object) and a memory
where some items could not be read exit 1 and 6 respectively. The MCP server uses the same codes and adds TIMEOUT
for tool calls past max_wall, CANCELLED and SELFTEST_FAILED.
MCP server
open289-mcp (or python -m open289.mcp_server) serves the meter over the Model Context Protocol (stdio, standard
library only; installed with pip install open289):
claude mcp add open289 -- open289-mcpIf pip's Scripts folder is not on PATH, give the full path to open289-mcp. Other clients:
{"mcpServers": {"open289": {"command": "open289-mcp"}}}. Each tool call runs one CLI command in a child process,
so the adapter is connected and released per call, a hang never blocks the server, and every rule of the CLI applies
(safety filter, adapter lock, error codes). The server's own instructions tell the agent to start with status.
11 tools:
Tool | Runs | Key arguments (defaults) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| none (reads the job's output) |
|
|
|
|
status, read, live, memory, query, selftest, log_status are marked read-only. All durations are strings
("30s"), all numbers are SI.
Background logging. log_start returns at once (after the first reading, at most 30 s) with a job id; the log
keeps running when the client disconnects, and its output and metadata are kept in state/mcp_jobs/<job>/, so a
restarted server, or another client's server, still sees it. Follow it with log_status, end it with log_stop
(or it ends after duration, or when the link stays lost longer than reconnect_for). The file holds the data;
the tool results are summaries and never hundreds of readings.
BUSY rules. The adapter accepts one connection, so tools run one at a time, and while a background log runs,
every other meter tool (status, read, live, memory, press, query, ...) returns BUSY until log_stop
(log_status and log_stop still work). BUSY also means another open289 (a terminal running live/log, another client) holds the adapter. Fluke Connect on a phone shows up as NOT_FOUND (the adapter stops advertising). METER_BUSY is different: a menu or result page is open on the meter; close it
(press F4) and retry. Safety for agents: press changes the meter (HOLD, MIN MAX, range); ONOFF needs
allow_off: true and should be used only when the user asked for it: it switches the meter off for good, and the
adapter goes to sleep with it.
CLI commands with no MCP tool: scan (the server finds the adapter itself), stop (used internally by log_stop),
and raw --unsafe (deliberately not exposed). An agent that has a shell can use the CLI for all of them.
Data folders
open289 keeps its files in one folder with two subfolders:
state/: the adapter lock (open289.lock), the stop request (stop.request), MCP background jobs (mcp_jobs/) and the virtual meter's state (sim_state.json,sim_writes.jsonl).data/: the default place of logs (log_<time>.csv) and recording downloads (recording<INDEX>_<time>.csv).--outwrites anywhere else; relative paths are relative to the current folder.
Where the folder is: OPEN289_HOME if set; else, in a source checkout (a pyproject.toml next to the package), the
checkout itself; else, for an installed package, %LOCALAPPDATA%\open289 on Windows and ~/.local/share/open289
elsewhere, so nothing is written into site-packages. The MCP server's child processes use the same folder.
Development
Layout:
open289/package:cli.py(commands, output),transport.py(backend selection real or virtual, discovery, connection),adapter.py(IR3000 service client, pass-through, the opcode allowlist, query filter and the 87-byte limit),meter.py(the meter's remote commands and the binary record decoders),reading.py(live notification decoder),records.py(JSON-ready dictionaries),logger.py(long-running log with reconnect),lock.py(single client lock),errors.py,paths.py,sim.py(virtual meter) andmcp_server.py(MCP server).tests/offline tests against the virtual meter: decoders (against real captures), the transport and its safety rules, the CLI, the MCP server (JSON-RPC, jobs, BUSY rules) and the simulator itself.docs/PROTOCOL.md(andPROTOCOL.zh-CN.md): the protocol, with evidence levels.tools/diagnostic scripts for the hardware..github/workflows/:ci.ymlandrelease.yml.
python -m pip install -e ".[test]"
OPEN289_SIM=1 OPEN289_NO_BLE=1 python -m pytest tests -m "not hw" -qThe tests set the simulator environment themselves (tests/conftest.py: a private OPEN289_HOME per test,
OPEN289_SIM=1, OPEN289_NO_BLE=1, a fast simulator), so a test can never reach Bluetooth. Tests marked hw need a
real adapter and meter and are skipped by default (pytest -m hw).
CI (ci.yml, on pushes to main, pull requests and by hand): the tests on Windows and Linux with Python 3.10, 3.12 and
3.14, all against the virtual meter; a check that tracked files carry no local paths; a package build with twine check --strict and a smoke test of the built wheel (open289 info, open289 live -n 5 on the virtual meter).
Release (release.yml): push a tag vX.Y.Z equal to open289.__version__ (v0.1.0 for 0.1.0). The workflow runs the
CI checks, refuses a tag that does not match the version, builds, creates a GitHub release with the files, and
publishes to PyPI by trusted publishing (no token). One-time setup on pypi.org: add a (pending) trusted
publisher for the project open289 with owner FrostFlops, repository open289, workflow release.yml and
environment pypi, and create the environment pypi in the repository's settings.
Protocol
How the adapter and the meter talk (GATT table, the CNX ID write that arms the control point, the pass-through framing, buffer limits, the 16-byte live packet layout, the binary saved-data records, key behaviour), with every statement tagged VERIFIED (seen on the hardware) or DERIVED (not exercised): docs/PROTOCOL.md (中文). Observed live encodings per function: section 4.8 of docs/PROTOCOL.md.
What is NOT here
Fluke's application (Fluke Connect), anything decompiled from it, and the private reverse-engineering workspace are kept out of this repository. The firmware images and updater of the adapter are not used.
Safety
The tool never writes the adapter's firmware-update characteristics (the MSP updater and the radio-image updater; writing the wrong value there erases flash) and never sends the adapter opcodes
0x90to0x94(the 1550 / 1555 insulation-test commands, which switch a high voltage).The adapter's out-buffer is capped at 87 bytes per send: larger sends got no reply or hung the adapter until it was power-cycled (373 bytes and more), so open289 refuses them.
rawis query-only (ID,IM,Q*) unless--unsafe.MPcan change settings,CSDerases saved data,RIresets the meter,OFFswitches it off.press ONOFFneeds--allow-off, and the meter asks for confirmation (F1). OFF and ONOFF cannot be undone remotely: neither the meter nor the adapter can be switched on again from the PC.pressotherwise changes meter state (HOLD, MIN MAX, range, saving a measurement on the save page): it is not read-only, and an agent should only use it on request.The adapter accepts one Bluetooth connection: close Fluke Connect on phones. open289 holds a lock so that only one open289 process connects at a time.
This is a measurement reader, not a safety device. Respect the meter's category rating and its own safety rules; open289 does not make a measurement safer.
License
GPL-3.0-only (see LICENSE). Fluke, Fluke Connect and IR3000 are trademarks of Fluke Corporation; this project is
not affiliated with Fluke Corporation.
Available Tools
11 toolsdownload_recordingDownload a trend recordingA
Download trend recording index (see memory) to a CSV or JSONL file, one row per record (start/end time, primary, minimum, average, maximum, unit, states). Long recordings take minutes over Bluetooth. Returns the file, the number of records, seconds per record, retries and the recording header. An interrupted download resumes with start and the same out (a failed result names both under resume).
| Name | Required | Description | Default |
|---|---|---|---|
| out | No | output file (default data/recording<index>_<time>.csv in the open289 folder) | |
| index | Yes | recording index (0 = the first) | |
| limit | No | stop before this record index (an end index, not a count) | |
| start | No | first record (resume an interrupted download into the same out file) | |
| format | No | file format (default: from the out extension, else csv) | |
| max_wall | No | time limit for the whole call in seconds (default 900); past it the command is stopped and TIMEOUT is returned |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
With no annotations, the description carries the full burden and does a good job: it warns that long recordings take minutes over Bluetooth, describes resume-after-interruption semantics, and enumerates the return payload (file, record count, seconds per record, retries, header). It omits file-overwrite behavior when 'out' already exists and does not discuss permissions or failure modes beyond timeouts.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
The core action and target format lead the description, followed by operational caveats and return shape — a sensible ordering. The parenthetical column list is slightly dense but earns its place by telling the agent what a row contains.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
For a six-parameter, file-writing tool with no output schema, the description covers the essentials: what is written, what is returned, latency expectations, and resume behavior. The main residual gaps are overwrite semantics for an existing 'out' path and interaction between `limit` and resume.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema description coverage is 100%, so every parameter is already documented, and the description's resume note largely restates what the `start` field says. It adds no semantics for `limit`, `max_wall`, or the default-path behavior of `out`, so the schema is doing the heavy lifting.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
States a specific verb and resource ('Download trend recording `index`') plus the output format (CSV/JSONL) and row shape. It also routes the agent to the sibling `memory` tool to obtain the index, so it is distinguishable from `read`, `query`, and `live` without opening a schema.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
Gives clear context: use this to pull a whole recording to a file, and use `memory` for indices. It also documents the resume path ('start' plus the same 'out'). It does not explicitly state when to prefer a sibling such as `read` or `query` for lighter-weight access, so it stops short of full when/when-not guidance.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
liveReal-time readingsARead-only
Collect real-time readings from the adapter (about 4 per second) for count readings or seconds, and return a summary instead of the readings: count, rate, first/last time, min/max/mean of the primary value per unit, the function(s) and unit(s) seen and the last 5 readings. Default: 20 readings. For longer runs or a file use log_start.
| Name | Required | Description | Default |
|---|---|---|---|
| count | No | stop after this many readings (1..400) | |
| seconds | No | stop after this long (0.1 to 120 s); with count: whichever comes first | |
| max_wall | No | time limit for the whole call in seconds (default: 120, or longer for a large count / seconds) |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations only declare readOnlyHint=true; the description adds meaningful behavior: the ~4 Hz sampling rate, that results are aggregated not streamed, the exact summary fields returned (count, rate, first/last time, min/max/mean, functions/units, last 5 readings), and the default window. It does not discuss adapter error behavior or what happens if the adapter is disconnected.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
The core action, sampling rate, stop conditions, and return shape are front-loaded in one dense sentence, with the routing hint to log_start placed last. The enumeration of summary fields is long but earns its place since there is no output schema.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
With no output schema, the description carries the return-value burden and does so thoroughly, listing every summary field. Combined with the sampling rate, entry conditions, defaults, and the log_start alternative, an agent has everything needed to call it correctly.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema coverage is 100%, so all three parameters are already documented, including the count/seconds 'whichever comes first' semantics. The description restates the default of 20 readings and the count-or-seconds choice but never mentions max_wall and adds no syntax or interaction detail beyond the schema, so baseline 3 applies.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
States a specific verb and resource ('Collect real-time readings from the adapter'), the sampling rate (~4/second), and crucially that it returns a summary rather than raw readings. The contrast with log_start makes it distinguishable from the closest sibling without opening either schema.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
Explicitly routes long-running or file-persisting cases to log_start, which is the key disambiguation for this tool, and gives the default of 20 readings. It does not differentiate from other siblings such as read, query, or status, so the guidance is clear but not exhaustive.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
log_startStart a background logA
Start logging live readings to a CSV or JSONL file in the background and return the job id and the file. The log ends after duration, at log_stop, or when the Bluetooth link stays lost longer than reconnect_for; it keeps running if this client disconnects. One log at a time, and meanwhile the other meter tools return BUSY. Follow it with log_status, end it with log_stop.
| Name | Required | Description | Default |
|---|---|---|---|
| out | No | output file (default data/log_<time>.csv in the open289 folder) | |
| every | No | write one reading per interval, e.g. '1s'; '0' = every reading (about 4/s) | 0 |
| format | No | file format (default: from the out extension, else csv) | |
| duration | No | e.g. '30s', '10m', '8h' or 'forever' (no limit: it ends at log_stop); must be longer than 0 | 10m |
| reconnect_for | No | keep retrying this long after the link is lost (default '5m'; '0' = end the log) | |
| snapshot_every | No | also save the full display (modes, MIN/MAX/AVG) at this interval, e.g. '10s' (a CSV log puts them in <file>.display.jsonl); '0' = off | 0 |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
With no annotations, the description carries the full behavioral burden and does so well: it discloses background execution, return values, termination triggers (duration, log_stop, prolonged Bluetooth loss), client-disconnect persistence, and concurrency BUSY behavior. These are exactly the operational traits an agent needs before invoking it.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Three sentences, front-loaded with purpose, then lifecycle, then constraints. Every sentence adds operational value; there is no repetition or filler.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
For a background logging tool with six optional parameters and no output schema, the description covers what is started, what is returned, how it ends, concurrency limits, and sibling follow-up tools. Nothing essential for correct invocation is missing.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema description coverage is 100%, so the schema already defines each parameter including defaults and examples; the description adds only high-level lifecycle context for duration and reconnect_for. It does not add syntax or format details beyond the schema, so baseline 3 applies.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description opens with a specific verb and resource ('Start logging live readings...') and names output formats, background execution, and return values (job id and file). It also distinguishes this tool from log_status and log_stop by defining their roles in the lifecycle.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
It gives clear lifecycle guidance: follow with log_status, end with log_stop, one log at a time, and other meter tools return BUSY while active. It also states termination conditions. It does not explicitly say when to use this instead of live/read for one-off readings, so it stops short of full alternative routing.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
log_statusBackground log statusARead-only
State of a background log (running / done / failed), the records written so far, the last status line, the file, and once it has ended its summary (min/max/mean per unit, link gaps).
| Name | Required | Description | Default |
|---|---|---|---|
| job | No | job id from log_start (default: the latest log) |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
readOnlyHint=true already covers the safety profile, so the description earns credit for adding genuinely new behavior: the summary fields (min/max/mean per unit, link gaps) only appear once the log has ended, which is non-obvious timing information an agent needs. It stops short of saying whether the call blocks, whether state transitions are push or poll, or what happens for an unknown or expired job id.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
One dense but well-ordered sentence that front-loads the core state before the trailing detail. No filler, though the long parenthetical list makes it slightly harder to scan than a two-sentence split would be.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
With no output schema, the description takes on the burden of describing return values and does so field by field, including the conditional post-completion summary. The main residual gap is that nothing describes the failed case beyond a label (error text, exit codes) or how to retrieve the referenced file.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
The single `job` parameter has full schema description coverage including its default ('the latest log'), so the schema carries the semantics. The description adds nothing about the parameter, and 100% coverage makes 3 the correct baseline.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
States a specific resource (a background log) and enumerates the exact facets returned: state, records written, last status line, file, and post-completion summary. An agent can tell this is a read-only progress/inspection call, but the description never names the sibling `status`, `live`, or `log_start` tools, so differentiation from `status` is left to inference.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
Usage is only implied: 'background log' plus the schema note 'job id from log_start' hints this is the follow-up to log_start for polling progress. There is no explicit statement of when to prefer this over `status` or `live`, nor whether polling repeatedly is expected.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
log_stopStop the background logA
Stop the background log cleanly and return its summary: file, records, duration, min/max/mean per unit, link gaps. Without job it also stops a log or live started elsewhere (a terminal, another client); already_ended true = the log had ended before this call (the summary is its old one).
| Name | Required | Description | Default |
|---|---|---|---|
| job | No | job id from log_start (default: the latest log) |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
With no annotations, the description carries the full burden and does well: it discloses a non-obvious cross-session side effect (stopping a log or live started in a terminal or another client when `job` is omitted) and explains the already_ended flag semantics, including that the summary is stale in that case. It does not cover failure modes, whether the log file is retained, or permission requirements, keeping it short of a 5.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Front-loaded with the action and return payload, then the conditional behavior and edge case. The parentheticals are dense but each clause carries information; the second sentence runs long and could be split for readability, but there is no filler.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
With no output schema and no annotations, the description supplies the return fields inline and covers the default-path behavior and the already-ended edge case, which is what an agent needs to call it correctly. Missing only error behavior when no log exists and whether data is preserved after stopping.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema coverage is already 100% for the single `job` parameter, so the baseline is 3. The description earns above baseline by explaining the consequence of omitting `job` — it will reach out and stop a log started elsewhere, not merely default to the latest one — which is meaning the schema does not convey.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
States a specific verb and resource ('Stop the background log') and immediately enumerates what comes back (file, records, duration, min/max/mean per unit, link gaps). This is clearly distinguishable from log_start, log_status, and live, which would start, inspect, or stream rather than terminate and summarize.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
The description makes the primary use case clear (terminate a running log and get its summary) and adds a useful conditional: without `job` it will also stop a log or live started elsewhere. It stops short of explicit routing guidance such as 'use log_status to inspect without stopping' or stating what happens when nothing is running, so there is context but no exclusions.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
memorySaved itemsARead-only
What is saved in the meter's memory: the counts and every saved measurement, MIN MAX session, peak session and trend recording (name, time, function, primary value and the other readings; recordings also interval, number of records and duration). download_recording fetches the records of a recording; the index is the item's index within its kind.
| Name | Required | Description | Default |
|---|---|---|---|
| max_wall | No | time limit for the whole call in seconds (default 600); past it the command is stopped and TIMEOUT is returned |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
The readOnlyHint annotation already establishes that this is a safe read operation. The description adds meaningful context beyond that: it lists the kinds of saved items, the fields each item exposes, and clarifies that recordings include interval, number of records, and duration. It does not describe pagination, error behavior, or response format, but it substantially improves behavioral understanding.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
The description is front-loaded and dense, with no filler sentences. The long parenthetical listing fields is somewhat hard to parse, but every clause conveys relevant structure. It is appropriately sized for the amount of return-shape information it needs to convey.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
There is no output schema, so the description carries the burden of explaining return values, and it does so thoroughly by enumerating item kinds and fields. It also explains the index relationship to download_recording. Minor gaps remain around pagination and error responses, but the definition is complete enough for an agent to understand what memory returns.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
The single parameter max_wall has complete schema description coverage, including its default and timeout behavior. The description does not mention max_wall or add any meaning beyond the schema. With 100% coverage, the schema carries parameter semantics adequately, so the baseline score of 3 is appropriate.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description clearly states that this tool reports what is saved in the meter's memory, enumerating counts, measurements, MIN MAX sessions, peak sessions, and trend recordings with their fields. It distinguishes the index from the record-fetching behavior of download_recording. The purpose is clear, though it reads more as a data inventory than a crisp verb-resource statement.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
The description implies usage by explaining that download_recording fetches the records of a recording, while this tool provides the index and item metadata. It does not explicitly state when to call memory versus status, read, or query, nor does it give exclusions or prerequisites. The guidance is implied rather than explicit.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
pressPress meter keysA
Simulate front-panel keys in order (soft keys F1-F4 under the display, the arrows, HOLD, MINMAX, RANGE, INFO, BACKLIGHT, ONOFF) and return the display state after each key. held: a long press (RANGE held = back to autorange). ONOFF switches the meter OFF for good (it cannot be switched on remotely, and the adapter goes to sleep with it): it is refused unless allow_off is true; the meter then asks for confirmation (F1 confirms, F4 cancels).
| Name | Required | Description | Default |
|---|---|---|---|
| held | No | long press (HELD_DOWN) for every key | |
| keys | Yes | keys, pressed in order | |
| max_wall | No | time limit for the whole call in seconds (default 120); past it the command is stopped and TIMEOUT is returned | |
| allow_off | No | permit ONOFF, which switches the meter off; only when the user asked for it |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
With no annotations, the description carries the full burden and does well: it discloses that display state is returned after each key, explains held behavior including RANGE autorange, and warns that ONOFF permanently powers off the meter, cannot be reversed remotely, puts the adapter to sleep, is refused unless allow_off is true, and requires F1/F4 confirmation.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
The description is a compact, front-loaded paragraph: main action first, then held semantics, then the ONOFF safety behavior. Every sentence adds useful information without repetition.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
For a mutation-style tool with no output schema and no annotations, the description covers the important return value, the sequential key behavior, long-press semantics, and the irreversible ONOFF flow. No critical invocation detail is missing.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema coverage is 100%, so baseline is 3, but the description adds meaningful semantics beyond the schema: held is a long press with special RANGE behavior, and allow_off controls the ONOFF safety gate and confirmation flow. max_wall is left to the schema, which is acceptable.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
States a specific verb (simulate) and resource (front-panel keys), lists the supported keys, and says it returns the display state after each key. The purpose is clear and distinct from sibling tools such as status, read, and live.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
The description gives important invocation constraints for held presses and ONOFF, but it does not explicitly say when to use this tool instead of reading state through siblings. Usage is implied by the purpose and special-key rules.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
queryRead-only meter queryARead-only
Send read-only remote commands to the meter and return each answer (text, or hex for binary replies): ID (model, firmware, serial), IM, and Q* queries such as QM (primary measurement), QDDB (display data), QBL (battery), QSLS (saved counts), QMP . Anything else (PRESS, MP, CSD, RI, OFF ...) is refused with UNSAFE_OPERATION before it reaches the meter.
| Name | Required | Description | Default |
|---|---|---|---|
| commands | Yes | e.g. ['ID', 'QM'] | |
| max_wall | No | time limit for the whole call in seconds (default 120); past it the command is stopped and TIMEOUT is returned |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already declare readOnlyHint=true, so the safety profile is covered structurally. The description does add real behavior beyond that: the UNSAFE_OPERATION refusal happens before the command reaches the meter, and binary replies come back as hex. It does not discuss per-command failure modes or what a partial multi-command failure looks like, so it adds useful but not complete context.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Front-loaded with the action and result, then the whitelist, then the refusal rule. Dense but nearly every clause carries information; the parenthetical command examples are slightly list-heavy but functional.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
There is no output schema, so the description correctly takes on the job of explaining responses (text, or hex for binary). Combined with the schema's timeout semantics, an agent has enough to call this correctly; only multi-command failure ordering is unaddressed.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema coverage is 100%, so the baseline is 3, but the description adds meaning the schema does not: it defines which command strings are legal and shows the QMP <property> sub-syntax, well past the schema's bare example ['ID', 'QM']. Timing semantics for max_wall are already in the schema and are not repeated unnecessarily.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
Specific verb plus resource: 'send read-only remote commands to the meter and return each answer'. It enumerates the accepted command vocabulary (ID, IM, Q*, QMP <property>), which lets an agent distinguish this from siblings like press or memory without opening any schema.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
Gives concrete when-to-use scope via the command whitelist and states the exclusion case: anything else (PRESS, MP, CSD, RI, OFF) is refused. It stops short of naming the specific sibling tool (press) an agent should call instead for those operations, leaving that routing to inference.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
readRead the displayARead-only
One snapshot of the meter display: function (V_DC, OHMS ...), secondary function, range and autorange, modes (HOLD, MIN_MAX_AVG, REL, RECORD ...), the primary reading and every other reading on screen (MINIMUM, MAXIMUM, AVERAGE, SECONDARY, REL_REFERENCE ...). value is in SI base units, display is the text the meter shows. METER_BUSY = a menu or result page is open.
| Name | Required | Description | Default |
|---|---|---|---|
| max_wall | No | time limit for the whole call in seconds (default 120); past it the command is stopped and TIMEOUT is returned |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already declare readOnlyHint=true, so the safety profile is covered. The description adds valuable behavior beyond annotations by explaining the returned display contents, the SI-base-unit meaning of value, the display text field, and the METER_BUSY error condition.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
The description is dense but front-loaded around the snapshot concept and then systematically lists returned components. The parenthetical enumerations are somewhat long, but every clause earns its place by clarifying return content or error behavior.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
With no output schema, the description carries the burden of explaining return values, and it does so well by naming the display fields, units, and METER_BUSY state. It is not fully complete on sibling selection or the max_wall parameter, but the core read behavior is sufficiently covered.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema description coverage is 100%, and the only parameter, max_wall, is fully documented in the input schema including its default, bounds, and timeout behavior. The description adds no additional parameter meaning beyond what the schema already provides, so the baseline of 3 is appropriate.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description clearly identifies the resource and scope: 'One snapshot of the meter display,' then enumerates what the snapshot includes. It is specific enough for an agent to understand the tool, but it does not explicitly contrast itself with siblings such as live or status.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
The phrase 'One snapshot' implies this is for a single sample rather than continuous streaming, which is useful context against the live sibling. However, the description never explicitly states when to use this tool instead of live, status, or memory, nor does it name alternatives.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
selftestSelf-testARead-only
Read-only health check of adapter, link and meter: adapter advertising, connection, meter attached, adapter firmware, ID, display snapshot, a live reading within 5 s, adapter and meter battery. Never changes the meter. SELFTEST_FAILED lists the failed checks (measured vs expected).
| Name | Required | Description | Default |
|---|---|---|---|
| max_wall | No | time limit for the whole call in seconds (default 120); past it the command is stopped and TIMEOUT is returned |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations only supply readOnlyHint=true; the description independently reinforces safety ('Never changes the meter') and adds behavior an agent cannot get elsewhere: the check includes a live reading that can take up to 5 s, and failures are surfaced as SELFTEST_FAILED with measured vs expected values. It does not quantify total runtime or describe the success-path return shape.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Two tight sentences, front-loaded with the read-only nature and then the concrete check list. The enumeration is dense but every item earns its place by telling the agent what a failure could mean; no filler or restated title beyond 'Self-test'.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
For a zero-required-parameter, no-output-schema tool, the description covers what is validated and how failure is reported, which is the information an agent needs. The only soft spot is no guidance on reading/acting on the result or on choosing max_wall, minor given the 100% schema coverage.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema description coverage is 100%, so the sole parameter max_wall (default 120 s, TIMEOUT on expiry) is already fully documented in the schema. The description says nothing about the time limit or how a 5 s live reading interacts with it, so baseline 3 applies.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
Names a specific verb and scope ('Read-only health check of adapter, link and meter') and then enumerates exactly what is verified — advertising, connection, meter attached, firmware, ID, display snapshot, live reading within 5 s, batteries. This is clearly distinguishable from data-reading siblings like read, live, status, and query.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
The 'health check' framing implies when to reach for it (diagnosing connectivity/setup problems), but there is no explicit when-to-use versus status/live, and no stated prerequisites or exclusions. An agent must infer that this is the diagnostic path rather than the data-retrieval path.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
statusAdapter and meter statusARead-only
Is the IR3000 FC adapter reachable and a Fluke 287/289 attached and on? Adapter address, firmware and battery %; meter model, firmware, serial, battery, memory level and clock; the number of saved items. meter.attached false = the adapter is not on a powered meter. NOT_FOUND = the adapter is not advertising (press its button until the LED flashes green; close Fluke Connect on any phone).
| Name | Required | Description | Default |
|---|---|---|---|
| max_wall | No | time limit for the whole call in seconds (default 120); past it the command is stopped and TIMEOUT is returned |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Beyond the readOnlyHint annotation, the description discloses the exact returned information, defines meter.attached false as no powered meter, and explains NOT_FOUND as the adapter not advertising with a concrete recovery action. That is unusually rich behavioral context for a read-only status tool.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
The description is dense but front-loaded: the core status question comes first, followed by returned fields and failure semantics. It is slightly run-on in listing adapter and meter fields, but nearly every phrase carries useful information.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
With no output schema, the description carries the return-value burden and does so by naming adapter address, firmware, battery, meter model, serial, memory, clock, saved-item count, and key failure states. It omits response formatting details, but is otherwise complete for this read-only status probe.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
The only parameter, max_wall, is fully documented in the schema with its default, bounds, and TIMEOUT behavior. The description adds no additional meaning about this parameter, so the schema baseline of 3 is appropriate.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description states a precise question and resource: whether the IR3000 FC adapter is reachable and a Fluke 287/289 meter is attached and powered. It also enumerates the adapter and meter fields returned, so an agent can tell this is a connectivity/status probe rather than a measurement read or self-test.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
It implies the tool is used to verify adapter and meter presence before other operations, and gives troubleshooting guidance for NOT_FOUND. However, it does not explicitly say when to prefer this over selftest, log_status, or query, nor does it state prerequisites beyond the NOT_FOUND remedy.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
11 tool updates
v0.1.0- First observed
download_recording - First observed
live - First observed
log_start - First observed
log_status - First observed
log_stop - First observed
memory - First observed
press - First observed
query - First observed
read - First observed
selftest - First observed
status
TDQS
Scored across 11 tools
Most tools map to distinct actions such as read, press, logging, memory inventory, and recording download. However, status and selftest both report adapter/link/meter health and battery state, and query can duplicate display data available through read, so an agent may occasionally choose the wrong diagnostic tool.
All names use lowercase snake_case, but the pattern is mixed: some are single-word noun-like names (memory, status), some are verbs (read, press, query), and the logging tools use a log_* prefix. It remains readable, but there is no predictable verb_noun convention across the set.
Eleven tools is well within the expected range for a meter-control server and each tool covers a meaningful workflow. The set divides cleanly into reading, logging, memory, control, and diagnostics without obvious filler tools.
The surface covers connection/health checks, display snapshots, live capture, background logging, keypress control, memory inventory, recording download, and diagnostics. Minor gaps remain, such as no direct retrieval or clearing of saved measurement items beyond trend recordings, but common workflows are supported.
Maintenance
Related MCP Connectors
Protocol-native energy infrastructure orchestration for AI data centers. Provides 46 MCP tools across 8 grid protocols (IEC-61850, DNP3, Modbus, OCPP, OpenADR, IEEE 2030.5, IEC 60870-5-104, ICCP) with 5 core API primitives: connect, dispatch, settle, comply, and intel. Enables AI agents to programmatically interact with substations, grid interfaces, and energy assets for real-time workload-grid coordination.
Connect any AI agent to 1,000+ apps and 27,000+ actions through one remote MCP server (OAuth).
A paid remote MCP for AI agent browser DevTools MCP, built to return verdicts, receipts, usage logs,
Automate 1,000+ services from any MCP-compatible AI agent: build Applets, run actions and queries.
Related MCP Servers
- AlicenseNot gradedqualityDmaintenanceEnables AI agents to control Bluetooth audio devices via MCP tools, including battery status, connect/disconnect, find-my, and snoop decoding.MIT
- AlicenseNot gradedqualityDmaintenanceEnables AI to measure DC voltage and waveform statistics from DreamSourceLab DSCope oscilloscopes via MCP tools.1MIT
- AlicenseNot gradedqualityBmaintenanceEnables AI assistants to automatically discover and control supported laboratory instruments over USB, LAN, and serial connections, including power-supply output, setpoint, and protection management plus multimeter measurements.2MIT
- AlicenseBqualityBmaintenanceEnables AI clients to scan for and connect to compatible BLE devices, control suction, vibration, linear, and rotary outputs, and safely stop or disconnect through MCP tools.221MIT