Ultrahuman optimizations

- fetch only new records instead of all records while the device is connected
- added PREF_POWER_SAVING support (disables / enables SpO2 sensing)
- added HRV visualization
- unified and secured Intent handling
- added BATTERY_CHARGING_FULL support
- honour PREF_TIME_SYNC in onSetTime
- various clean-ups
This commit is contained in:
Thomas Kuehne
2025-04-18 17:44:48 +02:00
parent 956e9cc4ba
commit ff761cf1e4
7 changed files with 184 additions and 70 deletions

View File

@@ -56,7 +56,7 @@ public class GBDaoGenerator {
public static void main(String[] args) throws Exception { public static void main(String[] args) throws Exception {
final Schema schema = new Schema(101, MAIN_PACKAGE + ".entities"); final Schema schema = new Schema(102, MAIN_PACKAGE + ".entities");
Entity userAttributes = addUserAttributes(schema); Entity userAttributes = addUserAttributes(schema);
Entity user = addUserInfo(schema, userAttributes); Entity user = addUserInfo(schema, userAttributes);
@@ -193,6 +193,7 @@ public class GBDaoGenerator {
addGenericHeartRateSample(schema, user, device); addGenericHeartRateSample(schema, user, device);
addGenericSpo2Sample(schema, user, device); addGenericSpo2Sample(schema, user, device);
addGenericStressSample(schema, user, device); addGenericStressSample(schema, user, device);
addGenericHrvSummarySample(schema, user, device);
addGenericHrvValueSample(schema, user, device); addGenericHrvValueSample(schema, user, device);
addGenericTemperatureSample(schema, user, device); addGenericTemperatureSample(schema, user, device);
@@ -1773,6 +1774,19 @@ public class GBDaoGenerator {
return heartRateSample; return heartRateSample;
} }
private static Entity addGenericHrvSummarySample(Schema schema, Entity user, Entity device) {
Entity hrvSummarySample = addEntity(schema, "GenericHrvSummarySample");
addCommonTimeSampleProperties("AbstractHrvSummarySample", hrvSummarySample, user, device);
hrvSummarySample.addIntProperty(SAMPLE_HRV_WEEKLY_AVERAGE).codeBeforeGetter(OVERRIDE);
hrvSummarySample.addIntProperty(SAMPLE_HRV_LAST_NIGHT_AVERAGE).codeBeforeGetter(OVERRIDE);
hrvSummarySample.addIntProperty(SAMPLE_HRV_LAST_NIGHT_5MIN_HIGH).codeBeforeGetter(OVERRIDE);
hrvSummarySample.addIntProperty(SAMPLE_HRV_BASELINE_LOW_UPPER).codeBeforeGetter(OVERRIDE);
hrvSummarySample.addIntProperty(SAMPLE_HRV_BASELINE_BALANCED_LOWER).codeBeforeGetter(OVERRIDE);
hrvSummarySample.addIntProperty(SAMPLE_HRV_BASELINE_BALANCED_UPPER).codeBeforeGetter(OVERRIDE);
hrvSummarySample.addIntProperty(SAMPLE_HRV_STATUS_NUM).codeBeforeGetter(OVERRIDE);
return hrvSummarySample;
}
private static Entity addGenericHrvValueSample(Schema schema, Entity user, Entity device) { private static Entity addGenericHrvValueSample(Schema schema, Entity user, Entity device) {
Entity hrvValueSample = addEntity(schema, "GenericHrvValueSample"); Entity hrvValueSample = addEntity(schema, "GenericHrvValueSample");
addCommonTimeSampleProperties("AbstractHrvValueSample", hrvValueSample, user, device); addCommonTimeSampleProperties("AbstractHrvValueSample", hrvValueSample, user, device);

View File

@@ -0,0 +1,56 @@
/* Copyright (C) 2025 Thomas Kuehne
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.devices;
import androidx.annotation.NonNull;
import de.greenrobot.dao.AbstractDao;
import de.greenrobot.dao.Property;
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
import nodomain.freeyourgadget.gadgetbridge.entities.GenericHrvSummarySample;
import nodomain.freeyourgadget.gadgetbridge.entities.GenericHrvSummarySampleDao;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
public class GenericHrvSummarySampleProvider extends AbstractTimeSampleProvider<GenericHrvSummarySample> {
public GenericHrvSummarySampleProvider(final GBDevice device, final DaoSession session) {
super(device, session);
}
@NonNull
@Override
public AbstractDao<GenericHrvSummarySample, ?> getSampleDao() {
return getSession().getGenericHrvSummarySampleDao();
}
@NonNull
@Override
protected Property getTimestampSampleProperty() {
return GenericHrvSummarySampleDao.Properties.Timestamp;
}
@NonNull
@Override
protected Property getDeviceIdentifierSampleProperty() {
return GenericHrvSummarySampleDao.Properties.DeviceId;
}
@Override
public GenericHrvSummarySample createSample() {
return new GenericHrvSummarySample();
}
}

View File

@@ -36,4 +36,6 @@ public class UltrahumanConstants {
public static final byte OPERATION_PING = 0x59; public static final byte OPERATION_PING = 0x59;
public static final byte OPERATION_ACTIVATE_AIRPLANE_MODE = 0x70; public static final byte OPERATION_ACTIVATE_AIRPLANE_MODE = 0x70;
public static final byte OPERATION_RESET = (byte) 0x98; public static final byte OPERATION_RESET = (byte) 0x98;
public static final byte OPERATION_DISABLE_POWERSAVE = (byte) 0xD1;
public static final byte OPERATION_ENABLE_POWERSAVE = (byte) 0xD2;
} }

View File

@@ -22,6 +22,7 @@ import android.content.Intent;
import com.google.android.material.dialog.MaterialAlertDialogBuilder; import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import nodomain.freeyourgadget.gadgetbridge.BuildConfig;
import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCardAction; import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCardAction;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
@@ -58,6 +59,7 @@ public class UltrahumanDeviceCardAction implements DeviceCardAction {
.setIcon(Icon) .setIcon(Icon)
.setPositiveButton(android.R.string.yes, (dialog, whichButton) -> { .setPositiveButton(android.R.string.yes, (dialog, whichButton) -> {
final Intent intent = new Intent(Action); final Intent intent = new Intent(Action);
intent.setPackage(BuildConfig.APPLICATION_ID);
intent.putExtra(GBDevice.EXTRA_DEVICE, device); intent.putExtra(GBDevice.EXTRA_DEVICE, device);
context.sendBroadcast(intent); context.sendBroadcast(intent);
}) })

View File

@@ -32,6 +32,7 @@ import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.devices.AbstractBLEDeviceCoordinator; import nodomain.freeyourgadget.gadgetbridge.devices.AbstractBLEDeviceCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCardAction; import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCardAction;
import nodomain.freeyourgadget.gadgetbridge.devices.GenericHeartRateSampleProvider; import nodomain.freeyourgadget.gadgetbridge.devices.GenericHeartRateSampleProvider;
import nodomain.freeyourgadget.gadgetbridge.devices.GenericHrvSummarySampleProvider;
import nodomain.freeyourgadget.gadgetbridge.devices.GenericHrvValueSampleProvider; import nodomain.freeyourgadget.gadgetbridge.devices.GenericHrvValueSampleProvider;
import nodomain.freeyourgadget.gadgetbridge.devices.GenericSpo2SampleProvider; import nodomain.freeyourgadget.gadgetbridge.devices.GenericSpo2SampleProvider;
import nodomain.freeyourgadget.gadgetbridge.devices.GenericStressSampleProvider; import nodomain.freeyourgadget.gadgetbridge.devices.GenericStressSampleProvider;
@@ -51,6 +52,7 @@ import nodomain.freeyourgadget.gadgetbridge.entities.UltrahumanDeviceStateSample
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample; import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample;
import nodomain.freeyourgadget.gadgetbridge.model.HeartRateSample; import nodomain.freeyourgadget.gadgetbridge.model.HeartRateSample;
import nodomain.freeyourgadget.gadgetbridge.model.HrvSummarySample;
import nodomain.freeyourgadget.gadgetbridge.model.HrvValueSample; import nodomain.freeyourgadget.gadgetbridge.model.HrvValueSample;
import nodomain.freeyourgadget.gadgetbridge.model.Spo2Sample; import nodomain.freeyourgadget.gadgetbridge.model.Spo2Sample;
import nodomain.freeyourgadget.gadgetbridge.model.StressSample; import nodomain.freeyourgadget.gadgetbridge.model.StressSample;
@@ -71,7 +73,7 @@ public class UltrahumanDeviceCoordinator extends AbstractBLEDeviceCoordinator {
protected void deleteDevice(@NonNull GBDevice gbDevice, @NonNull Device device, @NonNull DaoSession session) throws GBException { protected void deleteDevice(@NonNull GBDevice gbDevice, @NonNull Device device, @NonNull DaoSession session) throws GBException {
final Long deviceId = device.getId(); final Long deviceId = device.getId();
final Map<AbstractDao<?, ?>, Property> daoMap = new HashMap<AbstractDao<?, ?>, Property>() {{ final Map<AbstractDao<?, ?>, Property> daoMap = new HashMap<>() {{
put(session.getGenericHeartRateSampleDao(), GenericHeartRateSampleDao.Properties.DeviceId); put(session.getGenericHeartRateSampleDao(), GenericHeartRateSampleDao.Properties.DeviceId);
put(session.getGenericHrvValueSampleDao(), GenericHrvValueSampleDao.Properties.DeviceId); put(session.getGenericHrvValueSampleDao(), GenericHrvValueSampleDao.Properties.DeviceId);
put(session.getGenericSpo2SampleDao(), GenericSpo2SampleDao.Properties.DeviceId); put(session.getGenericSpo2SampleDao(), GenericSpo2SampleDao.Properties.DeviceId);
@@ -114,6 +116,11 @@ public class UltrahumanDeviceCoordinator extends AbstractBLEDeviceCoordinator {
return "Ultrahuman Healthcare Pvt Ltd"; return "Ultrahuman Healthcare Pvt Ltd";
} }
@Override
public TimeSampleProvider<? extends HrvSummarySample> getHrvSummarySampleProvider(GBDevice device, DaoSession session) {
return new GenericHrvSummarySampleProvider(device, session);
}
@Override @Override
public TimeSampleProvider<? extends HrvValueSample> getHrvValueSampleProvider(GBDevice device, DaoSession session) { public TimeSampleProvider<? extends HrvValueSample> getHrvValueSampleProvider(GBDevice device, DaoSession session) {
return new GenericHrvValueSampleProvider(device, session); return new GenericHrvValueSampleProvider(device, session);
@@ -147,7 +154,8 @@ public class UltrahumanDeviceCoordinator extends AbstractBLEDeviceCoordinator {
@Override @Override
public int[] getSupportedDeviceSpecificSettings(GBDevice device) { public int[] getSupportedDeviceSpecificSettings(GBDevice device) {
return new int[]{ return new int[]{
R.xml.devicesettings_time_sync R.xml.devicesettings_time_sync,
R.xml.devicesettings_power_saving
}; };
} }
@@ -183,8 +191,7 @@ public class UltrahumanDeviceCoordinator extends AbstractBLEDeviceCoordinator {
@Override @Override
public boolean supportsHrvMeasurement(final GBDevice device) { public boolean supportsHrvMeasurement(final GBDevice device) {
// TODO - needs getHrvSummarySampleProvider in addition to the implemented getHrvValueSampleProvider return true;
return false;
} }
@Override @Override

View File

@@ -30,6 +30,7 @@ import androidx.core.content.ContextCompat;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import java.util.Locale;
import java.util.UUID; import java.util.UUID;
import nodomain.freeyourgadget.gadgetbridge.GBApplication; import nodomain.freeyourgadget.gadgetbridge.GBApplication;
@@ -57,7 +58,6 @@ import nodomain.freeyourgadget.gadgetbridge.entities.UltrahumanActivitySample;
import nodomain.freeyourgadget.gadgetbridge.entities.UltrahumanDeviceStateSample; import nodomain.freeyourgadget.gadgetbridge.entities.UltrahumanDeviceStateSample;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.BatteryState; import nodomain.freeyourgadget.gadgetbridge.model.BatteryState;
import nodomain.freeyourgadget.gadgetbridge.model.DeviceType;
import nodomain.freeyourgadget.gadgetbridge.service.btle.AbstractBTLEDeviceSupport; import nodomain.freeyourgadget.gadgetbridge.service.btle.AbstractBTLEDeviceSupport;
import nodomain.freeyourgadget.gadgetbridge.service.btle.BLETypeConversions; import nodomain.freeyourgadget.gadgetbridge.service.btle.BLETypeConversions;
import nodomain.freeyourgadget.gadgetbridge.service.btle.GattService; import nodomain.freeyourgadget.gadgetbridge.service.btle.GattService;
@@ -71,30 +71,30 @@ import nodomain.freeyourgadget.gadgetbridge.util.StringUtils;
public class UltrahumanDeviceSupport extends AbstractBTLEDeviceSupport { public class UltrahumanDeviceSupport extends AbstractBTLEDeviceSupport {
private static final Logger LOG = LoggerFactory.getLogger(UltrahumanDeviceSupport.class); private static final Logger LOG = LoggerFactory.getLogger(UltrahumanDeviceSupport.class);
private BroadcastReceiver CommandReceiver; private final UltrahumanBroadcastReceiver CommandReceiver;
private int FetchTo; private int FetchTo;
private int FetchFrom; private int FetchFrom;
private int FetchCurrent; private int FetchCurrent;
public UltrahumanDeviceSupport(DeviceType type) { public UltrahumanDeviceSupport() {
super(LOG); super(LOG);
addSupportedService(UltrahumanConstants.UUID_SERVICE_COMMAND); addSupportedService(UltrahumanConstants.UUID_SERVICE_COMMAND);
addSupportedService(UltrahumanConstants.UUID_SERVICE_STATE); addSupportedService(UltrahumanConstants.UUID_SERVICE_STATE);
addSupportedService(GattService.UUID_SERVICE_DEVICE_INFORMATION); addSupportedService(GattService.UUID_SERVICE_DEVICE_INFORMATION);
CommandReceiver = new UltrahumanBroadcastReceiver();
DeviceInfoProfile<UltrahumanDeviceSupport> deviceProfile = new DeviceInfoProfile<>(this); DeviceInfoProfile<UltrahumanDeviceSupport> deviceProfile = new DeviceInfoProfile<>(this);
deviceProfile.addListener(new UltrahumanIntentListener()); deviceProfile.addListener(CommandReceiver);
addSupportedProfile(deviceProfile); addSupportedProfile(deviceProfile);
} }
@Override @Override
public void dispose() { public void dispose() {
BroadcastReceiver receiver = CommandReceiver; if (CommandReceiver.Registered) {
CommandReceiver = null; CommandReceiver.Registered = false;
getContext().unregisterReceiver(CommandReceiver);
if (receiver != null) {
getContext().unregisterReceiver(receiver);
} }
super.dispose(); super.dispose();
@@ -115,11 +115,11 @@ public class UltrahumanDeviceSupport extends AbstractBTLEDeviceSupport {
builder.add(new SetDeviceStateAction(getDevice(), GBDevice.State.INITIALIZING, getContext())); builder.add(new SetDeviceStateAction(getDevice(), GBDevice.State.INITIALIZING, getContext()));
if (CommandReceiver == null) { if (!CommandReceiver.Registered) {
CommandReceiver = new UltrahumanBroadcastReceiver();
IntentFilter filter = new IntentFilter(); IntentFilter filter = new IntentFilter();
filter.addAction(UltrahumanConstants.ACTION_AIRPLANE_MODE); filter.addAction(UltrahumanConstants.ACTION_AIRPLANE_MODE);
ContextCompat.registerReceiver(getContext(), CommandReceiver, filter, ContextCompat.RECEIVER_EXPORTED); ContextCompat.registerReceiver(getContext(), CommandReceiver, filter, ContextCompat.RECEIVER_EXPORTED);
CommandReceiver.Registered = true;
} }
// trying to read non-existing characteristics sometimes causes odd BLE failures // trying to read non-existing characteristics sometimes causes odd BLE failures
@@ -148,6 +148,9 @@ public class UltrahumanDeviceSupport extends AbstractBTLEDeviceSupport {
builder.add(new UltrahumanSetTimeAction(getCharacteristic(UltrahumanConstants.UUID_CHARACTERISTIC_COMMAND))); builder.add(new UltrahumanSetTimeAction(getCharacteristic(UltrahumanConstants.UUID_CHARACTERISTIC_COMMAND)));
} }
boolean powerSave = getDevicePrefs().getBoolean(DeviceSettingsPreferenceConst.PREF_POWER_SAVING, true);
builder.write(getCharacteristic(UltrahumanConstants.UUID_CHARACTERISTIC_COMMAND), new byte[]{powerSave ? UltrahumanConstants.OPERATION_ENABLE_POWERSAVE : UltrahumanConstants.OPERATION_DISABLE_POWERSAVE});
builder.add(new SetDeviceStateAction(getDevice(), GBDevice.State.INITIALIZED, getContext())); builder.add(new SetDeviceStateAction(getDevice(), GBDevice.State.INITIALIZED, getContext()));
return builder; return builder;
@@ -162,12 +165,15 @@ public class UltrahumanDeviceSupport extends AbstractBTLEDeviceSupport {
getDevice().setBusyTask(task); getDevice().setBusyTask(task);
getDevice().sendDeviceUpdateIntent(getContext()); getDevice().sendDeviceUpdateIntent(getContext());
FetchFrom = -1; // fetch deltas while the device is connected
FetchFrom = (FetchCurrent > 0 && FetchCurrent < Short.MAX_VALUE - 1000) ? FetchCurrent : -1;
FetchTo = -1; FetchTo = -1;
FetchCurrent = -1; FetchCurrent = -1;
TransactionBuilder builder = new TransactionBuilder("onFetchRecordedData"); TransactionBuilder builder = new TransactionBuilder("onFetchRecordedData");
builder.write(getCharacteristic(UltrahumanConstants.UUID_CHARACTERISTIC_COMMAND), new byte[]{UltrahumanConstants.OPERATION_GET_FIRST_RECORDING_NR}); if (FetchFrom <= 0) {
builder.write(getCharacteristic(UltrahumanConstants.UUID_CHARACTERISTIC_COMMAND), new byte[]{UltrahumanConstants.OPERATION_GET_FIRST_RECORDING_NR});
}
builder.write(getCharacteristic(UltrahumanConstants.UUID_CHARACTERISTIC_COMMAND), new byte[]{UltrahumanConstants.OPERATION_GET_LAST_RECORDING_NR}); builder.write(getCharacteristic(UltrahumanConstants.UUID_CHARACTERISTIC_COMMAND), new byte[]{UltrahumanConstants.OPERATION_GET_LAST_RECORDING_NR});
if (isConnected()) { if (isConnected()) {
@@ -209,19 +215,20 @@ public class UltrahumanDeviceSupport extends AbstractBTLEDeviceSupport {
return false; return false;
} }
byte op = raw[0]; final byte op = raw[0];
byte success = raw[1]; final byte success = raw[1];
byte result = raw[2]; final byte result = raw[2];
// ignore checksums for now - algorithm is unknown // ignore checksums for now - algorithm is unknown
//byte chk1 = raw[raw.length-1]; //byte chk1 = raw[raw.length-1];
//byte chk2 = raw[raw.length-2]; //byte chk2 = raw[raw.length-2];
final String info = String.format(Locale.US, "received BLE command response op=%02x, success=%02x, result=%02x : %s", op, success, result, StringUtils.bytesToHex(raw));
if (success == 0x00) { if (success == 0x00) {
LOG.debug("received BLE command response op={}, success={}, result={} : {}", op, success, result, StringUtils.bytesToHex(raw)); LOG.debug(info);
} else if ((0xFF & success) == 0xFF) { } else if ((0xFF & success) == 0xFF) {
LOG.warn("received BLE command response op={}, success={}, result={} : {}", op, success, result, StringUtils.bytesToHex(raw)); LOG.warn(info);
} else { } else {
LOG.info("received BLE command response op={}, success={}, result={} : {}", op, success, result, StringUtils.bytesToHex(raw)); LOG.info(info);
} }
Context context = getContext(); Context context = getContext();
@@ -230,13 +237,12 @@ public class UltrahumanDeviceSupport extends AbstractBTLEDeviceSupport {
case UltrahumanConstants.OPERATION_GET_RECORDINGS: case UltrahumanConstants.OPERATION_GET_RECORDINGS:
return decodeRecordings(raw); return decodeRecordings(raw);
case UltrahumanConstants.OPERATION_PING: case UltrahumanConstants.OPERATION_PING:
if (raw[1] != 0x00) { if (success == 0x00) {
String message = getContext().getString(R.string.ultrahuman_unhandled_error_response, raw[1], raw[0]); return true;
GB.toast(getContext(), message, Toast.LENGTH_LONG, GB.ERROR);
} }
return true; break;
case UltrahumanConstants.OPERATION_ACTIVATE_AIRPLANE_MODE: case UltrahumanConstants.OPERATION_ACTIVATE_AIRPLANE_MODE:
switch (raw[2]) { switch (result) {
case 0x01: case 0x01:
GB.toast(context, context.getString(R.string.ultrahuman_airplane_mode_activated), Toast.LENGTH_LONG, GB.INFO); GB.toast(context, context.getString(R.string.ultrahuman_airplane_mode_activated), Toast.LENGTH_LONG, GB.INFO);
return true; return true;
@@ -247,54 +253,66 @@ public class UltrahumanDeviceSupport extends AbstractBTLEDeviceSupport {
GB.toast(context, context.getString(R.string.ultrahuman_airplane_mode_too_full), Toast.LENGTH_LONG, GB.ERROR); GB.toast(context, context.getString(R.string.ultrahuman_airplane_mode_too_full), Toast.LENGTH_LONG, GB.ERROR);
return true; return true;
} }
LOG.warn("set airplane mode - unknown error: {} ", StringUtils.bytesToHex(raw));
GB.toast(context, context.getString(R.string.ultrahuman_airplane_mode_unknown), Toast.LENGTH_LONG, GB.ERROR); String airplaneMessage = getContext().getString(R.string.ultrahuman_airplane_mode_unknown, result);
GB.toast(context, airplaneMessage, Toast.LENGTH_LONG, GB.ERROR);
return false; return false;
case UltrahumanConstants.OPERATION_GET_FIRST_RECORDING_NR: case UltrahumanConstants.OPERATION_GET_FIRST_RECORDING_NR:
if (raw[1] == 0x00 || raw[2] == 0x01) { if (success == 0x00 && result == 0x01) {
FetchFrom = BLETypeConversions.toUint16(raw, 3); FetchFrom = BLETypeConversions.toUint16(raw, 3);
if (FetchTo != -1) { if (FetchTo != -1) {
fetchRecordingActually(); fetchRecordingActually();
} }
return true; return true;
} else {
String message = getContext().getString(R.string.ultrahuman_unhandled_error_response, raw[1], raw[0]);
GB.toast(getContext(), message, Toast.LENGTH_LONG, GB.ERROR);
fetchRecordedDataFinished();
} }
return false; fetchRecordedDataFinished();
break;
case UltrahumanConstants.OPERATION_GET_LAST_RECORDING_NR: case UltrahumanConstants.OPERATION_GET_LAST_RECORDING_NR:
if (raw[1] == 0x00 && raw[2] == 0x01) { if (success == 0x00 && result == 0x01) {
FetchTo = BLETypeConversions.toUint16(raw, 3); FetchTo = BLETypeConversions.toUint16(raw, 3);
if (FetchFrom != -1) { if (FetchFrom != -1) {
fetchRecordingActually(); fetchRecordingActually();
} }
return true; return true;
} else {
String message = getContext().getString(R.string.ultrahuman_unhandled_error_response, raw[1], raw[0]);
GB.toast(getContext(), message, Toast.LENGTH_LONG, GB.ERROR);
fetchRecordedDataFinished();
} }
return false; fetchRecordedDataFinished();
break;
case UltrahumanConstants.OPERATION_DISABLE_POWERSAVE:
case UltrahumanConstants.OPERATION_ENABLE_POWERSAVE:
case UltrahumanConstants.OPERATION_SETTIME: case UltrahumanConstants.OPERATION_SETTIME:
if (raw[1] != 0x00 || raw[2] != 0x01) { if (success == 0x00 && result == 0x01) {
String message = getContext().getString(R.string.ultrahuman_unhandled_error_response, raw[1], raw[0]); return true;
GB.toast(getContext(), message, Toast.LENGTH_LONG, GB.ERROR);
} }
return true; break;
default: default:
LOG.error("unhandled BLE command response: {} ", StringUtils.bytesToHex(raw)); String message = getContext().getString(R.string.ultrahuman_unhandled_operation_response, StringUtils.bytesToHex(raw));
GB.toast(getContext(), message, Toast.LENGTH_LONG, GB.ERROR);
return false; return false;
} }
String message = getContext().getString(R.string.ultrahuman_unhandled_error_response, op, success, result);
GB.toast(getContext(), message, Toast.LENGTH_LONG, GB.ERROR);
return false;
} }
LOG.error("unhandled characteristics {} - {} ", characteristicUUID, StringUtils.bytesToHex(raw)); LOG.error("unhandled characteristics {} - {} ", characteristicUUID, StringUtils.bytesToHex(raw));
return false; return false;
} }
@Override
public void onSendConfiguration(String config) {
if (DeviceSettingsPreferenceConst.PREF_TIME_SYNC.equals(config)) {
onSetTime();
} else if (DeviceSettingsPreferenceConst.PREF_POWER_SAVING.equals(config)) {
onSetPowerSaveMode();
} else {
super.onSendConfiguration(config);
}
}
@Override @Override
public boolean useAutoConnect() { public boolean useAutoConnect() {
@@ -310,7 +328,7 @@ public class UltrahumanDeviceSupport extends AbstractBTLEDeviceSupport {
if ((raw[1] & 0xFF) == 0xEE) { if ((raw[1] & 0xFF) == 0xEE) {
LOG.warn("no historic data recorded"); LOG.warn("no historic data recorded");
} else { } else {
String message = getContext().getString(R.string.ultrahuman_unhandled_error_response, raw[1], raw[0]); String message = getContext().getString(R.string.ultrahuman_unhandled_error_response, raw[0], raw[1], raw[2]);
GB.toast(getContext(), message, Toast.LENGTH_LONG, GB.ERROR); GB.toast(getContext(), message, Toast.LENGTH_LONG, GB.ERROR);
} }
fetchRecordedDataFinished(); fetchRecordedDataFinished();
@@ -439,7 +457,7 @@ public class UltrahumanDeviceSupport extends AbstractBTLEDeviceSupport {
batteryState = BatteryState.BATTERY_NORMAL; batteryState = BatteryState.BATTERY_NORMAL;
break; break;
case 0x03: case 0x03:
batteryState = BatteryState.BATTERY_CHARGING; batteryState = (batteryLevel > 99) ? BatteryState.BATTERY_CHARGING_FULL : BatteryState.BATTERY_CHARGING;
break; break;
default: default:
LOG.warn("DeviceState contains unhandled device state {}: {}", raw[5], StringUtils.bytesToHex(raw)); LOG.warn("DeviceState contains unhandled device state {}: {}", raw[5], StringUtils.bytesToHex(raw));
@@ -488,8 +506,8 @@ public class UltrahumanDeviceSupport extends AbstractBTLEDeviceSupport {
GB.signalActivityDataFinish(getDevice()); GB.signalActivityDataFinish(getDevice());
} }
public void activateAirplaneMode() { private void activateAirplaneMode() {
sendCommand("ActivateAirplaneMode", new byte[]{UltrahumanConstants.OPERATION_ACTIVATE_AIRPLANE_MODE}); sendCommand("activateAirplaneMode", new byte[]{UltrahumanConstants.OPERATION_ACTIVATE_AIRPLANE_MODE});
} }
@Override @Override
@@ -499,11 +517,10 @@ public class UltrahumanDeviceSupport extends AbstractBTLEDeviceSupport {
} }
} }
@Override private void onSetPowerSaveMode() {
public void onSetTime() { boolean powerSave = getDevicePrefs().getBoolean(DeviceSettingsPreferenceConst.PREF_POWER_SAVING, true);
TransactionBuilder builder = new TransactionBuilder("onSetTime"); TransactionBuilder builder = new TransactionBuilder("onSetPowerSaveMode");
UltrahumanSetTimeAction action = new UltrahumanSetTimeAction(getCharacteristic(UltrahumanConstants.UUID_CHARACTERISTIC_COMMAND)); builder.write(getCharacteristic(UltrahumanConstants.UUID_CHARACTERISTIC_COMMAND), new byte[]{powerSave ? UltrahumanConstants.OPERATION_ENABLE_POWERSAVE : UltrahumanConstants.OPERATION_DISABLE_POWERSAVE});
builder.add(action);
if (isConnected()) { if (isConnected()) {
builder.queue(getQueue()); builder.queue(getQueue());
@@ -512,6 +529,22 @@ public class UltrahumanDeviceSupport extends AbstractBTLEDeviceSupport {
} }
} }
@Override
public void onSetTime() {
boolean timeSync = getDevicePrefs().getBoolean(DeviceSettingsPreferenceConst.PREF_TIME_SYNC, true);
if (timeSync) {
TransactionBuilder builder = new TransactionBuilder("onSetTime");
UltrahumanSetTimeAction action = new UltrahumanSetTimeAction(getCharacteristic(UltrahumanConstants.UUID_CHARACTERISTIC_COMMAND));
builder.add(action);
if (isConnected()) {
builder.queue(getQueue());
} else {
GB.toast(getContext(), R.string.devicestatus_disconnected, Toast.LENGTH_LONG, GB.ERROR);
}
}
}
private void sendCommand(String taskName, byte[] contents) { private void sendCommand(String taskName, byte[] contents) {
LOG.debug("sendCommand {} : {}", taskName, StringUtils.bytesToHex(contents)); LOG.debug("sendCommand {} : {}", taskName, StringUtils.bytesToHex(contents));
TransactionBuilder builder = new TransactionBuilder(taskName); TransactionBuilder builder = new TransactionBuilder(taskName);
@@ -524,9 +557,16 @@ public class UltrahumanDeviceSupport extends AbstractBTLEDeviceSupport {
} }
} }
private class UltrahumanBroadcastReceiver extends BroadcastReceiver { private class UltrahumanBroadcastReceiver extends BroadcastReceiver implements IntentListener {
boolean Registered;
@Override @Override
public void onReceive(Context context, Intent intent) { public void onReceive(Context context, Intent intent) {
notify(intent);
}
@Override
public void notify(Intent intent) {
final GBDevice device = intent.getParcelableExtra(GBDevice.EXTRA_DEVICE); final GBDevice device = intent.getParcelableExtra(GBDevice.EXTRA_DEVICE);
if (device != null && !device.equals(getDevice())) { if (device != null && !device.equals(getDevice())) {
// this intent is for another device // this intent is for another device
@@ -536,15 +576,7 @@ public class UltrahumanDeviceSupport extends AbstractBTLEDeviceSupport {
String action = intent.getAction(); String action = intent.getAction();
if (UltrahumanConstants.ACTION_AIRPLANE_MODE.equals(action)) { if (UltrahumanConstants.ACTION_AIRPLANE_MODE.equals(action)) {
activateAirplaneMode(); activateAirplaneMode();
} } else if (DeviceInfoProfile.ACTION_DEVICE_INFO.equals(action)) {
}
}
private class UltrahumanIntentListener implements IntentListener {
@Override
public void notify(Intent intent) {
String action = intent.getAction();
if (DeviceInfoProfile.ACTION_DEVICE_INFO.equals(action)) {
handleDeviceInfo(intent.getParcelableExtra(DeviceInfoProfile.EXTRA_DEVICE_INFO)); handleDeviceInfo(intent.getParcelableExtra(DeviceInfoProfile.EXTRA_DEVICE_INFO));
} }
} }

View File

@@ -1189,7 +1189,7 @@
<string name="persian">Persian</string> <string name="persian">Persian</string>
<string name="scandinavian">Scandinavian</string> <string name="scandinavian">Scandinavian</string>
<string name="serbian">Serbian</string> <string name="serbian">Serbian</string>
<string name="ukranian">Ukranian</string> <string name="ukranian">Ukrainian</string>
<string name="armenian">Armenian</string> <string name="armenian">Armenian</string>
<string name="italian">Italian</string> <string name="italian">Italian</string>
<string name="french">French</string> <string name="french">French</string>
@@ -3875,10 +3875,11 @@
<string name="ultrahuman_airplane_mode_activated">airplane mode activated</string> <string name="ultrahuman_airplane_mode_activated">airplane mode activated</string>
<string name="ultrahuman_airplane_mode_on_charger">ring is charging - airplane mode NOT activated</string> <string name="ultrahuman_airplane_mode_on_charger">ring is charging - airplane mode NOT activated</string>
<string name="ultrahuman_airplane_mode_too_full">ring is fully charged - airplane mode NOT activated</string> <string name="ultrahuman_airplane_mode_too_full">ring is fully charged - airplane mode NOT activated</string>
<string name="ultrahuman_airplane_mode_unknown">unknown error - airplane mode NOT activated</string> <string name="ultrahuman_airplane_mode_unknown">unknown error %#02x - airplane mode NOT activated</string>
<string name="ultrahuman_airplane_mode_question">Activate airplane mode?</string> <string name="ultrahuman_airplane_mode_question">Activate airplane mode?</string>
<string name="ultrahuman_airplane_mode_title">airplane mode</string> <string name="ultrahuman_airplane_mode_title">airplane mode</string>
<string name="ultrahuman_unhandled_error_response">received unhandled response code 0x%x for operation 0x%x</string> <string name="ultrahuman_unhandled_error_response">unhandled response %2$#02x %3$#02x for operation %1$#02x</string>
<string name="ultrahuman_unhandled_operation_response">unhandled operation response %s</string>
<string name="huawei_stress_test_enable">Automatic stress test</string> <string name="huawei_stress_test_enable">Automatic stress test</string>
<string name="huawei_stress_test_enable_summary">Enable or disable automatic stress test</string> <string name="huawei_stress_test_enable_summary">Enable or disable automatic stress test</string>
<string name="huawei_stress_no_calibration_data">No initial data - calibrate first</string> <string name="huawei_stress_no_calibration_data">No initial data - calibrate first</string>