7. Peer-to-Peer Networking Using Bluetooth and Wi-Fi Direct
In this chapter, we’ll give peer-to-peer (P2P
) networks the attention they deserve. We’ve mastered the exchange of data between Android devices using Wi-Fi. Now it’s time to end our dependence on wireless infrastructure. Popular services such as Skype and BitTorrent are only two examples that use peer-to-peer technology. However, the concept of P2P communication doesn’t stop with telephony or file sharing and has little do with copyright.
P2P networking has several advantages. First of all, it’s free. We don’t require a carrier network or access to Wi-Fi
infrastructure, and we won’t be restricted by data quotas. P2P still works if wireless infrastructure is unavailable or overwhelmed due to high demand, for instance. It uses less power due to its short range and can help protect privacy because information remains decentralized. And finally, information flows directly from one device to the other—we can control the information flow and choose whether data is saved or retained.
P2P communication between two devices doesn’t preclude us from also reaching out to web or cloud servers. For example, if we are connected P2P while we are on the move, we can update an online database as soon as a carrier network becomes available. Both networking paradigms can coexist and complement each other. P2P has the advantage that it can reliably provide us with instantaneous feedback across devices due to the very small lag time, and it provides the transmission rates that are crucial for some multiuser or multiplayer apps.
The most common way to implement P2P exchanges of data between Android devices is to use Bluetooth, which is available on all Android devices shipping today. We’ll start by building a remote cursor app, where we visualize the cursor position of two devices connected via Bluetooth. Then we’ll build a peer-to-peer survey app that lets us create a poll for multiple users. Connected via Bluetooth, users pick answers from a common multiple choice questionnaire displayed on their device screens. Individual answers are gathered and visualized in real time, giving each user immediate feedback on the collective response and the distribution of answers as they accumulate.
Then we’ll move on to Wi-Fi Direct, an emerging peer-to-peer networking standard where each device can serve as the Wi-Fi access point for the network. We’ll revisit the remote cursor app and modify it to use Wi-Fi Direct so that we can directly compare its performance to Bluetooth. Wi-Fi Direct is designed to provide a higher bandwidth and better network range than Bluetooth. To get started, let’s first take a look at the main classes we’ll use in this chapter.
Introducing Short-Range Networking and UI Classes
For the apps we’ll develop in this chapter, we’ll use the following networking and UI classes from the Ketai library:
KetaiBluetooth
A Ketai class for working with Bluetooth on Android devices—the class contains the necessary methods for Bluetooth discovery, pairing, and communication using the popular Bluetooth standardKetaiWiFiDirect
A Ketai class to simplify working with Wi-Fi Direct on Android devices—the class contains the necessary methods for Wi-Fi Direct peer discovery and data exchange. In a Wi-Fi Direct network, every Wi-Fi Direct-enabled device can serve as the access point for the other devices in the Wi-Fi network.KetaiOSCMessage
A Ketai class that is identical to the oscP5 library’sOscMessage
class we worked with in Using the Open Sound Control Networking Format, with the difference being that it allows us to createKetaiOSCMessage
using a byte array—it makes some private methods inOscMessage
public so we can use it for Bluetooth communication.KetaiList
A Ketai UI class that makes it easier to work with the native AndroidListView
widget—this class contains methods to populate, display, refresh, and retrieve strings from a selected list item. AKetaiList
can be created using aString
array or aString
ArrayList
.KetaiKeyboard
A class included in the KetaiUI
package that allows us to toggle the Android software keyboard on and off without importing additional Android UI classes
We’ll start with Bluetooth because it’s the most ubiquitous peer-to-peer technology. Let’s take a closer look at the Bluetooth methods that Ketai provides.
Working with the KetaiBluetooth Class
Besides the usual start()
and stop()
methods, KetaiBluetooth
provides the following methods for discovering, pairing, and connecting Bluetooth devices:
onBluetoothDataEvent()
Returns data sent via Bluetooth, including the device name where it originated as aString
(useful when more than one Bluetooth device is connected) and the Bluetooth data asbyte[]
arraymakeDiscoverable()
Makes a Bluetooth device discoverable for 300 secondsdiscoverDevices()
Scans for discoverable Bluetooth devicesgetDiscoveredDeviceNames()
Returns a list of all Bluetooth devices found within range of the Bluetooth radioconnectToDeviceByName()
Connect to a device using its assigned Bluetooth namebroadcast()
Writes data to all connected Bluetooth devicesgetPairedDeviceNames()
Provides a list of devices that have been successfully paired with the Android device—paired devices can reconnect automatically if they are discoverable and within range, and they do not need to repeat the pairing process.
Now that we’ve seen the classes and methods that we’ll use to build apps in this chapter, let’s now take a closer look at Bluetooth, the most ubiquitous peer-to-peer standard.
Introducing Bluetooth
Every Android device ships with a Bluetooth radio. It is a popular communication standard used for consumer electronics, health and wellness devices, PC peripherals, sports and fitness equipment, and smart home appliances, and it is typically found in wireless game controllers, headsets, keyboards, printers, and heart rate sensors, to name a few. Because it is so ubiquitous, it will remain an important protocol even as new networking standards emerge.
The standard is managed by the Bluetooth Special Interest Group, which includes more than sixteen thousand member companies. “Bluetooth” refers to a tenth-century Danish king who united dissonant tribes into a single kingdom. The implication is that Bluetooth has done the same for the device and accessory market, uniting communication protocols into one universal standard.
Bluetooth uses short-wavelength radio frequencies of 2400-2483.5 MHz and allows us to transfer data within a range of about 30 feet. It requires relatively little power and rarely experiences interference from other devices. But before we can transfer data, we must first pair the devices involved. Once we’ve done that successfully, the Android stores a list of known devices, which we can use to reconnect them without pairing them again. If we already know the unique 48-bit address of the Bluetooth device to which we’d like to connect, we can skip the pairing process entirely.
If the Bluetooth radio is powered on, any Bluetooth-enabled device can send an inquiry to initiate a connection. If a device is discoverable, it sends information about itself to other Bluetooth devices within reach, including its own device name, allowing the devices to pair and exchange data securely. If we send data between Android devices, pairing and acceptance of the connection by the device owner is required for security reasons.
Let’s now connect two Android devices via Bluetooth.
Working with the Android Activity Life Cycle
When we launch a Processing sketch as an app on the Android, we create an Android activity ready for us to interact via the touch screen interface. We typically don’t need to deal with the activity life cycle on the Android because Processing takes care of it for us. To activate Bluetooth (and later NFC
), we need to initialize the Bluetooth object we’ll be working with at the very beginning of the activity life cycle.
When a new app or activity starts up, Android adds it to the stack of activities already running and places it on top of the stack to run in the foreground. The previous activity goes in the background and will come to the foreground again when the current activity exits.
We can summarize the four states an activity can take like this:
The activity is active and running in the foreground on top of the stack.
The activity lost focus because another non-fullscreen activity runs on top of the activity.
The activity stopped because another activity is covering it completely.
The paused or stopped activity is killed to make memory available for the active activity.
When an activity goes though this life cycle, Android provides the following callback methods for us to use. When the activity starts up, Android calls the following:
onCreate() | Called when the activity is starting |
onStart() | Called after onCreate() when the activity starts up—if the activity is already running, onRestart() is called before onStart() . |
onResume() | Called after onStart() when the activity becomes visible |
After onResume()
, the activity is running in the foreground and active. If we launch another activity, Android calls these methods:
onPause() | Called when another activity comes in the foreground |
onStop() | Called after onPause() when the activity is no longer visible |
onDestroy() | Called after onStop() when the activity is finishing or destroyed by the system |
Enabling Bluetooth
To work with Bluetooth for the Bluetooth apps we’ll create in this chapter, we will need to launch a new activity to initialize our Bluetooth right at the beginning when the activity starts up using onCreate()
. Once Bluetooth is active, this activity returns to us the Bluetooth object we need via onActivityResult()
, which is called when the app starts up immediately before onResume()
in the activity life cycle. We’ll look at the code to enable Bluetooth in more detail in this P2P/BluetoothCursors/EnableBluetooth.pde.
For the projects in this book, we’ll need to deal with the life cycle only for Bluetooth and NFC
. We’ll work more with the activity life cycle in Enable NFC and Bluetooth in the Activity Life Cycle, where we initiate NFC
and Bluetooth. For all other apps, we can let Processing handle the life cycle. Future versions of Processing might allow libraries to work with lifecycle callback methods, so we don’t need to include such code inside the sketch.
Connect Two Android Devices via Bluetooth
In the following sketch, we’ll work with three tabs. The main tab, BluetoothCursors
, contains our usual setup()
and draw()
methods and global variables. The second tab, EnableBluetooth
, contains code that is necessary to enable Bluetooth on startup, registering our Bluetooth class when the so-called Android activity is created (this step might not be necessary in future versions of Processing). Processing allows us to not dive too deep into the Android application life cycle, and we’ll try to keep it that way. The third tab, called UI
, contains all the code we’ll use for GUI elements like menus, an Android list to select Bluetooth devices, and the software keyboard to enter user input. When the sketch is complete, we’ll get a screen similar to the one shown in Figure 7.0.
Figure 7.0 - Bluetooth Cursors app.
The illustration shows the local (white) and the remote (red) mouse pointer positions, marked as ellipses on the screen. The software keyboard is made visible using the keyboard tab shown at the top. Also shown are the Bluetooth and Interact tabs, which we use to interact with the cursors.
The code needs to facilitate the Bluetooth pairing process as follows: We start by making both Androids discoverable for Bluetooth by listing discovered devices. Then we choose the device to connect to from the list of discovered devices (you might be surprised to see what shows up). Finally, we pair and connect the devices to transfer the data via OSC, which we’ve already used in Using the Open Sound Control Networking Format. We’ll need to confirm the Bluetooth connection in a popup window because we will be connecting the devices for the first time.
We’ll use the Android software keyboard to discover other Bluetooth devices, make the device itself discoverable, connect to another device, list already paired devices, and show the current Bluetooth status. To work with the keyboard, we’ll use the KetaiKeyboard
class. And to show and pick discoverable Bluetooth devices to connect to, we’ll use the KetaiList
class, making it easy for us to work with a native Android list without importing additional packages.
Working with a KetaiList
We can create a KetaiList
object using either a String
array or a String
ArrayList
. Because an ArrayList
stores a variable number of objects and we have a variable number of discoverable Bluetooth devices to connect to, it’s the better choice for us here. We can easily add or remove an item dynamically in an ArrayList
, and because we work with Bluetooth device names in our sketch, we’ll create an ArrayList
of type String
.
Andreas Schlegel has updated his excellent ControlP5 UI library (as of 09/2012) to also work with the Android mode in Processing 2, making it a great tool to develop all custom aspects of UI elements, such as controllers, lists, sliders, buttons, and input forms. Although the UI elements do not use Android’s native UI classes, ControlP5 elements can be fully customized to match the look and feel of your app while still maintaining consistency with the Android’s UI style guide.
Let’s get started with the main tab of our BluetoothCursors
sketch.
code/P2P/BluetoothCursors/BluetoothCursors.pde
// Required Bluetooth methods on startup
import android.os.Bundle; // 1
import android.content.Intent; // 2
import ketai.net.bluetooth.*;
import ketai.ui.*;
import ketai.net.*;
import oscP5.*;
KetaiBluetooth bt; // 3
KetaiList connectionList; // 4
String info = ""; // 5
PVector remoteCursor = new PVector();
boolean isConfiguring = true;
String UIText;
void setup()
{
orientation(PORTRAIT);
background(78, 93, 75);
stroke(255);
textSize(48);
bt.start(); // 6
UIText = "[b] - make this device discoverable\n" + // 7
"[d] - discover devices\n" +
"[c] - pick device to connect to\n" +
"[p] - list paired devices\n" +
"[i] - show Bluetooth info";
}
void draw()
{
if (isConfiguring)
{
ArrayList<String> devices; // 8
background(78, 93, 75);
if (key == 'i')
info = getBluetoothInformation(); // 9
else
{
if (key == 'p')
{
info = "Paired Devices:\n";
devices = bt.getPairedDeviceNames(); // 10
}
else
{
info = "Discovered Devices:\n";
devices = bt.getDiscoveredDeviceNames(); // 11
}
for (int i=0; i < devices.size(); i++)
{
info += "["+i+"] "+devices.get(i).toString() + "\n"; // 12
}
}
text(UIText + "\n\n" + info, 5, 200);
}
else
{
background(78, 93, 75);
pushStyle();
fill(255);
ellipse(mouseX, mouseY, 50, 50);
fill(0, 255, 0);
stroke(0, 255, 0);
ellipse(remoteCursor.x, remoteCursor.y, 50, 50); // 13
popStyle();
}
drawUI();
}
void mouseDragged()
{
if (isConfiguring)
return;
OscMessage m = new OscMessage("/remoteMouse/"); // 14
m.add(mouseX);
m.add(mouseY);
bt.broadcast(m.getBytes()); // 15
// use writeToDevice(String _devName, byte[] data) to target a specific device
ellipse(mouseX, mouseY, 20, 20);
}
void onBluetoothDataEvent(String who, byte[] data) // 16
{
if (isConfiguring)
return;
KetaiOSCMessage m = new KetaiOSCMessage(data); // 17
if (m.isValid())
{
if (m.checkAddrPattern("/remoteMouse/"))
{
if (m.checkTypetag("ii")) // 18
{
remoteCursor.x = m.get(0).intValue();
remoteCursor.y = m.get(1).intValue();
}
}
}
}
String getBluetoothInformation() // 19
{
String btInfo = "Server Running: ";
btInfo += bt.isStarted() + "\n";
btInfo += "Discovering: " + bt.isDiscovering() + "\n";
btInfo += "Device Discoverable: "+bt.isDiscoverable() + "\n";
btInfo += "\nConnected Devices: \n";
ArrayList<String> devices = bt.getConnectedDeviceNames(); // 20
for (String device: devices)
{
btInfo+= device+"\n";
}
return btInfo;
}
Here are the steps we need to take.
Import the
os.Bundle
Android package containing theonCreate()
method we need to initialize Bluetooth.Import the
content.Intent
Android package containing theonActivityResult()
, which is called when the app starts up.Create a
KetaiBluetooth
type variable,bt
.Create a
KetaiList
variable that we’ll use to select the device to connect to.Create a
String
variable,info
, to store changing status messages that we want to output to the Android screen.Start the
bt
Bluetooth object.Provide instructions for connecting to the other device using the keyboard.
Create an
ArrayList
of typeString
to store the Bluetooth device,devices
.Retrieve a Bluetooth status update and assign it to our
info
variable for screen output when we pressi
on the keyboard.Get a list of paired devices and assign it to the
devices
ArrayList
to update theKetaiList
when we press thep
on the keyboard.Get a list of discovered devices and assign it to the
devices
ArrayList
to update theKetaiList
when we pressd
on the keyboard.Append each Bluetooth device entry in
devices
to ourinfo
text output, converting each individualArrayList
item into human-readable text using thetoString()
method.Use the
x
andy
components of theremoteCursor
PVector
, which stores the remote cursor location, and draw an ellipse at the exact same x and y location.Create a new
OSC
messagem
to add ourmouseX
andmouseY
cursor position to.Broadcast the
OSC
message containing the cursor position to all connected devices using theOSC
broadcast()
method. Alternatively, we can usewriteToDevice(String _devName, byte[] data)
to send the message to only one specific device.Receive the
byte[]
array when the new Bluetoothdata
is sent from the remote device.Receive the
data
as anOSC
message.Check if the
OSC
message contains two integer values for the mouse x and y position. We’ve also checked if theOSC
message is valid and if the message we’ve sent contains the label “remoteCursor.”Return a
String
containing Bluetooth status info, including if BluetoothisStarted()
andisDiscoverable()
, as well as the individual names of connected Bluetooth devices.Get a list of all connected Bluetooth devices using
getConnectedDeviceNames()
.
We’ve completed the crucial components of our sketch in setup()
and draw()
. To enable Bluetooth when the app starts up, we’ll need to work with the activity life cycle as described in Working with the Android Activity Life Cycle. We’ll put the code to enable Bluetooth into the tab named EnableBluetooth
. Let’s take a look.
code/P2P/BluetoothCursors/EnableBluetooth.pde
void onCreate(Bundle savedInstanceState) { // 1
super.onCreate(savedInstanceState);
bt = new KetaiBluetooth(this); // 2
}
void onActivityResult(int requestCode, int resultCode, Intent data) {
bt.onActivityResult(requestCode, resultCode, data); // 3
}
These are the steps we need to take to enable Bluetooth.
Use the Android
onCreate()
method to initialize our Bluetooth object. The method is called when the activity is starting.Initialize the Bluetooth object
bt
when the activity is created.Return the
bt
object to the sketch usingonActivityResult()
, called right beforeonResume()
in the activity life cycle.
We looked at the required onCreate()
and onActivityResult()
methods to initialize Bluetooth at the beginning of the activity.
Programming the UI
Now let’s return to the part of the code that is responsible for all the UI elements, which we’ll put in the UI
tab of the sketch. It takes care of GUI elements and keyboard menu items.
Because the Bluetooth pairing process requires us to select a device from a whole list of discovered devices (you’ll probably be surprised by how many Bluetooth devices are broadcasting around you), we’ll use a KetaiList
to simplify the selection process. We’ll also need the keyboard to make menu selections during the pairing process, and we’ll use the KetaiKeyboard
class to toggle the keyboard on and off. For the KetaiList
, we’ll use the onKetaiListSelection()
method to capture when the user picks an item from the list. And to show and hide the KetaiKeyboard
, we’ll work with the toggle()
method.
Here are the steps to programming the UI.
code/P2P/BluetoothCursors/UI.pde
// UI methods
void mousePressed()
{
if (mouseY <= 100 && mouseX > 0 && mouseX < width/3)
KetaiKeyboard.toggle(this); // 1
else if
(mouseY <= 100 && mouseX > width/3 && mouseX < 2*(width/3)) //config button
isConfiguring=true; // 2
else if
(mouseY <= 100 && mouseX > 2*(width/3) && mouseX < width) // draw button
{
if (isConfiguring)
{
background(78, 93, 75);
isConfiguring=false;
}
}
}
void keyPressed() {
if (key =='c')
{
//If we have not discovered any devices, try prior paired devices
if (bt.getDiscoveredDeviceNames().size() > 0)
connectionList = new KetaiList(this, bt.getDiscoveredDeviceNames()); // 3
else if (bt.getPairedDeviceNames().size() > 0)
connectionList = new KetaiList(this, bt.getPairedDeviceNames()); // 4
}
else if (key == 'd')
bt.discoverDevices(); // 5
else if (key == 'b')
bt.makeDiscoverable(); // 6
}
void drawUI()
{
pushStyle(); // 7
fill(0);
stroke(255);
rect(0, 0, width/3, 100);
if (isConfiguring)
{
noStroke();
fill(78, 93, 75);
}
else
fill(0);
rect(width/3, 0, width/3, 100);
if (!isConfiguring)
{
noStroke();
fill(78, 93, 75);
}
else
{
fill(0);
stroke(255);
}
rect((width/3)*2, 0, width/3, 100);
fill(255);
text("Keyboard", 5, 70); // 8
text("Bluetooth", width/3+5, 70); // 9
text("Interact", width/3*2+5, 70); // 10
popStyle();
}
void onKetaiListSelection(KetaiList connectionList) // 11
{
String selection = connectionList.getSelection(); // 12
bt.connectToDeviceByName(selection); // 13
connectionList = null; // 14
}
Let’s take a look at the steps.
Toggle the Android’s software keyboard using
toggle()
. Make theKetaiKeyboard
visible if it’s hidden; hide it if it’s visible.Set the boolean variable
isConfiguring
totrue
so we can switch to the Bluetooth configuration screen and for the pairing process.Assign the list of paired Bluetooth devices to the
KetaiList
connectionList
, given there are paired devices.Assign the list of discovered Bluetooth devices to the
KetaiList
connectionList
using the String array returned bybt.getDiscoveredDeviceNames()
, given there are discovered devices.Discover Bluetooth devices using
discoverDevices()
when we pressd
on the software keyboard.Make the device discoverable using the
makeDiscoverable()
method.Save the current style settings using
pushStyle()
to preserve the stroke, text size, and alignment for the UI elements. Use the method in combination withpopStyle()
to restore the previous style settings.Draw the Keyboard UI tab.
Draw the Bluetooth UI tab.
Draw the Interact UI tab.
Call the
onKetaiListSelection()
event method when a user selects an item from theKetaiList
.Get the
String
that the user picked from theKetaiList
.Connect to the selected Bluetooth device using the
connectToDeviceByName()
method.Remove all items from the list by setting the
connectionList
object tonull
.Now let’s test the app.
Run the App
Let’s set the correct Android permissions before we run the sketch. Open the Android Permission Selector as we’ve done previously (see Setting Sketch Permissions), and check the following permissions:BLUETOOTH
BLUETOOTH ADMIN
INTERNET
Now connect your first device to your workstation with a USB cable and run the sketch. When the app is compiled, all three tabs in the sketch, BluetoothCursors
, EnableBluetooth
, and UI
, will be compiled into one app. You will see our Bluetooth tab active and the menu options displayed on the screen. Before we interact, let’s install the app on our second Android device.
Disconnect your first device from the USB port, connect your second Android, and install the identical BluetoothCursors
sketch on the device. The sketch launches, and we are ready to pair the two devices.
On your first device (currently disconnected from USB), show the software keyboard by pressing the Keyboard tab. Then press b
on the keyboard. If Bluetooth is turned off for your device (Settings ↦ Bluetooth), you will be prompted to allow Bluetooth, as shown below. Otherwise the device will become discoverable for 300 seconds.
Android 1
[b] - make this device discoverable
[d] - discover devices
[c] - pick device to connect to
[p] - list paired devices
[i] - show Bluetooth info
%%%b
Alert:
An app on your phone wants to
make your phone discoverable
by other Bluetooth devices for
300 seconds.
%%%ALLOW
Now switch to your second device (currently connected to USB), and follow the process of discovering devices. Pick the name of the first Android device:
Android 2
%%%d
Discovered Devices
[0] Nexus 6
%%%c
%%%Nexus 6
Android 1
Bluetooth pairing Request
%%%pair
Android 2
%%%p
Paired Devices
[0] Nexus 6
%%%i
Sever Running: true
Device Discoverable: true
Connected Devices:
Nexus S (78:47:1D:A6:20:48)
When your screen output looks like what’s shown above, the Bluetooth server is running on your second device and you have your first device show up in the list of connected devices. You are now ready to interact.
Press the Interact screen tab on both devices. You’ll see a white dot for the local cursor and a red one for the remote one. As you move your finger over the screen surface of one Android device, observe the other device and see how the red dot moves magically to that position.
Congratulations! You’ve established a data connection between two Android devices using Bluetooth. Now it all depends on your Bluetooth antenna, which should reach a distance of about thirty feet. If you have a friend nearby to test this, try it out. Otherwise, it will not be possible to observe how the connection goes out of range.
The process of discovering and pairing Bluetooth devices can seem cumbersome. However, Bluetooth can’t just accept an incoming connection without confirmation for good security reasons. Once paired, we can reconnect automatically by picking the device address again from the list of paired devices. This is a sketch refinement you can try. If you’d like to “unpair” previously paired devices on your Android, tap the device name under Settings ↦ Bluetooth ↦ Paired Devices, and choose Unpair.
We will implement this remote cursor’s app using Wi-Fi Direct later in this chapter P2P/WiFiDirectCursors/WiFiDirectCursors.pde, and you can then compare how the two standards perform in terms of update rate and wireless range.
Since you’ve mastered peer-to-peer networking using Bluetooth, let’s build on our Bluetooth skills and create a survey app for multiple Bluetooth users.
Create a Survey App Using Bluetooth
We’ll now move on to the chapter project, which is a survey app for multiple users using Bluetooth networking. Such an app is useful for teaching, decision-making, and learning assessments. We’ll build on our Bluetooth skills and put them into practice. We’ll learn how to send different data types via OSC
over Bluetooth, share numeric data across devices in real time, and learn how to work with custom Processing classes.
For this survey app, we’ll broadcast a number of questions that you can imagine as text slides shared by multiple users. All survey participants respond to the questions through each person’s individual device by picking one out of three answers from a multiple choice list. We’ll tally the responses in real time and send an update to all peer devices as illustrated in Figure 7.1 - Bluetooth survey app, giving each user instantaneous feedback on the survey as it unfolds.
Figure 7.1 - Bluetooth survey app.
The illustration shows the Bluetooth server running on one device (left) and a client running on the other (right). The server determines which question is displayed on the client screens by pressing the arrow pointing to the right (next question) and left (previous question).
Both server and client receive real-time feedback on how the question was answered. There are many examples of survey and polling applications that are available online. They typically use proprietary online databases or a dedicated hardware infrastructure. Virtually no app exists using peer-to-peer connectivity on mobile devices—let’s change that. Let’s go ahead and write a survey app using some of the code we’ve already created for the remote cursor app in Connect Two Android Devices via Bluetooth.
For this survey app, we’ll work with four tabs for this sketch: the main tab, which we’ll name BluetoothSurvey
, the EnableBluetooth
tab, which is identical to the tab with the same name P2P/BluetoothCursors/EnableBluetooth.pde, a Question
tab for a custom Processing class we’ll write to take care of our Q & A, and a slightly modified version of the UI
tab, which we developed P2P/BluetoothCursors/UI.pde.
Our approach is as follows. We’ll write a sketch that we’ll load onto every device participating in the survey. This way we can distribute the app without making adjustments for each individual device. The app needs to figure out whether it serves as the Bluetooth server for the other devices or connects as a client. As participants, we then send different messages to the other devices using OSC
(Using the Open Sound Control Networking Format), for example, to request the current question, to submit an answer, or to get an update on the statistics. We’ll give each OSC
message a dedicated label, which we can then use to determine what to do when an OSC
event occurs. When we receive an OSC
message, we check its label using checkAddrPattern()
, and depending on what pattern we detect, we can respond accordingly.
Program the BluetoothSurvey Main Tab
Let’s take a look at our main tab, which contains the following methods: setup()
, draw()
, onBluetoothDataEvent()
, getBluetoothInformation()
, loadQuestions()
, requestQuestions()
, and findQuestion()
.
code/P2P/BluetoothSurvey/BluetoothSurvey.pde
// Required Bluetooth methods on startup
import android.os.Bundle;
import android.content.Intent;
import ketai.net.bluetooth.*;
import ketai.ui.*;
import ketai.net.*;
import oscP5.*;
KetaiBluetooth bt;
KetaiList connectionList;
String info = "";
boolean isConfiguring = true;
String UIText;
color clientColor = color(112, 138, 144);
color serverColor = color(127);
boolean isServer = true;
ArrayList<Question> questions = new ArrayList<Question>();// 1
ArrayList<String> devicesDiscovered = new ArrayList();
Question currentQuestion; // 2
int currentStatID = 0;
Button previous, next;
void setup()
{
orientation(PORTRAIT);
background(78, 93, 75);
stroke(255);
textSize(48);
rectMode(CORNER);
previous = new Button("previous.png", 30, height/2);
next = new Button("next.png", width-30, height/2);
bt.start();
if (isServer)
loadQuestions();
UIText = "[b] - make this device discoverable\n" +
"[d] - discover devices\n" +
"[c] - connect to device from list\n" +
"[p] - list paired devices\n" +
"[i] - show Bluetooth info";
KetaiKeyboard.show(this);
}
void draw()
{
if (isConfiguring)
{
ArrayList<String> devices;
if (isServer)
background(serverColor); //green for server
else
background(clientColor); //grey for clients
if (key == 'i')
info = getBluetoothInformation();
else
{
if (key == 'p')
{
info = "Paired Devices:\n";
devices = bt.getPairedDeviceNames();
}
else
{
info = "Discovered Devices:\n";
devices = bt.getDiscoveredDeviceNames();
}
for (int i=0; i < devices.size(); i++)
{
info += "["+i+"] "+devices.get(i).toString() + "\n";
}
}
text(UIText + "\n\n" + info, 5, 200);
}
else
{
if (questions.size() < 1)
requestQuestions();
if (questions.size() > 0 && currentQuestion == null)
currentQuestion = questions.get(0); // 3
if (isServer)
background(serverColor);
else
background(clientColor);
pushStyle();
fill(255);
stroke(255);
ellipse(mouseX, mouseY, 20, 20);
if (currentQuestion != null)
currentQuestion.display(50, 200);
// text(currentQuestion.toString(), 75, 75); // 4
popStyle();
}
drawUI();
broadcastStats();
}
void onBluetoothDataEvent(String who, byte[] data)
{
// but allows construction by byte array
KetaiOSCMessage m = new KetaiOSCMessage(data);
if (m.isValid())
{
print(" addrpattern: "+m.addrPattern());
//handle request for questions
if (m.checkAddrPattern("/poll-request/")) // 5
{
if (isServer)
{
int lastID = m.get(0).intValue();
for (int j = 0; j < questions.size(); j++)
{
Question q = questions.get(j);
if (q.id <= lastID)
continue;
OscMessage msg = new OscMessage("/poll-question/"); // 6
msg.add(q.id);
msg.add(q.question);
msg.add(q.answer1);
msg.add(q.answer2);
msg.add(q.answer3);
bt.broadcast(msg.getBytes());
}
}
}
else if (m.checkAddrPattern("/poll-question/")) // 7
{
if (isServer)
return;
//id, question, choice a, choice b, choice c
if (m.checkTypetag("issss")) // 8
{
int _id = m.get(0).intValue();
println("processing question id: " + _id);
//we already have this question...skip
if (findQuestion(_id) != null)
return;
Question q = new Question( // 9
m.get(0).intValue(),
m.get(1).stringValue(),
m.get(2).stringValue(),
m.get(3).stringValue(),
m.get(4).stringValue());
questions.add(q);
}
}
else if (m.checkAddrPattern("/poll-answer/")) // 10
{
if (!isServer)
return;
//question id + answer
if (m.checkTypetag("ii")) // 11
{
Question _q = findQuestion(m.get(0).intValue());
if (_q != null)
{
println("got answer from " + who + " for question " +
m.get(0).intValue() + ", answer: " + m.get(1).intValue());
_q.processAnswerStat(m.get(1).intValue()); // 12
OscMessage msg = new OscMessage("/poll-update/");
println("sending poll update for question " + _q.id + "-" +
_q.total1 + "," + _q.total2 + "," + _q.total3);
msg.add(_q.id);
msg.add(_q.total1);
msg.add(_q.total2);
msg.add(_q.total3);
bt.broadcast(msg.getBytes());
}
}
}
//update answer stats
else if (m.checkAddrPattern("/poll-update/") && !isServer) // 13
{
//question id + 3 totals
if (m.checkTypetag("iiii")) // 14
{
int _id = m.get(0).intValue();
Question _q = findQuestion(_id);
if (_q != null)
{
println("got poll update for question " +
_id + " vals " + m.get(1).intValue() + ", " +
m.get(2).intValue() + "," + m.get(3).intValue());
_q.updateStats(m.get(1).intValue(), // 15
m.get(2).intValue(),
m.get(3).intValue());
}
}
}
else if (m.checkAddrPattern("/poll-current-question/") && !isServer)
{
int targetQuestionId = m.get(0).intValue();
Question q = findQuestion(targetQuestionId);
if (q != null)
currentQuestion = q;
}
}
}
String getBluetoothInformation()
{
String btInfo = "BT Server Running: ";
btInfo += bt.isStarted() + "\n";
btInfo += "Device Discoverable: "+bt.isDiscoverable() + "\n";
btInfo += "Is Poll Server: " + isServer + "\n";
btInfo += "Question(s) Loaded: " + questions.size();
btInfo += "\nConnected Devices: \n";
ArrayList<String> devices = bt.getConnectedDeviceNames();
for (String device: devices)
{
btInfo+= device+"\n";
}
return btInfo;
}
void loadQuestions()
{
String[] lines;
lines = loadStrings("questions.txt"); // 16
if (lines != null)
{
for (int i = 0; i < lines.length; i++)
{
Question q = new Question(lines[i]);
if (q.question.length() > 0)
{
q.id = i+1;
questions.add(q); // 17
}
}
}
}
void requestQuestions()
{
//throttle request
if (frameCount%30 == 0)
{
int lastID = 0;
if (questions.size() > 0)
{
Question _q = questions.get(questions.size()-1);
lastID = _q.id;
}
OscMessage m = new OscMessage("/poll-request/"); // 18
m.add(lastID);
bt.broadcast(m.getBytes());
}
}
Question findQuestion(int _id) // 19
{
for (int i=0; i < questions.size(); i++)
{
Question q = questions.get(i);
if (q.id == _id)
return q;
}
return null;
}
void broadcastStats()
{
if (!isServer)
return;
if (frameCount%60 == 0)
{
if ( currentStatID > 0 && currentStatID <= questions.size())
{
Question _q = findQuestion(currentStatID);
if (_q != null)
{
println("sending poll update for question " + _q.id + "-" +
_q.total1 + "," + _q.total2 + "," + _q.total3);
OscMessage msg = new OscMessage("/poll-update/"); // 20
msg.add(_q.id);
msg.add(_q.total1);
msg.add(_q.total2);
msg.add(_q.total3);
bt.broadcast(msg.getBytes());
currentStatID++;
}
}
else {
if (questions.size() > 0)
currentStatID = questions.get(0).id;
}
sendCurrentQuestionID();
}
}
void sendCurrentQuestionID() {
if (currentQuestion == null)
return;
OscMessage msg = new OscMessage("/poll-current-question/");
msg.add(currentQuestion.id);
bt.broadcast(msg.getBytes());
}
These are the main steps we need to take to implement the survey app.
Create an
ArrayList
calledquestions
, which we’ll use to store objects of the customQuestion
class we’ll write to store questions and corresponding answers.Store the current question in a variable,
currentQuestion
, which is presented on all devices simultaneously for the survey.Set the
currentQuestion
to the first question in thequestions
ArrayList
.Present the
currentQuestion
on the screen as formatted outputString
, using our customtoString()
method in theQuestion
class.Check if the
OSC
message we receive viaonBluetoothDataEvent()
contains the label “poll-request.”Create a new
OscMessage
msg
with the label “poll-question” for each object in ourquestions
ArrayList
, add the question and answers stored in theQuestion
object to theOSC
message, andbroadcast()
the message to all connected users.Check if we received an
OSC
message labeled “poll-question.”Check if the
OSC
message labeled “poll-question” contains aninteger
followed by fourString
values, if we are connected as a client.Create a new
Question
object based on the receivedinteger
andString
data.Check if the received
OSC
message is labeled “poll-answer.”Check if the “poll-answer” message contains two
integer
values if we are the server app.Find the corresponding question to the answer we’ve received and process the answer, adding to the tally of responses.
Check if the
OSC
message is labeled “poll-update.”Check if the
OSC
message labeled “poll-update” contains fourinteger
values.Update the poll statistic for the question we’ve received via OSC by updating the total number of responses for each answer.
Load the questions from the
questions.txt
flat file located in thedata
folder of the sketch usingloadStrings()
if we operate as the server.loadStrings()
loads each line of the text file as an item in theString
arraylines
. If we are connected as a client, we receive the questions peer-to-peer from the server.add()
theQuestion
object to thequestions
ArrayList
.Request question if we don’t have any in our
questions
ArrayList
. Retry every thirtieth frame, or once a second, at a defaultframeRate
of30
fps.Find the corresponding question to the received answer ID in the
questions
ArrayList
method.When the custom method
broadcastStats()
is called and the device is serving as the Bluetooth server, send an update with all totals to all connected client devices using anOSC
message labeled “poll-update.”
Program the Question Tab
Let’s now work on our questions and answers. We’ll build a custom Processing class called Question
that can take care of multiple questions and the corresponding answers for us. To make the number of questions we can work with flexible, we’ve already set up an ArrayList
for the questions
variable. Each question comes with three possible answers, which belong to a specific question; that’s why it’s best we work with a custom class.
A class is a composite of data stored in variables and methods. If we use a custom class, we can keep the Q & A data together in one object that consists of the actual question, three answers, three totals, and the answer given by the individual participant. We can also write a couple of methods that help us keep track of the totals for each answer and return the statistic back to us for a text output.
Now let’s take a look at the Question class, which gets its own tab in the sketch.
code/P2P/BluetoothSurvey/Question.pde
// Question Class
class Question // 1
int id=0;
String question="";
String answer1, answer2, answer3="";
int total1, total2, total3 = 0;
int myAnswer, correctAnswer;
Question(String _row) // 2
{
String[] parts = split(_row, '\t'); // 3
if (parts.length == 4)
{
question = parts[0];
answer1 = parts[1];
answer2 = parts[2];
answer3 = parts[3];
}
}
Question(int _id, String q, String a1, String a2, String a3) // 4
{
id = _id;
question = q;
answer1 = a1;
answer2 = a2;
answer3 = a3;
}
void updateStats(int s1, int s2, int s3) // 5
{
total1 = s1;
total2 = s2;
total3 = s3;
}
void processAnswerStat(int _answer) // 6
{
if (_answer == 1)
total1++;
else if (_answer == 2)
total2++;
else if (_answer == 3)
total3++;
}
void setAnswer(int _answer)
{
myAnswer = _answer;
processAnswerStat(_answer);
}
float getAnswerStat(int _answer) // 7
{
if (_answer == 1)
return total1;
else if (_answer == 2)
return total2;
else if (_answer == 3)
return total3;
return 0;
}
void saveResults() // 8
{
String line = question + "\t" +
answer1 + "\t" + total1 + "\t";
line += answer2 + "\t" + total2 + "\t" +
answer3 + "\t" + total3;
}
boolean isAnswered()
{
if (myAnswer == 0)
return false;
return true;
}
void display(int x, int y) // 9
{
pushStyle();
pushMatrix();
translate(x, y);
if (myAnswer == 0 && !isServer)
{
text(id+") " + question + "\n\n" +
"[1] " + answer1 + "\n" +
"[2] " + answer2 + "\n" +
"[3] " + answer3, 0, 0);
}
else
{
float total = total1+total2+total3;
//avoid div by 0
if (total == 0)
total = 1;
float lineheight = textAscent()+textDescent();
lineheight = 50;
text( id+") " + question, 0, 0);
textAlign(LEFT, TOP);
translate(0, lineheight*2);
text(answer1 + " (" + nf((total1/total)*100, 2, 2) + " %) ", 0, 0);
translate(0, lineheight*2);
rect(0, 0, map((total1/total)*200, 0, 200, 0, width-100), lineheight-5);
translate(0, lineheight*2);
text(answer2 + " (" +nf( (total2/total)*100, 2, 2 ) + " %) ", 0, 0);
translate(0, lineheight*2);
rect(0, 0, map((total2/total)*200, 0, 200, 0, width-100), lineheight-5);
translate(0, lineheight*2);
text(answer3 + " (" +nf( (total3/total)*100, 2, 2) + " %) ", 0, 0);
translate(0, lineheight*2);
rect(0, 0, map((total3/total)*200, 0, 200, 0, width-100), lineheight-5);
translate(0, lineheight*3);
if (isServer)
text("Number of answers for this question: " + total, 0, 0);
}
popMatrix();
popStyle();
}
}
Let’s take a closer look at the Question
class variables and methods.
Create a custom Processing
class
calledQuestion
. The class definition does not take parameters.Create the constructor for the
Question
class, taking oneString
parameter_row
.Split the
String
, which contains one row ofquestions.txt
. Use the tab character,\t
, as a delimiter tosplit()
the string.Add a second constructor for
Question
, taking five parameters instead of aString
like the first constructor does. Custom classes can be overloaded with multiple constructors to accommodate multiple types of parameters, here aninteger
for the ID, followed by aString
for the question, followed by threeString
values for the answers.Update the totals for each answer when we receive an answer from a participant.
Process the statistic for each answer (how many times each answer has been picked compared to the total number of answers).
Get the statistic for each answer.
Save each answer and its statistic.
Return the
String
output presented on the screen. If no answer has been given yet on the device, show the question and the multiple choice answers; otherwise, show the statistics as well.
Now it’s time modify the UI
tab.
Program the UI Tab
We need to make few adjustments to the UI
tab to modify it for our survey app based on the previous code P2P/BluetoothCursors/UI.pde
. Most of it is redundant and otherwise called out.
code/P2P/BluetoothSurvey/UI.pde
// UI methods
void mousePressed() {
if (mouseY <= 100 && mouseX > 0 && mouseX < width/3)
KetaiKeyboard.toggle(this);
else if (mouseY <= 100 && mouseX > width/3 && mouseX < 2*(width/3))
isConfiguring=true;
else if (mouseY <= 100 && mouseX > 2*(width/3) && mouseX < width &&
bt.getConnectedDeviceNames().size() > 0)
{
if (isConfiguring) {
background(127);
isConfiguring=false;
}
}
if (bt.getConnectedDeviceNames().size() > 0) {
if (currentQuestion == null)
return;
if (previous.isPressed() && isServer) //previous question
{
if (findQuestion(currentQuestion.id-1) != null)
currentQuestion = findQuestion(currentQuestion.id-1); // 1
sendCurrentQuestionID();
}
else if (next.isPressed() && isServer) //next question
{
if (findQuestion(currentQuestion.id+1) != null)
currentQuestion = findQuestion(currentQuestion.id+1); // 2
else
requestQuestions();
sendCurrentQuestionID();
}
}
}
void keyPressed() {
if (!isConfiguring && !isServer) {
if (currentQuestion != null && !currentQuestion.isAnswered())
if (key == '1') {
currentQuestion.setAnswer(1);
OscMessage m = new OscMessage("/poll-answer/");
m.add(currentQuestion.id);
m.add(1); // 3
bt.broadcast(m.getBytes());
}
else if (key == '2') {
currentQuestion.setAnswer(2);
OscMessage m = new OscMessage("/poll-answer/");
m.add(currentQuestion.id);
m.add(2); // 4
bt.broadcast(m.getBytes());
}
else if (key == '3') {
currentQuestion.setAnswer(3);
OscMessage m = new OscMessage("/poll-answer/");
m.add(currentQuestion.id);
m.add(3); // 5
bt.broadcast(m.getBytes());
}
}
else if (key =='c') {
if (bt.getDiscoveredDeviceNames().size() > 0)
connectionList = new KetaiList(this, bt.getDiscoveredDeviceNames());
else if (bt.getPairedDeviceNames().size() > 0)
connectionList = new KetaiList(this, bt.getPairedDeviceNames());
}
else if (key == 'd')
bt.discoverDevices();
else if (key == 'm')
bt.makeDiscoverable();
}
void drawUI() {
pushStyle();
fill(0);
stroke(255);
rect(0, 0, width/3, 100);
if (isConfiguring)
{
noStroke();
fill(127);
}
else if (bt.getConnectedDeviceNames().size() > 0 && isServer)
{
previous.draw();
next.draw();
}
rect(width/3, 0, width/3, 100);
if (!isConfiguring)
{
noStroke();
if (isServer)
fill(serverColor);
else
fill(clientColor);
}
else
{
fill(0);
stroke(255);
}
rect((width/3)*2, 0, width/3, 100);
fill(255);
text("Keyboard", 5, 65);
text("Bluetooth", width/3+5, 65);
if (bt.getConnectedDeviceNames().size() > 0)
if (isServer)
text("Survey Server", width/3*2+5, 65);
else
text("Survey Client", width/3*2+5, 65);
popStyle();
}
void onKetaiListSelection(KetaiList connectionList)
{
String selection = connectionList.getSelection();
bt.connectToDeviceByName(selection);
connectionList = null;
isServer = false;
}
class Button
{
PImage icon;
PVector location;
int Size = 100;
public Button(String imagefile, int x, int y)
{
icon = loadImage(imagefile);
location = new PVector(x, y);
}
void draw()
{
pushStyle();
imageMode(CENTER);
if (icon != null)
image(icon, location.x, location.y);
else
ellipse(location.x, location.y, Size, Size);
popStyle();
}
boolean isPressed()
{
return (dist(location.x, location.y, mouseX, mouseY) < Size/2);
}
}
Here are the changes we’ve made to the UI
tab.
Jump to the previous question by decrementing the ID for
currentQuestion
.Jump to the next question by incrementing the ID for
currentQuestion
.Send an
OSC
message called “poll-answer” if we press1
on the keyboard. The message contains the current question ID followed by the answer1
.Send an
OSC
message called “poll-answer” if we press2
that contains the current question ID followed by the answer2
.Send an
OSC
message called “poll-answer” if we press3
that contains the current question ID followed by the answer3
.
Now it’s time to test the app.
Run the App
Run the app on the Android device you’ve currently connected via USB. When the app is compiled, disconnect that device and run it on the other device. If you have a third (or fourth) device available, load the sketch onto as many Android devices as you’d like to test with.
Now follow the steps we took earlier in Run the App, to connect two devices via Bluetooth for the remote cursor app.
Finally, press the Survey tab and answer your first question. Press the left and right arrows to move through the questions. Respond to the questions using the software keyboard and notice how the statistics change as you punch in 1
, 2
, and 3
.
You’ve completed a survey app for multiple Bluetooth devices, where we’ve been diving deep into the peer-to-peer networking process. Although very ubiquitous, Bluetooth has its limitations in terms of transmission bandwidth and range.
Less ubiquitous than Bluetooth but more powerful in terms of bandwidth and range, is the emerging Wi-Fi Direct P2P networking standard. Let’s take a look at the final networking standard we’ll discuss in this chapter, which is also fairly easy to work with.
Working with Wi-Fi Direct
Wi-Fi Direct was introduced in Android 4.0 (API level 14) to enable Android devices to connect directly to each other via Wi-Fi without a fixed Wi-Fi access point. Each device in a Wi-Fi Direct network can serve as an access point for any of the other devices in the network. Like Bluetooth, Wi-Fi Direct allows us to discover Wi-Fi Direct devices and connect to them if confirmed by the user. Compared to Bluetooth, Wi-Fi Direct offers a faster connection across greater distances and is therefore the preferred networking protocol for multiplayer games or multiuser applications, when every connected device supports Wi-Fi Direct.
In many ways, Wi-Fi Direct is very familiar to us when it comes to allowing devices to connect to each other. We’ve also worked with Wi-Fi already and sent OSC
messages over the wireless local area network in Chapter 6, Networking Devices with Wi-Fi. When we use Wi-Fi Direct, we combine the P2P aspects and pairing process of Bluetooth with the ease of use of Wi-Fi.
Wi-Fi Direct is currently supported on a few of the newest Android devices, so the following section may describe operations that are not possible on your device just yet. But because it’s a powerful standard, we’ll discuss it now and compare it to Bluetooth’s wireless peer-to-peer performance using our earlier remote cursor sketch P2P/BluetoothCursors/BluetoothCursors.pde.
Let’s take a look at the KetaiWi-FiDirect
class first, which makes working with Android’s Wi-Fi Direct features an easy task. For this sketch, we’ll work with the following KetaiWi-FiDirect
methods:
connectToDevice() | Connects to a specific Wi-Fi Direct-enabled device |
getConnectionInfo() | Gets the status of the Wi-Fi Direct connection |
getIPAddress() | Gets the IP address of a specified Wi-Fi Direct device |
getPeerNameList() | Returns the list of connected Wi-Fi Direct devices |
Now, let’s go ahead and implement the remote cursor app using Wi-Fi Direct.
Use Wi-Fi Direct to Control Remote Cursors
We’ve already implemented the remote cursors app earlier in this chapter in Connect Two Android Devices via Bluetooth. In order to compare Wi-Fi Direct to Bluetooth, we’ll implement the same remote cursor app, replacing its Bluetooth peer-to-peer networking functionality with Wi-Fi Direct. Large portions of the code are identical to the Bluetooth version of the sketch shown in P2P/BluetoothCursors/BluetoothCursors.pde and P2P/BluetoothCursors/UI.pde, so we will focus only on the code that we’ll change from Bluetooth to Wi-Fi Direct.
Using Wi-Fi Direct, we’ll be able to use the OSC
protocol again to send data to remote devices, as we’ve already done in Network an Android with a Desktop PC.
Modify the Main Tab
code/P2P/WiFiDirectCursors/WiFiDirectCursors.pde
import ketai.net.wifidirect.*; // 1
import ketai.net.*;
import ketai.ui.*;
import oscP5.*;
import netP5.*;
KetaiWiFiDirect direct; // 2
KetaiList connectionList;
String info = "";
PVector remoteCursor = new PVector();
boolean isConfiguring = true;
String UIText;
ArrayList<String> devices = new ArrayList(); // 3
OscP5 oscP5; // 4
String clientIP = ""; // 5
void setup()
{
orientation(PORTRAIT);
background(78, 93, 75);
stroke(255);
textSize(48);
direct = new KetaiWiFiDirect(this); // 6
UIText = "[d] - discover devices\n" +
"[c] - pick device to connect to\n" +
"[p] - list connected devices\n" +
"[i] - show WiFi Direct info\n" +
"[o] - start OSC Server\n"; // 7
}
void draw()
{
background(78, 93, 75);
if (isConfiguring) {
info="";
if (key == 'i')
info = getNetInformation(); // 8
else if (key == 'd') {
info = "Discovered Devices:\n";
devices = direct.getPeerNameList(); // 9
for (int i=0; i < devices.size(); i++)
{
info += "["+i+"] "+devices.get(i).toString() + "\t\t"+devices.size()+"\n";
}
}
else if (key == 'p') {
info += "Peers: \n";
}
text(UIText + "\n\n" + info, 5, 200);
}
else {
pushStyle();
noStroke();
fill(255);
ellipse(mouseX, mouseY, 50, 50);
fill(255, 0, 0);
ellipse(remoteCursor.x, remoteCursor.y, 50, 50);
popStyle();
}
drawUI();
}
void mouseDragged() {
if (isConfiguring)
return;
OscMessage m = new OscMessage("/remoteCursor/");
m.add(pmouseX);
m.add(pmouseY);
if (oscP5 != null) {
NetAddress myRemoteLocation = null;
if (clientIP != "")
myRemoteLocation = new NetAddress(clientIP, 12000);
else if (direct.getIPAddress() != KetaiNet.getIP())
myRemoteLocation = new NetAddress(direct.getIPAddress(), 12000);
if (myRemoteLocation != null)
oscP5.send(m, myRemoteLocation);
}
}
void oscEvent(OscMessage m) {
if (direct.getIPAddress() != m.netAddress().address()) // 10
clientIP = m.netAddress().address(); // 11
if (m.checkAddrPattern("/remoteCursor/")) {
if (m.checkTypetag("ii")) {
remoteCursor.x = m.get(0).intValue();
remoteCursor.y = m.get(1).intValue();
}
}
}
String getNetInformation() // 12
{
String Info = "Server Running: ";
Info += "\n my IP: " + KetaiNet.getIP();
Info += "\n initiator's IP: " + direct.getIPAddress();
return Info;
}
Let’s take a look at the steps we took to change our remote cursor app to use Wi-Fi Direct.
Import the Ketai library’s Wi-Fi Direct package.
Create a variable
direct
of the typeKetaiWiFiDirect
.Create an
ArrayList
for discovered Wi-Fi Direct devices.Create a
OscP5
-type variable,oscP5
, as we’ve used already in Chapter 6, Networking Devices with Wi-Fi.Create a
String
variable to hold the client IP address.Create the
WiFiDirect
object,direct
.Include a keyboard menu item
o
to start theOSC
serverGet the Wi-Fi Direct network information.
Get the list of connected Wi-Fi Direct peer devices.
Check if the Wi-Fi Direct server IP address is different from our device IP, which means we are connecting as a client.
Set the
clientIP
variable to the device IP address, since we’ve determined we are connecting as a client to the Wi-Fi Direct network.Get the Wi-Fi Direct information, including our IP address and the server’s IP address.
For the UI, we won’t change very much compared to the previous
UI
P2P/BluetoothCursors/UI.pde. Let’s take a look.
Modify the UI Tab
Now it’s time to see what’s needed to modify the UI
tab to support Wi-Fi direct. We’ll need to adjust the discover key (d
) to call the Wi-Fi Direct discover()
method and the info key (i
) to get the Wi-Fi Direct connection info using getConnectionInfo()
. Also, we need to introduce an OSC
key (o
) to the menu, allowing us to start OSC
networking now over Wi-Fi Direct.
code/P2P/WiFiDirectCursors/UI.pde
// UI methods
void mousePressed() {
//keyboard button -- toggle virtual keyboard
if (mouseY <= 100 && mouseX > 0 && mouseX < width/3)
KetaiKeyboard.toggle(this);
else if (mouseY <= 100 && mouseX > width/3 && mouseX < 2*(width/3)) //config
{
isConfiguring=true;
}
else if (mouseY <= 100 && mouseX > 2*(width/3) && mouseX < width) { // draw
if (isConfiguring) {
isConfiguring=false;
}
}
}
void keyPressed() {
if (key == 'c') {
if (devices.size() > 0)
connectionList = new KetaiList(this, devices);
}
else if (key == 'd') {
direct.discover(); // 1
println("device list contains " + devices.size() + " elements");
}
else if (key == 'i')
direct.getConnectionInfo(); // 2
else if (key == 'o') {
if (direct.getIPAddress().length() > 0)
oscP5 = new OscP5(this, 12000); // 3
}
}
void drawUI()
{
pushStyle();
fill(0);
stroke(255);
rect(0, 0, width/3, 100);
if (isConfiguring)
{
noStroke();
fill(78, 93, 75);
}
else
fill(0);
rect(width/3, 0, width/3, 100);
if (!isConfiguring)
{
noStroke();
fill(78, 93, 75);
}
else
{
fill(0);
stroke(255);
}
rect((width/3)*2, 0, width/3, 100);
fill(255);
text("Keyboard", 5, 65);
text("WiFi Direct", width/3+5, 65);
text("Interact", width/3*2+5, 65);
popStyle();
}
void onKetaiListSelection(KetaiList klist)
{
String selection = klist.getSelection();
direct.connect(selection); // 4
connectionList = null;
}
Now let’s see what we adjusted for the UI
tab of the Wi-Fi Direct remote cursor sketch.
Discover Wi-Fi Direct devices if we press
d
on the keyboard.Get the Wi-Fi Direct information.
Initialize the
OSC
connection on port12000
.
Everything looks quite familiar from the Bluetooth version of this sketch. The difference is that we are connecting and sending OSC
messages like we’ve done in Chapter 6, Networking Devices with Wi-Fi.
Now let’s test the app.
Run the App
Run the app on your Wi-Fi Direct-enabled Android device. Disconnect the USB cable, and run the app on your second Wi-Fi Direct device. Press d
on the keyboard to discover Wi-Fi Direct devices. Then press c
to show the list of discovered devices and pick your second device. You need to allow the Wi-Fi Direct connection request. Once you do, press the Interact tab on both devices and move your finger across the screen. Notice how quickly both cursors respond; there seems to be no noticeable delay, and the motion is continuous at a high frame rate.
Compare the performance of the Wi-Fi Direct remote cursor app to the Bluetooth remote cursor app we’ve installed earlier. You can observe that Wi-Fi Direct performs better. Now grab a friend and put the connection range to the test for both flavors of the remote cursor sketch and see which one has the better range.
This concludes our explorations in the world of peer-to-peer networking. Now you are independent of networking infrastructure and ready to program your multiuser and multiplayer apps for users within close proximity.
Wrapping Up
We’ve learned that we don’t need Wi-Fi/3G/4G infrastructure to interact with other Android users. We can write a range of apps that use peer-to-peer principles. We’ve learned about Bluetooth discovery, pairing, connecting, and exchanging data. We’ve learned that Wi-Fi Direct uses a similar discovery process as Bluetooth but it provides more bandwidth and greater connection distances.
With a range of networking projects under our belt, it's time now to move on to another emerging standard, near field communication, or NFC, which allows us not only to interact with other NFC-enabled Android devices but also with NFC tags embedded in stickers, posters, objects, or point-of-sale payment systems.