diff --git a/.github/scripts/install_iioemu.sh b/.github/scripts/install_iioemu.sh old mode 100644 new mode 100755 diff --git a/.github/scripts/install_libiio.sh b/.github/scripts/install_libiio.sh index c4b0123f3..6a090a041 100755 --- a/.github/scripts/install_libiio.sh +++ b/.github/scripts/install_libiio.sh @@ -1,9 +1,29 @@ #!/bin/bash + +# Set LIBIIO_BRANCH if not set +if [ -z "$LIBIIO_BRANCH" ]; then + LIBIIO_BRANCH="v0.25" +fi + + sudo apt-get -qq update -sudo apt-get install -y git cmake graphviz libavahi-common-dev libavahi-client-dev libaio-dev libusb-1.0-0-dev libxml2-dev rpm tar bzip2 gzip flex bison git -git clone -b 'v0.25' --single-branch --depth 1 https://github.com/analogdevicesinc/libiio.git +sudo apt-get install -y git cmake graphviz libzstd-dev libavahi-common-dev libavahi-client-dev libaio-dev libusb-1.0-0-dev libxml2-dev rpm tar bzip2 gzip flex bison git +git clone -b $LIBIIO_BRANCH --single-branch --depth 1 https://github.com/analogdevicesinc/libiio.git cd libiio cmake . -DHAVE_DNS_SD=OFF make sudo make install cd .. +rm -rf libiio + +# Python pieces +sudo apt-get install -y python3-pip python3-setuptools +git clone -b $LIBIIO_BRANCH --single-branch --depth 1 https://github.com/analogdevicesinc/libiio.git +cd libiio +cmake . -DHAVE_DNS_SD=OFF -DPYTHON_BINDINGS=ON +make +cd bindings/python +pip install . +cd ../.. +cd .. +rm -rf libiio diff --git a/.github/scripts/install_part_libs.sh b/.github/scripts/install_part_libs.sh index 5109bb93a..82df7d733 100755 --- a/.github/scripts/install_part_libs.sh +++ b/.github/scripts/install_part_libs.sh @@ -1,7 +1,9 @@ #!/bin/bash +exit 0 + git clone -b 'master' --single-branch --depth 1 https://github.com/analogdevicesinc/libad9361-iio.git cd libad9361-iio -cmake -DPYTHON_BINDINGS=ON . +cmake -DPYTHON_BINDINGS=ON -DLIBIIO_INCLUDEDIR=/usr/include/iio . make cd bindings/python pip install . @@ -13,7 +15,7 @@ rm -rf libad9361-iio git clone -b 'master' --single-branch --depth 1 https://github.com/analogdevicesinc/libad9166-iio.git cd libad9166-iio -cmake -DPYTHON_BINDINGS=ON . +cmake -DPYTHON_BINDINGS=ON -DLIBIIO_INCLUDEDIR=/usr/include/iio . make cd bindings/python pip install . @@ -23,4 +25,4 @@ sudo ldconfig cd .. rm -rf libad9166-iio -pip install pylibiio==0.23.1 +# pip install pylibiio==0.23.1 diff --git a/.github/scripts/install_pydeps.sh b/.github/scripts/install_pydeps.sh old mode 100644 new mode 100755 index 64849051c..c065a6219 --- a/.github/scripts/install_pydeps.sh +++ b/.github/scripts/install_pydeps.sh @@ -2,4 +2,10 @@ sudo apt-get install -y python3-pip python3-setuptools pip install -r requirements.txt pip install -r requirements_dev.txt -pip install pylibiio + + +if [ "$LIBIIO_BRANCH" = "main" ]; then + pip install --no-deps --force-reinstall pytest-libiio@git+https://github.com/tfcollins/pytest-libiio.git@libiio-v1-support +else + pip install pytest-libiio==0.0.14 +fi diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 0e7233e14..c8c90452f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -8,6 +8,7 @@ jobs: strategy: matrix: python-version: [3.7, 3.8, 3.9, "3.10"] + libiio: ['main', 'v0.25'] steps: - uses: actions/checkout@v2 @@ -18,6 +19,7 @@ jobs: - name: Install dependencies run: | + export LIBIIO_BRANCH=${{ matrix.libiio }} bash ./.github/scripts/install_libiio.sh bash ./.github/scripts/install_pydeps.sh bash ./.github/scripts/install_part_libs.sh @@ -31,6 +33,7 @@ jobs: fail-fast: false matrix: python-version: [3.7, 3.8, 3.9, "3.10"] + libiio: ['main', 'v0.25'] steps: - uses: actions/checkout@v2 @@ -41,6 +44,7 @@ jobs: - name: Install dependencies run: | + export LIBIIO_BRANCH=${{ matrix.libiio }} bash ./.github/scripts/install_libiio.sh bash ./.github/scripts/install_part_libs.sh bash ./.github/scripts/install_iioemu.sh @@ -50,6 +54,11 @@ jobs: - name: Test run: | pytest -vs --cov=adi --scan-verbose --emu --junitxml="results.xml" -k 'not prod' + - name: Publish Test Report + uses: mikepenz/action-junit-report@v4 + if: success() || failure() # always run even if the previous step fails + with: + report_paths: 'results.xml' - name: Report coverage if: (github.event_name != 'pull_request') && (matrix.python-version == 3.6) @@ -67,6 +76,11 @@ jobs: run: | pip uninstall -y paramiko pytest -vs --scan-verbose --emu --junitxml="results.xml" -k 'not prod' + - name: Publish Test Report + uses: mikepenz/action-junit-report@v4 + if: success() || failure() # always run even if the previous step fails + with: + report_paths: 'results.xml' Lint: diff --git a/.github/workflows/win-test.yml b/.github/workflows/win-test.yml index ff4d3f8bf..3441ce59c 100644 --- a/.github/workflows/win-test.yml +++ b/.github/workflows/win-test.yml @@ -1,6 +1,7 @@ name: Windows Tests -on: [push, pull_request] +# on: [push, pull_request] +on: [pull_request] jobs: CondaBased: diff --git a/adi/compat.py b/adi/compat.py new file mode 100644 index 000000000..27c4af2cd --- /dev/null +++ b/adi/compat.py @@ -0,0 +1,192 @@ +# Copyright (C) 2023 Analog Devices, Inc. +# +# SPDX short identifier: ADIBSD +"""Compatibility module for libiio v1.X.""" + +from typing import Any, Dict, List, Optional, Tuple, Union + +import iio + +import numpy as np + + +def _is_libiio_v1() -> bool: + """Check is we are using >= v1.X.""" + v = iio.version + return v[0] >= 1 + + +class compat_libiio_v1_rx: + """Compatibility class for libiio v1.X RX.""" + + _rx_buffer_mask = None + _rx_stream = None + _rx_buffer_num_blocks = 4 + + def _rx_init_channels(self): + if not self._rx_buffer_mask: + self._rx_buffer_mask = iio.ChannelsMask(self._rxadc) + + channels = [] + if self._complex_data: + for m in self.rx_enabled_channels: + v = self._rxadc.find_channel(self._rx_channel_names[m * 2]) + channels.append(v) + v = self._rxadc.find_channel(self._rx_channel_names[m * 2 + 1]) + channels.append(v) + else: + for m in self.rx_enabled_channels: + v = self._rxadc.find_channel(self._rx_channel_names[m]) + channels.append(v) + + self._rx_buffer_mask.channels = channels + + self._rxbuf = iio.Buffer(self._rxadc, self._rx_buffer_mask) + self._rx_stream = iio.Stream( + buffer=self._rxbuf, + nb_blocks=self._rx_buffer_num_blocks, + samples_count=self.rx_buffer_size, + ) + + def _rx_buffered_data(self): + if not self._rx_stream: + self._rx_init_channels() + + block = next(self._rx_stream) + + data_channel_interleaved = [] + for chan in self._rx_buffer_mask.channels: + bytearray_data = chan.read(block) + # create format strings + df = chan.data_format + fmt = ("i" if df.is_signed is True else "u") + str(df.length // 8) + data_channel_interleaved.append(np.frombuffer(bytearray_data, dtype=fmt)) + + return data_channel_interleaved + + +class compat_libiio_v1_tx: + """Compatibility class for libiio v1.X TX.""" + + _tx_buffer_mask = None + _tx_stream = None + _tx_buffer_num_blocks = 4 + _tx_block = None + + def _tx_init_channels(self): + if not self._tx_buffer_mask: + self._tx_buffer_mask = iio.ChannelsMask(self._txdac) + + channels = [] + if self._complex_data: + for m in self.tx_enabled_channels: + v = self._txdac.find_channel(self._tx_channel_names[m * 2], True) + channels.append(v) + v = self._txdac.find_channel(self._tx_channel_names[m * 2 + 1], True) + channels.append(v) + else: + for m in self.tx_enabled_channels: + v = self._txdac.find_channel(self._tx_channel_names[m], True) + channels.append(v) + + self._tx_buffer_mask.channels = channels + + self._txbuf = iio.Buffer(self._txdac, self._tx_buffer_mask) + if not self._tx_cyclic_buffer: + self._tx_stream = iio.Stream( + buffer=self._txbuf, + nb_blocks=self._tx_buffer_num_blocks, + samples_count=self._tx_buffer_size, + ) + else: + self._tx_block = iio.Block(self._txbuf, self._tx_buffer_size) + + def _tx_buffer_push(self, data): + """Push data to TX buffer. + + data: bytearray + """ + if self._tx_cyclic_buffer: + self._tx_block.write(data) + self._tx_block.enqueue(None, self._tx_cyclic_buffer) + self._txbuf.enabled = True + else: + block = next(self._tx_stream) + block.write(data) + + +class compat_libiio_v0_rx: + """Compatibility class for libiio v0.X RX.""" + + def _rx_init_channels(self): + for m in self._rx_channel_names: + v = self._rxadc.find_channel(m) + if not v: + raise Exception(f"Channel {m} not found") + v.enabled = False + + if self._complex_data: + for m in self.rx_enabled_channels: + v = self._rxadc.find_channel(self._rx_channel_names[m * 2]) + v.enabled = True + v = self._rxadc.find_channel(self._rx_channel_names[m * 2 + 1]) + v.enabled = True + else: + for m in self.rx_enabled_channels: + v = self._rxadc.find_channel(self._rx_channel_names[m]) + v.enabled = True + self._rxbuf = iio.Buffer(self._rxadc, self._rx_buffer_size, False) + + def _rx_buffered_data(self) -> Union[List[np.ndarray], np.ndarray]: + """_rx_buffered_data: Read data from RX buffer + + Returns: + List of numpy arrays containing the data from the RX buffer that are + channel interleaved + """ + if not self._rxbuf: + self._rx_init_channels() + self._rxbuf.refill() + + data_channel_interleaved = [] + ecn = [] + if self._complex_data: + for m in self.rx_enabled_channels: + ecn.extend( + (self._rx_channel_names[m * 2], self._rx_channel_names[m * 2 + 1]) + ) + else: + ecn = [self._rx_channel_names[m] for m in self.rx_enabled_channels] + + for name in ecn: + chan = self._rxadc.find_channel(name) + bytearray_data = chan.read(self._rxbuf) # Do local type conversion + # create format strings + df = chan.data_format + fmt = ("i" if df.is_signed is True else "u") + str(df.length // 8) + data_channel_interleaved.append(np.frombuffer(bytearray_data, dtype=fmt)) + + return data_channel_interleaved + + +class compat_libiio_v0_tx: + """Compatibility class for libiio v0.X TX.""" + + def _tx_init_channels(self): + if self._complex_data: + for m in self.tx_enabled_channels: + v = self._txdac.find_channel(self._tx_channel_names[m * 2], True) + v.enabled = True + v = self._txdac.find_channel(self._tx_channel_names[m * 2 + 1], True) + v.enabled = True + else: + for m in self.tx_enabled_channels: + v = self._txdac.find_channel(self._tx_channel_names[m], True) + v.enabled = True + self._txbuf = iio.Buffer( + self._txdac, self._tx_buffer_size, self._tx_cyclic_buffer + ) + + def _tx_buffer_push(self, data): + self._txbuf.write(bytearray(data)) + self._txbuf.push() diff --git a/adi/dds.py b/adi/dds.py index 33c99d544..22247c8a0 100644 --- a/adi/dds.py +++ b/adi/dds.py @@ -142,6 +142,8 @@ def dds_single_tone(self, frequency, scale, channel=0): + "_F1", True, ) + if not chan: + raise Exception(f"DDS Channel {channel} not found") chan.attrs["frequency"].value = str(frequency) chan.attrs["phase"].value = str(90000) chan.attrs["scale"].value = str(scale) diff --git a/adi/rx_tx.py b/adi/rx_tx.py index 23fbb8b7b..8cd418f86 100644 --- a/adi/rx_tx.py +++ b/adi/rx_tx.py @@ -7,11 +7,19 @@ import iio +import adi.compat as cl import numpy as np from adi.attribute import attribute from adi.context_manager import context_manager from adi.dds import dds +if cl._is_libiio_v1(): + from adi.compat import compat_libiio_v1_rx as crx + from adi.compat import compat_libiio_v1_tx as ctx +else: + from adi.compat import compat_libiio_v0_rx as crx + from adi.compat import compat_libiio_v0_tx as ctx + class phy(attribute): _ctrl: iio.Device = [] @@ -27,7 +35,7 @@ def _annotate(self, data, cnames: List[str], echans: List[int]): return {cnames[ec]: data[i] for i, ec in enumerate(echans)} -class rx(rx_tx_common): +class rx_core(rx_tx_common, metaclass=ABCMeta): """Buffer handling for receive devices""" _rxadc: iio.Device = [] @@ -36,10 +44,10 @@ class rx(rx_tx_common): _rx_data_type = np.int16 _rx_data_si_type = np.int16 _rx_shift = 0 - __rx_buffer_size = 1024 + _rx_buffer_size = 1024 __rx_enabled_channels = [0] _rx_output_type = "raw" - __rxbuf = None + _rxbuf = None _rx_unbuffered_data = False _rx_annotated = False _rx_stack_interleaved = True # Convert from channel to sample interleaved @@ -84,11 +92,11 @@ def rx_output_type(self, value: str): @property def rx_buffer_size(self): """rx_buffer_size: Size of receive buffer in samples""" - return self.__rx_buffer_size + return self._rx_buffer_size @rx_buffer_size.setter def rx_buffer_size(self, value): - self.__rx_buffer_size = value + self._rx_buffer_size = value @property def rx_enabled_channels(self) -> Union[List[int], List[str]]: @@ -145,10 +153,10 @@ def _num_rx_channels_enabled(self): def rx_destroy_buffer(self): """rx_destroy_buffer: Clears RX buffer""" - self.__rxbuf = None + self._rxbuf = None def __del__(self): - self.__rxbuf = [] + self._rxbuf = [] if hasattr("self", "_rxadc") and self._rxadc: for m in self._rx_channel_names: v = self._rxadc.find_channel(m) @@ -177,25 +185,6 @@ def __get_rx_channel_offsets(self): rx_offset.append(offset) return rx_offset - def _rx_init_channels(self): - for m in self._rx_channel_names: - v = self._rxadc.find_channel(m) - if not v: - raise Exception(f"Channel {m} not found") - v.enabled = False - - if self._complex_data: - for m in self.rx_enabled_channels: - v = self._rxadc.find_channel(self._rx_channel_names[m * 2]) - v.enabled = True - v = self._rxadc.find_channel(self._rx_channel_names[m * 2 + 1]) - v.enabled = True - else: - for m in self.rx_enabled_channels: - v = self._rxadc.find_channel(self._rx_channel_names[m]) - v.enabled = True - self.__rxbuf = iio.Buffer(self._rxadc, self.__rx_buffer_size, False) - def __rx_unbuffered_data(self): x = [] t = ( @@ -223,39 +212,8 @@ def __rx_unbuffered_data(self): return x - def __rx_buffered_data(self) -> Union[List[np.ndarray], np.ndarray]: - """__rx_buffered_data: Read data from RX buffer - - Returns: - List of numpy arrays containing the data from the RX buffer that are - channel interleaved - """ - if not self.__rxbuf: - self._rx_init_channels() - self.__rxbuf.refill() - - data_channel_interleaved = [] - ecn = [] - if self._complex_data: - for m in self.rx_enabled_channels: - ecn.extend( - (self._rx_channel_names[m * 2], self._rx_channel_names[m * 2 + 1]) - ) - else: - ecn = [self._rx_channel_names[m] for m in self.rx_enabled_channels] - - for name in ecn: - chan = self._rxadc.find_channel(name) - bytearray_data = chan.read(self.__rxbuf) # Do local type conversion - # create format strings - df = chan.data_format - fmt = ("i" if df.is_signed is True else "u") + str(df.length // 8) - data_channel_interleaved.append(np.frombuffer(bytearray_data, dtype=fmt)) - - return data_channel_interleaved - def __rx_complex(self): - x = self.__rx_buffered_data() + x = self._rx_buffered_data() if len(x) % 2 != 0: raise Exception( "Complex data must have an even number of component channels" @@ -265,7 +223,7 @@ def __rx_complex(self): return out[0] if len(x) == 2 else out def __rx_non_complex(self): - x = self.__rx_buffered_data() + x = self._rx_buffered_data() if self._rx_output_type == "SI": rx_scale = self.__get_rx_channel_scales() rx_offset = self.__get_rx_channel_offsets() @@ -299,8 +257,18 @@ def rx(self): ) return data + @abstractmethod + def _rx_init_channels(self): + """Initialize RX channels""" + raise NotImplementedError + + @abstractmethod + def _rx_buffered_data(self): + """Read data from RX buffer""" + raise NotImplementedError -class tx(dds, rx_tx_common): + +class tx_core(dds, rx_tx_common, metaclass=ABCMeta): """Buffer handling for transmit devices""" _tx_buffer_size = 1024 @@ -308,9 +276,10 @@ class tx(dds, rx_tx_common): _tx_channel_names: List[str] = [] _complex_data = False _tx_data_type = None - __txbuf = None + _txbuf = None _output_byte_filename = "out.bin" _push_to_file = False + _tx_cyclic_buffer = False def __init__(self, tx_cyclic_buffer=False): if self._complex_data: @@ -324,7 +293,7 @@ def __init__(self, tx_cyclic_buffer=False): dds.__init__(self) def __del__(self): - self.__txbuf = [] + self._txbuf = [] if hasattr("self", "_txdac") and self._txdac: for m in self._tx_channel_names: v = self._txdac.find_channel(m) @@ -334,16 +303,16 @@ def __del__(self): @property def tx_cyclic_buffer(self): """tx_cyclic_buffer: Enable cyclic buffer for TX""" - return self.__tx_cyclic_buffer + return self._tx_cyclic_buffer @tx_cyclic_buffer.setter def tx_cyclic_buffer(self, value): - if self.__txbuf: + if self._txbuf: raise Exception( "TX buffer already created, buffer must be " "destroyed then recreated to modify tx_cyclic_buffer" ) - self.__tx_cyclic_buffer = value + self._tx_cyclic_buffer = value @property def _num_tx_channels_enabled(self): @@ -406,22 +375,7 @@ def tx_enabled_channels(self, value): def tx_destroy_buffer(self): """tx_destroy_buffer: Clears TX buffer""" - self.__txbuf = None - - def _tx_init_channels(self): - if self._complex_data: - for m in self.tx_enabled_channels: - v = self._txdac.find_channel(self._tx_channel_names[m * 2], True) - v.enabled = True - v = self._txdac.find_channel(self._tx_channel_names[m * 2 + 1], True) - v.enabled = True - else: - for m in self.tx_enabled_channels: - v = self._txdac.find_channel(self._tx_channel_names[m], True) - v.enabled = True - self.__txbuf = iio.Buffer( - self._txdac, self._tx_buffer_size, self.__tx_cyclic_buffer - ) + self._txbuf = None def tx(self, data_np=None): """Transmit data to hardware buffers for each channel index in @@ -455,7 +409,7 @@ def tx(self, data_np=None): return raise Exception("No DDS channels found for TX, TX zeroing does not apply") - if self.__txbuf and self.tx_cyclic_buffer: + if self._txbuf and self.tx_cyclic_buffer: raise Exception( "TX buffer has been submitted in cyclic mode. " "To push more data the tx buffer must be destroyed first." @@ -491,7 +445,7 @@ def tx(self, data_np=None): data[indx::stride] = chan.astype(self._tx_data_type) indx = indx + 1 - if not self.__txbuf: + if not self._txbuf: self.disable_dds() self._tx_buffer_size = len(data) // stride self._tx_init_channels() @@ -508,8 +462,28 @@ def tx(self, data_np=None): f.write(bytearray(data)) f.close() else: - self.__txbuf.write(bytearray(data)) - self.__txbuf.push() + self._tx_buffer_push(data) + + @abstractmethod + def _tx_buffer_push(self, data): + """Push data to TX buffer. + + data: bytearray + """ + raise NotImplementedError + + @abstractmethod + def _tx_init_channels(self): + """Initialize TX channels""" + raise NotImplementedError + + +class rx(crx, rx_core): + pass + + +class tx(ctx, tx_core): + pass class rx_tx(rx, tx, phy): @@ -584,7 +558,6 @@ def __handle_init_args(self, args, kwargs): def __init__( self, *args: Union[str, iio.Context], **kwargs: Union[str, iio.Context] ) -> None: - # Handle Older API and Newer APIs uri_ctx = self.__handle_init_args(args, kwargs) @@ -643,7 +616,6 @@ def _rx_data_device_name(self) -> None: def __init__( self, *args: Union[str, iio.Context], **kwargs: Union[str, iio.Context] ) -> None: - shared_def.__init__(self, *args, **kwargs) if self._rx_data_device_name: @@ -691,7 +663,6 @@ def _tx_data_device_name(self) -> None: def __init__( self, *args: Union[str, iio.Context], **kwargs: Union[str, iio.Context] ) -> None: - shared_def.__init__(self, *args, **kwargs) if self._tx_data_device_name: diff --git a/doc/update_devs.py b/doc/update_devs.py index 58080053b..0176e4499 100644 --- a/doc/update_devs.py +++ b/doc/update_devs.py @@ -46,6 +46,7 @@ def update_devs(): "jesd_internal", "sync_start", "dsp", + "compat", ] adi_rst_path = os.path.join(root, "source", "devices", "adi.rst") with open(adi_rst_path, "r") as f: diff --git a/requirements.txt b/requirements.txt index 1e46aa48a..efa7cc2c6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,3 @@ numpy>=1.20 -pylibiio==0.23.1 +pylibiio>=0.23.1 paramiko diff --git a/requirements_dev.txt b/requirements_dev.txt index 5b14a23b7..6959ee78c 100644 --- a/requirements_dev.txt +++ b/requirements_dev.txt @@ -8,9 +8,9 @@ scapy scipy pytest-cov coveralls -pytest-libiio==0.0.13 +pytest-libiio==0.0.14 bump2version -pytest-html +pytest-html==3.2.0 plotly-express pyvisa matplotlib