Small breadboard
Because the header pins are longer on the motor shield than on the Bluetooth shield, I chose to stack them in the following order:
| The Arduino board is on the bottom.
The motor shield is in the middle.
The Bluetooth shield is on the top. |
Now we need to get the DC motor set up. The motor shield I am using can control two DC motors. For channel A (motor 1), direction is set using digital pin 12, pulse width modulation (PWM) is set using digital pin 3, braking is set using digital pin 9, and the motor's current can be read from analog pin 0. For channel B (motor 2, if we were using one), direction is set using digital pin 13, PWM is set using digital pin 11, braking is set using digital pin 8, and the motor's current can be read from analog pin 1. All of this is handled in the motor's init()
function.
The red and white LEDs are connected to pins 4 and 5 respectively. With all of the wires connected to their respective pins, our hardware now looks the following. The straw in the picture was put on the motor's axle so that I could see which way it was spinning. It also helped to slow the motor down as speeds in excess of 200 sounded as though it was about to explode!
As far as the coding goes, the setup()
function is called once when the sketch starts. In it, we'll initialize some variables, pin modes, and libraries. For this project, I've opted for the Bluetooth shield to communicate over pins 6 and 7. After the bluetooth object has been constructed, we'll need to initialize it. That happens in setupBlueToothConnection()
. Codes in that function are described here. This all looks like:
#include <SoftwareSerial.h>
#include <ArduinoMotorShieldR3.h>
#define btTx 6
#define btRx 7
SoftwareSerial bluetoothSerial(btRx, btTx);
ArduinoMotorShieldR3 motor;
void setup()
{
Serial.begin(19200);
setupBlueToothConnection();
motor.init();
motor.setM1Speed(0);
pinMode(4, OUTPUT);
pinMode(5, OUTPUT);
}
The loop()
function runs consecutively, allowing the program to change, respond, and actively control the Arduino board. With each iteration, it first checks to make sure the Bluetooth shield is listening (for data coming from the Android device). If it is, we call readBytesUntil()
, which blocks until the terminator character is detected, the determined length has been read, or it times out (1 second default). When that function returns and it has data to process, we look it over to make sure it is what we are expecting. The only characters we are interested in are the digits 0-9, a dash, an asterisk, or a pound sign.
If an asterisk is found, the digit immediately following tells whether to turn the white LED on or off. If a pound sign is found, the digit immediately following tells whether to turn the red LED on or off. Otherwise, we treat what's found as a [negative] number representing a speed and write that to channel A (motor 1). This process continues indefinitely. The code for this looks like:
void loop()
{
if (bluetoothSerial.isListening())
{
char bufferIn[10] = {0};
int count = bluetoothSerial.readBytesUntil('\n', bufferIn, 10);
if (count > 0)
{
bufferIn[count] = '\0';
bool bProceed = true;
for (int x = 0; x < strlen(bufferIn) && bProceed; x++)
{
if (! isDesiredCharacter(bufferIn[x]))
bProceed = false;
}
if (bProceed)
{
if (bufferIn[0] == '*')
{
if (bufferIn[1] == '0')
digitalWrite(5, 0);
else if (bufferIn[1] == '1')
digitalWrite(5, 1);
}
else if (bufferIn[0] == '#')
{
if (bufferIn[1] == '0')
digitalWrite(4, 0);
else if (bufferIn[1] == '1')
digitalWrite(4, 1);
}
else
{
String str = bufferIn;
int speed = str.toInt();
motor.setM1Speed(speed);
}
}
}
}
}
bool isDesiredCharacter( char c )
{
return (isDigit(c) || c == '-' || c == '*' || c == '#');
}
void setupBlueToothConnection()
{
bluetoothSerial.begin(38400);
bluetoothSerial.print("\r\n+STBD=38400\r\n");
bluetoothSerial.print("\r\n+STWMOD=0\r\n");
bluetoothSerial.print("\r\n+STNA=SeeedBTSlave\r\n");
bluetoothSerial.print("\r\n+STOAUT=1\r\n");
bluetoothSerial.print("\r\n+STAUTO=0\r\n");
delay(2000);
bluetoothSerial.print("\r\n+INQ=1\r\n");
delay(2000);
bluetoothSerial.flush();
}
After this sketch is sent to the Arduino board, it will initialize and start listening for data over Bluetooth. Now let's get things going on the Android side.
Android
The Java code running on the Android device is only slightly more complicated. There are the UI components to contend with, the device's Bluetooth adapter, and the actual connection between the Arduino board and the Android device. All of those must play nicely together.
In the activity's onCreate()
method, one of the first things it does is enable the Bluetooth adapter if it's not already enabled. Before allowing this, the app will ask for permission like:
The code, specifically the Intent
, to accomplish this looks like:
mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
if (mBluetoothAdapter != null)
{
if (! mBluetoothAdapter.isEnabled())
{
Intent intent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
startActivity(intent);
}
else
{
tvBluetoothStatus.setText("Enabled");
if (! MainApplication.isSocketConnected())
{
btnConnect.setEnabled(true);
Toast.makeText(this, "Click the Connect button to connect to a device.", Toast.LENGTH_LONG).show();
}
}
}
else
{
tvBluetoothStatus.setText("Not supported");
showDialog(this, "Unsupported", "Bluetooth is not supported on this device.");
}
Once enabled, our UI now looks the following. Note that the only UI control that is enabled is the Connect button.
Unlike startActivityForResult()
, you will not receive any information when startActivity()
exits. However, the BluetoothAdapter.ACTION_REQUEST_ENABLE
intent makes an exception for this. Notification is posted to the onActivityResult()
callback. The resultCode
will be RESULT_OK
if Bluetooth has been turned on or RESULT_CANCELED
if the user has rejected the request or an error has occurred. Instead, this app will be listening for the ACTION_STATE_CHANGED
notification whenever Bluetooth is turned on or off. We'll set this up, along with other notifications, with a BroadcastReceiver
.
In this project, we are interested in the following notifications: when the Bluetooth adapter is 1) enabled or 2) disabled, and 3) when a Bluetooth device is connected or disconnected. These notifications will be handled by the BTReceiver
class, which will be discussed later. Setting up a receiver for these looks like:
try
{
unregisterReceiver(receiver);
}
catch(IllegalArgumentException iae)
{
}
catch(Exception e)
{
Log.e(TAG, "Unregistering BT receiver: " + e.getMessage());
}
receiver = new BTReceiver(this);
IntentFilter filter = new IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED);
registerReceiver(receiver, filter);
filter = new IntentFilter(BluetoothDevice.ACTION_ACL_CONNECTED);
registerReceiver(receiver, filter);
filter = new IntentFilter(BluetoothDevice.ACTION_ACL_DISCONNECTED);
registerReceiver(receiver, filter);
You may be wondering about the call to unregisterReceiver()
. When I first started working on this project, I coded it up and tested it on my Android device in portrait mode. I guess I tilted it a little too far one day and it switched to landscape mode. Hmmmm. The layout adjusted nicely, and things seemed to look in order, but when I went to interact with the UI, there was no Bluetooth communication with the Arduino. Ugh! One of the issues I found (yes, there were several) was the receiver was no longer receiving. No exceptions were being thrown, but still no notifications. Since I was reregistering the Intents in the onCreate()
method, I guessed that the initial Intents were being masked by subsequent creations. Calling unregisterReceiver()
not only unregisters a previously registered receiver, it also removes all filters (i.e., Intents) that have been registered for the receiver. Now I could change the device's orientation and still receive Bluetooth notifications.
The next issue I discovered had to do with the Bluetooth connection itself. Initially I had the BluetoothSocket
object as part of the activity. As long as there was no connection, restarting the activity was fine. After a connection, however, when the Android device's orientation changed and the onCreate()
method was called again, the socket would get re-created which resulted in no Bluetooth communication with the Arduino. I first thought about trying to save/load the socket object in the onSaveInstanceState()
and onRestoreInstanceState()
methods. Sounded good in theory, but it did not produce the desired result. I then decided to put the BluetoothSocket
object in the application class itself. Now when the Android device's orientation changes and the activity gets re-created, the one-and-only application instance keeps the BluetoothSocket
object in place.
The last issue I observed had to do with the DC motor or the LEDs being on while the Android device's orientation changed. During activity re-creation, the UI controls would be set to their default "off" state, yet the Arduino would still be powering the DC motor or LEDs. To solve this (i.e., make the UI match), I was able to use the onSaveInstanceState()
and onRestoreInstanceState()
methods. This looks like:
@Override
protected void onSaveInstanceState( Bundle outState )
{
Log.d(TAG, "onSaveInstanceState()");
super.onSaveInstanceState(outState);
int speed = seekbar.getProgress();
outState.putInt("SPEED", speed);
boolean white = ledWhite.isChecked();
outState.putBoolean("WHITE", white);
boolean red = ledRed.isChecked();
outState.putBoolean("RED", red);
}
@Override
protected void onRestoreInstanceState( Bundle savedInstanceState )
{
Log.d(TAG, "onRestoreInstanceState()");
super.onRestoreInstanceState(savedInstanceState);
if (mBluetoothAdapter.isEnabled() && MainApplication.isSocketConnected())
{
Log.d(TAG, "The device was rotated. Setting the motor and LEDs to what they were before rotation.");
tvDeviceStatus.setText("Connected to " + MainApplication.strDeviceName);
int speed = savedInstanceState.getInt("SPEED");
seekbar.setProgress(speed);
boolean white = savedInstanceState.getBoolean("WHITE");
ledWhite.setChecked(white);
boolean red = savedInstanceState.getBoolean("RED");
ledRed.setChecked(red);
}
else
{
Log.d(TAG, "The device was rotated but is neither enabled nor connected. Resetting the motor and LEDs");
seekbar.setProgress(50);
ledWhite.setChecked(false);
ledRed.setChecked(false);
}
}
Quite a bit of the above code and text dealt with a connection, which I haven't shown yet. I sort of put the cart before the horse, so to speak. As was mentioned earlier, once the Bluetooth adapter is enabled, the Connect button will be available. When clicked, it will start a secondary activity showing the list of previously paired devices:
This list is obtained using code like:
ArrayList<String> arrItems = new ArrayList<String>();
BluetoothAdapter btnAdapter = BluetoothAdapter.getDefaultAdapter();
Set<BluetoothDevice> pairedDevices = btnAdapter.getBondedDevices();
if (pairedDevices.size() > 0)
{
for (BluetoothDevice device : pairedDevices)
arrItems.add(device.getName() + "\n" + device.getAddress());
adapter.notifyDataSetChanged();
}
As was shown in the Arduino code, the name of our Bluetooth device is SeeedBTSlave. When that device is clicked, in the onItemClicked()
handler we package its MAC address in an Intent
to pass back to the main activity. The main activity receives this response in onActivityResult()
, like:
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data)
{
super.onActivityResult(requestCode, resultCode, data);
Log.d(TAG, getClass().getName() + "::onActivityResult(" + resultCode + ")");
if (requestCode == 1 && resultCode == RESULT_OK)
{
String strAddress = data.getExtras().getString("device_address");
Connect(strAddress);
}
}
With the MAC address of the device to connect to, we can now do the actual connecting. When I initially put the connecting code together, it was natural to just use the BluetoothDevice.createRfcommSocketToServiceRecord()
method to create a socket and the BluetoothSocket.connect()
method to connect to it. This worked, but only part of the time. I kept receiving sporadic java.io.IOException: read failed, socket might closed or timeout, read ret: -1 errors. After a bit of research, I found others that were experiencing the same problem. While not 100% reliable and no guarantee of it remaining available, several of those found success by using Java reflection to get access to a createRfcommSocket()
function. Using that code as a fallback method, I changed my connect()
function to look like:
private void Connect( String address )
{
Log.d(TAG, getClass().getName() + "::Connect()");
if (mBluetoothAdapter == null || ! mBluetoothAdapter.isEnabled())
{
Log.e(TAG, " Bluetooth adapter is not enabled.");
return;
}
BluetoothDevice device = mBluetoothAdapter.getRemoteDevice(address);
Toast.makeText(this, "Connecting to device " + device.getName() + "...", Toast.LENGTH_LONG).show();
mBluetoothAdapter.cancelDiscovery();
try
{
MainApplication.btSocket = device.createRfcommSocketToServiceRecord(BT_UUID);
MainApplication.btSocket.connect();
}
catch(IOException ioe)
{
Log.e(TAG, " Connection failed. Trying fallback method...");
try
{
MainApplication.btSocket = (BluetoothSocket) device.getClass().getMethod("createRfcommSocket", new Class[] {int.class}).invoke(device, 1);
MainApplication.btSocket.connect();
}
catch(Exception e)
{
Log.e(TAG, " Connection failed.");
showDialog(this, "Socket creation failed", e.getMessage() + "\n\nThe remote device may need to be restarted.");
}
}
}
This way, if the primary connection method fails, we try the fallback method. If that fails, too, we just pack up and go home!
Now that we have requested a connection, it's time to start listening for notifications. Earlier we registered a receiver with three filters (i.e., Intents). This is handled via a BroadcastReceiver
extension. I chose to put this extension in its own file rather than make it an internal class to the activity. One of the caveats of this is that no UI interaction could take place. In other words, when the Bluetooth adapter was enabled or disabled, or a device became disconnected, I could not update any of the status controls on the UI. One solution to this was to use an interface
. This class looks like:
public class BTReceiver extends BroadcastReceiver
{
private static String TAG = "Test2";
private Callback mCallback = null;
public interface Callback
{
public void updateStatus( String strAction, int nState, String strDevice );
}
public BTReceiver( Activity activity )
{
if (! (activity instanceof Callback))
throw new IllegalStateException("Activity must implement 'Callback' interface.");
Log.d(TAG, "BTReceive()");
mCallback = (Callback) activity;
}
@Override
public void onReceive( Context context, Intent intent)
{
Log.d(TAG, getClass().getName() + "::onReceive()");
try
{
String strAction = intent.getAction();
if (strAction.equals(BluetoothAdapter.ACTION_STATE_CHANGED))
{
int nState = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, BluetoothAdapter.ERROR);
mCallback.updateStatus(strAction, nState, "");
}
else if (strAction.equals(BluetoothDevice.ACTION_ACL_CONNECTED))
{
BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
MainApplication.strDeviceName = device.getName();
mCallback.updateStatus(strAction, -1, MainApplication.strDeviceName);
}
else if (strAction.equals(BluetoothDevice.ACTION_ACL_DISCONNECTED))
mCallback.updateStatus(strAction, -1, "");
}
catch(Exception e)
{
Log.e(TAG, e.getMessage());
}
}
}
Once a connection has been established, the UI looks like the following with all available controls enabled. You can click the Disconnect button to disconnect from the Arduino. You can slide the SeekBar
left and right to control the motor's speed. You can turn the red and white LEDs on and off with the corresponding Switch
.
When Bluetooth notifications are received, we can simply call an interface method that is being implemented by the activity. In the updateStatus()
implementation, we do different things depending on which of the notifications was received. If the Bluetooth device was either connected or disconnected, we just update a status control on the UI (like above). The state of the adapter can change in one of four ways: STATE_TURNING_ON
, STATE_ON
, STATE_TURNING_OFF
, and STATE_OFF
. If the adapter is turning on, we simply display a brief Toast
message. Most of the time you may not even see this message because the activity that is enabling the adapter still has its own messages on the screen. Once the adapter is actually on, we update a status control on the UI and display a brief Toast
message as a reminder to click the Connect button now that a connection can be made. If the adapter is turning off, we send 'stop' messages to the DC motor and the two LEDs. These messages are sent before the STATE_OFF
notification so they have time to be processed before the Bluetooth socket is closed. And lastly, if the adapter is actually off, we update a status control on the UI and close the Bluetooth socket. Code for all of this looks like:
@Override
public void updateStatus( String strAction, int nState, String strDevice )
{
Log.d(TAG, getClass().getName() + "::updateStatus()");
try
{
if (strAction.equals(BluetoothAdapter.ACTION_STATE_CHANGED))
{
if (nState == BluetoothAdapter.STATE_TURNING_ON)
Toast.makeText(MainActivity.this, "Bluetooth is turning on...", Toast.LENGTH_LONG).show();
else if (nState == BluetoothAdapter.STATE_ON)
{
tvBluetoothStatus.setText("Enabled");
Toast.makeText(MainActivity.this, "Click the Connect button to connect to a device.", Toast.LENGTH_LONG).show();
}
else if (nState == BluetoothAdapter.STATE_TURNING_OFF)
{
Toast.makeText(MainActivity.this, "Bluetooth is turning off...", Toast.LENGTH_LONG).show();
seekbar.setProgress(50);
ledWhite.setChecked(false);
ledRed.setChecked(false);
}
else if (nState == BluetoothAdapter.STATE_OFF)
{
tvBluetoothStatus.setText("Disabled");
if (MainApplication.btSocket != null)
{
MainApplication.btSocket.close();
MainApplication.btSocket = null;
}
}
}
else if (strAction.equals(BluetoothDevice.ACTION_ACL_CONNECTED))
{
tvDeviceStatus.setText("Connected to " + strDevice);
Toast.makeText(MainActivity.this, "Slide the bar left or right to control the speed of the motor.", Toast.LENGTH_LONG).show();
}
else if (strAction.equals(BluetoothDevice.ACTION_ACL_DISCONNECTED))
tvDeviceStatus.setText("Disconnected");
updateControls();
}
catch(Exception e)
{
Log.e(TAG, e.getMessage());
}
}
There are three other handlers (or two since two of them are nearly identical) we need to discuss: the DC motor and the two LEDs. The DC motor's speed is controlled by a SeekBar
, and the two LEDs are each controlled by a Switch
. For the latter, it's a simple matter of sending the appropriate bytes to the Arduino. The first of the two bytes is either an asterisk for the white LED or a pound sign for the red LED. The second of the two bytes is either 0 for off or 1 for on. Code for these two handlers looks like:
ledWhite = (Switch) findViewById(R.id.ledWhite);
ledWhite.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener()
{
@Override
public void onCheckedChanged( CompoundButton buttonView, boolean isChecked )
{
Log.d(TAG, "White LED checked = " + isChecked);
if (isChecked)
writeData("*1");
else
writeData("*0");
}
});
ledRed = (Switch) findViewById(R.id.ledRed);
ledRed.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener()
{
@Override
public void onCheckedChanged( CompoundButton buttonView, boolean isChecked )
{
Log.d(TAG, "Red LED checked = " + isChecked);
if (isChecked)
writeData("#1");
else
writeData("#0");
}
});
Those two are pretty straightforward. For the DC motor. however, it's not any more complicated per se, but there is the issue of mapping the motors' -400 to +400 range to the SeekBar
's 0-100 range. When the SeekBar
is in the 50% position, that maps to 0 (off) for the DC motor. When the SeekBar
is in the 0% or 100% position, that maps to -400 or +400 for the DC motor. Consider this illustration:
SeekBar position 0% 25% 50% 75% 100%
DC motor value -400 -200 0 200 400
The way I chose to go about translating one value to the other is first getting the range of the DC motor values, or 800 in this case. When the onProgressChanged()
method is called, one of the parameters passed is the percent (i.e., progress). It's a whole number so we'll need to divide it by 100 to get it down to a decimal. Now we can multiply this decimal percent by the above range to get how far we are into that range. For example, if the SeekBar
is at 25 percent, we should be 25 percent of the way into 800, which is 200. The value is right but not the sign. Since 25 percent is to the left of 0, our answer should be negative. The way to fix that is to simply add the motor's minimum value of -400 to our answer. So, -400 + 200 equals -200, which is the 25 percent value. Similarly, if the SeekBar
is at 75 percent, we should be 75 percent of the way into 800, which is 600. The sign is right but not the value. Again, by adding the motor's minimum value of -400 to our answer, we get -400 + 600 equals 200, which is the 75 percent value. With that, the code to set the motor's speed looks like:
int MIN = -400;
int MAX = 400;
int SPAN = MAX - MIN;
@Override
public void onProgressChanged( SeekBar seekBar, int progress, boolean fromUser )
{
Log.d(TAG, "Progress = " + progress + "%");
double dProgress = progress / 100.0;
String strSpeed = String.format(Locale.getDefault(), "%d", (int) Math.floor(MIN + (SPAN * dProgress)));
tvSpeed.setText(strSpeed);
writeData(strSpeed);
}
Something that I noticed part way through this exercise is that the listeners for the SeekBar
and both of the Switch
controls are notified whether a change is made from the UI or from code. For example, I initially had code in place that would send the 'off' command to the white LED, and then turn the white LED switch off. I later discovered that I could remove the code that was sending the 'off' command to the white LED and just use the code that was turning the 'white' switch off. The handler for the 'white' switch would take care of sending the 'off' command to the white LED. Doing this for both LEDs and for the DC motor allowed me to have one single location (rather than multiple) that sent data over Bluetooth to the Arduino. Sort of reminds me of a quote I saw on a poster over 20 years ago by French writer Antoine de Saint-Exupery, "Perfection is achieved not when there is nothing more to add, but when there is nothing left to take away."
With both LEDs on and the motor spinning along rather briskly, our screen finally looks like:
Conclusion
There is undoubtedly lots of room for improvement with this project, but hopefully I was able to successfully show how to control an Arduino motor shield with an Android device over Bluetooth.
Enjoy!