Skip to content

v0.2.0

Latest

Choose a tag to compare

@github-actions github-actions released this 05 Aug 02:21
48ebacc

Added

  • Protocol decoding: scope.decode and five MCP tools. The four BUS<n>
    decoders and their nine protocols - Parallel, RS232, SPI, I2C, I2S, LIN, CAN,
    FlexRay, MIL-STD-1553 - are now typed, with get_decode, set_decode,
    read_decode_events and a threshold get/set pair. There is no DECode command in
    this firmware: it calls the subsystem BUS<n> and tags all 134 of its commands
    decode<n>. The four buses' definitions are byte-identical apart from that tag, so
    the tables are written once.

    Six things about it are not what the programming guide says, all measured on an
    MSO5074 (firmware 00.01.03.03.00):

    • SPI:MISO:POLarity and SPI:MOSI:POLarity are one setting, sharing command
      id 8517: writing MISO left MOSI reading the new value. Likewise BAUD and
      BUSer per protocol. Both are published as a single field, since two fields that
      silently overwrite each other are worse than one.
    • FORMat, EVENt:FORMat and EVENt:VIEW change only the screen. With all
      three set to DEC, BUS1:DATA? still answered in hex, and EVENt:VIEW PAYLoad
      returned the same columns as PACKets.
    • A protocol must keep one source live: with the partner already OFF, both
      BUS2:RS232:TX OFF and BUS3:SPI:MISO:SOURce OFF are refused -200. So a patch
      moving a protocol from one line to the other is legal as a whole and illegal in
      declaration order, and the write switching a source on is now reordered ahead of
      the one switching its partner off - the same treatment the trigger's lower/upper
      level pairs get.
    • The parallel per-bit assignment needs the USER preset: under a fixed preset
      PARallel:SOURce is refused -200 rather than ignored, while BITX is accepted.
    • The three event-table settings - EVENt, EVENt:FORMat and EVENt:VIEW - are
      instrument-wide, not per bus, one setting each behind four command paths.
      Giving BUS1:EVENt:VIEW PACKets and BUS2:EVENt:VIEW PAYLoad left both
      reading PAYLoad; BUS1:EVENt ON with BUS2:EVENt OFF left both off. They stay
      on DecodeState because reading any bus yields the true shared value, but two
      buses can never disagree and setting one moves them all. FORMat, LABel,
      POSition and DISPlay are genuinely per bus, checked the same way.
    • EVENt:VIEW is only writable while the event table is on. With it off the
      write is discarded in both directions with an empty error queue - sixteen trials
      across SPI, RS232, I2C and LIN, no disagreement. So EVENt is now written before
      the view it gates, and a view that could not land is refused up front rather than
      silently dropped. This is the defect that made a post-*RST rebuild quietly come
      back with PACKets after asking for PAYLoad.
  • read_decode_events / Decode.events. BUS<n>:DATA? is parsed into typed
    rows. Its label is DECODE<n> - the bus number, not the decode type the guide's
    example shows - so nothing in the payload says which protocol produced it and the
    mode is read separately. Values are always hexadecimal, and the columns are
    protocol-specific and taken from the reply's own header rather than a table here:
    SPI answers MISO/MISOErr/MOSI/MOSIErr, RS232 Tx/Rx/Data/Err, I2C
    W/R/Addr/Data/ACK. Recorded payloads for three protocols are goldens, so the
    parser needs no instrument.

    The query does not fail cleanly - it stays silent for the whole timeout and only
    then queues -220 - so three measured causes are turned into [state] errors: an
    undisplayed bus (checked first, one query), a decoder that has produced nothing at
    all (a SPI bus with an idle clock refused; pointing it at a real clock made the same
    query answer 16 951 bytes), and mode PARallel, which refused unconditionally even
    displayed, with its width set and reading a real 1 kHz clock.

  • Digital waveform capture: acquire_logic and capture_logic. Digital lines can
    now be read as waveforms, one retained capture per line holding its own 0/1 samples -
    so find_waveform_edges at threshold 0.5 gives a line's transition timestamps and a
    summary's mean is its duty cycle. The waveform store and every retained-read tool
    took them unchanged.

    TRACe returns two bytes per sample, one per pod, not the one-byte-per-pod-group
    the guide describes: preamble.points is twice the sample count and the selected
    source changes nothing, with D0, D1 and D8 returning identical blocks. So one
    transfer carries every line, and asking for eight costs one transfer rather than
    eight. Reading it as one byte per sample made every duty come out at exactly half its
    NORMal value, which is how the layout was found: D4 is held high throughout and
    read 0.5, and reads 1.0 after de-interleaving.

    RAW is refused for digital sources, because it kills the instrument: WAVeform: DATA? in RAW mode with a digital source took down the VXI-11 service twice, at 20 MPt
    and at 125 points, each time needing a physical power cycle. Not a size effect, and
    the preamble answers normally first, so it cannot be discovered by asking.

    A TRACe block often does not span the displayed window, which is the one way a
    logic read differs from an analog one in what the data means. The instrument caps it
    at 500,000 samples - 1,000,000 bytes - while leaving x_increment at the analog
    record's sample interval, so a window needing N samples comes back covering
    min(500_000, N) / N of it: a prefix that starts at x_origin and stops early, with
    nothing in the reply admitting it. Measured on a stopped MSO5074 at AUTO memory
    depth, against an analog read of the same window:

    window analog span/window logic span/window
    100 ns - 50 us 1.000 1.000
    100 us 1.000 0.625
    1 ms 1.000 0.500
    10 ms 1.000 0.500
    100 ms 1.000 0.500

    The 0.625 row is the one that says what is happening rather than merely that
    something is: analog returned 800,000 samples for that window and logic 500,000, and
    500000/800000 is exactly 0.625. A block spanning the whole window at twice the
    reported x_increment - the other reading of a byte-counted points, and the one
    that would mean every capped logic timestamp is half its true value - gives 1.000 on
    every row, so it is excluded by measurement rather than by argument. Two further
    checks pin it: sweeping memory depth 10 kPt to 10 MPt at a fixed timebase left
    coverage flat at 0.500 and x_increment flat at 5 ns while the rate moved 2 MSa/s to
    2 GSa/s, so x_increment is a decimated display interval rather than the acquisition
    one; and against a 33-byte 115200 8N1 message triggered on its first byte in a 5 s
    window, the 2.865 ms burst sat at sample 20,000 and spanned 567 samples (2.835 ms),
    where twice the interval puts it at sample 10,000, which measured idle.

    The missing span is not fetchable - WAVeform:STARt past points wraps to the start
    of the block and returns the same bytes, and asking for twice points is refused
    -200 - so acquire_logic reads the timebase once per transfer and reports a short
    block as a scope warning naming the fraction it holds. Half a record does not look
    like an error; it looks like a signal that stops.

  • Logic analyzer channel groups. Digital.define_group, delete_group,
    set_group_display, group_displayed and autosort, published as
    configure_digital_group and autosort_digital. Group membership is not readable -
    LA:GROup:APPend is write-only and the aliases that would read it declare the
    unresolvable origin :LA::GROup - and APPend accumulates: appending D5,D6 to
    GROup1 and displaying it switched on D5, D6 and D7. So membership is
    stated rather than added to, and the delete-then-append that makes it deterministic
    happens inside the call. LA:AUTosort is an ENUM of "0"/"1" rather than the BOOL
    it reads as, so a Python bool is mapped here rather than refused by the formatter.

    Also measured: displaying a group cascades to its members, deleting one ungroups
    without hiding, and LA:DISPlay? answers immediately for lines, groups and pods
    alike - unlike LA:DIGital:POSition?, so it needs no hang avoidance.

  • DigitalState.deskew_s, read-only. LA:TCALibrate is calibration risk, so a
    typed setter would reach it without the permission that gates scpi_execute, making
    the risk policy depend on which tool you pick. The query is not classified at all.

    Three more things about the analyzer are documented rather than fixed, because
    nothing here can fix them:

    • A line's own enabled flag is not gated by the master switch, so it does not
      mean the line is on screen. After *RST the master switch is off while all sixteen
      lines report enabled true and answer the position query. A caller has to read
      both, and there is no query for the conjunction.
    • line_position inserts rather than swaps: writing a position another line
      holds pushes that line and everything above it up one slot, so moving D3 to 0
      while D0-D2 sat at 0-2 left them at 1-3. Reordering is a sequence of
      insertions; a loop assigning each line its target will not converge.
    • Whether labels are drawn cannot be reached over SCPI at all. The LA:
      subsystem has no such command, and the one switch describing itself as "the display
      of all the labels", REFerence:LABel:ENABle, is the analog and reference one -
      measured reading 0 while the logic labels were on screen. So a reset instrument
      stores every label written and draws D0-D15 instead, with the read-back
      agreeing with the write and the screen disagreeing with both. Only the front panel
      (LA -> Label -> Display) reaches it, which the MCP server's raw UI lane can drive.

Changed

  • TRACe on a running scope is documented as returning a partial window. It has
    always been described as "the displayed window at acquisition resolution", and on a
    stopped scope it is: an analog read spanned the window exactly at every timebase
    from 10 us/div to 50 ms/div. Running, the same reads came back as short prefixes -
    10% of the window from 100 us/div through 10 ms/div, 20% at 50 ms/div - because a
    sweep that has not finished has not been drawn, and this mode reads what is drawn.
    The preamble describes the prefix truthfully, so nothing is wrong with the record;
    it just is not the whole window, and nothing said so.

  • A command that times out is now diagnosed rather than guessed at. This
    instrument answers a message it rejects with an error-queue entry and no reply,
    so a refused command used to cost the full VISA timeout and was then reported as
    [timeout] ... the scope is not triggering - the wrong cause, pointing at the
    wrong fix. The error queue is now read once after any timed-out command, which
    the instrument itself answers definitively: entries mean the command was refused
    and it is reported as [command], naming the entries and the command, while an
    empty queue is reported as [timeout] as before and now says the queue was
    empty, which is positive evidence for the acquisition explanation rather than the
    absence of any. Verified on an MSO5074 (firmware 01.03.03.00): BUS1:DATA?, a
    query whose firmware definition understates its parameters, now reports
    -220,"Lack of paramater" instead of blaming the trigger.

    ScopeTimeoutError is correspondingly narrower - it now asserts that the scope
    had no complaint - so a caller that treats every timeout as "not acquiring" keeps
    being right, and one that used to see a timeout for a refused command now sees a
    ScopeCommandError carrying errors and command.

    The read costs one round trip, only on a path that has already paid for a
    timeout, and it leaves the queue genuinely empty afterwards - so the following
    checked write no longer needs a pre-drain to avoid inheriting the blame.

    That refusal is now its own type, ScopeSilentCommandError, a subclass of
    ScopeCommandError so every existing handler keeps catching it. Two callers need
    the distinction. measure() degrades it per request like the timeout it replaced,
    reporting availability refused with the queue entries in the note - without
    it, one refused request discarded every result the batch had already collected. And
    the MCP setter envelope raises it instead of reporting ok=false: it can come from
    a setter's read-back query rather than from any write, and by then each write's own
    error queue has already been checked, so reporting it as a refused write blamed the
    wrong command and dropped the explanation the envelope has no field for. The
    write-only envelope behind run, stop, single, the raw-UI tools and
    clear_measurement_items still reports it as ok=false, because those operations
    have no read-back to have gone silent and drain the queue before writing, so the
    entries are theirs.

    availability gains refused alongside timeout. Both are silence on the wire,
    separated by the error queue afterwards, and the fixes are opposite: timeout means
    provide a signal, refused means ask for something else.

  • Waveform transfers are about ten times faster, because nothing was setting
    pyvisa's read buffer.
    It defaults to 20 KiB and pyvisa loops until the reply
    ends, so the read count was set by the buffer rather than by the reply: a
    250,000-point window cost ~13 reads through the whole pyvisa stack and a 20 MPt
    capture ~1000. The default is now 1 MiB. Measured through
    Scope.waveform.inspect on an MSO5074, before and after: a 4 MPt RAW read went
    from 9.69 s to 1.01 s and a 1 MPt TRACe read from 6.17 s to 1.01 s, so the
    full-depth 20 MPt transfer that took 48.5 s takes about five seconds.

    What made those iterations expensive is specific to the message transports:
    VXI-11 serves each one with a device_read RPC, so a 20 KiB buffer means ~50
    network round trips per megabyte. A byte stream is indifferent - measured over a
    raw socket, a static 200,000-byte block took 0.045 s at 20 KiB and 0.049 s at
    1 MiB, every read byte-identical - so the setting is applied to both framings for
    uniformity rather than because it helps there.

    Neither thing that could make a large buffer cost something does. A short reply is
    unaffected (13.9 ms per CHANnel1:SCALe? at 20 KiB against 12.9 ms at 8 MiB,
    because VXI-11 answers on end-of-message rather than on a full buffer), and peak
    RSS was 51.2 MiB at every combination tried. It is a property of the link rather
    than of the instrument, so it is configurable: chunk_bytes= on Scope.connect
    and ScopeSession.create, --chunk-bytes / PYMSO5000_CHUNK_BYTES on the
    server.

  • The waveform transfer window is 1,000,000 points, and is a granularity knob
    rather than the instrument limit it was documented as.
    It was 250,000 on the
    stated grounds that this is "the documented per-transfer maximum for
    :WAVeform:DATA? in RAW mode" and that asking for more returns a short block.
    Neither is true of an MSO5000: its programming guide gives the range of
    :WAVeform:STARt and :WAVeform:STOP as "RAW: 1 to the current maximum memory
    depth", and its own worked example reads 120,000 points in one transfer. 250,000
    is the DS1000Z series' limit. Measured on an MSO5074, single transfers of 250,001
    through the full 20,000,000-point memory depth each returned exactly the count
    requested with an empty error queue and statistics identical to the windowed read.

    What actually bounds the window is that progress callbacks and cancellation checks
    happen once per window. 1,000,000 keeps 20 checkpoints on a 20 MPt capture and 200
    at this instrument's deepest. The latter was checked with only CH1 enabled: a full
    200,000,000-point RAW inspection returned the exact count through all 200 windows
    in 40.07 s with no restoration warnings. TRACe is the exception: an MSO5074
    reports exactly 1,000,000 TRACe points, so that read is a single window and cannot
    be cancelled mid-transfer - buying a checkpoint by halving the window would cost
    more time than it could save, since the same read took 0.66 s in one window and
    1.29 s in two.

    capture_waveform and inspect_waveform advertise the new cost, and
    PROGRESS_INTERVAL_S now genuinely throttles progress notifications where before
    it suppressed almost nothing.