diff --git a/app/src/main/java/svenmeier/coxswain/bluetooth/BlueUtils.java b/app/src/main/java/svenmeier/coxswain/bluetooth/BlueUtils.java deleted file mode 100644 index eca56d89..00000000 --- a/app/src/main/java/svenmeier/coxswain/bluetooth/BlueUtils.java +++ /dev/null @@ -1,73 +0,0 @@ -package svenmeier.coxswain.bluetooth; - -import android.annotation.TargetApi; -import android.bluetooth.BluetoothGatt; -import android.bluetooth.BluetoothGattCharacteristic; -import android.bluetooth.BluetoothGattDescriptor; -import android.os.Build; - -import com.google.android.gms.fitness.request.BleScanCallback; - -import java.util.UUID; - -@TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR2) -public class BlueUtils { - - public static final UUID SERVICE_HEART_RATE = uuid(0x180D); - - public static final UUID CHARACTERISTIC_HEART_RATE_MEASUREMENT = uuid(0x2A37); - - public static final UUID SERVICE_FITNESS_MACHINE = uuid(0x1826); - - public static final UUID CHARACTERISTIC_ROWER_DATA = uuid(0x2AD1); - - /** - * https://github.com/kinetic-fit/sensors-swift/blob/master/Sources/SwiftySensors/FitnessMachineService.swift - * https://github.com/kinetic-fit/sensors-swift/blob/master/Sources/SwiftySensors/FitnessMachineSerializer.swift - */ - public static final UUID CHARACTERISTIC_CONTROL_POINT = uuid(0x2AD9); - - public static final UUID CLIENT_CHARACTERISTIC_DESCIPRTOR = uuid(0x2902); - - public static final UUID uuid(int id) { - return UUID.fromString(String.format("%08X-0000-1000-8000-00805f9b34fb", id)); - } - - public static boolean enableNotification(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) { - - gatt.setCharacteristicNotification(characteristic, true); - - BluetoothGattDescriptor descriptor = characteristic.getDescriptor(BlueUtils.CLIENT_CHARACTERISTIC_DESCIPRTOR); - if (descriptor == null) { - return false; - } else { - descriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE); - gatt.writeDescriptor(descriptor); - return true; - } - } - - public static boolean enableIndication(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) { - - gatt.setCharacteristicNotification(characteristic, true); - - BluetoothGattDescriptor descriptor = characteristic.getDescriptor(BlueUtils.CLIENT_CHARACTERISTIC_DESCIPRTOR); - if (descriptor == null) { - return false; - } else { - descriptor.setValue(BluetoothGattDescriptor.ENABLE_INDICATION_VALUE); - gatt.writeDescriptor(descriptor); - return true; - } - } - - public static boolean write(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int value) { - characteristic.setValue(new byte[]{(byte)value}); - return gatt.writeCharacteristic(characteristic); - } - - public static boolean write(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, byte... value) { - characteristic.setValue(value); - return gatt.writeCharacteristic(characteristic); - } -} diff --git a/app/src/main/java/svenmeier/coxswain/bluetooth/BlueWriter.java b/app/src/main/java/svenmeier/coxswain/bluetooth/BlueWriter.java new file mode 100644 index 00000000..9a104ca0 --- /dev/null +++ b/app/src/main/java/svenmeier/coxswain/bluetooth/BlueWriter.java @@ -0,0 +1,160 @@ +package svenmeier.coxswain.bluetooth; + +import android.annotation.TargetApi; +import android.bluetooth.BluetoothGatt; +import android.bluetooth.BluetoothGattCallback; +import android.bluetooth.BluetoothGattCharacteristic; +import android.bluetooth.BluetoothGattDescriptor; +import android.bluetooth.BluetoothGattService; +import android.os.Build; +import android.support.annotation.CallSuper; +import android.util.Log; + +import java.util.ArrayDeque; +import java.util.Queue; +import java.util.UUID; + +import svenmeier.coxswain.Coxswain; + +@TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR2) +public class BlueWriter extends BluetoothGattCallback { + + public static final UUID SERVICE_HEART_RATE = uuid(0x180D); + public static final UUID CHARACTERISTIC_HEART_RATE_MEASUREMENT = uuid(0x2A37); + + public static final UUID SERVICE_DEVICE_INFORMATION = uuid(0x180A); + public static final UUID CHARACTERISTIC_SOFTWARE_REVISION = uuid(0x2A28); + + public static final UUID SERVICE_FITNESS_MACHINE = uuid(0x1826); + public static final UUID CHARACTERISTIC_ROWER_DATA = uuid(0x2AD1); + public static final UUID CHARACTERISTIC_CONTROL_POINT = uuid(0x2AD9); + + public static final UUID CLIENT_CHARACTERISTIC_DESCIPRTOR = uuid(0x2902); + + private Queue requests = new ArrayDeque<>(); + + private Request current = null; + + private interface Request { + + void request(); + } + + private void request(Request request) { + requests.add(request); + + requestNext(); + } + + private void requestNext() { + if (current != null) { + return; + } + + current = requests.poll(); + if (current != null) { + current.request(); + } + } + + public BluetoothGattCharacteristic get(BluetoothGatt gatt, UUID service, UUID characteristic) { + BluetoothGattService s = gatt.getService(service); + if (s == null) { + return null; + } + + BluetoothGattCharacteristic c = s.getCharacteristic(characteristic); + return c; + } + + @CallSuper + @Override + public void onDescriptorWrite(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) { + if (current != null) { + current = null; + requestNext(); + } + } + + @CallSuper + @Override + public void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) { + if (current != null) { + current = null; + requestNext(); + } + } + + @CallSuper + @Override + public void onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) { + if (current != null) { + current = null; + requestNext(); + } + } + + public void enableNotification(final BluetoothGatt gatt, final BluetoothGattCharacteristic characteristic) { + + request(new Request() { + @Override + public void request() { + gatt.setCharacteristicNotification(characteristic, true); + + BluetoothGattDescriptor descriptor = characteristic.getDescriptor(CLIENT_CHARACTERISTIC_DESCIPRTOR); + if (descriptor != null) { + descriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE); + if (gatt.writeDescriptor(descriptor) == false) { + Log.e(Coxswain.TAG, "bluetooth enabled notification failed"); + } + } + } + }); + } + + public void enableIndication(final BluetoothGatt gatt, final BluetoothGattCharacteristic characteristic) { + + request(new Request() { + @Override + public void request() { + gatt.setCharacteristicNotification(characteristic, true); + + BluetoothGattDescriptor descriptor = characteristic.getDescriptor(CLIENT_CHARACTERISTIC_DESCIPRTOR); + if (descriptor != null) { + descriptor.setValue(BluetoothGattDescriptor.ENABLE_INDICATION_VALUE); + if (gatt.writeDescriptor(descriptor) == false) { + Log.e(Coxswain.TAG, "bluetooth enabled indication failed"); + } + } + } + }); + } + + public void write(final BluetoothGatt gatt, final BluetoothGattCharacteristic characteristic, final byte value) { + + request(new Request() { + @Override + public void request() { + characteristic.setValue(value, BluetoothGattCharacteristic.FORMAT_UINT8, 0); + if (gatt.writeCharacteristic(characteristic) == false) { + Log.e(Coxswain.TAG, "bluetooth write failed"); + } + } + }); + } + + public void read(final BluetoothGatt gatt, final BluetoothGattCharacteristic characteristic) { + request(new Request() { + @Override + public void request() { + if (gatt.readCharacteristic(characteristic) == false) { + Log.e(Coxswain.TAG, "bluetooth read failed"); + } + } + }); + } + + public static final UUID uuid(int id) { + return UUID.fromString(String.format("%08X-0000-1000-8000-00805f9b34fb", id)); + } +} \ No newline at end of file diff --git a/app/src/main/java/svenmeier/coxswain/bluetooth/BluetoothHeart.java b/app/src/main/java/svenmeier/coxswain/bluetooth/BluetoothHeart.java index c5424685..478dfaf8 100644 --- a/app/src/main/java/svenmeier/coxswain/bluetooth/BluetoothHeart.java +++ b/app/src/main/java/svenmeier/coxswain/bluetooth/BluetoothHeart.java @@ -256,7 +256,7 @@ public void open() { } String name = context.getString(R.string.bluetooth_heart); - IntentFilter filter = BluetoothActivity.start(context, name, BlueUtils.SERVICE_HEART_RATE.toString()); + IntentFilter filter = BluetoothActivity.start(context, name, BlueWriter.SERVICE_HEART_RATE.toString()); context.registerReceiver(this, filter); registered = true; } @@ -297,7 +297,7 @@ public void close() } @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR2) - private class GattConnection extends BluetoothGattCallback implements Connection, Runnable { + private class GattConnection extends BlueWriter implements Connection, Runnable { private final String address; @@ -380,31 +380,25 @@ public synchronized void close() { } @Override - public synchronized void onServicesDiscovered(BluetoothGatt gatt, int status) { + public synchronized void onServicesDiscovered(final BluetoothGatt gatt, int status) { if (connected == null) { return; } - BluetoothGattService service = gatt.getService(BlueUtils.SERVICE_HEART_RATE); - if (service == null) { - Log.d(Coxswain.TAG, "bluetooth no heart rate"); + heartRateMeasurement = get(gatt, SERVICE_HEART_RATE, CHARACTERISTIC_HEART_RATE_MEASUREMENT); + if (heartRateMeasurement == null) { + Log.d(Coxswain.TAG, "bluetooth no heart rate measurement"); } else { - heartRateMeasurement = service.getCharacteristic(BlueUtils.CHARACTERISTIC_HEART_RATE_MEASUREMENT); - if (heartRateMeasurement == null) { - Log.d(Coxswain.TAG, "bluetooth no heart rate measurement"); - } else { - if (BlueUtils.enableNotification(gatt, heartRateMeasurement)) { - toast(context.getString(R.string.bluetooth_heart_connected, gatt.getDevice().getAddress())); - return; - } - Log.d(Coxswain.TAG, "bluetooth no heart rate measurement notification"); - } + enableNotification(gatt, heartRateMeasurement); } - heartRateMeasurement = null; - toast(context.getString(R.string.bluetooth_heart_not_found, gatt.getDevice().getAddress())); + if (heartRateMeasurement == null) { + toast(context.getString(R.string.bluetooth_heart_not_found, gatt.getDevice().getAddress())); - select(); + select(); + } else { + toast(context.getString(R.string.bluetooth_heart_connected, gatt.getDevice().getAddress())); + } } @Override @@ -414,15 +408,17 @@ public synchronized void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGa return; } - int heartRate; - Fields fields = new Fields(characteristic, Fields.UINT8); - if (fields.flag(0)) { - heartRate = fields.get(Fields.UINT16); - } else { - heartRate = fields.get(Fields.UINT8); - } + if (characteristic.getUuid().equals(heartRateMeasurement.getUuid())) { + int heartRate; + Fields fields = new Fields(characteristic, Fields.UINT8); + if (fields.flag(0)) { + heartRate = fields.get(Fields.UINT16); + } else { + heartRate = fields.get(Fields.UINT8); + } - onHeartRate(heartRate); + onHeartRate(heartRate); + } } /** diff --git a/app/src/main/java/svenmeier/coxswain/bluetooth/Fields.java b/app/src/main/java/svenmeier/coxswain/bluetooth/Fields.java index 187e58ab..824e8a00 100644 --- a/app/src/main/java/svenmeier/coxswain/bluetooth/Fields.java +++ b/app/src/main/java/svenmeier/coxswain/bluetooth/Fields.java @@ -4,10 +4,6 @@ import android.bluetooth.BluetoothGattCharacteristic; import android.os.Build; -import java.util.Calendar; - -import svenmeier.coxswain.bluetooth.BlueUtils; - @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR2) public class Fields { diff --git a/app/src/main/java/svenmeier/coxswain/rower/wired/Protocol3.java b/app/src/main/java/svenmeier/coxswain/rower/wired/Protocol3.java index 6f77d72c..7ac63e09 100644 --- a/app/src/main/java/svenmeier/coxswain/rower/wired/Protocol3.java +++ b/app/src/main/java/svenmeier/coxswain/rower/wired/Protocol3.java @@ -18,6 +18,7 @@ import svenmeier.coxswain.gym.Measurement; import svenmeier.coxswain.rower.wired.usb.Consumer; import svenmeier.coxswain.rower.wired.usb.ITransfer; +import svenmeier.coxswain.util.ByteUtils; public class Protocol3 implements IProtocol { @@ -74,22 +75,22 @@ public void transfer(Measurement measurement) { Consumer consumer = transfer.consumer(); while (consumer.hasNext()) { switch (consumer.next()) { - case (byte)0xFB: + case (byte) 0xFB: if (!consumer.hasNext()) { break; } measurement.setPulse(consumer.next() & 0xFF); - trace(consumer.consumed()); + trace.onInput(ByteUtils.toHex(consumer.consumed())); continue; - case (byte)0xFC: + case (byte) 0xFC: measurement.setStrokes(measurement.getStrokes() + 1); ratioCalculator.recovering(measurement, System.currentTimeMillis()); - trace(consumer.consumed()); + trace.onInput(ByteUtils.toHex(consumer.consumed())); continue; - case (byte)0xFD: + case (byte) 0xFD: // 2 bytes voltage not used if (!consumer.hasNext()) { break; @@ -102,9 +103,9 @@ public void transfer(Measurement measurement) { ratioCalculator.pulling(measurement, System.currentTimeMillis()); - trace(consumer.consumed()); + trace.onInput(ByteUtils.toHex(consumer.consumed())); continue; - case (byte)0xFE: + case (byte) 0xFE: if (!consumer.hasNext()) { break; } @@ -112,9 +113,9 @@ public void transfer(Measurement measurement) { distanceInDecimeters += consumer.next() & 0xFF; measurement.setDistance(distanceInDecimeters / 10); - trace(consumer.consumed()); + trace.onInput(ByteUtils.toHex(consumer.consumed())); continue; - case (byte)0xFF: + case (byte) 0xFF: if (!consumer.hasNext()) { break; } @@ -127,33 +128,14 @@ public void transfer(Measurement measurement) { measurement.setStrokeRate(strokeRate & 0xFF); measurement.setSpeed((speed & 0xFF) * 10); - trace(consumer.consumed()); + trace.onInput(ByteUtils.toHex(consumer.consumed())); continue; default: - trace(consumer.consumed()); + trace.onInput(ByteUtils.toHex(consumer.consumed())); trace.comment("unrecognized"); } } - measurement.setDuration((int)(System.currentTimeMillis() - start) / 1000); + measurement.setDuration((int) (System.currentTimeMillis() - start) / 1000); } - - private void trace(byte[] buffer) { - StringBuilder string = new StringBuilder(buffer.length * 3); - - for (int c = 0; c < buffer.length; c++) { - if (c > 0) { - string.append(' '); - } - - int b = buffer[c] & 0xFF; - - string.append(hex[b >>> 4]); - string.append(hex[b & 0x0F]); - } - - trace.onInput(string); - } - - private static final char[] hex = "0123456789ABCDEF".toCharArray(); } diff --git a/app/src/main/java/svenmeier/coxswain/rower/wireless/BluetoothRower.java b/app/src/main/java/svenmeier/coxswain/rower/wireless/BluetoothRower.java index a1cfda27..990b7071 100644 --- a/app/src/main/java/svenmeier/coxswain/rower/wireless/BluetoothRower.java +++ b/app/src/main/java/svenmeier/coxswain/rower/wireless/BluetoothRower.java @@ -5,9 +5,7 @@ import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothGatt; -import android.bluetooth.BluetoothGattCallback; import android.bluetooth.BluetoothGattCharacteristic; -import android.bluetooth.BluetoothGattService; import android.bluetooth.BluetoothManager; import android.bluetooth.BluetoothProfile; import android.content.BroadcastReceiver; @@ -22,15 +20,17 @@ import android.util.Log; import java.util.ArrayDeque; +import java.util.Arrays; import propoid.util.content.Preference; import svenmeier.coxswain.BuildConfig; import svenmeier.coxswain.Coxswain; import svenmeier.coxswain.R; -import svenmeier.coxswain.bluetooth.BlueUtils; +import svenmeier.coxswain.bluetooth.BlueWriter; import svenmeier.coxswain.bluetooth.BluetoothActivity; import svenmeier.coxswain.bluetooth.Fields; import svenmeier.coxswain.rower.Rower; +import svenmeier.coxswain.util.ByteUtils; import svenmeier.coxswain.util.PermissionBlock; public class BluetoothRower extends Rower { @@ -42,6 +42,9 @@ public class BluetoothRower extends Rower { */ private static final int NOTIFICATIONS_TIMEOUT = 2000; + private static byte OP_CODE_REQUEST_CONTROL = 0x00; + private static byte OP_CODE_RESET = 0x01; + private final Context context; private final Handler handler = new Handler(); @@ -50,6 +53,8 @@ public class BluetoothRower extends Rower { private ArrayDeque connections = new ArrayDeque<>(); + private boolean resetting = false; + public BluetoothRower(Context context, Callback callback) { super(context, callback); @@ -67,7 +72,9 @@ public void open() { @Override public void reset() { - Connection last = connections.peekLast(); + resetting = true; + + Connection last = connections.peek(); if (last instanceof GattConnection) { ((GattConnection) last).reset(); } @@ -275,7 +282,7 @@ public void open() { } String name = context.getString(R.string.bluetooth_rower); - IntentFilter filter = BluetoothActivity.start(context, name, BlueUtils.SERVICE_FITNESS_MACHINE.toString()); + IntentFilter filter = BluetoothActivity.start(context, name, BlueWriter.SERVICE_FITNESS_MACHINE.toString()); context.registerReceiver(this, filter); registered = true; } @@ -316,7 +323,7 @@ public void close() { } @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR2) - private class GattConnection extends BluetoothGattCallback implements Connection, Runnable { + private class GattConnection extends BlueWriter implements Connection, Runnable { private final String address; @@ -326,8 +333,8 @@ private class GattConnection extends BluetoothGattCallback implements Connection private BluetoothGatt connected; + private BluetoothGattCharacteristic softwareRevision; private BluetoothGattCharacteristic rowerData; - private BluetoothGattCharacteristic controlPoint; GattConnection(String address) { @@ -346,16 +353,17 @@ private void select() { @Override public synchronized void open() { + BluetoothManager manager = (BluetoothManager) context.getSystemService(Context.BLUETOOTH_SERVICE); adapter = manager.getAdapter(); try { BluetoothDevice device = adapter.getRemoteDevice(address); + Log.d(Coxswain.TAG, "bluetooth rower connecting " + address); connected = device.connectGatt(context, false, this); - handler.removeCallbacks(this); handler.postDelayed(this, CONNECT_TIMEOUT_MILLIS); } catch (IllegalArgumentException invalid) { select(); @@ -396,136 +404,180 @@ public synchronized void onConnectionStateChange(BluetoothGatt gatt, int status, public synchronized void close() { if (connected != null) { + connected.disconnect(); connected.close(); connected = null; - } - rowerData = null; - controlPoint = null; + rowerData = null; + } adapter = null; } public void reset() { - if (controlPoint != null) { - if (BlueUtils.write(connected, controlPoint, 0x01) == false) { - Log.d(Coxswain.TAG, "bluetooth no rower data reset"); - } + if (connected != null && controlPoint != null) { + write(connected, controlPoint, OP_CODE_RESET); } } @Override - public synchronized void onServicesDiscovered(BluetoothGatt gatt, int status) { + public synchronized void onServicesDiscovered(final BluetoothGatt gatt, int status) { if (connected == null) { return; } - BluetoothGattService service = gatt.getService(BlueUtils.SERVICE_FITNESS_MACHINE); - if (service == null) { - Log.d(Coxswain.TAG, "bluetooth no fitness machine"); + rowerData = get(gatt, SERVICE_FITNESS_MACHINE, CHARACTERISTIC_ROWER_DATA); + if (rowerData == null) { + Log.d(Coxswain.TAG, "bluetooth no rower data"); } else { - // the comm module hangs up as soon as something is written to the control point :/ - //controlPoint = service.getCharacteristic(BlueUtils.CHARACTERISTIC_CONTROL_POINT); - if (controlPoint == null) { - Log.d(Coxswain.TAG, "bluetooth no control point"); - } else { - if (BlueUtils.write(connected, controlPoint, 0x00) == false) { - Log.d(Coxswain.TAG, "bluetooth no rower data request control"); + enableNotification(gatt, rowerData); + } + + softwareRevision = get(gatt, SERVICE_DEVICE_INFORMATION, CHARACTERISTIC_SOFTWARE_REVISION); + if (softwareRevision == null) { + Log.d(Coxswain.TAG, "bluetooth no software revision"); + } else { + read(connected, softwareRevision); + } + + if (rowerData == null) { + toast(context.getString(R.string.bluetooth_rower_not_found, gatt.getDevice().getAddress())); + select(); + } else { + handler.post(new Runnable() { + @Override + public void run() { + callback.onConnected(); } - } + }); + } + } - rowerData = service.getCharacteristic(BlueUtils.CHARACTERISTIC_ROWER_DATA); - if (rowerData == null) { - Log.d(Coxswain.TAG, "bluetooth no rower data"); + @Override + public void onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) { + if (softwareRevision != null && characteristic.getUuid().equals(softwareRevision.getUuid())) { + String version = softwareRevision.getStringValue(0); + Log.d(Coxswain.TAG, String.format("bluetooth rower software revision %s", version)); + + String minVersion = "4.2"; + if (version.compareTo(minVersion) < 0) { + // old firmware rejects re-bonding of a previously bonded device, + // so do not write to the control point, as this triggeres a bond + toast(context.getString(R.string.bluetooth_rower_software_revision, version, minVersion)); } else { - if (BlueUtils.enableNotification(gatt, rowerData)) { - handler.post(new Runnable() { - @Override - public void run() { - callback.onConnected(); - } - }); - return; + controlPoint = get(gatt, SERVICE_FITNESS_MACHINE, CHARACTERISTIC_CONTROL_POINT); + if (controlPoint == null) { + Log.d(Coxswain.TAG, "bluetooth no control point"); + } else { + enableIndication(connected, controlPoint); + write(connected, controlPoint, OP_CODE_REQUEST_CONTROL); + + if (resetting == true) { + reset(); + } } - Log.d(Coxswain.TAG, "bluetooth no rower data notification"); } } - rowerData = null; - toast(context.getString(R.string.bluetooth_rower_not_found, gatt.getDevice().getAddress())); - - select(); + super.onCharacteristicRead(gatt, characteristic, status); } @Override public synchronized void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) { - if (connected == null) { + if (rowerData == null) { return; } - Fields fields = new Fields(characteristic, Fields.UINT16); + if (controlPoint != null && characteristic.getUuid().equals(controlPoint.getUuid())) { + Log.d(Coxswain.TAG, "bluetooth rower indication control-point"); - if (fields.flag(0) == false) { // more data - setStrokeRate(fields.get(Fields.UINT8) / 2); // stroke rate 0.5 - setStrokes(fields.get(Fields.UINT16)); // stroke count - } - if (fields.flag(1)) { - fields.get(Fields.UINT8); // average stroke rate - } - if (fields.flag(2)) { - setDistance(fields.get(Fields.UINT16) + - (fields.get(Fields.UINT8) << 16)); // total distance - } - if (fields.flag(3)) { - setSpeed(500 *100 / fields.get(Fields.UINT16)); // instantaneous pace - } - if (fields.flag(4)) { - fields.get(Fields.UINT16); // average pace - } - if (fields.flag(5)) { - setPower(fields.get(Fields.SINT16)); // instantaneous power - } - if (fields.flag(6)) { - fields.get(Fields.SINT16); // average power - } - if (fields.flag(7)) { - fields.get(Fields.SINT16); // resistance level - } - if (fields.flag(8)) { // expended energy - setEnergy(fields.get(Fields.UINT16)); // total energy - fields.get(Fields.UINT16); // energy per hour - fields.get(Fields.UINT8); // energy per minute - } - if (fields.flag(9)) { - int heartRate = fields.get(Fields.UINT8); // heart rate - if (heartRate > 0) { - setPulse(heartRate); + if (BuildConfig.DEBUG) { + toast("control-point changed " + ByteUtils.toHex(characteristic.getValue())); } - } - if (fields.flag(10)) { - fields.get(Fields.UINT8); // metabolic equivalent 0.1 - } - if (fields.flag(11)) { - int elapsedTime = fields.get(Fields.UINT16); // elapsed time + } else if (rowerData != null && characteristic.getUuid().equals(rowerData.getUuid())) { + keepAlive.onNotification(); + int duration = getDuration(); - int delta = Math.abs(elapsedTime - duration); - // erroneous values are sent on minute boundaries, so ignore these deltas - if (delta >= 58 && delta <= 60) { - // 359 ... 300 ... 360 - // 599 ... 659 ... 600 - // 478 ... 420 ... 479 - Log.d(Coxswain.TAG, String.format("bluetooth rower erroneous elapsed time %s, duration is %s", elapsedTime, duration)); + int distance = getDistance(); + int strokes = getStrokes(); + int energy = getEnergy(); + + Fields fields = new Fields(characteristic, Fields.UINT16); + try { + if (fields.flag(0) == false) { // more data + setStrokeRate(fields.get(Fields.UINT8) / 2); // stroke rate 0.5 + strokes = fields.get(Fields.UINT16); // stroke count + } + if (fields.flag(1)) { + fields.get(Fields.UINT8); // average stroke rate + } + if (fields.flag(2)) { + distance = (fields.get(Fields.UINT16) + + (fields.get(Fields.UINT8) << 16)); // total distance + } + if (fields.flag(3)) { + setSpeed(500 * 100 / fields.get(Fields.UINT16)); // instantaneous pace + } + if (fields.flag(4)) { + fields.get(Fields.UINT16); // average pace + } + if (fields.flag(5)) { + setPower(fields.get(Fields.SINT16)); // instantaneous power + } + if (fields.flag(6)) { + fields.get(Fields.SINT16); // average power + } + if (fields.flag(7)) { + fields.get(Fields.SINT16); // resistance level + } + if (fields.flag(8)) { // expended energy + energy = fields.get(Fields.UINT16); // total energy + fields.get(Fields.UINT16); // energy per hour + fields.get(Fields.UINT8); // energy per minute + } + if (fields.flag(9)) { + int heartRate = fields.get(Fields.UINT8); // heart rate + if (heartRate > 0) { + setPulse(heartRate); + } + } + if (fields.flag(10)) { + fields.get(Fields.UINT8); // metabolic equivalent 0.1 + } + if (fields.flag(11)) { + int elapsedTime = fields.get(Fields.UINT16); // elapsed time + // erroneous values are sent on minute and hour boundaries, + // so ignore these deltas + int delta = Math.abs(elapsedTime - duration); + if (delta >= 58 && delta <= 62) { + Log.d(Coxswain.TAG, String.format("bluetooth rower erroneous elapsed time %s, duration is %s", elapsedTime, duration)); + if (BuildConfig.DEBUG) { + toast(String.format("!! Time error %s !!", delta)); + } + } else { + duration = elapsedTime; + } + } + if (fields.flag(12)) { + fields.get(Fields.UINT16); // remaining time + } + } catch (NullPointerException ex) { + // rarely flags and fields do not match up + Log.d(Coxswain.TAG, "bluetooth rower field mismatch"); + } + + if (resetting) { + if (distance + duration + energy + strokes == 0) { + resetting = false; + } } else { - setDuration(elapsedTime); + setDistance(distance); + setDuration(duration); + setStrokes(strokes); + setEnergy(energy); } + notifyMeasurement(); } - if (fields.flag(12)) { - fields.get(Fields.UINT16); // remaining time - } - - keepAlive.onNotification(); - - notifyMeasurement(); } /** @@ -534,7 +586,7 @@ public synchronized void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGa @Override public void run() { if (connected != null && rowerData == null) { - toast(context.getString(R.string.bluetooth_heart_failed, connected.getDevice().getAddress())); + toast(context.getString(R.string.bluetooth_rower_failed, connected.getDevice().getAddress())); select(); } @@ -563,14 +615,15 @@ public void onNotification() { @Override public void run() { if (rowerData != null) { + Log.d(Coxswain.TAG, "bluetooth rower notifications time-out"); if (BuildConfig.DEBUG) { - toast(context.getString(R.string.bluetooth_rower_notification_timeout)); + toast("!! Notification timeout !!"); } // re-enable notification - BlueUtils.enableNotification(connected, rowerData); + enableNotification(connected, rowerData); } } } diff --git a/app/src/main/java/svenmeier/coxswain/util/ByteUtils.java b/app/src/main/java/svenmeier/coxswain/util/ByteUtils.java new file mode 100644 index 00000000..021de892 --- /dev/null +++ b/app/src/main/java/svenmeier/coxswain/util/ByteUtils.java @@ -0,0 +1,22 @@ +package svenmeier.coxswain.util; + +public class ByteUtils { + public static String toHex(byte[] buffer) { + StringBuilder string = new StringBuilder(buffer.length * 3); + + for (int c = 0; c < buffer.length; c++) { + if (c > 0) { + string.append(' '); + } + + int b = buffer[c] & 0xFF; + + string.append(hex[b >>> 4]); + string.append(hex[b & 0x0F]); + } + + return string.toString(); + } + + private static final char[] hex = "0123456789ABCDEF".toCharArray(); +} diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index b75b7185..14b9ca5f 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -155,7 +155,7 @@ Bluetooth erfordert aktivierte Standortdienste Bluetooth Verbindung mit %s gescheitert Bluetooth kein Ruderer auf %s gefunden - Bluetooth Benachrichtigungen Time-out + Bluetooth S4 Firmware ist %s, bitte auf %s aktualisieren Bluetooth Verbindung zu %s verloren Training diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index afff4ec6..f4efa317 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -158,7 +158,7 @@ Bluetooth requires enabled location services Bluetooth connection to %s failed Bluetooth no rower found on %s - Bluetooth notifiations time-out + Bluetooth S4 Firmware is %s, please update to %s Bluetooth connection lost to %s Training diff --git a/changelog b/changelog index b90b4d7f..c2e4052b 100644 --- a/changelog +++ b/changelog @@ -1,3 +1,7 @@ +6.8 + - reset rower via bluetooth + - fixed values of snapshot axis + 6.7 - another fix for premature ending of workout :/ diff --git a/doc/D52QGF-ap_4.02.00.zip b/doc/D52QGF-ap_4.02.00.zip new file mode 100644 index 00000000..d50cfde8 Binary files /dev/null and b/doc/D52QGF-ap_4.02.00.zip differ diff --git a/doc/FTMS_v1.0.pdf b/doc/FTMS_v1.0.pdf new file mode 100644 index 00000000..2f352f91 Binary files /dev/null and b/doc/FTMS_v1.0.pdf differ diff --git a/gradle.properties b/gradle.properties index 8ef7f52e..5d5936a1 100644 --- a/gradle.properties +++ b/gradle.properties @@ -18,7 +18,7 @@ # org.gradle.parallel=true # ./gradlew githubRelease -coxswain-versionCode=67 -coxswain-versionName=6.7 +coxswain-versionCode=68 +coxswain-versionName=6.8 coxswain-private=/home/sven/Documents/coxswain/coxswain-private.gradle