From 9ea49a2a59f62c36cbadf752a216da12c1b2ccd0 Mon Sep 17 00:00:00 2001
From: SGudla <Saikiran.Gudla@analog.com>
Date: Mon, 6 Nov 2023 15:08:51 +0530
Subject: [PATCH] Add adistream CLI tool

Add adistream CLI tool to support data generation
and streaming for tx devices like DACs.
Update doc to include new tools optional
dependency.

Signed-off-by: SGudla <Saikiran.Gudla@analog.com>
---
 MANIFEST.in                 |   1 +
 adi/__init__.py             |   5 +
 adi/tools/__init__.py       |   0
 adi/tools/adistream.py      | 293 ++++++++++++++++++++++++++++++++++++
 doc/source/guides/quick.rst |  10 ++
 pyproject.toml              |   6 +
 6 files changed, 315 insertions(+)
 create mode 100644 adi/tools/__init__.py
 create mode 100644 adi/tools/adistream.py

diff --git a/MANIFEST.in b/MANIFEST.in
index e67e0b8a0..469ab9511 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -1,6 +1,7 @@
 include LICENSE
 include README.md
 include adi/*
+include adi/tools/*
 
 exclude setup.cfg
 
diff --git a/adi/__init__.py b/adi/__init__.py
index dd8372a0a..4169a8319 100644
--- a/adi/__init__.py
+++ b/adi/__init__.py
@@ -109,5 +109,10 @@
 except ImportError:
     pass
 
+try:
+    from adi.tools.adistream import run_adi_stream
+except ImportError:
+    pass
+
 __version__ = "0.0.17"
 name = "Analog Devices Hardware Interfaces"
diff --git a/adi/tools/__init__.py b/adi/tools/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/adi/tools/adistream.py b/adi/tools/adistream.py
new file mode 100644
index 000000000..dd151d121
--- /dev/null
+++ b/adi/tools/adistream.py
@@ -0,0 +1,293 @@
+import argparse
+from time import sleep
+
+import adi
+import numpy as np
+from genalyzer import WaveformGen
+from pynput import keyboard
+
+
+class ADIStream(object):
+    def __init__(
+        self,
+        classname,
+        uri,
+        v_ref_n,
+        v_ref_p,
+        npts,
+        chn_list,
+        v_req_l,
+        v_req_u,
+        out_freq,
+        code_sel,
+        wave_list,
+        device_name,
+    ):
+        self.duty_cyc = None
+        self.sampling_freq = None
+        self.data = []
+        self.line = 0
+        self.data_stream_abort = False
+        self.classname = classname
+        self.stream = eval(
+            "adi."
+            + classname
+            + "(uri='"
+            + uri
+            + "', device_name='"
+            + device_name
+            + "')"
+        )
+        self.stream.tx_cyclic_buffer = True
+        self.wave_list = wave_list
+
+        self.chan_list = []
+        for val in chn_list:
+            self.chan_list += ["voltage" + str(val)]
+        self.stream.tx_enabled_channels = self.chan_list
+        self.chn_cnt = len(chn_list)
+
+        # get resolution
+        self.resolution = self.stream.output_bits[0]
+        if self.resolution > 16:
+            self.stream._tx_data_type = np.int32
+        elif self.resolution > 8:
+            self.stream._tx_data_type = np.int16
+        else:
+            self.stream._tx_data_type = np.int8
+
+        self.npts = npts
+        self.v_ref_n = v_ref_n
+        self.v_ref_p = v_ref_p
+        self.v_req_l = v_req_l
+        self.v_req_u = v_req_u
+        self.out_freq = out_freq
+        self.code_sel = code_sel
+
+    def write_buffered_data(self):
+
+        # Send data from device
+        self.stream.tx(self.data)
+
+        if self.line == 0:
+            print("Data streaming started >>")
+
+    def get_data(self):
+
+        try:
+            self.sampling_freq = int(self.stream.sampling_frequency)
+        except AttributeError:
+            print(
+                f"The device {self.classname} has no attribute named "
+                "sampling_frequency"
+                ""
+            )
+
+        if self.out_freq is None:
+            self.out_freq = self.sampling_freq / self.npts
+
+        if self.v_req_l is None:
+            self.v_req_l = self.v_ref_n
+
+        if self.v_req_u is None:
+            self.v_req_u = self.v_ref_p
+
+        if self.out_freq > self.sampling_freq / self.npts:
+            print(
+                f"The nearest possible output frequency that can be generated "
+                f"with the current config is {self.sampling_freq / self.npts} Hz"
+            )
+            self.out_freq = self.sampling_freq / self.npts
+            val = input("\nPress y to continue, any other key to abort: ")
+            if val.lower() != "y":
+                raise Exception("Recheck the configuration and try again..!")
+        elif self.out_freq < self.sampling_freq / self.npts:
+            self.stream.sampling_frequency = self.out_freq * self.npts
+            print(
+                "Actual generated output frequency: ",
+                str(int(self.sampling_freq) // self.npts),
+            )
+
+        gen_obj = WaveformGen(
+            self.npts,
+            self.out_freq,
+            self.code_sel,
+            self.resolution,
+            self.v_ref_n,
+            self.v_ref_p,
+            self.v_req_l,
+            self.v_req_u,
+        )
+
+        if "pwm" in self.wave_list:
+            self.duty_cyc = str(
+                float(input("Enter duty cycle in percent: Eg. 25 for 25% duty-cycle: "))
+                / 100.0
+            )
+
+        # get data using the genalyzer apis
+        for val in range(self.chn_cnt):
+            if self.chn_cnt == 1:
+                if self.wave_list[val] == "pwm":
+                    self.data = eval(
+                        "gen_obj.gen_pwm_wave(duty_cycle = " + self.duty_cyc + ")"
+                    )
+                else:
+                    self.data = eval("gen_obj.gen_" + self.wave_list[val] + "_wave()")
+            else:
+                if self.wave_list[val] == "pwm":
+                    self.data.append(
+                        eval("gen_obj.gen_pwm_wave(duty_cycle = " + self.duty_cyc + ")")
+                    )
+                else:
+                    self.data.append(
+                        eval("gen_obj.gen_" + self.wave_list[val] + "_wave()")
+                    )
+
+        self.data = np.array(self.data)
+
+    def key_press_event(self, key):
+        if key == keyboard.Key.delete:
+            self.data_stream_abort = True
+
+    def do_data_streaming(self):
+        self.get_data()
+
+        listener = keyboard.Listener(on_press=self.key_press_event)
+        listener.start()
+
+        print("Press " "delete" " key to stop cyclic data streaming..")
+        sleep(2)
+
+        self.write_buffered_data()
+
+        while not self.data_stream_abort:
+            # This should halt the control for the cyclic mode to work.
+            print("." * self.line, end="\r")
+
+            self.line = self.line + 1
+            if self.line == 100:
+                self.line = 1
+                print("\n", end="\r")
+
+        self.stream.tx_destroy_buffer()
+        print("\r\nData streaming finished\r\n")
+
+
+def run_adi_stream():
+    parser = argparse.ArgumentParser(
+        description="ADI data streaming app",
+        formatter_class=argparse.RawTextHelpFormatter,
+    )
+    parser.add_argument(
+        "class", help="pyadi class name to stream data to", action="store"
+    )
+    parser.add_argument("uri", help="URI of target device", action="store")
+    parser.add_argument(
+        "neg_voltage_ref",
+        help="Negative reference voltage of DAC in volts. \nInput 0 for unipolar DACs",
+        action="store",
+        type=float,
+        default=0,
+    )
+    parser.add_argument(
+        "pos_voltage_ref",
+        help="Positive reference voltage of DAC in volts.",
+        action="store",
+        type=float,
+    )
+
+    parser.add_argument(
+        "-d",
+        "--device_name",
+        help="Part name if the class supports more than 1 generic",
+        action="store",
+        default="",
+    )
+    parser.add_argument(
+        "-n",
+        "--data_points_per_wave",
+        help="Number of data points required per wave\n"
+        "Default value is 50 samples per wave",
+        action="store",
+        default=50,
+        type=int,
+    )
+    parser.add_argument(
+        "-cl",
+        "--chn_list",
+        help="Channels list to stream data to\nE.g. --chn_list 0 2 3 to stream data to channels 0, 2, 3\n"
+        "Default is channel 0",
+        nargs="+",
+        action="store",
+        default=[0],
+        type=int,
+    )
+    parser.add_argument(
+        "-vl",
+        "--v_lower_req",
+        help="Lower end Voltage required in volts. Should be in the accepted FSR.",
+        action="store",
+        type=float,
+    )
+    parser.add_argument(
+        "-vu",
+        "--v_upper_req",
+        help="Upper end Voltage required in volts. Should be in the accepted FSR.",
+        action="store",
+        type=float,
+    )
+    parser.add_argument(
+        "-f",
+        "--output_freq",
+        help="Output frequency required.\nNote: The effective output frequency per "
+        "channel will be the output frequency required divided by the number of"
+        " active channels enabled.\n"
+        "Default will be the maximum sampling frequency supported by the app.",
+        action="store",
+        type=int,
+    )
+    parser.add_argument(
+        "-c",
+        "--code_sel",
+        help="code data format to stream data in. \nAccepted:\n\t0 for binary offset"
+        "\n\t1 for 2s-complement.\nDefault is 0 (binary offset)",
+        action="store",
+        default=0,
+        type=int,
+        choices=[0, 1],
+    )
+    parser.add_argument(
+        "-w",
+        "--wave_types",
+        help="list of waveform type required. "
+        "\nAccepted: \n\tsine"
+        "\n\tcosine "
+        "\n\ttriangular "
+        "\n\tsquare "
+        "\n\tpwm"
+        "\nDefault is sine"
+        "\nE.g. --wave_types sine square triangular ",
+        nargs="+",
+        action="store",
+        default=["sine"],
+        choices=["sine", "cosine", "triangular", "square", "pwm"],
+    )
+
+    args = vars(parser.parse_args())
+
+    app = ADIStream(
+        args["class"],
+        args["uri"],
+        args["neg_voltage_ref"],
+        args["pos_voltage_ref"],
+        args["data_points_per_wave"],
+        args["chn_list"],
+        args["v_lower_req"],
+        args["v_upper_req"],
+        args["output_freq"],
+        args["code_sel"],
+        args["wave_types"],
+        args["device_name"],
+    )
+    app.do_data_streaming()
diff --git a/doc/source/guides/quick.rst b/doc/source/guides/quick.rst
index 7895e32d5..a493b92ef 100644
--- a/doc/source/guides/quick.rst
+++ b/doc/source/guides/quick.rst
@@ -30,6 +30,16 @@ To install the optional dependencies for JESD debugging and control
 
 Note that this is only needed for the ADRV9009-ZU11EG multi-SOM configuration.
 
+To install the optional dependencies for leverage the built-in CLI tools
+
+.. code-block:: bash
+
+  (sudo) pip install pyadi-iio[tools]
+  
+.. note::
+
+  For the adistream CLI tool to generate and stream data to the device, it's also needed to install the `genalyzer <https://analogdevicesinc.github.io/genalyzer/master/setup.html#windows>`_ and its python bindings.
+ 
 .. note::
 
   On Linux the libiio python bindings are sometimes installed in locations not on path when building from source. On Ubuntu this is a common fix
diff --git a/pyproject.toml b/pyproject.toml
index 2ebf5c698..20b0493f7 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -45,12 +45,18 @@ namespaces = true
 jesd = [
     "paramiko"
 ]
+tools = [
+    "pynput"
+]
 
 [project.urls]
 homepage = "https://analogdevicesinc.github.io/pyadi-iio/"
 documentation = "https://analogdevicesinc.github.io/pyadi-iio/"
 repository = "https://github/analogdevicesinc/pyadi-iio"
 
+[project.gui-scripts]
+adistream = "adi.tools.adistream:run_adi_stream"
+
 [tool.isort]
 multi_line_output=3
 include_trailing_comma="True"