package bluetooth; import java.util.UUID; import android.annotation.TargetApi; import android.bluetooth.BluetoothGattCharacteristic; import android.bluetooth.BluetoothGattDescriptor; import android.bluetooth.BluetoothGattServer; import android.bluetooth.BluetoothGattService; import android.util.Log; /** * * @author Isak & Linus * @see <a href="https://developer.bluetooth.org/TechnologyOverview/Pages/HOGP.aspx">HID over GATT Profile</a> and <a href="https://developer.bluetooth.org/gatt/profiles/Pages/ProfileViewer.aspx?u=org.bluetooth.profile.hid_over_gatt.xml">HID over GATT specification</a> * <p>This class implements the HID over GATT Profile (HOGP).</p> * * */ @TargetApi(18) public class HIDoverGattProfile { private static final String SIG_HID = "1812"; //private static final String SIG_HID = "1124"; private static final String SIG_DEVICE_INFORMATION = "180A"; private static final String SIG_BATTERY = "180F"; private static final String SIG_CHARA_BATTERY_LEVEL = "2A19"; private static final String SIG_CHARA_REPORT_MAP = "2A4B"; private static final String SIG_CHARA_HID_INFORMATION = "2A4A"; private static final String SIG_CHARA_HID_CONTROL_POINT = "2A4C"; private static final String SIG_DESC_PRESENTATION_FORMAT = "2904"; private static final String SIG_DESC_CLIENT_INFORMATION_CONFIGURATION = "2902"; private static final String SIG_UNIT_PERCENTAGE = "27AD"; // this is not used as it is hard coded directly in two bytes private static final String SIG_MANUFACTURER_NAME = "2A29"; public static final String SHORT_UUID_BASE = "00001000800000805F9B34FB"; // used to make 128 bit UUID from 16 bit SIG private BluetoothGattService HIDService; private BluetoothGattService batteryService; private BluetoothGattService deviceInfoService; private static final byte[] GAMEPAD_VALUE = { 0x05, 0x01, //; USAGE_PAGE (Generic Desktop) 0x09, 0x05, //; USAGE (Gamepad) (byte) 0xa1, 0x01, //; COLLECTION (Application) 0x05, 0x09,// ; USAGE_PAGE (Button) 0x19, 0x01, //; USAGE_MINIMUM (Button 1) 0x29, 0x09, //; USAGE_MAXIMUM (Button 9) 0x15, 0x00, //; LOGICAL_MINIMUM (0) 0x25, 0x01, //; LOGICAL_MAXIMUM (1) 0x75, 0x01, //; REPORT_SIZE (1) (byte) 0x95, 0x09, //; REPORT_COUNT (9) (byte) 0x81, 0x02, //; INPUT (Data,Var,Abs) (byte) 0x95, 0x07, // REPORT_COUNT (7) (byte) 0x81, 0x03, // INPUT (Cnst,Var,Abs) (byte) 0xC0// ; END_COLLECTION }; private static final byte[] MOUSE_VALUE = { 0x05, 0x01, // USAGE_PAGE // (Generic Desktop) 0x09, 0x02, // USAGE (Mouse) (byte) 0xa1, 0x01, // COLLECTION (Application) 0x09, 0x01, // USAGE (Pointer) (byte) 0xa1, 0x00, // COLLECTION (Physical) 0x05, 0x09, // USAGE_PAGE (Button) 0x19, 0x01, // USAGE_MINIMUM (Button 1) 0x29, 0x03, // USAGE_MAXIMUM (Button 3) 0x15, 0x00, // LOGICAL_MINIMUM (0) 0x25, 0x01, // LOGICAL_MAXIMUM (1) (byte) 0x95, 0x03, // REPORT_COUNT (3) 0x75, 0x01, // REPORT_SIZE (1) (byte) 0x81, 0x02, // INPUT (Data,Var,Abs) (byte) 0x95, 0x01, // REPORT_COUNT (1) 0x75, 0x05, // REPORT_SIZE (5) (byte) 0x81, 0x03, // INPUT (Cnst,Var,Abs) 0x05, 0x01, // USAGE_PAGE (Generic Desktop) 0x09, 0x30, // USAGE (X) 0x09, 0x31, // USAGE (Y) 0x15, (byte) 0x81, // LOGICAL_MINIMUM (-127) 0x25, 0x7f, // LOGICAL_MAXIMUM (127) 0x75, 0x08, // REPORT_SIZE (8) (byte) 0x95, 0x02, // REPORT_COUNT (2) (byte) 0x81, 0x06, // INPUT (Data,Var,Rel) (byte) 0xc0, // END_COLLECTION (byte) 0xc0 // END_COLLECTION }; private static final String TAG = "Gamepad"; //used for filtering the debug printout public HIDoverGattProfile() { //These services are mandatory for the HID over GATT Profile createBatteryService(); createDeviceInfoService(); createHIDService(); } public boolean addProfileToGATTServer(BluetoothGattServer server) { boolean fail = false; try { Thread.sleep(2000); } catch (InterruptedException e2) { e2.printStackTrace(); } if (!server.addService(batteryService)) { fail = true; Log.d(TAG, "fail adding BatteryService"); } else { Log.d(TAG, "added BatteryService"); } try { Thread.sleep(2000); } catch (InterruptedException e1) { e1.printStackTrace(); } if (!server.addService(HIDService)) { fail = true; Log.d(TAG, "fail adding HIDService"); } else { Log.d(TAG, "added HIDService"); } try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } if (!server.addService(deviceInfoService)) { fail = true; Log.d(TAG, "fail adding DeviceInfoService"); } else { Log.d(TAG, "added DeviceInfoService"); } return !fail; } private BluetoothGattService createHIDService() { UUID uuid = createUUID(SIG_HID); Log.d(TAG, "HID UUID: " + uuid.toString()); HIDService = new BluetoothGattService(uuid, BluetoothGattService.SERVICE_TYPE_PRIMARY); BluetoothGattCharacteristic reportMapCharacteristic = new BluetoothGattCharacteristic( createUUID(SIG_CHARA_REPORT_MAP), BluetoothGattCharacteristic.PROPERTY_READ, BluetoothGattCharacteristic.PERMISSION_READ); reportMapCharacteristic.setValue(GAMEPAD_VALUE); BluetoothGattCharacteristic HIDInformationCharacteristic = new BluetoothGattCharacteristic( createUUID(SIG_CHARA_HID_INFORMATION), BluetoothGattCharacteristic.PROPERTY_READ, BluetoothGattCharacteristic.PERMISSION_READ); byte[] HIDInformationValue = { 0x01, 0x11, 26, 0x40 }; // version, // version, // sweden's // country code // in decimal, // flags 0100 // 0000 HIDInformationCharacteristic.setValue(HIDInformationValue); BluetoothGattCharacteristic HIDControlPointCharacteristic = new BluetoothGattCharacteristic( createUUID(SIG_CHARA_HID_CONTROL_POINT), BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE, BluetoothGattCharacteristic.PERMISSION_WRITE); HIDService.addCharacteristic(reportMapCharacteristic); HIDService.addCharacteristic(HIDInformationCharacteristic); HIDService.addCharacteristic(HIDControlPointCharacteristic); return HIDService; } private BluetoothGattService createBatteryService() { UUID uuid = createUUID(SIG_BATTERY); Log.d(TAG, "BATTERY UUID: " + uuid.toString()); batteryService = new BluetoothGattService(uuid, BluetoothGattService.SERVICE_TYPE_PRIMARY); BluetoothGattCharacteristic batteryLevel = new BluetoothGattCharacteristic( createUUID(SIG_CHARA_BATTERY_LEVEL), BluetoothGattCharacteristic.PROPERTY_READ, BluetoothGattCharacteristic.PERMISSION_READ); BluetoothGattDescriptor presentationFormatDescriptor = new BluetoothGattDescriptor( createUUID(SIG_DESC_PRESENTATION_FORMAT), BluetoothGattDescriptor.PERMISSION_READ); byte[] presentationFormatValue = { 0x04, 0x00, 0x27, (byte) 0xAD, 0x01, 0x00, 0x00 }; presentationFormatDescriptor.setValue(presentationFormatValue); // uint8, // exponent, // unituuid, // unituuid, // namespace, // desiciption, // description byte[] characteristicValue = { 0x00, 0x00 }; BluetoothGattDescriptor clientCharacteristicConfigurationDescriptor = new BluetoothGattDescriptor( createUUID(SIG_DESC_CLIENT_INFORMATION_CONFIGURATION), BluetoothGattDescriptor.PERMISSION_WRITE); clientCharacteristicConfigurationDescriptor .setValue(characteristicValue); batteryLevel.addDescriptor(presentationFormatDescriptor); batteryLevel.addDescriptor(clientCharacteristicConfigurationDescriptor); byte[] value = { 100 }; // 100%, our battery is of course completely charged! batteryLevel.setValue(value); batteryService.addCharacteristic(batteryLevel); return batteryService; } private BluetoothGattService createDeviceInfoService() { UUID uuid = createUUID(SIG_DEVICE_INFORMATION); Log.d(TAG, "DEVICE UUID: " + uuid.toString()); deviceInfoService = new BluetoothGattService(uuid , BluetoothGattService.SERVICE_TYPE_PRIMARY); BluetoothGattCharacteristic manufacturerName = new BluetoothGattCharacteristic( createUUID(SIG_MANUFACTURER_NAME), BluetoothGattCharacteristic.PROPERTY_READ, BluetoothGattCharacteristic.PERMISSION_READ); manufacturerName.setValue("BGSEP"); deviceInfoService.addCharacteristic(manufacturerName); return deviceInfoService; } private java.util.UUID createUUID(String SIG) { String uuid = getUUIDString(SIG, true); Log.d(TAG, "CONVERTING " + uuid); String formattedUuid = uuid.substring(0, 8) + "-" + uuid.substring(8, 12) + "-" + uuid.substring(12, 16) + "-" + uuid.substring(16, 20) + "-" + uuid.substring(20); Log.d(TAG, "TO " + formattedUuid); return UUID.fromString(formattedUuid); } private String getUUIDString(String uuidValue, boolean shortUUID) { byte[] byteuuidValue; if (uuidValue == null) { throw new NullPointerException("uuidValue is null"); } int length = uuidValue.length(); if (shortUUID) { if (length < 1 || length > 8) { Log.d("gamepad", "LENGTH IS WRONG: " + uuidValue); throw new IllegalArgumentException(); } byteuuidValue = UUIDToByteArray("00000000".substring(length) + uuidValue + SHORT_UUID_BASE); } else { if (length < 1 || length > 32) { throw new IllegalArgumentException(); } byteuuidValue = UUIDToByteArray("00000000000000000000000000000000".substring(length) + uuidValue); } return UUIDByteArrayToString(byteuuidValue); } public static byte[] UUIDToByteArray(String uuidStringValue) { byte[] uuidValue = new byte[16]; if (uuidStringValue.indexOf('-') != -1) { throw new NumberFormatException("The '-' character is not allowed in UUID: " + uuidStringValue); } for (int i = 0; i < 16; i++) { uuidValue[i] = (byte) Integer.parseInt(uuidStringValue.substring(i * 2, i * 2 + 2), 16); } return uuidValue; } public static String UUIDByteArrayToString(byte[] uuidValue) { StringBuffer buf = new StringBuffer(); for (int i = 0; i < uuidValue.length; i++) { buf.append(Integer.toHexString(uuidValue[i] >> 4 & 0xf)); buf.append(Integer.toHexString(uuidValue[i] & 0xf)); } return buf.toString(); } }