Skip to content

Instrument Behavior

github-actions[bot] edited this page Nov 12, 2026 · 41 revisions

Instrument Behavior

This page is generated from docs/verified-hardware.md in the repository on every change to main. Do not edit it here; change that file in a pull request.

What this project has actually been tested against. Anything not listed here is untested.

Model Firmware Interface Port Verified
UNI-T UTS3036B V1.04.0040 LAN, raw SCPI over TCP 5025 *IDN? returns UNI-TREND,UTS3036B,<serial>,V1.04.0040 (2026-11-10)

Notes:

  • Operating systems: the unit/integration suite (which uses an in-process fake instrument, no hardware) runs in CI on Ubuntu, macOS and Windows with Node 22 and 24. Live hardware checks so far were done from macOS only; the SCPI/TCP code path is OS-independent.
  • Terminators: \n, \r\n, \r and no terminator were all accepted; use \n. Replies end with a newline. Ports 5024 and 23 gave no response.
  • Serial numbers are intentionally not recorded in this repo.
  • Documentation basis: the UTS3000B programming manual (see references.md). Per-command hardware verification status will be tracked in programming-guide.md.
  • UTS1000B/T and other UTS models are expected to be similar but are not verified.

Probe results (2026-11-10, issue #2, firmware V1.04.0040)

Command Result
*IDN? UNI-TREND,UTS3036B,<serial>,V1.04.0040
*OPC?, *ESR?, *STB?, *ESE?, *SRE? replied (1, 0, 0, 0, 0 at idle)
*CLS accepted, no reply
:SYSTem:ERRor? 0,"No error" (error queue exists)
:SYSTem:ERRor:NEXT?, :STATus:OPERation:CONDition?, :STATus:QUEStionable:CONDition?, :SYSTem:VERSion? no reply (unsupported)
:INITiate:CONTinuous? / :SWEep:POINts? / :SWEep:TIME? / :FORMat? 1 / 1001 / 2.647500e-03 / ASC8
unknown query no reply; the next query still works

Raw output (serial redacted): tests/fixtures/p1-basics-probe.txt. Reproduce with node scripts/probe-basics.mjs <host> (or set UTS_IP).

Observed during F6 (2026-11-10, firmware V1.04.0040)

  • A ;-joined query line (:INITiate:CONTinuous?;:FORMat?) returned only one reply (1). Send queries one at a time; scpi_query enforces this.
  • scpi_query/scpi_command behaved as designed against the instrument: :SWEep:POINts? -> 1001; *CLS accepted; an unsupported query timed out cleanly.

Trace and screenshot probe (2026-11-10, issue #3, firmware V1.04.0040)

Topic Result
Trace ASCII :TRACe:DATA? TRACE1 -> 1001 comma-separated values (-6.112838e+01), no spaces, CRLF terminated; count == :SWEep:POINts?; unit dBm (:UNIT:POWer? -> DBM)
Trace REAL32 / REAL64 #4 + 4-digit byte length (4004 / 8008), big-endian IEEE 754, then CRLF; 1001 values
:FORMat query returns ASC8; setters ASCii, REAL32, REAL64 work. ASC8 as a setter is ignored. Restored to ASC8 after probing
Frequency range at probe time start 10 MHz, stop 3.6 GHz
Reply terminator text replies and block trailers end in CRLF, not just LF
:SYSTem:PICture:FORMat queries BMP; PNG accepted and restored to BMP
:DISPlay:ZBmp? #9 + 9-digit length (varies per capture), 0x03, compressed BMP-like stream. Over raw TCP only 144,800 bytes ever arrive; over VXI-11 the whole reply arrives (see the VXI-11 section)
:DISPlay:DATA? #7 + 4096054, then an uncompressed BM bitmap. Over raw TCP only 144,800 bytes arrive and there is no reply after PNG was selected; over VXI-11 the whole bitmap arrives, and a PNG after PNG was selected (see the VXI-11 section)
Sweep state continuous sweep paused to test the screenshot, then restored to continuous

Samples (trace data only; screenshots are truncated so only their 48-byte headers are kept): tests/fixtures/p2/. Reproduce with node scripts/probe-trace.mjs <output-dir> [host] (host defaults to UTS_IP).

Command timing and error queue (2026-11-10, issue #45, firmware V1.04.0040)

  • Bursts: several lines written in the same millisecond (e.g. *CLS, two setters, :SYSTem:ERRor?) can make the instrument queue spurious -113 "Undefined header" errors even though the settings are applied. The connection therefore follows every command with *OPC? and waits for the reply before writing again (sendCommand(cmd, { sync: false }) opts out). 20 consecutive start/stop rounds produced no errors.
  • Stall after an unsupported query: after a query the instrument does not recognize (no reply, -113 queued) it stops responding for about 2.15 s and keeps only the last line received during that time. The connection holds the next job off until 2.5 s after the failed query was sent. Unsupported commands (no ?) and a query sent within a few ms of the bad one are not affected.
  • *CLS clears the error queue, which is global to the instrument and keeps stale errors from earlier unsupported queries. Tools that apply settings run *CLS first so only their own errors are reported.
  • One reply per burst of queries: two queries written back-to-back (or joined with ;) get one reply; send queries one at a time.
  • Late -113 for unsupported queries (issue #47): the error for an unsupported query lands in the error queue about 4 s after the query, even if *CLS ran in between (6 of 6 trials: *CLS after the hold-off read 0,"No error", and -113 "Undefined header" reappeared ~1.5 s later). Within 2 s of the bad query, an :SYSTem:ERRor? is itself lost to the stall. The connection records when a query last timed out (msSinceQueryTimeout()), and applyCommands ignores -113 for 10 s after a timeout. Verified live: two bad scpi_query calls followed by set_frequency (center/span, start/stop, restore) reported no errors, twice.
  • The 2.15 s stall, confirmed by firmware disassembly (2026-11-11, private uts3036b-firmware repo): on the raw socket (and the web server), a chunk containing ? makes the session thread poll for a reply 2000 times with usleep(1000) between tries — about 2.1-2.2 s, matching the measured 2.15 s exactly. An unsupported query never produces a reply, so the whole loop runs, and the socket is not read at all while it does; everything that arrives during the stall is later joined into one compound command (every CR/LF becomes ;) with a single reply wait, which is why only the last line survives. VXI-11 doesn't use this loop, consistent with the "no hold-off over VXI-11" result above. Confidence: CONFIRMED for the loop and the blocked reader (disassembly: TcpSession::handle_scpi, scpi_cmd_deal, get_scpi_msg_response); LIKELY (not independently matched) that this is exactly what produces the "merged chunk, keeps only the last line" effect. Confirms the existing 2.5 s hold-off is correctly sized and can't safely be shortened below ~2.2 s.
  • The "late -113" isn't actually delayed — it's a LIFO stack *CLS doesn't clear, confirmed by firmware disassembly (2026-11-11, private uts3036b-firmware repo): :SYSTem:ERRor? doesn't read the standard SCPI error queue; it pops a separate vendor stack (gSCPI_Err[100]), newest first. The -113 lands on that stack immediately when the bad query is parsed. *CLS clears only the standard queue and status registers, not this stack — and when an error was pending, *CLS pushes a new 0 entry onto the same stack (the parser's "queue now empty" callback). So after a bad query then *CLS the stack is [-113, 0]: the next :SYSTem:ERRor? reads 0,"No error", and the one after that reads the -113 — this is exactly the "reappeared ~1.5s after *CLS" result above, with no extra delay needed to explain it. The undocumented set form :SYSTem:ERRor (no ?, no parameter) empties the whole stack. Confidence: CONFIRMED for the two separate stores, the LIFO pop, *CLS not clearing the stack, and :SYSTem:ERRor clearing it (disassembly: SCPI_GetError, Sys_Error, SCPI_CoreCls→SCPI_ErrorEmitEmpty, SCPI_SetError; all writers of gSCPI_Err found by an xref scan). LIKELY, untested: errors raised by individual commands (e.g. -224) may not set the *ESR?/*STB? bits the way parser errors do. Not yet actioned in code (needs live confirmation first — proposed test in the firmware repo's findings, not run): draining should read :SYSTem:ERRor? until two consecutive 0s, or clear with :SYSTem:ERRor (set form) first rather than relying on *CLS alone; the existing "ignore -113 for 10s after a timeout" rule in connection.ts could potentially become "clear with :SYSTem:ERRor after a timeout" instead, once verified live. With several errors pending, the first one read back is the most recent, not the oldest.

Live-verified addendum (2026-11-12): a parser-raised error (e.g. an unknown header) sets both *ESR? bit 5 (Command Error, value 32) and *STB? bit 2 (Error Available, value 4), confirmed live — matching the dual-path theory above. The "does a callback-raised error (e.g. -224) skip ESR/STB" question is still open: the setter tried as a candidate, :SENSe:FREQuency:CENTer with an out-of-range value, turned out to raise no error signal at all — *ESR?, *STB? and :SYSTem:ERRor? all stayed clean, and the value was silently rejected (read back unchanged). Caution for callers: checking :SYSTem:ERRor?/*ESR?/*STB? after a setter is not sufficient to confirm the value was accepted for at least this command — read the parameter back when correctness matters, don't infer success from a clean error queue. See issue #223.

Amplitude (2026-11-10, issue #11, firmware V1.04.0040)

  • Queries that reply: :DISPlay:WINDow:TRACe:Y:RLEVel?, :PDIVision?, :SPACing?, :RLEVel:OFFSet?, :UNIT:POWer?, :POWer:ATTenuation?, :POWer:ATTenuation:AUTO?, :POWer:GAIN:STATe?, :SENSe:CORRection:IMPedance:INPut:MAGNitude?. :INPut:IMPedance? gets no reply and :INPut:IMPedance 50 is rejected as -113; the :SENSe:CORRection:IMPedance:INPut:MAGNitude alias works for both. Defaults seen: 0 dBm, 10 dB/div, LOGarithmic, offset 0, DBM, attenuation 10 (auto on), preamp off, 50 ohm.
  • Number format: setters accept bare numbers and unit suffixes (-10 dBm, 5 dB) alike; no error either way.
  • Out of range is clamped silently, no error: reference level to -100..30 dBm; scale per division to 0.1..20; offset to +-327.6; attenuation to 0..51 and rounded to an integer (7.6 -> 8); impedance other than 75 reads back 50 (60 -> 50). An invalid enum (:UNIT:POWer BOGUS, :SPACing BOGUS) gives -224 "Illegal parameter value".
  • Units: :UNIT:POWer takes DBM|DBMV|DBUV|V|W (case-insensitive, the query returns upper case). The reference level reads back in the current unit (-10 dBm -> 36.9897 dBmV, 96.9897 dBuV, 0.0707 V, 1e-4 W; V assumes 50 ohm); the offset stays in dB. In linear scale the reference level is likewise read in the Y unit.
  • Attenuation: setting a value turns auto off. :POWer:ATTenuation:AUTO ON (or 1) turns it on, and the attenuation value then follows the reference level (0 dBm -> 10 dB, -30 dBm -> 0 dB; the value read immediately after turning auto on can still be the old one). Turning the preamp on with auto attenuation raised it from 10 to 30 dB.
  • Coupling: the reference level readback includes the reference level offset (offset 2 dB reads 2 dBm for a 0 dBm level), and the maximum reference level depends on attenuation and preamp (20 dB manual attenuation with preamp on and a 2 dB offset: max -8 dBm). set_amplitude therefore sets preamp, attenuation and offset before the reference level.
  • Impedance 75: set and read back as 75 (a correction; the reference level readback in dBm was unchanged); restored to 50.
  • Error queue backlog: flooding the instrument with malformed lines left dozens of -101 "Invalid character" entries that outlast *CLS-less reads; *CLS then draining cleared them.
  • All settings were restored afterwards (0 dBm, 10 dB/div, LOGarithmic, offset 0, DBM, auto attenuation, preamp off, 50 ohm); :SYSTem:ERRor? clean.

Bandwidth, sweep, detector and trigger (2026-11-10, issue #12, firmware V1.04.0040)

  • Queries that reply: :SENSe:BANDwidth:RESolution?, :RESolution:AUTO?, :VIDeo?, :VIDeo:AUTO?, :VIDeo:RATio?, :SHAPe?; :SENSe:SWEep:TIME?, :TIME:AUTO?, :TIME:AUTO:RULes?, :TYPE?, :TYPE:AUTO?, :POINts?; :SENSe:DETector[:FUNCtion]?, :DETector:TRACe1..6? and :TRACe<n>:AUTO?; :TRIGger:SEQuence:SOURce? and :VIDeo:LEVel? (also without :SEQuence). No reply (unsupported): :TRIGger:SEQuence:EXTernal:SLOPe?, :EXTernal:DELay? (also without :SEQuence) and :DETector:TRACe7?. Defaults: RBW/VBW 3 MHz both auto, ratio 1, GAUSsian, time 2.6475 ms auto (rule NORMal), type SWEep auto, 1001 points, all detectors NORMal with auto on, trigger IMMediate at -25 dBm.
  • Clamping, no error: RBW and VBW clamp to 1 Hz..3 MHz and snap to 1-3-10 (50 kHz -> 30 kHz; 0 -> 1 Hz); VBW/RBW ratio clamps to 1e-5..3e6 (manual says 3e-7); sweep time clamps to 1 ms..4000 s at full span; points clamp to 11..40001 and non-integers truncate (1001.6 -> 1001); video trigger level clamps to -170..30 dBm. Invalid enums give -224 "Illegal parameter value".
  • Coupling: a manual RBW/VBW/sweep time turns its auto flag off. With VBW auto, VBW follows RBW times the ratio (manual ratio only matters while VBW is auto; it does not update when a manual VBW is set). Sweep time auto follows RBW, VBW, span and points (RBW 100 kHz at full span: 2.38 s; RBW 1 Hz: 3.9e7 s). RBW auto follows the span (1 MHz span -> 10 kHz). Setting a manual sweep time and then changing the span turned sweep time auto back on. Rule ACCuracy raised the automatic time from 2.6475 ms to 4.765 ms. Setting the points to 501 and back did not change the automatic time.
  • Sweep type FFT is accepted (type auto turns off; sweep time changed to 82 ms); :TYPE:AUTO 1 restores SWEep.
  • Detector: SAMPle, POSitive, NEGative, NORMal and AVERage are all accepted on each trace. Setting a detector turns that trace's auto flag off; :AUTO 1 returns it to the automatic choice (NORMal for a WRITe trace). Traces are independent. :DETector:TRACe<n>:AUTO exists on the 3000B although the manual lists it as 1000B only.
  • Trigger: source accepts IMMediate and VIDeo; EXTernal gives -224 and the source is unchanged. Setting :TRIGger:SEQuence:VIDeo:LEVel switches the source to VIDeo, so set the level first and the source after.
  • All settings were restored (RBW/VBW auto 3 MHz, ratio 1, GAUSsian, time auto, SWEep auto, 1001 points, detectors NORMal/auto on all six traces, trigger IMMediate at -25 dBm, span 3.59 GHz, continuous sweep on); :SYSTem:ERRor? clean.

Acquisition and sweep completion (2026-11-10, issue #13, firmware V1.04.0040)

  • :INITiate:CONTinuous accepts ON, OFF, 1 and 0 (also mixed case); the query returns 1 (continuous) or 0 (single). No errors.
  • :INITiate:IMMediate and :INITiate:RESTart work in spectrum-analyzer mode: in single mode each runs exactly one sweep (the trace is frozen afterwards; repeated trace reads return identical data), in continuous mode they are accepted without error. :INITiate:STOP, :PAUSe and :RESume are unsupported: -113 "Undefined header" in both modes, and the continuous sweep keeps running.
  • A single sweep takes the sweep time: 6 s set -> 5.8 s measured from the command to the first answer (plus ~0.3 s my script slept), 3 s -> 2.8 s, 2 s -> 1.8 s; auto time (2.6 ms) returns immediately.
  • The instrument ignores commands while a single sweep runs (raw socket). Every line sent during the sweep (queries, *OPC?, trace reads) is dropped, not queued: an *OPC? sent right after :INITiate:IMMediate got no reply even 20 s later (sweep time 6 s). Lines sent shortly before the end may be answered late, so a polling loop can see a duplicate 1; the helper settles 100 ms afterwards. So *OPC? does not block until the sweep ends, and :INITiate:IMMediate followed by *OPC? is the wrong pattern (the *OPC? that the connection adds to every command would be lost and time out).
  • Chosen sync: send :INITiate:IMMediate without *OPC? sync, stay silent for the sweep time read from :SENSe:SWEep:TIME?, then poll *OPC? (300 ms timeout, no hold-off); the first 1 means the sweep is complete. A timeout (default sweep time + 10 s) covers a sweep waiting for a video trigger. Tested end to end on hardware: single_sweep with a 5 s sweep time took 5.2-5.3 s; a 1 s timeout returned a clean error result and the instrument recovered after the sweep ended. Polling a trace or :INITiate:CONTinuous? cannot work for the same reason (queries are dropped while sweeping).
  • Practical consequence: with a long sweep time in continuous mode most commands are dropped too, so set the sweep time back to auto before relying on other tools.
  • Restored afterwards: continuous sweep ON, sweep time auto (2.6475 ms), :SYSTem:ERRor? clean.
  • Status-byte polling carries no sweep or OPC information (evaluated for T4; corrected in #139). The earlier conclusion that status polling "does not work" was too broad. Tested then: *CLS, *ESE 1, *SRE 32 (both read back), single sweep (4 s), :INITiate:IMMediate, then *OPC (command form) and polling *ESR?/*STB?. (1) *OPC and *ESR? sent while the sweep runs are dropped like every other line; (2) after the sweep, with *OPC sent while idle, *ESR? and *STB? stayed 0: this firmware never sets the operation-complete bit (ESR bit 0, STB bit 5), so there is nothing to wait for; (3) in continuous mode *CLS, *OPC, *ESR? also gave 0. A later investigation (see "VXI-11 SRQ" below) showed that the status model does work for error events, so polling *ESR?/*STB? is usable for those, but not for completion: the only usable completion signal is the instrument answering again after the sweep. *ESE/*SRE were restored to 0.

Error handling standard (issue #58)

Behavior by tool group, as implemented (mock tests, plus the live check below; see AGENTS.md "Error handling standard"):

  • Setters never throw: invalid input is rejected before anything is sent; the instrument's clamping/snapping is not an error but is reported in warnings after a readback (numbers, and also units, scale type, preamp, filter shape, sweep type/rule, trigger source and the requested auto flags). Instrument-reported errors (:SYSTem:ERRor?) are errors that name the commands sent and say the settings may be partially applied. A failure part-way through (write failure, closed socket, error queue unreadable) lists the commands that were already sent.
  • A timeout of :SYSTem:ERRor? itself is treated as "no information" (src/errors.ts), not as a failure; the readback comparison still catches wrong values.
  • Getters parse strictly; an empty or malformed reply is an error naming the command and quoting the reply. identify requires four comma-separated *IDN? fields.
  • scpi_command checks the error queue like other setters. scpi_query returns replies verbatim (text) and does not interpret them.
  • single_sweep deliberately leaves the instrument in single sweep mode; its failure messages say so.

Live check of the error handling standard (2026-11-10, issue #63, firmware V1.04.0040)

Run through the built server over stdio against the real analyzer (plus a local TCP proxy that can be cut, for the mid-operation failures). Everything was restored afterwards and :SYSTem:ERRor? ended clean.

  • Not connected: all 17 other tools (every tool except connect/disconnect) return Not connected to the spectrum analyzer. Use the 'connect' tool first. and send nothing.
  • Instrument errors through scpi_command: :FREQuency:CENTer 1 XYZ gives -224 "Illegal parameter value" plus -131 "Invalid suffix"; :UNIT:POWer BOGUS gives -224 twice. Both are returned as an error naming the codes and the command, and the queue is clean afterwards (*CLS first, the errors are drained).
  • Unsupported query through scpi_query: Query timeout after <n>ms for command: <cmd>. The next getter waits out the hold-off (about 1.6 s) and works; a setter right after (set_frequency) reported no error (the late -113 is ignored for 10 s). The stale -113 shows up in a later manual :SYSTem:ERRor? as documented above.
  • Clamping warnings (requested vs actual), all confirmed: center 5 GHz -> 3.60999995 GHz, span 10 GHz -> 3.61 GHz, stop 5 GHz -> 3.61 GHz, reference level 100 -> 30 dBm and -500 -> -100 dBm, offset 1000 -> 327.6 dB, RBW 50 kHz -> 30 kHz, 0.4 Hz -> 1 Hz, 1 GHz -> 3 MHz, VBW 50 kHz -> 30 kHz, ratio 1e-7 -> 1e-5, sweep time 1 us -> 1 ms, video trigger level 100 -> 30 dBm. Center 100 kHz is accepted but the span shrinks to 100 Hz (coupling, not reported as a warning because the requested value was taken). Bandwidth/sweep/detector and amplitude arguments outside the tools' own validation (attenuation not an integer 0..51, impedance other than 50/75, scale per division above 20, points outside 11..40001) are rejected before anything is sent.
  • No spurious warnings from the readback checks (about 100 setter calls): power unit (dBm, dBmV, dBuV, V, W; the instrument replies upper case and the tool maps it), scale type (linear/logarithmic), preamp on/off, attenuation_auto on/off immediately after being set (also right after a manual attenuation, a reference level change and turning the preamp on), impedance 75/50, RBW/VBW auto on/off, filter shape, sweep type fft/sweep with auto, rule, sweep time auto, points, trigger source, detector per trace with auto, set_acquisition continuous on/off, center_step_auto, offset, zero span and full span. Every warning that did fire was a real clamp (list above). No readback mismatch needed a tool fix.
  • Mid-operation connection failure (local proxy cut at chosen points; the instrument itself was never disturbed): the instrument applies a command it received even if the connection then drops, so a failure message must not claim otherwise. Findings and fixes: (1) a reset socket gave the raw Socket error: read ECONNRESET while a clean close gave Connection closed unexpectedly; both are now Connection to the spectrum analyzer was lost (<reason>) while sending|querying <command>; (2) the call after a drop said Not connected ... Use the 'connect' tool first as if the user had never connected; it now says the connection was lost and to reconnect (an explicit disconnect still gives the plain message); (3) applyCommands did not name the command that was in flight when the failure happened, although it may have executed; it now says so; (4) single_sweep cut while switching to single mode left the analyzer in single mode (verified: continuous: false on the next session) without saying so; its message now says it may have been left in single sweep mode. Getters cut part-way return an error naming the query that was in flight, never partial data. Cutting during the readback of set_amplitude reported the command already sent (PDIVision 5 was applied on the instrument and restored afterwards).
  • Cosmetic fix: a warning for a unitless setting read requested 1e-7 , now 0.00001 (stray space); it now reads requested 1e-7, now 0.00001.
  • Restored afterwards: 10 MHz-3.6 GHz (center 1.805 GHz), continuous sweep, sweep time auto 2.6475 ms, 0 dBm, 10 dB/div, LOG, offset 0, DBM, auto attenuation (10 dB), preamp off, 50 ohm, RBW/VBW auto 3 MHz, 1001 points, detectors NORMal with auto, trigger IMMediate, *ESE/*SRE 0; :SYSTem:ERRor? returned 0,"No error".

Concurrency (2026-11-10, issue #66, firmware V1.04.0040)

Parallel tool calls through the built server over stdio (one MCP client firing Promise.all), against the real analyzer. The lock is in the server, so what was checked is that the instrument's behavior does not get in the way: nothing was lost to the instrument ignoring commands during a sweep, and no error was misattributed.

  • Batch of 12 parallel calls (set_frequency 1 GHz/100 MHz, set_amplitude -10 dBm, get_frequency, get_amplitude, two scpi_query, a scpi_command the instrument rejects (:FREQuency:CENTer 1 XX), single_sweep, more getters, a second set_frequency), then the same batch in reverse order: every call returned the correct, uncorrupted result with no warnings; each getter showed the values of the setter that came before it in the queue (calls run in arrival order); the rejected command was the only failure, reported with its own -224 and -131 errors and the command it sent; no other call saw them. :SYSTem:ERRor? afterwards: 0,"No error".
  • Busy limit: with UTS_BUSY_TIMEOUT_S=0.2 and a 1.5 s manual sweep time, single_sweep completed normally and a get_frequency fired 100 ms after it failed with Instrument busy: waited 0.2 s for another tool call (single_sweep) ...; the next get_frequency worked.
  • A sweep started with start_sweep wait=false returns, and so releases the lock, while the instrument is still sweeping: a call made then is lost to the instrument (documented in the tools reference).
  • Restored afterwards: 10 MHz-3.6 GHz, continuous sweep, sweep time auto, 0 dBm, 10 dB/div, LOG, offset 0, DBM, auto attenuation (10 dB), preamp off, 50 ohm, RBW/VBW auto, 1001 points, detectors and traces 1-6 as found (all seven getters compared equal before and after); :SYSTem:ERRor? clean.

Several instruments (issue #67)

Not verified on hardware: only one analyzer is available. Calls to different instruments are independent by construction (each name owns its own connection, queue, hold-off state, error-queue handling and session lock) and are tested with two fake instruments (tests/integration/multi-instrument.test.ts, tests/e2e/multi.test.ts). With one instrument the behavior is unchanged: the single-instrument path was checked live with the owner's analyzer. Before the multi-instrument code, the "first setter clears a stale -113" rule (#133) was a process-wide flag; it is now per instrument, because each analyzer can carry its own stale error.

Trace configuration (2026-11-10, issue #14, firmware V1.04.0040)

Probed with raw SCPI, then through the built server (get_trace, set_trace, restart_trace, set_trace_math). Originals recorded first (trace 1-6 type WRITe, update/display 1,1,1,0,0,0, selected trace 1, average count 100, average type LOG) and restored afterwards; :SYSTem:ERRor? ended clean.

  • Queries that reply: :TRACe:SELect?, :TRACe<n>:TYPE? (WRITe, AVERage, MAXHold, MINHold), :TRACe:TYPE? (current trace), :TRACe<n>:UPDate:STATe?, :TRACe<n>:DISPlay:STATe? (also :DISPlay?), [:SENSe]:AVERage:COUNt?, :AVERage:TYPE? (VOLTage, POWEr, LOG; with or without :SENSe). No reply (unsupported, stalls ~2.15 s, posts -113): :CALCulate:MATH? and the variants :CALCulate:MATH:STATe?, :TYPE?, :TRACe?, :FUNCtion?, :TRACe<n>:MATH?, plus :TRACe0:TYPE? and :TRACe7:TYPE?. Trace math is therefore write-only.
  • Rejected, not clamped (error and the old value stays): :TRACe:SELect 0 gives -224 (re-checked in #147: 7 and 9 are ignored without any error, value unchanged, nothing arrived within 6 s; errors from this setter were also seen attributed to later commands); an unknown type or average type gives -224; :TRACe7:TYPE ... gives -108; :CALCulate:MATH with an offset of 200 gives -224, with OFF and no operands -224, with an unknown type -108.
  • Average count: the instrument does not enforce 1-999: 0 becomes 1, 7.6 rounds to 8, 1000 is kept as 1000. The tool validates integers 1-999 before sending. The count and type are global (not per trace).
  • Restart of hold/average (ambiguity 8), measured on a noise trace with max hold and averaging running (a freshly restarted max hold reads lower and climbs back over a few sweeps; a saturated one does not move): re-sending :TRACe<n>:TYPE <same type> restarts it; so do WRITe then back, :TRACe<n>:UPDate:STATe 0 then 1, and re-sending :AVERage:COUNt. :INITiate:RESTart and :INITiate:IMMediate in continuous mode do not restart the hold, and with an average trace running they leave the instrument ignoring commands (queries time out) for roughly count x sweep time. A source change (tracking generator level) also restarts the hold, so a hold cannot be shown to survive a signal drop that way. restart_trace uses the re-send of the current type.
  • Hold behaves as expected on a real signal: with the tracking generator on (looped to the input) max hold sat above the live trace (mean about 0.3 dB higher, smoothly flat) and the average trace was smoother (adjacent-point deviation 0.13 dB against 0.55 dB live). :TRACe<n>:UPDate:STATe 0 freezes the trace (view/hold): it kept its data while the live trace followed the generator.
  • Trace math works (verified through the trace data of the target trace): PDIFference and PSUM of a trace and a much weaker one both give about the stronger trace, LDIFference with +10 dB gave A-B+10 dB, LOFFset gave A (the offset was not visible in this arrangement, treat as unverified), OFF returns the trace to its live data. Enabling math on a trace switches that trace on (update and display become 1); OFF leaves it on.
  • :trace1:mode write (the manual appendix example) gives -113; :TRACe<n>:TYPE is the right form. :KEY:Default was not sent.
  • Late error posting: a rejected command can post its error well after *CLS: the first setter after a batch of rejected :CALCulate:MATH probes saw a stale -109 "Missing parameter" although the setting applied. A one-off stale error after probing is not a tool bug; re-running :SYSTem:ERRor? until 0 clears it.
  • Tracking generator side effects (found while testing, relevant to the tracking-generator tools): :OUTPut:STATe 1 makes the auto sweep time about 2.15 s (the instrument then drops most lines sent during the sweep in continuous mode). After :OUTPut:STATe 0 the automatic RBW stayed at 300 kHz (auto sweep time 264 ms instead of 2.6475 ms) until :SENSe:BANDwidth:RESolution:AUTO ON was sent again, which restored 3 MHz; both auto flags still read 1 while stale.

Trace data tool (2026-11-10, issue #15, firmware V1.04.0040)

Checked through the built server over stdio (get_trace_data) on both transports (UTS_TRANSPORT=raw and auto, which chose VXI-11). Originals recorded first and compared after: frequency 10 MHz-3.6 GHz, continuous sweep on, sweep time auto, amplitude, bandwidth (RBW/VBW auto), 1001 points, detectors, traces, :FORMat ASC8, picture format BMP, *ESE/*SRE 0, tracking generator off; :SYSTem:ERRor? clean.

  • Formats compared on one frozen trace (single sweep, then :TRACe1:UPDate:STATe 0, so repeated reads are identical; 5 reads each): all three formats return 1001 values equal to :SENSe:SWEep:POINts?. REAL32 equals ASCII exactly at the 7 significant digits ASCII carries (maximum difference 0); REAL64 differs from both by at most 5e-5 dB, which is the ASCII/float32 rounding (a value such as -110.3427 has only 4 decimals). So REAL32 gives no precision over ASCII and REAL64 is the only exact format. Transfer time per tool call (including the settings queries and, for the binary formats, the format switch and restore): raw ASCII 175 ms, REAL32 242 ms, REAL64 236 ms; VXI-11 ASCII 143 ms, REAL32 160 ms, REAL64 165 ms. ASCII is faster because it needs no format round trips, hence ascii is the default; payload size (14 KB against 4 KB) does not matter on a LAN.
  • :FORMat is restored after every binary read (ASCii setter, reads ASC8), also when the read fails; a final :FORMat? after each run was ASC8.
  • Zero span: :FREQuency:STARt? and :STOP? both equal the center frequency, :SENSe:SWEep:TIME? is the span of the trace, so the tool returns a time axis. In zero span set_frequency with start_hz/stop_hz re-clamps to the center (the restore must use full_span), the center step becomes 3 MHz until span is restored, and afterwards sweep_time_auto read false and sweep_type_auto true (its original was false) while the settings were being put back; both were re-set to the originals, the cause was not isolated.
  • Tracking generator loopback (generator level -30 dBm, :OUTPut:STATe 1; the generator output follows the sweep so the trace is a flat line, not a single peak): fresh reads showed min -30.9 dBm, max -27.8 dBm, mean -29.2 dBm, peak near 3.004 GHz, identical across ASCII, REAL32 and REAL64 within 0.1 dB (live sweep to sweep ripple) over both transports. The auto sweep time is 2.15 s with the generator on and the first read right after switching it on still saw the old 2.6 ms sweep time (the wait is by polling, so it still completed). Switched off afterwards and :SENSe:BANDwidth:RESolution:AUTO/VIDeo:AUTO re-asserted; RBW and VBW read 3 MHz.
  • fresh took about the sweep time (0.28 s for a 2.6 ms sweep including the checks, 2.6 s with the generator on) and put continuous mode back on both transports; with continuous mode already off it stays off.
  • Decimation to 100 points kept the global peak (identical peak_index) and returned in 160-220 ms.

Markers (2026-11-10, issue #16, firmware V1.04.0040)

Probed by hand over raw SCPI, all marker state restored afterwards (all markers off, trace 1, reference marker n+1, peak criteria automatic, center/span, reference level and automatic center step back).

  • Queries that answer: :CALCulate:MARKer<n>:MODE? (OFF, POSition, DELTa, FIXed), :TRACe?, :X?, :Y?, :REFerence?, :CALCulate:MARKer:SELect?, :PEAK:THReshold?, :PEAK:THReshold:STATe?, :PEAK:EXCursion?, :PEAK:EXCursion:STATe?. Markers 1-10 exist; :CALCulate:MARKer11:MODE? gets no reply (the usual stall and -113/-108), so the tools validate 1-10 themselves. Default reference marker of marker n is n+1 (marker 10: 1). Defaults seen: all modes OFF, trace 1, X 0.
  • Off markers: X? returns the last position (stale) and Y? a level, even while the marker is off, so get_marker reports null for both.
  • A search turns a marker on: :MAXimum on a marker whose mode is OFF sets it to POSition (resolves ambiguity 10). :MAXimum:NEXT, :LEFT, :RIGHt and :MINimum also work; :MAXimum:LEFT/:RIGHt on a live noisy trace can land on a peak on the other side of the marker, because the trace changes between sweeps.
  • :PTPeak leaves the marker in DELTa mode with X and Y relative to its reference marker (for example marker 1 delta X -1.777 GHz, Y -22.75 dB from the minimum to the maximum).
  • Delta markers: X? and Y? are relative to the reference marker (resolves ambiguity 10). :REFerence <own number> gives -224 and keeps the old value; the reference was read back after setting 1.
  • Setters: :MODE accepts OFF|POSition|DELTa|FIXed (a bad one gives -224); :TRACe 9 gives -224 and keeps the old trace; :X is not clamped: 5 GHz and 1 MHz read back unchanged on a 10 MHz to 3.6 GHz span (the tools therefore cannot warn about it from the readback; the level at such a position is not meaningful).
  • Setting :REFerence on a marker that is off turns it on in DELTa mode (seen through the built set_marker tool, which then returned mode: delta; markers_all_off cleared it). set_marker with only reference_marker therefore also switches the marker on.
  • Marker to instrument (:CALCulate:MARKer<n>:SET:CENTer|STEP|STARt|STOP|RLEVel): all five work and the targets read back the marker's values (center 1 GHz, start 2 GHz, stop 3 GHz, step 1 GHz, reference level -67.45 dBm). :SET:STEP turns automatic center step off. :SET:CENTer with a marker whose frequency would put the current span outside the range (marker at 1 GHz, span 3.59 GHz) gives -224, and the earlier command leaves frequencies as they were set by the other commands.
  • Peak criteria: setting :PEAK:THReshold <v> or :PEAK:EXCURSION <v> turns the matching :STATe (automatic) to 0; :STATe 1 switches it back and the value is then computed from the trace (it read -70.2 dBm and 5.9 dB). Clamping: threshold 50 reads 30, but -300 is accepted (the manual says -200 to 30), excursion 500 reads 200 and -3 reads 0 (the manual says 0 to 100); the tools validate the manual's ranges before sending.
  • Error queue was clean after every accepted command; :SYSTem:ERRor? ended 0,"No error".

Marker functions, N-dB bandwidth and frequency counter (2026-11-10, issue #17, firmware V1.04.0040)

Probed by hand over raw SCPI with the tracking generator on (the trace is nearly flat, so there is no real peak); every setting restored afterwards (marker off, function OFF, band span 0, N-dB off and -3.01, counter off with gate 0.1 s, 10 MHz to 3.6 GHz, tracking generator off, continuous sweep, error queue clean).

  • Function (:CALCulate:MARKer<n>:FUNCtion {OFF|NOISe|BPOWer|BDENsity}) reads back as given. :FUNCtion:BAND:SPAN, :BAND:LEFT? and :BAND:RIGHt? answer; the default span is 0 and, with a function on, the instrument set the band to 179.5 MHz around the marker. A 1 MHz span gave left/right at x -0.5/+0.5 MHz. The result is read with the marker's :Y?: noise gave -90.6 dBm (per Hz), band power over 1 MHz -32.8 dBm and band density -92.8 dBm (exactly 60 dB lower, 10 log 1 MHz). Right after switching to noise or band power Y? can still be nan until the next sweep; the tools do not read Y, use get_marker.
  • N-dB (:BANDwidth:STATe, :BANDwidth:NDB, :BANDwidth:RESult?): default N is -3.01. N is clamped: 5 reads -0.01, -0.001 reads -0.01, -200 reads -140. The tools refuse values outside -140 to -0.01 before sending. RESult? is a number in Hz (N = -1 on the flat trace gave 60.4 MHz) or nan when the trace never falls N dB on both sides (N = -3, -6, -20 on the flat trace); the tool reports nan as null. RESult? with N-dB off gave a value once and no reply once, so the tool only asks while N-dB is on.
  • Frequency counter (:CALCulate:MARKer:FCOunt:STATe, :GATetime): the counter is global (not per marker). Default off, gate 0.1 s. The gate is clamped to 1 us to 500 ms (9 reads 0.5, 1e-9 reads 1e-6). The result query is :CALCulate:MARKer:FCOunt:X? (not in the manual): it answers 9.910000000000e+37 while the counter is off and 0 with the counter on and no usable signal (the bench has none at the marker); :CALCulate:MARKer1:FCOunt:X? (with a marker number) is rejected with -113, and :FCOunt:Y? gets no reply. A real count was not seen for lack of a carrier, so the tool reports the value as returned.
  • Delta markers were covered with the marker tools (X and Y relative to the reference marker); nothing different for these functions.

Limit lines (2026-11-11, issue #21, firmware V1.04.0040)

Probed by hand over raw SCPI and then through the built tools, one command at a time. Line 1 was used for the experiments; afterwards all six lines were back to display off, type UPPer, margin off and no points, and :SYSTem:ERRor? was clean.

  • Reads: :CALCulate:LLINe<n>:DISPlay?, :TYPE? (UPPer/LOWer), :TRACe?, :MARGin?, :MARGin:STATe? and :DATA? all answer. Defaults: display 0, UPPer, trace equal to the line number, margin 0 with state 0. Lines 1 to 6 exist; line 7 gives no reply and a -108.
  • Points: :DATA f,a,c,... reads back as f,a,c triplets in scientific notation, the first point always with connect 1 whatever was sent, sorted by frequency. One point is accepted, and two points with the same frequency (a vertical step) are accepted. At most 10 points: a longer list is silently ignored (the old points stay, no error). A list that is not a multiple of three gave -200 ("Execution error") and changed nothing. :DATA? on a line without points gets no reply at all and no error, so the tool treats its timeout as an empty line.
  • Side effects: setting :DATA turns :DISPlay on; setting :MARGin turns :MARGin:STATe on (so the state is sent after the margin).
  • Clamping: :MARGin 99 reads 40 (clamped to the documented -40 to 40). :TRACe 9 is silently ignored (stays as it was, no error); :TRACe 2 is accepted.
  • Delete: :DELete is acknowledged by *OPC? before it has finished: a :DATA? right after still returned the old 10 points, and in one run a late delete erased points set by the next call. delete_limit_line therefore re-reads until the line is empty. :DELete does not reset type, trace, margin or display. :ALL:DELete emptied every line.
  • Pass/fail: none documented and none probed (the brief for this work allows only documented commands); see the tools reference.

Tracking generator (2026-11-11, issue #22, firmware V1.04.0040)

Probed by hand over raw SCPI and checked through the built tools on both transports (raw and VXI-11). Originals read first and restored: output off, level -30 dBm, offset 0 dB, reference trace off, normalization off, normalization reference level 10 dB, RBW/VBW 3 MHz auto, error queue clean.

  • Queries that reply: :OUTPut:STATe? (also :OUTPut:EXTernal:STATe?), :SOURce:POWer? (also the long :SOURce:EXTernal:POWer:LEVel:IMMediate:AMPLitude?), :SOURce:CORRection:OFFSet?, :SOURce:TRACe:REFerence:STATe?, :CALCulate:NTData:STATe?, :DISPlay:WINDow:TRACe:Y:SCALe:NRLevel?. Bare numbers and a dBm / dB suffix both work.
  • Clamping (silent, no error): level -40 to 0 dBm (-120 reads -40, 10 reads 0), level offset -200 to 200 dB, normalization reference level -100 to 100 dB. The tools refuse values outside these ranges.
  • Reference trace: :SOURce:TRACe:REFerence:STATe 1 only takes while the output is on (it reads 0 with the output off); it works with normalization on or off. :CALCulate:NTData:STATe takes ON/OFF at any time.
  • Side effects of the output (confirmed again): with the output on the auto sweep time is about 2.15 s. After :OUTPut:STATe 0 the automatic RBW/VBW drop to 300 kHz a moment later and stay there with both auto flags still 1; re-sending :SENSe:BANDwidth:RESolution:AUTO ON and :VIDeo:AUTO ON restores 3 MHz and the 2.6475 ms sweep time, but only when sent after a short pause: sent straight after the OFF it is overtaken by the drop. set_tracking_generator waits 0.6 s and verifies by RBW readback. Readback queries with the output on were answered normally on both transports.
  • Option absent: not testable on this unit (it has the option); the tools treat an unanswered :OUTPut:STATe? as an absent option (tested with a fake).

, #74; verified on hardware for issue #75, firmware V1.04.0040)

Everything below was measured on the real analyzer, through the built server over stdio where a tool is named, and with a small client on the VXI-11 core channel where the wire behavior is the point. Raw and VXI-11 were compared side by side.

Basics. The portmapper (TCP and UDP 111) registers the VXI-11 core, async and interrupt programs (395183, 395184, 395185) over TCP on a dynamic port. create_link("inst0") works, maxRecvSize is 1 MiB. HiSLIP (4880) is closed. True SRQ was not obtained: create_intr_chan and device_enable_srq succeed but the instrument never connected back to our listener, and device_readstb always returned 0x00 even while *STB? on the same link showed bits set (the status byte over VXI-11 looks like a stub), so the interrupt channel is not used. Details and the retests (issues #128, #139) are in "VXI-11 SRQ" below.

Every tool returns the same values over VXI-11 as over raw. All getters (identify, get_frequency, get_amplitude, get_bandwidth, get_sweep, get_detector, get_acquisition, get_trace) returned identical results, and one scripted sequence through every setter (set_frequency, set_amplitude, set_bandwidth, set_sweep, set_detector, set_trace, set_trace_math, restart_trace, set_acquisition, scpi_command, scpi_query, including a clamped value and an invalid argument) gave identical results, warnings and errors on both transports. connect with transport raw, vxi11 and auto works, auto picked VXI-11, and an explicit port with vxi11 is refused before anything is sent.

Reads: the instrument never answers a read that has nothing to deliver. This drove the transport design:

  • A device_read with a non-zero io_timeout and nothing to deliver is never answered with error 15. The instrument waits 5.4 times the io_timeout (0.5 s gave 2.7 s, 1 s gave 5.4 s, 3 s gave 16.1 s, 6 s gave 32.3 s; linear), then drops the request without a reply, and it serves no other call on that link meanwhile: a device_write right after only completes when the instrument has given up. Nothing is lost, the write is delayed.
  • A device_read with io_timeout 0 always answers error 15 at once, even when a reply is waiting. It cannot be used to poll or to drain.
  • So the only working timeout is the client's RPC deadline. The transport sends a fifth of the requested timeout as io_timeout (the instrument then waits about 1.08 times the timeout, long enough for anything that arrives in time and free again about when the client gives up) and fails the read with TransportTimeout at the requested timeout plus a 500 ms RPC margin, keeping the link. The first version of the transport (io_timeout = timeout, error 15 expected) declared the link lost after every timed-out query and stalled the next write for several times the timeout; the fix is the code described here, with tests built on a fake that reproduces these rules.
  • The output buffer has one slot: a second query written before the first reply was read replaces it (two stacked queries returned only the second reply), and the reply of a query whose read gave up never shows up in front of the next query's reply (checked mid-sweep and after a sweep). The old "read the leftover away before the next write" step is therefore gone.

Unsupported query over VXI-11. The read gets no reply and no error 15 (it would wait 5.4 times its io_timeout). The -113 "Undefined header" is in the error queue at once (about 1.6 s after the query, when the instrument is free again; over raw it appears about 4 s after), not late. Lines written meanwhile are not lost: a write is delayed until the instrument has given up the read, then executes. With the transport's one-fifth io_timeout that is about when the client gives up, so the next call runs at once (*IDN? 9 ms after a 1 s timeout, get_frequency 50 ms). A timed-out query reports Query timeout after <T>ms ... (over VXI-11) after T + about 0.5 s. A setter the instrument rejects (:NOT:ACOMMAND 1, a bad suffix) posts its error immediately: :SYSTem:ERRor? right after it, with no *OPC? in between, already shows it.

Command bursts need no *OPC? pacing (syncsCommands stays false): 40 unpaced :SENSe:FREQuency:CENTer writes took 71 ms and the readback was the last value; 30 rounds of set plus immediate query gave no mismatch; 10 rounds of RBW and VBW setters plus a query were correct; the error queue was empty. Each device_write returns in 1 to 3 ms.

Sweeps (single_sweep, single mode, one blocking *OPC? read; the table is the waited_ms of three runs each, raw next to VXI-11):

Sweep time raw VXI-11
0.1 s 322, 329, 310 ms 208, 186, 198 ms
1 s 1202, 1191, 1193 ms 1119, 1108, 1111 ms
4 s 4244, 4240, 4250 ms 4108, 4117, 4147 ms
20 s not measured 20137, 20098 ms (timeout_s 30)

The extra time is about 90 to 150 ms over VXI-11 (instrument overhead after the sweep) against 200 to 250 ms over raw. A deliberately too short timeout (timeout_s 1 for a 4 s sweep, and 0.2 s with start_sweep then wait_for_sweep_complete) fails with Timed out: the sweep did not complete within 1.0 s (no reply to the blocking *OPC? read over VXI-11) after 1.5 s (0.7 s for 0.2 s), the link stays up, and a get_frequency sent right after was queued behind the running sweep and answered when it ended (2.6 s later, correct value): nothing is dropped. Commands during a sweep are queued, as in the prototype (dropsInputDuringSweep stays false).

Large transfers, and the screenshot works over VXI-11 (issues #42, #20).

  • :TRACe:DATA? TRACE1 over ASCII: 14,015 bytes (1001 values) in 81 ms. REAL32 (:FORMat REAL32): #44004 + 4004 bytes in 75 ms; REAL64: #48008 + 8008 bytes. Same layout as over raw; :FORMat was set back with ASCii and reads ASC8 again.
  • :DISPlay:ZBmp? is not truncated over VXI-11: the full reply arrived, 200,687 and 202,462 bytes in two captures (#9 + 9-digit length, 0x03, a compressed BMP-like stream), in 54 ms, repeatably. Over raw the same query stops at exactly 144,800 bytes.
  • :DISPlay:DATA? is not truncated either: #7 + 4096054 followed by a complete uncompressed 1280 x 800, 32 bit BMP (4,096,054 bytes, 4,096,065 with header and CRLF) in 371 ms; it converts to a valid screenshot of the instrument screen. Over raw it times out.
  • With :SYSTem:PICture:FORMat PNG, :DISPlay:DATA? returned a real PNG (#590704 + 90,704 bytes, 3 s) over VXI-11, where raw returns nothing after PNG was selected. :DISPlay:ZBmp? still returns the BMP-like stream. The picture format was set back to BMP.
  • Through the scpi_query tool with reply: "block" the three transfers worked over VXI-11 (the tool shows the first 64 KiB and marks larger replies as truncated) and timed out over raw.

screenshot tool, live (issue #20). Through the built server over VXI-11: screenshot returned an image/png block of 91,483 bytes in about 3.0 s, a valid 1280 x 800 capture of the instrument screen; :SYSTem:PICture:FORMat? read BMP before and after, and :SYSTem:ERRor? was clean. Over raw TCP (UTS_TRANSPORT=raw) the tool refused at once without sending anything (the 144,800 byte limit of issue #42), and the format stayed BMP. No captured image is stored in the repository because a screen can show the instrument's serial number or address.

Links. One client was killed with SIGKILL 12 times in a row (no destroy_link): every next connect worked at once, so the instrument frees a link when its TCP connection goes away. 40 links were open at the same time and all of them kept answering; no limit was reached. A raw TCP connection (port 5025) and a VXI-11 link work at the same time: interleaved and simultaneous queries on both got correct replies; while a sweep started over VXI-11 runs the raw connection gets no reply (as always), the VXI-11 *OPC? answers at its end. The instrument web page (port 80) kept answering while a link was open and queried. Vendor software was not available to test.

Error paths over VXI-11 (the checks of issue #63 repeated), through the built tools (the VXI-11 core channel went through a local TCP proxy, with a fake portmapper pointing at it): a setter the instrument rejects returns The instrument reported errors: -224,"Illegal parameter value"; -131,"Invalid suffix" (after sending: ...), and the queue is clean afterwards; not connected gives Not connected to the spectrum analyzer. Use the 'connect' tool first.; cutting the connection at every RPC call of set_frequency, set_amplitude (including during the readback), get_amplitude, get_detector, get_trace, scpi_command, scpi_query and single_sweep (before the start command, before the *OPC? write, before its blocking read, and by destroying all sockets 1.2 s into a 3 s sweep) gave Connection to the spectrum analyzer was lost (VXI-11: ... aborted: connection closed) while ... naming the command, with the "may already be applied" hint for setters and sweeps, and every later call says to reconnect; a new link right after worked (the sweep continued on the instrument). Two batches of 12 parallel tool calls (setters, getters, a rejected command, single_sweep) were serialized by the session lock: all succeeded except the one rejected command, which failed alone.

Capability flags confirmed (VXI11_CAPABILITIES): syncsCommands false, dropsInputDuringSweep false, holdOffAfterTimeout false (no line is lost after a timeout; the instrument delays the next write by itself). No constant had to change.

Default transport: auto (it was raw). Every tool works over VXI-11 with the same values, VXI-11 is better where they differ (commands queued during a sweep instead of dropped, no hold-off after an unsupported query, an exact blocking sweep wait, the full screenshot), and auto still falls back to raw when the portmapper or the VXI-11 link is not available. Remaining reasons to use UTS_TRANSPORT=raw: a firewall that blocks the dynamic VXI-11 port (the fallback covers a blocked port 111 or a failing create_link), a tunnel on the raw port (giving a port without a transport already selects raw), and other analyzer models that were not tested.

Measurements: channel power, time-domain power, OBW (2026-11-10, issue #18, firmware V1.04.0040)

Verified on the real analyzer by hand over raw TCP and through the built tools over raw and VXI-11.

  • No query reports the active measurement. :CONFigure?, :CONFigure:CHPower?, :CONFigure:SANalyzer?, :SENSe:MEASure?, :CALCulate:MEASure? and :INSTrument:MEASure? get no reply (stall and -113); :INSTrument:SELect? answers SA in every measurement. What does discriminate: [:SENSe]:<meas>:AVERage:STATe? answers only while that measurement is active, :SENSe:DETector:TRACe1? and :TRACe1:TYPE? answer only in spectrum analyzer mode (in a measurement they time out). :FREQuency:CENTer?, :SPAN? (not in zero span), :SENSe:BANDwidth:RESolution?/:VIDeo?, :SENSe:SWEep:TIME? and :INITiate:CONTinuous? answer in the measurements.
  • Entering changes state and exiting does not undo it. :CONFigure:CHPower (and :NDEFault, same effect here) took span 3.59 GHz to 3 MHz (center unchanged, RBW and sweep time follow because they are auto); :CONFigure:TPOWer gives zero span; :CONFigure:SANalyzer returns to spectrum analyzer mode with the span still at 3 MHz. The detector, trace types, reference level, attenuation and the continuous flag were unchanged. The tools therefore save and restore center, span, RBW, VBW, sweep time (with auto flags) and continuous mode, and verify.
  • Parameter behavior. channel power: :BANDwidth:INTegration clamps to 100 Hz-3.61 GHz (1 gave 100, 1e12 gave 3.61e9); :AVERage:COUNt 5000 was accepted (the manual says 1-999; the tools refuse above 999); :AVERage:STATe, :TYPE (POWEr default, LOG) and :TCONtrol (EXPonential default, REPeat) round-trip; defaults after the preset: integration bandwidth 2 MHz, 10 averages, averaging on. OBW: :PERCent default 0.99, 5 clamps to 0.9999; :XDB default -26, 5 clamps to -0.1. Time power: :LLIMit default 0, :RLIMit default 10 ms and clamped to 10 ms (1 s gave 10 ms). Wrong-mode parameter setters give -113.
  • Results. :FETCh:CHPower? gives two values (power dBm, density dBm/Hz; noise floor around -74 dBm / -137 dBm/Hz with 3 MHz span and no signal); :FETCh:TPOWer? one; :FETCh:OBWidth? gives seven values (the manual lists four, order not as documented), so the tools query :FETCh:OBWidth:OBWidth?, :FERRor?, :XDB? and :APOWr? (they answer, not one atomic snapshot). :READ:OBWidth:OBWidth? and the other :READ:OBWidth:<sub>? forms get no reply, and :CHPower:CHPower?, :CHPower:DENSity? and :OBWidth:OBWidth? (without FETCh/READ) get none either. Right after entering a measurement :FETCh:CHPower? can return nan (no result yet): the tool reports it as an invalid reply.
  • READ has no line terminator over raw TCP (checked byte by byte: the reply ends with a space, no CRLF, while FETCh ends with CRLF), so a line reader waits until its timeout (:READ:OBWidth:OBWidth? and, before this was understood, the combined form timed out after 30 s). read_measurement/measure use FETCh over raw TCP (plus a 1.5 s settle after entering in measure) and say so in a warning; over VXI-11 READ works for all three (channel power 0.8 s, OBW 1.9 s, time power 0.7 s for a whole measure including entering and restoring).
  • Probing is expensive. A probe that gets no reply stalls the instrument. Over VXI-11 a miss followed immediately by more probes made a later device_write time out (5.5 s) and the link was lost, and the instrument then ignored raw TCP for about a minute; the first VXI-11 write of a new process right after another process also failed twice, then worked. The tools therefore never scan for the active measurement: one query for spectrum analyzer mode, one for a named measurement, 1.5 s timeout and a 3 s pause after a miss.
  • Restore verified: measure for each of the three measurements, over raw and VXI-11, left center 1.805 GHz, span 3.59 GHz, RBW/VBW 3 MHz auto, sweep time 2.6475 ms auto and continuous mode exactly as before; configure_measurement then exit_measurement restored the saved state; the error queue was clean afterwards.

Measurements: ACPR, TOI, C/N, harmonics (2026-11-10, issue #19, firmware V1.04.0040)

Verified over raw TCP with the built tools (one measurement per run) and by hand. Not verified: VXI-11 READ for these four (the code uses the same READ-then-FETCh pattern as OBW), and TOI/ACPR/C/N/harmonics results on a real signal (the bench had no signal, so values are noise-floor numbers; the tracking generator was not used).

  • Same mode behavior as #18: :CONFigure:<meas> presets the span (ACPR 10 MHz, TOI 8 MHz at the same center, harmonics around the center) and :CONFigure:SANalyzer does not undo it; :SENSe:<meas>:AVERage:STATe? tells the measurement. measure for each restored center, span, RBW, VBW and sweep time (each with its auto flag: the saved state holds the auto-vs-manual mode, not just the value) and continuous mode exactly (checked with a manual 300 kHz RBW and with auto). Once, harmonics then TOI back to back left the TOI span (8 MHz) although measure reported success; it did not reproduce in three further runs (single and back to back), so it is not understood. Since #133 the restore is checked by readback of every field and auto flag, resent once on a mismatch and reported as an error naming the field if it still differs, so this case can no longer pass silently (see the live results under #133). Live (#133): one fresh process ran measure for harmonics, toi, acpr, cn, channel_power and toi again back to back (2.6 s apart) from a clean start: every call restored with no warnings, and center 1.805 GHz, span 3.59 GHz, RBW/VBW 3 MHz auto, sweep time 2.6475 ms auto, continuous on and an empty error queue were confirmed afterwards. The 8 MHz span did not recur (6 of 6 clean), so its cause is still unknown; the readback check would now have reported it. The idle-gap *CLS timeout / ECONNRESET reports were not reproduced either (a fresh process after minutes idle worked on its first setter).
  • Parameters read back as set (all queries answer): ACPR BANDwidth:INTegration, CARRier:LIST:BANDwidth, OFFSet:LIST:FREQuency, OFFSet:LIST:SIDE (preset 2 MHz, 2 MHz, 3 MHz, BOTH); C/N BANDwidth:INTegration, BANDwidth:NOISe, OFFSet (preset 2 MHz each); harmonics FREQuency:FUNDamental (preset = center), NUMBer (preset 2), SWEeptime (preset 0.1 s). Setting a count of 5 and a fundamental of 50 MHz was accepted as sent; clamping of the new parameters was not probed.
  • ACPR: :FETCh:ACPower? returns three values (main dBm, lower dBc, upper dBc), the manual lists five. :FETCh:ACPower:MAIN? gives one value, :LOWer? and :UPPer? give (dBm, dBc). With offset_side POSitive the lower values are nan. Right after entering, nan can appear everywhere.
  • C/N: :FETCh:CNRatio? gives three values (carrier dBm, noise dBm, C/N dB) as documented.
  • TOI: :FETCh:TOI:LOW:TRD? and :UP:TRD? give four values, :LOW:TONE?/:UP:TONE? two, as documented. :FETCh:TOI:IPT? gets no reply (it stalled the instrument and posted a -113; the tools do not use it, the TOI is the fourth value of each TRD reply).
  • Harmonics: :FETCh:HARMonics:AMPLitude:ALL? and :FREQuency:ALL? always return 10 values (with trailing spaces); only the first NUMBer entries are results (count 5: five values, then five placeholders of +|fundamental| dBc). NUMBer counts the fundamental. :FETCh:HARMonics:DISTortion? gives two values (THD %, dBc). Amplitudes are nan until the first sweep finishes (a read_measurement right after configuring can fail with an invalid-reply error naming the command).
  • Session quirks seen while testing (not tool bugs): a stale -113 (from the unanswered IPT query of a previous process) made the next process's measure fail at its first setter; a fresh process whose first write came after several seconds of idle, or soon after another process, sometimes got *CLS timeouts and ECONNRESET and the instrument ignored raw TCP for a minute or so (the harness's idle wait was the likely cause; no instrument state was lost).

Network discovery (issue #85; firmware V1.04.0040)

  • The analyzer answers a UDP portmapper GETPORT (program 395183 v1, TCP) sent to its address on port 111 with the dynamic VXI-11 core port, within a few milliseconds; *IDN? over that VXI-11 port and a plain TCP connect to 5025 both work, and discovery sent nothing else (the error queue stayed clean, no setting touched).
  • The broadcast form (directed broadcast to the subnet) was not verifiable on the bench: the analyzer sits on a different (routed) subnet than the computer, and directed broadcasts do not cross routers. This is the intended limit: discovery never goes beyond the local subnets. A unicast request to the analyzer's address (the same code path with an explicit target) found it and identified it (UTS3036B, V1.04.0040).

VXI-11 SRQ (issues #128 and #139, firmware V1.04.0040)

Verdict: real SRQ is not available on this firmware, but the SCPI status model works. Correction (#139): an earlier version of this section said the analyzer has no working status-byte model and that its status byte never changes. That was wrong. The first tests used conditions that never set a bit on this firmware; a root-cause retest found the model works over SCPI for error events. The VXI-11 client side (src/vxi11/interrupt.ts, Vxi11Client.enableSrq) is implemented and tested and is kept, but no tool uses it; blocking *OPC? stays the way to await a sweep.

What works (SCPI, *STB? / *ESR?). With *ESE 32 and *SRE 32, an undefined-header setter (posts -113 in the error queue) makes *ESR? return 32 and *STB? return 100 (0x64: ESB bit 5, error-queue-available bit 2 and MSS bit 6). With *SRE 4, *STB? returns 68. Polling *STB? / *ESR? is therefore usable for error and event bits.

What never sets a bit (tested). A pending reply (MAV, bit 4), *OPC (the OPC bit never sets; *ESR? stays 0 after *OPC) and a single sweep (also with *OPC queued behind it). These were the triggers of the earlier tests, which is why they showed a constant 0. OPC and MAV are not implemented on this firmware, so status polling cannot be used to wait for a sweep.

VXI-11 device_readstb returned 0x00 in every case, including while *STB? on the same link read 100 or 68. It answers in about 1 ms, so it appears to be a stub in the analyzer's VXI-11 server rather than a reflection of the status model. It needs no callback, so it is not affected by routing or firewalls. For the real status byte, use *STB?.

Interrupt channel. create_intr_chan (hostAddr, port, program 395185, version 1, TCP) and device_enable_srq(true, handle) returned error 0 (the portmapper also lists program 395185). With the error condition as the trigger, and with all the mask settings tried, the analyzer never connected back to the listener (0 accepted, 0 rejected, 0 device_intr_srq calls). The listener itself works (a self-connect was counted). The listener was bound to this host's LAN address (the analyzer is on a routed subnet reached through a subnet router, so the address was passed explicitly). The macOS firewall (node explicitly allowed, stealth mode off) was not the cause.

Confirmed by firmware disassembly (2026-11-11, private uts3036b-firmware repo). The "not ruled out" note above is now resolved: device_readstb_1_svc, device_trigger_1_svc, device_enable_srq_1_svc, device_docmd_1_svc, create_intr_chan_1_svc and destroy_intr_chan_1_svc in the V1.04.0040 SignalAnalyzer binary (aarch64, unstripped, DWARF present) are all the identical 8-instruction stub: load sp-saved args, return a function-local, zero-initialized .bss static ::result (addresses 0x62b5f38-0x62b5fa8), nothing else. None of them read the real SCPI status byte or call anything. nm -D SignalAnalyzer | grep -i clnt finds no ONC-RPC client imports at all (only svc_* server-side symbols from libtirpc), so the binary has no code path that could ever open a connection back to an interrupt listener — the earlier "could the routed path have dropped the callback" question is moot, because no callback attempt exists at the code level, not merely one that failed to land. SRQ over VXI-11 is absent on this firmware, not merely buggy. This also explains the "Front-panel lock" findings below: device_local_1_svc, device_remote_1_svc, device_lock_1_svc, device_unlock_1_svc and destroy_link_1_svc are the same 8-instruction stub (so they provably cannot affect the panel lock), and device_clear_1_svc only calls scpi_msg_reponse_buf_clear (the response buffer, not the lock). The SCPI-layer lock location is not yet found (candidate symbols: SCPI_Lock, SCPI_GetLock, SCPI_SetInputLock), tracked as a follow-up in that repo.

Action for this repo: treat device_readstb/SRQ/interrupt-channel support as permanently unsupported on this model rather than "unresolved" — poll *STB?/*ESR? for error/event bits, use blocking *OPC? for sweep completion, and do not add a supportsSrq capability flag unless UNI-T ships firmware with real ONC-RPC client imports (retest if so).

Not tried: :STATus:* registers (not in either manual; :STATus:OPERation:CONDition? / :STATus:QUEStionable:CONDition? already gave no reply), device_docmd and device_trigger, the async/abort channel (395184; only abort), HiSLIP (port 4880 closed).

The instrument was restored (*SRE/*ESE 0, continuous sweep on, :SYSTem:ERRor? clean).

VXI-11 probe retry and teardown (issue #127)

Checked through the built server over stdio (read-only: connect, get_frequency, no setting touched), five short-lived processes back to back about 3 s apart: four with UTS_TRANSPORT=vxi11 and one with auto from a cold start. Every one connected in 0.10 to 0.11 s (create_link plus status poll plus *IDN?), the auto run chose VXI-11, get_frequency took 40 to 50 ms, and closing the client took 3 to 4 ms (the server sent destroy_link before exiting). The first write of a process right after a previous one did not time out in any of the five runs. The earlier failures (portmapper connect timing out in 3 of 4 cold tries over a routed Tailscale path, and an occasional 5.5 s first-write timeout) did not reproduce on this path, so the fix is verified against fake servers (slow or dropped portmapper connect, refused create_link, a stale link that never answers) and the live check shows no regression and a clean teardown. Retry behaviour is documented in the programming guide (UTS_VXI11_PROBE_TIMEOUT_S).

Front-panel lock (2026-11-11, issue #152, firmware V1.04.0040)

Observed in a live session on a UTS3036B V1.04.0040. Manuals: UTS3000B user manual and programming manual (both from gotronik.pl/testequity). No new instrument access was used for this write-up.

What the manuals say. User manual: the front panel has a physical [Touch/Lock] key whose indicator is red when the touch screen is locked; the status bar's "connection type" area shows the mouse/USB/screen-lock state (a padlock icon); Esc returns to local mode when the instrument is in remote control. Programming manual: :SYSTem:LOCK {1|ON|0|OFF} locks/unlocks the keyboard and touch screen; :KEY:<key>:LOCK {1|0}; :KEY:LOCK? (38 keys, bit order in Appendix 2); :KEY:LED? (3 characters: TG, Single, TLOCk); :KEY:<key> simulates a key press.

What was observed.

  • The analyzer was found with :SYSTem:LOCK? = 1, :KEY:LOCK? all 1s and :KEY:LED? = 001. The front keys and the touch screen were both unresponsive, but the screen itself was live (a screenshot over VXI-11 showed a padlock icon in the status bar).
  • :KEY:LOCK? returns 44 characters, not 38.
  • Firefox had held the instrument's web interface (ports 80 and 9001) open for about 14 h from the controlling Mac; closing it later had no effect.
  • :SYSTem:LOCK 0 and :SYSTem:LOCK OFF were accepted without error but changed nothing: :SYSTem:LOCK? stayed 1, even after the touch screen worked again. VXI-11 device_local and device_unlock returned success with no change. :KEY:ESC changed nothing.
  • Per-key :KEY:<key>:LOCK 0 does change :KEY:LOCK?, but not at the position the manual's bit order gives (:KEY:TLOCk:LOCK 0 changed position 43 of 44, not position 17). After unlocking all 37 listed keys the string was 00000000000100100000011000000000001101000001: 8 positions stayed 1 and could not be cleared.
  • :KEY:TLOCk (a simulated press) changed :KEY:LED? from 001 to 000.
  • Order of events: the remote unlock attempts, then :KEY:TLOCk (LED 001 to 000), then the per-key unlocks, then the owner pressed the physical [Touch/Lock] key, then the touch screen responded, then the front-panel buttons responded (confirmed responding after the physical [Touch/Lock] key was pressed). Which step restored them is not established: the remote steps are not shown to have caused the recovery. :SYSTem:LOCK? still read 1 afterwards.
  • The error queue attributed -101/-113 to the wrong commands during these tests, consistent with the documented late and phantom errors.

Follow-up test (2026-11-11, after a power cycle, owner watching the panel). One command per fresh raw TCP connection (VXI-11 was not separately isolated):

Sent Front panel
nothing (connect only) not locked
a single *IDN? not locked
a single :SYSTem:LOCK? not locked
a single :KEY:LOCK? (twice) touch screen and all buttons unresponsive
a single :KEY:LED? same
a single :SENSe:FREQuency:CENTer? same

Recovery was the physical [Esc] key (user manual: "If the instrument is in remote control mode, press [Esc] to return to local mode"); [Touch/Lock] alone also freed it once earlier. So the trigger is not specific to :KEY:: the first non-common command or query seems to put the analyzer into remote mode, which locks the front panel until Esc is pressed. This is probably normal remote behaviour. The defects are that the standard ways back to local do not work: :SYSTem:LOCK 0/OFF, VXI-11 device_local/device_unlock and a simulated :KEY:ESC all leave it in remote mode. The :SYSTem:LOCK?, :KEY:LOCK? and :KEY:LED? readbacks are unreliable lock indicators (:KEY:LOCK? returns 44 characters and becomes all 1s when remote); do not use them as lock checks.

Still unknown. Exactly which other headers trigger remote mode (only the five above were tested individually), and whether VXI-11 behaves the same. The original lock's cause was probably a :KEY: (or other non-common) query during manual probing; the earlier web-interface theory is unsupported.

Conclusions.

  • The cause of the original lock was most likely a :KEY: query sent during manual probing (see the follow-up test); the web-interface theory is unproven.
  • :SYSTem:LOCK does not behave as documented on this firmware (the unlock setters are no-ops and the query did not track the real lock state).
  • The :SYSTem:LOCK? manual text has a typo ("0 = unlocked / 1 = unlocked"); the 44-character :KEY:LOCK? bit order differs from the manual.
  • The SCPI denylist is unchanged (issue #155: the trigger is not specific to :KEY:, so a :KEY: denylist would not address it). The connect and disconnect results tell the user to press Esc.
  • Why :KEY:LOCK? is 44 characters, not 38 (confirmed by firmware disassembly, 2026-11-11, private uts3036b-firmware repo): it prints one character per entry of the firmware's own key list (dev_input::entity_key_ll), which has 44 entries (23 fixed codes, then 0x80-0x94), not the 38 in the manual's Appendix 2. Of the 8 positions this repo found stuck at 1 above, 5 (+, DEL, TAB, and two unlabeled codes) have no :KEY:<key>:LOCK command at all — only the global unlock (Esc or :SYSTem:LOCK 0) can clear them, which fits "8 positions stayed 1 and could not be cleared" above exactly. The other 3 (TRIGger, FILe, MMEas) do have lock commands; they are probably just missing from the manual's key list, not actually stuck. Confidence: CONFIRMED for the length, order and the position mapping (disassembly plus the :KEY:*:LOCK command table; :KEY:TLOCk:LOCK 0 landing at position 43 matches the live result above). Reinforces the existing rule: never use :KEY:LOCK? as a lock indicator — it structurally can't read all-zeros through per-key commands alone.

Recovery. Press the physical Esc key on the analyzer (leaves remote mode). [Touch/Lock] alone also freed the panel once. Power-cycling is a last resort and resets settings. The remote ways back to local do not work (above).

Detecting the lock automatically (2026-11-11, issue #172). A screenshot over VXI-11 shows the state in the status bar of the 1280x800 screen (the area around x 765-820, y 755-770): unlocked it holds only the connection (monitor) icon at x=785; locked, a padlock icon appears at x=787 and the connection icon moves to x=767, followed by a mouse/arrow icon at about x=803. src/panelLock.ts matches those two icons against reference crops from real captures (zero-dependency PNG/BMP decoding, a few pixels of shift tolerated) and answers locked, unlocked or unknown; scripts/check-panel-lock.mjs takes the screenshot (one :DISPlay:DATA?, no setter, any picture format; BMP is 4,096,054 bytes and arrives complete over VXI-11) and exits 0, 1 or 2. Measured live: with the panel just returned to local by Esc, a VXI-11 link plus :DISPlay:DATA? alone did not lock it (screenshot shows the connection icon only); after one :SENSe:FREQuency:CENTer? the same screenshot shows the padlock. Superseded (2026-11-12, re-tested live, see "Live-verified end to end" below): this one result was a timing race, not true passivity. A later live run with the owner present caught the same check reporting LOCKED on a second call right after an unlocked read, confirming the screenshot query re-locks the panel like any other non-exempt command. Follow the "SCPI-layer root cause" paragraph's guidance, not this measurement.

Ways back to local tried over VXI-11 (2026-11-11, issue #172), padlock checked from a screenshot after each; none worked. Starting state: locked (after one ordinary query).

Tried Result Padlock after
close the link and open a new one (done several times between the steps) accepted still locked
device_remote (16) then device_local (17) both returned error 0 still locked
device_clear (15) returned success still locked
create_link with lockDevice true, then device_unlock (19), destroy_link all error 0 still locked

Together with the earlier results (:SYSTem:LOCK 0/OFF, device_local, device_unlock, a simulated :KEY:ESC) there is no known automatic way back to local on V1.04.0040; only the physical Esc key works. Not tried, by rule: *RST, :SYSTem:DEFault, any :KEY: command, LAN setters, :SYSTem:LOCK ON. The server therefore keeps telling the user to press Esc, but a live run should first check the screenshot and say "press Esc" only when the panel is locked.

Confirmed by firmware disassembly (2026-11-11, private uts3036b-firmware repo): device_local, device_remote, device_lock, device_unlock and destroy_link in the VXI-11 server are stubs that return success without doing anything (see "VXI-11 SRQ" above for the exact evidence), and device_clear only clears the response buffer. This confirms, at the code level, why none of the VXI-11-side recovery attempts above could ever have worked — it is not a timing or sequencing issue on our side. The lock state itself lives in the SCPI layer (not yet located; that repo is continuing with candidate symbols SCPI_Lock/SCPI_GetLock/SCPI_SetInputLock), so there may still be an undocumented SCPI command that clears it even though no VXI-11 call can.

SCPI-layer root cause found (2026-11-11, private uts3036b-firmware repo, follow-up to the above). The lock is one byte, run_state.remote_lock, written only by parm_system::set_remoteLock(bool). Before executing every SCPI command, scpi_server::remote_lock() checks that byte; if it is clear and the uppercased command doesn't contain *IDN, SYSTEM:LOCK/SYST:LOCK or SYSTEM:INFO/SYST:INFO, it re-locks the panel. There is no separate "remote mode" that latches — every non-exempt command re-locks it on the spot. The physical Esc key, :SYSTem:LOCK 0/OFF, and (very probably) :KEY:ESC all do reach set_remoteLock(false) and do unlock — they only looked like no-ops above because the very next ordinary command re-locked it immediately, and :SYSTem:LOCK? never read the real byte (it ORs ~44 unrelated key states, not the lock flag). Confidence: CONFIRMED (disassembly, DWARF struct names, and a .text scan for writers of the byte) for the mechanism, the exemption list, :SYSTem:LOCK 0 unlocking, and :SYSTem:LOCK? not reading the real state. LIKELY, not independently live-verified: :KEY:ESC unlocking, and the exact timing of the re-lock race.

Live-verified end to end (2026-11-12, owner physically present at the analyzer). The full lock/re-lock cycle predicted above was run step by step and confirmed by the owner watching the panel: from a locked state, :SYSTem:LOCK OFF alone unlocked the touch screen (confirmed by touch); a following *IDN? left it unlocked (exempt command); a following :SENSe:FREQuency:CENTer? (an ordinary non-exempt query) re-locked it, confirmed visually. The owner then pressed Esc; scripts/check-panel-lock.mjs reported unlocked on the next successful read. Run again moments later, it reported LOCKED (padlock icon, 100% match) — confirming the screenshot check re-locks the panel itself, exactly as predicted (see the next paragraph). This upgrades the mechanism, the exemption list, :SYSTem:LOCK 0 unlocking, and the screenshot check's self-locking from LIKELY/predicted to CONFIRMED by live test. :KEY:ESC specifically was not exercised this run (only the physical Esc key and :SYSTem:LOCK OFF were used) and stays unverified.

This contradicts the "safe check" claim below — and is now confirmed live, not just by disassembly. The screenshot query :DISPlay:DATA? used by scripts/check-panel-lock.mjs is not exempt, so per this disassembly (and the live test above) it does re-lock the panel on every call — it only looks passive because it captures the framebuffer before the lock overlay is drawn, so the image it returns shows the panel as it was just before that same call locked it. The one live test behind the "did not lock it" claim two paragraphs below almost certainly caught a timing race (the UI thread hadn't redrawn yet), not true passivity. Practical effect: check-panel-lock.mjs's own exit code is still correct (it reports the state as of just before the check), but running it right after an Esc will usually re-lock the panel again, and it cannot be assumed harmless. To leave the analyzer unlocked at the end of a session: send :SYSTem:LOCK OFF as the last command and nothing after except exempt commands (use *IDN? as a sync point instead of *OPC?; avoid :SYSTem:INFo?, it returns the serial number).

Rule for live runs. Do not send :KEY: commands or queries and do not use the lock readbacks as checks. At the end of every live run run node scripts/check-panel-lock.mjs (a screenshot check, no setters) and tell the owner "press Esc on the analyzer to return it to local mode" only if it reports LOCKED. See AGENTS.md and docs/releasing.md.

Simulator conformance and the live gap list (2026-11-11, issues #147 and #25, firmware V1.04.0040)

One UTS3036B V1.04.0040 (the newest published firmware per UNI-T's download page; *IDN? does not show the hardware revision, HW1.0 or HW2.0), reached over a routed path. Procedure: docs/releasing.md "Simulator conformance". The 65-step exchange was run against the analyzer (raw TCP) and the simulator; after the fixes below both give the same transcript (tests/fixtures/conformance/exchange.txt, recorded from the analyzer and redacted), two further runs gave 0 differences, and every setting was restored and the error queue was clean.

Simulator mismatches found and fixed:

  • Reply format. Frequency-like replies (center, span, start, stop, center step, offset, RBW, VBW, marker X) have 10 decimals (1.0000000000e+09 ), other numbers 6 (1.000000e+01 ); a trailing space replaces the sign position, so every non-negative number ends in a space and negative ones do not (-2.500000e+01). The simulator printed 6 decimals and no space.
  • Span versus center. Setting the span caps it at 3.61 GHz and moves the center to keep start >= 0 and stop <= 3.61 GHz (center 1 GHz, span 10 GHz: span 3.61 GHz, center 1.805 GHz, start 0; span 3 GHz at 1 GHz: center 1.5 GHz; span 500 MHz at 3.58 GHz: center 3.36 GHz). Setting the center shrinks the span instead (center 100 kHz: span 200 kHz; center 1 GHz with span 3.59 GHz: span 2 GHz). The simulator shrank the span in both cases.
  • Invalid enum value (:UNIT:POWer, :CALCulate:MARKer1:MODE, :TRACe2:TYPE, :SENSe:DETector:TRACe1 with BOGUS): two -224 entries, not one. A bad suffix gives -224 then -131 as before.
  • :TRACe:SELect 7 and 9: no error, value unchanged (above).

Gap list from #25:

  • VXI-11 READ for ACPR, TOI, C/N and harmonics: works. measure and read_measurement for all four over VXI-11 returned results with command: "READ", FETCh returned the same shape, every restore verified. Values were noise-floor numbers, see next point.
  • Results with a real signal: not reachable with the tracking generator (RESOLVED 2026-11-12 with an external source, see below). In spectrum analyzer mode the generator is wired to the RF input and gives a flat trace (full span -30.8 to -27.8 dBm at -30 dBm output; 1 GHz span 1 MHz -29.3 dBm). But entering a measurement (:CONFigure:<meas>, so measure and configure_measurement) switches the generator output off (:OUTPut:STATe? read 0 afterwards) and :OUTPut:STATe is undefined (-113) while a measurement is active, so it cannot be turned on there either (set_tracking_generator then reports a query timeout and the "probably no tracking generator option" message, which is misleading in this case). The measurements therefore saw only noise (C/N about 0 dB, carrier about -72 dBm, ACPR dBc about 0, TOI -81 dBm, harmonic 2 +4.7 dBc). measure did not restore the generator output (issue #161, fixed): measure and configure_measurement now read :OUTPut:STATe? (and level and offset when on) before entering, and measure/exit_measurement re-send level, offset and :OUTPut:STATe ON after :CONFigure:SANalyzer and verify by readback (one resend, then an error naming the field); set_tracking_generator and get_tracking_generator say a measurement is active when this server entered one (nothing is sent) instead of blaming a missing option. An analyzer that does not answer :OUTPut:STATe? before entering is treated as having no option (nothing saved). The simulator models the switch-off and the undefined :OUTPut:STATe.
  • Signal-present measurement results: RESOLVED (2026-11-12) with an external CW source. The analyzer's own tracking generator cannot supply this: the firmware switches its output off the moment a measurement is entered (above), and separately a tracking generator by design tracks the sweep frequency, so it can never hold still at one frequency the way a real signal does. A Siglent SDG1032X (an external function generator the owner had on the bench) was connected to the RF input instead: CH1 set to a 10 MHz CW tone, -30 dBm, 50 ohm load. The analyzer (center 10 MHz, span 2 MHz) confirmed the signal with a marker peak search (X 9.998 MHz, Y -29.9 dBm, matching the generator setting). With that signal present: ACPR -29.71 dBm main, -37.21/-42.77 dBc lower/upper (previously ~0 dBc noise floor); C/N -29.69 dBm carrier, -71.81 dBm noise, 42.12 dB C/N (previously 0 dB); TOI LOW:TRD?/UP:TRD? returned structured non-degenerate four-value replies (previously flat -81 dBm noise-floor numbers) — a single CW tone has no genuine third-order intermodulation products, so the TOI numbers themselves aren't physically meaningful here (that needs two closely-spaced tones), but this confirms the FETCh path and parsing work correctly against a present signal, which was the actual gap; harmonics AMPLitude:ALL?/DISTortion? returned structured, non-identical values, though the reported fundamental amplitude (-81.9 dBm) is surprising for a 10 MHz/-30 dBm input and is noted here as an open oddity, not fully explained. Cleanup: generator output switched off, analyzer error queue clean, panel left unlocked via :SYSTem:LOCK OFF + *IDN? sync.
  • Frequency counter with a carrier: works in zero span with the generator on. Zero span at 1 GHz, generator on (a CW carrier at the center): :CALCulate:MARKer:FCOunt:X? read 999 999 999.8184 Hz (0.18 Hz low). With the generator off the counter returned a noise-driven 999 821 741 Hz. In a swept span the counter read 0 (a flat swept trace has no carrier).
  • N-dB bandwidth with a narrow signal: RESOLVED (2026-11-12) with the same external SDG1032X source. The analyzer's tracking generator's sweep gives a flat trace at any span or RBW (it can't hold a fixed frequency), so the result was always nan (bandwidth_hz: null). With the SDG1032X's 10 MHz CW tone present (same setup as above), :CALCulate:MARKer1:BANDwidth:NDB -3.01 then :RESult? returned 30268 Hz — a real, non-nan value, confirming the measurement works correctly against a genuinely narrow signal.
  • discover_instruments: the directed-broadcast discovery found nothing (portmapper broadcasts do not cross the routed path to the analyzer); connect with the explicit address and identify work. Unicast probing is not part of the tool.
  • Not done here: the Claude Desktop install of the .mcpb (needs the desktop app).
  • Also seen: :CALCulate:MARKer1:BANDwidth:NDB is not touched by the tools' own restore (reads -3 after a test; the original -3.01 was put back by hand).

Installed options and AD results readback (2026-11-11, firmware V1.04.0040)

This owner's unit has the EMI and analog-demodulation (AD) options installed. Confirmed live: :INSTrument EMI and :INSTrument AD each actually switched the mode (:INSTrument? read back the new mode; :SYSTem:ERRor? stayed 0,"No error" both times), then :INSTrument SA restored it. No documented or undocumented SCPI query reports installed options directly: *OPT? returns the exact same string as *IDN? (confirmed live, and by firmware disassembly in the private uts3036b-firmware repo: its callback is literally SCPI_CoreIdnQ, the *IDN? handler). The mode-switch itself is therefore the only way to probe licensing, and it is safe to do so: unlicensed modes are refused silently inside the firmware (logged, no SCPI error, mode unchanged), per the same disassembly.

:FETCh:AM#? / :FETCh:FM#? / :FETCh:PM#? (undocumented, confirmed live) return AD's numeric results — depth/deviation, SINAD, THD, carrier power — without a screenshot. The suffix selects what comes back (0 = RF spectrum trace, 1/bare = a 15-value metrics line, 2 = demodulated waveform min-hold; 3-6 untried individually but same code path per disassembly). Full field order and a live example are in docs/research/analog-demodulation.md section 6a. This answers that research doc's open "AD results investigation" question (proposal item 7) ahead of implementing any of the proposed tools.

Caution found live: immediately after an :INSTrument <mode> round trip (set, then read back), sending a second command on the same TCP connection once got no reply at all; a fresh connection showed the instrument had in fact already applied it correctly. Use a fresh connection or a couple of seconds' wait after a mode switch before the next command, same caution as after a sweep.

get_demod_result (#196) wraps this readback as a tool: the bare :FETCh:AM?/:FETCh:FM? form (suffix 1, confirmed equivalent to the default), parsed into the 15 named fields above. Built test-first against a dedicated fake (tests/integration/ad-results-tools.test.ts), not independently re-verified against this fake beyond the one live capture already cited.

Measurement function module tools (2026-11-11, issue #177, firmware V1.04.0040)

get_instrument_module and select_instrument_module wrap :INSTrument[:SELect] {SA|EMI|AD} per the findings above: no query reports which module is licensed, an unlicensed-module switch is refused silently (no SCPI error, module unchanged, per the firmware-disassembly finding), and :INSTrument:SELect? answers SA while a measurement sub-mode is active too (see "Measurements"). The tools cannot distinguish "unlicensed" from "the setter was accepted but the readback raced the switch" or a link glitch, so a readback that does not match the request is a warnings entry, never an error or a guessed cause.

ASSUMED in the fake analyzer (tests/fakeUts.ts, not independently verified beyond the live mode-switch findings already cited above): every module always switches when requested (the fake does not model licensing), the switch has no settle delay, and the earlier connection-level stale-reply quirk is not reproduced (it is timing-dependent and outside what the deterministic fake models). Live-testing the EMI/AD modules further is tracked as a separate hardware-verification pass.

Full SCPI command and stub audit (2026-11-11, private uts3036b-firmware repo, firmware V1.04.0040)

The private firmware-RE repo checked the handler code of all 1262 SCPI commands (not just the VXI-11 side covered above). Five handlers do nothing real: *TST? writes no result (should get no reply at all); the PNA-mode [:SENSe]:LPLot:ODRive and ...:ODRive:ATTenuation:ZERO setters are no-ops whose query always reads 0; :CALibration:CARD? always returns the same hard-coded string; CALibration:ABUS:READ? always returns 0. Every other handler does real work.

Commands are registered per mode, and per measurement. A header belonging to another mode gives -113; a header with only a setter gives no reply to its ? form. This explains several earlier "unsupported" results in this file: :INITiate:STOP/PAUSe/RESume exist only in EMI mode, :INPut:IMPedance only in VNA mode, :CALCulate:MATH is set-only, and :FCOunt has an :X query but no :Y query. Treat "-113 or no reply" as "not registered in the current mode, or set-only," not as "missing option" — this is a better explanation than the option-licensing theory for commands that aren't mode-gated by an actual license.

New hazard, confirmed by disassembly: SYSTem:INFo has a setter (SCPI_WriteInfo) that rewrites factory model/bandwidth configuration when a debug flag is on. Added to the denylist in src/scpi.ts along with FACTORY:*, CALibration:*, :SERVice:LATCh:VALue, MOUSE and :MMEMory:*:CALibration (none of these were tested live; the denylist additions are precautionary, based on the disassembly alone). SYSTem:INFo? and SYSTem:COMMunicate:MAC? replies contain identifiers and must not be logged.

Confidence: CONFIRMED that the five handlers above do nothing or return constants (a classifier script plus manual disassembly of each). LIKELY, not independently verified: per-mode command registration (the registrar function names are clear, but the mode-switch code that installs each set wasn't traced), and the SYSTem:INFo setter being a genuine factory write (the calls are clear, the parameter format wasn't decoded).

Not yet actioned: a clean per-command reference (mode, set/query, documented?, stub?, description) exists in the firmware repo as docs/scpi-command-reference.md; folding a condensed version of the undocumented-command list into this repo's own docs, or building a per-mode command allowlist from it, is tracked as follow-up work, not done in this pass.

AD live probe (issue #190, partial, 2026-11-11, firmware V1.04.0040)

SA state survives the AD module round trip without any explicit restore. Recorded :SENSe:FREQuency:CENTer? (1.805 GHz), :SENSe:FREQuency:SPAN? (3.59 GHz), :SENSe:BANDwidth:RESolution? (3 MHz), :DISPlay:WINDow:TRACe:Y:SCALe:RLEVel? (0 dBm) and :SENSe:POWer:RF:ATTenuation? (10 dB) before :INSTrument:SELect AD, then switched to AD, ran several AD getters, switched back with :INSTrument:SELect SA, and re-read the same five: every one came back identical with no setter resent. This confirms the "each module has its own settings" statement in docs/research/analog-demodulation.md section 1 means SA's own settings are preserved internally by the firmware across the trip, not merely that AD defaults are independent — enter_analog_demod/exit_analog_demod (issue #192) likely do not need the save/restore-by-readback machinery exit_measurement uses, though a confirmation run is worth doing once hardware access is stable (see caveat below).

Documented AD getters confirmed answering with real values, fresh connection per command, module already AD: :AM:FREQuency:SPAN? (200 kHz), :AM:BANDwidth:CHANnel? (20 kHz), :AM:BANDwidth:RESolution? (1.9 kHz), :AM:AFSPectrum:FREQuency:STARt?/:STOP? (0 Hz / 20 kHz), :AM:DWSWeep:TIME? (2 ms), :AM:BPFilter? (OFF), :INITiate:CONTinuous? (1), :SENSe:CORRection:IMPedance:INPut:MAGNitude? (50). These match the manual's documented syntax (section 2) and are safe, read-only confirmations.

Not completed — hardware/network became unreliable partway through this session's probe. :CONFigure?, :AM:DEMod:TIME?, :AM:AVERage:STATe?/:COUNt?, and (on retry) even baseline commands like *IDN? and :INSTrument:SELect? started timing out on fresh connections with no data at all — including repeats of commands (:INITiate:CONTinuous?) that had just answered successfully moments before in the same mode, which rules out firmware rejection and points to a flaky network path to the analyzer rather than firmware behavior. TCP connect times to the analyzer varied from 4 ms to 7+ seconds to never-completing within this session, independent of command content.

Caution — panel lock state unconfirmed at end of this probe session. The closing :SYSTem:LOCK OFF and a sync *IDN? both timed out (no reply), including on a generous 25 s retry. Earlier reads in the same session confirmed the panel was being used in remote mode as usual; whether it ended up locked or unlocked when connectivity dropped is unknown. Treat this unit as possibly still locked until someone checks the physical panel or a VXI-11 screenshot succeeds; press Esc if so.

Not yet probed (left for a follow-up session with stable connectivity and the owner present): :CONFigure:AM/FM and :NDEFault variants (destructive, must be probed carefully), demod/average timing getters, the marker-based depth/deviation readout versus the :FETCh:AM#? readback already confirmed above, audio/speaker commands (deliberately not sent — risk 3 in the research doc), and settle-time measurement for the module switch itself.

Confidence: CONFIRMED for the SA-state-preservation finding and the getter values listed. UNKNOWN (not a firmware finding) for which exact commands would time out under normal network conditions — this probe's timeouts are attributed to the network path, not to the instrument.

Actionable for uni-t-uts-mcp: issue #192 (enter_analog_demod/exit_analog_demod) can likely skip explicit SA-state save/restore and instead verify-by-readback as a safety net, per the confirmation above. Issue #190 should be reopened/followed up for the remaining untried getters and a clean lock-state confirmation once the network path is stable; this entry documents partial progress, not a full close-out.

Follow-up, closing out #190's remaining gaps (2026-11-12, owner present, network path stable this time):

  • :CONFigure? in AD mode answers AM — confirms get_ad_settings's design of reading this demod-type prefix fresh on every call (rather than caching it) is the right call, since there is no other way to know it.
  • Demod/average timing getters, using the prefixed forms the AD tools actually send (not a bare :SENSe: form — see caution below): :AM:DEMod:TIME? → 7.200000e-02 (72 ms), :AM:DEMod:TIME:AUTO? → 1 (on), :AM:AVERage:COUNt? → 10.
  • Caution, not a bug: the bare :SENSe:AVERage:COUNt?/a bare :SENSe:DEMod:TIME?-style form gets no reply at all in AD mode (same silent-timeout pattern as other unsupported commands elsewhere in this project) — AD's averaging/demod-timing fields live under the :AM:/:FM:/:PM: prefix, not bare :SENSe:. Checked src/tools/adSettings.ts and sibling AD tool files: they already use the correct ${p}:... prefixed form throughout, so there was nothing to fix. The bare :SENSe:AVERage:COUNt? form used in src/tools/trace.ts is correct there — that tool is SA-only (guarded by the module guard, #178) and the bare form is SA's own syntax, not a mismatch.
  • Panel-lock confirmation at session end: clean. After switching back to SA, sending :SYSTem:LOCK OFF then a *IDN? sync (which answered), scripts/check-panel-lock.mjs independently reported unlocked: the front panel is usable (status bar recognized (connection icon match 100%) and no padlock icon (match 52%)). No Esc needed.
  • Timing nuance worth knowing: immediately after the module switch and the three getters above (back-to-back, each its own fresh TCP connection but only ~0.3-1 s apart), :SYSTem:ERRor? kept returning a stale -113 "Undefined header" on five consecutive checks without ever draining — even though the commands just before it had answered with clearly valid data (72 ms, 10, etc.), ruling them out as the cause. Waiting ~5 s and retrying with 1 s gaps drained it to 0,"No error" immediately, and it stayed clean on 6 further checks. This looks like a timing-sensitive variant of the LIFO stale-error-stack behavior already documented under T04a, not a new bug — but don't trust a -113 read immediately after a burst of other commands without pausing and re-reading first.

Confidence: CONFIRMED (live) for all values and behaviors above. This closes #190's three remaining gaps; the AD proposal chain (#190-#197) is now fully complete.

EMI limit lines: point cap inferred, not live-verified (issue #182, 2026-11-12)

The analyzer was unreachable for a live probe (a TCP connect-scan of the local subnets found other bench instruments but not the UNI-T unit), so set_emi_limit_line's 10-point cap was not tested directly. It is instead inferred from the firmware's own command-callback audit (private uts3036b-firmware repo, docs/scpi-command-reference.md): the EMI-prefixed :CALCulate:FSCan:LLINe<n>:DATA/:CONTrol[:DATA]/:UPPer[:DATA]/:COPY/:BUILd commands and the plain :CALCulate:LLINe<n> ones used by set_emi_limit_line (and already by set_limit_line) share the exact same callback function addresses (SCPI_SetLimitData, SCPI_SetLimitXData, SCPI_SetLimitYUpData, SCPI_BuildFromTrace, parm_meas::set_limitCopy). Since set_limit_line already verified a 10-point cap live against this same implementation, the EMI module's cap is very likely identical — but this has not been independently confirmed on real hardware.

Confidence: CONFIRMED (disassembly) that the callback functions are shared. LIKELY, not independently live-verified: that the point cap therefore matches SA's.

Actionable for uni-t-uts-mcp: re-verify the EMI limit-line point cap live once hardware access is stable (EMI-12, #187), and drop the "inferred" caveat from set_emi_limit_line's code comment and docs/tools-reference.md if it's confirmed to match.

EMI correction factors: 10-point cap live-verified, silent rejection above it (issue #183, 2026-11-12)

Confirmed live (this unit, EMI module licensed): :SENSe:CORRection:CSET1:DATA with 10 frequency/gain pairs round-trips exactly on readback. Writing 11 pairs is silently rejected — no error on *ESR?, *STB? or :SYSTem:ERRor? (same silent-reject pattern already documented for other out-of-range setters) — and leaves the slot's DATA? silent afterward, the same way an empty slot behaves, meaning the rejected write emptied the slot's data rather than leaving the previous 10 points in place. [:SENSe]:CORRection:SELect was not needed: writing and reading CSET<n>:DATA/:STATe directly, by number, worked without ever sending SELect.

The live probe also hit the known flaky-network pattern (several fresh-connection queries timed out in a row after the 11-point write, recovering a few seconds later) — attributed to the network path, not the instrument; *IDN? kept answering normally once the path recovered.

Confidence: CONFIRMED (live) for the 10-point cap and the silent-rejection behavior above it. CONFIRMED (live) that SELect is unnecessary for direct CSET<n> addressing.

Actionable for uni-t-uts-mcp: set_emi_correction (#183) refuses a request over 10 points locally, before sending anything, rather than relying on the instrument's silent rejection.

EMI live probe: module entry, SA state round trip (2026-11-12, issue #176, owner present)

Ran the batched live probe from docs/research/emi-function.md section 8, with the owner physically present at the analyzer, over raw SCPI:

  1. *IDN? confirmed the unit (UNI-TREND,UTS3036B,<serial>,V1.04.0040).
  2. :INSTrument:SELect? read SA. SA snapshot taken: center 50 Hz, span 100 Hz, RBW 1 Hz, reference level 0 dB, attenuation 10 dB.
  3. :INSTrument:SELect EMI, waited ~3 s, then :SYSTem:ERRor? read 0,"No error".
  4. :INSTrument:SELect? read back EMI. Two EMI-only queries on fresh connections both answered: :SENSe:EMC:STANdard? → CISPr, :SENSe:FSCan:SEQuence? → SCAN.
  5. Per the research doc's own decision table ("select answers EMI and the EMI queries answer" → EMI available), this confirms the EMI module can be entered and its option is licensed on this unit — consistent with, and now the formal source for, what every downstream EMI tool issue (#177-#187) already assumed.
  6. :INSTrument:SELect SA switched back; :INSTrument:SELect? confirmed SA. The SA snapshot was re-read and matched the pre-probe values exactly (center 50 Hz, span 100 Hz, RBW 1 Hz, reference level 0 dB, attenuation 10 dB) — the SA↔EMI round trip does not disturb SA state. :SYSTem:ERRor? read 0,"No error" after the round trip.

This probe deliberately does not touch the front-panel lock (by design, to observe default behavior unmodified), so the owner was asked to check the panel afterward rather than relying on an automatic unlock. The owner reported the screen showing "EMI," matching the SCPI state exactly — an initial impression of "SA sweeping" turned out to be EMI's own frequency-scan display, which looks visually similar to an SA sweep but is a different module, not a discrepancy.

Confidence: CONFIRMED (live, owner present) for EMI module entry, the two EMI-only query replies, and the SA-state round trip being lossless.

Actionable for uni-t-uts-mcp: none beyond what's already shipped — every EMI tool issue this probe was a prerequisite for (#177-#187) is now built and separately hardware-verified.

EMI hardware verification pass (2026-11-12, issue #187, owner present)

Ran every merged EMI tool (module switch, settings snapshot, scan table, measurement, limit lines, correction factors, markers, signal list) against the real analyzer, with the owner physically present. emi_run/emi_stop (#186) was not yet merged during this pass and was not verified here.

Real bug found and fixed: EMI limit-line point cap is 12, not the inferred 10. The earlier inference (#182) assumed the EMI module's cap matched SA's because the firmware shares the exact same :CALCulate:LLINe<n>:DATA callback. Live-tested directly: 10, 11 and 12 points all round-trip exactly; 15 and 20 points are both silently truncated to the same 12. set_emi_limit_line's MAX_POINTS constant and its description were wrong and are now corrected to 12 (shared callback does not imply a shared cap — the EMI module evidently carries its own limit separately).

Real bug found and fixed: EMI marker X/Y readback used the wrong command form and would always fail. The manual's own text (and #184's reading of it) suggested the plain, non-FSCan-prefixed :CALCulate:MARKer<n>:X?/:Y? for numeric marker readback — the only documented numeric readback in the EMI chapter without the FSCan prefix other tools use. Live-tested directly, repeatedly, with a clean error queue each time: the plain form gets no reply at all on a query (same silent-timeout pattern as other unsupported commands) and -113 Undefined header with no effect on a set; the FSCan-prefixed form (:CALCulate:FSCan:MARKer<n>:X?/:Y?/set) works correctly for both. get_emi_marker/set_emi_marker used the plain form and would have silently hung (then errored) on every call that needed a marker's position — fixed to use the FSCan-prefixed form throughout.

Real bug found and fixed: get_emi_settings's default groups always errored. The tool's default group set included trigger, but three of its four trigger fields (:TRIGger:SEQuence:SOURce?, :EXTernal:SLOPe?, :EXTernal:DELay?) get no reply at all in EMI mode — only :VIDeo:LEVel? answers. Since the tool's contract is "a silent query is an error naming the command, never partial data," every ordinary (no-arguments) call to get_emi_settings would fail. trigger is now excluded from the default group set, the same way the expensive scan_table group already was; it can still be requested explicitly, where it will reliably error (documented, not fixed, since the firmware genuinely does not answer those three fields in this module).

Spot-checked and confirmed working as documented: get_emi_settings's standard/sequence/meters/search/peak-criteria/frequency/amplitude fields (standard CISPr, sequence SCAN, meter dwell 10 ms, average count 100/type LOG, search mode PONLy, peak threshold/excursion, meter center/RBW/attenuation) all answered correctly and matched their documented ranges. set_emi_scan_table's band setter (:SENSe:FSCan:SCAN<n>:STARt) and set_emi_measurement's detector setter (:SENSe:FSCan:DETector:TRACe<n>) both applied and read back correctly, with state restored afterward. get/set_emi_correction's 10-point cap (confirmed in #183) and module switching (SA state surviving the round trip) continued to behave exactly as previously documented.

Confidence: CONFIRMED (live) for the EMI limit-line 12-point cap, the marker X/Y FSCan-prefix requirement, and the trigger group's three silent fields. CONFIRMED (live, spot-check) for the settings/scan-table/measurement fields listed above; not every field or every scan-table band was individually exercised.

Actionable for uni-t-uts-mcp: with this pass and emi_run/emi_stop (#186, below) both merged, the EMI proposal chain (#176-#187) is complete.

EMI run control: cold scan stalls 25 s+, STOP/PAUSe/RESume accepted, :INITiate2:CONTinuous? silent (issue #186, 2026-11-12)

Live-verified with the owner present. Switched into EMI (:INSTrument:SELect EMI, confirmed by readback), sequence SCAN. Sending :INITiate:IMMediate on a cold/first scan left the instrument silent to every command, including :SYSTem:ERRor?, for over 25 seconds - not an error reply, literally no reply at all, the same "stall" pattern already documented elsewhere in this project for other long operations. After roughly 25-30 s it answered again on its own with a clean error queue, matching the manual's note that a scan-only sequence stops by itself when it finishes. The owner, watching the panel, confirmed it showed "EMI" and an active scan/sweep-like display during this window (visually similar to an SA sweep, just inside the EMI module).

On a second scan started shortly after the first completed, the instrument stayed fully responsive the whole time: :INITiate:PAUSe and :INITiate:RESume, sent shortly after :INITiate:IMMediate, both completed instantly with :SYSTem:ERRor? returning 0,"No error" after each - STOP/PAUSe/RESume are accepted without an instrument error in EMI, unlike SA where all three give -113 ("Undefined header", already documented). Whether PAUSe actually paused anything was not independently confirmed (the second scan may have already been near completion); treat this as "accepted without error", not as "confirmed to work".

:INITiate2:CONTinuous? got no reply at all in EMI mode, tried multiple times with retries - the same silent-timeout pattern as the cold-scan stall, not an error reply. This query appears unsupported/silent in EMI, unlike in SA where it answers.

Restored cleanly afterward: :INSTrument:SELect SA confirmed, center/span matched the pre-probe values, error queue clean, panel left unlocked via :SYSTem:LOCK OFF + a *IDN? sync (no Esc needed).

Confidence: CONFIRMED (live) for the cold-scan stall duration and self-recovery, for STOP/PAUSe/RESume being accepted without an instrument error, and for :INITiate2:CONTinuous? being silent. NOT independently confirmed: that PAUSe/RESume actually change scan behavior (no visual or state check beyond the clean error queue).

Actionable for uni-t-uts-mcp: emi_run cannot assume a quick reply - it sends :INITiate2:CONTinuous OFF unconditionally (its prior state cannot be read first) then :INITiate:IMMediate/:INITiate:FSCan:CLEar:IMMediate, and infers completion by polling *OPC? until anything answers again, within timeout_s (default 45 s, given the one observed 25 s+ stall). A timeout is reported as finished: false, not as an error - silence during a scan is expected, not a fault. emi_stop sends :INITiate:STOP with no read-back to confirm it halted anything. PAUSe/RESume are not exposed as tools (outside this issue's scope, and not independently confirmed to do anything observable).

AD research-stage tools: live verification pass (2026-11-12, firmware V1.04.0040)

Live-verified every remaining research-stage AD tool against the real analyzer, with an external Siglent SDG1032X (10 MHz CW, -30 dBm) connected to the RF input for the marker tests.

Real bug found and fixed: set_ad_filters's mutual-exclusion rule was wrong. The tool refused more than one of low_pass_filter/high_pass_filter/band_pass_filter per call, assuming all three were mutually exclusive. Live-tested directly: low-pass and high-pass can be set together — setting one does not turn the other off (confirmed both orders: LP then HP, and HP then LP, each leaving the other unchanged). Only band-pass is mutually exclusive with both — setting band-pass turns off any active low-pass and high-pass, and setting either low-pass or high-pass turns off an active band-pass. validateFilters now only refuses band_pass_filter combined with either of the other two; low_pass_filter and high_pass_filter together now send both commands in one call. The per-filter code comments ("setting this turns the band-pass filter off" / "...turns the low-pass and high-pass filters off") were already correct and needed no change.

set_ad_display confirmed working: window 1 (RF spectrum) :DISPlay:AM:WINDow1:TRACe:Y:RLEVel set/readback round-tripped exactly (0 dBm -> -10 dBm -> restored to 0). Window 3's linear/log spacing setter/getter round-tripped exactly (default was LOGarithmic, not assumed LINear; set to LOGarithmic again, restored to LINear). Window 2 was not separately exercised.

AD marker tools confirmed working, against a real signal: switched AD's center to 10 MHz to match the external generator, set marker 1 to normal mode on the rf_spectrum trace, ran a peak search (:MAXimum) — it found the generator's tone exactly (X = 1.0000e+07, Y = -29.94 dBm, matching the -30 dBm setting within normal ripple). Marker 2 set to delta mode with marker 1 as its reference marker worked (reference readback confirmed, X/Y relative to marker 1 as expected). :CALCulate:AM:MARKer:AOFF turned both markers off, confirmed by reading each mode back as OFF.

set_ad_audio: read path only confirmed, write path intentionally not exercised. :SPEaker:STATe? and :SYSTem:VOLume? both answered correctly (speaker off, volume 50%). The speaker-on/volume-raise commands were deliberately not sent in this pass — turning the speaker on produces an audible tone with a real-world side effect, and this verification pass could not get synchronous confirmation to do that; it needs a dedicated supervised session (the same gate set_ad_audio's own confirm: true requirement exists for).

Caution, not a new bug — command-prefix mistake made and corrected during this pass: an early attempt at set_ad_display's window commands used the bare :DISPlay:WINDow<n>:TRACe:Y:RLEVel? form (the SA module's syntax) and got no reply at all. The AD module's display commands take the demod-type prefix too: :DISPlay:AM:WINDow<n>:TRACe:Y:RLEVel?, which is what src/tools/adDisplayMarker.ts already sends correctly — same pattern as the :AM:/:FM: prefix already documented for AD's other command groups (settings, filters, demod timing).

Confidence: CONFIRMED (live) for the filter mutual-exclusion correction, window 1/3 display commands, and the full marker set (mode, trace, reference marker/delta, peak search, AOFF). CONFIRMED (live, read-only) for set_ad_audio's query path. NOT independently confirmed: set_ad_audio's write path, set_ad_display window 2's unit.

Actionable for uni-t-uts-mcp: set_ad_filters fixed (this pass). With this pass merged, every AD tool except set_ad_audio's write path has been live-verified at least once.

Clone this wiki locally