GRAndWalkthroughUSRP: Difference between revisions

From GNU Radio
Jump to navigation Jump to search
(Imported from Redmine)
 
(Fix formatting - Includes)
 
(7 intermediate revisions by 2 users not shown)
Line 3: Line 3:
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.
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.


[[File:android_usrp_connection-annotated.png|]]
[[File:android_usrp_connection-annotated.png|android_usrp_connection-annotated.png]]


See the main [[Android]] page for a list of devices that have been tested and work with USRPs.
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.
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.
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.
Line 19: Line 19:
Second, clone and build the [https://github.com/trondeau/GrHardwareService 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.
Second, clone and build the [https://github.com/trondeau/GrHardwareService 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 [https://developer.android.com/training/permissions/requesting.html 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.
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 [https://developer.android.com/training/permissions/requesting.html 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 ==
== Adding Support for USB Devices ==
Line 31: Line 31:
First, add these variables to your MainActivity class:
First, add these variables to your MainActivity class:


<pre>    public static Intent intent = null;
<syntaxhighlight lang="Java">    public static Intent intent = null;
     private String usbfs_path = null;
     private String usbfs_path = null;
     private int fd = -1;
     private int fd = -1;
     private static final String ACTION_USB_PERMISSION = &quot;com.android.example.USB_PERMISSION&quot;;
     private static final String ACTION_USB_PERMISSION = "com.android.example.USB_PERMISSION";
     private static final String DEFAULT_USBFS_PATH = &quot;/dev/bus/usb&quot;;
     private static final String DEFAULT_USBFS_PATH = "/dev/bus/usb";
     private UsbManager mUsbManager;
     private UsbManager mUsbManager;
     private UsbDevice mUsbDevice;</pre>
     private UsbDevice mUsbDevice;</syntaxhighlight>
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:
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:


<pre>    @Override
<syntaxhighlight lang="Java">    @Override
     protected void onCreate(Bundle savedInstanceState) {
     protected void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
         super.onCreate(savedInstanceState);
Line 47: Line 47:
         // Create the seekBar to move update the amplitude
         // Create the seekBar to move update the amplitude
         SeekBar ampSeekBar = (SeekBar) findViewById(R.id.seekBar);
         SeekBar ampSeekBar = (SeekBar) findViewById(R.id.seekBar);
         ampSeekBar.setMax(100);      // max value -&gt; 1.0
         ampSeekBar.setMax(100);      // max value -> 1.0
         ampSeekBar.setProgress(50);  // match 0.5 starting value
         ampSeekBar.setProgress(50);  // match 0.5 starting value
         ampSeekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
         ampSeekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
Line 56: Line 56:
                         new RPCConnection.KnobInfo(mult_knob_name, amp,
                         new RPCConnection.KnobInfo(mult_knob_name, amp,
                                 BaseTypes.DOUBLE);
                                 BaseTypes.DOUBLE);
                 HashMap _map = new HashMap&lt;&gt;();
                 HashMap _map = new HashMap<>();
                 _map.put(mult_knob_name, _k);
                 _map.put(mult_knob_name, _k);
                 postSetKnobMessage(_map);
                 postSetKnobMessage(_map);
Line 77: Line 77:
         mUsbDevice = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE);
         mUsbDevice = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE);
         if (mUsbDevice == null) {
         if (mUsbDevice == null) {
             Log.d(&quot;GrTemplateUSRP&quot;, &quot;Didn't get a device; finding it now.&quot;);
             Log.d("GrTemplateUSRP", "Didn't get a device; finding it now.");
             final HashSet allowed_devices = getAllowedDevices(this);
             final HashSet allowed_devices = getAllowedDevices(this);
             final HashMap usb_device_list = mUsbManager.getDeviceList();
             final HashMap usb_device_list = mUsbManager.getDeviceList();
             for (UsbDevice candidate : usb_device_list.values()) {
             for (UsbDevice candidate : usb_device_list.values()) {
                 String candstr = &quot;v&quot; + candidate.getVendorId() + &quot;p&quot; + candidate.getProductId();
                 String candstr = "v" + candidate.getVendorId() + "p" + candidate.getProductId();
                 if (allowed_devices.contains(candstr)) {
                 if (allowed_devices.contains(candstr)) {
                     // Need to handle case where we have more than one device connected
                     // Need to handle case where we have more than one device connected
Line 88: Line 88:
             }
             }
         }
         }
         Log.d(&quot;GrTemplateUSRP&quot;, &quot;Selected Device: &quot; + mUsbDevice);
         Log.d("GrTemplateUSRP", "Selected Device: " + mUsbDevice);
         PendingIntent permissionIntent = PendingIntent.getBroadcast(this, 0,
         PendingIntent permissionIntent = PendingIntent.getBroadcast(this, 0,
                 new Intent(ACTION_USB_PERMISSION), 0);
                 new Intent(ACTION_USB_PERMISSION), 0);
Line 95: Line 95:
         // If use hits OK, the broadcast receiver will be launched.
         // If use hits OK, the broadcast receiver will be launched.
         mUsbManager.requestPermission(mUsbDevice, permissionIntent);
         mUsbManager.requestPermission(mUsbDevice, permissionIntent);
     }</pre>
     }</syntaxhighlight>
Notice that we're still creating the Seek Bar here as the GUI setup.
Notice that we're still creating the Seek Bar here as the GUI setup.


Line 103: Line 103:


<pre>
<pre>
     
<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>
</pre>
</pre>
We may have to add more devices here for other hardware support in the future.
We may have to add more devices here for other hardware support in the future.
Line 115: Line 117:
And this is what '''getAllowedDevices''' looks like:
And this is what '''getAllowedDevices''' looks like:


<pre>    private static HashSet getAllowedDevices(final Context ctx) {
<syntaxhighlight lang="Java">    private static HashSet getAllowedDevices(final Context ctx) {
         final HashSet ans = new HashSet();
         final HashSet ans = new HashSet();
         try {
         try {
Line 126: Line 128:
                 switch (eventType) {
                 switch (eventType) {
                     case XmlPullParser.START_TAG:
                     case XmlPullParser.START_TAG:
                         if (xml.getName().equals(&quot;usb-device&quot;)) {
                         if (xml.getName().equals("usb-device")) {
                             final AttributeSet as = Xml.asAttributeSet(xml);
                             final AttributeSet as = Xml.asAttributeSet(xml);
                             final Integer vendorId = Integer.valueOf(as.getAttributeValue(null, &quot;vendor-id&quot;), 10);
                             final Integer vendorId = Integer.valueOf(as.getAttributeValue(null, "vendor-id"), 10);
                             final Integer productId = Integer.valueOf(as.getAttributeValue(null, &quot;product-id&quot;), 10);
                             final Integer productId = Integer.valueOf(as.getAttributeValue(null, "product-id"), 10);
                             ans.add(&quot;v&quot; + vendorId + &quot;p&quot; + productId);
                             ans.add("v" + vendorId + "p" + productId);
                         }
                         }
                         break;
                         break;
Line 141: Line 143:


         return ans;
         return ans;
     }</pre>
     }</syntaxhighlight>
The broadcast receiver (See the main [http://developer.android.com/guide/topics/connectivity/usb/host.html Android USB Page] for more in this) that we registered, '''mUsbReceiver''', looks like this:
The broadcast receiver (See the main [http://developer.android.com/guide/topics/connectivity/usb/host.html Android USB Page] for more in this) that we registered, '''mUsbReceiver''', looks like this:


<pre>
<syntaxhighlight lang="Java">
     private final BroadcastReceiver mUsbReceiver = new BroadcastReceiver() {
     private final BroadcastReceiver mUsbReceiver = new BroadcastReceiver() {
         public void onReceive(Context context, Intent intent) {
         public void onReceive(Context context, Intent intent) {
Line 159: Line 161:
                     }
                     }
                     else {
                     else {
                         Log.d(&quot;GrTemplateUSRP&quot;, &quot;Permission denied for device &quot; + device);
                         Log.d("GrTemplateUSRP", "Permission denied for device " + device);
                     }
                     }
                 }
                 }
             }
             }
         }
         }
     };</pre>
     };</syntaxhighlight>
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'''.
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'''.


<pre>    private void SetupUSB() {
<syntaxhighlight lang="Java">    private void SetupUSB() {
         final UsbDeviceConnection connection = mUsbManager.openDevice(mUsbDevice);
         final UsbDeviceConnection connection = mUsbManager.openDevice(mUsbDevice);


Line 173: Line 175:
             fd = connection.getFileDescriptor();
             fd = connection.getFileDescriptor();
         } else {
         } else {
             Log.d(&quot;GrTemplateUSRP&quot;, &quot;Didn't get a USB Device Connection&quot;);
             Log.d("GrTemplateUSRP", "Didn't get a USB Device Connection");
             finish();
             finish();
         }
         }
Line 180: Line 182:
             usbfs_path = properDeviceName(mUsbDevice.getDeviceName());
             usbfs_path = properDeviceName(mUsbDevice.getDeviceName());
         } else {
         } else {
             Log.d(&quot;GrTemplateUSRP&quot;, &quot;Didn't get a USB Device&quot;);
             Log.d("GrTemplateUSRP", "Didn't get a USB Device");
             finish();
             finish();
         }
         }
Line 187: Line 189:
         int pid = mUsbDevice.getProductId();
         int pid = mUsbDevice.getProductId();


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


         StartRadio();
         StartRadio();
     }</pre>
     }</syntaxhighlight>
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:
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:


<pre>    public final static String properDeviceName(String deviceName) {
<syntaxhighlight lang="Java">    public final static String properDeviceName(String deviceName) {
         if (deviceName == null) return DEFAULT_USBFS_PATH;
         if (deviceName == null) return DEFAULT_USBFS_PATH;
         deviceName = deviceName.trim();
         deviceName = deviceName.trim();
         if (deviceName.isEmpty()) return DEFAULT_USBFS_PATH;
         if (deviceName.isEmpty()) return DEFAULT_USBFS_PATH;


         final String[] paths = deviceName.split(&quot;/&quot;);
         final String[] paths = deviceName.split("/");
         final StringBuilder sb = new StringBuilder();
         final StringBuilder sb = new StringBuilder();
         for (int i = 0; i &lt; paths.length - 2; i++)
         for (int i = 0; i < paths.length - 2; i++)
             if (i == 0)
             if (i == 0)
                 sb.append(paths[i]);
                 sb.append(paths[i]);
             else
             else
                 sb.append(&quot;/&quot;).append(paths[i]);
                 sb.append("/").append(paths[i]);
         final String stripped_name = sb.toString().trim();
         final String stripped_name = sb.toString().trim();
         if (stripped_name.isEmpty())
         if (stripped_name.isEmpty())
Line 211: Line 213:
         else
         else
             return stripped_name;
             return stripped_name;
     }</pre>
     }</syntaxhighlight>
This always seems to be &quot;/dev/bus/usb&quot;, but we get it here each time in case something else changes.
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.
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.
Line 233: Line 235:
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 fd: 22  usbfs_path: /dev/bus/usb
04-22 11:30:01.627 14537 14537 D GrTemplateUSRP: Found vid: 9472  pid: 34</pre>
04-22 11:30:01.627 14537 14537 D GrTemplateUSRP: Found vid: 9472  pid: 34</pre>
Since we're launching our flowgraph from [[GRAndWalkthrougCP]], we'll also see the flowgraph start messages, though we still haven't done anything with the USRP, yet.
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 ==
== Creating a UHD USRP Source ==
Line 241: Line 243:
First, we need to pass the '''fd''' and '''usrpfs_path''' values to the flowgraph. Change the function signature in '''fg.cpp''' to this:
First, we need to pass the '''fd''' and '''usrpfs_path''' values to the flowgraph. Change the function signature in '''fg.cpp''' to this:


<pre>JNIEXPORT void JNICALL
<syntaxhighlight lang="Java">JNIEXPORT void JNICALL
Java_org_gnuradio_grtemplateusrp_MainActivity_FgInit(JNIEnv* env,
Java_org_gnuradio_grtemplateusrp_MainActivity_FgInit(JNIEnv* env,
                                                     jobject thiz,
                                                     jobject thiz,
                                                     int fd, jstring devname)</pre>
                                                     int fd, jstring devname)</syntaxhighlight>
And in '''MainActivity.java''' where we declare the JNI functions, we need to add the two arguments:
And in '''MainActivity.java''' where we declare the JNI functions, we need to add the two arguments:


<pre>public native void FgInit(int fd, String usbfs_path);</pre>
<syntaxhighlight lang="Java">public native void FgInit(int fd, String usbfs_path);</syntaxhighlight>
And in '''SetupRadio''', make sure to pass this information as &quot;FgInit(fd, usbfs_path)&quot;.
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:
We then extract the information and format a device argument string for the usrp_source block:


<pre>#include  
<syntaxhighlight lang="Java">
#include <gnuradio/uhd/usrp_source.h>


....
....


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


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


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


   src-&gt;set_samp_rate(200e3);
   src->set_samp_rate(200e3);
   src-&gt;set_center_freq(101.1e6);
   src->set_center_freq(101.1e6);
   src-&gt;set_gain(20); // adjust as needed</pre>
   src->set_gain(20); // adjust as needed</syntaxhighlight>
The '''src''' is now a complex source. So what I'll do is create a simple flowgraph that looks like:
The '''src''' is now a complex source. So what I'll do is create a simple flowgraph that looks like:


<pre>usrp_source -&gt; complex_to_real -&gt; multiply_const_ff -&gt; opensl_sink</pre>
<syntaxhighlight lang="Java">usrp_source -> complex_to_real -> multiply_const_ff -> opensl_sink</syntaxhighlight>
The full flowgraph '''FgInit''' looks like this
The full flowgraph '''FgInit''' looks like this


<pre>// Get any GNU Radio headers
<syntaxhighlight lang="Java">// Get any GNU Radio headers
#include  
#include <gnuradio/top_block.h>
#include  
#include <gnuradio/uhd/usrp_source.h>
#include  
#include <gnuradio/blocks/complex_to_real.h>
#include  
#include <gnuradio/blocks/multiply_const_ff.h>
#include  
#include <grand/opensl_sink.h>


// Declare the global virtual machine and top-block objects
// Declare the global virtual machine and top-block objects
Line 292: Line 295:
gr::top_block_sptr tb;
gr::top_block_sptr tb;


extern &quot;C&quot; {
extern "C" {


JNIEXPORT void JNICALL
JNIEXPORT void JNICALL
Line 299: Line 302:
                                                     int fd, jstring devname)
                                                     int fd, jstring devname)
{
{
   GR_INFO(&quot;fg&quot;, &quot;FgInit Called&quot;);
   GR_INFO("fg", "FgInit Called");


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


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


   float samp_rate = 48e3;  // 48 kHz
   float samp_rate = 48e3;  // 48 kHz
Line 319: Line 322:


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


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


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


....</pre>
....</syntaxhighlight>
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.
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 [https://github.com/trondeau/GrRxFM 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.
For more complex usage of a USRP app, take a look at the [https://github.com/trondeau/GrRxFM 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.

Latest revision as of 01:48, 21 March 2017

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.