diff --git a/.github/workflows/dist-test.yml b/.github/workflows/dist-test.yml index 4878c36..7d0eb97 100644 --- a/.github/workflows/dist-test.yml +++ b/.github/workflows/dist-test.yml @@ -69,6 +69,9 @@ jobs: python -m pip install --upgrade pip setuptools wheel pip install -U pytest pytest-forked pytest-cov pip install numpy + - name: Install py3 dependencies + run: pip install pytest-asyncio + if: ${{ matrix.python-version != 2.7 }} - name: Download artifacts uses: actions/download-artifact@v2 with: diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index d32cb9e..3157647 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -48,13 +48,15 @@ jobs: python -m pip install --upgrade pip setuptools wheel pip install -U pytest pytest-cov pip install numpy + - name: Install py3 dependencies + run: pip install pytest-asyncio + if: ${{ matrix.python-version != 2.7 }} - name: Install pyrtlsdr with lib if: ${{ matrix.python-version != 2.7 }} run: pip install -e '.[lib]' - name: Install pyrtlsdr run: pip install -e . if: ${{ matrix.python-version == 2.7 }} - - name: Test with pytest shell: bash run: | diff --git a/.readthedocs.yaml b/.readthedocs.yaml new file mode 100644 index 0000000..a1c17c9 --- /dev/null +++ b/.readthedocs.yaml @@ -0,0 +1,19 @@ +version: 2 + + +build: + os: ubuntu-22.04 + tools: + python: "3.11" + +python: + install: + - requirements: doc/requirements.txt + - method: pip + path: . + extra_requirements: + - lib + +sphinx: + configuration: doc/source/conf.py + fail_on_warning: true diff --git a/rtlsdr/librtlsdr.py b/rtlsdr/librtlsdr.py index 45b86e6..e64fa29 100644 --- a/rtlsdr/librtlsdr.py +++ b/rtlsdr/librtlsdr.py @@ -139,10 +139,41 @@ def load_librtlsdr(): f = librtlsdr.rtlsdr_set_agc_mode f.restype, f.argtypes = c_int, [p_rtlsdr_dev, c_int] -# RTLSDR_API int rtlsdr_set_direct_sampling(rtlsdr_dev_t *dev, int on) +# RTLSDR_API int rtlsdr_set_direct_sampling(rtlsdr_dev_t *dev, int on) f = librtlsdr.rtlsdr_set_direct_sampling f.restype, f.argtypes = c_int, [p_rtlsdr_dev, c_int] +# RTLSDR_API int rtlsdr_set_dithering(rtlsdr_dev *dev, int on) +f = librtlsdr.rtlsdr_set_dithering +f.restype, f.argtypes = c_int, [p_rtlsdr_dev, c_int] + +# RTLSDR_API int rtlsdr_set_gpio_output(rtlsdr_dev_t *dev, uint8_t gpio) +f = librtlsdr.rtlsdr_set_gpio_output +f.restype, f.argtypes = c_int, [p_rtlsdr_dev, c_uint8] + +# RTLSDR_API int rtlsdr_set_gpio_input(rtlsdr_dev_t *dev, uint8_t gpio) +f = librtlsdr.rtlsdr_set_gpio_input +f.restype, f.argtypes = c_int, [p_rtlsdr_dev, c_uint8] + +# RTLSDR_API int librtlsdr.rtlsdr_set_gpio_bit(rtlsdr_dev_t *dev, uint8_t gpio, int val) +f = librtlsdr.rtlsdr_set_gpio_bit +f.restype, f.argtypes = c_int, [p_rtlsdr_dev, c_uint8, c_int] + +# RTLSDR_API int librtlsdr.rtlsdr_get_gpio_bit(rtlsdr_dev_t *dev, uint8_t gpio, int *val) +f = librtlsdr.rtlsdr_get_gpio_bit +f.restype, f.argtypes = c_int, [p_rtlsdr_dev, c_uint8, POINTER(c_int)] + +# RTLSDR_API int rtlsdr_set_gpio_byte(rtlsdr_dev_t *dev, int val) +f = librtlsdr.rtlsdr_set_gpio_byte +f.restype, f.argtypes = c_int, [p_rtlsdr_dev, c_int] + +# RTLSDR_API int rtlsdr_get_gpio_byte(rtlsdr_dev_t *dev, int *val) +f = librtlsdr.rtlsdr_get_gpio_byte +f.restype, f.argtypes = c_int, [p_rtlsdr_dev, POINTER(c_int)] + +# RTLSDR_API int rtlsdr_set_gpio_status(rtlsdr_dev_t *dev, int *status ) +f = librtlsdr.rtlsdr_set_gpio_status +f.restype, f.argtypes = c_int, [p_rtlsdr_dev, POINTER(c_int)] # int rtlsdr_set_sample_rate(rtlsdr_dev_t *dev, uint32_t rate); f = librtlsdr.rtlsdr_set_sample_rate diff --git a/rtlsdr/rtlsdr.py b/rtlsdr/rtlsdr.py index 3ffac9c..c0ba477 100644 --- a/rtlsdr/rtlsdr.py +++ b/rtlsdr/rtlsdr.py @@ -129,10 +129,16 @@ def get_serial(device_index): num_devices = librtlsdr.rtlsdr_get_device_count() return [get_serial(i) for i in range(num_devices)] - def __init__(self, device_index=0, test_mode_enabled=False, serial_number=None): - self.open(device_index, test_mode_enabled, serial_number) - - def open(self, device_index=0, test_mode_enabled=False, serial_number=None): + def __init__( + self, device_index=0, test_mode_enabled=False, + serial_number=None, dithering_enabled=True + ): + self.open(device_index, test_mode_enabled, serial_number, dithering_enabled) + + def open( + self, device_index=0, test_mode_enabled=False, + serial_number=None, dithering_enabled=True + ): """Connect to the device through the underlying wrapper library Initializes communication with the device and retrieves information @@ -148,6 +154,8 @@ def open(self, device_index=0, test_mode_enabled=False, serial_number=None): serial_number (:obj:`str`, optional): If not None, the device will be searched for by the given serial_number by :meth:`get_device_index_by_serial` and the ``device_index`` returned will be used automatically. + dithering_enabled (:obj:`bool`, optional): If False, disables PLL dithering + to prevent it destroying phase coherence in CLK-synchronized dongles. Notes: The arguments used here are passed directly from object @@ -175,6 +183,13 @@ def open(self, device_index=0, test_mode_enabled=False, serial_number=None): if result < 0: raise LibUSBError(result, 'Could not set test mode') + # disable PLL dithering if necessary. If it's going to happen, it must + # happen before frequency is set. + result = librtlsdr.rtlsdr_set_dithering(self.dev_p, int(dithering_enabled)) + if result < 0: + raise IOError('Error code %d when setting PLL dithering mode'\ + % (result)) + # reset buffers result = librtlsdr.rtlsdr_reset_buffer(self.dev_p) if result < 0: @@ -437,6 +452,120 @@ def set_direct_sampling(self, direct): return result + def set_dithering(self, enabled): + """Enable/disable PLL dithering. + + Arguments: + enabled (bool): + """ + result = librtlsdr.rtlsdr_set_dithering(self.dev_p, int(enabled)) + if result < 0: + raise IOError('Error code %d when setting PLL dither mode'\ + % (result)) + + return result + + def set_gpio_output(self, gpio): + """Set GPIO pin to output mode. + + Arguments: + gpio (int): RTL-SDR GPIO pin number + """ + result = librtlsdr.rtlsdr_set_gpio_output(self.dev_p, int(gpio)) + if result < 0: + raise IOError('Error code %d when setting GPIO to output mode'\ + % (result)) + + return result + + def set_gpio_input(self, gpio): + """Set GPIO pin to input mode. + + Arguments: + gpio (int): RTL-SDR GPIO pin number + """ + result = librtlsdr.rtlsdr_set_gpio_input(self.dev_p, int(gpio)) + if result < 0: + raise IOError('Error code %d when setting GPIO to input mode'\ + % (result)) + + return result + + def set_gpio_bit(self, gpio, val): + """Set GPIO pin value. + + Arguments: + gpio (int): RTL-SDR GPIO pin number + val (int): state to set GPIO pin to, 0 or 1 + """ + result = librtlsdr.rtlsdr_set_gpio_bit(self.dev_p, int(gpio), int(val)) + if result < 0: + raise IOError('Error code %d when setting GPIO bit'\ + % (result)) + + return result + + def get_gpio_bit(self, gpio): + """Get GPIO pin value. + + Arguments: + gpio (int): RTL-SDR GPIO pin number + + Returns: + val (int): Setting of GPIO pin + """ + val = c_int32(-1) + result = librtlsdr.rtlsdr_get_gpio_bit(self.dev_p, int(gpio), byref(val)) + if result < 0: + raise IOError('Error code %d when getting GPIO bit'\ + % (result)) + + return int(val.value) + + def set_gpio_byte(self, val): + """Set multiple GPIO pins at once using a byte. + + Arguments: + val (int): byte + """ + result = librtlsdr.rtlsdr_set_gpio_byte(self.dev_p, int(val)) + if result < 0: + raise IOError('Error code %d when setting GPIO byte'\ + % (result)) + + return result + + def get_gpio_byte(self): + """Get multiple GPIO pin values at once as a byte. + + Returns: + val (int): byte containing settings of multiple GPIO pins + """ + val = c_int32(-1) + result = librtlsdr.rtlsdr_get_gpio_byte(self.dev_p, byref(val)) + if result < 0: + raise IOError('Error code %d when setting GPIO byte'\ + % (result)) + + return int(val.value) + + def set_gpio_status(self): + """Get GPD register status as a byte. + + Note that the librtlsdr API calls rtlsdr_read_reg, and this function + is used everywhere like a getter. + + Returns: + val (int): byte containing status of all GPIO pins + """ + val = c_int32(-1) + result = librtlsdr.rtlsdr_set_gpio_status(self.dev_p, byref(val)) + if result < 0: + raise IOError('Error code %d when setting GPIO byte'\ + % (result)) + + return int(val.value) + def get_tuner_type(self): """Get the tuner type. @@ -467,7 +596,7 @@ def read_bytes(self, num_bytes=DEFAULT_READ_SIZE): ctypes.Array[c_ubyte]: A buffer of len(num_bytes) containing the raw samples read. """ - # FIXME: libsdrrtl may not be able to read an arbitrary number of bytes + # FIXME: librtlsdr may not be able to read an arbitrary number of bytes num_bytes = int(num_bytes) diff --git a/rtlsdr/rtlsdrtcp/server.py b/rtlsdr/rtlsdrtcp/server.py index a3d585b..2a3ab67 100644 --- a/rtlsdr/rtlsdrtcp/server.py +++ b/rtlsdr/rtlsdrtcp/server.py @@ -30,16 +30,19 @@ class RtlSdrTcpServer(RtlSdr, RtlSdrTcpBase): """ def __init__(self, device_index=0, test_mode_enabled=False, serial_number=None, - hostname='127.0.0.1', port=None): + hostname='127.0.0.1', port=None, dithering_enabled=True): RtlSdrTcpBase.__init__(self, device_index, test_mode_enabled, hostname, port) - RtlSdr.__init__(self, device_index, test_mode_enabled, serial_number) + RtlSdr.__init__(self, device_index, test_mode_enabled, serial_number, dithering_enabled) - def open(self, device_index=0, test_mode_enabled=False, serial_number=None): + def open( + self, device_index=0, test_mode_enabled=False, + serial_number=None, dithering_enabled=True + ): if not self.device_ready: return - super(RtlSdrTcpServer, self).open(device_index, test_mode_enabled, serial_number) + super(RtlSdrTcpServer, self).open(device_index, test_mode_enabled, serial_number, dithering_enabled) def run(self): """Runs the server thread and returns. Use this only if you are diff --git a/setup.cfg b/setup.cfg index 067c4c0..fe0ae80 100644 --- a/setup.cfg +++ b/setup.cfg @@ -2,11 +2,40 @@ universal=1 [metadata] +name = pyrtlsdr +version = 0.3.0 +author = roger +description = A Python wrapper for librtlsdr (a driver for Realtek RTL2832U based SDRs) project_urls = Documentation = https://pyrtlsdr.readthedocs.io/ Source = https://github.com/pyrtlsdr/pyrtlsdr long_description = file: README.md, long_description_content_type = text/markdown +license = GPLv3 +keywords = radio, librtlsdr, rtlsdr, sdr +platforms = any +classifiers = + Development Status :: 4 - Beta + Environment :: Console + Intended Audience :: Developers + License :: OSI Approved :: GNU General Public License (GPL) + Natural Language :: English + Operating System :: OS Independent + Programming Language :: Python :: 2 + Programming Language :: Python :: 2.7 + Programming Language :: Python :: 3 + Programming Language :: Python :: 3.8 + Programming Language :: Python :: 3.9 + Programming Language :: Python :: 3.10 + Programming Language :: Python :: 3.11 + Topic :: Utilities + +[options] +packages = find: + +[options.packages.find] +exclude = + tests* [options.extras_require] lib = pyrtlsdrlib diff --git a/setup.py b/setup.py index cac5d14..577951f 100644 --- a/setup.py +++ b/setup.py @@ -15,56 +15,6 @@ # along with pyrlsdr. If not, see . -import os -import sys -import re -import shutil -from setuptools import setup, find_packages +from setuptools import setup -PACKAGE_NAME = 'pyrtlsdr' -VERSION = '0.3.0' - -BASE_DIR = os.path.dirname(os.path.abspath(__file__)) -IS_RTDBUILD = os.environ.get('READTHEDOCS', '').lower() == 'true' - -if IS_RTDBUILD: - # copy a mocked wrapper since we can't build librtlsdr on rtfd - def copy_mock_librtlsdr(): - mock_src = os.path.join(BASE_DIR, 'tests', 'testlibrtlsdr.py') - lib_dst = os.path.join(BASE_DIR, 'rtlsdr', 'librtlsdr.py') - orig_lib_dst = os.path.join(BASE_DIR, 'rtlsdr', 'librtlsdr.py.orig') - if not os.path.exists(orig_lib_dst): - print(' -> '.join([lib_dst, orig_lib_dst])) - os.rename(lib_dst, orig_lib_dst) - print(' -> '.join([mock_src, lib_dst])) - shutil.copy(mock_src, lib_dst) - if 'install' in sys.argv: - copy_mock_librtlsdr() - -#HERE = os.path.abspath(os.path.dirname(__file__)) -#README = open(os.path.join(HERE, 'README.md')).read() - -setup( - name=PACKAGE_NAME, - version=VERSION, - author='roger', - url='https://github.com/pyrtlsdr/pyrtlsdr', - description='A Python wrapper for librtlsdr (a driver for Realtek RTL2832U based SDR\'s)', - classifiers=['Development Status :: 4 - Beta', - 'Environment :: Console', - 'Intended Audience :: Developers', - 'License :: OSI Approved :: GNU General Public License (GPL)', - 'Natural Language :: English', - 'Operating System :: OS Independent', - 'Programming Language :: Python :: 2', - 'Programming Language :: Python :: 2.7', - 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.8', - 'Programming Language :: Python :: 3.9', - 'Programming Language :: Python :: 3.10', - 'Programming Language :: Python :: 3.11', - 'Topic :: Utilities'], - license='GPLv3', - keywords='radio librtlsdr rtlsdr sdr', - platforms=['any'], - packages=find_packages(exclude=['tests*'])) +setup() diff --git a/tests/no_override_client_mode.py b/tests/no_override_client_mode.py index 64d9963..0405ac2 100644 --- a/tests/no_override_client_mode.py +++ b/tests/no_override_client_mode.py @@ -12,7 +12,7 @@ def client_mode(monkeypatch): @pytest.mark.no_overrides def test_client_mode(client_mode): - with pytest.warns(None) as record: + with pytest.warns(Warning) as record: import rtlsdr assert len(record) >= 1 warn_classes = [rec.message.__class__ for rec in record] diff --git a/tests/test_aio.py b/tests/test_aio.py index 70b29f2..d14bb24 100644 --- a/tests/test_aio.py +++ b/tests/test_aio.py @@ -1,55 +1,54 @@ -import asyncio import pytest -def test(rtlsdraio): - import math - from utils import generic_test - - async def main(): - sdr = rtlsdraio.RtlSdrAio() - generic_test(sdr) +@pytest.fixture(params=[64*1024, 96*1024, 128*1024]) +def num_samples(request): + return request.param - print('Configuring SDR...') - sdr.rs = 2.4e6 - sdr.fc = 100e6 - sdr.gain = 10 - print(' sample rate: %0.6f MHz' % (sdr.rs/1e6)) - print(' center frequency %0.6f MHz' % (sdr.fc/1e6)) - print(' gain: %d dB' % sdr.gain) +@pytest.fixture(params=['samples', 'bytes']) +def read_format(request): + return request.param +@pytest.mark.asyncio +async def test(rtlsdraio, num_samples, read_format): + import math + from utils import generic_test - print('Streaming samples...') - await process_samples(sdr, 'samples') - await sdr.stop() + sdr = rtlsdraio.RtlSdrAio() + generic_test(sdr) - print('Streaming bytes...') - await process_samples(sdr, 'bytes') - await sdr.stop() + print('Configuring SDR...') + sdr.rs = 2.4e6 + sdr.fc = 100e6 + sdr.gain = 10 + print(' sample rate: %0.6f MHz' % (sdr.rs/1e6)) + print(' center frequency %0.6f MHz' % (sdr.fc/1e6)) + print(' gain: %d dB' % sdr.gain) - # make sure our format parameter checks work - with pytest.raises(ValueError): - await process_samples(sdr, 'foo') - print('Done') + print('Streaming %s...' % (read_format)) - sdr.close() + i = 0 + async_iter = sdr.stream(num_samples_or_bytes=num_samples, format=read_format) + async for samples in async_iter: + assert len(samples) == num_samples + if read_format == 'bytes': + samples = sdr.packed_bytes_to_iq(samples) + power = sum(abs(s)**2 for s in samples) / len(samples) + print('Relative power:', 10*math.log10(power), 'dB') + i += 1 - async def process_samples(sdr, fmt): - async def packed_bytes_to_iq(samples): - return sdr.packed_bytes_to_iq(samples) + if i > 20: + break + await sdr.stop() - i = 0 - async for samples in sdr.stream(format=fmt): - if fmt == 'bytes': - samples = await packed_bytes_to_iq(samples) - power = sum(abs(s)**2 for s in samples) / len(samples) - print('Relative power:', 10*math.log10(power), 'dB') + assert not async_iter.running + assert async_iter.executor_task.done() - i += 1 + # make sure our format parameter checks work + with pytest.raises(ValueError): + _ = sdr.stream(format='foo') - if i > 20: - break + print('Done') - loop = asyncio.get_event_loop() - loop.run_until_complete(main()) + sdr.close() diff --git a/tests/testlibrtlsdr.py b/tests/testlibrtlsdr.py index 6bfdf43..0e7d2c3 100644 --- a/tests/testlibrtlsdr.py +++ b/tests/testlibrtlsdr.py @@ -121,6 +121,23 @@ def rtlsdr_set_agc_mode(self, dev_p, mode): def rtlsdr_set_direct_sampling(self, dev_p, direct): self.direct_sampling = direct return ERROR_CODE + def rtlsdr_set_dithering(self, dev_p, dither): + self.dithering = dither + return ERROR_CODE + def rtlsdr_set_gpio_output(self, dev_p, gpio): + return ERROR_CODE + def rtlsdr_set_gpio_input(self, dev_p, gpio): + return ERROR_CODE + def rtlsdr_set_gpio_bit(self, dev_p, gpio, val): + return ERROR_CODE + def rtlsdr_get_gpio_bit(self, dev_p, gpio): + return ERROR_CODE + def rtlsdr_set_gpio_byte(self, dev_p, val): + return ERROR_CODE + def rtlsdr_get_gpio_byte(self, dev_p): + return ERROR_CODE + def rtlsdr_set_gpio_status(self, dev_p): + return ERROR_CODE def rtlsdr_get_tuner_type(self, *args): return ERROR_CODE def rtlsdr_read_sync(self, dev_p, buf, num_bytes, num_bytes_read):