GRAndWalkthrough
Building a Basic App
This walk-through basically builds the GrTemplate project. It is meant to serve as a set of steps to take to initialize a new GNU Radio on Android app with the setup and configuration necessary to get all of the layers talking to each other. Once the basics are done, the fun part starts, which is designing the flowgraph and application interface.
Start the Project
- Open Android Studio
- File
> New> New Project - Give it a name and a company domain
- Select the type -- probably Phone and Tablet
- And the API level is "API 21: Android 5.0 (Lollipop)"
- Select a Blank Activity
- Give the main activity a name (e.g., GrTemplate or stick with MainActivity)
- Click Finished
- File
- Open AndroidManifest.xml (under GrTemplate/app/main or manifests depending on your Android Studio view)
- Add necessary permissions. Put this before the <application> start tag.
- Necessary permissions (for preferences, volk config, gnuradio config, and USRP firmware/FPGA binaries)
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
- You will need access to the Internet interface for ControlPort and ZMQ
<uses-permission android:name="android.permission.INTERNET"/>
- For talking to the audio input or output
<uses-permission android:name="android.permission.CAPTURE_AUDIO_OUTPUT"/> <uses-permission android:name="android.permission.RECORD_AUDIO"/> <uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS"/>
Setup the JNI
- Create a new jni directory.
- app > src >main > Right Click> New > Directory> 'jni'
- This makes a folder app/src/main/jni
- Add the Android makefiles we provide
- GrAndroid.mk: This declares and allows us to pull in all of the GNU Radio dependencies for building the JNI library.
- Note that it assumes we've put the dependencies into /opt/grandroid.
- If not, change that up top in the "GRLIBPATH" variable.
- This creates:
- GR_WHOLE_STATIC_LIBRARIES: libUHD
- GR_SHARED_LIBRARIES: libUSB
- GR_STATIC_LIBRARIES: Boost, FFTW, Thrift, VOLK, and all of the support GNU Radio libraries
- We will use this in the Android.mk file.
- Note that it assumes we've put the dependencies into /opt/grandroid.
- Android.mk: Pulls from GrAndroid.mk and sets the rest of the libraries parameter.
- This example pulls in the OpenSLES library, which we need when working with the audio source or sinks.
- We also get the android and log libraries here.
- Most importantly, this is where we declare the source code files.
- settmp.cpp: a simple stand-alone file that defines a JNI function to allow us to pass the App's cache directory name to C++ for us to use when building the flowgraph.
- fg.cpp: Create this file, which is where we will put our flowgraph.
- Application.mk: Defines some of the build system parameters, like what type(s) of processors we're supporting.
- Currently, GNU Radio for Android and the dependencies have only been built for ARMv7 32-bit systems.
- GrAndroid.mk: This declares and allows us to pull in all of the GNU Radio dependencies for building the JNI library.
- We'll put all of our source code for the GNU Radio flowgraph here.
- Keep in mind that the JNI concept is very sensitive to the Java class name, so you'll have to make sure to update any JNI function names with your domain name, project name, and class name.
Make a flowgraph
- We've created the jni directory where our flowgraph lives.
- Add a new file to this directory: Right-Click -> New > New C/C++ Source File
- name this file fg.cpp and see the flowgraph file in the GrTemplate repo.
// Required
#include
// We'll likely want these
#include
#include
// Get any GNU Radio headers
#include
#include
#include
#include
// Other block headers next
// Declare the global virtual machine and top-block objects
JavaVM *vm;
gr::top_block_sptr tb;
extern "C" {
JNIEXPORT void JNICALL
Java_org_gnuradio_grtemplate_MainActivity_FgInit(JNIEnv* env,
jobject thiz)
{
GR_INFO("fg", "FgInit Called");
// Declare our GNU Radio blocks
gr::blocks::null_source::sptr src;
gr::blocks::throttle::sptr thr;
gr::blocks::null_sink::sptr snk;
// Construct the objects for every block in the flwograph
tb = gr::make_top_block("fg");
src = gr::blocks::null_source::make(sizeof(gr_complex));
thr = gr::blocks::throttle::make(sizeof(gr_complex), 10000);
snk = gr::blocks::null_sink::make(sizeof(gr_complex));
// Connect up the flowgraph
tb->connect(src, 0, thr, 0);
tb->connect(thr, 0, snk, 0);
}
JNIEXPORT void JNICALL
Java_org_gnuradio_grtemplate_MainActivity_FgStart(JNIEnv* env,
jobject thiz)
{
GR_INFO("fg", "FgStart Called");
tb->start();
}
JNIEXPORT void JNICALL
Java_org_gnuradio_grtemplate_MainActivity_FgStop(JNIEnv* env,
jobject thiz)
{
GR_INFO("fg", "FgStop Called");
tb->stop();
tb->wait();
GR_INFO("fg", "FgStop Exited");
}
JNIEXPORT jstring JNICALL
Java_org_gnuradio_grtemplate_MainActivity_FgRep(JNIEnv* env,
jobject thiz)
{
return env->NewStringUTF(tb->edge_list().c_str());
}
}
- This flowgraph is very simple and just creates the following flowgraph:
null_source -> throttle -> null_sink
- There are four JNI functions declared here that we can use from our Java app:
- FgInit: Where we ceate the blocks and construct the flowgraph.
- FgStart: Calls start() to start the flowgraph.
- FgStop: Calls stop() and wait() to shut down the flowgraph.
- FgRep: returns a string representation of the flowgraph.
- It's up to the radio designer to construct the flowgraph built inside FgInit.
- We want to minimize the number of JNI functions because it limits which class can access which function.
- To change any parameters in blocks, we want to use ControlPort.
- We might have to update a block with a ControlPort interface.
- I also made the GNU Radio blocks and the top_block global so that we can easily access them in the different JNI methods. We'll probably want a better way to handle this later.
- Add another C++ file called settmp.cpp that looks like:
#include
#include
extern "C" {
JNIEXPORT void JNICALL
Java_org_gnuradio_grtemplate_MainActivity_SetTMP(JNIEnv* env,
jobject thiz,
jstring tmpname)
{
const char *tmp_c;
tmp_c = env->GetStringUTFChars(tmpname, NULL);
setenv("TMP", tmp_c, 1);
}
}
Set up the MainActivity to call SetTMP
- In the main activity Java file, setup the JNI interface functions and call SetTMP in the onCreate function.
' For access to the JNI call, put the following at the bottom of the activity class:
public native void SetTMP(String tmpname); static { System.loadLibrary("fg"); }
' The library name is 'fg' because that's what we put into the Android.mk file's LOCAL_MODULE value.
' In the onCreate function, just add this line:
@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); SetTMP(getCacheDir().getAbsolutePath()); .... }
' This uses Java to get information about the application's cache directory, which is unknown by us or by the C++ application. We get it through these Android calls, and then pass it to the C++ part, which will set the environmental variable TMP to this directory. GNU Radio knows to look at TMP when setting up the scheduler.
- Similarly, let's add the flowgraph control JNI function to the MainActivity class
public native void FgInit(); public native void FgStart(); public native void FgStop(); public native void FgRep();
' Then in the onCreate function, add the init and start functions after SetTMP:
FgInit(); FgStart();
- In the example, fg.cpp from the GrTemplate project, you can find log messages for when each function was entered.
- When we run the app, we can watch logcat to monitor our GR_LOG messages from the JNI function calls.
Setup the NDK
- We have to tell the application to build the NDK.
- Tell Android Studio about the NDK:
- Under Gradle Scripts > local.properties
- Add a line fo the NDK:
- ndk.dir=/opt/ndk
- Or wherever you installed the NDK on your system.
- Next, we have to modify Gradle Scripts > "build.gradle (Module: app)" (Can also find this under the app directory).
- Here we will tell Android Studio not to try and do its own thing with the NDK but to run the command 'ndk-build' from the path directly. This makes sure that the build system uses our Android.mk file, which is important because we have a lot of libraries to install ourselves and there isn't a better way to do this in Android Studio.
- Add the following lines just after the 'buildToolsVersion' line:
// 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
}
tasks.withType(JavaCompile) {
compileTask -> compileTask.dependsOn ndkBuild
}
Build the Project
- If it complains about UTF-8 incompatibility, look down in the bottom right corner for the setting to click on and switch this project to UTF-8.
- Open up a terminal and go to the application's main directory, "AppName/app/src/main" and run the android update project command:
$ android update project -p . -s -t android-21
' I am sure there is a way to do this within Android Studio itself, but I've yet to figure out how. (Note: Tools>Android>'Sync Project with Gradle Files')
- Build the program: Build -> Make Project (or Ctrl + F9)
- At this point, if everything was done correctly above, the build should end with "BUILD SUCCESSFUL" in the Messages tab at the bottom of the Android Studio interface.
- Build the project with the fg.cpp we wrote. It will take a few seconds for the ndk-build to run, and you'll likely see a handful of warnings about the Android LOG levels, which you can ignore (or help us fix them!). You should not get any errors.
Configuring GNU Radio on Android
- On desktop systems, GNU Radio looks for a handful of files that define the various configurations and preferences for the given instance. On Android, it only looks in /sdcard/.gnuradio/config.conf.
- If you built Thrift for Android and then made sure to enable ControlPort when building GNU Radio, we can use the config.conf file to turn on ControlPort and export the performance counters. Create a config.conf file that looks like this:
[PerfCounters] on = true export = true [ControlPort] on = true edges_list = true config = /sdcard/.gnuradio/thrift.conf
- And a thrift.conf file that forces it to use port 9090 so we don't have to monitor it or guess. This file looks like:
[thrift] port = 9090 nthreads = 10 buffersize = 1434 init_attempts = 100
- Upload these to the device:
adb shell mkdir /sdcard/.gnuradio adb push config.conf /sdcard/.gnuradio/config.conf adb push thrift.conf /sdcard/.gnuradio/thrift.conf
- Next time you run, logcat should show a message like:
I/thrift_application_base(26518): Apache Thrift: -h localhost -p 9090
- Now go and do something more interesting!