Software Defined Radio - how does it work? Part 3

    Hi, Habr.

    The second part examined the practical aspects of using SDR. In this part, we will look at how to receive NOAA weather satellite data using Python and the inexpensive ($ 30) RTL-SDR receiver. The considered code will work everywhere - on Windows, OSX, Linux, and even on the Raspberry Pi.



    Who cares, continued under the cut.

    SoapySDR


    There are quite a lot of manufacturers of various SDR devices, and it would be very inconvenient to support each separately, and it’s expensive in terms of purchasing hardware for testing. In principle, for unified access, there are two libraries that have essentially become the standard. The first is the already quite old ExtIO DLL interface , which is probably at least 10 years old, the second is the more modern SoapySDR library , which we will look at.

    SoapySDR is a set of cross-platform libraries written in C ++ that provide unified access to SDR devices, both receivers and transceivers. If the manufacturer makes such an interface, then its device will automatically work with a fairly large number of popular programs (GQRX, GNU Radio, CubicSDR, etc.). Almost all adequate manufacturers, except for some, (taking this opportunity, I send greetings to EE) have SoapySDR support, a list of supported devices can be found on the project page . As you can see, it is quite large and includes HackRF, USRP, SDRPlay, LimeSDR, RTL-SDR, Red Pitaya and many others.

    The SoapySDR library is cross-platform, i.e. the code written for it will work under Windows, OSX, Linux, and even on the Raspberry Pi. For Windows, the required libraries are part of the PothosSDR package ; for other platforms, SoapySDR will have to be compiled independently. It is necessary to compile two parts - the library itself , and the “plug-in” for the desired receiver, in our case it will be SoapyRTLSDR (under Windows, the library can also be assembled from source, for this you need Visual Studio, Cmake, and SWIG). Now everything is ready, and you can write code.

    We import the library and get a list of receivers:

    from __future__ import print_function
    import SoapySDR
    # Enumerate devices
    print("SDR devices:")
    for d in SoapySDR.Device.enumerate(''):
        print(d)
    print()

    We connect the receiver, run the code and see a list of devices, among which is our rtlsdr.



    The rest of the devices are sound cards, as we recall, historically the first SDRs worked precisely through the PC linear input, and the library also supports them. We get information about the device - the number of available channels, frequency range, etc.:

    soapy_device = "rtlsdr"
    device = SoapySDR.Device(dict(driver = soapy_device))
    channels = list(range(device.getNumChannels(SoapySDR.SOAPY_SDR_RX)))
    print("Channels:", channels)
    ch = channels[0]
    sample_rates = device.listSampleRates(SoapySDR.SOAPY_SDR_RX, ch)
    print("Sample rates:\n", sample_rates)
    bandwidths = list(map(lambda r: int(r.maximum()), device.getBandwidthRange(SoapySDR.SOAPY_SDR_RX, ch)))
    print("Bandwidths:\n", bandwidths)
    print("Gain controls:")
    for gain in device.listGains(SoapySDR.SOAPY_SDR_RX, ch):
        print("  %s: %s" % (gain, device.getGainRange(SoapySDR.SOAPY_SDR_RX, ch, gain)))
    frequencies = device.listFrequencies(SoapySDR.SOAPY_SDR_RX, ch)
    print("Frequencies names:", frequencies)
    frequency_name = frequencies[0]
    print("Frequency channel name:", frequency_name)
    print("Frequency range:", device.getFrequencyRange(SoapySDR.SOAPY_SDR_RX, ch, frequency_name)[0])
    

    We start the program and see information about the receiver:



    We see that the receiver has one input channel with the name "RF", the possible sampling frequencies [250000.0, 1024000.0, 1536000.0, 1792000.0, 1920000.0, 2048000.0, 2160000.0, 2560000.0, 2880000.0, 3200000.0] and the frequency range of 24 MHz -1.7 GHz.

    Life hack - the same data can also be obtained from the command line by typing the command SoapySDRUtil --probe = "driver = rtlsdr" .

    Knowing this, we can record the data stream in WAV. As mentioned in the previous part, data from the SDR is represented by a stream of signals called I and Q, which are samples from the ADC, roughly they can be represented as RAW data from the camera. Who cares more, can read for example here. It’s enough for us to know that we can write this data, and other SDR programs can then work with them.

    The record itself is quite simple - the readStream function fills the buffer if there is data, if there is no data yet, then -1 will be returned. Below is a record code of 10 samples (non-essential parts of the code are omitted).

    device.setFrequency(SoapySDR.SOAPY_SDR_RX, channel, "RF", frequency)
    device.setGain(SoapySDR.SOAPY_SDR_RX, channel, "TUNER", gain)
    device.setGainMode(SoapySDR.SOAPY_SDR_RX, channel, False)
    device.setSampleRate(SoapySDR.SOAPY_SDR_RX, channel, sample_rate)
    # Number of blocks to save
    block, max_blocks = 0, 10
    block_size = device.getStreamMTU(stream)
    print("Block size:", block_size)
    buffer_format = np.int8
    buffer_size = 2*block_size  # I+Q
    buffer = np.empty(buffer_size, buffer_format)
    while True:
        d_info = device.readStream(stream, [buffer], buffer_size)
        if d_info.ret > 0:
            wav.write(buffer[0:2*d_info.ret])
            print("Bytes saved:", 2*d_info.ret)
            block += 1
            if block > max_blocks:
                break
    

    The result in the screenshot:



    As you can see, we get data blocks from the device, the size of one block is 131072 bytes, which at a sampling rate of 250,000 gives us a duration of about half a second. In general, those who previously worked with sound cards in Windows will find a lot in common.

    For the test, write the file and check that everything is fine - it can be played in SDR #. There is one more trick - in order for SDR # to correctly show the frequencies of the stations, the file name must be written in a format compatible with HDSDR, of the form “HDSDR_20190518_115500Z_101000kHz_RF.wav” (as you might guess, the date and time are in GMT at the beginning, then the frequency in kilohertz) . This is easy to write in Python:

    frequency = 101000000
    file_name = "HDSDR_%s_%dkHz_RF.wav" % (datetime.datetime.utcnow().strftime("%Y%m%d_%H%M%SZ"), frequency/1000)
    

    First, check on the FM band. Everything is fine, the station is visible, music is playing, RDS is working.



    You can start recording NOAA.

    NOAA intake


    So, we have a receiver and a recording program. We will be interested in the NOAA 15, NOAA 18 and NOAA 19 weather satellites transmitting images of the Earth’s surface at frequencies of 137.620, 137.9125 and 137.100 MHz. The main difficulty here is that you need to "catch" the moment when the satellite flies over us. You can find out the flight time online at https://www.n2yo.com/passes/?s=25338 , https://www.n2yo.com/passes/?s=28654 and https://www.n2yo.com / passes /? s = 33591 respectively.



    In order not to sit at the computer, add to the program the waiting for the right time. This will also allow you to run the program on the Raspberry Pi, without a display and keyboard.

    import datetime
    def wait_for_start(dt):
        # Wait for the start
        while True:
            now = datetime.datetime.now()
            diff = int((dt - now).total_seconds())
            print("{:02d}:{:02d}:{:02d}: Recording will be started after {}m {:02d}s...".format(now.hour, now.minute, now.second, int(diff / 60), diff % 60))
            time.sleep(5)
            if diff <= 1:
                break
    wait_for_start(datetime.datetime(2019, 5, 18, 21, 49, 0))


    By the way, to run the script on the Raspberry Pi and leave it working after closing the console, you need to enter the command "nohup python recorder.py &".

    Everything is ready, run the script and can do other things, the recording lasts about 20 minutes. In parallel, the question may arise - is it possible to see the passage of the satellite with the naked eye? According to the table, its maximum brightness is about 5.5m magnitude , the limit of the human eye in ideal conditions is 6m. Those. in a really dark sky, far beyond the city, the passage of the NOAA satellite can theoretically be noticed, in the city, of course, there is no chance (as they wrote on Habré , a generation of people who have never seen the Milky Way in their life has already grown ).

    The result of the script is a recorded wav file, its spectrum is shown in the screenshot.



    We see a completely distinguishable signal, although of course with a special antenna for receiving NOAA the quality would be much better. The signal format is called APT ( Automatic Picture Transmission ), from it you can get an image of the earth's surface, if anyone is interested, you can separately consider its decoding. But there are, of course, ready-made programs, you can decode such signals using WxToImg or MultiPSK.

    It is interesting to see the Doppler shift in the spectrum, which occurs because the satellite flies past us. Probably, it is not difficult to calculate its speed, knowing the time and frequency shift in hertz.

    Of course, the program can be used not only for recording NOAA, any bandwidth and frequency can be specified in the settings. For those who want to experiment with SoapySDR themselves, the program code is entirely located under the spoiler.

    Source
    from __future__ import print_function
    import SoapySDR
    import numpy as np
    import struct
    import sys
    import time
    import datetime
    def wait_for_start(dt):
        # Wait for the start
        while True:
            now = datetime.datetime.now()
            diff = int((dt - now).total_seconds())
            print("{:02d}:{:02d}:{:02d}: Recording will be started after {}m {:02d}s...".format(now.hour, now.minute, now.second, int(diff / 60), diff % 60))
            time.sleep(5)
            if diff <= 1:
                break
    def sdr_enumerate():
        # Enumerate SDR devices
        print("SDR devices:")
        for d in SoapySDR.Device.enumerate(''):
            print(d)
        print()
    def sdr_init():
        soapy_device = "rtlsdr"
        device = SoapySDR.Device({"driver": soapy_device})
        channels = list(range(device.getNumChannels(SoapySDR.SOAPY_SDR_RX)))
        print("Channels:", channels)
        ch = channels[0]
        sample_rates = device.listSampleRates(SoapySDR.SOAPY_SDR_RX, ch)
        print("Sample rates:\n", sample_rates)
        print("Gain controls:")
        for gain in device.listGains(SoapySDR.SOAPY_SDR_RX, ch):
            print("  %s: %s" % (gain, device.getGainRange(SoapySDR.SOAPY_SDR_RX, ch, gain)))
        frequencies = device.listFrequencies(SoapySDR.SOAPY_SDR_RX, ch)
        print("Frequencies names:", frequencies)
        frequency_name = frequencies[0]
        print("Frequency channel name:", frequency_name)
        print("Frequency range:", device.getFrequencyRange(SoapySDR.SOAPY_SDR_RX, ch, frequency_name)[0])
        print()
        return device
    def sdr_record(device, frequency, sample_rate, gain, blocks_count):
        print("Frequency:", frequency)
        print("Sample rate:", sample_rate)
        print("Gain:", gain)
        channel = 0  # Always for RTL-SDR
        device.setFrequency(SoapySDR.SOAPY_SDR_RX, channel, "RF", frequency)
        device.setGain(SoapySDR.SOAPY_SDR_RX, channel, "TUNER", gain)
        device.setGainMode(SoapySDR.SOAPY_SDR_RX, channel, False)
        device.setSampleRate(SoapySDR.SOAPY_SDR_RX, channel, sample_rate)
        data_format = SoapySDR.SOAPY_SDR_CS8 # if 'rtlsdr' in soapy_device or 'hackrf' in soapy_device else SoapySDR.SOAPY_SDR_CS16
        stream = device.setupStream(SoapySDR.SOAPY_SDR_RX, data_format, [channel], {})
        device.activateStream(stream)
        block_size = device.getStreamMTU(stream)
        print("Block size:", block_size)
        print("Data format:", data_format)
        print()
        # IQ: 2 digits ver variable
        buffer_format = np.int8
        buffer_size = 2 * block_size # I + Q samples
        buffer = np.empty(buffer_size, buffer_format)
        # Number of blocks to save
        block, max_blocks = 0, blocks_count
        # Save to file
        file_name = "HDSDR_%s_%dkHz_RF.wav" % (datetime.datetime.utcnow().strftime("%Y%m%d_%H%M%SZ"), frequency/1000)
        print("Saving file:", file_name)
        with open(file_name, "wb") as wav:
            # Wav data info
            bits_per_sample = 16
            channels_num, samples_num = 2, int(max_blocks * block_size)
            subchunk_size = 16  # always 16 for PCM
            subchunk2_size = int(samples_num * channels_num * bits_per_sample / 8)
            block_alignment = int(channels_num * bits_per_sample / 8)
            # Write RIFF header
            wav.write('RIFF'.encode('utf-8'))
            wav.write(struct.pack(' 0:
                    data = buffer[0:2*d_info.ret]
                    fileData = data
                    if data_format == SoapySDR.SOAPY_SDR_CS8:
                       fileData = data.astype('int16')
                    wav.write(fileData)
                    print("Block %d saved: %d bytes" % (block, 2*d_info.ret))
                    block += 1
                    if block > max_blocks:
                        break
        device.deactivateStream(stream)
        device.closeStream(stream)
    if __name__ == "__main__":
        print("App started")
        # Forecast for active NOAA satellites
        # NOAA 15: 137.620, https://www.n2yo.com/passes/?s=25338
        # NOAA 18: 137.9125, https://www.n2yo.com/passes/?s=28654
        # NOAA 19: 137.100, https://www.n2yo.com/passes/?s=33591
        # Wait for the start: 18-May 21:49 21:49:
        wait_for_start(datetime.datetime(2019, 5, 18, 21, 49, 0))
        device = sdr_init()
        t_start = time.time()
        sdr_record(device, frequency=137912500, sample_rate=250000, gain=35, blocks_count=2100)
        print("Recording complete, time = %ds" % int(time.time() - t_start))
        print()
    


    SoapySDR plus is that the same program with minimal changes will work with other receivers, for example with SDRPlay or HackRF. Well, about cross-platform, too, has already been mentioned.

    If readers still have an interest in the topic of radio reception, you can consider an example of using SDR with GNU Radio by creating several virtual receivers based on one hardware.

    Also popular now: