From dc608b9ca1084fdf13a2b5380303fb88a7a1ce41 Mon Sep 17 00:00:00 2001 From: NI-velmurugan Date: Tue, 27 Oct 2026 07:47:13 +0000 Subject: [PATCH 1/3] Adding Measurement Library example in NI-Scope --- CHANGELOG.md | 1 + build/templates/tox-system_tests.ini.mako | 3 + docs/niscope/examples.rst | 9 ++ generated/niscope/tox-system_tests.ini | 1 + .../examples/niscope_measurement_library.py | 133 ++++++++++++++++++ 5 files changed, 147 insertions(+) create mode 100644 src/niscope/examples/niscope_measurement_library.py diff --git a/CHANGELOG.md b/CHANGELOG.md index cfa7540d47..55a425b3d5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1900,6 +1900,7 @@ #### [niscope] Unreleased - Added - (Common) Added `nitlsconfig[grpc]>=1.0.0a2` to the optional gRPC dependencies. + - Added the `niscope_measurement_library.py` example. - Changed - Removed diff --git a/build/templates/tox-system_tests.ini.mako b/build/templates/tox-system_tests.ini.mako index c2b3d9131d..9c7f07cb3e 100644 --- a/build/templates/tox-system_tests.ini.mako +++ b/build/templates/tox-system_tests.ini.mako @@ -76,6 +76,9 @@ deps = % endif % if module_name == 'nidigital': ${module_name}-system_tests: nisync +% endif +% if module_name == 'niscope': + ${module_name}-system_tests: matplotlib; sys_platform != "win32" % endif ${module_name}-system_tests: pytest ${module_name}-system_tests: coverage diff --git a/docs/niscope/examples.rst b/docs/niscope/examples.rst index 71f84c469c..3cbdb11097 100644 --- a/docs/niscope/examples.rst +++ b/docs/niscope/examples.rst @@ -30,6 +30,15 @@ niscope_fetch_into.py :encoding: utf8 :caption: `(niscope_fetch_into.py) `_ +niscope_measurement_library.py +------------------------------ + +.. literalinclude:: ../../src/niscope/examples/niscope_measurement_library.py + :language: python + :linenos: + :encoding: utf8 + :caption: `(niscope_measurement_library.py) `_ + niscope_read.py --------------- diff --git a/generated/niscope/tox-system_tests.ini b/generated/niscope/tox-system_tests.ini index 2f232d27ab..afadc34815 100644 --- a/generated/niscope/tox-system_tests.ini +++ b/generated/niscope/tox-system_tests.ini @@ -39,6 +39,7 @@ commands = deps = niscope-wheel_dep: build + niscope-system_tests: matplotlib; sys_platform != "win32" niscope-system_tests: pytest niscope-system_tests: coverage niscope-system_tests: numpy diff --git a/src/niscope/examples/niscope_measurement_library.py b/src/niscope/examples/niscope_measurement_library.py new file mode 100644 index 0000000000..a44573ec0a --- /dev/null +++ b/src/niscope/examples/niscope_measurement_library.py @@ -0,0 +1,133 @@ +#!/usr/bin/python + +import argparse +import niscope +import pytest +import sys + +_TIMEOUT_ERROR_CODE = -1074126845 # "Maximum time exceeded before the operation completed" + + +def _create_chart(plt): + figure, axes = plt.subplots() + axes.set_title('Waveform Graph') + axes.set_xlabel('Time (s)') + axes.set_ylabel('Amplitude (V)') + axes.grid(True) + figure.tight_layout() + return figure, axes + + +def _update_chart(axes, waveforms): + while len(axes.lines) < len(waveforms): + axes.plot([], [], label=waveforms[len(axes.lines)].channel) + axes.legend(loc='upper right') + for line, waveform in zip(axes.lines, waveforms): + x = [waveform.relative_initial_x + i * waveform.x_increment for i in range(len(waveform.samples))] + # read() returns samples as a memoryview, which matplotlib cannot copy + line.set_data(x, list(waveform.samples)) + axes.relim() + axes.autoscale_view() + + +def example(resource_name, channels, options, vertical_range, vertical_coupling, min_sample_rate, min_record_length, + enforce_realtime, trigger_type, trigger_source, trigger_level, trigger_slope, trigger_coupling, timeout, + scalar_measurement, reference_level_units, low_ref, mid_ref, high_ref, iterations): + import matplotlib.pyplot as plt + + with niscope.Session(resource_name=resource_name, options=options) as session: + session.configure_vertical(range=vertical_range, coupling=niscope.VerticalCoupling[vertical_coupling]) + session.configure_horizontal_timing(min_sample_rate=min_sample_rate, min_num_pts=min_record_length, ref_position=50.0, num_records=1, enforce_realtime=enforce_realtime) + if trigger_type == 'EDGE': + session.configure_trigger_edge(trigger_source, trigger_level, niscope.TriggerCoupling[trigger_coupling], niscope.TriggerSlope[trigger_slope]) + else: + session.configure_trigger_immediate() + + channel = session.channels[channels] + channel.meas_ref_level_units = niscope.RefLevelUnits[reference_level_units] + channel.meas_chan_low_ref_level = low_ref + channel.meas_chan_mid_ref_level = mid_ref + channel.meas_chan_high_ref_level = high_ref + + figure, axes = _create_chart(plt) + + iteration = 0 + try: + # Run until the chart is closed, Ctrl+C is pressed, or the iteration limit is reached. + while plt.fignum_exists(figure.number) and (iterations is None or iteration < iterations): + # The statistics accumulate once per acquisition, so read a new waveform before each fetch. + try: + waveforms = channel.read(num_samples=min_record_length, timeout=timeout) + except niscope.errors.DriverError as e: + if e.code != _TIMEOUT_ERROR_CODE: + raise + print(f'Timed out after {timeout} s waiting for a trigger; check the trigger source and level, or use --trigger-type immediate.') + plt.pause(0.02) + continue + _update_chart(axes, waveforms) + stats = channel.fetch_measurement_stats(niscope.ScalarMeasurement[scalar_measurement]) + print(f'Acquisition {iteration}: {scalar_measurement}') + for stat in stats: + print(stat) + iteration += 1 + plt.pause(0.02) + + # Keep the final plot window open after acquisition stops until the user closes it. + if iterations is not None and plt.fignum_exists(figure.number): + plt.show() + except KeyboardInterrupt: + pass + finally: + plt.close(figure) + + +def _main(argsv): + parser = argparse.ArgumentParser(description='Fetches scalar measurements and their statistics from an NI digitizer.', formatter_class=argparse.ArgumentDefaultsHelpFormatter) + parser.add_argument('-n', '--resource-name', default='PXI1Slot3', help='Specifies the resource name of the device to initialize') + parser.add_argument('-m', '--scalar-measurement', default='FFT_FREQUENCY', choices=niscope.ScalarMeasurement.__members__.keys(), type=str.upper, help='The scalar measurement to be performed on each fetched waveform.') + parser.add_argument('--reference-level-units', default='PERCENTAGE', choices=niscope.RefLevelUnits.__members__.keys(), type=str.upper, help='Specifies the units of the reference levels.') + parser.add_argument('--low-ref', default=10.0, type=float, help='Stores the low reference level used in many scalar measurements.') + parser.add_argument('--mid-ref', default=50.0, type=float, help='Stores the mid reference level used in many scalar measurements.') + parser.add_argument('--high-ref', default=90.0, type=float, help='Stores the high reference level used in many scalar measurements.') + parser.add_argument('-s', '--min-sample-rate', default=1000000.0, type=float, help='Specify the sampling rate for the acquisition in Samples per second.') + parser.add_argument('-l', '--min-record-length', default=1000, type=int, help='Specifies the minimum number of points you require in the waveform record for each channel.') + parser.add_argument('--enforce-realtime', action=argparse.BooleanOptionalAction, default=True, help='Indicates whether the digitizer enforces real-time measurements or allows equivalent-time measurements.') + parser.add_argument('-c', '--channels', default='0', help='Comma-separated list of channels to plot, one line per channel') + parser.add_argument('-v', '--vertical-range', default=2.0, type=float, help='Specifies the absolute value of the input range for a channel in volts.') + parser.add_argument('--vertical-coupling', default='DC', choices=niscope.VerticalCoupling.__members__.keys(), type=str.upper, help='Specifies how the digitizer couples the input signal for the channel.') + parser.add_argument('--trigger-type', default='EDGE', choices=['EDGE', 'IMMEDIATE'], type=str.upper, help='Selects edge or immediate triggering.') + parser.add_argument('--trigger-source', default='0', help='Specifies the source the digitizer monitors for the trigger event.') + parser.add_argument('--trigger-level', default=0.0, type=float, help='Specifies the voltage threshold for the trigger subsystem.') + parser.add_argument('--trigger-slope', default='POSITIVE', choices=niscope.TriggerSlope.__members__.keys(), type=str.upper, help='Specifies if a rising or a falling edge triggers the digitizer.') + parser.add_argument('--trigger-coupling', default='DC', choices=niscope.TriggerCoupling.__members__.keys(), type=str.upper, help='Specifies how the digitizer couples the trigger source.') + parser.add_argument('-t', '--timeout', default=5.0, type=float, help='The time to wait in seconds for data to be acquired; using 0 for this parameter tells NI-SCOPE to fetch whatever is currently available.') + parser.add_argument('--total-iterations', default=None, type=int, help='Stops after this many acquisitions and keeps the chart open until its window is closed; omit to run until the chart window is closed or Ctrl+C is pressed.') + parser.add_argument('-op', '--option-string', default='', type=str, help='Specifies the initial value of certain properties for the session.') + args = parser.parse_args(argsv) + example(args.resource_name, args.channels, args.option_string, args.vertical_range, args.vertical_coupling, + args.min_sample_rate, args.min_record_length, args.enforce_realtime, args.trigger_type, args.trigger_source, + args.trigger_level, args.trigger_slope, args.trigger_coupling, args.timeout, args.scalar_measurement, + args.reference_level_units, args.low_ref, args.mid_ref, args.high_ref, args.total_iterations) + + +def main(): + _main(sys.argv[1:]) + + +def test_example(): + plt = pytest.importorskip('matplotlib.pyplot') + plt.switch_backend('Agg') # Avoid opening a chart window during the test + options = {'simulate': True, 'driver_setup': {'Model': '5164', 'BoardType': 'PXIe', }, } + example('PXI1Slot2', '0', options, 2.0, 'DC', 1000000.0, 1000, True, 'IMMEDIATE', '0', 0.0, 'POSITIVE', 'DC', 5.0, + 'FREQUENCY', 'PERCENTAGE', 10.0, 50.0, 90.0, 2) + + +def test_main(): + plt = pytest.importorskip('matplotlib.pyplot') + plt.switch_backend('Agg') # Avoid opening a chart window during the test + cmd_line = ['--option-string', 'Simulate=1, DriverSetup=Model:5164; BoardType:PXIe', '--trigger-type', 'immediate', '--total-iterations', '2', ] + _main(cmd_line) + + +if __name__ == '__main__': + main() From 5b21e43d27607ab8f14123b9417176a5e345dda5 Mon Sep 17 00:00:00 2001 From: NI-velmurugan Date: Tue, 27 Oct 2026 08:49:43 +0000 Subject: [PATCH 2/3] Removing error handling for the trigger source We can use immediate trigger to run the example and the external trigger depends on the external input --- src/niscope/examples/niscope_measurement_library.py | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/src/niscope/examples/niscope_measurement_library.py b/src/niscope/examples/niscope_measurement_library.py index a44573ec0a..4c458cfdfe 100644 --- a/src/niscope/examples/niscope_measurement_library.py +++ b/src/niscope/examples/niscope_measurement_library.py @@ -5,8 +5,6 @@ import pytest import sys -_TIMEOUT_ERROR_CODE = -1074126845 # "Maximum time exceeded before the operation completed" - def _create_chart(plt): figure, axes = plt.subplots() @@ -56,14 +54,7 @@ def example(resource_name, channels, options, vertical_range, vertical_coupling, # Run until the chart is closed, Ctrl+C is pressed, or the iteration limit is reached. while plt.fignum_exists(figure.number) and (iterations is None or iteration < iterations): # The statistics accumulate once per acquisition, so read a new waveform before each fetch. - try: - waveforms = channel.read(num_samples=min_record_length, timeout=timeout) - except niscope.errors.DriverError as e: - if e.code != _TIMEOUT_ERROR_CODE: - raise - print(f'Timed out after {timeout} s waiting for a trigger; check the trigger source and level, or use --trigger-type immediate.') - plt.pause(0.02) - continue + waveforms = channel.read(num_samples=min_record_length, timeout=timeout) _update_chart(axes, waveforms) stats = channel.fetch_measurement_stats(niscope.ScalarMeasurement[scalar_measurement]) print(f'Acquisition {iteration}: {scalar_measurement}') From 8b77fbd68ac08dd7055c088dcc24fa86bb82e593 Mon Sep 17 00:00:00 2001 From: NI-velmurugan Date: Wed, 28 Oct 2026 07:06:43 +0000 Subject: [PATCH 3/3] Resolving PR comments --- .../examples/niscope_measurement_library.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/niscope/examples/niscope_measurement_library.py b/src/niscope/examples/niscope_measurement_library.py index 4c458cfdfe..721e168a53 100644 --- a/src/niscope/examples/niscope_measurement_library.py +++ b/src/niscope/examples/niscope_measurement_library.py @@ -28,9 +28,10 @@ def _update_chart(axes, waveforms): axes.autoscale_view() -def example(resource_name, channels, options, vertical_range, vertical_coupling, min_sample_rate, min_record_length, - enforce_realtime, trigger_type, trigger_source, trigger_level, trigger_slope, trigger_coupling, timeout, - scalar_measurement, reference_level_units, low_ref, mid_ref, high_ref, iterations): +def example(resource_name, channels, options, vertical_range, vertical_coupling, + min_sample_rate, min_record_length, enforce_realtime, trigger_type, trigger_source, + trigger_level, trigger_slope, trigger_coupling, timeout, scalar_measurement, + reference_level_units, low_ref, mid_ref, high_ref, iterations): import matplotlib.pyplot as plt with niscope.Session(resource_name=resource_name, options=options) as session: @@ -49,18 +50,18 @@ def example(resource_name, channels, options, vertical_range, vertical_coupling, figure, axes = _create_chart(plt) - iteration = 0 + acquisition_count = 1 try: # Run until the chart is closed, Ctrl+C is pressed, or the iteration limit is reached. - while plt.fignum_exists(figure.number) and (iterations is None or iteration < iterations): + while plt.fignum_exists(figure.number) and (iterations is None or acquisition_count <= iterations): # The statistics accumulate once per acquisition, so read a new waveform before each fetch. waveforms = channel.read(num_samples=min_record_length, timeout=timeout) _update_chart(axes, waveforms) stats = channel.fetch_measurement_stats(niscope.ScalarMeasurement[scalar_measurement]) - print(f'Acquisition {iteration}: {scalar_measurement}') + print(f'Acquisition {acquisition_count}: {scalar_measurement}') for stat in stats: print(stat) - iteration += 1 + acquisition_count += 1 plt.pause(0.02) # Keep the final plot window open after acquisition stops until the user closes it. @@ -92,7 +93,7 @@ def _main(argsv): parser.add_argument('--trigger-slope', default='POSITIVE', choices=niscope.TriggerSlope.__members__.keys(), type=str.upper, help='Specifies if a rising or a falling edge triggers the digitizer.') parser.add_argument('--trigger-coupling', default='DC', choices=niscope.TriggerCoupling.__members__.keys(), type=str.upper, help='Specifies how the digitizer couples the trigger source.') parser.add_argument('-t', '--timeout', default=5.0, type=float, help='The time to wait in seconds for data to be acquired; using 0 for this parameter tells NI-SCOPE to fetch whatever is currently available.') - parser.add_argument('--total-iterations', default=None, type=int, help='Stops after this many acquisitions and keeps the chart open until its window is closed; omit to run until the chart window is closed or Ctrl+C is pressed.') + parser.add_argument('--total-iterations', default=None, type=int, help='Specifies the maximum number of acquisitions; omit to run until the chart closes or Ctrl+C is pressed.') parser.add_argument('-op', '--option-string', default='', type=str, help='Specifies the initial value of certain properties for the session.') args = parser.parse_args(argsv) example(args.resource_name, args.channels, args.option_string, args.vertical_range, args.vertical_coupling,