diff --git a/CHANGELOG.md b/CHANGELOG.md index 3556fcb1b7..4b5dde5b0d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1899,6 +1899,7 @@ #### [niscope] Unreleased - Added - (Common) Added `nitlsconfig[grpc]>=1.0.0a2` to the optional gRPC dependencies. + - Added the `niscope_external_clocking.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..f7308ebf4b 100644 --- a/docs/niscope/examples.rst +++ b/docs/niscope/examples.rst @@ -3,6 +3,15 @@ Examples `You can download all niscope examples for latest version here `_ +niscope_external_clocking.py +---------------------------- + +.. literalinclude:: ../../src/niscope/examples/niscope_external_clocking.py + :language: python + :linenos: + :encoding: utf8 + :caption: `(niscope_external_clocking.py) `_ + niscope_fetch.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_external_clocking.py b/src/niscope/examples/niscope_external_clocking.py new file mode 100644 index 0000000000..db66825e33 --- /dev/null +++ b/src/niscope/examples/niscope_external_clocking.py @@ -0,0 +1,104 @@ +#!/usr/bin/python + +import argparse +import niscope +import pytest +import sys + + +def _create_chart(plt): + figure, axes = plt.subplots() + axes.set_title('Waveform') + axes.set_xlabel('Time (s)') + axes.set_ylabel('Voltage (V)') + axes.grid(True) + figure.tight_layout() + return figure, axes + + +def _update_chart(axes, waveforms, actual_sample_rate): + if len(axes.lines) < len(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.set_title(f'Waveform (Actual Sample Rate: {actual_sample_rate:g} S/s)') + axes.relim() + axes.autoscale_view() + + +def example(resource_name, channels, options, min_record_length, voltage_range, voltage_offset, source, rate, divisor, multiplier, total_iterations): + import matplotlib.pyplot as plt + + with niscope.Session(resource_name=resource_name, options=options) as session: + channel_session = session.channels[channels] + channel_session.configure_vertical(range=voltage_range, coupling=niscope.VerticalCoupling.DC, offset=voltage_offset) + session.configure_trigger_immediate() + session.horz_min_num_pts = min_record_length + session.samp_clk_timebase_src = source + session.samp_clk_timebase_rate = rate + session.samp_clk_timebase_div = divisor + session.sample_clock_timebase_multiplier = multiplier + + figure, axes = _create_chart(plt) + + iterations = 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 (total_iterations is None or iterations < total_iterations): + # read() initiates the acquisition, waits for it to complete and fetches the data + waveforms = channel_session.read(num_samples=min_record_length) + _update_chart(axes, waveforms, session.horz_sample_rate) + iterations += 1 + plt.pause(0.01) + + # Keep the final plot window open after acquisition stops until the user closes it. + if total_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='Continuously acquires and plots a waveform using an external sample clock.', 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('-c', '--channels', default='0', help='Comma-separated list of channels to plot, one line per channel') + parser.add_argument('-v', '--voltage-range', default=1.0, type=float, help='Specifies the absolute value of the input range for a channel in volts.') + parser.add_argument('--voltage-offset', default=0.0, type=float, help='Specifies the location of the center of the range. The value is with respect to ground and is in volts.') + 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('-s', '--source', default='VAL_CLK_IN', type=str, help='Specifies the source of the sample clock timebase, which is the timebase used to control waveform sampling.') + parser.add_argument('-r', '--rate', default=1e8, type=float, help='Specifies the frequency in hertz of the external clock used as the timebase source.') + parser.add_argument('-d', '--divisor', default=1, type=int, help='Specifies the ratio between the sample clock timebase rate and the actual sample rate, which can be slower.') + parser.add_argument('-m', '--multiplier', default=1, type=int, help='Specifies the ratio between the samp_clk_timebase_rate and the actual sample rate, which can be higher.') + 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.min_record_length, args.voltage_range, args.voltage_offset, args.source, args.rate, args.divisor, args.multiplier, 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', }, } + # The simulated PXIe-5164 only accepts a 1 GHz sample clock timebase + example('PXI1Slot2', '0', options, 1000, 1.0, 0.0, 'VAL_CLK_IN', 1e9, 1, 1, 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', '--rate', '1e9', '--total-iterations', '2', ] + _main(cmd_line) + + +if __name__ == '__main__': + main()