kevinmehall / rust-soapysdr

Rust bindings for SoapySDR, the vendor-neutral software defined radio hardware abstraction layer
Apache License 2.0
75 stars 22 forks source link

soapysdr::RxStream::activate problem #2

Closed russel closed 6 years ago

russel commented 6 years ago

I am running on Debian Sid with SoapySDR 0.6.0 installed using Debian packages. If I run a variant of the SoapySDR Python bindings example the code runs fine with my NESDR Smart. If I write the same code in Rust with this binding, everything seems fine up to trying to activate the device.

If I have:

rx_stream.activate(None).unwrap();

then on execution it seems to hang. If instead I have:

rx_stream.activate(Some(1000)).unwrap();

then I get a NotSupported result. I am guessing the Rust code can't just be a rewrite of the Python code for some reasons, but I am not sure how to discover what and why.

kevinmehall commented 6 years ago

It should behave identically to the Python bindings -- it's the same code underneath. If you're able to post both versions I can take a look.

I also just pushed some recent commits that add more sample code. Try pulling the latest master and running cargo run --release --features=binaries --bin soapy-sdr-stream -- -d driver=rtlsdr -r out.cfile -f 96.5M -s 1M -n 5M to capture 5 seconds of FM radio into out.cfile. I've tested this with my rtlsdr, limesdr, and hackrf.

I don't think rtlsdr supports the activation_time argument to activate. Are you getting something other than SOAPY_SDR_NOT_SUPPORTED with that in Python?

russel commented 6 years ago

Thanks for the fast response, much appreciated.

Would it be better for me to send the codes to you by email, or would you prefer them posted in this thread?

kevinmehall commented 6 years ago

Easiest to post here to keep the thread in one place. If it's too long for a comment, you can post it to https://gist.github.com/ and paste a link.

russel commented 6 years ago

The Python code I have is:

#!/usr/bin/env python3

import contextlib
import numpy

from SoapySDR import SOAPY_SDR_CF32, SOAPY_SDR_RX, Device

@contextlib.contextmanager
def openStream(device, channel, data_type):
    stream = device.setupStream(channel, data_type)
    yield stream
    device.closeStream(stream)

@contextlib.contextmanager
def activateStream(device, stream):
    device.activateStream(stream)
    yield
    device.deactivateStream(stream)

C_12B = 225.648e6

device_args = Device.enumerate()
for d in device_args:
    print('Device args', d)
    device = Device(d)
    print('Number of channels:', device.getNumChannels(SOAPY_SDR_RX))
    print('Antennas:', device.listAntennas(SOAPY_SDR_RX, 0))
    print('Gains:', device.listGains(SOAPY_SDR_RX, 0))
    frequency_range = device.getFrequencyRange(SOAPY_SDR_RX, 0)
    print('Frequency range:', frequency_range)
    for f in frequency_range:
        print('Frequency:', f)
    sample_rates = device.getSampleRateRange(SOAPY_SDR_RX, 0)
    print('Sample rates:', sample_rates)
    for s in sample_rates:
        print('Sample rate:', s)
    device.setFrequency(SOAPY_SDR_RX, 0, C_12B)
    device.setSampleRate(SOAPY_SDR_RX, 0, sample_rates[0].minimum())
    print('Bandwidth range:', device.getBandwidthRange(SOAPY_SDR_RX, 0))
    print('Stream formats', device.getStreamFormats(SOAPY_SDR_RX, 0))
    with openStream(device, SOAPY_SDR_RX, SOAPY_SDR_CF32) as rxStream:
        buffer_size = device.getStreamMTU(rxStream)
        print('Buffer size:', buffer_size)
        buffer = numpy.array([0]*buffer_size, numpy.complex64)
        with activateStream(device, rxStream):
            for i in range(10):
                sr = device.readStream(rxStream, [buffer], len(buffer))
                print('Return code:', sr.ret)
                if sr.ret < 0:
                    assert sr.flags == 0
                    assert sr.timeNs == 0
                else:
                    print('\tFlags:', sr.flags)
                    print('\tTimestamp:', sr.timeNs)

when executed with the NESDR Smart connected I get:

Detached kernel driver
Found Rafael Micro R820T tuner
Reattached kernel driver
Device args {available=Yes, driver=rtlsdr, label=Generic RTL2832U OEM :: 00000001, manufacturer=Realtek, product=RTL2838UHIDIR, rtl=0, serial=00000001, tuner=Rafael Micro R820T}
Detached kernel driver
Found Rafael Micro R820T tuner
Number of channels: 1
Antennas: ('RX',)
Gains: ('TUNER',)
Frequency range: (<SoapySDR.Range; proxy of <Swig Object of type 'SoapySDR::Range *' at 0x7fec569e4cc0> >,)
Frequency: 2.3999e+07, 1.764e+09
Sample rates: (<SoapySDR.Range; proxy of <Swig Object of type 'SoapySDR::Range *' at 0x7fec569e4cf0> >, <SoapySDR.Range; proxy of <Swig Object of type 'SoapySDR::Range *' at 0x7fec569e4d20> >, <SoapySDR.Range; proxy of <Swig Object of type 'SoapySDR::Range *' at 0x7fec569e4d50> >, <SoapySDR.Range; proxy of <Swig Object of type 'SoapySDR::Range *' at 0x7fec569e4d80> >, <SoapySDR.Range; proxy of <Swig Object of type 'SoapySDR::Range *' at 0x7fec569e4db0> >, <SoapySDR.Range; proxy of <Swig Object of type 'SoapySDR::Range *' at 0x7fec569e4de0> >, <SoapySDR.Range; proxy of <Swig Object of type 'SoapySDR::Range *' at 0x7fec569e4e10> >, <SoapySDR.Range; proxy of <Swig Object of type 'SoapySDR::Range *' at 0x7fec569e4e40> >, <SoapySDR.Range; proxy of <Swig Object of type 'SoapySDR::Range *' at 0x7fec569e4e70> >, <SoapySDR.Range; proxy of <Swig Object of type 'SoapySDR::Range *' at 0x7fec569e4ea0> >)
Sample rate: 250000, 250000
Sample rate: 1.024e+06, 1.024e+06
Sample rate: 1.536e+06, 1.536e+06
Sample rate: 1.792e+06, 1.792e+06
Sample rate: 1.92e+06, 1.92e+06
Sample rate: 2.048e+06, 2.048e+06
Sample rate: 2.16e+06, 2.16e+06
Sample rate: 2.56e+06, 2.56e+06
Sample rate: 2.88e+06, 2.88e+06
Sample rate: 3.2e+06, 3.2e+06
Exact sample rate is: 250000.000414 Hz
Bandwidth range: ()
Stream formats ('CS8', 'CS16', 'CF32')
[INFO] Using format CF32.
Buffer size: 131072
Return code: -1
Return code: -1
Return code: -1
Return code: -1
Return code: -1
Return code: 131072
    Flags: 0
    Timestamp: 0
Return code: -1
Return code: -1
Return code: -1
Return code: -1
Reattached kernel driver

The Rust code I have is:

extern crate soapysdr;
extern crate num_complex;

use soapysdr::{Args, Device, Direction, RxStream};

use num_complex::Complex;

fn main() {
    let C_12B = 225.648e6;
    let device_args = soapysdr::enumerate(&Args::new()).unwrap();
    for d in device_args {
        println!("Device args: {}", &d);
        let device = Device::new(&d).unwrap();
        println!("Number of channels: {}", device.num_channels(Direction::Rx).unwrap());
        println!("Antennas: {:?}", device.antennas(Direction::Rx, 0).unwrap());
        println!("Gains: {:?}", device.list_gains(Direction::Rx, 0).unwrap());
        let frequency_range = device.frequency_range(Direction::Rx, 0).unwrap();
        println!("Frequency range: {:?}", frequency_range);
        for f in frequency_range {
            println!("Frequency: {:?}", f);
        }
        // Documentation says:
        //let sample_rates = device.list_sample_rates(soapysdr::Direction::Rx, 0).unwrap();
        // Code says:
        let sample_rates = device.get_sample_rate_range(Direction::Rx, 0).unwrap();
        println!("Sample rates: {:?}", sample_rates);
        // Set frequency before sample rate to avoid "PLL not locked!"
        device.set_frequency(Direction::Rx, 0, C_12B, &Args::new()).unwrap();
        device.set_sample_rate(Direction::Rx, 0, sample_rates[0].minimum).unwrap();
        let bandwidth_range = device.bandwidth_range(Direction::Rx, 0).unwrap();
        println!("Bandwidth range: {:?}", bandwidth_range);
        let stream_formats = device.stream_formats(Direction::Rx, 0).unwrap();
        println!("Stream formats: {:?}", stream_formats);
        let mut rx_stream: RxStream<Complex<f32>> = device.rx_stream(&[0], &Args::new()).unwrap();
        let buffer_size = rx_stream.mtu().unwrap();
        println!("Buffer size: {}", buffer_size);
        rx_stream.activate(None).unwrap();
        //let mut buffer = [Complex{re: 0.0, im: 0.0}; 200000];
        //let read_count = rx_stream.read(&[&mut buffer], 1000).unwrap();
        //println!("Read count: {}", read_count);
        //rx_stream.deactivate(Some(1000)).unwrap();
    }
}

and when run I get:

Detached kernel driver
Found Rafael Micro R820T tuner
Reattached kernel driver
Device args: available=Yes, driver=rtlsdr, label=Generic RTL2832U OEM :: 00000001, manufacturer=Realtek, product=RTL2838UHIDIR, rtl=0, serial=00000001, tuner=Rafael Micro R820T
Detached kernel driver
Found Rafael Micro R820T tuner
Number of channels: 1
Antennas: ["RX"]
Gains: ["TUNER"]
Frequency range: [SoapySDRRange { minimum: 23999000, maximum: 1764001000, step: 0 }]
Frequency: SoapySDRRange { minimum: 23999000, maximum: 1764001000, step: 0 }
Sample rates: [SoapySDRRange { minimum: 250000, maximum: 250000, step: 0 }, SoapySDRRange { minimum: 1024000, maximum: 1024000, step: 0 }, SoapySDRRange { minimum: 1536000, maximum: 1536000, step: 0 }, SoapySDRRange { minimum: 1792000, maximum: 1792000, step: 0 }, SoapySDRRange { minimum: 1920000, maximum: 1920000, step: 0 }, SoapySDRRange { minimum: 2048000, maximum: 2048000, step: 0 }, SoapySDRRange { minimum: 2160000, maximum: 2160000, step: 0 }, SoapySDRRange { minimum: 2560000, maximum: 2560000, step: 0 }, SoapySDRRange { minimum: 2880000, maximum: 2880000, step: 0 }, SoapySDRRange { minimum: 3200000, maximum: 3200000, step: 0 }]
Exact sample rate is: 250000.000414 Hz
Bandwidth range: []
Stream formats: [Format("CS8"), Format("CS16"), Format("CF32")]
[INFO] Using format CF32.
Buffer size: 131072

and it blocks there I am guessing forever, but eventually you get bored waiting and Ctrl+C :-)

kevinmehall commented 6 years ago

I was able to reproduce this, and it looks like a race condition in librtlsdr that makes it fail to cancel streaming when you activate and then immediately deactivate the stream.

If you add a println!() after rx_stream.activate(None).unwrap();, you'll notice that it's not actually the activate call that is hanging, but rather when rx_stream goes out of scope, its Drop impl deactivates the stream and that's what hangs.

The main thread is blocked here waiting for the streaming thread to exit after calling rtlsdr_cancel_async. The streaming thread is blocked here inside rtlsdr_read_async.

I suspect what's happening is that this line in rtlsdr_cancel_async testing RTLSDR_RUNNING == dev->async_status runs on the main thread before this line on the streaming thread sets dev->async_status = RTLSDR_RUNNING;, so the rtlsdr_cancel_async is ignored and streaming never stops.

Doesn't look like an issue with rust-soapysdr; unfortunately most of the drivers behind SoapySDR have thread safety issues so a deadlock is about the least-bad outcome you're going to get from doing something weird with it (this applies to the Python and C++ bindings as well).

russel commented 6 years ago

I guess the question then is why does the Python version work if the Rust version doesn't?

I'll tinker and find a way of making this work.

Thanks for looking into this, much appreciated.

kevinmehall commented 6 years ago

Your python code is reading samples, which blocks until the thread has started. If I uncomment the read in your Rust code and increase its timeout from 1ms to 1s, that's working for me. Have you had any problem with rust-soapysdr when you read samples from the stream before the drop deactivates it?

I haven't tested removing the read from the Python code, but Python might just be too slow to hit it. The window is only the time it takes for a thread to get started. If I print a few extra lines there in Rust the program exits successfully. So if you need a dirty workaround to activate and deactivate without reading any samples for some reason, sleeping for a few ms should work.

russel commented 6 years ago

I hadn't thought I would need to set the read timeout as high as 1s, but yes it does now work. :-)

rx_stream.activate(None).unwrap();
let mut buffer = [Complex{re: 0.0, im: 0.0}; 200000];
for i in 0..10 {
    let read_count = rx_stream.read(&[&mut buffer], 1000000).unwrap();
    println!("Read count: {}", read_count);
}
rx_stream.deactivate(None).unwrap();

works fine but anything less than 1000000 means the panic happens. But I am good to go. Thanks again for your help.

kevinmehall commented 6 years ago

It's reading 200k samples captured at 250kHz, so yes, it should take almost 1s.

russel commented 6 years ago

Aha, of course. Now begins the process of choosing the right balance to pipe into decoding. Technically I am a beginner at this, but there is DABtools, WelleIO, libsdrdab, Qt_DAB, etc. to get inspiration and direction.