GRAndWalkthroughUSRP

From GNU Radio
Jump to navigation Jump to search

Building a GNU Radio for Android App with a USRP

In this walk-through, we will add a connection to a USRP radio over USB. This will require a USRP B2xx (B200, B210, or B200mini) as these are the only ones fully tested. We will also need an OTG (USB On-the-Go) cable, which plugs in to the USB port of the Android device and the standard USB cable plugs into the USRP and the other end of the OTG cable.

android_usrp_connection-annotated.png

See the main Android page for a list of devices that have been tested and work with USRPs.

I suspect that network-connected USRPs will work pretty easily with Android. The biggest challenge with the B2xx series of USRPs is that they must have their firmware and FPGA images programmed in every time they are powered on. A network-connected USRP like an N200/N210 or X310 is pre-programmed and does not require the same workarounds for permissions issues that we face with USB devices. The only permission for a networked USRP is the "android.permission.INTERNET", which we have been using, anyways, for access to ControlPort.

RTL-SDR devices can also work with GNU Radio for Android apps. Like the USRP B2xx devices, these need the USB permissions workaround, but they do not need to be programmed. We build the RTL-SDR library as well as gr-osmosdr support for them into our GR for Android dependency bundle. I suspect, too, that adding support for other USB-based devices like the BladeRF, HackRF, Airspy, etc. will also be fairly straight-forward.

You can find the full example GrTemplateUSRP project on Github.

Getting Started

We will build a new application here, much like we did in the other two walk-throughs. To start with, follow the procedure outlined in GRAndWalkthroughCP to create a new app project space, but this time, call it GrTemplateUSRP. This sets us up with a project that has ControlPort support and a basic flowgraph in fg.cpp. Before you go any farther, make sure the application builds and runs on your device just like GrTemplateCP did.

Second, clone and build the GrHardwareService application. Install this app onto your Android device. If you don't have a USRP plugged in when this first starts, it will just crash (not particularly nice, I know, but we'll get there). With this loaded onto your machine, it installs the USRP firmware and FPGA images for the B2xx devices and starts listening for USB connections. And USB device that's plugged in where the vendor and produce IDs match any of the known-working USRPs, this app is launched and starts programming the device. It will have to ask you for permission to use the device twice, once to program the firmware and then again to program the FPGA image. Once that's done, the hardware is programmed and ready to go.

If the program crashes on you when you first plug in a USRP, it's likely a permissions issue. I've noticed that on Marshmallow (Android version 6), I had to go into the App section under Settings, find the GrHardwareService app, go in there to Permissions, and enable the Storage permission. This is a new "feature" in Marshmallow, and there are standard ways to handle it inside of the app. I'm not worrying about that here because I don't want to confuse the code any more than we have to by having version-specific checks.

Adding Support for USB Devices

We have to play some games with Android to get information in the Java app and pass this to the C++ flowgraph. Android doesn't like to hand out USB permissions to native apps, so we need to get the file descriptor (fd) and USB filesystem path (usrpfs_path) in Java and pass that through GNU Radio to libUHD and eventually libUSB to allow us access.

Here is another area where I am hoping we can eventually figure this out better and simplify the procedure. Also, I do not take great care here to add much fault protection, so when crashes occur, they often just shut down the app with an error. I am sure anyone competent with Android can do a better job than I can here.

Next, we need to add some support for getting USB permissions and finding the fd and usbfs_path information we require to connect to the USRP.

First, add these variables to your MainActivity class:

    public static Intent intent = null;
    private String usbfs_path = null;
    private int fd = -1;
    private static final String ACTION_USB_PERMISSION = "com.android.example.USB_PERMISSION";
    private static final String DEFAULT_USBFS_PATH = "/dev/bus/usb";
    private UsbManager mUsbManager;
    private UsbDevice mUsbDevice;

We need to make sure we have the USB permissions set before we can launch the radio. So instead of doing everything in OnCreate like we've been doing, we'll separate into a few different functions. First, in OnCreate, we need to get the USB device, set up a broadcast receiver, and then ask the user for permission to use the device. When the user grants permission, the broadcast receiver is launched. The broadcast receiver finishes up getting access to the USB device, and now we can act on the USB device to get the information out of it we need. This is what our OnCreate function looks like now:

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        // Create the seekBar to move update the amplitude
        SeekBar ampSeekBar = (SeekBar) findViewById(R.id.seekBar);
        ampSeekBar.setMax(100);      // max value -> 1.0
        ampSeekBar.setProgress(50);  // match 0.5 starting value
        ampSeekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
            @Override
            public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
                Double amp = progress / 100.0; // rescale by 100
                RPCConnection.KnobInfo _k =
                        new RPCConnection.KnobInfo(mult_knob_name, amp,
                                BaseTypes.DOUBLE);
                HashMap _map = new HashMap<>();
                _map.put(mult_knob_name, _k);
                postSetKnobMessage(_map);
            }

            @Override
            public void onStartTrackingTouch(SeekBar seekBar) {
            }

            @Override
            public void onStopTrackingTouch(SeekBar seekBar) {
            }
        });

        // Start setting up for USB permission request
        intent = getIntent();
        mUsbManager = (UsbManager) getSystemService(Context.USB_SERVICE);
        IntentFilter filter = new IntentFilter(ACTION_USB_PERMISSION);
        registerReceiver(mUsbReceiver, filter);
        mUsbDevice = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE);
        if (mUsbDevice == null) {
            Log.d("GrTemplateUSRP", "Didn't get a device; finding it now.");
            final HashSet allowed_devices = getAllowedDevices(this);
            final HashMap usb_device_list = mUsbManager.getDeviceList();
            for (UsbDevice candidate : usb_device_list.values()) {
                String candstr = "v" + candidate.getVendorId() + "p" + candidate.getProductId();
                if (allowed_devices.contains(candstr)) {
                    // Need to handle case where we have more than one device connected
                    mUsbDevice = candidate;
                }
            }
        }
        Log.d("GrTemplateUSRP", "Selected Device: " + mUsbDevice);
        PendingIntent permissionIntent = PendingIntent.getBroadcast(this, 0,
                new Intent(ACTION_USB_PERMISSION), 0);

        // Launch dialog to ask for permission.
        // If use hits OK, the broadcast receiver will be launched.
        mUsbManager.requestPermission(mUsbDevice, permissionIntent);
    }

Notice that we're still creating the Seek Bar here as the GUI setup.

We are also calling a new function here called getAllowedDevices. This function parses our XML file of devices that we want to be able to talk to. So the broadcast receiver is only fired if one of the devices listed in this file, based on the vendor and product ID, is seen.

We need to make our device_filter.xml file. In the res directory, add a new directory called xml. In the xml directory, create a new file called device_filter.xml. This file looks like:

<resources>
    <usb-device vendor-id="3034" product-id="10296" />  <!-- RTL-SDR -->
    <usb-device vendor-id="9472" product-id="2" />      <!-- USRP B100 -->
    <usb-device vendor-id="9472" product-id="32" />     <!-- USRP B200/B210 -->
    <usb-device vendor-id="9472" product-id="33" />     <!-- USRP B200/B210 -->
    <usb-device vendor-id="9472" product-id="34" />     <!-- USRP B200mini -->
    <usb-device vendor-id="14627" product-id="30739" /> <!-- NI B200/B210 -->
    <usb-device vendor-id="14627" product-id="30740" /> <!-- NI B200/B210 -->
</resources>

We may have to add more devices here for other hardware support in the future.

And this is what getAllowedDevices looks like:

    private static HashSet getAllowedDevices(final Context ctx) {
        final HashSet ans = new HashSet();
        try {
            final XmlResourceParser xml = ctx.getResources().getXml(R.xml.device_filter);

            xml.next();
            int eventType;
            while ((eventType = xml.getEventType()) != XmlPullParser.END_DOCUMENT) {

                switch (eventType) {
                    case XmlPullParser.START_TAG:
                        if (xml.getName().equals("usb-device")) {
                            final AttributeSet as = Xml.asAttributeSet(xml);
                            final Integer vendorId = Integer.valueOf(as.getAttributeValue(null, "vendor-id"), 10);
                            final Integer productId = Integer.valueOf(as.getAttributeValue(null, "product-id"), 10);
                            ans.add("v" + vendorId + "p" + productId);
                        }
                        break;
                }
                xml.next();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }

        return ans;
    }

The broadcast receiver (See the main Android USB Page for more in this) that we registered, mUsbReceiver, looks like this:

    private final BroadcastReceiver mUsbReceiver = new BroadcastReceiver() {
        public void onReceive(Context context, Intent intent) {
            String action = intent.getAction();
            if (ACTION_USB_PERMISSION.equals(action)) {
                synchronized (this) {
                    UsbDevice device = (UsbDevice)intent.getParcelableExtra(UsbManager.EXTRA_DEVICE);

                    if (intent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED, false)) {
                        if(device != null){
                            mUsbDevice = device;
                            SetupUSB();
                        }
                    }
                    else {
                        Log.d("GrTemplateUSRP", "Permission denied for device " + device);
                    }
                }
            }
        }
    };

If the device registers and looks good, we assign it to our class object mUsbDevice and launch the next stage of starting up our application by calling SetupUSB.

    private void SetupUSB() {
        final UsbDeviceConnection connection = mUsbManager.openDevice(mUsbDevice);

        if (connection != null) {
            fd = connection.getFileDescriptor();
        } else {
            Log.d("GrTemplateUSRP", "Didn't get a USB Device Connection");
            finish();
        }

        if (mUsbDevice != null) {
            usbfs_path = properDeviceName(mUsbDevice.getDeviceName());
        } else {
            Log.d("GrTemplateUSRP", "Didn't get a USB Device");
            finish();
        }

        int vid = mUsbDevice.getVendorId();
        int pid = mUsbDevice.getProductId();

        Log.d("GrTemplateUSRP", "Found fd: " + fd + "  usbfs_path: " + usbfs_path);
        Log.d("GrTemplateUSRP", "Found vid: " + vid + "  pid: " + pid);

        StartRadio();
    }

This function gets information from the USB device, prints it to logcat, and then calls StartRadio. We use a parsing function called properDeviceName to extract the USB device path from the device itself:

    public final static String properDeviceName(String deviceName) {
        if (deviceName == null) return DEFAULT_USBFS_PATH;
        deviceName = deviceName.trim();
        if (deviceName.isEmpty()) return DEFAULT_USBFS_PATH;

        final String[] paths = deviceName.split("/");
        final StringBuilder sb = new StringBuilder();
        for (int i = 0; i < paths.length - 2; i++)
            if (i == 0)
                sb.append(paths[i]);
            else
                sb.append("/").append(paths[i]);
        final String stripped_name = sb.toString().trim();
        if (stripped_name.isEmpty())
            return DEFAULT_USBFS_PATH;
        else
            return stripped_name;
    }

This always seems to be "/dev/bus/usb", but we get it here each time in case something else changes.

Finally, we call StartRadio, which is really the rest of the OnCreate stuff we have done previously, but now we're waiting until the USB device is properly assigned.

When we build and run this on the device with a USRP plugged in, a dialog box pops up asking if we want to grant permission to the USB device. Click OK, but do not click the check box. We don't want this app to be associated by default with USRPs since we want that to be done by the GrHardwareService app. After clicking OK, we should see the log messages in logcat that give us the fd, usbfs_path, and the vid and pid of the discovered device. This makes sure that we are properly talking to the USRP in Java. We'll then pass this information to the flowgraph to create a UHD USRP source block. You should see something like this in logcat:

04-22 11:30:01.624 14537 14537 D GrTemplateUSRP: Didn't get a device; finding it now.
04-22 11:30:01.626 14537 14537 D GrTemplateUSRP: Selected Device: UsbDevice[mName=/dev/bus/usb/002/003,mVendorId=9472,mProductId=34,mClass=255,mSubclass=0,mProtocol=0,mManufacturerName=Ettus Research LLC,mProductName=USRP B200,mVersion=2.16,mSerialNumber=30BFC51,mConfigurations=[
04-22 11:30:01.626 14537 14537 D GrTemplateUSRP: UsbConfiguration[mId=1,mName=null,mAttributes=128,mMaxPower=1,mInterfaces=[
04-22 11:30:01.626 14537 14537 D GrTemplateUSRP: UsbInterface[mId=0,mAlternateSetting=0,mName=USRP B200,mClass=255,mSubclass=0,mProtocol=0,mEndpoints=[]
04-22 11:30:01.626 14537 14537 D GrTemplateUSRP: UsbInterface[mId=1,mAlternateSetting=0,mName=USRP B200,mClass=255,mSubclass=0,mProtocol=0,mEndpoints=[
04-22 11:30:01.626 14537 14537 D GrTemplateUSRP: UsbEndpoint[mAddress=2,mAttributes=2,mMaxPacketSize=512,mInterval=0]]
04-22 11:30:01.626 14537 14537 D GrTemplateUSRP: UsbInterface[mId=2,mAlternateSetting=0,mName=USRP B200,mClass=255,mSubclass=0,mProtocol=0,mEndpoints=[
04-22 11:30:01.626 14537 14537 D GrTemplateUSRP: UsbEndpoint[mAddress=134,mAttributes=2,mMaxPacketSize=512,mInterval=0]]
04-22 11:30:01.626 14537 14537 D GrTemplateUSRP: UsbInterface[mId=3,mAlternateSetting=0,mName=USRP B200,mClass=255,mSubclass=0,mProtocol=0,mEndpoints=[
04-22 11:30:01.626 14537 14537 D GrTemplateUSRP: UsbEndpoint[mAddress=4,mAttributes=2,mMaxPacketSize=512,mInterval=0]]
04-22 11:30:01.626 14537 14537 D GrTemplateUSRP: UsbInterface[mId=4,mAlternateSetting=0,mName=USRP B200,mClass=255,mSubclass=0,mProtocol=0,mEndpoints=[
04-22 11:30:01.626 14537 14537 D GrTemplateUSRP: UsbEndpoint[mAddress=136,mAttributes=2,mMaxPacketSize=512,mInterval=0]]]]
04-22 11:30:01.626 14537 14537 D GrTemplateUSRP: Called Request Permission
04-22 11:30:01.627 14537 14537 D GrTemplateUSRP: Found fd: 22  usbfs_path: /dev/bus/usb
04-22 11:30:01.627 14537 14537 D GrTemplateUSRP: Found vid: 9472  pid: 34

Since we're launching our flowgraph from GRAndWalkthroughCP, we'll also see the flowgraph start messages, though we still haven't done anything with the USRP, yet.

Creating a UHD USRP Source

Now we have the permissions and all of the information we need to talk to the USRP. Now its time to change the flowgraph to use a UHD USRP Source block as the source instead of the sig_source_f.

First, we need to pass the fd and usrpfs_path values to the flowgraph. Change the function signature in fg.cpp to this:

JNIEXPORT void JNICALL
Java_org_gnuradio_grtemplateusrp_MainActivity_FgInit(JNIEnv* env,
                                                     jobject thiz,
                                                     int fd, jstring devname)

And in MainActivity.java where we declare the JNI functions, we need to add the two arguments:

public native void FgInit(int fd, String usbfs_path);

And in SetupRadio, make sure to pass this information as "FgInit(fd, usbfs_path)".

We then extract the information and format a device argument string for the usrp_source block:

#include <gnuradio/uhd/usrp_source.h>

....

  const char *usbfs_path = env->GetStringUTFChars(devname, NULL);
  std::stringstream args;
  args << "uhd,fd=" << fd << ",usbfs_path=" << usbfs_path;
  GR_INFO("fg", boost::str(boost::format("Using UHD args=%1%") % args.str()));

  uhd::stream_args_t stream_args;
  stream_args.cpu_format = "fc32";
  stream_args.otw_format = "sc16";

  ....

  gr::uhd::usrp_source::sptr src;

  ....

  src = gr::uhd::usrp_source::make(args.str(), stream_args);

  src->set_samp_rate(200e3);
  src->set_center_freq(101.1e6);
  src->set_gain(20); // adjust as needed

The src is now a complex source. So what I'll do is create a simple flowgraph that looks like:

usrp_source -> complex_to_real -> multiply_const_ff -> opensl_sink

The full flowgraph FgInit looks like this

// Get any GNU Radio headers
#include <gnuradio/top_block.h>
#include <gnuradio/uhd/usrp_source.h>
#include <gnuradio/blocks/complex_to_real.h>
#include <gnuradio/blocks/multiply_const_ff.h>
#include <grand/opensl_sink.h> 

// Declare the global virtual machine and top-block objects
JavaVM *vm;
gr::top_block_sptr tb;

extern "C" {

JNIEXPORT void JNICALL
Java_org_gnuradio_grtemplateusrp_MainActivity_FgInit(JNIEnv* env,
                                                     jobject thiz,
                                                     int fd, jstring devname)
{
  GR_INFO("fg", "FgInit Called");

  const char *usbfs_path = env->GetStringUTFChars(devname, NULL);
  std::stringstream args;
  args << "uhd,fd=" << fd << ",usbfs_path=" << usbfs_path;
  GR_INFO("fg", boost::str(boost::format("Using UHD args=%1%") % args.str()));

  uhd::stream_args_t stream_args;
  stream_args.cpu_format = "fc32";
  stream_args.otw_format = "sc16";

  float samp_rate = 48e3;  // 48 kHz

  // Declare our GNU Radio blocks
  gr::uhd::usrp_source::sptr src;
  gr::blocks::complex_to_real::sptr c2r;
  gr::blocks::multiply_const_ff::sptr mult;
  gr::grand::opensl_sink::sptr snk;

  // Construct the objects for every block in the flowgraph
  tb = gr::make_top_block("fg");
  src = gr::uhd::usrp_source::make(args.str(), stream_args);
  c2r = gr::blocks::complex_to_real::make();
  mult = gr::blocks::multiply_const_ff::make(0.0);
  snk = gr::grand::opensl_sink::make(int(samp_rate));

  src->set_samp_rate(200e3);
  src->set_center_freq(101.1e6);
  src->set_gain(20); // adjust as needed

  // Connect up the flowgraph
  tb->connect(src, 0, c2r, 0);
  tb->connect(c2r, 0, mult, 0);
  tb->connect(mult, 0, snk, 0);
}

....

This flowgraph does nothing particularly useful with the USRP, but we should be able to listen to static. We are not event setting up the sample rate, frequency, or gain values, so the incoming samples have no connection to anything real.

For more complex usage of a USRP app, take a look at the GrRxFM, which uses a USRP to receive and demodulate broadcast FM signals. It sets up sliders to adjust the frequency and gain as well as handles sample rates and rate changes within the flowgraph.