One of the challenges writing games for the touch-table is handling hidden information. So far we have used two solutions: Physical blinds that rest above the sensor and block the view of the other players, and a touch-to-reveal system where the player blocks the view with their hand and touches the screen to reveal their cards.
Many of our users have smartphones, and I thought that it would make sense to let them use their smartphone for the display of the hidden information. In the past, have experimented with a web based system where the game is hosted on a webpage and played on browsers. This works, but when the game is written in C++ for the touch-table, the game has to send data to the web-server so that the clients can display it. This creates extra overhead and lag.
Instead, I thought it would work better to send the hidden information directly to the phones over bluetooth. So I set out to write a C++ server that would use bluetooth to broadcast data to Android clients. This ended up being more difficult than I expected, but I did get it to work and wanted to post what I have done.
The first step was to create a C++ bluetooth server. I was surprised by how low-level the bluetooth API is in C++. Making a bluetooth server is much like setting up a socket connection where you have to locate and enable the network adapter first. Once the socket is open, you also have to broadcast the service that you are providing:
// Initialize Winsock WSADATA wsaData; int iResult = WSAStartup(MAKEWORD(2, 2), &wsaData); if (iResult != 0) { wprintf(L"WSAStartup failed: %d\n", iResult); return 1; } // Look for bluetooth radios BLUETOOTH_FIND_RADIO_PARAMS findParams; findParams.dwSize = sizeof(BLUETOOTH_FIND_RADIO_PARAMS ); HANDLE bluetoothRadio; HBLUETOOTH_RADIO_FIND radioFindHandle = BluetoothFindFirstRadio(&findParams, &bluetoothRadio); while ( radioFindHandle != nullptr) { wprintf(L"Found Bluetooth radio\n"); wprintf(L"Opening server socket for bluetooth connections...\n"); SOCKET btSock = socket(AF_BTH, SOCK_STREAM, BTHPROTO_RFCOMM); if ( btSock == INVALID_SOCKET ) wprintf(L"Error creating socket: %d\n", WSAGetLastError()); else { WSAPROTOCOL_INFO protoInfo; int infoStructSize = sizeof(protoInfo); if ( getsockopt(btSock, SOL_SOCKET, SO_PROTOCOL_INFO, (char*)&protoInfo, &infoStructSize) ) { wprintf(L"Error getting socket options: %d\n", WSAGetLastError()); } else { // Bind the socket SOCKADDR_BTH address; address.addressFamily = AF_BTH; address.btAddr = 0; address.serviceClassId = GUID_NULL; address.port = BT_PORT_ANY; if ( bind(btSock, (sockaddr*)&address, sizeof(address)) ) { wprintf(L"Error binding socket: %d\n", WSAGetLastError()); } else { int addressLen = sizeof(address); sockaddr* pAddress = (sockaddr*)&address; getsockname(btSock, pAddress, &addressLen); wprintf(L"Bind successfull: device=%04x%08x channel = %d\n", GET_NAP(address.btAddr), GET_SAP(address.btAddr), address.port); // Listen for connections. (This would normally go into a thread) if ( listen(btSock, SOMAXCONN) ) { wprintf(L"Error listening for connections: %d\n", WSAGetLastError()); } else { // Register our service so that the clients can lookup this server // by service name wprintf(L"Registering service...\n"); WSAQUERYSET service; memset(&service, 0, sizeof(service)); service.dwSize = sizeof(service); service.lpszServiceInstanceName = _T("TouchGameBTServer"); service.lpszComment = _T("Push game data to devices"); GUID serviceGUID = GenericNetworkingServiceClass_UUID; service.lpServiceClassId = &serviceGUID; service.dwNumberOfCsAddrs = 1; service.dwNameSpace = NS_BTH; CSADDR_INFO csAddr; memset(&csAddr, 0, sizeof(csAddr)); csAddr.LocalAddr.iSockaddrLength = sizeof(SOCKADDR_BTH); csAddr.LocalAddr.lpSockaddr = pAddress; csAddr.iSocketType = SOCK_STREAM; csAddr.iProtocol = BTHPROTO_RFCOMM; service.lpcsaBuffer = &csAddr; if ( WSASetService(&service, RNRSERVICE_REGISTER, 0) ) { wprintf(L"Error registering game service: %d\n", WSAGetLastError()); } else { // Now accept connections from clients. This call will block till a connection is made wprintf(L"Service registered. Accepting connections...\n"); SOCKADDR_BTH clientAddr; int addrSize = sizeof(clientAddr); SOCKET clientSocket = accept(btSock, (sockaddr*)&clientAddr, &addrSize); if ( clientSocket != INVALID_SOCKET ) { // Send some data. At this point we just have a full-duplex socket connection // so we can send/receive anything we need. wprintf(L"Client connected from %04x%08x on channel %d.", GET_NAP(clientAddr.btAddr), GET_SAP(clientAddr.btAddr), clientAddr.port); char* message = "Hello, world"; send(clientSocket, message, sizeof(message),0); char buffer[1024] = {0}; int dataSize = recv(clientSocket, buffer, sizeof(buffer), 0); } } } } } } CloseHandle(bluetoothRadio); if ( !BluetoothFindNextRadio(radioFindHandle, &bluetoothRadio) ) { BluetoothFindRadioClose(radioFindHandle); radioFindHandle = nullptr; } } WSACleanup();
Then I created the Android client. Android provided a higher level API, but had its own set of issues. Since it is a phone, the bluetooth antenna is usually off to save power. While it is possible to simply turn it on, the correct approach is to start an intent to ask the user to turn on the antenna. Of course we also had to request the bluetooth permissions. Here is the code to request the user turn on the antenna:
BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter(); StringBuilder message = new StringBuilder(); if (adapter.isEnabled()) { message.append("Bluetooth address: " + adapter.getAddress() + "\n"); message.append("Bluetooth name: " + adapter.getName() + "\n"); } else { message.append("Enabling bluetooth"); Intent enableBluetooth = new Intent( BluetoothAdapter.ACTION_REQUEST_ENABLE); startActivityForResult(enableBluetooth, ENABLE_BLUETOOTH_ACTIVITY); } @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { if ( requestCode == ENABLE_BLUETOOTH_ACTIVITY && resultCode == RESULT_OK ) { findServerDevice(); } }
Next we want to connect to the server device.
The bluetooth antenna can be bonded to a device (they have connected to each other before). Or it may need to scan for new devices to connect to. Performing a scan is expensive in time and battery, so we first check the bonded devices.
public void findServerDevice() { BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter(); // Start with the bonded devices if ( !detectDevice(adapter.getBondedDevices())){ // If that doesn't work, then perform a scan if ( adapter.isDiscovering()) adapter.cancelDiscovery(); DeviceReceiver receiver = new DeviceReceiver(); registerReceiver(receiver, new IntentFilter(BluetoothDevice.ACTION_FOUND)); registerReceiver(receiver, new IntentFilter(BluetoothAdapter.ACTION_DISCOVERY_FINISHED)); registerReceiver(receiver, new IntentFilter(BluetoothAdapter.ACTION_DISCOVERY_STARTED)); adapter.startDiscovery(); } } public boolean detectDevice(Set<BluetoothDevice> devicesToScan) { myDevice = null; mySocket = null; for ( BluetoothDevice device : devicesToScan ) { // Check to see if this device supports this app UUID serviceName = UUID.fromString("00001201-0000-1000-8000-00805F9B34FB"); try { BluetoothSocket client = device.createRfcommSocketToServiceRecord(serviceName); if ( client == null ) { continue; } client.connect(); myDevice = device; mySocket = client; } catch (IOException e) { continue; } break; } // Receive data over the socket if ( mySocket != null ) loadImage(message); return mySocket != null; }
As you can see, the Android code is much shorter and simpler. As usual, the Windows APIs are difficult to use with lots of ugly structs and magic constants.
I don’t show the cod in the C++ example, but I added the ability to send an image over bluetooth and display it on the phone. The C++ cod for this was very simple, just open up the file and stream the data over the socket. On the Android side, I needed to save the file and load it into a view. I used the internal storage on the phone to save the image and the BitmapFactory to load it into an image. Here is the Android code:
protected void loadImage(StringBuilder message) { // DEBUG: See what files we already have Log.d("Storage", getFilesDir().getAbsolutePath()); for ( String file : fileList() ) { Log.d("Storage", file); deleteFile(file); } try { InputStream is = mySocket.getInputStream(); BufferedReader reader = new BufferedReader(new InputStreamReader(is)); String command = reader.readLine(); if ( command.startsWith("SendFile") ) { String filename = reader.readLine(); int fileSize = Integer.parseInt(reader.readLine()); message.append("SendFile: "+filename+ "("+ fileSize+")\n"); byte[] buffer = new byte[1024]; FileOutputStream fos = openFileOutput(filename, Context.MODE_PRIVATE); int bytesLoaded = 0; while ( bytesLoaded < fileSize ) { int readSize = 0; Log.d("Transfer", "Recieved: "+bytesLoaded+"/"+fileSize+" remaining: "+(fileSize-bytesLoaded)); if ( fileSize - bytesLoaded > buffer.length ) readSize = is.read(buffer); else readSize = is.read(buffer, 0, fileSize-bytesLoaded); if ( readSize == -1 ){ message.append("Failed to load all data.\n"); break; } bytesLoaded += readSize; fos.write(buffer, 0, readSize); } Log.d("Transfer", "Data received and saved."); fos.close(); File newFile = new File(getFilesDir(), filename); Log.d("Storage", "Exists: "+newFile.exists() + " Readable: "+newFile.canRead() + " length: "+newFile.length()); DisplayView displayView = (DisplayView) findViewById(R.id.DISPLAY_VIEW); FileInputStream fis = openFileInput(filename); displayView.setBitmap(BitmapFactory.decodeStream(fis)); } } catch (IOException e) { message.append("Couldn't connect: "+e.getMessage()); e.printStackTrace(); } }
Overall I am happy with this prototype. Unfortunately there are a couple of fairly severe limitations. The first is that only Android phones that download the touchtable app can play. This isn’t too much of a problem for our group of friends, but is a problem when trying to sell to a larger audience. The second problem is that it is very difficult to have any client side logic without writing a separate app per game. And while it may not be an issue to ask people to download one app, they may be less interested in downloading an app per game.
One thought on “Experimenting with Bluetooth”