Thursday, October 9, 2014

Android: Working with USB and Arduino

Android Working with USB Peripherals

Android Working with USB Peripherals

Introduction

When it comes to working with popular tablets Google Android and Apple IOS are the likely choices. Google provides a near normal Java development platform and that gives it enough of an edge to be my platform of choice. This is more because I'm lazy rather than any real difference. Objective C is easy enough to get used to. The other advantage in favor of Google is a standard miniUSB connector. I have plenty of those lying around the house which again is a sign of laziness on my part. Ultimately if one were to do this sort of thing for a living learning both platforms makes you more marketable. I do this stuff part time so using a familiar platform is an important consideration.

In terms of asthetic appeal I think the Apple IOS platform wins. I seem to prefer using my iphone and ipad over the Google tablet I have. This is a very subjective consideration and my laziness wins out over it.

Self deprecation aside what I mean to say is that I am not trying to advocate one platform over another. If something I design is useful enough at some point an Apple application would be built. But most of my design work is based on learning things that interest me and not about market research into which is the best platform to launch an application on.

NOTE: for those who like to do rather than read a link to a zipped Eclipse project with source code is included at the end of this article.

USB

For a while now Google Android has provided a USB API. For the code in this article I have been developing on the Eclipse IDE with the Android SDK. Android provides both a USB accessory API and a USB Host API. The Host API is what this article is about. This is where the Google tablet provides the USB power and a USB device is attached via USB cable. In particular an Arduino MEGA 2256.

The Android documentation on UsbHost is good but it is not a tutorial. It is very general because there are a multitude of USB device you might want to attach. In this particular case there is only one device the Arduino and it acts as an RS232 device communicating over USB.

The references below provide some extra documentation and much of the code I ended up modifying came from: (place link here). For how basic the communications were there is quite a bit of code that is needed to accomplish the USB communication tasks.

Most of my changes came from creating an object class to encapsulate as much of the USB API calls into one class so it does not take up room in the MainActivity class. I don't really consider this to be a clean separation because so much of the access to the USB API relies on System level objects placed into the Context object of the MainActivity. The current code is certainly workable and hopefully to future users is not too cumbersome.

Application and USB states

The application (Activity in Android-speak) has lifecycle states that are imposed by the Android API. Some can be ignored in most activities as there is built in default behavior provided by the API. The Activity handles these lifecycle events by overriding onCreate, onStart, onResume, onPause, onStop and onDestroy methods as needed.

Android activities do not run like typical computer applications where the user starts them and then quits when they are done. In an activity they can pause or stop. But the user does not have one touch access to quitting the Activity. The Android OS will decide when to take the Activity out of memory and make it restart from scratch. Not the most useful model for controlling a piece of hardware with USB but that's what we have to work with.

The USB has states that don't exist directly in the API, but must be dealt with. For example is a USB device connected or not? Has the communication channel been set up if it is connected. These have to be determined as the Activity is running and continually checked.

Android lifecycle considerations

The main lifecycle method that most methods deal with is the onCreate method. This is where initialization occurs for many objects that make up the Activity. This is usually auto-generated if you are using Eclipse as the development platform when you first define the activity in Eclipse. The developer is then free to add in any initializations they want in this method.

Android logging - What is it and Where is it?

Android provides a Logging API via the Log class. After placing the Android device in developer debugging mode, this class allows the developer to place logging information (ie. simple text) in the Android system log.

The easy part is placing the logging code into the source code. I tend to use 2 methods from the log class: the "i" method (short for info) and the "e" method (short for error). The info logs are used to tell when a method has been entered or when certain USB functions have been called. The error logs are used for when the code takes an unexpected turn, or when a test for a null pointer is true.

The Log methods take 2 strings. The first string is a TAG that is usually set up to tell you what class you are operating in. The second string is the developer message for what is happening or where it is happening.

public static final String version = "Version 1.12";
private final String TAG = MainActivity.class.getSimpleName() + MainActivity.version;

The version field is made static and public so it can be used by other classes TAG fields for logging purposes. The purpose of using the version string is to allow the developer to search the large Android Log file and easily find the log messages output by the Activity.

The following is an example of a Log statement using the above TAG field:

Log.i(TAG, "onCreate(): entered");

The more difficult part is to access the log file once the Activity has run. The easiest way is to plug the tablet into the USB of your development computer. Then locate the location where your Android SDK is placed. Then open a terminal window (if you're using Mac or Unix) and set your PATH variable to point at the SDK/bin directory. Now you can use the adb command to run logcat on the tablet to dump the log file. The only problem is it never returns giving a live feed so you need to Ctrl-C out of the program to get access back to the command prompt.

adb logcat >android.log   
vi android.log

I happen to use vi as an editor for this but any text editor will do.

[Add copy of vi display of the log output]

Intents

Many of the system level events that happen are communicated back to the MainActivity through the use of Intent objects. Separate Handler classes are required to handle these Intent events. The Android system can be made to map these events to a particular Activity. This allows the system to open the application when the event occurs.

USB Permission Receiver

This code gets called after the User sees a Dialog box asking for permission to use a USB device and clicks the OK button. This is a non-system level intent meaning it is created by the Activity itself and the Activity must tell the Android OS about the Intent and then register it as follows:

// register the broadcast receiver
mPermissionIntent = PendingIntent.getBroadcast(this, 0, new Intent(
                ACTION_USB_PERMISSION), 0);
IntentFilter filter = new IntentFilter(ACTION_USB_PERMISSION);
registerReceiver(mUsbReceiver, filter);

This needs to be done because we need the Android OS to pause our Activity and display a permission dialog box. This seems to be a round about way to get permission when the application already knows which device it wants to control and that the Activity could just as easily asked the user directly. Android OS seems to need this to keep track of whether it can use the USB connection to the Arduino device. So a receiver must be defined in the MainActivity to handle this intent when we ask the OS to trigger it.

Once the permission has been established this receiver will go ahead and try to establish USB communication by calling setupUsbComm() in the UsbController class. Once that is accomplished communications should be able to take place between the Tablet and the Arduino board.

private final BroadcastReceiver mUsbDeviceDetachedReceiver = new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
                String action = intent.getAction();
                Log.i(TAG, "mUsbDeviceDetachedReceiver.onReceive()" + version);
                if (UsbManager.ACTION_USB_DEVICE_DETACHED.equals(action)) {
                        if (usbController != null) {
                                usbController.releaseUsb();
                                textSearchedEndpoint
                                                .append("\nACTION_USB_DEVICE_DETACHED: usbController released\n");
                        } else {
                                textSearchedEndpoint
                                                .append("\nACTION_USB_DEVICE_DETACHED: usbController null no release\n");
                        }
                }
        }
};

USB Device Attached Intent Receiver

This code gets called after the user attaches a USB device to the USB port of the Android Tablet. This Intent is generated by the Android OS and it has an ACTION string defined by the API already. So to register a handler for this intent requires one line in onCreate() to make that happen:

registerReceiver(mUsbDeviceAttachedReceiver, new IntentFilter(
                UsbManager.ACTION_USB_DEVICE_ATTACHED));

Now the receiver for this is somewhat complicated by the fact that if we just attached the USB then the UsbController class object that encapsulates the USB functionality will not have been set up properly (no device no setup). This handler must see to it that if the connected USB is the one we are looking for then we initialize it and get permission for it. Yes this handler must trigger the permission handler above. There are 2 possibilities:

  1. There is no UsbController object yet so we need to use the constructor for the class to set one up
  2. Some code has been run that has partially set up the object in the MainActivity but there was no device found to go any further

The second one occurs because we have to take into account that the device may already be connected when the Activity is run for the first time. In that case there will be no ACTIONUSBDEVICEATTACHED sent to the Activity because the device was attached long before the Activity came into being. So since the Activity may have gone through a partial setup this code is overly complex.

In both cases an important part of the code is to get the UsbDevice found by Android OS and place it in the UsbController object for use. The intent object seems to be the only place where this information (the UsbDevice) exists.

private final BroadcastReceiver mUsbDeviceAttachedReceiver = new BroadcastReceiver() {

        @Override
        public void onReceive(Context context, Intent intent) {
                String action = intent.getAction();
                Log.i(TAG, "mUsbDeviceAttachedReceiver.onReceive()");
                if (UsbManager.ACTION_USB_DEVICE_ATTACHED.equals(action)) {
                        // The device is provided as part of the intent
                        // in a larger context (ie. android attached to USB hub)
                        // it may not be our device of interest there could be other
                        // devices. But for now assume only one device
                        UsbDevice device = (UsbDevice) intent
                                        .getParcelableExtra(UsbManager.EXTRA_DEVICE);

                        if (usbController == null) {
                                textDisplayLog
                                                .append("\nACTION_USB_DEVICE_ATTACHED: usbController was null now created\n");
                                usbController = new UsbControls(
                                                UsbControls.targetVendorID,
                                                UsbControls.targetProductID,
                                                device);
                                Log.i(TAG,
                                                "mUsbDeviceAttachedReceiver: USB Controller set up");
                        } else {
                                textDisplayLog
                                                .append("\nACTION_USB_DEVICE_ATTACHED: usbController not null UsbDevice set\n");
                                usbController.setUsbDevice(device);
                                Log.i(TAG,
                                                "mUsbDeviceAttachedReceiver: USB Controller already available UsbDevice set");
                        }

                        usbController.usbInit((UsbManager) context
                                        .getSystemService(USB_SERVICE));

                        // Get permission to use
                        // mUsbManager.requestPermission(device, mPermissionIntent);
                        if (usbController.getUsbPermission(
                                        (UsbManager) context.getSystemService(USB_SERVICE),
                                        mPermissionIntent)) {
                                Log.i(TAG, "mUsbAttachedReceiver: USB ready");
                    } else {
                           Log.e(TAG, "mUsbAttachedReceiver: Cannot get permission for USB");
                    }
                }
        }
};

USB Device Detatched Intent Receiver

This code gets called when the device is disconnected from the USB port of the Android Tablet. This must be registered to the Android OS

registerReceiver(mUsbDeviceDetachedReceiver, new IntentFilter(
                UsbManager.ACTION_USB_DEVICE_DETACHED));

Without a device all of the USB objects held within the UsbController object need to be released, hence the call to usbController.releaseUsb(). There is the possiblility that something went wrong and there is no valid usbController object so we check it for a null pointer before using it.

private final BroadcastReceiver mUsbDeviceDetachedReceiver = new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
                String action = intent.getAction();
                Log.i(TAG, "mUsbDeviceDetachedReceiver.onReceive()");
                if (UsbManager.ACTION_USB_DEVICE_DETACHED.equals(action)) {
                        if (usbController != null) {
                                usbController.releaseUsb();
                                textDisplayLog
                                                .append("\nACTION_USB_DEVICE_DETACHED: usbController released\n");
                        } else {
                                textDisplayLog
                                                .append("\nACTION_USB_DEVICE_DETACHED: usbController null no release\n");
                        }
                }
        }
};

Finally after we have used the Activity to our heart's content and have started using other Activities on the tablet we should unregister the Intents if the MainActivity gets "destroyed" by the Android OS. So in the onDestroy lifecycle call back the following unregister calls are made:

unregisterReceiver(mUsbReceiver);
unregisterReceiver(mUsbDeviceAttachedReceiver);
unregisterReceiver(mUsbDeviceDetachedReceiver);

Strings and bytes

Strings are the usual class used for handling text and for most Android Activities involving graphics and user input this is fine. For USB communication however bytes are the coin of the realm. For this USB program the transmit message is setup directly in bytes. But to display the message returned by the Arduino over USB a conversion from bytes to string is needed. There maybe an easier way but I chose to terminate the USB bytes with the ASCII return character 0x0A. This way you just need to loop through the bytes and find that ASCII character as the end of the message:

for (int i = 0; i < result; i++) {
    if (buf[i] != 0x0a) {
        sb.append(String.format("%c", buf[i]));
    } else {
        synchronized (syncToken) {
            msg = sb.toString();
        }
        sb = new StringBuilder();
        mHandler.sendEmptyMessage(0);
        break; // There is a question whether there is new valid data 
              // after newline but am assuming not for now
    }
}

The Code

MainActivity.java - Simple user interface to test USB communication

This MainActivity has been set up with a title, a scrolling TextView to act as a DisplayLog window for realtime feedback as to how the application is running. Finally a button that when pressed sends a preset message to the Arduino board. onStart() is probably the most complex method because it makes sure that if a USB board is attached that we try to establish communication and make sure we have permission to connect to it. The rest of the code is as explained above with the Intent receivers in their proper location. I have implemented all of the lifecycle methods with logging so when debugging connectivity with the USB you have an understanding of when things are happening. Therefore as your Activity becomes more complex you will have some insight to where things should go in the lifecycle method callbacks.

The Graphical User Interface was set up with Eclipse and looks as follows:

The actual MainActivity code:

package com.llsc.androidusb;


import android.app.Activity;
import android.app.ActionBar;
import android.app.Fragment;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.hardware.usb.UsbConstants;
import android.hardware.usb.UsbDevice;
import android.hardware.usb.UsbManager;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.TextView;
import android.os.Build;

public class MainActivity extends Activity {
        public static final String version = "Version 1.12";
        private final String TAG = MainActivity.class.getSimpleName() + MainActivity.version;


        TextView textDisplayLog;

        Button msgButton;

        UsbControls usbController;

        Boolean fromOnPause = false;

        private static final String ACTION_USB_PERMISSION = "com.android.example.USB_PERMISSION";

        PendingIntent mPermissionIntent;

        Handler mHandler1;
        Object syncObj;

        @Override
        protected void onCreate(Bundle savedInstanceState) {
                super.onCreate(savedInstanceState);
                Log.i(TAG, "onCreate(): entered");

                setContentView(R.layout.activity_main);

                if (savedInstanceState == null) {
                        getFragmentManager().beginTransaction()
                                        .add(R.id.container, new PlaceholderFragment()).commit();
                }

                // register the broadcast receiver
                mPermissionIntent = PendingIntent.getBroadcast(this, 0, new Intent(
                                ACTION_USB_PERMISSION), 0);
                IntentFilter filter = new IntentFilter(ACTION_USB_PERMISSION);
                registerReceiver(mUsbReceiver, filter);

                registerReceiver(mUsbDeviceAttachedReceiver, new IntentFilter(
                                UsbManager.ACTION_USB_DEVICE_ATTACHED));
                registerReceiver(mUsbDeviceDetachedReceiver, new IntentFilter(
                                UsbManager.ACTION_USB_DEVICE_DETACHED));

                mHandler1 = new Handler() {
                        public void handleMessage(Message msg) {
                                onMsgRcv();
                        }
                };
                syncObj = new Object();

                usbController = new UsbControls(UsbControls.targetVendorID,
                                UsbControls.targetProductID);
                usbController.setRcvHandler(mHandler1);
                usbController.setSyncObj(syncObj);
                /*
                 * deviceFound = this.getUsbDevice(targetVendorID, targetProductID); if
                 * (deviceFound == null) usbAttached = false; else usbAttached = true;
                 */
        }

        @Override
        protected void onStart() {
                super.onStart();
                Log.i(TAG, "onStart(): entered ");

                textDisplayLog = (TextView) findViewById(R.id.displaylog);
                msgButton = (Button) findViewById(R.id.button1);

                textDisplayLog.append("\nonStart");

                // Make sure usb is ready to go
                // For this section of code to work you must have the device plugged in.
                // if the device is not plugged in null pointers will happen
                // I think this is why this app should trigger usb setup based on device
                // plugged in
                // intent and device detatched events. Then I should be able to do all
                // the setup
                // in the intent handlers.
                // This is not correct onStart must try to establish connectivity
                // because the
                // device may already be attached.
                UsbManager manager = (UsbManager) this.getSystemService(USB_SERVICE);

                // Device maybe attached but not initialized
                        // If USB connected get permission to use
                        // must setup device first before asking for permission
                        // How do I tell if there is a device attached??
                        // Right now none of the stuff below will set up the connection if
                        // something is connected
                        usbController.setUsbDevice(manager);

                                usbController.usbInit(manager);
                                if (usbController.hasPermission(manager)) {
                                        usbController.setupUsbComm(manager);
                                } else { 
                                        if (usbController.getUsbPermission(manager,
                                                mPermissionIntent)) {
                                        Log.i(TAG, "onStart: USB ready");
                                    } else {
                                          Log.e(TAG, "onStart: Cannot get permission for USB");
                                    }
                                }
                fromOnPause = false;
        }

        @Override
        public void onResume() {
                super.onResume();
                Log.i(TAG, "onResume(): entered");
                textDisplayLog.append("\nonResume: called");
        }

        @Override
        public void onPause() {
                super.onPause();
                Log.i(TAG, "onPause(): entered");
                textDisplayLog.append("\nonPause: called");
                fromOnPause = true;
        }

        @Override
        protected void onStop() {
                super.onStop();
                Log.i(TAG, "onStop(): entered");
                if (usbController.usbAttached()) {
                        usbController.releaseUsb();
                }
        }

        @Override
        protected void onDestroy() {
                super.onDestroy();
                Log.i(TAG, "onDestroy(): entered");
                textDisplayLog.append("\nonDestroy");
                unregisterReceiver(mUsbReceiver);
                unregisterReceiver(mUsbDeviceAttachedReceiver);
                unregisterReceiver(mUsbDeviceDetachedReceiver);
        }

        public void onButton1Pressed(View view) {
                byte[] bytesHello = new byte[] { (byte) 'H', 'e', 'l', 'l', 'o', ' ',
                                'f', 'r', 'o', 'm', ' ', 'A', 'n', 'd', 'r', 'o', 'i', 'd' };
                usbController.usbSend(bytesHello);
        }

        private void onMsgRcv() {
                // Message has come in over USB get it from the USB thread
                // Do it in a sync way so buffers don't get stepped on by different
                // threads
                String s = usbController.getRcvMsg();
                textDisplayLog.append("\n\nData Received");
                textDisplayLog.append("\n\n" + s);
        }

        /*
         * @Override protected void onCreate(Bundle savedInstanceState) {
         * super.onCreate(savedInstanceState);
         * setContentView(R.layout.activity_main);
         * 
         * if (savedInstanceState == null) { getFragmentManager().beginTransaction()
         * .add(R.id.container, new PlaceholderFragment()).commit(); } }
         */
        @Override
        public boolean onCreateOptionsMenu(Menu menu) {

                // Inflate the menu; this adds items to the action bar if it is present.
                getMenuInflater().inflate(R.menu.main, menu);
                return true;
        }

        @Override
        public boolean onOptionsItemSelected(MenuItem item) {
                // Handle action bar item clicks here. The action bar will
                // automatically handle clicks on the Home/Up button, so long
                // as you specify a parent activity in AndroidManifest.xml.
                int id = item.getItemId();
                if (id == R.id.action_settings) {
                        return true;
                }
                return super.onOptionsItemSelected(item);
        }

        private final BroadcastReceiver mUsbReceiver = new BroadcastReceiver() {

                @Override
                public void onReceive(Context context, Intent intent) {
                        String action = intent.getAction();
                        Log.i(TAG, "mUsbReceiver.onReceive()");
                        if (ACTION_USB_PERMISSION.equals(action)) {
                Log.i(TAG,"mUsbReceiver.onReceive: ACTION_USB_PERMISSION found");
                                synchronized (this) {
                                        UsbManager manager = (UsbManager) context
                                                        .getSystemService(USB_SERVICE);

                                        if (intent.getBooleanExtra(
                                                        UsbManager.EXTRA_PERMISSION_GRANTED, false)) {
                                                usbController.usbInit(manager);
                                                usbController.setupUsbComm(manager);

                                                textDisplayLog
                                                                .append("\npermission receiver: permission granted");
                                                Log.i(TAG,
                                                                "mUsbReceiver: permission granted com link attemped");
                                        } else {
                                                textDisplayLog.append("permission denied for device ");
                                                Log.e(TAG, "mUsbReceiver: Permisson denied for device ");
                                        }
                                }
                        }
                }
        };

        private final BroadcastReceiver mUsbDeviceAttachedReceiver = new BroadcastReceiver() {

                @Override
                public void onReceive(Context context, Intent intent) {
                        String action = intent.getAction();
                        Log.i(TAG, "mUsbDeviceAttachedReceiver.onReceive()");
                        if (UsbManager.ACTION_USB_DEVICE_ATTACHED.equals(action)) {
                                // The device is provided as part of the intent
                                // in a larger context (ie. android attached to USB hub)
                                // it may not be our device of interest there could be other
                                // devices. But for now assume only one device
                                UsbDevice device = (UsbDevice) intent
                                                .getParcelableExtra(UsbManager.EXTRA_DEVICE);

                                if (usbController == null) {
                                        textDisplayLog
                                                        .append("\nACTION_USB_DEVICE_ATTACHED: usbController was null now created\n");
                                        usbController = new UsbControls(
                                                        UsbControls.targetVendorID,
                                                        UsbControls.targetProductID,
                                                        device);
                                        Log.i(TAG,
                                                        "mUsbDeviceAttachedReceiver: USB Controller set up");
                                } else {
                                        textDisplayLog
                                                        .append("\nACTION_USB_DEVICE_ATTACHED: usbController not null UsbDevice set\n");
                                        usbController.setUsbDevice(device);
                                        Log.i(TAG,
                                                        "mUsbDeviceAttachedReceiver: USB Controller already available UsbDevice set");
                                }

                                usbController.usbInit((UsbManager) context
                                                .getSystemService(USB_SERVICE));

                                // Get permission to use
                                // mUsbManager.requestPermission(device, mPermissionIntent);
                                if (usbController.getUsbPermission(
                                                (UsbManager) context.getSystemService(USB_SERVICE),
                                                mPermissionIntent)) {
                                        Log.i(TAG, "mUsbAttachedReceiver: USB ready");
                            } else {
                                   Log.e(TAG, "mUsbAttachedReceiver: Cannot get permission for USB");
                            }
                        }
                }
        };

        private final BroadcastReceiver mUsbDeviceDetachedReceiver = new BroadcastReceiver() {
                @Override
                public void onReceive(Context context, Intent intent) {
                        String action = intent.getAction();
                        Log.i(TAG, "mUsbDeviceDetachedReceiver.onReceive()");
                        if (UsbManager.ACTION_USB_DEVICE_DETACHED.equals(action)) {
                                if (usbController != null) {
                                        usbController.releaseUsb();
                                        textDisplayLog
                                                        .append("\nACTION_USB_DEVICE_DETACHED: usbController released\n");
                                } else {
                                        textDisplayLog
                                                        .append("\nACTION_USB_DEVICE_DETACHED: usbController null no release\n");
                                }
                        }
                }
        };

        /**
         * A placeholder fragment containing a simple view.
         */
        public static class PlaceholderFragment extends Fragment {

                public PlaceholderFragment() {

                }

                @Override
                public View onCreateView(LayoutInflater inflater, ViewGroup container,
                                Bundle savedInstanceState) {
                        View rootView = inflater.inflate(R.layout.fragment_main, container,
                                        false);
                        return rootView;
                }
        }
}

UsbControls.java - the encapsulating class for USB setup and communication

This is the class that tries to encaspulate most of the USB code. The most difficult to understand is the setup of communications.

Enumerating USB devices

The Android UsbHost API documentation covers enumerating USB devices that is housed in the getUsbDevice(vendorID,productID,UsbManager) method. It requires a UsbManager parameter because this is available at the level of the MainActivity. Originally all this code was threaded into the MainActivity class.

Initializing and Endpoint enumeration

It turns out that some investigation of the USB device can take place before permission is given to communicate. These have been separated out into usbInit(UsbManager). Enpoints are USB speak for transmit, receive and control channels available to the board. These need to be in place and intialized before the board can actually transmit and receive information

Communications setup

When permission to use the USB device has been granted setupUsbComm(UsbManager) method does the final connection arrangements and establishes common RS232 control parameters such as Baud Rate, Data bits and Stop bits. This is accomplished by calls to the UsbHost API method controlTransfer().

Sending data

The usbSend(byte[]) method sends data down the USB via the bulkTransfer() method. This routine is rather straight forward and if communication has been established properly works as expected.

The rest is just some flag fields to try to maintain some state information about the communication link.

/**
 * AndroidUsb
 * UsbControls.java
 *
 * Author: Nasty Old Dog
 * Code modified from:
 * http://android.serverbox.ch/?p=370 
 * https://github.com/mik3y/usb-serial-for-android
 */
package com.llsc.androidusb;

import java.util.HashMap;
import java.util.Iterator;

import android.app.PendingIntent;
import android.content.Context;
import android.hardware.usb.UsbConstants;
import android.hardware.usb.UsbDevice;
import android.hardware.usb.UsbDeviceConnection;
import android.hardware.usb.UsbEndpoint;
import android.hardware.usb.UsbInterface;
import android.hardware.usb.UsbManager;
import android.os.Handler;
import android.util.Log;

/**
 * @author tmcguire
 *
 */
public class UsbControls {
        private final String TAG = UsbControls.class.getSimpleName();
        public static final int targetVendorID = 0x2341;
        public static final int targetProductID = 0x42;

        UsbDevice deviceFound = null;
        UsbInterface usbInterfaceFound = null;
        UsbEndpoint endpointIn = null;
        UsbEndpoint endpointOut = null;
        UsbReceiver rcvThrd;
        UsbInterface usbInterface;
        UsbDeviceConnection usbDeviceConnection;
        int vendorID = 0;
        int productID = 0;
        private String infoText = "No device connected";

        boolean usbAttached = false;
        boolean usbConnected = false;

        private Handler rcvHandler;
        public void setRcvHandler(Handler rcvHandler) {
                this.rcvHandler = rcvHandler;
        }

        private Object syncObj;

        public void setSyncObj(Object syncObj) {
                this.syncObj = syncObj;
        }

        public UsbControls(int vID, int pID) {
                vendorID = vID;
                productID = pID;
        }

        public UsbControls(int vID, int pID, UsbDevice usbDevice) {
                vendorID = vID;
                productID = pID;
                this.deviceFound = usbDevice;
        }

        public void setUsbDevice(UsbManager manager){
                deviceFound = this.getUsbDevice(vendorID, productID, manager);
        }

        public void setUsbDevice(UsbDevice usbDevice) {
                deviceFound = usbDevice;
        }
        public String getInfoText() {
                return infoText;
        }

        public String getRcvMsg() {
                return this.rcvThrd.getMsg();
        }

        public void usbInit(UsbManager manager) {

                Log.i(TAG,"usbInit");

                if (!usbAttached) {
                        deviceFound = this.getUsbDevice(vendorID, productID, manager);
                        if (!usbAttached) {
                                Log.e(TAG, "usbInit: No device connected");
                                return;
                        } else {
                                Log.i(TAG, "usbInit: device found");
                        }
                }

                searchEndPoint(manager);
/*
                if (usbInterfaceFound != null) {
                        setupUsbComm(manager);
                        Log.i(TAG,"connectUsb: usbInterface setup attempted");
                }
                else {
            Log.e(TAG,"connectUsb: usbInterface not found, usb not setup");
                }
*/
        }

        public void releaseUsb() {
                Log.i(TAG," releaseUsb");

                if (rcvThrd != null) {
                        rcvThrd.stopUsbReceiver();

                        // Inserting delay here
                        try {
                                Thread.sleep(5000);
                        } catch (InterruptedException e) {
                                Log.e(TAG, "sleep in releaseUsb failed");
                        }

                        rcvThrd = null;
                }
                if (usbDeviceConnection != null) {
                        if (usbInterface != null) {
                                usbDeviceConnection.releaseInterface(usbInterface);
                                usbInterface = null;
                        }
                        usbDeviceConnection.close();
                        usbDeviceConnection = null;
                }

                deviceFound = null;
                usbInterfaceFound = null;
                endpointIn = null;
                endpointOut = null;
                usbAttached = false;
                usbConnected = false;
        }

        public boolean isUsbConnected() {
                return usbConnected;
        }

        public void setUsbConnected(boolean usbConnected) {
                this.usbConnected = usbConnected;
        }

        public void searchEndPoint(UsbManager manager) {

        Log.i(TAG,"searchEndPoint");
                usbInterfaceFound = null;
                endpointOut = null;
                endpointIn = null;

                Log.i(TAG, "searchEndPoint: entered");
                // Search device for targetVendorID and targetProductID
                if (deviceFound == null) {
                        deviceFound = this.getUsbDevice(vendorID, productID, manager);
                }
                if (deviceFound == null) {
            Log.e(TAG,"searchEndPoint: Device not found.");
            this.usbAttached = false;
                        return;
                } else {
                        String s = deviceFound.toString() + "\n" + "DeviceID: "
                                        + deviceFound.getDeviceId() + "\n" + "DeviceName: "
                                        + deviceFound.getDeviceName() + "\n" + "DeviceClass: "
                                        + deviceFound.getDeviceClass() + "\n" + "DeviceSubClass: "
                                        + deviceFound.getDeviceSubclass() + "\n" + "VendorID: "
                                        + deviceFound.getVendorId() + "\n" + "ProductID: "
                                        + deviceFound.getProductId() + "\n" + "InterfaceCount: "
                                        + deviceFound.getInterfaceCount();
                        infoText = s;

                        Log.i(TAG, "searchEndPoint: " + s);

                        // Search for UsbInterface with Endpoint of USB_ENDPOINT_XFER_BULK,
                        // and direction USB_DIR_OUT and USB_DIR_IN

                        for (int i = 0; i < deviceFound.getInterfaceCount(); i++) {
                                UsbInterface usbif = deviceFound.getInterface(i);

                                UsbEndpoint tOut = null;
                                UsbEndpoint tIn = null;

                                int tEndpointCnt = usbif.getEndpointCount();
                                if (tEndpointCnt >= 2) {
                                        for (int j = 0; j < tEndpointCnt; j++) {
                                                if (usbif.getEndpoint(j).getType() == UsbConstants.USB_ENDPOINT_XFER_BULK) {
                                                        if (usbif.getEndpoint(j).getDirection() == UsbConstants.USB_DIR_OUT) {
                                                                tOut = usbif.getEndpoint(j);
                                                        } else if (usbif.getEndpoint(j).getDirection() == UsbConstants.USB_DIR_IN) {
                                                                tIn = usbif.getEndpoint(j);
                                                        }
                                                }
                                        }

                                        if (tOut != null && tIn != null) {
                                                // This interface have both USB_DIR_OUT
                                                // and USB_DIR_IN of USB_ENDPOINT_XFER_BULK
                                                usbInterfaceFound = usbif;
                                                endpointOut = tOut;
                                                endpointIn = tIn;
                                        }
                                }

                        }

                        if (usbInterfaceFound == null) {
                                Log.e(TAG,"searchEndPoint: No suitable interface found!");
                        } else {
                                Log.i(TAG,"searchEndPoint: UsbInterface found: "
                                                + usbInterfaceFound.toString() + "\n\n"
                                                + "Endpoint OUT: " + endpointOut.toString() + "\n\n"
                                                + "Endpoint IN: " + endpointIn.toString());

                        }
                }
        }

        public boolean setupUsbComm(UsbManager manager) {

                // for more info, search SET_LINE_CODING and

                final int RQSID_SET_LINE_CODING = 0x20;
                final int RQSID_SET_CONTROL_LINE_STATE = 0x22;
                // final int RQSID_SET_LINE_CODING = 0x32;
                // final int RQSID_SET_CONTROL_LINE_STATE = 0x34;

                boolean success = false;

                Log.i(TAG,"setupUsbComm");

                if (!usbAttached) {
                        Log.e(TAG, "setupUsbComm: device not attached");
                        return success;
                }
//              UsbManager manager = (UsbManager) getSystemService(Context.USB_SERVICE);
                if (deviceFound == null) {
            Log.e(TAG,"setupUsbComm: device not initialized");
                        return success;
                }

                Boolean permitToRead = manager.hasPermission(deviceFound);

                if (permitToRead) { 
                        Log.i(TAG,"setupUsbComm: permitted to read");
                        usbDeviceConnection = manager.openDevice(deviceFound);
                        if (usbDeviceConnection != null) {
                                usbDeviceConnection.claimInterface(usbInterfaceFound, true);

                                // showRawDescriptors(); // skip it if you no need show
                                // RawDescriptors
                                Log.i(TAG,"setupUsbComm: device connection made");

                                int usbResult;
                                usbResult = usbDeviceConnection.controlTransfer(0x21, // requestType
                                                // usbResult = usbDeviceConnection.controlTransfer(0x40,
                                                // // requestType
                                                RQSID_SET_CONTROL_LINE_STATE, // SET_CONTROL_LINE_STATE
                                                0, // value or in 0x01 for DTR or 0x02 for RTS
                                                0, // index
                                                null, // buffer
                                                0, // length
                                                0); // timeout

                                Log.i(TAG,"setupUsbComm: controlTransfer(SET_CONTROL_LINE_STATE): " + 
                                                usbResult);

                                // baud rate = 9600
                                // 8 data bit
                                // 1 stop bit
                                byte[] encodingSetting = new byte[] { (byte) 0x80, 0x25, 0x00,
                                                0x00, 0x00, 0x00, 0x08 };
                                usbResult = usbDeviceConnection.controlTransfer(0x21, // requestType
                                                // usbResult = usbDeviceConnection.controlTransfer(0x40,
                                                // // requestType
                                                RQSID_SET_LINE_CODING, // SET_LINE_CODING
                                                0, // value
                                                0, // index
                                                encodingSetting, // buffer
                                                7, // length
                                                0); // timeout

                                Log.i(TAG,"setupUsbComm: controlTransfer(RQSID_SET_LINE_CODING): " + 
                                                usbResult);

                                // can set up receive thread
                                // This needs to be in it's own routine. 
                                rcvThrd = new UsbReceiver(usbDeviceConnection, endpointIn,
                                                rcvHandler, syncObj);
                                rcvThrd.start();
                success = true;
                usbConnected = true;
                        }

                } else {
//                      manager.requestPermission(deviceFound, mPermissionIntent);
                        Log.i(TAG,"setupUsbComm: Permission requested");
                }
                return success;
        }

        public boolean hasPermission (UsbManager manager) {
                if (deviceFound != null) {
                return manager.hasPermission(deviceFound);
                } else {
                        Log.e(TAG, "hasPermission: device null should be initialized");
                }
                return false;
        }

        public boolean getUsbPermission (UsbManager manager, PendingIntent mPermissionIntent) {
                if (this.usbAttached) {
                if (deviceFound != null) {
                        manager.requestPermission(deviceFound, mPermissionIntent);
                        Log.i(TAG,"getUsbPermission: Permission requested");
                        if (manager.hasPermission(deviceFound)) {
                                Log.i(TAG,"getUsbPermission: Permission granted");
                                return true;
                        } else {
                                Log.i(TAG,"getUsbPermission: Permission denied");
                                return false;                   
                        }
                    }
                    Log.i(TAG,"getUsbPermission: USB attached but device null");
                    return false;
                }
                Log.i(TAG,"getUsbPermission: USB not attached");
                return false;
        }

        public UsbDevice getUsbDevice(int vendor, int product, UsbManager manager) {
//                      UsbManager manager = (UsbManager) getSystemService(Context.USB_SERVICE);
                        HashMap<String, UsbDevice> deviceList = manager.getDeviceList();
                        Iterator<UsbDevice> deviceIterator = deviceList.values().iterator();

                        if (deviceList.isEmpty()) {
                                this.usbAttached = false;
                                return null;
                        }
                        while (deviceIterator.hasNext()) {
                                UsbDevice device = deviceIterator.next();

                                if (device.getVendorId() == vendor) {
                                        if (device.getProductId() == product) {
                                                this.usbAttached = true;
                                                return device;
                                        }
                                }
                        }
                        this.usbAttached = false;
                        return null;
        }

        public boolean usbAttached() {
                return usbAttached;     // was deviceFound != null
        }

        public void setUsbAttached(boolean attach) {
                usbAttached = attach;
        }

    public void resetConnection(UsbManager manager) {
                deviceFound = this.getUsbDevice(vendorID, productID, manager);
            if (deviceFound == null) {
                Log.e(TAG, "resetConnection: can not find a device");
                return;
            }
            usbInit(manager);
            if (this.hasPermission(manager)) {
                setupUsbComm(manager);
            }
    }

    public void usbSend(byte[] msg) {
                int usbResult;

                if (endpointOut != null && usbDeviceConnection != null) {
                usbResult = usbDeviceConnection.bulkTransfer(endpointOut, msg,
                                msg.length, 1000);
                  Log.i(TAG,"usbSend: Transmitted msg: " + usbResult);
            }
                else {
                        Log.i(TAG,"usbSend: endpointOut null no xmit");
                }
    }
}

UsbReceiver.java - code to receive from USB and notify MainActivity

This class creates its own thread to capture data over the USB connection. Because data may not come in one fell swoop it must loop looking for a terminating character before it places data in the msg field. To create a receiver an handler back to the MainActivity must be established so MainActivity can be signaled when there is data to be inspected and displayed. You also need the inbound endpoint, UsbDeviceConnection and a thread synchronizing object.

The bulkTransfer method is used to receive data on the inbound endpoint and some initialization occurs to make sure control of the inbound endpoint is in place via controlTransfer method.

package com.llsc.androidusb;

import android.hardware.usb.UsbDeviceConnection;
import android.hardware.usb.UsbEndpoint;
import android.os.Handler;
import android.util.Log;

public class UsbReceiver extends Thread {
        private final String TAG = UsbReceiver.class.getSimpleName() + MainActivity.version;
        private boolean urStop = false;
        private Handler mHandler;
        private Object syncToken;
        private UsbDeviceConnection mConnection;
        private UsbEndpoint epIn;
        private String msg;
        final int RQSID_SET_LINE_CODING = 0x20;
        final int RQSID_SET_CONTROL_LINE_STATE = 0x22;

        UsbReceiver(UsbDeviceConnection mConnection, UsbEndpoint epIn, Handler mh,
                        Object st) {
                this.mConnection = mConnection;
                this.epIn = epIn;
                mHandler = mh;
                syncToken = st;
                Log.i(TAG,"UsbReceiver(): Constructor initialized object instance");
        }

        public void stopUsbReceiver() {
                synchronized (syncToken) {
                  urStop = true;
                }
        }

        @Override
        public void run() {
                byte[] buf = new byte[64];         // The Arduino MEGA 2256 sends 64byte messages
                StringBuilder sb = new StringBuilder();
                if (mConnection == null) return;
                // send DTR
                int result = mConnection.controlTransfer(0x21, // requestType
                                // usbResult = usbDeviceConnection.controlTransfer(0x40, //
                                // requestType
                                RQSID_SET_CONTROL_LINE_STATE, // SET_CONTROL_LINE_STATE
                                0x01, // value or in 0x01 for DTR or 0x02 for RTS
                                0, // index
                                null, // buffer
                                0, // length
                                0); // timeout
        Log.i(TAG,"run(): USB Receiver thread set up");
                while (true) {
                                if (urStop) {
                                        break;
                                }
                                if (mConnection == null) return;
                                if (epIn == null) return;

                        result = mConnection.bulkTransfer(epIn, buf, buf.length, 1000);

/*                      int nresult = mConnection.controlTransfer(0x21, // requestType
                                        // usbResult = usbDeviceConnection.controlTransfer(0x40, //
                                        // requestType
                                        RQSID_SET_CONTROL_LINE_STATE, // SET_CONTROL_LINE_STATE
                                        0x00, // value or in 0x01 for DTR or 0x02 for RTS
                                        0, // index
                                        null, // buffer
                                        0, // length
                                        0); // timeout
*/
                        // A full message will be terminated with a Newline character '\n'
                        // so read all characters collected so far. If newline detected that's the
                        // end of the message so store full message send signal to main thread and
                        // create new StringBuilder to get next message, break out of loop 
                        if (result > 0) {
                                for (int i = 0; i < result; i++) {
                                        if (buf[i] != 0x0a) {
                                                sb.append(String.format("%c", buf[i]));
                                        } else {
                                                synchronized (syncToken) {
                                                        msg = sb.toString();
                                                }
                                                sb = new StringBuilder();
                                                mHandler.sendEmptyMessage(0);
                                                break; // There is a question whether there is new valid data 
                                                       // after newline but am assuming not for now
                                        }
                                }
                        }
                }
        }

        public String getMsg() {
                synchronized (syncToken) {
                        return msg;
                }
        }

}

ArduinoUsbTest - Arduino code to accept a message on USB and spit it back out.

This is just some simple arduino code to spit back the incoming message. It also toggles the LED so you have some feed back that data was received and a round trip message was attempted.

/**
 * AndroidUsbTest - testing platform for usb communication with 
 *                  Android usb host
 */
char charRead = 0;
char buffer[41];
char printout[20];  //max char to print: 20
int ledPin = 13;

void setup() {
  // put your setup code here, to run once:
  //Setup Serial Port with baud rate of 9600
  Serial.begin(9600);
    pinMode(ledPin, OUTPUT);
  digitalWrite(ledPin,LOW);
  buffer[0] = 0;
}

void loop() {
  // put your main code here, to run repeatedly:
    while (Serial.available()) {
      int chrs = Serial.readBytesUntil('\n',buffer,40);
      buffer[chrs] = 0;
    }

  if (buffer[0] != 0) {
    digitalWrite(ledPin, digitalRead(ledPin) ^ 1);   // toggle LED pin
    Serial.print("How are you, ");
    Serial.println(buffer);
    buffer[0] = 0;
  }

}

Conclusion

This article is somewhat hastily put together because I spent a large amount of part-time effort on the code. I wanted to get the code out there in the hopes that it will be helpful to people and save them some time setting up Android USB communication. As time allows I will try to refine the article be a little more explanatory. For now hopefully the code and the comments will assist you in getting your USB project going.

All the references below were extremly helpful. Much of what I learned was gleaned from reference 3 with some mix of reference 4. If I could simplify code to match the Android UsbHost API documentation I did so where I could. What this code does over the other applications is it provides expected functionality if you should disconnect and reconnect the USB. This was troublesome to get working to my expectations on how I believe a connected device should work with an Activity.

Finally the Android UsbHost API allows you specify intents in the xml manifest. This will certainly make the application come up when a board is connected but I had a great deal of difficulty making it work well when things are already plugged in or when connect and disconnect occurs repeatedly.

Zipped Eclipse Project Containing Source Code

Author: Nasty Old Dog

Validate