diff --git a/.travis.yml b/.travis.yml index 11021c41..e78886fe 100644 --- a/.travis.yml +++ b/.travis.yml @@ -21,7 +21,7 @@ before_script: - sed -i -r -e 's/applicationId "[^"]+/\0.dev/' -e 's/(versionCode ).*/\1'$(date +%s)'/' - -e 's/versionName "[^"]+/\0-dev_'${TRAVIS_COMMIT:0:8}'/' + -e 's/versionName "[^"]+/\0-dev_'${TRAVIS_COMMIT:0:8}_$(date +%F)'/' android_app/app/build.gradle - sed -i -r -e 's/(]*>[^<]+)/\1 (dev)/' diff --git a/README.md b/README.md index 73105d00..63cb3db6 100644 --- a/README.md +++ b/README.md @@ -21,8 +21,8 @@ Install [openScale-dev-build.apk](https://github.com/oliexdev/openScale/releases - Import or export your data from/into a CSV file - Estimates body metrics (body fat, body water and lean body mass) based on scientic publications - Support for multiple users -- Partially or full support for custom made Bluetooth scale, Xiaomi Mi scale v1/v2, Sanitas SBF70, Medisana BS444/BS440, Digoo DG-S038H, Yunmai Mini, Excelvan CF369BLE, Yunmai SE, MGB, Exingtech Y1, Beurer BF700/BF710/BF800, Silvercrest SBF75, Runtastic Libra (see [openScale wiki](https://github.com/oliexdev/openScale/wiki/Supported-scales-in-openScale) for details) -- Partially or full translated into Brazilian Portuguese, Catalan, Chinese (traditional), Czech, Dutch, English, French, Galician, German, Greek, Italian, Japanese, Norwegian Bokmål, Polish, Romanian, Russian, Slovak, Spanish, Swedish, Turkish +- Partially or full support for custom made Bluetooth scale, Xiaomi Mi scale v1/v2, Sanitas SBF70, Medisana BS444/BS440, Digoo DG-S038H, Yunmai Mini, Excelvan CF369BLE/CF366BLE, Yunmai SE, MGB, Exingtech Y1, Beurer BF700/BF710/BF800, Silvercrest SBF75, Runtastic Libra, Hesley (Yunchen), iHealth HS3, Easy Home 64050, Accuway (see [openScale wiki](https://github.com/oliexdev/openScale/wiki/Supported-scales-in-openScale) for details) +- Partially or full translated into Brazilian Portuguese, Catalan, Chinese (traditional), Croatian, Czech, Danish, Dutch, English, French, Galician, German, Greek, Italian, Japanese, Norwegian Bokmål, Polish, Romanian, Russian, Slovak, Slovenian, Spanish, Swedish, Turkish - No advertising and for free - All data belongs to you (no cloud service) diff --git a/android_app/app/build.gradle b/android_app/app/build.gradle index 07f03e8a..bda2825f 100644 --- a/android_app/app/build.gradle +++ b/android_app/app/build.gradle @@ -42,6 +42,7 @@ dependencies { implementation "com.android.support:design:${supportLibVersion}" implementation "com.android.support:support-v4:${supportLibVersion}" implementation "com.android.support:appcompat-v7:${supportLibVersion}" + implementation "com.android.support:recyclerview-v7:${supportLibVersion}" // HelloCharts implementation 'com.github.lecho:hellocharts-library:1.5.8@aar' diff --git a/android_app/app/src/main/java/com/health/openscale/core/Application.java b/android_app/app/src/main/java/com/health/openscale/core/Application.java index 87e6bb98..cb006941 100644 --- a/android_app/app/src/main/java/com/health/openscale/core/Application.java +++ b/android_app/app/src/main/java/com/health/openscale/core/Application.java @@ -21,6 +21,7 @@ import com.health.openscale.BuildConfig; import timber.log.Timber; public class Application extends android.app.Application { + OpenScale openScale; private class TimberLogAdapter extends Timber.DebugTree { @Override @@ -40,5 +41,8 @@ public class Application extends android.app.Application { // Create OpenScale instance OpenScale.createInstance(getApplicationContext()); + + // Hold on to the instance for as long as the application exists + openScale = OpenScale.getInstance(); } } diff --git a/android_app/app/src/main/java/com/health/openscale/core/alarm/AlarmBackupHandler.java b/android_app/app/src/main/java/com/health/openscale/core/alarm/AlarmBackupHandler.java index 10734d06..810a2176 100644 --- a/android_app/app/src/main/java/com/health/openscale/core/alarm/AlarmBackupHandler.java +++ b/android_app/app/src/main/java/com/health/openscale/core/alarm/AlarmBackupHandler.java @@ -28,8 +28,9 @@ import com.health.openscale.core.OpenScale; import java.io.File; import java.io.IOException; -import java.text.DateFormat; +import java.text.SimpleDateFormat; import java.util.Date; +import java.util.Locale; import timber.log.Timber; @@ -95,7 +96,8 @@ public class AlarmBackupHandler SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); if (!prefs.getBoolean("overwriteBackup", false)) { - databaseName = DateFormat.getDateInstance(DateFormat.SHORT).format(new Date()) + "_" + databaseName; + SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd", Locale.US); + databaseName = dateFormat.format(new Date()) + "_" + databaseName; } File exportFile = new File(exportDir, databaseName); diff --git a/android_app/app/src/main/java/com/health/openscale/core/bluetooth/BluetoothCommunication.java b/android_app/app/src/main/java/com/health/openscale/core/bluetooth/BluetoothCommunication.java index a30e0bf3..f992f6c7 100644 --- a/android_app/app/src/main/java/com/health/openscale/core/bluetooth/BluetoothCommunication.java +++ b/android_app/app/src/main/java/com/health/openscale/core/bluetooth/BluetoothCommunication.java @@ -30,6 +30,7 @@ import android.content.Context; import android.content.pm.PackageManager; import android.os.Build; import android.os.Handler; +import android.os.SystemClock; import android.support.v4.content.ContextCompat; import com.health.openscale.core.datatypes.ScaleMeasurement; @@ -49,9 +50,12 @@ public abstract class BluetoothCommunication { public enum BT_MACHINE_STATE {BT_INIT_STATE, BT_CMD_STATE, BT_CLEANUP_STATE} + private static final long LE_SCAN_TIMEOUT_MS = 10 * 1000; + protected Context context; private Handler callbackBtHandler; + private Handler handler; private BluetoothGatt bluetoothGatt; private boolean connectionEstablished; private BluetoothGattCallback gattCallback; @@ -81,6 +85,7 @@ public abstract class BluetoothCommunication { public BluetoothCommunication(Context context) { this.context = context; + handler = new Handler(); btAdapter = BluetoothAdapter.getDefaultAdapter(); gattCallback = new GattCallback(); bluetoothGatt = null; @@ -96,8 +101,8 @@ public abstract class BluetoothCommunication { return bluetoothGatt.getServices(); } - protected boolean discoverDeviceBeforeConnecting() { - return false; + protected boolean hasBluetoothGattService(UUID service) { + return bluetoothGatt != null && bluetoothGatt.getService(service) != null; } /** @@ -125,7 +130,10 @@ public abstract class BluetoothCommunication { * @param infoText the information text that is displayed to the status code. */ protected void setBtStatus(BT_STATUS_CODE statusCode, String infoText) { - callbackBtHandler.obtainMessage(statusCode.ordinal(), infoText).sendToTarget(); + if (callbackBtHandler != null) { + callbackBtHandler.obtainMessage( + statusCode.ordinal(), infoText).sendToTarget(); + } } /** @@ -134,7 +142,10 @@ public abstract class BluetoothCommunication { * @param scaleMeasurement the scale data that should be added to openScale */ protected void addScaleData(ScaleMeasurement scaleMeasurement) { - callbackBtHandler.obtainMessage(BT_STATUS_CODE.BT_RETRIEVE_SCALE_DATA.ordinal(), scaleMeasurement).sendToTarget(); + if (callbackBtHandler != null) { + callbackBtHandler.obtainMessage( + BT_STATUS_CODE.BT_RETRIEVE_SCALE_DATA.ordinal(), scaleMeasurement).sendToTarget(); + } } /** @@ -144,7 +155,10 @@ public abstract class BluetoothCommunication { * @param value the value to be used */ protected void sendMessage(int msg, Object value) { - callbackBtHandler.obtainMessage(BT_STATUS_CODE.BT_SCALE_MESSAGE.ordinal(), msg, 0, value).sendToTarget(); + if (callbackBtHandler != null) { + callbackBtHandler.obtainMessage( + BT_STATUS_CODE.BT_SCALE_MESSAGE.ordinal(), msg, 0, value).sendToTarget(); + } } /** @@ -261,6 +275,7 @@ public abstract class BluetoothCommunication { BluetoothGattCharacteristic gattCharacteristic = bluetoothGatt.getService(service) .getCharacteristic(characteristic); + Timber.d("Read characteristic %s", characteristic); bluetoothGatt.readCharacteristic(gattCharacteristic); } @@ -268,6 +283,7 @@ public abstract class BluetoothCommunication { BluetoothGattDescriptor gattDescriptor = bluetoothGatt.getService(service) .getCharacteristic(characteristic).getDescriptor(descriptor); + Timber.d("Read descriptor %s", descriptor); bluetoothGatt.readDescriptor(gattDescriptor); } @@ -280,16 +296,21 @@ public abstract class BluetoothCommunication { protected void setIndicationOn(UUID service, UUID characteristic, UUID descriptor) { Timber.d("Set indication on for %s", characteristic); - BluetoothGattCharacteristic gattCharacteristic = - bluetoothGatt.getService(service).getCharacteristic(characteristic); - bluetoothGatt.setCharacteristicNotification(gattCharacteristic, true); + try { + BluetoothGattCharacteristic gattCharacteristic = + bluetoothGatt.getService(service).getCharacteristic(characteristic); + bluetoothGatt.setCharacteristicNotification(gattCharacteristic, true); - synchronized (lock) { - descriptorRequestQueue.add( - new GattObjectValue<>( - gattCharacteristic.getDescriptor(descriptor), - BluetoothGattDescriptor.ENABLE_INDICATION_VALUE)); - handleRequests(); + synchronized (lock) { + descriptorRequestQueue.add( + new GattObjectValue<>( + gattCharacteristic.getDescriptor(descriptor), + BluetoothGattDescriptor.ENABLE_INDICATION_VALUE)); + handleRequests(); + } + } + catch (Exception e) { + Timber.e(e); } } @@ -302,16 +323,21 @@ public abstract class BluetoothCommunication { protected void setNotificationOn(UUID service, UUID characteristic, UUID descriptor) { Timber.d("Set notification on for %s", characteristic); - BluetoothGattCharacteristic gattCharacteristic = - bluetoothGatt.getService(service).getCharacteristic(characteristic); - bluetoothGatt.setCharacteristicNotification(gattCharacteristic, true); + try { + BluetoothGattCharacteristic gattCharacteristic = + bluetoothGatt.getService(service).getCharacteristic(characteristic); + bluetoothGatt.setCharacteristicNotification(gattCharacteristic, true); - synchronized (lock) { - descriptorRequestQueue.add( - new GattObjectValue<>( - gattCharacteristic.getDescriptor(descriptor), - BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE)); - handleRequests(); + synchronized (lock) { + descriptorRequestQueue.add( + new GattObjectValue<>( + gattCharacteristic.getDescriptor(descriptor), + BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE)); + handleRequests(); + } + } + catch (Exception e) { + Timber.e(e); } } @@ -349,12 +375,16 @@ public abstract class BluetoothCommunication { return ""; } + if (data.length == 0) { + return ""; + } + final StringBuilder stringBuilder = new StringBuilder(3 * data.length); for (byte byteChar : data) { stringBuilder.append(String.format("%02X ", byteChar)); } - return stringBuilder.toString(); + return stringBuilder.substring(0, stringBuilder.length() - 1); } protected byte xorChecksum(byte[] data, int offset, int length) { @@ -384,9 +414,33 @@ public abstract class BluetoothCommunication { * * @param hwAddress the Bluetooth address to connect to */ - public void connect(final String hwAddress) { - Timber.i("Connecting to [%s] (driver: %s)", hwAddress, driverName()); + public void connect(String hwAddress) { + logBluetoothStatus(); + // Some good tips to improve BLE connections: + // https://android.jlelse.eu/lessons-for-first-time-android-bluetooth-le-developers-i-learned-the-hard-way-fee07646624 + + btAdapter.cancelDiscovery(); + stopLeScan(); + + // Don't do any cleanup if disconnected before fully connected + btMachineState = BT_MACHINE_STATE.BT_CLEANUP_STATE; + + // Running an LE scan during connect improves connectivity on some phones + // (e.g. Sony Xperia Z5 compact, Android 7.1.1). For some scales (e.g. Medisana BS444) + // it seems to be a requirement that the scale is discovered before connecting to it. + // Otherwise the connection almost never succeeds. + if (ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_COARSE_LOCATION) + == PackageManager.PERMISSION_GRANTED) { + startLeScanForDevice(hwAddress); + } + else { + Timber.d("No coarse location permission, connecting without LE scan"); + connectGatt(hwAddress); + } + } + + private void logBluetoothStatus() { Timber.d("BT is%s enabled, state=%d, scan mode=%d, is%s discovering", btAdapter.isEnabled() ? "" : " not", btAdapter.getState(), btAdapter.getScanMode(), btAdapter.isDiscovering() ? "" : " not"); @@ -401,47 +455,11 @@ public abstract class BluetoothCommunication { Timber.d("Connected GATT_SERVER device: %s [%s]", device.getName(), device.getAddress()); } - - // Some good tips to improve BLE connections: - // https://android.jlelse.eu/lessons-for-first-time-android-bluetooth-le-developers-i-learned-the-hard-way-fee07646624 - - final boolean doDiscoveryFirst = discoverDeviceBeforeConnecting(); - - // Running an LE scan during connect improves connectivity on some phones - // (e.g. Sony Xperia Z5 compact, Android 7.1.1). - btAdapter.cancelDiscovery(); - if (leScanCallback == null) { - if (ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_COARSE_LOCATION) - == PackageManager.PERMISSION_GRANTED) { - Timber.d("Starting LE scan"); - leScanCallback = new BluetoothAdapter.LeScanCallback() { - @Override - public void onLeScan(BluetoothDevice device, int rssi, byte[] scanRecord) { - Timber.d("Found LE device %s [%s]", device.getName(), device.getAddress()); - if (!doDiscoveryFirst || !device.getAddress().equals(hwAddress)) { - return; - } - synchronized (lock) { - connectGatt(device); - } - } - }; - btAdapter.startLeScan(leScanCallback); - } - else { - Timber.d("No coarse location permission, skipping LE scan"); - } - } - - // Don't do any cleanup if disconnected before fully connected - btMachineState = BT_MACHINE_STATE.BT_CLEANUP_STATE; - - if (!doDiscoveryFirst || leScanCallback == null) { - connectGatt(btAdapter.getRemoteDevice(hwAddress)); - } } private void connectGatt(BluetoothDevice device) { + Timber.i("Connecting to [%s] (driver: %s)", device.getAddress(), driverName()); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { bluetoothGatt = device.connectGatt( context, false, gattCallback, BluetoothDevice.TRANSPORT_LE); @@ -451,15 +469,55 @@ public abstract class BluetoothCommunication { } } + private void connectGatt(String hwAddress) { + connectGatt(btAdapter.getRemoteDevice(hwAddress)); + } + + private void startLeScanForDevice(final String hwAddress) { + leScanCallback = new BluetoothAdapter.LeScanCallback() { + @Override + public void onLeScan(BluetoothDevice device, int rssi, byte[] scanRecord) { + Timber.d("Found LE device %s [%s]", device.getName(), device.getAddress()); + if (!device.getAddress().equals(hwAddress)) { + return; + } + synchronized (lock) { + stopLeScan(); + connectGatt(device); + } + } + }; + + Timber.d("Starting LE scan for device [%s]", hwAddress); + btAdapter.startLeScan(leScanCallback); + + handler.postAtTime(new Runnable() { + @Override + public void run() { + Timber.d("Device not found in LE scan, connecting directly"); + synchronized (lock) { + stopLeScan(); + connectGatt(hwAddress); + } + } + }, leScanCallback, SystemClock.uptimeMillis() + LE_SCAN_TIMEOUT_MS); + } + + private void stopLeScan() { + if (leScanCallback != null) { + Timber.d("Stopping LE scan"); + btAdapter.stopLeScan(leScanCallback); + handler.removeCallbacksAndMessages(leScanCallback); + leScanCallback = null; + } + } + /** * Disconnect from a Bluetooth device */ public void disconnect(boolean doCleanup) { synchronized (lock) { - if (leScanCallback != null) { - btAdapter.stopLeScan(leScanCallback); - leScanCallback = null; - } + stopLeScan(); if (bluetoothGatt == null) { return; @@ -467,15 +525,32 @@ public abstract class BluetoothCommunication { Timber.i("Disconnecting%s", doCleanup ? " (with cleanup)" : ""); + handler.removeCallbacksAndMessages(null); + callbackBtHandler = null; + if (doCleanup) { if (btMachineState != BT_MACHINE_STATE.BT_CLEANUP_STATE) { setBtMachineState(BT_MACHINE_STATE.BT_CLEANUP_STATE); nextMachineStateStep(); } + handler.post(new Runnable() { + @Override + public void run() { + synchronized (lock) { + if (openRequest) { + handler.postDelayed(this, 10); + } else { + bluetoothGatt.close(); + bluetoothGatt = null; + } + } + } + }); + } + else { + bluetoothGatt.close(); + bluetoothGatt = null; } - - bluetoothGatt.close(); - bluetoothGatt = null; } } @@ -509,6 +584,8 @@ public abstract class BluetoothCommunication { synchronized (lock) { // check for pending request if (openRequest) { + Timber.d("Request pending (queue %d %d)", + descriptorRequestQueue.size(), characteristicRequestQueue.size()); return; // yes, do nothing } @@ -517,8 +594,9 @@ public abstract class BluetoothCommunication { if (descriptor != null) { descriptor.gattObject.setValue(descriptor.value); - Timber.d("Write descriptor %s: %s", - descriptor.gattObject.getUuid(), byteInHex(descriptor.gattObject.getValue())); + Timber.d("Write descriptor %s: %s (queue: %d %d)", + descriptor.gattObject.getUuid(), byteInHex(descriptor.gattObject.getValue()), + descriptorRequestQueue.size(), characteristicRequestQueue.size()); if (!bluetoothGatt.writeDescriptor(descriptor.gattObject)) { Timber.e("Failed to initiate write of descriptor %s", descriptor.gattObject.getUuid()); @@ -532,8 +610,9 @@ public abstract class BluetoothCommunication { if (characteristic != null) { characteristic.gattObject.setValue(characteristic.value); - Timber.d("Write characteristic %s: %s", - characteristic.gattObject.getUuid(), byteInHex(characteristic.gattObject.getValue())); + Timber.d("Write characteristic %s: %s (queue: %d %d)", + characteristic.gattObject.getUuid(), byteInHex(characteristic.gattObject.getValue()), + descriptorRequestQueue.size(), characteristicRequestQueue.size()); if (!bluetoothGatt.writeCharacteristic(characteristic.gattObject)) { Timber.e("Failed to initiate write of characteristic %s", characteristic.gattObject.getUuid()); @@ -557,10 +636,7 @@ public abstract class BluetoothCommunication { if (newState == BluetoothProfile.STATE_CONNECTED) { synchronized (lock) { - if (leScanCallback != null) { - btAdapter.stopLeScan(leScanCallback); - leScanCallback = null; - } + stopLeScan(); } connectionEstablished = true; @@ -588,7 +664,8 @@ public abstract class BluetoothCommunication { @Override public void onServicesDiscovered(final BluetoothGatt gatt, int status) { - Timber.d("onServicesDiscovered: status=%d", status); + Timber.d("onServicesDiscovered: status=%d (%d services)", + status, gatt.getServices().size()); synchronized (lock) { cmdStepNr = 0; @@ -615,24 +692,32 @@ public abstract class BluetoothCommunication { setBtMachineState(BT_MACHINE_STATE.BT_INIT_STATE); } + private void postDelayedHandleRequests() { + // Wait a short while before starting the next operation as suggested + // on the android.jlelse.eu link above. + handler.postDelayed(new Runnable() { + @Override + public void run() { + synchronized (lock) { + openRequest = false; + handleRequests(); + } + } + }, 60); + } + @Override public void onDescriptorWrite(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) { - synchronized (lock) { - openRequest = false; - handleRequests(); - } + postDelayedHandleRequests(); } @Override public void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) { - synchronized (lock) { - openRequest = false; - handleRequests(); - } + postDelayedHandleRequests(); } @Override @@ -644,8 +729,7 @@ public abstract class BluetoothCommunication { synchronized (lock) { onBluetoothDataRead(gatt, characteristic, status); - openRequest = false; - handleRequests(); + postDelayedHandleRequests(); } } @@ -667,10 +751,7 @@ public abstract class BluetoothCommunication { Timber.d("onDescriptorRead %s (status=%d): %s", descriptor.getUuid(), status, byteInHex(descriptor.getValue())); - synchronized (lock) { - openRequest = false; - handleRequests(); - } + postDelayedHandleRequests(); } } } diff --git a/android_app/app/src/main/java/com/health/openscale/core/bluetooth/BluetoothCustomOpenScale.java b/android_app/app/src/main/java/com/health/openscale/core/bluetooth/BluetoothCustomOpenScale.java index 6eb9aa1e..52f825d4 100644 --- a/android_app/app/src/main/java/com/health/openscale/core/bluetooth/BluetoothCustomOpenScale.java +++ b/android_app/app/src/main/java/com/health/openscale/core/bluetooth/BluetoothCustomOpenScale.java @@ -15,28 +15,26 @@ */ package com.health.openscale.core.bluetooth; -import android.bluetooth.BluetoothDevice; -import android.bluetooth.BluetoothSocket; +import android.bluetooth.BluetoothGatt; +import android.bluetooth.BluetoothGattCharacteristic; import android.content.Context; import com.health.openscale.core.datatypes.ScaleMeasurement; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; import java.text.ParseException; import java.text.SimpleDateFormat; +import java.util.Calendar; +import java.util.Locale; import java.util.UUID; import timber.log.Timber; public class BluetoothCustomOpenScale extends BluetoothCommunication { - private final UUID uuid = UUID.fromString("00001101-0000-1000-8000-00805f9b34fb"); // Standard SerialPortService ID + private final UUID WEIGHT_MEASUREMENT_SERVICE = UUID.fromString("0000ffe0-0000-1000-8000-00805f9b34fb"); + private final UUID WEIGHT_MEASUREMENT_CHARACTERISTIC = UUID.fromString("0000ffe1-0000-1000-8000-00805f9b34fb"); // Bluetooth Modul HM-10 + private final UUID WEIGHT_MEASUREMENT_CONFIG = UUID.fromString("00002902-0000-1000-8000-00805f9b34fb"); - private BluetoothSocket btSocket = null; - private BluetoothDevice btDevice = null; - - private BluetoothConnectedThread btConnectThread = null; + private String string_data = new String(); public BluetoothCustomOpenScale(Context context) { super(context); @@ -44,12 +42,33 @@ public class BluetoothCustomOpenScale extends BluetoothCommunication { @Override public String driverName() { - return "Custom Open Scale"; + return "Custom openScale"; } @Override protected boolean nextInitCmd(int stateNr) { - return false; + switch (stateNr) { + case 0: + setNotificationOn(WEIGHT_MEASUREMENT_SERVICE, WEIGHT_MEASUREMENT_CHARACTERISTIC, WEIGHT_MEASUREMENT_CONFIG); + break; + case 1: + Calendar cal = Calendar.getInstance(); + + String date_time = String.format(Locale.US, "2%1d,%1d,%1d,%1d,%1d,%1d,", + cal.get(Calendar.YEAR)-2000, + cal.get(Calendar.MONTH) + 1, + cal.get(Calendar.DAY_OF_MONTH), + cal.get(Calendar.HOUR_OF_DAY), + cal.get(Calendar.MINUTE), + cal.get(Calendar.SECOND)); + + writeBytes(WEIGHT_MEASUREMENT_SERVICE, WEIGHT_MEASUREMENT_CHARACTERISTIC, date_time.getBytes()); + break; + default: + return false; + } + + return true; } @Override @@ -62,211 +81,94 @@ public class BluetoothCustomOpenScale extends BluetoothCommunication { return false; } - @Override - public void connect(String hwAddress) { - - if (btAdapter == null) { - setBtStatus(BT_STATUS_CODE.BT_NO_DEVICE_FOUND); - return; - } - - btDevice = btAdapter.getRemoteDevice(hwAddress); - try { - // Get a BluetoothSocket to connect with the given BluetoothDevice - btSocket = btDevice.createRfcommSocketToServiceRecord(uuid); - } catch (IOException e) { - setBtStatus(BT_STATUS_CODE.BT_UNEXPECTED_ERROR, "Can't get a bluetooth socket"); - btDevice = null; - return; - } - - Thread socketThread = new Thread() { - @Override - public void run() { - try { - if (!btSocket.isConnected()) { - // Connect the device through the socket. This will block - // until it succeeds or throws an exception - btSocket.connect(); - - // Bluetooth connection was successful - setBtStatus(BT_STATUS_CODE.BT_CONNECTION_ESTABLISHED); - - btConnectThread = new BluetoothConnectedThread(); - btConnectThread.start(); - } - } catch (IOException connectException) { - // Unable to connect; close the socket and get out - disconnect(false); - setBtStatus(BT_STATUS_CODE.BT_NO_DEVICE_FOUND); - } - } - }; - - socketThread.start(); - } - - @Override - public void disconnect(boolean doCleanup) { - if (btSocket != null) { - if (btSocket.isConnected()) { - try { - btSocket.close(); - btSocket = null; - } catch (IOException closeException) { - setBtStatus(BT_STATUS_CODE.BT_UNEXPECTED_ERROR, "Can't close bluetooth socket"); - } - } - } - - if (btConnectThread != null) { - btConnectThread.cancel(); - btConnectThread = null; - } - - btDevice = null; - } - public void clearEEPROM() { - sendBtData("9"); + byte[] cmd = {(byte)'9'}; + writeBytes(WEIGHT_MEASUREMENT_SERVICE, WEIGHT_MEASUREMENT_CHARACTERISTIC, cmd); } - private boolean sendBtData(String data) { - if (btSocket.isConnected()) { - btConnectThread = new BluetoothConnectedThread(); - btConnectThread.write(data.getBytes()); + @Override + public void onBluetoothDataChange(BluetoothGatt bluetoothGatt, BluetoothGattCharacteristic gattCharacteristic) { + final byte[] data = gattCharacteristic.getValue(); - btConnectThread.cancel(); - return true; - } + if (data != null) { + for (byte character : data) { + string_data += (char) (character & 0xFF); - return false; - } - - private class BluetoothConnectedThread extends Thread { - private InputStream btInStream; - private OutputStream btOutStream; - private volatile boolean isCancel; - - public BluetoothConnectedThread() { - - isCancel = false; - - // Get the input and output bluetooth streams - try { - btInStream = btSocket.getInputStream(); - btOutStream = btSocket.getOutputStream(); - } catch (IOException e) { - setBtStatus(BT_STATUS_CODE.BT_UNEXPECTED_ERROR, "Can't get bluetooth input or output stream " + e.getMessage()); - } - } - - public void run() { - final StringBuilder btLine = new StringBuilder(); - - // Keep listening to the InputStream until an exception occurs (e.g. device partner goes offline) - while (!isCancel) { - try { - // stream read is a blocking method - char btChar = (char) btInStream.read(); - - btLine.append(btChar); - - if (btLine.charAt(btLine.length() - 1) == '\n') { - ScaleMeasurement scaleMeasurement = parseBtString(btLine.toString()); - - if (scaleMeasurement != null) { - addScaleData(scaleMeasurement); - } - - btLine.setLength(0); - } - - } catch (IOException e) { - cancel(); - setBtStatus(BT_STATUS_CODE.BT_CONNECTION_LOST); + if (character == '\n') { + parseBtString(string_data); + string_data = new String(); } } } - - private ScaleMeasurement parseBtString(String btString) throws IOException { - ScaleMeasurement scaleBtData = new ScaleMeasurement(); - btString = btString.substring(0, btString.length() - 1); // delete newline '\n' of the string - - if (btString.charAt(0) != '$' && btString.charAt(2) != '$') { - setBtStatus(BT_STATUS_CODE.BT_UNEXPECTED_ERROR, "Parse error of bluetooth string. String has not a valid format"); - } - - String btMsg = btString.substring(3, btString.length()); // message string - - switch (btString.charAt(1)) { - case 'I': - Timber.i("MCU Information: %s", btMsg); - break; - case 'E': - Timber.e("MCU Error: %s", btMsg); - break; - case 'S': - Timber.i("MCU stored data size: %s", btMsg); - break; - case 'D': - String[] csvField = btMsg.split(","); - - try { - int checksum = 0; - - checksum ^= Integer.parseInt(csvField[0]); - checksum ^= Integer.parseInt(csvField[1]); - checksum ^= Integer.parseInt(csvField[2]); - checksum ^= Integer.parseInt(csvField[3]); - checksum ^= Integer.parseInt(csvField[4]); - checksum ^= Integer.parseInt(csvField[5]); - checksum ^= (int) Float.parseFloat(csvField[6]); - checksum ^= (int) Float.parseFloat(csvField[7]); - checksum ^= (int) Float.parseFloat(csvField[8]); - checksum ^= (int) Float.parseFloat(csvField[9]); - - int btChecksum = Integer.parseInt(csvField[10]); - - if (checksum == btChecksum) { - scaleBtData.setId(-1); - scaleBtData.setUserId(Integer.parseInt(csvField[0])); - String date_string = csvField[1] + "/" + csvField[2] + "/" + csvField[3] + "/" + csvField[4] + "/" + csvField[5]; - scaleBtData.setDateTime(new SimpleDateFormat("yyyy/MM/dd/HH/mm").parse(date_string)); - - scaleBtData.setWeight(Float.parseFloat(csvField[6])); - scaleBtData.setFat(Float.parseFloat(csvField[7])); - scaleBtData.setWater(Float.parseFloat(csvField[8])); - scaleBtData.setMuscle(Float.parseFloat(csvField[9])); - - return scaleBtData; - } else { - setBtStatus(BT_STATUS_CODE.BT_UNEXPECTED_ERROR, "Error calculated checksum (" + checksum + ") and received checksum (" + btChecksum + ") is different"); - } - } catch (ParseException e) { - setBtStatus(BT_STATUS_CODE.BT_UNEXPECTED_ERROR, "Error while decoding bluetooth date string (" + e.getMessage() + ")"); - } catch (NumberFormatException e) { - setBtStatus(BT_STATUS_CODE.BT_UNEXPECTED_ERROR, "Error while decoding a number of bluetooth string (" + e.getMessage() + ")"); - } - break; - default: - setBtStatus(BT_STATUS_CODE.BT_UNEXPECTED_ERROR, "Error unknown MCU command"); - } - - return null; - } - - public void write(byte[] bytes) { - try { - btOutStream.write(bytes); - } catch (IOException e) { - setBtStatus(BT_STATUS_CODE.BT_UNEXPECTED_ERROR, "Error while writing to bluetooth socket " + e.getMessage()); - } - } - - public void cancel() { - isCancel = true; - } } + + private void parseBtString(String btString) { + btString = btString.substring(0, btString.length() - 1); // delete newline '\n' of the string + + if (btString.charAt(0) != '$' && btString.charAt(2) != '$') { + setBtStatus(BT_STATUS_CODE.BT_UNEXPECTED_ERROR, "Parse error of bluetooth string. String has not a valid format: " + btString); + } + + String btMsg = btString.substring(3, btString.length()); // message string + + switch (btString.charAt(1)) { + case 'I': + Timber.d("MCU Information: %s", btMsg); + break; + case 'E': + Timber.e("MCU Error: %s", btMsg); + break; + case 'S': + Timber.d("MCU stored data size: %s", btMsg); + break; + case 'F': + Timber.d("All data sent"); + clearEEPROM(); + disconnect(false); + break; + case 'D': + String[] csvField = btMsg.split(","); + + try { + int checksum = 0; + + checksum ^= Integer.parseInt(csvField[0]); + checksum ^= Integer.parseInt(csvField[1]); + checksum ^= Integer.parseInt(csvField[2]); + checksum ^= Integer.parseInt(csvField[3]); + checksum ^= Integer.parseInt(csvField[4]); + checksum ^= Integer.parseInt(csvField[5]); + checksum ^= (int) Float.parseFloat(csvField[6]); + checksum ^= (int) Float.parseFloat(csvField[7]); + checksum ^= (int) Float.parseFloat(csvField[8]); + checksum ^= (int) Float.parseFloat(csvField[9]); + + int btChecksum = Integer.parseInt(csvField[10]); + + if (checksum == btChecksum) { + ScaleMeasurement scaleBtData = new ScaleMeasurement(); + + String date_string = csvField[1] + "/" + csvField[2] + "/" + csvField[3] + "/" + csvField[4] + "/" + csvField[5]; + scaleBtData.setDateTime(new SimpleDateFormat("yyyy/MM/dd/HH/mm").parse(date_string)); + + scaleBtData.setWeight(Float.parseFloat(csvField[6])); + scaleBtData.setFat(Float.parseFloat(csvField[7])); + scaleBtData.setWater(Float.parseFloat(csvField[8])); + scaleBtData.setMuscle(Float.parseFloat(csvField[9])); + + addScaleData(scaleBtData); + } else { + setBtStatus(BT_STATUS_CODE.BT_UNEXPECTED_ERROR, "Error calculated checksum (" + checksum + ") and received checksum (" + btChecksum + ") is different"); + } + } catch (ParseException e) { + setBtStatus(BT_STATUS_CODE.BT_UNEXPECTED_ERROR, "Error while decoding bluetooth date string (" + e.getMessage() + ")"); + } catch (NumberFormatException e) { + setBtStatus(BT_STATUS_CODE.BT_UNEXPECTED_ERROR, "Error while decoding a number of bluetooth string (" + e.getMessage() + ")"); + } + break; + default: + setBtStatus(BT_STATUS_CODE.BT_UNEXPECTED_ERROR, "Error unknown MCU command : " + btString); + } + } } diff --git a/android_app/app/src/main/java/com/health/openscale/core/bluetooth/BluetoothDebug.java b/android_app/app/src/main/java/com/health/openscale/core/bluetooth/BluetoothDebug.java index 3ce553e4..6d54bfa2 100644 --- a/android_app/app/src/main/java/com/health/openscale/core/bluetooth/BluetoothDebug.java +++ b/android_app/app/src/main/java/com/health/openscale/core/bluetooth/BluetoothDebug.java @@ -156,8 +156,8 @@ public class BluetoothDebug extends BluetoothCommunication { logService(service, false); } - disconnect(false); setBtStatus(BT_STATUS_CODE.BT_CONNECTION_LOST); + disconnect(false); return false; } diff --git a/android_app/app/src/main/java/com/health/openscale/core/bluetooth/BluetoothExcelvanCF369BLE.java b/android_app/app/src/main/java/com/health/openscale/core/bluetooth/BluetoothExcelvanCF36xBLE.java similarity index 90% rename from android_app/app/src/main/java/com/health/openscale/core/bluetooth/BluetoothExcelvanCF369BLE.java rename to android_app/app/src/main/java/com/health/openscale/core/bluetooth/BluetoothExcelvanCF36xBLE.java index 7b4faab3..aa93327f 100644 --- a/android_app/app/src/main/java/com/health/openscale/core/bluetooth/BluetoothExcelvanCF369BLE.java +++ b/android_app/app/src/main/java/com/health/openscale/core/bluetooth/BluetoothExcelvanCF36xBLE.java @@ -29,7 +29,7 @@ import com.health.openscale.core.utils.Converters; import java.util.Arrays; import java.util.UUID; -public class BluetoothExcelvanCF369BLE extends BluetoothCommunication { +public class BluetoothExcelvanCF36xBLE extends BluetoothCommunication { private final UUID WEIGHT_MEASUREMENT_SERVICE = UUID.fromString("0000FFF0-0000-1000-8000-00805f9b34fb"); private final UUID WEIGHT_MEASUREMENT_CHARACTERISTIC = UUID.fromString("0000FFF1-0000-1000-8000-00805f9b34fb"); private final UUID WEIGHT_CUSTOM0_CHARACTERISTIC = UUID.fromString("0000FFF4-0000-1000-8000-00805f9b34fb"); @@ -37,13 +37,13 @@ public class BluetoothExcelvanCF369BLE extends BluetoothCommunication { private byte[] receivedData = new byte[]{}; - public BluetoothExcelvanCF369BLE(Context context) { + public BluetoothExcelvanCF36xBLE(Context context) { super(context); } @Override public String driverName() { - return "Excelvan CF369BLE"; + return "Excelvan CF36xBLE"; } @Override @@ -117,8 +117,10 @@ public class BluetoothExcelvanCF369BLE extends BluetoothCommunication { if (data != null && data.length > 0) { - // if data is body scale type - if (data.length == 16 && data[0] == (byte)0xcf) { + // if data is body scale type. At least some variants (e.g. CF366BLE) of this scale + // return a 17th byte representing "physiological age". Allow (but ignore) that byte + // to support those variants. + if ((data.length >= 16 && data.length <= 17) && data[0] == (byte)0xcf) { if (!Arrays.equals(data, receivedData)) { // accepts only one data of the same content receivedData = data; parseBytes(data); @@ -135,6 +137,7 @@ public class BluetoothExcelvanCF369BLE extends BluetoothCommunication { float visceralFat = weightBytes[11] & 0xFF; float water = Converters.fromUnsignedInt16Be(weightBytes, 12) / 10.0f; float bmr = Converters.fromUnsignedInt16Be(weightBytes, 14); + // weightBytes[16] is an (optional, ignored) "physiological age" in some scale variants. ScaleMeasurement scaleBtData = new ScaleMeasurement(); diff --git a/android_app/app/src/main/java/com/health/openscale/core/bluetooth/BluetoothExingtechY1.java b/android_app/app/src/main/java/com/health/openscale/core/bluetooth/BluetoothExingtechY1.java index 40f7821c..7e169a2e 100644 --- a/android_app/app/src/main/java/com/health/openscale/core/bluetooth/BluetoothExingtechY1.java +++ b/android_app/app/src/main/java/com/health/openscale/core/bluetooth/BluetoothExingtechY1.java @@ -109,7 +109,7 @@ public class BluetoothExingtechY1 extends BluetoothCommunication { final ScaleUser selectedUser = OpenScale.getInstance().getSelectedScaleUser(); - scaleBtData.setWeight(Converters.toKilogram(weight, selectedUser.getScaleUnit())); + scaleBtData.setWeight(weight); scaleBtData.setFat(fat); scaleBtData.setMuscle(muscle); scaleBtData.setWater(water); diff --git a/android_app/app/src/main/java/com/health/openscale/core/bluetooth/BluetoothFactory.java b/android_app/app/src/main/java/com/health/openscale/core/bluetooth/BluetoothFactory.java index 3000d636..e994a870 100644 --- a/android_app/app/src/main/java/com/health/openscale/core/bluetooth/BluetoothFactory.java +++ b/android_app/app/src/main/java/com/health/openscale/core/bluetooth/BluetoothFactory.java @@ -38,14 +38,14 @@ public class BluetoothFactory { if (name.startsWith("BEURER BF710".toLowerCase(Locale.US))) { return new BluetoothBeurerSanitas(context, BluetoothBeurerSanitas.DeviceType.BEURER_BF710); } - if (name.equals("openScale_MCU".toLowerCase(Locale.US))) { + if (name.equals("openScale".toLowerCase(Locale.US))) { return new BluetoothCustomOpenScale(context); } if (name.equals("Mengii".toLowerCase(Locale.US))) { return new BluetoothDigooDGSO38H(context); } if (name.equals("Electronic Scale".toLowerCase(Locale.US))) { - return new BluetoothExcelvanCF369BLE(context); + return new BluetoothExcelvanCF36xBLE(context); } if (name.equals("VScale".toLowerCase(Locale.US))) { return new BluetoothExingtechY1(context); @@ -58,7 +58,7 @@ public class BluetoothFactory { } // BS444 || BS440 if (deviceName.startsWith("013197") || deviceName.startsWith("0202B6")) { - return new BluetoothMedisanaBS444(context); + return new BluetoothMedisanaBS44x(context); } if (deviceName.startsWith("SWAN") || name.equals("icomon".toLowerCase(Locale.US))) { return new BluetoothMGB(context); diff --git a/android_app/app/src/main/java/com/health/openscale/core/bluetooth/BluetoothMedisanaBS444.java b/android_app/app/src/main/java/com/health/openscale/core/bluetooth/BluetoothMedisanaBS44x.java similarity index 96% rename from android_app/app/src/main/java/com/health/openscale/core/bluetooth/BluetoothMedisanaBS444.java rename to android_app/app/src/main/java/com/health/openscale/core/bluetooth/BluetoothMedisanaBS44x.java index ff56dc4a..f80ae6bb 100644 --- a/android_app/app/src/main/java/com/health/openscale/core/bluetooth/BluetoothMedisanaBS444.java +++ b/android_app/app/src/main/java/com/health/openscale/core/bluetooth/BluetoothMedisanaBS44x.java @@ -25,7 +25,7 @@ import com.health.openscale.core.utils.Converters; import java.util.Date; import java.util.UUID; -public class BluetoothMedisanaBS444 extends BluetoothCommunication { +public class BluetoothMedisanaBS44x extends BluetoothCommunication { private final UUID WEIGHT_MEASUREMENT_SERVICE = UUID.fromString("000078b2-0000-1000-8000-00805f9b34fb"); private final UUID WEIGHT_MEASUREMENT_CHARACTERISTIC = UUID.fromString("00008a21-0000-1000-8000-00805f9b34fb"); // indication, read-only private final UUID FEATURE_MEASUREMENT_CHARACTERISTIC = UUID.fromString("00008a22-0000-1000-8000-00805f9b34fb"); // indication, read-only @@ -39,16 +39,11 @@ public class BluetoothMedisanaBS444 extends BluetoothCommunication { // Scale time is in seconds since 2010-01-01 private static final long SCALE_UNIX_TIMESTAMP_OFFSET = 1262304000; - public BluetoothMedisanaBS444(Context context) { + public BluetoothMedisanaBS44x(Context context) { super(context); btScaleMeasurement = new ScaleMeasurement(); } - @Override - protected boolean discoverDeviceBeforeConnecting() { - return true; - } - @Override public String driverName() { return "Medisana BS44x"; diff --git a/android_app/app/src/main/java/com/health/openscale/core/bluetooth/BluetoothOneByone.java b/android_app/app/src/main/java/com/health/openscale/core/bluetooth/BluetoothOneByone.java index bf85e6c2..3a788da6 100644 --- a/android_app/app/src/main/java/com/health/openscale/core/bluetooth/BluetoothOneByone.java +++ b/android_app/app/src/main/java/com/health/openscale/core/bluetooth/BluetoothOneByone.java @@ -31,12 +31,16 @@ import timber.log.Timber; public class BluetoothOneByone extends BluetoothCommunication { private final UUID WEIGHT_MEASUREMENT_SERVICE_BODY_COMPOSITION = UUID.fromString("0000181B-0000-1000-8000-00805f9b34fb"); + private final UUID WEIGHT_MEASUREMENT_CHARACTERISTIC_BODY_COMPOSITION = UUID.fromString("00002A9C-0000-1000-8000-00805f9b34fb"); // read, indication + private final UUID WEIGHT_MEASUREMENT_CHARACTERISTIC_BODY_COMPOSITION_ALT = UUID.fromString("0000fff4-0000-1000-8000-00805f9b34fb"); // notify private final UUID WEIGHT_MEASUREMENT_SERVICE = UUID.fromString("0000fff0-0000-1000-8000-00805f9b34fb"); private final UUID CMD_MEASUREMENT_CHARACTERISTIC = UUID.fromString("0000fff1-0000-1000-8000-00805f9b34fb"); // write only private final UUID WEIGHT_MEASUREMENT_CONFIG = UUID.fromString("00002902-0000-1000-8000-00805f9b34fb"); + private float lastWeight; + public BluetoothOneByone(Context context) { super(context); } @@ -50,7 +54,18 @@ public class BluetoothOneByone extends BluetoothCommunication { protected boolean nextInitCmd(int stateNr) { switch (stateNr) { case 0: - setIndicationOn(WEIGHT_MEASUREMENT_SERVICE_BODY_COMPOSITION, WEIGHT_MEASUREMENT_CHARACTERISTIC_BODY_COMPOSITION, WEIGHT_MEASUREMENT_CONFIG); + lastWeight = 0; + + if (hasBluetoothGattService(WEIGHT_MEASUREMENT_SERVICE_BODY_COMPOSITION)) { + setIndicationOn(WEIGHT_MEASUREMENT_SERVICE_BODY_COMPOSITION, + WEIGHT_MEASUREMENT_CHARACTERISTIC_BODY_COMPOSITION, + WEIGHT_MEASUREMENT_CONFIG); + } + else { + setNotificationOn(WEIGHT_MEASUREMENT_SERVICE, + WEIGHT_MEASUREMENT_CHARACTERISTIC_BODY_COMPOSITION_ALT, + WEIGHT_MEASUREMENT_CONFIG); + } break; case 1: ScaleUser currentUser = OpenScale.getInstance().getSelectedScaleUser(); @@ -91,11 +106,20 @@ public class BluetoothOneByone extends BluetoothCommunication { @Override public void onBluetoothDataChange(BluetoothGatt bluetoothGatt, BluetoothGattCharacteristic gattCharacteristic) { final byte[] data = gattCharacteristic.getValue(); + if (data == null) { + return; + } + final UUID uuid = gattCharacteristic.getUuid(); // if data is valid data - if (data != null && data.length == 20) { + if (data.length == 20 + && uuid.equals(WEIGHT_MEASUREMENT_CHARACTERISTIC_BODY_COMPOSITION)) { parseBytes(data); } + else if (data.length == 11 + && uuid.equals(WEIGHT_MEASUREMENT_CHARACTERISTIC_BODY_COMPOSITION_ALT)) { + parseBytesAlt(data); + } } private void parseBytes(byte[] weightBytes) { @@ -104,10 +128,31 @@ public class BluetoothOneByone extends BluetoothCommunication { Timber.d("weight: %.2f, impedance: %d", weight, impedance); - ScaleMeasurement scaleBtData = new ScaleMeasurement(); + // This check should be a bit more elaborate, but it works for now... + if (weight != lastWeight) { + lastWeight = weight; - scaleBtData.setWeight(weight); + ScaleMeasurement scaleBtData = new ScaleMeasurement(); + scaleBtData.setWeight(weight); - addScaleData(scaleBtData); + addScaleData(scaleBtData); + } + } + + private void parseBytesAlt(byte[] weightBytes) { + float weight = Converters.fromUnsignedInt16Le(weightBytes, 3) / 100.0f; + boolean done = (weightBytes[9] & 0xff) == 0; + + Timber.d("weight: %.2f%s", weight, done ? " (done)" : ""); + + // This check should be a bit more elaborate, but it works for now... + if (done && weight != lastWeight) { + lastWeight = weight; + + ScaleMeasurement scaleBtData = new ScaleMeasurement(); + scaleBtData.setWeight(weight); + + addScaleData(scaleBtData); + } } } diff --git a/android_app/app/src/main/java/com/health/openscale/core/bluetooth/BluetoothYunmaiSE_Mini.java b/android_app/app/src/main/java/com/health/openscale/core/bluetooth/BluetoothYunmaiSE_Mini.java index 16108f77..adff58c5 100644 --- a/android_app/app/src/main/java/com/health/openscale/core/bluetooth/BluetoothYunmaiSE_Mini.java +++ b/android_app/app/src/main/java/com/health/openscale/core/bluetooth/BluetoothYunmaiSE_Mini.java @@ -127,7 +127,7 @@ public class BluetoothYunmaiSE_Mini extends BluetoothCommunication { scaleBtData.setDateTime(new Date(timestamp)); float weight = Converters.fromUnsignedInt16Be(weightBytes, 13) / 100.0f; - scaleBtData.setWeight(Converters.toKilogram(weight, selectedUser.getScaleUnit())); + scaleBtData.setWeight(weight); if (isMini) { float fat = Converters.fromUnsignedInt16Be(weightBytes, 17) / 100.0f; diff --git a/android_app/app/src/main/java/com/health/openscale/core/datatypes/ScaleMeasurement.java b/android_app/app/src/main/java/com/health/openscale/core/datatypes/ScaleMeasurement.java index 986a2b00..2634425f 100644 --- a/android_app/app/src/main/java/com/health/openscale/core/datatypes/ScaleMeasurement.java +++ b/android_app/app/src/main/java/com/health/openscale/core/datatypes/ScaleMeasurement.java @@ -353,7 +353,12 @@ public class ScaleMeasurement implements Cloneable { } public void setComment(String comment) { - this.comment = comment; + if (comment == null) { + this.comment = ""; + } + else { + this.comment = comment; + } } public float getBMI(float body_height) { diff --git a/android_app/app/src/main/java/com/health/openscale/gui/fragments/GraphFragment.java b/android_app/app/src/main/java/com/health/openscale/gui/fragments/GraphFragment.java index 18b1b1c7..43d21dcc 100644 --- a/android_app/app/src/main/java/com/health/openscale/gui/fragments/GraphFragment.java +++ b/android_app/app/src/main/java/com/health/openscale/gui/fragments/GraphFragment.java @@ -41,6 +41,8 @@ import android.widget.TextView; import com.health.openscale.R; import com.health.openscale.core.OpenScale; import com.health.openscale.core.datatypes.ScaleMeasurement; +import com.health.openscale.core.datatypes.ScaleUser; +import com.health.openscale.core.utils.Converters; import com.health.openscale.core.utils.PolynomialFitter; import com.health.openscale.gui.activities.DataEntryActivity; import com.health.openscale.gui.views.BMRMeasurementView; @@ -412,7 +414,8 @@ public class GraphFragment extends Fragment implements FragmentUpdateListener { if (prefs.getBoolean("goalLine", true)) { Stack valuesGoalLine = new Stack<>(); - float goalWeight = openScale.getSelectedScaleUser().getGoalWeight(); + final ScaleUser user = openScale.getSelectedScaleUser(); + float goalWeight = Converters.fromKilogram(user.getGoalWeight(), user.getScaleUnit()); valuesGoalLine.push(new PointValue(0, goalWeight)); valuesGoalLine.push(new PointValue(maxDays, goalWeight)); diff --git a/android_app/app/src/main/java/com/health/openscale/gui/fragments/TableFragment.java b/android_app/app/src/main/java/com/health/openscale/gui/fragments/TableFragment.java index 294410a8..22fbf4c4 100644 --- a/android_app/app/src/main/java/com/health/openscale/gui/fragments/TableFragment.java +++ b/android_app/app/src/main/java/com/health/openscale/gui/fragments/TableFragment.java @@ -17,24 +17,18 @@ package com.health.openscale.gui.fragments; import android.content.Intent; import android.content.res.Configuration; -import android.graphics.Color; -import android.graphics.Typeface; import android.os.Bundle; import android.support.v4.app.Fragment; -import android.support.v4.content.ContextCompat; +import android.support.v7.widget.DividerItemDecoration; +import android.support.v7.widget.LinearLayoutManager; +import android.support.v7.widget.RecyclerView; import android.text.SpannableStringBuilder; -import android.text.Spanned; import android.view.Gravity; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; -import android.widget.AdapterView; -import android.widget.BaseAdapter; -import android.widget.Button; import android.widget.ImageView; import android.widget.LinearLayout; -import android.widget.ListView; -import android.widget.TableLayout; import android.widget.TableRow; import android.widget.TextView; @@ -45,45 +39,43 @@ import com.health.openscale.gui.activities.DataEntryActivity; import com.health.openscale.gui.views.MeasurementView; import java.util.ArrayList; -import java.util.HashMap; +import java.util.Calendar; +import java.util.Date; import java.util.List; import static android.util.TypedValue.COMPLEX_UNIT_DIP; public class TableFragment extends Fragment implements FragmentUpdateListener { private View tableView; - private ListView tableDataView; private LinearLayout tableHeaderView; - private LinearLayout subpageView; + + private RecyclerView recyclerView; + private MeasurementsAdapter adapter; + private LinearLayoutManager layoutManager; private List measurementViews; - private int selectedSubpageNr; - private static final String SELECTED_SUBPAGE_NR_KEY = "selectedSubpageNr"; - public TableFragment() { } @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) - { + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { tableView = inflater.inflate(R.layout.fragment_table, container, false); - subpageView = tableView.findViewById(R.id.subpageView); - - tableDataView = tableView.findViewById(R.id.tableDataView); tableHeaderView = tableView.findViewById(R.id.tableHeaderView); + recyclerView = tableView.findViewById(R.id.tableDataView); - tableDataView.setAdapter(new ListViewAdapter()); - tableDataView.setOnItemClickListener(new onClickListenerRow()); + recyclerView.setHasFixedSize(true); - if (savedInstanceState == null) { - selectedSubpageNr = 0; - } - else { - selectedSubpageNr = savedInstanceState.getInt(SELECTED_SUBPAGE_NR_KEY); - } + layoutManager = new LinearLayoutManager(getContext()); + recyclerView.setLayoutManager(layoutManager); + + recyclerView.addItemDecoration(new DividerItemDecoration( + recyclerView.getContext(), layoutManager.getOrientation())); + + adapter = new MeasurementsAdapter(); + recyclerView.setAdapter(adapter); measurementViews = MeasurementView.getMeasurementList( getContext(), MeasurementView.DateTimeOrder.FIRST); @@ -103,236 +95,176 @@ public class TableFragment extends Fragment implements FragmentUpdateListener { super.onDestroyView(); } - @Override - public void onSaveInstanceState(Bundle outState) { - super.onSaveInstanceState(outState); - outState.putInt(SELECTED_SUBPAGE_NR_KEY, selectedSubpageNr); - } - @Override public void updateOnView(List scaleMeasurementList) { - final int maxSize = 25; - - final int subpageCount = (int)Math.ceil(scaleMeasurementList.size() / (double)maxSize); - if (selectedSubpageNr >= subpageCount) { - selectedSubpageNr = Math.max(0, subpageCount - 1); - } - - subpageView.removeAllViews(); - - Button moveSubpageLeft = new Button(tableView.getContext()); - moveSubpageLeft.setText("<"); - moveSubpageLeft.setPadding(0,0,0,0); - moveSubpageLeft.setTextColor(Color.WHITE); - moveSubpageLeft.setBackground(ContextCompat.getDrawable(tableView.getContext(), R.drawable.flat_selector)); - moveSubpageLeft.setLayoutParams(new LinearLayout.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT)); - moveSubpageLeft.getLayoutParams().height = pxImageDp(20); - moveSubpageLeft.getLayoutParams().width = pxImageDp(50); - moveSubpageLeft.setOnClickListener(new onClickListenerMoveSubpageLeft()); - moveSubpageLeft.setEnabled(selectedSubpageNr > 0); - subpageView.addView(moveSubpageLeft); - - for (int i = 0; i < subpageCount; i++) { - TextView subpageNrView = new TextView(tableView.getContext()); - subpageNrView.setOnClickListener(new onClickListenerSubpageSelect()); - subpageNrView.setText(Integer.toString(i+1)); - subpageNrView.setTextColor(Color.GRAY); - subpageNrView.setPadding(10, 10, 20, 10); - - subpageView.addView(subpageNrView); - } - - if (subpageView.getChildCount() > 1) { - TextView selectedSubpageNrView = (TextView) subpageView.getChildAt(selectedSubpageNr + 1); - if (selectedSubpageNrView != null) { - selectedSubpageNrView.setTypeface(null, Typeface.BOLD); - selectedSubpageNrView.setTextColor(Color.WHITE); - } - } - - Button moveSubpageRight = new Button(tableView.getContext()); - moveSubpageRight.setText(">"); - moveSubpageRight.setPadding(0,0,0,0); - moveSubpageRight.setTextColor(Color.WHITE); - moveSubpageRight.setBackground(ContextCompat.getDrawable(tableView.getContext(), R.drawable.flat_selector)); - moveSubpageRight.setLayoutParams(new LinearLayout.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT)); - moveSubpageRight.getLayoutParams().height = pxImageDp(20); - moveSubpageRight.getLayoutParams().width = pxImageDp(50); - moveSubpageRight.setOnClickListener(new onClickListenerMoveSubpageRight()); - moveSubpageRight.setEnabled(selectedSubpageNr + 1 < subpageCount); - subpageView.addView(moveSubpageRight); - - subpageView.setVisibility(subpageCount > 1 ? View.VISIBLE : View.GONE); - tableHeaderView.removeAllViews(); + final int iconHeight = pxImageDp(20); ArrayList visibleMeasurements = new ArrayList<>(); + for (MeasurementView measurement : measurementViews) { - - if (measurement.isVisible()) { - ImageView headerIcon = new ImageView(tableView.getContext()); - headerIcon.setImageDrawable(measurement.getIcon()); - headerIcon.setLayoutParams(new TableRow.LayoutParams(TableLayout.LayoutParams.MATCH_PARENT, TableLayout.LayoutParams.MATCH_PARENT, 1)); - headerIcon.getLayoutParams().width = 0; - headerIcon.setScaleType(ImageView.ScaleType.CENTER_INSIDE); - headerIcon.getLayoutParams().height = pxImageDp(20); - - tableHeaderView.addView(headerIcon); - - visibleMeasurements.add(measurement); + if (!measurement.isVisible()) { + continue; } + ImageView headerIcon = new ImageView(tableView.getContext()); + headerIcon.setImageDrawable(measurement.getIcon()); + headerIcon.setLayoutParams(new TableRow.LayoutParams(0, iconHeight, 1)); + headerIcon.setScaleType(ImageView.ScaleType.CENTER_INSIDE); + + tableHeaderView.addView(headerIcon); + + visibleMeasurements.add(measurement); } - ListViewAdapter adapter = (ListViewAdapter) tableDataView.getAdapter(); - - final int startOffset = maxSize * selectedSubpageNr; - final int endOffset = Math.min(startOffset + maxSize + 1, scaleMeasurementList.size()); - adapter.setMeasurements(visibleMeasurements, scaleMeasurementList.subList(startOffset, endOffset), maxSize); + adapter.setMeasurements(visibleMeasurements, scaleMeasurementList); } private int pxImageDp(float dp) { return (int)(dp * getResources().getDisplayMetrics().density + 0.5f); } - private class onClickListenerRow implements AdapterView.OnItemClickListener { - @Override - public void onItemClick(AdapterView parent, View view, int position, long id) { - Intent intent = new Intent(tableView.getContext(), DataEntryActivity.class); - intent.putExtra(DataEntryActivity.EXTRA_ID, (int)id); - startActivity(intent); - } - } + private class MeasurementsAdapter extends RecyclerView.Adapter { + public static final int VIEW_TYPE_MEASUREMENT = 0; + public static final int VIEW_TYPE_YEAR = 1; - private class onClickListenerMoveSubpageLeft implements View.OnClickListener { - @Override - public void onClick(View v) { - if (selectedSubpageNr > 0) { - selectedSubpageNr--; - updateOnView(OpenScale.getInstance().getScaleMeasurementList()); + public class ViewHolder extends RecyclerView.ViewHolder { + public LinearLayout measurementView; + public ViewHolder(LinearLayout view) { + super(view); + measurementView = view; } } - } - - private class onClickListenerMoveSubpageRight implements View.OnClickListener { - @Override - public void onClick(View v) { - if (selectedSubpageNr < (subpageView.getChildCount() - 3)) { - selectedSubpageNr++; - updateOnView(OpenScale.getInstance().getScaleMeasurementList()); - } - } - } - - private class onClickListenerSubpageSelect implements View.OnClickListener { - @Override - public void onClick(View v) { - TextView nrView = (TextView)v; - - selectedSubpageNr = Integer.parseInt(nrView.getText().toString())-1; - updateOnView(OpenScale.getInstance().getScaleMeasurementList()); - } - } - - private class ListViewAdapter extends BaseAdapter { private List visibleMeasurements; private List scaleMeasurements; - private int measurementsToShow = 0; - - private Spanned[][] stringCache; public void setMeasurements(List visibleMeasurements, - List scaleMeasurements, - int maxSize) { + List scaleMeasurements) { this.visibleMeasurements = visibleMeasurements; - this.scaleMeasurements = scaleMeasurements; - measurementsToShow = Math.min(scaleMeasurements.size(), maxSize); + this.scaleMeasurements = new ArrayList<>(scaleMeasurements.size() + 10); - stringCache = new Spanned[measurementsToShow][visibleMeasurements.size()]; + Calendar calendar = Calendar.getInstance(); + if (!scaleMeasurements.isEmpty()) { + calendar.setTime(scaleMeasurements.get(0).getDateTime()); + } + calendar.set(calendar.get(Calendar.YEAR), 0, 1, 0, 0, 0); + calendar.set(calendar.MILLISECOND, 0); + + // Copy all measurements from input parameter to member variable and insert + // an extra "null" entry when the year changes. + Date yearStart = calendar.getTime(); + for (int i = 0; i < scaleMeasurements.size(); ++i) { + final ScaleMeasurement measurement = scaleMeasurements.get(i); + + if (measurement.getDateTime().before(yearStart)) { + this.scaleMeasurements.add(null); + + Calendar newCalendar = Calendar.getInstance(); + newCalendar.setTime(measurement.getDateTime()); + calendar.set(Calendar.YEAR, newCalendar.get(Calendar.YEAR)); + yearStart = calendar.getTime(); + } + + this.scaleMeasurements.add(measurement); + } notifyDataSetChanged(); } @Override - public int getCount() { - return measurementsToShow; - } + public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + LinearLayout row = new LinearLayout(getContext()); + row.setLayoutParams(new LinearLayout.LayoutParams( + LinearLayout.LayoutParams.MATCH_PARENT, + LinearLayout.LayoutParams.WRAP_CONTENT)); - @Override - public Object getItem(int position) { - return scaleMeasurements.get(position); - } + final int screenSize = getResources().getConfiguration() + .screenLayout & Configuration.SCREENLAYOUT_SIZE_MASK; + final boolean isSmallScreen = + screenSize != Configuration.SCREENLAYOUT_SIZE_XLARGE + && screenSize != Configuration.SCREENLAYOUT_SIZE_LARGE; - @Override - public long getItemId(int position) { - return scaleMeasurements.get(position).getId(); - } + final int count = viewType == VIEW_TYPE_YEAR ? 1 : visibleMeasurements.size(); + for (int i = 0; i < count; ++i) { + TextView column = new TextView(getContext()); + column.setLayoutParams(new LinearLayout.LayoutParams( + 0, ViewGroup.LayoutParams.WRAP_CONTENT, 1)); - @Override - public View getView(int position, View convertView, ViewGroup parent) { - // Create entries in stringCache if needed - if (stringCache[position][0] == null) { - ScaleMeasurement measurement = scaleMeasurements.get(position); - ScaleMeasurement prevMeasurement = null; - if (position + 1 < scaleMeasurements.size()) { - prevMeasurement = scaleMeasurements.get(position + 1); - } - - for (int i = 0; i < visibleMeasurements.size(); ++i) { - visibleMeasurements.get(i).loadFrom(measurement, prevMeasurement); - - SpannableStringBuilder string = new SpannableStringBuilder(); - string.append(visibleMeasurements.get(i).getValueAsString(false)); - visibleMeasurements.get(i).appendDiffValue(string, true); - - stringCache[position][i] = string; - } - } - - // Create view if needed - LinearLayout row; - if (convertView == null) { - row = new LinearLayout(getContext()); - - final int screenSize = getResources().getConfiguration().screenLayout & Configuration.SCREENLAYOUT_SIZE_MASK; - final boolean isSmallScreen = - screenSize != Configuration.SCREENLAYOUT_SIZE_XLARGE - && screenSize != Configuration.SCREENLAYOUT_SIZE_LARGE; - - for (int i = 0; i < visibleMeasurements.size(); ++i) { - TextView column = new TextView(getContext()); - LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams( - LinearLayout.LayoutParams.WRAP_CONTENT, - LinearLayout.LayoutParams.WRAP_CONTENT, - 1); - layoutParams.width = 0; - column.setLayoutParams(layoutParams); + if (viewType == VIEW_TYPE_MEASUREMENT) { column.setMinLines(2); column.setGravity(Gravity.CENTER_HORIZONTAL); if (isSmallScreen) { column.setTextSize(COMPLEX_UNIT_DIP, 9); } - row.addView(column); } + else { + column.setPadding(0, 10, 0, 10); + column.setGravity(Gravity.CENTER); + column.setTextSize(COMPLEX_UNIT_DIP, 16); + } + + row.addView(column); } - else { - row = (LinearLayout) convertView; + + return new ViewHolder(row); + } + + @Override + public void onBindViewHolder(ViewHolder holder, int position) { + LinearLayout row = holder.measurementView; + + final ScaleMeasurement measurement = scaleMeasurements.get(position); + if (measurement == null) { + ScaleMeasurement nextMeasurement = scaleMeasurements.get(position + 1); + Calendar calendar = Calendar.getInstance(); + calendar.setTime(nextMeasurement.getDateTime()); + + TextView column = (TextView) row.getChildAt(0); + column.setText(String.format("%d", calendar.get(Calendar.YEAR))); + return; + } + + ScaleMeasurement prevMeasurement = null; + if (position + 1 < scaleMeasurements.size()) { + prevMeasurement = scaleMeasurements.get(position + 1); + if (prevMeasurement == null) { + prevMeasurement = scaleMeasurements.get(position + 2); + } } // Fill view with data for (int i = 0; i < visibleMeasurements.size(); ++i) { + final MeasurementView view = visibleMeasurements.get(i); + view.loadFrom(measurement, prevMeasurement); + + SpannableStringBuilder string = new SpannableStringBuilder(); + string.append(view.getValueAsString(false)); + view.appendDiffValue(string, true); + TextView column = (TextView) row.getChildAt(i); - column.setText(stringCache[position][i]); + column.setText(string); } - return row; + row.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + Intent intent = new Intent(getContext(), DataEntryActivity.class); + intent.putExtra(DataEntryActivity.EXTRA_ID, measurement.getId()); + startActivity(intent); + } + }); } @Override - public boolean hasStableIds() { - return true; + public int getItemCount() { + return scaleMeasurements == null ? 0 : scaleMeasurements.size(); + } + + @Override + public int getItemViewType(int position) { + return scaleMeasurements.get(position) != null ? VIEW_TYPE_MEASUREMENT : VIEW_TYPE_YEAR; } } } diff --git a/android_app/app/src/main/java/com/health/openscale/gui/preferences/AboutPreferences.java b/android_app/app/src/main/java/com/health/openscale/gui/preferences/AboutPreferences.java index 9c865de1..d0fcd02d 100644 --- a/android_app/app/src/main/java/com/health/openscale/gui/preferences/AboutPreferences.java +++ b/android_app/app/src/main/java/com/health/openscale/gui/preferences/AboutPreferences.java @@ -80,8 +80,9 @@ public class AboutPreferences extends PreferenceFragment { @Override protected void log(int priority, String tag, String message, Throwable t) { - writer.printf("%s %s %s: %s\n", - format.format(new Date()), priorityToString(priority), tag, message); + final long id = Thread.currentThread().getId(); + writer.printf("%s %s [%d] %s: %s\n", + format.format(new Date()), priorityToString(priority), id, tag, message); } } diff --git a/android_app/app/src/main/java/com/health/openscale/gui/preferences/BluetoothPreferences.java b/android_app/app/src/main/java/com/health/openscale/gui/preferences/BluetoothPreferences.java index 84207f62..bf3cf998 100644 --- a/android_app/app/src/main/java/com/health/openscale/gui/preferences/BluetoothPreferences.java +++ b/android_app/app/src/main/java/com/health/openscale/gui/preferences/BluetoothPreferences.java @@ -232,6 +232,7 @@ public class BluetoothPreferences extends PreferenceFragment { prefBtDevice.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() { @Override public boolean onPreferenceClick(Preference preference) { + stopDiscoveryAndLeScan(); getDebugInfo(device); return false; } @@ -306,7 +307,7 @@ public class BluetoothPreferences extends PreferenceFragment { super.onStart(); // Restart discovery after e.g. orientation change - if (btScanner.getDialog() != null) { + if (btScanner.getDialog() != null && btScanner.getDialog().isShowing()) { startBluetoothDiscovery(); } } diff --git a/android_app/app/src/main/java/com/health/openscale/gui/views/DateMeasurementView.java b/android_app/app/src/main/java/com/health/openscale/gui/views/DateMeasurementView.java index b807197f..c2ebc1fe 100644 --- a/android_app/app/src/main/java/com/health/openscale/gui/views/DateMeasurementView.java +++ b/android_app/app/src/main/java/com/health/openscale/gui/views/DateMeasurementView.java @@ -31,11 +31,12 @@ public class DateMeasurementView extends MeasurementView { // Don't change key value, it may be stored persistent in preferences public static final String KEY = "date"; - private static final DateFormat dateFormat = DateFormat.getDateInstance(); + private final DateFormat dateFormat; private Date date; public DateMeasurementView(Context context) { super(context, R.string.label_date, R.drawable.ic_lastmonth); + dateFormat = DateFormat.getDateInstance(); } @Override diff --git a/android_app/app/src/main/res/layout/fragment_table.xml b/android_app/app/src/main/res/layout/fragment_table.xml index bebd0020..0c5ac42f 100644 --- a/android_app/app/src/main/res/layout/fragment_table.xml +++ b/android_app/app/src/main/res/layout/fragment_table.xml @@ -1,68 +1,24 @@ - - - - - - - - - - - - - - + android:paddingTop="5dp" /> - + + - - - - + android:scrollbars="vertical" /> diff --git a/android_app/app/src/main/res/values-da/strings.xml b/android_app/app/src/main/res/values-da/strings.xml new file mode 100644 index 00000000..bf837b76 --- /dev/null +++ b/android_app/app/src/main/res/values-da/strings.xml @@ -0,0 +1,228 @@ + + + Oversigt + Diagram + Tabel + Statistik + Brugere + Målinger + Om + Indstillinger + Bluetooth-status + Afslut + Ok + Ja + Nej + Slet + Tilføj bruger + Kropsvægt + Body Mass Index (BMI) + Basalstofskifte (BMR) + Kropsfedt + Kropsvæske + Mandag + Tirsdag + Onsdag + Torsdag + Fredag + Lørdag + Søndag + Sikkerhedskopi + Dato for indvejning + Tid + Dage + Hjemmeside + Licens + Fejl i eksport + Køn + Mand + Kvinde + Målvægt + Måldato + Bruger + Seneste vejning + Mål + Dato + Tid + Fødselsdag + Navn + Højde + Vægtenhed + Dage tilbage + Måldatoen: + Vægtforskel + + %d dag + %d dage + + Gennemsnit den seneste uge + Gennemsnit den seneste måned + Muskler + Fedtfri kropsvægt + Taljemål + Hofteomkreds + Kommentar + Talje-hofte ratio + Talje-højde ratio + Knoglemasse + Smart tildeling af brugere + Import + Eksport + Slet alle + Obligatorisk + Værdien er uden for intervallet + Fejl ved import + Fejl: navn er påkrævet + Fejl: højde er påkrævet + Fejl: vægt er påkrævet + Fejl: målvægt er påkrævet + Data er slettet + Alle data er blevet slettet + Eksportér til + Importér fra + Værdien i + Kommentar + er synlig + er ikke synlig + aktiveret + deaktiveret + ikke tilgængelig + Forbinder til: + Ingen forbindelse til Bluetooth enhed + Ingen Bluetooth-enhed fundet + Forbindelse etableret + Initierer Bluetooth enhed + Uventet Bluetooth fejl + Tilføjede %1$.2f%2$s [%3$s] til %4$s + Dit navn + Vælg en bruger under indstillinger + Ingen evaluering til rådighed + Ønsker du at slette data? + Ønsker du at slette alle data? + Ønsker du at slette brugeren? + Bluetooth + Aktivér Bluetoth + Etablerer Bluetooth forbindelse + Data labels + Datapunkter + Bekræft sletning + Mållingsdatabase + Udviklere + auto + Påmindelse + Meddelelsestekst + + enheden er ikke understøttet + Exportér sikkerhedskopi + Importér sikkerhedskopi + Eksportmappe + blev ikke fundet + Ignorér data udenfor interval + Startvægt + Vægt tilbagegang + Grad af tilbagegang + Mållinje + Det maksimale antal brugere er nået + Træd op på vægten med bare fødder for at foretagen en referencemåling + Måler vægt: %.2f + Åben + Luk + Ingen Bluetooth enhed er valgt + En måling med samme dato og tidspunkt findes allerede + Generelt + Fejl: navnet skal være mindst tre tegn langt + Tema + Er du tilfreds med openScale? + Vil du rate app'en på Google Play eller GitHub? + Send feedback? + Ja + Nej + OK + Nej tak + En uventet fejl er opstået. +\n +\nOpret venligst en ny sag +\nhttps://github.com/oliexdev/openScale/issues + Genstart app + Luk app + Fejlinformation + Fejlinformation + Luk + Kopiér til udklipsholder + Kopieret til udklipsholder + Fejlinformation + Gem + + Del + + Søgning af sluttet + Hjælp + + Fold ud + Redigér + Månedsvis + + Tilladelse er ikke givet + Tilladelse til lokalbestemmelse er påkrævet for at søge efter Bluetooth-enheder + Information + + CSV dataeksport (%s) + + Næste + Indstil standardsortéring + + ugentlig + Flet med tidligere måling + Overskriv tidligere eksport \"%s\"? + Vis i oversigtsdiagram + Målt i % + Estimatmåling + Estimeringsformel + + Systemstandard + Sprog + Lyst + Mørkt + Bidrag med oversættelse + Tilføj eller ret oversættelse + Oversigtsdiagram + Procent + Skønsmæssigt + Baseret på vægt, højde, alder, køn mv. + Denne personvægt er ikke understøttet + Klik for at hjælpe med at tilføje understøttelse af personvægten + Automatisk sikkerhedskopiering + Planlæg sikkerhedskopiering + Overskriv tidligere sikkerhedskopi + daglig + ugentlig + månedsvis + Udvikling + Gem fejlfindingslog + Din Bluetooth personvægt +Tryk, og træk til den ønskede position + Klik på målinger for at konfigurere + Vælg målenhed + Vælg bruger + Konfigurér widget +Tilføj måling + Visceralt fedt omkring indre organer + Brystomkreds + Låromkreds + Bicepsomkreds + Halsomkreds + Hudfoldsmåling + Bryst hudmåling + Mave hudmåling + Lår hudmåling + Triceps hudmåling + Mave hudmåling + Hofte hudmåling + Målenhed + Aktivitetsniveau +Stillesiddende + let + Moderat + hård + Ekstrem + \ No newline at end of file diff --git a/android_app/app/src/main/res/values-fr/strings.xml b/android_app/app/src/main/res/values-fr/strings.xml index 90d89748..a614133d 100644 --- a/android_app/app/src/main/res/values-fr/strings.xml +++ b/android_app/app/src/main/res/values-fr/strings.xml @@ -21,7 +21,7 @@ IMC Graisse corporelle Pourcentage d\'eau - Pourcentage musculaire + Muscle Tour de taille Tour de hanches Commentaire @@ -33,8 +33,8 @@ %d jour %d jours - 7 derniers jours - 30 derniers jours + Moyenne sur la dernière semaine + Moyenne sur le mois dernier Différence de poids La date de l\'objectif : Jours restants @@ -54,14 +54,14 @@ utilisateur dernière mesure - objectif + Objectif Importer Exporter Tout supprimer Valeur requise - La valeur n\'est pas dans la plage + Valeur hors plage Erreur durant l\'exportation Erreur durant l\'importation Erreur : Nom d\'utilisateur requis @@ -70,8 +70,8 @@ L\'entrée a été supprimée Toutes les entrées ont été supprimées - Données exportées vers - Donnéees importées depuis + Exporté vers + Importé depuis Valeur en Commentaire optionnel est visible @@ -103,10 +103,10 @@ Supprimer la confirmation Rappel - Jours de la semaine + Jours Heure Intitulé de la notification - C\'est l\'heure de se peser! + C\'est l\'heure de se peser ! le @@ -144,7 +144,7 @@ Sauvegarder Permission non accordée - Information + Info Suivant À propos Partager @@ -176,10 +176,10 @@ Masse sans graisse %1$.2f%2$s [%3$s] to %4$s rajouté - des mesures à la même date et heure existent déjà + une mesure à la même date et heure existe déjà Fusionner avec la dernière mesure - Détection de pèse personne en cours + Détection de votre pèse-personne Bluetooth en cours Détection finalisé Base de données de mesures @@ -223,4 +223,30 @@ Cliquer ici pour contribuer Développement Enregistrez les données de débogage dans un fichier +Ajouter une mesure + Effectuez un appui long suivi d\'un déplacement des mesures pour les réorganiser + Cliquez sur une mesure pour la configurer + Votre pèse-personne Bluetooth + Sélectionner une mesure + Sélectionner un utilisateur + Configurer le widget + Graisse viscérale + Tour de poitrine + Tour de cuisse + Tour des biceps + Tour du cou + Pince adipomètre + Pli cutané de la poitrine + Pli cutané abdominal + Pli cutané de la cuisse + Pli cutané du triceps + Pli cutané abdominal + Pli cutané à la hanche + Unité de mesure + Niveau d\'activité + Sédentaire + Faible + Modéré + Important + Extrême diff --git a/android_app/app/src/main/res/values-hr/strings.xml b/android_app/app/src/main/res/values-hr/strings.xml new file mode 100644 index 00000000..fe452571 --- /dev/null +++ b/android_app/app/src/main/res/values-hr/strings.xml @@ -0,0 +1,246 @@ + +Pregled + Grafikon + Tablica + Statistika + Korisnici + Mjerenja + O programu + Općenito + + Postavke + Bluetooth Status + otvori + zatvori + + Odustani + U redu + Da + Ne + Izbriši + Dodaj korisnika + Dodaj mjerenje + Podjeli + CSV izvoz (%s) + + Težina + Indeks tjelesne mase (BMI) + Bazalni metabolizam (BMR) + Masnoća + Voda + Mišići + Težina bez masnoća + Opseg struka + Opseg kukova + Komentar + Omjer struka i visine + Omjer struka i kukova + Masa kostiju + Pametna dodjela korisnika + + + Jedan + Nekoliko + Ostalo + + Prosjek prošlog tjedna + Prosjek prošlog mjeseca + Razlika težine + Ciljani datum: + Preostalo dana + + Datum + Vrijeme + Datum rođenja + Ime + Visina + Mjerne jedinice + Spol + Muški + Ženski + Ciljana težina + Ciljani datum + + korisnik + zadnje mjerenje + Cilj + + Uvoz + Izvoz + Izbriši sve + + Potrebna vrijednost + Vrijednost izvan granica + Greška u izvozu + Greška u uvozu + Greška: Unesite ime + Greška: Ime mora imati barem 3 znaka + Greška: Unesite težinu + Greška: Unesite početnu težinu + Greška: Unesite ciljanu težinu + + Stavka obrisana + Sve stavke obrisane + Izvezeno u + Uvezeno iz + Vrijednost u + Komentar (neobavezan) + je vidljivo + nije vidljivo + uključeno + isključeno + nedostupno + Spajanje na: + Izgubljena Bluetooth veza + Nije pronađen nijedan uređaj + Nije odabran nijedan uređaj + Veza uspostavljena + Inicijalizacija Bluetooth uređaja + Neočekivana Bluetooth greška + %1$.2f%2$s [%3$s] do %4$s dodan + mjerenje s istim datumom i vremenom već postoji + + Vaše ime + Ne postoji niti jedan korisnik. Stvorite ga u postavkama. + Nije moguće procijeniti vrijednost + + Izbrisati stavku? + Izbrisati sve stavke svih korisnika ? + Izbrisati korisnika? + + Bluetooth + Povezivanje sa vagom kod pokretanja + Spoji sa zadnjim mjerenjem + Tražim Vašu Bluetooth vagu + Pretraga završena + + Oznake + Točka + + Potvrda brisanja + + Baza podataka + + Održavatelj + Web stranica + Licenca + + automatski + + Tema + + Podsjetnik + Dani + Vrijeme + Tekst obavijesti + Vaganje za + + u + + Ponedjeljak + Utorak + Srijeda + Četvrtak + Petak + Subota + Nedjelja + uređaj nije podržan + Izvoz sigurnosne kopije + Uvoz sigurnosne kopije + Sigurnosna kopija + Mapa za izvoz + nije pronađeno + Zanemari podatke izvan granica + Početna težina + Linija regresije + Polinom regresije + Ciljana linija + Pomoć + + Preporučate openScale? + A ocjena na Google Play ili GitHub ? + Želite li dati svoje mišljenje ? + Da + Baš i ne + U redu + Ne, hvala + + Dosegnut maksimalni broj korisnika + Stanite bosi na vagu radi referentnog mjerenja + Mjerenje težine: %.2f + + Došlo je do neočekivane pogreške +\n +\nMolimo da prijavite problem sa podacima o grešci na +\nhttps://github.com/oliexdev/openScale/issues + Ponovo pokreni + Zatvori + Detalji o pogrešci + Detalji o pogrešci + Zatvori + Kopiraj u međuspremnik + Kopirano u međuspremnik + Informacije o pogrešci + Proširi + Uredi + Spremi + + Mjesec + Tjedan + + Nije dozvoljeno + Dozvola pristupu lokaciji je potrebna za traženje Bluetooth uređaja. Može se opet zabraniti pristup nakon pronalaska uređaja. + Info + Sljedeći + Dugo pritisnite i povucite mjerenja za promjenu redoslijeda + Odaberite mjerenje za uređivanje + Zadan redosljed + Prebriši prijašnji izvoz \"%s\"? + Prikaži na pregledu grafa + Mjerenja u % + Procjenjeno mjerenje + Formula za procjenu + Zadana postavka + Jezik + Svjetlo + Tamno + Pomognite prevesti + Dodaj novi ili popravi postojeći + Pregled grafikona + Posto + Procjena + Temeljeno na težini, visini, dobi, spolu, itd. + Automatska sigurnosna kopija + Raspored sigurnosne kopije + Prebriši prijašnju sigurnosnu kopiju + dnevno + tjedno + mjesečno + Da li je Vaša vaga podržana ? + Pomognite da bi ju podržali + Razvoj + Spremi log zapis u datoteku + Vaša Bluetooth vaga + Odaberite mjerenje + Odaberite korisnika + Podesiti widget + Visceralne masnoće + Opseg prsa + Opseg bedra + Opseg bicepsa + Opseg vrata + Kaliper za mjerenje nabora + Nabor na prsima + Nabor na trbuhu + Nabor na bedru + Nabor na tricepsu + Nabor na trbuhu + Nabor na kukovima + Mjerna jedinica + Razina aktivnosti + Sjedeći + Blag + Umjeren + Jak + Ekstreman + diff --git a/android_app/app/src/main/res/values-sl/strings.xml b/android_app/app/src/main/res/values-sl/strings.xml new file mode 100644 index 00000000..0f08694e --- /dev/null +++ b/android_app/app/src/main/res/values-sl/strings.xml @@ -0,0 +1,247 @@ + +Pregled + Diagram + Tabela + Statistika + Uporabniki + Meritve + Informacije + Splošno + + Nastavitve + Bluetooth nastavitve + odpri + zapri + + Prekliči + OK + Da + Ne + Izbriši + Dodaj uporabnika + Dodaj meritev + Deli + CSV izvoz podatkov (%s) + + Teža + Indeks telesne mase (ITM) + Bazalni metabolizem (BM) + Telesna maščoba + Vsebnost vode + Mišice + Čvrsta telesna masa + Obseg pasu + Obseg bokov + Komentar + Razmerje trebuh - višina + Razmerje trebuh - boki + Masa kosti + Pametna določitev uporabnika + + + %d dan + %d dneva + %d dnevi + + + Povprečje prejšnjega tedna + Povprečje prejšnjega meseca + Razlika teže + Datum cilja: + Preostali dnevi + + Datum + Čas + Rojstni dan + Ime + Višina + Enote + Spol + Moški + Ženski + Ciljna teža + Ciljni datum + + uporabnik + zadnja meritev + Cilj + + Uvozi + Izvozi + Izbriši vse + + Zahtevana vrednost + Vrednost je zunaj meje + Napaka pri izvozu + Napaka pri uvozu + Napaka: zahtevno ime + Napaka: Ime mora imeti vsaj 3 znake + Napaka: Višina zahtevana + Napaka: Začetna teža zahtevana + Napaka: Ciljna teža zahtevana + + Vnos je izbrisan + Vsi vnosi so izbrisani + Izvozi v + Uvozi iz + Vrednost med + Komentiraj(ni zahtevano) + je viden + ni viden + omogočeno + onemogočeno + ni na voljo + Poveži z: + Bluetooth povezava izgubljena + Nobena Bluetooth naprava ni bila najdena + Nobena Bluetooth naprava ni izbrana + Povezava vzpostavljena + Zaženi Bluetooth napravo + Nepričakovana Bluetooth napaka + %1$.2f%2$s [%3$s] to %4$s dodan + meritev z istim datumom in časom že obstaja + + Tvoje ime + Trenutno še ne obstaja noben uporabnik. Prosim, ustvarite ga v nastavitvah. + Vrednosti ni bilo mogoče določiti + + Želite izbrisati vnos? + Želite izbrisati vse vnose vseh uporabnikov? + Izbriši uporabnika? + + Bluetooth + Poveži se s tehtnico ob zagonu aplikacije + Združi z zadnjimi meritvami + Iščem Bluetooth tehtnico + Iskanje končano + + Podatkovna oznaka + Podatek + + Izbriši potrditev + + Baza meritev + + Vzdrževalec + Spletna stran + Licenca + + Avtomatsko + + Tema + + Opomnik + Dnevi + Čas + Besedilo obvestila + Čas za tehtanje + + v + + ponedeljek + torek + sreda + četrtek + petek + sobota + nedelja + Naprava ni podprta + Izvozi varnostno datoteko + Uvozi varnostno datoteko + Varnostna datoteka + Izvozi mapo + nič ni bilo najdeno + Ignoriraj vrednosti zunaj meje + Začetna teža + Regresijska krivulja teže + Regresija polinomske stopnje + Ciljna krivulja + Pomoč + + Vam je všeč openScale? + Bi želeli podati oceno na Google Play ali GitHub? + Bi nam radi priskrbeli povratno mnenje? + Da + Ne zares + OK + Ne, hvala + + Doseženo je bilo maksimalno število sočasnih uporabnikov tehtnice + Prosim, stopite bosi na tehtnico za referenčno meritev + Izmerjena teža: %.2f + + Zgodila se je nepričakovana napaka. +\n +\nProsim, ustvarite novo težavo(issue) vključno s podatki o napaki na: +\nhttps://github.com/oliexdev/openScale/issues + Ponovno zaženi aplikacijo + Zapri aplikacijo + Podatki o napaki + Podatki o napaki + Zapri + Kopiraj v odložišče + Kopirano v odložišče + Informacije o napaki + Spremeni v razširjen pogled + Uredi + Shrani + + Mesečni pogled + Tedenski pogled + + Dovoljenja niso bila pridobljena + Dovoljenje je potrebno za približno določanje lokacije Bluetooth naprave. Dovoljenje je lahko odvzeto takoj po tem, ko je naprava najdena. + Informacije + Naprej + Dolgo držite in potegnite meritve za prerazporejanja + Kliknite meritve za rekonfiguracijo + Nastavi privzet vrstni red + Zamenjaj prejšnji izvoz \"%s\"? + Vključi v pregledni graf + Meritev v % + Ocena meritve + Ocenjevalna formula + Privzete nastavitve sistema + Jezik + Svetlo + Temno + Prispevaj prevod + Dodaj ali popravi obstoječega + Pregledni graf + Procent + Ocenjeno + Bazirano na teži, višini, starosti, spolu, ... + Samodejna varnostna kopija + Urnik varnostnih kopij + Zamenjaj prejšnjo varnostno kopijo + dnevno + tedensko + mesečno + Je vaša tehtnica že podprta? + Kliknite tukaj, če želite pomagati + Razvoj + Shrani poročilo o napakah v datoteko + Vaša Bluetooth tehtnica + Izberi meritev + Izberi uporabnika + Konfiguriraj pripomoček(widget) + Visceralna maščoba + Obseg prsnega koša + Obseg stegen + Obseg bicepsa + Obseg vratu + Kožna guba + Prsna kožna guba + Trebušna kožna guba + Stegenska kožna guba + Kožna guba tricepsa + Trebušna kožna guba + Kožna guba bokov + Enote meritev + Stopnja aktivnosti + Sedeč + Blag + Zmeren + Hud + Ekstremna + diff --git a/android_app/app/src/main/res/values-zh-rTW/strings.xml b/android_app/app/src/main/res/values-zh-rTW/strings.xml index 6920e5ed..7d2ba62d 100644 --- a/android_app/app/src/main/res/values-zh-rTW/strings.xml +++ b/android_app/app/src/main/res/values-zh-rTW/strings.xml @@ -40,8 +40,8 @@ - 過去7日 - 過去30日 + 平均過去一週 + 平均過去一個月 體重差距 目標日期: 剩餘日數 @@ -216,4 +216,5 @@ 每月 開發 除錯記錄存檔 - +增加測量 + diff --git a/android_app/app/src/main/res/values/arrays.xml b/android_app/app/src/main/res/values/arrays.xml index 3f82b8d0..f8780d0a 100644 --- a/android_app/app/src/main/res/values/arrays.xml +++ b/android_app/app/src/main/res/values/arrays.xml @@ -6,7 +6,9 @@ Catalan (català) Chinese (traditional; 中文 (繁體)) + Croatian (hrvatski jezik) Czech (čeština) + Danish (dansk) Dutch (Nederlands) English French (français) @@ -21,6 +23,7 @@ Romanian (Română) Russian (русский) Slovak (Slovenčina) + Slovenian (Slovenski Jezik) Spanish (Español) Swedish (Svenska) Turkish (Türkçe) @@ -31,7 +34,9 @@ ca zh-TW + hr cs + da nl en fr @@ -46,6 +51,7 @@ ro ru sk + sl es sv tr diff --git a/android_app/build.gradle b/android_app/build.gradle index bbb6c687..e8d1144c 100644 --- a/android_app/build.gradle +++ b/android_app/build.gradle @@ -5,7 +5,7 @@ buildscript { jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:3.1.2' + classpath 'com.android.tools.build:gradle:3.1.3' } } diff --git a/arduino_mcu/openScale_MCU/openScale_MCU.ino b/arduino_mcu/openScale_MCU/openScale_MCU.ino index 11392dda..b8edaa6f 100644 --- a/arduino_mcu/openScale_MCU/openScale_MCU.ino +++ b/arduino_mcu/openScale_MCU/openScale_MCU.ino @@ -252,7 +252,14 @@ void after_sleep_event() } else { Serial.println("$I$ Successful sync to RTC clock"); } + + print_date_time(); + + Serial.println("$I$ openScale MCU ready!"); +} +void print_date_time() +{ Serial.print("$I$ Time: "); Serial.print(hour()); Serial.write(':'); @@ -266,11 +273,8 @@ void after_sleep_event() Serial.write('/'); Serial.print(year()); Serial.println(); - - Serial.println("$I$ openScale MCU ready!"); } - void check_display_activity() { if (no_activity_cycles > MAX_NO_ACTIVITY_CYCLES) @@ -392,6 +396,8 @@ void send_scale_data() Serial.print('\n'); } + Serial.println("$F$ Scale data sent"); + } void clear_scale_data() @@ -461,8 +467,12 @@ void loop() seg_samples_3.getMedian(seg_value_3); seg_samples_4.getMedian(seg_value_4); - if (seg_value_4 == ' ') { + if (seg_value_4 == ' ' || seg_value_4 == '1' || seg_value_4 == '2') { measured_weight = char_to_int(seg_value_1) + char_to_int(seg_value_2)*10 + char_to_int(seg_value_3)*100; + + if (seg_value_4 == '1' || seg_value_4 == '2') { + measured_weight += char_to_int(seg_value_4)*1000; + } } if (seg_value_4 == 'F') { @@ -494,12 +504,19 @@ void loop() switch(command) { case '0': - Serial.println("$I$ openScale MCU Version 1.0"); + Serial.println("$I$ openScale MCU Version 1.1"); break; case '1': Serial.println("$I$ Sending scale data!"); send_scale_data(); break; + case '2': + set_rtc_time(); + break; + case '3': + Serial.println("$I$ Print RTC Time"); + print_date_time(); + break; case '9': clear_scale_data(); Serial.println("$I$ Scale data cleared!"); @@ -508,6 +525,48 @@ void loop() } } +void set_rtc_time() { + static time_t tLast; + time_t t; + tmElements_t tm; + + setSyncProvider(RTC.get); + + boolean param_finished = false; + + while (!param_finished) { + //check for input to set the RTC, minimum length is 12, i.e. yy,m,d,h,m,s + if (Serial.available() >= 12) { + //note that the tmElements_t Year member is an offset from 1970, + //but the RTC wants the last two digits of the calendar year. + //use the convenience macros from Time.h to do the conversions. + int y = Serial.parseInt(); + if (y >= 100 && y < 1000) + Serial.println("$E$ Error: Year must be two digits or four digits!"); + else { + if (y >= 1000) + tm.Year = CalendarYrToTm(y); + else //(y < 100) + tm.Year = y2kYearToTm(y); + tm.Month = Serial.parseInt(); + tm.Day = Serial.parseInt(); + tm.Hour = Serial.parseInt(); + tm.Minute = Serial.parseInt(); + tm.Second = Serial.parseInt(); + t = makeTime(tm); + RTC.set(t); //use the time_t value to ensure correct weekday is set + setTime(t); + Serial.println("$I$ RTC set to: "); + print_date_time(); + //dump any extraneous input + while (Serial.available() > 0) Serial.read(); + + param_finished = true; + } + } + } +} + int char_to_int(char c) { if (c == ' ') diff --git a/doc/custom_scale/hm_10/HM_10_Manual.pdf b/doc/custom_scale/hm_10/HM_10_Manual.pdf new file mode 100644 index 00000000..1cb15c22 Binary files /dev/null and b/doc/custom_scale/hm_10/HM_10_Manual.pdf differ diff --git a/doc/custom_scale/parts/smart_bluetooth_back.jpg b/doc/custom_scale/parts/smart_bluetooth_back.jpg new file mode 100644 index 00000000..12e271fb Binary files /dev/null and b/doc/custom_scale/parts/smart_bluetooth_back.jpg differ diff --git a/doc/custom_scale/parts/smart_bluetooth_front.jpg b/doc/custom_scale/parts/smart_bluetooth_front.jpg new file mode 100644 index 00000000..654e0db0 Binary files /dev/null and b/doc/custom_scale/parts/smart_bluetooth_front.jpg differ diff --git a/doc/scales/accuway.jpg b/doc/scales/accuway.jpg new file mode 100644 index 00000000..058e80c0 Binary files /dev/null and b/doc/scales/accuway.jpg differ diff --git a/doc/scales/excelvan_cf366ble.jpg b/doc/scales/excelvan_cf366ble.jpg new file mode 100644 index 00000000..495eaa90 Binary files /dev/null and b/doc/scales/excelvan_cf366ble.jpg differ