package net.kwatts.powtools.util; import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothGatt; import android.bluetooth.BluetoothGattCallback; import android.bluetooth.BluetoothGattCharacteristic; import android.bluetooth.BluetoothGattDescriptor; import android.bluetooth.BluetoothGattService; import android.bluetooth.BluetoothManager; import android.bluetooth.BluetoothProfile; import android.bluetooth.le.BluetoothLeScanner; import android.bluetooth.le.ScanCallback; import android.bluetooth.le.ScanFilter; import android.bluetooth.le.ScanResult; import android.bluetooth.le.ScanSettings; import android.content.Context; import android.content.Intent; import android.os.ParcelUuid; import android.os.Handler; import android.os.Looper; import android.widget.Toast; import android.databinding.ObservableField; import net.kwatts.powtools.App; import net.kwatts.powtools.BuildConfig; import net.kwatts.powtools.MainActivity; import net.kwatts.powtools.model.OWDevice; import net.kwatts.powtools.model.OWDevice.DeviceCharacteristic; import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Queue; import java.util.UUID; import java.util.concurrent.TimeUnit; import java.security.MessageDigest; import java.io.ByteArrayOutputStream; import java.io.ByteArrayInputStream; import java.security.DigestInputStream; import timber.log.Timber; public class BluetoothUtilImpl implements BluetoothUtil{ private static final String TAG = BluetoothUtilImpl.class.getSimpleName(); private static final int REQUEST_ENABLE_BT = 1; public static ByteArrayOutputStream inkey = new ByteArrayOutputStream(); public static ObservableField<String> isOWFound = new ObservableField<>(); public Context mContext; Queue<BluetoothGattCharacteristic> characteristicReadQueue = new LinkedList<>(); Queue<BluetoothGattDescriptor> descriptorWriteQueue = new LinkedList<>(); private android.bluetooth.BluetoothAdapter mBluetoothAdapter; private BluetoothLeScanner mBluetoothLeScanner; BluetoothGatt mGatt; BluetoothGattService owGatService; private Map<String, String> mScanResults = new HashMap<>(); private MainActivity mainActivity; OWDevice mOWDevice; public boolean sendKey = true; private ScanSettings settings; private boolean mScanning; private long mDisconnected_time; private int mRetryCount = 0; private int statusMode = 0; private Handler handler; private static int periodicSchedulerCount = 0; public static boolean isGemini = false; //TODO: decouple this crap from the UI/MainActivity @Override public void init(MainActivity mainActivity, OWDevice mOWDevice) { this.mainActivity = mainActivity; this.mContext = mainActivity.getApplicationContext(); this.mOWDevice = mOWDevice; this.mBluetoothAdapter = ((BluetoothManager) mContext.getSystemService(Context.BLUETOOTH_SERVICE)).getAdapter(); //final BluetoothManager manager = (BluetoothManager) mainActivity.getSystemService(Context.BLUETOOTH_SERVICE); //assert manager != null; //mBluetoothAdapter = manager.getAdapter(); mOWDevice.bluetoothLe.set("On"); handler = new Handler(Looper.getMainLooper()); periodicCharacteristics(); } private final BluetoothGattCallback mGattCallback = new BluetoothGattCallback() { @Override public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) { Timber.d( "Bluetooth connection state change: address=" + gatt.getDevice().getAddress()+ " status=" + status + " newState=" + newState); if (newState == BluetoothProfile.STATE_CONNECTED) { Timber.d("STATE_CONNECTED: name=" + gatt.getDevice().getName() + " address=" + gatt.getDevice().getAddress()); BluetoothUtilImpl.isOWFound.set("true"); gatt.discoverServices(); Battery.initStateTwoX(App.INSTANCE.getSharedPreferences()); } else if (newState == BluetoothProfile.STATE_DISCONNECTED) { Timber.d("STATE_DISCONNECTED: name=" + gatt.getDevice().getName() + " address=" + gatt.getDevice().getAddress()); statusMode = 0; BluetoothUtilImpl.isOWFound.set("false"); if (gatt.getDevice().getAddress().equals(mOWDevice.deviceMacAddress.get())) { BluetoothUtilImpl bluetoothUtilImpl = BluetoothUtilImpl.this; onOWStateChangedToDisconnected(gatt,bluetoothUtilImpl.mContext); } //updateLog("--> Closed " + gatt.getDevice().getAddress()); //Timber.d( "Disconnect:" + gatt.getDevice().getAddress()); } } //@SuppressLint("WakelockTimeout") @Override public void onServicesDiscovered(BluetoothGatt gatt, int status) { Timber.d("Only should be here if connecting to OW:" + gatt.getDevice().getAddress()); owGatService = gatt.getService(UUID.fromString(OWDevice.OnewheelServiceUUID)); if (owGatService == null) { if (gatt.getDevice().getName() == null) { Timber.i("--> " + gatt.getDevice().getAddress() + " not OW, moving on."); } else { Timber.i("--> " + gatt.getDevice().getName() + " not OW, moving on."); } return; } mGatt = gatt; Timber.i("Hey, I found the OneWheel Service: " + owGatService.getUuid().toString()); mainActivity.deviceConnectedTimer(true); mOWDevice.isConnected.set(true); App.INSTANCE.acquireWakeLock(); String deviceMacAddress = mGatt.getDevice().toString(); String deviceMacName = mGatt.getDevice().getName(); mOWDevice.deviceMacAddress.set(deviceMacAddress); mOWDevice.deviceMacName.set(deviceMacName); App.INSTANCE.getSharedPreferences().saveMacAddress( mOWDevice.deviceMacAddress.get(), mOWDevice.deviceMacName.get() ); scanLeDevice(false); // Stability updates per https://github.com/ponewheel/android-ponewheel/issues/86#issuecomment-460033659 // Step 1: In OnServicesDiscovered, JUST read the firmware version. Timber.d("Stability Step 1: Only reading the firmware version!"); //new Handler(Looper.getMainLooper()).postDelayed(new Runnable() { handler.postDelayed(new Runnable() { public void run() { BluetoothUtilImpl.this.mGatt.readCharacteristic(BluetoothUtilImpl.this.owGatService.getCharacteristic(UUID.fromString(OWDevice.OnewheelCharacteristicFirmwareRevision))); } }, 500); /* BluetoothGattCharacteristic c = owGatService.getCharacteristic(UUID.fromString(OWDevice.OnewheelCharacteristicFirmwareRevision)); if (c != null) { if (isCharacteristicReadable(c)) { characteristicReadQueue.add(c); Timber.d( "characteristicReadQueue.size =" + characteristicReadQueue.size() + " descriptorWriteQueue.size:" + descriptorWriteQueue.size()); if (characteristicReadQueue.size() == 1 && (descriptorWriteQueue.size() == 0)) { mGatt.readCharacteristic(c); } } } for(OWDevice.DeviceCharacteristic deviceCharacteristic: mOWDevice.getNotifyCharacteristics()) { String uuid = deviceCharacteristic.uuid.get(); if (uuid != null && deviceCharacteristic.isNotifyCharacteristic) { BluetoothGattCharacteristic localCharacteristic = owGatService.getCharacteristic(UUID.fromString(uuid)); if (localCharacteristic != null) { if (isCharacteristicNotifiable(localCharacteristic) && deviceCharacteristic.isNotifyCharacteristic) { mGatt.setCharacteristicNotification(localCharacteristic, true); BluetoothGattDescriptor descriptor = localCharacteristic.getDescriptor(UUID.fromString(OWDevice.OnewheelConfigUUID)); Timber.d( "descriptorWriteQueue.size:" + descriptorWriteQueue.size()); if (descriptor == null) { Timber.e( uuid + " has a null descriptor!"); } else { descriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE); descriptorWriteQueue.add(descriptor); if (descriptorWriteQueue.size() == 1) { mGatt.writeDescriptor(descriptor); } Timber.d( uuid + " has been set for notifications"); } } } } } for(OWDevice.DeviceCharacteristic dc : mOWDevice.getReadCharacteristics()) { if (dc.uuid.get() != null) { BluetoothGattCharacteristic c = owGatService.getCharacteristic(UUID.fromString(dc.uuid.get())); if (c != null) { if (isCharacteristicReadable(c)) { characteristicReadQueue.add(c); //Read if 1 in the queue, if > 1 then we handle asynchronously in the onCharacteristicRead callback //GIVE PRECEDENCE to descriptor writes. They must all finish first. Timber.d( "characteristicReadQueue.size =" + characteristicReadQueue.size() + " descriptorWriteQueue.size:" + descriptorWriteQueue.size()); if (characteristicReadQueue.size() == 1 && (descriptorWriteQueue.size() == 0)) { Timber.i( dc.uuid.get() + " is readable and added to queue"); mGatt.readCharacteristic(c); } } } } } */ } @Override public void onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic c, int status) { String characteristic_uuid = c.getUuid().toString(); Timber.d( "BluetoothGattCallback.onCharacteristicRead: CharacteristicUuid=" + characteristic_uuid + ",status=" + status + ",isGemini=" + isGemini); if (characteristicReadQueue.size() > 0) { characteristicReadQueue.remove(); } // Stability Step 2: In OnCharacteristicRead, if the value is of the char firmware version, parse it's value. // If its >= 4034, JUST write the descriptor for the Serial Read characteristic to Enable notifications, // and set notify to true with gatt. Otherwise its Andromeda or lower and we can call the method to // read & notify all the characteristics we want. (Although I learned doing this that some android devices // have a max of 12 notify characteristics at once for some reason. At least I'm pretty sure.) // I also set a class-wide boolean value isGemini to true here so I don't have to keep checking if its Andromeda // or Gemini later on. if (characteristic_uuid.equals(OWDevice.OnewheelCharacteristicFirmwareRevision)) { Timber.d("We have the firmware revision! Checking version."); if (unsignedShort(c.getValue()) >= 4034) { Timber.d("It's Gemini!"); isGemini = true; Timber.d("Stability Step 2.1: JUST write the descriptor for the Serial Read characteristic to Enable notifications"); BluetoothGattCharacteristic gC = owGatService.getCharacteristic(UUID.fromString(OWDevice.OnewheelCharacteristicUartSerialRead)); gatt.setCharacteristicNotification(gC, true); Timber.d("and set notify to true with gatt..."); BluetoothGattDescriptor descriptor = gC.getDescriptor(UUID.fromString(OWDevice.OnewheelConfigUUID)); descriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE); gatt.writeDescriptor(descriptor); } else { Timber.d("It's before Gemini, likely Andromeda - calling read and notify characteristics"); isGemini = false; whenActuallyConnected(); } } else if (characteristic_uuid.equals(OWDevice.OnewheelCharacteristicRidingMode)) { Timber.d( "Got ride mode from the main UI thread:" + c.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT8, 1)); } //else if (characteristic_uuid.equals(OWDevice.OnewheelCharacteristicUartSerialRead)) { // Timber.d("Got OnewheelCharacteristicUartSerialRead, calling unlockKeyGemini! "); // unlockKeyGemini(gatt, c.getValue()); // } if (BuildConfig.DEBUG) { byte[] v_bytes = c.getValue(); StringBuilder sb = new StringBuilder(); for (byte b : c.getValue()) { sb.append(String.format("%02x", b)); } Timber.d( "HEX %02x: " + sb); Timber.d( "Arrays.toString() value: " + Arrays.toString(v_bytes)); Timber.d( "String value: " + c.getStringValue(0)); Timber.d( "Unsigned short: " + unsignedShort(v_bytes)); Timber.d( "getIntValue(FORMAT_UINT8,0) " + c.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT8, 0)); Timber.d( "getIntValue(FORMAT_UINT8,1) " + c.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT8, 1)); } mOWDevice.processUUID(c); mOWDevice.setBatteryRemaining(mainActivity); // Callback to make sure the queue is drained if (characteristicReadQueue.size() > 0) { gatt.readCharacteristic(characteristicReadQueue.element()); } } @Override public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic c) { //Timber.d( "BluetoothGattCallback.onCharacteristicChanged: CharacteristicUuid=" + c.getUuid().toString()); // https://github.com/ponewheel/android-ponewheel/issues/86 //if (isGemini && (c.getUuid().toString().equals(OWDevice.OnewheelCharacteristicUartSerialRead))) { // Step 4: In OnCharacteristicChanged, if isGemini and characteristic is serial read, // do the gemini hash crap and stuff and setNotify for serial read to false. // Timber.d("Stability Step 4: Gemini unlock & setting setNotify for serial read to false"); // unlockKeyGemini(gatt,c.getValue()); //} BluetoothGatt bluetoothGatt = gatt; BluetoothGattCharacteristic bluetoothGattCharacteristic = c; if (isGemini && (c.getUuid().toString().equals(OWDevice.OnewheelCharacteristicUartSerialRead))) { try { Timber.d("Setting up inkey!"); inkey.write(c.getValue()); if (inkey.toByteArray().length >= 20 && sendKey) { StringBuilder sb = new StringBuilder(); sb.append("GEMINI Step #2: convert inkey="); sb.append(Arrays.toString(inkey.toByteArray())); Timber.d("GEMINI:" + sb.toString()); ByteArrayOutputStream outkey = new ByteArrayOutputStream(); outkey.write(Util.StringToByteArrayFastest("43:52:58")); byte[] arrayToMD5_part1 = Arrays.copyOfRange(BluetoothUtilImpl.inkey.toByteArray(), 3, 19); byte[] arrayToMD5_part2 = Util.StringToByteArrayFastest("D9255F0F23354E19BA739CCDC4A91765"); ByteBuffer arrayToMD5 = ByteBuffer.allocate(arrayToMD5_part1.length + arrayToMD5_part2.length); arrayToMD5.put(arrayToMD5_part1); arrayToMD5.put(arrayToMD5_part2); MessageDigest localMessageDigest = MessageDigest.getInstance("MD5"); DigestInputStream digestInputStream = new DigestInputStream(new ByteArrayInputStream(arrayToMD5.array()), localMessageDigest); while (digestInputStream.read(new byte[]{101}) != -1) { } digestInputStream.close(); outkey.write(localMessageDigest.digest()); byte checkByte = 0; for (byte b : outkey.toByteArray()) { checkByte = (byte) (b ^ checkByte); } outkey.write(checkByte); StringBuilder sb2 = new StringBuilder(); byte[] bArr = arrayToMD5_part1; sb2.append("GEMINI Step #3: write outkey="); sb2.append(Arrays.toString(outkey.toByteArray())); Timber.d("GEMINI" + sb2.toString()); BluetoothGattCharacteristic lc = owGatService.getCharacteristic(UUID.fromString(OWDevice.OnewheelCharacteristicUartSerialWrite)); lc.setValue(outkey.toByteArray()); if (!bluetoothGatt.writeCharacteristic(lc)) { BluetoothGattCharacteristic bluetoothGattCharacteristic2 = lc; sendKey = true; } else { sendKey = false; bluetoothGatt.setCharacteristicNotification(bluetoothGattCharacteristic, false); } outkey.reset(); } } catch (Exception e) { StringBuilder sb3 = new StringBuilder(); sb3.append("Exception with GEMINI obfuckstation:"); sb3.append(e.getMessage()); Timber.d("GEMINI" + sb3.toString()); } } mOWDevice.processUUID(bluetoothGattCharacteristic); mOWDevice.setBatteryRemaining(mainActivity); } @Override public void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic c, int status) { Timber.i( "onCharacteristicWrite: " + status + ", CharacteristicUuid=" + c.getUuid().toString()); // Step 5: In OnCharacteristicWrite, if isGemini & characteristic is Serial Write, NOW setNotify // and read all the characteristics you want. its also only now that I start the // repeated handshake clock thing but I don't think it really matters, this all happens pretty quick. if (isGemini && (c.getUuid().toString().equals(OWDevice.OnewheelCharacteristicUartSerialWrite))) { Timber.d("Step 5: Gemini and serial write, kicking off all the read and notifies..."); whenActuallyConnected(); } } @Override public void onDescriptorWrite(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) { Timber.i( "onDescriptorWrite: " + status + ",descriptor=" + descriptor.getUuid().toString() + ",descriptor_characteristic=" + descriptor.getCharacteristic().getUuid().toString()); if (isGemini && descriptor.getCharacteristic().getUuid().toString().equals(OWDevice.OnewheelCharacteristicUartSerialRead)) { Timber.d("Stability Step 3: if isGemini and the characteristic descriptor that was written was Serial Write" + "then trigger the 20 byte input key over multiple serial ble notification stream by writing the firmware version onto itself"); gatt.writeCharacteristic(owGatService.getCharacteristic(UUID.fromString(OWDevice.OnewheelCharacteristicFirmwareRevision))); } //DeviceCharacteristic dc = mOWDevice.characteristics.get(descriptor.getCharacteristic().getUuid().toString()); //if (dc != null && (dc.state == 0 || dc.state == 1)) { // gatt.setCharacteristicNotification( owGatService.getCharacteristic(UUID.fromString(dc.uuid.get())), true); // // } if (descriptorWriteQueue.size() > 0) { descriptorWriteQueue.remove(); if (descriptorWriteQueue.size() > 0) { gatt.writeDescriptor(descriptorWriteQueue.element()); } else if (characteristicReadQueue.size() > 0) { gatt.readCharacteristic(characteristicReadQueue.element()); } } // Step 3: In OnDescriptorWrite, if isGemini and the characteristic descriptor that was // written was Serial Write, then trigger the byte stream by writing the firmware version // onto itself. /* if (isGemini && (descriptor.equals(OWDevice.OnewheelCharacteristicUartSerialWrite))) { Timber.d("Step 3: Is Gemini, writing the descriptor onto itself"); gatt.writeDescriptor(descriptor); } */ } }; private void updateLog(String s) { mainActivity.updateLog(s); } void scanLeDevice(final boolean enable) { Timber.d("scanLeDevice enable = " + enable); if (enable) { mScanning = true; List<ScanFilter> filters_v2 = new ArrayList<>(); ScanFilter scanFilter = new ScanFilter.Builder() .setServiceUuid(ParcelUuid.fromString(OWDevice.OnewheelServiceUUID)) .build(); filters_v2.add(scanFilter); //c03f7c8d-5e96-4a75-b4b6-333d36230365 mBluetoothLeScanner.startScan(filters_v2, settings, mScanCallback); } else { mScanning = false; mBluetoothLeScanner.stopScan(mScanCallback); // added 10/23 to try cleanup mBluetoothLeScanner.flushPendingScanResults(mScanCallback); } mainActivity.invalidateOptionsMenu(); } private ScanCallback mScanCallback = new ScanCallback() { @Override public void onScanResult(int callbackType, ScanResult result) { String deviceName = result.getDevice().getName(); String deviceAddress = result.getDevice().getAddress(); Timber.i( "ScanCallback.onScanResult: " + mScanResults.entrySet()); if (!mScanResults.containsKey(deviceAddress)) { Timber.i( "ScanCallback.deviceName:" + deviceName); mScanResults.put(deviceAddress, deviceName); if (deviceName == null) { Timber.i("Found " + deviceAddress); } else { Timber.i("Found " + deviceAddress + " (" + deviceName + ")"); } if (deviceName != null && (deviceName.startsWith("ow") || deviceName.startsWith("Onewheel"))) { mRetryCount = 0; Timber.i("Looks like we found our OW device (" + deviceName + ") discovering services!"); connectToDevice(result.getDevice()); } else { Timber.d("onScanResult: found another device:" + deviceName + "-" + deviceAddress); } } else { Timber.d("onScanResult: mScanResults already had our key, still connecting to OW services or something is up with the BT stack."); // Timber.d("onScanResult: mScanResults already had our key," + "deviceName=" + deviceName + ",deviceAddress=" + deviceAddress); // still connect //connectToDevice(result.getDevice()); } } @Override public void onBatchScanResults(List<ScanResult> results) { for (ScanResult sr : results) { Timber.i("ScanCallback.onBatchScanResults.each:" + sr.toString()); } } @Override public void onScanFailed(int errorCode) { Timber.e( "ScanCallback.onScanFailed:" + errorCode); } }; public void connectToDevice(BluetoothDevice device) { Timber.d( "connectToDevice:" + device.getName()); device.connectGatt(mainActivity, false, mGattCallback); } public void connectToGatt(BluetoothGatt gatt) { Timber.d( "connectToGatt:" + gatt.getDevice().getName()); gatt.connect(); } private void onOWStateChangedToDisconnected(BluetoothGatt gatt, Context context) { //TODO: we really should have a BluetoothService we kill and restart Timber.i("We got disconnected from our Device: " + gatt.getDevice().getAddress()); if (Looper.myLooper() == null) { Looper.prepare(); } Toast.makeText(mainActivity, "We got disconnected from our device: " + gatt.getDevice().getAddress(), Toast.LENGTH_SHORT).show(); mainActivity.deviceConnectedTimer(false); mOWDevice.isConnected.set(false); App.INSTANCE.releaseWakeLock(); mScanResults.clear(); if (App.INSTANCE.getSharedPreferences().shouldAutoReconnect()) { mRetryCount++; Timber.i("mRetryCount=" + mRetryCount); try { if (mRetryCount == 20) { Timber.i("Reached too many retries, stopping search"); gatt.close(); stopScanning(); disconnect(); //mainActivity.invalidateOptionsMenu(); } else { Timber.i("Waiting for 5 seconds until trying to connect to OW again."); TimeUnit.SECONDS.sleep(5); Timber.i("Trying to connect to OW at " + mOWDevice.deviceMacAddress.get()); //BluetoothDevice device = mBluetoothAdapter.getRemoteDevice(mOWDevice.deviceMacAddress.get()); //connectToDevice(device); gatt.connect(); } } catch (InterruptedException e) { Timber.d("Connection to OW got interrupted:" + e.getMessage()); } } else { gatt.close(); mainActivity.invalidateOptionsMenu(); } } public static boolean isCharacteristicWriteable(BluetoothGattCharacteristic c) { return (c.getProperties() & (BluetoothGattCharacteristic.PROPERTY_WRITE | BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE)) != 0; } public static boolean isCharacteristicReadable(BluetoothGattCharacteristic c) { return ((c.getProperties() & BluetoothGattCharacteristic.PROPERTY_READ) != 0); } public static boolean isCharacteristicNotifiable(BluetoothGattCharacteristic c) { return (c.getProperties() & BluetoothGattCharacteristic.PROPERTY_NOTIFY) != 0; } /* public void unlockKeyGemini(BluetoothGatt gatt, byte[] c) { try { inkey.write(c); if (inkey.toByteArray().length == 20) { ByteArrayOutputStream outkey = new ByteArrayOutputStream(); outkey.write(Util.StringToByteArrayFastest("43:52:58")); // Take almost all of the bytes from the input array. This is almost the same as the last part as // we are ignoring the first 3 and the last bytes. byte[] arrayToMD5_part1 = Arrays.copyOfRange(inkey.toByteArray(), 3, 19); byte[] arrayToMD5_part2 = Util.StringToByteArrayFastest("D9255F0F23354E19BA739CCDC4A91765"); // New byte array we are going to MD5 hash. Part of the input string, part of this static string. ByteBuffer arrayToMD5 = ByteBuffer.allocate(arrayToMD5_part1.length + arrayToMD5_part2.length); arrayToMD5.put(arrayToMD5_part1); arrayToMD5.put(arrayToMD5_part2); // Start prepping the MD5 hash MessageDigest localMessageDigest = MessageDigest.getInstance("MD5"); DigestInputStream digestInputStream = new DigestInputStream(new ByteArrayInputStream(arrayToMD5.array()), localMessageDigest); // This is actually the byte that represents a space character. ¯\_(ツ)_/¯ byte[] arrayOfByte = new byte[]{101}; while (digestInputStream.read(arrayOfByte) != -1) { } digestInputStream.close(); byte[] md5Hash = localMessageDigest.digest(); // Add it to the 3 bytes we already have. outkey.write(md5Hash); // Validate the check byte. byte checkByte = 0; int j = outkey.toByteArray().length; int i = 0; while (i < j) { checkByte = ((byte) (outkey.toByteArray()[i] ^ checkByte)); i += 1; } outkey.write(checkByte); // Finally, write out to the OW serial UART characteristic Timber.d("GEMINI Step #1: write outkey=" + Arrays.toString(outkey.toByteArray())); BluetoothGattCharacteristic lc = owGatService.getCharacteristic(UUID.fromString(OWDevice.OnewheelCharacteristicUartSerialWrite)); lc.setValue(outkey.toByteArray()); gatt.writeCharacteristic(lc); // cleanup and stop notifications to serial read outkey.reset(); inkey.reset(); BluetoothGattCharacteristic lcn = owGatService.getCharacteristic(UUID.fromString(OWDevice.OnewheelCharacteristicUartSerialRead)); gatt.setCharacteristicNotification(lcn, false); } } catch(Exception e){ Timber.e("Exception with GEMINI obfuckstation:" + e.getMessage()); } } */ // Helpers public static int unsignedByte(byte var0) { return var0 & 255; } public static int unsignedShort(byte[] var0) { // Short.valueOf(ByteBuffer.wrap(v_bytes).getShort()) also works int var1; if(var0.length < 2) { var1 = -1; } else { var1 = (unsignedByte(var0[0]) << 8) + unsignedByte(var0[1]); } return var1; } @Override public boolean isConnected() { return mBluetoothAdapter != null && mBluetoothAdapter.isEnabled(); } @Override public boolean isGemini() { return this.isGemini; } @Override public void reconnect(MainActivity activity) { activity.startActivityForResult(new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE), 1); } @Override public void stopScanning() { scanLeDevice(false); if (mGatt != null) { mGatt.disconnect(); mGatt.close(); mGatt = null; } mOWDevice.isConnected.set(false); this.mScanResults.clear(); descriptorWriteQueue.clear(); this.mRetryCount = 0; // Added stuff 10/23 to clean fix owGatService = null; // Added more 3/12/2018 this.characteristicReadQueue.clear(); inkey.reset(); isOWFound.set("false"); this.sendKey = true; } @Override public boolean isScanning() { return mScanning; } @Override public void startScanning() { mBluetoothLeScanner = mBluetoothAdapter.getBluetoothLeScanner(); settings = new ScanSettings.Builder().setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY).build(); scanLeDevice(true); } @Override public void disconnect() { scanLeDevice(false); if (mGatt != null) { mGatt.disconnect(); mGatt.close(); mGatt = null; } this.mScanResults.clear(); descriptorWriteQueue.clear(); // Added stuff 10/23 to clean fix owGatService = null; inkey.reset(); isOWFound.set("false"); this.sendKey = true; statusMode = 0; } @Override public BluetoothGattCharacteristic getCharacteristic(String uuidLookup) { return owGatService.getCharacteristic(UUID.fromString(uuidLookup)); } @Override public void writeCharacteristic(BluetoothGattCharacteristic bluetoothGattCharacteristic) { mGatt.writeCharacteristic(bluetoothGattCharacteristic); } @Override public int getStatusMode() { return statusMode; } private void periodicCharacteristics() { final int repeatTime = 60000; //every minute periodicSchedulerCount++; if (statusMode == 2) { walkReadQueue(1); } handler.postDelayed(new Runnable() { @Override public void run() { if (statusMode == 2) { walkReadQueue(1); } if (periodicSchedulerCount == 1) { handler.postDelayed(this, repeatTime); } else { periodicSchedulerCount--; } } }, repeatTime); } private void walkNotifyQueue(int state) { for(OWDevice.DeviceCharacteristic dc: mOWDevice.getNotifyCharacteristics()) { String uuid = dc.uuid.get(); if (uuid != null && dc.isNotifyCharacteristic && dc.state == state) { BluetoothGattCharacteristic localCharacteristic = owGatService.getCharacteristic(UUID.fromString(uuid)); if (localCharacteristic != null) { if (isCharacteristicNotifiable(localCharacteristic) && dc.isNotifyCharacteristic) { mGatt.setCharacteristicNotification(localCharacteristic, true); BluetoothGattDescriptor descriptor = localCharacteristic.getDescriptor(UUID.fromString(OWDevice.OnewheelConfigUUID)); Timber.d( "descriptorWriteQueue.size:" + descriptorWriteQueue.size()); if (descriptor == null) { Timber.e( uuid + " has a null descriptor!"); } else { descriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE); descriptorWriteQueue.add(descriptor); if (descriptorWriteQueue.size() == 1) { mGatt.writeDescriptor(descriptor); } Timber.d( uuid + " has been set for notifications"); } } } } } } private void walkReadQueue(int state) { for(OWDevice.DeviceCharacteristic dc : mOWDevice.getReadCharacteristics()) { if (dc.uuid.get() != null && !dc.isNotifyCharacteristic && dc.state == state) { Timber.d("uuid:%s, state:%d", dc.uuid.get(), dc.state); BluetoothGattCharacteristic c = owGatService.getCharacteristic(UUID.fromString(dc.uuid.get())); if (c != null) { if (isCharacteristicReadable(c)) { characteristicReadQueue.add(c); //Read if 1 in the queue, if > 1 then we handle asynchronously in the onCharacteristicRead callback //GIVE PRECEDENCE to descriptor writes. They must all finish first. Timber.d( "characteristicReadQueue.size =" + characteristicReadQueue.size() + " descriptorWriteQueue.size:" + descriptorWriteQueue.size()); if (characteristicReadQueue.size() == 1 && (descriptorWriteQueue.size() == 0)) { Timber.i( dc.uuid.get() + " is readable and added to queue"); mGatt.readCharacteristic(c); } } } } } } public void whenActuallyConnected() { walkNotifyQueue(0); walkReadQueue(0); walkReadQueue(1); statusMode = 2; /* for (DeviceCharacteristic dc : mOWDevice.getNotifyCharacteristics()) { String uuid = dc.uuid.get(); if (uuid != null && dc.state == 0) { try { BluetoothGattCharacteristic gC = owGatService.getCharacteristic(UUID.fromString(uuid)); if (gC != null) { BluetoothGattDescriptor descriptor = gC.getDescriptor(UUID.fromString(OWDevice.OnewheelConfigUUID)); descriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE); descriptorWriteQueue.add(descriptor); if (this.descriptorWriteQueue.size() == 1) { this.mGatt.writeDescriptor(descriptor); } Timber.d( uuid + " has been set for notifications"); //this.characteristicReadQueue.add(gC); } } catch (Exception e) { Timber.e("Exception trying to set notification for: " + uuid); } } } for (DeviceCharacteristic dc2 : this.mOWDevice.getReadCharacteristics()) { String uuid = dc2.uuid.get(); if (uuid != null && dc2.state == 3) { BluetoothGattCharacteristic gC2 = this.owGatService.getCharacteristic(UUID.fromString(uuid)); if (gC2 != null) { this.characteristicReadQueue.add(gC2); } } } for(OWDevice.DeviceCharacteristic deviceCharacteristic: mOWDevice.getNotifyCharacteristics()) { String uuid = deviceCharacteristic.uuid.get(); if (uuid != null && deviceCharacteristic.state == 0) { BluetoothGattCharacteristic localCharacteristic = owGatService.getCharacteristic(UUID.fromString(uuid)); if (localCharacteristic != null) { if (isCharacteristicNotifiable(localCharacteristic)) { mGatt.setCharacteristicNotification(localCharacteristic, true); BluetoothGattDescriptor descriptor = localCharacteristic.getDescriptor(UUID.fromString(OWDevice.OnewheelConfigUUID)); Timber.d( "descriptorWriteQueue.size:" + descriptorWriteQueue.size()); if (descriptor == null) { Timber.e( uuid + " has a null descriptor!"); } else { descriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE); descriptorWriteQueue.add(descriptor); if (descriptorWriteQueue.size() == 1) { mGatt.writeDescriptor(descriptor); } Timber.d( uuid + " has been set for notifications"); } } } } } for(OWDevice.DeviceCharacteristic dc : mOWDevice.getReadCharacteristics()) { if (dc.uuid.get() != null) { BluetoothGattCharacteristic c = owGatService.getCharacteristic(UUID.fromString(dc.uuid.get())); if (c != null && dc.state == 3) { if (isCharacteristicReadable(c)) { characteristicReadQueue.add(c); //Read if 1 in the queue, if > 1 then we handle asynchronously in the onCharacteristicRead callback //GIVE PRECEDENCE to descriptor writes. They must all finish first. Timber.d( "characteristicReadQueue.size =" + characteristicReadQueue.size() + " descriptorWriteQueue.size:" + descriptorWriteQueue.size()); if (characteristicReadQueue.size() == 1 && (descriptorWriteQueue.size() == 0)) { Timber.i( dc.uuid.get() + " is readable and added to queue"); mGatt.readCharacteristic(c); } } } } } */ } }