Skip to content

Releases: mabl/pyMSO5000

Release list

v0.2.0

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
      ...
Read more

v0.1.0

Choose a tag to compare

@github-actions github-actions released this 02 Aug 08:28

First release. This entry describes what the package contains rather than how it
got here: nothing before this was installable from an index, so a per-change
history would be a list of deltas against states no one could have been running.
The development history is in the git log.

Alpha in the trove classifier and 0.x in the version: the API may still break.

Library

  • Scope, a typed high-level API over VISA (pyvisa / pyvisa-py). Channels,
    timebase, acquisition, run/stop/single, autoscale and instrument identity, with
    the densely-parameterized subsystems as their own objects: scope.trigger,
    scope.measure, scope.generator, scope.waveform and scope.digital.
  • Every call is validated against the scope's own firmware definitions -
    arity, value types and enum spellings - so a bad argument is refused locally
    instead of becoming a silent entry in the instrument's error queue. Short SCPI
    spellings are accepted wherever a mnemonic is, and replies are canonicalized
    back to the published spelling.
  • Complete typed trigger control across all 20 trigger modes, as a
    discriminated union of per-mode configs.
  • Batched measurements: up to 32 waveform, digital-timing or two-source
    comparison requests per call, plus accumulated statistics and percentage or
    absolute reference levels.
  • Waveform acquisition in three depths - NORMal (the 1000 displayed
    points), TRACe (the displayed window at acquisition resolution) and RAW
    (the whole acquisition memory). inspect computes complete statistics with an
    optional bounded preview and retains nothing; acquire keeps every point for
    values, summarize and find_edges, which are also available as pure
    functions in pymso5000.api.waveform_analysis. Deep memory is transferred in
    windows, with statistics accumulated in that single pass.
  • Both built-in generators, including modulation, sweep and burst modes and
    arbitrary waveform upload.
  • saved_setup(), a context manager that exports the instrument setup and
    restores it on either exit path - the rollback point for anything broad, such
    as autoscale(). It does not cover the generator, which is documented on the
    API rather than left to be discovered.
  • An error taxonomy: ScopeError carrying a kind and a retryable flag,
    specialized into usage, policy, state, command, transport, timeout, reply and
    framing errors, so a caller can tell a bad argument from a dropped link from an
    instrument in the wrong state without matching on message text. Setters that
    apply several settings report what already landed in partial, because the
    instrument does not roll back.
  • ScpiCatalog and CommandDocs search, resolve and document the full
    2236-command firmware set and its 784 compatibility aliases, and need no
    connection at all.
  • MSO5000, the low-level client: serializes a command and parses the reply,
    including IEEE 488.2 binary blocks for screenshots, waveforms and setups.
    Message-based VISA transports are supported generally - VXI-11, HiSLIP, USBTMC,
    GPIB, raw socket and serial - with framing decided from the resource name.
  • SCPI definitions are bundled, and are the only copy the package ships. With
    the firmware extra they can instead be extracted from a Rigol .GEL image by
    an in-tree UBI/UBIFS reader; the test suite extracts one and asserts the result
    matches the shipped definitions byte for byte.
  • Typed: py.typed is shipped and the package is checked under pyright
    strict.

MCP server (pymso5000[mcp])

  • A stdio MCP server, pymso5000-mcp, exposing the instrument to AI agents:
    screenshot and touchscreen/key/knob control, acquisition and setup, batched
    measurements, the two-path waveform workflow, and generic SCPI search, describe
    and execute for anything the typed tools do not cover.
  • Structured output schemas and tool annotations on every tool, with the
    instrument's own enum and range constraints published in the input schemas.
  • Process-local retained stores for waveform captures and instrument setups,
    addressed by opaque bearer handles with quotas and a TTL. Samples and setup
    blobs are reachable only through bounded tools, never inlined into model
    context.
  • Six direct-SCPI risk categories - storage, connectivity, security,
    calibration, licensing, firmware - all disabled by default and matched on
    the resolved canonical command, so a short form cannot bypass one. Raw
    touch/key/knob control is deliberately a trusted front-panel-equivalent lane,
    which the README states rather than leaves to be inferred.
  • Resources scpi-doc://command/{command}, scope://state,
    oscilloscope://captures/{capture_id} and oscilloscope://setups/{setup_id},
    and prompts characterize_signal and debug_no_trigger.
  • Audit logging to stderr for every call - tool, arguments, duration, and
    outcome or error kind - leaving stdout to carry only the JSON-RPC stream.

Distribution

  • pip install pymso5000, with the MCP server behind the mcp extra and
    firmware extraction behind firmware. The library imports and works with
    neither installed.
  • CI runs ruff, ruff format, pyright and the test suite under a coverage floor on
    Linux, and the suite alone on macOS, Windows and free-threaded CPython. It
    builds both artifacts, installs each, runs the unpacked sdist's own test suite
    from inside itself, smoke-tests the installed console script, and builds the
    Nix derivation.
  • Releases are published to PyPI from a tagged commit through Trusted Publishing,
    so no API token exists to leak, and are attested with build provenance.