GRAndWalkthroughCP: Difference between revisions

From GNU Radio
Jump to navigation Jump to search
Line 203: Line 203:
Below, I show what the handler class looks like. This handler is specifically created to call the setKnobs ControlPort interface to pass a new set of knobs to the flowgraph.
Below, I show what the handler class looks like. This handler is specifically created to call the setKnobs ControlPort interface to pass a new set of knobs to the flowgraph.


<pre>   private static class SetKnobsHandler extends Handler {
<syntaxhighlight lang="Java">
    private static class SetKnobsHandler extends Handler {
         private RPCConnection mConnection;
         private RPCConnection mConnection;
         public SetKnobsHandler(RPCConnection conn) {
         public SetKnobsHandler(RPCConnection conn) {
Line 213: Line 214:
             Bundle b = m.getData();
             Bundle b = m.getData();
             if(b != null) {
             if(b != null) {
                 HashMap k =
                 HashMap<String, RPCConnection.KnobInfo> k =
                         (HashMap) b.getSerializable(&quot;Knobs&quot;);
                         (HashMap<String, RPCConnection.KnobInfo>) b.getSerializable("Knobs");


                 Log.d(&quot;MainActivity&quot;, &quot;Set Knobs: &quot; + k);
                 Log.d("MainActivity", "Set Knobs: " + k);
                 if((k != null) &amp;&amp; (!k.isEmpty())) {
                 if((k != null) && (!k.isEmpty())) {
                     mConnection.setKnobs(k);
                     mConnection.setKnobs(k);
                 }
                 }
             }
             }
         }
         }
     }</pre>
     }
</syntaxhighlight>
The Runnable class that manages the life cycle of the network thread is next:
The Runnable class that manages the life cycle of the network thread is next:


<pre>   public class RunNetworkThread implements Runnable {
<syntaxhighlight lang="Java">
public class RunNetworkThread implements Runnable {


         private RPCConnection mConnection;
         private RPCConnection mConnection;
Line 252: Line 255:
         public RPCConnection getConnection() {
         public RPCConnection getConnection() {
             if(mConnection == null) {
             if(mConnection == null) {
                 throw new IllegalStateException(&quot;connection not established&quot;);
                 throw new IllegalStateException("connection not established");
             }
             }
             return mConnection;
             return mConnection;
Line 260: Line 263:
             return mHandler;
             return mHandler;
         }
         }
     }</pre>
     }
</syntaxhighlight>
I then create a function designed to pass the knob information to the handler of the runnable thread:
I then create a function designed to pass the knob information to the handler of the runnable thread:


<pre>    private void postSetKnobMessage(HashMap knobs) {
<syntaxhighlight lang="Java">     
private void postSetKnobMessage(HashMap<String, RPCConnection.KnobInfo> knobs) {
         Handler h = mControlPortThread.getHandler();
         Handler h = mControlPortThread.getHandler();
         Bundle b = new Bundle();
         Bundle b = new Bundle();
         b.putSerializable(&quot;Knobs&quot;, knobs);
         b.putSerializable("Knobs", knobs);
         Message m = h.obtainMessage();
         Message m = h.obtainMessage();
         m.setData(b);
         m.setData(b);
         h.sendMessage(m);
         h.sendMessage(m);
     }</pre>
     }</syntaxhighlight>
Here, I am taking a String:KnobInfo map that we'll create in our activity, bundles it up into a serialized object called &quot;Knobs&quot; and then passes this as a message to the handler class to actually send this message on to ControlPort. Let's then set up the thread and ControlPort connection management in our OnCreate function of MainActivity. We'll need a public object for the RunNetworkThread class, since we directly refer to it in our postSetKnobMessage function above. Below is the first part of our new OnCreate function, just before we actually try to use the network thread. It's important that the RunNetworkThread is created <s>after</s> '''FgStart''' is called to make sure that the ControlPort endpoint already exists and is waiting for a connection.
Here, I am taking a String:KnobInfo map that we'll create in our activity, bundles it up into a serialized object called &quot;Knobs&quot; and then passes this as a message to the handler class to actually send this message on to ControlPort. Let's then set up the thread and ControlPort connection management in our OnCreate function of MainActivity. We'll need a public object for the RunNetworkThread class, since we directly refer to it in our postSetKnobMessage function above. Below is the first part of our new OnCreate function, just before we actually try to use the network thread. It's important that the RunNetworkThread is created <s>after</s> '''FgStart''' is called to make sure that the ControlPort endpoint already exists and is waiting for a connection.


<pre>public RunNetworkThread mControlPortThread;
<syntaxhighlight lang="Java">
public RunNetworkThread mControlPortThread;


     @Override
     @Override
Line 286: Line 292:


         // Make the ControlPort connection in the network thread
         // Make the ControlPort connection in the network thread
         mControlPortThread = new RunNetworkThread(&quot;localhost&quot;, 9090);
         mControlPortThread = new RunNetworkThread("localhost", 9090);
         Executor executor = Executors.newSingleThreadExecutor();
         Executor executor = Executors.newSingleThreadExecutor();
         executor.execute(mControlPortThread);
         executor.execute(mControlPortThread);
Line 304: Line 310:
         }
         }


         ....</pre>
         ....</syntaxhighlight>
Now, we have to format the KnobInfo to send the right knob information through ControlPort. We have to create the KnobInfo object, which is a class that we get from the GrControlPort library. We need to know the interface name of the knob we are trying to set. In a real application, we would use ControlPort to get the information from the flowgraph. But for our purposes, we just know that this is &quot;multiply_const_ff0::coefficient&quot;. We then need a new value to pass as well as the data type of the data. Let's just initialize the value of the multiplier to 0.5, so that's easy to plug in here. The data type <s>must</s> be one of the BaseType's (a class exported from GrControlPort). In this case, it's BaseType.DOUBLE.
Now, we have to format the KnobInfo to send the right knob information through ControlPort. We have to create the KnobInfo object, which is a class that we get from the GrControlPort library. We need to know the interface name of the knob we are trying to set. In a real application, we would use ControlPort to get the information from the flowgraph. But for our purposes, we just know that this is &quot;multiply_const_ff0::coefficient&quot;. We then need a new value to pass as well as the data type of the data. Let's just initialize the value of the multiplier to 0.5, so that's easy to plug in here. The data type <s>must</s> be one of the BaseType's (a class exported from GrControlPort). In this case, it's BaseType.DOUBLE.



Revision as of 17:21, 18 March 2017

Building a GNU Radio for Android App with ControlPort

We use ControlPort to communicate commands and control information between the Java interface of the application and the running GNU Radio flowgraph. This walk-through goes over how to use the GrControlPort Android library to allow us to set up the ControlPort connection.

We will use a multiply_const_ff block to change the amplitude of a sine wave and output this to the opensl_sink to hear the tone. This flowgraph simply looks like:

sig_source_f -> multiply_const_ff -> opensl_sink.

This walk-through builds the project GrTemplateCP that can be clone from the linked github repo.

Get GrControlPort

We first need to get the ControlPort Android library, GrControlPort:

git clone https://github.com/trondeau/GrControlPort.git

Create a New Project in Android Studio

In Android Studio, got to File > New> New Project

I named this GrTemplateCP and made it a Phone and Tablet app at API 21; starting with an Empty Activity that I named MainActivity.

We need to perform most of the same setup tasks as we did in the first GRAndWalkthrough tutorial. I'll highlight the big parts here.

Oh, and if your MainActivity class extends "AppCompatActivity", change this to "Activity" or other things seem to screw up. I really don't understand Android and Android Studio.

Edit AndroidManifest.xml

Add permission flags for reading and writing to the external storage and Internet permissions. We aren't dealing with USB hardware here, so we can skip the intent filter and USB access.

    
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.INTERNET"/>

JNI and Flowgraph Creation

Create the jni directory and populate it with the GrAndroid.mk, Android.mk, Application.mk, fg.cpp, and settmp.cpp files from GrTemplate, the same as in GRAndWalkthrough. We will use the fg.cpp file name here, too, so nothing should need to be changed in the makefiles.

We're copying over the same fg.cpp and settmp.cpp files we had before because it lays out the basic code setup we need. However, we'll need to change the project namespace from "grtemplate" to "grtemplatecp". We're using the same MainActivity name of the main class, so we don't have to change that. The main changes required here are to set up the flowgraph. So in the section where we're including the header files, we now need:

#include <gnuradio/top_block.h>
#include <gnuradio/analog/sig_source_f.h>
#include <gnuradio/blocks/multiply_const_ff.h>
#include <grand/opensl_sink.h>

Then, in the FgInit function, we need to change our block creation and top block connections. It should look like this:

JNIEXPORT void JNICALL
Java_org_gnuradio_grtemplatecp_MainActivity_FgInit(JNIEnv* env,
                                                   jobject thiz)
{
  GR_INFO("fg", "FgInit Called");

  float samp_rate = 48e3;  // 48 kHz

  // Declare our GNU Radio blocks
  gr::analog::sig_source_f::sptr src;
  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::analog::sig_source_f::make(samp_rate, gr::analog::GR_SIN_WAVE,
                                       400, 1.0, 0.0);
  mult = gr::blocks::multiply_const_ff::make(0.0);
  snk = gr::grand::opensl_sink::make(int(samp_rate));

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

Configure Project

First, go to the command line and do the project update step:

cd GrTemplateCP/app/src/main
android update project -p . -s -t android-21

Now let's bring in the GrControlPort as a library. Go to New -> Import Module. Point the source directory to where ever you checked out GrControlPort. Make sure you only have the "Import" checked for the "library" source location. Rename the "Module name" to ":controlport" to help us keep it properly identified. When this is imported, you will not see a "controlport" directory added to the root of your GrTemplateCP project.

Then, right click on the "app" in the Android project view and go the "Dependencies" tab. Click the + sign and click "Module Dependency" and then OK on ":controlport" to properly link the app against the library.

Now we need to update the local.properties to point the NDK:

sdk.dir=/opt/android
ndk.dir=/opt/ndk

And the gradle.build script for the app to run the NDK steps. IMPORTANT: This is different than what we did in GRAndWalkthrough. Because we'll be pulling in GrControlPort library, that library pulls in some of it's own built binaries and causes a local conflict. It's very likely that I'm doing something wrong with handling the Android Library concept or it could be a basic flaw in the NDK support between multiple projects, but until that issue is worked out, we handle this problem ourselves by adding the ndkCleanup task. We also add a packagingOptions section to allow gradle to ignore certain duplicated text files. Your gradle.build should now look like the following.

apply plugin: 'com.android.application'

android {
    compileSdkVersion 21
    buildToolsVersion "23.0.2"

    // SETUP TO USE OUR OWN Android.mk FILE
    sourceSets.main {
        jniLibs.srcDirs = ['src/main/libs']
        jni.srcDirs = [] //disable automatic ndk-build call with auto-generated Android.mk
    }

    // call regular ndk-build against our Android.mk
    task ndkBuild(type: Exec) {
        commandLine 'ndk-build', '-C', file('src/main/jni').absolutePath
    }

    task ndkCleanup(type: Exec) {
        mustRunAfter 'ndkBuild'
        commandLine 'rm', '-f', file('src/main/libs/armeabi-v7a/libgnustl_shared.so').absolutePath
    }

    tasks.withType(JavaCompile) {
        compileTask -> compileTask.dependsOn ndkBuild
    }

    tasks.withType(JavaCompile) {
        compileTask -> compileTask.dependsOn ndkCleanup
    }

    defaultConfig {
        applicationId "org.gnuradio.grtemplatecp"
        minSdkVersion 21
        targetSdkVersion 23
        versionCode 1
        versionName "1.0"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }


    packagingOptions {
        exclude 'META-INF/LICENSE.txt'
        exclude 'META-INF/NOTICE.txt'
        exclude 'META-INF/LICENSE'
        exclude 'META-INF/NOTICE'
        exclude 'lib/armeabi-v7a/*'
    }
}

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    testCompile 'junit:junit:4.12'
    compile 'com.android.support:appcompat-v7:23.2.1'
    compile 'com.android.support:design:23.2.1'
    compile project(':controlport')
}

As an aside, you might need to open up the project-level build.gradle file and adjust the classpath to "com.android.tools.build:gradle:1.5.0".

At this point, the project should compile, though we haven't done anything about the project's main activity, yet.

Construct the Activity

We'll build the activity first to just make sure we can build and launch the activity, but we won't add any controls just yet, so we won't be able to change the coefficient in our multiply_const_ff block, yet. To do this, we'll just add the SetTMP, FgInit, and FgStart functions to MainActivty.java and call them, in this order, in OnCreate, like this:

public class MainActivity extends Activity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        setContentView(R.layout.activity_main);

        SetTMP(getCacheDir().getAbsolutePath());
        FgInit();
        FgStart();
    }

    public native void SetTMP(String tmpname);
    public native void FgInit();
    public native void FgStart();

    static {
        System.loadLibrary("fg");
    }
}

When we run this application, logcat will print out something like:

04-21 14:01:50.657 23535-23535/org.gnuradio.grtemplatecp I/fg: FgInit Called
04-21 14:01:50.658 23535-23535/org.gnuradio.grtemplatecp V/VOLK: Using Volk machine: neon_softfp
04-21 14:01:50.830 23535-23535/org.gnuradio.grtemplatecp I/fg: FgStart Called
04-21 14:01:51.034 23535-23535/org.gnuradio.grtemplatecp I/thrift_application_base: Apache Thrift: -h localhost -p 9090
04-21 14:01:51.035 23535-23535/org.gnuradio.grtemplatecp I/gr_log: gr::mvcircbuf_mmap_tmpfile: opened: 31
04-21 14:01:51.035 23535-23535/org.gnuradio.grtemplatecp I/gr_log: gr::mvcircbuf_mmap_tmpfile: opened: 31
04-21 14:01:51.045 23535-23584/org.gnuradio.grtemplatecp V/VOLK: volk_32f_s32f_multiply_32f_a --> u_neon
04-21 14:01:51.045 23535-23584/org.gnuradio.grtemplatecp V/VOLK: volk_32f_s32f_multiply_32f_u --> u_neon
04-21 14:01:51.048 23535-23585/org.gnuradio.grtemplatecp V/VOLK: volk_32f_s32f_convert_16i_a --> a_generic
04-21 14:01:51.048 23535-23585/org.gnuradio.grtemplatecp V/VOLK: volk_32f_s32f_convert_16i_u --> generic

Notice that the app prints out information about the ControlPort connection point, which is localhost on port 9090. We set up the thrift.conf file in GRAndWalkthrough with the port number used here. We can remember that or even query the /sdcard/.gnuradio/thrift.conf file for this information from our Java app. However we get that information, we need it for the next stage, which will create a network runner thread to send commands to the flowgraph over ControlPort.

Create a Runnable Network Class

ControlPort operates over TCP/IP (in the current use of Thrift, at least), and Android Activities cannot make network calls from their main threads. Instead, we'll create a Runnable subclass as a private class of our activity and use this to actually send messages. The data is actually passed to ControlPort through a class that extends the Handler class.

It is quite likely that there is a much easier way to manage the network connections over ControlPort.

Below, I show what the handler class looks like. This handler is specifically created to call the setKnobs ControlPort interface to pass a new set of knobs to the flowgraph.

    private static class SetKnobsHandler extends Handler {
        private RPCConnection mConnection;
        public SetKnobsHandler(RPCConnection conn) {
            super();
            mConnection = conn;
        }

        public void handleMessage(Message m) {
            Bundle b = m.getData();
            if(b != null) {
                HashMap<String, RPCConnection.KnobInfo> k =
                        (HashMap<String, RPCConnection.KnobInfo>) b.getSerializable("Knobs");

                Log.d("MainActivity", "Set Knobs: " + k);
                if((k != null) && (!k.isEmpty())) {
                    mConnection.setKnobs(k);
                }
            }
        }
    }

The Runnable class that manages the life cycle of the network thread is next:

public class RunNetworkThread implements Runnable {

        private RPCConnection mConnection;
        private String mHost;
        private Integer mPort;
        private Boolean mConnected;
        private Handler mHandler;

        RunNetworkThread(String host, Integer port) {
            this.mHost = host;
            this.mPort = port;
            this.mConnected = false;
        }

        public void run() {
            if(!mConnected) {
                mConnection = new RPCConnectionThrift(mHost, mPort);
                mConnected = true;
            }

            Looper.prepare();
            mHandler = new SetKnobsHandler(mConnection);
            Looper.loop();
        }

        public RPCConnection getConnection() {
            if(mConnection == null) {
                throw new IllegalStateException("connection not established");
            }
            return mConnection;
        }

        public Handler getHandler() {
            return mHandler;
        }
    }

I then create a function designed to pass the knob information to the handler of the runnable thread:

    
private void postSetKnobMessage(HashMap<String, RPCConnection.KnobInfo> knobs) {
        Handler h = mControlPortThread.getHandler();
        Bundle b = new Bundle();
        b.putSerializable("Knobs", knobs);
        Message m = h.obtainMessage();
        m.setData(b);
        h.sendMessage(m);
    }

Here, I am taking a String:KnobInfo map that we'll create in our activity, bundles it up into a serialized object called "Knobs" and then passes this as a message to the handler class to actually send this message on to ControlPort. Let's then set up the thread and ControlPort connection management in our OnCreate function of MainActivity. We'll need a public object for the RunNetworkThread class, since we directly refer to it in our postSetKnobMessage function above. Below is the first part of our new OnCreate function, just before we actually try to use the network thread. It's important that the RunNetworkThread is created after FgStart is called to make sure that the ControlPort endpoint already exists and is waiting for a connection.

public RunNetworkThread mControlPortThread;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        setContentView(R.layout.activity_main);

        SetTMP(getCacheDir().getAbsolutePath());
        FgInit();
        FgStart();

        // Make the ControlPort connection in the network thread
        mControlPortThread = new RunNetworkThread("localhost", 9090);
        Executor executor = Executors.newSingleThreadExecutor();
        executor.execute(mControlPortThread);

        // Wait here until we have confirmation that the connection succeeded.
        while(true) {
            try {
                mControlPortThread.getConnection();
                break;
            } catch (IllegalStateException e0) {
                try {
                    Thread.sleep(250);
                } catch (InterruptedException e1) {
                    Thread.currentThread().interrupt();
                }
            }
        }

        ....

Now, we have to format the KnobInfo to send the right knob information through ControlPort. We have to create the KnobInfo object, which is a class that we get from the GrControlPort library. We need to know the interface name of the knob we are trying to set. In a real application, we would use ControlPort to get the information from the flowgraph. But for our purposes, we just know that this is "multiply_const_ff0::coefficient". We then need a new value to pass as well as the data type of the data. Let's just initialize the value of the multiplier to 0.5, so that's easy to plug in here. The data type must be one of the BaseType's (a class exported from GrControlPort). In this case, it's BaseType.DOUBLE.

final String mult_knob_name = "multiply_const_ff0::coefficient";
RPCConnection.KnobInfo k = 
                new RPCConnection.KnobInfo(mult_knob_name, 0.5,
                        BaseTypes.DOUBLE);

We've packaged up the knob as a KnobInfo object, but our ControlPort endpoint takes a HashMap of these KnobInfo objects because it allows us to bundle together many ControlPort knobs. Here's how we do that and pass this to our post function:

        HashMap map = new HashMap<>();
        map.put(mult_knob_name, k);
        postSetKnobMessage(map);

Now, when we run this application on our device, the flowgraph creates the multiply_const_ff block with a coefficient of 0, but through ControlPort, we immediately change this value to 0.5, so we should now here our 400 Hz tone coming through the speaker.

NOTE: Notice that the signal clicks pretty frequently. This is because the call to post data to an OpenSLES device is blocking, so during work, we pass a buffer of samples to the device, wait for it to process, then exit work, get more samples, and re-enter work to pass the new buffer on. The clicks correspond to the time between exiting and re-entering work. We need to redo this work function to properly double-buffer the samples so we can keep a continuous stream of samples going to the audio device.

Adding a Slider

The final step in this example is to add a slider to allow us to control the amplitude easily when the application is running. This is as easy as adding a SeekBar into the main activity. Go to layouts and open activity_main.xml. Find the SeekBar and place it on the app under the text that reads "Hello World" (which I have changed to "Change Amplitude"). The SeekBar has an ID of "seekBar" that we'll use in the app to get access to it.

The following code in OnCreate (make sure this is after the "setContentView(R.layout.activity_main);" line so the seekBar has been created) will set up the seekBar to have 100 steps between 0 and 1. Whenever the seek bar is changed, the listener code is fired, which gets the current state of the bar ("progress") that we convert to the new coefficient for the ControlPort knob for the multiplier block.

       // 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) {
            }
        });