Repository: NTIA/tekrsa-api-wrap (language: python)

Issue:
numpy and python versions



Discussion / hints:

Fix patch:
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..c61a718
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,5 @@
+__pycache__
+.idea
+dist
+src/*.egg-info
+Tektronix
diff --git a/GitHubRepoPublicReleaseApproval.md b/GitHubRepoPublicReleaseApproval.md
new file mode 100644
index 0000000..248436e
--- /dev/null
+++ b/GitHubRepoPublicReleaseApproval.md
@@ -0,0 +1,18 @@
+# GitHub Repository Public Release Approval
+
+### Project Name: Spectrum Monitoring
+### Software Name: Tektronix RSA API Wrapper for Python
+
+The project identified above, which is contained within the repository this document is stored in, has met the following criteria for public release:
+
+1. [x] The project, including the test criteria, meets the requirements defined in the ITS Software Development Publication Policy for making a repository public. The major pre-established criteria for publication are listed below, and the check mark next to each attests that the criterion has been met.
+    * [x] The software has been tested both as a dependency for the scos-tekrsa plugin as well as as a standalone API wrapper.
+    * [x] The repository contains unit tests and the software has been tested against the unit tests.
+2. [x] Any test data necessary for the code to function is included in this GitHub repository.
+3. [x] The README.md file is complete.
+4. [x] The project complies with the ITS Code Style Guide or an appropriate style guide as agreed to by the sponsor, project lead, or Supervising Division Chief.
+5. [x] Approved disclaimer and licensing language has been included.
+
+In order to complete this approval, please create a new branch, upload and commit your version of this Markdown document to that branch, then create a pull request for that branch. The following must login to GitHub and approve that pull request before the pull request can be merged and this repo made public:
+* Project Lead: Doug Boulware
+* Supervising Division Chief or Release Authority: Mike Cotton and Lilli Segre
diff --git a/LICENSE.md b/LICENSE.md
new file mode 100644
index 0000000..12746a1
--- /dev/null
+++ b/LICENSE.md
@@ -0,0 +1,14 @@
+SOFTWARE DISCLAIMER / RELEASE
+
+This software was developed by employees of the National Telecommunications and Information Administration (NTIA), an agency of the Federal Government and is provided to you as a public service.  Pursuant to Title 15 United States Code Section 105, works of NTIA employees are not subject to copyright protection within the United States.
+
+The software is provided by NTIA “AS IS.”  NTIA MAKES NO WARRANTY OF ANY KIND, EXPRESS, IMPLIED OR STATUTORY, INCLUDING, WITHOUT LIMITATION, THE IMPLIED WARRANTY OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, NON-INFRINGEMENT AND DATA ACCURACY. NTIA does not warrant or make any representations regarding the use of the software or the results thereof, including but not limited to the correctness, accuracy, reliability or usefulness of the software. 
+
+To the extent that NTIA holds rights in countries other than the United States, you are hereby granted the non-exclusive irrevocable and unconditional right to print, publish, prepare derivative works and distribute the NTIA software, in any medium, or authorize others to do so on your behalf, on a royalty-free basis throughout the World.
+
+You may improve, modify, and create derivative works of the software or any portion of the software, and you may copy and distribute such modifications or works. Modified works should carry a notice stating that you changed the software and should note the date and nature of any such change.
+
+You are solely responsible for determining the appropriateness of using and distributing the software and you assume all risks associated with its use, including but not limited to the risks and costs of program errors, compliance with applicable laws, damage to or loss of data, programs or equipment, and the unavailability or interruption of operation. This software is not intended to be used in any situation where a failure could cause risk of injury or damage to property. 
+
+Please provide appropriate acknowledgments of NTIA’s creation of the software in any copies or derivative works of this software.
+
diff --git a/README.md b/README.md
index ce75d58..3750ec3 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,118 @@
-# tekrsa_api_wrap
-A wrapper for the Tektronix RSA API for Linux, aiming to streamline control of Tektronix RSA devices using Python.
+# NTIA/ITS Python Wrapper for Tektronix® RSA API for Linux
+
+This Python package provides a module which wraps the [Tektronix Python/Ctypes RSA API](https://github.com/tektronix/RSA_API/tree/master/Python), with the goal of masking the Ctypes dependency and streamlining use of the API in a Python development environment. It implements most of the available RSA API functionality (see below for more information). Basic documentation is included in docstrings for quick reference during development, but this is not meant as a substitute for the comprehensive [RSA API Programming Reference manual](https://www.tek.com/spectrum-analyzer/rsa306-manual/rsa306-rsa306b-and-rsa500a-600a-0) offered by Tektronix. The manual details many peculiarities in API or device behavior which are not immediately obvious, and yet are important for developing software to control an RSA device.
+
+This wrapper was developed for applications involving programmatic control of Tektronix RSA devices from Linux. Depending on your use case, and especially if you plan to run your program from Microsoft Windows®, it may be worth looking into the [Tektronix Python/Cython RSA API](https://github.com/tektronix/RSA_API/tree/master/Python/Cython%20Version) instead of using this wrapper.
+
+## Table of Contents
+
+- [Installation](#installation)
+- [Usage](#usage)
+- [Development](#development)
+- [License](#license)
+- [Contact](#contact)
+- [Disclaimer](#disclaimer)
+
+## Installation
+
+Requires python>=3.7, numpy>=1.21, and the Tektronix RSA API for Linux.
+
+First, download and install the [RSA API for Linux](https://www.tek.com/spectrum-analyzer/rsa306-software/rsa-application-programming-interface--api-for-64bit-linux--v100014) from Tektronix. Follow the included installation instructions, then copy the `libRSA_API.so` and `libcyusb_shared.so` files into your project.
+
+These shared object files are required, and this API wrapper by default expects to find them in the [`scos-sensor`](https://github.com/NTIA/scos-sensor/) drivers directory (`/drivers/`). If you are running without `scos-sensor`, you will need to specify your drivers directory when instantiating the API wrapper. See the [Usage section](#usage) below for an example of how to do this.
+
+Next, download the most recent [release](https://github.com/NTIA/tekrsa-api-ntia/releases) of this package, and install it using `pip`:
+
+```bash
+pip install tekrsa-api-wrap-1.1.0.tar.gz
+```
+
+## Usage
+
+Interface with a supported Tektronix RSA device using Python as follows:
+
+```python
+import rsa_api
+
+# Directory which contains both libRSA_API.so and libcyusb_shared.so
+drivers_path = '/path/to/shared_objects/'
+
+# Initialize an RSA device using the API wrapper
+rsa = rsa_api.RSA(so_dir=drivers_path)
+
+# Example usage: connect, print current center frequency, then disconnect
+rsa.DEVICE_SearchAndConnect()
+print(f"Current Center Frequency (Hz): {rsa.CONFIG_GetCenterFreq()}")
+rsa.DEVICE_Disconnect()
+
+# Print docstrings for any implemented API function
+help(rsa.IQSTREAM_Tempfile) # Requires initialized RSA device
+help(rsa_api.RSA.IQSTREAM_Tempfile)  # Does not require initalized RSA device
+```
+
+### List of API functions NOT implemented
+
+- All functions not supported by the RSA API for Linux (see "Known Issues" below)
+- All `DPX`, `PLAYBACK`, `IFSTREAM` and `TRKGEN` functions
+- `DEVICE_GetErrorString()`
+  - Alternate error handling is implemented.
+- `DEVICE_GetNomenclatureW()` and `IQSTREAM_SetDiskFilenameBaseW()`
+  - `DEVICE_GetNomenclature()` and `IQSTREAM_SetDiskFilenameBase()` are used instead.
+- `IQBLK_GetIQDataCplx()`
+  - `IQBLK_GetIQData()` and `IQBLK_GetIQDataDeinterleaved()` are used instead.
+- `IQSTREAM_GetIQData()`
+  - `IQSTREAM_Tempfile()` is used instead.
+
+### List of API "Helper" functions
+
+A handful of useful functions are included in this wrapper which streamline some common tasks. These "helper functions" include:
+
+- `IQBLK_Acquire()`
+- `IQBLK_Configure()`
+- `SPECTRUM_Acquire()`
+- `IQSTREAM_StatusParser()`
+- `IQSTREAM_Tempfile()`
+- `IQSTREAM_Tempfile_NoConfig()`
+- `DEVICE_SearchAndConnect()`
+
+To read more about these functions, check their docstrings with `help()`.
+
+### Known Issues
+
+Known issues exist in the underlying Tektronix RSA API for Linux, and therefore this wrapper is limited in certain ways. The list of known issues is provided by Tektronix in the [Tektronix RSA API for Linux release notes](https://download.tek.com/software/supporting_files/ReleaseNotes_1_0_0014_64bit_066207701.txt) (up-to-date as of version 1.0.0014):
+
+### Running Tests
+
+A testing file is included in the `tests` directory of this repository. The test uses `unittest` to test supported API functions. Running a test requires an RSA device to be connected. The same test is used for any supported RSA device, with some tests being enabled, disabled, or modified as needed depending on the device's specific supported API functions. For example, tests of the preamp configuration are not run when testing with an RSA which does not have a preamp.
+
+From the top-level directory of this repository, run the test, with segmentation fault handling, by running:
+
+`python3 -q -X faulthandler tests/rsa_api_test.py <path-to-shared-objects>`
+
+Replacing `<path-to-shared-objects>` with the path to a directory containing both `libRSA_API.so` and `libcyusb_shared.so`.
+
+This testing code was been adapted from the [Tektronix Cython RSA API testing code for the 306B](https://github.com/tektronix/RSA_API/blob/master/Python/Cython%20Version/test_rsa306b.py) and [for the 500A/600A series devices](https://github.com/tektronix/RSA_API/blob/master/Python/Cython%20Version/test_rsa500-600.py). In addition to adapting this code to work with this API wrapper, various tests were also added which were not present in the original versions, and the test was made to be universal for all supported RSA devices.
+
+## Development
+
+Build the Python package from the top level directory in this repository by running:
+
+`python3 -m pip install --upgrade build`
+
+`python3 -m build`
+
+## License
+
+See [LICENSE](LICENSE.md)
+
+TEKTRONIX and TEK are registered trademarks of Tektronix, Inc.
+
+Microsoft and Windows are trademarks of the Microsoft group of companies.
+
+## Contact
+
+For technical questions, contact Anthony Romaniello, aromaniello@ntia.gov
+
+### Disclaimer
+
+Certain commercial equipment, instruments, or materials are identified in this project were used for the convenience of the developers. In no case does such identification imply recommendation or endorsement by the National Telecommunications and Information Administration, nor does it imply that the material or equipment identified is necessarily the best available for the purpose.
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 0000000..83fede9
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,7 @@
+[build-system]
+requires = [
+    "setuptools>=42",
+    "wheel",
+    "numpy>=1.21"
+]
+build-backend = "setuptools.build_meta"
\ No newline at end of file
diff --git a/setup.cfg b/setup.cfg
new file mode 100644
index 0000000..e2e32fb
--- /dev/null
+++ b/setup.cfg
@@ -0,0 +1,28 @@
+[metadata]
+name = tekrsa-api-wrap
+version = 1.1.0
+author = The Institute for Telecommunication Sciences
+description = NTIA Python wrapper for Tektronix RSA API for Linux
+long_description = file: README.md
+long_description_content_type = text/markdown
+url = https://github.com/ntia/tekrsa-api-wrap
+project_urls =
+	Bug Tracker = https://github.com/ntia/tekrsa-api-wrap/issues
+classifiers =
+    Intended Audience :: Developers
+    Intended Audience :: Science/Research
+    Intended Audience :: Telecommunications Industry
+    Natural Language :: English
+    Operating System :: Unix
+    Programming Language :: Python :: 3
+
+[options]
+package_dir =
+    = src
+packages = find:
+python_requires = >=3.7
+install_requires =
+    numpy>=1.21
+
+[options.packages.find]
+where = src
diff --git a/src/rsa_api/__init__.py b/src/rsa_api/__init__.py
new file mode 100644
index 0000000..406612f
--- /dev/null
+++ b/src/rsa_api/__init__.py
@@ -0,0 +1,1 @@
+from .rsa_api_wrap import *
\ No newline at end of file
diff --git a/src/rsa_api/rsa_api_wrap.py b/src/rsa_api/rsa_api_wrap.py
new file mode 100644
index 0000000..0e0f307
--- /dev/null
+++ b/src/rsa_api/rsa_api_wrap.py
@@ -0,0 +1,2257 @@
+# Written for Tektronix RSA API for Linux v1.0.0014
+# Refer to the RSA API Programming Reference Manual for details
+# on any functions implemented from this module.
+import tempfile
+from ctypes import *
+from enum import Enum
+from os.path import abspath, join
+from time import sleep
+from typing import Union, Any, Tuple
+
+import numpy as np
+
+""" GLOBAL CONSTANTS """
+
+_MAX_NUM_DEVICES = 10  # Max num. of devices that could be found
+_MAX_SERIAL_STRLEN = 8  # Bytes allocated for serial number string
+_MAX_DEVTYPE_STRLEN = 8  # Bytes allocated for device type string
+_FPGA_VERSION_STRLEN = 6  # Bytes allocated for FPGA version number string
+_FW_VERSION_STRLEN = 6  # Bytes allocated for FW version number string
+_HW_VERSION_STRLEN = 4  # Bytes allocated for HW version number string
+_NOMENCLATURE_STRLEN = 8  # Bytes allocated for device nomenclature string
+_API_VERSION_STRLEN = 8  # Bytes allocated for API version number string
+
+""" ENUMERATION TUPLES """
+
+_DEV_EVENT = ('OVERRANGE', 'TRIGGER', '1PPS')
+_FREQ_REF_SOURCE = ('INTERNAL', 'EXTREF', 'GNSS', 'USER')
+_IQS_OUT_DEST = ('CLIENT', 'FILE_TIQ', 'FILE_SIQ', 'FILE_SIQ_SPLIT')
+_IQS_OUT_DTYPE = ('SINGLE', 'INT32', 'INT16', 'SINGLE_SCALE_INT32')
+_SPECTRUM_DETECTORS = ('PosPeak', 'NegPeak', 'AverageVRMS', 'Sample')
+_SPECTRUM_TRACES = ('Trace1', 'Trace2', 'Trace3')
+_SPECTRUM_VERTICAL_UNITS = ('dBm', 'Watt', 'Volt', 'Amp', 'dBmV')
+_SPECTRUM_WINDOWS = ('Kaiser', 'Mil6dB', 'BlackmanHarris', 'Rectangular', 'FlatTop', 'Hann')
+_TRIGGER_MODE = ('freeRun', 'triggered')
+_TRIGGER_SOURCE = ('External', 'IFPowerLevel')
+_TRIGGER_TRANSITION = ('LH', 'HL', 'Either')
+
+""" CUSTOM DATA STRUCTURES """
+
+
+class _SpectrumLimits(Structure):
+    _fields_ = [('maxSpan', c_double),
+                ('minSpan', c_double),
+                ('maxRBW', c_double),
+                ('minRBW', c_double),
+                ('maxVBW', c_double),
+                ('minVBW', c_double),
+                ('maxTraceLength', c_int),  # Incorrectly documented as a double
+                ('minTraceLength', c_int)]  # Incorrectly documented as a double
+
+
+class _SpectrumSettings(Structure):
+    _fields_ = [('span', c_double),
+                ('rbw', c_double),
+                ('enableVBW', c_bool),
+                ('vbw', c_double),
+                ('traceLength', c_int),
+                ('window', c_int),
+                ('verticalUnit', c_int),
+                ('actualStartFreq', c_double),
+                ('actualStopFreq', c_double),
+                ('actualFreqStepSize', c_double),
+                ('actualRBW', c_double),
+                ('actualVBW', c_double),
+                ('actualNumIQSamples', c_int)]
+
+
+class _SpectrumTraceInfo(Structure):
+    _fields_ = [('timestamp', c_int64),
+                ('acqDataStatus', c_uint16)]
+
+
+class _IQBlkAcqInfo(Structure):
+    _fields_ = [('sample0Timestamp', c_uint64),
+                ('triggerSampleIndex', c_uint64),
+                ('triggerTimestamp', c_uint64),
+                ('acqStatus', c_uint32)]
+
+
+class _IQStreamFileInfo(Structure):
+    _fields_ = [('numberSamples', c_uint64),
+                ('sample0Timestamp', c_uint64),
+                ('triggerSampleIndex', c_uint64),
+                ('triggerTimestamp', c_uint64),
+                ('acqStatus', c_uint32),
+                ('filenames', c_wchar_p)]
+
+
+""" ERROR HANDLING """
+
+
+class RSAError(Exception):
+    def __init__(self, err_txt=""):
+        self.err_txt = err_txt
+        err = "RSA Error: {}".format(self.err_txt)
+        super(RSAError, self).__init__(err)
+
+
+class RSA:
+
+    def __init__(self, so_dir: str = '/drivers/'):
+        """ Load the RSA USB Driver """
+        # Param. 'so_dir' is the directory containing libRSA_API.so and
+        # libcyusb_shared.so.
+        rtld_lazy = 0x0001
+        lazy_load = rtld_lazy | RTLD_GLOBAL
+        self.rsa = CDLL(join(abspath(so_dir), 'libRSA_API.so'), lazy_load)
+        self.usb_api = CDLL(join(abspath(so_dir), 'libcyusb_shared.so'), lazy_load)
+
+    """ ERROR HANDLING """
+
+    class ReturnStatus(Enum):
+        noError = 0
+
+        # Connection
+        errorNotConnected = 101
+        errorIncompatibleFirmware = 102
+        errorBootLoaderNotRunning = 103
+        errorTooManyBootLoadersConnected = 104
+        errorRebootFailure = 105
+
+        # POST
+        errorPOSTFailureFPGALoad = 201
+        errorPOSTFailureHiPower = 202
+        errorPOSTFailureI2C = 203
+        errorPOSTFailureGPIF = 204
+        errorPOSTFailureUsbSpeed = 205
+        errorPOSTDiagFailure = 206
+
+        # General Msmt
+        errorBufferAllocFailed = 301
+        errorParameter = 302
+        errorDataNotReady = 304
+
+        # Spectrum
+        errorParameterTraceLength = 1101
+        errorMeasurementNotEnabled = 1102
+        errorSpanIsLessThanRBW = 1103
+        errorFrequencyOutOfRange = 1104
+
+        # IF streaming
+        errorStreamADCToDiskFileOpen = 1201
+        errorStreamADCToDiskAlreadyStreaming = 1202
+        errorStreamADCToDiskBadPath = 1203
+        errorStreamADCToDiskThreadFailure = 1204
+        errorStreamedFileInvalidHeader = 1205
+        errorStreamedFileOpenFailure = 1206
+        errorStreamingOperationNotSupported = 1207
+        errorStreamingFastForwardTimeInvalid = 1208
+        errorStreamingInvalidParameters = 1209
+        errorStreamingEOF = 1210
+
+        # IQ streaming
+        errorIQStreamInvalidFileDataType = 1301
+        errorIQStreamFileOpenFailed = 1302
+        errorIQStreamBandwidthOutOfRange = 1303
+
+        # -----------------
+        # Internal errors
+        # -----------------
+        errorTimeout = 3001
+        errorTransfer = 3002
+        errorFileOpen = 3003
+        errorFailed = 3004
+        errorCRC = 3005
+        errorChangeToFlashMode = 3006
+        errorChangeToRunMode = 3007
+        errorDSPLError = 3008
+        errorLOLockFailure = 3009
+        errorExternalReferenceNotEnabled = 3010
+        errorLogFailure = 3011
+        errorRegisterIO = 3012
+        errorFileRead = 3013
+
+        errorDisconnectedDeviceRemoved = 3101
+        errorDisconnectedDeviceNodeChangedAndRemoved = 3102
+        errorDisconnectedTimeoutWaitingForADcData = 3103
+        errorDisconnectedIOBeginTransfer = 3104
+        errorOperationNotSupportedInSimMode = 3015
+
+        errorFPGAConfigureFailure = 3201
+        errorCalCWNormFailure = 3202
+        errorSystemAppDataDirectory = 3203
+        errorFileCreateMRU = 3204
+        errorDeleteUnsuitableCachePath = 3205
+        errorUnableToSetFilePermissions = 3206
+        errorCreateCachePath = 3207
+        errorCreateCachePathBoost = 3208
+        errorCreateCachePathStd = 3209
+        errorCreateCachePathGen = 3210
+        errorBufferLengthTooSmall = 3211
+        errorRemoveCachePath = 3212
+        errorGetCachingDirectoryBoost = 3213
+        errorGetCachingDirectoryStd = 3214
+        errorGetCachingDirectoryGen = 3215
+        errorInconsistentFileSystem = 3216
+
+        errorWriteCalConfigHeader = 3301
+        errorWriteCalConfigData = 3302
+        errorReadCalConfigHeader = 3303
+        errorReadCalConfigData = 3304
+        errorEraseCalConfig = 3305
+        errorCalConfigFileSize = 3306
+        errorInvalidCalibConstantFileFormat = 3307
+        errorMismatchCalibConstantsSize = 3308
+        errorCalConfigInvalid = 3309
+
+        # flash
+        errorFlashFileSystemUnexpectedSize = 3401,
+        errorFlashFileSystemNotMounted = 3402
+        errorFlashFileSystemOutOfRange = 3403
+        errorFlashFileSystemIndexNotFound = 3404
+        errorFlashFileSystemReadErrorCRC = 3405
+        errorFlashFileSystemReadFileMissing = 3406
+        errorFlashFileSystemCreateCacheIndex = 3407
+        errorFlashFileSystemCreateCachedDataFile = 3408
+        errorFlashFileSystemUnsupportedFileSize = 3409
+        errorFlashFileSystemInsufficentSpace = 3410
+        errorFlashFileSystemInconsistentState = 3411
+        errorFlashFileSystemTooManyFiles = 3412
+        errorFlashFileSystemImportFileNotFound = 3413
+        errorFlashFileSystemImportFileReadError = 3414
+        errorFlashFileSystemImportFileError = 3415
+        errorFlashFileSystemFileNotFoundError = 3416
+        errorFlashFileSystemReadBufferTooSmall = 3417
+        errorFlashWriteFailure = 3418
+        errorFlashReadFailure = 3419
+        errorFlashFileSystemBadArgument = 3420
+        errorFlashFileSystemCreateFile = 3421
+
+        # Aux monitoring
+        errorMonitoringNotSupported = 3501,
+        errorAuxDataNotAvailable = 3502
+
+        # battery
+        errorBatteryCommFailure = 3601
+        errorBatteryChargerCommFailure = 3602
+        errorBatteryNotPresent = 3603
+
+        # EST
+        errorESTOutputPathFile = 3701
+        errorESTPathNotDirectory = 3702
+        errorESTPathDoesntExist = 3703
+        errorESTUnableToOpenLog = 3704
+        errorESTUnableToOpenLimits = 3705
+
+        # Revision information
+        errorRevisionDataNotFound = 3801
+
+        # alignment
+        error112MHzAlignmentSignalLevelTooLow = 3901
+        error10MHzAlignmentSignalLevelTooLow = 3902
+        errorInvalidCalConstant = 3903
+        errorNormalizationCacheInvalid = 3904
+        errorInvalidAlignmentCache = 3905
+
+        # acq status
+        errorADCOverrange = 9000  # must not change the location of these error codes without coordinating with MFG TEST
+        errorOscUnlock = 9001
+
+        errorNotSupported = 9901
+
+        errorPlaceholder = 9999
+        notImplemented = -1
+
+    def err_check(self, rs):
+        """Obtain internal API ErrorStatus and pass to RSAError."""
+        if self.ReturnStatus(rs) != self.ReturnStatus.noError:
+            raise RSAError(self.ReturnStatus(rs).name)
+
+    """ INPUT VALIDATION """
+
+    @staticmethod
+    def check_range(in_var: Union[float, int], min_val: Union[float, int],
+                    max_val: Union[float, int], incl: bool = True) -> Union[float, int]:
+        """Check if input is in valid range, inclusive or exclusive"""
+        if incl:
+            if min_val <= in_var <= max_val:
+                return in_var
+            else:
+                raise ValueError(f"Input must be in range {min_val} to {max_val}, inclusive.")
+        else:
+            if min_val < in_var < max_val:
+                return in_var
+            else:
+                raise ValueError(f"Input must be in range {min_val} to {max_val}, exclusive.")
+
+    @staticmethod
+    def check_int(value: Any) -> int:
+        """Check if input is an integer."""
+        if type(value) is int:
+            return value
+        elif type(value) is float and value.is_integer():
+            # Accept floats if they are whole numbers
+            return int(value)
+        else:
+            raise TypeError("Input must be an integer.")
+
+    @staticmethod
+    def check_string(value: Any) -> str:
+        """Check if input is a string."""
+        if type(value) is str:
+            return value
+        else:
+            raise TypeError("Input must be a string.")
+
+    @staticmethod
+    def check_num(value: Any) -> Union[float, int]:
+        """Check if input is a number (float or int)."""
+        if type(value) is int or type(value) is float:
+            return value
+        else:
+            raise TypeError("Input must be a number (float or int).")
+
+    @staticmethod
+    def check_bool(value: Any) -> bool:
+        """Check if input is a boolean."""
+        if type(value) is bool:
+            return value
+        else:
+            raise TypeError("Input must be a boolean.")
+
+    """ ALIGNMENT METHODS """
+
+    def ALIGN_GetAlignmentNeeded(self) -> bool:
+        """
+        Determine if an alignment is needed or not.
+
+        Returns
+        -------
+        bool
+            True indicates an alignment is needed, False for not needed.
+        """
+        needed = c_bool()
+        self.err_check(self.rsa.ALIGN_GetAlignmentNeeded(byref(needed)))
+        return needed.value
+
+    def ALIGN_GetWarmupStatus(self) -> bool:
+        """
+        Report device warm-up status.
+
+        Returns
+        -------
+        bool
+            True indicates device warm-up interval reached.
+            False indicates warm-up has not been reached
+        """
+        warmed_up = c_bool()
+        self.err_check(self.rsa.ALIGN_GetWarmupStatus(byref(warmed_up)))
+        return warmed_up.value
+
+    def ALIGN_RunAlignment(self) -> None:
+        """Run the device alignment process."""
+        self.err_check(self.rsa.ALIGN_RunAlignment())
+
+    """ AUDIO METHODS NOT IMPLEMENTED """
+    """ BROKEN IN RSA API FOR LINUX v1.0.0014 """
+
+    """ CONFIG METHODS """
+
+    def CONFIG_GetCenterFreq(self) -> float:
+        """Return the current center frequency in Hz."""
+        cf = c_double()
+        self.err_check(self.rsa.CONFIG_GetCenterFreq(byref(cf)))
+        return cf.value
+
+    def CONFIG_GetExternalRefEnable(self) -> bool:
+        """
+        Return the state of the external reference.
+
+        Returns
+        -------
+        bool
+            True means external reference is enabled, False means disabled.
+        """
+        ext_ref_en = c_bool()
+        self.err_check(self.rsa.CONFIG_GetExternalRefEnable(byref(ext_ref_en)))
+        return ext_ref_en.value
+
+    def CONFIG_GetExternalRefFrequency(self) -> float:
+        """
+        Return the frequency, in Hz, of the external reference.
+
+        Returns
+        -------
+        float
+            The external reference frequency, measured in Hz.
+
+        Raises
+        ------
+        RSAError
+            If there is no external reference input in use.
+        """
+        global _FREQ_REF_SOURCE
+        src = self.CONFIG_GetFrequencyReferenceSource()
+        if src == _FREQ_REF_SOURCE[0]:
+            raise RSAError("External frequency reference not in use.")
+        else:
+            ext_freq = c_double()
+            self.err_check(self.rsa.CONFIG_GetExternalRefFrequency(byref(ext_freq)))
+            return ext_freq.value
+
+    def CONFIG_GetFrequencyReferenceSource(self) -> str:
+        """
+        Return a string representing the frequency reference source.
+
+        Returns
+        -------
+        string
+            Name of the frequency reference source. Valid results:
+                INTERNAL : Internal frequency reference.
+                EXTREF : External (Ref In) frequency reference.
+                GNSS : Internal GNSS receiver reference
+                USER : Previously set USER setting, or, if none, INTERNAL.
+        """
+        global _FREQ_REF_SOURCE
+        src = c_int()
+        self.err_check(self.rsa.CONFIG_GetFrequencyReferenceSource(byref(src)))
+        return _FREQ_REF_SOURCE[src.value]
+
+    def CONFIG_GetMaxCenterFreq(self) -> float:
+        """Return the maximum center frequency in Hz."""
+        max_cf = c_double()
+        self.err_check(self.rsa.CONFIG_GetMaxCenterFreq(byref(max_cf)))
+        return max_cf.value
+
+    def CONFIG_GetMinCenterFreq(self) -> float:
+        """Return the minimum center frequency in Hz."""
+        min_cf = c_double()
+        self.err_check(self.rsa.CONFIG_GetMinCenterFreq(byref(min_cf)))
+        return min_cf.value
+
+    def CONFIG_GetReferenceLevel(self) -> float:
+        """Return the current reference level, measured in dBm."""
+        ref_level = c_double()
+        self.err_check(self.rsa.CONFIG_GetReferenceLevel(byref(ref_level)))
+        return ref_level.value
+
+    def CONFIG_Preset(self) -> None:
+        """
+        Set the connected device to preset values.
+        """
+        self.err_check(self.rsa.CONFIG_Preset())
+        # For some reason, the record length is not successfully set.
+        # Manual override:
+        self.err_check(self.rsa.IQBLK_SetIQRecordLength(1024))
+
+    def CONFIG_SetCenterFreq(self, cf: Union[float, int]) -> None:
+        """
+        Set the center frequency value, in Hz.
+
+        Parameters
+        ----------
+        cf : float or int
+            Value to set center frequency, in Hz.
+        """
+        cf = RSA.check_num(cf)
+        cf = RSA.check_range(cf, self.CONFIG_GetMinCenterFreq(), self.CONFIG_GetMaxCenterFreq())
+        self.err_check(self.rsa.CONFIG_SetCenterFreq(c_double(cf)))
+
+    def CONFIG_SetExternalRefEnable(self, ext_ref_en: bool) -> None:
+        """
+        Enable or disable the external reference.
+
+        Parameters
+        ----------
+        ext_ref_en : bool
+            True enables the external reference. False disables it.
+        """
+        ext_ref_en = RSA.check_bool(ext_ref_en)
+        self.err_check(self.rsa.CONFIG_SetExternalRefEnable(c_bool(ext_ref_en)))
+
+    def CONFIG_SetFrequencyReferenceSource(self, src: str) -> None:
+        """
+        Select the device frequency reference source.
+
+        Parameters
+        ----------
+        src : string
+            Frequency reference source selection. Valid settings:
+                INTERNAL : Internal frequency reference.
+                EXTREF : External (Ref In) frequency reference.
+                GNSS : Internal GNSS receiver reference
+                USER : Previously set USER setting, or, if none, INTERNAL.
+
+        Raises
+        ------
+        RSAError
+            If the input string does not match one of the valid settings.
+        """
+        global _FREQ_REF_SOURCE
+        src = RSA.check_string(src)
+        if src in _FREQ_REF_SOURCE:
+            if src == 'GNSS' and self.DEVICE_GetNomenclature() in ['RSA306', 'RSA306B']:
+                raise RSAError("RSA 300 series device does not support GNSS reference.")
+            else:
+                value = c_int(_FREQ_REF_SOURCE.index(src))
+                self.err_check(self.rsa.CONFIG_SetFrequencyReferenceSource(value))
+        else:
+            raise RSAError("Input does not match a valid setting.")
+
+    def CONFIG_SetReferenceLevel(self, ref_level: Union[float, int]) -> None:
+        """
+        Set the reference level
+
+        Parameters
+        ----------
+        ref_level : float or int
+            Reference level, in dBm. Valid range: -130 dBm to 30 dBm.
+        """
+        ref_level = RSA.check_num(ref_level)
+        ref_level = RSA.check_range(ref_level, -130, 30)
+        self.err_check(self.rsa.CONFIG_SetReferenceLevel(c_double(ref_level)))
+
+    def CONFIG_GetAutoAttenuationEnable(self) -> bool:
+        """
+        Return the signal path auto-attenuation enable state.
+
+        Returns
+        -------
+        bool
+            True indicates that auto-attenuation operation is enabled.
+            False indicated it is disabled.
+        """
+        enable = c_bool()
+        self.err_check(self.rsa.CONFIG_GetAutoAttenuationEnable(byref(enable)))
+        return enable.value
+
+    def CONFIG_SetAutoAttenuationEnable(self, enable: bool) -> None:
+        """
+        Set the signal path auto-attenuation enable state.
+
+        The device Run state is cycled in order to apply the setting.
+
+        Parameters
+        ----------
+        enable : bool
+            True enables auto-attenuation operation. False disables it.
+        """
+        enable = RSA.check_bool(enable)
+        self.rsa.DEVICE_Stop()
+        self.err_check(self.rsa.CONFIG_SetAutoAttenuationEnable(c_bool(enable)))
+        self.rsa.DEVICE_Run()
+
+    def CONFIG_GetRFPreampEnable(self) -> bool:
+        """
+        Return the state of the RF Preamplifier.
+
+        Returns
+        -------
+        bool
+            True indicates the RF Preamplifier is enabled. False indicates
+            it is disabled.
+        """
+        enable = c_bool()
+        self.err_check(self.rsa.CONFIG_GetRFPreampEnable(byref(enable)))
+        return enable.value
+
+    def CONFIG_SetRFPreampEnable(self, enable: bool) -> None:
+        """
+        Set the RF Preamplifier enable state.
+        
+        The device Run state is cycled in order to apply the setting.
+        
+        Parameters
+        ----------
+        enable : bool
+            True enables the RF Preamplifier. False disables it.
+        """
+        enable = RSA.check_bool(enable)
+        self.rsa.DEVICE_Stop()
+        self.err_check(self.rsa.CONFIG_SetRFPreampEnable(c_bool(enable)))
+        self.rsa.DEVICE_Run()
+
+    def CONFIG_GetRFAttenuator(self) -> float:
+        """
+        Return the setting of the RF Input Attenuator.
+
+        Returns
+        -------
+        float
+            The RF Input Attenuator setting value, in dB.
+        """
+        value = c_double()
+        self.err_check(self.rsa.CONFIG_GetRFAttenuator(byref(value)))
+        return value.value
+
+    def CONFIG_SetRFAttenuator(self, value: Union[float, int]) -> None:
+        """
+        Set the RF Input Attenuator value manually.
+
+        The device Run state is cycled in order to apply the setting.
+
+        Parameters
+        ----------
+        value : float
+            The desired RF Input Attenuator setting, in dB. Values are
+            rounded to the nearest integer, in the range -51 dB to 0 dB.
+        """
+        value = RSA.check_num(value)
+        value = RSA.check_range(value, -51, 0)
+        self.rsa.DEVICE_Stop()
+        self.err_check(self.rsa.CONFIG_SetRFAttenuator(c_double(value)))
+        self.rsa.DEVICE_Run()
+
+    """ DEVICE METHODS """
+
+    def DEVICE_Connect(self, device_id: int = 0) -> None:
+        """
+        Connect to a device specified by the device_id parameter.
+
+        Parameters
+        ----------
+        device_id : int
+            The device ID of the target device. Defaults to zero.
+        """
+        device_id = RSA.check_int(device_id)
+        device_id = RSA.check_range(device_id, 0, float('inf'))
+        self.err_check(self.rsa.DEVICE_Connect(c_int(device_id)))
+
+    def DEVICE_Disconnect(self) -> None:
+        """Stop data acquisition and disconnect from connected device."""
+        self.err_check(self.rsa.DEVICE_Disconnect())
+
+    def DEVICE_GetEnable(self) -> bool:
+        """
+        Query the run state.
+
+        Returns
+        -------
+        bool
+           True indicates the device is in the run state. False indicates
+           that it is in the stop state.
+        """
+        enable = c_bool()
+        self.err_check(self.rsa.DEVICE_GetEnable(byref(enable)))
+        return enable.value
+
+    def DEVICE_GetFPGAVersion(self) -> str:
+        """
+        Retrieve the FPGA version number.
+
+        Returns
+        -------
+        string
+            The FPGA version number.
+        """
+        global _FPGA_VERSION_STRLEN
+        fpga_version = (c_char * _FPGA_VERSION_STRLEN)()
+        self.err_check(self.rsa.DEVICE_GetFPGAVersion(byref(fpga_version)))
+        return fpga_version.value.decode('utf-8')
+
+    def DEVICE_GetFWVersion(self) -> str:
+        """
+        Retrieve the firmware version number.
+
+        Returns
+        -------
+        string
+            The firmware version number.
+        """
+        global _FW_VERSION_STRLEN
+        fw_version = (c_char * _FW_VERSION_STRLEN)()
+        self.err_check(self.rsa.DEVICE_GetFWVersion(byref(fw_version)))
+        return fw_version.value.decode('utf-8')
+
+    def DEVICE_GetHWVersion(self) -> str:
+        """
+        Retrieve the hardware version number.
+
+        Returns
+        -------
+        string
+            The hardware version number.
+        """
+        global _HW_VERSION_STRLEN
+        hw_version = (c_char * _HW_VERSION_STRLEN)()
+        self.err_check(self.rsa.DEVICE_GetHWVersion(byref(hw_version)))
+        return hw_version.value.decode('utf-8')
+
+    def DEVICE_GetNomenclature(self) -> str:
+        """
+        Retrieve the name of the device.
+
+        Returns
+        -------
+        string
+            Name of the device.
+        """
+        global _NOMENCLATURE_STRLEN
+        nomenclature = (c_char * _NOMENCLATURE_STRLEN)()
+        self.err_check(self.rsa.DEVICE_GetNomenclature(byref(nomenclature)))
+        return nomenclature.value.decode('utf-8')
+
+    def DEVICE_GetSerialNumber(self) -> str:
+        """
+        Retrieve the serial number of the device.
+
+        Returns
+        -------
+        string
+            Serial number of the device.
+        """
+        global _MAX_SERIAL_STRLEN
+        serial_num = (c_char * _MAX_SERIAL_STRLEN)()
+        self.err_check(self.rsa.DEVICE_GetSerialNumber(byref(serial_num)))
+        return serial_num.value.decode('utf-8')
+
+    def DEVICE_GetAPIVersion(self) -> str:
+        """
+        Retrieve the API version number.
+
+        Returns
+        -------
+        string
+            The API version number.
+        """
+        global _API_VERSION_STRLEN
+        api_version = (c_char * _API_VERSION_STRLEN)()
+        self.err_check(self.rsa.DEVICE_GetAPIVersion(byref(api_version)))
+        return api_version.value.decode('utf-8')
+
+    def DEVICE_PrepareForRun(self) -> None:
+        """
+        Put the system in a known state, ready to stream data.
+        """
+        self.err_check(self.rsa.DEVICE_PrepareForRun())
+
+    def DEVICE_GetInfo(self) -> dict:
+        """
+        Retrieve multiple device and information strings.
+
+        Returns
+        -------
+        dict
+            All of the above listed information as strings.
+            Keys: nomenclature, serialNum, fwVersion, fpgaVersion,
+                  hwVersion, apiVersion
+        """
+        nomenclature = self.DEVICE_GetNomenclature()
+        serial_num = self.DEVICE_GetSerialNumber()
+        fw_version = self.DEVICE_GetFWVersion()
+        fpga_version = self.DEVICE_GetFPGAVersion()
+        hw_version = self.DEVICE_GetHWVersion()
+        api_version = self.DEVICE_GetAPIVersion()
+        info = {
+            "nomenclature": nomenclature,
+            "serialNum": serial_num,
+            "fwVersion": fw_version,
+            "fpgaVersion": fpga_version,
+            "hwVersion": hw_version,
+            "apiVersion": api_version
+        }
+        return info
+
+    def DEVICE_GetOverTemperatureStatus(self) -> bool:
+        """
+        Query device for over-temperature status.
+
+        Returns
+        -------
+        bool
+            Over-temperature status. True indicates device above nominal
+            safe operating range, and may result in reduced accuracy and/
+            or damage to the device. False indicates device temperature is
+            within the safe operating range.
+        """
+        over_temp = c_bool()
+        self.err_check(self.rsa.DEVICE_GetOverTemperatureStatus(byref(over_temp)))
+        return over_temp.value
+
+    def DEVICE_Reset(self, device_id: int = -1) -> None:
+        """
+        Reboot the specified device.
+
+        Parameters
+        ----------
+        device_id : int
+            The device ID of the target device.
+
+        Raises
+        ------
+        RSAError
+            If multiple devices are found but no device ID is specified.
+        """
+        device_id = RSA.check_int(device_id)
+        self.DEVICE_Disconnect()
+        found_devices = self.DEVICE_Search()
+        num_found = len(found_devices)
+        if num_found == 1:
+            device_id = 0
+        elif num_found > 1 and device_id == -1:
+            raise RSAError("Multiple devices found, but no ID specified.")
+        self.err_check(self.rsa.DEVICE_Reset(c_int(device_id)))
+
+    def DEVICE_Run(self) -> None:
+        """Start data acquisition."""
+        self.err_check(self.rsa.DEVICE_Run())
+
+    def DEVICE_Search(self) -> dict:
+        """
+        Search for connectable devices.
+
+        Returns
+        -------
+        dict
+            Found devices: {device_id : (deviceSerial, deviceType)}.
+                device_id : int
+                deviceSerial : string
+                deviceType : string
+
+        Raises
+        ------
+        RSAError
+            If no devices are found.
+        """
+        global _MAX_NUM_DEVICES, _MAX_SERIAL_STRLEN, _MAX_DEVTYPE_STRLEN
+        num_found = c_int()
+        dev_ids = (c_int * _MAX_NUM_DEVICES)()
+        dev_serial = ((c_char * _MAX_NUM_DEVICES) * _MAX_SERIAL_STRLEN)()
+        dev_type = ((c_char * _MAX_NUM_DEVICES) * _MAX_DEVTYPE_STRLEN)()
+
+        self.err_check(
+            self.rsa.DEVICE_Search(byref(num_found), byref(dev_ids), dev_serial, dev_type))
+
+        found_devices = {ID: (dev_serial[ID].value.decode(), dev_type[ID].value.decode())
+                         for ID in dev_ids}
+
+        # If there are no devices, there is still a dict returned
+        # with a device ID, but the other elements are empty.
+        if found_devices[0] == ('', ''):
+            raise RSAError("Could not find a matching Tektronix RSA device.")
+        else:
+            return found_devices
+
+    def DEVICE_StartFrameTransfer(self) -> None:
+        """
+        Start data transfer.
+        """
+        self.err_check(self.rsa.DEVICE_StartFrameTransfer())
+
+    def DEVICE_Stop(self) -> None:
+        """
+        Stop data acquisition.
+        """
+        self.err_check(self.rsa.DEVICE_Stop())
+
+    def DEVICE_GetEventStatus(self, event_id: str) -> Tuple[bool, int]:
+        """
+        Return global device real-time event status.
+
+        Parameters
+        ----------
+        event_id : string
+            Identifier for the event status to query. Valid settings:
+                OVERRANGE : Overrange event detection.
+                TRIGGER : Trigger event detection.
+                1PPS : 1PPS event detection (RSA500A/600A only).
+
+        Returns
+        -------
+        occurred : bool
+            Indicates whether the event has occurred.
+        timestamp : int
+            Event occurrence timestamp. Only valid if occurred is True.
+
+        Raises
+        ------
+        RSAError
+            If the input string does not match one of the valid settings.
+        """
+        global _DEV_EVENT
+        occurred = c_bool()
+        timestamp = c_uint64()
+        event_id = RSA.check_string(event_id)
+        if event_id in _DEV_EVENT:
+            value = c_int(_DEV_EVENT.index(event_id))
+        else:
+            raise RSAError("Input string does not match one of the valid settings.")
+        self.err_check(self.rsa.DEVICE_GetEventStatus(value, byref(occurred),
+                                                      byref(timestamp)))
+        return occurred.value, timestamp.value
+
+    """ GNSS METHODS NOT IMPLEMENTED """
+    """ BROKEN IN RSA API FOR LINUX v1.0.0014 """
+
+    """ IQ BLOCK METHODS """
+
+    def IQBLK_GetIQAcqInfo(self) -> Tuple[int, int, int, int]:
+        """
+        Return IQ acquisition status info for the most recent IQ block.
+
+        Returns
+        -------
+        sample0Timestamp : int
+            Timestamp of the first sample of the IQ block record.
+        triggerSampleIndex : int
+            Index to the sample corresponding to the trigger point.
+        triggerTimestamp : int
+            Timestamp of the trigger sample.
+        acqStatus : int
+            "Word" with acquisition status bits. See above for details.
+        """
+        acq_info = _IQBlkAcqInfo()
+        self.err_check(self.rsa.IQBLK_GetIQAcqInfo(byref(acq_info)))
+        info = (acq_info.sample0Timestamp.value, acq_info.triggerSampleIndex.value,
+                acq_info.triggerTimestamp.value, acq_info.acqStatus.value)
+        return info
+
+    def IQBLK_AcquireIQData(self) -> None:
+        """
+        Initiate an IQ block record acquisition.
+        """
+        self.err_check(self.rsa.IQBLK_AcquireIQData())
+
+    def IQBLK_GetIQBandwidth(self) -> float:
+        """
+        Query the IQ bandwidth value.
+
+        Returns
+        -------
+        float
+            The IQ bandwidth value.
+        """
+        iq_bandwidth = c_double()
+        self.err_check(self.rsa.IQBLK_GetIQBandwidth(byref(iq_bandwidth)))
+        return iq_bandwidth.value
+
+    def IQBLK_GetIQData(self, req_length: int) -> np.ndarray:
+        """
+        Retrieve an IQ block data record in a single interleaved array.
+
+        Parameters
+        ----------
+        req_length : int
+            Number of IQ sample pairs requested to be returned.
+            The maximum value of reqLength is equal to the recordLength
+            value set in IQBLK_SetIQRecordLength(). Smaller values allow
+            retrieving partial IQ records.
+
+        Returns
+        -------
+        Numpy array
+            I-data and Q-data stored in a single array.
+            I-data is stored at even indexes of the returned array,
+            and Q-data is stored at the odd indexes.
+        """
+        req_length = RSA.check_int(req_length)
+        req_length = RSA.check_range(req_length, 2, self.IQBLK_GetIQRecordLength())
+        out_length = c_int()
+        iq_data = (c_float * (req_length * 2))()
+        self.err_check(
+            self.rsa.IQBLK_GetIQData(byref(iq_data), byref(out_length), c_int(req_length)))
+        return np.ctypeslib.as_array(iq_data)
+
+    def IQBLK_GetIQDataDeinterleaved(self, req_length: int) -> Tuple[np.ndarray, np.ndarray]:
+        """
+        Retrieve an IQ block data record in separate I and Q array format.
+
+        Parameters
+        ----------
+        req_length : int
+            Number of IQ samples requested to be returned in data arrays.
+            The maximum value of reqLength is equal to the recordLength
+            value set in IQBLK_SetIQRecordLength(). Smaller values of
+            reqLength allow retrieving partial IQ records.
+
+        Returns
+        -------
+        iData : Numpy array
+            Array of I-data.
+        qData : Numpy array
+            Array of Q-data.
+        """
+        req_length = RSA.check_int(req_length)
+        req_length = RSA.check_range(req_length, 2, self.IQBLK_GetIQRecordLength())
+        i_data = (c_float * req_length)()
+        q_data = (c_float * req_length)()
+        out_length = c_int()
+        self.err_check(self.rsa.IQBLK_GetIQDataDeinterleaved(byref(i_data), byref(q_data),
+                                                             byref(out_length), c_int(req_length)))
+        return np.ctypeslib.as_array(i_data), np.ctypeslib.as_array(q_data)
+
+    def IQBLK_GetIQRecordLength(self) -> int:
+        """
+        Query the IQ record length.
+
+        Returns
+        -------
+        int
+            Number of IQ data samples to be generated with each acquisition.
+        """
+        record_length = c_int()
+        self.err_check(self.rsa.IQBLK_GetIQRecordLength(byref(record_length)))
+        return record_length.value
+
+    def IQBLK_GetIQSampleRate(self) -> float:
+        """
+        Query the IQ sample rate value.
+
+        Returns
+        -------
+        float
+            The IQ sampling frequency, in samples/second.
+        """
+        iq_sr = c_double()
+        self.err_check(self.rsa.IQBLK_GetIQSampleRate(byref(iq_sr)))
+        return iq_sr.value
+
+    def IQBLK_GetMaxIQBandwidth(self) -> float:
+        """
+        Query the maximum IQ bandwidth of the connected device.
+
+        Returns
+        -------
+        float
+            The maximum IQ bandwidth, measured in Hz.
+        """
+        max_bw = c_double()
+        self.err_check(self.rsa.IQBLK_GetMaxIQBandwidth(byref(max_bw)))
+        return max_bw.value
+
+    def IQBLK_GetMaxIQRecordLength(self) -> int:
+        """
+        Query the maximum IQ record length.
+
+        Returns
+        -------
+        int
+            The maximum IQ record length, measured in samples.
+        """
+        max_iq_rec_len = c_int()
+        self.err_check(self.rsa.IQBLK_GetMaxIQRecordLength(byref(max_iq_rec_len)))
+        return max_iq_rec_len.value
+
+    def IQBLK_GetMinIQBandwidth(self) -> float:
+        """
+        Query the minimum IQ bandwidth of the connected device.
+
+        Returns
+        -------
+        float
+            The minimum IQ bandwidth, measured in Hz.
+        """
+        min_bw = c_double()
+        self.err_check(self.rsa.IQBLK_GetMinIQBandwidth(byref(min_bw)))
+        return min_bw.value
+
+    def IQBLK_SetIQBandwidth(self, iq_bandwidth: Union[float, int]) -> None:
+        """
+        Set the IQ bandwidth value.
+
+        Parameters
+        ----------
+        iq_bandwidth : float or int
+            IQ bandwidth value measured in Hz
+        """
+        iq_bandwidth = RSA.check_num(iq_bandwidth)
+        iq_bandwidth = RSA.check_range(iq_bandwidth, self.IQBLK_GetMinIQBandwidth(),
+                                       self.IQBLK_GetMaxIQBandwidth())
+        self.err_check(self.rsa.IQBLK_SetIQBandwidth(c_double(iq_bandwidth)))
+
+    def IQBLK_SetIQRecordLength(self, record_length: int) -> None:
+        """
+        Set the number of IQ samples generated by each IQ block acquisition.
+
+        Parameters
+        ----------
+        record_length : int
+            IQ record length, measured in samples. Minimum value of 2.
+        """
+        record_length = RSA.check_int(record_length)
+        record_length = RSA.check_range(record_length, 2, self.IQBLK_GetMaxIQRecordLength())
+        self.err_check(self.rsa.IQBLK_SetIQRecordLength(c_int(record_length)))
+
+    def IQBLK_WaitForIQDataReady(self, timeout_msec: int) -> bool:
+        """
+        Wait for the data to be ready to be queried.
+
+        Parameters
+        ----------
+        timeout_msec : int
+            Timeout value measured in ms.
+
+        Returns
+        -------
+        bool
+            True indicates data is ready for acquisition. False indicates
+            the data is not ready and the timeout value is exceeded.
+        """
+        timeout_msec = RSA.check_int(timeout_msec)
+        ready = c_bool()
+        self.err_check(self.rsa.IQBLK_WaitForIQDataReady(c_int(timeout_msec), byref(ready)))
+        return ready.value
+
+    """ IQ STREAM METHODS """
+
+    def IQSTREAM_GetMaxAcqBandwidth(self) -> float:
+        """
+        Query the maximum IQ bandwidth for IQ streaming.
+
+        Returns
+        -------
+        float
+            The maximum IQ bandwidth supported by IQ streaming, in Hz.
+        """
+        max_bw_hz = c_double()
+        self.err_check(self.rsa.IQSTREAM_GetMaxAcqBandwidth(byref(max_bw_hz)))
+        return max_bw_hz.value
+
+    def IQSTREAM_GetMinAcqBandwidth(self) -> float:
+        """
+        Query the minimum IQ bandwidth for IQ streaming.
+
+        Returns
+        -------
+        float
+            The minimum IQ bandwidth supported by IQ streaming, in Hz.
+        """
+        min_bw_hz = c_double()
+        self.err_check(self.rsa.IQSTREAM_GetMinAcqBandwidth(byref(min_bw_hz)))
+        return min_bw_hz.value
+
+    def IQSTREAM_ClearAcqStatus(self) -> None:
+        """
+        Reset the "sticky" status bits of the acqStatus info element during
+        an IQ streaming run interval.
+        """
+        self.err_check(self.rsa.IQSTREAM_ClearAcqStatus())
+
+    def IQSTREAM_GetAcqParameters(self) -> Tuple[float, float]:
+        """
+        Retrieve the processing parameters of IQ streaming output bandwidth
+        and sample rate, resulting from the user's requested bandwidth.
+
+        Returns
+        -------
+        float: bwHz_act
+            Actual acquisition bandwidth of IQ streaming output data in Hz.
+        float: srSps
+            Actual sample rate of IQ streaming output data in Samples/sec.
+        """
+        bw_hz_act = c_double()
+        sr_sps = c_double()
+        self.err_check(self.rsa.IQSTREAM_GetAcqParameters(byref(bw_hz_act), byref(sr_sps)))
+        return bw_hz_act.value, sr_sps.value
+
+    def IQSTREAM_GetDiskFileInfo(self) -> _IQStreamFileInfo:
+        """
+        Retrieve information about the previous file output operation.
+
+        Returns
+        -------
+        An _IQStreamFileInfo structure which contains:
+        numberSamples : int
+            Number of IQ sample pairs written to the file.
+        sample0Timestamp : int
+            Timestamp of the first sample written to file.
+        triggerSampleIndex : int
+            Sample index where the trigger event occurred. This is only
+            valid if triggering has been enabled. Set to 0 otherwise.
+        triggerTimestamp : int
+            Timestamp of the trigger event. This is only valid if
+            triggering has been enabled. Set to 0 otherwise.
+        filenames : strings
+        acqStatus : int
+            Acquisition status flags for the run interval. Individual bits
+            are used as indicators as follows:
+                Individual Internal Write Block Status (Bits 0-15, starting
+                from LSB):
+                    Bits 0-15 indicate status for each internal write block,
+                        so may not be very useful. Bits 16-31 indicate the
+                        entire run status up to the time of query.
+                    Bit 0 : 1 = Input overrange.
+                    Bit 1 : 1 = USB data stream discontinuity.
+                    Bit 2 : 1 = Input buffer > 75% full (IQStream
+                        processing heavily loaded).
+                    Bit 3 : 1 = Input buffer overflow (IQStream processing
+                        overloaded, data loss has occurred).
+                    Bit 4 : 1 = Output buffer > 75% full (File output
+                        falling behind writing data).
+                    Bit 5 : 1 = Output buffer overflow (File output too
+                        slow, data loss has occurred).
+                    Bit 6 - Bit 15 : Unused, always 0.
+                Entire Run Summary Status ("Sticky Bits"):
+                    The bits in this range are essentially the same as Bits
+                        0-15, except once they are set (to 1) they remain
+                        set for the entire run interval. They can be used to
+                        determine if any of the status events occurred at
+                        any time during the run.
+                    Bit 16 : 1 = Input overrange.
+                    Bit 17 : 1 = USB data stream discontinuity.
+                    Bit 18 : 1 = Input buffer > 75% full (IQStream
+                        processing heavily loaded).
+                    Bit 19 : 1 = Input buffer overflow (IQStream processing
+                        overloaded, data loss has occurred).
+                    Bit 20 : 1 = Output buffer > 75% full (File output
+                        falling behind writing data).
+                    Bit 21 : 1 = Output buffer overflow (File output too
+                        slow, data loss has occurred).
+                    Bit 22 - Bit 31 : Unused, always 0.
+        """
+        file_info = _IQStreamFileInfo()
+        self.err_check(self.rsa.IQSTREAM_GetDiskFileInfo(byref(file_info)))
+        return file_info
+
+    def IQSTREAM_GetDiskFileWriteStatus(self) -> Tuple[bool, bool]:
+        """
+        Allow monitoring the progress of file output.
+
+        Returns
+        -------
+        bool: isComplete
+            Whether the IQ stream file output writing is complete.
+        bool: isWriting
+            Whether the IQ stream processing has started writing to file.
+        """
+        is_complete = c_bool()
+        is_writing = c_bool()
+        self.err_check(self.rsa.IQSTREAM_GetDiskFileWriteStatus(byref(is_complete),
+                                                                byref(is_writing)))
+        return is_complete.value, is_writing.value
+
+    def IQSTREAM_GetEnable(self) -> bool:
+        """
+        Retrieve the current IQ stream processing state.
+
+        Returns
+        -------
+        bool
+            The current IQ stream processing enable status. True if active,
+            False if inactive.
+        """
+        enabled = c_bool()
+        self.err_check(self.rsa.IQSTREAM_GetEnable(byref(enabled)))
+        return enabled.value
+
+    def IQSTREAM_GetIQDataBufferSize(self) -> int:
+        """
+        Get the maximum number of IQ sample pairs to be returned by IQSTREAM_GetData().
+
+        Returns
+        -------
+        int
+            Maximum size IQ output data buffer required when using client
+            IQ access. Size value is in IQ sample pairs.
+        """
+        max_size = c_int()
+        self.err_check(self.rsa.IQSTREAM_GetIQDataBufferSize(byref(max_size)))
+        return max_size.value
+
+    def IQSTREAM_SetAcqBandwidth(self, bw_hz_req: Union[float, int]) -> None:
+        """
+        Request the acquisition bandwidth of the output IQ stream samples.
+
+        The device Run state is cycled in order to apply the setting.
+
+        Parameters
+        ----------
+        bw_hz_req : float or int
+            Requested acquisition bandwidth of IQ streaming data, in Hz.
+        """
+        bw_hz_req = RSA.check_num(bw_hz_req)
+        bw_hz_req = RSA.check_range(bw_hz_req, self.IQSTREAM_GetMinAcqBandwidth(),
+                                    self.IQSTREAM_GetMaxAcqBandwidth())
+        self.rsa.DEVICE_Stop()
+        self.err_check(self.rsa.IQSTREAM_SetAcqBandwidth(c_double(bw_hz_req)))
+        self.rsa.DEVICE_Run()
+
+    def IQSTREAM_SetDiskFileLength(self, msec: int) -> None:
+        """
+        Set the time length of IQ data written to an output file.
+
+        Parameters
+        ----------
+        msec : int
+            Length of time in milliseconds to record IQ samples to file.
+        """
+        msec = RSA.check_int(msec)
+        msec = RSA.check_range(msec, 0, float('inf'))
+        self.err_check(self.rsa.IQSTREAM_SetDiskFileLength(c_int(msec)))
+
+    def IQSTREAM_SetDiskFilenameBase(self, filename_base: str) -> None:
+        """
+        Set the base filename for file output.
+
+        Parameters
+        ----------
+        filename_base : string
+            Base filename for file output.
+        """
+        filename_base = RSA.check_string(filename_base)
+        self.err_check(self.rsa.IQSTREAM_SetDiskFilenameBaseW(c_wchar_p(filename_base)))
+
+    def IQSTREAM_SetDiskFilenameSuffix(self, suffix_ctl: int) -> None:
+        """
+        Set the control that determines the appended filename suffix.
+
+        Parameters
+        ----------
+        suffix_ctl : int
+            The filename suffix control value.
+        """
+        suffix_ctl = RSA.check_int(suffix_ctl)
+        suffix_ctl = RSA.check_range(suffix_ctl, -2, float('inf'))
+        self.err_check(self.rsa.IQSTREAM_SetDiskFilenameSuffix(c_int(suffix_ctl)))
+
+    def IQSTREAM_SetIQDataBufferSize(self, req_size: int) -> None:
+        """
+        Set the requested size, in sample pairs, of the returned IQ record.
+
+        Parameters
+        ----------
+        req_size : int
+            Requested size of IQ output data buffer in IQ sample pairs.
+            0 resets to default.
+        """
+        req_size = RSA.check_int(req_size)
+        self.err_check(self.rsa.IQSTREAM_SetIQDataBufferSize(c_int(req_size)))
+
+    def IQSTREAM_SetOutputConfiguration(self, dest: str, dtype: str) -> None:
+        """
+        Set the output data destination and IQ data type.
+
+        Parameters
+        ----------
+        dest : string
+            Destination (sink) for IQ sample output. Valid settings:
+                CLIENT : Client application
+                FILE_TIQ : TIQ format file (.tiq extension)
+                FILE_SIQ : SIQ format file with header and data combined in
+                    one file (.siq extension)
+                FILE_SIQ_SPLIT : SIQ format with header and data in separate
+                    files (.siqh and .siqd extensions)
+        dtype : string
+            Output IQ data type. Valid settings:
+                SINGLE : 32-bit single precision floating point (not valid
+                    with TIQ file destination)
+                INT32 : 32-bit integer
+                INT16 : 16-bit integer
+                SINGLE_SCALE_INT32 : 32-bit single precision float, with
+                    data scaled the same as INT32 data type (not valid with
+                    TIQ file destination)
+
+        Raises
+        ------
+        RSAError
+            If inputs are not valid settings, or if single data type is
+            selected along with TIQ file format.
+        """
+        global _IQS_OUT_DEST, _IQS_OUT_DTYPE
+        dest = RSA.check_string(dest)
+        dtype = RSA.check_string(dtype)
+        if dest in _IQS_OUT_DEST and dtype in _IQS_OUT_DTYPE:
+            if dest == "FILE_TIQ" and "SINGLE" in dtype:
+                raise RSAError("Invalid selection of TIQ file with"
+                               + " single precision data type.")
+            else:
+                val1 = c_int(_IQS_OUT_DEST.index(dest))
+                val2 = c_int(_IQS_OUT_DTYPE.index(dtype))
+                self.err_check(self.rsa.IQSTREAM_SetOutputConfiguration(val1, val2))
+        else:
+            raise RSAError("Input data type or destination string invalid.")
+
+    def IQSTREAM_Start(self) -> None:
+        """
+        Initialize IQ stream processing and initiate data output.
+        """
+        self.err_check(self.rsa.IQSTREAM_Start())
+
+    def IQSTREAM_Stop(self) -> None:
+        """
+        Terminate IQ stream processing and disable data output.
+        """
+        self.err_check(self.rsa.IQSTREAM_Stop())
+
+    def IQSTREAM_WaitForIQDataReady(self, timeout_msec: int) -> bool:
+        """
+        Block while waiting for IQ Stream data output.
+
+        Parameters
+        ----------
+        timeout_msec : int
+            Timeout interval in milliseconds.
+
+        Returns
+        -------
+        bool
+            Ready status. True if data is ready, False if data not ready.
+        """
+        timeout_msec = RSA.check_int(timeout_msec)
+        timeout_msec = RSA.check_range(timeout_msec, 0, float('inf'))
+        ready = c_bool()
+        self.err_check(self.rsa.IQSTREAM_WaitForIQDataReady(c_int(timeout_msec), byref(ready)))
+        return ready.value
+
+    """ PLAYBACK METHODS NOT IMPLEMENTED """
+
+    """ POWER METHODS NOT IMPLEMENTED """
+    """ BROKEN IN RSA API FOR LINUX v1.0.0014 """
+
+    """ SPECTRUM METHODS """
+
+    def SPECTRUM_AcquireTrace(self) -> None:
+        """
+        Initiate a spectrum trace acquisition.
+        """
+        self.err_check(self.rsa.SPECTRUM_AcquireTrace())
+
+    def SPECTRUM_GetEnable(self) -> bool:
+        """
+        Return the spectrum measurement enable status.
+
+        Returns
+        -------
+        bool
+            True if spectrum measurement enabled, False if disabled.
+        """
+        enable = c_bool()
+        self.err_check(self.rsa.SPECTRUM_GetEnable(byref(enable)))
+        return enable.value
+
+    def SPECTRUM_GetLimits(self) -> dict:
+        """
+        Return the limits of the spectrum settings.
+
+        Returns
+        -------
+        Dict including the following:
+        maxSpan : float
+            Maximum span (device dependent).
+        minSpan : float
+            Minimum span.
+        maxRBW : float
+            Maximum resolution bandwidth.
+        minRBW : float
+            Minimum resolution bandwidth.
+        maxVBW : float
+            Maximum video bandwidth.
+        minVBW : float
+            Minimum video bandwidth.
+        maxTraceLength : int
+            Maximum trace length.
+        minTraceLength : int
+            Minimum trace length.
+        """
+        limits = _SpectrumLimits()
+        self.err_check(self.rsa.SPECTRUM_GetLimits(byref(limits)))
+        limits_dict = {'maxSpan': limits.maxSpan,
+                       'minSpan': limits.minSpan, 'maxRBW': limits.maxRBW,
+                       'minRBW': limits.minRBW, 'maxVBW': limits.maxVBW,
+                       'minVBW': limits.minVBW, 'maxTraceLength': limits.maxTraceLength,
+                       'minTraceLength': limits.minTraceLength
+                       }
+        return limits_dict
+
+    def SPECTRUM_GetSettings(self) -> dict:
+        """
+        Return the spectrum settings.
+
+        Returns
+        -------
+        All of the following as a dict, in this order:
+        span : float
+            Span measured in Hz.
+        rbw : float
+            Resolution bandwidth measured in Hz.
+        enableVBW : bool
+            True for video bandwidth enabled, False for disabled.
+        vbw : float
+            Video bandwidth measured in Hz.
+        traceLength : int
+            Number of trace points.
+        window : string
+            Windowing method used for the transform.
+        verticalUnit : string
+            Vertical units.
+        actualStartFreq : float
+            Actual start frequency in Hz.
+        actualStopFreq : float
+            Actual stop frequency in Hz.
+        actualFreqStepSize : float
+            Actual frequency step size in Hz.
+        actualRBW : float
+            Actual resolution bandwidth in Hz.
+        actualVBW : float
+            Not used.
+        actualNumIQSamples : int
+            Actual number of IQ samples used for transform.
+        """
+        global _SPECTRUM_WINDOWS, _SPECTRUM_VERTICAL_UNITS
+        sets = _SpectrumSettings()
+        self.err_check(self.rsa.SPECTRUM_GetSettings(byref(sets)))
+        settings_dict = {'span': sets.span,
+                         'rbw': sets.rbw,
+                         'enableVBW': sets.enableVBW,
+                         'vbw': sets.vbw,
+                         'traceLength': sets.traceLength,
+                         'window': _SPECTRUM_WINDOWS[sets.window],
+                         'verticalUnit': _SPECTRUM_VERTICAL_UNITS[sets.verticalUnit],
+                         'actualStartFreq': sets.actualStartFreq,
+                         'actualStopFreq': sets.actualStopFreq,
+                         'actualFreqStepSize': sets.actualFreqStepSize,
+                         'actualRBW': sets.actualRBW,
+                         'actualVBW': sets.actualVBW,
+                         'actualNumIQSamples': sets.actualNumIQSamples}
+        return settings_dict
+
+    def SPECTRUM_GetTrace(self, trace: str, max_trace_points: int) -> Tuple[np.ndarray, int]:
+        """
+        Return the spectrum trace data.
+
+        Parameters
+        ----------
+        trace : str
+            Selected trace. Must be 'Trace1', 'Trace2', or 'Trace3'.
+        max_trace_points : int
+            Maximum number of trace points to retrieve. The traceData array
+            should be at least this size.
+
+        Returns
+        -------
+        traceData : np.ndarray of floats
+            Spectrum trace data, in the unit of verticalunit specified in
+            the spectrum settings.
+        outTracePoints : int
+            Actual number of valid trace points in traceData array.
+
+        Raises
+        ------
+        RSAError
+            If the trace input does not match one of the valid strings.
+        """
+        global _SPECTRUM_TRACES
+        trace = RSA.check_string(trace)
+        max_trace_points = RSA.check_int(max_trace_points)
+        if trace in _SPECTRUM_TRACES:
+            trace_val = c_int(_SPECTRUM_TRACES.index(trace))
+        else:
+            raise RSAError("Invalid trace input.")
+        trace_data = (c_float * max_trace_points)()
+        out_trace_points = c_int()
+        self.err_check(self.rsa.SPECTRUM_GetTrace(trace_val, c_int(max_trace_points),
+                                                  byref(trace_data), byref(out_trace_points)))
+        return np.ctypeslib.as_array(trace_data), out_trace_points.value
+
+    def SPECTRUM_GetTraceInfo(self) -> dict:
+        """
+        Return the spectrum result information.
+
+        Returns
+        -------
+        Dict including:
+        timestamp : int
+            Timestamp. See REFTIME_GetTimeFromTimestamp() for converting
+            from timestamp to time.
+        acqDataStatus : int
+            1 for adcOverrange, 2 for refFreqUnlock, and 32 for adcDataLost.
+        """
+        trace_info = _SpectrumTraceInfo()
+        self.err_check(self.rsa.SPECTRUM_GetTraceInfo(byref(trace_info)))
+        info_dict = {'timestamp': trace_info.timestamp,
+                     'acqDataStatus': trace_info.acqDataStatus}
+        return info_dict
+
+    def SPECTRUM_GetTraceType(self, trace: str) -> Tuple[bool, str]:
+        """
+        Query the trace settings.
+
+        Parameters
+        ----------
+        trace : str
+            Desired trace. Must be 'Trace1', 'Trace2', or 'Trace3'.
+
+        Returns
+        -------
+        enable : bool
+            Trace enable status. True for enabled, False for disabled.
+        detector : string
+            Detector type. Valid results are:
+                PosPeak, NegPeak, AverageVRMS, or Sample.
+
+        Raises
+        ------
+        RSAError
+            If the trace input does not match a valid setting.
+        """
+        global _SPECTRUM_TRACES, _SPECTRUM_DETECTORS
+        trace = RSA.check_string(trace)
+        if trace in _SPECTRUM_TRACES:
+            trace_val = c_int(_SPECTRUM_TRACES.index(trace))
+        else:
+            raise RSAError("Invalid trace input.")
+        enable = c_bool()
+        detector = c_int()
+        self.err_check(self.rsa.SPECTRUM_GetTraceType(trace_val, byref(enable), byref(detector)))
+        return enable.value, _SPECTRUM_DETECTORS[detector.value]
+
+    def SPECTRUM_SetDefault(self) -> None:
+        """
+        Set the spectrum settings to their default values.
+        """
+        self.err_check(self.rsa.SPECTRUM_SetDefault())
+
+    def SPECTRUM_SetEnable(self, enable: bool) -> None:
+        """
+        Set the spectrum enable status.
+
+        Parameters
+        ----------
+        enable : bool
+            True enables the spectrum measurement. False disables it.
+        """
+        enable = RSA.check_bool(enable)
+        self.err_check(self.rsa.SPECTRUM_SetEnable(c_bool(enable)))
+
+    def SPECTRUM_SetSettings(self, span: Union[float, int], rbw: Union[float, int],
+                             enable_vbw: bool, vbw: Union[float, int], trace_len: int,
+                             win: str, vert_unit: str) -> None:
+        """
+        Set the spectrum settings.
+
+        Parameters
+        ----------
+        span : float or int
+            Span measured in Hz.
+        rbw : float or int
+            Resolution bandwidth measured in Hz.
+        enable_vbw : bool
+            True for video bandwidth enabled, False for disabled.
+        vbw : float or int
+            Video bandwidth measured in Hz.
+        trace_len : int
+            Number of trace points.
+        win : string
+            Windowing method used for the transform. Valid settings:
+            Kaiser, Mil6dB, BlackmanHarris, Rectangular, FlatTop, or Hann.
+        vert_unit : string
+            Vertical units. Valid settings: dBm, Watt, Volt, Amp, or dBmV.
+
+        Raises
+        ------
+        RSAError
+            If window or verticalUnit string inputs are not one of the
+            allowed settings.
+        """
+        global _SPECTRUM_WINDOWS, _SPECTRUM_VERTICAL_UNITS
+        win = RSA.check_string(win)
+        vert_unit = RSA.check_string(vert_unit)
+        if win in _SPECTRUM_WINDOWS and vert_unit in _SPECTRUM_VERTICAL_UNITS:
+            settings = _SpectrumSettings()
+            settings.span = RSA.check_num(span)
+            settings.rbw = RSA.check_num(rbw)
+            settings.enableVBW = RSA.check_bool(enable_vbw)
+            settings.vbw = RSA.check_num(vbw)
+            settings.traceLength = RSA.check_int(trace_len)
+            settings.window = _SPECTRUM_WINDOWS.index(win)
+            settings.verticalUnit = _SPECTRUM_VERTICAL_UNITS.index(vert_unit)
+            self.err_check(self.rsa.SPECTRUM_SetSettings(settings))
+        else:
+            raise RSAError("Window or vertical unit input invalid.")
+
+    def SPECTRUM_SetTraceType(self, trace: str = 'Trace1', enable: bool = True,
+                              detector: str = 'AverageVRMS') -> None:
+        """
+        Set the trace settings.
+
+        Parameters
+        ----------
+        trace : str
+            One of the spectrum traces. Can be 'Trace1', 'Trace2', or 'Trace3'.
+            Set to Trace1 by default.
+        enable : bool
+            True enables trace output. False disables it. True by default.
+        detector : string
+            Detector type. Default to AverageVRMS. Valid settings:
+                PosPeak, NegPeak, AverageVRMS, or Sample.
+
+        Raises
+        ------
+        RSAError
+            If the trace or detector type input is not one of the valid settings.
+        """
+        global _SPECTRUM_TRACES, _SPECTRUM_DETECTORS
+        trace = RSA.check_string(trace)
+        detector = RSA.check_string(detector)
+        if trace in _SPECTRUM_TRACES and detector in _SPECTRUM_DETECTORS:
+            trace_val = c_int(_SPECTRUM_TRACES.index(trace))
+            det_val = c_int(_SPECTRUM_DETECTORS.index(detector))
+            self.err_check(self.rsa.SPECTRUM_SetTraceType(trace_val, c_bool(enable), det_val))
+        else:
+            raise RSAError("Trace or detector type input invalid.")
+
+    def SPECTRUM_WaitForTraceReady(self, timeout_msec: int) -> bool:
+        """
+        Wait for the spectrum trace data to be ready to be queried.
+
+        Parameters
+        ----------
+        timeout_msec : int
+            Timeout value in msec.
+
+        Returns
+        -------
+        bool
+            True indicates spectrum trace data is ready for acquisition.
+            False indicates it is not ready, and timeout value is exceeded.
+        """
+        timeout_msec = RSA.check_int(timeout_msec)
+        ready = c_bool()
+        self.err_check(self.rsa.SPECTRUM_WaitForTraceReady(c_int(timeout_msec), byref(ready)))
+        return ready.value
+
+    """ TRIGGER METHODS """
+
+    def TRIG_ForceTrigger(self) -> None:
+        """Force the device to trigger."""
+        self.err_check(self.rsa.TRIG_ForceTrigger())
+
+    def TRIG_GetIFPowerTriggerLevel(self) -> float:
+        """
+        Return the trigger power level.
+
+        Returns
+        -------
+        float
+            Detection power level for the IF power trigger source
+        """
+        level = c_double()
+        self.err_check(self.rsa.TRIG_GetIFPowerTriggerLevel(byref(level)))
+        return level.value
+
+    def TRIG_GetTriggerMode(self) -> str:
+        """
+        Return the trigger mode (either freeRun or triggered).
+
+        Returns
+        -------
+        string
+            Either "freeRun" or "Triggered".
+        """
+        global _TRIGGER_MODE
+        mode = c_int()
+        self.err_check(self.rsa.TRIG_GetTriggerMode(byref(mode)))
+        return _TRIGGER_MODE[mode.value]
+
+    def TRIG_GetTriggerPositionPercent(self) -> float:
+        """
+        Return the trigger position percent.
+
+        Returns
+        -------
+        float
+            Trigger position percent value when the method completes.
+        """
+        trig_pos_percent = c_double()
+        self.err_check(self.rsa.TRIG_GetTriggerPositionPercent(byref(trig_pos_percent)))
+        return trig_pos_percent.value
+
+    def TRIG_GetTriggerSource(self) -> str:
+        """
+        Return the trigger source.
+
+        Returns
+        -------
+        string
+            The trigger source type. Valid results:
+                External : External source.
+                IFPowerLevel : IF power level source.
+        """
+        global _TRIGGER_SOURCE
+        source = c_int()
+        self.err_check(self.rsa.TRIG_GetTriggerSource(byref(source)))
+        return _TRIGGER_SOURCE[source.value]
+
+    def TRIG_GetTriggerTransition(self) -> str:
+        """
+        Return the current trigger transition mode.
+
+        Returns
+        -------
+        string
+            Name of the trigger transition mode. Valid results:
+                LH : Trigger on low-to-high input level change.
+                HL : Trigger on high-to-low input level change.
+                Either : Trigger on either LH or HL transitions.
+        """
+        global _TRIGGER_TRANSITION
+        transition = c_int()
+        self.err_check(self.rsa.TRIG_GetTriggerTransition(byref(transition)))
+        return _TRIGGER_TRANSITION[transition.value]
+
+    def TRIG_SetIFPowerTriggerLevel(self, level: Union[float, int]) -> None:
+        """
+        Set the IF power detection level.
+
+        Parameters
+         ----------
+        level : float or int
+            The detection power level setting for the IF power trigger
+            source.
+        """
+        level = RSA.check_num(level)
+        level = RSA.check_range(level, -130, 30)
+        self.err_check(self.rsa.TRIG_SetIFPowerTriggerLevel(c_double(level)))
+
+    def TRIG_SetTriggerMode(self, mode: str) -> None:
+        """
+        Set the trigger mode.
+
+        Parameters
+        ----------
+        mode : string
+            The trigger mode. Valid settings:
+                freeRun : to continually gather data
+                Triggered : do not acquire new data unless triggered
+
+        Raises
+        ------
+        RSAError
+            If the input string is not one of the valid settings.
+        """
+        global _TRIGGER_MODE
+        mode = RSA.check_string(mode)
+        if mode in _TRIGGER_MODE:
+            mode_value = _TRIGGER_MODE.index(mode)
+            self.err_check(self.rsa.TRIG_SetTriggerMode(c_int(mode_value)))
+        else:
+            raise RSAError("Invalid trigger mode input string.")
+
+    def TRIG_SetTriggerPositionPercent(self, trig_pos_percent: Union[float, int] = 50) -> None:
+        """
+        Set the trigger position percentage.
+
+        Parameters
+        ----------
+        trig_pos_percent : float or int
+            The trigger position percentage, from 1% to 99%.
+        """
+        trig_pos_percent = RSA.check_num(trig_pos_percent)
+        trig_pos_percent = RSA.check_range(trig_pos_percent, 1, 99)
+        self.err_check(self.rsa.TRIG_SetTriggerPositionPercent(c_double(trig_pos_percent)))
+
+    def TRIG_SetTriggerSource(self, source: str) -> None:
+        """
+        Set the trigger source.
+
+        Parameters
+        ----------
+        source : string
+            A trigger source type. Valid settings:
+                External : External source.
+                IFPowerLevel: IF power level source.
+
+        Raises
+        ------
+        RSAError
+            If the input string does not match one of the valid settings.
+        """
+        global _TRIGGER_SOURCE
+        source = RSA.check_string(source)
+        if source in _TRIGGER_SOURCE:
+            source_value = _TRIGGER_SOURCE.index(source)
+            self.err_check(self.rsa.TRIG_SetTriggerSource(c_int(source_value)))
+        else:
+            raise RSAError("Invalid trigger source input string.")
+
+    def TRIG_SetTriggerTransition(self, transition: str) -> None:
+        """
+        Set the trigger transition detection mode.
+
+        Parameters
+        ----------
+        transition : string
+            A trigger transition mode. Valid settings:
+                LH : Trigger on low-to-high input level change.
+                HL : Trigger on high-to-low input level change.
+                Either : Trigger on either LH or HL transitions.
+
+        Raises
+        ------
+        RSAError
+            If the input string does not match one of the valid settings.
+        """
+        global _TRIGGER_TRANSITION
+        transition = RSA.check_string(transition)
+        if transition in _TRIGGER_TRANSITION:
+            trans_value = _TRIGGER_TRANSITION.index(transition)
+            self.err_check(self.rsa.TRIG_SetTriggerTransition(c_int(trans_value)))
+        else:
+            raise RSAError("Invalid trigger transition mode input string.")
+
+    """ HELPER METHODS """
+
+    def DEVICE_SearchAndConnect(self, verbose: bool = False) -> None:
+        """
+        Search for and connect to a Tektronix RSA device.
+
+        More than 10 devices cannot be found at once. Connection only
+        occurs if exactly one device is found. It may be more convenient to
+        simply use DEVICE_Connect(), however this helper method is useful
+        if problems occur when searching for or connecting to a device.
+
+        Parameters
+        ----------
+        verbose : bool
+            Whether to print the steps of the process as they happen.
+
+        Raises
+        ------
+        RSAError
+            If no matching device is found, if more than one matching
+            device are found, or if connection fails.
+        """
+        if verbose:
+            print("Searching for devices...")
+
+        found_devices = self.DEVICE_Search()
+        num_found = len(found_devices)
+
+        # Zero devices found case handled within DEVICE_Search()
+        found_dev_str = ''
+        if num_found == 1:
+            found_dev_str += "The following device was found:"
+        elif num_found > 1:
+            found_dev_str += "The following devices were found:"
+        for (k, v) in found_devices.items():
+            found_dev_str += f'\r\n{str(k)}: {str(v)}'
+
+        if verbose:
+            print(f"Device search completed.\n{found_dev_str}\n")
+
+        # Multiple devices found case:
+        if num_found > 1:
+            raise RSAError(f"Found {num_found} devices, need exactly 1.")
+        else:
+            if verbose:
+                print("Connecting to device...")
+            self.DEVICE_Connect()
+            if verbose:
+                print("Device connected.\n")
+                
+                
+    def IQSTREAM_Tempfile_NoConfig(self, duration_msec: int,
+                                   return_status: bool=False) -> np.ndarray:
+        """
+        Retrieve IQ data from device by first writing to a tempfile.
+        Does not perform any device configuration: only captures data.
+
+        Parameters
+        ----------
+        duration_msec : int
+            Duration of time to record IQ data, in milliseconds.
+        return_status : bool
+            Whether or not to return the IQ capture status integer.
+            If False, errors will be raised for buffer overflow and
+            input overrange events.
+
+        Returns
+        -------
+        iq_data : np.ndarray of np.complex64 values
+            IQ data, with each element in the form (I + j*Q)
+        iq_status : str (optional)
+            The status string for the IQ capture, as defined in
+            the documentation for IQSTREAM_StatusParser().
+        """
+        # Configuration parameters
+        global _IQS_OUT_DEST, _IQS_OUT_DTYPE
+        dest = _IQS_OUT_DEST[3]  # Split SIQ format
+        dtype = _IQS_OUT_DTYPE[0]  # 32-bit single precision floating point
+        suffix_ctl = -2  # No file suffix
+        filename = 'tempIQ'
+        sleep_time_sec = 0.05  # Loop sleep time checking if acquisition complete
+
+        # Ensure device is stopped before proceeding
+        self.DEVICE_Stop()
+
+        # Create temp directory and collect data
+        with tempfile.TemporaryDirectory() as tmp_dir:
+            filename_base = tmp_dir + '/' + filename
+
+            # Configure device
+            self.IQSTREAM_SetOutputConfiguration(dest, dtype)
+            self.IQSTREAM_SetDiskFilenameBase(filename_base)
+            self.IQSTREAM_SetDiskFilenameSuffix(suffix_ctl)
+            self.IQSTREAM_SetDiskFileLength(duration_msec)
+            self.IQSTREAM_ClearAcqStatus()
+            self.DEVICE_PrepareForRun()
+
+            # Collect data
+            complete = False
+
+            self.DEVICE_Run()
+            self.IQSTREAM_Start()
+            while not complete:
+                sleep(sleep_time_sec)
+                complete = self.IQSTREAM_GetDiskFileWriteStatus()[0]
+            self.IQSTREAM_Stop()
+
+            # Check acquisition status
+            file_info = self.IQSTREAM_GetDiskFileInfo()
+            iq_status = self.IQSTREAM_StatusParser(file_info, not return_status)
+
+            self.DEVICE_Stop()
+
+            # Read data back in from file
+            with open(filename_base + '.siqd', 'rb') as f:
+                d = np.frombuffer(f.read(), dtype=np.float32)
+
+        # Deinterleave I and Q
+        i = d[0:-1:2]
+        q = np.append(d[1:-1:2], d[-1])
+        # Re-interleave as numpy complex64)
+        iq_data = i + 1j * q
+        assert iq_data.dtype == np.complex64
+        
+        if return_status:
+            return iq_data, iq_status
+        else:
+            return iq_data
+
+    def IQSTREAM_Tempfile(self, cf: Union[float, int], ref_level: Union[float, int],
+                          bw: Union[float, int], duration_msec: int,
+                          return_status: bool=False) -> np.ndarray:
+        """
+        Retrieve IQ data from device by first writing to a tempfile.
+        Performs device configuration before capturing.
+
+        Parameters
+        ----------
+        cf : float or int
+            Center frequency value in Hz.
+        ref_level : float or int
+            Reference level value in dBm.
+        bw : float or int
+            Requested IQ streaming bandwidth in Hz.
+        duration_msec : int
+            Duration of time to record IQ data, in milliseconds.
+        return_status : bool
+            Whether or not to return the IQ capture status integer.
+            If False, errors will be raised for buffer overflow and
+            input overrange events.
+
+        Returns
+        -------
+        iq_data : np.ndarray of np.complex64 values
+            IQ data, with each element in the form (I + j*Q)
+        iq_status : str (optional)
+            The status code for the IQ capture, as defined in
+            the documentation for IQSTREAM_StatusParser().
+        """
+        # Configuration parameters
+        global _IQS_OUT_DEST, _IQS_OUT_DTYPE
+        dest = _IQS_OUT_DEST[3]  # Split SIQ format
+        dtype = _IQS_OUT_DTYPE[0]  # 32-bit single precision floating point
+        suffix_ctl = -2  # No file suffix
+        filename = 'tempIQ'
+        sleep_time_sec = 0.05  # Loop sleep time checking if acquisition complete
+
+        # Ensure device is stopped before proceeding
+        self.DEVICE_Stop()
+
+        # Create temp directory and configure/collect data
+        with tempfile.TemporaryDirectory() as tmp_dir:
+            filename_base = tmp_dir + '/' + filename
+
+            # Configure device
+            self.CONFIG_SetCenterFreq(cf)
+            self.CONFIG_SetReferenceLevel(ref_level)
+            self.IQSTREAM_SetAcqBandwidth(bw)
+            self.IQSTREAM_SetOutputConfiguration(dest, dtype)
+            self.IQSTREAM_SetDiskFilenameBase(filename_base)
+            self.IQSTREAM_SetDiskFilenameSuffix(suffix_ctl)
+            self.IQSTREAM_SetDiskFileLength(duration_msec)
+            self.IQSTREAM_ClearAcqStatus()
+            self.DEVICE_PrepareForRun()
+
+            # Collect data
+            complete = False
+
+            self.DEVICE_Run()
+            self.IQSTREAM_Start()
+            while not complete:
+                sleep(sleep_time_sec)
+                complete = self.IQSTREAM_GetDiskFileWriteStatus()[0]
+            self.IQSTREAM_Stop()
+
+            # Check acquisition status
+            file_info = self.IQSTREAM_GetDiskFileInfo()
+            iq_status = self.IQSTREAM_StatusParser(file_info, not return_status)
+
+            self.DEVICE_Stop()
+
+            # Read data back in from file
+            with open(filename_base + '.siqd', 'rb') as f:
+                d = np.frombuffer(f.read(), dtype=np.float32)
+
+        # Deinterleave I and Q
+        i = d[0:-1:2]
+        q = np.append(d[1:-1:2], d[-1])
+        # Re-interleave as numpy complex64)
+        iq_data = i + 1j * q
+        assert iq_data.dtype == np.complex64
+        
+        if return_status:
+            return iq_data, iq_status
+        else:
+            return iq_data
+
+    @staticmethod
+    def IQSTREAM_StatusParser(iq_stream_info: _IQStreamFileInfo, exit: bool=True):
+        """
+        Parse _IQStreamFileInfo structure.
+
+        Depending on the 'exit' parameter, this method will either raise an
+        error, or return a status string. Possible values for the
+        returned status indicator are:
+
+        status | Definition
+        -------------------
+           0   | No error
+           1   | Input overrange.
+           2   | USB data stream discontinuity.
+           3   | Input buffer > 75% full.
+           4   | Input buffer overflow. IQ Stream processing
+               | too slow. Data loss has occurred.
+           5   | Output buffer > 75% full.
+           6   | Output buffer overflow. File writing
+               | too slow. Data loss has occurred.
+
+        In the case of multiple status codes being returned, the status
+        string will contain all returned status strings, separated by line
+        breaks.
+
+        Parameters
+        ----------
+        iq_stream_info : _IQStreamFileInfo
+            The IQ streaming status information structure.
+        exit : bool
+            If True, raise an exception for any error status in the IQ stream.
+            If False, return a flag representing the error, without raising an exception.
+
+        Returns
+        -------
+        status: str
+            A string containing all returned status messages.
+
+        Raises
+        ------
+        RSAError
+            If errors have occurred during IQ streaming, and exit is True.
+        """
+        status = iq_stream_info.acqStatus
+
+        # Handle no error case
+        if status == 0:
+            if exit:
+                return
+            else:
+                return 'No error.'
+        else:
+            # Construct status string if status != 0
+            status_str = ''
+            if bool(status & 0x10000):  # mask bit 16
+                status_str += 'Input overrange\n.'
+            if bool(status & 0x20000): # mask bit 17
+                status_str += 'USB data stream discontinuity.\n'
+            if bool(status & 0x40000):  # mask bit 18
+                status_str += 'Input buffer > 75{} full.\n'.format('%')
+            if bool(status & 0x80000):  # mask bit 19
+                status_str += 'Input buffer overflow. IQStream processing too'
+                status_str += ' slow, data loss has occurred.\n'
+            if bool(status & 0x100000):  # mask bit 20
+                status_str += 'Output buffer > 75{} full.\n'.format('%')
+            if bool(status & 0x200000):  # mask bit 21
+                status_str += 'Output buffer overflow. File writing too slow, '
+                status_str += 'data loss has occurred.\n'
+            if exit:
+                # Raise error with full string if configured
+                raise RSAError(status_str)
+            else:
+                # Or just return the status string
+                return status_str
+
+    def SPECTRUM_Acquire(self, trace: str = 'Trace1', trace_points: int = 801,
+                         timeout_msec: int = 10) -> Tuple[np.ndarray, int]:
+        """
+        Acquire spectrum trace.
+
+        Parameters
+        ----------
+        trace : str
+            Desired spectrum trace. Valid settings:
+            'Trace1', 'Trace2', or 'Trace3'
+        trace_points : int
+            Maximum number of trace points to receive.
+        timeout_msec : int
+            How long to wait for trace data to be ready, in milliseconds.
+
+        Returns
+        -------
+        traceData : float array
+            Spectrum trace data, in the unit of verticalunit specified in
+            the spectrum settings.
+        outTracePoints : int
+            Actual number of valid trace points in traceData array.
+        """
+        self.DEVICE_Run()
+        self.SPECTRUM_AcquireTrace()
+        while not self.SPECTRUM_WaitForTraceReady(timeout_msec):
+            pass
+        return self.SPECTRUM_GetTrace(trace, trace_points)
+
+    def IQBLK_Configure(self, cf: Union[float, int] = 1e9, ref_level: Union[float, int] = 0,
+                        iq_bw: Union[float, int] = 40e6, record_length: int = 1024) -> None:
+        """
+        Configure device for IQ block collection.
+
+        Parameters
+        ----------
+        cf : float or int
+            Desired center frequency in Hz.
+        ref_level : float or int
+            Desired reference level in dBm.
+        iq_bw : float or int
+            Desired IQ bandwidth in Hz.
+        record_length : int
+            Desired IQBLK record length, a number of samples.
+        """
+        self.CONFIG_SetCenterFreq(cf)
+        self.CONFIG_SetReferenceLevel(ref_level)
+        self.IQBLK_SetIQBandwidth(iq_bw)
+        self.IQBLK_SetIQRecordLength(record_length)
+
+    def IQBLK_Acquire(self, rec_len: int = 1024, timeout_ms: int = 10) -> Tuple[np.ndarray, np.ndarray]:
+        """
+        Acquire IQBLK data using IQBLK_GetIQDataDeinterleaved.
+
+        Parameters
+        ----------
+        rec_len : int
+            Requested IQBLK record length, a number of samples.
+        timeout_ms : int
+            How long to wait for IQBLK data to be ready, in milliseconds.
+
+        Returns
+        -------
+        i_data : np.ndarray
+            Deinterleaved I samples.
+        q_data : np.ndarray
+            Deinterleaved Q samples.
+        """
+        self.DEVICE_Run()
+        self.IQBLK_AcquireIQData()
+        while not self.IQBLK_WaitForIQDataReady(timeout_ms):
+            pass
+        return self.IQBLK_GetIQDataDeinterleaved(req_length=rec_len)


Test patch:
diff --git a/tests/rsa_api_test.py b/tests/rsa_api_test.py
new file mode 100644
index 0000000..25d2be2
--- /dev/null
+++ b/tests/rsa_api_test.py
@@ -0,0 +1,483 @@
+import unittest
+from os.path import isdir
+from os import mkdir
+import rsa_api
+from time import sleep
+import numpy as np
+import sys
+
+"""
+This is a test for the entire API Wrapper.
+It requires a compatible RSA device to be connected.
+"""
+
+class rsa_api_test(unittest.TestCase):
+    """Test for rsa_api_wrap.py"""
+
+    """ALIGN Command Testing"""
+
+    def test_ALIGN_GetAlignmentNeeded(self):
+        self.assertIsInstance(rsa.ALIGN_GetAlignmentNeeded(), bool)
+
+    def test_ALIGN_GetWarmupStatus(self):
+        self.assertIsInstance(rsa.ALIGN_GetWarmupStatus(), bool)
+
+    def test_ALIGN_RunAlignment(self):
+        self.assertIsNone(rsa.ALIGN_RunAlignment())
+
+    """CONFIG Command Testing"""
+
+    def test_CONFIG_CenterFreq(self):
+        cf = 2.4453e9
+        self.assertIsNone(rsa.CONFIG_SetCenterFreq(cf))
+        self.assertEqual(rsa.CONFIG_GetCenterFreq(), cf)
+
+        self.assertRaises(TypeError, rsa.CONFIG_SetCenterFreq, 'abc')
+        self.assertRaises(TypeError, rsa.CONFIG_SetCenterFreq, False)
+        self.assertRaises(ValueError, rsa.CONFIG_SetCenterFreq, 400e9)
+        self.assertRaises(ValueError, rsa.CONFIG_SetCenterFreq, -40e6)
+
+    """
+    # The following test requires an external reference to be in use.
+    def test_CONFIG_ExternalRef(self):
+        self.assertIsNone(CONFIG_SetExternalRefEnable(exRefEn=True))
+        self.assertTrue(CONFIG_GetExternalRefEnable())
+        extRefFreq = 10e6
+        self.assertEqual(CONFIG_GetExternalRefFrequency(), extRefFreq)
+        CONFIG_SetExternalRefEnable(exRefEn=False)
+        self.assertRaises(rsa_api.RSAError, CONFIG_GetExternalRefFrequency)
+    """
+
+    def test_CONFIG_FrequencyReferenceSource(self):
+        self.assertRaises(rsa_api.RSAError, rsa.CONFIG_SetFrequencyReferenceSource, 'GNSS')
+        self.assertRaises(rsa_api.RSAError, rsa.CONFIG_SetFrequencyReferenceSource, 'abc')
+        self.assertRaises(TypeError, rsa.CONFIG_SetFrequencyReferenceSource, 0)
+        self.assertIsNone(rsa.CONFIG_SetFrequencyReferenceSource('INTERNAL'))
+        self.assertIsInstance(rsa.CONFIG_GetFrequencyReferenceSource(), str)
+
+    def test_CONFIG_Preset(self):
+        self.assertIsNone(rsa.CONFIG_Preset())
+        self.assertEqual(rsa.CONFIG_GetCenterFreq(), 1.5e9)
+        self.assertEqual(rsa.CONFIG_GetReferenceLevel(), 0)
+        self.assertEqual(rsa.IQBLK_GetIQBandwidth(), 40e6)
+        self.assertEqual(rsa.IQBLK_GetIQRecordLength(), 1024)
+
+    def test_CONFIG_ReferenceLevel(self):
+        refLevel = 17
+        self.assertIsNone(rsa.CONFIG_SetReferenceLevel(refLevel))
+        self.assertEqual(rsa.CONFIG_GetReferenceLevel(), refLevel)
+        self.assertRaises(TypeError, rsa.CONFIG_SetReferenceLevel, 'abc')
+        self.assertRaises(ValueError, rsa.CONFIG_SetReferenceLevel, 31)
+        self.assertRaises(ValueError, rsa.CONFIG_SetReferenceLevel, -131)
+
+    def test_CONFIG_GetMaxCenterFreq(self):
+        global device
+        maxCf = 0
+        if device in ['RSA306B', 'RSA306']:
+            # Unsure if RSA306 (non-B variant) has same max freq.
+            maxCf = 6.2e9
+        elif device in ['RSA503A', 'RSA603A']:
+            maxCf = 3.0e9
+        elif device in ['RSA507A', 'RSA607A']:
+            maxCf = 7.5e9
+        elif device == 'RSA513A':
+            maxCf = 13.6e9
+        elif device == 'RSA518A':
+            maxCf = 18.0e9
+        self.assertEqual(rsa.CONFIG_GetMaxCenterFreq(), maxCf)
+
+    def test_CONFIG_GetMinCenterFreq(self):
+        minCf = 9e3
+        self.assertEqual(rsa.CONFIG_GetMinCenterFreq(), minCf)
+
+    def test_CONFIG_Attenuation(self):
+        # Only run test for RSA 500A/600A devices
+        global device
+        if device not in ['RSA306B', 'RSA306']:
+            self.assertIsNone(rsa.CONFIG_SetAutoAttenuationEnable(True))
+            self.assertTrue(rsa.CONFIG_GetAutoAttenuationEnable())
+            self.assertIsNone(rsa.CONFIG_SetAutoAttenuationEnable(False))
+            self.assertFalse(rsa.CONFIG_GetAutoAttenuationEnable())
+            atten_setting = -25
+            self.assertIsNone(rsa.CONFIG_SetRFAttenuator(atten_setting))
+            self.assertEqual(rsa.CONFIG_GetRFAttenuator(), atten_setting)
+
+    def test_CONFIG_Preamp(self):
+        # Only run test for RSA 500A/600A devices.
+        global device
+        if device not in ['RSA306B', 'RSA306']:
+            self.assertIsNone(rsa.CONFIG_SetRFPreampEnable(False))
+            rsa.DEVICE_Stop()
+            rsa.CONFIG_SetAutoAttenuationEnable(True)
+            rsa.DEVICE_Run()
+            self.assertFalse(rsa.CONFIG_GetRFPreampEnable())
+            self.assertIsNone(rsa.CONFIG_SetRFPreampEnable(True))
+            rsa.DEVICE_Stop()
+            rsa.DEVICE_Run()
+            self.assertTrue(rsa.CONFIG_GetRFPreampEnable())
+
+    """DEVICE Command Testing"""
+
+    def test_DEVICE_Connect(self):
+        # Stop and disconnect device first
+        rsa.DEVICE_Stop()
+        self.assertIsNone(rsa.DEVICE_Disconnect())
+        self.assertRaises(TypeError, rsa.DEVICE_Connect, 'abc')
+        self.assertRaises(ValueError, rsa.DEVICE_Connect, neg)
+        self.assertIsNone(rsa.DEVICE_Connect(0))
+
+    def test_DEVICE_PrepareForRun(self):
+        self.assertIsNone(rsa.DEVICE_PrepareForRun())
+
+    def test_DEVICE_Run(self):
+        self.assertIsNone(rsa.DEVICE_Run())
+        self.assertTrue(rsa.DEVICE_GetEnable())
+
+    def test_DEVICE_Stop(self):
+        self.assertIsNone(rsa.DEVICE_Stop())
+        self.assertFalse(rsa.DEVICE_GetEnable())
+
+    def test_DEVICE_GetEventStatus_no_signal(self):
+        eventType = ['OVERRANGE', 'TRIGGER', '1PPS']
+        for e in eventType:
+            event, timestamp = rsa.DEVICE_GetEventStatus(e)
+            self.assertFalse(event)
+            self.assertEqual(timestamp, 0)
+        self.assertRaises(TypeError, rsa.DEVICE_GetEventStatus, 0)
+        self.assertRaises(rsa_api.RSAError, rsa.DEVICE_GetEventStatus, 'abc')
+
+    def test_DEVICE_GetEventStatus_trig_event(self):
+        rsa.DEVICE_Run()
+        rsa.TRIG_ForceTrigger()
+        sleep(0.05)
+        trig, trigTs = rsa.DEVICE_GetEventStatus('TRIGGER')
+        self.assertTrue(trig)
+        self.assertGreater(trigTs, 0)
+
+    """
+    # The following test would require an actual overrange event.
+    def test_DEVICE_GetEventStatus_overrange(self):
+        pass
+    """
+
+    def test_DEVICE_GetOverTemperatureStatus(self):
+        self.assertIsInstance(rsa.DEVICE_GetOverTemperatureStatus(), bool)
+        self.assertEqual(rsa.DEVICE_GetOverTemperatureStatus(), False)
+
+    def test_DEVICE_GetNomenclature(self):
+        self.assertIsInstance(rsa.DEVICE_GetNomenclature(), str)
+
+    def test_DEVICE_GetSerialNumber(self):
+        sn = rsa.DEVICE_GetSerialNumber()
+        self.assertIsInstance(sn, str)
+        self.assertEqual(len(sn), 7)
+
+    def test_DEVICE_GetAPIVersion(self):
+        # This uses the Linux API version number
+        self.assertEqual(rsa.DEVICE_GetAPIVersion(), '1.0.0014')
+
+    def test_DEVICE_GetFWVersion(self):
+        self.assertIsInstance(rsa.DEVICE_GetFWVersion(), str)
+
+    def test_DEVICE_GetFPGAVersion(self):
+        self.assertIsInstance(rsa.DEVICE_GetFPGAVersion(), str)
+
+    def test_DEVICE_GetHWVersion(self):
+        self.assertIsInstance(rsa.DEVICE_GetHWVersion(), str)
+
+    def test_DEVICE_GetInfo(self):
+        info = rsa.DEVICE_GetInfo()
+        self.assertIsInstance(info, dict)
+        self.assertEqual(len(info), 6)
+        self.assertEqual(len(info['serialNum']), 7)
+        self.assertEqual(info['apiVersion'], '1.0.0014')
+        self.assertIsInstance(info['fwVersion'], str)
+        self.assertIsInstance(info['fpgaVersion'], str)
+        self.assertIsInstance(info['hwVersion'], str)
+
+    """IQBLK Command Testing"""
+
+    def test_IQBLK_MinMaxIQBandwidth(self):
+        maxBw = rsa.IQBLK_GetMaxIQBandwidth()
+        minBw = rsa.IQBLK_GetMinIQBandwidth()
+        rsa.IQBLK_SetIQBandwidth(maxBw)  # To get maxRl properly
+        maxRl = rsa.IQBLK_GetMaxIQRecordLength()
+        self.assertEqual(maxBw, 40e6)
+        self.assertEqual(minBw, 100)
+        self.assertEqual(maxRl, 126000000)
+
+    def test_IQBLK_IQBandwidth(self):
+        iqBw = 20e6
+        self.assertIsNone(rsa.IQBLK_SetIQBandwidth(iqBw))
+        self.assertEqual(iqBw, rsa.IQBLK_GetIQBandwidth())
+        self.assertRaises(ValueError, rsa.IQBLK_SetIQBandwidth, neg)
+        self.assertRaises(ValueError, rsa.IQBLK_SetIQBandwidth, 100e6)
+        self.assertRaises(TypeError, rsa.IQBLK_SetIQBandwidth, 'abc')
+
+    def test_IQBLK_IQRecordLength(self):
+        iqRl = 8192
+        self.assertIsNone(rsa.IQBLK_SetIQRecordLength(iqRl))
+        self.assertEqual(iqRl, rsa.IQBLK_GetIQRecordLength())
+        self.assertRaises(ValueError, rsa.IQBLK_SetIQRecordLength, neg)
+        self.assertRaises(ValueError, rsa.IQBLK_SetIQRecordLength, 200e6)
+        self.assertRaises(TypeError, rsa.IQBLK_SetIQRecordLength, 'abc')
+
+    def test_IQBLK_GetSampleRate(self):
+        self.assertIsInstance(rsa.IQBLK_GetIQSampleRate(), float)
+
+    def test_IQBLK_GetIQData(self):
+        rl = 1000
+        rsa.IQBLK_Configure()  # Configure to defaults
+        i, q = rsa.IQBLK_Acquire(rl, 10)
+        self.assertEqual(len(i), rl)
+        self.assertEqual(len(q), rl)
+
+        self.assertRaises(ValueError, rsa.IQBLK_Acquire, rec_len=neg)
+        self.assertRaises(ValueError, rsa.IQBLK_Acquire, rec_len=200000000)
+        self.assertRaises(TypeError, rsa.IQBLK_Acquire, rec_len='abc')
+
+    """IQSTREAM Command Testing"""
+
+    def test_IQSTREAM_MinMaxIQBandwidth(self):
+        minBandwidthHz = rsa.IQSTREAM_GetMinAcqBandwidth()
+        maxBandwidthHz = rsa.IQSTREAM_GetMaxAcqBandwidth()
+        self.assertEqual(minBandwidthHz, 9765.625)
+        self.assertEqual(maxBandwidthHz, 40e6)
+
+    def test_IQSTREAM_AcqBandwidth(self):
+        bwHz_req = [40e6, 20e6, 10e6, 5e6, 2.5e6, 1.25e6, 625e3, 312.5e3,
+                    156.25e3, 78125, 39062.5, 19531.25, 9765.625]
+        srSps_req = [56e6, 28e6, 14e6, 7e6, 3.5e6, 1.75e6, 875e3,
+                     437.5e3, 218.75e3, 109.375e3, 54687.5, 27343.75,
+                     13671.875]
+        baseSize = [65536, 65536, 65536, 65536, 65536, 32768, 16384, 8192,
+                    4096, 2048, 1024, 512, 256, 128]
+        for b, s, r in zip(bwHz_req, srSps_req, baseSize):
+            self.assertIsNone(rsa.IQSTREAM_SetAcqBandwidth(b))
+            bwHz_act, srSps = rsa.IQSTREAM_GetAcqParameters()
+            self.assertEqual(bwHz_act, b)
+            self.assertEqual(srSps, s)
+            self.assertIsNone(rsa.IQSTREAM_SetIQDataBufferSize(r))
+            self.assertEqual(rsa.IQSTREAM_GetIQDataBufferSize(), r)
+
+        self.assertRaises(TypeError, rsa.IQSTREAM_SetAcqBandwidth, 'abc')
+        self.assertRaises(TypeError, rsa.IQSTREAM_SetAcqBandwidth, [num])
+        self.assertRaises(ValueError, rsa.IQSTREAM_SetAcqBandwidth, 41e6)
+
+    def test_IQSTREAM_SetOutputConfiguration(self):
+        dest = ['CLIENT', 'FILE_TIQ', 'FILE_SIQ', 'FILE_SIQ_SPLIT']
+        dtype = ['SINGLE', 'INT32', 'INT16', 'SINGLE_SCALE_INT32']
+
+        for d in dest:
+            for t in dtype:
+                if d == 'FILE_TIQ' and 'SINGLE' in t:
+                    self.assertRaises(rsa_api.RSAError,
+                                      rsa.IQSTREAM_SetOutputConfiguration, d, t)
+                else:
+                    self.assertIsNone(rsa.IQSTREAM_SetOutputConfiguration(d, t))
+
+        self.assertRaises(TypeError, rsa.IQSTREAM_SetOutputConfiguration,
+                          num, dtype[0])
+        self.assertRaises(TypeError, rsa.IQSTREAM_SetOutputConfiguration,
+                          dest[0], num)
+        self.assertRaises(rsa_api.RSAError, rsa.IQSTREAM_SetOutputConfiguration, 'a', 'SINGLE')
+        self.assertRaises(rsa_api.RSAError, rsa.IQSTREAM_SetOutputConfiguration, 'CLIENT', 'a')
+
+    def test_IQSTREAM_SetDiskFilenameBase(self):
+        path = '/tmp/rsa_api_unittest'
+        if not isdir(path):
+            mkdir(path)
+        filename = 'iqstream_test'
+        filenameBase = path + filename
+        self.assertIsNone(rsa.IQSTREAM_SetDiskFilenameBase(filenameBase))
+
+        self.assertRaises(TypeError, rsa.IQSTREAM_SetDiskFilenameBase, num)
+        self.assertRaises(TypeError, rsa.IQSTREAM_SetDiskFilenameBase, b'abc')
+        self.assertRaises(TypeError, rsa.IQSTREAM_SetDiskFilenameBase, [num])
+
+    def test_IQSTREAM_SetDiskFilenameSuffix(self):
+        suffixCtl = [0, -1, -2]
+        for s in suffixCtl:
+            self.assertIsNone(rsa.IQSTREAM_SetDiskFilenameSuffix(s))
+
+        self.assertRaises(TypeError, rsa.IQSTREAM_SetDiskFilenameSuffix, 'abc')
+        self.assertRaises(ValueError, rsa.IQSTREAM_SetDiskFilenameSuffix, neg)
+
+    def test_IQSTREAM_SetDiskFileLength(self):
+        length = 100
+        self.assertIsNone(rsa.IQSTREAM_SetDiskFileLength(length))
+        self.assertRaises(TypeError, rsa.IQSTREAM_SetDiskFileLength, 'abc')
+        self.assertRaises(ValueError, rsa.IQSTREAM_SetDiskFileLength, neg)
+
+    def test_IQSTREAM_Operation(self):
+        rsa.IQSTREAM_SetAcqBandwidth(5e6)
+        rsa.IQSTREAM_SetOutputConfiguration('CLIENT', 'INT16')
+        rsa.IQSTREAM_GetAcqParameters()
+        rsa.DEVICE_Run()
+
+        self.assertIsNone(rsa.IQSTREAM_Start())
+        self.assertTrue(rsa.IQSTREAM_GetEnable())
+
+        self.assertIsNone(rsa.IQSTREAM_Stop())
+        self.assertFalse(rsa.IQSTREAM_GetEnable())
+
+        rsa.DEVICE_Stop()
+
+    def test_IQSTREAM_ClearAcqStatus(self):
+        self.assertIsNone(rsa.IQSTREAM_ClearAcqStatus())
+
+    """TRIG Command Testing"""
+
+    def test_TRIG_TriggerMode(self):
+        mode = ["freeRun", "triggered"]
+        for m in mode:
+            self.assertIsNone(rsa.TRIG_SetTriggerMode(m))
+            self.assertEqual(rsa.TRIG_GetTriggerMode(), m)
+
+    def test_TRIG_TriggerSource(self):
+        source = ["External", "IFPowerLevel"]
+        for s in source:
+            self.assertIsNone(rsa.TRIG_SetTriggerSource(s))
+            self.assertEqual(rsa.TRIG_GetTriggerSource(), s)
+
+    def test_TRIG_TriggerTransition(self):
+        trans = ["LH", "HL", "Either"]
+        for t in trans:
+            self.assertIsNone(rsa.TRIG_SetTriggerTransition(t))
+            self.assertEqual(rsa.TRIG_GetTriggerTransition(), t)
+        self.assertRaises(TypeError, rsa.TRIG_SetTriggerTransition, 0)
+
+    def test_TRIG_IFPowerTriggerLevel(self):
+        trigLevel = -10
+        self.assertIsNone(rsa.TRIG_SetIFPowerTriggerLevel(trigLevel))
+        self.assertEqual(rsa.TRIG_GetIFPowerTriggerLevel(), trigLevel)
+        self.assertRaises(TypeError, rsa.TRIG_SetIFPowerTriggerLevel, 'trigger')
+        self.assertRaises(ValueError, rsa.TRIG_SetIFPowerTriggerLevel, 31)
+        self.assertRaises(ValueError, rsa.TRIG_SetIFPowerTriggerLevel, -131)
+
+    def test_TRIG_TriggerPositionPercent(self):
+        self.assertRaises(ValueError, rsa.TRIG_SetTriggerPositionPercent, 0.5)
+        self.assertRaises(ValueError, rsa.TRIG_SetTriggerPositionPercent, 100)
+        self.assertRaises(TypeError, rsa.TRIG_SetTriggerPositionPercent, 'abc')
+
+        pos = 20
+        self.assertIsNone(rsa.TRIG_SetTriggerPositionPercent(pos))
+        self.assertEqual(rsa.TRIG_GetTriggerPositionPercent(), pos)
+
+    def test_TRIG_ForceTrigger(self):
+        self.assertIsNone(rsa.TRIG_ForceTrigger())
+
+    """SPECTRUM Command Testing"""
+
+    def test_SPECTRUM_Enable(self):
+        enable = [False, True]
+        for e in enable:
+            self.assertIsNone(rsa.SPECTRUM_SetEnable(e))
+            self.assertEqual(rsa.SPECTRUM_GetEnable(), e)
+
+    def test_SPECTRUM_Settings(self):
+        self.assertIsNone(rsa.SPECTRUM_SetDefault())
+
+        span = 20e6
+        rbw = 100e3
+        enableVBW = True
+        vbw = 50e3
+        traceLength = 1601
+        window = 'Hann'
+        verticalUnit = 'dBm'
+        self.assertIsNone(rsa.SPECTRUM_SetSettings(span, rbw, enableVBW, vbw,
+                                                   traceLength, window,
+                                                   verticalUnit))
+        settings = rsa.SPECTRUM_GetSettings()
+        self.assertIsInstance(settings, dict)
+        self.assertEqual(len(settings), 13)
+        self.assertEqual(settings['span'], span)
+        self.assertEqual(settings['rbw'], rbw)
+        self.assertEqual(settings['enableVBW'], enableVBW)
+        self.assertEqual(settings['vbw'], vbw)
+        self.assertEqual(settings['window'], window)
+        self.assertEqual(settings['traceLength'], traceLength)
+        self.assertEqual(settings['verticalUnit'], verticalUnit)
+
+        self.assertRaises(TypeError, rsa.SPECTRUM_SetSettings, 'span', 'rbw',
+                          'enableVBW', 'vbw', 'traceLength',
+                          1, 0)
+
+    def test_SPECTRUM_TraceType(self):
+        trace = 'Trace2'
+        enable = True
+        detector = 'AverageVRMS'
+        self.assertIsNone(rsa.SPECTRUM_SetTraceType(trace, enable, detector))
+        o_enable, o_detector = rsa.SPECTRUM_GetTraceType(trace)
+        self.assertEqual(enable, o_enable)
+        self.assertEqual(detector, o_detector)
+
+        self.assertRaises(rsa_api.RSAError, rsa.SPECTRUM_SetTraceType, trace='abc')
+        self.assertRaises(TypeError, rsa.SPECTRUM_SetTraceType, trace=40e5)
+        self.assertRaises(rsa_api.RSAError, rsa.SPECTRUM_SetTraceType,
+                          detector='abc')
+        self.assertRaises(TypeError, rsa.SPECTRUM_SetTraceType, detector=40e5)
+
+    def test_SPECTRUM_GetLimits(self):
+        global device
+        if device in ['RSA306B', 'RSA306']:
+            maxSpan = 6.2e9
+        elif device in ['RSA503A', 'RSA603A']:
+            maxSpan = 3.0e9
+        elif device in ['RSA507A', 'RSA607A']:
+            maxSpan = 7.5e9
+        elif device == 'RSA513A':
+            maxSpan = 13.6e9
+        elif device == 'RSA518A':
+            maxSpan = 18.0e9
+
+        limits = rsa.SPECTRUM_GetLimits()
+        self.assertIsInstance(limits, dict)
+        self.assertEqual(len(limits), 8)
+        self.assertEqual(limits['maxSpan'], maxSpan)
+        self.assertEqual(limits['minSpan'], 1e3)
+        self.assertEqual(limits['maxRBW'], 10e6)
+        self.assertEqual(limits['minRBW'], 10)
+        self.assertEqual(limits['maxVBW'], 10e6)
+        self.assertEqual(limits['minVBW'], 1)
+        self.assertEqual(limits['maxTraceLength'], 64001)
+        self.assertEqual(limits['minTraceLength'], 801)
+
+    def test_SPECTRUM_Acquire(self):
+        rsa.SPECTRUM_SetEnable(True)
+        span = 20e6
+        rbw = 100e3
+        enableVBW = True
+        vbw = 50e3
+        traceLength = 1601
+        window = 'Hann'
+        verticalUnit = 'dBm'
+        rsa.SPECTRUM_SetSettings(span, rbw, enableVBW, vbw, traceLength, window,
+                                 verticalUnit)
+        spectrum, outTracePoints = rsa.SPECTRUM_Acquire(trace='Trace1',
+                                                        trace_points=traceLength)
+        self.assertEqual(len(spectrum), traceLength)
+        self.assertIsInstance(spectrum, np.ndarray)
+        self.assertRaises(TypeError, rsa.SPECTRUM_Acquire, trace=1)
+
+        traceInfo = rsa.SPECTRUM_GetTraceInfo()
+        self.assertIsInstance(traceInfo, dict)
+        self.assertEqual(len(traceInfo), 2)
+
+
+if __name__ == '__main__':
+    """There must be a connected RSA device in order to test."""
+    TEST_SO_DIR = sys.argv[1]
+    del sys.argv[1:]
+    rsa = rsa_api.RSA(so_dir=TEST_SO_DIR)
+    rsa.DEVICE_Connect(0)
+    device = rsa.DEVICE_GetNomenclature()
+
+    # Some values used in testing
+    num = 400
+    neg = -400
+    unittest.main()
+
+    # Test cleanup
+    rsa.DEVICE_Stop()
+    rsa.DEVICE_Disconnect()