diff --git a/examples/nidaqmx/nidaqmx_analog_input_filtering/README.md b/examples/nidaqmx/nidaqmx_analog_input_filtering/README.md new file mode 100644 index 0000000..caf340c --- /dev/null +++ b/examples/nidaqmx/nidaqmx_analog_input_filtering/README.md @@ -0,0 +1,23 @@ +Prerequisites +=============== +Requires a Physical or Simulated Device. Refer to the [Getting Started Section](https://github.com/ni/nidaqmx-python/blob/master/README.rst) to learn how to create a simulated device. +## Sample + +This is an nipanel example that displays an interactive Streamlit app and updates and fetches data from device. + +### Feature + +Script demonstrates analog input data getting continuously acquired, and being filtered. +- Supports various data types + +### Required Software + +- Python 3.9 or later + +### Usage + +```pwsh +poetry install --with examples +poetry run python examples\nidaqmx\nidaqmx_analog_input_filtering\nidaqmx_analog_input_filtering.py +``` + diff --git a/examples/nidaqmx/nidaqmx_analog_input_filtering/nidaqmx_analog_input_filtering.py b/examples/nidaqmx/nidaqmx_analog_input_filtering/nidaqmx_analog_input_filtering.py new file mode 100644 index 0000000..62b6511 --- /dev/null +++ b/examples/nidaqmx/nidaqmx_analog_input_filtering/nidaqmx_analog_input_filtering.py @@ -0,0 +1,154 @@ +"""Data acquisition script that continuously acquires analog input data.""" + +import time +from pathlib import Path + +import nidaqmx +import nidaqmx.system +from nidaqmx.constants import ( + AcquisitionType, + CurrentShuntResistorLocation, + CurrentUnits, + Edge, + ExcitationSource, + FilterResponse, + Slope, + StrainGageBridgeType, + TerminalConfiguration, +) +from nidaqmx.errors import DaqError + +import nipanel + +panel_script_path = Path(__file__).with_name("nidaqmx_analog_input_filtering_panel.py") +panel = nipanel.create_streamlit_panel(panel_script_path) +panel.set_value("is_running", False) + +system = nidaqmx.system.System.local() + +available_channel_names = [] +for dev in system.devices: + for chan in dev.ai_physical_chans: + available_channel_names.append(chan.name) +panel.set_value("available_channel_names", available_channel_names) + +available_trigger_sources = [""] +for dev in system.devices: + if hasattr(dev, "terminals"): + for term in dev.terminals: + available_trigger_sources.append(term) +panel.set_value("available_trigger_sources", available_trigger_sources) +try: + panel.set_value("daq_error", "") + print(f"Panel URL: {panel.panel_url}") + print(f"Waiting for the 'Run' button to be pressed...") + print(f"(Press Ctrl + C to quit)") + while True: + panel.set_value("run_button", False) + while not panel.get_value("run_button", False): + time.sleep(0.1) + # How to use nidaqmx: https://nidaqmx-python.readthedocs.io/en/stable/ + with nidaqmx.Task() as task: + + chan_type = panel.get_value("chan_type", "1") + + if chan_type == "2": + chan = task.ai_channels.add_ai_current_chan( + panel.get_value("physical_channel", ""), + max_val=panel.get_value("max_value_current", 0.01), + min_val=panel.get_value("min_value_current", -0.01), + ext_shunt_resistor_val=panel.get_value("shunt_resistor_value", 249.0), + shunt_resistor_loc=panel.get_value( + "shunt_location", CurrentShuntResistorLocation.EXTERNAL + ), + units=panel.get_value("units", CurrentUnits.AMPS), + ) + + elif chan_type == "3": + chan = task.ai_channels.add_ai_strain_gage_chan( + panel.get_value("physical_channel", ""), + nominal_gage_resistance=panel.get_value("gage_resistance", 350.0), + voltage_excit_source=ExcitationSource.EXTERNAL, # Only mode that works + max_val=panel.get_value("max_value_strain", 0.001), + min_val=panel.get_value("min_value_strain", -0.001), + poisson_ratio=panel.get_value("poisson_ratio", 0.3), + lead_wire_resistance=panel.get_value("wire_resistance", 0.0), + initial_bridge_voltage=panel.get_value("initial_voltage", 0.0), + gage_factor=panel.get_value("gage_factor", 2.0), + voltage_excit_val=panel.get_value("voltage_excitation_value", 0.0), + strain_config=panel.get_value( + "strain_configuration", StrainGageBridgeType.FULL_BRIDGE_I + ), + ) + else: + chan = task.ai_channels.add_ai_voltage_chan( + panel.get_value("physical_channel", ""), + terminal_config=panel.get_value( + "terminal_configuration", TerminalConfiguration.DEFAULT + ), + max_val=panel.get_value("max_value_voltage", 5.0), + min_val=panel.get_value("min_value_voltage", -5.0), + ) + task.timing.cfg_samp_clk_timing( + source=panel.get_value("source", ""), # "" - means Onboard Clock (default value) + rate=panel.get_value("rate", 1000.0), + sample_mode=AcquisitionType.CONTINUOUS, + samps_per_chan=panel.get_value("total_samples", 100), + ) + panel.set_value("sample_rate", task.timing.samp_clk_rate) + # Not all hardware supports all filter types. + # Refer to your device documentation for more information. + if panel.get_value("filter", "Filter") == "Filter": + chan.ai_filter_enable = True + chan.ai_filter_freq = panel.get_value("filter_freq", 0.0) + chan.ai_filter_response = panel.get_value("filter_response", FilterResponse.COMB) + chan.ai_filter_order = panel.get_value("filter_order", 1) + panel.set_value("actual_filter_freq", chan.ai_filter_freq) + panel.set_value("actual_filter_response", chan.ai_filter_response) + panel.set_value("actual_filter_order", chan.ai_filter_order) + else: + panel.set_value("actual_filter_freq", 0.0) + panel.set_value("actual_filter_response", FilterResponse.COMB) + panel.set_value("actual_filter_order", 0) + # Not all hardware supports all filter types. + # Refer to your device documentation for more information. + trigger_type = panel.get_value("trigger_type") + if trigger_type == "5": + task.triggers.start_trigger.cfg_anlg_edge_start_trig( + trigger_source=panel.get_value("analog_source", ""), + trigger_slope=panel.get_value("slope", Slope.FALLING), + trigger_level=panel.get_value("level", 0.0), + ) + + if trigger_type == "2": + task.triggers.start_trigger.cfg_dig_edge_start_trig( + trigger_source=panel.get_value("digital_source", ""), + trigger_edge=panel.get_value("edge", Edge.FALLING), + ) + task.triggers.start_trigger.anlg_edge_hyst = hysteresis = panel.get_value( + "hysteresis", 0.0 + ) + + try: + task.start() + panel.set_value("is_running", True) + + panel.set_value("stop_button", False) + while not panel.get_value("stop_button", False): + data = task.read( + number_of_samples_per_channel=100 # pyright: ignore[reportArgumentType] + ) + panel.set_value("acquired_data", data) + except KeyboardInterrupt: + pass + finally: + task.stop() + panel.set_value("is_running", False) + +except DaqError as e: + daq_error = str(e) + print(daq_error) + panel.set_value("daq_error", daq_error) + +except KeyboardInterrupt: + pass diff --git a/examples/nidaqmx/nidaqmx_analog_input_filtering/nidaqmx_analog_input_filtering_panel.py b/examples/nidaqmx/nidaqmx_analog_input_filtering/nidaqmx_analog_input_filtering_panel.py new file mode 100644 index 0000000..fb16937 --- /dev/null +++ b/examples/nidaqmx/nidaqmx_analog_input_filtering/nidaqmx_analog_input_filtering_panel.py @@ -0,0 +1,408 @@ +"""Streamlit visualization script to display data acquired by nidaqmx_analog_input_filtering.py.""" + +import extra_streamlit_components as stx # type: ignore[import-untyped] +import streamlit as st +from nidaqmx.constants import ( + CurrentShuntResistorLocation, + CurrentUnits, + Edge, + FilterResponse, + Slope, + StrainGageBridgeType, + TerminalConfiguration, +) +from streamlit_echarts import st_echarts + +import nipanel +from nipanel.controls import enum_selectbox + +st.set_page_config(page_title="Analog Input Filtering", page_icon="📈", layout="wide") +st.title("Analog Input - Filtering") +panel = nipanel.get_streamlit_panel_accessor() + +left_col, right_col = st.columns(2) + + +st.markdown( + """ + + + """, + unsafe_allow_html=True, +) + + +with left_col: + with st.container(border=True): + with st.container(border=True): + if panel.get_value("is_running", True): + st.button("Stop", key="stop_button") + elif not panel.get_value("is_running", True) and panel.get_value("daq_error", "") == "": + run_button = st.button("Run", key="run_button") + else: + st.error( + f"There was an error running the script. Fix the issue and re-run nidaqmx_analog_input_filtering.py \n\n {panel.get_value('daq_error', '')}" + ) + st.title("Channel Settings") + physical_channel = st.selectbox( + options=panel.get_value("available_channel_names", ["Mod2/ai0"]), + index=0, + label="Physical Channels", + disabled=panel.get_value("is_running", False), + ) + panel.set_value("physical_channel", physical_channel) + enum_selectbox( + panel, + label="Terminal Configuration", + value=TerminalConfiguration.DEFAULT, + disabled=panel.get_value("is_running", False), + key="terminal_configuration", + ) + + st.title("Timing Settings") + + source = st.selectbox( + "Sample Clock Source", + options=panel.get_value("available_trigger_sources", [""]), + index=0, + disabled=panel.get_value("is_running", False), + ) + panel.set_value("source", source) + st.number_input( + "Sample Rate", + value=1000.0, + min_value=1.0, + step=1.0, + disabled=panel.get_value("is_running", False), + key="rate", + ) + st.number_input( + "Number of Samples", + value=100, + min_value=1, + step=1, + disabled=panel.get_value("is_running", False), + key="total_samples", + ) + st.number_input( + "Actual Sample Rate", + value=panel.get_value("sample_rate", 1000.0), + key="actual_sample_rate", + step=1.0, + disabled=True, + ) + st.title("Filtering Settings") + + filter = st.selectbox( + "Filter", + options=["No Filtering", "Filter"], + disabled=panel.get_value("is_running", False), + ) + panel.set_value("filter", filter) + enum_selectbox( + panel, + label="Filter Response", + value=FilterResponse.COMB, + disabled=panel.get_value("is_running", False), + key="filter_response", + ) + + filter_freq = st.number_input( + "Filtering Frequency", + value=1000.0, + step=1.0, + disabled=panel.get_value("is_running", False), + ) + filter_order = st.number_input( + "Filter Order", + min_value=0, + max_value=1, + value=1, + disabled=panel.get_value("is_running", False), + ) + st.selectbox( + "Actual Filter Frequency", + options=[panel.get_value("actual_filter_freq", 0.0)], + disabled=True, + ) + st.selectbox( + "Actual Filter Order", + options=[panel.get_value("actual_filter_order", 0)], + disabled=True, + ) + +with right_col: + + with st.container(border=True): + st.title("Acquired Data") + acquired_data = panel.get_value("acquired_data", [0.0]) + sample_rate = panel.get_value("sample_rate", 100.0) + acquired_data_graph = { + "animation": False, + "tooltip": {"trigger": "axis"}, + "legend": {"data": ["Voltage (V)"]}, + "xAxis": { + "type": "category", + "data": [x / sample_rate for x in range(len(acquired_data))], + "name": "Time", + "nameLocation": "center", + "nameGap": 40, + }, + "yAxis": { + "type": "value", + "name": "Volts", + "nameRotate": 90, + "nameLocation": "center", + "nameGap": 40, + }, + "series": [ + { + "name": "voltage_amplitude", + "type": "line", + "data": acquired_data, + "emphasis": {"focus": "series"}, + "smooth": True, + "seriesLayoutBy": "row", + }, + ], + } + st_echarts(options=acquired_data_graph, height="400px", key="graph", width="100%") + + st.title("Trigger Settings") + trigger_type = stx.tab_bar( + data=[ + stx.TabBarItemData(id=1, title="No Trigger", description=""), + stx.TabBarItemData(id=2, title="Digital Start", description=""), + stx.TabBarItemData(id=3, title="Digital Pause", description=""), + stx.TabBarItemData(id=4, title="Digital Reference", description=""), + stx.TabBarItemData(id=5, title="Analog Start", description=""), + stx.TabBarItemData(id=6, title="Analog Pause", description=""), + stx.TabBarItemData(id=7, title="Analog Reference", description=""), + ], + default=1, + ) + panel.set_value("trigger_type", trigger_type) + + if trigger_type == "1": + with st.container(border=True): + st.write( + "To enable triggers, select a tab above, and configure the settings. \n Not all hardware supports all trigger types. Refer to your device documentation for more information." + ) + if trigger_type == "2": + with st.container(border=True): + source = st.selectbox( + "Source", options=panel.get_value("available_trigger_sources", [""]) + ) + panel.set_value("digital_source", source) + enum_selectbox( + panel, + label="Edge", + value=Edge.FALLING, + disabled=panel.get_value("is_running", False), + key="edge", + ) + if trigger_type == "3": + with st.container(border=True): + st.write( + "This trigger type is not supported in continuous sample timing. Refer to your device documentation for more information on which triggers are supported" + ) + if trigger_type == "4": + with st.container(border=True): + st.write( + "This trigger type is not supported in continuous sample timing. Refer to your device documentation for more information on which triggers are supported" + ) + if trigger_type == "5": + with st.container(border=True): + analog_source = st.text_input("Source", "APFI0", key="analog_source") + enum_selectbox( + panel, + label="Slope", + value=Slope.FALLING, + disabled=panel.get_value("is_running", False), + key="slope", + ) + + level = st.number_input("Level", key="level") + hysteriesis = st.number_input( + "Hysteriesis", + disabled=panel.get_value("is_running", False), + key="hysteriesis", + ) + + if trigger_type == "6": + with st.container(border=True): + st.write( + "This trigger type is not supported in continuous sample timing. Refer to your device documentation for more information on which triggers are supported" + ) + if trigger_type == "7": + with st.container(border=True): + st.write( + "This trigger type is not supported in continuous sample timing. Refer to your device documentation for more information on which triggers are supported." + ) + st.title("Task Types") + + chan_type = stx.tab_bar( + data=[ + stx.TabBarItemData(id=1, title="Voltage", description=""), + stx.TabBarItemData(id=2, title="Current", description=""), + stx.TabBarItemData(id=3, title="Strain Gage", description=""), + ], + default=1, + ) + + panel.set_value("chan_type", chan_type) + if chan_type == "1": + with st.container(border=True): + st.title("Voltage Data") + channel_left, channel_right = st.columns(2) + with channel_left: + max_value_voltage = st.number_input( + "Max Value", + value=5.0, + step=0.1, + disabled=panel.get_value("is_running", False), + key="max_value_voltage", + ) + + min_value_voltage = st.number_input( + "Min Value", + value=-5.0, + step=0.1, + disabled=panel.get_value("is_running", False), + key="min_value_voltage", + ) + + if chan_type == "2": + with st.container(border=True): + st.title("Current Data") + channel_left, channel_right = st.columns(2) + with channel_left: + enum_selectbox( + panel, + label="Shunt Resistor Location", + value=CurrentShuntResistorLocation.EXTERNAL, + disabled=panel.get_value("is_running", False), + key="shunt_location", + ) + enum_selectbox( + panel, + label="Units", + value=CurrentUnits.AMPS, + disabled=panel.get_value("is_running", False), + key="units", + ) + min_value_current = st.number_input( + "Min Value", + value=-0.01, + step=0.001, + disabled=panel.get_value("is_running", False), + ) + max_value_current = st.number_input( + "Max Value", + value=0.01, + step=1.0, + key="max_value_current", + disabled=panel.get_value("is_running", False), + ) + shunt_resistor_value = st.number_input( + "Shunt Resistor Value", + value=249.0, + step=1.0, + disabled=panel.get_value("is_running", False), + ) + if chan_type == "3": + with st.container(border=True): + st.title("Strain Gage Data") + channel_left, channel_right = st.columns(2) + with channel_left: + min_value_strain = st.number_input( + "Min Value", + value=-0.01, + step=0.01, + key="min_value_strain", + ) + max_value_strain = st.number_input( + "Max Value", + value=0.01, + step=0.01, + max_value=2.0, + key="max_value_strain", + ) + enum_selectbox( + panel, + label="Strain Units", + value=CurrentUnits.AMPS, + disabled=panel.get_value("is_running", False), + key="strain_units", + ) + st.title("Strain Gage Information") + gage_factor = st.number_input( + "Gage Factor", + value=2.0, + step=1.0, + disabled=panel.get_value("is_running", False), + key="gage_factor", + ) + nominal_gage = st.number_input( + "Nominal Gage Resistance", + value=350.0, + step=1.0, + disabled=panel.get_value("is_running", False), + key="gage_resistance", + ) + poisson_ratio = st.number_input( + "Poisson Ratio", + value=0.3, + step=1.0, + disabled=panel.get_value("is_running", False), + key="poisson_ratio", + ) + st.title("Bridge Information") + enum_selectbox( + panel, + label="Strain Configuration", + value=StrainGageBridgeType.FULL_BRIDGE_I, + disabled=panel.get_value("is_running", False), + key="strain_configuration", + ) + wire_resistance = st.number_input( + "Lead Wire Resistance", + value=0.0, + step=1.0, + key="wire_resistance", + ) + initial_voltage = st.number_input( + "Initial Bridge Voltage", + value=0.0, + step=1.0, + disabled=panel.get_value("is_running", False), + key="initial_voltage", + ) + + st.selectbox( + label="Voltage Excitation Source", + key="voltage_excit", + options=["External"], + disabled=True, + ) + panel.set_value("voltage_excitation_source", "voltage_excit") + voltage_excit = st.number_input( + "Voltage Excitation Value", + value=2.5, + step=1.0, + key="voltage_excitation_value", + disabled=panel.get_value("is_running", False), + ) diff --git a/examples/nidaqmx/README.md b/examples/nidaqmx/nidaqmx_continuous_analog_input/README.md similarity index 100% rename from examples/nidaqmx/README.md rename to examples/nidaqmx/nidaqmx_continuous_analog_input/README.md diff --git a/examples/nidaqmx/nidaqmx_continuous_analog_input.py b/examples/nidaqmx/nidaqmx_continuous_analog_input/nidaqmx_continuous_analog_input.py similarity index 97% rename from examples/nidaqmx/nidaqmx_continuous_analog_input.py rename to examples/nidaqmx/nidaqmx_continuous_analog_input/nidaqmx_continuous_analog_input.py index f8210a8..ec089ad 100644 --- a/examples/nidaqmx/nidaqmx_continuous_analog_input.py +++ b/examples/nidaqmx/nidaqmx_continuous_analog_input/nidaqmx_continuous_analog_input.py @@ -59,7 +59,7 @@ logging_mode=panel.get_value("logging_mode", LoggingMode.OFF), operation=LoggingOperation.OPEN_OR_CREATE, ) - panel.set_value("sample_rate", task._timing.samp_clk_rate) + panel.set_value("sample_rate", task.timing.samp_clk_rate) try: print(f"Starting data acquisition...") task.start() diff --git a/examples/nidaqmx/nidaqmx_continuous_analog_input_panel.py b/examples/nidaqmx/nidaqmx_continuous_analog_input/nidaqmx_continuous_analog_input_panel.py similarity index 100% rename from examples/nidaqmx/nidaqmx_continuous_analog_input_panel.py rename to examples/nidaqmx/nidaqmx_continuous_analog_input/nidaqmx_continuous_analog_input_panel.py diff --git a/poetry.lock b/poetry.lock index 7143409..74a657d 100644 --- a/poetry.lock +++ b/poetry.lock @@ -502,6 +502,20 @@ typing-extensions = {version = ">=4.6.0", markers = "python_version < \"3.13\""} [package.extras] test = ["pytest (>=6)"] +[[package]] +name = "extra-streamlit-components" +version = "0.1.80" +description = "An all-in-one place, to find complex or just natively unavailable components on streamlit." +optional = false +python-versions = ">=3.6" +files = [ + {file = "extra_streamlit_components-0.1.80-py3-none-any.whl", hash = "sha256:7a8c151da5dcd1f1f97b6c29caa812a1d77928d20fc4bf42a3a4fd788274dd9e"}, + {file = "extra_streamlit_components-0.1.80.tar.gz", hash = "sha256:87d6c38e07381501d8882796adef17d1c1d4e3a79615864d95c1163d2bc136f6"}, +] + +[package.dependencies] +streamlit = ">=1.40.1" + [[package]] name = "flake8" version = "5.0.4" @@ -3263,4 +3277,4 @@ files = [ [metadata] lock-version = "2.0" python-versions = ">=3.9,<4.0,!=3.9.7" -content-hash = "45dc36d0ef51734fbde48be7f49ed705209f3b0d46c827cd0de4115fbf15ce62" +content-hash = "1b4c4105e78ce7eca099f1a45a130c93565d568f2263e70d6389ab59ed3bbe27" diff --git a/pyproject.toml b/pyproject.toml index 199b079..41393ee 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -64,6 +64,7 @@ optional = true [tool.poetry.group.examples.dependencies] streamlit-echarts = ">=0.4.0" +extra-streamlit-components = "^0.1.80" nidaqmx = { version = ">=0.8.0", allow-prereleases = true } niscope = "^1.4.9"