Repository navigation
Releases: mabl/pyMSO5000
Release list
v0.2.0
Added
-
Protocol decoding:
scope.decodeand five MCP tools. The fourBUS<n>
decoders and their nine protocols - Parallel, RS232, SPI, I2C, I2S, LIN, CAN,
FlexRay, MIL-STD-1553 - are now typed, withget_decode,set_decode,
read_decode_eventsand a threshold get/set pair. There is noDECodecommand in
this firmware: it calls the subsystemBUS<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:POLarityandSPI:MOSI:POLarityare one setting, sharing command
id 8517: writing MISO left MOSI reading the new value. LikewiseBAUDand
BUSerper protocol. Both are published as a single field, since two fields that
silently overwrite each other are worse than one.FORMat,EVENt:FORMatandEVENt:VIEWchange only the screen. With all
three set toDEC,BUS1:DATA?still answered in hex, andEVENt:VIEW PAYLoad
returned the same columns asPACKets.- A protocol must keep one source live: with the partner already
OFF, both
BUS2:RS232:TX OFFandBUS3:SPI:MISO:SOURce OFFare 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
USERpreset: under a fixed preset
PARallel:SOURceis refused-200rather than ignored, whileBITXis accepted. - The three event-table settings -
EVENt,EVENt:FORMatandEVENt:VIEW- are
instrument-wide, not per bus, one setting each behind four command paths.
GivingBUS1:EVENt:VIEWPACKetsandBUS2:EVENt:VIEWPAYLoadleft both
readingPAYLoad;BUS1:EVENt ONwithBUS2:EVENt OFFleft both off. They stay
onDecodeStatebecause reading any bus yields the true shared value, but two
buses can never disagree and setting one moves them all.FORMat,LABel,
POSitionandDISPlayare genuinely per bus, checked the same way. EVENt:VIEWis 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. SoEVENtis 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-*RSTrebuild quietly come
back withPACKetsafter asking forPAYLoad.
-
read_decode_events/Decode.events.BUS<n>:DATA?is parsed into typed
rows. Its label isDECODE<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 answersMISO/MISOErr/MOSI/MOSIErr, RS232Tx/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 modePARallel, which refused unconditionally even
displayed, with its width set and reading a real 1 kHz clock. -
Digital waveform capture:
acquire_logicandcapture_logic. Digital lines can
now be read as waveforms, one retained capture per line holding its own 0/1 samples -
sofind_waveform_edgesat 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.TRACereturns two bytes per sample, one per pod, not the one-byte-per-pod-group
the guide describes:preamble.pointsis twice the sample count and the selected
source changes nothing, withD0,D1andD8returning 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
NORMalvalue, which is how the layout was found:D4is held high throughout and
read 0.5, and reads 1.0 after de-interleaving.RAWis 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
TRACeblock 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 leavingx_incrementat the analog
record's sample interval, so a window needingNsamples comes back covering
min(500_000, N) / Nof it: a prefix that starts atx_originand stops early, with
nothing in the reply admitting it. Measured on a stopped MSO5074 atAUTOmemory
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.625row 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
reportedx_increment- the other reading of a byte-countedpoints, 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 andx_incrementflat at 5 ns while the rate moved 2 MSa/s to
2 GSa/s, sox_incrementis 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:STARtpastpointswraps to the start
of the block and returns the same bytes, and asking for twicepointsis refused
-200- soacquire_logicreads 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_displayedandautosort, published as
configure_digital_groupandautosort_digital. Group membership is not readable -
LA:GROup:APPendis write-only and the aliases that would read it declare the
unresolvable origin:LA::GROup- andAPPendaccumulates: appendingD5,D6to
GROup1and displaying it switched onD5,D6andD7. So membership is
stated rather than added to, and the delete-then-append that makes it deterministic
happens inside the call.LA:AUTosortis 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, andLA:DISPlay?answers immediately for lines, groups and pods
alike - unlikeLA:DIGital:POSition?, so it needs no hang avoidance. -
DigitalState.deskew_s, read-only.LA:TCALibrateiscalibrationrisk, so a
typed setter would reach it without the permission that gatesscpi_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
enabledflag is not gated by the master switch, so it does not
mean the line is on screen. After*RSTthe master switch is off while all sixteen
lines reportenabledtrue and answer the position query. A caller has to read
both, and there is no query for the conjunction. line_positioninserts rather than swaps: writing a position another line
holds pushes that line and everything above it up one slot, so movingD3to 0
whileD0-D2sat 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 drawsD0-D15instead, with the read-back
...
- A line's own
v0.1.0
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.waveformandscope.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) andRAW
(the whole acquisition memory).inspectcomputes complete statistics with an
optional bounded preview and retains nothing;acquirekeeps every point for
values,summarizeandfind_edges, which are also available as pure
functions inpymso5000.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
asautoscale(). It does not cover the generator, which is documented on the
API rather than left to be discovered.- An error taxonomy:
ScopeErrorcarrying akindand aretryableflag,
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 inpartial, because the
instrument does not roll back. ScpiCatalogandCommandDocssearch, 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
thefirmwareextra they can instead be extracted from a Rigol.GELimage 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.typedis 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}andoscilloscope://setups/{setup_id},
and promptscharacterize_signalanddebug_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 themcpextra and
firmware extraction behindfirmware. 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.